diff --git a/client/components/jetpack/backup-schedule-setting/index.tsx b/client/components/jetpack/backup-schedule-setting/index.tsx index a6f1c895da67a..9515f198124ad 100644 --- a/client/components/jetpack/backup-schedule-setting/index.tsx +++ b/client/components/jetpack/backup-schedule-setting/index.tsx @@ -1,31 +1,111 @@ import { Card } from '@automattic/components'; +import { useQueryClient } from '@tanstack/react-query'; import { SelectControl } from '@wordpress/components'; -import { useTranslate } from 'i18n-calypso'; +import { TranslateResult, useTranslate } from 'i18n-calypso'; +import { useLocalizedMoment } from 'calypso/components/localized-moment'; +import useScheduledTimeMutation from 'calypso/data/jetpack-backup/use-scheduled-time-mutation'; +import useScheduledTimeQuery from 'calypso/data/jetpack-backup/use-scheduled-time-query'; +import { applySiteOffset } from 'calypso/lib/site/timezone'; +import { useDispatch, useSelector } from 'calypso/state'; +import { errorNotice, successNotice } from 'calypso/state/notices/actions'; +import getSiteGmtOffset from 'calypso/state/selectors/get-site-gmt-offset'; +import getSiteTimezoneValue from 'calypso/state/selectors/get-site-timezone-value'; +import { getSelectedSiteId } from 'calypso/state/ui/selectors'; import type { FunctionComponent } from 'react'; import './style.scss'; -// Helper function to generate all time slots -const generateTimeSlots = (): { label: string; value: string }[] => { - const options = []; - for ( let hour = 0; hour < 24; hour++ ) { - const startTime = hour.toString().padStart( 2, '0' ) + ':00'; - const endTime = hour.toString().padStart( 2, '0' ) + ':59'; - options.push( { - label: `${ startTime } - ${ endTime }`, - value: hour.toString(), - } ); - } - return options; -}; - const BackupScheduleSetting: FunctionComponent = () => { + const dispatch = useDispatch(); const translate = useTranslate(); - const options = generateTimeSlots(); + const queryClient = useQueryClient(); + const moment = useLocalizedMoment(); + const siteId = useSelector( getSelectedSiteId ) as number; + const timezone = useSelector( ( state ) => getSiteTimezoneValue( state, siteId ) ); + const gmtOffset = useSelector( ( state ) => getSiteGmtOffset( state, siteId ) ); + + const convertHourToRange = ( hour: number, isUtc: boolean = false ): string => { + const time = isUtc + ? moment.utc().startOf( 'day' ).hour( hour ) + : moment().startOf( 'day' ).hour( hour ); + + const formatString = isUtc ? 'HH:mm' : 'LT'; // 24-hour format for UTC, 12-hour for local + + const startTime = time.format( formatString ); + const endTime = time.add( 59, 'minutes' ).format( formatString ); + + return `${ startTime } - ${ endTime }`; + }; + + const generateTimeSlots = (): { label: string; value: string }[] => { + const options = []; + for ( let hour = 0; hour < 24; hour++ ) { + const utcTime = moment.utc().startOf( 'day' ).hour( hour ); + const localTime = + timezone && gmtOffset + ? applySiteOffset( utcTime, { timezone, gmtOffset } ) + : utcTime.local(); + const localHour = localTime.hour(); + const timeRange = convertHourToRange( localHour ); + + options.push( { + label: timeRange, + value: hour.toString(), + localHour, // for sorting + } ); + } + + // Sort options by local hour before returning + options.sort( ( a, b ) => a.localHour - b.localHour ); + + // Remove the localHour from the final result as it's not needed anymore + return options.map( ( { label, value } ) => ( { label, value } ) ); + }; + + const timeSlotOptions = generateTimeSlots(); + const { isFetching: isScheduledTimeQueryFetching, data } = useScheduledTimeQuery( siteId ); + const { isPending: isScheduledTimeMutationLoading, mutate: scheduledTimeMutate } = + useScheduledTimeMutation( { + onSuccess: () => { + queryClient.invalidateQueries( { queryKey: [ 'jetpack-backup-scheduled-time', siteId ] } ); + dispatch( + successNotice( translate( 'Daily backup time successfully changed.' ), { + duration: 5000, + isPersistent: true, + } ) + ); + }, + onError: () => { + dispatch( + errorNotice( translate( 'Update daily backup time failed. Please, try again.' ), { + duration: 5000, + isPersistent: true, + } ) + ); + }, + } ); + + const isLoading = isScheduledTimeQueryFetching || isScheduledTimeMutationLoading; + + const updateScheduledTime = ( selectedTime: string ) => { + scheduledTimeMutate( { scheduledHour: Number( selectedTime ) } ); + }; + + const getScheduleInfoMessage = (): TranslateResult => { + const hour = data?.scheduledHour || 0; + const range = convertHourToRange( hour, true ); + + if ( ! data || ! data.scheduledBy ) { + return `${ translate( 'Default time' ) }. UTC: ${ range }`; + } + return `${ translate( 'Time set by %(scheduledBy)s', { + args: { scheduledBy: data.scheduledBy }, + } ) }. UTC: ${ range }`; + }; return (
-

{ translate( 'Backup schedule' ) }

+

{ translate( 'Daily backup time schedule' ) }

@@ -33,7 +113,14 @@ const BackupScheduleSetting: FunctionComponent = () => { 'Pick a timeframe for your backup to run. Some site owners prefer scheduling backups at specific times for better control.' ) }

- +
); diff --git a/client/data/jetpack-backup/use-scheduled-time-mutation.ts b/client/data/jetpack-backup/use-scheduled-time-mutation.ts new file mode 100644 index 0000000000000..99ca5a2f88c1b --- /dev/null +++ b/client/data/jetpack-backup/use-scheduled-time-mutation.ts @@ -0,0 +1,34 @@ +import { useMutation, UseMutationResult, UseMutationOptions } from '@tanstack/react-query'; +import wpcom from 'calypso/lib/wp'; +import { useSelector } from 'calypso/state'; +import { getSelectedSiteId } from 'calypso/state/ui/selectors'; + +export interface UpdateSchedulePayload { + scheduledHour: number; // The new scheduled hour (0-23) +} + +export interface UpdateScheduleResponse { + ok: boolean; + error: string; +} + +export default function useScheduledTimeMutation< + TData = UpdateScheduleResponse, + TError = Error, + TContext = unknown, +>( + options: UseMutationOptions< TData, TError, UpdateSchedulePayload, TContext > = {} +): UseMutationResult< TData, TError, UpdateSchedulePayload, TContext > { + const siteId = useSelector( getSelectedSiteId ) as number; + + return useMutation< TData, TError, UpdateSchedulePayload, TContext >( { + ...options, + mutationFn: ( { scheduledHour }: UpdateSchedulePayload ): Promise< TData > => { + return wpcom.req.post( { + path: `/sites/${ siteId }/rewind/scheduled`, + apiNamespace: 'wpcom/v2', + body: { schedule_hour: scheduledHour }, + } ); + }, + } ); +} diff --git a/client/data/jetpack-backup/use-scheduled-time-query.ts b/client/data/jetpack-backup/use-scheduled-time-query.ts new file mode 100644 index 0000000000000..80b46d7091a0a --- /dev/null +++ b/client/data/jetpack-backup/use-scheduled-time-query.ts @@ -0,0 +1,34 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import wpcom from 'calypso/lib/wp'; + +export interface ScheduledTimeApi { + ok: boolean; + scheduled_hour: number; + scheduled_by: string | null; +} + +export interface ScheduledTime { + scheduledHour: number; + scheduledBy: string | null; +} + +const useScheduledTimeQuery = ( blogId: number ): UseQueryResult< ScheduledTime, Error > => { + const queryKey = [ 'jetpack-backup-scheduled-time', blogId ]; + + return useQuery< ScheduledTimeApi, Error, ScheduledTime >( { + queryKey, + queryFn: async () => + wpcom.req.get( { + path: `/sites/${ blogId }/rewind/scheduled`, + apiNamespace: 'wpcom/v2', + } ), + refetchIntervalInBackground: false, + refetchOnWindowFocus: false, + select: ( data ) => ( { + scheduledHour: data.scheduled_hour, + scheduledBy: data.scheduled_by, + } ), + } ); +}; + +export default useScheduledTimeQuery; diff --git a/client/jetpack-cloud/sections/settings/index.js b/client/jetpack-cloud/sections/settings/index.js index 7763b64cd04df..ef5bb6b190382 100644 --- a/client/jetpack-cloud/sections/settings/index.js +++ b/client/jetpack-cloud/sections/settings/index.js @@ -10,6 +10,7 @@ import { } from 'calypso/jetpack-cloud/sections/settings/controller'; import isJetpackCloud from 'calypso/lib/jetpack/is-jetpack-cloud'; import { confirmDisconnectPath, disconnectPath, settingsPath } from 'calypso/lib/jetpack/paths'; +import wrapInSiteOffsetProvider from 'calypso/lib/wrap-in-site-offset'; import { navigation, siteSelection, sites } from 'calypso/my-sites/controller'; export default function () { @@ -20,6 +21,7 @@ export default function () { siteSelection, navigation, isEnabled( 'jetpack/server-credentials-advanced-flow' ) ? advancedCredentials : settings, + wrapInSiteOffsetProvider, showNotAuthorizedForNonAdmins, makeLayout, clientRender diff --git a/config/jetpack-cloud-development.json b/config/jetpack-cloud-development.json index 9012ec84be9d9..fab4d7b658c91 100644 --- a/config/jetpack-cloud-development.json +++ b/config/jetpack-cloud-development.json @@ -47,6 +47,7 @@ "jetpack/backup-messaging-i3": true, "jetpack/backup-restore-preflight-checks": true, "jetpack/backup-retention-settings": true, + "jetpack/backup-schedule-setting": true, "jetpack/card-addition-improvements": true, "jetpack/golden-token": true, "jetpack/plugin-management": true, diff --git a/config/jetpack-cloud-horizon.json b/config/jetpack-cloud-horizon.json index ef8122da545c2..9b56b00ebf32d 100644 --- a/config/jetpack-cloud-horizon.json +++ b/config/jetpack-cloud-horizon.json @@ -41,6 +41,7 @@ "jetpack/backup-messaging-i3": true, "jetpack/backup-restore-preflight-checks": true, "jetpack/backup-retention-settings": true, + "jetpack/backup-schedule-setting": true, "jetpack/card-addition-improvements": true, "jetpack/golden-token": false, "jetpack/plugin-management": true,