-
Notifications
You must be signed in to change notification settings - Fork 22
Feature: Query Loop Block Support #98
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
base: feature/classic-editor-support
Are you sure you want to change the base?
Changes from all commits
f111a95
d86a2c8
d747367
3d55313
69149c8
278be0b
ff6658f
bdca82a
f593472
afeae34
31d0285
860ac4e
fad2d96
8c30bcc
150c31e
89ef678
48977e9
e33921d
7a7a66d
4260c99
668c8d0
0476cd5
01c68d6
2793617
cdee5fe
1466f35
e031119
ffd56e0
fc7263a
572eef7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "extends": [ | ||
| "@10up/eslint-config/wordpress" | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,273 @@ | ||||||||
| /* eslint-disable @wordpress/no-unsafe-wp-apis */ | ||||||||
| /* eslint-disable import/no-extraneous-dependencies */ | ||||||||
| /* eslint-disable import/extensions */ | ||||||||
| /** | ||||||||
| * External dependencies | ||||||||
| */ | ||||||||
| import { v4 as uuidv4 } from 'uuid'; | ||||||||
| import { registerBlockExtension, ContentPicker } from '@10up/block-components'; | ||||||||
|
|
||||||||
| /** | ||||||||
| * WordPress dependencies | ||||||||
| */ | ||||||||
| import { | ||||||||
| ToggleControl, | ||||||||
| Notice, | ||||||||
| SelectControl, | ||||||||
| BaseControl, | ||||||||
| __experimentalToolsPanel as ToolsPanel, | ||||||||
| __experimentalToolsPanelItem as ToolsPanelItem, | ||||||||
| } from '@wordpress/components'; | ||||||||
| import { InspectorControls } from '@wordpress/block-editor'; | ||||||||
| import { useSelect } from '@wordpress/data'; | ||||||||
| import { __ } from '@wordpress/i18n'; | ||||||||
| import { store as coreStore } from '@wordpress/core-data'; | ||||||||
| import { store as editorStore } from '@wordpress/editor'; | ||||||||
| import { useMemo, useEffect, useRef } from '@wordpress/element'; | ||||||||
|
|
||||||||
| /** | ||||||||
| * Internal dependencies | ||||||||
| */ | ||||||||
| import { store } from '../store'; | ||||||||
|
|
||||||||
| const BlockEdit = ({ setAttributes, attributes }) => { | ||||||||
| const { query, showRelated, relationshipKey, sourcePost, orderByRelationship } = attributes; | ||||||||
| const { postType: queriedPostType } = query; | ||||||||
|
|
||||||||
| const { postTypes, relationships, hasRelationships, currentPostId, currentPostType } = | ||||||||
| useSelect( | ||||||||
| (select) => { | ||||||||
| const { getPostTypes } = select(coreStore); | ||||||||
| const excludedPostTypes = ['attachment']; | ||||||||
| const filteredPostTypes = getPostTypes({ per_page: -1 })?.filter( | ||||||||
| ({ viewable, slug }) => viewable && !excludedPostTypes.includes(slug), | ||||||||
| ); | ||||||||
|
|
||||||||
| const currentPostId = select(editorStore).getCurrentPostId(); | ||||||||
| const currentPostType = select(editorStore).getCurrentPostType(); | ||||||||
|
|
||||||||
| const postRelationships = select(store).getRelationships( | ||||||||
| sourcePost?.[0]?.id || currentPostId, | ||||||||
| ); | ||||||||
| const filteredRelationships = Object.values(postRelationships).filter( | ||||||||
| (relationship) => | ||||||||
| Array.isArray(relationship.post_type) && | ||||||||
| relationship.post_type.includes(queriedPostType), | ||||||||
| ); | ||||||||
|
|
||||||||
| return { | ||||||||
| postTypes: filteredPostTypes, | ||||||||
| relationships: filteredRelationships, | ||||||||
| hasRelationships: filteredRelationships.length > 0, | ||||||||
| currentPostId, | ||||||||
| currentPostType, | ||||||||
| }; | ||||||||
| }, | ||||||||
| [queriedPostType, sourcePost?.length], | ||||||||
| ); | ||||||||
|
|
||||||||
| const postTypesSlugs = useMemo(() => (postTypes || []).map(({ slug }) => slug), [postTypes]); | ||||||||
|
|
||||||||
| const relationshipsOptions = useMemo( | ||||||||
| () => | ||||||||
| relationships.map((relationship) => ({ | ||||||||
| value: relationship.rel_key, | ||||||||
| label: relationship.labels.name, | ||||||||
| })), | ||||||||
| [relationships], | ||||||||
| ); | ||||||||
|
|
||||||||
| const selectedRelationshipKey = useMemo( | ||||||||
| () => | ||||||||
| relationshipKey && relationships.some((rel) => rel.rel_key === relationshipKey) | ||||||||
| ? relationshipKey | ||||||||
| : relationships[0]?.rel_key, | ||||||||
| [relationshipKey, relationships], | ||||||||
| ); | ||||||||
|
|
||||||||
| const selectedRelationship = useMemo( | ||||||||
| () => | ||||||||
| relationships.find((relationship) => relationship.rel_key === selectedRelationshipKey), | ||||||||
| [relationships, selectedRelationshipKey], | ||||||||
| ); | ||||||||
|
|
||||||||
| const lastQueryRef = useRef(query); | ||||||||
|
|
||||||||
| const updatedQuery = useMemo(() => { | ||||||||
| if (!showRelated || !selectedRelationship) { | ||||||||
| const { relationshipQuery, orderByRelationship, ...cleanQuery } = query; | ||||||||
| return cleanQuery; | ||||||||
| } | ||||||||
|
|
||||||||
| return { | ||||||||
| ...query, | ||||||||
| orderByRelationship: orderByRelationship && selectedRelationship.sortable, | ||||||||
| relationshipQuery: [ | ||||||||
| { | ||||||||
| name: selectedRelationship.rel_name, | ||||||||
| related_to_post: sourcePost?.[0]?.id || currentPostId, | ||||||||
| }, | ||||||||
| ], | ||||||||
| }; | ||||||||
| }, [query, showRelated, orderByRelationship, selectedRelationship, sourcePost, currentPostId]); | ||||||||
|
|
||||||||
| useEffect(() => { | ||||||||
| if (JSON.stringify(lastQueryRef.current) !== JSON.stringify(updatedQuery)) { | ||||||||
| setAttributes({ query: updatedQuery }); | ||||||||
| lastQueryRef.current = updatedQuery; | ||||||||
| } | ||||||||
| }, [updatedQuery, setAttributes]); | ||||||||
|
|
||||||||
| const relationshipsControlLabel = __('Relationship', 'tenup-content-connect'); | ||||||||
| const relationshipsControlHelp = __( | ||||||||
| 'Select a relationship to determine how related entities are retrieved.', | ||||||||
| 'tenup-content-connect', | ||||||||
| ); | ||||||||
| const sourcePostControlHelp = __( | ||||||||
| 'Choose the post from which related entities will be pulled. Defaults to the current post.', | ||||||||
| 'tenup-content-connect', | ||||||||
| ); | ||||||||
|
|
||||||||
| const resetAll = () => { | ||||||||
| setAttributes({ | ||||||||
| showRelated: false, | ||||||||
| sourcePost: undefined, | ||||||||
| relationshipKey: undefined, | ||||||||
| orderByRelationship: true, | ||||||||
| }); | ||||||||
| }; | ||||||||
|
|
||||||||
| const onShowRelatedChange = (value) => { | ||||||||
| if (!value) { | ||||||||
| resetAll(); | ||||||||
| } else { | ||||||||
| setAttributes({ | ||||||||
| showRelated: value, | ||||||||
| sourcePost: [ | ||||||||
| { | ||||||||
| id: currentPostId, | ||||||||
| type: currentPostType, | ||||||||
| uuid: uuidv4(), | ||||||||
| }, | ||||||||
| ], | ||||||||
| }); | ||||||||
| } | ||||||||
| }; | ||||||||
|
|
||||||||
| const onSourcePostChange = (ids) => { | ||||||||
| setAttributes({ sourcePost: ids.length ? ids : undefined }); | ||||||||
| }; | ||||||||
|
|
||||||||
| const onRelationshipChange = (value) => { | ||||||||
| setAttributes({ relationshipKey: value }); | ||||||||
| }; | ||||||||
|
|
||||||||
| const onOrderByRelationshipChange = (value) => { | ||||||||
| setAttributes({ orderByRelationship: value }); | ||||||||
| }; | ||||||||
|
|
||||||||
| return ( | ||||||||
| <InspectorControls> | ||||||||
| <ToolsPanel label={__('Related', 'tenup-content-connect')} resetAll={resetAll}> | ||||||||
| <ToolsPanelItem | ||||||||
| hasValue={() => !!showRelated} | ||||||||
| label={__('Related entities', 'tenup-content-connect')} | ||||||||
| onDeselect={() => resetAll()} | ||||||||
| isShownByDefault | ||||||||
| > | ||||||||
| <ToggleControl | ||||||||
| label={__('Only show related entities', 'tenup-content-connect')} | ||||||||
| checked={showRelated} | ||||||||
| onChange={onShowRelatedChange} | ||||||||
| /> | ||||||||
| </ToolsPanelItem> | ||||||||
| {showRelated && ( | ||||||||
| <ToolsPanelItem | ||||||||
| hasValue={() => !!sourcePost} | ||||||||
| label={__('Source post', 'tenup-content-connect')} | ||||||||
| onDeselect={() => setAttributes({ sourcePost: undefined })} | ||||||||
| isShownByDefault | ||||||||
| > | ||||||||
| <BaseControl help={sourcePostControlHelp}> | ||||||||
| <ContentPicker | ||||||||
| onPickChange={onSourcePostChange} | ||||||||
| mode="post" | ||||||||
| content={sourcePost} | ||||||||
| contentTypes={postTypesSlugs} | ||||||||
| maxContentItems={1} | ||||||||
| singlePickedLabel={__('Selected post:', 'tenup-content-connect')} | ||||||||
| /> | ||||||||
| </BaseControl> | ||||||||
| </ToolsPanelItem> | ||||||||
|
Comment on lines
+185
to
+201
Member
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 way this currently works is odd. By default it already has the current post selected. I am not sure if this is how it should be 🤔 I think instead of that we should hide the That way users that want to select a different post to relate to (which should be a very niche feature) can still do it but it isn't there front and center. |
||||||||
| )} | ||||||||
| {showRelated && ( | ||||||||
| <ToolsPanelItem | ||||||||
| hasValue={() => !!relationshipKey} | ||||||||
| label={__('Relationship', 'tenup-content-connect')} | ||||||||
| onDeselect={() => setAttributes({ relationshipKey: undefined })} | ||||||||
| isShownByDefault | ||||||||
| > | ||||||||
| {hasRelationships && ( | ||||||||
| <SelectControl | ||||||||
| options={relationshipsOptions} | ||||||||
| value={relationshipKey} | ||||||||
| label={relationshipsControlLabel} | ||||||||
| onChange={onRelationshipChange} | ||||||||
| help={relationshipsControlHelp} | ||||||||
| __nextHasNoMarginBottom | ||||||||
|
Member
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.
Suggested change
If we need this control we should make sure it has the new default size to make it look better :) |
||||||||
| /> | ||||||||
| )} | ||||||||
|
Comment on lines
+210
to
+219
Member
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. We should only show this select control if there is more than one relationship available. I got it in an example where it only had one option |
||||||||
| {!hasRelationships && ( | ||||||||
| <Notice spokenMessage={null} status="warning" isDismissible={false}> | ||||||||
| {__( | ||||||||
| 'No relationships exist for the selected post type or post. Try selecting a different post or post type.', | ||||||||
| 'tenup-content-connect', | ||||||||
| )} | ||||||||
| </Notice> | ||||||||
| )} | ||||||||
| </ToolsPanelItem> | ||||||||
| )} | ||||||||
| {showRelated && selectedRelationship?.sortable && ( | ||||||||
| <ToolsPanelItem | ||||||||
| hasValue={() => !!orderByRelationship} | ||||||||
| label={__('Order by relationship', 'tenup-content-connect')} | ||||||||
| onDeselect={() => setAttributes({ orderByRelationship: undefined })} | ||||||||
| > | ||||||||
| <ToggleControl | ||||||||
| __nextHasNoMarginBottom | ||||||||
| label={__('Order by relationship', 'tenup-content-connect')} | ||||||||
| checked={orderByRelationship} | ||||||||
| onChange={onOrderByRelationshipChange} | ||||||||
| help={__( | ||||||||
| 'If enabled, the order of the posts will be determined by the selected relationship. This supersedes any other ordering.', | ||||||||
| 'tenup-content-connect', | ||||||||
| )} | ||||||||
| /> | ||||||||
| </ToolsPanelItem> | ||||||||
| )} | ||||||||
| </ToolsPanel> | ||||||||
| </InspectorControls> | ||||||||
| ); | ||||||||
| }; | ||||||||
|
|
||||||||
| registerBlockExtension('core/query', { | ||||||||
| extensionName: 'content-connect', | ||||||||
| attributes: { | ||||||||
| showRelated: { | ||||||||
| type: 'boolean', | ||||||||
| default: false, | ||||||||
| }, | ||||||||
| sourcePost: { | ||||||||
| type: 'array', | ||||||||
| }, | ||||||||
| relationshipKey: { | ||||||||
| type: 'string', | ||||||||
| }, | ||||||||
| orderByRelationship: { | ||||||||
| type: 'boolean', | ||||||||
| default: true, | ||||||||
| }, | ||||||||
| }, | ||||||||
| classNameGenerator: () => '', | ||||||||
| Edit: BlockEdit, | ||||||||
| }); | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| import './core-query'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,6 @@ | ||
| import React from 'react'; | ||
| import { useSelect } from '@wordpress/data'; | ||
| import { store as editorStore } from '@wordpress/editor'; | ||
| import { PluginDocumentSettingPanel } from '@wordpress/edit-post'; | ||
|
Member
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. Nice! We should always use the version from |
||
| import { store as editorStore, PluginDocumentSettingPanel } from '@wordpress/editor'; | ||
| import { store } from '../store'; | ||
| import { RelationshipManager } from './relationship-manager'; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { createReduxStore, register, select, dispatch, createSelector } from '@wordpress/data'; | ||
| import { store as editorStore } from '@wordpress/editor'; | ||
| import { addFilter } from '@wordpress/hooks'; | ||
| import * as api from './api'; | ||
| import { ContentConnectRelatedEntities, ContentConnectRelationships, ContentConnectState } from './types'; | ||
|
|
@@ -88,6 +89,13 @@ const actions = { | |
| * @returns The action to mark the post as dirty. | ||
| */ | ||
| markPostAsDirty(postId: number): MarkPostAsDirtyAction { | ||
| // Trigger the block editor to mark the post as dirty. | ||
| dispatch(editorStore).editPost({ | ||
| meta: { | ||
| _content_connect_edit_lock: Date.now() | ||
| } | ||
| }); | ||
|
|
||
| return { | ||
| type: 'MARK_POST_AS_DIRTY', | ||
| postId, | ||
|
|
@@ -124,19 +132,7 @@ const actions = { | |
|
|
||
| const key = getRelatedEntitiesKey(postId, relKey); | ||
|
|
||
| const relatedIds = entities.map(entity => { | ||
| return typeof entity.id === 'string' ? parseInt(entity.id, 10) : entity.id; | ||
| }); | ||
|
|
||
| dispatch.setRelatedEntities(key, entities); | ||
|
|
||
| await api.updateRelatedEntities( | ||
| postId, | ||
| relKey, | ||
| relType, | ||
| relatedIds | ||
| ); | ||
|
|
||
| dispatch.markPostAsDirty(postId); | ||
| }; | ||
| }, | ||
|
|
@@ -183,7 +179,7 @@ export const store = createReduxStore(STORE_NAME, { | |
| }, | ||
| actions, | ||
| selectors: { | ||
| getRelationships(state: ContentConnectState, postId: number | null, options?: api.GetRelationshipsOptions) { | ||
| getRelationships(state: ContentConnectState, postId: number | null, options?: api.GetRelationshipsOptions): Record<string, { rel_key: string; rel_type: string }> { | ||
| if (postId === null) { | ||
| return {}; | ||
| } | ||
|
|
@@ -234,25 +230,29 @@ async function persistContentConnectionChanges() { | |
|
|
||
| // Update each relationship for the post | ||
| await Promise.all( | ||
| Object.entries(relationships).map(async ([relKey, relType]) => { | ||
| (Object.values(relationships) as Array<{ rel_key: string; rel_type: string }>).map(async (relationship) => { | ||
| const relatedEntities = select(STORE_NAME).getRelatedEntities(postId, { | ||
| rel_key: relKey, | ||
| rel_type: relType, | ||
| rel_key: relationship.rel_key, | ||
| rel_type: relationship.rel_type, | ||
| }); | ||
|
|
||
| const relatedIds = relatedEntities.map(entity => | ||
| typeof entity.id === 'string' ? parseInt(entity.id, 10) : entity.id | ||
| ); | ||
|
|
||
| await api.updateRelatedEntities( | ||
| postId, | ||
| relKey, | ||
| relType as string, | ||
| relatedEntities.map(post => post.id), | ||
| relationship.rel_key, | ||
| relationship.rel_type, | ||
| relatedIds | ||
| ); | ||
| }) | ||
| ); | ||
| }) | ||
| ); | ||
|
|
||
| // Clear dirty entities after successful save | ||
| dispatch(STORE_NAME).clearDirtyEntities(); | ||
| (dispatch(STORE_NAME) as any).clearDirtyEntities(); | ||
|
Member
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. Instead of typing it here as any we should use the actual store reference here instead of just the string of the name. this will ensure the types come across properly |
||
| } | ||
|
|
||
| // Add the pre-save hook to persist changes | ||
|
|
||
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.
If we update to version 1.21.1 of the block components we can use the new code split import syntax for these also:
which will result in a smaller bundle size :)