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