diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.css b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.css
new file mode 100644
index 000000000000..5e50b8f8a105
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.css
@@ -0,0 +1,23 @@
+::ng-deep .dx-scheduler-cell-sizes-horizontal {
+ width: 100px;
+}
+
+::ng-deep .dx-scheduler-group-header {
+ min-width: 120px;
+}
+
+::ng-deep .dx-scheduler-group-header,
+::ng-deep .dx-scheduler-header-panel-empty-cell,
+::ng-deep .dx-scheduler-work-space-vertical-group-table,
+::ng-deep .dx-scheduler-work-space-vertical-grouped .dx-scheduler-time-panel-cell,
+::ng-deep .dx-scheduler-work-space-grouped:not(.dx-scheduler-work-space-vertical-grouped)
+ .dx-scheduler-header-panel-cell {
+ background-color: var(--dx-color-main-bg);
+}
+
+::ng-deep .dx-scheduler-group-header,
+::ng-deep .dx-scheduler-group-header .dx-scheduler-group-header-content {
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 16px;
+}
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.html b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.html
new file mode 100644
index 000000000000..92d182da6bbc
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.ts b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.ts
new file mode 100644
index 000000000000..ed0e1d8f7cc6
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.ts
@@ -0,0 +1,215 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { DxSchedulerModule } from 'devextreme-angular';
+import { DxSchedulerTypes } from 'devextreme-angular/ui/scheduler';
+import { Appointment, Assignee, Service } from './app.service';
+
+type Form = DxSchedulerTypes.AppointmentFormOpeningEvent['form'];
+type FormItem = {
+ name?: string;
+ dataField?: string;
+ items?: FormItem[];
+ [key: string]: unknown;
+};
+type FoundItem = { list: FormItem[]; index: number } | null;
+
+if (!/localhost/.test(document.location.host)) {
+ enableProdMode();
+}
+
+let modulePrefix = '';
+// @ts-ignore
+if (window && window.config?.packageConfigPaths) {
+ modulePrefix = '/app';
+}
+
+@Component({
+ selector: 'demo-app',
+ templateUrl: `.${modulePrefix}/app.component.html`,
+ styleUrls: [`.${modulePrefix}/app.component.css`],
+ providers: [Service],
+ imports: [
+ CommonModule,
+ DxSchedulerModule,
+ ],
+})
+export class AppComponent {
+ appointments: Appointment[];
+
+ assignees: Assignee[];
+
+ rooms: Assignee[];
+
+ groups: string[] = ['assigneeId'];
+
+ currentDate: Date = new Date(2026, 6, 13);
+
+ constructor(service: Service) {
+ this.appointments = service.getAppointments();
+ this.assignees = service.getAssignees();
+ this.rooms = this.assignees.filter((item) => item.parentId === null);
+ }
+
+ employeesOf(roomId: string | null): Assignee[] {
+ return this.assignees.filter((item) => item.parentId === roomId);
+ }
+
+ roomOf(assigneeId: number | undefined): string | null {
+ return this.assignees.find((item) => item.id === assigneeId)?.parentId ?? null;
+ }
+
+ findItem(items: FormItem[], predicate: (item: FormItem) => boolean): FoundItem {
+ for (let i = 0; i < items.length; i += 1) {
+ if (predicate(items[i])) {
+ return { list: items, index: i };
+ }
+
+ const nested = items[i].items;
+
+ if (nested) {
+ const found = this.findItem(nested, predicate);
+
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ createRoomGroup(form: Form, roomId: string | null): FormItem {
+ return {
+ itemType: 'group',
+ name: 'roomGroup',
+ cssClass: 'dx-scheduler-form-group-with-icon',
+ colCount: 2,
+ colCountByScreen: { xs: 2 },
+ items: [
+ {
+ colSpan: 1,
+ name: 'roomIcon',
+ cssClass: 'dx-scheduler-form-icon',
+ template: () => '
').addClass('dx-icon dx-icon-conferenceroomoutline'),
+ },
+ {
+ itemType: 'simple',
+ name: 'roomEditor',
+ colSpan: 1,
+ label: { visible: false },
+ editorType: 'dxSelectBox',
+ editorOptions: {
+ dataSource: rooms,
+ displayExpr: 'shortText',
+ valueExpr: 'id',
+ value: roomId,
+ placeholder: 'Room',
+ stylingMode: form.getEditor('assigneeId')?.option('stylingMode'),
+ onValueChanged(e) {
+ const editor = form.getEditor('assigneeId');
+
+ editor?.option('dataSource', e.value ? employeesOf(e.value) : []);
+ editor?.option('value', []);
+ },
+ },
+ },
+ ],
+});
+
+const renderEmployeeTag = (data) => {
+ const tag = document.createElement('div');
+
+ tag.className = 'dx-tag-content';
+ tag.style.backgroundColor = data.color ?? '';
+ tag.style.borderColor = data.color ?? 'transparent';
+ tag.textContent = data.text;
+
+ const removeButton = document.createElement('div');
+
+ removeButton.className = 'dx-tag-remove-button';
+ tag.appendChild(removeButton);
+
+ return tag;
+};
+
+const hideLabels = (items) => {
+ items.forEach((item) => {
+ if (item.items) {
+ hideLabels(item.items);
+ } else if (item.dataField !== 'allDay') {
+ item.label = { ...item.label, visible: false };
+ }
+ });
+};
+
+const roomIdOf = (appointmentData) => {
+ const assigneeIds = appointmentData?.assigneeId;
+ const [assigneeId] = Array.isArray(assigneeIds) ? assigneeIds : [assigneeIds];
+
+ return roomOf(assigneeId);
+};
+
+const requireEmployee = (items) => {
+ const found = findItem(items, (item) => item.dataField === 'assigneeId');
+
+ if (!found) {
+ return;
+ }
+
+ const item = found.list[found.index];
+
+ item.validationRules = [{ type: 'required', message: 'Employee is required' }];
+ item.editorOptions = { ...item.editorOptions, tagTemplate: renderEmployeeTag };
+};
+
+const hideGroup = (items, name) => {
+ const found = findItem(items, (item) => item.name === name);
+
+ if (found) {
+ found.list[found.index].visible = false;
+ }
+};
+
+$(() => {
+ $('#scheduler').dxScheduler({
+ dataSource: appointments,
+ views: [{
+ type: 'workWeek',
+ name: 'Vertical Grouping',
+ groupOrientation: 'vertical',
+ cellDuration: 60,
+ }, {
+ type: 'workWeek',
+ name: 'Horizontal Grouping',
+ groupOrientation: 'horizontal',
+ }],
+ currentView: 'Vertical Grouping',
+ currentDate: new Date(2026, 6, 13),
+ startDayHour: 9,
+ endDayHour: 16,
+ groups: ['assigneeId'],
+ resources: [
+ {
+ fieldExpr: 'assigneeId',
+ dataSource: assignees,
+ parentIdExpr: 'parentId',
+ label: 'Employee',
+ allowMultiple: true,
+ icon: 'user',
+ },
+ ],
+ crossScrollingEnabled: false,
+ showAllDayPanel: false,
+ showCurrentTimeIndicator: false,
+ height: 700,
+ onOptionChanged(e) {
+ if (e.name === 'currentView') {
+ e.component.option('crossScrollingEnabled', e.value === 'Horizontal Grouping');
+ }
+ },
+ onAppointmentFormOpening(e) {
+ const { form } = e;
+ const items = form.option('items');
+
+ if (findItem(items, (item) => item.name === 'roomGroup')) {
+ return;
+ }
+
+ const roomId = roomIdOf(e.appointmentData);
+ const mainGroup = items.find((item) => item.name === 'mainGroup');
+ const employee = findItem(items, (item) => item.name === 'assigneeIdGroup');
+ const repeatValue = form.getEditor('repeatEditor')?.option('value');
+
+ employee?.list.splice(employee.index, 0, createRoomGroup(form, roomId));
+ hideLabels(mainGroup?.items ?? []);
+ requireEmployee(items);
+ hideGroup(items, 'descriptionGroup');
+
+ form.option('items', items.slice());
+
+ form.getEditor('repeatEditor')?.option('value', repeatValue);
+ form.getEditor('assigneeId')?.option('dataSource', roomId ? employeesOf(roomId) : []);
+ },
+ });
+});
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/styles.css b/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/styles.css
new file mode 100644
index 000000000000..2fdf6823a33d
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/styles.css
@@ -0,0 +1,23 @@
+.dx-scheduler-cell-sizes-horizontal {
+ width: 100px;
+}
+
+.dx-scheduler-group-header {
+ min-width: 120px;
+}
+
+.dx-scheduler-group-header,
+.dx-scheduler-header-panel-empty-cell,
+.dx-scheduler-work-space-vertical-group-table,
+.dx-scheduler-work-space-vertical-grouped .dx-scheduler-time-panel-cell,
+.dx-scheduler-work-space-grouped:not(.dx-scheduler-work-space-vertical-grouped)
+ .dx-scheduler-header-panel-cell {
+ background-color: var(--dx-color-main-bg);
+}
+
+.dx-scheduler-group-header,
+.dx-scheduler-group-header .dx-scheduler-group-header-content {
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 16px;
+}
diff --git a/apps/demos/menuMeta.json b/apps/demos/menuMeta.json
index f761c41660c8..a30456d15cfa 100644
--- a/apps/demos/menuMeta.json
+++ b/apps/demos/menuMeta.json
@@ -3978,6 +3978,12 @@
"/Models/SampleData/GroupByDateTasks.cs"
],
"DemoType": "Web"
+ },
+ {
+ "Title": "Multilevel Grouping",
+ "Name": "MultilevelGrouping",
+ "Widget": "Scheduler",
+ "DemoType": "Web"
}
]
},
diff --git a/apps/demos/testing/etalons/Scheduler-MultilevelGrouping (fluent.blue.light).png b/apps/demos/testing/etalons/Scheduler-MultilevelGrouping (fluent.blue.light).png
new file mode 100644
index 000000000000..089784244266
Binary files /dev/null and b/apps/demos/testing/etalons/Scheduler-MultilevelGrouping (fluent.blue.light).png differ
diff --git a/apps/demos/testing/etalons/Scheduler-MultilevelGrouping (material.blue.light).png b/apps/demos/testing/etalons/Scheduler-MultilevelGrouping (material.blue.light).png
new file mode 100644
index 000000000000..6fe5dc8aad5f
Binary files /dev/null and b/apps/demos/testing/etalons/Scheduler-MultilevelGrouping (material.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/accessibility/scheduler/scheduler.ts b/e2e/testcafe-devextreme/tests/accessibility/scheduler/scheduler.ts
index 745573effa48..7ba23fe3582b 100644
--- a/e2e/testcafe-devextreme/tests/accessibility/scheduler/scheduler.ts
+++ b/e2e/testcafe-devextreme/tests/accessibility/scheduler/scheduler.ts
@@ -69,3 +69,58 @@ test('Scheduler table elements have right aria attributes', async (t) => {
});
});
});
+
+const groupedRooms = [
+ { id: 'building-a', text: 'Building A', parentId: null },
+ { id: 'floor-a1', text: 'Floor 1', parentId: 'building-a' },
+ { id: 101, text: 'Room 101', parentId: 'floor-a1' },
+ { id: 102, text: 'Room 102', parentId: 'floor-a1' },
+ { id: 'building-b', text: 'Building B', parentId: null },
+ { id: 201, text: 'Room 201', parentId: 'building-b' },
+];
+
+const flatRooms = groupedRooms.filter(
+ (room) => !groupedRooms.some((item) => item.parentId === room.id),
+);
+
+const groupedAppointments = [
+ {
+ text: 'Standup',
+ roomId: 101,
+ startDate: new Date('2021-04-29T16:30:00.000Z'),
+ endDate: new Date('2021-04-29T18:30:00.000Z'),
+ },
+];
+
+const groupingCases = [
+ { view: 'day', orientation: 'vertical' },
+ { view: 'day', orientation: 'horizontal' },
+ { view: 'workWeek', orientation: 'vertical' },
+ { view: 'workWeek', orientation: 'horizontal' },
+ { view: 'timelineDay', orientation: 'vertical' },
+ { view: 'timelineDay', orientation: 'horizontal' },
+ { view: 'agenda', orientation: 'vertical' },
+];
+
+([
+ { name: 'flat', dataSource: flatRooms, parentIdExpr: undefined },
+ { name: 'hierarchical', dataSource: groupedRooms, parentIdExpr: 'parentId' },
+] as const).forEach(({ name, dataSource, parentIdExpr }) => {
+ groupingCases.forEach(({ view, orientation }) => {
+ test(`Scheduler should pass accessibility checks with ${name} grouping on view ${view} (${orientation})`, async (t) => {
+ await a11yCheck(t, a11yCheckConfig, '#container');
+ }).before(async () => {
+ await createWidget('dxScheduler', {
+ dataSource: groupedAppointments,
+ views: [{ type: view, groupOrientation: orientation }],
+ currentView: view,
+ currentDate: new Date(2021, 3, 29),
+ startDayHour: 9,
+ endDayHour: 12,
+ groups: ['roomId'],
+ resources: [{ fieldExpr: 'roomId', dataSource, parentIdExpr }],
+ height: 800,
+ });
+ });
+ });
+});
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/etalons/drag-n-drop-previous-day-cell (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/etalons/drag-n-drop-previous-day-cell (fluent.blue.light).png
index 3df757d96630..87df434cee2b 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/etalons/drag-n-drop-previous-day-cell (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/etalons/drag-n-drop-previous-day-cell (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/etalons/scheduler-after-hiding-and-resizing (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/etalons/scheduler-after-hiding-and-resizing (fluent.blue.light).png
index 2c7e0caf626e..581d2ac95ea4 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/etalons/scheduler-after-hiding-and-resizing (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/etalons/scheduler-after-hiding-and-resizing (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-vertical-grouping-long-names (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-vertical-grouping-long-names (fluent.blue.light).png
index b7e44a97db81..6f7d4a0de8bf 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-vertical-grouping-long-names (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-header-css-vertical-grouping-long-names (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-allDay-0-24) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-allDay-0-24) (fluent.blue.light).png
index 99693189ec59..b6463b6ec067 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-allDay-0-24) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-allDay-0-24) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-allDay-9-14) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-allDay-9-14) (fluent.blue.light).png
index 659c29d0ea43..1a3f53fe0e9d 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-allDay-9-14) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-allDay-9-14) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-hidden-0-24) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-hidden-0-24) (fluent.blue.light).png
index 99693189ec59..b6463b6ec067 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-hidden-0-24) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-hidden-0-24) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-hidden-9-14) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-hidden-9-14) (fluent.blue.light).png
index 659c29d0ea43..1a3f53fe0e9d 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-hidden-9-14) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(month-vertical-hidden-9-14) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-allDay-0-24) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-allDay-0-24) (fluent.blue.light).png
index 3e675e77c5c4..d056047261b1 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-allDay-0-24) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-allDay-0-24) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-allDay-9-14) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-allDay-9-14) (fluent.blue.light).png
index b39a6bbba1e1..43f195bcd1c1 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-allDay-9-14) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-allDay-9-14) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-hidden-0-24) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-hidden-0-24) (fluent.blue.light).png
index 3e675e77c5c4..d056047261b1 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-hidden-0-24) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-hidden-0-24) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-hidden-9-14) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-hidden-9-14) (fluent.blue.light).png
index b39a6bbba1e1..43f195bcd1c1 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-hidden-9-14) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/group-overflow-(week-vertical-hidden-9-14) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=agenda-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=agenda-orientation=vertical) (fluent.blue.light).png
new file mode 100644
index 000000000000..9db4b0d449b1
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=agenda-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=horizontal) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=horizontal) (fluent.blue.light).png
new file mode 100644
index 000000000000..fc007ad01c41
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=horizontal) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=horizontal-rtl) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=horizontal-rtl) (fluent.blue.light).png
new file mode 100644
index 000000000000..2811daf30f7b
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=horizontal-rtl) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=vertical) (fluent.blue.light).png
new file mode 100644
index 000000000000..64e7d3e9bf94
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=vertical-rtl) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=vertical-rtl) (fluent.blue.light).png
new file mode 100644
index 000000000000..bbd3db19130f
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=day-orientation=vertical-rtl) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=month-orientation=horizontal) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=month-orientation=horizontal) (fluent.blue.light).png
new file mode 100644
index 000000000000..36cf7f032ef6
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=month-orientation=horizontal) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=month-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=month-orientation=vertical) (fluent.blue.light).png
new file mode 100644
index 000000000000..239bd0a2de27
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=month-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineDay-orientation=horizontal) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineDay-orientation=horizontal) (fluent.blue.light).png
new file mode 100644
index 000000000000..1f9888a7ebf9
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineDay-orientation=horizontal) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineDay-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineDay-orientation=vertical) (fluent.blue.light).png
new file mode 100644
index 000000000000..67071c6e83f2
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineDay-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineMonth-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineMonth-orientation=vertical) (fluent.blue.light).png
new file mode 100644
index 000000000000..ab0368f0f995
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineMonth-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineWeek-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineWeek-orientation=vertical) (fluent.blue.light).png
new file mode 100644
index 000000000000..34a18025e730
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineWeek-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineWeek-orientation=vertical-rtl) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineWeek-orientation=vertical-rtl) (fluent.blue.light).png
new file mode 100644
index 000000000000..9b5be5dd161f
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=timelineWeek-orientation=vertical-rtl) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=week-orientation=horizontal) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=week-orientation=horizontal) (fluent.blue.light).png
new file mode 100644
index 000000000000..3237de0273f0
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=week-orientation=horizontal) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=week-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=week-orientation=vertical) (fluent.blue.light).png
new file mode 100644
index 000000000000..5244928ead4f
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=week-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=workWeek-orientation=horizontal) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=workWeek-orientation=horizontal) (fluent.blue.light).png
new file mode 100644
index 000000000000..9db0eb110355
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=workWeek-orientation=horizontal) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=workWeek-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=workWeek-orientation=vertical) (fluent.blue.light).png
new file mode 100644
index 000000000000..2724d0b8ce97
Binary files /dev/null and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/hierarchical-grouping(view=workWeek-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_fist-app-part_T1122185 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_fist-app-part_T1122185 (fluent.blue.light).png
index c272d4e50be6..e1e0d62eea5f 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_fist-app-part_T1122185 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_fist-app-part_T1122185 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_last-app-part_T1122185 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_last-app-part_T1122185 (fluent.blue.light).png
index ac5da0d7bba4..671605fe45c3 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_last-app-part_T1122185 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_last-app-part_T1122185 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_middle-app-part_T1122185 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_middle-app-part_T1122185 (fluent.blue.light).png
index 9addc222a620..abd815196218 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_middle-app-part_T1122185 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/etalons/month-view_vertical-grouping_middle-app-part_T1122185 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/hierarchicalGrouping.data.ts b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/hierarchicalGrouping.data.ts
new file mode 100644
index 000000000000..825a61452b45
--- /dev/null
+++ b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/hierarchicalGrouping.data.ts
@@ -0,0 +1,48 @@
+export const hierarchicalRooms = [
+ { id: 'A', text: 'Building A', parentId: null },
+ { id: 'A1', text: 'Floor 1', parentId: 'A' },
+ {
+ id: 101, text: 'Room 101', parentId: 'A1', color: '#3f51b5',
+ },
+ {
+ id: 102, text: 'Room 102', parentId: 'A1', color: '#8e24aa',
+ },
+ { id: 'A2', text: 'Floor 2', parentId: 'A' },
+ {
+ id: 201, text: 'Room 201', parentId: 'A2', color: '#00897b',
+ },
+ { id: 'B', text: 'Building B', parentId: null },
+ {
+ id: 301, text: 'Room 301', parentId: 'B', color: '#e65100',
+ },
+ {
+ id: 'lobby', text: 'Lobby', parentId: null, color: '#c62828',
+ },
+];
+
+export const hierarchicalAppointments = [
+ {
+ text: 'Standup', roomId: 101, startDate: new Date(2021, 3, 26, 9), endDate: new Date(2021, 3, 26, 10),
+ },
+ {
+ text: 'Interview', roomId: 102, startDate: new Date(2021, 3, 26, 11), endDate: new Date(2021, 3, 26, 12, 30),
+ },
+ {
+ text: 'Retro', roomId: 201, startDate: new Date(2021, 3, 26, 10), endDate: new Date(2021, 3, 26, 11),
+ },
+ {
+ text: 'Training', roomId: 301, startDate: new Date(2021, 3, 26, 13), endDate: new Date(2021, 3, 26, 15),
+ },
+ {
+ text: 'Welcome coffee', roomId: 'lobby', startDate: new Date(2021, 3, 26, 9, 30), endDate: new Date(2021, 3, 26, 10, 30),
+ },
+ {
+ text: 'Workshop', roomId: [102, 201], startDate: new Date(2021, 3, 26, 16), endDate: new Date(2021, 3, 26, 17),
+ },
+ {
+ text: 'All-day event', roomId: 101, startDate: new Date(2021, 3, 26), endDate: new Date(2021, 3, 27), allDay: true,
+ },
+ {
+ text: 'Multi-day', roomId: 301, startDate: new Date(2021, 3, 27, 10), endDate: new Date(2021, 3, 29, 12),
+ },
+];
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/grouping/hierarchicalGrouping.ts b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/hierarchicalGrouping.ts
new file mode 100644
index 000000000000..979bbf916910
--- /dev/null
+++ b/e2e/testcafe-devextreme/tests/scheduler/common/grouping/hierarchicalGrouping.ts
@@ -0,0 +1,112 @@
+import { createScreenshotsComparer } from 'devextreme-screenshot-comparer';
+import { createWidget } from '../../../../helpers/createWidget';
+import url from '../../../../helpers/getPageUrl';
+import { testScreenshot } from '../../../../helpers/themeUtils';
+import { scrollToDate } from '../../helpers/utils';
+import {
+ hierarchicalAppointments,
+ hierarchicalRooms,
+} from './hierarchicalGrouping.data';
+
+fixture.disablePageReloads`Scheduler: Hierarchical grouping layout`
+ .page(url(__dirname, '../../../container.html'));
+
+const createScheduler = async (
+ view: string,
+ groupOrientation: string,
+ rtlEnabled = false,
+): Promise
=> {
+ await createWidget('dxScheduler', {
+ dataSource: hierarchicalAppointments,
+ currentDate: new Date(2021, 3, 26),
+ startDayHour: 9,
+ endDayHour: 18,
+ height: 780,
+ crossScrollingEnabled: true,
+ rtlEnabled,
+ groups: ['roomId'],
+ resources: [{
+ fieldExpr: 'roomId',
+ parentIdExpr: 'parentId',
+ dataSource: hierarchicalRooms,
+ label: 'Room',
+ allowMultiple: true,
+ }],
+ resourceCellTemplate(itemData, _index, element) {
+ const { text, color, isLeaf } = itemData;
+
+ $(element).append(
+ $('')
+ .css({ padding: '2px 6px', textAlign: 'left', borderLeft: `4px solid ${color ?? 'transparent'}` })
+ .append($('
').css({ fontWeight: isLeaf ? 400 : 700 }).text(text)),
+ );
+ },
+ views: [{
+ type: view,
+ name: view,
+ groupOrientation,
+ }],
+ currentView: view,
+ });
+};
+
+const shouldScrollToMiddleGroup = (view: string, groupOrientation: string): boolean => (
+ groupOrientation === 'horizontal' && view.startsWith('timeline')
+);
+
+const runScreenshotTest = (
+ view: string,
+ groupOrientation: string,
+ rtlEnabled = false,
+): void => {
+ test(`Hierarchical grouping layout test (view='${view}', groupOrientation=${groupOrientation}${rtlEnabled ? ', rtl=true' : ''})`, async (t) => {
+ const { takeScreenshot, compareResults } = createScreenshotsComparer(t);
+
+ if (shouldScrollToMiddleGroup(view, groupOrientation)) {
+ await scrollToDate(new Date(2021, 3, 26, 12), { roomId: 201 });
+ await t.wait(50);
+ }
+
+ await testScreenshot(
+ t,
+ takeScreenshot,
+ `hierarchical-grouping(view=${view}-orientation=${groupOrientation}${rtlEnabled ? '-rtl' : ''}).png`,
+ );
+
+ await t
+ .expect(compareResults.isValid())
+ .ok(compareResults.errorMessages());
+ }).before(async () => createScheduler(view, groupOrientation, rtlEnabled));
+};
+
+// visual: generic.light
+// visual: fluent.blue.light
+// visual: material.blue.light
+['vertical', 'horizontal'].forEach((groupOrientation) => {
+ ['day', 'week', 'workWeek', 'month'].forEach((view) => {
+ runScreenshotTest(view, groupOrientation);
+ });
+});
+
+// visual: generic.light
+// visual: fluent.blue.light
+// visual: material.blue.light
+['vertical', 'horizontal'].forEach((groupOrientation) => {
+ ['timelineDay', 'timelineWeek', 'timelineMonth']
+ .filter((view) => groupOrientation !== 'horizontal' || view === 'timelineDay')
+ .forEach((view) => {
+ runScreenshotTest(view, groupOrientation);
+ });
+});
+
+// visual: generic.light
+// visual: fluent.blue.light
+// visual: material.blue.light
+runScreenshotTest('agenda', 'vertical');
+
+// visual: generic.light
+// visual: fluent.blue.light
+// visual: material.blue.light
+runScreenshotTest('day', 'horizontal', true);
+runScreenshotTest('day', 'vertical', true);
+runScreenshotTest('timelineWeek', 'vertical', true);
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=false-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=false-vertical (fluent.blue.light).png
index 9bf4c833736e..84c04a6c19cb 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=false-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=false-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=false-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=false-vertical-rtl (fluent.blue.light).png
index e1ecc9630f1b..66541a5530d9 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=false-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=false-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=true-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=true-vertical (fluent.blue.light).png
index 9679a70b5f1d..dccd6a7ca896 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=true-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=true-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=true-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=true-vertical-rtl (fluent.blue.light).png
index 53eec7dc5e16..0ac433fcc4c3 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=true-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=day-crossScrolling=true-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=false-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=false-vertical (fluent.blue.light).png
index a335aa07caa9..f5b5ef1d4d18 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=false-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=false-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=false-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=false-vertical-rtl (fluent.blue.light).png
index 971174bc415e..77e3b5559dae 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=false-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=false-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=true-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=true-vertical (fluent.blue.light).png
index 800cf78a0393..da348c235a30 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=true-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=true-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=true-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=true-vertical-rtl (fluent.blue.light).png
index 5b6abe091f83..a243e822c71e 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=true-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=month-crossScrolling=true-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=false-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=false-vertical (fluent.blue.light).png
index 7e300026142c..1fe98fe11766 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=false-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=false-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=false-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=false-vertical-rtl (fluent.blue.light).png
index 406aadf88c32..bb974f79ba21 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=false-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=false-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=true-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=true-vertical (fluent.blue.light).png
index 7e300026142c..1fe98fe11766 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=true-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=true-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=true-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=true-vertical-rtl (fluent.blue.light).png
index 406aadf88c32..bb974f79ba21 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=true-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineDay-crossScrolling=true-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=false-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=false-vertical (fluent.blue.light).png
index d31a94ed3796..35a8fbeee296 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=false-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=false-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=false-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=false-vertical-rtl (fluent.blue.light).png
index dec7f5298c35..c42a5f564824 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=false-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=false-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=true-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=true-vertical (fluent.blue.light).png
index d31a94ed3796..35a8fbeee296 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=true-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=true-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=true-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=true-vertical-rtl (fluent.blue.light).png
index dec7f5298c35..c42a5f564824 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=true-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineMonth-crossScrolling=true-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=false-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=false-vertical (fluent.blue.light).png
index 23823b4c32e8..0e6356f20a4f 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=false-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=false-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=false-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=false-vertical-rtl (fluent.blue.light).png
index 0e371c009f9e..11d868a17078 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=false-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=false-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=true-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=true-vertical (fluent.blue.light).png
index 23823b4c32e8..0e6356f20a4f 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=true-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=true-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=true-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=true-vertical-rtl (fluent.blue.light).png
index 0e371c009f9e..11d868a17078 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=true-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=timelineWeek-crossScrolling=true-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=false-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=false-vertical (fluent.blue.light).png
index 9d5eede8f620..c5f0b7de620c 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=false-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=false-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=false-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=false-vertical-rtl (fluent.blue.light).png
index 791e00269114..daa72cdea31f 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=false-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=false-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=true-vertical (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=true-vertical (fluent.blue.light).png
index 62196c44379d..b755bf71d4a4 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=true-vertical (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=true-vertical (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=true-vertical-rtl (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=true-vertical-rtl (fluent.blue.light).png
index 8c2cb327d041..e8656b3dfc63 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=true-vertical-rtl (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/adaptive/etalons/view=week-crossScrolling=true-vertical-rtl (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-cell-sizes-in-timelineMonth (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-cell-sizes-in-timelineMonth (fluent.blue.light).png
index a93b1a376bae..680ddac53871 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-cell-sizes-in-timelineMonth (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-cell-sizes-in-timelineMonth (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-cell-sizes-in-timelineWeek (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-cell-sizes-in-timelineWeek (fluent.blue.light).png
index c766c5cc8422..849d13af0d9a 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-cell-sizes-in-timelineWeek (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-cell-sizes-in-timelineWeek (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-month-cross-scrolling=false (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-month-cross-scrolling=false (fluent.blue.light).png
index bf1d8dcab532..4acba24f179b 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-month-cross-scrolling=false (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-month-cross-scrolling=false (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-month-cross-scrolling=true (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-month-cross-scrolling=true (fluent.blue.light).png
index 9bcd9c19092c..130694e3b17b 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-month-cross-scrolling=true (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-month-cross-scrolling=true (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineMonth-cross-scrolling=false (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineMonth-cross-scrolling=false (fluent.blue.light).png
index 30e6b0a0b236..2c2844b00124 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineMonth-cross-scrolling=false (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineMonth-cross-scrolling=false (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineMonth-cross-scrolling=true (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineMonth-cross-scrolling=true (fluent.blue.light).png
index 30e6b0a0b236..2c2844b00124 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineMonth-cross-scrolling=true (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineMonth-cross-scrolling=true (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineWeek-cross-scrolling=false (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineWeek-cross-scrolling=false (fluent.blue.light).png
index 7b19599f57a0..93cc17123d57 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineWeek-cross-scrolling=false (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineWeek-cross-scrolling=false (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineWeek-cross-scrolling=true (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineWeek-cross-scrolling=true (fluent.blue.light).png
index 7b19599f57a0..93cc17123d57 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineWeek-cross-scrolling=true (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-timelineWeek-cross-scrolling=true (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-week-cross-scrolling=false (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-week-cross-scrolling=false (fluent.blue.light).png
index e2c2ecaa1c44..b47e4c943747 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-week-cross-scrolling=false (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-week-cross-scrolling=false (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-week-cross-scrolling=true (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-week-cross-scrolling=true (fluent.blue.light).png
index 6f5ee6bb128a..c3e43ba081fb 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-week-cross-scrolling=true (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/customization/etalons/custom-group-panel-in-week-cross-scrolling=true (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=day-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=day-orientation=vertical) (fluent.blue.light).png
index 786d74dd09c1..06a7f38ed4c0 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=day-orientation=vertical) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=day-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=month-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=month-orientation=vertical) (fluent.blue.light).png
index d04615fed4ac..4addf28f5886 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=month-orientation=vertical) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=month-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineDay-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineDay-orientation=vertical) (fluent.blue.light).png
index 221b7fd3a0f8..decca4e453ef 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineDay-orientation=vertical) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineDay-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineMonth-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineMonth-orientation=vertical) (fluent.blue.light).png
index c285e1cd7ddb..6de2f3cd47d7 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineMonth-orientation=vertical) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineMonth-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineWeek-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineWeek-orientation=vertical) (fluent.blue.light).png
index 2cb2697b28e9..af3adb64d6ee 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineWeek-orientation=vertical) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineWeek-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineWorkWeek-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineWorkWeek-orientation=vertical) (fluent.blue.light).png
index 47b473f97cd9..50c64047839e 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineWorkWeek-orientation=vertical) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=timelineWorkWeek-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=week-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=week-orientation=vertical) (fluent.blue.light).png
index 38b63a0fd4e9..e0499baa89e0 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=week-orientation=vertical) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=week-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=workWeek-orientation=vertical) (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=workWeek-orientation=vertical) (fluent.blue.light).png
index a1a8f1734b1a..13decd8d9e63 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=workWeek-orientation=vertical) (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/resources/groups/etalons/groups(view=workWeek-orientation=vertical) (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png
index 5dcf8985f1e7..cba40c1aaa91 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png
index 57d0e68e23bf..958213a8342d 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png
index 92581490ff77..4058f2e1ce21 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png
index b5b9cd14c64b..b02f02bdb13a 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png
index 11a0605c2bfc..ffc38bed5652 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png
index fffb2c3b670b..f8a468e4f5bb 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png
index 8cd32c2041d8..74d7e294cfaa 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png
index e4e129798f76..caeb36a7437a 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png
index a6726d6e7eb2..f67dac7471b9 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png
index e539bc140584..497a64244882 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_day_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png
index aa7eec857954..386b3c1809a5 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png
index ed5c60a8157c..f78e746a8560 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png
index ac125c52364a..5f4b9e2b1ec1 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png
index 9bf475a45cf6..24f0ec275b97 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png
index 717f9157b611..978705701a35 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png
index dcf95723a436..e3d27d1c0774 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png
index 425fd227583b..0e897081b19d 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png
index 9fab9d9d53ac..6e74b98c5697 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png
index b8a79ba55206..0bbf97c2aea4 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png
index 0908b7b948f5..0c660a0bf744 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineDay_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png
index d1e781c7f5a0..62c7eaced7ea 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png
index 334c79a83dc2..201d4d59af35 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png
index 88488cf294a5..6538d0224f32 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png
index e2199db31bfd..3a112cb90d1b 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png
index e9a837dea37e..4b961487ad87 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineMonth_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png
index 0e18ef06cab3..1ff6f01a97fe 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png
index df36f46c0d1a..9fef481e6099 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png
index 4a0b8fd6780a..a21d66de3004 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png
index 43e0b9ce6f6a..80ba563bd4dd 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png
index e81987daea26..3b8d52e99cb8 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png
index 7f9c8249196a..dc2b5efadb92 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png
index 54a44a0a0522..9c1d97c1041a 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png
index cbdccfefb93c..57a4053e711b 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png
index 7112c1cad2b4..d858e5f06b93 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png
index 777b68bc75f7..9118aac2afbe 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_timelineWeek_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png
index 54c44d86587b..c621efea1262 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T00-00-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png
index 77528e459746..13a3a3d8fd5a 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T00-00-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png
index 7dcc1dc0b520..0e9d0a8b9005 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T06-30-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png
index 0440330a1abb..a0ddfef17d6b 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T06-30-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png
index 4d8fc505bd45..376285e3362f 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T12-00-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png
index 74b5e7163cc5..6063a32a0231 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T12-00-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png
index cef74682ddcf..0071f108207e 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T17-30-00_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png
index 9756c55402a4..694b47a6aa5c 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T17-30-00_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png
index 12bb680db982..73d47c6a9b41 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T23-59-59_g-vertical_0_24 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png
index c653600cc086..3cfea25964b4 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/current-time-indicator_week_2023-12-03T23-59-59_g-vertical_6_18 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-day-crossScrolling=false-vertical-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-day-crossScrolling=false-vertical-grouping (fluent.blue.light).png
index b52c8a7d3516..1db1011cd559 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-day-crossScrolling=false-vertical-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-day-crossScrolling=false-vertical-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-day-crossScrolling=true-vertical-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-day-crossScrolling=true-vertical-grouping (fluent.blue.light).png
index 54174761b001..b42f1c021346 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-day-crossScrolling=true-vertical-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-day-crossScrolling=true-vertical-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=false-horizontal-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=false-horizontal-grouping (fluent.blue.light).png
index 189c1867a96f..efd6b50c3bf6 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=false-horizontal-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=false-horizontal-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=false-vertical-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=false-vertical-grouping (fluent.blue.light).png
index 189c1867a96f..efd6b50c3bf6 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=false-vertical-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=false-vertical-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=true-horizontal-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=true-horizontal-grouping (fluent.blue.light).png
index 189c1867a96f..efd6b50c3bf6 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=true-horizontal-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=true-horizontal-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=true-vertical-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=true-vertical-grouping (fluent.blue.light).png
index 189c1867a96f..efd6b50c3bf6 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=true-vertical-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineDay-crossScrolling=true-vertical-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineMonth-crossScrolling=false-vertical-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineMonth-crossScrolling=false-vertical-grouping (fluent.blue.light).png
index 2bbf5d162759..1a160d2c2237 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineMonth-crossScrolling=false-vertical-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineMonth-crossScrolling=false-vertical-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineMonth-crossScrolling=true-vertical-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineMonth-crossScrolling=true-vertical-grouping (fluent.blue.light).png
index 2bbf5d162759..1a160d2c2237 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineMonth-crossScrolling=true-vertical-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineMonth-crossScrolling=true-vertical-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineWeek-crossScrolling=false-vertical-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineWeek-crossScrolling=false-vertical-grouping (fluent.blue.light).png
index 2d7fe75817c4..f5b123f0acdb 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineWeek-crossScrolling=false-vertical-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineWeek-crossScrolling=false-vertical-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineWeek-crossScrolling=true-vertical-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineWeek-crossScrolling=true-vertical-grouping (fluent.blue.light).png
index 2d7fe75817c4..f5b123f0acdb 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineWeek-crossScrolling=true-vertical-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-timelineWeek-crossScrolling=true-vertical-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-week-crossScrolling=false-vertical-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-week-crossScrolling=false-vertical-grouping (fluent.blue.light).png
index 3cdd0b058a4f..1cb016c71939 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-week-crossScrolling=false-vertical-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-week-crossScrolling=false-vertical-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-week-crossScrolling=true-vertical-grouping (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-week-crossScrolling=true-vertical-grouping (fluent.blue.light).png
index 114e355c9b20..a9582d659998 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-week-crossScrolling=true-vertical-grouping (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/timeIndication/etalons/shader-in-week-crossScrolling=true-vertical-grouping (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=false-interval=1 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=false-interval=1 (fluent.blue.light).png
index 4072554376b5..d6866f43b44e 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=false-interval=1 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=false-interval=1 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=false-interval=2 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=false-interval=2 (fluent.blue.light).png
index 4dae4814c633..32814601463b 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=false-interval=2 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=false-interval=2 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=true-interval=1 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=true-interval=1 (fluent.blue.light).png
index 506cbc4fe40c..2dba3aedfbf5 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=true-interval=1 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=true-interval=1 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=true-interval=2 (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=true-interval=2 (fluent.blue.light).png
index 689181ec1f72..ecb1fba7bc2e 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=true-interval=2 (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/layout/views/day/etalons/day-orientation=vertical-allDay=true-interval=2 (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-month-vertical-start (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-month-vertical-start (fluent.blue.light).png
index ee732181b7ad..75e9b51afdfe 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-month-vertical-start (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-month-vertical-start (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-week-vertical-start (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-week-vertical-start (fluent.blue.light).png
index 83434b2758b5..eaa86ed005f9 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-week-vertical-start (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-week-vertical-start (fluent.blue.light).png differ
diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-workWeek-vertical-start (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-workWeek-vertical-start (fluent.blue.light).png
index 812c2cfaa5f0..dc8ce90ac828 100644
Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-workWeek-vertical-start (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/virtualScrolling/etalons/virtual-scrolling-many-cells-workWeek-vertical-start (fluent.blue.light).png differ
diff --git a/packages/devextreme-angular/src/ui/scheduler/index.ts b/packages/devextreme-angular/src/ui/scheduler/index.ts
index 57f11fdf79e9..08e719a41fa4 100644
--- a/packages/devextreme-angular/src/ui/scheduler/index.ts
+++ b/packages/devextreme-angular/src/ui/scheduler/index.ts
@@ -685,10 +685,10 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh
*/
@Input()
- get resources(): { allowMultiple?: boolean, colorExpr?: string, dataSource?: Array
| DataSource | DataSourceOptions | null | Store | string, displayExpr?: ((resource: any) => string) | string, fieldExpr?: string, icon?: string, label?: string, useColorAsDefault?: boolean, valueExpr?: Function | string }[] {
+ get resources(): { allowMultiple?: boolean, colorExpr?: string, dataSource?: Array | DataSource | DataSourceOptions | null | Store | string, displayExpr?: ((resource: any) => string) | string, fieldExpr?: string, icon?: string, label?: string, parentIdExpr?: string, useColorAsDefault?: boolean, valueExpr?: Function | string }[] {
return this._getOption('resources');
}
- set resources(value: { allowMultiple?: boolean, colorExpr?: string, dataSource?: Array | DataSource | DataSourceOptions | null | Store | string, displayExpr?: ((resource: any) => string) | string, fieldExpr?: string, icon?: string, label?: string, useColorAsDefault?: boolean, valueExpr?: Function | string }[]) {
+ set resources(value: { allowMultiple?: boolean, colorExpr?: string, dataSource?: Array | DataSource | DataSourceOptions | null | Store | string, displayExpr?: ((resource: any) => string) | string, fieldExpr?: string, icon?: string, label?: string, parentIdExpr?: string, useColorAsDefault?: boolean, valueExpr?: Function | string }[]) {
this._setOption('resources', value);
}
@@ -1387,7 +1387,7 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh
* This member supports the internal infrastructure and is not intended to be used directly from your code.
*/
- @Output() resourcesChange: EventEmitter<{ allowMultiple?: boolean, colorExpr?: string, dataSource?: Array | DataSource | DataSourceOptions | null | Store | string, displayExpr?: ((resource: any) => string) | string, fieldExpr?: string, icon?: string, label?: string, useColorAsDefault?: boolean, valueExpr?: Function | string }[]>;
+ @Output() resourcesChange: EventEmitter<{ allowMultiple?: boolean, colorExpr?: string, dataSource?: Array | DataSource | DataSourceOptions | null | Store | string, displayExpr?: ((resource: any) => string) | string, fieldExpr?: string, icon?: string, label?: string, parentIdExpr?: string, useColorAsDefault?: boolean, valueExpr?: Function | string }[]>;
/**
diff --git a/packages/devextreme-angular/src/ui/scheduler/nested/resource-dxi.ts b/packages/devextreme-angular/src/ui/scheduler/nested/resource-dxi.ts
index f07d22779a89..eee08211583f 100644
--- a/packages/devextreme-angular/src/ui/scheduler/nested/resource-dxi.ts
+++ b/packages/devextreme-angular/src/ui/scheduler/nested/resource-dxi.ts
@@ -93,6 +93,14 @@ export class DxiSchedulerResourceComponent extends CollectionNestedOption {
this._setOption('label', value);
}
+ @Input()
+ get parentIdExpr(): string {
+ return this._getOption('parentIdExpr');
+ }
+ set parentIdExpr(value: string) {
+ this._setOption('parentIdExpr', value);
+ }
+
@Input()
get useColorAsDefault(): boolean {
return this._getOption('useColorAsDefault');
diff --git a/packages/devextreme-metadata/make-angular-metadata.ts b/packages/devextreme-metadata/make-angular-metadata.ts
index 8ddde37f2223..50372fcae427 100644
--- a/packages/devextreme-metadata/make-angular-metadata.ts
+++ b/packages/devextreme-metadata/make-angular-metadata.ts
@@ -68,6 +68,7 @@ Ng.makeMetadata({
removeMembers(/\/scheduler:dxSchedulerOptions\.editing\.form/),
removeMembers(/\/scheduler:dxSchedulerOptions\.editing\.popup/),
removeMembers(/\/scheduler:dxSchedulerOptions\.resources\.icon/),
+ removeMembers(/\/scheduler:dxSchedulerOptions\.resources\.parentIdExpr/),
removeMembers(/\/scheduler:.*\.snapToCellsMode/),
removeMembers(/\/scheduler:.*\.hiddenWeekDays/),
removeMembers(/\/stepper:/),
diff --git a/packages/devextreme-react/src/scheduler.ts b/packages/devextreme-react/src/scheduler.ts
index 28bed71c907b..37a2c0b97db1 100644
--- a/packages/devextreme-react/src/scheduler.ts
+++ b/packages/devextreme-react/src/scheduler.ts
@@ -977,6 +977,7 @@ type IResourceProps = React.PropsWithChildren<{
fieldExpr?: string;
icon?: string;
label?: string;
+ parentIdExpr?: string;
useColorAsDefault?: boolean;
valueExpr?: (() => void) | string;
}>
diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/views/_index.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/views/_index.scss
index af44f6164aae..8d79e3155667 100644
--- a/packages/devextreme-scss/scss/widgets/base/scheduler/views/_index.scss
+++ b/packages/devextreme-scss/scss/widgets/base/scheduler/views/_index.scss
@@ -310,11 +310,6 @@ $scheduler-month-date-text-padding: 6px;
flex: 0 0 auto;
min-width: 0;
- &:last-child .dx-scheduler-group-header {
- border-right: $scheduler-base-border;
- border-right-color: $scheduler-base-border-color;
- }
-
.dx-scheduler-group-header {
@include flex-container(row, nowrap);
@@ -332,6 +327,80 @@ $scheduler-month-date-text-padding: 6px;
border-top-color: $scheduler-base-border-color;
}
}
+
+ &:not(.dx-scheduler-group-flex-container-hierarchical) .dx-scheduler-group-row > .dx-scheduler-group-header {
+ border-right: $scheduler-base-border;
+ border-right-color: $scheduler-base-border-color;
+ }
+
+ > .dx-scheduler-group-row:first-child > .dx-scheduler-group-header:first-child {
+ border-top-color: transparent;
+ }
+
+ .dx-scheduler-group-row:not(:first-child) .dx-scheduler-group-row:first-child > .dx-scheduler-group-header:first-child {
+ border-top-color: $scheduler-base-border-color;
+ }
+}
+
+.dx-scheduler-work-space-vertical-group-table,
+.dx-scheduler-group-table {
+ > .dx-scheduler-group-flex-container:not(.dx-scheduler-group-flex-container-hierarchical) > .dx-scheduler-group-row > .dx-scheduler-group-header:first-child {
+ border-top-color: transparent;
+ }
+}
+
+.dx-scheduler-group-flex-container-hierarchical {
+ flex-direction: column;
+
+ .dx-scheduler-group-row > .dx-scheduler-group-header:not(.dx-scheduler-group-header-leaf) {
+ border-right: $scheduler-base-border;
+ border-right-color: $scheduler-base-border-color;
+ }
+
+ .dx-scheduler-group-row {
+ @include flex-container(row, nowrap);
+
+ flex: 1 1 0;
+ width: 100%;
+ min-width: 0;
+
+ .dx-scheduler-group-header {
+ @include flex-container(row, nowrap);
+
+ flex: 0 0 auto;
+ padding: 0 5px;
+ height: 100%;
+ width: $scheduler-left-column-width;
+ justify-content: flex-start;
+ align-items: flex-start;
+ text-align: left;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ border: none;
+ border-top: $scheduler-base-border;
+ border-top-color: $scheduler-base-border-color;
+
+ &.dx-scheduler-group-header-leaf {
+ flex: 1 1 auto;
+ width: auto;
+ border-right: $scheduler-base-border;
+ border-right-color: $scheduler-base-border-color;
+ }
+ }
+
+ .dx-scheduler-group-flex-container {
+ flex: 1 1 auto;
+ flex-direction: column;
+ min-height: 0;
+ width: auto;
+ }
+ }
+
+ .dx-scheduler-group-row > .dx-scheduler-group-header.dx-scheduler-group-header-leaf:only-child {
+ flex: 0 0 auto;
+ width: 100%;
+ min-width: $scheduler-left-column-width;
+ }
}
.dx-scheduler-header-scrollable {
@@ -798,7 +867,7 @@ $scheduler-month-date-text-padding: 6px;
.dx-scheduler-work-space-vertical-grouped,
.dx-scheduler-sidebar-scrollable {
- .dx-scheduler-group-row:last-child .dx-scheduler-group-header {
+ .dx-scheduler-group-row > .dx-scheduler-group-header {
border-right: none;
border-left: $scheduler-base-border;
border-left-color: $scheduler-base-border-color;
diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/views/timelines/_index.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/views/timelines/_index.scss
index d5226a597875..b8469f2b1e83 100644
--- a/packages/devextreme-scss/scss/widgets/base/scheduler/views/timelines/_index.scss
+++ b/packages/devextreme-scss/scss/widgets/base/scheduler/views/timelines/_index.scss
@@ -195,11 +195,16 @@ $scheduler-timeline-min-height: 100px;
}
.dx-scheduler-group-row .dx-scheduler-group-header {
- border: none;
+ border-bottom: none;
border-top: $scheduler-base-border;
border-top-color: $scheduler-base-border-color;
}
+ .dx-scheduler-group-row th.dx-scheduler-group-header {
+ border-right: none;
+ border-left: none;
+ }
+
&.dx-scheduler-work-space-group-by-date {
.dx-scheduler-group-row .dx-scheduler-group-header {
border-right: $scheduler-base-border;
diff --git a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss
index a40c6ea98837..411da87e0147 100644
--- a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss
+++ b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss
@@ -654,6 +654,11 @@ $fluent-scheduler-agenda-time-panel-cell-padding: 8px;
.dx-scheduler-group-header {
border-bottom: none;
}
+
+ .dx-scheduler-header-panel .dx-scheduler-group-row:has(+ .dx-scheduler-group-row) .dx-scheduler-group-header {
+ border-bottom: baseScheduler.$scheduler-base-border;
+ border-bottom-color: $scheduler-base-border-color;
+ }
}
&.dx-scheduler-agenda {
@@ -771,7 +776,7 @@ $fluent-scheduler-agenda-time-panel-cell-padding: 8px;
border-right: 1px solid;
border-right-color: $scheduler-group-separator-border-color;
- &:last-child {
+ &:last-child:not(:where(.dx-scheduler-group-header-inner-column)) {
border-right: none;
}
@@ -781,7 +786,7 @@ $fluent-scheduler-agenda-time-panel-cell-padding: 8px;
border-right: baseScheduler.$scheduler-base-border;
border-right-color: $scheduler-base-border-color;
- &:last-child {
+ &:last-child:not(:where(.dx-scheduler-group-header-inner-column)) {
border-left: none;
}
}
@@ -1035,7 +1040,11 @@ $fluent-scheduler-agenda-time-panel-cell-padding: 8px;
}
.dx-scheduler-timeline .dx-scheduler-group-flex-container .dx-scheduler-group-header {
- width: 100px;
+ width: $fluent-scheduler-group-header-table-cell-width;
+}
+
+.dx-scheduler-timeline .dx-scheduler-group-flex-container-hierarchical .dx-scheduler-group-row > .dx-scheduler-group-header.dx-scheduler-group-header-leaf:only-child {
+ min-width: $fluent-scheduler-group-header-table-cell-width;
}
.dx-scheduler-header-panel-empty-cell {
diff --git a/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss
index 2abce2057d9c..222707c3bc74 100644
--- a/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss
+++ b/packages/devextreme-scss/scss/widgets/generic/scheduler/_index.scss
@@ -164,15 +164,16 @@ $generic-scheduler-agenda-group-header-padding: $generic-scheduler-agenda-time-c
background-color: $scheduler-header-bg;
}
-.dx-scheduler-header-panel { // stylelint-disable-line no-duplicate-selectors
+.dx-scheduler-group-table,
+.dx-scheduler-header-panel {
.dx-scheduler-group-row {
- &:not(:first-child) {
+ .dx-scheduler-group-header {
border-bottom: baseScheduler.$scheduler-base-border;
border-bottom-color: $scheduler-base-border-color;
+ }
- .dx-scheduler-group-header {
- color: $scheduler-panel-text-color;
- }
+ &:not(:first-child) .dx-scheduler-group-header {
+ color: $scheduler-panel-text-color;
}
}
}
@@ -198,6 +199,17 @@ $generic-scheduler-agenda-group-header-padding: $generic-scheduler-agenda-time-c
border-top-color: $scheduler-base-border-color;
}
+ &.dx-scheduler-work-space-all-day:not(.dx-scheduler-work-space-week):not(.dx-scheduler-work-space-work-week) {
+ .dx-scheduler-group-header {
+ border-bottom: none;
+ }
+
+ .dx-scheduler-header-panel .dx-scheduler-group-row:has(+ .dx-scheduler-group-row) .dx-scheduler-group-header {
+ border-bottom: baseScheduler.$scheduler-base-border;
+ border-bottom-color: $scheduler-base-border-color;
+ }
+ }
+
&.dx-scheduler-agenda {
.dx-scheduler-date-table-cell {
border: none;
@@ -324,7 +336,7 @@ $generic-scheduler-agenda-group-header-padding: $generic-scheduler-agenda-time-c
border-right: 1px solid;
border-right-color: $scheduler-group-separator-border-color;
- &:last-child {
+ &:last-child:not(:where(.dx-scheduler-group-header-inner-column)) {
border-right: none;
}
@@ -334,7 +346,7 @@ $generic-scheduler-agenda-group-header-padding: $generic-scheduler-agenda-time-c
border-right: baseScheduler.$scheduler-base-border;
border-right-color: $scheduler-base-border-color;
- &:last-child {
+ &:last-child:not(:where(.dx-scheduler-group-header-inner-column)) {
border-left: none;
}
}
@@ -365,7 +377,7 @@ $generic-scheduler-agenda-group-header-padding: $generic-scheduler-agenda-time-c
border-right: 1px solid;
border-right-color: $scheduler-group-separator-border-color;
- &:last-child {
+ &:last-child:not(:where(.dx-scheduler-group-header-inner-column)) {
border-right: none;
}
@@ -375,7 +387,7 @@ $generic-scheduler-agenda-group-header-padding: $generic-scheduler-agenda-time-c
border-right: baseScheduler.$scheduler-base-border;
border-right-color: $scheduler-base-border-color;
- &:last-child {
+ &:last-child:not(:where(.dx-scheduler-group-header-inner-column)) {
border-left: none;
}
}
@@ -534,6 +546,22 @@ $generic-scheduler-agenda-group-header-padding: $generic-scheduler-agenda-time-c
box-shadow: inset 0 -1px 0 0 $scheduler-accent-border-color;
}
}
+
+ &.dx-scheduler-work-space-grouped {
+ .dx-scheduler-group-header {
+ border-bottom: none;
+ }
+
+ .dx-scheduler-date-table-row.dx-scheduler-date-table-last-row,
+ .dx-scheduler-time-panel-row.dx-scheduler-date-table-last-row,
+ .dx-scheduler-group-table .dx-scheduler-group-row {
+ border-bottom: none;
+
+ &:not(:last-child) {
+ box-shadow: inset 0 -1px 0 0 $scheduler-group-separator-border-color;
+ }
+ }
+ }
}
.dx-scheduler-agenda-nodata {
diff --git a/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss
index 8f45e24750f9..b4bac4364e4d 100644
--- a/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss
+++ b/packages/devextreme-scss/scss/widgets/material/scheduler/_index.scss
@@ -594,6 +594,11 @@ $material-scheduler-agenda-time-panel-cell-padding: 8px;
.dx-scheduler-group-header {
border-bottom: none;
}
+
+ .dx-scheduler-header-panel .dx-scheduler-group-row:has(+ .dx-scheduler-group-row) .dx-scheduler-group-header {
+ border-bottom: baseScheduler.$scheduler-base-border;
+ border-bottom-color: $scheduler-base-border-color;
+ }
}
&.dx-scheduler-agenda {
@@ -744,7 +749,7 @@ $material-scheduler-agenda-time-panel-cell-padding: 8px;
border-right: 1px solid;
border-right-color: $scheduler-group-separator-border-color;
- &:last-child {
+ &:last-child:not(:where(.dx-scheduler-group-header-inner-column)) {
border-right: none;
}
@@ -754,7 +759,7 @@ $material-scheduler-agenda-time-panel-cell-padding: 8px;
border-right: baseScheduler.$scheduler-base-border;
border-right-color: $scheduler-base-border-color;
- &:last-child {
+ &:last-child:not(:where(.dx-scheduler-group-header-inner-column)) {
border-left: none;
}
}
@@ -970,7 +975,11 @@ $material-scheduler-agenda-time-panel-cell-padding: 8px;
}
.dx-scheduler-timeline .dx-scheduler-group-flex-container .dx-scheduler-group-header {
- width: 100px;
+ width: $material-scheduler-group-header-table-cell-width;
+}
+
+.dx-scheduler-timeline .dx-scheduler-group-flex-container-hierarchical .dx-scheduler-group-row > .dx-scheduler-group-header.dx-scheduler-group-header-leaf:only-child {
+ min-width: $material-scheduler-group-header-table-cell-width;
}
.dx-scheduler-header-panel-empty-cell {
diff --git a/packages/devextreme-vue/src/scheduler.ts b/packages/devextreme-vue/src/scheduler.ts
index ead3818f5f1b..44159f2066e5 100644
--- a/packages/devextreme-vue/src/scheduler.ts
+++ b/packages/devextreme-vue/src/scheduler.ts
@@ -1295,6 +1295,7 @@ const DxResourceConfig = {
"update:fieldExpr": null,
"update:icon": null,
"update:label": null,
+ "update:parentIdExpr": null,
"update:useColorAsDefault": null,
"update:valueExpr": null,
},
@@ -1306,6 +1307,7 @@ const DxResourceConfig = {
fieldExpr: String,
icon: String,
label: String,
+ parentIdExpr: String,
useColorAsDefault: Boolean,
valueExpr: [Function, String] as PropType<((() => void)) | string>
}
diff --git a/packages/devextreme/js/__internal/scheduler/__mock__/resource_manager.mock.ts b/packages/devextreme/js/__internal/scheduler/__mock__/resource_manager.mock.ts
index efa7be516244..e3f87d23b054 100644
--- a/packages/devextreme/js/__internal/scheduler/__mock__/resource_manager.mock.ts
+++ b/packages/devextreme/js/__internal/scheduler/__mock__/resource_manager.mock.ts
@@ -42,6 +42,21 @@ export const complexIdResourceMock = [{
],
}];
+export const hierarchicalRoomsMock = [
+ { id: 'board', text: 'Board rooms', parentId: null },
+ { id: 'open', text: 'Open spaces', parentId: null },
+ { id: 11, text: 'Room 11', parentId: 'board' },
+ { id: 12, text: 'Room 12', parentId: 'board' },
+ { id: 21, text: 'Room 21', parentId: 'open' },
+ { id: 'solo', text: 'Solo room', parentId: null },
+];
+
+export const hierarchicalRoomsConfigMock = {
+ fieldExpr: 'roomId',
+ dataSource: hierarchicalRoomsMock,
+ parentIdExpr: 'parentId',
+};
+
export const resourceIndexesMock = Object.keys(resourceItemsByIdMock);
export const resourceConfigMock = [{
diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts
index 6f5313ff9856..2bbb20e8c231 100644
--- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts
+++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts
@@ -66,6 +66,7 @@ interface CreateAppointmentPopupOptions {
startDayHour?: number;
timeZone?: string;
resources?: ResourceConfig[];
+ preloadResources?: boolean;
onAppointmentFormOpening?: (...args: unknown[]) => void;
onSave?: jest.Mock<(appointment: Record) => PromiseLike>;
title?: string;
@@ -112,7 +113,7 @@ export const createAppointmentPopup = async (
const timeZoneCalculator = createTimeZoneCalculator(options.timeZone ?? NO_TIMEZONE);
const editing = { ...DEFAULT_EDITING, ...options.editing };
- if (options.resources?.length) {
+ if (options.resources?.length && options.preloadResources !== false) {
await Promise.all(
resourceManager.resources.map((r) => r.load()),
);
diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/hierarchical_grouping.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/hierarchical_grouping.test.ts
new file mode 100644
index 000000000000..201f5dd55fa5
--- /dev/null
+++ b/packages/devextreme/js/__internal/scheduler/__tests__/hierarchical_grouping.test.ts
@@ -0,0 +1,250 @@
+import {
+ afterEach, beforeEach, describe, expect, it,
+} from '@jest/globals';
+import fx from '@js/common/core/animation/fx';
+import $ from '@js/core/renderer';
+import type { Appointment, Properties } from '@js/ui/scheduler';
+import { hierarchicalRoomsConfigMock } from '@ts/scheduler/__mock__/resource_manager.mock';
+import type { ResourceCellTemplateData } from '@ts/scheduler/r1/components/types';
+import type Scheduler from '@ts/scheduler/scheduler';
+import type ViewDataProvider from '@ts/scheduler/workspaces/view_model/view_data_provider';
+
+import { createScheduler } from './__mock__/create_scheduler';
+import { DEFAULT_CELL_HEIGHT, setupSchedulerTestEnvironment } from './__mock__/mock_scheduler';
+import type { SchedulerModel } from './__mock__/model/scheduler';
+
+// A 3 hour day with a 60 min cellDuration makes every leaf group 3 rows tall
+const ROWS_IN_GROUP = 3;
+const GROUP_HEIGHT = ROWS_IN_GROUP * DEFAULT_CELL_HEIGHT;
+
+const createAppointment = (text: string, roomId: unknown): Appointment => ({
+ text,
+ roomId,
+ startDate: new Date(2015, 1, 9, 9),
+ endDate: new Date(2015, 1, 9, 10),
+} as Appointment);
+
+const getViewDataProvider = (scheduler: Scheduler): ViewDataProvider => (
+ (scheduler as unknown as { _workSpace: { viewDataProvider: ViewDataProvider } })._workSpace
+ .viewDataProvider
+);
+
+const getGroupCount = (scheduler: Scheduler): number => (
+ (scheduler as unknown as { resourceManager: { groupCount: () => number } })
+ .resourceManager.groupCount()
+);
+
+const createHierarchicalScheduler = (
+ dataSource: Appointment[] = [],
+): ReturnType => createScheduler({
+ currentView: 'day',
+ views: [{ type: 'day', groupOrientation: 'vertical' }],
+ currentDate: new Date(2015, 1, 9),
+ startDayHour: 9,
+ endDayHour: 12,
+ cellDuration: 60,
+ showAllDayPanel: false,
+ groups: ['roomId'],
+ resources: [{ ...hierarchicalRoomsConfigMock }] as unknown as Properties['resources'],
+ dataSource,
+ height: 1200,
+});
+
+const getAppointmentTops = (POM: SchedulerModel): Record => POM
+ .getAppointments()
+ .reduce>((result, appointment) => {
+ result[appointment.getText()] = appointment.getGeometry().top;
+ return result;
+ }, {});
+
+describe('Hierarchical grouping', () => {
+ beforeEach(() => {
+ fx.off = true;
+ setupSchedulerTestEnvironment();
+ });
+
+ afterEach(() => {
+ const $scheduler = $('.dx-scheduler');
+ // @ts-expect-error
+ $scheduler.dxScheduler('dispose');
+ document.body.innerHTML = '';
+ fx.off = false;
+ });
+
+ it('should create one contiguous group band per hierarchy leaf, parents excluded', async () => {
+ const { scheduler } = await createHierarchicalScheduler();
+ const viewDataProvider = getViewDataProvider(scheduler);
+
+ expect(getGroupCount(scheduler)).toBe(4);
+ expect([0, 1, 2, 3].map((groupIndex) => viewDataProvider.getCellsGroup(groupIndex))).toEqual([
+ { roomId: 11 },
+ { roomId: 12 },
+ { roomId: 21 },
+ { roomId: 'solo' },
+ ]);
+
+ expect([0, 1, 2, 3].map(
+ (groupIndex) => viewDataProvider.getRowCountInGroup(groupIndex),
+ )).toEqual([ROWS_IN_GROUP, ROWS_IN_GROUP, ROWS_IN_GROUP, ROWS_IN_GROUP]);
+
+ expect([0, 1, 2, 3].map(
+ (groupIndex) => viewDataProvider.getLastGroupCellPosition(groupIndex),
+ )).toEqual([
+ { rowIndex: 2, columnIndex: 0 },
+ { rowIndex: 5, columnIndex: 0 },
+ { rowIndex: 8, columnIndex: 0 },
+ { rowIndex: 11, columnIndex: 0 },
+ ]);
+
+ expect(document.querySelectorAll('.dx-scheduler-date-table-row')).toHaveLength(12);
+ });
+
+ it('should render an appointment in the band of its own leaf', async () => {
+ const { POM } = await createHierarchicalScheduler([
+ createAppointment('Room 11', 11),
+ createAppointment('Room 12', 12),
+ createAppointment('Room 21', 21),
+ createAppointment('Solo room', 'solo'),
+ ]);
+
+ expect(getAppointmentTops(POM)).toEqual({
+ 'Room 11': 0,
+ 'Room 12': GROUP_HEIGHT,
+ 'Room 21': GROUP_HEIGHT * 2,
+ 'Solo room': GROUP_HEIGHT * 3,
+ });
+ });
+
+ it('should not render an appointment bound to a parent id', async () => {
+ const { POM } = await createHierarchicalScheduler([
+ createAppointment('Board rooms', 'board'),
+ createAppointment('Room 21', 21),
+ ]);
+
+ expect(POM.getAppointments().map((appointment) => appointment.getText()))
+ .toEqual(['Room 21']);
+ });
+
+ it('should render an allowMultiple appointment in each of its leaf bands', async () => {
+ const { POM } = await createScheduler({
+ currentView: 'day',
+ views: [{ type: 'day', groupOrientation: 'vertical' }],
+ currentDate: new Date(2015, 1, 9),
+ startDayHour: 9,
+ endDayHour: 12,
+ cellDuration: 60,
+ showAllDayPanel: false,
+ groups: ['roomId'],
+ resources: [{
+ ...hierarchicalRoomsConfigMock,
+ allowMultiple: true,
+ }] as unknown as Properties['resources'],
+ dataSource: [createAppointment('Shared', [12, 'solo'])],
+ height: 1200,
+ });
+
+ expect(POM.getAppointments().map((appointment) => appointment.getGeometry().top))
+ .toEqual([GROUP_HEIGHT, GROUP_HEIGHT * 3]);
+ });
+
+ describe('resourceCellTemplate', () => {
+ const createSchedulerWithTemplate = async (
+ groupOrientation: 'vertical' | 'horizontal',
+ ): Promise => {
+ const templateData: ResourceCellTemplateData[] = [];
+
+ await createScheduler({
+ currentView: 'day',
+ views: [{ type: 'day', groupOrientation }],
+ currentDate: new Date(2015, 1, 9),
+ startDayHour: 9,
+ endDayHour: 12,
+ cellDuration: 60,
+ showAllDayPanel: false,
+ groups: ['roomId'],
+ resources: [{ ...hierarchicalRoomsConfigMock }] as unknown as Properties['resources'],
+ height: 1200,
+ resourceCellTemplate: (itemData: ResourceCellTemplateData, _: number, element: Element) => {
+ templateData.push(itemData);
+ element.textContent = `custom ${itemData.text ?? ''}`;
+ },
+ });
+
+ return templateData;
+ };
+
+ const describeCell = (
+ data: ResourceCellTemplateData,
+ ): Record => ({
+ text: data.text,
+ level: data.level,
+ isLeaf: data.isLeaf,
+ resourceIndex: data.resourceIndex,
+ path: data.path.map((item) => item.text),
+ });
+
+ const boardRooms = {
+ text: 'Board rooms', level: 0, isLeaf: false, resourceIndex: 'roomId', path: ['Board rooms'],
+ };
+ const room11 = {
+ text: 'Room 11', level: 1, isLeaf: true, resourceIndex: 'roomId', path: ['Board rooms', 'Room 11'],
+ };
+ const room12 = {
+ text: 'Room 12', level: 1, isLeaf: true, resourceIndex: 'roomId', path: ['Board rooms', 'Room 12'],
+ };
+ const openSpaces = {
+ text: 'Open spaces', level: 0, isLeaf: false, resourceIndex: 'roomId', path: ['Open spaces'],
+ };
+ const room21 = {
+ text: 'Room 21', level: 1, isLeaf: true, resourceIndex: 'roomId', path: ['Open spaces', 'Room 21'],
+ };
+ const soloRoom = {
+ text: 'Solo room', level: 0, isLeaf: true, resourceIndex: 'roomId', path: ['Solo room'],
+ };
+
+ it('should pass hierarchy-aware data to every header cell template for vertical grouping', async () => {
+ const templateData = await createSchedulerWithTemplate('vertical');
+
+ expect(templateData.map(describeCell)).toEqual([
+ boardRooms, room11, room12, openSpaces, room21, soloRoom,
+ ]);
+ });
+
+ it('should pass hierarchy-aware data to every header cell template for horizontal grouping', async () => {
+ const templateData = await createSchedulerWithTemplate('horizontal');
+
+ expect(templateData.map(describeCell)).toEqual([
+ boardRooms, openSpaces, soloRoom, room11, room12, room21,
+ ]);
+ });
+
+ it('should pass the resource data of the cell and of every path item', async () => {
+ const templateData = await createSchedulerWithTemplate('vertical');
+ const room11Data = templateData
+ .find((data) => data.text === 'Room 11') as ResourceCellTemplateData;
+
+ expect(room11Data.data).toEqual({ id: 11, text: 'Room 11', parentId: 'board' });
+ expect(room11Data.id).toBe(11);
+ expect(room11Data.path.map((item) => item.data)).toEqual([
+ { id: 'board', text: 'Board rooms', parentId: null },
+ { id: 11, text: 'Room 11', parentId: 'board' },
+ ]);
+ });
+
+ it('should render custom template content in parent and leaf header cells', async () => {
+ await createSchedulerWithTemplate('vertical');
+
+ const headerTexts = [...document.querySelectorAll('.dx-scheduler-group-header')]
+ .map((cell) => cell.textContent);
+
+ expect(headerTexts).toEqual([
+ 'custom Board rooms',
+ 'custom Room 11',
+ 'custom Room 12',
+ 'custom Open spaces',
+ 'custom Room 21',
+ 'custom Solo room',
+ ]);
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts
index ef7d5aaa7747..d5acda52128b 100644
--- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts
@@ -2,6 +2,7 @@ import {
afterEach, beforeEach, describe, expect, it, jest,
} from '@jest/globals';
import eventsEngine from '@js/common/core/events/core/events_engine';
+import type DataSource from '@js/data/data_source';
import { loadMessages, locale } from '@js/localization';
import type { GroupItem } from '@js/ui/form';
import { fireEvent } from '@testing-library/dom';
@@ -1339,6 +1340,88 @@ describe('Isolated AppointmentPopup environment', () => {
expect(resourceEditor.NAME).toBe('dxSelectBox');
expect(resourceEditor.option('value')).toEqual(2);
});
+
+ describe('Hierarchical resources', () => {
+ const rooms = [
+ { id: 'board', text: 'Board rooms' },
+ { id: 11, text: 'Room 11', parentId: 'board' },
+ { id: 12, text: 'Room 12', parentId: 'board' },
+ { id: 21, text: 'Room 21' },
+ ];
+
+ const appointmentData = {
+ text: 'Resource test app',
+ startDate: new Date(2017, 4, 9, 9, 30),
+ endDate: new Date(2017, 4, 9, 11),
+ roomId: 11,
+ };
+
+ const hierarchicalResources = [{
+ fieldExpr: 'roomId',
+ parentIdExpr: 'parentId',
+ dataSource: rooms,
+ }];
+
+ it('should offer hierarchy leaves in the editor', async () => {
+ const { POM } = await createAppointmentPopup({
+ appointmentData,
+ resources: hierarchicalResources,
+ });
+
+ const resourceEditor = POM.dxForm.getEditor('roomId');
+ expect(resourceEditor?.option('dataSource')).toEqual([rooms[1], rooms[2], rooms[3]]);
+ expect(resourceEditor?.option('value')).toBe(11);
+ });
+
+ it('should keep the resource displayExpr and valueExpr', async () => {
+ const resources = [{
+ fieldExpr: 'roomId',
+ parentIdExpr: 'parent',
+ valueExpr: 'key',
+ displayExpr: 'name',
+ dataSource: [
+ { key: 'board', name: 'Board rooms' },
+ { key: 'room-11', name: 'Room 11', parent: 'board' },
+ ],
+ }];
+
+ const { POM } = await createAppointmentPopup({
+ appointmentData: { ...appointmentData, roomId: 'room-11' },
+ resources,
+ });
+
+ const resourceEditor = POM.dxForm.getEditor('roomId');
+ expect(resourceEditor?.option('displayExpr')).toBe('name');
+ expect(resourceEditor?.option('valueExpr')).toBe('key');
+ expect(resourceEditor?.option('dataSource')).toEqual([
+ { key: 'room-11', name: 'Room 11', parent: 'board' },
+ ]);
+ expect(resourceEditor?.option('value')).toBe('room-11');
+ });
+
+ it('should offer hierarchy leaves for a resource that was not loaded yet', async () => {
+ const { POM } = await createAppointmentPopup({
+ preloadResources: false,
+ appointmentData,
+ resources: hierarchicalResources,
+ });
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const editorDataSource = (POM.dxForm.getEditor('roomId') as any).getDataSource();
+
+ await editorDataSource.load();
+
+ expect(editorDataSource.items()).toEqual([rooms[1], rooms[2], rooms[3]]);
+ });
+
+ it('should keep the lazy dataSource for a resource without parentIdExpr', async () => {
+ const resources = [{ fieldExpr: 'roomId', dataSource: rooms }];
+
+ const { POM } = await createAppointmentPopup({ appointmentData, resources });
+
+ const editorDataSource = POM.dxForm.getEditor('roomId')?.option('dataSource');
+ expect((editorDataSource as DataSource).items()).toEqual(rooms);
+ });
+ });
});
describe('Icons', () => {
diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/form.ts
index 2f2d232b519b..089d8b543d90 100644
--- a/packages/devextreme/js/__internal/scheduler/appointment_popup/form.ts
+++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/form.ts
@@ -6,6 +6,7 @@ import '@js/ui/select_box';
import type { DayOfWeek, TextEditorButton } from '@js/common';
import messageLocalization from '@js/common/core/localization/message';
+import type { DataSourceOptions } from '@js/common/data';
import { DataSource } from '@js/common/data';
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
@@ -31,6 +32,7 @@ import type { TimeZoneCalculator } from '../r1/timezone_calculator/calculator';
import type { CreateComponentFn, SafeAppointment } from '../types';
import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor';
import type { ResourceLoader } from '../utils/loader/resource_loader';
+import type { RawResourceData } from '../utils/loader/types';
import { DEFAULT_ICONS_SHOW_MODE } from '../utils/options/constants';
import { getAppointmentGroupIndex, getRawAppointmentGroupValues, getSafeGroupValues } from '../utils/resource_manager/appointment_groups_utils';
import type { ResourceManager } from '../utils/resource_manager/resource_manager';
@@ -97,6 +99,23 @@ const CLASSES = {
recurrenceHidden: 'dx-scheduler-form-recurrence-group-hidden',
};
+const getResourceEditorDataSource = (
+ resourceLoader: ResourceLoader,
+): ResourceLoader['dataSource'] | RawResourceData[] | DataSourceOptions => {
+ if (!resourceLoader.hasHierarchy) {
+ return resourceLoader.dataSource;
+ }
+
+ return resourceLoader.isLoaded()
+ ? resourceLoader.leafData
+ : {
+ store: resourceLoader.dataSource?.store(),
+ postProcess: (data: RawResourceData[]): RawResourceData[] => resourceLoader
+ .collectLeafData(data),
+ paginate: false,
+ };
+};
+
const createTimeZoneDataSource = (): DataSource => new DataSource({
store: timeZoneUtils.getTimeZonesCache(),
paginate: true,
@@ -750,11 +769,10 @@ export class AppointmentForm {
}
private createResourcesGroup(): GroupItem {
- const resourceById = Object.values(this.config.resourceManager.resourceById);
- const resourcesLoaders: ResourceLoader[] = resourceById;
+ const resourcesLoaders = this.resourceManager.resources;
let resourcesItems: FormItem[] = resourcesLoaders.map((resourceLoader) => {
- const { dataSource, dataAccessor } = resourceLoader;
+ const { dataAccessor } = resourceLoader;
const dataField = resourceLoader.resourceIndex;
const name = `${dataField}Editor`;
const label = resourceLoader.resourceName ?? dataField;
@@ -768,7 +786,7 @@ export class AppointmentForm {
colSpan: 1,
editorType,
editorOptions: {
- dataSource,
+ dataSource: getResourceEditorDataSource(resourceLoader),
displayExpr: dataAccessor.textExpr,
valueExpr: dataAccessor.idExpr,
},
diff --git a/packages/devextreme/js/__internal/scheduler/classes.ts b/packages/devextreme/js/__internal/scheduler/classes.ts
index bc75e49b2287..2c841e30927f 100644
--- a/packages/devextreme/js/__internal/scheduler/classes.ts
+++ b/packages/devextreme/js/__internal/scheduler/classes.ts
@@ -42,9 +42,3 @@ export const GROUP_ROW_CLASS = 'dx-scheduler-group-row';
export const GROUP_HEADER_CONTENT_CLASS = 'dx-scheduler-group-header-content';
export const LAST_GROUP_CELL_CLASS = 'dx-scheduler-last-group-cell';
export const FIRST_GROUP_CELL_CLASS = 'dx-scheduler-first-group-cell';
-
-export const VERTICAL_GROUP_COUNT_CLASSES = [
- 'dx-scheduler-group-column-count-one',
- 'dx-scheduler-group-column-count-two',
- 'dx-scheduler-group-column-count-three',
-];
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel.tsx
index bfe4fb8e4a7e..288236f2e39d 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel.tsx
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel.tsx
@@ -1,6 +1,5 @@
import type { InfernoEffect } from '@ts/core/r1/runtime/inferno/index';
import { createReRenderEffect, InfernoWrapperComponent } from '@ts/core/r1/runtime/inferno/index';
-import type { RefObject } from '@ts/core/r1/types';
import { VERTICAL_GROUP_ORIENTATION } from '../../../constants';
import type { Group, GroupOrientation } from '../../../types';
@@ -14,7 +13,6 @@ import { GroupPanelVertical } from './group_panel_vertical';
export interface GroupPanelProps extends GroupPanelBaseProps {
groups: Group[];
groupOrientation: GroupOrientation;
- elementRef?: RefObject;
}
export const GroupPanelDefaultProps: DefaultProps = {
@@ -39,7 +37,7 @@ export class GroupPanel extends InfernoWrapperComponent {
groupOrientation,
groups,
styles,
- rowHeights,
+ groupByDate,
} = this.props;
const isVerticalLayout = isVerticalGroupingApplied(groups.length, groupOrientation);
@@ -54,10 +52,7 @@ export class GroupPanel extends InfernoWrapperComponent {
groupPanelData={groupPanelData}
elementRef={elementRef}
styles={styles}
- rowHeights={rowHeights}
- groups={GroupPanelDefaultProps.groups}
- groupOrientation={GroupPanelDefaultProps.groupOrientation}
- groupByDate={GroupPanelDefaultProps.groupByDate}
+ groupByDate={groupByDate}
/>
);
}
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal.tsx
index e4038952313f..ac21886ffe29 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal.tsx
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal.tsx
@@ -1,65 +1,15 @@
import { BaseInfernoComponent } from '@ts/core/r1/runtime/inferno/index';
-import type { GroupRenderItem } from '../../../types';
import { GroupPanelHorizontalRow } from './group_panel_horizontal_row';
import type { GroupPanelBaseProps } from './group_panel_props';
import { GroupPanelBaseDefaultProps } from './group_panel_props';
export class GroupPanelHorizontal extends BaseInfernoComponent {
- private groupPanelItems: GroupRenderItem[][] | null = null;
-
- getGroupPanelItems(): GroupRenderItem[][] {
- if (this.groupPanelItems !== null) {
- return this.groupPanelItems;
- }
-
- const {
- groupPanelData: {
- baseColSpan,
- groupPanelItems,
- },
- } = this.props;
-
- const colSpans: number[] = groupPanelItems.reduceRight((
- currentColSpans,
- groupsRow,
- idx,
- ) => {
- const nextColSpans = currentColSpans;
- const currentLevelGroupCount = groupsRow.length;
- const previousColSpan = idx === groupPanelItems.length - 1
- ? baseColSpan
- : currentColSpans[idx + 1];
- const previousLevelGroupCount = idx === groupPanelItems.length - 1
- ? currentLevelGroupCount
- : groupPanelItems[idx + 1].length;
- const groupCountDiff = previousLevelGroupCount / currentLevelGroupCount;
- nextColSpans[idx] = groupCountDiff * previousColSpan;
- return nextColSpans;
- }, [...new Array(groupPanelItems.length)]);
-
- this.groupPanelItems = groupPanelItems.map((groupsRenderRow, index) => {
- const colSpan = colSpans[index];
- return groupsRenderRow.map((groupItem) => ({
- ...groupItem,
- colSpan,
- }));
- });
-
- return this.groupPanelItems;
- }
-
- componentWillUpdate(nextProps: GroupPanelBaseProps): void {
- if (this.props.groupPanelData !== nextProps.groupPanelData) {
- this.groupPanelItems = null;
- }
- }
-
render(): JSX.Element {
const {
+ groupPanelData: { groupPanelItems },
resourceCellTemplate,
} = this.props;
- const groupPanelItems = this.getGroupPanelItems();
return (
<>
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_cell.test.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_cell.test.tsx
new file mode 100644
index 000000000000..15fcb63293e9
--- /dev/null
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_cell.test.tsx
@@ -0,0 +1,134 @@
+import {
+ describe, expect, it,
+} from '@jest/globals';
+
+import type { ResourceCellTemplateData } from '../types';
+import type { GroupPanelHorizontalCellProps } from './group_panel_horizontal_cell';
+import { GroupPanelHorizontalCell } from './group_panel_horizontal_cell';
+
+interface VirtualNodeLike {
+ className?: string;
+ props?: {
+ colspan?: number;
+ rowspan?: number;
+ title?: string;
+ scope?: string;
+ role?: string;
+ templateProps?: { data: ResourceCellTemplateData };
+ };
+ children?: VirtualNodeLike | VirtualNodeLike[];
+}
+
+const baseProps = {
+ id: 1,
+ text: 'Room 1',
+ data: { id: 1, text: 'Room 1' },
+ index: 0,
+ colSpan: 3,
+ isFirstGroupCell: false,
+ isLastGroupCell: false,
+};
+
+describe('GroupPanelHorizontalCell', () => {
+ it('should render colSpan and omit rowSpan when it is not provided', () => {
+ const component = new GroupPanelHorizontalCell(baseProps);
+ const result = component.render() as VirtualNodeLike;
+
+ expect(result.props?.colspan).toBe(3);
+ expect(result.props?.rowspan).toBeUndefined();
+ });
+
+ it('should render rowSpan when greater than 1 (shallow leaf filling missing depth rows)', () => {
+ const component = new GroupPanelHorizontalCell({ ...baseProps, rowSpan: 2 });
+ const result = component.render() as VirtualNodeLike;
+
+ expect(result.props?.rowspan).toBe(2);
+ });
+
+ it('should omit rowSpan attribute when it equals 1', () => {
+ const component = new GroupPanelHorizontalCell({ ...baseProps, rowSpan: 1 });
+ const result = component.render() as VirtualNodeLike;
+
+ expect(result.props?.rowspan).toBeUndefined();
+ });
+
+ it('should set a title attribute with the cell text for overflow tooltips', () => {
+ const component = new GroupPanelHorizontalCell(baseProps);
+ const result = component.render() as VirtualNodeLike;
+
+ expect(result.props?.title).toBe('Room 1');
+ });
+
+ it('should set scope="colgroup" when colSpan is greater than 1', () => {
+ const component = new GroupPanelHorizontalCell(baseProps);
+ const result = component.render() as VirtualNodeLike;
+
+ expect(result.props?.scope).toBe('colgroup');
+ expect(result.props?.role).toBe('columnheader');
+ });
+
+ it('should set scope="col" when colSpan equals 1', () => {
+ const component = new GroupPanelHorizontalCell({ ...baseProps, colSpan: 1 });
+ const result = component.render() as VirtualNodeLike;
+
+ expect(result.props?.scope).toBe('col');
+ expect(result.props?.role).toBe('columnheader');
+ });
+
+ it('should keep the group separator border on a cell that does not reach the last column', () => {
+ const component = new GroupPanelHorizontalCell({ ...baseProps, isLastColumn: false });
+ const result = component.render() as VirtualNodeLike;
+
+ expect(result.className).toContain('dx-scheduler-group-header-inner-column');
+ });
+
+ it('should not mark a cell that reaches the last column', () => {
+ const component = new GroupPanelHorizontalCell({ ...baseProps, isLastColumn: true });
+ const result = component.render() as VirtualNodeLike;
+
+ expect(result.className).not.toContain('dx-scheduler-group-header-inner-column');
+ });
+
+ describe('resourceCellTemplate', () => {
+ const cellTemplate = (): JSX.Element => ;
+
+ const renderTemplateData = (
+ props: Partial,
+ ): ResourceCellTemplateData => {
+ const result = new GroupPanelHorizontalCell({
+ ...baseProps, ...props, cellTemplate,
+ }).render() as VirtualNodeLike;
+ const content = result.children as VirtualNodeLike;
+ const templateNode = (Array.isArray(content.children)
+ ? content.children[0]
+ : content.children) as VirtualNodeLike;
+
+ return templateNode.props?.templateProps?.data as ResourceCellTemplateData;
+ };
+
+ it('should pass hierarchy-aware data to a parent header cell template', () => {
+ const buildingPathItem = {
+ id: 'A', text: 'Building A', resourceIndex: 'buildingId', data: { id: 'A', text: 'Building A' },
+ };
+ const templateData = renderTemplateData({
+ id: 'A',
+ text: 'Building A',
+ data: { id: 'A', text: 'Building A' },
+ resourceIndex: 'buildingId',
+ isLeaf: false,
+ path: [buildingPathItem],
+ });
+
+ expect(templateData).toEqual({
+ data: { id: 'A', text: 'Building A' },
+ id: 'A',
+ text: 'Building A',
+ color: undefined,
+ resourceIndex: 'buildingId',
+ level: 0,
+ isLeaf: false,
+ path: [buildingPathItem],
+ });
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_cell.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_cell.tsx
index e08c3c579aca..613320bbfa66 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_cell.tsx
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_cell.tsx
@@ -3,6 +3,7 @@ import { PublicTemplate } from '@ts/scheduler/r1/components/templates/index';
import type { ResourceCellTemplateProps } from '@ts/scheduler/r1/components/types';
import { combineClasses } from '../../../../core/r1/utils/render_utils';
+import { getResourceCellTemplateData } from '../../utils/group_panel_tree';
import type { GroupPanelCellProps } from './group_panel_props';
import { GroupPanelCellDefaultProps } from './group_panel_props';
@@ -10,6 +11,8 @@ export interface GroupPanelHorizontalCellProps extends GroupPanelCellProps {
isFirstGroupCell: boolean;
isLastGroupCell: boolean;
colSpan: number;
+ rowSpan?: number;
+ isLastColumn?: boolean;
}
export const GroupPanelHorizontalCellDefaultProps = {
@@ -24,6 +27,7 @@ export class GroupPanelHorizontalCell extends BaseInfernoComponent 1 ? 'colgroup' : 'col';
+
return (
1 ? rowSpan : undefined}
+ title={text}
+ scope={scope}
+ role="columnheader"
>
{
@@ -51,12 +66,9 @@ export class GroupPanelHorizontalCell extends BaseInfernoComponent
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_row.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_row.tsx
index 7d75748689fe..d973823134a4 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_row.tsx
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_horizontal_row.tsx
@@ -17,13 +17,18 @@ export class GroupPanelHorizontalRow extends BaseInfernoComponent )
}
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_props.ts b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_props.ts
index ecfa7c1393e4..204e2fdd4a84 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_props.ts
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_props.ts
@@ -1,7 +1,10 @@
import type { PropsWithClassName, PropsWithStyles } from '@ts/core/r1/index';
-import type { JSXTemplate } from '@ts/core/r1/types';
+import type { JSXTemplate, RefObject } from '@ts/core/r1/types';
-import type { GroupItem, GroupPanelData, GroupRenderItem } from '../../../types';
+import type {
+ GroupHeaderHierarchy, GroupItem, GroupPanelData, GroupRenderItem,
+} from '../../../types';
+import type { ResourceId } from '../../../utils/loader/types';
import type { DefaultProps, PropsWithViewContext, ResourceCellTemplateProps } from '../types';
export interface GroupPanelBaseProps extends
@@ -11,24 +14,29 @@ export interface GroupPanelBaseProps extends
groupPanelData: GroupPanelData;
groupByDate: boolean;
height?: number;
- rowHeights?: number[];
+ elementRef?: RefObject;
resourceCellTemplate?: JSXTemplate;
}
export const GroupPanelBaseDefaultProps: DefaultProps = {
groupPanelData: {
+ groupTree: [],
groupPanelItems: [],
+ maxDepth: 0,
baseColSpan: 1,
+ columnCountPerGroup: 1,
+ hasHierarchy: false,
},
groupByDate: false,
styles: {},
};
-export interface GroupPanelCellProps extends PropsWithClassName {
- id: string | number;
+export interface GroupPanelCellProps extends PropsWithClassName, Partial {
+ id: ResourceId;
text?: string;
color?: string;
data: GroupItem;
+ resourceIndex?: string;
index: number;
cellTemplate?: JSXTemplate;
}
@@ -44,7 +52,6 @@ export const GroupPanelCellDefaultProps = {
export interface GroupPanelRowProps extends PropsWithClassName {
groupItems: GroupRenderItem[];
- height?: number;
cellTemplate?: JSXTemplate;
}
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical.tsx
index fe2749291431..9ad01ccffad4 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical.tsx
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical.tsx
@@ -1,10 +1,61 @@
import { BaseInfernoComponent, normalizeStyles } from '@ts/core/r1/runtime/inferno/index';
-import { renderUtils } from '../../utils/index';
+import { getTimelineGroupPanelRows, renderUtils } from '../../utils/index';
import type { GroupPanelProps } from './group_panel';
import { GroupPanelBaseDefaultProps } from './group_panel_props';
+import { GroupPanelVerticalCell } from './group_panel_vertical_cell';
+import { GroupPanelVerticalNode } from './group_panel_vertical_node';
import { GroupPanelVerticalRow } from './group_panel_vertical_row';
+const HIERARCHICAL_GROUP_FLEX_CONTAINER_CLASS = 'dx-scheduler-group-flex-container-hierarchical';
+const TIMELINE_GROUP_TABLE_CLASS = 'dx-scheduler-group-table';
+
+const renderGroupPanelContent = (
+ groupPanelData: GroupPanelProps['groupPanelData'],
+ resourceCellTemplate: GroupPanelProps['resourceCellTemplate'],
+ isTimelineGroupTable: boolean,
+ isHierarchical: boolean,
+ groupByDate: boolean,
+): JSX.Element | JSX.Element[] => {
+ if (isTimelineGroupTable && !isHierarchical) {
+ return getTimelineGroupPanelRows(groupPanelData, groupByDate)
+ .map((group) => );
+ }
+
+ if (isHierarchical) {
+ return groupPanelData.groupTree
+ .map((node, index) => );
+ }
+
+ return (
+
+ {
+ groupPanelData.groupTree.map((node, index) => )
+ }
+
+ );
+};
+
export class GroupPanelVertical extends BaseInfernoComponent {
render(): JSX.Element {
const {
@@ -14,9 +65,24 @@ export class GroupPanelVertical extends BaseInfernoComponent {
resourceCellTemplate,
height,
styles,
- rowHeights,
+ groupByDate,
} = this.props;
const style = normalizeStyles(renderUtils.addHeightToStyle(height, styles));
+ const isTimelineGroupTable = className === TIMELINE_GROUP_TABLE_CLASS;
+ const useResourceHierarchyLayout = groupPanelData.hasHierarchy && groupPanelData.maxDepth > 1;
+ const isHierarchical = isTimelineGroupTable
+ ? useResourceHierarchyLayout
+ : groupPanelData.maxDepth > 1;
+ const flexContainerClassName = isHierarchical
+ ? `dx-scheduler-group-flex-container ${HIERARCHICAL_GROUP_FLEX_CONTAINER_CLASS}`
+ : 'dx-scheduler-group-flex-container';
+ const groupPanelContent = renderGroupPanelContent(
+ groupPanelData,
+ resourceCellTemplate,
+ isTimelineGroupTable,
+ isHierarchical,
+ groupByDate,
+ );
return (
{
className={className}
style={style}
>
-
- {
- groupPanelData.groupPanelItems
- .map((group, index) => )
- }
+
+ {groupPanelContent}
);
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_cell.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_cell.tsx
index 8db4f47d8e6b..8037e5ed4821 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_cell.tsx
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_cell.tsx
@@ -2,6 +2,7 @@ import { BaseInfernoComponent } from '@ts/core/r1/runtime/inferno/index';
import { PublicTemplate } from '@ts/scheduler/r1/components/templates/index';
import type { ResourceCellTemplateProps } from '@ts/scheduler/r1/components/types';
+import { getResourceCellTemplateData } from '../../utils/group_panel_tree';
import type { GroupPanelCellProps } from './group_panel_props';
import { GroupPanelCellDefaultProps } from './group_panel_props';
@@ -15,21 +16,21 @@ export class GroupPanelVerticalCell extends BaseInfernoComponent
+
{
cellTemplate
?
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_node.test.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_node.test.tsx
new file mode 100644
index 000000000000..b9d67d09a764
--- /dev/null
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_node.test.tsx
@@ -0,0 +1,142 @@
+import {
+ describe, expect, it,
+} from '@jest/globals';
+
+import type { GroupPanelTreeNode } from '../../../types';
+import { buildGroupPanelTree } from '../../utils/group_panel_tree';
+import type { ResourceCellTemplateData } from '../types';
+import { GroupPanelVerticalNode } from './group_panel_vertical_node';
+
+interface VirtualNodeLike {
+ className?: string;
+ props?: Record ;
+ children?: VirtualNodeLike | VirtualNodeLike[];
+}
+
+const leafNode = (
+ key: string,
+ text: string,
+ leafCount: number,
+): GroupPanelTreeNode => ({
+ key,
+ id: key,
+ text,
+ data: { id: key, text },
+ resourceIndex: 'roomId',
+ path: [{
+ id: key, text, color: undefined, resourceIndex: 'roomId', data: { id: key, text },
+ }],
+ leafCount,
+ children: [],
+});
+
+describe('GroupPanelVerticalNode', () => {
+ it('should set flexGrow from leafCount so height is proportional to descendant leaves', () => {
+ const component = new GroupPanelVerticalNode({ node: leafNode('a', 'Room A', 3), index: 0 });
+ const result = component.render() as VirtualNodeLike;
+
+ expect(result.props?.style).toEqual({ 'flex-grow': 3 });
+ });
+
+ it('should mark a childless node as a leaf cell and render no nested children container', () => {
+ const component = new GroupPanelVerticalNode({ node: leafNode('a', 'Room A', 1), index: 0 });
+ const result = component.render() as VirtualNodeLike;
+ const children = result.children as VirtualNodeLike[];
+
+ expect(children).toHaveLength(1);
+ expect(children[0].className).toContain('dx-scheduler-group-header-leaf');
+ });
+
+ it('should set title/aria-label for the accessible label and overflow tooltip', () => {
+ const component = new GroupPanelVerticalNode({ node: leafNode('a', 'Room A', 1), index: 0 });
+ const result = component.render() as VirtualNodeLike;
+ const children = result.children as VirtualNodeLike[];
+ const cell = children[0];
+
+ expect(cell.props?.title).toBe('Room A');
+ expect(cell.props?.['aria-label']).toBe('Room A');
+ });
+
+ it('should recurse into children via a nested flex container when the node has children', () => {
+ const parent: GroupPanelTreeNode = {
+ ...leafNode('parent', 'Building A', 2),
+ children: [leafNode('child1', 'Room A1', 1), leafNode('child2', 'Room A2', 1)],
+ };
+ const component = new GroupPanelVerticalNode({ node: parent, index: 0 });
+ const result = component.render() as VirtualNodeLike;
+ const children = result.children as VirtualNodeLike[];
+
+ expect(children).toHaveLength(2);
+ expect(children[0].className).not.toContain('dx-scheduler-group-header-leaf');
+
+ const nestedContainer = children[1];
+ expect(nestedContainer.className).toBe('dx-scheduler-group-flex-container');
+ expect(nestedContainer.children).toHaveLength(2);
+ });
+
+ describe('resourceCellTemplate', () => {
+ const cellTemplate = (): JSX.Element => ;
+ const hierarchy = buildGroupPanelTree([
+ {
+ id: 'A',
+ resourceText: 'Building A',
+ resourceIndex: 'buildingId',
+ grouped: { buildingId: 'A' },
+ children: [{
+ id: 1,
+ resourceText: 'Room A1',
+ color: '#aaa',
+ resourceIndex: 'roomId',
+ grouped: { buildingId: 'A', roomId: 1 },
+ children: [],
+ }],
+ },
+ ]);
+ const [building] = hierarchy;
+ const [room] = building.children;
+
+ const renderTemplateData = (
+ node: GroupPanelTreeNode,
+ index: number,
+ ): ResourceCellTemplateData => {
+ const result = new GroupPanelVerticalNode({ node, index, cellTemplate }).render();
+ const cell = (result as VirtualNodeLike).children as VirtualNodeLike[];
+ const templateNode = (Array.isArray(cell[0].children)
+ ? cell[0].children[0]
+ : cell[0].children) as VirtualNodeLike;
+
+ return (templateNode.props as {
+ templateProps: { data: ResourceCellTemplateData };
+ }).templateProps.data;
+ };
+
+ it('should pass hierarchy-aware data to a parent header cell template', () => {
+ expect(renderTemplateData(building, 0)).toEqual({
+ data: { id: 'A', text: 'Building A' },
+ id: 'A',
+ text: 'Building A',
+ color: undefined,
+ resourceIndex: 'buildingId',
+ level: 0,
+ isLeaf: false,
+ path: [expect.objectContaining({ id: 'A', text: 'Building A' })],
+ });
+ });
+
+ it('should pass hierarchy-aware data to a leaf header cell template', () => {
+ expect(renderTemplateData(room, 0)).toEqual({
+ data: { id: 1, text: 'Room A1', color: '#aaa' },
+ id: 1,
+ text: 'Room A1',
+ color: '#aaa',
+ resourceIndex: 'roomId',
+ level: 1,
+ isLeaf: true,
+ path: [
+ expect.objectContaining({ text: 'Building A' }),
+ expect.objectContaining({ text: 'Room A1' }),
+ ],
+ });
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_node.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_node.tsx
new file mode 100644
index 000000000000..b5ec17e6f355
--- /dev/null
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_node.tsx
@@ -0,0 +1,90 @@
+import type { PropsWithClassName } from '@ts/core/r1/index';
+import { BaseInfernoComponent, normalizeStyles } from '@ts/core/r1/runtime/inferno/index';
+import type { JSXTemplate } from '@ts/core/r1/types';
+import { PublicTemplate } from '@ts/scheduler/r1/components/templates/index';
+import type { ResourceCellTemplateProps } from '@ts/scheduler/r1/components/types';
+
+import { combineClasses } from '../../../../core/r1/utils/render_utils';
+import type { GroupPanelTreeNode } from '../../../types';
+import { getResourceCellTemplateData } from '../../utils/group_panel_tree';
+
+export interface GroupPanelVerticalNodeProps extends Partial {
+ node: GroupPanelTreeNode;
+ index: number;
+ cellTemplate?: JSXTemplate;
+}
+
+export const GroupPanelVerticalNodeDefaultProps: GroupPanelVerticalNodeProps = {
+ node: {
+ key: '',
+ id: 0,
+ text: '',
+ data: { id: 0 },
+ resourceIndex: '',
+ path: [],
+ leafCount: 1,
+ children: [],
+ },
+ index: 0,
+ className: '',
+};
+
+export class GroupPanelVerticalNode extends BaseInfernoComponent {
+ render(): JSX.Element {
+ const {
+ node, index, cellTemplate, className,
+ } = this.props;
+ const isLeaf = node.children.length === 0;
+
+ const rowClasses = combineClasses({
+ 'dx-scheduler-group-row': true,
+ [className ?? '']: Boolean(className),
+ });
+ const cellClasses = combineClasses({
+ 'dx-scheduler-group-header': true,
+ 'dx-scheduler-group-header-leaf': isLeaf,
+ });
+
+ return (
+
+
+ {
+ cellTemplate
+ ?
+ : (
+
+ {node.text}
+
+ )
+ }
+
+ {
+ !isLeaf && (
+
+ {
+ node.children.map((child, childIndex) => )
+ }
+
+ )
+ }
+
+ );
+ }
+}
+
+GroupPanelVerticalNode.defaultProps = GroupPanelVerticalNodeDefaultProps;
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_row.test.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_row.test.tsx
index 5bf6832f691b..087d3c95f0fa 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_row.test.tsx
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_row.test.tsx
@@ -1,48 +1,39 @@
import {
- describe, expect, it, jest,
+ describe, expect, it,
} from '@jest/globals';
import { GroupPanelVerticalRow } from './group_panel_vertical_row';
-interface RenderUtilsMock {
- renderUtils: {
- addHeightToStyle: (
- height: number | undefined,
- styles?: Record,
- ) => Record;
- };
+interface VirtualNodeLike {
+ className?: string;
+ children?: VirtualNodeLike | VirtualNodeLike[];
}
-jest.mock('../../utils/index', (): RenderUtilsMock => ({
- renderUtils: {
- addHeightToStyle: (
- height: number | undefined,
- styles: Record = {},
- ): Record => (height === undefined ? styles : { ...styles, height }),
- },
-}));
+const toChildrenArray = (
+ children: VirtualNodeLike | VirtualNodeLike[] | undefined,
+): VirtualNodeLike[] => {
+ if (!children) {
+ return [];
+ }
-interface VirtualNodeLike {
- props?: {
- style?: unknown;
- };
-}
+ return Array.isArray(children) ? children : [children];
+};
describe('GroupPanelVerticalRow', () => {
- it('should apply row height', () => {
+ it('should render one group row with a cell per group item', () => {
const component = new GroupPanelVerticalRow({
- groupItems: [{
- key: '0',
- id: 0,
- text: 'Group 0',
- data: { id: 0 },
- resourceName: 'ownerId',
- }],
- height: 140,
- className: '',
+ groupItems: [
+ {
+ id: 1, text: 'a', key: 'one_1', resourceIndex: 'one', data: { id: 1, text: 'a' }, colSpan: 2, isLeaf: true, path: [],
+ },
+ {
+ id: 2, text: 'b', key: 'one_2', resourceIndex: 'one', data: { id: 2, text: 'b' }, colSpan: 2, isLeaf: true, path: [],
+ },
+ ],
});
const result = component.render() as VirtualNodeLike;
- expect(result.props?.style).toEqual({ height: '140px' });
+ expect(result.className).toContain('dx-scheduler-group-row');
+ expect(toChildrenArray(result.children)).toHaveLength(2);
});
});
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_row.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_row.tsx
index 024e9f102f95..0eb6e689a99c 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_row.tsx
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/base/group_panel_vertical_row.tsx
@@ -1,6 +1,5 @@
-import { BaseInfernoComponent, normalizeStyles } from '@ts/core/r1/runtime/inferno/index';
+import { BaseInfernoComponent } from '@ts/core/r1/runtime/inferno/index';
-import { renderUtils } from '../../utils/index';
import type { GroupPanelRowProps } from './group_panel_props';
import { GroupPanelRowDefaultProps } from './group_panel_props';
import { GroupPanelVerticalCell } from './group_panel_vertical_cell';
@@ -10,18 +9,11 @@ export class GroupPanelVerticalRow extends BaseInfernoComponent
+
{
groupItems.map(({
color,
@@ -29,6 +21,9 @@ export class GroupPanelVerticalRow extends BaseInfernoComponent )
}
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/timeline/header_panel_timeline.test.tsx b/packages/devextreme/js/__internal/scheduler/r1/components/timeline/header_panel_timeline.test.tsx
new file mode 100644
index 000000000000..071cefa0c532
--- /dev/null
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/timeline/header_panel_timeline.test.tsx
@@ -0,0 +1,82 @@
+import {
+ describe, expect, it,
+} from '@jest/globals';
+
+import type { GroupPanelData } from '../../../types';
+import type { GroupNode } from '../../../utils/resource_manager/types';
+import { getGroupPanelData } from '../../utils/base';
+import { HeaderPanel } from '../base/header_panel';
+import { HeaderPanelTimeline } from './header_panel_timeline';
+
+interface VirtualNodeLike {
+ type?: { name?: string } | string;
+ props?: Record;
+}
+
+const hierarchicalGroupsTree: GroupNode[] = [
+ {
+ id: 'A',
+ resourceText: 'Building A',
+ resourceIndex: 'buildingId',
+ grouped: { buildingId: 'A' },
+ children: [
+ {
+ id: 1,
+ resourceText: 'Room A1',
+ resourceIndex: 'roomId',
+ grouped: { buildingId: 'A', roomId: 1 },
+ children: [],
+ },
+ {
+ id: 2,
+ resourceText: 'Room A2',
+ resourceIndex: 'roomId',
+ grouped: { buildingId: 'A', roomId: 2 },
+ children: [],
+ },
+ ],
+ },
+];
+
+const hierarchicalGroupPanelData: GroupPanelData = getGroupPanelData(
+ hierarchicalGroupsTree,
+ 1,
+ false,
+ 1,
+ true,
+);
+
+const baseProps = {
+ groupPanelData: hierarchicalGroupPanelData,
+ groups: [{ name: 'buildingId', items: [], data: [] }],
+ groupByDate: false,
+ isRenderDateHeader: true,
+ dateHeaderData: {
+ dataMap: [], leftVirtualCellCount: 0, rightVirtualCellCount: 0,
+ },
+};
+
+describe('HeaderPanelTimeline', () => {
+ it('should delegate a hierarchical groupPanelData through to the shared HeaderPanel unchanged', () => {
+ const component = new HeaderPanelTimeline({
+ ...baseProps,
+ groupOrientation: 'horizontal',
+ } as any);
+ const result = component.render() as VirtualNodeLike;
+
+ expect((result.type as { name?: string })?.name).toBe(HeaderPanel.name);
+ expect(result.props?.groupPanelData).toBe(hierarchicalGroupPanelData);
+ expect(result.props?.groupOrientation).toBe('horizontal');
+ });
+
+ it('should still delegate correctly for vertical grouping (sidebar-driven, no header GroupPanel)', () => {
+ const component = new HeaderPanelTimeline({
+ ...baseProps,
+ groupOrientation: 'vertical',
+ } as any);
+ const result = component.render() as VirtualNodeLike;
+
+ expect(result.props?.groupPanelData).toBe(hierarchicalGroupPanelData);
+ expect(result.props?.groupOrientation).toBe('vertical');
+ });
+});
diff --git a/packages/devextreme/js/__internal/scheduler/r1/components/types.ts b/packages/devextreme/js/__internal/scheduler/r1/components/types.ts
index e710f36c81db..9be55b1aeaa4 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/components/types.ts
+++ b/packages/devextreme/js/__internal/scheduler/r1/components/types.ts
@@ -1,7 +1,10 @@
import type { JSXTemplate } from '@ts/core/r1/types';
import type { GroupLeaf } from '@ts/scheduler/utils/resource_manager/types';
-import type { GroupItem, ViewDataBase, ViewType } from '../../types';
+import type {
+ GroupHeaderPathItem, GroupItem, ViewDataBase, ViewType,
+} from '../../types';
+import type { ResourceId } from '../../utils/loader/types';
export interface BaseTemplateProps {
index: number;
@@ -32,11 +35,15 @@ export interface DateTimeCellTemplateProps extends BaseTemplateProps {
data: DateCellTemplateData;
}
-interface ResourceCellTemplateData {
+export interface ResourceCellTemplateData {
data: GroupItem;
- id: number | string;
+ id: ResourceId;
text?: string;
color?: string;
+ resourceIndex: string;
+ level: number;
+ isLeaf: boolean;
+ path: GroupHeaderPathItem[];
}
export interface ResourceCellTemplateProps extends BaseTemplateProps {
diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts
index 4f53f96fdf21..4cb270ff3656 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts
@@ -3,7 +3,8 @@ import {
} from '@jest/globals';
import { HORIZONTAL_GROUP_ORIENTATION, VERTICAL_GROUP_ORIENTATION } from '@ts/scheduler/constants';
-import type { ViewType } from '../../../types';
+import type { GroupHeaderPathItem, GroupRenderItem, ViewType } from '../../../types';
+import type { GroupNode } from '../../../utils/resource_manager/types';
import {
getAppointmentKey,
getCellDuration,
@@ -12,6 +13,7 @@ import {
getIsGroupedAllDayPanel,
getKeyByGroup,
getSkippedHoursInRange,
+ getTimelineGroupPanelRows,
isAppointmentTakesAllDay,
isGroupingByDate,
isHorizontalGroupingApplied,
@@ -434,218 +436,269 @@ describe('base utils', () => {
});
describe('getGroupPanelData', () => {
- const groupsBase: any = [{
- resourceIndex: 'group1',
- resourceName: 'group 1',
- items: [{
- text: 'item 1', id: 1, color: 'color 1',
- }, {
- text: 'item 2', id: 2, color: 'color 2',
- }],
- data: [{
- text: 'item 1', id: 1, color: 'color 1',
- }, {
- text: 'item 2', id: 2, color: 'color 2',
- }],
- }, {
- resourceIndex: 'group2',
- resourceName: 'group 2',
- items: [{
- text: 'item 3', id: 1, color: 'color 3',
- }, {
- text: 'item 4', id: 2, color: 'color 4',
- }],
- data: [{
- text: 'item 3', id: 1, color: 'color 3',
- }, {
- text: 'item 4', id: 2, color: 'color 4',
- }],
- }];
-
- it('should transform grouping data into group items', () => {
- const groupPanelData = getGroupPanelData(groupsBase, 1, false, 3);
-
- expect(groupPanelData)
- .toEqual({
- groupPanelItems: [[{
- ...groupsBase[0].items[0],
- data: groupsBase[0].data[0],
- resourceName: groupsBase[0].resourceName,
- key: '0_group1_1',
- }, {
- ...groupsBase[0].items[1],
- data: groupsBase[0].data[1],
- resourceName: groupsBase[0].resourceName,
- key: '0_group1_2',
- }], [{
- ...groupsBase[1].items[0],
- data: groupsBase[1].data[0],
- resourceName: groupsBase[1].resourceName,
- key: '0_group2_1',
- }, {
- ...groupsBase[1].items[1],
- data: groupsBase[1].data[1],
- resourceName: groupsBase[1].resourceName,
- key: '0_group2_2',
- }, {
- ...groupsBase[1].items[0],
- data: groupsBase[1].data[0],
- resourceName: groupsBase[1].resourceName,
- key: '1_group2_1',
- }, {
- ...groupsBase[1].items[1],
- data: groupsBase[1].data[1],
- resourceName: groupsBase[1].resourceName,
- key: '1_group2_2',
- }]],
- baseColSpan: 3,
- });
+ // group1 (2 items) x group2 (2 items), uniform 2-level tree
+ const groupsTreeBase: GroupNode[] = [
+ {
+ id: 1,
+ resourceText: 'item 1',
+ color: 'color 1',
+ resourceIndex: 'group1',
+ grouped: { group1: 1 },
+ children: [
+ {
+ id: 1, resourceText: 'item 3', color: 'color 3', resourceIndex: 'group2', grouped: { group1: 1, group2: 1 }, children: [],
+ },
+ {
+ id: 2, resourceText: 'item 4', color: 'color 4', resourceIndex: 'group2', grouped: { group1: 1, group2: 2 }, children: [],
+ },
+ ],
+ },
+ {
+ id: 2,
+ resourceText: 'item 2',
+ color: 'color 2',
+ resourceIndex: 'group1',
+ grouped: { group1: 2 },
+ children: [
+ {
+ id: 1, resourceText: 'item 3', color: 'color 3', resourceIndex: 'group2', grouped: { group1: 2, group2: 1 }, children: [],
+ },
+ {
+ id: 2, resourceText: 'item 4', color: 'color 4', resourceIndex: 'group2', grouped: { group1: 2, group2: 2 }, children: [],
+ },
+ ],
+ },
+ ];
+
+ type AncestorArgs = [
+ id: number | string,
+ text: string,
+ color: string | undefined,
+ resourceIndex: string,
+ ];
+
+ const pathItem = (
+ [id, text, color, resourceIndex]: AncestorArgs,
+ ): GroupHeaderPathItem => ({
+ id,
+ text,
+ color,
+ resourceIndex,
+ data: { id, text, color },
});
- it('should work when data parameter is undefined', () => {
- const groups = [{
- resourceIndex: 'group1',
- resourceName: 'group 1',
- items: [{
- text: 'item 1', id: 1, color: 'color 1',
- }, {
- text: 'item 2', id: 2, color: 'color 2',
- }],
- }] as any;
- const groupPanelData = getGroupPanelData(groups, 1, false, 5);
+ const renderItem = (
+ id: number | string,
+ text: string,
+ color: string | undefined,
+ key: string,
+ resourceIndex: string,
+ colSpan: number,
+ extra: Partial = {},
+ ancestors: AncestorArgs[] = [],
+ ): GroupRenderItem => ({
+ id,
+ text,
+ color,
+ key,
+ resourceIndex,
+ data: { id, text, color },
+ colSpan,
+ isLeaf: true,
+ isLastColumn: false,
+ path: [...ancestors, [id, text, color, resourceIndex] as AncestorArgs].map(pathItem),
+ ...extra,
+ });
- expect(groupPanelData)
- .toEqual({
- groupPanelItems: [[{
- ...groups[0].items[0],
- resourceName: groups[0].resourceName,
- key: '0_group1_1',
- }, {
- ...groups[0].items[1],
- resourceName: groups[0].resourceName,
- key: '0_group1_2',
- }]],
- baseColSpan: 5,
- });
+ const group1Item1: AncestorArgs = [1, 'item 1', 'color 1', 'group1'];
+ const group1Item2: AncestorArgs = [2, 'item 2', 'color 2', 'group1'];
+
+ const firstInRepeat = { isFirstGroupCell: true, isLastGroupCell: false };
+ const middleInRepeat = { isFirstGroupCell: false, isLastGroupCell: false };
+ const lastInRepeat = { isFirstGroupCell: false, isLastGroupCell: true };
+
+ it('should transform a uniform-depth tree into per-depth rows with real colSpan', () => {
+ const groupPanelData = getGroupPanelData(groupsTreeBase, 1, false, 3);
+
+ expect(groupPanelData.maxDepth).toBe(2);
+ expect(groupPanelData.baseColSpan).toBe(3);
+ expect(groupPanelData.groupTree[0].leafCount).toBe(2);
+ expect(groupPanelData.groupTree[0].children[0].leafCount).toBe(1);
+ expect(groupPanelData.groupPanelItems).toEqual([
+ [
+ renderItem(1, 'item 1', 'color 1', 'group1_1', 'group1', 6, { isLeaf: false }),
+ renderItem(2, 'item 2', 'color 2', 'group1_2', 'group1', 6, { isLeaf: false, isLastColumn: true }),
+ ],
+ [
+ renderItem(1, 'item 3', 'color 3', 'group1_1_group2_1', 'group2', 3, {}, [group1Item1]),
+ renderItem(2, 'item 4', 'color 4', 'group1_1_group2_2', 'group2', 3, {}, [group1Item1]),
+ renderItem(1, 'item 3', 'color 3', 'group1_2_group2_1', 'group2', 3, {}, [group1Item2]),
+ renderItem(2, 'item 4', 'color 4', 'group1_2_group2_2', 'group2', 3, { isLastColumn: true }, [group1Item2]),
+ ],
+ ]);
});
- it('should exclude zero items resources', () => {
- const groups = [{
- resourceIndex: 'group1',
- resourceName: 'group 1',
- items: [{
- text: 'item 1', id: 1, color: 'color 1',
- }],
- }, {
- resourceIndex: 'group2',
- resourceName: 'group 2',
- items: [],
- }] as any;
+ it('should work for a single-level tree (maxDepth 1, no rowSpan)', () => {
+ const groups: GroupNode[] = [
+ {
+ id: 1, resourceText: 'item 1', color: 'color 1', resourceIndex: 'group1', grouped: { group1: 1 }, children: [],
+ },
+ {
+ id: 2, resourceText: 'item 2', color: 'color 2', resourceIndex: 'group1', grouped: { group1: 2 }, children: [],
+ },
+ ];
const groupPanelData = getGroupPanelData(groups, 1, false, 5);
- expect(groupPanelData)
- .toEqual({
- groupPanelItems: [[{
- ...groups[0].items[0],
- resourceName: groups[0].resourceName,
- key: '0_group1_1',
- }]],
- baseColSpan: 5,
- });
+ expect(groupPanelData.maxDepth).toBe(1);
+ expect(groupPanelData.groupPanelItems).toEqual([
+ [
+ renderItem(1, 'item 1', 'color 1', 'group1_1', 'group1', 5),
+ renderItem(2, 'item 2', 'color 2', 'group1_2', 'group1', 5, { isLastColumn: true }),
+ ],
+ ]);
});
- it('should transform grouping data into group items corectly when appointments are groupped by date', () => {
- const groupPanelData = getGroupPanelData(groupsBase, 2, true, 7);
-
- expect(groupPanelData)
- .toEqual({
- groupPanelItems: [[{
- ...groupsBase[0].items[0],
- data: groupsBase[0].data[0],
- resourceName: groupsBase[0].resourceName,
- key: '0_group1_1_group_by_date_0',
- isFirstGroupCell: true,
- isLastGroupCell: false,
- }, {
- ...groupsBase[0].items[1],
- data: groupsBase[0].data[1],
- resourceName: groupsBase[0].resourceName,
- key: '0_group1_2_group_by_date_0',
- isFirstGroupCell: false,
- isLastGroupCell: true,
- }, {
- ...groupsBase[0].items[0],
- data: groupsBase[0].data[0],
- resourceName: groupsBase[0].resourceName,
- key: '0_group1_1_group_by_date_1',
- isFirstGroupCell: true,
- isLastGroupCell: false,
- }, {
- ...groupsBase[0].items[1],
- data: groupsBase[0].data[1],
- resourceName: groupsBase[0].resourceName,
- key: '0_group1_2_group_by_date_1',
- isFirstGroupCell: false,
- isLastGroupCell: true,
- }], [{
- ...groupsBase[1].items[0],
- data: groupsBase[1].data[0],
- resourceName: groupsBase[1].resourceName,
- key: '0_group2_1_group_by_date_0',
- isFirstGroupCell: true,
- isLastGroupCell: false,
- }, {
- ...groupsBase[1].items[1],
- data: groupsBase[1].data[1],
- resourceName: groupsBase[1].resourceName,
- key: '0_group2_2_group_by_date_0',
- isFirstGroupCell: false,
- isLastGroupCell: false,
- }, {
- ...groupsBase[1].items[0],
- data: groupsBase[1].data[0],
- resourceName: groupsBase[1].resourceName,
- key: '1_group2_1_group_by_date_0',
- isFirstGroupCell: false,
- isLastGroupCell: false,
- }, {
- ...groupsBase[1].items[1],
- data: groupsBase[1].data[1],
- resourceName: groupsBase[1].resourceName,
- key: '1_group2_2_group_by_date_0',
- isFirstGroupCell: false,
- isLastGroupCell: true,
- }, {
- ...groupsBase[1].items[0],
- data: groupsBase[1].data[0],
- resourceName: groupsBase[1].resourceName,
- key: '0_group2_1_group_by_date_1',
- isFirstGroupCell: true,
- isLastGroupCell: false,
- }, {
- ...groupsBase[1].items[1],
- data: groupsBase[1].data[1],
- resourceName: groupsBase[1].resourceName,
- key: '0_group2_2_group_by_date_1',
- isFirstGroupCell: false,
- isLastGroupCell: false,
- }, {
- ...groupsBase[1].items[0],
- data: groupsBase[1].data[0],
- resourceName: groupsBase[1].resourceName,
- key: '1_group2_1_group_by_date_1',
- isFirstGroupCell: false,
- isLastGroupCell: false,
- }, {
- ...groupsBase[1].items[1],
- data: groupsBase[1].data[1],
- resourceName: groupsBase[1].resourceName,
- key: '1_group2_2_group_by_date_1',
- isFirstGroupCell: false,
- isLastGroupCell: true,
- }]],
- baseColSpan: 7,
- });
+ it('should fill a childless (shallower) branch down via rowSpan for non-uniform depth', () => {
+ const groups: GroupNode[] = [
+ {
+ id: 'A',
+ resourceText: 'Building A',
+ resourceIndex: 'buildingId',
+ grouped: { buildingId: 'A' },
+ children: [
+ {
+ id: 1, resourceText: 'Room A1', resourceIndex: 'roomId', grouped: { buildingId: 'A', roomId: 1 }, children: [],
+ },
+ ],
+ },
+ {
+ id: 'B', resourceText: 'Building B', resourceIndex: 'buildingId', grouped: { buildingId: 'B' }, children: [],
+ },
+ ];
+ const groupPanelData = getGroupPanelData(groups, 1, false, 1);
+
+ expect(groupPanelData.maxDepth).toBe(2);
+ expect(groupPanelData.groupPanelItems).toEqual([
+ [
+ renderItem('A', 'Building A', undefined, 'buildingId_A', 'buildingId', 1, { isLeaf: false }),
+ renderItem('B', 'Building B', undefined, 'buildingId_B', 'buildingId', 1, { rowSpan: 2, isLastColumn: true }),
+ ],
+ [
+ renderItem(1, 'Room A1', undefined, 'buildingId_A_roomId_1', 'roomId', 1, {}, [['A', 'Building A', undefined, 'buildingId']]),
+ ],
+ ]);
+ });
+
+ it('should transform grouping data into group items correctly when appointments are grouped by date', () => {
+ const groupPanelData = getGroupPanelData(groupsTreeBase, 2, true, 7);
+
+ expect(groupPanelData.groupPanelItems).toEqual([
+ [
+ renderItem(1, 'item 1', 'color 1', 'group1_1_group_by_date_0', 'group1', 14, { isLeaf: false, ...firstInRepeat }),
+ renderItem(2, 'item 2', 'color 2', 'group1_2_group_by_date_0', 'group1', 14, { isLeaf: false, isLastColumn: true, ...lastInRepeat }),
+ renderItem(1, 'item 1', 'color 1', 'group1_1_group_by_date_1', 'group1', 14, { isLeaf: false, ...firstInRepeat }),
+ renderItem(2, 'item 2', 'color 2', 'group1_2_group_by_date_1', 'group1', 14, { isLeaf: false, isLastColumn: true, ...lastInRepeat }),
+ ],
+ [
+ renderItem(1, 'item 3', 'color 3', 'group1_1_group2_1_group_by_date_0', 'group2', 7, { ...firstInRepeat }, [group1Item1]),
+ renderItem(2, 'item 4', 'color 4', 'group1_1_group2_2_group_by_date_0', 'group2', 7, { ...middleInRepeat }, [group1Item1]),
+ renderItem(1, 'item 3', 'color 3', 'group1_2_group2_1_group_by_date_0', 'group2', 7, { ...middleInRepeat }, [group1Item2]),
+ renderItem(2, 'item 4', 'color 4', 'group1_2_group2_2_group_by_date_0', 'group2', 7, { isLastColumn: true, ...lastInRepeat }, [group1Item2]),
+ renderItem(1, 'item 3', 'color 3', 'group1_1_group2_1_group_by_date_1', 'group2', 7, { ...firstInRepeat }, [group1Item1]),
+ renderItem(2, 'item 4', 'color 4', 'group1_1_group2_2_group_by_date_1', 'group2', 7, { ...middleInRepeat }, [group1Item1]),
+ renderItem(1, 'item 3', 'color 3', 'group1_2_group2_1_group_by_date_1', 'group2', 7, { ...middleInRepeat }, [group1Item2]),
+ renderItem(2, 'item 4', 'color 4', 'group1_2_group2_2_group_by_date_1', 'group2', 7, { isLastColumn: true, ...lastInRepeat }, [group1Item2]),
+ ],
+ ]);
+ expect(groupPanelData.baseColSpan).toBe(7);
+ });
+ });
+
+ describe('getTimelineGroupPanelRows', () => {
+ it('should keep a single stacked row for flat timeline grouping', () => {
+ const groups: GroupNode[] = [
+ {
+ id: 0, resourceText: 'Group_0', resourceIndex: 'any', grouped: { any: 0 }, children: [],
+ },
+ {
+ id: 1, resourceText: 'Group_1', resourceIndex: 'any', grouped: { any: 1 }, children: [],
+ },
+ ];
+ const groupPanelData = getGroupPanelData(groups, 1, false, 3);
+ const timelineRows = getTimelineGroupPanelRows(groupPanelData, false);
+
+ expect(timelineRows).toEqual(groupPanelData.groupPanelItems);
+ expect(timelineRows).toHaveLength(1);
+ });
+
+ it('should keep depth rows for multi-group cartesian timeline grouping', () => {
+ const groups: GroupNode[] = [
+ {
+ id: 1,
+ resourceText: 'item 1',
+ resourceIndex: 'group1',
+ grouped: { group1: 1 },
+ children: [
+ {
+ id: 1, resourceText: 'item 3', resourceIndex: 'group2', grouped: { group1: 1, group2: 1 }, children: [],
+ },
+ {
+ id: 2, resourceText: 'item 4', resourceIndex: 'group2', grouped: { group1: 1, group2: 2 }, children: [],
+ },
+ ],
+ },
+ {
+ id: 2,
+ resourceText: 'item 2',
+ resourceIndex: 'group1',
+ grouped: { group1: 2 },
+ children: [
+ {
+ id: 1, resourceText: 'item 3', resourceIndex: 'group2', grouped: { group1: 2, group2: 1 }, children: [],
+ },
+ {
+ id: 2, resourceText: 'item 4', resourceIndex: 'group2', grouped: { group1: 2, group2: 2 }, children: [],
+ },
+ ],
+ },
+ ];
+ const groupPanelData = getGroupPanelData(groups, 1, false, 3, false);
+ const timelineRows = getTimelineGroupPanelRows(groupPanelData, false);
+
+ expect(timelineRows).toEqual(groupPanelData.groupPanelItems);
+ expect(timelineRows).toHaveLength(2);
+ });
+
+ it('should use one row per leaf path for hierarchical timeline grouping', () => {
+ const groups: GroupNode[] = [
+ {
+ id: 'A',
+ resourceText: 'Building A',
+ resourceIndex: 'buildingId',
+ grouped: { buildingId: 'A' },
+ children: [
+ {
+ id: 1, resourceText: 'Room A1', resourceIndex: 'roomId', grouped: { buildingId: 'A', roomId: 1 }, children: [],
+ },
+ ],
+ },
+ {
+ id: 'B', resourceText: 'Building B', resourceIndex: 'buildingId', grouped: { buildingId: 'B' }, children: [],
+ },
+ ];
+ const groupPanelData = getGroupPanelData(groups, 1, false, 1, true);
+ const timelineRows = getTimelineGroupPanelRows(groupPanelData, false);
+
+ expect(timelineRows).toEqual([
+ [
+ expect.objectContaining({ key: 'buildingId_A', text: 'Building A' }),
+ expect.objectContaining({ key: 'buildingId_A_roomId_1', text: 'Room A1' }),
+ ],
+ [
+ expect.objectContaining({ key: 'buildingId_B', text: 'Building B' }),
+ ],
+ ]);
+ expect(timelineRows).not.toEqual(groupPanelData.groupPanelItems);
});
});
diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/group_panel_tree.test.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/group_panel_tree.test.ts
new file mode 100644
index 000000000000..4c7f8137cd21
--- /dev/null
+++ b/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/group_panel_tree.test.ts
@@ -0,0 +1,441 @@
+import {
+ describe, expect, it,
+} from '@jest/globals';
+
+import type { ResourceId } from '../../../utils/loader/types';
+import type { GroupNode } from '../../../utils/resource_manager/types';
+import {
+ buildGroupPanelTree,
+ flattenGroupPanelTreeToLeafRows,
+ flattenGroupPanelTreeToRows,
+ getGroupPanelTreeDepth,
+ getResourceCellTemplateData,
+} from '../group_panel_tree';
+
+const node = (
+ id: ResourceId,
+ resourceText: string,
+ resourceIndex: string,
+ children: GroupNode[] = [],
+ color?: string,
+): GroupNode => ({
+ id,
+ resourceText,
+ resourceIndex,
+ color,
+ grouped: { [resourceIndex]: id },
+ children,
+});
+
+describe('group_panel_tree', () => {
+ describe('buildGroupPanelTree', () => {
+ it('should annotate a flat (single-level) tree', () => {
+ const tree = [
+ node('1', 'Room 1', 'roomId', [], '#aaa'),
+ node('2', 'Room 2', 'roomId', [], '#ccc'),
+ ];
+
+ const result = buildGroupPanelTree(tree);
+
+ expect(result).toEqual([
+ {
+ key: 'roomId_1',
+ id: '1',
+ text: 'Room 1',
+ color: '#aaa',
+ data: { id: '1', text: 'Room 1', color: '#aaa' },
+ resourceIndex: 'roomId',
+ path: [{
+ id: '1',
+ text: 'Room 1',
+ color: '#aaa',
+ resourceIndex: 'roomId',
+ data: { id: '1', text: 'Room 1', color: '#aaa' },
+ }],
+ leafCount: 1,
+ children: [],
+ },
+ {
+ key: 'roomId_2',
+ id: '2',
+ text: 'Room 2',
+ color: '#ccc',
+ data: { id: '2', text: 'Room 2', color: '#ccc' },
+ resourceIndex: 'roomId',
+ path: [{
+ id: '2',
+ text: 'Room 2',
+ color: '#ccc',
+ resourceIndex: 'roomId',
+ data: { id: '2', text: 'Room 2', color: '#ccc' },
+ }],
+ leafCount: 1,
+ children: [],
+ },
+ ]);
+ });
+
+ it('should pass through full resourceData for resourceCellTemplate', () => {
+ const tree = [{
+ ...node('1', 'John Heart', 'employeeID', [], '#aaa'),
+ resourceData: {
+ id: 1,
+ text: 'John Heart',
+ color: '#aaa',
+ age: 27,
+ avatar: '19.png',
+ discipline: 'ABS, Fitball, StepFit',
+ },
+ }];
+
+ const result = buildGroupPanelTree(tree);
+
+ expect(result[0].data).toEqual({
+ id: 1,
+ text: 'John Heart',
+ color: '#aaa',
+ age: 27,
+ avatar: '19.png',
+ discipline: 'ABS, Fitball, StepFit',
+ });
+ });
+
+ it('should preserve object resource ids in group panel data', () => {
+ const objectId = { _value: 'guid-1' };
+ const tree = [{
+ ...node(objectId, 'Owner one', 'ownerId', [], 'rgb(255, 0, 0)'),
+ resourceData: {
+ id: objectId,
+ text: 'Owner one',
+ color: 'rgb(255, 0, 0)',
+ },
+ }];
+
+ const result = buildGroupPanelTree(tree);
+
+ expect(result[0].id).toBe(objectId);
+ expect(result[0].data.id).toBe(objectId);
+ expect(result[0].key).toBe('ownerId_{"_value":"guid-1"}');
+ });
+
+ it('should omit color from data when it is undefined', () => {
+ const tree = [
+ node('1', 'John', 'ownerId'),
+ ];
+
+ const result = buildGroupPanelTree(tree);
+
+ expect(result[0].data).toEqual({ id: '1', text: 'John' });
+ expect(result[0].data).not.toHaveProperty('color');
+ });
+
+ it('should compute leafCount for a uniform-depth tree from real descendant counts', () => {
+ const tree = [
+ node('A', 'Room A', 'roomId', [
+ node('1', 'John', 'ownerId'),
+ node('2', 'Jane', 'ownerId'),
+ ]),
+ node('B', 'Room B', 'roomId', [
+ node('1', 'John', 'ownerId'),
+ ]),
+ ];
+
+ const result = buildGroupPanelTree(tree);
+
+ expect(result[0].leafCount).toBe(2);
+ expect(result[1].leafCount).toBe(1);
+ });
+
+ it('should give every node a unique key even when the same id repeats under different parents', () => {
+ const tree = [
+ node('A', 'Room A', 'roomId', [node('1', 'John', 'ownerId')]),
+ node('B', 'Room B', 'roomId', [node('1', 'John', 'ownerId')]),
+ ];
+
+ const result = buildGroupPanelTree(tree);
+ const childKeys = result.map((parent) => parent.children[0].key);
+
+ expect(new Set(childKeys).size).toBe(2);
+ });
+
+ it('should treat a childless node as its own leaf (leafCount 1) for non-uniform depth', () => {
+ const tree = [
+ node('A', 'Building A', 'buildingId', [
+ node('1', 'Room A1', 'roomId'),
+ node('2', 'Room A2', 'roomId'),
+ ]),
+ node('B', 'Building B', 'buildingId', []),
+ ];
+
+ const result = buildGroupPanelTree(tree);
+
+ expect(result[0].leafCount).toBe(2);
+ expect(result[1].leafCount).toBe(1);
+ });
+
+ it('should annotate the root-to-cell path for every node', () => {
+ const tree = [
+ node('A', 'Building A', 'buildingId', [
+ node('1', 'Room A1', 'roomId', [
+ node('x', 'Desk x', 'deskId'),
+ ]),
+ ]),
+ ];
+
+ const [building] = buildGroupPanelTree(tree);
+ const [room] = building.children;
+ const [desk] = room.children;
+
+ expect(building.path).toEqual([
+ {
+ id: 'A', text: 'Building A', color: undefined, resourceIndex: 'buildingId', data: { id: 'A', text: 'Building A' },
+ },
+ ]);
+ expect(desk.path.map((item) => item.text)).toEqual(['Building A', 'Room A1', 'Desk x']);
+ expect(desk.path[desk.path.length - 1].id).toBe('x');
+ });
+
+ it('should not share path items between branches with equal ids', () => {
+ const tree = [
+ node('A', 'Building A', 'buildingId', [node('1', 'Room 1', 'roomId')]),
+ node('B', 'Building B', 'buildingId', [node('1', 'Room 1', 'roomId')]),
+ ];
+
+ const result = buildGroupPanelTree(tree);
+
+ expect(result[0].children[0].path.map((item) => item.text)).toEqual(['Building A', 'Room 1']);
+ expect(result[1].children[0].path.map((item) => item.text)).toEqual(['Building B', 'Room 1']);
+ });
+ });
+
+ describe('getGroupPanelTreeDepth', () => {
+ it('should return 0 for an empty tree', () => {
+ expect(getGroupPanelTreeDepth([])).toBe(0);
+ });
+
+ it('should return 1 for a flat single-level tree', () => {
+ const tree = buildGroupPanelTree([node('1', 'Room 1', 'roomId')]);
+
+ expect(getGroupPanelTreeDepth(tree)).toBe(1);
+ });
+
+ it('should return the depth of the deepest branch for a non-uniform tree', () => {
+ const tree = buildGroupPanelTree([
+ node('A', 'Building A', 'buildingId', [
+ node('1', 'Room A1', 'roomId', [
+ node('x', 'Desk x', 'deskId'),
+ ]),
+ node('2', 'Room A2', 'roomId'),
+ ]),
+ node('B', 'Building B', 'buildingId'),
+ ]);
+
+ expect(getGroupPanelTreeDepth(tree)).toBe(3);
+ });
+ });
+
+ describe('flattenGroupPanelTreeToRows', () => {
+ it('should compute colSpan from leafCount and fill missing depth with rowSpan for a shallow leaf', () => {
+ const tree = buildGroupPanelTree([
+ node('A', 'Building A', 'buildingId', [
+ node('1', 'Room A1', 'roomId'),
+ node('2', 'Room A2', 'roomId'),
+ ]),
+ node('B', 'Building B', 'buildingId'),
+ ]);
+ const maxDepth = getGroupPanelTreeDepth(tree);
+
+ const rows = flattenGroupPanelTreeToRows(tree, maxDepth, 3);
+
+ expect(maxDepth).toBe(2);
+ expect(rows).toHaveLength(2);
+ expect(rows[0]).toEqual([
+ expect.objectContaining({ id: 'A', colSpan: 2 * 3 }),
+ expect.objectContaining({ id: 'B', colSpan: 1 * 3, rowSpan: 2 }),
+ ]);
+ expect(rows[1]).toEqual([
+ expect.objectContaining({ id: '1', colSpan: 1 * 3 }),
+ expect.objectContaining({ id: '2', colSpan: 1 * 3 }),
+ ]);
+ });
+
+ it('should mark cells that do not reach the last column of the table', () => {
+ const tree = buildGroupPanelTree([
+ node('A', 'Building A', 'buildingId', [
+ node('1', 'Room A1', 'roomId'),
+ node('2', 'Room A2', 'roomId'),
+ ]),
+ node('B', 'Building B', 'buildingId', [
+ node('3', 'Room B1', 'roomId'),
+ ]),
+ node('lobby', 'Lobby', 'buildingId'),
+ ]);
+
+ const rows = flattenGroupPanelTreeToRows(tree, 2, 1);
+
+ expect(rows[0].map(({ text, isLastColumn }) => [text, isLastColumn])).toEqual([
+ ['Building A', false],
+ ['Building B', false],
+ ['Lobby', true],
+ ]);
+ // Room B1 ends the last row, but the Lobby cell spans the columns to its right
+ expect(rows[1].map(({ text, isLastColumn }) => [text, isLastColumn])).toEqual([
+ ['Room A1', false],
+ ['Room A2', false],
+ ['Room B1', false],
+ ]);
+ });
+
+ it('should keep isLeaf and path on the flattened header rows', () => {
+ const tree = buildGroupPanelTree([
+ node('A', 'Building A', 'buildingId', [
+ node('1', 'Room A1', 'roomId'),
+ ]),
+ ]);
+
+ const rows = flattenGroupPanelTreeToRows(tree, 2, 1);
+
+ expect(rows[0][0]).toEqual(expect.objectContaining({
+ isLeaf: false,
+ path: [expect.objectContaining({ text: 'Building A' })],
+ }));
+ expect(rows[1][0]).toEqual(expect.objectContaining({
+ isLeaf: true,
+ path: [
+ expect.objectContaining({ text: 'Building A' }),
+ expect.objectContaining({ text: 'Room A1' }),
+ ],
+ }));
+ });
+
+ it('should handle 3-level non-uniform depth (shallow leaf spans the remaining 2 rows)', () => {
+ const tree = buildGroupPanelTree([
+ node('X', 'Region X', 'regionId', [
+ node('X1', 'Building X1', 'buildingId', [
+ node('a', 'Room a', 'roomId'),
+ node('b', 'Room b', 'roomId'),
+ ]),
+ node('X2', 'Building X2', 'buildingId'),
+ ]),
+ ]);
+ const maxDepth = getGroupPanelTreeDepth(tree);
+
+ const rows = flattenGroupPanelTreeToRows(tree, maxDepth, 1);
+
+ expect(maxDepth).toBe(3);
+ expect(rows[0]).toEqual([
+ expect.objectContaining({ id: 'X', colSpan: 3 }),
+ ]);
+ expect(rows[1]).toEqual([
+ expect.objectContaining({ id: 'X1', colSpan: 2 }),
+ expect.objectContaining({ id: 'X2', colSpan: 1, rowSpan: 2 }),
+ ]);
+ expect(rows[2]).toEqual([
+ expect.objectContaining({ id: 'a', colSpan: 1 }),
+ expect.objectContaining({ id: 'b', colSpan: 1 }),
+ ]);
+ });
+ });
+
+ describe('flattenGroupPanelTreeToLeafRows', () => {
+ it('should produce one row per leaf with cells along the root-to-leaf path', () => {
+ const tree = buildGroupPanelTree([
+ node('A', 'Building A', 'buildingId', [
+ node('1', 'Room A1', 'roomId'),
+ node('2', 'Room A2', 'roomId'),
+ ]),
+ node('B', 'Building B', 'buildingId'),
+ ]);
+
+ expect(flattenGroupPanelTreeToLeafRows(tree, 1)).toEqual([
+ [
+ expect.objectContaining({ key: 'buildingId_A', text: 'Building A' }),
+ expect.objectContaining({ key: 'buildingId_A_roomId_1', text: 'Room A1' }),
+ ],
+ [
+ expect.objectContaining({ key: 'buildingId_A', text: 'Building A' }),
+ expect.objectContaining({ key: 'buildingId_A_roomId_2', text: 'Room A2' }),
+ ],
+ [
+ expect.objectContaining({ key: 'buildingId_B', text: 'Building B' }),
+ ],
+ ]);
+ });
+
+ it('should produce one row per top-level leaf for a flat tree', () => {
+ const tree = buildGroupPanelTree([
+ node('1', 'Room 1', 'roomId'),
+ node('2', 'Room 2', 'roomId'),
+ ]);
+
+ expect(flattenGroupPanelTreeToLeafRows(tree, 3)).toEqual([
+ [expect.objectContaining({ key: 'roomId_1', text: 'Room 1', colSpan: 3 })],
+ [expect.objectContaining({ key: 'roomId_2', text: 'Room 2', colSpan: 3 })],
+ ]);
+ });
+
+ it('should keep isLeaf and path on every cell of a leaf row', () => {
+ const tree = buildGroupPanelTree([
+ node('A', 'Building A', 'buildingId', [
+ node('1', 'Room A1', 'roomId'),
+ ]),
+ ]);
+
+ const [row] = flattenGroupPanelTreeToLeafRows(tree, 1);
+
+ expect(row.map(({ isLeaf }) => isLeaf)).toEqual([false, true]);
+ expect(row[1].path.map((item) => item.text)).toEqual(['Building A', 'Room A1']);
+ });
+ });
+
+ describe('getResourceCellTemplateData', () => {
+ it('should build the template model from a group panel tree node', () => {
+ const tree = buildGroupPanelTree([
+ node('A', 'Building A', 'buildingId', [
+ node('1', 'Room A1', 'roomId', [], '#aaa'),
+ ]),
+ ]);
+ const [room] = tree[0].children;
+
+ expect(getResourceCellTemplateData(room)).toEqual({
+ data: { id: '1', text: 'Room A1', color: '#aaa' },
+ id: '1',
+ text: 'Room A1',
+ color: '#aaa',
+ resourceIndex: 'roomId',
+ level: 1,
+ isLeaf: true,
+ path: room.path,
+ });
+ });
+
+ it.each([
+ ['is not provided', undefined],
+ ['is empty', []],
+ ])('should fall back to a single-level model when the path %s', (_, path) => {
+ expect(getResourceCellTemplateData({
+ id: 1,
+ text: 'Room 1',
+ color: '#aaa',
+ data: { id: 1, text: 'Room 1' },
+ resourceIndex: 'roomId',
+ path,
+ })).toEqual({
+ data: { id: 1, text: 'Room 1' },
+ id: 1,
+ text: 'Room 1',
+ color: '#aaa',
+ resourceIndex: 'roomId',
+ level: 0,
+ isLeaf: true,
+ path: [{
+ id: 1,
+ text: 'Room 1',
+ color: '#aaa',
+ resourceIndex: 'roomId',
+ data: { id: 1, text: 'Room 1' },
+ }],
+ });
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts
index 5b81793a51af..8833292877ce 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts
+++ b/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts
@@ -1,9 +1,8 @@
import dateLocalization from '@js/common/core/localization/date';
import dateUtils from '@js/core/utils/date';
-import { isDefined, isObject } from '@js/core/utils/type';
+import { isDefined } from '@js/core/utils/type';
import { dateUtilsTs } from '@ts/core/utils/date';
-import { VERTICAL_GROUP_COUNT_CLASSES } from '../../classes';
import {
HORIZONTAL_GROUP_ORIENTATION, VERTICAL_GROUP_ORIENTATION,
} from '../../constants';
@@ -20,11 +19,16 @@ import type {
ViewDataProviderType,
ViewType,
} from '../../types';
-import type { ResourceLoader } from '../../utils/loader/resource_loader';
-import type { ResourceId } from '../../utils/loader/types';
import { VIEWS } from '../../utils/options/constants_view';
+import type { GroupNode } from '../../utils/resource_manager/types';
import timeZoneUtils from '../../utils_time_zone';
import type { TimeZoneCalculator } from '../timezone_calculator';
+import {
+ buildGroupPanelTree,
+ flattenGroupPanelTreeToLeafRows,
+ flattenGroupPanelTreeToRows,
+ getGroupPanelTreeDepth,
+} from './group_panel_tree';
const toMs = dateUtils.dateToMilliseconds;
const DAY_HOURS = 24;
@@ -94,19 +98,6 @@ export const getOverflowIndicatorColor = (color: string, colors: string[]): stri
: undefined
);
-export const getVerticalGroupCountClass = (groups: unknown[]): string | undefined => {
- switch (groups?.length) {
- case 1:
- return VERTICAL_GROUP_COUNT_CLASSES[0];
- case 2:
- return VERTICAL_GROUP_COUNT_CLASSES[1];
- case 3:
- return VERTICAL_GROUP_COUNT_CLASSES[2];
- default:
- return undefined;
- }
-};
-
export const setOptionHour = (date: Date, optionHour: number): Date => {
const nextDate = new Date(date);
@@ -449,50 +440,46 @@ export const extendGroupItemsForGroupingByDate = (
] as GroupRenderItem[];
}), []) as GroupRenderItem[][];
-const stringifyId = (id: ResourceId): string => (isObject(id)
- ? JSON.stringify(id)
- : String(id));
-
export const getGroupPanelData = (
- groupResources: ResourceLoader[],
+ groupsTree: GroupNode[],
columnCountPerGroup: number,
groupByDate: boolean,
baseColSpan: number,
+ hasHierarchy = false,
): GroupPanelData => {
- let repeatCount = 1;
- let groupPanelItems = groupResources
- .map((group) => {
- const result = [] as GroupRenderItem[];
- const {
- resourceName, resourceIndex, items, data,
- } = group;
-
- for (let i = 0; i < repeatCount; i += 1) {
- result.push(...items.map(({ id, text, color }, index) => ({
- id,
- text,
- color,
- key: `${i}_${resourceIndex}_${stringifyId(id)}`,
- resourceName,
- data: data?.[index],
- }) as GroupRenderItem));
- }
-
- repeatCount *= items.length;
- return result;
- })
- .filter((group) => group.length);
+ const groupTree = buildGroupPanelTree(groupsTree);
+ const maxDepth = getGroupPanelTreeDepth(groupTree);
+ let groupPanelItems = flattenGroupPanelTreeToRows(groupTree, maxDepth, baseColSpan);
if (groupByDate) {
groupPanelItems = extendGroupItemsForGroupingByDate(groupPanelItems, columnCountPerGroup);
}
return {
+ groupTree,
groupPanelItems,
+ maxDepth,
baseColSpan,
+ columnCountPerGroup,
+ hasHierarchy,
};
};
+export const getTimelineGroupPanelRows = (
+ groupPanelData: GroupPanelData,
+ groupByDate: boolean,
+): GroupRenderItem[][] => {
+ let rows = groupPanelData.hasHierarchy
+ ? flattenGroupPanelTreeToLeafRows(groupPanelData.groupTree, groupPanelData.baseColSpan)
+ : groupPanelData.groupPanelItems;
+
+ if (groupByDate) {
+ rows = extendGroupItemsForGroupingByDate(rows, groupPanelData.columnCountPerGroup);
+ }
+
+ return rows;
+};
+
export const splitNumber = (value: number, splitValue: number): number[] => Array.from(
{ length: Math.ceil(value / splitValue) },
(_, index) => Math.min(value - (splitValue * index), splitValue),
diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/group_panel_tree.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/group_panel_tree.ts
new file mode 100644
index 000000000000..b37407800480
--- /dev/null
+++ b/packages/devextreme/js/__internal/scheduler/r1/utils/group_panel_tree.ts
@@ -0,0 +1,169 @@
+import { getKeyHash } from '@js/core/utils/common';
+
+import type {
+ GroupHeaderHierarchy, GroupHeaderPathItem, GroupItem, GroupPanelTreeNode, GroupRenderItem,
+} from '../../types';
+import type { ResourceId } from '../../utils/loader/types';
+import type { GroupNode } from '../../utils/resource_manager/types';
+import type { ResourceCellTemplateData } from '../components/types';
+
+export const stringifyId = (id: ResourceId): string => String(getKeyHash(id));
+
+const buildGroupPanelData = (node: GroupNode): GroupItem => {
+ if (node.resourceData) {
+ return node.resourceData as GroupItem;
+ }
+
+ const data: GroupItem = { id: node.id, text: node.resourceText };
+
+ if (node.color !== undefined) {
+ data.color = node.color;
+ }
+
+ return data;
+};
+
+const buildGroupPanelNode = (
+ node: GroupNode,
+ parentKey: string,
+ parentPath: GroupHeaderPathItem[],
+): GroupPanelTreeNode => {
+ const key = `${parentKey}${node.resourceIndex}_${stringifyId(node.id)}`;
+ const cell: GroupHeaderPathItem = {
+ id: node.id,
+ text: node.resourceText,
+ color: node.color,
+ resourceIndex: node.resourceIndex,
+ data: buildGroupPanelData(node),
+ };
+ const path = [...parentPath, cell];
+ const children = node.children.map(
+ (child) => buildGroupPanelNode(child, `${key}_`, path),
+ );
+ const leafCount = children.length === 0
+ ? 1
+ : children.reduce((sum, child) => sum + child.leafCount, 0);
+
+ return {
+ key,
+ ...cell,
+ path,
+ leafCount,
+ children,
+ };
+};
+
+export const buildGroupPanelTree = (
+ groupsTree: GroupNode[],
+): GroupPanelTreeNode[] => groupsTree.map(
+ (node) => buildGroupPanelNode(node, '', []),
+);
+
+export const getGroupPanelTreeDepth = (tree: GroupPanelTreeNode[]): number => {
+ if (tree.length === 0) {
+ return 0;
+ }
+
+ return 1 + Math.max(...tree.map((node) => getGroupPanelTreeDepth(node.children)));
+};
+
+export const flattenGroupPanelTreeToRows = (
+ tree: GroupPanelTreeNode[],
+ maxDepth: number,
+ baseColSpan: number,
+): GroupRenderItem[][] => {
+ const rows: GroupRenderItem[][] = Array.from({ length: maxDepth }, () => []);
+
+ const walk = (node: GroupPanelTreeNode, depth: number, isLastColumn: boolean): void => {
+ const isLeaf = node.children.length === 0;
+ const isShallowLeaf = isLeaf && depth < maxDepth - 1;
+
+ rows[depth].push({
+ id: node.id,
+ text: node.text,
+ color: node.color,
+ key: node.key,
+ resourceIndex: node.resourceIndex,
+ data: node.data,
+ isLeaf,
+ isLastColumn,
+ path: node.path,
+ colSpan: node.leafCount * baseColSpan,
+ ...(isShallowLeaf ? { rowSpan: maxDepth - depth } : {}),
+ });
+
+ node.children.forEach((child, index) => walk(
+ child,
+ depth + 1,
+ isLastColumn && index === node.children.length - 1,
+ ));
+ };
+
+ tree.forEach((node, index) => walk(node, 0, index === tree.length - 1));
+
+ return rows;
+};
+
+const toGroupRenderItem = (
+ node: GroupPanelTreeNode,
+ baseColSpan: number,
+): GroupRenderItem => ({
+ id: node.id,
+ text: node.text,
+ color: node.color,
+ key: node.key,
+ resourceIndex: node.resourceIndex,
+ data: node.data,
+ isLeaf: node.children.length === 0,
+ path: node.path,
+ colSpan: baseColSpan,
+});
+
+export const flattenGroupPanelTreeToLeafRows = (
+ tree: GroupPanelTreeNode[],
+ baseColSpan: number,
+): GroupRenderItem[][] => {
+ const rows: GroupRenderItem[][] = [];
+
+ const walk = (node: GroupPanelTreeNode, path: GroupRenderItem[]): void => {
+ const currentPath = [...path, toGroupRenderItem(node, baseColSpan)];
+
+ if (node.children.length === 0) {
+ rows.push(currentPath);
+ return;
+ }
+
+ node.children.forEach((child) => walk(child, currentPath));
+ };
+
+ tree.forEach((node) => walk(node, []));
+
+ return rows;
+};
+
+interface GroupHeaderCellInfo extends Partial {
+ id: ResourceId;
+ text?: string;
+ color?: string;
+ resourceIndex?: string;
+ data: GroupItem;
+}
+
+export const getResourceCellTemplateData = ({
+ id, text, color, data, resourceIndex = '', isLeaf = true, path,
+}: GroupHeaderCellInfo): ResourceCellTemplateData => {
+ const cellPath = path?.length ? path : [{
+ id, text, color, resourceIndex, data,
+ }];
+
+ return {
+ data,
+ id,
+ text,
+ color,
+ resourceIndex,
+ isLeaf,
+ path: cellPath,
+ level: cellPath.length - 1,
+ };
+};
diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts
index e1a2585159ad..07515d74ea3d 100644
--- a/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts
+++ b/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts
@@ -57,11 +57,11 @@ export {
getSkippedHoursInRange,
getStartViewDateTimeOffset,
getStartViewDateWithoutDST,
+ getTimelineGroupPanelRows,
getToday,
getTotalCellCountByCompleteData,
getTotalRowCountByCompleteData,
getValidCellDateForLocalTimeFormat,
- getVerticalGroupCountClass,
getViewStartByOptions,
isAppointmentTakesAllDay,
isDateAndTimeView,
@@ -82,6 +82,14 @@ export {
formatWeekday,
formatWeekdayAndDay,
} from './format_weekday';
+export {
+ buildGroupPanelTree,
+ flattenGroupPanelTreeToLeafRows,
+ flattenGroupPanelTreeToRows,
+ getGroupPanelTreeDepth,
+ getResourceCellTemplateData,
+ stringifyId,
+} from './group_panel_tree';
export const agendaUtils = {
calculateEndViewDate,
diff --git a/packages/devextreme/js/__internal/scheduler/types.ts b/packages/devextreme/js/__internal/scheduler/types.ts
index 11d0bcbf465b..8f5108b2019a 100644
--- a/packages/devextreme/js/__internal/scheduler/types.ts
+++ b/packages/devextreme/js/__internal/scheduler/types.ts
@@ -4,6 +4,7 @@ import type { Appointment, Properties } from '@js/ui/scheduler';
import type { Component } from '@ts/core/widget/component';
import type { ResourceLoader } from './utils/loader/resource_loader';
+import type { ResourceId } from './utils/loader/types';
import type { GroupLeaf, GroupValues, RawGroupValues } from './utils/resource_manager/types';
import type { AppointmentItemViewModel } from './view_model/types';
@@ -57,7 +58,7 @@ export type GetDateForHeaderText = (
) => Date;
export interface GroupItem {
- id: number | string;
+ id: ResourceId;
text?: string;
color?: string;
}
@@ -129,13 +130,33 @@ export interface ViewOptions {
endDayHour: number;
}
-export interface GroupRenderItem extends GroupItem {
- key: string;
- resourceName: string;
+export interface GroupHeaderPathItem {
+ id: ResourceId;
+ text?: string;
+ color?: string;
+ resourceIndex: string;
data: GroupItem;
+}
+
+export interface GroupHeaderHierarchy {
+ isLeaf: boolean;
+ path: GroupHeaderPathItem[];
+}
+
+export interface GroupRenderItem extends GroupHeaderPathItem, GroupHeaderHierarchy {
+ key: string;
colSpan?: number;
+ rowSpan?: number;
isFirstGroupCell?: boolean;
isLastGroupCell?: boolean;
+ isLastColumn?: boolean;
+}
+
+export interface GroupPanelTreeNode extends GroupHeaderPathItem {
+ key: string;
+ path: GroupHeaderPathItem[];
+ leafCount: number;
+ children: GroupPanelTreeNode[];
}
export interface CellPositionData {
@@ -223,8 +244,12 @@ export interface DateHeaderData {
}
export interface GroupPanelData {
+ groupTree: GroupPanelTreeNode[];
groupPanelItems: GroupRenderItem[][];
+ maxDepth: number;
baseColSpan: number;
+ columnCountPerGroup: number;
+ hasHierarchy: boolean;
}
export interface ViewDataBase {
diff --git a/packages/devextreme/js/__internal/scheduler/utils/data_accessor/resource_data_accessor.test.ts b/packages/devextreme/js/__internal/scheduler/utils/data_accessor/resource_data_accessor.test.ts
index 529da5a22104..83c679da41e7 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/data_accessor/resource_data_accessor.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/data_accessor/resource_data_accessor.test.ts
@@ -2,6 +2,7 @@ import {
describe, expect, it,
} from '@jest/globals';
+import type { RawResourceData } from '../loader/types';
import { ResourceDataAccessor } from './resource_data_accessor';
describe('ResourceDataAccessor', () => {
@@ -56,4 +57,45 @@ describe('ResourceDataAccessor', () => {
expect(customResource.mainColor).toBe('color');
});
});
+
+ describe('function valueExpr/displayExpr', () => {
+ const customResource: any = { complex: { item: { guid: '0' } }, name: 'Room 1' };
+ const accessor = new ResourceDataAccessor({
+ fieldExpr: 'roomId',
+ dataSource: [],
+ valueExpr: (resource: any) => resource.complex.item.guid,
+ displayExpr: (resource: any) => resource.name,
+ label: 'Room',
+ });
+
+ it('should get fields via a function expression', () => {
+ expect(accessor.get('id', customResource)).toBe(customResource.complex.item.guid);
+ expect(accessor.get('text', customResource)).toBe(customResource.name);
+ });
+
+ it('should not throw when setting a field defined by a function expression', () => {
+ expect(() => accessor.set('id', customResource, 10)).not.toThrow();
+ expect(() => accessor.set('text', customResource, 'text')).not.toThrow();
+ });
+ });
+
+ describe('parentId', () => {
+ const hierarchicalResource: RawResourceData = { id: 11, text: 'Room 11', parentId: 'board' };
+ const accessor = new ResourceDataAccessor({
+ fieldExpr: 'roomId',
+ dataSource: [],
+ parentIdExpr: 'parentId',
+ label: 'Room',
+ });
+
+ it('should get parentId with configured parentIdExpr', () => {
+ expect(accessor.get('parentId', hierarchicalResource)).toBe('board');
+ });
+
+ it('should set parentId with configured parentIdExpr', () => {
+ accessor.set('parentId', hierarchicalResource, 'open');
+
+ expect(hierarchicalResource.parentId).toBe('open');
+ });
+ });
});
diff --git a/packages/devextreme/js/__internal/scheduler/utils/data_accessor/resource_data_accessor.ts b/packages/devextreme/js/__internal/scheduler/utils/data_accessor/resource_data_accessor.ts
index 81a28fe04db2..6bcd5785acac 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/data_accessor/resource_data_accessor.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/data_accessor/resource_data_accessor.ts
@@ -11,6 +11,8 @@ export class ResourceDataAccessor extends DataAccessor = {
idExpr: this.idExpr,
textExpr: this.textExpr,
colorExpr: this.colorExpr,
- });
+ };
+
+ if (this.parentIdExpr) {
+ expressions.parentIdExpr = this.parentIdExpr;
+ }
+
+ this.updateExpressions(expressions);
}
- public updateExpression(field: string, expr: string | undefined): void {
+ public updateExpression(field: string, expr: string | Function | undefined): void {
const name = field.replace('Expr', '');
if (!expr) {
@@ -37,6 +47,13 @@ export class ResourceDataAccessor extends DataAccessor;
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete this.setter[name];
+ return;
+ }
+
this.getter[name] = compileGetter(expr) as DataAccessorGetter;
this.setter[name] = compileSetter(expr) as DataAccessorSetter;
}
diff --git a/packages/devextreme/js/__internal/scheduler/utils/loader/resource_loader.test.ts b/packages/devextreme/js/__internal/scheduler/utils/loader/resource_loader.test.ts
index ab5215c4919e..9589f0a76a3e 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/loader/resource_loader.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/loader/resource_loader.test.ts
@@ -54,4 +54,104 @@ describe('resource loader', () => {
expect(loader.isLoaded()).toBe(true);
});
});
+
+ describe('Hierarchy', () => {
+ const roomData = [
+ {
+ id: 'board', text: 'Board rooms', color: '#111', parentId: null,
+ },
+ {
+ id: 'open', text: 'Open spaces', color: '#222', parentId: null,
+ },
+ {
+ id: 11, text: 'Room 11', color: '#333', parentId: 'board',
+ },
+ {
+ id: 12, text: 'Room 12', color: '#444', parentId: 'board',
+ },
+ {
+ id: 21, text: 'Room 21', color: '#555', parentId: 'open',
+ },
+ ];
+ const getHierarchyConfig = (dataSource) => ({
+ fieldExpr: 'roomId',
+ parentIdExpr: 'parentId',
+ dataSource,
+ label: 'Room',
+ });
+
+ it('should build hierarchy tree and leaf items from flat dataSource', async () => {
+ const loader = new ResourceLoader(getHierarchyConfig(roomData));
+
+ await loader.load();
+
+ expect(loader.hasHierarchy).toBe(true);
+ expect(loader.items).toHaveLength(5);
+ expect(loader.leafItems.map((item) => item.id)).toEqual([11, 12, 21]);
+ expect(loader.hierarchyTree.map((node) => node.data.id)).toEqual(['board', 'open']);
+ expect(loader.hierarchyTree[0].children.map((node) => node.data.id)).toEqual([11, 12]);
+ });
+
+ it('should keep flat behavior when parentIdExpr is not configured', async () => {
+ const loader = new ResourceLoader({
+ fieldExpr: 'roomId',
+ dataSource: roomData,
+ label: 'Room',
+ });
+
+ await loader.load();
+
+ expect(loader.hasHierarchy).toBe(false);
+ expect(loader.hierarchyTree).toEqual([]);
+ expect(loader.leafItems).toEqual(loader.items);
+ expect(loader.leafData).toEqual(loader.data);
+ });
+
+ it('should collect raw leaf items into leafData', async () => {
+ const loader = new ResourceLoader(getHierarchyConfig(roomData));
+
+ await loader.load();
+
+ expect(loader.leafData).toEqual([roomData[2], roomData[3], roomData[4]]);
+ });
+
+ it('should collect raw leaf items from a given array', () => {
+ const loader = new ResourceLoader(getHierarchyConfig(roomData));
+
+ const leafData = loader.collectLeafData(roomData);
+
+ expect(leafData).toEqual([roomData[2], roomData[3], roomData[4]]);
+ });
+
+ it('should keep the user fields and expressions usable in leafData', async () => {
+ const rawData = [
+ { key: 'board', name: 'Board rooms', floor: 1 },
+ {
+ key: 11, name: 'Room 11', parent: 'board', floor: 2,
+ },
+ ];
+ const loader = new ResourceLoader({
+ fieldExpr: 'roomId',
+ parentIdExpr: 'parent',
+ valueExpr: 'key',
+ displayExpr: 'name',
+ dataSource: rawData,
+ });
+
+ await loader.load();
+
+ expect(loader.leafData).toEqual([rawData[1]]);
+ });
+
+ it('should clear hierarchy tree and leaf items on dispose', async () => {
+ const loader = new ResourceLoader(getHierarchyConfig(roomData));
+
+ await loader.load();
+ loader.dispose();
+
+ expect(loader.hierarchyTree).toEqual([]);
+ expect(loader.leafItems).toEqual([]);
+ expect(loader.leafData).toEqual([]);
+ });
+ });
});
diff --git a/packages/devextreme/js/__internal/scheduler/utils/loader/resource_loader.ts b/packages/devextreme/js/__internal/scheduler/utils/loader/resource_loader.ts
index 910f50415fb4..07c4330e5385 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/loader/resource_loader.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/loader/resource_loader.ts
@@ -4,6 +4,11 @@ import {
type ResourceIdAccessor,
} from '../data_accessor/appointment_resource_data_accessor';
import { ResourceDataAccessor } from '../data_accessor/resource_data_accessor';
+import type { ResourceHierarchyNode } from '../resource_manager/hierarchy_tree_utils';
+import {
+ buildHierarchyTree,
+ collectHierarchyLeaves,
+} from '../resource_manager/hierarchy_tree_utils';
import { Loader } from './loader';
import type {
RawResourceData,
@@ -11,6 +16,16 @@ import type {
ResourceData,
} from './types';
+const filterLeafData = (
+ data: RawResourceData[],
+ items: ResourceData[],
+ leafItems: ResourceData[],
+): RawResourceData[] => {
+ const leaves = new Set(leafItems);
+
+ return data.filter((_, index) => leaves.has(items[index]));
+};
+
export class ResourceLoader extends Loader {
public idsGetter: ResourceIdAccessor['idsGetter'];
@@ -28,6 +43,14 @@ export class ResourceLoader extends Loader {
public icon?: string;
+ public hasHierarchy: boolean;
+
+ public hierarchyTree: ResourceHierarchyNode[] = [];
+
+ public leafItems: ResourceData[] = [];
+
+ public leafData: RawResourceData[] = [];
+
constructor(config: ResourceConfig) {
super(config, { pageSize: 0 });
const accessor = getAppointmentResourceAccessor(config);
@@ -40,22 +63,64 @@ export class ResourceLoader extends Loader {
this.resourceIndex = String(getResourceIndex(config));
this.resourceName = config.label;
this.icon = config.icon;
+ this.hasHierarchy = Boolean(config.parentIdExpr);
this.onInit();
}
protected onLoadTransform(items: RawResourceData[]): ResourceData[] {
- return items.map((item) => ({
- id: this.dataAccessor.get('id', item),
- text: this.dataAccessor.get('text', item),
- color: this.dataAccessor.get('color', item),
- }));
+ return items.map((item) => {
+ const resource: ResourceData = {
+ id: this.dataAccessor.get('id', item),
+ text: this.dataAccessor.get('text', item),
+ color: this.dataAccessor.get('color', item),
+ };
+
+ if (this.hasHierarchy) {
+ resource.parentId = this.dataAccessor.get('parentId', item) ?? null;
+ }
+
+ return resource;
+ });
}
protected applyChanges(items: RawResourceData[]): void {
+ const hasChanged = Boolean(items) && items !== this.data;
+
super.applyChanges(items);
+
+ if (hasChanged) {
+ this.rebuildHierarchy();
+ }
+ }
+
+ protected rebuildHierarchy(): void {
+ if (!this.hasHierarchy) {
+ this.hierarchyTree = [];
+ this.leafItems = this.items;
+ this.leafData = this.data;
+ return;
+ }
+
+ this.hierarchyTree = buildHierarchyTree(this.items);
+ this.leafItems = collectHierarchyLeaves(this.hierarchyTree);
+ this.leafData = filterLeafData(this.data, this.items, this.leafItems);
+ }
+
+ public collectLeafData(data: RawResourceData[]): RawResourceData[] {
+ const items = this.onLoadTransform(data);
+ const leafItems = collectHierarchyLeaves(buildHierarchyTree(items));
+
+ return filterLeafData(data, items, leafItems);
}
protected onLoadError(): void {}
protected onChange(): void {}
+
+ public dispose(): void {
+ super.dispose();
+ this.hierarchyTree = [];
+ this.leafItems = [];
+ this.leafData = [];
+ }
}
diff --git a/packages/devextreme/js/__internal/scheduler/utils/loader/types.ts b/packages/devextreme/js/__internal/scheduler/utils/loader/types.ts
index 92c7317dc07f..4a4b9e67677c 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/loader/types.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/loader/types.ts
@@ -6,17 +6,20 @@ export type AppointmentResourceConfig = ResourceId | ResourceId[];
type ResourcesConfig = Required['resources'];
export type ResourceConfig = ResourcesConfig[number] & {
- field?: string; // old notation of fieldExpr
+ field?: string;
+ parentIdExpr?: string;
};
-export type RawResourceData = Record & {
+export type RawResourceData = Record & {
id?: ResourceId;
text?: string;
color?: string;
+ parentId?: ResourceId | null;
};
export interface ResourceData extends Record {
id: ResourceId;
text: string;
color: string;
+ parentId?: ResourceId | null;
}
diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/agenda_group_utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/agenda_group_utils.test.ts
index e2eb52c0e553..61844470e51a 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/agenda_group_utils.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/agenda_group_utils.test.ts
@@ -5,24 +5,50 @@ import {
import { getResourceManagerMock } from '../../__mock__/resource_manager.mock';
import { reduceResourcesTree } from './agenda_group_utils';
+const omitResourceData = (value: T): T => {
+ const strip = (obj: unknown): unknown => {
+ if (Array.isArray(obj)) {
+ return obj.map(strip);
+ }
+
+ if (obj && typeof obj === 'object') {
+ const { resourceData, ...rest } = obj as Record;
+
+ return Object.fromEntries(
+ Object.entries(rest).map(([key, val]) => [key, strip(val)]),
+ );
+ }
+
+ return obj;
+ };
+
+ return strip(value) as T;
+};
+
describe('agenda group utils', () => {
describe('reduceResourcesTree', () => {
it('should reduce tree by appointments resources', async () => {
const manager = getResourceManagerMock();
await manager.loadGroupResources(['roomId', 'nested.priorityId']);
- expect(reduceResourcesTree(manager.resourceById, manager.groupsTree, [
+ expect(omitResourceData(reduceResourcesTree(manager.resourceById, manager.groupsTree, [
{ itemData: { roomId: 0, nested: { priorityId: [1, 2] } } },
{ itemData: { roomId: 1, nested: { priorityId: 2 } } },
- ] as any)).toEqual([
+ ] as any))).toEqual([
{
+ id: 0,
+ color: '#aaa',
children: [
{
+ id: 1,
+ color: '#1e90ff',
children: [],
grouped: { 'nested.priorityId': 1, roomId: 0 },
resourceIndex: 'nested.priorityId',
resourceText: 'Low Priority',
},
{
+ id: 2,
+ color: '#ff9747',
children: [],
grouped: { 'nested.priorityId': 2, roomId: 0 },
resourceIndex: 'nested.priorityId',
@@ -34,14 +60,20 @@ describe('agenda group utils', () => {
resourceText: 'Room 1',
},
{
+ id: 1,
+ color: '#ccc',
children: [
{
+ id: 1,
+ color: '#1e90ff',
children: [],
grouped: { 'nested.priorityId': 1, roomId: 1 },
resourceIndex: 'nested.priorityId',
resourceText: 'Low Priority',
},
{
+ id: 2,
+ color: '#ff9747',
children: [],
grouped: { 'nested.priorityId': 2, roomId: 1 },
resourceIndex: 'nested.priorityId',
diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/appointment_groups_utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/appointment_groups_utils.test.ts
index 205d39e48465..7fa526e55507 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/appointment_groups_utils.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/appointment_groups_utils.test.ts
@@ -3,7 +3,8 @@ import {
} from '@jest/globals';
import {
complexIdResourceMock,
- getResourceManagerMock, resourceIndexesMock, resourceItemsByIdMock,
+ getResourceManagerMock, hierarchicalRoomsConfigMock,
+ resourceIndexesMock, resourceItemsByIdMock,
} from '@ts/scheduler/__mock__/resource_manager.mock';
import {
@@ -15,6 +16,7 @@ import {
groupAppointmentsByGroupLeafs,
setAppointmentGroupValues,
} from './appointment_groups_utils';
+import type { GroupLeaf } from './types';
describe('appointment groups utils', () => {
describe('getResourceItemById', () => {
@@ -181,6 +183,43 @@ describe('appointment groups utils', () => {
}, manager.groupsLeafs),
).toEqual([2, 5]);
});
+
+ describe('hierarchical resource', () => {
+ const loadLeafs = async (): Promise => {
+ const manager = getResourceManagerMock([{ ...hierarchicalRoomsConfigMock }]);
+ await manager.loadGroupResources(['roomId']);
+
+ return manager.groupsLeafs;
+ };
+
+ it.each([
+ { title: 'a leaf in the middle of a branch', values: [11], expected: [0] },
+ { title: 'a leaf of the last branch', values: [21], expected: [2] },
+ { title: 'a leaf at the root level', values: ['solo'], expected: [3] },
+ { title: 'leafs of different branches', values: [12, 21], expected: [1, 2] },
+ ])('should return leaf group indexes for $title', async ({ values, expected }) => {
+ expect(getAppointmentGroupIndex({ roomId: values }, await loadLeafs()))
+ .toEqual(expected);
+ });
+
+ it.each([
+ { title: 'a parent id', values: ['board'] },
+ { title: 'an unknown id', values: [404] },
+ ])('should return no group indexes for $title', async ({ values }) => {
+ expect(getAppointmentGroupIndex({ roomId: values }, await loadLeafs()))
+ .toEqual([]);
+ });
+ });
+
+ // Regression: ids are compared by value, so non-primitive ids (valueExpr) must match
+ it('should return appointment group indexes for complex ids', async () => {
+ const manager = getResourceManagerMock(complexIdResourceMock);
+ await manager.loadGroupResources(['ownerId']);
+
+ expect(
+ getAppointmentGroupIndex({ ownerId: [{ _value: 'guid-2' }] }, manager.groupsLeafs),
+ ).toEqual([1]);
+ });
});
describe('groupAppointmentsByGroupLeafs', () => {
diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/appointment_groups_utils.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/appointment_groups_utils.ts
index 4d77398d1663..5b09bf2595cb 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/appointment_groups_utils.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/appointment_groups_utils.ts
@@ -87,17 +87,21 @@ export const setAppointmentGroupValues = >(
});
};
+// Note: ids can be non-primitive (see valueExpr), so they need a by-value comparison
+const isGroupLeafMatched = (
+ leaf: GroupLeaf,
+ appointmentGroupValues: GroupValues,
+): boolean => Object
+ .entries(leaf.grouped)
+ .every(([resourceIndex, resourceId]) => appointmentGroupValues[resourceIndex]
+ ?.some((id) => equalByValue(id, resourceId)));
+
export const getAppointmentGroupIndex = (
appointmentGroupValues: GroupValues,
groupLeafs: GroupLeaf[],
): GroupLeaf['groupIndex'][] => groupLeafs
- .filter(
- (leaf) => Object
- .entries(leaf.grouped)
- .every((
- [resourceIndex, resourceId],
- ) => appointmentGroupValues[resourceIndex]?.includes(resourceId)),
- ).map((leaf) => leaf.groupIndex);
+ .filter((leaf) => isGroupLeafMatched(leaf, appointmentGroupValues))
+ .map((leaf) => leaf.groupIndex);
export const groupAppointmentsByGroupLeafs = (
resourceById: Record,
@@ -108,15 +112,11 @@ export const groupAppointmentsByGroupLeafs = (
return [appointments];
}
+ const resources = Object.values(resourceById);
+
return groupLeafs.map(
- (leaf) => appointments.filter((item) => {
- const appointmentGroupValues = getAppointmentGroupValues(item, Object.values(resourceById));
-
- return Object
- .entries(leaf.grouped)
- .every((
- [resourceIndex, resourceId],
- ) => appointmentGroupValues[resourceIndex]?.includes(resourceId));
- }),
+ (leaf) => appointments.filter(
+ (item) => isGroupLeafMatched(leaf, getAppointmentGroupValues(item, resources)),
+ ),
);
};
diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/group_utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/group_utils.test.ts
index ed85fdb1379f..79475e64b57a 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/group_utils.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/group_utils.test.ts
@@ -1,30 +1,120 @@
import {
- describe, expect, it,
+ beforeAll, describe, expect, it,
} from '@jest/globals';
import { getResourceManagerMock } from '@ts/scheduler/__mock__/resource_manager.mock';
+import { ResourceLoader } from '../loader/resource_loader';
+import type { RawResourceData } from '../loader/types';
import {
getAllGroupValues, getGroupTexts, getLeafGroupValues, getResourcesByGroupIndex, groupResources,
} from './group_utils';
+import type { GroupLeaf } from './types';
-const groupsLeafs: any = [
- { groupIndex: 0, grouped: { assigneeId: 1, roomId: 3 } },
- { groupIndex: 1, grouped: { assigneeId: 3, roomId: 4 } },
- { groupIndex: 2, grouped: { roomId: 0 } },
- { groupIndex: 3, grouped: { assigneeId: 0, roomId: 0 } },
+const assigneeData: RawResourceData[] = [
+ { id: 0, text: 'Samantha Bright', color: '#727bd2' },
+ { id: 1, text: 'John Heart', color: '#32c9ed' },
];
-const resourceById: any = {
- assigneeId: {
- resourceIndex: 'assigneeId',
- items: [{ id: 0, text: 'Samantha Bright' }, { id: 1, text: 'John Heart' }],
+
+const roomData: RawResourceData[] = [
+ { id: 0, text: 'Room 1', color: '#aaa' },
+ { id: 1, text: 'Room 2', color: '#ccc' },
+];
+
+const roomHierarchyData: RawResourceData[] = [
+ {
+ id: 'board', text: 'Board rooms', color: '#111', parentId: null,
+ },
+ {
+ id: 'open', text: 'Open spaces', color: '#222', parentId: null,
},
- roomId: {
- resourceIndex: 'roomId',
- items: [{ id: 0, text: 'Room 1' }, { id: 1, text: 'Room 2' }],
+ {
+ id: 11, text: 'Room 11', color: '#333', parentId: 'board',
},
+ {
+ id: 12, text: 'Room 12', color: '#444', parentId: 'board',
+ },
+ {
+ id: 21, text: 'Room 21', color: '#555', parentId: 'open',
+ },
+];
+
+const createResourceLoader = async (
+ fieldExpr: string,
+ dataSource: RawResourceData[],
+ label: string,
+ parentIdExpr?: string,
+): Promise => {
+ const loader = new ResourceLoader({
+ fieldExpr,
+ dataSource,
+ label,
+ parentIdExpr,
+ });
+
+ await loader.load();
+
+ return loader;
+};
+
+const createHierarchicalRoomResource = (): Promise => createResourceLoader(
+ 'roomId',
+ roomHierarchyData,
+ 'Room',
+ 'parentId',
+);
+
+const createGroupLeaf = (
+ groupIndex: number,
+ grouped: GroupLeaf['grouped'],
+): GroupLeaf => ({
+ groupIndex,
+ grouped,
+ id: 0,
+ resourceText: '',
+ resourceIndex: '',
+ children: [],
+});
+
+const omitResourceData = (value: T): T => {
+ const strip = (obj: unknown): unknown => {
+ if (Array.isArray(obj)) {
+ return obj.map(strip);
+ }
+
+ if (obj && typeof obj === 'object') {
+ const { resourceData, ...rest } = obj as Record;
+
+ return Object.fromEntries(
+ Object.entries(rest).map(([key, val]) => [key, strip(val)]),
+ );
+ }
+
+ return obj;
+ };
+
+ return strip(value) as T;
};
+const groupsLeafs: GroupLeaf[] = [
+ createGroupLeaf(0, { assigneeId: 1, roomId: 3 }),
+ createGroupLeaf(1, { assigneeId: 3, roomId: 4 }),
+ createGroupLeaf(2, { roomId: 0 }),
+ createGroupLeaf(3, { assigneeId: 0, roomId: 0 }),
+];
+
describe('groups utils', () => {
+ // eslint-disable-next-line @typescript-eslint/init-declarations
+ let resourceById: Record;
+
+ beforeAll(async () => {
+ const [assigneeId, roomId] = await Promise.all([
+ createResourceLoader('assigneeId', assigneeData, 'Assignee'),
+ createResourceLoader('roomId', roomData, 'Room'),
+ ]);
+
+ resourceById = { assigneeId, roomId };
+ });
+
describe('groupResources', () => {
it('should return empty tree for empty groups', () => {
expect(groupResources(resourceById, [])).toEqual({
@@ -40,16 +130,27 @@ describe('groups utils', () => {
});
});
+ it('should return empty tree when groups only contains keys missing from resourceById', () => {
+ expect(groupResources(resourceById, ['unknownId'])).toEqual({
+ groupTree: [],
+ groupLeafs: [],
+ });
+ });
+
it('should group by one group', () => {
- expect(groupResources(resourceById, ['roomId'])).toEqual({
+ expect(omitResourceData(groupResources(resourceById, ['roomId']))).toEqual({
groupTree: [
{
+ id: 0,
+ color: '#aaa',
children: [],
grouped: { roomId: 0 },
resourceIndex: 'roomId',
resourceText: 'Room 1',
},
{
+ id: 1,
+ color: '#ccc',
children: [],
grouped: { roomId: 1 },
resourceIndex: 'roomId',
@@ -58,6 +159,8 @@ describe('groups utils', () => {
],
groupLeafs: [
{
+ id: 0,
+ color: '#aaa',
children: [],
groupIndex: 0,
grouped: { roomId: 0 },
@@ -65,6 +168,8 @@ describe('groups utils', () => {
resourceText: 'Room 1',
},
{
+ id: 1,
+ color: '#ccc',
children: [],
groupIndex: 1,
grouped: { roomId: 1 },
@@ -75,18 +180,61 @@ describe('groups utils', () => {
});
});
+ it('should attach raw resourceData to group nodes', () => {
+ const { groupTree } = groupResources(resourceById, ['roomId', 'assigneeId']);
+
+ expect(groupTree[0].resourceData).toEqual(roomData[0]);
+ expect(groupTree[0].children[0].resourceData).toEqual(assigneeData[0]);
+ expect(groupTree[0].children[1].resourceData).toEqual(assigneeData[1]);
+ });
+
+ it('should attach raw resourceData to hierarchical nodes when valueExpr returns new object ids', async () => {
+ const hierarchyData: RawResourceData[] = [
+ {
+ id: { guid: 'board' },
+ text: 'Board rooms',
+ color: '#111',
+ parentId: null,
+ },
+ {
+ id: { guid: 11 },
+ text: 'Room 11',
+ color: '#333',
+ parentId: { guid: 'board' },
+ },
+ ];
+ const loader = new ResourceLoader({
+ fieldExpr: 'roomId',
+ dataSource: hierarchyData,
+ label: 'Room',
+ parentIdExpr: 'parentId',
+ valueExpr: (item: RawResourceData) => ({ ...(item.id as object) }),
+ });
+
+ await loader.load();
+
+ const { groupTree } = groupResources({ roomId: loader }, ['roomId']);
+
+ expect(groupTree[0].resourceData).toEqual(hierarchyData[0]);
+ expect(groupTree[0].children[0].resourceData).toEqual(hierarchyData[1]);
+ });
+
it('should ignore missed resources and group by one group', () => {
- expect(groupResources({
+ expect(omitResourceData(groupResources({
roomId: resourceById.roomId,
- }, ['roomId', 'assigneeId'])).toEqual({
+ }, ['roomId', 'assigneeId']))).toEqual({
groupTree: [
{
+ id: 0,
+ color: '#aaa',
children: [],
grouped: { roomId: 0 },
resourceIndex: 'roomId',
resourceText: 'Room 1',
},
{
+ id: 1,
+ color: '#ccc',
children: [],
grouped: { roomId: 1 },
resourceIndex: 'roomId',
@@ -95,6 +243,8 @@ describe('groups utils', () => {
],
groupLeafs: [
{
+ id: 0,
+ color: '#aaa',
children: [],
groupIndex: 0,
grouped: { roomId: 0 },
@@ -102,6 +252,8 @@ describe('groups utils', () => {
resourceText: 'Room 1',
},
{
+ id: 1,
+ color: '#ccc',
children: [],
groupIndex: 1,
grouped: { roomId: 1 },
@@ -113,17 +265,23 @@ describe('groups utils', () => {
});
it('should group by multiple groups with correct order', () => {
- expect(groupResources(resourceById, ['roomId', 'assigneeId'])).toEqual({
+ expect(omitResourceData(groupResources(resourceById, ['roomId', 'assigneeId']))).toEqual({
groupTree: [
{
+ id: 0,
+ color: '#aaa',
children: [
{
+ id: 0,
+ color: '#727bd2',
children: [],
grouped: { assigneeId: 0, roomId: 0 },
resourceIndex: 'assigneeId',
resourceText: 'Samantha Bright',
},
{
+ id: 1,
+ color: '#32c9ed',
children: [],
grouped: { assigneeId: 1, roomId: 0 },
resourceIndex: 'assigneeId',
@@ -135,14 +293,20 @@ describe('groups utils', () => {
resourceText: 'Room 1',
},
{
+ id: 1,
+ color: '#ccc',
children: [
{
+ id: 0,
+ color: '#727bd2',
children: [],
grouped: { assigneeId: 0, roomId: 1 },
resourceIndex: 'assigneeId',
resourceText: 'Samantha Bright',
},
{
+ id: 1,
+ color: '#32c9ed',
children: [],
grouped: { assigneeId: 1, roomId: 1 },
resourceIndex: 'assigneeId',
@@ -156,6 +320,8 @@ describe('groups utils', () => {
],
groupLeafs: [
{
+ id: 0,
+ color: '#727bd2',
children: [],
groupIndex: 0,
grouped: { assigneeId: 0, roomId: 0 },
@@ -163,6 +329,8 @@ describe('groups utils', () => {
resourceText: 'Samantha Bright',
},
{
+ id: 1,
+ color: '#32c9ed',
children: [],
groupIndex: 1,
grouped: { assigneeId: 1, roomId: 0 },
@@ -170,6 +338,8 @@ describe('groups utils', () => {
resourceText: 'John Heart',
},
{
+ id: 0,
+ color: '#727bd2',
children: [],
groupIndex: 2,
grouped: { assigneeId: 0, roomId: 1 },
@@ -177,6 +347,8 @@ describe('groups utils', () => {
resourceText: 'Samantha Bright',
},
{
+ id: 1,
+ color: '#32c9ed',
children: [],
groupIndex: 3,
grouped: { assigneeId: 1, roomId: 1 },
@@ -186,6 +358,122 @@ describe('groups utils', () => {
],
});
});
+
+ it('should group hierarchical resource by parent-child tree', async () => {
+ const hierarchicalRoom = await createHierarchicalRoomResource();
+
+ expect(omitResourceData(groupResources({ roomId: hierarchicalRoom }, ['roomId']))).toEqual({
+ groupTree: [
+ {
+ id: 'board',
+ color: '#111',
+ resourceText: 'Board rooms',
+ resourceIndex: 'roomId',
+ grouped: { roomId: 'board' },
+ children: [
+ {
+ id: 11,
+ color: '#333',
+ resourceText: 'Room 11',
+ resourceIndex: 'roomId',
+ grouped: { roomId: 11 },
+ children: [],
+ },
+ {
+ id: 12,
+ color: '#444',
+ resourceText: 'Room 12',
+ resourceIndex: 'roomId',
+ grouped: { roomId: 12 },
+ children: [],
+ },
+ ],
+ },
+ {
+ id: 'open',
+ color: '#222',
+ resourceText: 'Open spaces',
+ resourceIndex: 'roomId',
+ grouped: { roomId: 'open' },
+ children: [
+ {
+ id: 21,
+ color: '#555',
+ resourceText: 'Room 21',
+ resourceIndex: 'roomId',
+ grouped: { roomId: 21 },
+ children: [],
+ },
+ ],
+ },
+ ],
+ groupLeafs: [
+ {
+ id: 11,
+ color: '#333',
+ resourceText: 'Room 11',
+ resourceIndex: 'roomId',
+ grouped: { roomId: 11 },
+ children: [],
+ groupIndex: 0,
+ },
+ {
+ id: 12,
+ color: '#444',
+ resourceText: 'Room 12',
+ resourceIndex: 'roomId',
+ grouped: { roomId: 12 },
+ children: [],
+ groupIndex: 1,
+ },
+ {
+ id: 21,
+ color: '#555',
+ resourceText: 'Room 21',
+ resourceIndex: 'roomId',
+ grouped: { roomId: 21 },
+ children: [],
+ groupIndex: 2,
+ },
+ ],
+ });
+ });
+
+ it('should use only leaf bands for hierarchical resource instead of cartesian product', async () => {
+ const hierarchicalRoom = await createHierarchicalRoomResource();
+
+ const { groupLeafs } = groupResources({ roomId: hierarchicalRoom }, ['roomId']);
+
+ expect(groupLeafs).toHaveLength(3);
+ expect(groupLeafs.map((leaf) => leaf.grouped.roomId)).toEqual([11, 12, 21]);
+ });
+
+ it('should combine hierarchical and flat resources', async () => {
+ const hierarchicalRoom = await createHierarchicalRoomResource();
+
+ const { groupTree, groupLeafs } = groupResources({
+ roomId: hierarchicalRoom,
+ assigneeId: resourceById.assigneeId,
+ }, ['roomId', 'assigneeId']);
+
+ expect(groupLeafs).toHaveLength(6);
+ expect(groupLeafs[0].grouped).toEqual({ roomId: 11, assigneeId: 0 });
+ expect(groupLeafs[1].grouped).toEqual({ roomId: 11, assigneeId: 1 });
+ expect(groupTree[0].resourceText).toBe('Board rooms');
+ expect(groupTree[0].children[0].children[0].resourceText).toBe('Samantha Bright');
+ });
+
+ it('should return an empty group tree when an earlier resource in groups has an empty dataSource', async () => {
+ const emptyRoom = await createResourceLoader('roomId', [], 'Room');
+
+ expect(groupResources({
+ roomId: emptyRoom,
+ assigneeId: resourceById.assigneeId,
+ }, ['roomId', 'assigneeId'])).toEqual({
+ groupTree: [],
+ groupLeafs: [],
+ });
+ });
});
describe('getAllGroupValues', () => {
@@ -246,12 +534,12 @@ describe('groups utils', () => {
it('should return resources of groupIndex', () => {
expect(getResourcesByGroupIndex(groupsLeafs, resourceById, 3)).toEqual([
{
- items: [{ id: 0, text: 'Samantha Bright' }],
- resourceIndex: 'assigneeId',
+ ...resourceById.assigneeId,
+ items: [{ id: 0, text: 'Samantha Bright', color: '#727bd2' }],
},
{
- items: [{ id: 0, text: 'Room 1' }],
- resourceIndex: 'roomId',
+ ...resourceById.roomId,
+ items: [{ id: 0, text: 'Room 1', color: '#aaa' }],
},
]);
});
diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/group_utils.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/group_utils.ts
index 097c7fd2cf3a..fc93d53650c2 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/group_utils.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/group_utils.ts
@@ -1,41 +1,147 @@
+import { getKeyHash } from '@js/core/utils/common';
+
import type { ResourceLoader } from '../loader/resource_loader';
+import type { RawResourceData, ResourceId } from '../loader/types';
+import type { ResourceHierarchyNode } from './hierarchy_tree_utils';
import type { GroupLeaf, GroupNode } from './types';
+const isVirtualRoot = (node: GroupNode): boolean => !node.resourceIndex;
+
+const buildRawResourceDataById = (
+ resource: ResourceLoader,
+): Map, RawResourceData> => {
+ const rawDataById = new Map, RawResourceData>();
+
+ resource.data.forEach((item) => {
+ rawDataById.set(getKeyHash(resource.dataAccessor.get('id', item)), item);
+ });
+
+ return rawDataById;
+};
+
+const createFlatResourceNodes = (
+ resource: ResourceLoader,
+): GroupNode[] => resource.items.map((item, index) => ({
+ id: item.id,
+ resourceText: item.text,
+ color: item.color,
+ resourceIndex: resource.resourceIndex,
+ grouped: { [resource.resourceIndex]: item.id },
+ children: [],
+ resourceData: resource.data[index],
+}));
+
+const hierarchyToGroupNodes = (
+ hierarchyNodes: ResourceHierarchyNode[],
+ resource: ResourceLoader,
+ parentGrouped: Record,
+ rawDataById: Map, RawResourceData>,
+): GroupNode[] => hierarchyNodes.map((node) => {
+ const grouped = { ...parentGrouped, [resource.resourceIndex]: node.data.id };
+
+ return {
+ id: node.data.id,
+ resourceText: node.data.text,
+ color: node.data.color,
+ resourceIndex: resource.resourceIndex,
+ grouped,
+ resourceData: rawDataById.get(getKeyHash(node.data.id)),
+ children: hierarchyToGroupNodes(node.children, resource, grouped, rawDataById),
+ };
+});
+
+const collectGroupLeaves = (nodes: GroupNode[]): GroupNode[] => {
+ const leaves: GroupNode[] = [];
+
+ const walk = (node: GroupNode): void => {
+ if (node.children.length === 0) {
+ leaves.push(node);
+ return;
+ }
+
+ node.children.forEach(walk);
+ };
+
+ nodes.forEach(walk);
+
+ return leaves;
+};
+
+const mergeGroupedIntoTree = (
+ node: GroupNode,
+ parentGrouped: Record,
+): GroupNode => ({
+ ...node,
+ grouped: { ...parentGrouped, ...node.grouped },
+ children: node.children.map((child) => mergeGroupedIntoTree(child, parentGrouped)),
+});
+
+const createResourceNodes = (
+ resource: ResourceLoader,
+): GroupNode[] => {
+ if (resource.hasHierarchy) {
+ return hierarchyToGroupNodes(
+ resource.hierarchyTree,
+ resource,
+ {},
+ buildRawResourceDataById(resource),
+ );
+ }
+
+ return createFlatResourceNodes(resource);
+};
+
+const attachResourceNodes = (
+ leafs: GroupNode[],
+ nodes: GroupNode[],
+): GroupNode[] => {
+ const nextLeafs: GroupNode[] = [];
+
+ leafs.forEach((leaf) => {
+ leaf.children = nodes.map((node) => mergeGroupedIntoTree(node, leaf.grouped));
+
+ leaf.children.forEach((child) => {
+ nextLeafs.push(...collectGroupLeaves([child]));
+ });
+ });
+
+ return nextLeafs;
+};
+
export const groupResources = (resourceById: Record, groups: string[]): {
groupTree: GroupNode[];
groupLeafs: GroupLeaf[];
} => {
- if (!groups.length || Object.keys(resourceById).length === 0) {
+ const validGroups = groups.filter((group) => resourceById[group]);
+
+ if (!validGroups.length) {
return {
groupTree: [],
groupLeafs: [],
};
}
- const head: GroupNode[] = [{} as GroupNode];
+ const head: GroupNode[] = [{
+ id: '',
+ resourceText: '',
+ resourceIndex: '',
+ grouped: {},
+ children: [],
+ }];
let leafs: GroupNode[] = head;
- groups
- .filter((group) => resourceById[group])
- .forEach((group) => {
- const resource = resourceById[group];
- const nodes = resource.items.map((item) => ({
- resourceText: item.text,
- resourceIndex: resource.resourceIndex,
- grouped: { [resource.resourceIndex]: item.id },
- children: [],
- }));
- const nextLeafs: GroupNode[] = [];
-
- leafs.forEach((leaf) => {
- leaf.children = nodes.map((node) => ({
- ...node,
- grouped: { ...node.grouped, ...leaf.grouped },
- }));
- nextLeafs.push(...leaf.children);
- });
- leafs = nextLeafs;
- });
+ validGroups.forEach((group) => {
+ const resource = resourceById[group];
+ const nodes = createResourceNodes(resource);
+
+ if (leafs.length > 0 && isVirtualRoot(leafs[0])) {
+ head[0].children = nodes;
+ leafs = collectGroupLeaves(nodes);
+ return;
+ }
+
+ leafs = attachResourceNodes(leafs, nodes);
+ });
const groupLeafs = leafs.map((leaf, index) => ({
...leaf,
diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/hierarchy_tree_utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/hierarchy_tree_utils.test.ts
new file mode 100644
index 000000000000..6f3c2b597059
--- /dev/null
+++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/hierarchy_tree_utils.test.ts
@@ -0,0 +1,151 @@
+import {
+ describe, expect, it,
+} from '@jest/globals';
+
+import type { ResourceData } from '../loader/types';
+import {
+ buildHierarchyTree,
+ collectHierarchyLeaves,
+} from './hierarchy_tree_utils';
+
+const item = (
+ id: ResourceData['id'],
+ text: string,
+ parentId: ResourceData['parentId'] = null,
+): ResourceData => ({
+ id,
+ text,
+ color: '#000',
+ parentId,
+});
+
+describe('hierarchy_tree_utils', () => {
+ describe('buildHierarchyTree', () => {
+ it('should build parent-child tree from flat items', () => {
+ const items = [
+ item('board', 'Board rooms'),
+ item('open', 'Open spaces'),
+ item(11, 'Room 11', 'board'),
+ item(12, 'Room 12', 'board'),
+ item(21, 'Room 21', 'open'),
+ ];
+
+ const tree = buildHierarchyTree(items);
+
+ expect(tree).toHaveLength(2);
+ expect(tree[0].data.id).toBe('board');
+ expect(tree[0].children.map((node) => node.data.id)).toEqual([11, 12]);
+ expect(tree[1].data.id).toBe('open');
+ expect(tree[1].children.map((node) => node.data.id)).toEqual([21]);
+ });
+
+ it('should treat items with missing parent as roots', () => {
+ const items = [
+ item(1, 'Root'),
+ item(2, 'Orphan', 'missing'),
+ ];
+
+ const tree = buildHierarchyTree(items);
+
+ expect(tree).toHaveLength(2);
+ expect(tree.map((node) => node.data.id)).toEqual([1, 2]);
+ });
+
+ it('should support non-uniform depth', () => {
+ const items = [
+ item('a', 'A'),
+ item('b', 'B', 'a'),
+ item('c', 'C', 'b'),
+ item('d', 'D'),
+ ];
+
+ const tree = buildHierarchyTree(items);
+
+ expect(tree).toHaveLength(2);
+ expect(tree[0].children[0].children[0].data.id).toBe('c');
+ expect(tree[1].data.id).toBe('d');
+ });
+
+ it('should support depth greater than 3', () => {
+ const items = [
+ item(1, 'L1'),
+ item(2, 'L2', 1),
+ item(3, 'L3', 2),
+ item(4, 'L4', 3),
+ item(5, 'L5', 4),
+ ];
+
+ const tree = buildHierarchyTree(items);
+
+ expect(tree).toHaveLength(1);
+ expect(
+ tree[0].children[0].children[0].children[0].children[0].data.id,
+ ).toBe(5);
+ });
+
+ it('should break a two-node parentId cycle instead of looping forever', () => {
+ const items = [
+ item('a', 'A', 'b'),
+ item('b', 'B', 'a'),
+ ];
+
+ const tree = buildHierarchyTree(items);
+
+ expect(tree.map((node) => node.data.id)).toEqual(['a', 'b']);
+ expect(tree.every((node) => node.children.length === 0)).toBe(true);
+ });
+
+ it('should link a child to its parent when the id is an object compared by value', () => {
+ const items = [
+ item({ room: 1 }, 'Board', null),
+ item({ room: 2 }, 'Room 11', { room: 1 }),
+ ];
+
+ const tree = buildHierarchyTree(items);
+
+ expect(tree).toHaveLength(1);
+ expect(tree[0].children.map((node) => node.data.id)).toEqual([{ room: 2 }]);
+ });
+ });
+
+ describe('collectHierarchyLeaves', () => {
+ it('should return only nodes without children in DFS order', () => {
+ const items = [
+ item('board', 'Board rooms'),
+ item('open', 'Open spaces'),
+ item(11, 'Room 11', 'board'),
+ item(12, 'Room 12', 'board'),
+ item(21, 'Room 21', 'open'),
+ ];
+
+ const tree = buildHierarchyTree(items);
+ const leaves = collectHierarchyLeaves(tree);
+
+ expect(leaves.map((leaf) => leaf.id)).toEqual([11, 12, 21]);
+ });
+
+ it('should return all items when every node is a leaf', () => {
+ const items = [
+ item(1, 'One'),
+ item(2, 'Two'),
+ ];
+
+ const tree = buildHierarchyTree(items);
+ const leaves = collectHierarchyLeaves(tree);
+
+ expect(leaves.map((leaf) => leaf.id)).toEqual([1, 2]);
+ });
+
+ it('should not recurse infinitely when the tree came from a parentId cycle', () => {
+ const items = [
+ item('a', 'A', 'b'),
+ item('b', 'B', 'a'),
+ ];
+
+ const tree = buildHierarchyTree(items);
+ const leaves = collectHierarchyLeaves(tree);
+
+ expect(leaves.map((leaf) => leaf.id)).toEqual(['a', 'b']);
+ });
+ });
+});
diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/hierarchy_tree_utils.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/hierarchy_tree_utils.ts
new file mode 100644
index 000000000000..29f7e295362c
--- /dev/null
+++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/hierarchy_tree_utils.ts
@@ -0,0 +1,101 @@
+import { getKeyHash } from '@ts/core/utils/m_common';
+
+import type { ResourceData } from '../loader/types';
+
+export interface ResourceHierarchyNode {
+ data: ResourceData;
+ children: ResourceHierarchyNode[];
+}
+
+type Hash = string | number | object;
+const hashOf = (id: ResourceData['id']): Hash => getKeyHash(id) as Hash;
+
+const isRootItem = (
+ item: ResourceData,
+ nodeByHash: Map,
+): boolean => {
+ const { parentId, id } = item;
+
+ return parentId == null || !nodeByHash.has(hashOf(parentId)) || hashOf(parentId) === hashOf(id);
+};
+
+// Without this check, a parentId loop (A's parent is B, B's parent is A) causes a stack overflow
+const isAncestorCycle = (
+ id: ResourceData['id'],
+ parentId: ResourceData['id'],
+ nodeByHash: Map,
+): boolean => {
+ const targetHash = hashOf(id);
+ const visited = new Set();
+ let currentId: ResourceData['id'] | null | undefined = parentId;
+
+ while (currentId != null && !visited.has(hashOf(currentId))) {
+ if (hashOf(currentId) === targetHash) {
+ return true;
+ }
+
+ visited.add(hashOf(currentId));
+ currentId = nodeByHash.get(hashOf(currentId))?.data.parentId;
+ }
+
+ return false;
+};
+
+export const buildHierarchyTree = (items: ResourceData[]): ResourceHierarchyNode[] => {
+ const nodeByHash = new Map();
+ const attachedHashes = new Set();
+
+ items.forEach((data) => {
+ nodeByHash.set(hashOf(data.id), { data, children: [] });
+ });
+
+ items.forEach((data) => {
+ if (isRootItem(data, nodeByHash)) {
+ return;
+ }
+
+ const node = nodeByHash.get(hashOf(data.id));
+ const { parentId } = data;
+
+ if (node === undefined || parentId == null) {
+ return;
+ }
+
+ if (isAncestorCycle(data.id, parentId, nodeByHash)) {
+ return;
+ }
+
+ const parent = nodeByHash.get(hashOf(parentId));
+
+ if (parent === undefined) {
+ return;
+ }
+
+ parent.children.push(node);
+ attachedHashes.add(hashOf(data.id));
+ });
+
+ return items
+ .filter((data) => !attachedHashes.has(hashOf(data.id)))
+ .map((data) => nodeByHash.get(hashOf(data.id)))
+ .filter((node): node is ResourceHierarchyNode => node !== undefined);
+};
+
+export const collectHierarchyLeaves = (
+ tree: ResourceHierarchyNode[],
+): ResourceData[] => {
+ const leaves: ResourceData[] = [];
+
+ const walk = (node: ResourceHierarchyNode): void => {
+ if (node.children.length === 0) {
+ leaves.push(node.data);
+ return;
+ }
+
+ node.children.forEach(walk);
+ };
+
+ tree.forEach(walk);
+
+ return leaves;
+};
diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.test.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.test.ts
index 69bcb3676602..bd94a3025116 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.test.ts
@@ -90,4 +90,39 @@ describe('ResourceManager', () => {
expect(resources[1].isLoaded()).toBe(true);
});
});
+
+ describe('hierarchical resources', () => {
+ const roomData = [
+ {
+ id: 'board', text: 'Board rooms', color: '#111', parentId: null,
+ },
+ {
+ id: 'open', text: 'Open spaces', color: '#222', parentId: null,
+ },
+ {
+ id: 11, text: 'Room 11', color: '#333', parentId: 'board',
+ },
+ {
+ id: 12, text: 'Room 12', color: '#444', parentId: 'board',
+ },
+ {
+ id: 21, text: 'Room 21', color: '#555', parentId: 'open',
+ },
+ ];
+
+ it('should expose hierarchy tree and leaf items for hierarchical resource', async () => {
+ const manager = new ResourceManager([{
+ fieldExpr: 'roomId',
+ parentIdExpr: 'parentId',
+ dataSource: roomData,
+ label: 'Room',
+ }]);
+
+ await manager.loadGroupResources(['roomId']);
+
+ expect(manager.isHierarchicalResource('roomId')).toBe(true);
+ expect(manager.getResourceLeafItems('roomId').map((item) => item.id)).toEqual([11, 12, 21]);
+ expect(manager.getResourceHierarchyTree('roomId').map((node) => node.data.id)).toEqual(['board', 'open']);
+ });
+ });
});
diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.ts
index 98a66f5c01f7..3bd578772216 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/resource_manager.ts
@@ -4,6 +4,7 @@ import { getResourceIndex } from '../data_accessor/appointment_resource_data_acc
import { ResourceLoader } from '../loader/resource_loader';
import type {
ResourceConfig,
+ ResourceData,
} from '../loader/types';
import { getAppointmentColor } from './appointment_color_utils';
import type { AppointmentResource } from './appointment_groups_utils';
@@ -12,6 +13,7 @@ import {
getAppointmentResources,
} from './appointment_groups_utils';
import { groupResources } from './group_utils';
+import type { ResourceHierarchyNode } from './hierarchy_tree_utils';
import type { GroupLeaf, GroupNode } from './types';
export class ResourceManager {
@@ -60,6 +62,20 @@ export class ResourceManager {
.filter(Boolean);
}
+ public isHierarchicalResource(resourceIndex: string): boolean {
+ return Boolean(this.resourceById[resourceIndex]?.hasHierarchy);
+ }
+
+ public getResourceHierarchyTree(resourceIndex: string): ResourceHierarchyNode[] {
+ return this.resourceById[resourceIndex]?.hierarchyTree ?? [];
+ }
+
+ public getResourceLeafItems(resourceIndex: string): ResourceData[] {
+ const resource = this.resourceById[resourceIndex];
+
+ return resource?.leafItems ?? [];
+ }
+
public async loadAppointmentsResources(
items: SafeAppointment[],
forceReload = false,
diff --git a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/types.ts b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/types.ts
index 3d95dec0e4b5..40f93d425376 100644
--- a/packages/devextreme/js/__internal/scheduler/utils/resource_manager/types.ts
+++ b/packages/devextreme/js/__internal/scheduler/utils/resource_manager/types.ts
@@ -1,13 +1,16 @@
-import type { ResourceId } from '../loader/types';
+import type { RawResourceData, ResourceId } from '../loader/types';
export type GroupValues = Record;
export type RawGroupValues = Record;
export interface GroupNode {
+ id: ResourceId;
resourceText: string;
+ color?: string;
resourceIndex: string;
grouped: Record;
children: GroupNode[];
+ resourceData?: RawResourceData;
}
export interface GroupLeaf extends GroupNode {
diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/filter_by_attributes/is_appointment_matched_resources.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/filter_by_attributes/is_appointment_matched_resources.test.ts
index 8dd101a8d874..b383dcbffda2 100644
--- a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/filter_by_attributes/is_appointment_matched_resources.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/filter_by_attributes/is_appointment_matched_resources.test.ts
@@ -1,6 +1,7 @@
import {
describe, expect, it,
} from '@jest/globals';
+import { hierarchicalRoomsConfigMock } from '@ts/scheduler/__mock__/resource_manager.mock';
import { ResourceLoader } from '../../../../utils/loader/resource_loader';
import {
@@ -61,4 +62,41 @@ describe('isAppointmentMatchedResources', () => {
[assignee],
)).toBe(false);
});
+
+ describe('hierarchical resource', () => {
+ const loadRoom = async (allowMultiple = false): Promise => {
+ const room = new ResourceLoader({ ...hierarchicalRoomsConfigMock, allowMultiple });
+ await room.load();
+
+ return room;
+ };
+
+ it('should match a leaf id', async () => {
+ expect(isAppointmentMatchedResources(
+ { roomId: 21 } as any,
+ [await loadRoom()],
+ )).toBe(true);
+ });
+
+ it('should not match a parent id', async () => {
+ expect(isAppointmentMatchedResources(
+ { roomId: 'board' } as any,
+ [await loadRoom()],
+ )).toBe(false);
+ });
+
+ it('should match an allowMultiple appointment by any of its leaf ids', async () => {
+ expect(isAppointmentMatchedResources(
+ { roomId: ['board', 21] } as any,
+ [await loadRoom(true)],
+ )).toBe(true);
+ });
+
+ it('should not match an allowMultiple appointment bound to parent ids only', async () => {
+ expect(isAppointmentMatchedResources(
+ { roomId: ['board', 'open'] } as any,
+ [await loadRoom(true)],
+ )).toBe(false);
+ });
+ });
});
diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/filter_by_attributes/is_appointment_matched_resources.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/filter_by_attributes/is_appointment_matched_resources.ts
index f7768a79502b..378a261611be 100644
--- a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/filter_by_attributes/is_appointment_matched_resources.ts
+++ b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/filter_by_attributes/is_appointment_matched_resources.ts
@@ -16,9 +16,10 @@ export const isAppointmentMatchedResources = (
return groupsResources.every((resource) => {
const value = appointmentGroupValues[resource.resourceIndex];
+ const validItems = resource.hasHierarchy ? resource.leafItems : resource.items;
return value?.some(
- (id) => resource.items.some(
+ (id) => validItems.some(
(item) => equalByValue(id, item.id),
),
);
diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/split_by_group_index.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/split_by_group_index.test.ts
index 00335fb8e37f..9f8850928c10 100644
--- a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/split_by_group_index.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/split_by_group_index.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, it } from '@jest/globals';
+import { hierarchicalRoomsConfigMock } from '@ts/scheduler/__mock__/resource_manager.mock';
import type { ResourceConfig } from '../../../utils/loader/types';
import { ResourceManager } from '../../../utils/resource_manager/resource_manager';
@@ -67,4 +68,37 @@ describe('splitByGroupIndex', () => {
{ ...items[2], groupIndex: 5 },
]);
});
+
+ describe('hierarchical resource', () => {
+ // Leaf group order: 11 → 0, 12 → 1, 21 → 2, solo → 3
+ const asItem = (roomId: unknown): MinimalAppointmentEntity => ({
+ itemData: { roomId } as unknown as MinimalAppointmentEntity['itemData'],
+ } as MinimalAppointmentEntity);
+
+ it('should set groupIndex of the appointment leaf', async () => {
+ const options = await getFilterOptions([{ ...hierarchicalRoomsConfigMock }]);
+ const item = asItem(21);
+
+ expect(splitByGroupIndex([item], options)).toEqual([{ ...item, groupIndex: 2 }]);
+ });
+
+ it('should drop an appointment that matches no leaf', async () => {
+ const options = await getFilterOptions([{ ...hierarchicalRoomsConfigMock }]);
+
+ expect(splitByGroupIndex([asItem('board')], options)).toEqual([]);
+ });
+
+ it('should split an allowMultiple appointment across its leaf bands', async () => {
+ const options = await getFilterOptions([{
+ ...hierarchicalRoomsConfigMock,
+ allowMultiple: true,
+ }]);
+ const item = asItem(['board', 12, 'solo']);
+
+ expect(splitByGroupIndex([item], options)).toEqual([
+ { ...item, groupIndex: 1 },
+ { ...item, groupIndex: 3 },
+ ]);
+ });
+ });
});
diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/agenda.ts b/packages/devextreme/js/__internal/scheduler/workspaces/agenda.ts
index b007f58caa95..c7eac2219570 100644
--- a/packages/devextreme/js/__internal/scheduler/workspaces/agenda.ts
+++ b/packages/devextreme/js/__internal/scheduler/workspaces/agenda.ts
@@ -21,7 +21,7 @@ import {
GROUP_ROW_CLASS,
TIME_PANEL_CLASS,
} from '../classes';
-import { agendaUtils, formatWeekday, getVerticalGroupCountClass } from '../r1/utils/index';
+import { agendaUtils, formatWeekday } from '../r1/utils/index';
import tableCreatorModule, { type GroupRows } from '../table_creator';
import type { ResourceId } from '../utils/loader/types';
import { VIEWS } from '../utils/options/constants_view';
@@ -102,7 +102,6 @@ class SchedulerAgenda extends WorkSpace {
if (this.$groupTable) {
this.$groupTable.remove();
this.$groupTable = null;
- this.detachGroupCountClass();
}
} else if (!this.$groupTable) {
this.initGroupTable();
@@ -230,13 +229,6 @@ class SchedulerAgenda extends WorkSpace {
return rows.every((groupRow) => groupRow.every((cell) => !cell));
}
- protected override attachGroupCountClass(): void {
- const className = getVerticalGroupCountClass(this.option().groups);
- if (className) {
- this.$element().addClass(className);
- }
- }
-
private removeEmptyRows(rows: number[][]): number[][] {
const isEmpty = (data: number[]): boolean => !data.some((value) => value > 0);
return rows.filter((row) => row.length && !isEmpty(row));
diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/view_data_provider.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/view_data_provider.ts
index bdf42413edca..da580482fa07 100644
--- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/view_data_provider.ts
+++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/view_data_provider.ts
@@ -196,15 +196,21 @@ export default class ViewDataProvider {
getGroupPanelData(options: ViewDataProviderOptions): GroupPanelData | undefined {
const renderOptions = this.transformRenderOptions(options);
- const groupResources = renderOptions.getResourceManager().groupResources();
+ const resourceManager = renderOptions.getResourceManager();
+ const { groupsTree, groups } = resourceManager;
- if (groupResources.length > 0) {
+ if (groupsTree.length > 0) {
const cellCount = this.getCellCount(renderOptions);
+ const hasHierarchy = groups.some(
+ (group) => resourceManager.isHierarchicalResource(group),
+ );
+
return getGroupPanelData(
- groupResources,
+ groupsTree,
cellCount,
renderOptions.isGroupedByDate,
renderOptions.isGroupedByDate ? 1 : cellCount,
+ hasHierarchy,
);
}
diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts
index bd0149d8a5e3..ebc8991954fa 100644
--- a/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts
+++ b/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts
@@ -78,7 +78,6 @@ import {
GROUP_HEADER_CONTENT_CLASS,
GROUP_ROW_CLASS,
TIME_PANEL_CLASS,
- VERTICAL_GROUP_COUNT_CLASSES,
VIRTUAL_CELL_CLASS,
} from '../classes';
import { APPOINTMENT_SETTINGS_KEY } from '../constants';
@@ -2389,7 +2388,6 @@ class SchedulerWorkSpace extends Widget {
};
if (this.option().groups?.length) {
- this.attachGroupCountClass();
const $groupHeaderContainer = this.getGroupHeaderContainer();
if ($groupHeaderContainer) {
this.renderRenovatedComponent(
@@ -2399,8 +2397,6 @@ class SchedulerWorkSpace extends Widget {
options,
);
}
- } else {
- this.detachGroupCountClass();
}
}
@@ -2473,12 +2469,6 @@ class SchedulerWorkSpace extends Widget {
}
renderRHeaderPanel(isRenderDateHeader = true): void {
- if (this.option().groups?.length) {
- this.attachGroupCountClass();
- } else {
- this.detachGroupCountClass();
- }
-
this.renderRenovatedComponent(
this.$thead,
this.renovatedHeaderPanelComponent,
@@ -3219,20 +3209,6 @@ class SchedulerWorkSpace extends Widget {
protected setIndicationUpdateInterval(): void { return noop(); }
- protected detachGroupCountClass(): void {
- VERTICAL_GROUP_COUNT_CLASSES.forEach((className) => {
- this.$element().removeClass(className);
- });
- }
-
- protected attachGroupCountClass(): void {
- const className = this.groupedStrategy.getGroupCountClass(this.option().groups);
-
- if (className) {
- this.$element().addClass(className);
- }
- }
-
protected getDateHeaderTemplate(): TemplateBase | null | undefined {
return this.option().dateCellTemplate;
}
@@ -3351,12 +3327,9 @@ class SchedulerWorkSpace extends Widget {
let cellTemplates: (() => dxElementWrapper)[] = [];
if (groupCount && $container) {
const groupRows = this.makeGroupRows(this.option().groups, this.option().groupByDate);
- this.attachGroupCountClass();
const { elements } = groupRows;
$container.append(Array.isArray(elements) ? elements : elements.toArray());
cellTemplates = groupRows.cellTemplates;
- } else {
- this.detachGroupCountClass();
}
return cellTemplates;
diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_horizontal.ts b/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_horizontal.ts
index 6c7c18e41408..5e852d38e91a 100644
--- a/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_horizontal.ts
+++ b/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_horizontal.ts
@@ -8,7 +8,6 @@ import type {
import { WORK_SPACE_BORDER_PX } from '@ts/scheduler/workspaces/const';
import { FIRST_GROUP_CELL_CLASS, LAST_GROUP_CELL_CLASS } from '../classes';
-import type { ResourceLoader } from '../utils/loader/resource_loader';
import type { GroupedStrategyConfig } from './work_space_grouped_strategy_config';
class HorizontalGroupedStrategy {
@@ -77,11 +76,6 @@ class HorizontalGroupedStrategy {
return this.config.getAllDayHeight();
}
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- getGroupCountClass(groups: ResourceLoader[]): string | undefined {
- return undefined;
- }
-
getLeftOffset(): number {
return this.config.getTimePanelWidth();
}
diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_vertical.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_vertical.test.ts
index 2913d192235d..73eb05320668 100644
--- a/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_vertical.test.ts
+++ b/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_vertical.test.ts
@@ -8,12 +8,10 @@ import VerticalGroupedStrategy from './work_space_grouped_strategy_vertical';
jest.mock('@ts/scheduler/r1/utils/index', (): {
calculateDayDuration: (startDayHour: number, endDayHour: number) => number;
- getVerticalGroupCountClass: () => undefined;
} => ({
calculateDayDuration: (startDayHour: number, endDayHour: number): number => (
endDayHour - startDayHour
),
- getVerticalGroupCountClass: (): undefined => undefined,
}));
const createElement = ({
diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_vertical.ts b/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_vertical.ts
index 8018efecb51c..bf3146d9269d 100644
--- a/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_vertical.ts
+++ b/packages/devextreme/js/__internal/scheduler/workspaces/work_space_grouped_strategy_vertical.ts
@@ -1,12 +1,11 @@
import type { dxElementWrapper } from '@js/core/renderer';
import { getBoundingRect } from '@js/core/utils/position';
-import { calculateDayDuration, getVerticalGroupCountClass } from '@ts/scheduler/r1/utils/index';
+import { calculateDayDuration } from '@ts/scheduler/r1/utils/index';
import type { CellPositionData, GroupBoundsOffset } from '@ts/scheduler/types';
import { WORK_SPACE_BORDER_PX } from '@ts/scheduler/workspaces/const';
import { FIRST_GROUP_CELL_CLASS, LAST_GROUP_CELL_CLASS } from '../classes';
import { Cache } from '../global_cache';
-import type { ResourceLoader } from '../utils/loader/resource_loader';
import type { GroupedStrategyConfig } from './work_space_grouped_strategy_config';
class VerticalGroupedStrategy {
@@ -77,10 +76,6 @@ class VerticalGroupedStrategy {
return 0;
}
- getGroupCountClass(groups: ResourceLoader[]): string | undefined {
- return getVerticalGroupCountClass(groups);
- }
-
getLeftOffset(): number {
return this.config.getTimePanelWidth() + this.config.getGroupTableWidth();
}
diff --git a/packages/devextreme/js/ui/scheduler.d.ts b/packages/devextreme/js/ui/scheduler.d.ts
index fc511bc8af95..5cf074cdba1b 100644
--- a/packages/devextreme/js/ui/scheduler.d.ts
+++ b/packages/devextreme/js/ui/scheduler.d.ts
@@ -940,6 +940,11 @@ export interface dxSchedulerOptions extends WidgetOptions {
* @default ""
*/
label?: string;
+ /**
+ * @docid
+ * @default undefined
+ */
+ parentIdExpr?: string;
/**
* @docid
* @default false
diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.resources.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.resources.tests.js
index e9510824e838..28b70a3d09cd 100644
--- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.resources.tests.js
+++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.resources.tests.js
@@ -39,7 +39,7 @@ QUnit.module('Integration: Resources', moduleConfig, () => {
},
'appointment2': {
top: 202,
- left: 430
+ left: 411
}
}, {
'appointment1': {
diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timeline.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timeline.markup.tests.js
index 0f7affa43e3e..a010732318f0 100644
--- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timeline.markup.tests.js
+++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/timeline.markup.tests.js
@@ -138,26 +138,6 @@ QUnit.module('Timeline markup', moduleConfig, () => {
assert.equal($firstColumnCells.length, 2, 'Cell count is OK');
assert.equal($secondColumnCells.length, 4, 'Cell count is OK');
});
-
- QUnit.test('Timeline should have correct group-count class depending on group count', async function(assert) {
- const $element = this.instance.$element();
-
- await applyWorkspaceGroups(this.instance, [{
- label: 'one',
- fieldExpr: 'one',
- dataSource: [{ id: 1, text: 'a' }, { id: 2, text: 'b' }]
- }, {
- label: 'two',
- fieldExpr: 'two',
- dataSource: [{ id: 1, text: '1' }, { id: 2, text: '2' }]
- }]);
-
- assert.ok($element.hasClass('dx-scheduler-group-column-count-two'), 'Correct class');
-
- await applyWorkspaceGroups(this.instance, []);
-
- assert.notOk($element.hasClass('dx-scheduler-group-column-count-two'), 'group-count class was not applied');
- });
});
let timelineDayModuleConfig = {
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 ab17d137d9a1..f9c7962bff81 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
@@ -1950,30 +1950,61 @@ module('View Data Provider', {
renderOptions: await getRenderOptions(),
});
- const expectedGroupPanelData = {
- baseColSpan: 2,
- groupPanelItems: [[{
- color: 'red',
- data: {
- id: 1,
- color: 'red',
- text: 'First group'
- },
+ const firstGroupCell = {
+ id: 1,
+ text: 'First group',
+ color: 'red',
+ resourceIndex: 'groupId',
+ data: {
id: 1,
- key: '0_groupId_1',
- resourceName: 'groupId',
+ color: 'red',
text: 'First group'
- }, {
- color: 'green',
- data: {
- id: 2,
- color: 'green',
- text: 'Second group'
- },
+ }
+ };
+ const secondGroupCell = {
+ id: 2,
+ text: 'Second group',
+ color: 'green',
+ resourceIndex: 'groupId',
+ data: {
id: 2,
- key: '0_groupId_2',
- resourceName: 'groupId',
+ color: 'green',
text: 'Second group'
+ }
+ };
+
+ const expectedGroupPanelData = {
+ baseColSpan: 2,
+ maxDepth: 1,
+ columnCountPerGroup: 2,
+ hasHierarchy: false,
+ groupTree: [{
+ ...firstGroupCell,
+ key: 'groupId_1',
+ path: [firstGroupCell],
+ leafCount: 1,
+ children: [],
+ }, {
+ ...secondGroupCell,
+ key: 'groupId_2',
+ path: [secondGroupCell],
+ leafCount: 1,
+ children: [],
+ }],
+ groupPanelItems: [[{
+ ...firstGroupCell,
+ key: 'groupId_1',
+ isLeaf: true,
+ isLastColumn: false,
+ path: [firstGroupCell],
+ colSpan: 2
+ }, {
+ ...secondGroupCell,
+ key: 'groupId_2',
+ isLeaf: true,
+ isLastColumn: true,
+ path: [secondGroupCell],
+ colSpan: 2
}]]
};
diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts
index 8b1e40dc981f..6af5e097c9e9 100644
--- a/packages/devextreme/ts/dx.all.d.ts
+++ b/packages/devextreme/ts/dx.all.d.ts
@@ -27319,6 +27319,10 @@ declare module DevExpress.ui {
* [descr:dxSchedulerOptions.resources.label]
*/
label?: string;
+ /**
+ * [descr:dxSchedulerOptions.resources.parentIdExpr]
+ */
+ parentIdExpr?: string;
/**
* [descr:dxSchedulerOptions.resources.useColorAsDefault]
*/
|