Skip to content

Commit 4e27ad2

Browse files
simisonCGastrell
andauthored
Forms: move componentized view actions to modal header on mobile (#45410)
* Componentise view actions and use them in header on mobile * changelog * Rebase unread/read action * Conditionally call function * add toggle for read/unread unconditionally, add loading state * switch to RenderModal on view action, compose with new component to wrap InboxResponse with actions and navigation * remove unused code * remove onMarkAsRead handler, let onActionHandler to deal with both status swaps and read toggles properly * trigger onActionComplete optimistically on response actions * wrap responses and navigation, use hstack for spacing on mobile view * fix some weird typecheck issue: declaration was duped --------- Co-authored-by: Christian Gastrell <[email protected]>
1 parent 01f4127 commit 4e27ad2

File tree

7 files changed

+490
-261
lines changed

7 files changed

+490
-261
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: changed
3+
4+
Forms: move view actions to modal header on mobile
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { Button } from '@wordpress/components';
5+
import { useRegistry } from '@wordpress/data';
6+
import { useCallback, useState } from '@wordpress/element';
7+
/**
8+
* Internal dependencies
9+
*/
10+
import {
11+
markAsSpamAction,
12+
markAsNotSpamAction,
13+
moveToTrashAction,
14+
restoreAction,
15+
deleteAction,
16+
markAsReadAction,
17+
markAsUnreadAction,
18+
} from '../../inbox/dataviews/actions';
19+
/**
20+
* Types
21+
*/
22+
import type { FormResponse } from '../../../types';
23+
24+
type ResponseNavigationProps = {
25+
onActionComplete?: ( FormResponse ) => void;
26+
response: FormResponse;
27+
};
28+
29+
const ResponseActions = ( {
30+
onActionComplete,
31+
response,
32+
}: ResponseNavigationProps ): JSX.Element => {
33+
const [ isMarkingAsSpam, setIsMarkingAsSpam ] = useState( false );
34+
const [ isMarkingAsNotSpam, setIsMarkingAsNotSpam ] = useState( false );
35+
const [ isMovingToTrash, setIsMovingToTrash ] = useState( false );
36+
const [ isRestoring, setIsRestoring ] = useState( false );
37+
const [ isDeleting, setIsDeleting ] = useState( false );
38+
const [ isTogglingReadStatus, setIsTogglingReadStatus ] = useState( false );
39+
40+
const registry = useRegistry();
41+
42+
const handleMarkAsSpam = useCallback( async () => {
43+
onActionComplete?.( response );
44+
setIsMarkingAsSpam( true );
45+
await markAsSpamAction.callback( [ response ], { registry } );
46+
setIsMarkingAsSpam( false );
47+
}, [ response, registry, onActionComplete ] );
48+
49+
const handleMarkAsNotSpam = useCallback( async () => {
50+
onActionComplete?.( response );
51+
setIsMarkingAsNotSpam( true );
52+
await markAsNotSpamAction.callback( [ response ], { registry } );
53+
setIsMarkingAsNotSpam( false );
54+
}, [ response, registry, onActionComplete ] );
55+
56+
const handleMoveToTrash = useCallback( async () => {
57+
onActionComplete?.( response );
58+
setIsMovingToTrash( true );
59+
await moveToTrashAction.callback( [ response ], { registry } );
60+
setIsMovingToTrash( false );
61+
}, [ response, registry, onActionComplete ] );
62+
63+
const handleRestore = useCallback( async () => {
64+
onActionComplete?.( response );
65+
setIsRestoring( true );
66+
await restoreAction.callback( [ response ], { registry } );
67+
setIsRestoring( false );
68+
}, [ response, registry, onActionComplete ] );
69+
70+
const handleDelete = useCallback( async () => {
71+
onActionComplete?.( response );
72+
setIsDeleting( true );
73+
await deleteAction.callback( [ response ], { registry } );
74+
setIsDeleting( false );
75+
}, [ response, registry, onActionComplete ] );
76+
77+
const handleMarkAsRead = useCallback( async () => {
78+
setIsTogglingReadStatus( true );
79+
await markAsReadAction.callback( [ response ], { registry } );
80+
setIsTogglingReadStatus( false );
81+
onActionComplete?.( { ...response, is_unread: false } );
82+
}, [ response, registry, onActionComplete ] );
83+
84+
const handleMarkAsUnread = useCallback( async () => {
85+
setIsTogglingReadStatus( true );
86+
await markAsUnreadAction.callback( [ response ], { registry } );
87+
setIsTogglingReadStatus( false );
88+
onActionComplete?.( { ...response, is_unread: true } );
89+
}, [ response, registry, onActionComplete ] );
90+
91+
const readUnreadButtons = (
92+
<>
93+
{ response.is_unread && (
94+
<Button
95+
variant="tertiary"
96+
onClick={ handleMarkAsRead }
97+
isBusy={ isTogglingReadStatus }
98+
showTooltip={ true }
99+
label={ markAsReadAction.label }
100+
iconSize={ 24 }
101+
icon={ markAsReadAction.icon }
102+
size="compact"
103+
></Button>
104+
) }
105+
{ ! response.is_unread && (
106+
<Button
107+
variant="tertiary"
108+
onClick={ handleMarkAsUnread }
109+
isBusy={ isTogglingReadStatus }
110+
showTooltip={ true }
111+
label={ markAsUnreadAction.label }
112+
iconSize={ 24 }
113+
icon={ markAsUnreadAction.icon }
114+
size="compact"
115+
></Button>
116+
) }
117+
</>
118+
);
119+
120+
switch ( response.status ) {
121+
case 'spam':
122+
return (
123+
<div>
124+
{ readUnreadButtons }
125+
<Button
126+
variant="tertiary"
127+
onClick={ handleMarkAsNotSpam }
128+
isBusy={ isMarkingAsNotSpam }
129+
showTooltip={ true }
130+
label={ markAsNotSpamAction.label }
131+
iconSize={ 24 }
132+
icon={ markAsNotSpamAction.icon }
133+
size="compact"
134+
></Button>
135+
<Button
136+
variant="tertiary"
137+
onClick={ handleMoveToTrash }
138+
isBusy={ isMovingToTrash }
139+
showTooltip={ true }
140+
label={ moveToTrashAction.label }
141+
iconSize={ 24 }
142+
icon={ moveToTrashAction.icon }
143+
size="compact"
144+
></Button>
145+
</div>
146+
);
147+
148+
case 'trash':
149+
return (
150+
<div>
151+
{ readUnreadButtons }
152+
<Button
153+
variant="tertiary"
154+
onClick={ handleRestore }
155+
isBusy={ isRestoring }
156+
showTooltip={ true }
157+
label={ restoreAction.label }
158+
iconSize={ 24 }
159+
icon={ restoreAction.icon }
160+
size="compact"
161+
></Button>
162+
<Button
163+
variant="tertiary"
164+
onClick={ handleDelete }
165+
showTooltip={ true }
166+
isBusy={ isDeleting }
167+
label={ deleteAction.label }
168+
iconSize={ 24 }
169+
icon={ deleteAction.icon }
170+
size="compact"
171+
></Button>
172+
</div>
173+
);
174+
175+
default: // 'publish' (inbox) or any other status
176+
return (
177+
<div>
178+
{ readUnreadButtons }
179+
<Button
180+
variant="tertiary"
181+
onClick={ handleMarkAsSpam }
182+
isBusy={ isMarkingAsSpam }
183+
showTooltip={ true }
184+
label={ markAsSpamAction.label }
185+
iconSize={ 24 }
186+
icon={ markAsSpamAction.icon }
187+
size="compact"
188+
></Button>
189+
<Button
190+
variant="tertiary"
191+
onClick={ handleMoveToTrash }
192+
isBusy={ isMovingToTrash }
193+
showTooltip={ true }
194+
label={ moveToTrashAction.label }
195+
iconSize={ 24 }
196+
icon={ moveToTrashAction.icon }
197+
size="compact"
198+
></Button>
199+
</div>
200+
);
201+
}
202+
};
203+
204+
export default ResponseActions;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { Button } from '@wordpress/components';
5+
import { __ } from '@wordpress/i18n';
6+
import { close, chevronLeft, chevronRight } from '@wordpress/icons';
7+
8+
type ResponseNavigationProps = {
9+
hasNext: boolean;
10+
hasPrevious: boolean;
11+
onClose: ( () => void ) | null;
12+
onNext: () => void;
13+
onPrevious: () => void;
14+
};
15+
16+
const ResponseNavigation = ( {
17+
hasNext,
18+
hasPrevious,
19+
onClose,
20+
onNext,
21+
onPrevious,
22+
}: ResponseNavigationProps ): JSX.Element => {
23+
return (
24+
<div>
25+
{ onPrevious && (
26+
<Button
27+
accessibleWhenDisabled={ true }
28+
disabled={ ! hasPrevious }
29+
icon={ chevronLeft }
30+
label={ __( 'Previous', 'jetpack-forms' ) }
31+
onClick={ onPrevious }
32+
showTooltip={ true }
33+
size="compact"
34+
variant="tertiary"
35+
></Button>
36+
) }
37+
{ onNext && (
38+
<Button
39+
accessibleWhenDisabled={ true }
40+
disabled={ ! hasNext }
41+
icon={ chevronRight }
42+
label={ __( 'Next', 'jetpack-forms' ) }
43+
onClick={ onNext }
44+
showTooltip={ true }
45+
size="compact"
46+
variant="tertiary"
47+
></Button>
48+
) }
49+
{ onClose && (
50+
<Button
51+
icon={ close }
52+
label={ __( 'Close', 'jetpack-forms' ) }
53+
onClick={ onClose }
54+
showTooltip={ true }
55+
size="compact"
56+
variant="tertiary"
57+
></Button>
58+
) }
59+
</div>
60+
);
61+
};
62+
63+
export default ResponseNavigation;

projects/packages/forms/src/dashboard/inbox/dataviews/actions.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { seen, unseen, trash, backup, commentContent } from '@wordpress/icons';
66
import { store as noticesStore } from '@wordpress/notices';
77
import { notSpam, spam } from '../../icons';
88
import { store as dashboardStore } from '../../store';
9-
import InboxResponse from '../response';
109
import { updateMenuCounter, updateMenuCounterOptimistically } from '../utils';
1110

1211
export const BULK_ACTIONS = {
@@ -20,10 +19,6 @@ export const viewAction = {
2019
isPrimary: true,
2120
label: __( 'View response', 'jetpack-forms' ),
2221
modalHeader: __( 'Response', 'jetpack-forms' ),
23-
RenderModal: ( { items } ) => {
24-
const [ item ] = items;
25-
return <InboxResponse isLoading={ false } response={ item } />;
26-
},
2722
};
2823

2924
// TODO: We should probably have better error messages in case of failure.

0 commit comments

Comments
 (0)