Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Use components for empty actions in trash/spam for new wp-build dashboard.
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The changelog entry uses "actions" instead of "buttons" or "CTAs" which could be confusing since "actions" has a specific meaning in WordPress (data store actions, form actions, etc.). Consider changing to "Use components for empty trash/spam buttons in wp-build dashboard." for clarity.

Suggested change
Use components for empty actions in trash/spam for new wp-build dashboard.
Use components for empty trash/spam buttons in wp-build dashboard.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

Changelog entry should end with a period according to the project's changelog guidelines.

Copilot generated this review using guidance from repository custom instructions.
159 changes: 82 additions & 77 deletions projects/packages/forms/routes/responses/stage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ import * as React from 'react';
*/
import IntegrationsModal from '../../src/blocks/contact-form/components/jetpack-integrations-modal';
import EmptyResponses from '../../src/dashboard/components/empty-responses';
import EmptySpamButton from '../../src/dashboard/components/empty-spam-button';
import EmptyTrashButton from '../../src/dashboard/components/empty-trash-button';
import Flag from '../../src/dashboard/components/flag';
import Gravatar from '../../src/dashboard/components/gravatar';
import Page from '../../src/dashboard/components/page';
import './style.scss';
import * as Tabs from '../../src/dashboard/components/tabs';
import useCreateForm from '../../src/dashboard/hooks/use-create-form';
import { getPath } from '../../src/dashboard/inbox/utils';
import WpRouteDashboardSearchParamsProvider from '../../src/dashboard/router/wp-route-dashboard-search-params-provider.tsx';
import { store as dashboardStore } from '../../src/dashboard/store';
import useConfigValue from '../../src/hooks/use-config-value';
import { INTEGRATIONS_STORE, IntegrationsSelectors } from '../../src/store/integrations';
Comment on lines 35 to 41
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

EmptyTrashButton / EmptySpamButton rely on useInboxData and the dashboard store’s currentQuery for invalidating core-data entity records after emptying trash/spam, but this route (Stage) never sets currentQuery in that store. In the wp-build context this means the buttons will invalidate only the store’s default query, so the DataViews list driven by this stage’s own useEntityRecords query may not be refreshed correctly after the empty actions; consider wiring this route’s active query into setCurrentQuery (similar to the React dashboard) or providing wp-build-specific invalidation so the list updates immediately after emptying.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -185,7 +188,7 @@ function Stage() {
[]
);
const { updateCountsOptimistically, invalidateCounts } = useDispatch(
dashboardStore
( dashboardStore as unknown as { name: string } ).name
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The store object should be passed directly to useDispatch, not its name property. This change from useDispatch(dashboardStore) to useDispatch(dashboardStore.name) breaks the standard WordPress data store pattern. The useDispatch function expects a store descriptor object, not a string identifier. This will cause runtime errors when trying to access store actions.

Suggested change
( dashboardStore as unknown as { name: string } ).name
dashboardStore

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

I was curious about this change as well. Can you elaborate, @dhasilva ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, that is a leftover from trying to fix types.

) as DispatchActions;
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

This type assertion suggests a mismatch between the expected store type and what useDispatch returns. Consider defining a proper type for the store or accessing the store name property in a type-safe way.

Suggested change
( dashboardStore as unknown as { name: string } ).name
) as DispatchActions;
dashboardStore
) as unknown as DispatchActions;

Copilot uses AI. Check for mistakes.
const filterOptions = useFilterOptions();
let status = 'publish';
Expand Down Expand Up @@ -1016,25 +1019,22 @@ function Stage() {
}

actionsArray.push(
<Button key="export" variant="primary" size="compact" icon={ download }>
<Button
key="export"
variant={ params.view === 'inbox' ? 'primary' : 'secondary' }
size="compact"
icon={ download }
>
{ __( 'Export', 'jetpack-forms' ) }
</Button>
);

if ( params.view === 'trash' ) {
actionsArray.push(
<Button key="empty-trash" variant="secondary" isDestructive size="compact">
{ __( 'Empty Trash', 'jetpack-forms' ) }
</Button>
);
actionsArray.push( <EmptyTrashButton key="empty-trash" /> );
}

if ( params.view === 'spam' ) {
actionsArray.push(
<Button key="empty-spam" variant="secondary" isDestructive size="compact">
{ __( 'Empty Spam', 'jetpack-forms' ) }
</Button>
);
actionsArray.push( <EmptySpamButton key="empty-spam" /> );
}

return actionsArray;
Expand All @@ -1050,75 +1050,80 @@ function Stage() {
const readStatusFilter = view.filters?.find( filter => filter.field === 'read_status' )?.value;

return (
<Page
showSidebarToggle={ false }
title={
<Stack align="center" gap="xs">
<JetpackLogo showText={ false } width={ 20 } />
{ __( 'Forms', 'jetpack-forms' ) }
</Stack>
}
subTitle={ __( 'View and manage all your form submissions in one place.', 'jetpack-forms' ) }
actions={ headerActions }
hasPadding={ false }
>
<DataViews
empty={
<EmptyResponses
status={ params.view }
isSearch={ !! view.search }
readStatusFilter={ readStatusFilter }
/>
<WpRouteDashboardSearchParamsProvider from="/responses/$view">
<Page
showSidebarToggle={ false }
title={
<Stack align="center" gap="xs">
<JetpackLogo showText={ false } width={ 20 } />
{ __( 'Forms', 'jetpack-forms' ) }
</Stack>
}
data={ records || EMPTY_ARRAY }
fields={ fields as Field< unknown >[] }
view={ view }
onChangeView={ onChangeView }
paginationInfo={ paginationInfo }
isLoading={ isResolving }
getItemId={ getItemId }
defaultLayouts={ defaultLayouts }
selection={ selection }
onChangeSelection={ onChangeSelection }
actions={ actions }
subTitle={ __(
'View and manage all your form submissions in one place.',
'jetpack-forms'
) }
actions={ headerActions }
hasPadding={ false }
>
<Stack
align="center"
className="jp-forms-dataviews__view-actions"
gap="sm"
justify="space-between"
<DataViews
empty={
<EmptyResponses
status={ params.view }
isSearch={ !! view.search }
readStatusFilter={ readStatusFilter }
/>
}
data={ records || EMPTY_ARRAY }
fields={ fields as Field< unknown >[] }
view={ view }
onChangeView={ onChangeView }
paginationInfo={ paginationInfo }
isLoading={ isResolving }
getItemId={ getItemId }
defaultLayouts={ defaultLayouts }
selection={ selection }
onChangeSelection={ onChangeSelection }
actions={ actions }
>
<Stack align="center" gap="sm">
<Tabs.Root value={ params.view || 'inbox' } onValueChange={ handleTabChange }>
<Tabs.List density="compact">
{ statusTabs.map( tab => (
<Tabs.Tab value={ tab.slug } key={ tab.slug }>
{ tab.label }
</Tabs.Tab>
) ) }
</Tabs.List>
</Tabs.Root>
</Stack>
<Stack align="center" gap="sm">
<DataViews.Search />
<DataViews.FiltersToggle />
<DataViews.ViewConfig />
<Stack
align="center"
className="jp-forms-dataviews__view-actions"
gap="sm"
justify="space-between"
>
<Stack align="center" gap="sm">
<Tabs.Root value={ params.view || 'inbox' } onValueChange={ handleTabChange }>
<Tabs.List density="compact">
{ statusTabs.map( tab => (
<Tabs.Tab value={ tab.slug } key={ tab.slug }>
{ tab.label }
</Tabs.Tab>
) ) }
</Tabs.List>
</Tabs.Root>
</Stack>
<Stack align="center" gap="sm">
<DataViews.Search />
<DataViews.FiltersToggle />
<DataViews.ViewConfig />
</Stack>
</Stack>
</Stack>
<DataViews.Filters className="dataviews-filters__container" />
<DataViews.Layout />
<DataViews.Footer />
</DataViews>
<IntegrationsModal
isOpen={ isIntegrationsModalOpen }
onClose={ closeIntegrationsModal }
attributes={ undefined }
setAttributes={ undefined }
integrationsData={ integrations }
refreshIntegrations={ refreshIntegrations }
context="dashboard"
/>
</Page>
<DataViews.Filters className="dataviews-filters__container" />
<DataViews.Layout />
<DataViews.Footer />
</DataViews>
<IntegrationsModal
isOpen={ isIntegrationsModalOpen }
onClose={ closeIntegrationsModal }
attributes={ undefined }
setAttributes={ undefined }
integrationsData={ integrations }
refreshIntegrations={ refreshIntegrations }
context="dashboard"
/>
</Page>
</WpRouteDashboardSearchParamsProvider>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { formatNumberCompact } from '@automattic/number-formatters';
import { Badge } from '@automattic/ui';
import { __, _x } from '@wordpress/i18n';
import { useCallback } from 'react';
import { useSearchParams } from 'react-router';
/**
* Internal dependencies
*/
import useInboxData from '../../hooks/use-inbox-data.ts';
import { useDashboardSearchParams } from '../../router/dashboard-search-params-context.tsx';
import * as Tabs from '../tabs/index.ts';

/**
Expand Down Expand Up @@ -41,7 +41,7 @@ type InboxStatusToggleProps = {
* @return {JSX.Element} The status toggle component.
*/
export default function InboxStatusToggle( { onChange }: InboxStatusToggleProps ): JSX.Element {
const [ searchParams, setSearchParams ] = useSearchParams();
const [ searchParams, setSearchParams ] = useDashboardSearchParams();
const status = searchParams.get( 'status' ) || 'inbox';
const [ isSm ] = useBreakpointMatch( 'sm' );

Expand Down
4 changes: 2 additions & 2 deletions projects/packages/forms/src/dashboard/forms/views.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from '@wordpress/element';
import { useSearchParams } from 'react-router';
import { useDashboardSearchParams } from '../router/dashboard-search-params-context.tsx';
import type { View } from '@wordpress/dataviews/wp';

const LAYOUT_TABLE = 'table';
Expand All @@ -23,7 +23,7 @@ export const defaultLayouts = {
* @return {[typeof defaultView, (newView: typeof defaultView) => void]} The current DataViews view and a setter that updates the URL.
*/
export function useView() {
const [ searchParams, setSearchParams ] = useSearchParams();
const [ searchParams, setSearchParams ] = useDashboardSearchParams();
const urlSearch = searchParams.get( 'search' );

const [ view, setView ] = useState( () => ( {
Expand Down
14 changes: 9 additions & 5 deletions projects/packages/forms/src/dashboard/hooks/use-inbox-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import { useEntityRecords, store as coreDataStore } from '@wordpress/core-data';
import { useDispatch, useSelect } from '@wordpress/data';
import { useMemo, useRef, useEffect, useState } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { isEmpty } from 'lodash';
import { useSearchParams } from 'react-router';
/**
* Internal dependencies
*/
import { useDashboardSearchParams } from '../router/dashboard-search-params-context.tsx';
import { store as dashboardStore } from '../store/index.js';
/**
* Types
Expand Down Expand Up @@ -38,10 +37,15 @@ const formatFieldName = fieldName => {
return fieldName;
};

// https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore?tab=readme-ov-file#_isempty
const isEmpty = obj =>
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The arrow function isEmpty should be defined as a named function declaration or include type annotations for its parameter and return value to maintain consistency with TypeScript best practices in this file.

Suggested change
const isEmpty = obj =>
const isEmpty = ( obj: unknown ): boolean =>

Copilot uses AI. Check for mistakes.
[ Object, Array ].includes( ( obj || {} ).constructor ) && ! Object.entries( obj || {} ).length;

const formatFieldValue = fieldValue => {
if ( isEmpty( fieldValue ) ) {
if ( ! fieldValue || isEmpty( fieldValue ) ) {
return '-';
} else if ( Array.isArray( fieldValue ) ) {
}
if ( Array.isArray( fieldValue ) ) {
Comment on lines 44 to +48
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

Switching from lodash/isEmpty to the custom isEmpty helper while also changing the condition to if ( ! fieldValue || isEmpty( fieldValue ) ) alters the previous behavior: values like 0 or false that were previously rendered as "0" / "false" will now be treated as empty and displayed as '-'. If preserving the original semantics is desired, this should rely solely on the structural emptiness check (and/or explicitly handle only the cases that should map to '-'), so that falsy but meaningful values are not hidden from the UI.

Copilot uses AI. Check for mistakes.
return fieldValue.join( ', ' );
}
return fieldValue;
Expand Down Expand Up @@ -73,7 +77,7 @@ interface UseInboxDataReturn {
* @return {UseInboxDataReturn} The inbox related data.
*/
export default function useInboxData(): UseInboxDataReturn {
const [ searchParams ] = useSearchParams();
const [ searchParams ] = useDashboardSearchParams();
const { setCurrentQuery, setSelectedResponses } = useDispatch( dashboardStore );
const urlStatus = searchParams.get( 'status' );
const statusFilter = getStatusFilter( urlStatus );
Expand Down
4 changes: 2 additions & 2 deletions projects/packages/forms/src/dashboard/inbox/stage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { __ } from '@wordpress/i18n';
import { Icon, globe } from '@wordpress/icons';
import clsx from 'clsx';
import { useEffect } from 'react';
import { useSearchParams } from 'react-router';
/**
* Internal dependencies
*/
Expand All @@ -35,6 +34,7 @@ import { ResponseMobileView, SingleResponseView } from '../../components/inspect
import IntegrationsButton from '../../components/integrations-button/index.tsx';
import Page from '../../components/page/index.tsx';
import useInboxData from '../../hooks/use-inbox-data.ts';
import { useDashboardSearchParams } from '../../router/dashboard-search-params-context.tsx';
import { getPath, getItemId } from '../utils.js';
import {
viewAction,
Expand Down Expand Up @@ -87,7 +87,7 @@ const setupSidebarWidthObserver = () => {
*/
export default function InboxView() {
const [ view, setView ] = useView();
const [ searchParams, setSearchParams ] = useSearchParams();
const [ searchParams, setSearchParams ] = useDashboardSearchParams();
const [ containerWidth, setContainerWidth ] = useState( 0 );

const dateSettings = getDateSettings();
Expand Down
4 changes: 2 additions & 2 deletions projects/packages/forms/src/dashboard/inbox/stage/views.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import { useEvent } from '@wordpress/compose';
import { useEffect, useState } from '@wordpress/element';
import { useSearchParams } from 'react-router';
import { useDashboardSearchParams } from '../../router/dashboard-search-params-context.tsx';

const LAYOUT_TABLE = 'table';

Expand All @@ -30,7 +30,7 @@ export const defaultLayouts = {
* @return {Array} The [ state, setState ] tuple.
*/
export function useView() {
const [ searchParams, setSearchParams ] = useSearchParams();
const [ searchParams, setSearchParams ] = useDashboardSearchParams();
// Normalize missing query param to empty string so we don't treat
// `null` (missing) and `''` (empty) as different values.
const urlSearch = searchParams.get( 'search' ) ?? '';
Expand Down
9 changes: 8 additions & 1 deletion projects/packages/forms/src/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Layout from './components/layout/index.tsx';
import FormsDashboardForms from './forms/index.tsx';
import Inbox from './inbox/index.js';
import DashboardNotices from './notices-list.tsx';
import ReactRouterDashboardSearchParamsProvider from './router/react-router-dashboard-search-params-provider.tsx';
import './style.scss';

declare global {
Expand Down Expand Up @@ -48,10 +49,16 @@ function initFormsDashboard() {
return null;
};

const DashboardRoot = () => (
<ReactRouterDashboardSearchParamsProvider>
<Layout />
</ReactRouterDashboardSearchParamsProvider>
);

const router = createHashRouter( [
{
path: '/',
element: <Layout />,
element: <DashboardRoot />,
children: [
{
index: true,
Expand Down
Loading
Loading