From b4d9f7b2057ca508ddb51efc988614f02feab47c Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 26 Feb 2026 07:10:32 -0300 Subject: [PATCH 01/12] Scheduler overlapping demo --- .../Angular/app/app.component.css | 24 +++ .../Angular/app/app.component.html | 23 +++ .../Angular/app/app.component.ts | 165 ++++++++++++++++ .../Angular/app/app.service.ts | 46 +++++ .../AppointmentOverlapping/Angular/index.html | 26 +++ .../AppointmentOverlapping/React/App.tsx | 173 ++++++++++++++++ .../AppointmentOverlapping/React/data.ts | 33 ++++ .../AppointmentOverlapping/React/index.html | 24 +++ .../AppointmentOverlapping/React/index.tsx | 9 + .../AppointmentOverlapping/React/styles.css | 24 +++ .../AppointmentOverlapping/ReactJs/App.js | 165 ++++++++++++++++ .../AppointmentOverlapping/ReactJs/data.js | 25 +++ .../AppointmentOverlapping/ReactJs/index.html | 24 +++ .../AppointmentOverlapping/ReactJs/index.js | 5 + .../AppointmentOverlapping/ReactJs/styles.css | 24 +++ .../AppointmentOverlapping/Vue/App.vue | 186 ++++++++++++++++++ .../AppointmentOverlapping/Vue/data.ts | 33 ++++ .../AppointmentOverlapping/Vue/index.html | 29 +++ .../AppointmentOverlapping/Vue/index.ts | 4 + .../AppointmentOverlapping/jQuery/data.js | 25 +++ .../AppointmentOverlapping/jQuery/index.html | 28 +++ .../AppointmentOverlapping/jQuery/index.js | 129 ++++++++++++ .../AppointmentOverlapping/jQuery/styles.css | 24 +++ apps/demos/menuMeta.json | 6 + 24 files changed, 1254 insertions(+) create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.css create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.html create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.ts create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.service.ts create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/index.html create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/React/App.tsx create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/React/data.ts create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.html create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.tsx create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/React/styles.css create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/App.js create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/data.js create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.html create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.js create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/styles.css create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/App.vue create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/data.ts create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.html create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.ts create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/data.js create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js create mode 100644 apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.css b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.css new file mode 100644 index 000000000000..976d4f1327e5 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.css @@ -0,0 +1,24 @@ +::ng-deep .conflict-informer { + background-color: #fceae8; + color: #c50f1f; + padding: 8px 12px; + border-radius: 4px; +} + +.options { + padding: 20px; + background-color: rgba(191, 191, 191, 0.15); + margin-top: 20px; +} + +.caption { + font-size: 18px; + font-weight: 500; +} + +.option { + margin-top: 10px; + display: flex; + align-items: center; + gap: 8px; +} diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.html b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.html new file mode 100644 index 000000000000..76e4687797a8 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.html @@ -0,0 +1,23 @@ + +
+
Options
+
+ Allow Appointment Overlapping + +
+
diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.ts b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.ts new file mode 100644 index 000000000000..42d038e3645b --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.ts @@ -0,0 +1,165 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { Component, enableProdMode, ViewChild, provideZoneChangeDetection } from '@angular/core'; +import { DxSchedulerModule, DxSchedulerComponent, DxSwitchModule } from 'devextreme-angular'; +import { DxSchedulerTypes } from 'devextreme-angular/ui/scheduler'; +import notify from 'devextreme/ui/notify'; +import dxForm from 'devextreme/ui/form'; +import { Appointment, Service, projects } from './app.service'; + +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], + preserveWhitespaces: true, + imports: [ + DxSchedulerModule, + DxSwitchModule, + ], +}) +export class AppComponent { + @ViewChild(DxSchedulerComponent, { static: false }) scheduler!: DxSchedulerComponent; + + appointmentsData: Appointment[]; + + currentDate: Date = new Date(2026, 1, 10); + + allowOverlapping = false; + + resources = [{ + fieldExpr: 'projectId', + dataSource: projects, + valueExpr: 'id', + colorExpr: 'color', + }]; + + editingConfig = { + form: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + customizeItem: (item: any) => { + if (item.name === 'endDateEditor') { + const alreadyAdded = (item.validationRules ?? []).some( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (r: any) => r.type === 'custom' && r.message === 'This time slot conflicts with another appointment.', + ); + if (alreadyAdded) return; + item.validationRules = [ + ...(item.validationRules ?? []), + { + type: 'custom', + message: 'This time slot conflicts with another appointment.', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validationCallback: ({ validator }: any): boolean => { + if (this.allowOverlapping) return true; + const formEl = validator.$element().closest('.dx-form')[0]; + const formInstance = dxForm.getInstance(formEl); + if (!formInstance) return true; + const formData = formInstance.option('formData') as Appointment; + const hasConflict = this.detectConflict(formData); + const informerEl = formEl.querySelector('.conflict-informer') as HTMLElement | null; + if (informerEl) { + informerEl.style.display = hasConflict ? '' : 'none'; + } + return !hasConflict; + }, + }, + ]; + } + }, + }, + }; + + constructor(service: Service) { + this.appointmentsData = service.getAppointments(); + } + + private isOverlapping( + a: { startDate: Date; endDate: Date }, + b: { startDate: Date; endDate: Date }, + ): boolean { + return a.startDate < b.endDate && a.endDate > b.startDate; + } + + detectConflict(newAppt: Appointment): boolean { + const instance = this.scheduler?.instance; + if (!instance) return false; + + const allItems = instance.getDataSource().items() as Appointment[]; + + const existingOccurrences = instance + .getOccurrences(new Date(newAppt.startDate), new Date(newAppt.endDate), allItems) + .filter((occ) => (occ.appointmentData as Appointment).id !== newAppt.id); + + const expandEnd = new Date(newAppt.endDate); + expandEnd.setDate(expandEnd.getDate() + 14); + + const newOccurrences = instance.getOccurrences( + new Date(newAppt.startDate), + expandEnd, + [newAppt], + ); + + return newOccurrences.some((newOcc) => + existingOccurrences.some((existingOcc) => this.isOverlapping(newOcc, existingOcc)), + ); + } + + onAppointmentFormOpening(e: DxSchedulerTypes.AppointmentFormOpeningEvent): void { + const { form } = e; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const items = form.option('items') as any[]; + if (!items.some((item: any) => item.name === 'conflictInformer')) { + form.option('items', [ + { + name: 'conflictInformer', + itemType: 'simple', + label: { visible: false }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + template: (_: unknown, element: any) => { + const div = document.createElement('div'); + div.className = 'conflict-informer'; + div.textContent = 'This time slot conflicts with another appointment.'; + div.style.display = 'none'; + (element[0] ?? element).appendChild(div); + }, + }, + ...items, + ]); + } + } + + onAppointmentAdding(e: DxSchedulerTypes.AppointmentAddingEvent): void { + if (this.allowOverlapping) return; + + if (this.detectConflict(e.appointmentData as Appointment)) { + e.cancel = true; + notify('Cannot create an appointment that overlaps with an existing one.', 'warning', 2000); + } + } + + onAppointmentUpdating(e: DxSchedulerTypes.AppointmentUpdatingEvent): void { + if (this.allowOverlapping) return; + + const updatedAppt = { ...e.appointmentData, ...e.newData } as Appointment; + if (this.detectConflict(updatedAppt)) { + e.cancel = true; + notify('Cannot move an appointment to a time slot that is already occupied.', 'warning', 2000); + } + } +} + +bootstrapApplication(AppComponent, { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), + ], +}); diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.service.ts b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.service.ts new file mode 100644 index 000000000000..70e04aa3cbce --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; + +export class Appointment { + id: number; + + text: string; + + startDate: Date; + + endDate: Date; + + projectId: number; +} + +export const projects = [ + { id: 1, color: '#A7E3A5' }, + { id: 2, color: '#CFE4FA' }, + { id: 3, color: '#F9E2AE' }, + { id: 4, color: '#F1BBBC' }, +]; + +const appointments: Appointment[] = [ + // Mon Feb 9 + { id: 1, text: 'Website Re-Design Plan', startDate: new Date(2026, 1, 9, 9, 30), endDate: new Date(2026, 1, 9, 11, 30), projectId: 2 }, + { id: 2, text: 'Install New Router in Dev Room', startDate: new Date(2026, 1, 9, 14, 30), endDate: new Date(2026, 1, 9, 15, 30), projectId: 3 }, + // Tue Feb 10 + { id: 3, text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2026, 1, 10, 10, 0), endDate: new Date(2026, 1, 10, 11, 0), projectId: 1 }, + { id: 4, text: 'Final Budget Review', startDate: new Date(2026, 1, 10, 12, 0), endDate: new Date(2026, 1, 10, 13, 35), projectId: 1 }, + // Wed Feb 11 + { id: 5, text: 'Install New Database', startDate: new Date(2026, 1, 11, 9, 45), endDate: new Date(2026, 1, 11, 11, 15), projectId: 4 }, + { id: 6, text: 'Approve New Online Marketing Strategy', startDate: new Date(2026, 1, 11, 12, 0), endDate: new Date(2026, 1, 11, 14, 0), projectId: 2 }, + // Thu Feb 12 + { id: 7, text: 'Prepare 2021 Marketing Plan', startDate: new Date(2026, 1, 12, 11, 0), endDate: new Date(2026, 1, 12, 13, 30), projectId: 3 }, + { id: 8, text: 'Brochure Design Review', startDate: new Date(2026, 1, 12, 14, 0), endDate: new Date(2026, 1, 12, 15, 30), projectId: 2 }, + // Fri Feb 13 + { id: 9, text: 'Create Icons for Website', startDate: new Date(2026, 1, 13, 10, 0), endDate: new Date(2026, 1, 13, 11, 30), projectId: 1 }, + { id: 10, text: 'Launch New Website', startDate: new Date(2026, 1, 13, 12, 20), endDate: new Date(2026, 1, 13, 14, 0), projectId: 4 }, + { id: 11, text: 'Upgrade Server Hardware', startDate: new Date(2026, 1, 13, 14, 30), endDate: new Date(2026, 1, 13, 16, 0), projectId: 2 }, +]; + +@Injectable() +export class Service { + getAppointments(): Appointment[] { + return appointments; + } +} diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/index.html b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/index.html new file mode 100644 index 000000000000..1ab1fb54a1df --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/index.html @@ -0,0 +1,26 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + + + +
+ Loading... +
+ + diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/App.tsx b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/App.tsx new file mode 100644 index 000000000000..bbc63c1889a0 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/App.tsx @@ -0,0 +1,173 @@ +import React, { + useCallback, useEffect, useMemo, useRef, useState, +} from 'react'; +import Scheduler, { type SchedulerRef, type SchedulerTypes } from 'devextreme-react/scheduler'; +import Switch, { type SwitchTypes } from 'devextreme-react/switch'; +import notify from 'devextreme/ui/notify'; +import dxForm from 'devextreme/ui/form'; +import { data, projects, type Appointment } from './data.ts'; + +const currentDate = new Date(2026, 1, 10); +const views: SchedulerTypes.ViewType[] = ['day', 'week']; +const resources = [{ + fieldExpr: 'projectId', + dataSource: projects, + valueExpr: 'id', + colorExpr: 'color', +}]; + +function isOverlapping( + a: { startDate: Date; endDate: Date }, + b: { startDate: Date; endDate: Date }, +): boolean { + return a.startDate < b.endDate && a.endDate > b.startDate; +} + +const App = () => { + const schedulerRef = useRef(null); + const [allowOverlapping, setAllowOverlapping] = useState(false); + const allowOverlappingRef = useRef(allowOverlapping); + + useEffect(() => { + allowOverlappingRef.current = allowOverlapping; + }, [allowOverlapping]); + + const detectConflict = useCallback((newAppt: Appointment): boolean => { + const scheduler = schedulerRef.current?.instance?.(); + if (!scheduler) return false; + + const allItems = scheduler.getDataSource().items() as Appointment[]; + + const existingOccurrences = scheduler + .getOccurrences(newAppt.startDate, newAppt.endDate, allItems) + .filter((occ) => (occ.appointmentData as Appointment).id !== newAppt.id); + + const expandEnd = new Date(newAppt.endDate); + expandEnd.setDate(expandEnd.getDate() + 14); + + const newOccurrences = scheduler.getOccurrences( + newAppt.startDate, + expandEnd, + [newAppt], + ); + + return newOccurrences.some((newOcc) => + existingOccurrences.some((existingOcc) => isOverlapping(newOcc, existingOcc)), + ); + }, []); + + const onAppointmentAdding = useCallback((e: SchedulerTypes.AppointmentAddingEvent) => { + if (allowOverlapping) return; + + if (detectConflict(e.appointmentData as Appointment)) { + e.cancel = true; + notify('Cannot create an appointment that overlaps with an existing one.', 'warning', 2000); + } + }, [allowOverlapping, detectConflict]); + + const onAppointmentUpdating = useCallback((e: SchedulerTypes.AppointmentUpdatingEvent) => { + if (allowOverlapping) return; + + const updatedAppt = { ...e.appointmentData, ...e.newData } as Appointment; + if (detectConflict(updatedAppt)) { + e.cancel = true; + notify('Cannot move an appointment to a time slot that is already occupied.', 'warning', 2000); + } + }, [allowOverlapping, detectConflict]); + + const onAllowOverlappingChanged = useCallback((e: SwitchTypes.ValueChangedEvent) => { + setAllowOverlapping(e.value); + }, []); + + const onAppointmentFormOpening = useCallback((e: SchedulerTypes.AppointmentFormOpeningEvent) => { + const { form } = e; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const items = form.option('items') as any[]; + if (!items.some((item: any) => item.name === 'conflictInformer')) { + form.option('items', [ + { + name: 'conflictInformer', + itemType: 'simple', + label: { visible: false }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + template: (_: unknown, element: any) => { + const div = document.createElement('div'); + div.className = 'conflict-informer'; + div.textContent = 'This time slot conflicts with another appointment.'; + div.style.display = 'none'; + (element[0] ?? element).appendChild(div); + }, + }, + ...items, + ]); + } + }, []); + + const editing = useMemo(() => ({ + form: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + customizeItem(item: any) { + if (item.name === 'endDateEditor') { + const alreadyAdded = (item.validationRules ?? []).some( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (r: any) => r.type === 'custom' && r.message === 'This time slot conflicts with another appointment.', + ); + if (alreadyAdded) return; + item.validationRules = [ + ...(item.validationRules ?? []), + { + type: 'custom', + message: 'This time slot conflicts with another appointment.', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validationCallback({ validator }: any): boolean { + if (allowOverlappingRef.current) return true; + const formEl = validator.$element().closest('.dx-form')[0]; + const formInstance = dxForm.getInstance(formEl); + if (!formInstance) return true; + const formData = formInstance.option('formData') as Appointment; + const hasConflict = detectConflict(formData); + const informerEl = formEl.querySelector('.conflict-informer') as HTMLElement | null; + if (informerEl) { + informerEl.style.display = hasConflict ? '' : 'none'; + } + return !hasConflict; + }, + }, + ]; + } + }, + }, + }), [detectConflict]); + + return ( + + +
+
Options
+
+ Allow Appointment Overlapping + +
+
+
+ ); +}; + +export default App; diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/data.ts b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/data.ts new file mode 100644 index 000000000000..e878da52431a --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/data.ts @@ -0,0 +1,33 @@ +export interface Appointment { + id: number; + text: string; + startDate: Date; + endDate: Date; + projectId: number; +} + +export const projects = [ + { id: 1, color: '#A7E3A5' }, + { id: 2, color: '#CFE4FA' }, + { id: 3, color: '#F9E2AE' }, + { id: 4, color: '#F1BBBC' }, +]; + +export const data: Appointment[] = [ + // Mon Feb 9 + { id: 1, text: 'Website Re-Design Plan', startDate: new Date(2026, 1, 9, 9, 30), endDate: new Date(2026, 1, 9, 11, 30), projectId: 2 }, + { id: 2, text: 'Install New Router in Dev Room', startDate: new Date(2026, 1, 9, 14, 30), endDate: new Date(2026, 1, 9, 15, 30), projectId: 3 }, + // Tue Feb 10 + { id: 3, text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2026, 1, 10, 10, 0), endDate: new Date(2026, 1, 10, 11, 0), projectId: 1 }, + { id: 4, text: 'Final Budget Review', startDate: new Date(2026, 1, 10, 12, 0), endDate: new Date(2026, 1, 10, 13, 35), projectId: 1 }, + // Wed Feb 11 + { id: 5, text: 'Install New Database', startDate: new Date(2026, 1, 11, 9, 45), endDate: new Date(2026, 1, 11, 11, 15), projectId: 4 }, + { id: 6, text: 'Approve New Online Marketing Strategy', startDate: new Date(2026, 1, 11, 12, 0), endDate: new Date(2026, 1, 11, 14, 0), projectId: 2 }, + // Thu Feb 12 + { id: 7, text: 'Prepare 2021 Marketing Plan', startDate: new Date(2026, 1, 12, 11, 0), endDate: new Date(2026, 1, 12, 13, 30), projectId: 3 }, + { id: 8, text: 'Brochure Design Review', startDate: new Date(2026, 1, 12, 14, 0), endDate: new Date(2026, 1, 12, 15, 30), projectId: 2 }, + // Fri Feb 13 + { id: 9, text: 'Create Icons for Website', startDate: new Date(2026, 1, 13, 10, 0), endDate: new Date(2026, 1, 13, 11, 30), projectId: 1 }, + { id: 10, text: 'Launch New Website', startDate: new Date(2026, 1, 13, 12, 20), endDate: new Date(2026, 1, 13, 14, 0), projectId: 4 }, + { id: 11, text: 'Upgrade Server Hardware', startDate: new Date(2026, 1, 13, 14, 30), endDate: new Date(2026, 1, 13, 16, 0), projectId: 2 }, +]; diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.html b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.html new file mode 100644 index 000000000000..ee451f8288ff --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.html @@ -0,0 +1,24 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.tsx b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.tsx new file mode 100644 index 000000000000..8acbec4b6179 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/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/AppointmentOverlapping/React/styles.css b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/styles.css new file mode 100644 index 000000000000..dd8de37ce0f6 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/styles.css @@ -0,0 +1,24 @@ +.conflict-informer { + background-color: #fceae8; + color: #c50f1f; + padding: 8px 12px; + border-radius: 4px; +} + +.options { + padding: 20px; + background-color: rgba(191, 191, 191, 0.15); + margin-top: 20px; +} + +.caption { + font-size: 18px; + font-weight: 500; +} + +.option { + margin-top: 10px; + display: flex; + align-items: center; + gap: 8px; +} diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/App.js b/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/App.js new file mode 100644 index 000000000000..dff3de2f6440 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/App.js @@ -0,0 +1,165 @@ +import React, { + useCallback, useEffect, useMemo, useRef, useState, +} from 'react'; +import Scheduler from 'devextreme-react/scheduler'; +import Switch from 'devextreme-react/switch'; +import notify from 'devextreme/ui/notify'; +import dxForm from 'devextreme/ui/form'; +import { data, projects } from './data.js'; + +const currentDate = new Date(2026, 1, 10); +const views = ['day', 'week']; +const resources = [{ + fieldExpr: 'projectId', + dataSource: projects, + valueExpr: 'id', + colorExpr: 'color', +}]; + +function isOverlapping(a, b) { + return a.startDate < b.endDate && a.endDate > b.startDate; +} + +const App = () => { + const schedulerRef = useRef(null); + const [allowOverlapping, setAllowOverlapping] = useState(false); + const allowOverlappingRef = useRef(allowOverlapping); + + useEffect(() => { + allowOverlappingRef.current = allowOverlapping; + }, [allowOverlapping]); + + const detectConflict = useCallback((newAppt) => { + const scheduler = schedulerRef.current?.instance?.(); + if (!scheduler) return false; + + const allItems = scheduler.getDataSource().items(); + + const existingOccurrences = scheduler + .getOccurrences(newAppt.startDate, newAppt.endDate, allItems) + .filter((occ) => occ.appointmentData.id !== newAppt.id); + + const expandEnd = new Date(newAppt.endDate); + expandEnd.setDate(expandEnd.getDate() + 14); + + const newOccurrences = scheduler.getOccurrences( + newAppt.startDate, + expandEnd, + [newAppt], + ); + + return newOccurrences.some((newOcc) => + existingOccurrences.some((existingOcc) => isOverlapping(newOcc, existingOcc)), + ); + }, []); + + const onAppointmentAdding = useCallback((e) => { + if (allowOverlapping) return; + + if (detectConflict(e.appointmentData)) { + e.cancel = true; + notify('Cannot create an appointment that overlaps with an existing one.', 'warning', 2000); + } + }, [allowOverlapping, detectConflict]); + + const onAppointmentUpdating = useCallback((e) => { + if (allowOverlapping) return; + + const updatedAppt = { ...e.appointmentData, ...e.newData }; + if (detectConflict(updatedAppt)) { + e.cancel = true; + notify('Cannot move an appointment to a time slot that is already occupied.', 'warning', 2000); + } + }, [allowOverlapping, detectConflict]); + + const onAllowOverlappingChanged = useCallback((e) => { + setAllowOverlapping(e.value); + }, []); + + const onAppointmentFormOpening = useCallback((e) => { + const { form } = e; + const items = form.option('items'); + if (!items.some((item) => item.name === 'conflictInformer')) { + form.option('items', [ + { + name: 'conflictInformer', + itemType: 'simple', + label: { visible: false }, + template: (_, element) => { + const div = document.createElement('div'); + div.className = 'conflict-informer'; + div.textContent = 'This time slot conflicts with another appointment.'; + div.style.display = 'none'; + (element[0] ?? element).appendChild(div); + }, + }, + ...items, + ]); + } + }, []); + + const editing = useMemo(() => ({ + form: { + customizeItem(item) { + if (item.name === 'endDateEditor') { + const alreadyAdded = (item.validationRules ?? []).some( + (r) => r.type === 'custom' && r.message === 'This time slot conflicts with another appointment.', + ); + if (alreadyAdded) return; + item.validationRules = [ + ...(item.validationRules ?? []), + { + type: 'custom', + message: 'This time slot conflicts with another appointment.', + validationCallback({ validator }) { + if (allowOverlappingRef.current) return true; + const formEl = validator.$element().closest('.dx-form')[0]; + const formInstance = dxForm.getInstance(formEl); + if (!formInstance) return true; + const formData = formInstance.option('formData'); + const hasConflict = detectConflict(formData); + const informerEl = formEl.querySelector('.conflict-informer'); + if (informerEl) { + informerEl.style.display = hasConflict ? '' : 'none'; + } + return !hasConflict; + }, + }, + ]; + } + }, + }, + }), [detectConflict]); + + return ( + + +
+
Options
+
+ Allow Appointment Overlapping + +
+
+
+ ); +}; + +export default App; diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/data.js b/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/data.js new file mode 100644 index 000000000000..6dd0ed9210e3 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/data.js @@ -0,0 +1,25 @@ +export const projects = [ + { id: 1, color: '#A7E3A5' }, + { id: 2, color: '#CFE4FA' }, + { id: 3, color: '#F9E2AE' }, + { id: 4, color: '#F1BBBC' }, +]; + +export const data = [ + // Mon Feb 9 + { id: 1, text: 'Website Re-Design Plan', startDate: new Date(2026, 1, 9, 9, 30), endDate: new Date(2026, 1, 9, 11, 30), projectId: 2 }, + { id: 2, text: 'Install New Router in Dev Room', startDate: new Date(2026, 1, 9, 14, 30), endDate: new Date(2026, 1, 9, 15, 30), projectId: 3 }, + // Tue Feb 10 + { id: 3, text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2026, 1, 10, 10, 0), endDate: new Date(2026, 1, 10, 11, 0), projectId: 1 }, + { id: 4, text: 'Final Budget Review', startDate: new Date(2026, 1, 10, 12, 0), endDate: new Date(2026, 1, 10, 13, 35), projectId: 1 }, + // Wed Feb 11 + { id: 5, text: 'Install New Database', startDate: new Date(2026, 1, 11, 9, 45), endDate: new Date(2026, 1, 11, 11, 15), projectId: 4 }, + { id: 6, text: 'Approve New Online Marketing Strategy', startDate: new Date(2026, 1, 11, 12, 0), endDate: new Date(2026, 1, 11, 14, 0), projectId: 2 }, + // Thu Feb 12 + { id: 7, text: 'Prepare 2021 Marketing Plan', startDate: new Date(2026, 1, 12, 11, 0), endDate: new Date(2026, 1, 12, 13, 30), projectId: 3 }, + { id: 8, text: 'Brochure Design Review', startDate: new Date(2026, 1, 12, 14, 0), endDate: new Date(2026, 1, 12, 15, 30), projectId: 2 }, + // Fri Feb 13 + { id: 9, text: 'Create Icons for Website', startDate: new Date(2026, 1, 13, 10, 0), endDate: new Date(2026, 1, 13, 11, 30), projectId: 1 }, + { id: 10, text: 'Launch New Website', startDate: new Date(2026, 1, 13, 12, 20), endDate: new Date(2026, 1, 13, 14, 0), projectId: 4 }, + { id: 11, text: 'Upgrade Server Hardware', startDate: new Date(2026, 1, 13, 14, 30), endDate: new Date(2026, 1, 13, 16, 0), projectId: 2 }, +]; diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.html b/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.html new file mode 100644 index 000000000000..7b3238927b60 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.html @@ -0,0 +1,24 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.js b/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.js new file mode 100644 index 000000000000..b853e0be8242 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/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/AppointmentOverlapping/ReactJs/styles.css b/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/styles.css new file mode 100644 index 000000000000..dd8de37ce0f6 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/styles.css @@ -0,0 +1,24 @@ +.conflict-informer { + background-color: #fceae8; + color: #c50f1f; + padding: 8px 12px; + border-radius: 4px; +} + +.options { + padding: 20px; + background-color: rgba(191, 191, 191, 0.15); + margin-top: 20px; +} + +.caption { + font-size: 18px; + font-weight: 500; +} + +.option { + margin-top: 10px; + display: flex; + align-items: center; + gap: 8px; +} diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/App.vue b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/App.vue new file mode 100644 index 000000000000..e13c5a630cb4 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/App.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/data.ts b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/data.ts new file mode 100644 index 000000000000..e878da52431a --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/data.ts @@ -0,0 +1,33 @@ +export interface Appointment { + id: number; + text: string; + startDate: Date; + endDate: Date; + projectId: number; +} + +export const projects = [ + { id: 1, color: '#A7E3A5' }, + { id: 2, color: '#CFE4FA' }, + { id: 3, color: '#F9E2AE' }, + { id: 4, color: '#F1BBBC' }, +]; + +export const data: Appointment[] = [ + // Mon Feb 9 + { id: 1, text: 'Website Re-Design Plan', startDate: new Date(2026, 1, 9, 9, 30), endDate: new Date(2026, 1, 9, 11, 30), projectId: 2 }, + { id: 2, text: 'Install New Router in Dev Room', startDate: new Date(2026, 1, 9, 14, 30), endDate: new Date(2026, 1, 9, 15, 30), projectId: 3 }, + // Tue Feb 10 + { id: 3, text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2026, 1, 10, 10, 0), endDate: new Date(2026, 1, 10, 11, 0), projectId: 1 }, + { id: 4, text: 'Final Budget Review', startDate: new Date(2026, 1, 10, 12, 0), endDate: new Date(2026, 1, 10, 13, 35), projectId: 1 }, + // Wed Feb 11 + { id: 5, text: 'Install New Database', startDate: new Date(2026, 1, 11, 9, 45), endDate: new Date(2026, 1, 11, 11, 15), projectId: 4 }, + { id: 6, text: 'Approve New Online Marketing Strategy', startDate: new Date(2026, 1, 11, 12, 0), endDate: new Date(2026, 1, 11, 14, 0), projectId: 2 }, + // Thu Feb 12 + { id: 7, text: 'Prepare 2021 Marketing Plan', startDate: new Date(2026, 1, 12, 11, 0), endDate: new Date(2026, 1, 12, 13, 30), projectId: 3 }, + { id: 8, text: 'Brochure Design Review', startDate: new Date(2026, 1, 12, 14, 0), endDate: new Date(2026, 1, 12, 15, 30), projectId: 2 }, + // Fri Feb 13 + { id: 9, text: 'Create Icons for Website', startDate: new Date(2026, 1, 13, 10, 0), endDate: new Date(2026, 1, 13, 11, 30), projectId: 1 }, + { id: 10, text: 'Launch New Website', startDate: new Date(2026, 1, 13, 12, 20), endDate: new Date(2026, 1, 13, 14, 0), projectId: 4 }, + { id: 11, text: 'Upgrade Server Hardware', startDate: new Date(2026, 1, 13, 14, 30), endDate: new Date(2026, 1, 13, 16, 0), projectId: 2 }, +]; diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.html b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.html new file mode 100644 index 000000000000..c3ac7d2ffbef --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.html @@ -0,0 +1,29 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.ts b/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.ts new file mode 100644 index 000000000000..684d04215d72 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/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/AppointmentOverlapping/jQuery/data.js b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/data.js new file mode 100644 index 000000000000..5685975ea3f4 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/data.js @@ -0,0 +1,25 @@ +const projects = [ + { id: 1, color: '#A7E3A5' }, + { id: 2, color: '#CFE4FA' }, + { id: 3, color: '#F9E2AE' }, + { id: 4, color: '#F1BBBC' }, +]; + +const data = [ + // Mon Feb 9 + { id: 1, text: 'Website Re-Design Plan', startDate: new Date(2026, 1, 9, 9, 30), endDate: new Date(2026, 1, 9, 11, 30), projectId: 2 }, + { id: 2, text: 'Install New Router in Dev Room', startDate: new Date(2026, 1, 9, 14, 30), endDate: new Date(2026, 1, 9, 15, 30), projectId: 3 }, + // Tue Feb 10 + { id: 3, text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2026, 1, 10, 10, 0), endDate: new Date(2026, 1, 10, 11, 0), projectId: 1 }, + { id: 4, text: 'Final Budget Review', startDate: new Date(2026, 1, 10, 12, 0), endDate: new Date(2026, 1, 10, 13, 35), projectId: 1 }, + // Wed Feb 11 + { id: 5, text: 'Install New Database', startDate: new Date(2026, 1, 11, 9, 45), endDate: new Date(2026, 1, 11, 11, 15), projectId: 4 }, + { id: 6, text: 'Approve New Online Marketing Strategy', startDate: new Date(2026, 1, 11, 12, 0), endDate: new Date(2026, 1, 11, 14, 0), projectId: 2 }, + // Thu Feb 12 + { id: 7, text: 'Prepare 2021 Marketing Plan', startDate: new Date(2026, 1, 12, 11, 0), endDate: new Date(2026, 1, 12, 13, 30), projectId: 3 }, + { id: 8, text: 'Brochure Design Review', startDate: new Date(2026, 1, 12, 14, 0), endDate: new Date(2026, 1, 12, 15, 30), projectId: 2 }, + // Fri Feb 13 + { id: 9, text: 'Create Icons for Website', startDate: new Date(2026, 1, 13, 10, 0), endDate: new Date(2026, 1, 13, 11, 30), projectId: 1 }, + { id: 10, text: 'Launch New Website', startDate: new Date(2026, 1, 13, 12, 20), endDate: new Date(2026, 1, 13, 14, 0), projectId: 4 }, + { id: 11, text: 'Upgrade Server Hardware', startDate: new Date(2026, 1, 13, 14, 30), endDate: new Date(2026, 1, 13, 16, 0), projectId: 2 }, +]; diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html new file mode 100644 index 000000000000..fe9843136f3f --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html @@ -0,0 +1,28 @@ + + + + DevExtreme Demo + + + + + + + + + + + + +
+
+
+
Options
+
+ Allow Appointment Overlapping +
+
+
+
+ + diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js new file mode 100644 index 000000000000..b9572417d4b7 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js @@ -0,0 +1,129 @@ +$(() => { + let allowOverlapping = false; + + function isOverlapping(a, b) { + return a.startDate < b.endDate && a.endDate > b.startDate; + } + + function detectConflict(newAppt) { + const allItems = scheduler.getDataSource().items(); + + const existingOccurrences = scheduler + .getOccurrences(new Date(newAppt.startDate), new Date(newAppt.endDate), allItems) + .filter((occ) => occ.appointmentData.id !== newAppt.id); + + const expandEnd = new Date(newAppt.endDate); + expandEnd.setDate(expandEnd.getDate() + 14); + + const newOccurrences = scheduler.getOccurrences( + new Date(newAppt.startDate), + expandEnd, + [newAppt], + ); + + return newOccurrences.some((newOcc) => + existingOccurrences.some((existingOcc) => isOverlapping(newOcc, existingOcc)), + ); + } + + const scheduler = $('#scheduler').dxScheduler({ + dataSource: data, + views: ['day', 'week'], + currentView: 'week', + currentDate: new Date(2026, 1, 10), + startDayHour: 9, + endDayHour: 19, + height: 600, + resources: [{ + fieldExpr: 'projectId', + dataSource: projects, + valueExpr: 'id', + colorExpr: 'color', + }], + editing: { + form: { + customizeItem(item) { + if (item.name === 'endDateEditor') { + const alreadyAdded = (item.validationRules ?? []).some( + (r) => r.type === 'custom' && r.message === 'This time slot conflicts with another appointment.', + ); + if (alreadyAdded) return; + item.validationRules = [ + ...(item.validationRules ?? []), + { + type: 'custom', + message: 'This time slot conflicts with another appointment.', + validationCallback({ validator }) { + if (allowOverlapping) return true; + const formEl = validator.$element().closest('.dx-form')[0]; + const formInstance = DevExpress.ui.dxForm.getInstance(formEl); + if (!formInstance) return true; + const formData = formInstance.option('formData'); + const hasConflict = detectConflict(formData); + const informerEl = formEl.querySelector('.conflict-informer'); + if (informerEl) { + informerEl.style.display = hasConflict ? '' : 'none'; + } + return !hasConflict; + }, + }, + ]; + } + }, + }, + }, + onAppointmentFormOpening(e) { + const { form } = e; + const items = form.option('items'); + if (!items.some((item) => item.name === 'conflictInformer')) { + form.option('items', [ + { + name: 'conflictInformer', + itemType: 'simple', + label: { visible: false }, + template(_, element) { + const div = document.createElement('div'); + div.className = 'conflict-informer'; + div.textContent = 'This time slot conflicts with another appointment.'; + div.style.display = 'none'; + (element[0] ?? element).appendChild(div); + }, + }, + ...items, + ]); + } + }, + onAppointmentAdding(e) { + if (allowOverlapping) return; + + if (detectConflict(e.appointmentData)) { + e.cancel = true; + DevExpress.ui.notify( + 'Cannot create an appointment that overlaps with an existing one.', + 'warning', + 2000, + ); + } + }, + onAppointmentUpdating(e) { + if (allowOverlapping) return; + + const updatedAppt = { ...e.appointmentData, ...e.newData }; + if (detectConflict(updatedAppt)) { + e.cancel = true; + DevExpress.ui.notify( + 'Cannot move an appointment to a time slot that is already occupied.', + 'warning', + 2000, + ); + } + }, + }).dxScheduler('instance'); + + $('#allow-overlapping').dxSwitch({ + value: allowOverlapping, + onValueChanged(e) { + allowOverlapping = e.value; + }, + }); +}); diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css new file mode 100644 index 000000000000..dd8de37ce0f6 --- /dev/null +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css @@ -0,0 +1,24 @@ +.conflict-informer { + background-color: #fceae8; + color: #c50f1f; + padding: 8px 12px; + border-radius: 4px; +} + +.options { + padding: 20px; + background-color: rgba(191, 191, 191, 0.15); + margin-top: 20px; +} + +.caption { + font-size: 18px; + font-weight: 500; +} + +.option { + margin-top: 10px; + display: flex; + align-items: center; + gap: 8px; +} diff --git a/apps/demos/menuMeta.json b/apps/demos/menuMeta.json index 2df5e0acaae5..299e5470e613 100644 --- a/apps/demos/menuMeta.json +++ b/apps/demos/menuMeta.json @@ -3685,6 +3685,12 @@ "/Models/SampleData/Appointments.cs" ], "DemoType": "Web" + }, + { + "Title": "Appointment Overlapping", + "Name": "AppointmentOverlapping", + "Widget": "Scheduler", + "DemoType": "Web" } ] }, From 54de1b4281a5d03afb7c61e8943f51163d597456 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Fri, 27 Feb 2026 14:06:12 +0800 Subject: [PATCH 02/12] rewrite jquery demo --- .../AppointmentOverlapping/jQuery/data.js | 103 ++++++-- .../AppointmentOverlapping/jQuery/index.html | 9 +- .../AppointmentOverlapping/jQuery/index.js | 239 +++++++++++------- .../AppointmentOverlapping/jQuery/styles.css | 28 +- 4 files changed, 235 insertions(+), 144 deletions(-) diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/data.js b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/data.js index 5685975ea3f4..65546ac13a93 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/data.js +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/data.js @@ -1,25 +1,86 @@ -const projects = [ - { id: 1, color: '#A7E3A5' }, - { id: 2, color: '#CFE4FA' }, - { id: 3, color: '#F9E2AE' }, - { id: 4, color: '#F1BBBC' }, +const assignees = [ + { id: 1, text: 'Samantha Bright', color: '#A7E3A5' }, + { id: 2, text: 'John Heart', color: '#CFE4FA' }, + { id: 3, text: 'Todd Hoffman', color: '#F9E2AE' }, + { id: 4, text: 'Sandra Johnson', color: '#F1BBBC' }, ]; const data = [ - // Mon Feb 9 - { id: 1, text: 'Website Re-Design Plan', startDate: new Date(2026, 1, 9, 9, 30), endDate: new Date(2026, 1, 9, 11, 30), projectId: 2 }, - { id: 2, text: 'Install New Router in Dev Room', startDate: new Date(2026, 1, 9, 14, 30), endDate: new Date(2026, 1, 9, 15, 30), projectId: 3 }, - // Tue Feb 10 - { id: 3, text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2026, 1, 10, 10, 0), endDate: new Date(2026, 1, 10, 11, 0), projectId: 1 }, - { id: 4, text: 'Final Budget Review', startDate: new Date(2026, 1, 10, 12, 0), endDate: new Date(2026, 1, 10, 13, 35), projectId: 1 }, - // Wed Feb 11 - { id: 5, text: 'Install New Database', startDate: new Date(2026, 1, 11, 9, 45), endDate: new Date(2026, 1, 11, 11, 15), projectId: 4 }, - { id: 6, text: 'Approve New Online Marketing Strategy', startDate: new Date(2026, 1, 11, 12, 0), endDate: new Date(2026, 1, 11, 14, 0), projectId: 2 }, - // Thu Feb 12 - { id: 7, text: 'Prepare 2021 Marketing Plan', startDate: new Date(2026, 1, 12, 11, 0), endDate: new Date(2026, 1, 12, 13, 30), projectId: 3 }, - { id: 8, text: 'Brochure Design Review', startDate: new Date(2026, 1, 12, 14, 0), endDate: new Date(2026, 1, 12, 15, 30), projectId: 2 }, - // Fri Feb 13 - { id: 9, text: 'Create Icons for Website', startDate: new Date(2026, 1, 13, 10, 0), endDate: new Date(2026, 1, 13, 11, 30), projectId: 1 }, - { id: 10, text: 'Launch New Website', startDate: new Date(2026, 1, 13, 12, 20), endDate: new Date(2026, 1, 13, 14, 0), projectId: 4 }, - { id: 11, text: 'Upgrade Server Hardware', startDate: new Date(2026, 1, 13, 14, 30), endDate: new Date(2026, 1, 13, 16, 0), projectId: 2 }, + { + id: 1, + text: 'Website Re-Design Plan', + startDate: new Date(2026, 1, 9, 9, 30), + endDate: new Date(2026, 1, 9, 11, 30), + assigneeId: [2], + }, + { + id: 2, + text: 'Install New Router in Dev Room', + startDate: new Date(2026, 1, 9, 14, 30), + endDate: new Date(2026, 1, 9, 15, 30), + assigneeId: [3], + }, + { + id: 3, + text: 'Approve Personal Computer Upgrade Plan', + startDate: new Date(2026, 1, 10, 10, 0), + endDate: new Date(2026, 1, 10, 11, 0), + assigneeId: [1], + }, + { + id: 4, + text: 'Final Budget Review', + startDate: new Date(2026, 1, 10, 12, 0), + endDate: new Date(2026, 1, 10, 13, 35), + assigneeId: [1], + }, + { + id: 5, + text: 'Install New Database', + startDate: new Date(2026, 1, 11, 9, 45), + endDate: new Date(2026, 1, 11, 11, 15), + assigneeId: [4], + }, + { + id: 6, + text: 'Approve New Online Marketing Strategy', + startDate: new Date(2026, 1, 11, 12, 0), + endDate: new Date(2026, 1, 11, 14, 0), + assigneeId: [2], + }, + { + id: 7, + text: 'Prepare 2021 Marketing Plan', + startDate: new Date(2026, 1, 12, 11, 0), + endDate: new Date(2026, 1, 12, 13, 30), + assigneeId: [3], + }, + { + id: 8, + text: 'Brochure Design Review', + startDate: new Date(2026, 1, 12, 14, 0), + endDate: new Date(2026, 1, 12, 15, 30), + assigneeId: [2], + }, + { + id: 9, + text: 'Create Icons for Website', + startDate: new Date(2026, 1, 13, 10, 0), + endDate: new Date(2026, 1, 13, 11, 30), + assigneeId: [1], + }, + { + id: 10, + text: 'Launch New Website', + startDate: new Date(2026, 1, 13, 12, 20), + endDate: new Date(2026, 1, 13, 14, 0), + assigneeId: [4], + }, + { + id: 11, + text: 'Upgrade Server Hardware', + startDate: new Date(2026, 1, 13, 14, 30), + endDate: new Date(2026, 1, 13, 16, 0), + assigneeId: [2], + }, ]; diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html index fe9843136f3f..c9f40a2fe01a 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html @@ -6,7 +6,7 @@ - + @@ -16,13 +16,6 @@
-
-
Options
-
- Allow Appointment Overlapping -
-
-
diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js index b9572417d4b7..0464806a8a10 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js @@ -1,30 +1,7 @@ $(() => { - let allowOverlapping = false; - - function isOverlapping(a, b) { - return a.startDate < b.endDate && a.endDate > b.startDate; - } - - function detectConflict(newAppt) { - const allItems = scheduler.getDataSource().items(); - - const existingOccurrences = scheduler - .getOccurrences(new Date(newAppt.startDate), new Date(newAppt.endDate), allItems) - .filter((occ) => occ.appointmentData.id !== newAppt.id); - - const expandEnd = new Date(newAppt.endDate); - expandEnd.setDate(expandEnd.getDate() + 14); - - const newOccurrences = scheduler.getOccurrences( - new Date(newAppt.startDate), - expandEnd, - [newAppt], - ); - - return newOccurrences.some((newOcc) => - existingOccurrences.some((existingOcc) => isOverlapping(newOcc, existingOcc)), - ); - } + let popup; + let form; + let shouldShowValidationError = false; const scheduler = $('#scheduler').dxScheduler({ dataSource: data, @@ -34,96 +11,164 @@ $(() => { startDayHour: 9, endDayHour: 19, height: 600, + showAllDayPanel: false, resources: [{ - fieldExpr: 'projectId', - dataSource: projects, + fieldExpr: 'assigneeId', + dataSource: assignees, valueExpr: 'id', colorExpr: 'color', + icon: 'user', + allowMultiple: true, }], editing: { + popup: { + onInitialized: (e) => { + popup = e.component; + }, + }, form: { - customizeItem(item) { - if (item.name === 'endDateEditor') { - const alreadyAdded = (item.validationRules ?? []).some( - (r) => r.type === 'custom' && r.message === 'This time slot conflicts with another appointment.', - ); - if (alreadyAdded) return; + onInitialized: (e) => { + form = e.component; + + const defaultFieldDataChangedHandler = form.option('onFieldDataChanged'); + + form.option('onFieldDataChanged', (e) => { + defaultFieldDataChangedHandler?.(e); + + if (shouldShowValidationError && (e.dataField === 'startDate' || e.dataField === 'endDate')) { + shouldShowValidationError = false; + form.itemOption('mainGroup.conflictInformer', { visible: false }); + form.validate(); + } + }); + }, + labelMode: 'hidden', + items: [ + { + name: 'mainGroup', + items: [ + { + name: 'conflictInformer', + visible: false, + template: () => $('
').dxInformer({ + text: 'This time slot conflicts with another appointment.', + }), + }, + 'subjectGroup', + 'dateGroup', + 'repeatGroup', + { + name: 'assigneeIdGroup', + items: [ + 'assigneeIdIcon', + { + name: 'assigneeId', + isRequired: true, + editorOptions: { + onValueChanged: (e) => { + if (e.value.length > 1) { + e.component.option('value', [e.value[e.value.length - 1]]); + } + }, + tagTemplate: (tagData) => $('
') + .css('background-color', tagData.color) + .css('border-color', tagData.color) + .addClass('dx-tag-content') + .append( + $('').text(tagData.text), + $('
').addClass('dx-tag-remove-button'), + ), + }, + }, + ], + }, + ], + }, + 'recurrenceGroup', + ], + customizeItem: (item) => { + if (item.name === 'allDayEditor' || item.name === 'recurrenceEndEditor') { + item.label.visible = true; + } else if (item.name === 'subjectEditor') { + item.editorOptions.placeholder = 'Add title'; + } + + if (item.name === 'startTimeEditor' || item.name === 'endTimeEditor') { item.validationRules = [ - ...(item.validationRules ?? []), + { type: 'required' }, { type: 'custom', - message: 'This time slot conflicts with another appointment.', - validationCallback({ validator }) { - if (allowOverlapping) return true; - const formEl = validator.$element().closest('.dx-form')[0]; - const formInstance = DevExpress.ui.dxForm.getInstance(formEl); - if (!formInstance) return true; - const formData = formInstance.option('formData'); - const hasConflict = detectConflict(formData); - const informerEl = formEl.querySelector('.conflict-informer'); - if (informerEl) { - informerEl.style.display = hasConflict ? '' : 'none'; - } - return !hasConflict; - }, + message: 'Time conflict', + ignoreEmptyValue: true, + reevaluate: false, + validationCallback: () => !shouldShowValidationError, }, ]; } }, }, }, - onAppointmentFormOpening(e) { - const { form } = e; - const items = form.option('items'); - if (!items.some((item) => item.name === 'conflictInformer')) { - form.option('items', [ - { - name: 'conflictInformer', - itemType: 'simple', - label: { visible: false }, - template(_, element) { - const div = document.createElement('div'); - div.className = 'conflict-informer'; - div.textContent = 'This time slot conflicts with another appointment.'; - div.style.display = 'none'; - (element[0] ?? element).appendChild(div); - }, - }, - ...items, - ]); - } - }, onAppointmentAdding(e) { - if (allowOverlapping) return; - - if (detectConflict(e.appointmentData)) { - e.cancel = true; - DevExpress.ui.notify( - 'Cannot create an appointment that overlaps with an existing one.', - 'warning', - 2000, - ); - } + alertConflictIfNeeded(e, e.appointmentData); }, onAppointmentUpdating(e) { - if (allowOverlapping) return; - - const updatedAppt = { ...e.appointmentData, ...e.newData }; - if (detectConflict(updatedAppt)) { - e.cancel = true; - DevExpress.ui.notify( - 'Cannot move an appointment to a time slot that is already occupied.', - 'warning', - 2000, - ); - } + alertConflictIfNeeded(e, e.newData); }, }).dxScheduler('instance'); - $('#allow-overlapping').dxSwitch({ - value: allowOverlapping, - onValueChanged(e) { - allowOverlapping = e.value; - }, - }); + function alertConflictIfNeeded(e, appointmentData) { + const hasConflict = detectConflict(appointmentData); + + if (!hasConflict) { + shouldShowValidationError = false; + return; + } + + e.cancel = true; + + if (popup.option('visible')) { + shouldShowValidationError = true; + form.itemOption('mainGroup.conflictInformer', { visible: true }); + form.validate(); + } else { + const dialog = DevExpress.ui.dialog.custom({ + showTitle: false, + messageHtml: 'This time slot conflicts with another appointment.', + buttons: [{ + type: 'default', + text: 'cancel', + stylingMode: 'contained', + onClick: () => { + dialog.hide(); + }, + }], + }); + dialog.show(); + } + } + + function isOverlapping(a, b) { + return a.appointmentData.assigneeId[0] === b.appointmentData.assigneeId[0] && + a.startDate < b.endDate && a.endDate > b.startDate; + } + + function detectConflict(newAppointment) { + const allAppointments = scheduler.getDataSource().items(); + const startDate = new Date(newAppointment.startDate); + const endDate = newAppointment.recurrenceRule + ? scheduler.getEndViewDate() + : new Date(newAppointment.endDate); + + const existingOccurrences = scheduler + .getOccurrences(startDate, endDate, allAppointments) + .filter((occurrence) => occurrence.appointmentData.id !== newAppointment.id); + + const newOccurrences = scheduler.getOccurrences(startDate, endDate, [newAppointment]); + + return newOccurrences.some((newOccurrence) => + existingOccurrences.some((existingOccurrence) => + isOverlapping(newOccurrence, existingOccurrence), + ), + ); + } }); diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css index dd8de37ce0f6..c76672e1d68f 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css @@ -1,24 +1,16 @@ -.conflict-informer { - background-color: #fceae8; - color: #c50f1f; - padding: 8px 12px; - border-radius: 4px; +.dx-informer-error.dx-informer-bg { + background-color: #FCEAE8; } -.options { - padding: 20px; - background-color: rgba(191, 191, 191, 0.15); - margin-top: 20px; +.dx-informer-text { + font-size: 13px; } -.caption { - font-size: 18px; - font-weight: 500; +.dx-dialog .dx-overlay-content { + width: 280px; } -.option { - margin-top: 10px; - display: flex; - align-items: center; - gap: 8px; -} +.dx-dialog .dx-toolbar-center, +.dx-dialog .dx-button { + width: 100%; +} \ No newline at end of file From 90752e8400c3ffb363b4788a2d225f59f25cfe62 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Fri, 27 Feb 2026 17:03:55 +0800 Subject: [PATCH 03/12] rewrite react demo --- .../AppointmentOverlapping/React/App.tsx | 326 ++++++++++-------- .../AppointmentOverlapping/React/data.ts | 41 ++- .../AppointmentOverlapping/React/index.html | 2 +- .../AppointmentOverlapping/React/styles.css | 37 +- .../AppointmentOverlapping/jQuery/index.html | 2 +- .../AppointmentOverlapping/jQuery/index.js | 28 +- .../AppointmentOverlapping/jQuery/styles.css | 17 +- 7 files changed, 260 insertions(+), 193 deletions(-) diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/App.tsx b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/App.tsx index bbc63c1889a0..f3960bfd0fe1 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/App.tsx +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/App.tsx @@ -1,172 +1,218 @@ import React, { - useCallback, useEffect, useMemo, useRef, useState, + useCallback, useMemo, useRef, } from 'react'; -import Scheduler, { type SchedulerRef, type SchedulerTypes } from 'devextreme-react/scheduler'; -import Switch, { type SwitchTypes } from 'devextreme-react/switch'; -import notify from 'devextreme/ui/notify'; -import dxForm from 'devextreme/ui/form'; -import { data, projects, type Appointment } from './data.ts'; +import Scheduler, { Form, Editing, Resource, Item, SchedulerTypes } from 'devextreme-react/scheduler'; +import type { FormRef, FormTypes } from 'devextreme-react/form'; +import type { PopupRef } from 'devextreme-react/popup'; +import type { TagBoxTypes } from 'devextreme-react/tag-box'; +import { custom as customDialog } from 'devextreme/ui/dialog'; +import dxScheduler from 'devextreme/ui/scheduler'; +import { data, assignees, type Appointment, Assignee } from './data.ts'; const currentDate = new Date(2026, 1, 10); const views: SchedulerTypes.ViewType[] = ['day', 'week']; -const resources = [{ - fieldExpr: 'projectId', - dataSource: projects, - valueExpr: 'id', - colorExpr: 'color', -}]; - -function isOverlapping( - a: { startDate: Date; endDate: Date }, - b: { startDate: Date; endDate: Date }, -): boolean { - return a.startDate < b.endDate && a.endDate > b.startDate; + +function isOverlapping(a: SchedulerTypes.Occurrence, b: SchedulerTypes.Occurrence): boolean { + return a.appointmentData.assigneeId[0] === b.appointmentData.assigneeId[0] && + a.startDate < b.endDate && a.endDate > b.startDate; } -const App = () => { - const schedulerRef = useRef(null); - const [allowOverlapping, setAllowOverlapping] = useState(false); - const allowOverlappingRef = useRef(allowOverlapping); +const detectConflict = (scheduler: dxScheduler, newAppointment: Appointment): boolean => { + const allAppointments = scheduler.getDataSource().items() as Appointment[]; + const startDate = new Date(newAppointment.startDate); + const endDate = newAppointment.recurrenceRule + ? scheduler.getEndViewDate() + : new Date(newAppointment.endDate); + + const existingOccurrences = scheduler + .getOccurrences(startDate, endDate, allAppointments) + .filter((occurrence) => occurrence.appointmentData.id !== newAppointment.id); + + const newOccurrences = scheduler.getOccurrences( + startDate, + endDate, + [newAppointment], + ); - useEffect(() => { - allowOverlappingRef.current = allowOverlapping; - }, [allowOverlapping]); + return newOccurrences.some((newOccurrence) => + existingOccurrences.some((existingOccurrence) => + isOverlapping(newOccurrence, existingOccurrence), + ), + ); +}; - const detectConflict = useCallback((newAppt: Appointment): boolean => { - const scheduler = schedulerRef.current?.instance?.(); - if (!scheduler) return false; +const assigneeIdEditorOptions = { + onValueChanged: (e: TagBoxTypes.ValueChangedEvent) => { + if (e.value.length > 1) { + e.component.option('value', [e.value[e.value.length - 1]]); + } + }, + tagTemplate: (itemData: Assignee, tagElement: HTMLElement) => { + const root = document.createElement('div'); + root.className = 'dx-tag-content'; + root.style.backgroundColor = itemData.color; + root.style.borderColor = itemData.color; + + const span = document.createElement('span'); + span.textContent = itemData.text; + root.appendChild(span); + + const div = document.createElement('div'); + div.className = 'dx-tag-remove-button'; + root.appendChild(div); + + tagElement.appendChild(root); + }, +}; - const allItems = scheduler.getDataSource().items() as Appointment[]; +const conflictInformerRender = () => ( +
This time slot conflicts with another appointment
+); - const existingOccurrences = scheduler - .getOccurrences(newAppt.startDate, newAppt.endDate, allItems) - .filter((occ) => (occ.appointmentData as Appointment).id !== newAppt.id); +const App = () => { + const popupRef = useRef(null); + const formRef = useRef(null); + const showConflictErrorRef = useRef(false); - const expandEnd = new Date(newAppt.endDate); - expandEnd.setDate(expandEnd.getDate() + 14); + const alertConflictIfNeeded = useCallback((e: SchedulerTypes.AppointmentAddingEvent | SchedulerTypes.AppointmentUpdatingEvent, appointmentData: Appointment) => { + const hasConflict = detectConflict(e.component, appointmentData); - const newOccurrences = scheduler.getOccurrences( - newAppt.startDate, - expandEnd, - [newAppt], - ); + if (!hasConflict) { + showConflictErrorRef.current = false; + return; + } - return newOccurrences.some((newOcc) => - existingOccurrences.some((existingOcc) => isOverlapping(newOcc, existingOcc)), - ); + e.cancel = true; + + if (popupRef.current.instance().option('visible')) { + showConflictErrorRef.current = true; + formRef.current.instance().validate(); + formRef.current.instance().option('elementAttr.class', ''); + } else { + const dialog = customDialog({ + showTitle: false, + messageHtml: 'This time slot conflicts with another appointment.', + buttons: [{ + type: 'default', + text: 'cancel', + stylingMode: 'contained', + onClick: () => { + dialog.hide(); + }, + }], + }); + dialog.show(); + } }, []); const onAppointmentAdding = useCallback((e: SchedulerTypes.AppointmentAddingEvent) => { - if (allowOverlapping) return; - - if (detectConflict(e.appointmentData as Appointment)) { - e.cancel = true; - notify('Cannot create an appointment that overlaps with an existing one.', 'warning', 2000); - } - }, [allowOverlapping, detectConflict]); + alertConflictIfNeeded(e, e.appointmentData as Appointment); + }, [alertConflictIfNeeded]); const onAppointmentUpdating = useCallback((e: SchedulerTypes.AppointmentUpdatingEvent) => { - if (allowOverlapping) return; + alertConflictIfNeeded(e, e.newData); + }, [alertConflictIfNeeded]); - const updatedAppt = { ...e.appointmentData, ...e.newData } as Appointment; - if (detectConflict(updatedAppt)) { - e.cancel = true; - notify('Cannot move an appointment to a time slot that is already occupied.', 'warning', 2000); + const popupOptions = useMemo(() => ({ + onInitialized: (e: any) => { + popupRef.current = e.component; + }, + }), []); + + const onFormInitialized = useCallback((e: FormTypes.InitializedEvent) => { + formRef.current = e.component; + + const defaultFieldDataChangedHandler = e.component.option('onFieldDataChanged'); + + e.component.option('onFieldDataChanged', (fieldEvent: any) => { + if (defaultFieldDataChangedHandler) { + defaultFieldDataChangedHandler(fieldEvent); + } + if ( + showConflictErrorRef.current && + ['startDate', 'endDate', 'assigneeId'].includes(fieldEvent.dataField) + ) { + showConflictErrorRef.current = false; + formRef.current.instance().option('elementAttr.class', 'hide-informer'); + formRef.current.instance().validate(); + } + }); + }, [formRef]); + + const customizeItem = useCallback((item: FormTypes.SimpleItem) => { + if (item.name === 'allDayEditor' || item.name === 'recurrenceEndEditor') { + item.label.visible = true; + } else if (item.name === 'subjectEditor') { + item.editorOptions = item.editorOptions || {}; + item.editorOptions.placeholder = 'Add title'; } - }, [allowOverlapping, detectConflict]); - - const onAllowOverlappingChanged = useCallback((e: SwitchTypes.ValueChangedEvent) => { - setAllowOverlapping(e.value); - }, []); - const onAppointmentFormOpening = useCallback((e: SchedulerTypes.AppointmentFormOpeningEvent) => { - const { form } = e; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const items = form.option('items') as any[]; - if (!items.some((item: any) => item.name === 'conflictInformer')) { - form.option('items', [ + if (item.name === 'startTimeEditor' || item.name === 'endTimeEditor') { + item.validationRules = [ + { type: 'required' }, { - name: 'conflictInformer', - itemType: 'simple', - label: { visible: false }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - template: (_: unknown, element: any) => { - const div = document.createElement('div'); - div.className = 'conflict-informer'; - div.textContent = 'This time slot conflicts with another appointment.'; - div.style.display = 'none'; - (element[0] ?? element).appendChild(div); - }, + type: 'custom', + message: 'Time conflict', + ignoreEmptyValue: true, + reevaluate: true, + validationCallback: () => !showConflictErrorRef.current, }, - ...items, - ]); + ]; } }, []); - const editing = useMemo(() => ({ - form: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - customizeItem(item: any) { - if (item.name === 'endDateEditor') { - const alreadyAdded = (item.validationRules ?? []).some( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (r: any) => r.type === 'custom' && r.message === 'This time slot conflicts with another appointment.', - ); - if (alreadyAdded) return; - item.validationRules = [ - ...(item.validationRules ?? []), - { - type: 'custom', - message: 'This time slot conflicts with another appointment.', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - validationCallback({ validator }: any): boolean { - if (allowOverlappingRef.current) return true; - const formEl = validator.$element().closest('.dx-form')[0]; - const formInstance = dxForm.getInstance(formEl); - if (!formInstance) return true; - const formData = formInstance.option('formData') as Appointment; - const hasConflict = detectConflict(formData); - const informerEl = formEl.querySelector('.conflict-informer') as HTMLElement | null; - if (informerEl) { - informerEl.style.display = hasConflict ? '' : 'none'; - } - return !hasConflict; - }, - }, - ]; - } - }, - }, - }), [detectConflict]); - return ( - - + -
-
Options
-
- Allow Appointment Overlapping - -
-
-
+ + +
+ + + + + + + + + + + + +
+ ); }; diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/data.ts b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/data.ts index e878da52431a..8dbc5e97cc05 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/data.ts +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/data.ts @@ -3,31 +3,38 @@ export interface Appointment { text: string; startDate: Date; endDate: Date; - projectId: number; + assigneeId: number[]; + recurrenceRule?: string; } -export const projects = [ - { id: 1, color: '#A7E3A5' }, - { id: 2, color: '#CFE4FA' }, - { id: 3, color: '#F9E2AE' }, - { id: 4, color: '#F1BBBC' }, +export interface Assignee { + id: number; + text: string; + color: string; +} + +export const assignees: Assignee[] = [ + { id: 1, text: 'Samantha Bright', color: '#A7E3A5' }, + { id: 2, text: 'John Heart', color: '#CFE4FA' }, + { id: 3, text: 'Todd Hoffman', color: '#F9E2AE' }, + { id: 4, text: 'Sandra Johnson', color: '#F1BBBC' }, ]; export const data: Appointment[] = [ // Mon Feb 9 - { id: 1, text: 'Website Re-Design Plan', startDate: new Date(2026, 1, 9, 9, 30), endDate: new Date(2026, 1, 9, 11, 30), projectId: 2 }, - { id: 2, text: 'Install New Router in Dev Room', startDate: new Date(2026, 1, 9, 14, 30), endDate: new Date(2026, 1, 9, 15, 30), projectId: 3 }, + { id: 1, text: 'Website Re-Design Plan', startDate: new Date(2026, 1, 9, 9, 30), endDate: new Date(2026, 1, 9, 11, 30), assigneeId: [2] }, + { id: 2, text: 'Install New Router in Dev Room', startDate: new Date(2026, 1, 9, 14, 30), endDate: new Date(2026, 1, 9, 15, 30), assigneeId: [3] }, // Tue Feb 10 - { id: 3, text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2026, 1, 10, 10, 0), endDate: new Date(2026, 1, 10, 11, 0), projectId: 1 }, - { id: 4, text: 'Final Budget Review', startDate: new Date(2026, 1, 10, 12, 0), endDate: new Date(2026, 1, 10, 13, 35), projectId: 1 }, + { id: 3, text: 'Approve Personal Computer Upgrade Plan', startDate: new Date(2026, 1, 10, 10, 0), endDate: new Date(2026, 1, 10, 11, 0), assigneeId: [1] }, + { id: 4, text: 'Final Budget Review', startDate: new Date(2026, 1, 10, 12, 0), endDate: new Date(2026, 1, 10, 13, 35), assigneeId: [1] }, // Wed Feb 11 - { id: 5, text: 'Install New Database', startDate: new Date(2026, 1, 11, 9, 45), endDate: new Date(2026, 1, 11, 11, 15), projectId: 4 }, - { id: 6, text: 'Approve New Online Marketing Strategy', startDate: new Date(2026, 1, 11, 12, 0), endDate: new Date(2026, 1, 11, 14, 0), projectId: 2 }, + { id: 5, text: 'Install New Database', startDate: new Date(2026, 1, 11, 9, 45), endDate: new Date(2026, 1, 11, 11, 15), assigneeId: [4] }, + { id: 6, text: 'Approve New Online Marketing Strategy', startDate: new Date(2026, 1, 11, 12, 0), endDate: new Date(2026, 1, 11, 14, 0), assigneeId: [2] }, // Thu Feb 12 - { id: 7, text: 'Prepare 2021 Marketing Plan', startDate: new Date(2026, 1, 12, 11, 0), endDate: new Date(2026, 1, 12, 13, 30), projectId: 3 }, - { id: 8, text: 'Brochure Design Review', startDate: new Date(2026, 1, 12, 14, 0), endDate: new Date(2026, 1, 12, 15, 30), projectId: 2 }, + { id: 7, text: 'Prepare 2021 Marketing Plan', startDate: new Date(2026, 1, 12, 11, 0), endDate: new Date(2026, 1, 12, 13, 30), assigneeId: [3] }, + { id: 8, text: 'Brochure Design Review', startDate: new Date(2026, 1, 12, 14, 0), endDate: new Date(2026, 1, 12, 15, 30), assigneeId: [2] }, // Fri Feb 13 - { id: 9, text: 'Create Icons for Website', startDate: new Date(2026, 1, 13, 10, 0), endDate: new Date(2026, 1, 13, 11, 30), projectId: 1 }, - { id: 10, text: 'Launch New Website', startDate: new Date(2026, 1, 13, 12, 20), endDate: new Date(2026, 1, 13, 14, 0), projectId: 4 }, - { id: 11, text: 'Upgrade Server Hardware', startDate: new Date(2026, 1, 13, 14, 30), endDate: new Date(2026, 1, 13, 16, 0), projectId: 2 }, + { id: 9, text: 'Create Icons for Website', startDate: new Date(2026, 1, 13, 10, 0), endDate: new Date(2026, 1, 13, 11, 30), assigneeId: [1] }, + { id: 10, text: 'Launch New Website', startDate: new Date(2026, 1, 13, 12, 20), endDate: new Date(2026, 1, 13, 14, 0), assigneeId: [4] }, + { id: 11, text: 'Upgrade Server Hardware', startDate: new Date(2026, 1, 13, 14, 30), endDate: new Date(2026, 1, 13, 16, 0), assigneeId: [2] }, ]; diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.html b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.html index ee451f8288ff..55590cfea1ad 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.html +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.html @@ -5,7 +5,7 @@ - + diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/styles.css b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/styles.css index dd8de37ce0f6..4119450676a8 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/styles.css +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/styles.css @@ -1,24 +1,27 @@ -.conflict-informer { - background-color: #fceae8; - color: #c50f1f; - padding: 8px 12px; - border-radius: 4px; +.dx-scheduler-appointment { + color: #242424; } -.options { - padding: 20px; - background-color: rgba(191, 191, 191, 0.15); - margin-top: 20px; +.hide-informer .dx-scheduler-form-main-group .dx-item:has(.conflict-informer) { + display: none !important; } -.caption { - font-size: 18px; - font-weight: 500; +.hide-informer .dx-scheduler-form-main-group .dx-scheduler-form-subject-group { + padding-top: 0 !important; } -.option { - margin-top: 10px; - display: flex; - align-items: center; - gap: 8px; +.conflict-informer { + background-color: #FCEAE8; + color: #C50F1F; + font-size: 13px; + padding: 8px 12px; } + +.dx-dialog .dx-overlay-content { + width: 280px; +} + +.dx-dialog .dx-toolbar-center, +.dx-dialog .dx-button { + width: 100%; +} \ No newline at end of file diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html index c9f40a2fe01a..51858c304cde 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html @@ -6,7 +6,7 @@ - + diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js index 0464806a8a10..59eaa4fd2456 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js @@ -1,7 +1,7 @@ $(() => { let popup; let form; - let shouldShowValidationError = false; + let showConflictError = false; const scheduler = $('#scheduler').dxScheduler({ dataSource: data, @@ -27,6 +27,8 @@ $(() => { }, }, form: { + labelMode: 'hidden', + elementAttr: { class: 'hide-informer' }, onInitialized: (e) => { form = e.component; @@ -35,24 +37,22 @@ $(() => { form.option('onFieldDataChanged', (e) => { defaultFieldDataChangedHandler?.(e); - if (shouldShowValidationError && (e.dataField === 'startDate' || e.dataField === 'endDate')) { - shouldShowValidationError = false; - form.itemOption('mainGroup.conflictInformer', { visible: false }); + if (showConflictError && (e.dataField === 'startDate' || e.dataField === 'endDate')) { + showConflictError = false; + form.option('elementAttr.class', 'hide-informer'); form.validate(); } }); }, - labelMode: 'hidden', items: [ { name: 'mainGroup', items: [ { name: 'conflictInformer', - visible: false, - template: () => $('
').dxInformer({ - text: 'This time slot conflicts with another appointment.', - }), + template: () => $('
') + .addClass('conflict-informer') + .text('This time slot conflicts with another appointment.'), }, 'subjectGroup', 'dateGroup', @@ -100,8 +100,8 @@ $(() => { type: 'custom', message: 'Time conflict', ignoreEmptyValue: true, - reevaluate: false, - validationCallback: () => !shouldShowValidationError, + reevaluate: true, + validationCallback: () => !showConflictError, }, ]; } @@ -120,15 +120,15 @@ $(() => { const hasConflict = detectConflict(appointmentData); if (!hasConflict) { - shouldShowValidationError = false; + showConflictError = false; return; } e.cancel = true; if (popup.option('visible')) { - shouldShowValidationError = true; - form.itemOption('mainGroup.conflictInformer', { visible: true }); + showConflictError = true; + form.option('elementAttr.class', ''); form.validate(); } else { const dialog = DevExpress.ui.dialog.custom({ diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css index c76672e1d68f..4119450676a8 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css +++ b/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css @@ -1,9 +1,20 @@ -.dx-informer-error.dx-informer-bg { - background-color: #FCEAE8; +.dx-scheduler-appointment { + color: #242424; +} + +.hide-informer .dx-scheduler-form-main-group .dx-item:has(.conflict-informer) { + display: none !important; } -.dx-informer-text { +.hide-informer .dx-scheduler-form-main-group .dx-scheduler-form-subject-group { + padding-top: 0 !important; +} + +.conflict-informer { + background-color: #FCEAE8; + color: #C50F1F; font-size: 13px; + padding: 8px 12px; } .dx-dialog .dx-overlay-content { From a3d6f71f9d3b67683b9e59d8725dc81b25afa795 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Fri, 27 Feb 2026 17:15:27 +0800 Subject: [PATCH 04/12] rename demo --- .../Angular/app/app.component.css | 0 .../Angular/app/app.component.html | 0 .../Angular/app/app.component.ts | 0 .../Angular/app/app.service.ts | 0 .../Angular/index.html | 0 .../React/App.tsx | 0 .../React/data.ts | 0 .../React/index.html | 0 .../React/index.tsx | 0 .../React/styles.css | 0 .../ReactJs/App.js | 0 .../ReactJs/data.js | 0 .../ReactJs/index.html | 0 .../ReactJs/index.js | 0 .../ReactJs/styles.css | 0 .../Vue/App.vue | 0 .../Vue/data.ts | 0 .../Vue/index.html | 0 .../Vue/index.ts | 0 .../jQuery/data.js | 0 .../jQuery/index.html | 0 .../jQuery/index.js | 2 +- .../jQuery/styles.css | 0 apps/demos/menuMeta.json | 12 ++++++------ 24 files changed, 7 insertions(+), 7 deletions(-) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/Angular/app/app.component.css (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/Angular/app/app.component.html (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/Angular/app/app.component.ts (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/Angular/app/app.service.ts (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/Angular/index.html (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/React/App.tsx (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/React/data.ts (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/React/index.html (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/React/index.tsx (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/React/styles.css (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/ReactJs/App.js (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/ReactJs/data.js (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/ReactJs/index.html (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/ReactJs/index.js (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/ReactJs/styles.css (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/Vue/App.vue (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/Vue/data.ts (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/Vue/index.html (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/Vue/index.ts (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/jQuery/data.js (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/jQuery/index.html (100%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/jQuery/index.js (99%) rename apps/demos/Demos/Scheduler/{AppointmentOverlapping => ResolveTimeConflicts}/jQuery/styles.css (100%) diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.css b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/Angular/app/app.component.css similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.css rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/Angular/app/app.component.css diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.html b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/Angular/app/app.component.html similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.html rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/Angular/app/app.component.html diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.ts b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/Angular/app/app.component.ts similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.component.ts rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/Angular/app/app.component.ts diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.service.ts b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/Angular/app/app.service.ts similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/app/app.service.ts rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/Angular/app/app.service.ts diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/index.html b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/Angular/index.html similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/Angular/index.html rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/Angular/index.html diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/App.tsx b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/App.tsx similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/React/App.tsx rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/App.tsx diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/data.ts b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/data.ts similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/React/data.ts rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/data.ts diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.html b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/index.html similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.html rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/index.html diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.tsx b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/index.tsx similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/React/index.tsx rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/index.tsx diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/React/styles.css b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/styles.css similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/React/styles.css rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/styles.css diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/App.js b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/ReactJs/App.js similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/App.js rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/ReactJs/App.js diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/data.js b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/ReactJs/data.js similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/data.js rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/ReactJs/data.js diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.html b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/ReactJs/index.html similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.html rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/ReactJs/index.html diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.js b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/ReactJs/index.js similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/index.js rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/ReactJs/index.js diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/styles.css b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/ReactJs/styles.css similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/ReactJs/styles.css rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/ReactJs/styles.css diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/App.vue b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/Vue/App.vue similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/App.vue rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/Vue/App.vue diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/data.ts b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/Vue/data.ts similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/data.ts rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/Vue/data.ts diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.html b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/Vue/index.html similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.html rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/Vue/index.html diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.ts b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/Vue/index.ts similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/Vue/index.ts rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/Vue/index.ts diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/data.js b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/jQuery/data.js similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/data.js rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/jQuery/data.js diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/jQuery/index.html similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.html rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/jQuery/index.html diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/jQuery/index.js similarity index 99% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/jQuery/index.js index 59eaa4fd2456..6eba8e7d8b2b 100644 --- a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/index.js +++ b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/jQuery/index.js @@ -126,7 +126,7 @@ $(() => { e.cancel = true; - if (popup.option('visible')) { + if (popup && popup.option('visible')) { showConflictError = true; form.option('elementAttr.class', ''); form.validate(); diff --git a/apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/jQuery/styles.css similarity index 100% rename from apps/demos/Demos/Scheduler/AppointmentOverlapping/jQuery/styles.css rename to apps/demos/Demos/Scheduler/ResolveTimeConflicts/jQuery/styles.css diff --git a/apps/demos/menuMeta.json b/apps/demos/menuMeta.json index 299e5470e613..6a80299b380d 100644 --- a/apps/demos/menuMeta.json +++ b/apps/demos/menuMeta.json @@ -3614,6 +3614,12 @@ "/Models/SampleData/PriorityResources.cs" ], "DemoType": "Web" + }, + { + "Title": "Resolve Time Conflicts", + "Name": "ResolveTimeConflicts", + "Widget": "Scheduler", + "DemoType": "Web" } ] }, @@ -3685,12 +3691,6 @@ "/Models/SampleData/Appointments.cs" ], "DemoType": "Web" - }, - { - "Title": "Appointment Overlapping", - "Name": "AppointmentOverlapping", - "Widget": "Scheduler", - "DemoType": "Web" } ] }, From 1dc2da147600b7c435cb79e9c4cca67e1518460c Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Mon, 2 Mar 2026 14:44:44 +0800 Subject: [PATCH 05/12] rewrite Vue demo --- .../ResolveTimeConflicts/React/App.tsx | 35 +- .../ResolveTimeConflicts/React/data.ts | 93 ++++- .../ResolveTimeConflicts/React/index.html | 2 +- .../ResolveTimeConflicts/Vue/App.vue | 368 ++++++++++-------- .../ResolveTimeConflicts/Vue/data.ts | 112 ++++-- .../ResolveTimeConflicts/jQuery/index.js | 6 +- 6 files changed, 397 insertions(+), 219 deletions(-) diff --git a/apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/App.tsx b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/App.tsx index f3960bfd0fe1..8b0f45fbd4e3 100644 --- a/apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/App.tsx +++ b/apps/demos/Demos/Scheduler/ResolveTimeConflicts/React/App.tsx @@ -6,6 +6,7 @@ import type { FormRef, FormTypes } from 'devextreme-react/form'; import type { PopupRef } from 'devextreme-react/popup'; import type { TagBoxTypes } from 'devextreme-react/tag-box'; import { custom as customDialog } from 'devextreme/ui/dialog'; +import { Template } from 'devextreme-react/core/template'; import dxScheduler from 'devextreme/ui/scheduler'; import { data, assignees, type Appointment, Assignee } from './data.ts'; @@ -47,24 +48,16 @@ const assigneeIdEditorOptions = { e.component.option('value', [e.value[e.value.length - 1]]); } }, - tagTemplate: (itemData: Assignee, tagElement: HTMLElement) => { - const root = document.createElement('div'); - root.className = 'dx-tag-content'; - root.style.backgroundColor = itemData.color; - root.style.borderColor = itemData.color; - - const span = document.createElement('span'); - span.textContent = itemData.text; - root.appendChild(span); - - const div = document.createElement('div'); - div.className = 'dx-tag-remove-button'; - root.appendChild(div); - - tagElement.appendChild(root); - }, + tagTemplate: 'tagTemplate', }; +const tagTemplate = (itemData: Assignee) => ( +
+ {itemData.text} +
+
+); + const conflictInformerRender = () => (
This time slot conflicts with another appointment
); @@ -84,7 +77,7 @@ const App = () => { e.cancel = true; - if (popupRef.current.instance().option('visible')) { + if (popupRef.current?.instance().option('visible')) { showConflictErrorRef.current = true; formRef.current.instance().validate(); formRef.current.instance().option('elementAttr.class', ''); @@ -122,12 +115,7 @@ const App = () => { const onFormInitialized = useCallback((e: FormTypes.InitializedEvent) => { formRef.current = e.component; - const defaultFieldDataChangedHandler = e.component.option('onFieldDataChanged'); - - e.component.option('onFieldDataChanged', (fieldEvent: any) => { - if (defaultFieldDataChangedHandler) { - defaultFieldDataChangedHandler(fieldEvent); - } + e.component.on('fieldDataChanged', (fieldEvent: any) => { if ( showConflictErrorRef.current && ['startDate', 'endDate', 'assigneeId'].includes(fieldEvent.dataField) @@ -207,6 +195,7 @@ const App = () => { isRequired={true} editorOptions={assigneeIdEditorOptions} /> +