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..9cd1e9577524
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
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..db314ace272a
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.component.ts
@@ -0,0 +1,200 @@
+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, Resource, 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[];
+
+ resources: Resource[];
+
+ rooms: Assignee[];
+
+ groups: string[] = ['assigneeId'];
+
+ currentDate: Date = new Date(2026, 6, 13);
+
+ constructor(service: Service) {
+ this.appointments = service.getAppointments();
+ this.assignees = service.getAssignees();
+ this.resources = service.getResources();
+ 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: () => '
',
+ },
+ {
+ itemType: 'simple',
+ name: 'roomEditor',
+ colSpan: 1,
+ label: { visible: false },
+ editorType: 'dxSelectBox',
+ editorOptions: {
+ dataSource: this.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', this.employeesOf(e.value));
+ editor?.option('value', []);
+ },
+ },
+ },
+ ],
+ };
+ }
+
+ renderEmployeeTag(data: { text: string; color?: string }) {
+ 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;
+ }
+
+ hideLabels(items: FormItem[]) {
+ items.forEach((item) => {
+ if (item.items) {
+ this.hideLabels(item.items);
+ } else if (item.dataField !== 'allDay') {
+ item.label = { ...(item.label as object), visible: false };
+ }
+ });
+ }
+
+ onAppointmentFormOpening(e: DxSchedulerTypes.AppointmentFormOpeningEvent) {
+ const { form } = e;
+ const items = form.option('items') as FormItem[];
+
+ if (this.findItem(items, (item) => item.name === 'roomGroup')) {
+ return;
+ }
+
+ const appointment = e.appointmentData ?? {};
+ const assigneeIds = appointment.assigneeId as number | number[] | undefined;
+ const [assigneeId] = Array.isArray(assigneeIds) ? assigneeIds : [assigneeIds];
+ const roomId = this.roomOf(assigneeId);
+ const employee = this.findItem(items, (item) => item.name === 'assigneeIdGroup');
+
+ const repeatValue = form.getEditor('repeatEditor')?.option('value');
+
+ employee?.list.splice(employee.index, 0, this.createRoomGroup(form, roomId));
+ this.hideLabels(items);
+
+ const employeeEditor = this.findItem(items, (item) => item.dataField === 'assigneeId');
+
+ if (employeeEditor) {
+ employeeEditor.list[employeeEditor.index].validationRules = [
+ { type: 'required', message: 'Employee is required' },
+ ];
+ employeeEditor.list[employeeEditor.index].editorOptions = {
+ ...(employeeEditor.list[employeeEditor.index].editorOptions as object),
+ tagTemplate: this.renderEmployeeTag,
+ };
+ }
+
+ const description = this.findItem(items, (item) => item.name === 'descriptionGroup');
+
+ if (description) {
+ description.list[description.index].visible = false;
+ }
+ form.option('items', items.slice());
+
+ form.getEditor('repeatEditor')?.option('value', repeatValue);
+
+ form.getEditor('assigneeId')?.option('dataSource', roomId ? this.employeesOf(roomId) : []);
+ }
+}
+
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
+ ],
+});
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.service.ts b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.service.ts
new file mode 100644
index 000000000000..80bb5e1008ff
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/app/app.service.ts
@@ -0,0 +1,223 @@
+import { Injectable } from '@angular/core';
+
+export class Appointment {
+ text: string;
+
+ assigneeId: number[];
+
+ startDate: Date;
+
+ endDate: Date;
+}
+
+export class Assignee {
+ id: number | string;
+
+ text: string;
+
+ parentId: string | null;
+
+ shortText?: string;
+
+ color?: string;
+}
+
+export class Resource {
+ fieldExpr: string;
+
+ dataSource: Assignee[];
+
+ parentIdExpr: string;
+
+ label: string;
+
+ allowMultiple: boolean;
+
+ icon: string;
+}
+
+const assignees: Assignee[] = [
+ {
+ id: 'room-1', text: '🏢 Room 1', shortText: 'Room 1', parentId: null,
+ },
+ {
+ id: 1, text: 'Samantha Bright', parentId: 'room-1', color: '#A7E3A5',
+ },
+ {
+ id: 2, text: 'John Heart', parentId: 'room-1', color: '#F9E2AE',
+ },
+ {
+ id: 'room-2', text: '🏢 Room 2', shortText: 'Room 2', parentId: null,
+ },
+ {
+ id: 3, text: 'Todd Hoffman', parentId: 'room-2', color: '#F1BBBC',
+ },
+ {
+ id: 4, text: 'Sandra Johnson', parentId: 'room-2', color: '#CFE4FA',
+ },
+];
+
+const appointments: Appointment[] = [
+ {
+ text: 'Upgrade Personal Computers',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 13, 9, 30),
+ endDate: new Date(2026, 6, 13, 11, 30),
+ }, {
+ text: 'Install New Database',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 30),
+ }, {
+ text: 'Install New Router in Dev Room',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 14, 9, 0),
+ endDate: new Date(2026, 6, 14, 12, 15),
+ }, {
+ text: 'Brochure Design Review',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 15, 11, 0),
+ endDate: new Date(2026, 6, 15, 13, 30),
+ }, {
+ text: 'Book Flights to San Fran for Sales Trip',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 16, 10, 0),
+ endDate: new Date(2026, 6, 16, 12, 0),
+ }, {
+ text: 'Upgrade Server Hardware',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 17, 9, 0),
+ endDate: new Date(2026, 6, 17, 15, 0),
+ }, {
+ text: 'Google AdWords Strategy',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 13, 9, 0),
+ endDate: new Date(2026, 6, 13, 12, 0),
+ }, {
+ text: 'Rollout of New Website and Marketing Brochures',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 30),
+ }, {
+ text: 'Update NDA Agreement',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 14, 11, 0),
+ endDate: new Date(2026, 6, 14, 14, 15),
+ }, {
+ text: 'Submit Questions Regarding New NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 15, 10, 0),
+ endDate: new Date(2026, 6, 15, 11, 30),
+ }, {
+ text: 'Submit Signed NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 15, 13, 0),
+ endDate: new Date(2026, 6, 15, 15, 0),
+ }, {
+ text: 'Review Training Course for any Omissions',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 16, 11, 0),
+ endDate: new Date(2026, 6, 16, 14, 0),
+ }, {
+ text: 'Update Employee Files with New NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 17, 9, 0),
+ endDate: new Date(2026, 6, 17, 11, 45),
+ }, {
+ text: 'Website Re-Design Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 13, 9, 30),
+ endDate: new Date(2026, 6, 13, 11, 30),
+ }, {
+ text: 'New Brochures',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 15),
+ }, {
+ text: 'Approve Personal Computer Upgrade Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 14, 10, 0),
+ endDate: new Date(2026, 6, 14, 11, 0),
+ }, {
+ text: 'Final Budget Review',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 14, 12, 0),
+ endDate: new Date(2026, 6, 14, 13, 35),
+ }, {
+ text: 'Approve New Online Marketing Strategy',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 15, 12, 0),
+ endDate: new Date(2026, 6, 15, 14, 0),
+ }, {
+ text: 'Prepare 2026 Marketing Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 16, 11, 0),
+ endDate: new Date(2026, 6, 16, 13, 30),
+ }, {
+ text: 'Create Icons for Website',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 17, 10, 0),
+ endDate: new Date(2026, 6, 17, 11, 30),
+ }, {
+ text: 'Launch New Website',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 17, 12, 20),
+ endDate: new Date(2026, 6, 17, 14, 0),
+ }, {
+ text: 'Comment on Revenue Projections',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 13, 10, 0),
+ endDate: new Date(2026, 6, 13, 13, 0),
+ }, {
+ text: 'Approve Hiring of John Jeffers',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 14, 9, 0),
+ endDate: new Date(2026, 6, 14, 12, 0),
+ }, {
+ text: 'Non-Compete Agreements',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 14, 13, 0),
+ endDate: new Date(2026, 6, 14, 15, 45),
+ }, {
+ text: 'Review Revenue Projections',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 15, 11, 0),
+ endDate: new Date(2026, 6, 15, 14, 0),
+ }, {
+ text: 'Review Changes to Health Insurance Coverage',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 16, 9, 0),
+ endDate: new Date(2026, 6, 16, 13, 0),
+ }, {
+ text: 'Provide New Health Insurance Docs',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 17, 12, 0),
+ endDate: new Date(2026, 6, 17, 15, 0),
+ },
+];
+
+const resources: Resource[] = [
+ {
+ fieldExpr: 'assigneeId',
+ dataSource: assignees,
+ parentIdExpr: 'parentId',
+ label: 'Employee',
+ allowMultiple: true,
+ icon: 'user',
+ },
+];
+
+@Injectable()
+export class Service {
+ getAppointments(): Appointment[] {
+ return appointments;
+ }
+
+ getAssignees(): Assignee[] {
+ return assignees;
+ }
+
+ getResources(): Resource[] {
+ return resources;
+ }
+}
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/index.html b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/index.html
new file mode 100644
index 000000000000..1ab1fb54a1df
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Angular/index.html
@@ -0,0 +1,26 @@
+
+
+
+ DevExtreme Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading...
+
+
+
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/React/App.tsx b/apps/demos/Demos/Scheduler/MultilevelGrouping/React/App.tsx
new file mode 100644
index 000000000000..c59522550640
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/React/App.tsx
@@ -0,0 +1,194 @@
+import React from 'react';
+
+import Scheduler, { View } from 'devextreme-react/scheduler';
+import type { SchedulerTypes } from 'devextreme-react/scheduler';
+import { appointments, assignees } from './data.ts';
+
+const currentDate = new Date(2026, 6, 13);
+
+const groups = ['assigneeId'];
+
+const resources = [
+ {
+ fieldExpr: 'assigneeId',
+ dataSource: assignees,
+ parentIdExpr: 'parentId',
+ label: 'Employee',
+ allowMultiple: true,
+ icon: 'user',
+ },
+];
+
+type Form = SchedulerTypes.AppointmentFormOpeningEvent['form'];
+type FormItem = {
+ name?: string;
+ dataField?: string;
+ items?: FormItem[];
+ [key: string]: unknown;
+};
+type FoundItem = { list: FormItem[]; index: number } | null;
+
+const rooms = assignees.filter((item) => item.parentId === null);
+
+const employeesOf = (roomId: string | null) => assignees
+ .filter((item) => item.parentId === roomId);
+
+const roomOf = (assigneeId: number | undefined): string | null => assignees
+ .find((item) => item.id === assigneeId)?.parentId ?? null;
+
+const 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 = findItem(nested, predicate);
+
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+};
+
+const createRoomGroup = (form: Form, roomId: string | null): FormItem => ({
+ 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: () => '',
+ },
+ {
+ 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: { value: string }) {
+ const editor = form.getEditor('assigneeId');
+
+ editor?.option('dataSource', employeesOf(e.value));
+ editor?.option('value', []);
+ },
+ },
+ },
+ ],
+});
+
+const renderEmployeeTag = (data: { text: string; color?: string }) => {
+ 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: FormItem[]) => {
+ items.forEach((item) => {
+ if (item.items) {
+ hideLabels(item.items);
+ } else if (item.dataField !== 'allDay') {
+ item.label = { ...(item.label as object), visible: false };
+ }
+ });
+};
+
+const onAppointmentFormOpening = (e: SchedulerTypes.AppointmentFormOpeningEvent) => {
+ const { form } = e;
+ const items = form.option('items') as FormItem[];
+
+ if (findItem(items, (item) => item.name === 'roomGroup')) {
+ return;
+ }
+
+ const appointment = e.appointmentData ?? {};
+ const assigneeIds = appointment.assigneeId as number | number[] | undefined;
+ const [assigneeId] = Array.isArray(assigneeIds) ? assigneeIds : [assigneeIds];
+ const roomId = roomOf(assigneeId);
+ 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(items);
+
+ const employeeEditor = findItem(items, (item) => item.dataField === 'assigneeId');
+
+ if (employeeEditor) {
+ employeeEditor.list[employeeEditor.index].validationRules = [
+ { type: 'required', message: 'Employee is required' },
+ ];
+ employeeEditor.list[employeeEditor.index].editorOptions = {
+ ...(employeeEditor.list[employeeEditor.index].editorOptions as object),
+ tagTemplate: renderEmployeeTag,
+ };
+ }
+
+ const description = findItem(items, (item) => item.name === 'descriptionGroup');
+
+ if (description) {
+ description.list[description.index].visible = false;
+ }
+ form.option('items', items.slice());
+
+ form.getEditor('repeatEditor')?.option('value', repeatValue);
+
+ form.getEditor('assigneeId')?.option('dataSource', roomId ? employeesOf(roomId) : []);
+};
+
+const App = () => (
+
+
+
+
+);
+
+export default App;
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/React/data.ts b/apps/demos/Demos/Scheduler/MultilevelGrouping/React/data.ts
new file mode 100644
index 000000000000..03e0ea45df7a
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/React/data.ts
@@ -0,0 +1,171 @@
+import type { SchedulerTypes } from 'devextreme-react/scheduler';
+
+type Appointment = SchedulerTypes.Appointment & { assigneeId: number[] };
+
+type Assignee = {
+ id: number | string;
+ text: string;
+ parentId: string | null;
+ shortText?: string;
+ color?: string;
+};
+
+export const assignees: Assignee[] = [
+ {
+ id: 'room-1', text: '🏢 Room 1', shortText: 'Room 1', parentId: null,
+ },
+ {
+ id: 1, text: 'Samantha Bright', parentId: 'room-1', color: '#A7E3A5',
+ },
+ {
+ id: 2, text: 'John Heart', parentId: 'room-1', color: '#F9E2AE',
+ },
+ {
+ id: 'room-2', text: '🏢 Room 2', shortText: 'Room 2', parentId: null,
+ },
+ {
+ id: 3, text: 'Todd Hoffman', parentId: 'room-2', color: '#F1BBBC',
+ },
+ {
+ id: 4, text: 'Sandra Johnson', parentId: 'room-2', color: '#CFE4FA',
+ },
+];
+
+export const appointments: Appointment[] = [
+ {
+ text: 'Upgrade Personal Computers',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 13, 9, 30),
+ endDate: new Date(2026, 6, 13, 11, 30),
+ }, {
+ text: 'Install New Database',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 30),
+ }, {
+ text: 'Install New Router in Dev Room',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 14, 9, 0),
+ endDate: new Date(2026, 6, 14, 12, 15),
+ }, {
+ text: 'Brochure Design Review',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 15, 11, 0),
+ endDate: new Date(2026, 6, 15, 13, 30),
+ }, {
+ text: 'Book Flights to San Fran for Sales Trip',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 16, 10, 0),
+ endDate: new Date(2026, 6, 16, 12, 0),
+ }, {
+ text: 'Upgrade Server Hardware',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 17, 9, 0),
+ endDate: new Date(2026, 6, 17, 15, 0),
+ }, {
+ text: 'Google AdWords Strategy',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 13, 9, 0),
+ endDate: new Date(2026, 6, 13, 12, 0),
+ }, {
+ text: 'Rollout of New Website and Marketing Brochures',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 30),
+ }, {
+ text: 'Update NDA Agreement',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 14, 11, 0),
+ endDate: new Date(2026, 6, 14, 14, 15),
+ }, {
+ text: 'Submit Questions Regarding New NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 15, 10, 0),
+ endDate: new Date(2026, 6, 15, 11, 30),
+ }, {
+ text: 'Submit Signed NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 15, 13, 0),
+ endDate: new Date(2026, 6, 15, 15, 0),
+ }, {
+ text: 'Review Training Course for any Omissions',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 16, 11, 0),
+ endDate: new Date(2026, 6, 16, 14, 0),
+ }, {
+ text: 'Update Employee Files with New NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 17, 9, 0),
+ endDate: new Date(2026, 6, 17, 11, 45),
+ }, {
+ text: 'Website Re-Design Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 13, 9, 30),
+ endDate: new Date(2026, 6, 13, 11, 30),
+ }, {
+ text: 'New Brochures',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 15),
+ }, {
+ text: 'Approve Personal Computer Upgrade Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 14, 10, 0),
+ endDate: new Date(2026, 6, 14, 11, 0),
+ }, {
+ text: 'Final Budget Review',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 14, 12, 0),
+ endDate: new Date(2026, 6, 14, 13, 35),
+ }, {
+ text: 'Approve New Online Marketing Strategy',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 15, 12, 0),
+ endDate: new Date(2026, 6, 15, 14, 0),
+ }, {
+ text: 'Prepare 2026 Marketing Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 16, 11, 0),
+ endDate: new Date(2026, 6, 16, 13, 30),
+ }, {
+ text: 'Create Icons for Website',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 17, 10, 0),
+ endDate: new Date(2026, 6, 17, 11, 30),
+ }, {
+ text: 'Launch New Website',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 17, 12, 20),
+ endDate: new Date(2026, 6, 17, 14, 0),
+ }, {
+ text: 'Comment on Revenue Projections',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 13, 10, 0),
+ endDate: new Date(2026, 6, 13, 13, 0),
+ }, {
+ text: 'Approve Hiring of John Jeffers',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 14, 9, 0),
+ endDate: new Date(2026, 6, 14, 12, 0),
+ }, {
+ text: 'Non-Compete Agreements',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 14, 13, 0),
+ endDate: new Date(2026, 6, 14, 15, 45),
+ }, {
+ text: 'Review Revenue Projections',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 15, 11, 0),
+ endDate: new Date(2026, 6, 15, 14, 0),
+ }, {
+ text: 'Review Changes to Health Insurance Coverage',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 16, 9, 0),
+ endDate: new Date(2026, 6, 16, 13, 0),
+ }, {
+ text: 'Provide New Health Insurance Docs',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 17, 12, 0),
+ endDate: new Date(2026, 6, 17, 15, 0),
+ },
+];
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/React/index.html b/apps/demos/Demos/Scheduler/MultilevelGrouping/React/index.html
new file mode 100644
index 000000000000..ee451f8288ff
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/React/index.html
@@ -0,0 +1,24 @@
+
+
+
+ DevExtreme Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/React/index.tsx b/apps/demos/Demos/Scheduler/MultilevelGrouping/React/index.tsx
new file mode 100644
index 000000000000..8acbec4b6179
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/React/index.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import App from './App.tsx';
+
+ReactDOM.render(
+ ,
+ document.getElementById('app'),
+);
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/React/styles.css b/apps/demos/Demos/Scheduler/MultilevelGrouping/React/styles.css
new file mode 100644
index 000000000000..2fdf6823a33d
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/React/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/Demos/Scheduler/MultilevelGrouping/ReactJs/App.js b/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/App.js
new file mode 100644
index 000000000000..a6c6d9e1c5d3
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/App.js
@@ -0,0 +1,150 @@
+import React from 'react';
+import Scheduler, { View } from 'devextreme-react/scheduler';
+import { appointments, assignees } from './data.js';
+
+const currentDate = new Date(2026, 6, 13);
+const groups = ['assigneeId'];
+const resources = [
+ {
+ fieldExpr: 'assigneeId',
+ dataSource: assignees,
+ parentIdExpr: 'parentId',
+ label: 'Employee',
+ allowMultiple: true,
+ icon: 'user',
+ },
+];
+const rooms = assignees.filter((item) => item.parentId === null);
+const employeesOf = (roomId) => assignees.filter((item) => item.parentId === roomId);
+const roomOf = (assigneeId) => assignees.find((item) => item.id === assigneeId)?.parentId ?? null;
+const findItem = (items, predicate) => {
+ 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 = findItem(nested, predicate);
+ if (found) {
+ return found;
+ }
+ }
+ }
+ return null;
+};
+const createRoomGroup = (form, roomId) => ({
+ 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: () => '',
+ },
+ {
+ 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', 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 onAppointmentFormOpening = (e) => {
+ const { form } = e;
+ const items = form.option('items');
+ if (findItem(items, (item) => item.name === 'roomGroup')) {
+ return;
+ }
+ const appointment = e.appointmentData ?? {};
+ const assigneeIds = appointment.assigneeId;
+ const [assigneeId] = Array.isArray(assigneeIds) ? assigneeIds : [assigneeIds];
+ const roomId = roomOf(assigneeId);
+ 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(items);
+ const employeeEditor = findItem(items, (item) => item.dataField === 'assigneeId');
+ if (employeeEditor) {
+ employeeEditor.list[employeeEditor.index].validationRules = [
+ { type: 'required', message: 'Employee is required' },
+ ];
+ employeeEditor.list[employeeEditor.index].editorOptions = {
+ ...employeeEditor.list[employeeEditor.index].editorOptions,
+ tagTemplate: renderEmployeeTag,
+ };
+ }
+ const description = findItem(items, (item) => item.name === 'descriptionGroup');
+ if (description) {
+ description.list[description.index].visible = false;
+ }
+ form.option('items', items.slice());
+ form.getEditor('repeatEditor')?.option('value', repeatValue);
+ form.getEditor('assigneeId')?.option('dataSource', roomId ? employeesOf(roomId) : []);
+};
+const App = () => (
+
+
+
+
+);
+export default App;
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/data.js b/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/data.js
new file mode 100644
index 000000000000..4c73285e8f2e
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/data.js
@@ -0,0 +1,202 @@
+export const assignees = [
+ {
+ id: 'room-1',
+ text: '🏢 Room 1',
+ shortText: 'Room 1',
+ parentId: null,
+ },
+ {
+ id: 1,
+ text: 'Samantha Bright',
+ parentId: 'room-1',
+ color: '#A7E3A5',
+ },
+ {
+ id: 2,
+ text: 'John Heart',
+ parentId: 'room-1',
+ color: '#F9E2AE',
+ },
+ {
+ id: 'room-2',
+ text: '🏢 Room 2',
+ shortText: 'Room 2',
+ parentId: null,
+ },
+ {
+ id: 3,
+ text: 'Todd Hoffman',
+ parentId: 'room-2',
+ color: '#F1BBBC',
+ },
+ {
+ id: 4,
+ text: 'Sandra Johnson',
+ parentId: 'room-2',
+ color: '#CFE4FA',
+ },
+];
+export const appointments = [
+ {
+ text: 'Upgrade Personal Computers',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 13, 9, 30),
+ endDate: new Date(2026, 6, 13, 11, 30),
+ },
+ {
+ text: 'Install New Database',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 30),
+ },
+ {
+ text: 'Install New Router in Dev Room',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 14, 9, 0),
+ endDate: new Date(2026, 6, 14, 12, 15),
+ },
+ {
+ text: 'Brochure Design Review',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 15, 11, 0),
+ endDate: new Date(2026, 6, 15, 13, 30),
+ },
+ {
+ text: 'Book Flights to San Fran for Sales Trip',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 16, 10, 0),
+ endDate: new Date(2026, 6, 16, 12, 0),
+ },
+ {
+ text: 'Upgrade Server Hardware',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 17, 9, 0),
+ endDate: new Date(2026, 6, 17, 15, 0),
+ },
+ {
+ text: 'Google AdWords Strategy',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 13, 9, 0),
+ endDate: new Date(2026, 6, 13, 12, 0),
+ },
+ {
+ text: 'Rollout of New Website and Marketing Brochures',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 30),
+ },
+ {
+ text: 'Update NDA Agreement',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 14, 11, 0),
+ endDate: new Date(2026, 6, 14, 14, 15),
+ },
+ {
+ text: 'Submit Questions Regarding New NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 15, 10, 0),
+ endDate: new Date(2026, 6, 15, 11, 30),
+ },
+ {
+ text: 'Submit Signed NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 15, 13, 0),
+ endDate: new Date(2026, 6, 15, 15, 0),
+ },
+ {
+ text: 'Review Training Course for any Omissions',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 16, 11, 0),
+ endDate: new Date(2026, 6, 16, 14, 0),
+ },
+ {
+ text: 'Update Employee Files with New NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 17, 9, 0),
+ endDate: new Date(2026, 6, 17, 11, 45),
+ },
+ {
+ text: 'Website Re-Design Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 13, 9, 30),
+ endDate: new Date(2026, 6, 13, 11, 30),
+ },
+ {
+ text: 'New Brochures',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 15),
+ },
+ {
+ text: 'Approve Personal Computer Upgrade Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 14, 10, 0),
+ endDate: new Date(2026, 6, 14, 11, 0),
+ },
+ {
+ text: 'Final Budget Review',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 14, 12, 0),
+ endDate: new Date(2026, 6, 14, 13, 35),
+ },
+ {
+ text: 'Approve New Online Marketing Strategy',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 15, 12, 0),
+ endDate: new Date(2026, 6, 15, 14, 0),
+ },
+ {
+ text: 'Prepare 2026 Marketing Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 16, 11, 0),
+ endDate: new Date(2026, 6, 16, 13, 30),
+ },
+ {
+ text: 'Create Icons for Website',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 17, 10, 0),
+ endDate: new Date(2026, 6, 17, 11, 30),
+ },
+ {
+ text: 'Launch New Website',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 17, 12, 20),
+ endDate: new Date(2026, 6, 17, 14, 0),
+ },
+ {
+ text: 'Comment on Revenue Projections',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 13, 10, 0),
+ endDate: new Date(2026, 6, 13, 13, 0),
+ },
+ {
+ text: 'Approve Hiring of John Jeffers',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 14, 9, 0),
+ endDate: new Date(2026, 6, 14, 12, 0),
+ },
+ {
+ text: 'Non-Compete Agreements',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 14, 13, 0),
+ endDate: new Date(2026, 6, 14, 15, 45),
+ },
+ {
+ text: 'Review Revenue Projections',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 15, 11, 0),
+ endDate: new Date(2026, 6, 15, 14, 0),
+ },
+ {
+ text: 'Review Changes to Health Insurance Coverage',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 16, 9, 0),
+ endDate: new Date(2026, 6, 16, 13, 0),
+ },
+ {
+ text: 'Provide New Health Insurance Docs',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 17, 12, 0),
+ endDate: new Date(2026, 6, 17, 15, 0),
+ },
+];
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/index.html b/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/index.html
new file mode 100644
index 000000000000..db31b0fd60c6
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/index.html
@@ -0,0 +1,44 @@
+
+
+
+ DevExtreme Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/index.js b/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/index.js
new file mode 100644
index 000000000000..b853e0be8242
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/index.js
@@ -0,0 +1,5 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App.js';
+
+ReactDOM.render(, document.getElementById('app'));
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/styles.css b/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/styles.css
new file mode 100644
index 000000000000..2fdf6823a33d
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/ReactJs/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/Demos/Scheduler/MultilevelGrouping/Vue/App.vue b/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/App.vue
new file mode 100644
index 000000000000..667f1e4dbc83
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/App.vue
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/data.ts b/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/data.ts
new file mode 100644
index 000000000000..6a9ffea09cb4
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/data.ts
@@ -0,0 +1,159 @@
+export const assignees = [
+ {
+ id: 'room-1', text: '🏢 Room 1', shortText: 'Room 1', parentId: null,
+ },
+ {
+ id: 1, text: 'Samantha Bright', parentId: 'room-1', color: '#A7E3A5',
+ },
+ {
+ id: 2, text: 'John Heart', parentId: 'room-1', color: '#F9E2AE',
+ },
+ {
+ id: 'room-2', text: '🏢 Room 2', shortText: 'Room 2', parentId: null,
+ },
+ {
+ id: 3, text: 'Todd Hoffman', parentId: 'room-2', color: '#F1BBBC',
+ },
+ {
+ id: 4, text: 'Sandra Johnson', parentId: 'room-2', color: '#CFE4FA',
+ },
+];
+
+export const appointments = [
+ {
+ text: 'Upgrade Personal Computers',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 13, 9, 30),
+ endDate: new Date(2026, 6, 13, 11, 30),
+ }, {
+ text: 'Install New Database',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 30),
+ }, {
+ text: 'Install New Router in Dev Room',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 14, 9, 0),
+ endDate: new Date(2026, 6, 14, 12, 15),
+ }, {
+ text: 'Brochure Design Review',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 15, 11, 0),
+ endDate: new Date(2026, 6, 15, 13, 30),
+ }, {
+ text: 'Book Flights to San Fran for Sales Trip',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 16, 10, 0),
+ endDate: new Date(2026, 6, 16, 12, 0),
+ }, {
+ text: 'Upgrade Server Hardware',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 17, 9, 0),
+ endDate: new Date(2026, 6, 17, 15, 0),
+ }, {
+ text: 'Google AdWords Strategy',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 13, 9, 0),
+ endDate: new Date(2026, 6, 13, 12, 0),
+ }, {
+ text: 'Rollout of New Website and Marketing Brochures',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 30),
+ }, {
+ text: 'Update NDA Agreement',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 14, 11, 0),
+ endDate: new Date(2026, 6, 14, 14, 15),
+ }, {
+ text: 'Submit Questions Regarding New NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 15, 10, 0),
+ endDate: new Date(2026, 6, 15, 11, 30),
+ }, {
+ text: 'Submit Signed NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 15, 13, 0),
+ endDate: new Date(2026, 6, 15, 15, 0),
+ }, {
+ text: 'Review Training Course for any Omissions',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 16, 11, 0),
+ endDate: new Date(2026, 6, 16, 14, 0),
+ }, {
+ text: 'Update Employee Files with New NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 17, 9, 0),
+ endDate: new Date(2026, 6, 17, 11, 45),
+ }, {
+ text: 'Website Re-Design Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 13, 9, 30),
+ endDate: new Date(2026, 6, 13, 11, 30),
+ }, {
+ text: 'New Brochures',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 15),
+ }, {
+ text: 'Approve Personal Computer Upgrade Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 14, 10, 0),
+ endDate: new Date(2026, 6, 14, 11, 0),
+ }, {
+ text: 'Final Budget Review',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 14, 12, 0),
+ endDate: new Date(2026, 6, 14, 13, 35),
+ }, {
+ text: 'Approve New Online Marketing Strategy',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 15, 12, 0),
+ endDate: new Date(2026, 6, 15, 14, 0),
+ }, {
+ text: 'Prepare 2026 Marketing Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 16, 11, 0),
+ endDate: new Date(2026, 6, 16, 13, 30),
+ }, {
+ text: 'Create Icons for Website',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 17, 10, 0),
+ endDate: new Date(2026, 6, 17, 11, 30),
+ }, {
+ text: 'Launch New Website',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 17, 12, 20),
+ endDate: new Date(2026, 6, 17, 14, 0),
+ }, {
+ text: 'Comment on Revenue Projections',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 13, 10, 0),
+ endDate: new Date(2026, 6, 13, 13, 0),
+ }, {
+ text: 'Approve Hiring of John Jeffers',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 14, 9, 0),
+ endDate: new Date(2026, 6, 14, 12, 0),
+ }, {
+ text: 'Non-Compete Agreements',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 14, 13, 0),
+ endDate: new Date(2026, 6, 14, 15, 45),
+ }, {
+ text: 'Review Revenue Projections',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 15, 11, 0),
+ endDate: new Date(2026, 6, 15, 14, 0),
+ }, {
+ text: 'Review Changes to Health Insurance Coverage',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 16, 9, 0),
+ endDate: new Date(2026, 6, 16, 13, 0),
+ }, {
+ text: 'Provide New Health Insurance Docs',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 17, 12, 0),
+ endDate: new Date(2026, 6, 17, 15, 0),
+ },
+];
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/index.html b/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/index.html
new file mode 100644
index 000000000000..c90687f1390f
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/index.html
@@ -0,0 +1,27 @@
+
+
+
+ DevExtreme Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/index.ts b/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/index.ts
new file mode 100644
index 000000000000..684d04215d72
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/Vue/index.ts
@@ -0,0 +1,4 @@
+import { createApp } from 'vue';
+import App from './App.vue';
+
+createApp(App).mount('#app');
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/data.js b/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/data.js
new file mode 100644
index 000000000000..fddf43749178
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/data.js
@@ -0,0 +1,159 @@
+const assignees = [
+ {
+ id: 'room-1', text: '🏢 Room 1', shortText: 'Room 1', parentId: null,
+ },
+ {
+ id: 1, text: 'Samantha Bright', parentId: 'room-1', color: '#A7E3A5',
+ },
+ {
+ id: 2, text: 'John Heart', parentId: 'room-1', color: '#F9E2AE',
+ },
+ {
+ id: 'room-2', text: '🏢 Room 2', shortText: 'Room 2', parentId: null,
+ },
+ {
+ id: 3, text: 'Todd Hoffman', parentId: 'room-2', color: '#F1BBBC',
+ },
+ {
+ id: 4, text: 'Sandra Johnson', parentId: 'room-2', color: '#CFE4FA',
+ },
+];
+
+const appointments = [
+ {
+ text: 'Upgrade Personal Computers',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 13, 9, 30),
+ endDate: new Date(2026, 6, 13, 11, 30),
+ }, {
+ text: 'Install New Database',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 30),
+ }, {
+ text: 'Install New Router in Dev Room',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 14, 9, 0),
+ endDate: new Date(2026, 6, 14, 12, 15),
+ }, {
+ text: 'Brochure Design Review',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 15, 11, 0),
+ endDate: new Date(2026, 6, 15, 13, 30),
+ }, {
+ text: 'Book Flights to San Fran for Sales Trip',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 16, 10, 0),
+ endDate: new Date(2026, 6, 16, 12, 0),
+ }, {
+ text: 'Upgrade Server Hardware',
+ assigneeId: [1],
+ startDate: new Date(2026, 6, 17, 9, 0),
+ endDate: new Date(2026, 6, 17, 15, 0),
+ }, {
+ text: 'Google AdWords Strategy',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 13, 9, 0),
+ endDate: new Date(2026, 6, 13, 12, 0),
+ }, {
+ text: 'Rollout of New Website and Marketing Brochures',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 30),
+ }, {
+ text: 'Update NDA Agreement',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 14, 11, 0),
+ endDate: new Date(2026, 6, 14, 14, 15),
+ }, {
+ text: 'Submit Questions Regarding New NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 15, 10, 0),
+ endDate: new Date(2026, 6, 15, 11, 30),
+ }, {
+ text: 'Submit Signed NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 15, 13, 0),
+ endDate: new Date(2026, 6, 15, 15, 0),
+ }, {
+ text: 'Review Training Course for any Omissions',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 16, 11, 0),
+ endDate: new Date(2026, 6, 16, 14, 0),
+ }, {
+ text: 'Update Employee Files with New NDA',
+ assigneeId: [2],
+ startDate: new Date(2026, 6, 17, 9, 0),
+ endDate: new Date(2026, 6, 17, 11, 45),
+ }, {
+ text: 'Website Re-Design Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 13, 9, 30),
+ endDate: new Date(2026, 6, 13, 11, 30),
+ }, {
+ text: 'New Brochures',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 13, 13, 0),
+ endDate: new Date(2026, 6, 13, 15, 15),
+ }, {
+ text: 'Approve Personal Computer Upgrade Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 14, 10, 0),
+ endDate: new Date(2026, 6, 14, 11, 0),
+ }, {
+ text: 'Final Budget Review',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 14, 12, 0),
+ endDate: new Date(2026, 6, 14, 13, 35),
+ }, {
+ text: 'Approve New Online Marketing Strategy',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 15, 12, 0),
+ endDate: new Date(2026, 6, 15, 14, 0),
+ }, {
+ text: 'Prepare 2026 Marketing Plan',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 16, 11, 0),
+ endDate: new Date(2026, 6, 16, 13, 30),
+ }, {
+ text: 'Create Icons for Website',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 17, 10, 0),
+ endDate: new Date(2026, 6, 17, 11, 30),
+ }, {
+ text: 'Launch New Website',
+ assigneeId: [3],
+ startDate: new Date(2026, 6, 17, 12, 20),
+ endDate: new Date(2026, 6, 17, 14, 0),
+ }, {
+ text: 'Comment on Revenue Projections',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 13, 10, 0),
+ endDate: new Date(2026, 6, 13, 13, 0),
+ }, {
+ text: 'Approve Hiring of John Jeffers',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 14, 9, 0),
+ endDate: new Date(2026, 6, 14, 12, 0),
+ }, {
+ text: 'Non-Compete Agreements',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 14, 13, 0),
+ endDate: new Date(2026, 6, 14, 15, 45),
+ }, {
+ text: 'Review Revenue Projections',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 15, 11, 0),
+ endDate: new Date(2026, 6, 15, 14, 0),
+ }, {
+ text: 'Review Changes to Health Insurance Coverage',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 16, 9, 0),
+ endDate: new Date(2026, 6, 16, 13, 0),
+ }, {
+ text: 'Provide New Health Insurance Docs',
+ assigneeId: [4],
+ startDate: new Date(2026, 6, 17, 12, 0),
+ endDate: new Date(2026, 6, 17, 15, 0),
+ },
+];
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/index.html b/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/index.html
new file mode 100644
index 000000000000..2ae8d3e24cc1
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/index.html
@@ -0,0 +1,20 @@
+
+
+
+ DevExtreme Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/index.js b/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/index.js
new file mode 100644
index 000000000000..a416140d5762
--- /dev/null
+++ b/apps/demos/Demos/Scheduler/MultilevelGrouping/jQuery/index.js
@@ -0,0 +1,167 @@
+const rooms = assignees.filter((item) => item.parentId === null);
+
+const employeesOf = (roomId) => assignees.filter((item) => item.parentId === roomId);
+
+const roomOf = (assigneeId) => {
+ const employee = assignees.find((item) => item.id === assigneeId);
+
+ return employee ? employee.parentId : null;
+};
+
+const findItem = (items, predicate) => {
+ for (let i = 0; i < items.length; i += 1) {
+ if (predicate(items[i])) {
+ return { list: items, index: i };
+ }
+
+ if (items[i].items) {
+ const found = findItem(items[i].items, predicate);
+
+ if (found) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+};
+
+const createRoomGroup = (form, roomId) => ({
+ 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', 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 };
+ }
+ });
+};
+
+$(() => {
+ $('#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: true,
+ showAllDayPanel: false,
+ showCurrentTimeIndicator: false,
+ height: 700,
+ onAppointmentFormOpening(e) {
+ const { form } = e;
+ const items = form.option('items');
+
+ if (findItem(items, (item) => item.name === 'roomGroup')) {
+ return;
+ }
+
+ const appointment = e.appointmentData ?? {};
+ const assigneeIds = appointment.assigneeId;
+ const [assigneeId] = Array.isArray(assigneeIds) ? assigneeIds : [assigneeIds];
+ const roomId = roomOf(assigneeId);
+ 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(items);
+
+ const employeeEditor = findItem(items, (item) => item.dataField === 'assigneeId');
+
+ if (employeeEditor) {
+ employeeEditor.list[employeeEditor.index].validationRules = [
+ { type: 'required', message: 'Employee is required' },
+ ];
+ employeeEditor.list[employeeEditor.index].editorOptions = {
+ ...employeeEditor.list[employeeEditor.index].editorOptions,
+ tagTemplate: renderEmployeeTag,
+ };
+ }
+
+ const description = findItem(items, (item) => item.name === 'descriptionGroup');
+
+ if (description) {
+ description.list[description.index].visible = false;
+ }
+ 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 615934456789..ca85a670ba07 100644
--- a/apps/demos/menuMeta.json
+++ b/apps/demos/menuMeta.json
@@ -3927,6 +3927,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..44df84eb8854
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..0414a9042d85
Binary files /dev/null and b/apps/demos/testing/etalons/Scheduler-MultilevelGrouping (material.blue.light).png differ