Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Activity Log: refreshed the free-tier upsell illustration to match Jetpack's branding.
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export function UpsellCallout() {
<div className="jp-activity-log__upsell-callout">
<div className="jp-activity-log__upsell-callout-content">
<h2 className="jp-activity-log__upsell-callout-title">
{ __( 'Track every action with Activity logs', 'jetpack-activity-log' ) }
{ __( 'Track every action with activity logs', 'jetpack-activity-log' ) }
</h2>
<Text as="p" variant="muted">
{ __(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,75 @@ 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 `<DataViews actions=… />`.
*/
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: <Icon icon={ backup } />,
// 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' );
},
};

// Keep the flag referenced so the param isn't flagged as unused.
void isLoading;

return [ backupAction ];
}, [ isLoading ] );
}, [ isLoading, tracks ] );
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 );
Expand Down Expand Up @@ -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 ) => {
Expand Down
20 changes: 7 additions & 13 deletions projects/packages/activity-log/src/js/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading