PluginProbe
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor / 4.0.3
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor v4.0.3
4.0.3 4.0.2 4.0.1 4.0.0 3.16.6 3.16.5 3.16.4 3.16.3 3.16.2 3.16.1 3.16.0 3.15.9 3.9.9 3.9.5 3.9.6 3.9.7 3.9.8 1.1.7 1.1.8 1.1.9 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 341 releases
profile-builder / form-builder / blocks / src / blocks / field-repeater / edit.js

edit.js in User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor 4.0.3, at form-builder/blocks/src/blocks/field-repeater/edit.js

228 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { __, sprintf } from '@wordpress/i18n';
2 import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
3 import {
4 PanelBody,
5 TextControl,
6 TextareaControl,
7 CheckboxControl,
8 BaseControl,
9 } from '@wordpress/components';
10 import { useMemo } from '@wordpress/element';
11 import { useAllocateFieldId } from '../../components/useAllocateFieldId';
12 import { FieldSettingsFill } from '../../components/FieldSettingsSlotFill';
13 import CrossCuttingFieldPanels from '../../components/CrossCuttingFieldPanels';
14 import AddFieldButton from '../../components/AddFieldButton';
15
16 const LIMIT_MESSAGE_DEFAULT = 'The maximum number of fields has been reached.';
17
18 /**
19 * Decode the legacy rpf-role-limit JSON shape:
20 * {"rules":[{"role":"subscriber","value":"5"}, …]}
21 *
22 * Returns a flat { role => stringValue } map for editor UI consumption.
23 * Tolerant of missing / malformed input — returns an empty object.
24 */
25 function decodeRoleLimitRules( raw ) {
26 if ( ! raw ) return {};
27 try {
28 const parsed = JSON.parse( raw );
29 if ( ! parsed || ! Array.isArray( parsed.rules ) ) return {};
30 return parsed.rules.reduce( ( acc, rule ) => {
31 if ( rule && typeof rule.role === 'string' ) {
32 acc[ rule.role ] = String( rule.value ?? '' );
33 }
34 return acc;
35 }, {} );
36 } catch ( _ ) {
37 return {};
38 }
39 }
40
41 /**
42 * Re-encode a { role => stringValue } map into the legacy JSON shape.
43 * Returns an empty string when no rules survive — matches the legacy
44 * "no rules set" sentinel that the front-end short-circuits on.
45 */
46 function encodeRoleLimitRules( ruleMap ) {
47 const rules = Object.entries( ruleMap )
48 .filter( ( [ , value ] ) => value !== '' && value !== undefined )
49 .map( ( [ role, value ] ) => ( { role, value: String( value ) } ) );
50 return rules.length ? JSON.stringify( { rules } ) : '';
51 }
52
53 export default function Edit( { attributes, setAttributes, clientId, isSelected = false } ) {
54 const {
55 id,
56 'field-title': title,
57 description,
58 required,
59 'rpf-enable-limit': enableLimit,
60 'rpf-limit': limit,
61 'rpf-limit-reached-message': limitReachedMessage,
62 'rpf-role-limit': roleLimitJson,
63 } = attributes;
64
65 // Same selection gate as BaseFieldEdit — only the selected block's Fill
66 // populates the Field Settings PluginSidebar slot. Gutenberg passes
67 // isSelected for real blocks; VirtualFieldEdit (Existing Fields panel)
68 // passes it explicitly for not-in-form previews.
69 const isBlockSelected = !! isSelected;
70
71 // Allowed sub-field blocks come from the server bridge — registry minus
72 // wppb_rpf_manage_fields_get_excluded_fields(). Fallback to a single safe
73 // type if the bridge isn't loaded (e.g. add-on disabled mid-session).
74 const repeaterBridge = ( window.wppbFb && window.wppbFb.repeater ) || {};
75 const ALLOWED_BLOCKS = ( Array.isArray( repeaterBridge.allowedBlocks ) && repeaterBridge.allowedBlocks.length )
76 ? repeaterBridge.allowedBlocks
77 : [ 'profile-builder/field-input' ];
78 const limitEnabled = enableLimit === 'yes';
79
80 // ID handshake — see BaseFieldEdit for rationale.
81 useAllocateFieldId( id, setAttributes );
82
83 // Role list from the form-builder bridge (window.wppbFb.formSettings.roles).
84 // Strip the "Default Role" sentinel — it's a registration-form Set-Role
85 // option, not a real role, and per-role limits don't apply to it.
86 const allRoles = useMemo( () => {
87 const bridge = ( window.wppbFb && window.wppbFb.formSettings && window.wppbFb.formSettings.roles ) || [];
88 return bridge.filter( ( opt ) => opt.value !== 'default role' );
89 }, [] );
90
91 const roleLimitMap = useMemo( () => decodeRoleLimitRules( roleLimitJson ), [ roleLimitJson ] );
92
93 const setRoleLimit = ( roleSlug, value ) => {
94 const next = { ...roleLimitMap, [ roleSlug ]: value };
95 setAttributes( { 'rpf-role-limit': encodeRoleLimitRules( next ) } );
96 };
97
98 return (
99 <div { ...useBlockProps( { className: 'wppb-fb-field-block wppb-fb-repeater-block' } ) }>
100 { isBlockSelected && (
101 <FieldSettingsFill>
102 <PanelBody title={ __( 'Repeater Settings', 'profile-builder' ) }>
103 <TextControl
104 label={ __( 'Field Title', 'profile-builder' ) }
105 value={ title }
106 onChange={ ( val ) => setAttributes( { 'field-title': val } ) }
107 />
108 { /* No `rpf-button` control by design.
109 The classic `.row-rpf-button` row is not a stored label —
110 it's a WCK custom type that renders an "Edit field group"
111 button opening the sub-field iframe
112 (add-ons/repeater-field/admin/repeater-manage-fields.php:280-285),
113 and nothing on the front end ever reads the key. The
114 InnerBlocks canvas below plus "Add sub-field" IS that
115 affordance here, so the attribute was dropped from
116 block.json rather than given a control that would store a
117 value no renderer consumes. */ }
118 </PanelBody>
119
120 <PanelBody title={ __( 'Limit', 'profile-builder' ) } initialOpen={ false }>
121 <CheckboxControl
122 label={ __( 'Enable limit', 'profile-builder' ) }
123 checked={ limitEnabled }
124 onChange={ ( checked ) => setAttributes( { 'rpf-enable-limit': checked ? 'yes' : '' } ) }
125 help={ __( 'Enable a limit on how many repeater groups users can add on the front-end.', 'profile-builder' ) }
126 />
127 { limitEnabled && (
128 <>
129 <TextControl
130 label={ __( 'General Limit', 'profile-builder' ) }
131 type="number"
132 min={ 0 }
133 step={ 1 }
134 value={ limit ?? '0' }
135 onChange={ ( val ) => setAttributes( { 'rpf-limit': val } ) }
136 help={ __( 'Default limit for this repeater group. Leave 0 for unlimited.', 'profile-builder' ) }
137 />
138 <TextareaControl
139 label={ __( 'Limit Reached Message', 'profile-builder' ) }
140 value={ limitReachedMessage }
141 placeholder={ __( LIMIT_MESSAGE_DEFAULT, 'profile-builder' ) }
142 onChange={ ( val ) => setAttributes( { 'rpf-limit-reached-message': val } ) }
143 help={ __( 'Popup message shown when the limit is reached.', 'profile-builder' ) }
144 />
145 <BaseControl
146 label={ __( 'Limit per Role', 'profile-builder' ) }
147 help={ __( 'Override the general limit for specific roles. Leave 0 for unlimited.', 'profile-builder' ) }
148 >
149 { allRoles.length === 0 && (
150 <em>{ __( 'No roles available.', 'profile-builder' ) }</em>
151 ) }
152 { allRoles.map( ( role ) => (
153 <TextControl
154 key={ role.value }
155 label={ role.label }
156 type="number"
157 min={ 0 }
158 step={ 1 }
159 value={ roleLimitMap[ role.value ] ?? '' }
160 onChange={ ( val ) => setRoleLimit( role.value, val ) }
161 />
162 ) ) }
163 </BaseControl>
164 </>
165 ) }
166 </PanelBody>
167
168 { /* Conditional Logic / Field Visibility / admin-approval /
169 add-on properties. This block hosts its own Fill instead of
170 routing through BaseFieldEdit (it needs bespoke container
171 markup for the InnerBlocks), which used to mean the
172 cross-cutting panels were unreachable on a Repeater even
173 though the attributes were injected and mirrored for it
174 server-side. A top-level Repeater is
175 never nested (the parent's allowedBlocks forbid it), so
176 insideRepeater is constant false. */ }
177 <CrossCuttingFieldPanels
178 attributes={ attributes }
179 setAttributes={ setAttributes }
180 insideRepeater={ false }
181 />
182 </FieldSettingsFill>
183 ) }
184
185 <div className="wppb-fb-repeater-block__head">
186 <span className="wppb-fb-repeater-block__icon" aria-hidden="true">
187 <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
188 <rect x="3" y="4" width="18" height="6" rx="1.5" stroke="currentColor" strokeWidth="1.7" />
189 <rect x="3" y="14" width="18" height="6" rx="1.5" stroke="currentColor" strokeWidth="1.7" />
190 </svg>
191 </span>
192 <span className="wppb-fb-repeater-block__title">
193 { title || __( 'Repeater', 'profile-builder' ) }
194 { required === 'Yes' && <span className="wppb-fb-field-required">*</span> }
195 </span>
196 <span className="wppb-fb-repeater-block__meta">
197 <span className="wppb-fb-field-tag">{ __( 'Repeater', 'profile-builder' ) }</span>
198 <span className="wppb-fb-field-num">
199 { id
200 ? sprintf( /* translators: %s: field id */ __( 'Field #%s', 'profile-builder' ), id )
201 : __( 'Field #…', 'profile-builder' ) }
202 </span>
203 </span>
204 </div>
205
206 { description && <div className="wppb-fb-repeater-block__desc">{ description }</div> }
207
208 <div className="wppb-fb-repeater-block__hint">
209 { __( 'Sub-fields are scoped to this Repeater — add new fields using the inserter inside this box. They can\'t be dragged in or out.', 'profile-builder' ) }
210 </div>
211
212 <div className="wppb-fb-repeater-block__body">
213 <InnerBlocks
214 allowedBlocks={ ALLOWED_BLOCKS }
215 template={ [ [ 'profile-builder/field-input' ] ] }
216 renderAppender={ () => null }
217 />
218 { /* Same mini inserter as everywhere else. Its "Existing Fields"
219 tab is hidden automatically because the destination is inside
220 a Repeater — sub-fields live in the per-Repeater option, not
221 wppb_manage_fields, so an existing top-level field can't be
222 reused as one. See lib/miniInserterTarget.js. */ }
223 <AddFieldButton rootClientId={ clientId } label={ __( 'Add sub-field', 'profile-builder' ) } />
224 </div>
225 </div>
226 );
227 }
228