@@ -81,6 +81,8 @@ interface DisableUserCellProps {
8181
8282interface AccountValidPeriodCellProps {
8383 data : AccountStatus ;
84+ onClickSetAccountValidPeriod : ( ) => void ;
85+ onClickEditAccountValidPeriod : ( ) => void ;
8486}
8587
8688interface AnonymizeUserCellProps {
@@ -450,7 +452,11 @@ const AccountValidPeriodCell: React.VFC<AccountValidPeriodCellProps> =
450452 function AccountValidPeriodCell ( props ) {
451453 const { locale } = useContext ( Context ) ;
452454 const { themes } = useSystemConfig ( ) ;
453- const { data } = props ;
455+ const {
456+ data,
457+ onClickSetAccountValidPeriod,
458+ onClickEditAccountValidPeriod,
459+ } = props ;
454460 const buttonStates = getButtonStates ( data ) ;
455461 return (
456462 < ListCellLayout className = { styles . actionCell } >
@@ -523,6 +529,12 @@ const AccountValidPeriodCell: React.VFC<AccountValidPeriodCellProps> =
523529 < FormattedMessage id = "UserDetailsAccountStatus.account-valid-period.action.edit" />
524530 )
525531 }
532+ onClick = {
533+ buttonStates . setAccountValidPeriod . accountValidFrom == null &&
534+ buttonStates . setAccountValidPeriod . accountValidUntil == null
535+ ? onClickSetAccountValidPeriod
536+ : onClickEditAccountValidPeriod
537+ }
526538 />
527539 </ ListCellLayout >
528540 ) ;
@@ -683,6 +695,16 @@ const UserDetailsAccountStatus: React.VFC<UserDetailsAccountStatusProps> =
683695 setDialogKey ( ( prev ) => prev + 1 ) ;
684696 setDialogHidden ( false ) ;
685697 } , [ ] ) ;
698+ const onClickSetAccountValidPeriod = useCallback ( ( ) => {
699+ setMode ( "set-account-valid-period" ) ;
700+ setDialogKey ( ( prev ) => prev + 1 ) ;
701+ setDialogHidden ( false ) ;
702+ } , [ ] ) ;
703+ const onClickEditAccountValidPeriod = useCallback ( ( ) => {
704+ setMode ( "edit-account-valid-period" ) ;
705+ setDialogKey ( ( prev ) => prev + 1 ) ;
706+ setDialogHidden ( false ) ;
707+ } , [ ] ) ;
686708 const onClickAnonymizeOrSchedule = useCallback ( ( ) => {
687709 setMode ( "anonymize-or-schedule" ) ;
688710 setDialogKey ( ( prev ) => prev + 1 ) ;
@@ -737,7 +759,11 @@ const UserDetailsAccountStatus: React.VFC<UserDetailsAccountStatusProps> =
737759 onClickDisable = { onClickDisable }
738760 onClickReenable = { onClickReenable }
739761 />
740- < AccountValidPeriodCell data = { data } />
762+ < AccountValidPeriodCell
763+ data = { data }
764+ onClickSetAccountValidPeriod = { onClickSetAccountValidPeriod }
765+ onClickEditAccountValidPeriod = { onClickEditAccountValidPeriod }
766+ />
741767 < AnonymizeUserCell
742768 data = { data }
743769 onClickAnonymizeImmediately = { onClickAnonymizeImmediately }
@@ -780,6 +806,114 @@ export interface AccountStatusDialogProps {
780806 accountStatus : AccountStatus ;
781807}
782808
809+ interface AccountValidDateTimeControlProps {
810+ label : React . ReactElement ;
811+ pickedDateTime : Date | null ;
812+ onPickDateTime : ( datetime : Date | null ) => void ;
813+ }
814+
815+ function AccountValidDateTimeControl ( props : AccountValidDateTimeControlProps ) {
816+ const { label, pickedDateTime, onPickDateTime } = props ;
817+ const { themes } = useSystemConfig ( ) ;
818+
819+ // TimePicker has some problem with its controlled component behavior.
820+ //
821+ // 1. When we clear the field, value=undefined does not cause it to render empty.
822+ // 2. Changing the date picker and thus value=something does not cause it to render.
823+ //
824+ // So we always remount it in these two cases.
825+ const [ timePickerKey , setTimePickerKey ] = useState ( 0 ) ;
826+
827+ const onSelectDate = useCallback (
828+ ( date : Date | null | undefined ) => {
829+ if ( date == null ) {
830+ onPickDateTime ( null ) ;
831+ } else {
832+ const datetime =
833+ pickedDateTime != null ? DateTime . fromJSDate ( pickedDateTime ) : null ;
834+ onPickDateTime (
835+ DateTime . fromJSDate ( date )
836+ . set ( {
837+ hour : datetime ?. hour ?? 0 ,
838+ minute : datetime ?. minute ?? 0 ,
839+ second : 0 ,
840+ millisecond : 0 ,
841+ } )
842+ . toJSDate ( )
843+ ) ;
844+ }
845+ setTimePickerKey ( ( prev ) => prev + 1 ) ;
846+ } ,
847+ [ onPickDateTime , pickedDateTime ]
848+ ) ;
849+
850+ const onChange = useCallback (
851+ ( _e : React . FormEvent < IComboBox > , time : Date ) => {
852+ if ( pickedDateTime == null ) {
853+ return ;
854+ }
855+ const datetime = DateTime . fromJSDate ( time ) ;
856+ onPickDateTime (
857+ DateTime . fromJSDate ( pickedDateTime )
858+ . set ( {
859+ hour : datetime . hour ,
860+ minute : datetime . minute ,
861+ second : 0 ,
862+ millisecond : 0 ,
863+ } )
864+ . toJSDate ( )
865+ ) ;
866+ } ,
867+ [ onPickDateTime , pickedDateTime ]
868+ ) ;
869+
870+ const onClickClear = useCallback ( ( ) => {
871+ onPickDateTime ( null ) ;
872+ setTimePickerKey ( ( prev ) => prev + 1 ) ;
873+ } , [ onPickDateTime ] ) ;
874+
875+ return (
876+ < div className = "flex flex-col" >
877+ < Label > { label } </ Label >
878+ < div className = "flex flex-row gap-2" >
879+ < DatePicker
880+ className = "flex-1"
881+ value = { pickedDateTime ?? undefined }
882+ onSelectDate = { onSelectDate }
883+ />
884+ < TimePicker
885+ key = { String ( timePickerKey ) }
886+ className = "flex-1"
887+ increments = { 60 }
888+ allowFreeform = { false }
889+ showSeconds = { false }
890+ useHour12 = { false }
891+ dateAnchor = { pickedDateTime ?? undefined }
892+ value = { pickedDateTime ?? undefined }
893+ onChange = { onChange }
894+ />
895+ < DefaultButton
896+ className = "self-start"
897+ text = {
898+ < FormattedMessage id = "AccountStatusDialog.account-valid-period.action.clear" />
899+ }
900+ onClick = { onClickClear }
901+ />
902+ </ div >
903+ < Text
904+ variant = "small"
905+ styles = { {
906+ root : {
907+ color : themes . main . semanticColors . bodySubtext ,
908+ } ,
909+ } }
910+ >
911+ < FormattedMessage id = "AccountStatusDialog.account-valid-period.clear.hint" />
912+ </ Text >
913+ </ div >
914+ ) ;
915+ }
916+
783917export function AccountStatusDialog (
784918 props : AccountStatusDialogProps
785919) : React . ReactElement {
@@ -828,6 +962,52 @@ export function AccountStatusDialog(
828962 return trimmed ;
829963 } , [ disableReason ] ) ;
830964
965+ const [ accountValidFrom , setAccountValidFrom ] = useState < Date | null > ( ( ) => {
966+ if ( accountStatus . accountValidFrom != null ) {
967+ return new Date ( accountStatus . accountValidFrom ) ;
968+ }
969+ return null ;
970+ } ) ;
971+ const [ accountValidUntil , setAccountValidUntil ] = useState < Date | null > (
972+ ( ) => {
973+ if ( accountStatus . accountValidUntil != null ) {
974+ return new Date ( accountStatus . accountValidUntil ) ;
975+ }
976+ return null ;
977+ }
978+ ) ;
979+
980+ const onPickAccountValidFrom = useCallback (
981+ ( date : Date | null ) => {
982+ if ( date == null ) {
983+ setAccountValidFrom ( null ) ;
984+ } else if ( accountValidUntil == null ) {
985+ setAccountValidFrom ( date ) ;
986+ } else if ( date . getTime ( ) > accountValidUntil . getTime ( ) ) {
987+ setAccountValidFrom ( accountValidUntil ) ;
988+ setAccountValidUntil ( date ) ;
989+ } else {
990+ setAccountValidFrom ( date ) ;
991+ }
992+ } ,
993+ [ accountValidUntil ]
994+ ) ;
995+ const onPickAccountValidUntil = useCallback (
996+ ( date : Date | null ) => {
997+ if ( date == null ) {
998+ setAccountValidUntil ( null ) ;
999+ } else if ( accountValidFrom == null ) {
1000+ setAccountValidUntil ( date ) ;
1001+ } else if ( date . getTime ( ) < accountValidFrom . getTime ( ) ) {
1002+ setAccountValidFrom ( date ) ;
1003+ setAccountValidUntil ( accountValidFrom ) ;
1004+ } else {
1005+ setAccountValidUntil ( date ) ;
1006+ }
1007+ } ,
1008+ [ accountValidFrom ]
1009+ ) ;
1010+
8311011 const onRenderTemporarilyDisableFormField = useCallback (
8321012 (
8331013 props ?: IChoiceGroupOption & IChoiceGroupOptionProps ,
@@ -958,6 +1138,49 @@ export function AccountStatusDialog(
9581138 renderToString ,
9591139 ] ) ;
9601140
1141+ const accountValidPeriodForm = useMemo ( ( ) => {
1142+ const formattedZone = formatSystemZone ( new Date ( ) , locale ) ;
1143+ return (
1144+ < div className = "flex flex-col gap-2" >
1145+ < MessageBar
1146+ messageBarType = { MessageBarType . info }
1147+ styles = { {
1148+ iconContainer : {
1149+ display : "none" ,
1150+ } ,
1151+ } }
1152+ >
1153+ < FormattedMessage
1154+ id = "AccountStatusDialog.disable-user.timezone-description"
1155+ values = { {
1156+ timezone : formattedZone ,
1157+ } }
1158+ />
1159+ </ MessageBar >
1160+ < AccountValidDateTimeControl
1161+ label = {
1162+ < FormattedMessage id = "AccountStatusDialog.account-valid-period.start-at.label" />
1163+ }
1164+ pickedDateTime = { accountValidFrom }
1165+ onPickDateTime = { onPickAccountValidFrom }
1166+ />
1167+ < AccountValidDateTimeControl
1168+ label = {
1169+ < FormattedMessage id = "AccountStatusDialog.account-valid-period.end-at.label" />
1170+ }
1171+ pickedDateTime = { accountValidUntil }
1172+ onPickDateTime = { onPickAccountValidUntil }
1173+ />
1174+ </ div >
1175+ ) ;
1176+ } , [
1177+ accountValidFrom ,
1178+ accountValidUntil ,
1179+ locale ,
1180+ onPickAccountValidFrom ,
1181+ onPickAccountValidUntil ,
1182+ ] ) ;
1183+
9611184 const {
9621185 setDisabledStatus,
9631186 loading : setDisabledStatusLoading ,
@@ -1239,8 +1462,46 @@ export function AccountStatusDialog(
12391462 prepareReenable ( ) ;
12401463 break ;
12411464 case "set-account-valid-period" :
1465+ title = (
1466+ < FormattedMessage id = "AccountStatusDialog.account-valid-period.title--set" />
1467+ ) ;
1468+ subText = (
1469+ < FormattedMessage
1470+ id = "AccountStatusDialog.account-valid-period.description--set"
1471+ values = { args }
1472+ />
1473+ ) ;
1474+ body = accountValidPeriodForm ;
1475+ button1 = (
1476+ < PrimaryButton
1477+ theme = { themes . main }
1478+ disabled = { loading }
1479+ text = {
1480+ < FormattedMessage id = "AccountStatusDialog.account-valid-period.action.save" />
1481+ }
1482+ />
1483+ ) ;
12421484 break ;
12431485 case "edit-account-valid-period" :
1486+ title = (
1487+ < FormattedMessage id = "AccountStatusDialog.account-valid-period.title--edit" />
1488+ ) ;
1489+ subText = (
1490+ < FormattedMessage
1491+ id = "AccountStatusDialog.account-valid-period.description--edit"
1492+ values = { args }
1493+ />
1494+ ) ;
1495+ body = accountValidPeriodForm ;
1496+ button1 = (
1497+ < PrimaryButton
1498+ theme = { themes . main }
1499+ disabled = { loading }
1500+ text = {
1501+ < FormattedMessage id = "AccountStatusDialog.account-valid-period.action.edit" />
1502+ }
1503+ />
1504+ ) ;
12441505 break ;
12451506 case "anonymize-or-schedule" :
12461507 title = (
@@ -1374,6 +1635,7 @@ export function AccountStatusDialog(
13741635 return { dialogContentProps : { title, subText } , body, button1, button2 } ;
13751636 } , [
13761637 accountStatus ,
1638+ accountValidPeriodForm ,
13771639 disableForm ,
13781640 loading ,
13791641 mode ,
0 commit comments