-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Data: Persist user preferences via user meta #19177
Conversation
$wp_scripts->registered['wp-data']->extra['after'] = array(); | ||
wp_add_inline_script( 'wp-data', $persistence_script ); | ||
} | ||
add_action( 'enqueue_block_editor_assets', 'gutenberg_user_settings_data_persistence_inline_script', 20 ); |
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.
Note: The priority here shouldn't be necessary, but the behavior of the function is such that it replaces any inline scripts for the wp-data
handle. This conflicts with Gutenberg's own gutenberg_enqueue_block_editor_assets
function, which does the same and was intended to serve as a temporary implementation of the preferences migration. Since this was migrated to core as part of Trac#46429, it should be removed from Gutenberg, at which point the priority here would no longer be necessary.
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.
Since this was migrated to core as part of Trac#46429, it should be removed from Gutenberg, at which point the priority here would no longer be necessary.
See #19178 . If the other is merged first, this can be rebased to remove the priority parameter.
That is what I was thinking as well.
I think it's fine for them to be able to view those preferences.
As in settings specific to a single site in a multisite? |
Yes, there was a bit of discussion in #15105 about how certain preferences could make sense to be associated for a user in the context of a specific site: Frequently used blocks, "enabled" blocks; especially considering that two sites in a network may have different blocks installed. Right now we don't have any way to express this. It sort of "just worked" previously because the storage would be unique per domain. I actually expect it could be an issue already for subdirectory-based multi-sites (where I assume the browser storage could still be shared across sites). |
The key for these settings should not be
|
I could imagine there to be some use-cases for storing user preferences global to all sites in a multi-site. Per previous discussion, it's acknowledged there would need to be some way to express this distinction which doesn't currently exist. On reflection though, I would agree that, at least based on current use of preferences, storing the value as distinct per site would probably be the safer default for now. As to the specific implementation, there might be a challenge in making this database prefix available to the browser for use in calling to the REST API. I suppose that could be part of the "bootstrapping" logic for how this inline script is generated (i.e. generate the meta key server-side, and inject into the inline script). |
As a maintainer of multisite in core, I would say that per site is a much safer options. There are no other options in WordPress that a global like this. There maybe good reason you want different settings per site, you never know. |
One possibility to handle the bootstrapping, we can utilize the global $wpdb;
register_meta( 'user', $wpdb->prefix . 'data_persistence', [
'show_in_rest' => [
'name' => 'data_persistence',
'type' => 'object',
'schema' => [
'type' => 'object',
'additionalProperties' => true,
]
]
] ); That way the preferences for the current site will always be available as |
Thanks for the tip, @TimothyBJacobs . That seems to work pretty well in cb0bb4b. One open question here is whether to treat this value as an object, or as a (JSON) string. I noticed in your example that you structured it as an object. As least as it concerns the usage in the client-side data module, it's currently implementing as conforming to the Web Storage API interface, where the value is always stored as a string. So at least as to the practical extent of how this value is used, it's expected as a JSON string. But, since that string represents an object, I could grant an argument that its natural form is that of an object, and we could define its REST schema accordingly. In the end, it's just a matter of doing some JSON parse/stringify dance for the sake of supporting it as an object over REST transmission. |
Awesome!
I would personally prefer us to send the value as an actual object instead of a JSON string. That way when the data is transferred it is easily readable, and can be validated natively by the REST API. I also think it is far more common in core to store non scalar data as serialized PHP vs encoded as a JSON string. That being said, I think there are many people who would like to move to storing that info in JSON. Though, even if we were to move to start storing those non-scalars as JSON, we wouldn't need to send the values as JSON strings. So I think my preference would be to send an object, but I'm not strongly opposed if you think it'd be best to conform literally to the Storage API. |
It seems like treating it as an object could open some options for inspecting or transforming that value more easily server-side. I think the main blocker for me was in whether it's premature to consider this, when the main use-case we're targeting (persistence for the client-side data module) explicitly does not expect it to be an object. I also considered that it's ultimately persisted server-side as a string anyways (in the PHP serialization format, rather than as JSON). As you mention, this is more of an implementation detail, and not really of much consequence one way or the other. I think I'll change it toward the object schema. It should be pretty trivial to transform it back into the appropriate format client-side. Edit: See e4f2f1c . |
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.
Posts a couple of code tweaks.
'name' => 'data_persistence', | ||
'type' => 'object', | ||
'schema' => array( | ||
'type' => 'object', |
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.
Should a default value be useful here?
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.
Should a default value be useful here?
Would you expect it to have an impact? Or what would you hope should happen by providing a default?
My concern with this might be: We don't want to set a meta value unless one is actually provided with the request.
Trying to follow how this logic flows in the REST API implementation, it doesn't appear it should have an impact one way or the other (at least currently), since it seems like the schema defined here is referenced only after considering that a value was provided.
Interestingly, the failing build seems expected considering that the preferences are now more sticky than they were previously, so certain tests which manipulate preferences might have an unexpected bleed-over into other tests. Previously we relied on clearing Rough ideas:
|
@aduth Why not just delete / set to empty object the meta on every test? The existing user api should be able to do this. |
Yeah, this is like the second point in my previous comment. I think we'd need/want a test-specific plugin for this (since the tests themselves are run in Node and the browser), but it should be simple enough to do. We have plenty of precedent. |
Tangentially related: https://core.trac.wordpress.org/ticket/33542 Instead of user_meta, you will likely want to move to using the user_option() functions. User Options are used by per-site preferences that persistent across all page loads. Things like admin-color-scheme use it, allowing for per-user per-site option of what color the dashboard area should be. They use cookies (ugh) and are only used by the currently logged in user, but their purpose is to provide functional local storage.
Ultimately site owners want to have smart defaults and rational overrides. By default, it makes sense for a fresh WordPress installation to show the “Welcome to blocks” pop up, but 5 years from now Time Magazine may want to hide this permanently for all of their experienced authors and editors, and this is what a global Editor setting to act as the default for all users is for. IMO, solving this for users alone addresses an immediate need (for a problem that shouldn’t exist anyways) which is great, so long as the follow-up work continues up the chain to allow these user-preferences to also have smart defaults, and not simply be hard-coded in place as is. |
Did you link the correct ticket? I'm not sure how this one relates.
There was also a similar suggestion to this effect at #15105 (comment) , with follow-on evaluation at #15105 (comment) in discussing REST API availability and how the approach here should be effectively equivalent based on the underlying implementation of
Is this in reference to the |
No, I didn’t. Too many tickets open. Fixed my original comment.
Oh, totally non-viable. This is a shortcoming of the user settings API, which surely should be fixable in WordPress itself. Cookies for storage was an idea that came before browsers offered any alternative. Now that it’s been several years of having more than cookies, WordPress has some obligation to update its implementation. |
@JJJ I like what you're proposing at Trac#33542. I think for many of the "preferences" used by Gutenberg today, it does make sense that they can be user-specific, but the idea of establishing some hierarchy where the site might want to assign its own defaults is similarly sensible for most. In fact, in thinking about this, I'd probably consider my own Disable NUX plugin as something of a roundabout implementation of that very idea (allowing site-level override of the NUX preference). I'm trying to think what might be the best way to go about maximizing future-compatibility, while still allowing the more immediate effort to progress. One thing I might worry about with the current implementation is that it treats "preferences" as a single object, vs. individual keys. While this might be convenient as a way to limit what we're providing to the browser and it reflects how the data is actually managed in client-side state, I'm wondering if there might be a better way to go about expressing that for how we store the values server-side and make them available more generally. Some ideas:
A few additional challenges to consider:
For reference, here is a more-or-less complete set of the current active preferences used:
|
They'll be queried at once. The Gutenberg plugin could use |
I've been thinking on this over the past few days, and another alternative I can consider for an idea of individual preferences could be something more like: Rather than establish a convention for how all preferences map between a server-side meta/option and client-side state, selectively choose specific client state values to "upgrade" as being persisted to a specific user meta. The way I see this working:
Pseudo-code: $data_persistence_map = apply_filters( 'data_persistence_map', array(
'show_welcome_guide' => array( 'core/edit-post', 'features', 'showWelcomeGuide' ),
) );
$persisted_data = array();
foreach ( $data_persistence_map as $key => $data_path ) {
if ( ! wp_has_user_preference( get_current_user_id(), $key ) {
continue;
}
$value = wp_get_user_preference( get_current_user_id(), $key );
$working_path =& $persisted_data;
while ( ! empty( $data_path ) ) {
$data_key = array_shift( $data_path );
$working_path[ $data_key ] = empty( $data_path ) ? $value : array();
$working_path =& $working_path[ $data_key ];
}
}
printf(
'var persisted = _.merge( localStorage.getItem( "persistence" ), %s );',
wp_json_encode( $persisted_data )
);
// var persisted = _.merge( localStorage.getItem( "persistence" ), {"core\/edit-post":{"features":{"showWelcomeGuide":false}}} ); I think there are both pros and cons to this: Pros:
Cons:
There are some challenges as well:
|
For what is worth, saving as one object in one user meta, makes sense to make. It keep it flexible to add new properties to the object in the future and doesn't require multiple rows to be loaded on every authenicated request. If we have say 15 settings and say 30 settings via a plugin that has it's own settings, that could result in 45 rows in database per site. On multisite, that is a a lot of rows. |
Please do not save these as 1 unique meta key. Saving them all in one user meta key completely defeats the purpose of using a persistent remote data source (like a database table) capable of having multiple keys and values. Databases are robust and sophisticated and fast. The REST API implementation for WordPress has a meta endpoint for Users. If it is not sufficient for Gutenberg’s needs, then it should be updated, and Gutenberg should not compromise its brand new approach for the worse. The number of rows in the usermeta database table will hardly matter when querying for them by user ID or meta key, and not doing a meta value comparison. |
@gziolo I think that path forward makes a lot of sense. |
d5f01c9
to
50f3f7e
Compare
Persistent user options are becoming more and more crucial for block themes (think block-styles API as an example), so it would be nice if we could revisit this concept. I tried implementing something similar but ended up writing a pretty similar implementation before finding this one, so instead of submitting a new PR I rebased this one and resolved conflicts with the current trunk. @aduth do you think we can use this implementation and continue working on it, addressing the feedback from comments above? Or is it no longer relevant and we should go with a different solution? |
Co-Authored-By: Jonny Harris <[email protected]>
Co-Authored-By: Jonny Harris <[email protected]>
Co-Authored-By: Jonny Harris <[email protected]>
50f3f7e
to
f0ca212
Compare
1c93a89
to
f0ca212
Compare
Co-authored-by: Timothy Jacobs <[email protected]>
I think this is a bit too outdated now with the recent changes to preferences (tracked in #31965). I'm actively working on something that builds on the idea of using user meta from this PR, but plugs into the new preferences package instead of the data store #39795. Closing this PR, thanks for all the hard work on it. |
Closes #15105
This pull request seeks to implement a custom data persistence storage implementation for the purpose of storing preferences via user metadata. This uses existing REST API functionality for updating metadata via a registered meta key. The storage implementation bootstraps the stored metadata value for use in the initialization of the store. When a preference update occurs, it is saved to the REST API using the users endpoint for the current user.
Implementation Notes:
prepare_value
property ofshow_in_rest
, which still isn't provided with the specific user ID of the meta value being served (in order to distinguish whether it should be served based on matching the current user). I may be over-thinking this, and it may be perfectly fine to assume that if a user has permissions to request details of other users on the site, they should also be permitted to view persisted preferences details of that user.Follow-up Tasks:
At the earliest opportunity, this code should be migrated into the core codebase.
The inline script should take the place of this segment of code:
https://github.com/WordPress/wordpress-develop/blob/8a88cfa/src/wp-includes/script-loader.php#L679-L693
As far as I can find, there is no prior art for explicit meta registration in core. Most meta is not referenced without being registered. This meta value must be registered in order for it to be accessible via the REST API. It's unclear to me where this should reside in the core codebase (
rest-api.php
? something akin topost.php
'screate_initial_post_types
, but for users and their meta?).Testing Instructions:
localStorage
by enteringlocalStorage.clear()
in your browser console (or follow these instructions to manage Application Storage in Chrome)