From 5ff57437f38769ed070b9101bb84f989cd671820 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Thu, 30 Apr 2026 11:10:29 -0300 Subject: [PATCH] Revert "Activity Log: Port AL into wp-admin as a native page (#48244)" This reverts commit b1cb072ed69502ba3b1addac1027c1d1d4395da4. --- pnpm-lock.yaml | 103 ----- .../base-styles/admin-page-layout.scss | 24 - .../changelog/admin-page-layout-flex-children | 4 - .../changelog/admin-page-add-unwrapped-prop | 4 - .../components/admin-page/index.tsx | 11 +- .../components/components/admin-page/types.ts | 9 - projects/packages/activity-log/.gitattributes | 15 - projects/packages/activity-log/.gitignore | 6 - .../packages/activity-log/.phan/config.php | 13 - projects/packages/activity-log/.phpcs.dir.xml | 24 - projects/packages/activity-log/AGENTS.md | 33 -- projects/packages/activity-log/CHANGELOG.md | 10 - projects/packages/activity-log/README.md | 15 - .../packages/activity-log/babel.config.js | 10 - .../packages/activity-log/changelog/.gitkeep | 0 .../changelog/add-package-scaffold | 4 - .../changelog/use-adminpage-unwrapped | 4 - projects/packages/activity-log/composer.json | 73 ---- .../packages/activity-log/eslint.config.mjs | 26 -- projects/packages/activity-log/package.json | 64 --- .../activity-log/src/class-initial-state.php | 84 ---- .../src/class-jetpack-activity-log.php | 190 -------- .../src/class-package-version.php | 35 -- .../src/class-rest-controller.php | 410 ------------------ .../components/ActivityLog/ActivityActor.tsx | 154 ------- .../components/ActivityLog/ActivityEvent.tsx | 101 ----- .../components/ActivityLog/UpsellCallout.tsx | 114 ----- .../src/js/components/ActivityLog/actions.tsx | 46 -- .../ActivityLog/activity-actor.scss | 21 - .../ActivityLog/activity-event.scss | 35 -- .../activity-logs-callout-illustration.svg | 52 --- .../ActivityLog/activity-transformer.ts | 90 ---- .../js/components/ActivityLog/admin-links.ts | 111 ----- .../src/js/components/ActivityLog/fields.tsx | 352 --------------- .../src/js/components/ActivityLog/filters.ts | 16 - .../ActivityLog/formatted-block/index.tsx | 164 ------- .../ActivityLog/formatted-block/parser.ts | 265 ----------- .../ActivityLog/formatted-block/types.ts | 34 -- .../js/components/ActivityLog/gridicons.ts | 79 ---- .../src/js/components/ActivityLog/index.tsx | 405 ----------------- .../src/js/components/ActivityLog/types.ts | 134 ------ .../ActivityLog/upsell-callout.scss | 49 --- .../src/js/components/ActivityLog/views.ts | 71 --- .../DateRangePicker/date-inputs.tsx | 137 ------ .../DateRangePicker/date-range-content.tsx | 365 ---------------- .../js/components/DateRangePicker/datetime.ts | 88 ---- .../js/components/DateRangePicker/index.tsx | 232 ---------- .../DateRangePicker/presets-listbox.tsx | 79 ---- .../js/components/DateRangePicker/style.scss | 76 ---- .../js/components/DateRangePicker/utils.ts | 195 --------- .../src/js/hooks/use-activity-log.ts | 101 ----- .../src/js/hooks/use-analytics.ts | 49 --- .../src/js/hooks/use-persistent-view.ts | 168 ------- .../packages/activity-log/src/js/index.js | 41 -- .../packages/activity-log/src/js/style.scss | 68 --- projects/packages/activity-log/tsconfig.json | 4 - projects/packages/activity-log/types.d.ts | 4 - .../packages/activity-log/webpack.config.js | 75 ---- .../changelog/hide-activity-log-native | 4 - .../wpcom-admin-menu/wpcom-admin-menu.php | 5 +- .../changelog/remove-activitylog-menu | 4 - .../my-jetpack/src/class-activitylog.php | 58 +++ .../my-jetpack/src/class-initializer.php | 3 + .../my-jetpack/tests/php/Activitylog_Test.php | 84 ++++ .../changelog/add-activity-log-package | 4 - projects/plugins/jetpack/class.jetpack.php | 2 - projects/plugins/jetpack/composer.json | 1 - projects/plugins/jetpack/composer.lock | 69 +-- 68 files changed, 151 insertions(+), 5229 deletions(-) delete mode 100644 projects/js-packages/base-styles/changelog/admin-page-layout-flex-children delete mode 100644 projects/js-packages/components/changelog/admin-page-add-unwrapped-prop delete mode 100644 projects/packages/activity-log/.gitattributes delete mode 100644 projects/packages/activity-log/.gitignore delete mode 100644 projects/packages/activity-log/.phan/config.php delete mode 100644 projects/packages/activity-log/.phpcs.dir.xml delete mode 100644 projects/packages/activity-log/AGENTS.md delete mode 100644 projects/packages/activity-log/CHANGELOG.md delete mode 100644 projects/packages/activity-log/README.md delete mode 100644 projects/packages/activity-log/babel.config.js delete mode 100644 projects/packages/activity-log/changelog/.gitkeep delete mode 100644 projects/packages/activity-log/changelog/add-package-scaffold delete mode 100644 projects/packages/activity-log/changelog/use-adminpage-unwrapped delete mode 100644 projects/packages/activity-log/composer.json delete mode 100644 projects/packages/activity-log/eslint.config.mjs delete mode 100644 projects/packages/activity-log/package.json delete mode 100644 projects/packages/activity-log/src/class-initial-state.php delete mode 100644 projects/packages/activity-log/src/class-jetpack-activity-log.php delete mode 100644 projects/packages/activity-log/src/class-package-version.php delete mode 100644 projects/packages/activity-log/src/class-rest-controller.php delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/ActivityActor.tsx delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/ActivityEvent.tsx delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/UpsellCallout.tsx delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/actions.tsx delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/activity-actor.scss delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/activity-event.scss delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/activity-logs-callout-illustration.svg delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/activity-transformer.ts delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/admin-links.ts delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/fields.tsx delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/filters.ts delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/index.tsx delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/parser.ts delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/types.ts delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/gridicons.ts delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/index.tsx delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/types.ts delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/upsell-callout.scss delete mode 100644 projects/packages/activity-log/src/js/components/ActivityLog/views.ts delete mode 100644 projects/packages/activity-log/src/js/components/DateRangePicker/date-inputs.tsx delete mode 100644 projects/packages/activity-log/src/js/components/DateRangePicker/date-range-content.tsx delete mode 100644 projects/packages/activity-log/src/js/components/DateRangePicker/datetime.ts delete mode 100644 projects/packages/activity-log/src/js/components/DateRangePicker/index.tsx delete mode 100644 projects/packages/activity-log/src/js/components/DateRangePicker/presets-listbox.tsx delete mode 100644 projects/packages/activity-log/src/js/components/DateRangePicker/style.scss delete mode 100644 projects/packages/activity-log/src/js/components/DateRangePicker/utils.ts delete mode 100644 projects/packages/activity-log/src/js/hooks/use-activity-log.ts delete mode 100644 projects/packages/activity-log/src/js/hooks/use-analytics.ts delete mode 100644 projects/packages/activity-log/src/js/hooks/use-persistent-view.ts delete mode 100644 projects/packages/activity-log/src/js/index.js delete mode 100644 projects/packages/activity-log/src/js/style.scss delete mode 100644 projects/packages/activity-log/tsconfig.json delete mode 100644 projects/packages/activity-log/types.d.ts delete mode 100644 projects/packages/activity-log/webpack.config.js delete mode 100644 projects/packages/jetpack-mu-wpcom/changelog/hide-activity-log-native delete mode 100644 projects/packages/my-jetpack/changelog/remove-activitylog-menu create mode 100644 projects/packages/my-jetpack/src/class-activitylog.php create mode 100644 projects/packages/my-jetpack/tests/php/Activitylog_Test.php delete mode 100644 projects/plugins/jetpack/changelog/add-activity-log-package diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4558a911acc7..2717fe56e3cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1831,109 +1831,6 @@ importers: projects/packages/account-protection: {} - projects/packages/activity-log: - dependencies: - '@automattic/jetpack-analytics': - specifier: workspace:* - version: link:../../js-packages/analytics - '@automattic/jetpack-components': - specifier: workspace:* - version: link:../../js-packages/components - '@automattic/jetpack-connection': - specifier: workspace:* - version: link:../../js-packages/connection - '@automattic/ui': - specifier: 1.0.2 - version: 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/react-query': - specifier: ^5.90.8 - version: 5.90.8(react@18.3.1) - '@wordpress/api-fetch': - specifier: 7.44.0 - version: 7.44.0 - '@wordpress/components': - specifier: 32.6.0 - version: 32.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/compose': - specifier: 7.44.0 - version: 7.44.0(react@18.3.1) - '@wordpress/data': - specifier: 10.44.0 - version: 10.44.0(react@18.3.1) - '@wordpress/dataviews': - specifier: 14.1.0 - version: 14.1.0(@types/react@18.3.28)(react@18.3.1)(stylelint@17.7.0) - '@wordpress/date': - specifier: 5.44.0 - version: 5.44.0 - '@wordpress/element': - specifier: 6.44.0 - version: 6.44.0 - '@wordpress/i18n': - specifier: 6.17.0 - version: 6.17.0 - '@wordpress/icons': - specifier: 12.2.0 - version: 12.2.0(react@18.3.1) - '@wordpress/url': - specifier: 4.44.0 - version: 4.44.0 - date-fns: - specifier: 4.1.0 - version: 4.1.0 - fast-deep-equal: - specifier: 3.1.3 - version: 3.1.3 - react: - specifier: 18.3.1 - version: 18.3.1 - react-dom: - specifier: 18.3.1 - version: 18.3.1(react@18.3.1) - devDependencies: - '@automattic/babel-plugin-replace-textdomain': - specifier: workspace:* - version: link:../../js-packages/babel-plugin-replace-textdomain - '@automattic/jetpack-base-styles': - specifier: workspace:* - version: link:../../js-packages/base-styles - '@automattic/jetpack-webpack-config': - specifier: workspace:* - version: link:../../js-packages/webpack-config - '@babel/core': - specifier: 7.29.0 - version: 7.29.0 - '@babel/preset-env': - specifier: 7.29.2 - version: 7.29.2(@babel/core@7.29.0) - '@babel/runtime': - specifier: 7.29.2 - version: 7.29.2 - '@types/react': - specifier: 18.3.28 - version: 18.3.28 - '@typescript/native-preview': - specifier: 7.0.0-dev.20260225.1 - version: 7.0.0-dev.20260225.1 - '@wordpress/browserslist-config': - specifier: 6.44.0 - version: 6.44.0 - concurrently: - specifier: 9.2.1 - version: 9.2.1 - sass-embedded: - specifier: 1.97.3 - version: 1.97.3 - sass-loader: - specifier: 16.0.5 - version: 16.0.5(sass-embedded@1.97.3)(webpack@5.105.2) - webpack: - specifier: 5.105.2 - version: 5.105.2(webpack-cli@6.0.1) - webpack-cli: - specifier: 6.0.1 - version: 6.0.1(webpack@5.105.2) - projects/packages/admin-ui: devDependencies: '@automattic/jetpack-base-styles': diff --git a/projects/js-packages/base-styles/admin-page-layout.scss b/projects/js-packages/base-styles/admin-page-layout.scss index 802ad499d0cd..e201ac4e3df9 100644 --- a/projects/js-packages/base-styles/admin-page-layout.scss +++ b/projects/js-packages/base-styles/admin-page-layout.scss @@ -196,35 +196,11 @@ $jp-breakpoint-mobile: 782px; // (dashboard grids with fixed column widths, wide tables, `100vw` // descendants) scroll horizontally inside the middle instead of // dragging the whole window into a horizontal scrollbar. - - // `` (title-branch) wraps children in - // `{children}` - // — that outer Container is `display: grid` and the single Col is a - // grid cell. Without intervention, the chain breaks here: any inner - // flex sizing on the consumer's wrapper (`flex: 1 1 auto; min-height: 0`) - // is inert under a non-flex parent, so DataViews-style pages let their - // content grow to its natural size and the outer Container scrolls - // the whole thing — instead of letting the consumer's own internal - // scroll surface (e.g. `.dataviews-layout__container`) handle it. - // Force the outer Container/Col pair to flex column so consumers can - // fill their bounded slot. Form-style pages keep working: their - // children stay content-sized (default `flex: 0 1 auto`) and any - // overflow still falls back to Container's `overflow: auto`. .admin-ui-page > :not(.admin-ui-page__header):not(.jetpack-footer) { flex: 1 1 auto; min-height: 0; min-width: 0; overflow: auto; - display: flex; - flex-direction: column; - - > * { - flex: 1 1 auto; - min-height: 0; - min-width: 0; - display: flex; - flex-direction: column; - } } // ── pinned at the bottom ───────────────────────── diff --git a/projects/js-packages/base-styles/changelog/admin-page-layout-flex-children b/projects/js-packages/base-styles/changelog/admin-page-layout-flex-children deleted file mode 100644 index 321f2c02d5ef..000000000000 --- a/projects/js-packages/base-styles/changelog/admin-page-layout-flex-children +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -admin-page-layout mixin: extend the flex chain into AdminPage's outer Container/Col so DataViews-style consumers can fill their bounded slot and let their own internal scroll handle the table body. diff --git a/projects/js-packages/components/changelog/admin-page-add-unwrapped-prop b/projects/js-packages/components/changelog/admin-page-add-unwrapped-prop deleted file mode 100644 index 9fbe04636dba..000000000000 --- a/projects/js-packages/components/changelog/admin-page-add-unwrapped-prop +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: added - -AdminPage: add `unwrapped` prop to render children directly inside the admin-ui Page, skipping the default Container/Col grid wrap. Use for full-bleed pages (DataViews-based admin surfaces) that own their own scroll/layout model. diff --git a/projects/js-packages/components/components/admin-page/index.tsx b/projects/js-packages/components/components/admin-page/index.tsx index 17d5b7bef26f..85db4371e324 100644 --- a/projects/js-packages/components/components/admin-page/index.tsx +++ b/projects/js-packages/components/components/admin-page/index.tsx @@ -41,7 +41,6 @@ const AdminPage: FC< AdminPageProps > = ( { breadcrumbs, tabs, showBottomBorder = true, - unwrapped = false, } ) => { useEffect( () => { restApi.setApiRoot( apiRoot ); @@ -96,13 +95,9 @@ const AdminPage: FC< AdminPageProps > = ( { showSidebarToggle={ false } > { tabs } - { unwrapped ? ( - children - ) : ( - - { children } - - ) } + + { children } + { showFooter && } diff --git a/projects/js-packages/components/components/admin-page/types.ts b/projects/js-packages/components/components/admin-page/types.ts index 79184b62d90e..b911caa740e9 100644 --- a/projects/js-packages/components/components/admin-page/types.ts +++ b/projects/js-packages/components/components/admin-page/types.ts @@ -94,13 +94,4 @@ export type AdminPageProps = { * Hidden when `tabs` is used. */ showBottomBorder?: boolean; - - /** - * Render `children` directly inside the admin-ui Page, skipping the - * default `{children}` - * wrap. Use for full-bleed pages (DataViews-based admin surfaces, full-app - * dashboards) that own their own scroll/layout model and don't want the - * outer Container's grid to break their flex chain. Defaults to `false`. - */ - unwrapped?: boolean; }; diff --git a/projects/packages/activity-log/.gitattributes b/projects/packages/activity-log/.gitattributes deleted file mode 100644 index 43e1b87b5811..000000000000 --- a/projects/packages/activity-log/.gitattributes +++ /dev/null @@ -1,15 +0,0 @@ -# Files not needed to be distributed. -.babelrc export-ignore -.gitattributes export-ignore -.github/ export-ignore -.gitignore export-ignore - -# Files not needed in the production build. -.phpcs.dir.xml production-exclude -/changelog/** production-exclude -/tests/** production-exclude -types.d.ts production-exclude -/src/js/** production-exclude - -# Files needed in the production build. -build/** production-include diff --git a/projects/packages/activity-log/.gitignore b/projects/packages/activity-log/.gitignore deleted file mode 100644 index 346f5b0e15f4..000000000000 --- a/projects/packages/activity-log/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -wordpress -node_modules -vendor -jetpack_vendor -.cache -build diff --git a/projects/packages/activity-log/.phan/config.php b/projects/packages/activity-log/.phan/config.php deleted file mode 100644 index 7e5e0a0d7a8d..000000000000 --- a/projects/packages/activity-log/.phan/config.php +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/projects/packages/activity-log/AGENTS.md b/projects/packages/activity-log/AGENTS.md deleted file mode 100644 index 4842575da84c..000000000000 --- a/projects/packages/activity-log/AGENTS.md +++ /dev/null @@ -1,33 +0,0 @@ -# Activity Log - -## UI primitives - -When adding React UI in this package, prefer the WordPress Design System -packages in this order: - -1. **`@wordpress/ui`** — foundational primitives. Check each component's - Storybook "Status" badge (anything other than "stable" is still in - flux); avoid experimental APIs here. -2. **`@wordpress/components`** — general-purpose legacy library. - Predates the design system. Use only when `@wordpress/ui` doesn't - have a stable equivalent, and still check Status in Storybook. -3. **`@wordpress/dataviews`** — higher-level data presentation (tables, - lists, grids). Already the backbone here. Extend via its - sub-components (`DataViews.Search`, `DataViews.FiltersToggle`, - `DataViews.Layout`, `DataViews.Footer`) before reaching for lower- - level primitives. -4. **`@wordpress/admin-ui`** — page layout primitives, accessed via - `AdminPage` from `@automattic/jetpack-components` (which wraps - admin-ui's `Page`). - -Rationale: WordPress is moving new work to `@wordpress/ui`; -`@wordpress/components` is being kept as a legacy fallback. Guidance -from the WordPress Design System P2 (April 2026). - -## Design-system lookup - -A dedicated MCP server is wired into this project's local Claude Code -config: `@wordpress/design-system-mcp`. It exposes the authoritative -list of stable `@wordpress/ui` + `@wordpress/components` components and -`--wpds-*` design tokens. Prefer querying it over spelunking through -`node_modules/@wordpress/components/src/**` for component metadata. diff --git a/projects/packages/activity-log/CHANGELOG.md b/projects/packages/activity-log/CHANGELOG.md deleted file mode 100644 index 304d2c8c1d8c..000000000000 --- a/projects/packages/activity-log/CHANGELOG.md +++ /dev/null @@ -1,10 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## 0.1.0-alpha - unreleased - -Initial release. diff --git a/projects/packages/activity-log/README.md b/projects/packages/activity-log/README.md deleted file mode 100644 index 9d8d1250d291..000000000000 --- a/projects/packages/activity-log/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Activity Log - -Activity Log UI for the Jetpack plugin in wp-admin. - -## Using this package in your WordPress plugin - -If you plan on using this package in your WordPress plugin, we would recommend that you use [Jetpack Autoloader](https://packagist.org/packages/automattic/jetpack-autoloader) as your autoloader. This will allow for maximum interoperability with other plugins that use this package as well. - -## Security - -Need to report a security vulnerability? Go to [https://automattic.com/security/](https://automattic.com/security/) or directly to our security bug bounty site [https://hackerone.com/automattic](https://hackerone.com/automattic). - -## License - -jetpack-activity-log is licensed under [GNU General Public License v2 (or later)](./LICENSE.txt) diff --git a/projects/packages/activity-log/babel.config.js b/projects/packages/activity-log/babel.config.js deleted file mode 100644 index c7d8a7f3fe38..000000000000 --- a/projects/packages/activity-log/babel.config.js +++ /dev/null @@ -1,10 +0,0 @@ -const config = { - presets: [ - [ - '@automattic/jetpack-webpack-config/babel/preset', - { pluginReplaceTextdomain: { textdomain: 'jetpack-activity-log' } }, - ], - ], -}; - -module.exports = config; diff --git a/projects/packages/activity-log/changelog/.gitkeep b/projects/packages/activity-log/changelog/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/projects/packages/activity-log/changelog/add-package-scaffold b/projects/packages/activity-log/changelog/add-package-scaffold deleted file mode 100644 index 42ae66790162..000000000000 --- a/projects/packages/activity-log/changelog/add-package-scaffold +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: added - -Initial release of the Activity Log package: hosts the in-wp-admin Activity Log UI and its REST endpoints. diff --git a/projects/packages/activity-log/changelog/use-adminpage-unwrapped b/projects/packages/activity-log/changelog/use-adminpage-unwrapped deleted file mode 100644 index 31818e76e374..000000000000 --- a/projects/packages/activity-log/changelog/use-adminpage-unwrapped +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Activity Log: opt into `` so DataViews can fill the bounded content slot and scroll its table body internally. Header, date picker, and DataViews toolbar stay pinned on short viewports. diff --git a/projects/packages/activity-log/composer.json b/projects/packages/activity-log/composer.json deleted file mode 100644 index 358f82167871..000000000000 --- a/projects/packages/activity-log/composer.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "automattic/jetpack-activity-log", - "description": "Activity Log UI for the Jetpack plugin in wp-admin.", - "type": "jetpack-library", - "license": "GPL-2.0-or-later", - "require": { - "php": ">=7.2", - "automattic/jetpack-admin-ui": "@dev", - "automattic/jetpack-assets": "@dev", - "automattic/jetpack-autoloader": "@dev", - "automattic/jetpack-composer-plugin": "@dev", - "automattic/jetpack-connection": "@dev", - "automattic/jetpack-status": "@dev" - }, - "require-dev": { - "automattic/jetpack-changelogger": "@dev", - "yoast/phpunit-polyfills": "^4.0.0", - "automattic/jetpack-test-environment": "@dev", - "automattic/phpunit-select-config": "@dev" - }, - "suggest": { - "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "scripts": { - "build-development": [ - "pnpm run build" - ], - "build-production": [ - "pnpm run build-production-concurrently" - ], - "watch": [ - "Composer\\Config::disableProcessTimeout", - "pnpm run watch" - ] - }, - "repositories": [ - { - "type": "path", - "url": "../*", - "options": { - "monorepo": true - } - } - ], - "minimum-stability": "dev", - "prefer-stable": true, - "extra": { - "autotagger": true, - "mirror-repo": "Automattic/jetpack-activity-log", - "textdomain": "jetpack-activity-log", - "version-constants": { - "::PACKAGE_VERSION": "src/class-package-version.php" - }, - "changelogger": { - "link-template": "https://github.com/Automattic/jetpack-activity-log/compare/v${old}...v${new}" - }, - "branch-alias": { - "dev-trunk": "0.1.x-dev" - } - }, - "config": { - "allow-plugins": { - "roots/wordpress-core-installer": true, - "automattic/jetpack-autoloader": true, - "automattic/jetpack-composer-plugin": true - } - } -} diff --git a/projects/packages/activity-log/eslint.config.mjs b/projects/packages/activity-log/eslint.config.mjs deleted file mode 100644 index 65c80344e400..000000000000 --- a/projects/packages/activity-log/eslint.config.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import { makeBaseConfig, defineConfig } from 'jetpack-js-tools/eslintrc/base.mjs'; - -/** - * The `DateRangePicker/` subdirectory is a near-verbatim port of - * Calypso's `components/date-range-picker/` — forcing full JSDoc on - * every internal helper would add churn on each upstream re-sync - * without adding clarity (param names match signatures, behavior - * matches the Calypso docs). Soften the Jetpack eslint profile for - * those files only. - */ -export default defineConfig( makeBaseConfig( import.meta.url ), { - files: [ 'src/js/components/DateRangePicker/**' ], - rules: { - 'jsdoc/require-description': 'off', - 'jsdoc/require-param-description': 'off', - 'jsdoc/require-returns': 'off', - 'jsdoc/require-returns-description': 'off', - '@wordpress/no-unused-vars-before-return': 'off', - // Calypso's picker passes inline arrow callbacks to a bunch of - // child components (Dropdown renderToggle/renderContent, - // DateInputs onFrom/onTo handlers). Keeping the verbatim form - // makes upstream re-syncs mechanical; the callbacks aren't - // performance-critical at this scale. - 'react/jsx-no-bind': 'off', - }, -} ); diff --git a/projects/packages/activity-log/package.json b/projects/packages/activity-log/package.json deleted file mode 100644 index c15567423b3b..000000000000 --- a/projects/packages/activity-log/package.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "private": true, - "description": "Activity Log UI for the Jetpack plugin in wp-admin.", - "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/activity-log/#readme", - "bugs": { - "url": "https://github.com/Automattic/jetpack/labels/[Package] Activity Log" - }, - "repository": { - "type": "git", - "url": "https://github.com/Automattic/jetpack.git", - "directory": "projects/packages/activity-log" - }, - "license": "GPL-2.0-or-later", - "author": "Automattic", - "scripts": { - "build": "pnpm run clean && pnpm run build-client", - "build-client": "webpack", - "build-production-concurrently": "pnpm run clean && concurrently 'NODE_ENV=production BABEL_ENV=production pnpm run build-client' && pnpm run validate", - "clean": "rm -rf build/", - "typecheck": "tsgo --noEmit", - "validate": "pnpm exec validate-es build/", - "watch": "pnpm run build && webpack watch" - }, - "browserslist": [ - "extends @wordpress/browserslist-config" - ], - "dependencies": { - "@automattic/jetpack-analytics": "workspace:*", - "@automattic/jetpack-components": "workspace:*", - "@automattic/jetpack-connection": "workspace:*", - "@automattic/ui": "1.0.2", - "@tanstack/react-query": "^5.90.8", - "@wordpress/api-fetch": "7.44.0", - "@wordpress/components": "32.6.0", - "@wordpress/compose": "7.44.0", - "@wordpress/data": "10.44.0", - "@wordpress/dataviews": "14.1.0", - "@wordpress/date": "5.44.0", - "@wordpress/element": "6.44.0", - "@wordpress/i18n": "6.17.0", - "@wordpress/icons": "12.2.0", - "@wordpress/url": "4.44.0", - "date-fns": "4.1.0", - "fast-deep-equal": "3.1.3", - "react": "18.3.1", - "react-dom": "18.3.1" - }, - "devDependencies": { - "@automattic/babel-plugin-replace-textdomain": "workspace:*", - "@automattic/jetpack-base-styles": "workspace:*", - "@automattic/jetpack-webpack-config": "workspace:*", - "@babel/core": "7.29.0", - "@babel/preset-env": "7.29.2", - "@babel/runtime": "7.29.2", - "@types/react": "18.3.28", - "@typescript/native-preview": "7.0.0-dev.20260225.1", - "@wordpress/browserslist-config": "6.44.0", - "concurrently": "9.2.1", - "sass-embedded": "1.97.3", - "sass-loader": "16.0.5", - "webpack": "5.105.2", - "webpack-cli": "6.0.1" - } -} diff --git a/projects/packages/activity-log/src/class-initial-state.php b/projects/packages/activity-log/src/class-initial-state.php deleted file mode 100644 index d767e43f4865..000000000000 --- a/projects/packages/activity-log/src/class-initial-state.php +++ /dev/null @@ -1,84 +0,0 @@ - array( - 'WP_API_root' => esc_url_raw( rest_url() ), - 'WP_API_nonce' => wp_create_nonce( 'wp_rest' ), - ), - 'jetpackStatus' => array( - 'calypsoSlug' => ( new Status() )->get_site_suffix(), - ), - 'siteData' => array( - 'id' => Jetpack_Options::get_option( 'id' ), - 'title' => get_bloginfo( 'name' ) ? get_bloginfo( 'name' ) : get_site_url(), - 'adminUrl' => esc_url_raw( admin_url() ), - 'slug' => is_string( $home_host ) ? $home_host : '', - 'gmtOffset' => is_numeric( $gmt_offset ) ? (float) $gmt_offset : 0.0, - 'timezoneString' => is_string( $timezone_string ) ? $timezone_string : '', - 'locale' => str_replace( '_', '-', (string) get_locale() ), - // The paid-plan capability check. Drives the free-tier - // upsell callout and matches the server-side clamp in - // REST_Controller::get_activity_log(). The result is - // cached in a site transient by the REST controller - // (5-minute TTL); on a cache miss this call issues a - // synchronous WPCOM request (~200–800ms typical, up to - // the 2s internal timeout) that blocks page rendering - // until it returns. - 'hasActivityLogsAccess' => REST_Controller::has_activity_logs_access(), - ), - 'nonces' => array( - // Consumed by `UpsellCallout` to build a `redirect_to` - // URL that invalidates the access cache on return from - // checkout. See `Jetpack_Activity_Log::admin_init()`. - 'refreshAccess' => wp_create_nonce( Jetpack_Activity_Log::REFRESH_ACCESS_NONCE_ACTION ), - ), - 'assets' => array( - 'buildUrl' => plugins_url( '../build/', __FILE__ ), - ), - ); - } - - /** - * Render the initial state into a JavaScript variable. - * - * @return string - */ - public function render() { - return 'var JPACTIVITYLOG_INITIAL_STATE=' . wp_json_encode( $this->get_data(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';'; - } -} diff --git a/projects/packages/activity-log/src/class-jetpack-activity-log.php b/projects/packages/activity-log/src/class-jetpack-activity-log.php deleted file mode 100644 index c89f5ab8c316..000000000000 --- a/projects/packages/activity-log/src/class-jetpack-activity-log.php +++ /dev/null @@ -1,190 +0,0 @@ -is_user_connected(); - } - - /** - * Fires when the admin page is loaded. - * - * When the user is returning from a successful checkout, the upsell - * CTA appends `?refresh_access=1&_wpnonce=…` to the `redirect_to` - * value it hands off to WordPress.com. Detect that here, verify the - * nonce, and drop the cached paid-plan signal so - * `Initial_State::get_data()` (which runs later in the same request, - * when the bundle is enqueued) rehydrates from WPCOM instead of - * re-serving the pre-checkout value. Mirrors the pattern in - * `Automattic\Jetpack\Publicize\Social_Admin_Page::admin_init()`. - */ - public static function admin_init() { - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- verified with wp_verify_nonce below. - if ( isset( $_GET['refresh_access'] ) && isset( $_GET['_wpnonce'] ) ) { - $nonce = sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ); - if ( wp_verify_nonce( $nonce, self::REFRESH_ACCESS_NONCE_ACTION ) ) { - REST_Controller::clear_access_cache(); - } - } - - add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_scripts' ) ); - } - - /** - * Enqueue the admin bundle and seed initial state. - */ - public static function enqueue_admin_scripts() { - Assets::register_script( - self::SCRIPT_HANDLE, - '../build/index.js', - __FILE__, - array( - 'in_footer' => true, - 'textdomain' => 'jetpack-activity-log', - ) - ); - Assets::enqueue_script( self::SCRIPT_HANDLE ); - - wp_add_inline_script( self::SCRIPT_HANDLE, ( new Activity_Log_Initial_State() )->render(), 'before' ); - Connection_Initial_State::render_script( self::SCRIPT_HANDLE ); - } - - /** - * Render the admin page root node. React mounts into this element. - */ - public static function render_page() { - ?> -
- array( - 'description' => __( 'Number of items to return per page.', 'jetpack-activity-log' ), - 'type' => 'integer', - 'minimum' => 1, - 'maximum' => 1000, - ), - 'page' => array( - 'description' => __( '1-indexed page number.', 'jetpack-activity-log' ), - 'type' => 'integer', - 'minimum' => 1, - ), - 'sort_order' => array( - 'description' => __( 'Sort direction.', 'jetpack-activity-log' ), - 'type' => 'string', - 'enum' => array( 'asc', 'desc' ), - ), - 'after' => array( - 'description' => __( 'ISO 8601 lower bound on event timestamp.', 'jetpack-activity-log' ), - 'type' => 'string', - 'format' => 'date-time', - ), - 'before' => array( - 'description' => __( 'ISO 8601 upper bound on event timestamp.', 'jetpack-activity-log' ), - 'type' => 'string', - 'format' => 'date-time', - ), - 'group' => array( - 'description' => __( 'Only return events in these groups.', 'jetpack-activity-log' ), - 'type' => 'array', - 'items' => array( 'type' => 'string' ), - ), - 'not_group' => array( - 'description' => __( 'Exclude events in these groups.', 'jetpack-activity-log' ), - 'type' => 'array', - 'items' => array( 'type' => 'string' ), - ), - 'text_search' => array( - 'description' => __( 'Full-text search string.', 'jetpack-activity-log' ), - 'type' => 'string', - ), - ); - } - - /** - * Query params accepted by the group-counts endpoint. A subset of the - * list params — no pagination or sort, no text search. - * - * @return array - */ - private static function group_counts_args() { - return array( - 'number' => array( - 'description' => __( 'Cap on the number of events considered when counting groups.', 'jetpack-activity-log' ), - 'type' => 'integer', - 'minimum' => 1, - 'maximum' => 1000, - ), - 'after' => array( - 'description' => __( 'ISO 8601 lower bound on event timestamp.', 'jetpack-activity-log' ), - 'type' => 'string', - 'format' => 'date-time', - ), - 'before' => array( - 'description' => __( 'ISO 8601 upper bound on event timestamp.', 'jetpack-activity-log' ), - 'type' => 'string', - 'format' => 'date-time', - ), - 'group' => array( - 'description' => __( 'Only count events in these groups.', 'jetpack-activity-log' ), - 'type' => 'array', - 'items' => array( 'type' => 'string' ), - ), - 'not_group' => array( - 'description' => __( 'Exclude events in these groups.', 'jetpack-activity-log' ), - 'type' => 'array', - 'items' => array( 'type' => 'string' ), - ), - ); - } - - /** - * Register the Activity Log REST routes. - * - * Hooked on `rest_api_init` by {@see Jetpack_Activity_Log::initialize()}. - */ - public static function register_rest_routes() { - register_rest_route( - self::REST_NAMESPACE, - '/activity-log', - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( __CLASS__, 'get_activity_log' ), - 'permission_callback' => array( __CLASS__, 'permissions_callback' ), - 'args' => self::list_args(), - ) - ); - - register_rest_route( - self::REST_NAMESPACE, - '/activity-log/count/group', - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( __CLASS__, 'get_activity_log_group_counts' ), - 'permission_callback' => array( __CLASS__, 'permissions_callback' ), - 'args' => self::group_counts_args(), - ) - ); - } - - /** - * Permission callback. Mirrors the menu gating — any admin on a - * non-multisite install with a user-level WPCOM connection can read - * the log. A user-level connection is required because the upstream - * WPCOM endpoint is user-gated (it needs to identify *which* admin - * is asking); signing as the blog gets rejected with "Only - * Administrators can query information about the current site." - * - * @return bool|WP_Error - */ - public static function permissions_callback() { - if ( ! current_user_can( 'manage_options' ) ) { - return false; - } - - if ( ! ( new Connection_Manager() )->is_user_connected() ) { - return new WP_Error( - 'activity_log_user_not_connected', - esc_html__( 'Your WordPress.com account is not connected to this site. Connect it to use the Activity Log.', 'jetpack-activity-log' ), - array( 'status' => 403 ) - ); - } - - return true; - } - - /** - * Whether the site's current plan unlocks the full activity log. - * - * Reads the WPCOM `/sites/{id}/rewind` state endpoint (same signal - * `Jetpack_Backup::has_backup_plan()` uses) and caches the boolean for - * {@see self::CAPABILITY_CACHE_TTL} seconds in a site transient so the - * list endpoint doesn't pay the round-trip on every pagination page. - * The cache is per-blog (fine for multisite) and keyed on `blog_id`. - * - * @return bool True when the site has a paid Backup-enabled plan. - */ - public static function has_activity_logs_access() { - $blog_id = (int) Jetpack_Options::get_option( 'id' ); - if ( ! $blog_id ) { - return false; - } - - $cache_key = self::CAPABILITY_CACHE_KEY . $blog_id; - $cached = get_site_transient( $cache_key ); - if ( false !== $cached ) { - return 'yes' === $cached; - } - - $response = Client::wpcom_json_api_request_as_blog( - sprintf( '/sites/%d/rewind?force=wpcom', $blog_id ), - '2', - array( 'timeout' => 2 ), - null, - 'wpcom' - ); - - if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { - // Fail closed: assume no access if we can't reach WPCOM. Cache for - // a short window to avoid hammering the endpoint on every call. - set_site_transient( $cache_key, 'no', 10 ); - return false; - } - - $body = json_decode( wp_remote_retrieve_body( $response ) ); - $state = is_object( $body ) && isset( $body->state ) ? (string) $body->state : ''; - $has_it = $state !== '' && $state !== 'unavailable'; - set_site_transient( $cache_key, $has_it ? 'yes' : 'no', self::CAPABILITY_CACHE_TTL ); - return $has_it; - } - - /** - * Clear the cached has-access flag. Exposed so front-end flows that - * know the plan just changed (e.g. a successful checkout redirect) can - * force a refresh on the next request. - * - * @return void - */ - public static function clear_access_cache() { - $blog_id = (int) Jetpack_Options::get_option( 'id' ); - if ( $blog_id ) { - delete_site_transient( self::CAPABILITY_CACHE_KEY . $blog_id ); - } - } - - /** - * Free-tier params that survive the filter strip in - * {@see self::get_activity_log()}. Anything outside this list is - * nulled out before the request reaches WPCOM. - * - * @var string[] - */ - const FREE_TIER_ALLOWED_PARAMS = array( 'number', 'page', 'sort_order' ); - - /** - * Proxy the paginated activity list. - * - * Enforces the free-tier boundary server-side. When the site doesn't - * have access: - * - * 1. `number` is clamped to {@see self::FREE_TIER_ITEM_CAP}. - * 2. `page` is forced to 1. - * 3. All filter inputs (`after`, `before`, `group`, `not_group`, - * `text_search`) are dropped. - * - * Together these mean a client-side bypass (DevTools, direct - * `wp.apiFetch`) is bounded to "the 20 most recent events overall" — - * the same dataset the locked-down UI surfaces. Without (3), - * date-walking via `before` would let a free-tier caller page through - * the entire history 20 rows at a time. - * - * @param WP_REST_Request $request Request. - * @return mixed - */ - public static function get_activity_log( WP_REST_Request $request ) { - if ( ! self::has_activity_logs_access() ) { - // Mutating the request in-place is deliberate: any - // downstream `rest_request_*` filter sees the clamped - // values, not the caller's originals. For a security - // clamp that's the right side of the trade — no filter - // can undo the limit. - $requested = (int) $request->get_param( 'number' ); - $request->set_param( - 'number', - $requested > 0 ? min( $requested, self::FREE_TIER_ITEM_CAP ) : self::FREE_TIER_ITEM_CAP - ); - $request->set_param( 'page', 1 ); - - foreach ( array_keys( self::list_args() ) as $key ) { - if ( ! in_array( $key, self::FREE_TIER_ALLOWED_PARAMS, true ) ) { - $request->set_param( $key, null ); - } - } - } - return self::proxy_get( '/activity', $request, array_keys( self::list_args() ) ); - } - - /** - * Proxy the group-counts endpoint. - * - * Deliberately not tier-clamped — the free-tier list clamp - * (`number` → 20 / `page` → 1) is the security boundary; the group - * counts are cosmetic metadata that powers the filter dropdown. A - * stable, full-history count keeps the dropdown from flickering as - * users type in the search field, matching Calypso's behavior at - * `wp-calypso:client/dashboard/sites/logs-activity/dataviews/ - * index.tsx:100-102`. - * - * @param WP_REST_Request $request Request. - * @return mixed - */ - public static function get_activity_log_group_counts( WP_REST_Request $request ) { - return self::proxy_get( '/activity/count/group', $request, array_keys( self::group_counts_args() ) ); - } - - /** - * Shared helper: forward whitelisted query params from $request to the - * equivalent WPCOM v2 path under `/sites/{blog_id}`. - * - * @param string $wpcom_path Path relative to the site, starting with "/". - * @param WP_REST_Request $request Incoming request. - * @param array $allowed_keys Params to forward. Any unset keys are dropped. - * @return mixed Decoded JSON response from WPCOM, or WP_Error on failure. - */ - private static function proxy_get( $wpcom_path, WP_REST_Request $request, array $allowed_keys ) { - $blog_id = Jetpack_Options::get_option( 'id' ); - if ( ! $blog_id ) { - return new WP_Error( - 'activity_log_not_connected', - esc_html__( 'This site is not connected to WordPress.com.', 'jetpack-activity-log' ), - array( 'status' => 400 ) - ); - } - - $params = array(); - foreach ( $allowed_keys as $key ) { - $value = $request->get_param( $key ); - if ( $value !== null ) { - $params[ $key ] = $value; - } - } - - $path = sprintf( '/sites/%d%s', (int) $blog_id, $wpcom_path ); - if ( ! empty( $params ) ) { - $path .= '?' . http_build_query( $params ); - } - - // Sign as the current user, not the blog: the upstream /sites/{id}/activity - // endpoint checks that a specific admin is asking. Forward the visitor IP - // so WPCOM logs match the existing /jetpack/v4/site/activity proxy. - $response = Client::wpcom_json_api_request_as_user( - $path, - '2', - array( - 'method' => 'GET', - 'headers' => array( - 'X-Forwarded-For' => ( new Visitor() )->get_ip( true ), - ), - ), - null, - 'wpcom' - ); - - if ( is_wp_error( $response ) ) { - return new WP_Error( - 'activity_log_request_failed', - $response->get_error_message(), - array( 'status' => 500 ) - ); - } - - $status = (int) wp_remote_retrieve_response_code( $response ); - $body = json_decode( wp_remote_retrieve_body( $response ), true ); - - if ( 200 !== $status ) { - return new WP_Error( - 'activity_log_request_failed', - isset( $body['message'] ) ? (string) $body['message'] : esc_html__( 'Unable to fetch activity log.', 'jetpack-activity-log' ), - array( 'status' => $status ? $status : 500 ) - ); - } - - return rest_ensure_response( $body ); - } -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/ActivityActor.tsx b/projects/packages/activity-log/src/js/components/ActivityLog/ActivityActor.tsx deleted file mode 100644 index 9260668f4b97..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/ActivityActor.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { JetpackLogo } from '@automattic/jetpack-components'; -import { __experimentalHStack as HStack, Icon } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis -import { __, sprintf } from '@wordpress/i18n'; -import { commentAuthorAvatar, globe, wordpress } from '@wordpress/icons'; -import type { ActivityActorDetails } from './types'; -import type { ReactNode } from 'react'; -import './activity-actor.scss'; - -const ICON_SIZE = 24; - -/** - * Build the short "via " string shown under the actor name - * when the originating agent was an MCP client. - * - * @param actor - The actor details for the current log row. - * @return The formatted string, or null if the actor isn't an MCP agent. - */ -function getMcpIndicator( actor?: ActivityActorDetails ): string | null { - if ( ! actor?.isMcpAgent ) { - return null; - } - return actor.mcpClient - ? sprintf( - /* translators: %s: MCP client name and version */ - __( 'via %s (MCP)', 'jetpack-activity-log' ), - actor.mcpClient - ) - : __( 'via MCP', 'jetpack-activity-log' ); -} - -/** - * Decide the icon + label for a given actor. Matches Calypso's mapping so - * WordPress / Jetpack / Server / Happiness Engineer / avatar branches render - * the same thing users already recognize. - * - * @param actor - The actor details for the current log row. - * @return Icon element (or null) + display label. - */ -function getActorPresentation( actor?: ActivityActorDetails ): { icon: ReactNode; label: string } { - // tsgo types `__()`'s return as a branded `TransformedText<…>` rather - // than plain `string`; annotating the variable keeps later - // `actorName = name || actorName` assignments widened to `string`. - let actorName: string = __( 'Unknown', 'jetpack-activity-log' ); - - if ( ! actor ) { - return { icon: null, label: actorName }; - } - - const { actorName: name, actorType, actorAvatarUrl } = actor; - actorName = name || actorName; - - switch ( actorType ) { - case 'Application': - if ( name === 'WordPress' ) { - return { - icon: ( - - ), - label: name, - }; - } - if ( name === 'Jetpack' || name === 'Jetpack Boost' ) { - return { - icon: ( - - ), - label: name, - }; - } - break; - case 'Person': - if ( name === 'Server' ) { - return { - icon: ( - - ), - label: __( 'Server', 'jetpack-activity-log' ), - }; - } - break; - case 'Happiness Engineer': - return { - icon: ( - - ), - label: __( 'Happiness Engineer', 'jetpack-activity-log' ), - }; - } - - if ( actorAvatarUrl ) { - return { - icon: ( - { - ), - label: actorName, - }; - } - - return { - icon: ( - - ), - label: actorName, - }; -} - -/** - * DataViews cell renderer for the "User" column. Shows the actor's avatar - * (or a branded icon for system actors) next to their name. - * - * @param props - Component props. - * @param props.actor - Actor details for the current log row. - * @return The actor cell. - */ -export function ActivityActor( { actor }: { actor?: ActivityActorDetails } ) { - const { icon, label } = getActorPresentation( actor ); - const mcpIndicator = getMcpIndicator( actor ); - - return ( - - { icon } - - { label } - { mcpIndicator && { mcpIndicator } } - - - ); -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/ActivityEvent.tsx b/projects/packages/activity-log/src/js/components/ActivityLog/ActivityEvent.tsx deleted file mode 100644 index 763be0fc2c6f..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/ActivityEvent.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis -import { Icon } from '@wordpress/icons'; -import { buildObjectAdminLink } from './admin-links'; -import { renderFormattedContent } from './formatted-block'; -import { gridiconToWordPressIcon } from './gridicons'; -import type { Activity } from './types'; -import './activity-event.scss'; - -/** - * Just the gridicon for an activity row. Exposed as its own component - * so the Activity layout can mount it in DataViews' `mediaField` slot - * (rendering left-aligned outside the title block). - * - * @param props - Component props. - * @param props.activity - Normalized Activity for the current log row. - * @return The icon element, or null when the activity has none. - */ -export function ActivityEventIcon( { activity }: { activity: Activity } ) { - const { activityIcon } = activity; - if ( ! activityIcon ) { - return null; - } - return ( - - ); -} - -/** - * Just the bold activity title (e.g. "Plugin activated"). Plugged into - * DataViews' `titleField` slot for the Activity layout. - * - * @param props - Component props. - * @param props.activity - Normalized Activity for the current log row. - * @return The title element. - */ -export function ActivityEventTitle( { activity }: { activity: Activity } ) { - // `.site-activity-logs__event-title` both lightens the default - // `` font-weight to 500 *and* — because the class is on a - // non-span element — keeps the `> span { color: grey }` rule on the - // enclosing `.site-activity-logs__event-content` from dimming the - // title in the Table layout. - return { activity.activityTitle }; -} - -/** - * The formatted description body — parsed ranges (entity links, - * release-notes, etc.) with an `object`-level link fallback for - * range-less payloads like `post__published`. Plugs into DataViews' - * `descriptionField` slot for the Activity layout. - * - * @param props - Component props. - * @param props.activity - Normalized Activity for the current log row. - * @return The description element, or null when the activity has none. - */ -export function ActivityEventDescription( { activity }: { activity: Activity } ) { - const { activityDescription, activityObject } = activity; - const hasRanges = activityDescription.items.some( - item => typeof item === 'object' && item !== null && 'type' in item - ); - const objectHref = hasRanges ? null : buildObjectAdminLink( activityObject ); - const formattedContent = activityDescription.items.length - ? renderFormattedContent( { items: activityDescription.items } ) - : null; - if ( ! formattedContent ) { - return null; - } - return ( - { objectHref ? { formattedContent } : formattedContent } - ); -} - -/** - * DataViews cell renderer for the Table layout's "Event" column. - * Composes the icon, title, and description into a single cell. The - * Activity layout uses the three sub-components directly in their - * respective field slots instead. - * - * @param props - Component props. - * @param props.activity - Normalized Activity for the current log row. - * @return The event cell. - */ -export function ActivityEvent( { activity }: { activity: Activity } ) { - return ( - - - - - - - - ); -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/UpsellCallout.tsx b/projects/packages/activity-log/src/js/components/ActivityLog/UpsellCallout.tsx deleted file mode 100644 index 44bce8790691..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/UpsellCallout.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Free-tier upsell shown beneath the Activity Log table. Title, copy, - * and illustration are a 1:1 port of Calypso's `ActivityLogsCallout` - * (client/dashboard/sites/logs-activity/activity-logs-callout.tsx). - * The CTA is wp-admin-native: it routes through Jetpack's standard - * `useProductCheckoutWorkflow` into wordpress.com/checkout/{siteSuffix}/ - * {productSlug}?source=activity-log-page-purchase&redirect_to=. - * - * Destination product: `jetpack_security_t1_yearly` — the Security - * bundle unlocks 30 days of activity history (the cap documented on - * cloud.jetpack.com/features/comparison). - */ -import { useProductCheckoutWorkflow } from '@automattic/jetpack-connection'; -import { Button, __experimentalText as Text } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis -import { __ } from '@wordpress/i18n'; -import { addQueryArgs } from '@wordpress/url'; -import { useCallback } from 'react'; -import { useAnalytics } from '../../hooks/use-analytics'; -import illustrationUrl from './activity-logs-callout-illustration.svg'; -import './upsell-callout.scss'; - -const PRODUCT_SLUG = 'jetpack_security_t1_yearly'; -const UPSELL_SOURCE = 'activity-log-page-purchase'; - -interface InitialStateWithNonce { - nonces?: { refreshAccess?: string }; -} - -declare const JPACTIVITYLOG_INITIAL_STATE: InitialStateWithNonce | undefined; - -/** - * Compute the URL we want WordPress.com to send the user back to after - * checkout. We stay on the Activity Log page but append - * `refresh_access=1&_wpnonce=…`, which `Jetpack_Activity_Log::admin_init()` - * detects to drop the paid-plan access cache — eliminating the 5-minute - * "still showing the upsell after upgrade" window. - * - * @return Absolute URL to pass as `redirectUrl`. - */ -const buildPostCheckoutReturnUrl = (): string => { - if ( typeof window === 'undefined' ) { - return ''; - } - const nonce = - typeof JPACTIVITYLOG_INITIAL_STATE !== 'undefined' - ? JPACTIVITYLOG_INITIAL_STATE?.nonces?.refreshAccess - : undefined; - if ( ! nonce ) { - return window.location.href; - } - return addQueryArgs( window.location.href, { refresh_access: '1', _wpnonce: nonce } ); -}; - -/** - * DataViews-adjacent upsell banner. Rendered as a sibling to the table - * (not nested inside DataViews) so it sits below the locked view and - * aligns with the page's AdminPage container. - * - * @return The callout element. - */ -export function UpsellCallout() { - const { tracks } = useAnalytics(); - const { run, hasCheckoutStarted } = useProductCheckoutWorkflow( { - productSlug: PRODUCT_SLUG, - redirectUrl: buildPostCheckoutReturnUrl(), - from: UPSELL_SOURCE, - } ); - - const onClickUpgrade = useCallback( () => { - tracks.recordEvent( 'jetpack_activity_log_upsell_cta_click', { - source: 'free_tier_callout', - } ); - run(); - }, [ run, tracks ] ); - - return ( -
- -
-

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

- - { __( - 'Debug issues faster with insights from a comprehensive audit log of all your admin activities.', - 'jetpack-activity-log' - ) } - - - { __( - 'With your free plan, you can see your 20 most recent events. Upgrade for 30 days of history, plus filtering and date range controls.', - 'jetpack-activity-log' - ) } - - - { __( 'Available on the Jetpack Security and Complete plans.', '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 deleted file mode 100644 index 90b604bf6a32..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/actions.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Icon } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { backup } from '@wordpress/icons'; -import { useMemo } from 'react'; -import type { Activity } from './types'; -import type { Action } from '@wordpress/dataviews'; - -type UseActivityActionsOptions = { - isLoading: boolean; -}; - -/** - * 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. - * - * @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. - * @return The actions array for ``. - */ -export function useActivityActions( { - isLoading, -}: UseActivityActionsOptions ): Action< Activity >[] { - return useMemo( () => { - const backupAction: Action< Activity > = { - id: 'backup', - isPrimary: true, - label: __( 'Manage 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 */ - }, - }; - - // Keep the flag referenced so the param isn't flagged as unused. - void isLoading; - - return [ backupAction ]; - }, [ isLoading ] ); -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/activity-actor.scss b/projects/packages/activity-log/src/js/components/ActivityLog/activity-actor.scss deleted file mode 100644 index 27fda3f575bf..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/activity-actor.scss +++ /dev/null @@ -1,21 +0,0 @@ -.site-activity-logs__actor { - - > [class^="site-activity-logs__actor-icon-"] { - border-radius: 50%; - height: 24px; - width: 24px; - } - - .site-activity-logs__actor-icon-server, - .site-activity-logs__actor-icon-default { - background-color: var(--wpds-color-bg-surface-neutral-weak, #dcdcde); - fill: var(--wpds-color-fg-content-neutral-weak, #50575e); - } - - .site-activity-logs__actor-mcp { - display: block; - font-size: 0.75rem; - color: var(--wpds-color-fg-content-neutral-weak, #757575); - margin-block-start: 2px; - } -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/activity-event.scss b/projects/packages/activity-log/src/js/components/ActivityLog/activity-event.scss deleted file mode 100644 index f30f4e93a965..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/activity-event.scss +++ /dev/null @@ -1,35 +0,0 @@ -.site-activity-logs__event { - color: var(--wpds-color-fg-content-neutral, #1e1e1e); - - // Event row icon — fixed 32×32 tile inside the Table layout's composite - // cell so the row alignment is stable regardless of surrounding - // cascade. The inner SVG renders at 24×24 (via the Icon size prop) and - // the 4px padding completes the tile. Scoped to `.site-activity-logs__event` - // so the *same* `` used standalone in the Activity - // layout's mediaField slot picks up DataViews' own circular bullet - // styling instead of our square-tile overrides (otherwise min-width:32px - // wins against DataViews' width:100% and produces a non-square rect). - .site-activity-logs__event-icon { - box-sizing: border-box; - width: 32px; - height: 32px; - min-width: 32px; - padding: 4px; - border-radius: 2px; - fill: var(--wpds-color-fg-content-neutral-weak, #757575); - background-color: var(--wpds-color-bg-surface-neutral-weak, #f0f0f1); - } -} - -.site-activity-logs__event-content { - flex: 1 0 0; - flex-wrap: wrap; - - > span { - color: var(--wpds-color-fg-content-neutral-weak, #50575e); - } -} - -.site-activity-logs__event-title { - font-weight: 500; -} 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 deleted file mode 100644 index e6b0fb17ee8c..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/activity-logs-callout-illustration.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/activity-transformer.ts b/projects/packages/activity-log/src/js/components/ActivityLog/activity-transformer.ts deleted file mode 100644 index 4f287246b1bc..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/activity-transformer.ts +++ /dev/null @@ -1,90 +0,0 @@ -import parseActivityLogEntryContent from './formatted-block/parser'; -import type { - Activity, - ActivityLogEntry, - ActivityLogEntryImage, - ActivityMediaDetails, -} from './types'; - -const parseTimestamp = ( published?: string ): number => { - if ( ! published ) { - return 0; - } - const timestamp = Date.parse( published ); - return Number.isNaN( timestamp ) ? 0 : timestamp; -}; - -const normalizeActivityMedia = ( image?: ActivityLogEntryImage | null ): ActivityMediaDetails => { - if ( ! image ) { - return { - available: false, - medium_url: '', - name: '', - thumbnail_url: '', - type: '', - url: '', - }; - } - - return { - available: Boolean( image.available ), - medium_url: image.medium_url ?? '', - name: image.name ?? '', - thumbnail_url: image.thumbnail_url ?? '', - type: image.type ?? '', - url: image.url ?? '', - }; -}; - -/** - * Transform an ActivityLogEntry (raw WPCOM shape) into an Activity (UI shape). - * - * @param entry - Raw entry from the /jetpack/v4/activity-log endpoint. - * @return Normalized Activity for the DataViews table. - */ -export const transformActivityLogEntry = ( entry: ActivityLogEntry ): Activity => { - const { - content, - actor, - image, - gridicon, - activity_id: rawActivityId, - name, - object, - status, - summary, - published, - is_rewindable, - rewind_id, - } = entry; - const descriptionItems = parseActivityLogEntryContent( content ); - const textDescription = content?.text ?? ''; - - return { - activityDescription: { - textDescription, - items: descriptionItems, - }, - activityIcon: gridicon, - activityId: rawActivityId, - activityMedia: normalizeActivityMedia( image ), - activityName: name, - activityObject: object, - activityStatus: status ?? '', - activityTitle: summary, - activityUnparsedTs: published ?? '', - activityTs: parseTimestamp( published ), - activityActor: { - actorAvatarUrl: actor?.icon?.url, - actorName: actor?.name, - actorRole: actor?.role, - actorType: actor?.type, - isCli: actor?.is_cli, - isSupport: actor?.is_happiness, - isMcpAgent: actor?.is_mcp_agent, - mcpClient: actor?.mcp_client, - }, - activityIsRewindable: Boolean( is_rewindable ), - rewindId: rewind_id || undefined, - }; -}; diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/admin-links.ts b/projects/packages/activity-log/src/js/components/ActivityLog/admin-links.ts deleted file mode 100644 index e8cb46452e24..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/admin-links.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Resolve wp-admin URLs for activity-log entities. The WPCOM activity-log - * API exposes entity identity three different ways, and we handle all - * three: - * - * 1. Typed range nodes (type: 'post'/'person'/'plugin'/…) with dedicated - * id/slug fields — handled by `buildAdminLink`. - * 2. Anchor ranges (type: 'a'/'link') with a `section` hint and `id` - * (e.g. section: 'user', id: 42) — also handled by `buildAdminLink`. - * 3. A top-level `object` on the entry (e.g. object: { type: 'Article', - * object_id: 25 }) used when the description has no ranges at all — - * handled by `buildObjectAdminLink`. - * - * The `adminUrl` prefix comes from the Initial_State payload - * (`class-initial-state.php::get_data()`) so non-standard installs - * (subdirectory, custom `admin_url` filter) are respected instead of - * hard-coding `/wp-admin/`. - */ -import type { ActivityBlockNode } from './formatted-block/types'; -import type { ActivityLogObject } from './types'; - -interface InitialStateWithAdminUrl { - siteData?: { adminUrl?: string }; -} - -declare const JPACTIVITYLOG_INITIAL_STATE: InitialStateWithAdminUrl | undefined; - -// Read once at module load; the value doesn't change within a session. -const adminUrlPrefix: string = ( () => { - const raw = - typeof JPACTIVITYLOG_INITIAL_STATE !== 'undefined' - ? JPACTIVITYLOG_INITIAL_STATE?.siteData?.adminUrl - : undefined; - const base = raw && raw.length > 0 ? raw : '/wp-admin/'; - return base.endsWith( '/' ) ? base : `${ base }/`; -} )(); - -const q = ( value: string | number ) => encodeURIComponent( String( value ) ); - -const postEditUrl = ( id: string | number ) => - `${ adminUrlPrefix }post.php?post=${ q( id ) }&action=edit`; -const userEditUrl = ( id: string | number ) => - `${ adminUrlPrefix }user-edit.php?user_id=${ q( id ) }`; -const commentEditUrl = ( id: string | number ) => - `${ adminUrlPrefix }comment.php?action=editcomment&c=${ q( id ) }`; -const pluginSearchUrl = ( slug: string ) => `${ adminUrlPrefix }plugins.php?s=${ q( slug ) }`; -const themeDetailsUrl = ( slug: string ) => `${ adminUrlPrefix }themes.php?theme=${ q( slug ) }`; - -/** - * Build a wp-admin URL from a parsed block node (typed entity range or - * section-tagged anchor), or null when no target can be derived. - * - * @param node - The parsed activity-log block node. - * @return A fully-qualified wp-admin URL string, or null. - */ -export function buildAdminLink( node: ActivityBlockNode ): string | null { - // Typed entity ranges — dedicated id/slug fields. - switch ( node.type ) { - case 'post': - return node.postId ? postEditUrl( node.postId ) : null; - case 'person': - return node.userId ? userEditUrl( node.userId ) : null; - case 'comment': - return node.commentId ? commentEditUrl( node.commentId ) : null; - case 'plugin': - return node.pluginSlug ? pluginSearchUrl( String( node.pluginSlug ) ) : null; - case 'theme': - return node.themeSlug ? themeDetailsUrl( String( node.themeSlug ) ) : null; - } - - // Anchor ranges carrying entity identity via `section` + `id`. Common - // when the WPCOM payload wraps a name in an `` pointing at a - // wordpress.com path (e.g. /people/edit/{blog}/{name}) and tags the - // range with section: 'user'. - if ( ( node.type === 'link' || node.type === 'a' ) && node.id !== undefined ) { - switch ( node.section ) { - case 'user': - return userEditUrl( node.id ); - case 'post': - return postEditUrl( node.id ); - case 'comment': - return commentEditUrl( node.id ); - } - } - - return null; -} - -/** - * Build a wp-admin URL from the entry's top-level `object` field, used - * when the activity description carries no ranges and the only way to - * identify the subject is the entry-level object (e.g. a `post__published` - * event whose content.text is literally the post title). - * - * @param object - The entry's `object` field, if present. - * @return A fully-qualified wp-admin URL string, or null. - */ -export function buildObjectAdminLink( object?: ActivityLogObject ): string | null { - if ( ! object ) { - return null; - } - const { type, object_id: objectId, external_user_id: externalUserId } = object; - switch ( type ) { - case 'Article': - return objectId ? postEditUrl( objectId ) : null; - case 'Person': - return externalUserId ? userEditUrl( externalUserId ) : null; - default: - return null; - } -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/fields.tsx b/projects/packages/activity-log/src/js/components/ActivityLog/fields.tsx deleted file mode 100644 index 74e4cf1ca825..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/fields.tsx +++ /dev/null @@ -1,352 +0,0 @@ -import { useViewportMatch } from '@wordpress/compose'; -import { dateI18n } from '@wordpress/date'; -import { __, sprintf } from '@wordpress/i18n'; -import { useMemo } from 'react'; -import { ActivityActor } from './ActivityActor'; -import { - ActivityEvent, - ActivityEventDescription, - ActivityEventIcon, - ActivityEventTitle, -} from './ActivityEvent'; -import type { Activity, ActivityLogGroupCountResponse } from './types'; -import type { Field, Operator } from '@wordpress/dataviews'; - -export type ActivityLogTypeOption = { - value: string; - label: string; -}; - -type UseActivityFieldsArgs = { - timezoneString?: string; - gmtOffset?: number; - activityLogTypes?: ActivityLogGroupCountResponse[ 'groups' ] | undefined; -}; - -/** - * Extract the leading "group" segment from an event name like - * "plugin__updated" → "plugin". Used both as the DataViews filter value - * and to look up a human-readable description from the group-counts - * payload. - * - * @param name - Raw event name from the API (e.g. "plugin__updated"). - * @return The group slug, or an empty string if no name was given. - */ -const getActivityLogTypeSlugFromName = ( name?: string ): string => { - if ( ! name ) { - return ''; - } - const [ group ] = name.split( '__' ); - return group ?? ''; -}; - -/** - * Resolve a user-facing description for an event name by looking the - * leading group segment up in the group-counts response. Falls back to - * the slug itself when the lookup misses. - * - * @param name - Raw event name. - * @param activityLogTypes - Group map from /activity-log/count/group. - * @return Display label (e.g. "Plugins and Themes") or the slug. - */ -const getActivityLogTypeDescriptionFromName = ( - name?: string, - activityLogTypes?: ActivityLogGroupCountResponse[ 'groups' ] | undefined -): string => { - if ( ! name ) { - return ''; - } - const slug = getActivityLogTypeSlugFromName( name ); - return activityLogTypes?.[ slug ]?.name ?? slug; -}; - -/** - * Format a numeric hour offset (e.g. `-5`, `5.5`) as "UTC±HH:MM". - * - * @param gmtOffset - Hour offset from UTC, decimal hours. - * @return The formatted offset string. - */ -const formatUtcOffset = ( gmtOffset: number ): string => { - const sign = gmtOffset < 0 ? '-' : '+'; - const abs = Math.abs( gmtOffset ); - const hours = Math.floor( abs ); - const minutes = Math.round( ( abs - hours ) * 60 ); - return `UTC${ sign }${ String( hours ).padStart( 2, '0' ) }:${ String( minutes ).padStart( - 2, - '0' - ) }`; -}; - -/** - * Compute the date column header. Includes the site's timezone (on wide - * screens when we know it) or its UTC offset. - * - * @param args - Inputs. - * @param args.timezoneString - IANA timezone (e.g. "Europe/London"). - * @param args.gmtOffset - Decimal hour offset from UTC. - * @param args.isLargeScreen - True when the viewport is wide enough to - * show the full timezone name. - * @return The header label for the date column. - */ -const getDateTimeLabel = ( { - timezoneString, - gmtOffset, - isLargeScreen, -}: { - timezoneString?: string; - gmtOffset?: number; - isLargeScreen: boolean; -} ): string => { - /* translators: %s is the site's timezone (e.g., "Europe/London") or UTC offset (e.g., "UTC+02:00") */ - const template = __( 'Date & time (%s)', 'jetpack-activity-log' ); - if ( timezoneString && isLargeScreen ) { - return sprintf( template, timezoneString ); - } - if ( typeof gmtOffset === 'number' ) { - return sprintf( template, formatUtcOffset( gmtOffset ) ); - } - return __( 'Date & time', 'jetpack-activity-log' ); -}; - -/** - * Format a single date cell value, honoring the site's timezone preference - * and (optionally) forcing a UTC rendering for the parallel "UTC" column. - * - * @param args - Inputs. - * @param args.value - ISO string or unix-seconds timestamp. - * @param args.timezoneString - IANA timezone (e.g. "Europe/London"). - * @param args.gmtOffset - Decimal hour offset from UTC. - * @param args.formatAsUTC - True to render in UTC regardless of the - * site preference. - * @param args.dateFormat - `dateI18n` format string, defaulting to - * "M j, Y at g:i A" (used for the Date & - * time column). Pass "F j, Y" for the - * day-only group header. - * @return The formatted date string. - */ -const formatDateCell = ( { - timezoneString, - gmtOffset, - value, - formatAsUTC, - dateFormat = 'M j, Y \\a\\t g:i A', -}: { - timezoneString?: string; - gmtOffset?: number; - value?: string | number; - formatAsUTC?: boolean; - dateFormat?: string; -} ): string => { - if ( ! value ) { - return ''; - } - const date = typeof value === 'number' ? new Date( value * 1000 ) : new Date( value ); - if ( formatAsUTC ) { - return dateI18n( dateFormat, date, 'UTC' ); - } - if ( timezoneString ) { - return dateI18n( dateFormat, date, timezoneString ); - } - if ( typeof gmtOffset === 'number' ) { - // `@wordpress/date` accepts the offset in minutes when passed a number; - // translate the site's hour-offset accordingly. - return dateI18n( dateFormat, date, gmtOffset * 60 ); - } - return dateI18n( dateFormat, date ); -}; - -/** - * Build the DataViews `fields` array for the Activity Log table: the - * Date & time column (optionally paired with a UTC column when the site - * isn't already on UTC), the Event cell, the User cell, and the hidden - * `activity_type` field that powers the filter dropdown. - * - * @param args - Hook options. - * @param args.timezoneString - IANA timezone (e.g. "Europe/London"). - * @param args.gmtOffset - Decimal hour offset from UTC. - * @param args.activityLogTypes - Group map from /activity-log/count/group. - * @return The fields array passed to ``. - */ -export function useActivityFields( { - timezoneString, - gmtOffset, - activityLogTypes, -}: UseActivityFieldsArgs ): Field< Activity >[] { - const isLargeScreen = useViewportMatch( 'huge', '>=' ); - const dateTimeLabel = getDateTimeLabel( { timezoneString, gmtOffset, isLargeScreen } ); - const localIsUTC = gmtOffset === 0; - - const activityLogTypeElements = useMemo< ActivityLogTypeOption[] >( () => { - if ( ! activityLogTypes ) { - return []; - } - return Object.entries( activityLogTypes ) - .map( ( [ value, { name, count } ] ) => ( { - value, - label: `${ name } (${ count })`, - } ) ) - .sort( ( a, b ) => a.label.localeCompare( b.label ) ); - }, [ activityLogTypes ] ); - - return useMemo( () => { - const fields: Field< Activity >[] = [ - { - id: 'published', - type: 'datetime', - label: dateTimeLabel, - enableHiding: true, - enableSorting: true, - getValue: ( { item } ) => item.activityUnparsedTs, - render: ( { item } ) => ( - - { formatDateCell( { - value: item.activityUnparsedTs, - timezoneString, - gmtOffset, - } ) } - - ), - filterBy: { operators: [] }, - }, - ]; - - if ( ! localIsUTC ) { - fields.push( { - id: 'published_utc', - type: 'datetime', - label: __( 'Date & time (UTC)', 'jetpack-activity-log' ), - enableHiding: true, - enableSorting: true, - getValue: ( { item } ) => item.activityUnparsedTs, - render: ( { item } ) => ( - - { formatDateCell( { - value: item.activityUnparsedTs, - timezoneString, - gmtOffset, - formatAsUTC: true, - } ) } - - ), - filterBy: { operators: [] }, - } ); - } - - fields.push( - { - id: 'event', - type: 'text', - label: __( 'Event', 'jetpack-activity-log' ), - enableSorting: false, - enableHiding: false, - getValue: ( { item } ) => - `${ item.activityTitle }: ${ item.activityDescription.textDescription }`, - render: ( { item } ) => , - filterBy: { operators: [] }, - }, - // The Activity layout renders icon / title / description in - // dedicated mediaField / titleField / descriptionField slots, - // so we expose three atomic fields alongside the Table - // layout's composite `event`. DataViews' `getHideableFields` - // excludes slot-bound fields from the Properties toggle - // list, so these don't double-expose in the cog popover. - { - id: 'event_icon', - type: 'text', - label: __( 'Icon', 'jetpack-activity-log' ), - enableSorting: false, - enableHiding: false, - getValue: ( { item } ) => item.activityIcon ?? '', - render: ( { item } ) => , - filterBy: { operators: [] }, - }, - { - id: 'event_title', - type: 'text', - label: __( 'Title', 'jetpack-activity-log' ), - enableSorting: false, - enableHiding: false, - getValue: ( { item } ) => item.activityTitle, - render: ( { item } ) => , - filterBy: { operators: [] }, - }, - { - id: 'event_description', - type: 'text', - label: __( 'Description', 'jetpack-activity-log' ), - enableSorting: false, - enableHiding: false, - getValue: ( { item } ) => item.activityDescription.textDescription, - render: ( { item } ) => , - filterBy: { operators: [] }, - }, - { - id: 'actor', - type: 'text', - label: __( 'User', 'jetpack-activity-log' ), - enableSorting: false, - enableHiding: false, - getValue: ( { item } ) => - item.activityActor?.actorName || __( 'Unknown', 'jetpack-activity-log' ), - render: ( { item } ) => , - filterBy: { operators: [] }, - }, - { - // Day-level grouping key for the Activity layout. Returns - // e.g. "Apr 24, 2026" in the site's local timezone so - // events on the same calendar day collapse under a - // single group header. Never rendered as a column — - // `enableHiding: false` keeps it out of the Properties - // toggle (getHideableFields filters those out) and it's - // never included in any layout's `view.fields`, so the - // Table layout ignores it entirely. - id: 'published_date', - type: 'text', - label: __( 'Date', 'jetpack-activity-log' ), - enableSorting: false, - enableHiding: false, - getValue: ( { item } ) => - formatDateCell( { - value: item.activityUnparsedTs, - timezoneString, - gmtOffset, - dateFormat: 'F j, Y', - } ), - render: ( { item } ) => ( - - { formatDateCell( { - value: item.activityUnparsedTs, - timezoneString, - gmtOffset, - dateFormat: 'F j, Y', - } ) } - - ), - filterBy: { operators: [] }, - }, - { - id: 'activity_type', - type: 'text', - label: __( 'Activity type', 'jetpack-activity-log' ), - getValue: ( { item } ) => getActivityLogTypeSlugFromName( item.activityName ), - render: ( { item } ) => ( - - { getActivityLogTypeDescriptionFromName( item.activityName, activityLogTypes ) } - - ), - elements: activityLogTypeElements, - isVisible: () => false, - filterBy: { operators: [ 'isAny' as Operator ] }, - } - ); - - return fields; - }, [ - timezoneString, - gmtOffset, - dateTimeLabel, - activityLogTypeElements, - activityLogTypes, - localIsUTC, - ] ); -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/filters.ts b/projects/packages/activity-log/src/js/components/ActivityLog/filters.ts deleted file mode 100644 index 0b0cffd29159..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/filters.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Filter } from '@wordpress/dataviews'; - -export const extractActivityLogTypeValues = ( filters: Filter[] ): string[] => { - const filter = filters.find( item => item.field === 'activity_type' ); - if ( ! filter ) { - return []; - } - const { value } = filter; - if ( Array.isArray( value ) ) { - return value.filter( ( item ): item is string => typeof item === 'string' && item.length > 0 ); - } - if ( typeof value === 'string' && value.length > 0 ) { - return [ value ]; - } - return []; -}; diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/index.tsx b/projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/index.tsx deleted file mode 100644 index 5881d5c0f265..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/index.tsx +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Renders the structured tokens produced by the parser. - * - * Ported (simplified) from Calypso's logs-activity-formatted-block. Calypso - * links entities into its own routes (/reader/blogs/…, /people/edit/…, - * /plugins/…); in wp-admin we link into the equivalent core screens - * (post.php, user-edit.php, plugins.php, themes.php, comment.php) via - * `buildAdminLink`. Entities without a wp-admin equivalent (site, backup) - * fall through to plain strong text. Direct URL ranges (release notes, - * docs) still render as external links. - */ -import { ExternalLink } from '@wordpress/components'; -import { Fragment, type MouseEvent, type ReactNode } from 'react'; -import { buildAdminLink } from '../admin-links'; -import type { ActivityBlockContent, ActivityBlockMeta, ActivityBlockNode } from './types'; - -type BlockClickHandler = ( event: MouseEvent< HTMLAnchorElement > ) => void; - -type BlockRenderer = ( args: { - content: ActivityBlockNode; - children: ReactNode[]; - onClick: BlockClickHandler | undefined; - meta: ActivityBlockMeta; -} ) => ReactNode; - -interface FormattedBlockProps { - content: ActivityBlockContent; - onClick: BlockClickHandler | undefined; - meta: ActivityBlockMeta; -} - -const Strong = ( { children }: { children: ReactNode } ) => { children }; -const Emphasis = ( { children }: { children: ReactNode } ) => { children }; -const Preformatted = ( { children }: { children: ReactNode } ) =>
{ children }
; -const FilePath = ( { children }: { children: ReactNode } ) => ( -
- { children } -
-); - -// The extra trailing slash prevents hostnames like -// `wordpress.com.malicious.example` from matching. Same guard Calypso's -// formatted-block uses. -const isWordPressDotComUrl = ( url?: string | null ): boolean => - !! url && url.startsWith( 'https://wordpress.com/' ); - -const Link: BlockRenderer = ( { content, children, onClick, meta } ) => { - const { url, activity, section, intent } = content; - - if ( ! url ) { - return { children }; - } - - // Anchor ranges frequently carry section + id hints (e.g. - // section: 'user', id: 42) pointing at a WordPress.com URL. Prefer - // the local wp-admin equivalent when we can derive one, regardless - // of the outer URL. - const adminHref = buildAdminLink( content ); - if ( adminHref ) { - return ( -
- { children } - - ); - } - - // No local equivalent. If the URL itself is a wordpress.com URL, - // drop it — those destinations aren't useful from wp-admin and any - // nested entity renderer (EntityLink) can still emit its own link - // from the children tree. - if ( isWordPressDotComUrl( url ) ) { - return { children }; - } - - return ( - - { children } - - ); -}; - -// Resolve a token's wp-admin destination (if any). Entities without a -// target (site, backup, or a malformed payload missing an id/slug) fall -// through to plain text. -const EntityLink: BlockRenderer = ( { content, children } ) => { - const href = buildAdminLink( content ); - if ( ! href ) { - return { children }; - } - return { children }; -}; - -const blockTypeMapping: Record< string, BlockRenderer > = { - b: ( { children } ) => { children }, - strong: ( { children } ) => { children }, - i: ( { children } ) => { children }, - em: ( { children } ) => { children }, - pre: ( { children } ) => { children }, - a: Link, - link: Link, - filepath: ( { children } ) => { children }, - post: EntityLink, - comment: EntityLink, - person: EntityLink, - plugin: EntityLink, - theme: EntityLink, - // site (we're already on it) and backup (needs the Backup plugin's own - // route) have no generic wp-admin target — render as plain text. - site: ( { children } ) => { children }, - backup: ( { children } ) => { children }, -}; - -export const createFormattedBlock = ( mapping: Record< string, BlockRenderer > ) => { - const FormattedBlock = ( { content, onClick, meta }: FormattedBlockProps ): ReactNode => { - if ( typeof content === 'string' ) { - return <>{ content }; - } - - const nestedContent = content.children ?? []; - const { type, text } = content; - - if ( type === undefined && nestedContent.length === 0 ) { - return text ? <>{ text } : null; - } - - const children = nestedContent.map( ( child, index ) => ( - - ) ); - - if ( type ) { - const renderer = mapping[ type ]; - if ( renderer ) { - return renderer( { content, children, onClick, meta } ); - } - } - - return <>{ children }; - }; - - return FormattedBlock; -}; - -const FormattedBlock = createFormattedBlock( blockTypeMapping ); - -export const renderFormattedContent = ( { - items, - onClick = null, - meta = {}, -}: { - items: ActivityBlockContent[]; - onClick?: BlockClickHandler | null; - meta?: ActivityBlockMeta; -} ): ReactNode[] => - items.map( ( item, index ) => ( - - ) ); - -export default FormattedBlock; diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/parser.ts b/projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/parser.ts deleted file mode 100644 index e9d0eae327fb..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/parser.ts +++ /dev/null @@ -1,265 +0,0 @@ -/** - * Rewrites activity-log payloads from the WPCOM API into a tree of nodes that - * the FormattedBlock renderer understands. Ported verbatim from Calypso's - * `client/dashboard/components/logs-activity-formatted-block/api-core-parser.ts`. - */ -import type { ActivityBlockContent, ActivityBlockNode } from './types'; -import type { ActivityLogEntry, ActivityNotificationRange } from '../types'; - -interface RangeWithChildren extends ActivityNotificationRange { - children: RangeWithChildren[]; - [ key: string ]: unknown; -} - -type ParseState = [ ActivityBlockContent[], string, number ]; - -type RangePredicate = ( range: RangeWithChildren ) => boolean; - -const isNonEmpty = < T >( value: T | null | undefined | false | '' ): value is T => - Boolean( value ); - -const rangeSort = ( - { indices: [ aStart, aEnd ] }: RangeWithChildren, - { indices: [ bStart, bEnd ] }: RangeWithChildren -) => { - if ( aStart === 0 && aEnd === 0 && bEnd !== 0 ) { - return -1; - } - if ( aStart < bStart ) { - return -1; - } - if ( bStart < aStart ) { - return 1; - } - return bEnd - aEnd; -}; - -const encloses = - ( { indices: [ innerStart, innerEnd ] }: RangeWithChildren ): RangePredicate => - ( { indices: [ outerStart = 0, outerEnd = 0 ] = [ 0, 0 ] } ) => - innerStart !== 0 && innerEnd !== 0 && outerStart <= innerStart && outerEnd >= innerEnd; - -const addRange = ( ranges: RangeWithChildren[], range: RangeWithChildren ): RangeWithChildren[] => { - const parentIndex = [ ...ranges ] - .reverse() - .findIndex( candidate => encloses( range )( candidate ) ); - - if ( parentIndex === -1 ) { - return [ ...ranges, range ]; - } - - const actualIndex = ranges.length - 1 - parentIndex; - const parent = ranges[ actualIndex ]; - const updatedChildren = addRange( parent.children, range ); - const updatedParent: RangeWithChildren = { - ...parent, - children: updatedChildren, - }; - - return [ ...ranges.slice( 0, actualIndex ), updatedParent, ...ranges.slice( actualIndex + 1 ) ]; -}; - -const commentNode = ( { - id: commentId, - post_id: postId, - site_id: siteId, -}: RangeWithChildren ) => ( { - type: 'comment', - commentId, - postId, - siteId, -} ); - -const linkNode = ( { url, intent, section, id, site_id: siteId }: RangeWithChildren ) => ( { - type: 'link', - url, - intent, - section, - // `id` + `site_id` let the renderer build a local wp-admin link for - // anchors that carry section hints (e.g. section: 'user', id: 42 → - // `user-edit.php?user_id=42`), even when the `url` itself points at - // wordpress.com. - id, - siteId, -} ); - -const postNode = ( { id: postId, site_id: siteId, published }: RangeWithChildren ) => ( { - type: 'post', - postId, - siteId, - published, -} ); - -const siteNode = ( { id: siteId, intent, section }: RangeWithChildren ) => ( { - type: 'site', - siteId, - intent, - section, -} ); - -const userNode = ( { - id: userId, - name, - site_id: siteId, - intent, - section, -}: RangeWithChildren ) => ( { - type: 'person', - name, - siteId, - userId, - intent, - section, -} ); - -const pluginNode = ( { - site_slug: siteSlug, - slug, - version, - intent, - section, -}: RangeWithChildren ) => ( { - type: 'plugin', - siteSlug, - pluginSlug: slug, - version, - intent, - section, -} ); - -const themeNode = ( { - site_slug: siteSlug, - slug, - version, - uri, - intent, - section, -}: RangeWithChildren ) => ( { - type: 'theme', - siteSlug, - themeSlug: slug, - themeUri: uri, - version, - intent, - section, -} ); - -const backupNode = ( { - site_slug: siteSlug, - rewind_id: rewindId, - intent, - section, -}: RangeWithChildren ) => ( { - type: 'backup', - siteSlug, - rewindId, - intent, - section, -} ); - -const inferNode = ( range: RangeWithChildren ) => { - if ( range.url ) { - return linkNode( range ); - } - if ( range.type ) { - return { type: range.type }; - } - return range; -}; - -const nodeMappings = ( type?: string ) => { - switch ( type ) { - case 'comment': - return commentNode; - case 'post': - return postNode; - case 'site': - return siteNode; - case 'user': - return userNode; - case 'plugin': - return pluginNode; - case 'theme': - return themeNode; - case 'backup': - return backupNode; - default: - return inferNode; - } -}; - -const newNode = ( text: string, range: RangeWithChildren ): ActivityBlockNode => ( { - ...nodeMappings( range.type )( range ), - text, - children: text ? [ text ] : [], -} ); - -const joinResults = ( [ reduced, remainder ]: [ - ActivityBlockContent[], - string, -] ): ActivityBlockContent[] => { - if ( reduced.length ) { - return [ ...reduced, remainder ].filter( isNonEmpty ); - } - return remainder ? [ remainder ] : []; -}; - -const parseRange = ( - [ prev, text, offset ]: ParseState, - nextRange: RangeWithChildren -): ParseState => { - const { - indices: [ start, end ], - } = nextRange; - const offsetStart = start - offset; - const offsetEnd = end - offset; - const preText = offsetStart > 0 ? [ text.slice( 0, offsetStart ) ] : []; - const innerText = text.slice( offsetStart, offsetEnd ); - const [ childReduced, childRemainder ] = nextRange.children.reduce< ParseState >( - ( state, range ) => parseRange( state, range ), - [ [], innerText, start ] - ); - const parsedChildren = joinResults( [ childReduced, childRemainder ] ); - const baseNode = newNode( innerText, nextRange ); - const parsedNode: ActivityBlockNode = parsedChildren.length - ? { ...baseNode, children: parsedChildren } - : baseNode; - - return [ [ ...prev, ...preText, parsedNode ], text.slice( offsetEnd ), end ]; -}; - -export const parseActivityLogEntryContent = ( - content?: string | ActivityLogEntry[ 'content' ] -): ActivityBlockContent[] => { - if ( typeof content === 'string' ) { - return content ? [ content ] : []; - } - if ( Array.isArray( content ) ) { - return content; - } - if ( ! content ) { - return []; - } - const { text = '' } = content; - - if ( ! content.ranges || ! content.ranges.length ) { - return text ? [ text ] : []; - } - - const rangesWithChildren = content.ranges - .map< RangeWithChildren >( range => ( { - ...range, - children: [] as RangeWithChildren[], - } ) ) - .sort( rangeSort ) - .reduce( addRange, [] as RangeWithChildren[] ); - - const [ reduced, remainder ] = rangesWithChildren.reduce< ParseState >( - ( state, range ) => parseRange( state, range ), - [ [], text, 0 ] - ); - - return joinResults( [ reduced, remainder ] ); -}; - -export default parseActivityLogEntryContent; diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/types.ts b/projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/types.ts deleted file mode 100644 index c72e1a254f2f..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/formatted-block/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -export interface ActivityBlockNode { - type?: string; - text?: string | null; - children?: ActivityBlockContent[]; - url?: string | null; - activity?: string; - section?: string; - intent?: string; - // `id` arrives on anchor ranges that carry a `section` hint (e.g. the - // local WP user id on a user anchor). Typed entity ranges put their - // identifier into a dedicated field (postId/userId/…) instead. - id?: number | string; - siteId?: number | string; - postId?: number | string; - isTrashed?: boolean; - commentId?: number | string; - userId?: number | string; - name?: string; - siteSlug?: string; - pluginSlug?: string; - themeUri?: string; - themeSlug?: string; - version?: string; - rewindId?: string; -} - -export type ActivityBlockContent = string | ActivityBlockNode; - -export interface ActivityBlockMeta { - activity?: string; - intent?: string; - section?: string; - published?: number | string; -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/gridicons.ts b/projects/packages/activity-log/src/js/components/ActivityLog/gridicons.ts deleted file mode 100644 index 68b7d5550daa..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/gridicons.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - audio, - background, - backup, - brush, - caution, - check, - cloud, - cog, - comment, - commentAuthorAvatar, - connection, - customPostType, - error, - globe, - homeButton, - image, - layout, - lock, - menu, - pages, - people, - postContent, - plugins, - published, - receipt, - rotateRight, - swatch, - trash, - update, - video, - wordpress, -} from '@wordpress/icons'; -import type { ReactElement } from 'react'; - -const icons: Record< string, ReactElement > = { - audio, - checkmark: check, - cart: receipt, - cloud, - cog, - comment, - 'custom-post-type': customPostType, - globe, - history: backup, - image, - layout, - lock, - menu, - 'multiple-users': people, - 'my-sites': wordpress, - notice: caution, - posts: postContent, - pages, - plans: connection, - plugins, - published, - rotateRight, - science: swatch, - spam: error, - status: homeButton, - sync: update, - themes: brush, - trash, - user: commentAuthorAvatar, - video, -}; - -/** - * Translate a Calypso gridicon slug (as returned by the WPCOM activity log - * in `entry.gridicon`) into a `@wordpress/icons` element. Falls back to the - * generic `background` icon for unknown slugs. - * - * @param slug - The gridicon slug (e.g. "plugins", "posts", "cloud"). - * @return The corresponding WP icon element. - */ -export function gridiconToWordPressIcon( slug: string ): ReactElement { - return icons[ slug ] ?? background; -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/index.tsx b/projects/packages/activity-log/src/js/components/ActivityLog/index.tsx deleted file mode 100644 index bac3a9381b1a..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/index.tsx +++ /dev/null @@ -1,405 +0,0 @@ -/** - * Top-level Activity Log admin page. Ported from Calypso's - * `client/dashboard/sites/logs-activity/dataviews/index.tsx`. Scope - * simplifications vs. the source are tracked in the PR (#48244): view - * state persists to localStorage rather than URL params, the actor - * column isn't linked, and the "Manage backup" row action is stubbed - * until #48236 lands. - */ -import { AdminPage } from '@automattic/jetpack-components'; -import { useQuery } from '@tanstack/react-query'; -import { DataViews } from '@wordpress/dataviews'; -import { __ } from '@wordpress/i18n'; -import fastDeepEqual from 'fast-deep-equal/es6'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { activityLogQuery, activityLogGroupCountsQuery } from '../../hooks/use-activity-log'; -import { useAnalytics } from '../../hooks/use-analytics'; -import { usePersistentView } from '../../hooks/use-persistent-view'; -import { DateRangePicker } from '../DateRangePicker'; -import { formatYmd, parseYmdLocal } from '../DateRangePicker/datetime'; -import { UpsellCallout } from './UpsellCallout'; -import { useActivityActions } from './actions'; -import { transformActivityLogEntry } from './activity-transformer'; -import { useActivityFields } from './fields'; -import { extractActivityLogTypeValues } from './filters'; -import { DEFAULT_LAYOUTS, DEFAULT_VIEW } from './views'; -import type { Activity, ActivityLogParams } from './types'; -import type { Field, Filter, View } from '@wordpress/dataviews'; - -const ACTIVITY_LOGS_DEFAULT_PAGE_SIZE = 20; - -interface InitialState { - siteData?: { - gmtOffset?: number; - timezoneString?: string; - hasActivityLogsAccess?: boolean; - locale?: string; - }; -} - -declare global { - const JPACTIVITYLOG_INITIAL_STATE: InitialState | undefined; -} - -/** - * Read the site's timezone/offset from the Initial_State payload seeded - * by class-initial-state.php. Falls back to UTC when the global isn't - * present (e.g. in storybook or tests). - * - * @return The resolved site time context. - */ -const readSiteTimeContext = (): { - gmtOffset: number; - timezoneString?: string; - locale: string; -} => { - const state = - typeof JPACTIVITYLOG_INITIAL_STATE !== 'undefined' ? JPACTIVITYLOG_INITIAL_STATE : undefined; - return { - gmtOffset: state?.siteData?.gmtOffset ?? 0, - timezoneString: state?.siteData?.timezoneString || undefined, - // Fall back to the browser's navigator locale if Initial_State - // didn't seed one — the picker's date labels only care about - // number/weekday formatting. - locale: - state?.siteData?.locale || ( typeof navigator !== 'undefined' ? navigator.language : 'en' ), - }; -}; - -/** - * Read the paid-plan capability flag seeded by Initial_State. Defaults - * to `true` when the global isn't present (storybook/tests) so the - * free-tier gating path only activates from a real backend signal. - * - * @return Whether the site has full Activity Log access. - */ -const readHasActivityLogsAccess = (): boolean => { - if ( typeof JPACTIVITYLOG_INITIAL_STATE === 'undefined' ) { - return true; - } - return JPACTIVITYLOG_INITIAL_STATE?.siteData?.hasActivityLogsAccess !== false; -}; - -/** - * The Activity Log admin page. Renders the DataViews table and drives - * its dataset/filter/counts queries against /jetpack/v4/activity-log. - * - * @return The admin page. - */ -export default function ActivityLog() { - const { gmtOffset, timezoneString, locale } = readSiteTimeContext(); - const hasActivityLogsAccess = readHasActivityLogsAccess(); - const { view, setView, resetView, isViewModified } = usePersistentView( DEFAULT_VIEW ); - 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(); - }, [] ); - - // Date-range defaults to "Last 7 days" anchored at the site's calendar - // today (not the browser's) — matches Calypso's `getDefaultDateRange`. - // The range is client-only state: refreshes reset to the default - // instead of persisting, so users don't return to a stale narrow - // window. - const [ dateRange, setDateRange ] = useState( () => { - const siteToday = - parseYmdLocal( formatYmd( new Date(), timezoneString, gmtOffset ) ) ?? new Date(); - return { - start: new Date( siteToday.getFullYear(), siteToday.getMonth(), siteToday.getDate() - 6 ), - end: siteToday, - }; - } ); - - const activityLogTypeValues = useMemo( () => { - const filters = ( view.filters as Filter[] | undefined ) ?? []; - return extractActivityLogTypeValues( filters ); - }, [ view.filters ] ); - - const searchTerm = view.search?.trim() ?? ''; - - // The picker hands us start-of-day / end-of-day Dates at local-midnight - // (see the `dateRange` initializer below). For the WPCOM query, stretch - // `end` to the end of its calendar day so single-day ranges like "Today" - // aren't empty (UTC midnight → UTC midnight would match no events). - const afterIso = useMemo( () => dateRange.start.toISOString(), [ dateRange.start ] ); - const beforeIso = useMemo( () => { - const endOfDay = new Date( dateRange.end ); - endOfDay.setHours( 23, 59, 59, 999 ); - return endOfDay.toISOString(); - }, [ dateRange.end ] ); - - const listParams: ActivityLogParams = useMemo( () => { - const params: ActivityLogParams = { - sort_order: view.sort?.direction, - number: view.perPage || ACTIVITY_LOGS_DEFAULT_PAGE_SIZE, - // `view.page` starts `undefined` and settles to `1` on the - // first user-triggered change; defaulting here keeps the - // query key stable across that transition so we don't fire - // a duplicate list request on page load. - page: view.page ?? 1, - after: afterIso, - before: beforeIso, - }; - if ( searchTerm ) { - params.text_search = searchTerm; - } - if ( activityLogTypeValues.length ) { - params.group = activityLogTypeValues; - } - return params; - }, [ - view.sort?.direction, - view.perPage, - view.page, - searchTerm, - activityLogTypeValues, - afterIso, - beforeIso, - ] ); - - const { - data: activityLogData, - isFetching: isFetchingData, - isLoading: isLoadingList, - } = useQuery( { - ...activityLogQuery( listParams ), - select: data => ( { - ...data, - activityLogs: ( data.activityLogs ?? [] ).map( transformActivityLogEntry ), - } ), - } ); - - // Counts query scopes to the same date window as the list (so - // filter counts match what's displayed), but excludes `text_search` - // so the filter dropdown stays stable as users type (matches - // Calypso's behavior at logs-activity/dataviews/index.tsx:100-102). - const { data: groupCountsData, isFetching: isFetchingFilters } = useQuery( - activityLogGroupCountsQuery( { - number: 1000, - after: afterIso, - before: beforeIso, - } ) - ); - - const isFetching = isFetchingData || isFetchingFilters; - - const paginationInfo = { - totalItems: activityLogData?.totalItems ?? 0, - // Zero `totalPages` on the free tier to hide DataViews' pagination - // controls. The server-side clamp in REST_Controller already caps - // the returned set at FREE_TIER_ITEM_CAP; this just keeps the UI - // honest. - totalPages: hasActivityLogsAccess ? activityLogData?.totalPages ?? 0 : 0, - }; - - const fields = useActivityFields( { - gmtOffset, - timezoneString, - activityLogTypes: groupCountsData?.groups, - } ); - - const actions = useActivityActions( { isLoading: isFetching } ); - - const onChangeView = useCallback( - ( next: View ) => { - const nextSearch = next.search?.trim() ?? ''; - const currentPage = view.page ?? 1; - const requestedPage = next.page ?? currentPage; - - const perPageChanged = next.perPage !== view.perPage; - const sortChanged = next.sort?.direction !== view.sort?.direction; - const filtersChanged = ! fastDeepEqual( next.filters, view.filters ); - const searchChanged = nextSearch !== searchTerm; - const layoutChanged = next.type !== view.type; - - const datasetChanged = perPageChanged || sortChanged || filtersChanged || searchChanged; - - // Tracking — same breakdown Calypso records (per_page / - // filter / search / page_changed), namespaced under - // `jetpack_activity_log_*`, plus a wp-admin-only - // `layout_changed` since Jetpack exposes the DataViews - // Activity timeline as a second layout. - if ( layoutChanged ) { - tracks.recordEvent( 'jetpack_activity_log_layout_changed', { - layout: next.type, - } ); - } - if ( perPageChanged ) { - tracks.recordEvent( 'jetpack_activity_log_per_page_changed', { - per_page: next.perPage, - } ); - } - if ( filtersChanged ) { - const activityTypes = extractActivityLogTypeValues( - ( next.filters as Filter[] | undefined ) ?? [] - ); - const eventProps: Record< string, boolean | number > = { - num_groups_selected: activityTypes.length, - }; - let totalActivitiesSelected = 0; - Object.entries( groupCountsData?.groups ?? {} ).forEach( ( [ groupKey, { count } ] ) => { - const isSelected = activityTypes.includes( groupKey ); - eventProps[ `group_${ groupKey }` ] = isSelected; - if ( isSelected ) { - totalActivitiesSelected += count ?? 0; - } - } ); - eventProps.num_total_activities_selected = totalActivitiesSelected; - tracks.recordEvent( 'jetpack_activity_log_filter_changed', eventProps ); - } - if ( searchChanged ) { - tracks.recordEvent( 'jetpack_activity_log_search', { - has_query: nextSearch.length > 0, - } ); - } - if ( ! datasetChanged && requestedPage !== currentPage ) { - tracks.recordEvent( 'jetpack_activity_log_page_changed', { - page: requestedPage, - } ); - } - - setView( { - ...next, - page: datasetChanged ? 1 : requestedPage, - } ); - }, - [ setView, view, searchTerm, tracks, groupCountsData ] - ); - - const onChangeDateRange = useCallback( - ( next: { start: Date; end: Date } ) => { - const daysInRange = - Math.round( ( next.end.getTime() - next.start.getTime() ) / 86_400_000 ) + 1; - tracks.recordEvent( 'jetpack_activity_log_date_range_changed', { - days_in_range: daysInRange, - } ); - // A new range is its own dataset boundary — snap back to - // page 1, matching how `onChangeView` handles other dataset - // changes (perPage, sort, filters, search). - setDateRange( next ); - setView( { ...view, page: 1 } ); - }, - [ setView, view, tracks ] - ); - - const onResetView = useCallback( () => { - tracks.recordEvent( 'jetpack_activity_log_reset_view_click', {} ); - resetView(); - }, [ resetView, tracks ] ); - - const getItemId = useCallback( ( item: Activity ) => item.activityId.toString(), [] ); - - const logData = ( activityLogData?.activityLogs ?? [] ) as Activity[]; - - // Mounting the picker as an admin-ui `actions` slot places it in the - // AdminPage header alongside the title/subtitle — matches MSD's - // layout for the logs pages. - const headerActions = hasActivityLogsAccess ? ( - - ) : undefined; - - return ( - -
- - data={ logData } - isLoading={ isFetching || isLoadingList } - paginationInfo={ paginationInfo } - fields={ fields as Field< Activity >[] } - view={ view } - actions={ actions } - getItemId={ getItemId } - search - // Advertise both DataViews' built-in Activity timeline - // (the default) and a Table layout. Toggle lives in - // the cog popover's layout switcher. Each layout maps - // the event parts to the right slots: - // - Activity: `event_icon` → mediaField (left - // bullet slot), `event_title` → titleField, - // `event_description` → descriptionField, plus - // `groupBy: published_date` for day headers. - // - Table: one composite `event` column alongside - // Date / User. - // See DEFAULT_LAYOUTS in ./views for the full shape — - // it explicitly nulls slot/groupBy refs on Table so a - // round-trip Activity → Table doesn't carry those - // over and double-render as a primary column. - defaultLayouts={ DEFAULT_LAYOUTS } - onChangeView={ onChangeView } - onReset={ isViewModified ? onResetView : false } - // On the free tier, lock the perPage selector to the - // capped size and hide search/filters/sort/view-config - // by replacing the default UI with just the table (same - // switches Calypso uses at logs-activity/dataviews/ - // index.tsx:201-208). - config={ - hasActivityLogsAccess - ? undefined - : { perPageSizes: [ ACTIVITY_LOGS_DEFAULT_PAGE_SIZE ] } - } - empty={ -

- { view.search - ? __( 'No activity found', 'jetpack-activity-log' ) - : __( 'No activities', 'jetpack-activity-log' ) } -

- } - > - { hasActivityLogsAccess ? undefined : } - - { ! hasActivityLogsAccess && ! isFetching && logData.length > 0 && } -
-
- ); -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/types.ts b/projects/packages/activity-log/src/js/components/ActivityLog/types.ts deleted file mode 100644 index b9ec50b69388..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/types.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { ActivityBlockContent } from './formatted-block/types'; - -export interface ActivityDescription { - textDescription: string; - items: ActivityBlockContent[]; -} - -export interface ActivityActorDetails { - actorAvatarUrl?: string; - actorName?: string; - actorRole?: string; - actorType?: string; - isCli?: boolean; - isSupport?: boolean; - isMcpAgent?: boolean; - mcpClient?: string; -} - -export interface ActivityMediaDetails { - available: boolean; - medium_url: string; - name: string; - thumbnail_url: string; - type: string; - url: string; -} - -export interface Activity { - activityDescription: ActivityDescription; - activityIcon?: string; - activityId: string; - activityMedia: ActivityMediaDetails; - activityName: string; - activityObject?: ActivityLogObject; - activityStatus: string; - activityTitle: string; - activityTs: number; - activityUnparsedTs: string; - activityActor: ActivityActorDetails; - activityIsRewindable: boolean; - rewindId?: string; -} - -/** - * Minimal shape from the WPCOM activity log endpoint. We only type the - * fields the UI actually consumes; other fields flow through untyped. - */ -export interface ActivityNotificationRange { - indices: [ number, number ]; - type?: string; - url?: string; - section?: string; - intent?: string; - activity?: string; - id?: number | string; - name?: string; - site_id?: number | string; - post_id?: number | string; - site_slug?: string; - slug?: string; - version?: string; - uri?: string; - rewind_id?: string; - published?: string; - [ key: string ]: unknown; -} - -export interface ActivityLogActor { - type?: 'Person' | 'Application' | 'Happiness Engineer'; - name?: string; - role?: string; - icon?: { type?: string; url?: string; width?: number; height?: number }; - is_cli?: boolean; - is_happiness?: boolean; - is_mcp_agent?: boolean; - mcp_client?: string; -} - -export interface ActivityLogEntryImage { - available?: boolean; - medium_url?: string; - thumbnail_url?: string; - type?: string; - name?: string; - url?: string; -} - -export interface ActivityLogObject { - type?: string; - name?: string; - object_id?: number | string; - external_user_id?: number | string; - wpcom_user_id?: number | string; - [ key: string ]: unknown; -} - -export interface ActivityLogEntry { - activity_id: string; - actor?: ActivityLogActor; - content?: { text?: string; ranges?: ActivityNotificationRange[] }; - gridicon?: string; - image?: ActivityLogEntryImage | null; - name: string; - is_rewindable?: boolean; - object?: ActivityLogObject; - published?: string; - rewind_id?: string; - status?: 'error' | 'info' | 'success' | 'warning' | null; - summary: string; -} - -export interface ActivityLogsData { - activityLogs: ActivityLogEntry[]; - totalItems?: number; - pages?: number; - itemsPerPage?: number; - totalPages?: number; -} - -export interface ActivityLogGroupCountResponse { - groups: Record< string, { name: string; count: number } >; - totalItems?: number; -} - -export interface ActivityLogParams { - number?: number; - page?: number; - sort_order?: 'asc' | 'desc'; - after?: string; - before?: string; - group?: string[]; - not_group?: string[]; - text_search?: string; -} diff --git a/projects/packages/activity-log/src/js/components/ActivityLog/upsell-callout.scss b/projects/packages/activity-log/src/js/components/ActivityLog/upsell-callout.scss deleted file mode 100644 index 3d025da7b103..000000000000 --- a/projects/packages/activity-log/src/js/components/ActivityLog/upsell-callout.scss +++ /dev/null @@ -1,49 +0,0 @@ -// Upsell callout rendered below the Activity Log table on free plans. -// Visual intent mirrors Calypso's `ActivityLogsCallout` layout: a centered -// card with the illustration on one side, copy + CTA on the other. -.jp-activity-log__upsell-callout { - display: flex; - flex-direction: column-reverse; - align-items: center; - gap: 24px; - padding: 32px 24px; - margin: 32px auto 0; - max-width: 960px; - - @media (min-width: 782px) { - flex-direction: row; - align-items: center; - gap: 48px; - padding: 40px; - } -} - -.jp-activity-log__upsell-callout-image { - display: block; - width: 100%; - max-width: 320px; - height: auto; - flex-shrink: 0; -} - -.jp-activity-log__upsell-callout-content { - display: flex; - flex-direction: column; - gap: 16px; - flex: 1 1 auto; - min-width: 0; -} - -.jp-activity-log__upsell-callout-title { - margin: 0; - font-size: 24px; - line-height: 1.3; - font-weight: 500; - color: var(--wpds-color-fg-content-neutral, #1e1e1e); -} - -// The - - - - ); -} diff --git a/projects/packages/activity-log/src/js/components/DateRangePicker/datetime.ts b/projects/packages/activity-log/src/js/components/DateRangePicker/datetime.ts deleted file mode 100644 index b1944d0ef1cf..000000000000 --- a/projects/packages/activity-log/src/js/components/DateRangePicker/datetime.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Minimum-surface date/time helpers used by the Activity Log date-range - * picker. Ported from Calypso's `client/dashboard/utils/datetime.ts` — - * only the four functions the picker actually needs are lifted here so - * the port stays self-contained (no cross-package utils dependency). - */ -import { dateI18n } from '@wordpress/date'; -import { parse, isValid, format as fnsFormat } from 'date-fns'; - -const HOUR_MS = 3_600_000; -const YMD_REGEX = /^\d{4}-\d{2}-\d{2}$/; - -/** - * Localized date formatting via `Intl.DateTimeFormat`. - * - * @param date - The date to format. - * @param locale - BCP 47 locale tag (e.g. `en-US`). - * @param formatOptions - `Intl.DateTimeFormatOptions`; defaults to medium. - * @return Formatted string, or `''` when `date` is invalid. - */ -export function formatDate( - date: Date, - locale: string, - formatOptions: Intl.DateTimeFormatOptions = { dateStyle: 'medium' } -): string { - if ( isNaN( date.getTime() ) ) { - return ''; - } - return new Intl.DateTimeFormat( locale, formatOptions ).format( date ); -} - -/** - * Parse a `YYYY-MM-DD` string as a local-time Date. Returns null for - * anything malformed or for "overflow" dates like `2023-02-31` that - * date-fns would otherwise silently normalize. - * - * @param value - The input string. - * @return Parsed Date, or null. - */ -export function parseYmdLocal( value: string ): Date | null { - if ( ! YMD_REGEX.test( value ) ) { - return null; - } - const parsed = parse( value, 'yyyy-MM-dd', new Date() ); - if ( ! isValid( parsed ) ) { - return null; - } - return fnsFormat( parsed, 'yyyy-MM-dd' ) === value ? parsed : null; -} - -/** - * Format a Date as the site's calendar day (`YYYY-MM-DD`). Respects - * the site timezone string when provided; otherwise falls back to the - * numeric `gmtOffset`; finally to the user-locale default. - * - * @param date - The Date to format. - * @param timezoneString - IANA timezone identifier. - * @param gmtOffset - Offset in hours. - * @return `YYYY-MM-DD` string. - */ -export function formatYmd( date: Date, timezoneString?: string, gmtOffset?: number ): string { - if ( timezoneString ) { - return dateI18n( 'Y-m-d', date, timezoneString ); - } - if ( typeof gmtOffset === 'number' ) { - const shifted = new Date( date.getTime() + gmtOffset * HOUR_MS ); - const year = shifted.getUTCFullYear(); - const month = String( shifted.getUTCMonth() + 1 ).padStart( 2, '0' ); - const day = String( shifted.getUTCDate() ).padStart( 2, '0' ); - return `${ year }-${ month }-${ day }`; - } - return dateI18n( 'Y-m-d', date ); -} - -/** - * Format a Date that already represents a site calendar day as - * `YYYY-MM-DD`, without reapplying timezone math. Used for Dates that - * came out of the picker or from a URL. - * - * @param date - Date to format. - * @return `YYYY-MM-DD` string. - */ -export function formatSiteYmd( date: Date ): string { - const year = date.getFullYear(); - const month = String( date.getMonth() + 1 ).padStart( 2, '0' ); - const day = String( date.getDate() ).padStart( 2, '0' ); - return `${ year }-${ month }-${ day }`; -} diff --git a/projects/packages/activity-log/src/js/components/DateRangePicker/index.tsx b/projects/packages/activity-log/src/js/components/DateRangePicker/index.tsx deleted file mode 100644 index 2cca7fca9924..000000000000 --- a/projects/packages/activity-log/src/js/components/DateRangePicker/index.tsx +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Date-range picker shown above the Activity Log table on paid tiers. - * Port of Calypso's - * `client/dashboard/components/date-range-picker/index.tsx`. - * - * The picker is two pieces: a Dropdown toggle showing the current - * label, and the `DateRangeContent` popover with the preset sidebar, - * date inputs, and calendar. State inside the popover is intentionally - * remounted whenever the committed range changes (via the `resetKey`) - * so draft edits don't linger across opens. - */ -import { Dropdown, Tooltip, Button } from '@wordpress/components'; -import { useMediaQuery, useInstanceId } from '@wordpress/compose'; -import { __, sprintf } from '@wordpress/i18n'; -import { calendar } from '@wordpress/icons'; -import { useMemo, useState } from 'react'; -import { DateRangeContent } from './date-range-content'; -import { parseYmdLocal, formatYmd, formatSiteYmd } from './datetime'; -import { formatLabel } from './utils'; -import type { PresetId } from './utils'; -import './style.scss'; - -// `@automattic/ui`'s `DateRangeCalendar` styling lives in its own -// stylesheet — the JS bundle doesn't carry it. Import it here so the -// calendar renders with the Calypso-style clean day numbers instead -// of wp-admin's default button boxes. -import '@automattic/ui/style.css'; - -type DateRangePickerProps = { - start: Date; - end: Date; - onChange: ( next: { start: Date; end: Date } ) => void; - timezoneString?: string; - gmtOffset?: number; - locale: string; - disableFuture?: boolean; - defaultFallbackPreset?: PresetId; - inputsProps?: { - onStartFocus?: ( e: React.FocusEvent< HTMLInputElement > ) => void; - onEndFocus?: ( e: React.FocusEvent< HTMLInputElement > ) => void; - onStartBlur?: ( e: React.FocusEvent< HTMLInputElement > ) => void; - onEndBlur?: ( e: React.FocusEvent< HTMLInputElement > ) => void; - }; -}; - -/** - * - * @param root0 - * @param root0.start - * @param root0.end - * @param root0.onChange - * @param root0.gmtOffset - * @param root0.timezoneString - * @param root0.locale - * @param root0.disableFuture - * @param root0.defaultFallbackPreset - * @param root0.inputsProps - */ -export function DateRangePicker( { - start, - end, - onChange, - gmtOffset, - timezoneString, - locale, - disableFuture = true, - defaultFallbackPreset = 'last-7-days', - inputsProps, -}: DateRangePickerProps ) { - const isSmall = useMediaQuery( '(max-width: 600px)' ); - const showTwoMonths = useMediaQuery( '(min-width: 900px)' ); - const instanceId = useInstanceId( DateRangePicker, 'daterange' ); - const mobileLabelId = `presets-label-${ instanceId }-mobile`; - const desktopLabelId = `presets-label-${ instanceId }-desktop`; - - const label = formatLabel( start, end, locale ); - - const resetKey = [ - formatSiteYmd( start ), - formatSiteYmd( end ), - timezoneString ?? '', - gmtOffset ?? '', - ].join( '|' ); - - return ( - ( - -
- -
-
- ) } - renderContent={ ( { onClose } ) => ( - - ) } - /> - ); -} - -/** - * - * @param root0 - * @param root0.isSmall - * @param root0.showTwoMonths - * @param root0.start - * @param root0.end - * @param root0.timezoneString - * @param root0.gmtOffset - * @param root0.onChange - * @param root0.onClose - * @param root0.mobileLabelId - * @param root0.desktopLabelId - * @param root0.disableFuture - * @param root0.defaultFallbackPreset - * @param root0.inputsProps - * @param root0.inputsProps.onStartFocus - * @param root0.inputsProps.onEndFocus - * @param root0.inputsProps.onStartBlur - * @param root0.inputsProps.onEndBlur - */ -function DateRangePickerInner( { - isSmall, - showTwoMonths, - start, - end, - timezoneString, - gmtOffset, - onChange, - onClose, - mobileLabelId, - desktopLabelId, - disableFuture, - defaultFallbackPreset, - inputsProps, -}: { - isSmall: boolean; - showTwoMonths: boolean; - start: Date; - end: Date; - timezoneString?: string; - gmtOffset?: number; - onChange: ( next: { start: Date; end: Date } ) => void; - onClose: () => void; - mobileLabelId: string; - desktopLabelId: string; - disableFuture: boolean; - defaultFallbackPreset: PresetId; - inputsProps?: { - onStartFocus?: ( e: React.FocusEvent< HTMLInputElement > ) => void; - onEndFocus?: ( e: React.FocusEvent< HTMLInputElement > ) => void; - onStartBlur?: ( e: React.FocusEvent< HTMLInputElement > ) => void; - onEndBlur?: ( e: React.FocusEvent< HTMLInputElement > ) => void; - }; -} ) { - const [ fromDraft, setFromDraft ] = useState< Date | undefined >( () => start ); - const [ toDraft, setToDraft ] = useState< Date | undefined >( () => end ); - const [ fromStr, setFromStr ] = useState( () => formatSiteYmd( start ) ); - const [ toStr, setToStr ] = useState( () => formatSiteYmd( end ) ); - const [ compositeActiveId, setCompositeActiveId ] = useState< string | null >( null ); - - const today = useMemo( () => { - const parsed = parseYmdLocal( formatYmd( new Date(), timezoneString, gmtOffset ) ); - return ( - parsed ?? new Date( new Date().getFullYear(), new Date().getMonth(), new Date().getDate() ) - ); - }, [ timezoneString, gmtOffset ] ); - - const todayStr = useMemo( () => formatSiteYmd( today ), [ today ] ); - - return ( - - ); -} diff --git a/projects/packages/activity-log/src/js/components/DateRangePicker/presets-listbox.tsx b/projects/packages/activity-log/src/js/components/DateRangePicker/presets-listbox.tsx deleted file mode 100644 index 3a5a1b2721f9..000000000000 --- a/projects/packages/activity-log/src/js/components/DateRangePicker/presets-listbox.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Keyboard-navigable list of date-range presets. Verbatim port of - * Calypso's `client/dashboard/components/date-range-picker/presets-listbox.tsx` - * with a local `utils` import. - */ -import { - Button, - __experimentalVStack as VStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis - Composite, - VisuallyHidden, -} from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { presetDefs } from './utils'; -import type { PresetId } from './utils'; - -type PresetsListboxProps = { - labelId: string; - activePresetId?: PresetId; - onSelect: ( id: PresetId ) => void; - compositeActiveId: string | null; - setCompositeActiveId: ( id: string | null ) => void; -}; - -/** - * - * @param root0 - * @param root0.labelId - * @param root0.activePresetId - * @param root0.onSelect - * @param root0.compositeActiveId - * @param root0.setCompositeActiveId - */ -export function PresetsListbox( { - labelId, - activePresetId, - onSelect, - compositeActiveId, - setCompositeActiveId, -}: PresetsListboxProps ) { - const items: ReadonlyArray< { id: PresetId; label: string } > = [ - ...presetDefs, - { id: 'custom' as const, label: __( 'Custom', 'jetpack-activity-log' ) }, - ]; - - return ( - - - { __( 'Date range presets', 'jetpack-activity-log' ) } - - setCompositeActiveId( id ?? null ) } - focusLoop - virtualFocus - role="listbox" - > - - { items.map( preset => { - const isSelected = activePresetId === preset.id; - return ( - } - onClick={ () => onSelect( preset.id ) } - role="option" - aria-selected={ isSelected || undefined } - className="preset-listbox__item" - > - { preset.label } - - ); - } ) } - - - - ); -} diff --git a/projects/packages/activity-log/src/js/components/DateRangePicker/style.scss b/projects/packages/activity-log/src/js/components/DateRangePicker/style.scss deleted file mode 100644 index 9a83bed07b70..000000000000 --- a/projects/packages/activity-log/src/js/components/DateRangePicker/style.scss +++ /dev/null @@ -1,76 +0,0 @@ -// Port of Calypso's `date-range-picker/style.scss`, with Calypso's -// base-style SCSS variables resolved to either literal values or the -// matching `--wpds-*` tokens we adopted in style.scss. - -.daterange-popover .components-popover__content { - overflow: visible; - width: auto; - max-width: calc(100vw - 48px); -} - -.daterange-inputs { - margin-bottom: 12px; -} - -.daterange-input { - - &__field { - - &.components-button { - background: #fff; - padding: 4px 4px 4px 8px; - box-shadow: none; - border-radius: 2px; - border: 1px solid var(--wpds-color-stroke-surface-neutral, #949494); - } - - svg { - color: var(--wpds-color-fg-content-neutral, #1e1e1e); - padding: 4px; - } - } - - &__text { - color: var(--wpds-color-fg-content-neutral, #1e1e1e); - padding: 0 4px; - } -} - -@media (max-width: 600px) { - - .daterange-calendar { - display: flex; - justify-content: center; - width: 100%; - } -} - -@media (min-width: 601px) and (max-width: 899px) { - - .daterange-body { - justify-content: space-between !important; - } -} - -@media (min-width: 601px) { - - .daterange-presets { - min-width: 240px; - } -} - -.preset-listbox__item .components-button { - width: 100%; - justify-content: flex-start; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.daterange-inputs input[type="date"]::-webkit-calendar-picker-indicator { - display: none; -} - -.daterange-inputs input[type="date"]::-webkit-clear-button { - display: none; -} diff --git a/projects/packages/activity-log/src/js/components/DateRangePicker/utils.ts b/projects/packages/activity-log/src/js/components/DateRangePicker/utils.ts deleted file mode 100644 index de4ce1bdc18d..000000000000 --- a/projects/packages/activity-log/src/js/components/DateRangePicker/utils.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** - * Preset computation + active-preset detection for the date-range - * picker. Verbatim port of Calypso's - * `client/dashboard/components/date-range-picker/utils.ts` with local - * datetime imports. - */ -import { __, sprintf } from '@wordpress/i18n'; -import { - startOfDay, - isSameDay, - addDays, - addYears, - startOfMonth, - startOfYear, - differenceInCalendarDays, -} from 'date-fns'; -import { formatDate, parseYmdLocal, formatYmd } from './datetime'; - -const lastNDays = ( date: Date, number: number ) => ( { - from: new Date( date.getFullYear(), date.getMonth(), date.getDate() - ( number - 1 ) ), - to: date, -} ); -const monthToDate = ( date: Date ) => ( { - from: new Date( date.getFullYear(), date.getMonth(), 1 ), - to: date, -} ); -const yearToDate = ( date: Date ) => ( { - from: new Date( date.getFullYear(), 0, 1 ), - to: date, -} ); -const lastTwelveMonths = ( date: Date ) => ( { - from: new Date( date.getFullYear() - 1, date.getMonth(), date.getDate() + 1 ), - to: date, -} ); -const lastThreeYears = ( date: Date ) => ( { - from: new Date( date.getFullYear() - 3, date.getMonth(), date.getDate() + 1 ), - to: date, -} ); - -export type PresetId = - | 'today' - | 'yesterday' - | 'last-7-days' - | 'last-30-days' - | 'month-to-date' - | 'last-12-months' - | 'year-to-date' - | 'last-3-years' - | 'custom'; - -export const presetDefs = [ - { id: 'today', label: __( 'Today', 'jetpack-activity-log' ) }, - { id: 'yesterday', label: __( 'Yesterday', 'jetpack-activity-log' ) }, - { id: 'last-7-days', label: __( 'Last 7 days', 'jetpack-activity-log' ) }, - { id: 'last-30-days', label: __( 'Last 30 days', 'jetpack-activity-log' ) }, - { id: 'month-to-date', label: __( 'Month to date', 'jetpack-activity-log' ) }, - { id: 'last-12-months', label: __( 'Last 12 months', 'jetpack-activity-log' ) }, - { id: 'year-to-date', label: __( 'Year to date', 'jetpack-activity-log' ) }, - { id: 'last-3-years', label: __( 'Last 3 years', 'jetpack-activity-log' ) }, -] as const satisfies ReadonlyArray< { id: Exclude< PresetId, 'custom' >; label: string } >; - -/** - * - * @param preset - * @param baseDate - */ -export function computePresetRange( preset: PresetId, baseDate: Date ) { - switch ( preset ) { - case 'today': - return { from: baseDate, to: baseDate }; - case 'yesterday': - return { - from: new Date( baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate() - 1 ), - to: new Date( baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate() - 1 ), - }; - case 'last-7-days': - return lastNDays( baseDate, 7 ); - case 'last-30-days': - return lastNDays( baseDate, 30 ); - case 'month-to-date': - return monthToDate( baseDate ); - case 'last-12-months': - return lastTwelveMonths( baseDate ); - case 'year-to-date': - return yearToDate( baseDate ); - case 'last-3-years': - return lastThreeYears( baseDate ); - default: - return undefined; - } -} - -/** - * - * @param from - * @param to - * @param baseDate - */ -export function getActivePresetId( from?: Date, to?: Date, baseDate?: Date ): PresetId | undefined { - if ( ! from || ! to || ! baseDate ) { - return; - } - let newFrom = startOfDay( from ); - let newTo = startOfDay( to ); - if ( newFrom.getTime() > newTo.getTime() ) { - const tmp = newFrom; - newFrom = newTo; - newTo = tmp; - } - - const todayStart = startOfDay( baseDate ); - const yesterdayStart = addDays( todayStart, -1 ); - - if ( isSameDay( newFrom, todayStart ) && isSameDay( newTo, todayStart ) ) { - return 'today'; - } - if ( isSameDay( newFrom, yesterdayStart ) && isSameDay( newTo, yesterdayStart ) ) { - return 'yesterday'; - } - - if ( isSameDay( newTo, todayStart ) ) { - const diff = differenceInCalendarDays( todayStart, newFrom ); - if ( diff === 6 ) { - return 'last-7-days'; - } - if ( diff === 29 ) { - return 'last-30-days'; - } - if ( - isSameDay( newFrom, addYears( todayStart, -1 ) ) || - isSameDay( newFrom, addDays( addYears( todayStart, -1 ), 1 ) ) - ) { - return 'last-12-months'; - } - if ( - isSameDay( newFrom, addYears( todayStart, -3 ) ) || - isSameDay( newFrom, addDays( addYears( todayStart, -3 ), 1 ) ) - ) { - return 'last-3-years'; - } - } - - if ( isSameDay( newFrom, startOfMonth( todayStart ) ) && isSameDay( newTo, todayStart ) ) { - return 'month-to-date'; - } - if ( isSameDay( newFrom, startOfYear( todayStart ) ) && isSameDay( newTo, todayStart ) ) { - return 'year-to-date'; - } - - for ( const preset of presetDefs ) { - const range = computePresetRange( preset.id as PresetId, todayStart ); - if ( - range && - isSameDay( newFrom, startOfDay( range.from ) ) && - isSameDay( newTo, startOfDay( range.to ) ) - ) { - return preset.id as PresetId; - } - } - return undefined; -} - -/** - * - * @param start - * @param end - * @param locale - */ -export function formatLabel( start: Date, end: Date, locale: string ): string { - return sprintf( - /* translators: %1$s: start date, %2$s: end date */ - __( '%1$s to %2$s', 'jetpack-activity-log' ), - formatDate( start, locale, { dateStyle: 'medium' } ), - formatDate( end, locale, { dateStyle: 'medium' } ) - ); -} - -/** - * - * @param range - * @param range.start - * @param range.end - * @param timezoneString - * @param gmtOffset - */ -export function isLast7Days( - range: { start: Date; end: Date }, - timezoneString?: string, - gmtOffset?: number -): boolean { - const siteToday = - parseYmdLocal( formatYmd( new Date(), timezoneString, gmtOffset ) ) ?? - new Date( new Date().getFullYear(), new Date().getMonth(), new Date().getDate() ); - return getActivePresetId( range.start, range.end, siteToday ) === 'last-7-days'; -} diff --git a/projects/packages/activity-log/src/js/hooks/use-activity-log.ts b/projects/packages/activity-log/src/js/hooks/use-activity-log.ts deleted file mode 100644 index 6d9c619cfb34..000000000000 --- a/projects/packages/activity-log/src/js/hooks/use-activity-log.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * TanStack Query factories for the Activity Log REST endpoints. - * - * These mirror Calypso's `siteActivityLogQuery` / `siteActivityLogGroupCountsQuery` - * shapes — same query keys, same response transform (`current.orderedItems → - * activityLogs`) — so the UI code ports with minimal changes. The transport - * is `@wordpress/api-fetch` against `/jetpack/v4/activity-log/*` instead of - * WPCOM directly. - */ -import { queryOptions } from '@tanstack/react-query'; -import apiFetch from '@wordpress/api-fetch'; -import type { - ActivityLogEntry, - ActivityLogGroupCountResponse, - ActivityLogParams, - ActivityLogsData, -} from '../components/ActivityLog/types'; - -interface RawActivityLogResponse { - current?: { orderedItems?: ActivityLogEntry[] }; - totalItems?: number; - pages?: number; - itemsPerPage?: number; - totalPages?: number; -} - -/** - * Assemble a REST path with a query string, handling array params by - * appending them with the PHP-style `[]` suffix our REST controller - * understands. - * - * Note that `apiFetch`/`@wordpress/url` re-serialize the path before - * dispatching, so the live wire form is typically indexed - * (`group[0]=plugin`) rather than the bare `group[]=plugin` this - * function emits — either form round-trips through the controller's - * `type: array` validation, so both are accepted. - * - * @param base - Path prefix, e.g. `/jetpack/v4/activity-log`. - * @param params - Key/value map of query params. Arrays produce repeated - * `key[]=value` pairs; undefined/null are dropped. - * @return The combined path. - */ -const buildPath = ( base: string, params: ActivityLogParams ): string => { - const search = new URLSearchParams(); - Object.entries( params ).forEach( ( [ key, value ] ) => { - if ( value === undefined || value === null ) { - return; - } - if ( Array.isArray( value ) ) { - value.forEach( entry => search.append( `${ key }[]`, String( entry ) ) ); - return; - } - search.append( key, String( value ) ); - } ); - const query = search.toString(); - return query ? `${ base }?${ query }` : base; -}; - -/** - * TanStack Query options for the paginated activity list. Unwraps the WPCOM - * `current.orderedItems` shape into the flatter `activityLogs` shape the UI - * components consume. - * - * @param params - Forwarded to the server as query params. - * @return `queryOptions` ready to pass to `useQuery`. - */ -export function activityLogQuery( params: ActivityLogParams ) { - return queryOptions( { - queryKey: [ 'jetpack-activity-log', 'list', params ], - queryFn: async (): Promise< ActivityLogsData > => { - const response = await apiFetch< RawActivityLogResponse >( { - path: buildPath( '/jetpack/v4/activity-log', params ), - } ); - return { - activityLogs: response.current?.orderedItems ?? [], - totalItems: response.totalItems, - pages: response.pages, - itemsPerPage: response.itemsPerPage, - totalPages: response.totalPages, - }; - }, - } ); -} - -/** - * TanStack Query options for the group-counts endpoint powering the - * Activity Type filter dropdown. - * - * @param params - Forwarded to the server as query params. - * @return `queryOptions` ready to pass to `useQuery`. - */ -export function activityLogGroupCountsQuery( params: ActivityLogParams ) { - return queryOptions( { - queryKey: [ 'jetpack-activity-log', 'group-counts', params ], - queryFn: async (): Promise< ActivityLogGroupCountResponse > => { - return apiFetch< ActivityLogGroupCountResponse >( { - path: buildPath( '/jetpack/v4/activity-log/count/group', params ), - } ); - }, - } ); -} diff --git a/projects/packages/activity-log/src/js/hooks/use-analytics.ts b/projects/packages/activity-log/src/js/hooks/use-analytics.ts deleted file mode 100644 index ee5983b08dd9..000000000000 --- a/projects/packages/activity-log/src/js/hooks/use-analytics.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Thin wrapper around `@automattic/jetpack-analytics` that identifies - * the tracker with the connected WPCOM user (matching Backup's - * `useAnalytics` hook). Components call `useAnalytics()` and then - * `tracks.recordEvent( 'jetpack_activity_log_', { …props } )`. - * - * Anonymous events still fire if the user-connection state isn't ready - * yet; the identify call just fills in ID + login when available. - */ -import jetpackAnalytics from '@automattic/jetpack-analytics'; -import { useConnection } from '@automattic/jetpack-connection'; -import { useEffect } from 'react'; - -// Module-level guard so multiple consumers of `useAnalytics` don't -// re-call `initialize()` with the same identity on every mount. -// `jetpackAnalytics` is a singleton; once it's been identified for a -// given (id, login) pair there's nothing to redo. -let identifiedFor: string | null = null; - -/** - * Returns the shared `jetpackAnalytics` tracker, identified with the - * connected WPCOM user once that state becomes available. - * - * @return The `jetpackAnalytics` singleton. Callers typically - * destructure `{ tracks }` and record events via - * `tracks.recordEvent( name, props )`. - */ -export function useAnalytics() { - const { isUserConnected, userConnectionData } = useConnection( {} ); - const wpcomUser = userConnectionData?.currentUser?.wpcomUser; - const wpcomId = wpcomUser?.ID; - const wpcomLogin = wpcomUser?.login; - - useEffect( () => { - if ( ! isUserConnected || ! wpcomId || ! wpcomLogin ) { - return; - } - const key = `${ wpcomId }:${ wpcomLogin }`; - if ( identifiedFor === key ) { - return; - } - jetpackAnalytics.initialize( wpcomId, wpcomLogin ); - identifiedFor = key; - }, [ isUserConnected, wpcomId, wpcomLogin ] ); - - return jetpackAnalytics; -} - -export default useAnalytics; diff --git a/projects/packages/activity-log/src/js/hooks/use-persistent-view.ts b/projects/packages/activity-log/src/js/hooks/use-persistent-view.ts deleted file mode 100644 index fcb9fb0483d4..000000000000 --- a/projects/packages/activity-log/src/js/hooks/use-persistent-view.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Persistent DataViews view state for the Activity Log. - * - * Mirrors the behavior of Calypso's `usePersistentView` - * (client/dashboard/app/hooks/use-persistent-view.ts): persist the - * non-transient view config (fields, density, perPage, sort, layout), - * not the transient bits (`page`, `search`, empty `filters`). Calypso - * persists to WordPress.com user preferences; in a self-hosted Jetpack - * plugin we don't have that API, so we back the store with - * `localStorage` instead. The hook signature stays swappable: a future - * move to a user-meta-backed store only touches this file. - */ -import fastDeepEqual from 'fast-deep-equal/es6'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import type { View } from '@wordpress/dataviews'; - -interface InitialStateShape { - siteData?: { id?: number | string }; -} - -declare const JPACTIVITYLOG_INITIAL_STATE: InitialStateShape | undefined; - -// Site-scope the storage key on the WPCOM blog ID seeded by Initial_State. -// Without this, an admin who manages multiple Jetpack-connected sites in the -// same browser would share a single view across all of them. Falls back to -// `default` when the global is absent (storybook/tests) or the id is unset. -const getStorageKey = (): string => { - const state = - typeof JPACTIVITYLOG_INITIAL_STATE !== 'undefined' ? JPACTIVITYLOG_INITIAL_STATE : undefined; - const siteId = state?.siteData?.id; - const scope = siteId !== undefined && siteId !== null && siteId !== '' ? siteId : 'default'; - return `jetpack-activity-log:view:${ scope }`; -}; - -const readPersistedView = (): View | null => { - if ( typeof window === 'undefined' ) { - return null; - } - try { - const raw = window.localStorage.getItem( getStorageKey() ); - if ( ! raw ) { - return null; - } - const parsed = JSON.parse( raw ); - return parsed && typeof parsed === 'object' ? ( parsed as View ) : null; - } catch { - return null; - } -}; - -const writePersistedView = ( view: View | null ): void => { - if ( typeof window === 'undefined' ) { - return; - } - try { - if ( view === null ) { - window.localStorage.removeItem( getStorageKey() ); - } else { - window.localStorage.setItem( getStorageKey(), JSON.stringify( view ) ); - } - } catch { - // Quota exceeded or localStorage disabled — drop silently. - } -}; - -const stripTransient = ( v: View ): View => { - const next = { ...v }; - delete next.page; - delete next.search; - // Filters count as transient too — a selected activity-type is - // scoped to the current debugging session, not a long-lived - // preference. (Previously only empty-array filters were dropped, - // which meant a "Plugins" selection survived across reloads — - // reported in review.) - delete next.filters; - return next; -}; - -// Narrow whitelist of the fields the user can actually edit from the -// settings cog (sort, order, properties, density, items-per-page, plus -// filters). Comparing the full view object instead can flip the -// "modified" bit when DataViews normalizes an unrelated internal field -// on mount; comparing only the signature avoids that false positive. -const viewSignature = ( v: View ) => ( { - fields: v.fields, - sort: v.sort, - perPage: v.perPage, - density: v.layout?.density, - filters: v.filters?.length ? v.filters : undefined, -} ); - -const isMeaningfullyModified = ( current: View, base: View ): boolean => - ! fastDeepEqual( viewSignature( current ), viewSignature( base ) ); - -/** - * Hook that tracks a DataViews view and persists the non-transient - * parts to localStorage. - * - * @param defaultView - The fallback view used when no persisted entry - * exists. Also the reference point for `isViewModified` and the target - * of `resetView`. - * @return An object with the current `view`, a `setView` persistence- - * aware setter, a `resetView` function, and the `isViewModified` flag - * the `onReset` prop needs to decide whether to show the Reset view - * button. - */ -export function usePersistentView( defaultView: View ): { - view: View; - setView: ( next: View ) => void; - resetView: () => void; - isViewModified: boolean; -} { - const [ view, setViewState ] = useState< View >( () => { - const persisted = readPersistedView(); - // Self-heal: if a previous session wrote a "not really modified" - // view (e.g. because DataViews touched a layout subfield on mount - // before we added the viewSignature whitelist), boot with the - // default so Reset view stays disabled on load. The matching - // localStorage cleanup runs in the effect below — keeping side - // effects out of the lazy initializer so React 18 strict-mode's - // double-invoke doesn't write twice. - if ( persisted && ! isMeaningfullyModified( persisted, defaultView ) ) { - return defaultView; - } - return persisted ?? defaultView; - } ); - - // One-shot mount cleanup for the self-heal case above. - useEffect( () => { - const persisted = readPersistedView(); - if ( persisted && ! isMeaningfullyModified( persisted, defaultView ) ) { - writePersistedView( null ); - } - // Mount-only — `defaultView` is a stable module-level constant - // (DEFAULT_VIEW), so re-checking on identity changes would be - // noise. Intentional empty deps. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [] ); - - const setView = useCallback( - ( next: View ) => { - setViewState( next ); - - // Persist only when the signature differs. We still write the - // full stripped view (not just the signature) so future fields - // restore correctly — the signature just gates whether we - // persist at all. - if ( isMeaningfullyModified( next, defaultView ) ) { - writePersistedView( stripTransient( next ) ); - } else { - writePersistedView( null ); - } - }, - [ defaultView ] - ); - - const resetView = useCallback( () => { - setViewState( defaultView ); - writePersistedView( null ); - }, [ defaultView ] ); - - const isViewModified = useMemo( - () => isMeaningfullyModified( view, defaultView ), - [ view, defaultView ] - ); - - return { view, setView, resetView, isViewModified }; -} diff --git a/projects/packages/activity-log/src/js/index.js b/projects/packages/activity-log/src/js/index.js deleted file mode 100644 index fabb77c80e00..000000000000 --- a/projects/packages/activity-log/src/js/index.js +++ /dev/null @@ -1,41 +0,0 @@ -import { ThemeProvider } from '@automattic/jetpack-components'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import * as WPElement from '@wordpress/element'; -import ActivityLog from './components/ActivityLog'; -import './style.scss'; - -// The activity log is append-only: new events land upstream while this -// page stays open, so a cached snapshot goes stale within seconds. -// A finite `staleTime` + `refetchOnWindowFocus` keeps the list current -// without hammering WPCOM on every keystroke — react-query still -// de-dupes requests that share a key inside the window. -const queryClient = new QueryClient( { - defaultOptions: { - queries: { - staleTime: 60_000, - refetchOnWindowFocus: true, - }, - }, -} ); - -/** - * Initial render function. - */ -function render() { - const container = document.getElementById( 'jetpack-activity-log-root' ); - - if ( null === container ) { - return; - } - - const component = ( - - - - - - ); - WPElement.createRoot( container ).render( component ); -} - -render(); diff --git a/projects/packages/activity-log/src/js/style.scss b/projects/packages/activity-log/src/js/style.scss deleted file mode 100644 index e3bfde46e02b..000000000000 --- a/projects/packages/activity-log/src/js/style.scss +++ /dev/null @@ -1,68 +0,0 @@ -@use "@automattic/jetpack-base-styles/admin-page-layout" as *; -@use "sass:meta"; - -// DataViews is a bundled (not externalized) @wordpress package in -// jetpack-webpack-config, so its stylesheet must be brought in alongside -// the JS. See projects/packages/forms/routes/shared.scss for the same pattern. -@include meta.load-css("@wordpress/dataviews/build-style/style.css"); - - -body.toplevel_page_jetpack-activity-log, -body.jetpack_page_jetpack-activity-log { - - @include jetpack-admin-page-layout; -} - -// AdminPage provides the Jetpack header + footer + viewport-pinned -// scroll column; let the DataViews table live full-bleed inside it with -// no extra inset. -.jp-activity-log__dataviews-wrapper { - display: flex; - flex-direction: column; - min-height: 0; - flex: 1 1 auto; - padding: 0; - - // DataViews renders its own surface; don't double-pad it. - > .dataviews-wrapper { - margin: 0; - } - - // DataViews always renders the Activity-layout description slot, - // even when our renderer returns `null` for entries with no - // description payload (e.g. "User removed"). The empty - // `
` then - // participates in the column stack's gap and leaves a visible - // blank line under the title. Collapse the row when the slot has - // no children so the layout flows tightly. - .dataviews-view-activity__item-description:empty { - display: none; - } - - // DataViews shows a small "modified" dot on the cog whenever the - // current view differs from the default (driven by our `onReset` - // prop). Hide it: the same signal already gates the Reset view - // button inside the popover, which is enough — the cog dot just - // adds visual noise on a page where users routinely toggle sort - // order, density, or filters. - .dataviews-view-config__modified-indicator { - display: none; - } - - // Activity-layout meta row ([date] … [actor]). DataViews renders - // this as a flex row with the default `align-items: stretch`, which - // left-aligns the date text to the top while the actor's icon+name - // HStack sits centered — producing a visible baseline mismatch - // (`Apr 24 …` at top, avatar+name in the middle). Center the row - // so every cell shares a single vertical midline, and push the - // trailing cell (Actor) to the far right so the date anchors left - // and the actor anchors right. - .dataviews-view-activity__item-fields { - align-items: center; - - .dataviews-view-activity__item-field:last-child { - margin-inline-start: auto; - } - } -} - diff --git a/projects/packages/activity-log/tsconfig.json b/projects/packages/activity-log/tsconfig.json deleted file mode 100644 index 2a129589160d..000000000000 --- a/projects/packages/activity-log/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "jetpack-js-tools/tsconfig.base.json", - "include": [ "./src/js", "types.d.ts" ] -} diff --git a/projects/packages/activity-log/types.d.ts b/projects/packages/activity-log/types.d.ts deleted file mode 100644 index 4333ece78302..000000000000 --- a/projects/packages/activity-log/types.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.svg' { - const content: string; - export default content; -} diff --git a/projects/packages/activity-log/webpack.config.js b/projects/packages/activity-log/webpack.config.js deleted file mode 100644 index 431ac9e9e5c9..000000000000 --- a/projects/packages/activity-log/webpack.config.js +++ /dev/null @@ -1,75 +0,0 @@ -const path = require( 'path' ); -const jetpackWebpackConfig = require( '@automattic/jetpack-webpack-config/webpack' ); - -module.exports = [ - { - entry: { - index: './src/js/index.js', - }, - mode: jetpackWebpackConfig.mode, - devtool: jetpackWebpackConfig.devtool, - output: { - ...jetpackWebpackConfig.output, - path: path.resolve( './build' ), - }, - optimization: { - ...jetpackWebpackConfig.optimization, - }, - resolve: { - ...jetpackWebpackConfig.resolve, - }, - node: false, - plugins: [ ...jetpackWebpackConfig.StandardPlugins() ], - module: { - strictExportPresence: true, - rules: [ - // Transpile JavaScript - jetpackWebpackConfig.TranspileRule( { - exclude: /node_modules\//, - } ), - - // Transpile @automattic/jetpack-* in node_modules too. - jetpackWebpackConfig.TranspileRule( { - includeNodeModules: [ '@automattic/jetpack-' ], - } ), - - // Workarounds for non-extracted `@wordpress/*` packages. - ...jetpackWebpackConfig.BundledWpPkgsTranspileRules(), - - // `@automattic/ui` ships `__("Date calendar")` / etc. - // inside `DateRangeCalendar` without a text-domain arg - // — Jetpack's production i18n check plugin flags those - // as errors in the bundle. Run the textdomain-replace - // babel plugin over it, same recipe as the `@wordpress/*` - // bundled packages above. - jetpackWebpackConfig.TranspileRule( { - includeNodeModules: [ '@automattic/ui/' ], - babelOpts: { - configFile: false, - plugins: [ - [ - require.resolve( '@automattic/babel-plugin-replace-textdomain' ), - { textdomain: 'jetpack-activity-log' }, - ], - ], - }, - } ), - - // Handle CSS. - jetpackWebpackConfig.CssRule( { - extensions: [ 'css', 'sass', 'scss' ], - extraLoaders: [ { loader: 'sass-loader', options: { api: 'modern-compiler' } } ], - } ), - - // Handle images. - jetpackWebpackConfig.FileRule(), - ], - }, - externals: { - ...jetpackWebpackConfig.externals, - jetpackConfig: JSON.stringify( { - consumer_slug: 'jetpack-activity-log', - } ), - }, - }, -]; diff --git a/projects/packages/jetpack-mu-wpcom/changelog/hide-activity-log-native b/projects/packages/jetpack-mu-wpcom/changelog/hide-activity-log-native deleted file mode 100644 index b1b786ad74e2..000000000000 --- a/projects/packages/jetpack-mu-wpcom/changelog/hide-activity-log-native +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -wpcom-admin-menu: hide the new jetpack-activity-log submenu on WPCOM hosts so the direct wordpress.com/activity-log link wins. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-menu/wpcom-admin-menu.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-menu/wpcom-admin-menu.php index f299e5fdf496..aed36460d337 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-menu/wpcom-admin-menu.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-menu/wpcom-admin-menu.php @@ -430,9 +430,8 @@ function () { ); } - // Jetpack > Activity Log. On WPCOM hosts we prefer the direct wordpress.com/activity-log link - // below; hide the native Jetpack Activity Log page added by the `jetpack-activity-log` package. - wpcom_hide_submenu_page( 'jetpack', 'jetpack-activity-log' ); + // Jetpack > Activity Log. + wpcom_hide_submenu_page( 'jetpack', esc_url( Redirect::get_url( 'cloud-activity-log-wp-menu', array( 'site' => $blog_id ) ) ) ); add_submenu_page( 'jetpack', /** "Activity Log" is a product name, do not translate. */ diff --git a/projects/packages/my-jetpack/changelog/remove-activitylog-menu b/projects/packages/my-jetpack/changelog/remove-activitylog-menu deleted file mode 100644 index b271071ac160..000000000000 --- a/projects/packages/my-jetpack/changelog/remove-activitylog-menu +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: removed - -Drop the legacy Activity Log menu registration; the new activity-log package now owns that menu item. diff --git a/projects/packages/my-jetpack/src/class-activitylog.php b/projects/packages/my-jetpack/src/class-activitylog.php new file mode 100644 index 000000000000..fecdce9ca310 --- /dev/null +++ b/projects/packages/my-jetpack/src/class-activitylog.php @@ -0,0 +1,58 @@ +is_user_connected() ) { + return; + } + + // Do not display the menu on Multisite. + if ( is_multisite() ) { + return; + } + + $args = array(); + + $blog_id = Connection_Manager::get_site_id( true ); + if ( $blog_id ) { + $args = array( 'site' => $blog_id ); + } + + return Admin_Menu::add_menu( + /** "Activity Log" is a product name, do not translate. */ + 'Activity Log', + 'Activity Log ', + 'manage_options', + esc_url( Redirect::get_url( 'cloud-activity-log-wp-menu', $args ) ), + null, + 14 + ); + } +} diff --git a/projects/packages/my-jetpack/src/class-initializer.php b/projects/packages/my-jetpack/src/class-initializer.php index ea8d477ef0cb..406de03a7928 100644 --- a/projects/packages/my-jetpack/src/class-initializer.php +++ b/projects/packages/my-jetpack/src/class-initializer.php @@ -104,6 +104,9 @@ public static function init() { // Sets up JITMS. JITM::configure(); + // Add "Activity Log" menu item. + Activitylog::init(); + // Add "Jetpack Manage" menu item. Jetpack_Manage::init(); diff --git a/projects/packages/my-jetpack/tests/php/Activitylog_Test.php b/projects/packages/my-jetpack/tests/php/Activitylog_Test.php new file mode 100644 index 000000000000..b07248c307de --- /dev/null +++ b/projects/packages/my-jetpack/tests/php/Activitylog_Test.php @@ -0,0 +1,84 @@ +admin_id = wp_insert_user( + array( + 'user_login' => 'dummy_user', + 'user_pass' => 'dummy_pass', + 'role' => 'administrator', + ) + ); + + $this->editor_id = wp_insert_user( + array( + 'user_login' => 'dummy_user_2', + 'user_pass' => 'dummy_pass_2', + 'role' => 'editor', + ) + ); + wp_set_current_user( 0 ); + } + + /** + * Tear down after each test. + */ + public function tear_down() { + wp_set_current_user( 0 ); + } + + /** + * Test that the menu is not added when on multisite. + */ + public function test_add_submenu_jetpack_multisite() { + if ( is_multisite() ) { + $this->assertFalse( Activitylog::add_submenu_jetpack() ); + } + + $this->assertNotFalse( Activitylog::add_submenu_jetpack() ); + } + + /** + * Test that the menu doesn't appear for non-admins. + */ + public function test_add_submenu_jetpack_editor() { + wp_set_current_user( $this->editor_id ); + + $this->assertNull( Activitylog::add_submenu_jetpack() ); + } + + /** + * Test that the menu appears for admins. + */ + public function test_add_submenu_jetpack_admin() { + wp_set_current_user( $this->admin_id ); + + $this->assertNotFalse( Activitylog::add_submenu_jetpack() ); + } +} diff --git a/projects/plugins/jetpack/changelog/add-activity-log-package b/projects/plugins/jetpack/changelog/add-activity-log-package deleted file mode 100644 index 14427e7d5ec4..000000000000 --- a/projects/plugins/jetpack/changelog/add-activity-log-package +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: enhancement - -Activity Log: replace the external sidebar redirect with a native wp-admin page — search, activity-type filter, sort, pagination, and a date-range picker. diff --git a/projects/plugins/jetpack/class.jetpack.php b/projects/plugins/jetpack/class.jetpack.php index d06b83c30ab3..dfbcacad2a74 100644 --- a/projects/plugins/jetpack/class.jetpack.php +++ b/projects/plugins/jetpack/class.jetpack.php @@ -7,7 +7,6 @@ * @package automattic/jetpack */ -use Automattic\Jetpack\Activity_Log\Jetpack_Activity_Log as Activity_Log_Init; use Automattic\Jetpack\Assets; use Automattic\Jetpack\Boost_Speed_Score\Speed_Score; use Automattic\Jetpack\Config; @@ -870,7 +869,6 @@ function ( $methods ) { public function late_initialization() { add_action( 'after_setup_theme', array( 'Jetpack', 'load_modules' ), -2 ); My_Jetpack_Initializer::init(); - Activity_Log_Init::initialize(); // Initialize Boost Speed Score new Speed_Score( array(), 'jetpack-dashboard' ); diff --git a/projects/plugins/jetpack/composer.json b/projects/plugins/jetpack/composer.json index c4e4afbc3301..3e2d5fd1c6a2 100644 --- a/projects/plugins/jetpack/composer.json +++ b/projects/plugins/jetpack/composer.json @@ -14,7 +14,6 @@ "automattic/block-delimiter": "@dev", "automattic/jetpack-a8c-mc-stats": "@dev", "automattic/jetpack-account-protection": "@dev", - "automattic/jetpack-activity-log": "@dev", "automattic/jetpack-admin-ui": "@dev", "automattic/jetpack-assets": "@dev", "automattic/jetpack-autoloader": "@dev", diff --git a/projects/plugins/jetpack/composer.lock b/projects/plugins/jetpack/composer.lock index fbeaa6f969c5..57f29e01629b 100644 --- a/projects/plugins/jetpack/composer.lock +++ b/projects/plugins/jetpack/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7f4ec822d399a46053e8c16b122223c0", + "content-hash": "ff5646ec92656db4a5e84144887bd6ab", "packages": [ { "name": "automattic/block-delimiter", @@ -190,72 +190,6 @@ "relative": true } }, - { - "name": "automattic/jetpack-activity-log", - "version": "dev-trunk", - "dist": { - "type": "path", - "url": "../../packages/activity-log", - "reference": "dc868729e923f3ba834fa42cc9a2e156b89368b8" - }, - "require": { - "automattic/jetpack-admin-ui": "@dev", - "automattic/jetpack-assets": "@dev", - "automattic/jetpack-autoloader": "@dev", - "automattic/jetpack-composer-plugin": "@dev", - "automattic/jetpack-connection": "@dev", - "automattic/jetpack-status": "@dev", - "php": ">=7.2" - }, - "require-dev": { - "automattic/jetpack-changelogger": "@dev", - "automattic/jetpack-test-environment": "@dev", - "automattic/phpunit-select-config": "@dev", - "yoast/phpunit-polyfills": "^4.0.0" - }, - "suggest": { - "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." - }, - "type": "jetpack-library", - "extra": { - "autotagger": true, - "mirror-repo": "Automattic/jetpack-activity-log", - "textdomain": "jetpack-activity-log", - "version-constants": { - "::PACKAGE_VERSION": "src/class-package-version.php" - }, - "changelogger": { - "link-template": "https://github.com/Automattic/jetpack-activity-log/compare/v${old}...v${new}" - }, - "branch-alias": { - "dev-trunk": "0.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "scripts": { - "build-development": [ - "pnpm run build" - ], - "build-production": [ - "pnpm run build-production-concurrently" - ], - "watch": [ - "Composer\\Config::disableProcessTimeout", - "pnpm run watch" - ] - }, - "license": [ - "GPL-2.0-or-later" - ], - "description": "Activity Log UI for the Jetpack plugin in wp-admin.", - "transport-options": { - "relative": true - } - }, { "name": "automattic/jetpack-admin-ui", "version": "dev-trunk", @@ -5749,7 +5683,6 @@ "automattic/block-delimiter": 20, "automattic/jetpack-a8c-mc-stats": 20, "automattic/jetpack-account-protection": 20, - "automattic/jetpack-activity-log": 20, "automattic/jetpack-admin-ui": 20, "automattic/jetpack-assets": 20, "automattic/jetpack-autoloader": 20,