diff --git a/projects/packages/activity-log/changelog/manage-backup-action-deep-link-calypso b/projects/packages/activity-log/changelog/manage-backup-action-deep-link-calypso new file mode 100644 index 000000000000..0de46b467fd0 --- /dev/null +++ b/projects/packages/activity-log/changelog/manage-backup-action-deep-link-calypso @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Activity Log: the "Manage backup" row action now opens the Jetpack Cloud Backup restore flow for that point in time, instead of being a disabled placeholder. diff --git a/projects/packages/activity-log/changelog/restore-backup-action-label-and-temp-doc b/projects/packages/activity-log/changelog/restore-backup-action-label-and-temp-doc new file mode 100644 index 000000000000..74cdc176759e --- /dev/null +++ b/projects/packages/activity-log/changelog/restore-backup-action-label-and-temp-doc @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Activity Log: rename the row action to "Restore backup" so the label matches what clicking it actually does — open the restore flow. diff --git a/projects/packages/activity-log/changelog/upsell-illustration-and-title-casing b/projects/packages/activity-log/changelog/upsell-illustration-and-title-casing new file mode 100644 index 000000000000..03de6c28efde --- /dev/null +++ b/projects/packages/activity-log/changelog/upsell-illustration-and-title-casing @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Activity Log: refreshed the free-tier upsell illustration to match Jetpack's branding. diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/UpsellCallout.tsx b/projects/packages/activity-log/src/js/components/ActivityLog/UpsellCallout.tsx index 0bf291fb9a29..08a7f98fddd3 100644 --- a/projects/packages/activity-log/src/js/components/ActivityLog/UpsellCallout.tsx +++ b/projects/packages/activity-log/src/js/components/ActivityLog/UpsellCallout.tsx @@ -78,7 +78,7 @@ export function UpsellCallout() {

- { __( 'Track every action with Activity logs', 'jetpack-activity-log' ) } + { __( 'Track every action with activity logs', 'jetpack-activity-log' ) }

{ __( diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/actions.tsx b/projects/packages/activity-log/src/js/components/ActivityLog/actions.tsx index 90b604bf6a32..a0ce3ce41bd0 100644 --- a/projects/packages/activity-log/src/js/components/ActivityLog/actions.tsx +++ b/projects/packages/activity-log/src/js/components/ActivityLog/actions.tsx @@ -5,36 +5,69 @@ import { useMemo } from 'react'; import type { Activity } from './types'; import type { Action } from '@wordpress/dataviews'; +interface InitialStateWithCalypsoSlug { + jetpackStatus?: { calypsoSlug?: string }; +} + +declare const JPACTIVITYLOG_INITIAL_STATE: InitialStateWithCalypsoSlug | undefined; + +// Read once at module load; the value doesn't change within a session. +const calypsoSlug: string = + ( typeof JPACTIVITYLOG_INITIAL_STATE !== 'undefined' + ? JPACTIVITYLOG_INITIAL_STATE?.jetpackStatus?.calypsoSlug + : undefined ) ?? ''; + +type Tracks = { recordEvent: ( name: string, props?: Record< string, unknown > ) => void }; + type UseActivityActionsOptions = { isLoading: boolean; + tracks?: Tracks; }; /** - * Row actions for the DataViews table. Phase 5 wires the "Manage backup" - * action into the Backup package's admin page; for now the action is - * present but disabled so the column space is preserved and the planned - * feature is visible. + * Row actions for the DataViews table. The single primary action deep- + * links into the Jetpack Cloud Backup restore flow for the row's rewind + * point (`https://cloud.jetpack.com/backup/{slug}/restore/{rewindId}`) + * and opens in a new tab. Eligibility requires `activityIsRewindable`, + * a `rewindId`, and a `calypsoSlug` from Initial_State; rows missing + * any of those don't render the action. + * + * TEMPORARY: this off-site link is a stop-gap until the Backup wp-admin + * port (https://github.com/Automattic/jetpack/pull/48236) lands. Once + * that ships, every row action here should point at the in-admin + * Backup page instead of cloud.jetpack.com so users stay inside their + * own wp-admin for the restore flow. * * @param options - Hook options. * @param options.isLoading - Whether the list is currently fetching. Kept - * in the API so Phase 5 doesn't need to refactor - * the call site. + * in the API for symmetry with the call site. + * @param options.tracks - Optional analytics handle for the click event. * @return The actions array for ``. */ export function useActivityActions( { isLoading, + tracks, }: UseActivityActionsOptions ): Action< Activity >[] { return useMemo( () => { const backupAction: Action< Activity > = { id: 'backup', isPrimary: true, - label: __( 'Manage backup', 'jetpack-activity-log' ), + label: __( 'Restore backup', 'jetpack-activity-log' ), icon: , - // Phase 5: enable and deep-link into the Backup package's admin page. - disabled: true, - isEligible: item => item.activityIsRewindable, - callback: async () => { - /* no-op until Phase 5 */ + isEligible: item => Boolean( item.activityIsRewindable && item.rewindId && calypsoSlug ), + callback: async items => { + const item = items[ 0 ]; + if ( ! item?.rewindId || ! calypsoSlug ) { + return; + } + const url = `https://cloud.jetpack.com/backup/${ encodeURIComponent( + calypsoSlug + ) }/restore/${ encodeURIComponent( item.rewindId ) }`; + tracks?.recordEvent( 'jetpack_activity_log_restore_backup_click', { + rewind_id: item.rewindId, + activity_name: item.activityName, + } ); + window.open( url, '_blank', 'noopener,noreferrer' ); }, }; @@ -42,5 +75,5 @@ export function useActivityActions( { void isLoading; return [ backupAction ]; - }, [ isLoading ] ); + }, [ isLoading, tracks ] ); } diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/activity-logs-callout-illustration.svg b/projects/packages/activity-log/src/js/components/ActivityLog/activity-logs-callout-illustration.svg index e6b0fb17ee8c..08990245ca79 100644 --- a/projects/packages/activity-log/src/js/components/ActivityLog/activity-logs-callout-illustration.svg +++ b/projects/packages/activity-log/src/js/components/ActivityLog/activity-logs-callout-illustration.svg @@ -1,52 +1,45 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + + - - - + + + - - - - diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/index.tsx b/projects/packages/activity-log/src/js/components/ActivityLog/index.tsx index 6ea493cbfce4..521992f52e98 100644 --- a/projects/packages/activity-log/src/js/components/ActivityLog/index.tsx +++ b/projects/packages/activity-log/src/js/components/ActivityLog/index.tsx @@ -93,56 +93,18 @@ export default function ActivityLog() { const { tracks } = useAnalytics(); const wrapperRef = useRef< HTMLDivElement >( null ); - // DataViews' `Action` API doesn't expose a tooltip prop, so the - // disabled "Manage backup" stub renders without any hint as to *why* - // it can't be clicked. Attach a `title` to those buttons after each - // render so users hovering get the "Coming soon" context — and - // screen readers pick it up too. The MutationObserver re-runs on - // pagination / filter changes, when DataViews swaps the row DOM. - // - // TODO(#48236): drop this whole effect. Once the Backup wp-admin - // page lands the action stops being a stub and the row will render - // as an enabled link, so this textContent-matching DOM hack — which - // is fragile across translations and re-fires on every DataViews - // mutation — won't be needed at all. - useEffect( () => { - const wrapper = wrapperRef.current; - if ( ! wrapper ) { - return; - } - const manageBackupLabel = __( 'Manage backup', 'jetpack-activity-log' ); - const tooltipText = __( 'Coming soon', 'jetpack-activity-log' ); - const apply = ( root: ParentNode ) => { - const buttons = root.querySelectorAll< HTMLButtonElement >( - '.dataviews-item-actions button[disabled]' - ); - buttons.forEach( btn => { - if ( - btn.textContent?.trim() === manageBackupLabel && - btn.getAttribute( 'title' ) !== tooltipText - ) { - btn.setAttribute( 'title', tooltipText ); - } - } ); - }; - apply( wrapper ); - const observer = new MutationObserver( () => apply( wrapper ) ); - observer.observe( wrapper, { subtree: true, childList: true } ); - return () => observer.disconnect(); - }, [] ); - // On free tier, neutralize DataViews' real search + filter cluster // (the `.dataviews__search` Stack rendered by `DataViews`'s default // UI). We let DataViews ship its own toolbar so the page tracks - // upstream changes for free, then attach: `aria-disabled` for - // assistive tech, a `title` attribute that surfaces the upgrade - // nudge as a native browser tooltip on hover, and `tabindex="-1"` - // on every focusable descendant so the cluster is unreachable via - // keyboard. Pointer-event blocking on the children is handled in - // CSS via the `[aria-disabled="true"]` rule. - // `MutationObserver` re-applies after DataViews remounts the - // toolbar / re-renders the input (e.g., on initial fetch resolution - // or layout switch) so React's render doesn't strip the attributes. + // upstream changes for free, then mark the cluster `inert` — which + // blocks pointer + keyboard interaction and removes descendants from + // the a11y tree in one shot — and add a `title` attribute that + // surfaces the upgrade nudge as a native browser tooltip on hover. + // Tradeoff: Firefox suppresses `title` tooltips inside an inert + // subtree, so the nudge doesn't appear there; accepted per #48527. + // `MutationObserver` re-applies after DataViews remounts the toolbar + // / re-renders the input (e.g., on initial fetch resolution or + // layout switch) so React's render doesn't strip the attributes. useEffect( () => { if ( hasActivityLogsAccess ) { return; @@ -157,20 +119,12 @@ export default function ActivityLog() { const apply = ( root: ParentNode ) => { const cluster = root.querySelector< HTMLElement >( '.dataviews__search' ); - if ( ! cluster ) { + if ( ! cluster || cluster.hasAttribute( 'inert' ) ) { return; } - if ( cluster.getAttribute( 'aria-disabled' ) !== 'true' ) { - cluster.setAttribute( 'aria-disabled', 'true' ); - cluster.setAttribute( 'title', tooltipText ); - } - - cluster.querySelectorAll< HTMLElement >( 'input, button, [tabindex]' ).forEach( el => { - if ( el.getAttribute( 'tabindex' ) !== '-1' ) { - el.setAttribute( 'tabindex', '-1' ); - } - } ); + cluster.setAttribute( 'inert', '' ); + cluster.setAttribute( 'title', tooltipText ); }; apply( wrapper ); @@ -282,7 +236,7 @@ export default function ActivityLog() { activityLogTypes: groupCountsData?.groups, } ); - const actions = useActivityActions( { isLoading: isFetching } ); + const actions = useActivityActions( { isLoading: isFetching, tracks } ); const onChangeView = useCallback( ( next: View ) => { diff --git a/projects/packages/activity-log/src/js/style.scss b/projects/packages/activity-log/src/js/style.scss index 0890c48b6fbf..153c486bfa7e 100644 --- a/projects/packages/activity-log/src/js/style.scss +++ b/projects/packages/activity-log/src/js/style.scss @@ -57,21 +57,15 @@ body.jetpack_page_jetpack-activity-log { } } - // Visual + click-blocking half of the free-tier "overlay" approach: - // the effect in ActivityLog/index.tsx toggles `aria-disabled` and - // `tabindex="-1"` on DataViews' real `.dataviews__search` cluster - // (search input + filter toggle), and adds a `title` for the - // upgrade tooltip. This rule dims the cluster, shows a not-allowed - // cursor, and shuts off pointer events on the controls inside so - // clicks fall through to the cluster (keeping it hoverable for the - // browser-native title tooltip). - .dataviews__search[aria-disabled="true"] { + // Visual half of the free-tier "overlay" approach: the effect in + // ActivityLog/index.tsx marks DataViews' real `.dataviews__search` + // cluster (search input + filter toggle) `inert` and adds a `title` + // for the upgrade tooltip. `inert` natively blocks pointer + + // keyboard interaction and hides descendants from a11y, so this + // rule only carries the visual disabled state. + .dataviews__search[inert] { opacity: 0.5; cursor: not-allowed; - - > * { - pointer-events: none; - } } // DataViews renders its own surface; don't double-pad it.