diff --git a/projects/packages/forms/changelog/update-forms-dashboard-dataview-actions b/projects/packages/forms/changelog/update-forms-dashboard-dataview-actions new file mode 100644 index 0000000000000..8d4b839cec0f8 --- /dev/null +++ b/projects/packages/forms/changelog/update-forms-dashboard-dataview-actions @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Forms: Update dataview actions diff --git a/projects/packages/forms/src/dashboard/components/response-actions/index.tsx b/projects/packages/forms/src/dashboard/components/response-actions/index.tsx index 8493f56a5c355..be0a1758f3073 100644 --- a/projects/packages/forms/src/dashboard/components/response-actions/index.tsx +++ b/projects/packages/forms/src/dashboard/components/response-actions/index.tsx @@ -20,9 +20,10 @@ import { * Types */ import type { FormResponse } from '../../../types'; +import type { Registry } from '../../inbox/dataviews/types'; type ResponseNavigationProps = { - onActionComplete?: ( FormResponse ) => void; + onActionComplete?: ( response: FormResponse ) => void; response: FormResponse; }; @@ -37,7 +38,7 @@ const ResponseActions = ( { const [ isDeleting, setIsDeleting ] = useState( false ); const [ isTogglingReadStatus, setIsTogglingReadStatus ] = useState( false ); - const registry = useRegistry(); + const registry = useRegistry() as unknown as Registry; const handleMarkAsSpam = useCallback( async () => { onActionComplete?.( response ); diff --git a/projects/packages/forms/src/dashboard/inbox/dataviews/actions.js b/projects/packages/forms/src/dashboard/inbox/dataviews/actions.tsx similarity index 93% rename from projects/packages/forms/src/dashboard/inbox/dataviews/actions.js rename to projects/packages/forms/src/dashboard/inbox/dataviews/actions.tsx index ad7ab4268346c..dc299925d6327 100644 --- a/projects/packages/forms/src/dashboard/inbox/dataviews/actions.js +++ b/projects/packages/forms/src/dashboard/inbox/dataviews/actions.tsx @@ -1,3 +1,6 @@ +/** + * External dependencies + */ import jetpackAnalytics from '@automattic/jetpack-analytics'; import apiFetch from '@wordpress/api-fetch'; import { Icon } from '@wordpress/components'; @@ -5,10 +8,17 @@ import { store as coreStore } from '@wordpress/core-data'; import { __, _n, sprintf } from '@wordpress/i18n'; import { seen, unseen, trash, backup, commentContent } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; +/** + * Internal dependencies + */ import { notSpam, spam } from '../../icons'; import { store as dashboardStore } from '../../store'; import { updateMenuCounter, updateMenuCounterOptimistically } from '../utils'; import { defaultView } from './views'; +/** + * Types + */ +import type { Action, QueryParams, Registry } from './types'; /** * Helper function to extract count-relevant query params from the current query. @@ -16,8 +26,9 @@ import { defaultView } from './views'; * @param {object} currentQuery - The current query from the store. * @return {object} Query params relevant for count caching. */ -const getCountQueryParams = currentQuery => { - const queryParams = {}; +const getCountQueryParams = ( currentQuery: QueryParams ): QueryParams => { + const queryParams: QueryParams = {}; + if ( currentQuery?.search ) { queryParams.search = currentQuery.search; } @@ -33,6 +44,7 @@ const getCountQueryParams = currentQuery => { if ( currentQuery?.is_unread !== undefined ) { queryParams.is_unread = currentQuery.is_unread; } + return queryParams; }; @@ -45,11 +57,11 @@ const getCountQueryParams = currentQuery => { * @param {string} statusBeingRemovedFrom - The status items are being removed from ('trash', 'spam', or 'inbox'). */ const invalidateCacheAndNavigate = ( - registry, - currentQuery, - queryParams, - statusBeingRemovedFrom -) => { + registry: Registry, + currentQuery: QueryParams, + queryParams: QueryParams, + statusBeingRemovedFrom: string +): void => { // Invalidate counts to ensure accurate totals registry.dispatch( dashboardStore ).invalidateCounts(); @@ -78,23 +90,40 @@ const invalidateCacheAndNavigate = ( } }; +// TODO: We should probably have better error messages in case of failure. +const getGenericErrorMessage = ( numberOfErrors: number ): string => { + return numberOfErrors === 1 + ? __( 'An error occurred.', 'jetpack-forms' ) + : sprintf( + /* translators: %d: the number of responses. */ + _n( + 'An error occurred for %d response.', + 'An error occurred for %d responses.', + numberOfErrors, + 'jetpack-forms' + ), + numberOfErrors + ); +}; + export const BULK_ACTIONS = { markAsSpam: 'mark_as_spam', markAsNotSpam: 'mark_as_not_spam', }; -export const viewAction = { +export const viewAction: Action = { id: 'view-response', - icon: , isPrimary: true, - label: __( 'View response', 'jetpack-forms' ), + icon: , + label: __( 'View', 'jetpack-forms' ), modalHeader: __( 'Response', 'jetpack-forms' ), }; -export const editFormAction = { +export const editFormAction: Action = { id: 'edit-form', - label: __( 'Edit form', 'jetpack-forms' ), + isPrimary: false, icon: , + label: __( 'Edit form', 'jetpack-forms' ), isEligible: item => !! item?.edit_form_url, supportsBulk: false, async callback( items ) { @@ -102,7 +131,9 @@ export const editFormAction = { action: 'edit-form', multiple: false, } ); + const [ item ] = items; + if ( item?.edit_form_url ) { const url = new URL( item.edit_form_url, window.location.origin ); // redirect to the form edit page @@ -111,33 +142,19 @@ export const editFormAction = { }, }; -// TODO: We should probably have better error messages in case of failure. -const getGenericErrorMessage = numberOfErrors => { - return numberOfErrors.length === 1 - ? __( 'An error occurred.', 'jetpack-forms' ) - : sprintf( - /* translators: %d: the number of responses. */ - _n( - 'An error occurred for %d response.', - 'An error occurred for %d responses.', - numberOfErrors, - 'jetpack-forms' - ), - numberOfErrors - ); -}; - -export const markAsSpamAction = { +export const markAsSpamAction: Action = { id: 'mark-as-spam', - label: __( 'Mark as spam', 'jetpack-forms' ), + isPrimary: true, + icon: , + label: __( 'Spam', 'jetpack-forms' ), isEligible: item => item.status !== 'spam', supportsBulk: true, - icon: , async callback( items, { registry } ) { jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { action: 'mark-as-spam', multiple: items.length > 1, } ); + const { createSuccessNotice, createErrorNotice } = registry.dispatch( noticesStore ); const { saveEntityRecord } = registry.dispatch( coreStore ); const { updateCountsOptimistically } = registry.dispatch( dashboardStore ); @@ -149,10 +166,13 @@ export const markAsSpamAction = { updateCountsOptimistically( item.status, 'spam', 1, queryParams ); } ); - const promises = await Promise.allSettled( + const promises = ( await Promise.allSettled( items.map( ( { id } ) => saveEntityRecord( 'postType', 'feedback', { id, status: 'spam' } ) ) - ); - const itemsUpdated = promises.filter( ( { status } ) => status === 'fulfilled' ); + ) ) as PromiseSettledResult< { id: string } >[]; + + const itemsUpdated = promises.filter( + ( { status } ) => status === 'fulfilled' + ) as PromiseFulfilledResult< { id: string } >[]; // If there is at least one successful update, invalidate the cache and navigate if needed if ( itemsUpdated.length ) { @@ -178,6 +198,7 @@ export const markAsSpamAction = { ), items.length ); + createSuccessNotice( successMessage, { type: 'snackbar', id: 'mark-as-spam-action', @@ -194,8 +215,10 @@ export const markAsSpamAction = { // There is at least one failure. const numberOfErrors = promises.filter( ( { status } ) => status === 'rejected' ).length; const errorMessage = getGenericErrorMessage( numberOfErrors ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); } + // Make the REST request which performs the `contact_form_akismet` `ham` action. if ( itemsUpdated.length ) { registry.dispatch( dashboardStore ).doBulkAction( @@ -206,17 +229,19 @@ export const markAsSpamAction = { }, }; -export const markAsNotSpamAction = { +export const markAsNotSpamAction: Action = { id: 'mark-as-not-spam', + isPrimary: true, + icon: , label: __( 'Not spam', 'jetpack-forms' ), isEligible: item => item.status === 'spam', supportsBulk: true, - icon: , async callback( items, { registry } ) { jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { action: 'mark-as-not-spam', multiple: items.length > 1, } ); + const { createSuccessNotice, createErrorNotice } = registry.dispatch( noticesStore ); const { saveEntityRecord } = registry.dispatch( coreStore ); const { updateCountsOptimistically } = registry.dispatch( dashboardStore ); @@ -228,12 +253,15 @@ export const markAsNotSpamAction = { updateCountsOptimistically( 'spam', 'publish', 1, queryParams ); } ); - const promises = await Promise.allSettled( + const promises = ( await Promise.allSettled( items.map( ( { id } ) => saveEntityRecord( 'postType', 'feedback', { id, status: 'publish' } ) ) - ); - const itemsUpdated = promises.filter( ( { status } ) => status === 'fulfilled' ); + ) ) as PromiseSettledResult< { id: string } >[]; + + const itemsUpdated = promises.filter( + ( { status } ) => status === 'fulfilled' + ) as PromiseFulfilledResult< { id: string } >[]; // If there is at least one successful update, invalidate the cache and navigate if needed if ( itemsUpdated.length ) { @@ -255,6 +283,7 @@ export const markAsNotSpamAction = { ), items.length ); + createSuccessNotice( successMessage, { type: 'snackbar', id: 'mark-as-not-spam-action', @@ -271,6 +300,7 @@ export const markAsNotSpamAction = { // There is at least one failure. const numberOfErrors = promises.filter( ( { status } ) => status === 'rejected' ).length; const errorMessage = getGenericErrorMessage( numberOfErrors ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); } // Make the REST request which performs the `contact_form_akismet` `ham` action. @@ -283,17 +313,19 @@ export const markAsNotSpamAction = { }, }; -export const restoreAction = { +export const restoreAction: Action = { id: 'restore', + isPrimary: true, + icon: , label: __( 'Restore', 'jetpack-forms' ), isEligible: item => item.status === 'trash', supportsBulk: true, - icon: , async callback( items, { registry } ) { jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { action: 'restore', multiple: items.length > 1, } ); + const { saveEntityRecord } = registry.dispatch( coreStore ); const { createSuccessNotice, createErrorNotice } = registry.dispatch( noticesStore ); const { updateCountsOptimistically } = registry.dispatch( dashboardStore ); @@ -310,6 +342,7 @@ export const restoreAction = { saveEntityRecord( 'postType', 'feedback', { id, status: 'publish' } ) ) ); + const itemsUpdated = promises.filter( ( { status } ) => status === 'fulfilled' ); // If there is at least one successful update, invalidate the cache and navigate if needed @@ -331,6 +364,7 @@ export const restoreAction = { ), items.length ); + createSuccessNotice( successMessage, { type: 'snackbar', id: 'restore-action', @@ -343,27 +377,31 @@ export const restoreAction = { }, ], } ); + return; } + // There is at least one failure. const numberOfErrors = promises.filter( ( { status } ) => status === 'rejected' ).length; const errorMessage = getGenericErrorMessage( numberOfErrors ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); }, }; -export const moveToTrashAction = { +export const moveToTrashAction: Action = { id: 'move-to-trash', - label: __( 'Move to trash', 'jetpack-forms' ), - isEligible: item => item.status !== 'trash', isPrimary: true, - supportsBulk: true, icon: , + label: __( 'Trash', 'jetpack-forms' ), + isEligible: item => item.status !== 'trash', + supportsBulk: true, async callback( items, { registry } ) { jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { action: 'move-to-trash', multiple: items.length > 1, } ); + const { deleteEntityRecord } = registry.dispatch( coreStore ); const { createSuccessNotice, createErrorNotice } = registry.dispatch( noticesStore ); const { updateCountsOptimistically } = registry.dispatch( dashboardStore ); @@ -406,6 +444,7 @@ export const moveToTrashAction = { ), items.length ); + createSuccessNotice( successMessage, { type: 'snackbar', id: 'move-to-trash-action', @@ -418,26 +457,31 @@ export const moveToTrashAction = { }, ], } ); + return; } + // There is at least one failure. const numberOfErrors = promises.filter( ( { status } ) => status === 'rejected' ).length; const errorMessage = getGenericErrorMessage( numberOfErrors ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); }, }; -export const deleteAction = { +export const deleteAction: Action = { id: 'delete', - label: __( 'Delete permanently', 'jetpack-forms' ), + isPrimary: true, + icon: , + label: __( 'Delete', 'jetpack-forms' ), isEligible: item => item.status === 'trash', supportsBulk: true, - icon: , async callback( items, { registry } ) { jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { action: 'delete', multiple: items.length > 1, } ); + const { deleteEntityRecord } = registry.dispatch( coreStore ); const { invalidateFilters, updateCountsOptimistically } = registry.dispatch( dashboardStore ); const { getCurrentQuery } = registry.select( dashboardStore ); @@ -454,12 +498,15 @@ export const deleteAction = { deleteEntityRecord( 'postType', 'feedback', id, { force: true }, { throwOnError: true } ) ) ); + const itemsUpdated = promises.filter( ( { status } ) => status === 'fulfilled' ); + // If there is at least one successful update, invalidate the cache for filters. if ( itemsUpdated.length ) { invalidateFilters(); invalidateCacheAndNavigate( registry, getCurrentQuery(), queryParams, 'trash' ); } + if ( itemsUpdated.length === items.length ) { // Every request was successful. const successMessage = @@ -475,6 +522,7 @@ export const deleteAction = { ), items.length ); + createSuccessNotice( successMessage, { type: 'snackbar', id: 'move-to-trash-action' } ); // Update the URL to remove references to deleted items. @@ -497,27 +545,30 @@ export const deleteAction = { const hashString = hashParams.toString(); window.location.hash = hashString ? `${ hashBase }?${ hashString }` : hashBase; + return; } // There is at least one failure. const numberOfErrors = promises.filter( ( { status } ) => status === 'rejected' ).length; const errorMessage = getGenericErrorMessage( numberOfErrors ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); }, }; -export const markAsReadAction = { +export const markAsReadAction: Action = { id: 'mark-as-read', + isPrimary: false, + icon: , label: __( 'Mark as read', 'jetpack-forms' ), isEligible: item => item.is_unread, supportsBulk: true, - icon: , async callback( items, { registry } ) { jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { action: 'mark-as-read', multiple: items.length > 1, } ); - // const { receiveEntityRecords, editEntityRecord } = registry.dispatch( coreStore ); + const { editEntityRecord } = registry.dispatch( coreStore ); const { getEntityRecord } = registry.select( coreStore ); const { createSuccessNotice, createErrorNotice } = registry.dispatch( noticesStore ); @@ -562,6 +613,7 @@ export const markAsReadAction = { updateMenuCounterOptimistically( 1 ); } } + throw new Error( 'Failed to mark as read' ); } ); } ) @@ -571,9 +623,11 @@ export const markAsReadAction = { if ( promises.some( ( { status } ) => status === 'fulfilled' ) ) { invalidateCounts(); // Mark successfully updated records as invalid instead of removing from view + const updatedIds = items .filter( ( _, index ) => promises[ index ]?.status === 'fulfilled' ) .map( item => item.id ); + markRecordsAsInvalid( updatedIds ); } @@ -592,6 +646,7 @@ export const markAsReadAction = { ), items.length ); + createSuccessNotice( successMessage, { type: 'snackbar', id: 'mark-as-read-action', @@ -604,26 +659,31 @@ export const markAsReadAction = { }, ], } ); + return; } + // There is at least one failure. const numberOfErrors = promises.filter( ( { status } ) => status === 'rejected' ).length; const errorMessage = getGenericErrorMessage( numberOfErrors ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); }, }; -export const markAsUnreadAction = { +export const markAsUnreadAction: Action = { id: 'mark-as-unread', + isPrimary: false, + icon: , label: __( 'Mark as unread', 'jetpack-forms' ), isEligible: item => ! item.is_unread, supportsBulk: true, - icon: , async callback( items, { registry } ) { jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { action: 'mark-as-unread', multiple: items.length > 1, } ); + const { editEntityRecord } = registry.dispatch( coreStore ); const { getEntityRecord } = registry.select( coreStore ); const { createSuccessNotice, createErrorNotice } = registry.dispatch( noticesStore ); @@ -668,10 +728,12 @@ export const markAsUnreadAction = { updateMenuCounterOptimistically( -1 ); } } + throw new Error( 'Failed to mark as unread' ); } ); } ) ); + if ( promises.every( ( { status } ) => status === 'fulfilled' ) ) { // Invalidate counts cache to ensure counts are refetched and stay accurate invalidateCounts(); @@ -692,6 +754,7 @@ export const markAsUnreadAction = { ), items.length ); + createSuccessNotice( successMessage, { type: 'snackbar', id: 'mark-as-unread-action', @@ -704,11 +767,14 @@ export const markAsUnreadAction = { }, ], } ); + return; } + // There is at least one failure. const numberOfErrors = promises.filter( ( { status } ) => status === 'rejected' ).length; const errorMessage = getGenericErrorMessage( numberOfErrors ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); }, }; diff --git a/projects/packages/forms/src/dashboard/inbox/dataviews/index.js b/projects/packages/forms/src/dashboard/inbox/dataviews/index.js index 99f55321c7bcf..e7b7fd638a834 100644 --- a/projects/packages/forms/src/dashboard/inbox/dataviews/index.js +++ b/projects/packages/forms/src/dashboard/inbox/dataviews/index.js @@ -6,14 +6,16 @@ import { ExternalLink, // eslint-disable-next-line @wordpress/no-unsafe-wp-apis __experimentalHStack as HStack, + Modal, } from '@wordpress/components'; -import { useResizeObserver } from '@wordpress/compose'; +import { useResizeObserver, useViewportMatch } from '@wordpress/compose'; import { DataViews } from '@wordpress/dataviews/wp'; import { dateI18n, getSettings as getDateSettings } from '@wordpress/date'; import { useCallback, useMemo, useState } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; import { __ } from '@wordpress/i18n'; import { Icon, globe } from '@wordpress/icons'; +import clsx from 'clsx'; import { useEffect } from 'react'; import { useSearchParams } from 'react-router'; /** @@ -89,6 +91,30 @@ export default function InboxView() { ); const isMobile = containerWidth <= MOBILE_BREAKPOINT; const selectedResponses = searchParams.get( 'r' ); + const isMobileViewport = useViewportMatch( 'medium', '<' ); + const [ isResponseModalOpen, setIsResponseModalOpen ] = useState( false ); + const [ responseModal, setResponseModal ] = useState( null ); + + const closeResponseModal = useCallback( () => { + setIsResponseModalOpen( false ); + setResponseModal( null ); + }, [ setIsResponseModalOpen, setResponseModal ] ); + + const openResponseModal = useCallback( + item => { + const content = ; + + setResponseModal( content ); + setIsResponseModalOpen( true ); + }, + [ setIsResponseModalOpen, closeResponseModal, setResponseModal ] + ); + + useEffect( () => { + if ( ! isMobileViewport ) { + closeResponseModal(); + } + }, [ isMobileViewport, closeResponseModal ] ); useEffect( () => { return setupSidebarWidthObserver(); @@ -228,8 +254,22 @@ export default function InboxView() { item.author_name || item.author_email || item.author_url || item.ip ); const defaultImage = item.author_name || item.author_email ? 'initials' : 'mp'; + + const handleClick = isMobileViewport ? () => openResponseModal( item ) : undefined; + return ( -
+
{}, + role: 'button', + tabIndex: 0, + } ) } + > { item.is_unread && ( { - const _actions = [ - markAsReadAction, - markAsUnreadAction, - markAsSpamAction, - markAsNotSpamAction, - moveToTrashAction, - editFormAction, - restoreAction, - deleteAction, - ]; - if ( isMobile ) { - _actions.unshift( { - ...viewAction, - RenderModal: ( { items, closeModal } ) => { - jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { - action: 'view-response', - multiple: items.length > 1, - } ); - const [ item ] = items; - return ; - }, - hideModalHeader: true, - } ); - } else { - _actions.unshift( { - ...viewAction, - callback( items ) { - jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { - action: 'view-response', - multiple: items.length > 1, - } ); - const [ item ] = items; - const selectedId = item.id.toString(); - const selectionWithoutSelectedId = selection.filter( id => id !== selectedId ); - onChangeSelection( [ ...selectionWithoutSelectedId, selectedId ] ); - }, - } ); + const mobileViewAction = { + ...viewAction, + RenderModal: ( { items, closeModal } ) => { + jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { + action: 'view-response', + multiple: items.length > 1, + } ); + + const [ item ] = items; + + return ; + }, + hideModalHeader: true, + }; + + const desktopViewAction = { + ...viewAction, + callback( items ) { + jetpackAnalytics.tracks.recordEvent( 'jetpack_forms_inbox_action_click', { + action: 'view-response', + multiple: items.length > 1, + } ); + + const [ item ] = items; + const selectedId = item.id.toString(); + const selectionWithoutSelectedId = selection.filter( id => id !== selectedId ); + + onChangeSelection( [ ...selectionWithoutSelectedId, selectedId ] ); + }, + }; + + const viewResponseAction = isMobile ? mobileViewAction : desktopViewAction; + + const primaryActions = [ viewResponseAction ]; + const secondaryActions = [ markAsUnreadAction, editFormAction ]; + + switch ( statusFilter ) { + case 'trash': + return [ ...primaryActions, restoreAction, deleteAction, ...secondaryActions ]; + case 'spam': + return [ ...primaryActions, markAsNotSpamAction, moveToTrashAction, ...secondaryActions ]; + default: + return [ + ...primaryActions, + markAsReadAction, + markAsSpamAction, + moveToTrashAction, + ...secondaryActions, + ]; } - return _actions; - }, [ isMobile, onChangeSelection, selection ] ); + }, [ isMobile, onChangeSelection, selection, statusFilter ] ); const resetPage = useCallback( () => { view.page = 1; @@ -410,6 +467,15 @@ export default function InboxView() { /> } /> + { isResponseModalOpen && ( + + { responseModal } + + ) }
{ + // Notices store actions + createSuccessNotice: ( + message: string, + options: { type?: string; id?: string; actions?: { label: string; onClick: () => void }[] } + ) => void; + createErrorNotice: ( + message: string, + options: { type?: string; id?: string; actions?: { label: string; onClick: () => void }[] } + ) => void; + + // Core store actions + saveEntityRecord: ( + kind: string, + name: string, + record: Record< string, unknown > + ) => Promise< void >; + deleteEntityRecord: ( + kind: string, + name: string, + recordId: number, + query: Record< string, unknown >, + options?: { throwOnError?: boolean } + ) => Promise< void >; + editEntityRecord: ( + kind: string, + name: string, + recordId: number, + edits: Record< string, unknown > + ) => Promise< void >; + + // Dashboard store actions + updateCountsOptimistically: ( + status: string, + newStatus: string, + count: number, + queryParams: QueryParams + ) => void; + doBulkAction: ( ids: string[], action: string ) => void; + invalidateFilters: () => void; + invalidateCounts: () => void; + markRecordsAsInvalid: ( ids: number[] ) => void; + setCurrentQuery: ( queryParams: QueryParams ) => void; + }; + select: ( store: StoreDescriptor ) => { + // Dashboard store select actions + getCurrentQuery: () => QueryParams; + getTrashCount: ( queryParams: QueryParams ) => number; + getSpamCount: ( queryParams: QueryParams ) => number; + getInboxCount: ( queryParams: QueryParams ) => number; + + // Core store select actions + getEntityRecord: ( + kind: string, + name: string, + recordId: number + ) => Record< string, unknown > | undefined; + }; +}; + +export type Action = { + id: string; + isPrimary: boolean; + icon: React.ReactNode; + label: string; + modalHeader?: string; + isEligible?: ( item: FormResponse ) => boolean; + supportsBulk?: boolean; + callback?: ( items: FormResponse[], { registry }: { registry: Registry } ) => Promise< void >; +}; diff --git a/projects/packages/forms/src/dashboard/inbox/style.scss b/projects/packages/forms/src/dashboard/inbox/style.scss index 90142c4ec0fb5..154536f0e2acf 100644 --- a/projects/packages/forms/src/dashboard/inbox/style.scss +++ b/projects/packages/forms/src/dashboard/inbox/style.scss @@ -350,6 +350,10 @@ display: flex; align-items: center; gap: 12px; + + &--mobile { + text-decoration: underline; + } } .jp-forms__inbox__unread-indicator { diff --git a/projects/packages/forms/src/types/index.ts b/projects/packages/forms/src/types/index.ts index 4438476a52421..83b019fb6484d 100644 --- a/projects/packages/forms/src/types/index.ts +++ b/projects/packages/forms/src/types/index.ts @@ -114,6 +114,8 @@ export interface FormResponse { is_unread: boolean; /** The fields of the response. */ fields: Record< string, unknown >; + /** The URL to edit the form that the response was submitted to. */ + edit_form_url: string; } /**