Skip to content
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

Block API: Add role for 'internal' attributes not copied on duplication #34750

Open
wants to merge 17 commits into
base: trunk
Choose a base branch
from

Conversation

stacimc
Copy link
Contributor

@stacimc stacimc commented Sep 10, 2021

Closes #29693
Alternative to #32604

Description

This PR:

  • Adds handling for a new internal attribute experimentalRole. Attributes registered with this role will not be copied on block duplication.
  • Refactors __internalWidgetID to use the new experimental role

Example fragment of block.json:

            "attributes": {
		"myInternalAttribute": {
			"type": "string",
			"default": "my-default-value",
			"__experimentalRole": "internal"
		}
	},

For just one example of how this might be used: the Jetpack Pay with Paypal block uses a productId attribute to identify the actual product record referenced by the block. This data is meant to be internal to the block and should not be exposed to the user. When the user duplicates the block, we want to copy the other attributes used to store price info and other details, but create a brand new product record/productId.

This is similar conceptually to #32604 with a few changes:

  • The use of internal terminology rather than duplicable or unique, which implies additional constraints
  • The use of the __experimentalRole and associated utilities (thanks @ntsekouras for the suggestion)

Notes

  • In other regards, an attribute with this role acts like a normal attribute
    • It respects any configured default values Default values are not filled in on block duplication either; support for this could be easily added if a viable use case becomes apparent, though
    • It can be managed as normal via setAttributes
    • It can apply to any type of attribute (ie it is not necessarily an id field)
  • While you can give this role to an attribute with a "source"/"selector" configuration, it is not meaningful to do so. For example, adding it to the content attribute of the Code block would not make sense, because although the attribute would not be copied on duplication, it will populate with the same value from the html.
  • Because this is experimental and also relies on experimental features, I have not added documentation; happy to do so if that is appropriate!
  • A note about Reusable blocks:
    Internal attributes are not removed when you convert a block into a Reusable block. I don’t know a ton about Reusable blocks, so calling this out so that someone more familiar can correct me; my understanding is that multiple instances of a Reusable block are not independent of each other (ie editing one copy affects them all), so internal attributes should also be shared.

How has this been tested?

  • Play around with adding __experimentalRole: 'internal' to any block attribute.
  • Insert the block into a new post, change its attributes, and duplicate it
  • Verify that the internal attributes were not copied, but all other attributes were
  • Test with attributes that have a default value and ensure that the default is respected in the duplicated block
  • Test Copy+Paste using both the Copy menu item, and by keyboard commands
  • Test duplicating a block with internal attributes in the widget editor
  • Test adding and editing widgets in both the Widget editor and the Customizer
    • Test moving widgets between widget areas/side panels
    • Test a server-side rendered block in the widget editor and make sure the preview is displayed correctly (Basically verify that Fix server rendered widget not previewing #29197 is still fixed with the new implementation)

Screenshots

Types of changes

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • I've tested my changes with keyboard and screen readers.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR (please manually search all *.native.js files for terms that need renaming or removal).

@stacimc stacimc added [Feature] Block API API that allows to express the block paradigm. Needs Technical Feedback Needs testing from a developer perspective. [Type] New API New API to be used by plugin developers or package users. labels Sep 10, 2021
@stacimc stacimc self-assigned this Sep 10, 2021
@github-actions
Copy link

github-actions bot commented Sep 10, 2021

Size Change: +82 B (0%)

Total Size: 1.14 MB

Filename Size Change
build/block-editor/index.min.js 141 kB +26 B (0%)
build/blocks/index.min.js 46.4 kB -14 B (0%)
build/customize-widgets/index.min.js 11.4 kB +19 B (0%)
build/edit-widgets/index.min.js 16.6 kB +12 B (0%)
build/server-side-render/index.min.js 1.54 kB -36 B (-2%)
build/widgets/index.min.js 7.23 kB +75 B (+1%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 960 B
build/admin-manifest/index.min.js 1.1 kB
build/annotations/index.min.js 2.75 kB
build/api-fetch/index.min.js 2.22 kB
build/autop/index.min.js 2.12 kB
build/blob/index.min.js 459 B
build/block-directory/index.min.js 6.28 kB
build/block-directory/style-rtl.css 1.01 kB
build/block-directory/style.css 1.01 kB
build/block-editor/default-editor-styles-rtl.css 378 B
build/block-editor/default-editor-styles.css 378 B
build/block-editor/style-rtl.css 14.6 kB
build/block-editor/style.css 14.6 kB
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 65 B
build/block-library/blocks/archives/style.css 65 B
build/block-library/blocks/audio/editor-rtl.css 150 B
build/block-library/blocks/audio/editor.css 150 B
build/block-library/blocks/audio/style-rtl.css 111 B
build/block-library/blocks/audio/style.css 111 B
build/block-library/blocks/audio/theme-rtl.css 125 B
build/block-library/blocks/audio/theme.css 125 B
build/block-library/blocks/block/editor-rtl.css 161 B
build/block-library/blocks/block/editor.css 161 B
build/block-library/blocks/button/editor-rtl.css 470 B
build/block-library/blocks/button/editor.css 470 B
build/block-library/blocks/button/style-rtl.css 560 B
build/block-library/blocks/button/style.css 560 B
build/block-library/blocks/buttons/editor-rtl.css 292 B
build/block-library/blocks/buttons/editor.css 292 B
build/block-library/blocks/buttons/style-rtl.css 275 B
build/block-library/blocks/buttons/style.css 275 B
build/block-library/blocks/calendar/style-rtl.css 207 B
build/block-library/blocks/calendar/style.css 207 B
build/block-library/blocks/categories/editor-rtl.css 84 B
build/block-library/blocks/categories/editor.css 83 B
build/block-library/blocks/categories/style-rtl.css 79 B
build/block-library/blocks/categories/style.css 79 B
build/block-library/blocks/code/style-rtl.css 90 B
build/block-library/blocks/code/style.css 90 B
build/block-library/blocks/code/theme-rtl.css 131 B
build/block-library/blocks/code/theme.css 131 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 406 B
build/block-library/blocks/columns/style.css 406 B
build/block-library/blocks/comment-template/style-rtl.css 127 B
build/block-library/blocks/comment-template/style.css 127 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-query-loop/editor-rtl.css 95 B
build/block-library/blocks/comments-query-loop/editor.css 95 B
build/block-library/blocks/cover/editor-rtl.css 546 B
build/block-library/blocks/cover/editor.css 547 B
build/block-library/blocks/cover/style-rtl.css 1.22 kB
build/block-library/blocks/cover/style.css 1.22 kB
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 B
build/block-library/blocks/embed/style-rtl.css 417 B
build/block-library/blocks/embed/style.css 417 B
build/block-library/blocks/embed/theme-rtl.css 124 B
build/block-library/blocks/embed/theme.css 124 B
build/block-library/blocks/file/editor-rtl.css 300 B
build/block-library/blocks/file/editor.css 300 B
build/block-library/blocks/file/style-rtl.css 255 B
build/block-library/blocks/file/style.css 255 B
build/block-library/blocks/file/view.min.js 322 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB
build/block-library/blocks/freeform/editor.css 2.44 kB
build/block-library/blocks/gallery/editor-rtl.css 965 B
build/block-library/blocks/gallery/editor.css 967 B
build/block-library/blocks/gallery/style-rtl.css 1.61 kB
build/block-library/blocks/gallery/style.css 1.61 kB
build/block-library/blocks/gallery/theme-rtl.css 122 B
build/block-library/blocks/gallery/theme.css 122 B
build/block-library/blocks/group/editor-rtl.css 159 B
build/block-library/blocks/group/editor.css 159 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 114 B
build/block-library/blocks/heading/style.css 114 B
build/block-library/blocks/html/editor-rtl.css 332 B
build/block-library/blocks/html/editor.css 333 B
build/block-library/blocks/image/editor-rtl.css 810 B
build/block-library/blocks/image/editor.css 809 B
build/block-library/blocks/image/style-rtl.css 507 B
build/block-library/blocks/image/style.css 511 B
build/block-library/blocks/image/theme-rtl.css 124 B
build/block-library/blocks/image/theme.css 124 B
build/block-library/blocks/latest-comments/style-rtl.css 284 B
build/block-library/blocks/latest-comments/style.css 284 B
build/block-library/blocks/latest-posts/editor-rtl.css 199 B
build/block-library/blocks/latest-posts/editor.css 198 B
build/block-library/blocks/latest-posts/style-rtl.css 447 B
build/block-library/blocks/latest-posts/style.css 446 B
build/block-library/blocks/list/style-rtl.css 94 B
build/block-library/blocks/list/style.css 94 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 493 B
build/block-library/blocks/media-text/style.css 490 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 649 B
build/block-library/blocks/navigation-link/editor.css 650 B
build/block-library/blocks/navigation-link/style-rtl.css 94 B
build/block-library/blocks/navigation-link/style.css 94 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 299 B
build/block-library/blocks/navigation-submenu/editor.css 299 B
build/block-library/blocks/navigation-submenu/view.min.js 343 B
build/block-library/blocks/navigation/editor-rtl.css 1.92 kB
build/block-library/blocks/navigation/editor.css 1.93 kB
build/block-library/blocks/navigation/style-rtl.css 1.85 kB
build/block-library/blocks/navigation/style.css 1.84 kB
build/block-library/blocks/navigation/view.min.js 2.81 kB
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 401 B
build/block-library/blocks/page-list/editor.css 402 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 B
build/block-library/blocks/paragraph/editor-rtl.css 157 B
build/block-library/blocks/paragraph/editor.css 157 B
build/block-library/blocks/paragraph/style-rtl.css 273 B
build/block-library/blocks/paragraph/style.css 273 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/style-rtl.css 446 B
build/block-library/blocks/post-comments-form/style.css 446 B
build/block-library/blocks/post-comments/style-rtl.css 521 B
build/block-library/blocks/post-comments/style.css 521 B
build/block-library/blocks/post-excerpt/editor-rtl.css 73 B
build/block-library/blocks/post-excerpt/editor.css 73 B
build/block-library/blocks/post-excerpt/style-rtl.css 69 B
build/block-library/blocks/post-excerpt/style.css 69 B
build/block-library/blocks/post-featured-image/editor-rtl.css 721 B
build/block-library/blocks/post-featured-image/editor.css 721 B
build/block-library/blocks/post-featured-image/style-rtl.css 153 B
build/block-library/blocks/post-featured-image/style.css 153 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 323 B
build/block-library/blocks/post-template/style.css 323 B
build/block-library/blocks/post-terms/style-rtl.css 73 B
build/block-library/blocks/post-terms/style.css 73 B
build/block-library/blocks/post-title/style-rtl.css 80 B
build/block-library/blocks/post-title/style.css 80 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 198 B
build/block-library/blocks/pullquote/editor.css 198 B
build/block-library/blocks/pullquote/style-rtl.css 389 B
build/block-library/blocks/pullquote/style.css 388 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 221 B
build/block-library/blocks/query-pagination/editor.css 211 B
build/block-library/blocks/query-pagination/style-rtl.css 234 B
build/block-library/blocks/query-pagination/style.css 231 B
build/block-library/blocks/query/editor-rtl.css 131 B
build/block-library/blocks/query/editor.css 132 B
build/block-library/blocks/quote/style-rtl.css 201 B
build/block-library/blocks/quote/style.css 201 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/rss/editor-rtl.css 202 B
build/block-library/blocks/rss/editor.css 204 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 165 B
build/block-library/blocks/search/editor.css 165 B
build/block-library/blocks/search/style-rtl.css 397 B
build/block-library/blocks/search/style.css 398 B
build/block-library/blocks/search/theme-rtl.css 64 B
build/block-library/blocks/search/theme.css 64 B
build/block-library/blocks/separator/editor-rtl.css 99 B
build/block-library/blocks/separator/editor.css 99 B
build/block-library/blocks/separator/style-rtl.css 245 B
build/block-library/blocks/separator/style.css 245 B
build/block-library/blocks/separator/theme-rtl.css 172 B
build/block-library/blocks/separator/theme.css 172 B
build/block-library/blocks/shortcode/editor-rtl.css 474 B
build/block-library/blocks/shortcode/editor.css 474 B
build/block-library/blocks/site-logo/editor-rtl.css 744 B
build/block-library/blocks/site-logo/editor.css 744 B
build/block-library/blocks/site-logo/style-rtl.css 181 B
build/block-library/blocks/site-logo/style.css 181 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 84 B
build/block-library/blocks/site-title/editor.css 84 B
build/block-library/blocks/social-link/editor-rtl.css 177 B
build/block-library/blocks/social-link/editor.css 177 B
build/block-library/blocks/social-links/editor-rtl.css 674 B
build/block-library/blocks/social-links/editor.css 673 B
build/block-library/blocks/social-links/style-rtl.css 1.37 kB
build/block-library/blocks/social-links/style.css 1.36 kB
build/block-library/blocks/spacer/editor-rtl.css 332 B
build/block-library/blocks/spacer/editor.css 332 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 471 B
build/block-library/blocks/table/editor.css 472 B
build/block-library/blocks/table/style-rtl.css 481 B
build/block-library/blocks/table/style.css 481 B
build/block-library/blocks/table/theme-rtl.css 188 B
build/block-library/blocks/table/theme.css 188 B
build/block-library/blocks/tag-cloud/style-rtl.css 214 B
build/block-library/blocks/tag-cloud/style.css 215 B
build/block-library/blocks/template-part/editor-rtl.css 560 B
build/block-library/blocks/template-part/editor.css 559 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 87 B
build/block-library/blocks/verse/style.css 87 B
build/block-library/blocks/video/editor-rtl.css 571 B
build/block-library/blocks/video/editor.css 572 B
build/block-library/blocks/video/style-rtl.css 173 B
build/block-library/blocks/video/style.css 173 B
build/block-library/blocks/video/theme-rtl.css 124 B
build/block-library/blocks/video/theme.css 124 B
build/block-library/common-rtl.css 908 B
build/block-library/common.css 905 B
build/block-library/editor-rtl.css 10 kB
build/block-library/editor.css 10 kB
build/block-library/index.min.js 166 kB
build/block-library/reset-rtl.css 474 B
build/block-library/reset.css 474 B
build/block-library/style-rtl.css 10.8 kB
build/block-library/style.css 10.8 kB
build/block-library/theme-rtl.css 672 B
build/block-library/theme.css 676 B
build/block-serialization-default-parser/index.min.js 1.09 kB
build/block-serialization-spec-parser/index.min.js 2.79 kB
build/components/index.min.js 215 kB
build/components/style-rtl.css 15.5 kB
build/components/style.css 15.5 kB
build/compose/index.min.js 11.2 kB
build/core-data/index.min.js 13.4 kB
build/customize-widgets/style-rtl.css 1.5 kB
build/customize-widgets/style.css 1.49 kB
build/data-controls/index.min.js 631 B
build/data/index.min.js 7.49 kB
build/date/index.min.js 31.9 kB
build/deprecated/index.min.js 485 B
build/dom-ready/index.min.js 304 B
build/dom/index.min.js 4.5 kB
build/edit-navigation/index.min.js 16.2 kB
build/edit-navigation/style-rtl.css 3.76 kB
build/edit-navigation/style.css 3.76 kB
build/edit-post/classic-rtl.css 546 B
build/edit-post/classic.css 547 B
build/edit-post/index.min.js 29.6 kB
build/edit-post/style-rtl.css 7.15 kB
build/edit-post/style.css 7.14 kB
build/edit-site/index.min.js 41.5 kB
build/edit-site/style-rtl.css 7.22 kB
build/edit-site/style.css 7.21 kB
build/edit-widgets/style-rtl.css 4.17 kB
build/edit-widgets/style.css 4.17 kB
build/editor/index.min.js 38.4 kB
build/editor/style-rtl.css 3.71 kB
build/editor/style.css 3.71 kB
build/element/index.min.js 3.29 kB
build/escape-html/index.min.js 517 B
build/format-library/index.min.js 6.58 kB
build/format-library/style-rtl.css 571 B
build/format-library/style.css 571 B
build/hooks/index.min.js 1.63 kB
build/html-entities/index.min.js 424 B
build/i18n/index.min.js 3.75 kB
build/is-shallow-equal/index.min.js 501 B
build/keyboard-shortcuts/index.min.js 1.8 kB
build/keycodes/index.min.js 1.39 kB
build/list-reusable-blocks/index.min.js 1.72 kB
build/list-reusable-blocks/style-rtl.css 838 B
build/list-reusable-blocks/style.css 838 B
build/media-utils/index.min.js 2.92 kB
build/notices/index.min.js 925 B
build/nux/index.min.js 2.08 kB
build/nux/style-rtl.css 747 B
build/nux/style.css 743 B
build/plugins/index.min.js 1.84 kB
build/primitives/index.min.js 917 B
build/priority-queue/index.min.js 582 B
build/react-i18n/index.min.js 671 B
build/react-refresh-entry/index.min.js 8.44 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/redux-routine/index.min.js 2.65 kB
build/reusable-blocks/index.min.js 2.22 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 11 kB
build/shortcode/index.min.js 1.49 kB
build/token-list/index.min.js 639 B
build/url/index.min.js 1.9 kB
build/viewport/index.min.js 1.05 kB
build/warning/index.min.js 248 B
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.04 kB

compressed-size-action

@glendaviesnz
Copy link
Contributor

This works as expected for me when using the block Duplicate option but not when using the Copy option and pasting the block somewhere else. Should we make the attribute behave the same in duplicate and copy?

@andrewserong
Copy link
Contributor

Nice approach @stacimc, I like the addition of the internal role, and other than the copy/paste behaviour that Glen mentions, it's testing well for me, too!

@@ -32,6 +32,7 @@ import {
} from './registration';
import {
normalizeBlockType,
__experimentalStripInternalBlockAttributes,
__experimentalSanitizeBlockAttributes,
Copy link
Contributor

Choose a reason for hiding this comment

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

@kevin940726 @noisysocks It'd be good to look into whether this PR makes it possible to remove __experimentalSanitizeBlockAttributes, and update the widget editor to use an internal attribute instead.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe! __internalWidgetId can be set on any block type, so we'd have to hook into registerBlockType and add widgetId: { type: 'string', __experimentalRole: 'internal' } to the schema of every single registered block.

If we can do that then that will make __experimentalCloneSanitizedBlock unnecessary as this was only added so that duplicating blocks would not copy any attributes that don't exist in the schema e.g. __internalWidgetId. See #29111.

It might be nice to try and do all of that along with this change so that there is a usage of __experimentalRole: 'internal' within core. We're less likely to break APIs that we ourselves use 🙂

Copy link
Member

Choose a reason for hiding this comment

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

When discussing it in #29693 I envisioned that the API proposed in this PR would be sufficient to refactor the __internalWidgetId use case. It'd be great to confirm that.

Copy link
Member

Choose a reason for hiding this comment

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

I can try implementing what I described above tomorrow unless @stacimc wants to give it a try 🙂

Copy link
Contributor Author

@stacimc stacimc Sep 13, 2021

Choose a reason for hiding this comment

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

Unfortunately I think this may require a little more tinkering -- as is, I'm running into the same issue in #29111 where internal attributes get dropped when the user makes changes in the widget editor 🤔

In fact at the moment I'm seeing the issue even if I keep the separated cloneBlock and __experimentalCloneSanitizedBlock, because in order to fix the issue mentioned by @glendaviesnz where internal attributes aren't reset when you Copy + paste a block, I need to strip the internal attributes within createBlock as well. My somewhat tenuous understanding of the widget editor is that it fetches widgets and transforms them into blocks using createBlock -- so the internal attributes get reset every time you refresh the page. This ends up causing block invalidation errors.

Edit: I missed @ntsekouras' suggestion below which looks like a better alternative 😄 I'll see what I can do with this and take a shot at removing __experimentalCloneSanitizedBlock 👍

Copy link
Contributor

@ntsekouras ntsekouras left a comment

Choose a reason for hiding this comment

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

Thanks for working on this @stacimc!

IMO this PR could be the place to remove the __experimentalCloneSanitizedBlock as mentioned in comments.

What we need to handle this properly is something like the cloneBlock without the new clientId (that means recursively applying this to innerBlocks). IMO a good place to add this functionality for copy action is here: https://github.com/WordPress/gutenberg/blob/trunk/packages/block-editor/src/components/copy-handler/index.js#L124, before the blocks' serialization with something like this:

let blocks = getBlocksByClientId( selectedBlockClientIds );
if ( event.type === 'copy' ) {
	blocks = blocks.map( ( block ) =>
		__experimentalRemoveAttributesByRole( block, 'internal' )
	);
}

packages/blocks/src/api/utils.js Outdated Show resolved Hide resolved
@apeatling apeatling self-requested a review September 13, 2021 18:21
Copy link
Contributor

@apeatling apeatling left a comment

Choose a reason for hiding this comment

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

This tested well for me. The internal terminology here feels better. 👍

@stacimc
Copy link
Contributor Author

stacimc commented Sep 22, 2021

Some updates:

  • I’ve gotten it working for Copy+Paste, both when using keyboard commands and when using the Copy menu item
  • I’ve fixed a couple bugs that I noticed (more info in commit descriptions):
    • Internal attributes getting stripped from both the copy and the original block in the widget editor
    • Block validation errors caused by not filling in default values for stripped internal attributes on Copy
  • I’ve taken a look at the __internalWidgetId and what we can do to as far as removing __experimentalCloneSanitizedBlock.

Unfortunately I don’t think it’s going to be so easy to get rid of __experimentalCloneSanitizedBlock. With internal attributes, we run into the same problem as described in #29111/#28379. That is:

Making widgetId into an internal attribute doesn’t actually fix this, at least that I could come up with. We still need cloneBlock to strip those attributes in some cases (on duplication) and not in others (useBlockSync). Moreover I don’t think we can even get rid of __experimentalSanitizeBlockAttributes, as this is really useful for filling in any default values for internal attributes after they’ve been removed.

That being said, I think it might still be worth making widgetId into an internal attribute, because:

  • To @noisysocks point, it would be nice to have a usage of the internal role in core
  • I’d prefer not to accomplish the same thing in two different ways (unregistered attrs, internal attrs)

In my latest commit, I've hooked into registerBlockType to register it as an internal attribute with really minimal changes and it seems to be working so far. I'll definitely be updating/adding new tests, but it seems like a good time to check in to see how others feel about the approach. @noisysocks @gziolo does that square with your knowledge of the widgets issues?

@noisysocks
Copy link
Member

@noisysocks @gziolo does that square with your knowledge of the widgets issues?

Yep, that all makes sense 🙂

'blocks.registerBlockType',
'core/widget/addAttributes',
addAttributes
);
Copy link
Member

Choose a reason for hiding this comment

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

Probably it doesn't affect anything in practice but it might be better to have this addFilter() call inside a function which initialize() in edit-widgets and customize-widgets calls. That way merely importing @wordpress/widgets doesn't cause something to happen.

*
* @return {Object} Updated block settings.
*/
function addAttributes( settings ) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we give this a more specific name? Maybe addInternalWidgetIdAttribute?

packages/widgets/src/utils.js Outdated Show resolved Hide resolved
@ntsekouras
Copy link
Contributor

Block validation errors caused by not filling in default values for stripped internal attributes on Copy

I haven't checked the changes yet, but in which case would we need to have a default value of an internal property? Wouldn't the absence of this prop be enough for a block's edit function to handle the generation of that prop?

@stacimc
Copy link
Contributor Author

stacimc commented Sep 23, 2021

I haven't checked the changes yet, but in which case would we need to have a default value of an internal property? Wouldn't the absence of this prop be enough for a block's edit function to handle the generation of that prop?

@ntsekouras In the case of widgetId, as well as my productId use case described in the PR description, that's correct. I was thinking of the internal role as something that could be applied to any arbitrary attribute, which would not be copied on duplication but would otherwise function as a normal attribute.

Top of my head I can't think of any great use cases where someone might want an internal attribute with a default value, so I'm certainly not attached to keeping it, though. If others can't think of a reason for allowing defaults, I'd be happy to rip out __experimentalSanitizeBlockAttributes and update the docs!

@stacimc stacimc force-pushed the try/internal-block-attributes branch from d3509cc to 5e28faa Compare October 11, 2021 22:44
@stacimc
Copy link
Contributor Author

stacimc commented Oct 11, 2021

Added a couple of changes:

Removed use of __experimentalSanitizeBlockAttributes in __experimentalCloneSanitizedBlock
I only kept this previously to allow filling in default values for internal attributes that get stripped on cloning, but since I can't think of a reasonable use case for an internal attribute with a default, I'm pulling it out. This means we could technically remove __experimentalSanitizeBlockAttributes altogether, because it's only used in one place now (createBlock); however I decided to leave it because it makes unit testing neater in my opinion.

Fixed a bug when moving inner blocks between sidebars in the Widgets section of the Customizer
Tricky one, documented in this e2e test -- when you transform blocks into a Group, they are 'copied' via createBlock into the inner blocks of the new Group. With the previous widgetId implementation, running through createBlock like this would drop the widgetId, but that doesn't happen with the new implementation.

This caused problems because we were using the absence of the widgetId to detect when a block in the widget editor is an inner block and handle it appropriately when moving it between widget sidebars. I've updated it now to check not only that the block has a widgetId but also that there is a corresponding widget.

I'm calling this out because it does mean that we can have dangling __internalWidgetId attributes on blocks that used to be widgets but were converted to inner blocks. Another solution would be to strip internal attributes on createBlock as well as on duplication -- but while this is great for widgetId, it's not what we want for internal attributes in general. (Eg: we have some sort of internal productId; we don't want that to get stripped from our block if we Group it!)

@gziolo
Copy link
Member

gziolo commented Oct 12, 2021

This means we could technically remove __experimentalSanitizeBlockAttributes altogether, because it's only used in one place now (createBlock); however I decided to leave it because it makes unit testing neater in my opinion.

It doesn't sound like a compelling reason to keep an experimental public API. If possible let's test the same functionality through the public method which is what we care about the most.

@noisysocks
Copy link
Member

Thanks for connecting those dots @gziolo! What @youknowriad says in #38191 (comment) is very relevant and sways me away from a copy function.

The server might also need to know about this information and declaring this as a JS function forbids that entirely. So I'm actually more in favor of the opposite direction (assign a "role" to an attribute)


I think I'm leaning towards renaming "role": "internal" to "role": "identifier" (or "foreignKey"?) as that describes, semantically, what the attribute is.

We would then have two roles:

  • "content" — indicates that the attribute the block's primary text content.
  • "identifier" (or "foreignKey"?) — indicates that the attribute is a reference to some external resource.

Then, to fix the problem identified in #34750 (comment):

  1. We could just set 'additionalProperties' to true on the server. I don't really see the need to be so strict there. This might require a Core patch, though, as I'm not sure that it's possible for a plugin to re-register a route.

  2. Or, we could have ServerSideRender perform a GET on the /block-types endpoint before making its request to /block-renderer. If an attribute isn't in the attributes object that comes back from /block-types, then it hasn't been defined on the server, and so we must not send it to /block-renderer.

Sorry to derail the conversation @stacimc—you've put your foot into some interesting problems here 😀

@stacimc
Copy link
Contributor Author

stacimc commented Feb 2, 2022

Sorry to derail the conversation @stacimc—you've put your foot into some interesting problems here 😀

@noisysocks Not derailed at all, I think it's important to get this right!

I think I'm leaning towards renaming "role": "internal" to "role": "identifier" (or "foreignKey"?) as that describes, semantically, what the attribute is.

My concern with this is that ultimately the goal of this PR is to provide a way to make an attribute that isn't copied on block duplication -- and I'm not sure it's obvious or desirable that an attribute with the "identifier" role would always behave that way. You might also have other attributes which aren't foreign keys, but which you'd still like to prevent being copied.

I don't think it's perfect but I'd still lean towards internal as a better descriptor of the behavior we'd expect from an attribute with this role. I wouldn't assume that an internal attribute has expectations about privacy/shouldn't be shared with the server, unlike some more loaded terms like private or unique.

We could just set 'additionalProperties' to true on the server. I don't really see the need to be so strict there. This might require a Core patch, though, as I'm not sure that it's possible for a plugin to re-register a route.

Or, we could have ServerSideRender perform a GET on the /block-types endpoint before making its request to /block-renderer. If an attribute isn't in the attributes object that comes back from /block-types, then it hasn't been defined on the server, and so we must not send it to /block-renderer.

Would you prefer this approach to bringing back __experimentalSanitizeBlockAttributes? This PR didn't introduce that API or include it in ServerSideRender, so it isn't a new addition. We could always remove it later if we're able to set additionalProperties on the server.

@andrewserong
Copy link
Contributor

I don't think it's perfect but I'd still lean towards internal as a better descriptor of the behavior we'd expect from an attribute with this role.

Apologies if I'm missing context, but I quite like the idea of internal, too. While a semantic description could be useful, I think in this case, it sounds like it might be more important to flag how the attribute is used, rather than what it is? One of the ideas I had in the back of my mind is that we might want to one day use a public role for attributes, where we might expose those attributes or render them inline in data-my-attribute attributes in rendered HTML so that a consumer of post content via the REST API could then grab those attributes via JS and then do something with them (e.g. possibly to regenerate / rebuild block styles or add other dynamic block behaviour). And there's a nice symmetry with public and internal.

That's clearly well outside the scope of this PR, but just thought I'd share the potential future use case in case it helps with deciding on naming!

@noisysocks
Copy link
Member

My concern with this is that ultimately the goal of this PR is to provide a way to make an attribute that isn't copied on block duplication -- and I'm not sure it's obvious or desirable that an attribute with the "identifier" role would always behave that way. You might also have other attributes which aren't foreign keys, but which you'd still like to prevent being copied.

I don't think it's perfect but I'd still lean towards internal as a better descriptor of the behavior we'd expect from an attribute with this role. I wouldn't assume that an internal attribute has expectations about privacy/shouldn't be shared with the server, unlike some more loaded terms like private or unique.

While a semantic description could be useful, I think in this case, it sounds like it might be more important to flag how the attribute is used, rather than what it is?

It seems like maybe we have two concepts: role (content, identifier, styling, etc.) which gives semantic meaning to an attribute, and access (public, internal, private) which controls where an attribute can be used (client, server, serialised HTML, etc.)

I don't really think public, internal, private, etc. are intuitive, though. Do you think it would be clearer to specify precisely what behaviours are desired? For example, "copy": false on an attribute could mean that it isn't copied.

Would you prefer this approach to bringing back __experimentalSanitizeBlockAttributes? This PR didn't introduce that API or include it in ServerSideRender, so it isn't a new addition. We could always remove it later if we're able to set additionalProperties on the server.

Yeah we can bring back __experimentalSanitizeBlockAttributes. Better that we iterate on this API in subsequent changes instead of trying to get it perfect the first try in this PR.

@dmsnell
Copy link
Member

dmsnell commented Feb 3, 2022

Even with the brief discussion here it's clear that singular roles are an over-simplification of an attributes relevance in various contexts. At least having a set of roles that an attribute participates in could go a long way to helping us build a reliable interface for attribute selection.

Granted, it's clear that these roles can be dependent upon the values of the attributes, but at least as far as describing an attribute's role goes specifying a set of roles or context in which that attribute carries weight goes pretty far.

Maybe it could resolve some of the inherent tension behind trying to figure out what exactly and attribute is by asking instead "do you do this too?" These are contexts in which the attributes are relevant or possibly capabilities each attribute has.

"attributes": {
	"myInternalAttribute": {
		"type": "string",
		"default": "my-default-value",
		"__experimentalRoles": {} // an empty set means we participate in no roles
	},
	"myNormalAttribute": {
		"type": "string", // no property set means we participate in everything
	},
	"produceId": {
		"type": "number",
		"__experimentalRoles": { "copy": false } // everything except false values
	},
	"styleThing": {
		"type": "string",
		"__experimentalRoles": { "style": true, "copy": true } // only these things
	}
},

@stacimc
Copy link
Contributor Author

stacimc commented Feb 3, 2022

It seems like maybe we have two concepts: role (content, identifier, styling, etc.) which gives semantic meaning to an attribute, and access (public, internal, private) which controls where an attribute can be used (client, server, serialised HTML, etc.)

I don't really think public, internal, private, etc. are intuitive, though. Do you think it would be clearer to specify precisely what behaviours are desired? For example, "copy": false on an attribute could mean that it isn't copied.

My initial idea for this work was along those lines -- I actually opened an earlier PR (#32604) that added a duplicable property, so you could configure: "duplicable": false. At the time we decided to go with using role instead because it made use of existing utilities and didn't require adding an extra attribute property.

I like copy a lot as an alternative for clarity -- especially if we intend to add public in the future, since public/internal feel like they would be related, when we're not really concerned with access 🤔

Even with the brief discussion here it's clear that singular roles are an over-simplification of an attributes relevance in various contexts. At least having a set of roles that an attribute participates in could go a long way to helping us build a reliable interface for attribute selection.

I think this would help considerably. It could possibly get a bit tricky if an attribute attempts to configure multiple roles that cannot be simultaneously held, but it's definitely clear that limiting an attribute to a single role is insufficient if we're going to use role to describe a mix of semantic and behavioral properties.

@noisysocks
Copy link
Member

noisysocks commented Feb 4, 2022

Even with the brief discussion here it's clear that singular roles are an over-simplification of an attributes relevance in various contexts.

Agreed. It's difficult to draw neat boxes around different behaviours, and, even when you do, naming them in a way that makes it clear to extenders what those behaviours are is very difficult.

Maybe it could resolve some of the inherent tension behind trying to figure out what exactly and attribute is by asking instead "do you do this too?"

Agreed. This feels more flexible and future proof to me. It's analogous to what we already do with block definitions with supports.

My initial idea for this work was along those lines -- I actually opened an earlier PR (#32604) that added a duplicable property, so you could configure: "duplicable": false. At the time we decided to go with using role instead because it made use of existing utilities and didn't require adding an extra attribute property.

I think role may have turned out to be a red herring 😬 What do you think about returning to your earlier approach? We can apply some of what we learned here by renaming unique to something like supports.copy and perhaps try to get some code-reuse by turning getBlockAttributesNamesByRole into something more generic, e.g.

"attributes": {
	"content": {
		"type": "string",
		"role": "content",
	},
	"productId": {
		"type": "string",
		"supports": {
			"copy": false,
		},
	},
}
const contentAttributes = filterBlockAttributes( block.name, block.attributes, { role: 'content' } );
const copyableAttributes = filterBlockAttributes( block.name, block.attributes, { supports: { copy: true } } );

@stacimc
Copy link
Contributor Author

stacimc commented Feb 8, 2022

Since it's a different approach to the API, I've opened up #38643 as an alternative to this PR following suggestions by @noisysocks 😄

That PR removes the use of __experimentalRole and instead adds a supports object:

"attributes": {
	"content": {
		"type": "string",
		"role": "content",
	},
	"productId": {
		"type": "string",
		"supports": {
			"copy": false,
		},
	},
}

@ntsekouras
Copy link
Contributor

ntsekouras commented Feb 10, 2022

Sorry for catching up late with this one and thanks for all the work here @stacimc!

It seems like maybe we have two concepts: role (content, identifier, styling, etc.) which gives semantic meaning to an attribute, and access (public, internal, private) which controls where an attribute can be used (client, server, serialised HTML, etc.)

For me the above feels what's the case here. I don't think we should try to mix two different things in one API. For me the use case we need here is to have a way to mark an attribute as unique and handle everything that this entails, like copy, duplication, etc.. That's why I had suggested to use something like role: unique|identifier or something similar.

Why do we need to create a new supports API with copy? What's the difference from a new role and does it bring more value? Do we expect many other supports? Could this lead to an overly declarative (even bloated) blocks API?

I think that many discussions here have been triggered by the widgetId case which if I understand correctly didn't declare the attribute. Is this something though that could dictate the augmentation of blocks API with semantics?

I'll catch up with the code now, but that were my first thoughts after reading all the discussions.

@dmsnell
Copy link
Member

dmsnell commented Feb 10, 2022

Could this lead to an overly declarative (even bloated) blocks API?

This is definitely a concern I had which led me to propose a function-based approach. For one, I imagine many blocks want to look at the values to determine these things; two, it seems like something where the potential space of roles and context these things play can kind of explode in complexity if defining that with a DSL.

The idea of having both seems workable to me: either you have a list of relevant roles for an attribute or you supply a function to work out the attribute sets.

Even with a DSL here we're not going to have the problem solved on the server since attributes still need to be sourced from the block's HTML. It would only be able to be relied upon in the server if coupled to a block.json file with the attributes defined, and even then we're going to have contexts (such as undo) which don't make much sense on the server.

@stacimc
Copy link
Contributor Author

stacimc commented Feb 10, 2022

Thanks @ntsekouras, @dmsnell.

I think that many discussions here have been triggered by the widgetId case which if I understand correctly didn't declare the attribute.

The widgetId has a similar problem to my original productId use case -- it's an attribute that shouldn't be copied on block duplication -- and was brought in so we would have an established use case of the new API.

That's why I had suggested to use something like role: unique|identifier or something similar.

Why do we need to create a new supports API with copy? What's the difference from a new role and does it bring more value? Do we expect many other supports? Could this lead to an overly declarative (even bloated) blocks API?

The problem I've been having is finding a semantic 'role' that strictly describes the behavior (attribute isn't copied on block dupe/copy).

  • unique is a loaded term that suggests actual uniqueness across instances will be enforced
  • identifier is a good semantic descriptor for the productId and widgetId examples but might not imply the behavior we're implementing (should we assume all identifiers shouldn't be copied when you duplicate a block? What if my attribute isn't an identifier, but I still don't want it duplicated?)
  • internal works pretty well but might imply something about access, especially alongside roles like public/private
  • copy describes the behavior very explicitly, but at this point are we overloading the meaning of a role?

We can support multiple __experimentalRoles for a single attribute, but if each role is permitted to define its own set of behaviors (copy/undo/etc) for an attribute, how do we handle the interaction of conflicting roles? Discrete supports might be easier to reason about in that case.

@dmsnell
Copy link
Member

dmsnell commented Feb 10, 2022

Discrete supports might be easier to reason about in that case.

for what it's worth I find supports a tighter fit to most of the discussion here. in my own thinking I consider "relevance" to "contexts" as the factor. This is why I have trouble understanding fixed static roles for each attribute.

I wouldn't expect roles to have its own supports, but either could work.

identifier

This reads to me more like attributes that identify the block, which I think would normally imply that they copy when duplicated instead of the inverse. Could these be other things that aren't ids that serve the same purpose which could help us figure out a good name for them?

  • a page in a book
  • serial number
  • part number
  • day of year

Maybe these are some kind of reference or xref or cross-ref? They do uniquely identify a block but they are some kind of selection inside other block settings?

The concept of identity here is vague.

@ntsekouras
Copy link
Contributor

The idea of having both seems workable to me: either you have a list of relevant roles for an attribute or you supply a function to work out the attribute sets.

This is not flexible as functions aren't serialisable and we need to have access to things like that server side and sometimes we need to register things server side. The same problem occurred for isActive function for block variations where we needed to define dynamically(server side) variations. We ended up having js filters for that function(example here).

The problem I've been having is finding a semantic 'role' that strictly describes the behavior (attribute isn't copied on block dupe/copy).
The concept of identity here is vague.

I can see your points and maybe it's okay to try with supports but make it experimental or unstable for start and only for copy.

For the record the use case that made me look into this problem more is the Query Loop queryId. It needs to be unique because it's used for the associated pagination to work properly.

So I guess I'm going to check your other PR 😄

@Tropicalista
Copy link

Will this take in account also the removal of unique attributes when we export content?

@stacimc
Copy link
Contributor Author

stacimc commented Feb 14, 2022

Will this take in account also the removal of unique attributes when we export content?

I'm leaving this open for the moment as discussion is ongoing, but it's likely this PR will be closed in favor of #38643. That PR removes the use of internal/unique and instead provides a supports object, with support for a copy key:

"attributes": {
	"productId": {
		"type": "string",
		"__experimentalSupports": {
			"copy": false,
		},
	},
}

So to answer your question @Tropicalista I think that approach would only handle removing those attributes on block copy/duplication -- but an export support could theoretically be added as another key to __experimentalSupports. In fact I think that's a point in favor of that approach.

@youknowriad
Copy link
Contributor

but an export support could theoretically be added as another key to __experimentalSupports. In fact I think that's a point in favor of that approach.

I actually think it's a point in favor of the current PR because with the "supports", we'd have to think and add a flag for everyone case where the current PR adds meaning/semantics and not be dependent on specific use-cases. We can make informed decisions for each use-case depending on the role of the attribute.

@noisysocks
Copy link
Member

@youknowriad: What "role" do you think something like productId or widgetId would have? It's been difficult to define, see #34750 (comment).

@youknowriad
Copy link
Contributor

@noisysocks I see the point, I think it's not clear when you say this blocks is identified by this productId whether this block should be unique or not. So I think identifier doesn't mean unique and unique doesn't seem to be a "role" but more its own property of the attribute: unique: true that could imply a number of restrictions on the editor:

  • What happens when you copy/export...
  • What happens if the uniqueness is not guaranteed (say I modified the HTML of the post), does it show a message?...

To clarify, I'm not totally against supports either, just sharing my thoughts to help you all make the right decisions.

@stacimc
Copy link
Contributor Author

stacimc commented Feb 17, 2022

doesn't seem to be a "role" but more its own property of the attribute: unique: true that could imply a number of restrictions on the editor

@youknowriad The earliest implementation of this work (#32604) was actually exactly that!

{
    productId: {
        type: 'number',
        unique: true,
    }
}

The concern was that unique is quite a loaded term, and implies much more than we need (for the productId example, it's even possible you might want to manually create multiple blocks linking the same underlying product). Some alternative name suggestions explored were internal and duplicable. The former is a bit unclear, but maybe duplicable still has promise?

@youknowriad
Copy link
Contributor

for the productId example, it's even possible you might want to manually create multiple blocks linking the same underlying product

Shouldn't you be able to duplicate and keep the same productId if that's possible?

@ntsekouras
Copy link
Contributor

for the productId example, it's even possible you might want to manually create multiple blocks linking the same underlying product

I agree with Riad. I think in the above use case we don't need any semantic marking. It's just the default behavior for block attributes. Am I missing something?

@stacimc
Copy link
Contributor Author

stacimc commented Feb 21, 2022

Shouldn't you be able to duplicate and keep the same productId if that's possible?

I was just pointing out why the name unique is misleading/undesired for some use cases. Ie: I want a way to configure an attribute such that it is not automatically copied when I copy or duplicate the block, but I have no expectation that uniqueness will be enforced otherwise. To continue using my product example: maybe in the future I want to allow users to update the productId.

Other suggestions were internal, duplicable, copy.

{
    productId: {
        type: 'number',
        duplicable: false,
    }
}

@noisysocks
Copy link
Member

Yes, to me, unique implies more than what will actually happen which is no good.

I still like supports.copy as it describes minimally the behaviour that setting it will cause. This makes it easier to compose different behaviours together. For example, one day we might have a whole suite of customisable attribute behaviours.

{
    attributeName: {
        type: 'number',
	supports: {
	        copy: true, // whether attribute remains when block is copied or duplicated
		serialize: true, // whether attribute remains when block is serialised to HTML
		restApi: true, // whether attribute is shown in the REST API
		undo: true, // whether changes to the attribute are undoable / redoable
	},
    }
}

At any rate, I feel the discussion is going in circles a bit and that we ought to lean on __experimental instead of our prescience. I'm happy with #38643 😀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Block API API that allows to express the block paradigm. Needs Technical Feedback Needs testing from a developer perspective. [Type] New API New API to be used by plugin developers or package users.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Block API: Allow for internal, non-duplicable block attributes