-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Nav Block bug as Contributor user via fix to *loadPostTypeEntities
#18669
Changes from all commits
880d127
24a1fec
b8465d7
46b0980
83b5c56
07a0821
013d759
d9287c5
55bf64c
11a4ff8
8bbe13a
90f92fa
dab1a15
0a5d67f
5b89402
585a6db
641ba9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
* WordPress dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
import { useEffect, useState } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
|
@@ -156,6 +157,42 @@ function apiFetch( options ) { | |
} ); | ||
} | ||
|
||
/** | ||
* Function that fetches data using apiFetch, and updates the status. | ||
* | ||
* @param {string} path Query path. | ||
*/ | ||
function useApiFetch( path ) { | ||
// Indicate the fetching status | ||
const [ isLoading, setIsLoading ] = useState( true ); | ||
const [ data, setData ] = useState( null ); | ||
const [ error, setError ] = useState( null ); | ||
|
||
useEffect( () => { | ||
setIsLoading( true ); | ||
setData( null ); | ||
setError( null ); | ||
|
||
apiFetch( { path } ) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might be wrong, but I'm wondering if the state should be reset prior to calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oooh good point, it probably won't because only the effect will re-run. |
||
.then( ( fetchedData ) => { | ||
setData( fetchedData ); | ||
// We've stopped fetching | ||
setIsLoading( false ); | ||
} ) | ||
.catch( ( err ) => { | ||
setError( err ); | ||
// We've stopped fetching | ||
setIsLoading( false ); | ||
} ); | ||
Comment on lines
+177
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no guarantee that the component is still mounted at the point we're calling See also: https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can I also reinforce that this anti pattern is super easy to introduce. I wonder whether we could make the hook handle this edge cases automatically by:
For an example see Automattic/jetpack#15228 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This point makes me think of the behaviour of resolvers in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we could promote good behaviour1 here by having useEffect( () => {
setIsLoading( true );
setData( null );
setError( null );
const cancelFetch = apiFetch( { path } )
.then( ( fetchedData ) => {
setData( fetchedData );
// We've stopped fetching
setIsLoading( false );
} )
.catch( ( err ) => {
setError( err );
// We've stopped fetching
setIsLoading( false );
} );
return cancelFetch;
}, [ path ] ); 1: If it's easy to do the right thing, it's more likely that one will do the right thing 🙂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm motivated toward anything which keeps |
||
}, [ path ] ); | ||
|
||
return { | ||
isLoading, | ||
data, | ||
error, | ||
}; | ||
} | ||
|
||
apiFetch.use = registerMiddleware; | ||
apiFetch.setFetchHandler = setFetchHandler; | ||
|
||
|
@@ -165,4 +202,6 @@ apiFetch.createRootURLMiddleware = createRootURLMiddleware; | |
apiFetch.fetchAllMiddleware = fetchAllMiddleware; | ||
apiFetch.mediaUploadMiddleware = mediaUploadMiddleware; | ||
|
||
apiFetch.useApiFetch = useApiFetch; | ||
|
||
export default apiFetch; |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -32,7 +32,8 @@ import { | |||||||||||||
import { compose } from '@wordpress/compose'; | ||||||||||||||
import { __ } from '@wordpress/i18n'; | ||||||||||||||
import { menu } from '@wordpress/icons'; | ||||||||||||||
|
||||||||||||||
import { addQueryArgs } from '@wordpress/url'; | ||||||||||||||
import { useApiFetch } from '@wordpress/api-fetch'; | ||||||||||||||
/** | ||||||||||||||
* Internal dependencies | ||||||||||||||
*/ | ||||||||||||||
|
@@ -46,9 +47,6 @@ function Navigation( { | |||||||||||||
clientId, | ||||||||||||||
fontSize, | ||||||||||||||
hasExistingNavItems, | ||||||||||||||
hasResolvedPages, | ||||||||||||||
isRequestingPages, | ||||||||||||||
pages, | ||||||||||||||
setAttributes, | ||||||||||||||
setFontSize, | ||||||||||||||
updateNavItemBlocks, | ||||||||||||||
|
@@ -57,10 +55,10 @@ function Navigation( { | |||||||||||||
// | ||||||||||||||
// HOOKS | ||||||||||||||
// | ||||||||||||||
/* eslint-disable @wordpress/no-unused-vars-before-return */ | ||||||||||||||
const ref = useRef(); | ||||||||||||||
const { selectBlock } = useDispatch( 'core/block-editor' ); | ||||||||||||||
|
||||||||||||||
/* eslint-disable @wordpress/no-unused-vars-before-return */ | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we find ourselves having to disable this often in components? If it's not a useful rule then we should just remove it... (Not relevant to this PR!) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The value of the rule is explained here: Based on specific instances, we can choose to improve it as well. For example, it should probably be exempting hooks as a special circumstances, due to constraints imposed on us externally via Rules of Hooks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I should have also mentioned there was previously quite a few offending cases that were cleaned up as part of the introduction of the rule, in case that's a more convincing method for interpreting the value. See: #12827 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might not have been necessary to disable the rule here anyways? It was removed in #20884. If I recall correctly, the rule makes exceptions for object destructuring. To the general problem:
This already exists as of #16737: gutenberg/packages/eslint-plugin/configs/react.js Lines 10 to 15 in 1f00c74
The problem here is that the hook is named as experimental,
|
||||||||||||||
const { | ||||||||||||||
TextColor, | ||||||||||||||
BackgroundColor, | ||||||||||||||
|
@@ -86,15 +84,37 @@ function Navigation( { | |||||||||||||
}, | ||||||||||||||
[ fontSize.size ] | ||||||||||||||
); | ||||||||||||||
|
||||||||||||||
/* eslint-enable @wordpress/no-unused-vars-before-return */ | ||||||||||||||
|
||||||||||||||
const { navigatorToolbarButton, navigatorModal } = useBlockNavigator( | ||||||||||||||
clientId | ||||||||||||||
); | ||||||||||||||
|
||||||||||||||
const baseUrl = '/wp/v2/pages'; | ||||||||||||||
|
||||||||||||||
// "view" is required to ensure Pages are returned by REST API | ||||||||||||||
// for users with lower capabilities such as "Contributor" otherwise | ||||||||||||||
// Pages are not returned in the request if "edit" context is set | ||||||||||||||
const context = 'view'; | ||||||||||||||
|
||||||||||||||
const filterDefaultPages = { | ||||||||||||||
parent: 0, | ||||||||||||||
order: 'asc', | ||||||||||||||
orderby: 'id', | ||||||||||||||
context, | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
const queryPath = addQueryArgs( baseUrl, filterDefaultPages ); | ||||||||||||||
|
||||||||||||||
const { isLoading: isRequestingPages, data: pages } = useApiFetch( | ||||||||||||||
queryPath | ||||||||||||||
); | ||||||||||||||
|
||||||||||||||
const hasPages = !! pages; | ||||||||||||||
|
||||||||||||||
// Builds navigation links from default Pages. | ||||||||||||||
const defaultPagesNavigationItems = useMemo( () => { | ||||||||||||||
if ( ! pages ) { | ||||||||||||||
if ( ! hasPages ) { | ||||||||||||||
return null; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
|
@@ -134,8 +154,6 @@ function Navigation( { | |||||||||||||
selectBlock( clientId ); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
const hasPages = hasResolvedPages && pages && pages.length; | ||||||||||||||
|
||||||||||||||
const blockClassNames = classnames( className, { | ||||||||||||||
[ `items-justified-${ attributes.itemsJustification }` ]: attributes.itemsJustification, | ||||||||||||||
[ fontSize.class ]: fontSize.class, | ||||||||||||||
|
@@ -290,31 +308,8 @@ export default compose( [ | |||||||||||||
withSelect( ( select, { clientId } ) => { | ||||||||||||||
const innerBlocks = select( 'core/block-editor' ).getBlocks( clientId ); | ||||||||||||||
|
||||||||||||||
const filterDefaultPages = { | ||||||||||||||
parent: 0, | ||||||||||||||
order: 'asc', | ||||||||||||||
orderby: 'id', | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
const pagesSelect = [ | ||||||||||||||
'core', | ||||||||||||||
'getEntityRecords', | ||||||||||||||
[ 'postType', 'page', filterDefaultPages ], | ||||||||||||||
]; | ||||||||||||||
|
||||||||||||||
return { | ||||||||||||||
hasExistingNavItems: !! innerBlocks.length, | ||||||||||||||
pages: select( 'core' ).getEntityRecords( | ||||||||||||||
'postType', | ||||||||||||||
'page', | ||||||||||||||
filterDefaultPages | ||||||||||||||
), | ||||||||||||||
isRequestingPages: select( 'core/data' ).isResolving( | ||||||||||||||
...pagesSelect | ||||||||||||||
), | ||||||||||||||
hasResolvedPages: select( 'core/data' ).hasFinishedResolution( | ||||||||||||||
...pagesSelect | ||||||||||||||
), | ||||||||||||||
}; | ||||||||||||||
} ), | ||||||||||||||
withDispatch( ( dispatch, { clientId } ) => { | ||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By naming standards, this should be
useAPIFetch
.See: https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/#abbreviations-and-acronyms