Jake Vanderwerf
2026-02-10 ad052f72a6c994dfb2fe0aa11970c9d110564004
=Fix for FAQpage schema not outputting correctly, as well as Form.php status radios and editForm on CRUDSkeleton.php
1 files added
6 files modified
187 ■■■■ changed files
assets/js/concise/PopulateForm.js 4 ●●●● patch | view | raw | blame | history
assets/js/min/populate.min.js 2 ●●● patch | view | raw | blame | history
inc/managers/SEO/schemas/SchemaResolverRegistry.php 3 ●●●● patch | view | raw | blame | history
inc/managers/SEO/schemas/_setup.php 1 ●●●● patch | view | raw | blame | history
inc/managers/SEO/schemas/resolvers/FAQPageResolver.php 75 ●●●●● patch | view | raw | blame | history
inc/meta/Form.php 43 ●●●● patch | view | raw | blame | history
inc/ui/CRUDSkeleton.php 59 ●●●● patch | view | raw | blame | history
assets/js/concise/PopulateForm.js
@@ -203,7 +203,7 @@
        }
        const grid = field.querySelector('.item-grid');
        let uploadContainer = field.querySelector('.file-upload-container');
        let uploadContainer = field.querySelector('.file-upload-wrapper');
        uploadContainer.hidden = ids.length > 0;
        field.querySelector('.progress')?.remove();
        if (grid) {
@@ -251,7 +251,7 @@
            if (!value || !Array.isArray(value) || value.length === 0) return;
            let grid = field.querySelector('.item-grid');
            let uploadContainer = field.querySelector('.file-upload-container');
            let uploadContainer = field.querySelector('.file-upload-wrapper');
            uploadContainer.hidden = value.length > 0;
            if (grid) {
                window.removeChildren(grid);
assets/js/min/populate.min.js
@@ -1 +1 @@
(()=>{class e{constructor(){this.templates=window.jvbTemplates,this.formHelper=window.jvbForm,this.defineTemplates(),this.data=null,this.form=null}populate(e,t={}){if(this.data=t,this.mergeRootData(),this.form=e,this.formHelper||(this.formHelper=window.jvbForm),this.formHelper){if(Object.hasOwn(this.data,"fields")&&0!==Object.keys(this.data.fields).length)for(let[t,i]of Object.entries(this.data.fields)){let a=e.querySelector(`[data-field="${t}"]`);a&&this.populateField(a,t,i)}}else requestAnimationFrame((()=>{this.populate(e,t)}))}mergeRootData(){["status","date","modified"].forEach((e=>{this.data.fields[`post_${e}`]=this.data[e]}))}populateField(e,t,i){let a=this.formHelper.getFieldType(e);if(!a||this.isEmptyValue(t)||this.isEmptyValue(i))return;const l={repeater:this.populateRepeater.bind(this),"tag-list":this.populateTagList.bind(this),location:this.populateLocation.bind(this),selector:this.populateTaxonomy.bind(this),user:this.populateUser.bind(this),upload:this.populateUpload.bind(this),set:this.populateMultiValue.bind(this),checkbox:this.populateMultiValue.bind(this),select:this.populateSingleValue.bind(this),radio:this.populateSingleValue.bind(this),"true-false":this.populateBoolean.bind(this),date:this.populateDate.bind(this),time:this.populateDate.bind(this),datetime:this.populateDate.bind(this),number:this.populateNumber.bind(this),textarea:this.populateTextarea.bind(this)};Object.hasOwn(l,a)?l[a](e,t,i):this.populateText(e,t,i)}populateRepeater(e,t,i){if(!i||!Array.isArray(i))return;const a=e.querySelector(".repeater-items");let l=e.querySelector("template")?.className??!1;a&&l&&(window.removeChildren(a),i.forEach(((e,t)=>{e.index=t;const i=this.templates.create(l,e);if(i){for(let[t,a]of Object.entries(e)){if("index"===t)continue;let e=i.querySelector(`[data-field="${t}"]`);e&&this.populateField(e,t,a)}a.append(i)}})))}populateTagList(e,t,i){if(!i||!Array.isArray(i))return;const a=e.querySelector(".tag-items");let l=e.querySelector("template")?.className??!1;a&&l&&(window.removeChildren(a),i.forEach(((i,s)=>{const r=this.templates.create(l,{label:this.getTagLabel(i,e.dataset.tagFormat??"first_field"),fieldName:t,...i});r&&(r.querySelectorAll('input[type="hidden"]').forEach((e=>{const t=e.dataset.field;t&&void 0!==i[t]&&(e.value=i[t])})),a.append(r))})))}getTagLabel(e,t){const i=Object.values(e).filter((e=>!this.isEmptyValue(e)));switch(t){case"first_field":return i[0]??"New Item";case"all_fields":return i.join(", ")||"New Item";default:if(t.includes("{")){let i=t;for(const[t,a]of Object.entries(e))i=i.replace(`{${t}}`,a);return i}return e[t]??i[0]??"New Item"}}populateLocation(e,t,i){["address","lat","lng","street","city","province","postal_code","country"].forEach((t=>{if(Object.hasOwn(i,t)){let a=e.querySelector(`[data-location-field="${t}"]`);a&&(a.value=String(i[t]||""))}}))}populateTaxonomy(e,t,i){let a=this.splitIDs(i);if(0===a.length)return;const l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=a.join(","),window.jvbSelector&&requestAnimationFrame((()=>{window.jvbSelector.updateFieldFromInput(l)})))}populateUser(e,t,i){this.populateTaxonomy(e,t,i)}populateUpload(e,t,i){if("timeline"===t||e.dataset.subtype&&"timeline"===e.dataset.subtype)return void this.populateTimelineGallery(e,t,i);if(this.isEmptyValue(i))return;const a=this.splitIDs(i);if(0===a.length)return;const l=e.querySelector('input[type="hidden"]');l&&(l.value=a.join(","));const s=e.querySelector(".item-grid");e.querySelector(".file-upload-container").hidden=a.length>0,e.querySelector(".progress")?.remove(),s&&(window.removeChildren(s),a.forEach((e=>{let t=this.data.images[e]??{};t.field={config:{showMeta:!0}},t.id=e,s.append(this.templates.create("uploadItem",t))}))),this.populateUploadMeta(e,t,i)}populateUploadMeta(e,t,i){const a=e.querySelector('[data-field="image_data"]');if(!a)return;let l=this.data.images[i]??!1;if(!l)return;a.dataset.attachmentId=l.id,a.setAttribute("data-ignore","");const s=["image-title","image-alt-text","image-caption"];for(const e of s){const t=a.querySelector(`[data-field="${e}"] input, [data-field="${e}"] textarea`);t&&""!==l[e]&&(t.value=l[e])}}populateTimelineGallery(e,t,i){if(!i||!Array.isArray(i)||0===i.length)return;let a=e.querySelector(".item-grid");if(e.querySelector(".file-upload-container").hidden=i.length>0,a){window.removeChildren(a),e.querySelector(".progress")?.remove();for(let e of i){let t=this.templates.create("timelineItem",e);t&&a.append(t)}}}populateMultiValue(e,t,i){if("string"==typeof i)try{i=JSON.parse(i)}catch(e){i=i.split(",").map((e=>e.trim()))}Array.isArray(i)||(i=[String(i)]);let a=e.querySelector(`select[name="${t}"]`);if(a&&a.multiple)for(let e of a.options)e.selected=i.includes(e.value);else e.querySelectorAll(`[type="checkbox"][name=${t}]`).forEach((e=>{e.checked=i.includes(e.value)}))}populateSingleValue(e,t,i){i=String(i||"");let a=e.querySelector(`select[name="${t}"]`);if(a)return void(a.value=i);let l=e.querySelector(`[name="${t}"][value="${i}"]`);l&&(l.checked=!0)}populateBoolean(e,t,i){const a=e.querySelector(`[name="${t}"], input[type="checkbox"]`);a&&(a.checked=Boolean(i))}populateDate(e,t,i){const a=e.querySelector(`[name="${t}"], input`);if(a){"object"==typeof i&&Object.hasOwn(i,"date")&&(i=i.date);try{const e=new Date(i);if(!isNaN(e.getTime()))switch(a.type){case"date":a.value=e.toISOString().split("T")[0];break;case"time":a.value=e.toTimeString().slice(0,5);break;case"datetime-local":a.value=e.toISOString().slice(0,16);break;default:a.value=i}}catch(e){a.value=i}}}populateNumber(e,t,i){const a=e.querySelector(`[name="${t}"], input[type="number"]`);a&&(a.value=Number(i)||0)}populateTextarea(e,t,i){let a=e.querySelector("textarea");a.dataset.editor?(a.value=String(i||""),a.dispatchEvent(new Event("change",{bubbles:!0}))):this.populateText(e,t,i)}populateText(e,t,i){let a=e.querySelector(`[name="${t}"], input, textarea`);a&&"file"!==a.type&&(a.value=String(i||""))}getFormHelper(){window.requestAnimationFrame((()=>{this.formHelper=window.jvbForm}))}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e)&&e>0))}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}defineTemplates(){const e=this.templates,t=this;e.define("timelineItem",{refs:{select:'[name="select-item"]',video:"video",file:".select-item span",img:"img",details:"details[data-field]",imgAlt:'[name="image-alt-text"]',imgTitle:'[name="image-title"]',imgDesc:'[name="image-caption"]'},manyRefs:{fields:".field"},setup({el:e,refs:i,manyRefs:a,data:l}){if(e.dataset.itemId=l.id,i.select){let e=i.select.closest(".preview");window.prefixInput(i.select,`${l.id}-`,e)}i.video&&i.video.remove(),i.file&&i.file.remove();let s=t.data.images[l.post_thumbnail]??!1;if(i.img&&s&&(i.img.src=s.medium||s.small||s.large||"",i.img.title=s["image-title"]??"",i.img.alt=s["image-alt-text"]??""),i.details){let e=t.data.images[l.post_thumbnail];i.details.setAttribute("data-ignore",""),i.details.dataset.attachmentId=l.post_thumbnail,Object.hasOwn(e,"image-alt-text")&&i.alt&&(i.alt.value=e["image-alt-text"]),(Object.hasOwn(e,"image-title")||Object.hasOwn(l,"file"))&&i.title&&(i.title.value=e["image-title"]||l.file.name),Object.hasOwn(e,"image-caption")&&i.description&&(i.description.value=e["image-caption"])}if(a.fields)for(let e of a.fields){if("group"===e.dataset.fieldType)continue;if("post_thumbnail"===e.dataset.field){e.remove();continue}let i=e.dataset.field,a=l[i]??"";t.isEmptyValue(a)||t.populateField(e,i,a);const s=e.querySelector('input:not([type="file"])');s&&window.prefixInput(s,`[${l.id}]`,e)}}})}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbPopulate=new e)}))}))})();
(()=>{class e{constructor(){this.templates=window.jvbTemplates,this.formHelper=window.jvbForm,this.defineTemplates(),this.data=null,this.form=null}populate(e,t={}){if(this.data=t,this.mergeRootData(),this.form=e,this.formHelper||(this.formHelper=window.jvbForm),this.formHelper){if(Object.hasOwn(this.data,"fields")&&0!==Object.keys(this.data.fields).length)for(let[t,i]of Object.entries(this.data.fields)){let a=e.querySelector(`[data-field="${t}"]`);a&&this.populateField(a,t,i)}}else requestAnimationFrame((()=>{this.populate(e,t)}))}mergeRootData(){["status","date","modified"].forEach((e=>{this.data.fields[`post_${e}`]=this.data[e]}))}populateField(e,t,i){let a=this.formHelper.getFieldType(e);if(!a||this.isEmptyValue(t)||this.isEmptyValue(i))return;const l={repeater:this.populateRepeater.bind(this),"tag-list":this.populateTagList.bind(this),location:this.populateLocation.bind(this),selector:this.populateTaxonomy.bind(this),user:this.populateUser.bind(this),upload:this.populateUpload.bind(this),set:this.populateMultiValue.bind(this),checkbox:this.populateMultiValue.bind(this),select:this.populateSingleValue.bind(this),radio:this.populateSingleValue.bind(this),"true-false":this.populateBoolean.bind(this),date:this.populateDate.bind(this),time:this.populateDate.bind(this),datetime:this.populateDate.bind(this),number:this.populateNumber.bind(this),textarea:this.populateTextarea.bind(this)};Object.hasOwn(l,a)?l[a](e,t,i):this.populateText(e,t,i)}populateRepeater(e,t,i){if(!i||!Array.isArray(i))return;const a=e.querySelector(".repeater-items");let l=e.querySelector("template")?.className??!1;a&&l&&(window.removeChildren(a),i.forEach(((e,t)=>{e.index=t;const i=this.templates.create(l,e);if(i){for(let[t,a]of Object.entries(e)){if("index"===t)continue;let e=i.querySelector(`[data-field="${t}"]`);e&&this.populateField(e,t,a)}a.append(i)}})))}populateTagList(e,t,i){if(!i||!Array.isArray(i))return;const a=e.querySelector(".tag-items");let l=e.querySelector("template")?.className??!1;a&&l&&(window.removeChildren(a),i.forEach(((i,r)=>{const s=this.templates.create(l,{label:this.getTagLabel(i,e.dataset.tagFormat??"first_field"),fieldName:t,...i});s&&(s.querySelectorAll('input[type="hidden"]').forEach((e=>{const t=e.dataset.field;t&&void 0!==i[t]&&(e.value=i[t])})),a.append(s))})))}getTagLabel(e,t){const i=Object.values(e).filter((e=>!this.isEmptyValue(e)));switch(t){case"first_field":return i[0]??"New Item";case"all_fields":return i.join(", ")||"New Item";default:if(t.includes("{")){let i=t;for(const[t,a]of Object.entries(e))i=i.replace(`{${t}}`,a);return i}return e[t]??i[0]??"New Item"}}populateLocation(e,t,i){["address","lat","lng","street","city","province","postal_code","country"].forEach((t=>{if(Object.hasOwn(i,t)){let a=e.querySelector(`[data-location-field="${t}"]`);a&&(a.value=String(i[t]||""))}}))}populateTaxonomy(e,t,i){let a=this.splitIDs(i);if(0===a.length)return;const l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=a.join(","),window.jvbSelector&&requestAnimationFrame((()=>{window.jvbSelector.updateFieldFromInput(l)})))}populateUser(e,t,i){this.populateTaxonomy(e,t,i)}populateUpload(e,t,i){if("timeline"===t||e.dataset.subtype&&"timeline"===e.dataset.subtype)return void this.populateTimelineGallery(e,t,i);if(this.isEmptyValue(i))return;const a=this.splitIDs(i);if(0===a.length)return;const l=e.querySelector('input[type="hidden"]');l&&(l.value=a.join(","));const r=e.querySelector(".item-grid");e.querySelector(".file-upload-wrapper").hidden=a.length>0,e.querySelector(".progress")?.remove(),r&&(window.removeChildren(r),a.forEach((e=>{let t=this.data.images[e]??{};t.field={config:{showMeta:!0}},t.id=e,r.append(this.templates.create("uploadItem",t))}))),this.populateUploadMeta(e,t,i)}populateUploadMeta(e,t,i){const a=e.querySelector('[data-field="image_data"]');if(!a)return;let l=this.data.images[i]??!1;if(!l)return;a.dataset.attachmentId=l.id,a.setAttribute("data-ignore","");const r=["image-title","image-alt-text","image-caption"];for(const e of r){const t=a.querySelector(`[data-field="${e}"] input, [data-field="${e}"] textarea`);t&&""!==l[e]&&(t.value=l[e])}}populateTimelineGallery(e,t,i){if(!i||!Array.isArray(i)||0===i.length)return;let a=e.querySelector(".item-grid");if(e.querySelector(".file-upload-wrapper").hidden=i.length>0,a){window.removeChildren(a),e.querySelector(".progress")?.remove();for(let e of i){let t=this.templates.create("timelineItem",e);t&&a.append(t)}}}populateMultiValue(e,t,i){if("string"==typeof i)try{i=JSON.parse(i)}catch(e){i=i.split(",").map((e=>e.trim()))}Array.isArray(i)||(i=[String(i)]);let a=e.querySelector(`select[name="${t}"]`);if(a&&a.multiple)for(let e of a.options)e.selected=i.includes(e.value);else e.querySelectorAll(`[type="checkbox"][name=${t}]`).forEach((e=>{e.checked=i.includes(e.value)}))}populateSingleValue(e,t,i){i=String(i||"");let a=e.querySelector(`select[name="${t}"]`);if(a)return void(a.value=i);let l=e.querySelector(`[name="${t}"][value="${i}"]`);l&&(l.checked=!0)}populateBoolean(e,t,i){const a=e.querySelector(`[name="${t}"], input[type="checkbox"]`);a&&(a.checked=Boolean(i))}populateDate(e,t,i){const a=e.querySelector(`[name="${t}"], input`);if(a){"object"==typeof i&&Object.hasOwn(i,"date")&&(i=i.date);try{const e=new Date(i);if(!isNaN(e.getTime()))switch(a.type){case"date":a.value=e.toISOString().split("T")[0];break;case"time":a.value=e.toTimeString().slice(0,5);break;case"datetime-local":a.value=e.toISOString().slice(0,16);break;default:a.value=i}}catch(e){a.value=i}}}populateNumber(e,t,i){const a=e.querySelector(`[name="${t}"], input[type="number"]`);a&&(a.value=Number(i)||0)}populateTextarea(e,t,i){let a=e.querySelector("textarea");a.dataset.editor?(a.value=String(i||""),a.dispatchEvent(new Event("change",{bubbles:!0}))):this.populateText(e,t,i)}populateText(e,t,i){let a=e.querySelector(`[name="${t}"], input, textarea`);a&&"file"!==a.type&&(a.value=String(i||""))}getFormHelper(){window.requestAnimationFrame((()=>{this.formHelper=window.jvbForm}))}splitIDs(e){return String(e).split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e)&&e>0))}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}defineTemplates(){const e=this.templates,t=this;e.define("timelineItem",{refs:{select:'[name="select-item"]',video:"video",file:".select-item span",img:"img",details:"details[data-field]",imgAlt:'[name="image-alt-text"]',imgTitle:'[name="image-title"]',imgDesc:'[name="image-caption"]'},manyRefs:{fields:".field"},setup({el:e,refs:i,manyRefs:a,data:l}){if(e.dataset.itemId=l.id,i.select){let e=i.select.closest(".preview");window.prefixInput(i.select,`${l.id}-`,e)}i.video&&i.video.remove(),i.file&&i.file.remove();let r=t.data.images[l.post_thumbnail]??!1;if(i.img&&r&&(i.img.src=r.medium||r.small||r.large||"",i.img.title=r["image-title"]??"",i.img.alt=r["image-alt-text"]??""),i.details){let e=t.data.images[l.post_thumbnail];i.details.setAttribute("data-ignore",""),i.details.dataset.attachmentId=l.post_thumbnail,Object.hasOwn(e,"image-alt-text")&&i.alt&&(i.alt.value=e["image-alt-text"]),(Object.hasOwn(e,"image-title")||Object.hasOwn(l,"file"))&&i.title&&(i.title.value=e["image-title"]||l.file.name),Object.hasOwn(e,"image-caption")&&i.description&&(i.description.value=e["image-caption"])}if(a.fields)for(let e of a.fields){if("group"===e.dataset.fieldType)continue;if("post_thumbnail"===e.dataset.field){e.remove();continue}let i=e.dataset.field,a=l[i]??"";t.isEmptyValue(a)||t.populateField(e,i,a);const r=e.querySelector('input:not([type="file"])');r&&window.prefixInput(r,`[${l.id}]`,e)}}})}}document.addEventListener("DOMContentLoaded",(function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbPopulate=new e)}))}))})();
inc/managers/SEO/schemas/SchemaResolverRegistry.php
@@ -68,7 +68,8 @@
        $collection = new CollectionPageResolver();
        $this->register('CollectionPage', $collection);
        $this->register('FAQPage', $collection);
        $this->register('DefinedTermSet', $collection);
        $this->register('FAQPage', new FAQPageResolver());
    }
}
inc/managers/SEO/schemas/_setup.php
@@ -21,6 +21,7 @@
require(JVB_DIR . '/inc/managers/SEO/schemas/resolvers/LocalBusinessResolver.php');
require(JVB_DIR . '/inc/managers/SEO/schemas/resolvers/VisualArtworkResolver.php');
require(JVB_DIR . '/inc/managers/SEO/schemas/resolvers/PersonResolver.php');
require(JVB_DIR . '/inc/managers/SEO/schemas/resolvers/FAQPageResolver.php');
require(JVB_DIR . '/inc/managers/SEO/schemas/resolvers/CollectionPageResolver.php');
require(JVB_DIR . '/inc/managers/SEO/schemas/SchemaResolverRegistry.php');
inc/managers/SEO/schemas/resolvers/FAQPageResolver.php
New file
@@ -0,0 +1,75 @@
<?php
namespace JVBase\managers\SEO\schemas\resolvers;
use JVBase\managers\SEO\schemas\SchemaDefinition;
use JVBase\meta\Meta;
if (!defined('ABSPATH')) {
    exit;
}
/**
 * Resolver for FAQPage schema.
 *
 * Handles two contexts:
 * - Single post: transforms question/answer into mainEntity Question structure
 * - Archive/term: delegates to CollectionPageResolver for mainEntity from posts
 */
class FAQPageResolver extends BaseResolver
{
    private CollectionPageResolver $collectionResolver;
    public function __construct()
    {
        $this->collectionResolver = new CollectionPageResolver();
    }
    public function resolve(SchemaDefinition $definition, ?Meta $meta = null): ?array
    {
        // Single FAQ post: restructure question/answer into mainEntity
        if ($definition->objectType === 'post') {
            $this->transformQuestionAnswer($definition);
        }
        return parent::resolve($definition, $meta);
    }
    public function getAutoFields(SchemaDefinition $definition): array
    {
        // Archive/term context: delegate to CollectionPageResolver
        if (in_array($definition->objectType, ['archive', 'term'])) {
            return $this->collectionResolver->getAutoFields($definition);
        }
        return [];
    }
    /**
     * Transform question/answer into proper mainEntity structure.
     *
     * Schema.org requires FAQPage Q&A nested as:
     *   mainEntity: [{ @type: Question, name: ..., acceptedAnswer: { @type: Answer, text: ... } }]
     */
    private function transformQuestionAnswer(SchemaDefinition $definition): void
    {
        $question = $definition->config['question'] ?? null;
        $answer   = $definition->config['answer'] ?? null;
        if (!$question && !$answer) {
            return;
        }
        unset($definition->config['question'], $definition->config['answer']);
        $questionEntity = ['@type' => 'Question', 'name' => $question ?? ''];
        if ($answer) {
            $questionEntity['acceptedAnswer'] = [
                '@type' => 'Answer',
                'text'  => $answer,
            ];
        }
        $definition->config['mainEntity'] = [$questionEntity];
    }
}
inc/meta/Form.php
@@ -107,6 +107,8 @@
        $output .= static::buildLabel($name, $config);
        if (!array_key_exists('skipInput', $config)) {
            $output .= static::buildInput($content);
        } else {
            $output .= $content;
        }
        $output .= static::buildHint($config);
@@ -571,29 +573,46 @@
    protected static function renderRadio(string $name, mixed $value, array $config): string
    {
        $options = $config['options'] ?? [];
        $inputClass = !empty($config['inputClass']) ? ' class="' . esc_attr($config['inputClass']) . '"' : '';
        $idPrefix   = $config['idPrefix'] ?? '';
        $radios = sprintf(
            '<fieldset>
            <legend>%s%s</legend>',
            array_key_exists('label', $config) ? esc_html($config['label']) : 'Select an option',
            array_key_exists('required', $config) && $config['required']===true ? '<span class="required" aria-label="required">*</span>' : ''
            array_key_exists('required', $config) && $config['required'] === true
                ? '<span class="required" aria-label="required">*</span>' : ''
        );
        foreach ($options as $optValue => $optLabel) {
        foreach ($options as $optValue => $optConfig) {
            if (is_array($optConfig)) {
                $optLabel    = $optConfig['label'] ?? $optValue;
                $optIcon     = $optConfig['icon'] ?? null;
                $optDisabled = !empty($optConfig['disabled']) ? ' disabled' : '';
            } else {
                $optLabel    = $optConfig;
                $optIcon     = null;
                $optDisabled = '';
            }
            $labelContent = $optIcon
                ? jvbDashIcon($optIcon)
                : '<span>' . esc_html($optLabel) . '</span>';
            $optId = esc_attr($idPrefix . $name . '-' . $optValue);
            $radios .= sprintf(
                '
                    <input type="radio" name="%s" id="%s-%s" value="%s"%s />
                <label class="radio-option" for="%s-%s">
                    <span>%s</span>
                </label>',
                '<input type="radio" name="%s" id="%s" value="%s"%s%s%s />
            <label class="radio-option" for="%s" title="%s">%s</label>',
                esc_attr($name),
                esc_attr($name),
                $optValue,
                $optId,
                esc_attr($optValue),
                checked($value, $optValue,false),
                esc_attr($name),
                $optValue,
                esc_html($optLabel)
                $optDisabled,
                $inputClass,
                $optId,
                esc_html($optLabel),
                $labelContent
            );
        }
inc/ui/CRUDSkeleton.php
@@ -1543,15 +1543,9 @@
            <input type="hidden" name="form-id" value="<?=uniqid('new-')?>" />
            <input type="hidden" name="content" value="<?=$this->dataType?>" />
            <div class="fields">
                <div class="field-group radio-options row" data-field="post_status" data-field-type="radio">
                    <span>Status:</span>
                    <?php
                    $this->getApplicableStatuses('edit');
                    ?>
                </div>
                <?php if (!$this->userCanPublish) { ?>
                    <p class="description">Your account needs to be verified before you can publish content.</p>
                <?php }
                echo Form::render('post_status', '', $this->getStatusFieldConfig('edit-'));
                if (!empty($this->sections)) {
                    $tabs = [];
@@ -1620,13 +1614,13 @@
                    $fields = $this->nonTimelineFields;
                }
                foreach ($fields as $n => $config) {
                    if (in_array($config['type'], ['taxonomy', 'selector'])) {
                        $config = array_merge($config, $this->taxConfig($config['taxonomy'], $config['label']));
                    }
                    if ($tabs) {
                        $section = (array_key_exists('section', $config)) ? $config['section'] : 'basic';
                        $tabs[$section]['content'] .= Form::render($n,'', $config);
                    } else {
                        if (in_array($config['type'], ['taxonomy', 'selector'])) {
                            $config = array_merge($config, $this->taxConfig($config['taxonomy'], $config['label']));
                        }
                        echo Form::render($n, '', $config);
                    }
                }
@@ -1663,9 +1657,8 @@
            <p class="hint"><strong>IMPORTANT: </strong> Whatever changes you make here will be applied to all selected <?=$this->plural?>.</p>
            <div class="fields">
                <?php
                $this->getApplicableStatuses('bulk-');
                ?>
                <?php
                echo Form::render('post_status', '', $this->getStatusFieldConfig('bulk-'));
                if (!empty($this->taxonomies)) {
                    ?>
                    <div class="taxonomies">
@@ -1702,6 +1695,44 @@
        );
    }
    protected function getStatusFieldConfig(string $prefix): array
    {
        $options = [];
        foreach ($this->statuses as $status) {
            if ($status === 'all' || !array_key_exists($status, $this->allowedStatuses)) {
                continue;
            }
            $config = $this->allowedStatuses[$status];
            if (in_array($status, ['future', 'past'])) {
                if ($status === 'future') {
                    $status = 'publish';
                    $config = ['icon' => 'eye', 'label' => 'Live'];
                } else {
                    continue;
                }
            }
            $options[$status] = [
                'label'    => $config['label'],
                'icon'     => $config['icon'],
                'disabled' => ($status === 'publish' && !$this->userCanPublish),
            ];
        }
        return [
            'type'       => 'radio',
            'label'      => 'Status',
            'options'    => $options,
            'inputClass' => 'btn',
            'idPrefix'   => $prefix,
            'class'      => 'radio-options row',
            'hint'       => !$this->userCanPublish
                ? 'Your account needs to be verified before you can publish content.'
                : '',
        ];
    }
    protected function getApplicableStatuses(string $prefix) {
        ob_start();
        foreach ($this->statuses as $status) {