1
1
import { useRouter } from "next/navigation" ;
2
- import { useState } from "react" ;
3
- import { Controller , useFieldArray , useForm } from "react-hook-form" ;
2
+ import { useMemo , useState } from "react" ;
3
+ import { Controller , useFieldArray , useForm , useWatch } from "react-hook-form" ;
4
4
5
5
import dayjs from "@calcom/dayjs" ;
6
6
import { DateOverrideInputDialog , DateOverrideList } from "@calcom/features/schedules" ;
7
7
import Schedule from "@calcom/features/schedules/components/Schedule" ;
8
8
import Shell from "@calcom/features/shell/Shell" ;
9
9
import { classNames } from "@calcom/lib" ;
10
10
import { availabilityAsString } from "@calcom/lib/availability" ;
11
+ import { withErrorFromUnknown } from "@calcom/lib/getClientErrorFromUnknown" ;
11
12
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams" ;
12
13
import { useLocale } from "@calcom/lib/hooks/useLocale" ;
13
14
import { HttpError } from "@calcom/lib/http-error" ;
@@ -29,25 +30,43 @@ import {
29
30
Tooltip ,
30
31
VerticalDivider ,
31
32
} from "@calcom/ui" ;
32
- import { Info , MoreVertical , ArrowLeft , Plus , Trash } from "@calcom/ui/components/icon" ;
33
+ import { ArrowLeft , Info , MoreVertical , Plus , Trash } from "@calcom/ui/components/icon" ;
33
34
34
35
import PageWrapper from "@components/PageWrapper" ;
35
36
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader" ;
36
37
import EditableHeading from "@components/ui/EditableHeading" ;
37
38
38
- type AvailabilityFormValues = {
39
+ export type AvailabilityFormValues = {
39
40
name : string ;
40
41
schedule : ScheduleType ;
41
42
dateOverrides : { ranges : TimeRange [ ] } [ ] ;
42
43
timeZone : string ;
43
44
isDefault : boolean ;
44
45
} ;
45
46
47
+ const useExcludedDates = ( ) => {
48
+ const watchValues = useWatch < AvailabilityFormValues > ( { name : "dateOverrides" } ) as {
49
+ ranges : TimeRange [ ] ;
50
+ } [ ] ;
51
+ return useMemo ( ( ) => {
52
+ return watchValues ?. map ( ( field ) => dayjs ( field . ranges [ 0 ] . start ) . utc ( ) . format ( "YYYY-MM-DD" ) ) ;
53
+ } , [ watchValues ] ) ;
54
+ } ;
55
+
56
+ const useSettings = ( ) => {
57
+ const { data } = useMeQuery ( ) ;
58
+ return {
59
+ hour12 : data ?. timeFormat === 12 ,
60
+ timeZone : data ?. timeZone ,
61
+ } ;
62
+ } ;
63
+
46
64
const DateOverride = ( { workingHours } : { workingHours : WorkingHours [ ] } ) => {
47
- const { remove, append, replace, fields } = useFieldArray < AvailabilityFormValues , "dateOverrides" > ( {
65
+ const { hour12 } = useSettings ( ) ;
66
+ const { append, replace, fields } = useFieldArray < AvailabilityFormValues , "dateOverrides" > ( {
48
67
name : "dateOverrides" ,
49
68
} ) ;
50
- const excludedDates = fields . map ( ( field ) => dayjs ( field . ranges [ 0 ] . start ) . utc ( ) . format ( "YYYY-MM-DD" ) ) ;
69
+ const excludedDates = useExcludedDates ( ) ;
51
70
const { t } = useLocale ( ) ;
52
71
return (
53
72
< div className = "p-6" >
@@ -62,10 +81,10 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
62
81
< p className = "text-subtle mb-4 text-sm" > { t ( "date_overrides_subtitle" ) } </ p >
63
82
< div className = "space-y-2" >
64
83
< DateOverrideList
65
- excludedDates = { excludedDates }
66
- remove = { remove }
84
+ hour12 = { hour12 }
67
85
replace = { replace }
68
- items = { fields }
86
+ fields = { fields }
87
+ excludedDates = { excludedDates }
69
88
workingHours = { workingHours }
70
89
/>
71
90
< DateOverrideInputDialog
@@ -83,11 +102,85 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
83
102
) ;
84
103
} ;
85
104
105
+ const DeleteDialogButton = ( {
106
+ disabled,
107
+ scheduleId,
108
+ buttonClassName,
109
+ onDeleteConfirmed,
110
+ } : {
111
+ disabled ?: boolean ;
112
+ onDeleteConfirmed ?: ( ) => void ;
113
+ buttonClassName : string ;
114
+ scheduleId : number ;
115
+ } ) => {
116
+ const { t } = useLocale ( ) ;
117
+ const router = useRouter ( ) ;
118
+ const utils = trpc . useUtils ( ) ;
119
+ const { isPending, mutate } = trpc . viewer . availability . schedule . delete . useMutation ( {
120
+ onError : withErrorFromUnknown ( ( err ) => {
121
+ showToast ( err . message , "error" ) ;
122
+ } ) ,
123
+ onSettled : ( ) => {
124
+ utils . viewer . availability . list . invalidate ( ) ;
125
+ } ,
126
+ onSuccess : ( ) => {
127
+ showToast ( t ( "schedule_deleted_successfully" ) , "success" ) ;
128
+ router . push ( "/availability" ) ;
129
+ } ,
130
+ } ) ;
131
+
132
+ return (
133
+ < Dialog >
134
+ < DialogTrigger asChild >
135
+ < Button
136
+ StartIcon = { Trash }
137
+ variant = "icon"
138
+ color = "destructive"
139
+ aria-label = { t ( "delete" ) }
140
+ className = { buttonClassName }
141
+ disabled = { disabled }
142
+ tooltip = { disabled ? t ( "requires_at_least_one_schedule" ) : t ( "delete" ) }
143
+ />
144
+ </ DialogTrigger >
145
+ < ConfirmationDialogContent
146
+ isPending = { isPending }
147
+ variety = "danger"
148
+ title = { t ( "delete_schedule" ) }
149
+ confirmBtnText = { t ( "delete" ) }
150
+ loadingText = { t ( "delete" ) }
151
+ onConfirm = { ( ) => {
152
+ scheduleId && mutate ( { scheduleId } ) ;
153
+ onDeleteConfirmed ?.( ) ;
154
+ } } >
155
+ { t ( "delete_schedule_description" ) }
156
+ </ ConfirmationDialogContent >
157
+ </ Dialog >
158
+ ) ;
159
+ } ;
160
+
161
+ // Simplify logic by assuming this will never be opened on a large screen
162
+ const SmallScreenSideBar = ( { open, children } : { open : boolean ; children : JSX . Element } ) => {
163
+ return (
164
+ < div
165
+ className = { classNames (
166
+ open
167
+ ? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
168
+ : ""
169
+ ) } >
170
+ < div
171
+ className = { classNames (
172
+ "bg-default fixed right-0 z-20 flex h-screen w-80 flex-col space-y-2 overflow-x-hidden rounded-md px-2 pb-3 transition-transform" ,
173
+ open ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
174
+ ) } >
175
+ { open ? children : null }
176
+ </ div >
177
+ </ div >
178
+ ) ;
179
+ } ;
86
180
export default function Availability ( ) {
87
181
const searchParams = useCompatSearchParams ( ) ;
88
182
const { t, i18n } = useLocale ( ) ;
89
- const router = useRouter ( ) ;
90
- const utils = trpc . useContext ( ) ;
183
+ const utils = trpc . useUtils ( ) ;
91
184
const me = useMeQuery ( ) ;
92
185
const scheduleId = searchParams ?. get ( "schedule" ) ? Number ( searchParams . get ( "schedule" ) ) : - 1 ;
93
186
const fromEventType = searchParams ?. get ( "fromEventType" ) ;
@@ -133,22 +226,6 @@ export default function Availability() {
133
226
} ,
134
227
} ) ;
135
228
136
- const deleteMutation = trpc . viewer . availability . schedule . delete . useMutation ( {
137
- onError : ( err ) => {
138
- if ( err instanceof HttpError ) {
139
- const message = `${ err . statusCode } : ${ err . message } ` ;
140
- showToast ( message , "error" ) ;
141
- }
142
- } ,
143
- onSettled : ( ) => {
144
- utils . viewer . availability . list . invalidate ( ) ;
145
- } ,
146
- onSuccess : ( ) => {
147
- showToast ( t ( "schedule_deleted_successfully" ) , "success" ) ;
148
- router . push ( "/availability" ) ;
149
- } ,
150
- } ) ;
151
-
152
229
return (
153
230
< Shell
154
231
backPath = { fromEventType ? true : "/availability" }
@@ -179,88 +256,58 @@ export default function Availability() {
179
256
CTA = {
180
257
< div className = "flex items-center justify-end" >
181
258
< div className = "sm:hover:bg-muted hidden items-center rounded-md px-2 sm:flex" >
182
- < Skeleton
183
- as = { Label }
184
- htmlFor = "hiddenSwitch"
185
- className = "mt-2 cursor-pointer self-center pe-2"
186
- loadingClassName = "me-4" >
187
- { t ( "set_to_default" ) }
188
- </ Skeleton >
189
- < Switch
190
- id = "hiddenSwitch"
191
- disabled = { isPending || schedule ?. isDefault }
192
- checked = { form . watch ( "isDefault" ) }
193
- onCheckedChange = { ( e ) => {
194
- form . setValue ( "isDefault" , e ) ;
195
- } }
196
- />
259
+ { ! openSidebar ? (
260
+ < >
261
+ < Skeleton
262
+ as = { Label }
263
+ htmlFor = "hiddenSwitch"
264
+ className = "mt-2 cursor-pointer self-center pe-2"
265
+ loadingClassName = "me-4" >
266
+ { t ( "set_to_default" ) }
267
+ </ Skeleton >
268
+ < Controller
269
+ control = { form . control }
270
+ name = "isDefault"
271
+ render = { ( { field : { value, onChange } } ) => (
272
+ < Switch
273
+ id = "hiddenSwitch"
274
+ disabled = { isPending || schedule ?. isDefault }
275
+ checked = { value }
276
+ onCheckedChange = { onChange }
277
+ />
278
+ ) }
279
+ />
280
+ </ >
281
+ ) : null }
197
282
</ div >
198
283
199
284
< VerticalDivider className = "hidden sm:inline" />
200
- < Dialog >
201
- < DialogTrigger asChild >
202
- < Button
203
- StartIcon = { Trash }
204
- variant = "icon"
205
- color = "destructive"
206
- aria-label = { t ( "delete" ) }
207
- className = "hidden sm:inline"
208
- disabled = { schedule ?. isLastSchedule }
209
- tooltip = { schedule ?. isLastSchedule ? t ( "requires_at_least_one_schedule" ) : t ( "delete" ) }
210
- />
211
- </ DialogTrigger >
212
- < ConfirmationDialogContent
213
- isPending = { deleteMutation . isPending }
214
- variety = "danger"
215
- title = { t ( "delete_schedule" ) }
216
- confirmBtnText = { t ( "delete" ) }
217
- loadingText = { t ( "delete" ) }
218
- onConfirm = { ( ) => {
219
- scheduleId && deleteMutation . mutate ( { scheduleId } ) ;
220
- } } >
221
- { t ( "delete_schedule_description" ) }
222
- </ ConfirmationDialogContent >
223
- </ Dialog >
285
+ < DeleteDialogButton
286
+ buttonClassName = "hidden sm:inline"
287
+ scheduleId = { scheduleId }
288
+ disabled = { schedule ?. isLastSchedule }
289
+ />
224
290
< VerticalDivider className = "hidden sm:inline" />
225
- < div
226
- className = { classNames (
227
- openSidebar
228
- ? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
229
- : ""
230
- ) } >
231
- < div
232
- className = { classNames (
233
- "bg-default fixed right-0 z-20 flex h-screen w-80 flex-col space-y-2 overflow-x-hidden rounded-md px-2 pb-3 transition-transform" ,
234
- openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
235
- ) } >
291
+
292
+ < SmallScreenSideBar open = { openSidebar } >
293
+ < >
236
294
< div className = "flex flex-row items-center pt-5" >
237
- < Button StartIcon = { ArrowLeft } color = "minimal" onClick = { ( ) => setOpenSidebar ( false ) } />
295
+ < Button
296
+ StartIcon = { ArrowLeft }
297
+ color = "minimal"
298
+ onClick = { ( ) => {
299
+ setOpenSidebar ( false ) ;
300
+ } }
301
+ />
238
302
< p className = "-ml-2" > { t ( "availability_settings" ) } </ p >
239
- < Dialog >
240
- < DialogTrigger asChild >
241
- < Button
242
- StartIcon = { Trash }
243
- variant = "icon"
244
- color = "destructive"
245
- aria-label = { t ( "delete" ) }
246
- className = "ml-16 inline"
247
- disabled = { schedule ?. isLastSchedule }
248
- tooltip = { schedule ?. isLastSchedule ? t ( "requires_at_least_one_schedule" ) : t ( "delete" ) }
249
- />
250
- </ DialogTrigger >
251
- < ConfirmationDialogContent
252
- isPending = { deleteMutation . isPending }
253
- variety = "danger"
254
- title = { t ( "delete_schedule" ) }
255
- confirmBtnText = { t ( "delete" ) }
256
- loadingText = { t ( "delete" ) }
257
- onConfirm = { ( ) => {
258
- scheduleId && deleteMutation . mutate ( { scheduleId } ) ;
259
- setOpenSidebar ( false ) ;
260
- } } >
261
- { t ( "delete_schedule_description" ) }
262
- </ ConfirmationDialogContent >
263
- </ Dialog >
303
+ < DeleteDialogButton
304
+ buttonClassName = "ml-16 inline"
305
+ scheduleId = { scheduleId }
306
+ disabled = { schedule ?. isLastSchedule }
307
+ onDeleteConfirmed = { ( ) => {
308
+ setOpenSidebar ( false ) ;
309
+ } }
310
+ />
264
311
</ div >
265
312
< div className = "flex flex-col px-2 py-2" >
266
313
< Skeleton as = { Label } > { t ( "name" ) } </ Skeleton >
@@ -275,20 +322,25 @@ export default function Availability() {
275
322
) }
276
323
/>
277
324
</ div >
325
+
278
326
< div className = "flex h-9 flex-row-reverse items-center justify-end gap-3 px-2" >
279
327
< Skeleton
280
328
as = { Label }
281
329
htmlFor = "hiddenSwitch"
282
330
className = "mt-2 cursor-pointer self-center pr-2 sm:inline" >
283
331
{ t ( "set_to_default" ) }
284
332
</ Skeleton >
285
- < Switch
286
- id = "hiddenSwitch"
287
- disabled = { isPending || schedule ?. isDefault }
288
- checked = { form . watch ( "isDefault" ) }
289
- onCheckedChange = { ( e ) => {
290
- form . setValue ( "isDefault" , e ) ;
291
- } }
333
+ < Controller
334
+ control = { form . control }
335
+ name = "isDefault"
336
+ render = { ( { field : { value, onChange } } ) => (
337
+ < Switch
338
+ id = "hiddenSwitch"
339
+ disabled = { isPending || value }
340
+ checked = { value }
341
+ onCheckedChange = { onChange }
342
+ />
343
+ ) }
292
344
/>
293
345
</ div >
294
346
@@ -331,8 +383,9 @@ export default function Availability() {
331
383
</ div >
332
384
</ div >
333
385
</ div >
334
- </ div >
335
- </ div >
386
+ </ >
387
+ </ SmallScreenSideBar >
388
+
336
389
< div className = "border-default border-l-2" />
337
390
< Button
338
391
className = "ml-4 lg:ml-0"
0 commit comments