Skip to content

Conversation

rcrdortiz
Copy link
Contributor

@rcrdortiz rcrdortiz commented Sep 5, 2025

Part of:

Solves:

Proposed Changes

  • Implemented API queries that support the different actions needed by the data view.
  • Implemented the actions that consume the API queries.
  • Implemented messaging and notices when performing actions.

Known issues:

  • When performing an action, the dataview doesn't update the state. I'll handle it in a follow-up PR.
manage-plugins.mp4

Why are these changes being made?

  • On the hosting dashboard, we need to support multi-site plugin management.

Testing Instructions

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • Have you tested accessibility for your changes? Ensure the feature remains usable with various user agents (e.g., browsers), interfaces (e.g., keyboard navigation), and assistive technologies (e.g., screen readers) (PCYsg-S3g-p2).
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

@rcrdortiz rcrdortiz requested a review from a team September 5, 2025 12:46
@rcrdortiz rcrdortiz requested a review from a team as a code owner September 5, 2025 12:46
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Sep 5, 2025
Copy link

github-actions bot commented Sep 5, 2025

@matticbot
Copy link
Contributor

matticbot commented Sep 5, 2025

This PR modifies the release build for the following Calypso Apps:

For info about this notification, see here: PCYsg-OT6-p2

  • help-center
  • notifications
  • wpcom-block-editor

To test WordPress.com changes, run install-plugin.sh $pluginSlug dsgcom-232-hosting-dashboard-plugins-port-calypso-actions-to-api on your sandbox.

plugins: SitePlugin[];
};

export type SitePlugin = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a different structure than the existing PluginItem? I'm guessing these types should be abstracted/shared on some level?

export interface PluginItem {
	slug: string;
	active: boolean;
	id: string;
	name: string;
	plugin_url: string;
	version: string;
	description: string;
	author: string;
	author_url: string;
	network: boolean;
	autoupdate?: boolean;
	update: PluginUpdate;
	uninstallable?: boolean;
	is_managed?: boolean;
	action_links?: Record< string, string >;
}

We will need the is_managed?: boolean; property be added for the scheduled updates, but wondering why we have two types that are nearly identical.

/>
),
isEligible: ( item: PluginListRow ) => {
return item.isActive === 'none';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried deactivating a plugin first to then delete it, but I don't see the option:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the plugins need to be deactivated before you can delete the plugin. From your action list you can see that some aren't deactivated since the deactivate action is available.

import type { PluginListRow } from '../types';
import type { PluginItem, PluginsResponse } from '@automattic/api-core';

function toTriState( count: number, total: number ): 'all' | 'some' | 'none' {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the function name a bit confusing, maybe something like countToQuantifier would be more easily understandable. Wdyt?

return [];
}
const sites = response.sites;
type Aggregated = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure it makes any difference, but I'd move the type outside the function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't make a difference from a compiled point of view. I'll move it outside anyways for clarity.

Copy link
Member

@p-jackson p-jackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting PR because we haven't got many examples that can be copied for how TanStack Query can be used to handle bulk actions.

errorCount: number;
};

export const runPerSitePlugin = async (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this way of manually driving a chain of mutations isn't quite right. Yes we can manually call onSuccess, but what about onSettled, the retry option, etc., etc.

In order to get the magic that TanStack Query provides, the mutation options need to be called along with the useMutation hook. If you just want to treat the actions as regular promises, then I think it makes sense to simply bypass the mutation mechanism all together. Call the api-core functions directly and use Promise.allSettled to wait for them all to finish. You could then wrap it up with a final call to queryClient.invalidateQueries.

If you do want to use the TanStack magic (retries, error handling, etc.) then useMutation will need to be called for each of the different types of mutation. I think it could look something like this:

const { mutateAsync: activatePlugin } = useMutation( sitePluginActivateMutation() );
const { mutateAsync: deactivatePlugin } = useMutation( sitePluginDeactivateMutation() );
// ... there needs to be a `useMutation` for each of these

const tasks = siteIds.map( ( siteId ) => {
	switch ( action ) {
		case 'activate':
			return activatePlugin( siteId );
		case 'deactivate':
			return deactivatePlugin( siteId );
		// ... and so on
	}
} );

Note that I've used the mutateAsync return value from useMutation, since this is how you get a function that returns a promise. And also I've moved the site ID from being a mutation option parameter, to a mutationFn parameter. Since we can't know when the hook is called what the actual site IDs will be.

If you follow the previous suggestion of merging some of the api-core and api-queries functions together, the number of hooks and switch cases should be smaller too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll refactor the code to use useMutation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants