Skip to content

Experiment: Connect block attributes with custom fields via UI #176

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

Open
wants to merge 29 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
464fa1f
First commit
cbravobernal Jun 12, 2025
66c9f18
Set binding
cbravobernal Jun 12, 2025
3606d36
fix margin
cbravobernal Jun 12, 2025
07ae7d7
Include it as beta feature
cbravobernal Jun 12, 2025
8b31839
Add clear all fields button
cbravobernal Jun 15, 2025
e8d2d18
Fix format date
cbravobernal Jun 16, 2025
dc2f05a
Add edit button, still not working
cbravobernal Jun 17, 2025
1cea2b0
Fix option name
ockham Jun 24, 2025
8d7c6b4
Make work with all attributes
cbravobernal Jun 25, 2025
6c47ea5
Pending refactor, allow all attributes connection for images
cbravobernal Jun 25, 2025
521b36d
Add a not working change, used ai a lot here
cbravobernal Jun 26, 2025
6cd6504
Remove edit stuff
cbravobernal Jun 26, 2025
d9a43fd
Remove now-obsolete modal related code
ockham Jun 26, 2025
24db51e
Use link/unlink button
ockham Jun 26, 2025
69d65aa
Use toolspanel
cbravobernal Jun 26, 2025
8493b04
Use better link button
cbravobernal Jun 27, 2025
ffa345b
Make some cleaning
cbravobernal Jun 27, 2025
7fa68ab
update lock son
cbravobernal Jul 7, 2025
f4473db
Simplify the UI to connect images
cbravobernal Jul 8, 2025
8ab8f9c
Remove clog
cbravobernal Jul 8, 2025
8d17f95
Address copilot review
cbravobernal Jul 8, 2025
6dd8c20
Remove not used stuff
cbravobernal Jul 9, 2025
c0b9367
Address suggestion
cbravobernal Jul 9, 2025
afe14e4
AI powered refactor
cbravobernal Jul 9, 2025
4ec1ada
Add range
cbravobernal Jul 9, 2025
89d69ff
Fix not allowed bindings
cbravobernal Jul 9, 2025
2c96d62
Restructure processFieldBinding
ockham Jul 9, 2025
60976f6
Remove non used bindings
cbravobernal Jul 9, 2025
c57b271
Indentation
ockham Jul 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
407 changes: 407 additions & 0 deletions assets/src/js/bindings/block-editor.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/src/js/bindings/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import './sources.js';
import './block-editor.js';
122 changes: 90 additions & 32 deletions assets/src/js/bindings/sources.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,135 @@
/**
* WordPress dependencies.
*/
import { __ } from '@wordpress/i18n';
import { registerBlockBindingsSource } from '@wordpress/blocks';
import { store as coreDataStore } from '@wordpress/core-data';
import { dateI18n } from '@wordpress/date';

/**
* Get the value of a specific field from the ACF fields.
* Get the value of a specific field from the SCF fields.
*
* @param {Object} fields The ACF fields object.
* @param {Object} fields The SCF fields object.
* @param {string} fieldName The name of the field to retrieve.
* @returns {string} The value of the specified field, or undefined if not found.
* @returns {*} The value of the specified field, or undefined if not found.
*/
const getFieldValue = ( fields, fieldName ) => fields?.acf?.[ fieldName ];

/**
* Create a lookup map of field names to field objects from field groups.
*
* @param {Object} fields The fields object containing scf_field_groups.
* @returns {Object} A map of field names to field objects.
*/
const createFieldsLookupMap = ( fields ) => {
if ( ! fields?.scf_field_groups ) {
return {};
}

return Object.fromEntries(
fields.scf_field_groups
.flatMap( ( group ) => group.fields || [] )
.map( ( field ) => [ field.name, field ] )
);
};

/**
* Resolve image attribute values from an image object.
*
* @param {Object} imageObj The image object from WordPress media.
* @param {string} attribute The attribute to resolve.
* @returns {string} The resolved attribute value.
*/
const resolveImageAttribute = ( imageObj, attribute ) => {
if ( ! imageObj ) return '';
switch ( attribute ) {
case 'url':
case 'content':
return imageObj.source_url;
case 'alt':
return imageObj.alt_text || '';
case 'title':
return imageObj.title?.rendered || '';
case 'id':
return imageObj.id;
default:
return '';
}
};

/**
* Process a single field binding and return its resolved value.
*
* @param {string} attribute The attribute being bound.
* @param {Object} args The binding arguments.
* @param {Object} fields The post fields object.
* @param {Object} fieldsLookupMap The fields lookup map.
* @param {Function} getMedia Function to get media object by ID.
* @returns {string} The resolved field value.
*/
const processFieldBinding = (
attribute,
args,
fields,
fieldsLookupMap,
getMedia
) => {
const fieldName = args?.key;
const fieldValue = getFieldValue( fields, fieldName );
const fieldConfig = fieldsLookupMap[ fieldName ];
const fieldType = fieldConfig?.type;

switch ( fieldType ) {
case 'number':
case 'range':
if ( attribute === 'content' ) {
return fieldValue.toString() || '';
}
break;
case 'date_picker':
if ( ! fieldValue ) {
return '';
}
return dateI18n( fieldConfig?.display_format, fieldValue ) || '';
case 'image':
// fieldValue is a (numeric) image ID.
const imageObj = getMedia( fieldValue );
return resolveImageAttribute( imageObj, attribute );
case 'select':
case 'text':
case 'textarea':
case 'url':
default:
return fieldValue || '';
}
};

registerBlockBindingsSource( {
name: 'acf/field',
label: 'SCF Fields',
getValues( { context, bindings, select } ) {
const { getEditedEntityRecord, getMedia } = select( coreDataStore );
let fields =

const fields =
context?.postType && context?.postId
? getEditedEntityRecord(
'postType',
context.postType,
context.postId
)
: undefined;

const fieldsLookupMap = createFieldsLookupMap( fields );

const result = {};

Object.entries( bindings ).forEach(
( [ attribute, { args } = {} ] ) => {
const fieldName = args?.key;

const fieldValue = getFieldValue( fields, fieldName );
if ( typeof fieldValue === 'object' && fieldValue !== null ) {
let value = '';

if ( fieldValue[ attribute ] ) {
value = fieldValue[ attribute ];
} else if ( attribute === 'content' && fieldValue.url ) {
value = fieldValue.url;
}

result[ attribute ] = value;
} else if ( typeof fieldValue === 'number' ) {
if ( attribute === 'content' ) {
result[ attribute ] = fieldValue.toString() || '';
} else {
const imageObj = getMedia( fieldValue );
result[ attribute ] = resolveImageAttribute(
imageObj,
attribute
);
}
} else {
result[ attribute ] = fieldValue || '';
}
result[ attribute ] = processFieldBinding(
attribute,
args,
fields,
fieldsLookupMap,
getMedia
);
}
);

Expand Down
3 changes: 1 addition & 2 deletions assets/src/sass/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -313,5 +313,4 @@ p.submit .acf-spinner {
margin: 0;
}
}
}

}
50 changes: 25 additions & 25 deletions includes/admin/beta-features.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ class SCF_Admin_Beta_Features {
* @return void
*/
public function __construct() {
// Temporarily disabled - will be enabled when beta feature is ready
// add_action( 'admin_menu', array( $this, 'admin_menu' ), 20 );
add_action( 'admin_menu', array( $this, 'admin_menu' ), 20 );
}

/**
Expand Down Expand Up @@ -78,26 +77,6 @@ public function get_beta_features() {
return $this->beta_features;
}

/**
* Localizes the beta features data.
*
* @since SCF 6.5.0
*
* @return void
*/
public function localize_beta_features() {
$beta_features = array();
foreach ( $this->get_beta_features() as $name => $beta_feature ) {
$beta_features[ $name ] = $beta_feature->is_enabled();
}

acf_localize_data(
array(
'betaFeatures' => $beta_features,
)
);
}

/**
* This function will add the SCF beta features menu item to the WP admin
*
Expand All @@ -115,7 +94,7 @@ public function admin_menu() {
$page = add_submenu_page( 'edit.php?post_type=acf-field-group', __( 'Beta Features', 'secure-custom-fields' ), __( 'Beta Features', 'secure-custom-fields' ), acf_get_setting( 'capability' ), 'scf-beta-features', array( $this, 'html' ) );

add_action( 'load-' . $page, array( $this, 'load' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'localize_beta_features' ), 20 );
add_action( 'admin_enqueue_scripts', array( $this, 'add_beta_features_script' ), 20 );
}

/**
Expand Down Expand Up @@ -155,7 +134,7 @@ public function admin_body_class( $classes ) {
*/
private function include_beta_features() {
acf_include( 'includes/admin/beta-features/class-scf-beta-feature.php' );
acf_include( 'includes/admin/beta-features/class-scf-beta-feature-editor-sidebar.php' );
acf_include( 'includes/admin/beta-features/class-scf-beta-feature-connect-fields.php' );

add_action( 'scf/include_admin_beta_features', array( $this, 'register_beta_features' ) );

Expand All @@ -170,7 +149,7 @@ private function include_beta_features() {
* @return void
*/
public function register_beta_features() {
scf_register_admin_beta_feature( 'SCF_Admin_Beta_Feature_Editor_Sidebar' );
scf_register_admin_beta_feature( 'SCF_Admin_Beta_Feature_Connect_Fields' );
}

/**
Expand Down Expand Up @@ -255,6 +234,27 @@ public function metabox_html( $post, $metabox ) {
acf_nonce_input( $beta_feature->name );
echo '</form>';
}

/**
* Adds the editor sidebar script to the page.
*
* @since SCF 6.5.0
*
* @return void
*/
public function add_beta_features_script() {
// Check if the connected fields feature is enabled

$script = 'window.scf = window.scf || {};
window.scf.betaFeatures = window.scf.betaFeatures || {};';
foreach ( $this->get_beta_features() as $name => $beta_feature ) {
if ( $beta_feature->is_enabled() ) {
$script .= sprintf( 'window.scf.betaFeatures.%s = true;', esc_js( $name ) );
}
}

wp_add_inline_script( 'wp-block-editor', $script, 'before' );
}
}

// initialize
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* Connect Fields Feature
*
* This beta feature allows moving field group elements to the editor sidebar.
*
* @package Secure Custom Fields
* @since SCF 6.5.0
*/

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}

if ( ! class_exists( 'SCF_Admin_Beta_Feature_Connect_Fields' ) ) :
/**
* Class SCF_Admin_Beta_Feature_Connect_Fields
*
* Implements a beta feature that connects fields to compatible block attributes.
*
* @package Secure Custom Fields
* @since SCF 6.5.0
*/
class SCF_Admin_Beta_Feature_Connect_Fields extends SCF_Admin_Beta_Feature {

/**
* Initialize the beta feature.
*
* @return void
*/
protected function initialize() {
$this->name = 'connect_fields';
$this->title = __( 'Connect Fields', 'secure-custom-fields' );
$this->description = __( 'Connects field to binding compatible blocks.', 'secure-custom-fields' );
}
}
endif;
22 changes: 16 additions & 6 deletions includes/rest-api/class-acf-rest-types-endpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ public function __construct() {
* @return void
*/
public function register_extra_fields() {
if ( ! (bool) get_option( 'scf_beta_feature_editor_sidebar_enabled', false ) ) {
if ( ! (bool) get_option( 'scf_beta_feature_connect_fields_enabled', false ) ) {
return;
}
$post_types = get_post_types( array( 'show_in_rest' => true ) );
register_rest_field(
'type',
$post_types,
'scf_field_groups',
array(
'get_callback' => array( $this, 'get_scf_fields' ),
Expand All @@ -53,13 +54,16 @@ public function register_extra_fields() {
/**
* Get SCF fields for a post type.
*
* @since SCF 6.5.0
* @since 6.5.0
*
* @param array $post_type_object The post type object.
* @return array Array of field data.
*/
public function get_scf_fields( $post_type_object ) {
$post_type = $post_type_object['slug'];
if ( ! isset( $post_type_object['id'] ) || ! isset( $post_type_object['type'] ) ) {
return array();
}
$post_type = $post_type_object['type'];
$field_groups = acf_get_field_groups( array( 'post_type' => $post_type ) );
$field_groups_data = array();

Expand All @@ -68,9 +72,15 @@ public function get_scf_fields( $post_type_object ) {
$group_fields = array();

foreach ( $fields as $field ) {
if ( isset( $field['allow_in_bindings'] ) && ! $field['allow_in_bindings'] ) {
// Skip fields that are not allowed in bindings.
continue;
}
$group_fields[] = array(
'label' => $field['label'],
'type' => $field['type'],
'label' => $field['label'],
'type' => $field['type'],
'name' => $field['name'],
'display_format' => isset( $field['display_format'] ) ? $field['display_format'] : '',
);
}

Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"watch": "webpack --watch"
},
"dependencies": {
"@wordpress/icons": "^10.26.0",
"md5": "^2.3.0"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions secure-custom-fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ function scf_plugin_uninstall() {
// List of known beta features.
$beta_features = array(
'editor_sidebar',
'connect_fields',
);

foreach ( $beta_features as $beta_feature ) {
Expand Down
Loading