Skip to content

Commit c9a2bb6

Browse files
Scheduler - Regenerate ReactJs demo for ResolveTimeConflicts
1 parent 06a34bc commit c9a2bb6

3 files changed

Lines changed: 221 additions & 301 deletions

File tree

Lines changed: 139 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -1,257 +1,178 @@
1-
import React, { useCallback, useMemo, useRef } from 'react';
2-
import Scheduler, {
3-
Form, Editing, Resource, Item,
4-
} from 'devextreme-react/scheduler';
1+
import React, { useCallback, useMemo, useRef, } from 'react';
2+
import Scheduler, { Form, Editing, Resource, Item, } from 'devextreme-react/scheduler';
53
import SelectBox from 'devextreme-react/select-box';
64
import { custom as customDialog } from 'devextreme/ui/dialog';
75
import { Template } from 'devextreme-react/core/template';
8-
import { data, assignees } from './data.js';
9-
6+
import { data, assignees } from './data'; /* temporary import fix */
107
const currentDate = new Date(2026, 1, 10);
118
const views = ['day', 'week', 'workWeek', 'month'];
129
const overlappingRuleItems = [
13-
{ value: 'sameResource', text: 'Allow across resources' },
14-
{ value: 'allResources', text: 'Disallow all overlaps' },
10+
{ value: 'sameResource', text: 'Allow across resources' },
11+
{ value: 'allResources', text: 'Disallow all overlaps' },
1512
];
1613
function getNextDay(date) {
17-
const next = new Date(date);
18-
next.setDate(next.getDate() + 1);
19-
return next;
14+
const next = new Date(date);
15+
next.setDate(next.getDate() + 1);
16+
return next;
2017
}
2118
function getEndDate(occurrence) {
22-
return occurrence.appointmentData.allDay ? getNextDay(occurrence.startDate) : occurrence.endDate;
19+
return occurrence.appointmentData.allDay
20+
? getNextDay(occurrence.startDate)
21+
: occurrence.endDate;
2322
}
2423
function isOverlapping(a, b, overlappingRule) {
25-
const aEnd = getEndDate(a);
26-
const bEnd = getEndDate(b);
27-
if (a.startDate >= bEnd || b.startDate >= aEnd) return false;
28-
if (overlappingRule === 'sameResource') {
29-
return a.appointmentData.assigneeId[0] === b.appointmentData.assigneeId[0];
30-
}
31-
return true;
24+
const aEnd = getEndDate(a);
25+
const bEnd = getEndDate(b);
26+
if (a.startDate >= bEnd || b.startDate >= aEnd)
27+
return false;
28+
if (overlappingRule === 'sameResource') {
29+
return a.appointmentData.assigneeId[0] === b.appointmentData.assigneeId[0];
30+
}
31+
return true;
3232
}
3333
function detectConflict(scheduler, newAppointment, overlappingRule) {
34-
const allAppointments = scheduler.getDataSource().items();
35-
const startDate = new Date(newAppointment.startDate);
36-
let endDate;
37-
if (newAppointment.recurrenceRule) {
38-
endDate = scheduler.getEndViewDate();
39-
} else if (newAppointment.allDay) {
40-
endDate = getNextDay(startDate);
41-
} else {
42-
endDate = new Date(newAppointment.endDate);
43-
}
44-
const existingOccurrences = scheduler
45-
.getOccurrences(startDate, endDate, allAppointments)
46-
.filter((occurrence) => occurrence.appointmentData.id !== newAppointment.id);
47-
const newOccurrences = scheduler.getOccurrences(startDate, endDate, [newAppointment]);
48-
return newOccurrences.some((newOccurrence) =>
49-
existingOccurrences.some((existingOccurrence) =>
50-
isOverlapping(newOccurrence, existingOccurrence, overlappingRule),
51-
),
52-
);
34+
const allAppointments = scheduler.getDataSource().items();
35+
const startDate = new Date(newAppointment.startDate);
36+
let endDate;
37+
if (newAppointment.recurrenceRule) {
38+
endDate = scheduler.getEndViewDate();
39+
}
40+
else if (newAppointment.allDay) {
41+
endDate = getNextDay(startDate);
42+
}
43+
else {
44+
endDate = new Date(newAppointment.endDate);
45+
}
46+
const existingOccurrences = scheduler
47+
.getOccurrences(startDate, endDate, allAppointments)
48+
.filter((occurrence) => occurrence.appointmentData.id !== newAppointment.id);
49+
const newOccurrences = scheduler.getOccurrences(startDate, endDate, [newAppointment]);
50+
return newOccurrences.some((newOccurrence) => existingOccurrences.some((existingOccurrence) => isOverlapping(newOccurrence, existingOccurrence, overlappingRule)));
5351
}
5452
const assigneeIdEditorOptions = {
55-
onValueChanged: (e) => {
56-
if (e.value?.length > 1) {
57-
e.component.option('value', [e.value[e.value.length - 1]]);
58-
}
59-
},
60-
tagTemplate: 'tagTemplate',
53+
onValueChanged: (e) => {
54+
if (e.value?.length > 1) {
55+
e.component.option('value', [e.value[e.value.length - 1]]);
56+
}
57+
},
58+
tagTemplate: 'tagTemplate',
6159
};
62-
const tagTemplate = (itemData) => (
63-
<div
64-
className="dx-tag-content"
65-
style={{ backgroundColor: itemData.color, borderColor: itemData.color }}
66-
>
60+
const tagTemplate = (itemData) => (<div className="dx-tag-content" style={{ backgroundColor: itemData.color, borderColor: itemData.color }}>
6761
{itemData.text}
6862
<div className="dx-tag-remove-button"></div>
69-
</div>
70-
);
71-
const conflictInformerRender = () => (
72-
<div className="conflict-informer">This time slot conflicts with another appointment.</div>
73-
);
63+
</div>);
64+
const conflictInformerRender = () => (<div className="conflict-informer">This time slot conflicts with another appointment.</div>);
7465
const App = () => {
75-
const popupRef = useRef(null);
76-
const formRef = useRef(null);
77-
const showConflictErrorRef = useRef(false);
78-
const overlappingRuleRef = useRef('sameResource');
79-
const setConflictError = useCallback((show) => {
80-
showConflictErrorRef.current = show;
81-
formRef.current?.option('elementAttr.class', show ? '' : 'hide-informer');
82-
}, []);
83-
const alertConflictIfNeeded = useCallback(
84-
(e, appointmentData) => {
85-
if (!detectConflict(e.component, appointmentData, overlappingRuleRef.current)) {
86-
setConflictError(false);
87-
return;
88-
}
89-
e.cancel = true;
90-
if (popupRef.current?.option('visible')) {
91-
setConflictError(true);
92-
formRef.current?.validate();
93-
} else {
94-
const dialog = customDialog({
95-
showTitle: false,
96-
messageHtml:
97-
'<p id="conflict-dialog">This time slot conflicts with another appointment.</p>',
98-
buttons: [
99-
{
100-
type: 'default',
101-
text: 'Close',
102-
stylingMode: 'contained',
103-
onClick: () => {
104-
dialog.hide();
105-
},
106-
},
107-
],
108-
});
109-
dialog.show();
110-
}
111-
},
112-
[setConflictError],
113-
);
114-
const onAppointmentAdding = useCallback(
115-
(e) => {
116-
alertConflictIfNeeded(e, e.appointmentData);
117-
},
118-
[alertConflictIfNeeded],
119-
);
120-
const onAppointmentUpdating = useCallback(
121-
(e) => {
122-
alertConflictIfNeeded(e, { ...e.oldData, ...e.newData });
123-
},
124-
[alertConflictIfNeeded],
125-
);
126-
const popupOptions = useMemo(
127-
() => ({
128-
onInitialized: (e) => {
129-
popupRef.current = e.component ?? null;
130-
},
131-
onHidden: () => {
132-
setConflictError(false);
133-
formRef.current?.updateData('assigneeId', []);
134-
},
135-
}),
136-
[setConflictError],
137-
);
138-
const onFormInitialized = useCallback(
139-
(e) => {
140-
if (!e.component) return;
141-
formRef.current = e.component;
142-
e.component.on('fieldDataChanged', (fieldEvent) => {
143-
if (
144-
showConflictErrorRef.current &&
145-
['startDate', 'endDate', 'assigneeId', 'recurrenceRule'].includes(
146-
fieldEvent.dataField ?? '',
147-
)
148-
) {
149-
setConflictError(false);
150-
formRef.current?.validate();
66+
const popupRef = useRef(null);
67+
const formRef = useRef(null);
68+
const showConflictErrorRef = useRef(false);
69+
const overlappingRuleRef = useRef('sameResource');
70+
const setConflictError = useCallback((show) => {
71+
showConflictErrorRef.current = show;
72+
formRef.current?.option('elementAttr.class', show ? '' : 'hide-informer');
73+
}, []);
74+
const alertConflictIfNeeded = useCallback((e, appointmentData) => {
75+
if (!detectConflict(e.component, appointmentData, overlappingRuleRef.current)) {
76+
setConflictError(false);
77+
return;
15178
}
152-
});
153-
},
154-
[setConflictError],
155-
);
156-
const customizeItem = useCallback((item) => {
157-
if (item.name === 'allDayEditor' || item.name === 'recurrenceEndEditor') {
158-
item.label = { ...item.label, visible: true };
159-
} else if (item.name === 'subjectEditor') {
160-
item.editorOptions = item.editorOptions || {};
161-
item.editorOptions.placeholder = 'Add title';
162-
}
163-
if (item.name === 'startTimeEditor' || item.name === 'endTimeEditor') {
164-
item.validationRules = [
165-
{ type: 'required' },
166-
{
167-
type: 'custom',
168-
message: 'Time conflict',
169-
ignoreEmptyValue: true,
170-
reevaluate: true,
171-
validationCallback: () => !showConflictErrorRef.current,
79+
e.cancel = true;
80+
if (popupRef.current?.option('visible')) {
81+
setConflictError(true);
82+
formRef.current?.validate();
83+
}
84+
else {
85+
const dialog = customDialog({
86+
showTitle: false,
87+
messageHtml: '<p id="conflict-dialog">This time slot conflicts with another appointment.</p>',
88+
buttons: [{
89+
type: 'default',
90+
text: 'Close',
91+
stylingMode: 'contained',
92+
onClick: () => {
93+
dialog.hide();
94+
},
95+
}],
96+
});
97+
dialog.show();
98+
}
99+
}, [setConflictError]);
100+
const onAppointmentAdding = useCallback((e) => {
101+
alertConflictIfNeeded(e, e.appointmentData);
102+
}, [alertConflictIfNeeded]);
103+
const onAppointmentUpdating = useCallback((e) => {
104+
alertConflictIfNeeded(e, { ...e.oldData, ...e.newData });
105+
}, [alertConflictIfNeeded]);
106+
const popupOptions = useMemo(() => ({
107+
onInitialized: (e) => {
108+
popupRef.current = e.component ?? null;
172109
},
173-
];
174-
}
175-
}, []);
176-
return (
177-
<>
178-
<Scheduler
179-
dataSource={data}
180-
views={views}
181-
defaultCurrentView="week"
182-
defaultCurrentDate={currentDate}
183-
startDayHour={9}
184-
endDayHour={19}
185-
height={600}
186-
showAllDayPanel={false}
187-
allDayPanelMode="hidden"
188-
onAppointmentAdding={onAppointmentAdding}
189-
onAppointmentUpdating={onAppointmentUpdating}
190-
>
191-
<Resource
192-
fieldExpr="assigneeId"
193-
dataSource={assignees}
194-
valueExpr="id"
195-
colorExpr="color"
196-
icon="user"
197-
allowMultiple={true}
198-
/>
110+
onHidden: () => {
111+
setConflictError(false);
112+
},
113+
}), [setConflictError]);
114+
const onFormInitialized = useCallback((e) => {
115+
if (!e.component)
116+
return;
117+
formRef.current = e.component;
118+
e.component.on('fieldDataChanged', (fieldEvent) => {
119+
if (showConflictErrorRef.current &&
120+
['startDate', 'endDate', 'assigneeId', 'recurrenceRule'].includes(fieldEvent.dataField ?? '')) {
121+
setConflictError(false);
122+
formRef.current?.validate();
123+
}
124+
});
125+
}, [setConflictError]);
126+
const customizeItem = useCallback((item) => {
127+
if (item.name === 'allDayEditor' || item.name === 'recurrenceEndEditor') {
128+
item.label = { ...item.label, visible: true };
129+
}
130+
else if (item.name === 'subjectEditor') {
131+
item.editorOptions = item.editorOptions || {};
132+
item.editorOptions.placeholder = 'Add title';
133+
}
134+
if (item.name === 'startTimeEditor' || item.name === 'endTimeEditor') {
135+
item.validationRules = [
136+
{ type: 'required' },
137+
{
138+
type: 'custom',
139+
message: 'Time conflict',
140+
ignoreEmptyValue: true,
141+
reevaluate: true,
142+
validationCallback: () => !showConflictErrorRef.current,
143+
},
144+
];
145+
}
146+
}, []);
147+
return (<>
148+
<Scheduler dataSource={data} views={views} defaultCurrentView="week" defaultCurrentDate={currentDate} startDayHour={9} endDayHour={19} height={600} showAllDayPanel={false} allDayPanelMode="hidden" onAppointmentAdding={onAppointmentAdding} onAppointmentUpdating={onAppointmentUpdating}>
149+
<Resource fieldExpr="assigneeId" dataSource={assignees} valueExpr="id" colorExpr="color" icon="user" allowMultiple={true}/>
199150

200151
<Editing popup={popupOptions}>
201-
<Form
202-
labelMode="hidden"
203-
onInitialized={onFormInitialized}
204-
customizeItem={customizeItem}
205-
elementAttr={{ class: 'hide-informer', id: 'form' }}
206-
>
207-
<Item
208-
name="conflictInformer"
209-
render={conflictInformerRender}
210-
/>
211-
<Item
212-
type="group"
213-
name="mainGroup"
214-
>
215-
<Item name="subjectGroup" />
216-
<Item name="dateGroup" />
217-
<Item name="repeatGroup" />
152+
<Form labelMode="hidden" onInitialized={onFormInitialized} customizeItem={customizeItem} elementAttr={{ class: 'hide-informer', id: 'form' }}>
153+
<Item name="conflictInformer" render={conflictInformerRender}/>
154+
<Item type="group" name="mainGroup">
155+
<Item name="subjectGroup"/>
156+
<Item name="dateGroup"/>
157+
<Item name="repeatGroup"/>
218158
<Item name="assigneeIdGroup">
219-
<Item name="assigneeIdIcon" />
220-
<Item
221-
name="assigneeIdEditor"
222-
isRequired={true}
223-
editorOptions={assigneeIdEditorOptions}
224-
/>
159+
<Item name="assigneeIdIcon"/>
160+
<Item name="assigneeIdEditor" isRequired={true} editorOptions={assigneeIdEditorOptions}/>
225161
</Item>
226162
</Item>
227-
<Item
228-
type="group"
229-
name="recurrenceGroup"
230-
/>
163+
<Item type="group" name="recurrenceGroup"/>
231164
</Form>
232165
</Editing>
233166

234-
<Template
235-
name="tagTemplate"
236-
render={tagTemplate}
237-
/>
167+
<Template name="tagTemplate" render={tagTemplate}/>
238168
</Scheduler>
239169

240170
<div className="options">
241171
<div className="option">
242172
<span>Overlapping Rule</span>
243-
<SelectBox
244-
items={overlappingRuleItems}
245-
valueExpr="value"
246-
displayExpr="text"
247-
defaultValue="sameResource"
248-
onValueChanged={(e) => {
249-
overlappingRuleRef.current = e.value;
250-
}}
251-
/>
173+
<SelectBox items={overlappingRuleItems} valueExpr="value" displayExpr="text" defaultValue="sameResource" onValueChanged={(e) => { overlappingRuleRef.current = e.value; }}/>
252174
</div>
253175
</div>
254-
</>
255-
);
176+
</>);
256177
};
257178
export default App;

0 commit comments

Comments
 (0)