From 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Nov 2025 07:42:23 +0000
Subject: [PATCH] =Feed block mostly good! Referrals look good to go. Ready for Madi and Heidi to approve
---
inc/managers/CRUDManager.php | 349 ++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 300 insertions(+), 49 deletions(-)
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index 9589fdc..e05a0e5 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -2,7 +2,9 @@
namespace JVBase\managers;
use JVBase\managers\UserTermsManager;
+use JVBase\meta\MetaForm;
use JVBase\meta\MetaManager;
+use JVBase\utility\Features;
use WP_User;
if (!defined('ABSPATH')) {
@@ -19,12 +21,20 @@
protected array $filters;
protected array $bulkActions;
protected MetaManager $meta;
+ protected MetaForm $form;
protected array $taxonomies;
protected array $statuses;
protected array $fields;
protected array $sections;
protected array $stuck;
+
+ //For Timeline-specific posts
+ protected bool $isTimeline = false;
+ protected array $nonTimelineFields = [];
+ protected array $timelineSharedFields = [];
+ protected array $timelineUniqueFields = [];
+
protected bool $userCanPublish = false;
public function __construct(string $content)
@@ -40,6 +50,7 @@
$this->plural = $this->config['plural'];
$this->content = $content;
$this->fields = jvbGetFields($this->content, 'post');
+ $this->maybeSetupTimeline();
$this->sections = jvbGetSections($this->content, 'post');
$this->stuck = [
'post_title',
@@ -47,6 +58,11 @@
];
$this->init();
+
+ if ($this->isTimeline) {
+ $this->stuck[] = 'post_thumbnail';
+ }
+ add_filter('jvbAdditionalActions', [$this, 'createItem']);
}
protected function init():void
@@ -56,13 +72,42 @@
$this->initTaxonomies();
$this->initFilters();
$this->meta = new MetaManager(null, 'post', $this->content);
-
+ $this->form = new MetaForm();
$plural = strtolower($this->config['plural']??$this->content.'s');
$this->userCanPublish = (jvbUserIsVerified()) ?
user_can($this->user_id, "publish_{$plural}") : false;
}
+ protected function maybeSetupTimeline():void {
+ $this->isTimeline = Features::forContent($this->content)->has('is_timeline');
+
+ if (!$this->isTimeline) {
+ return;
+ }
+ $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
+ if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
+ return true;
+ }
+ return false;
+ }));
+ array_unshift($this->timelineSharedFields, 'post_thumbnail');
+ array_unshift($this->timelineSharedFields, 'post_title');
+ array_unshift($this->timelineSharedFields, 'post_status');
+
+ $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
+ if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
+ return true;
+ }
+ return false;
+ }));
+
+ $all = array_merge($this->timelineUniqueFields, $this->timelineSharedFields);
+ $this->nonTimelineFields = array_filter($this->fields, function ($field) use ($all) {
+ return !in_array($field, $all);
+ }, ARRAY_FILTER_USE_KEY);
+ }
+
protected function initTaxonomies():void
{
$this->taxonomies = array_filter(JVB_TAXONOMY, function ($config) {
@@ -80,41 +125,41 @@
],
'future'=> [
'label' => 'Upcoming',
- 'icon' => 'future',
+ 'icon' => 'clock-clockwise',
],
'past' => [
'label' => 'Past',
- 'icon' => 'past',
+ 'icon' => 'clock-counter-clockwise',
],
'repeat'=> [
'label' => 'Recurring',
'icon' => 'repeat',
],
'draft' => [
- 'icon' => 'hide',
+ 'icon' => 'eye-closed',
'label' => 'Hidden',
],
'trash' => [
'label' => 'Scrapped',
- 'icon' => 'delete',
+ 'icon' => 'trash',
],
] :
[
'all' => [
- 'icon' => 'all',
+ 'icon' => 'infinity',
'label' => 'Everything',
],
'publish'=> [
- 'icon' => 'publish',
+ 'icon' => 'eye',
'label' => 'Live',
],
'draft' => [
- 'icon' => 'hide',
+ 'icon' => 'eye-closed',
'label' => 'Hidden',
],
'trash' => [
'label' => 'Scrapped',
- 'icon' => 'delete',
+ 'icon' => 'trash',
],
];
}
@@ -143,7 +188,7 @@
foreach ($this->taxonomies as $taxonomy=> $config) {
$this->filters['taxonomy'][$taxonomy] = [
'label' => $config['singular'],
- 'icon' => $taxonomy
+ 'icon' => $config['icon']??'folder'
];
}
}
@@ -152,7 +197,7 @@
{
ob_start();
?>
- <div class="dashboard-page <?= esc_attr($this->content) ?>">
+ <div class="dashboard-page <?= esc_attr($this->content) ?>"<?=($this->isTimeline) ? ' data-timeline' : ''?>>
<?php
$this->renderHeader();
$this->renderContent();
@@ -181,7 +226,8 @@
protected function renderHeaderActions():void
{
$uploadConfig = [
- 'type' => 'image',
+ 'type' => 'upload',
+ 'subtype' => 'image',
'mode' => (jvbCheck('single_image', $this->config)) ? 'direct' : 'selection',
'create_new' => true,
'label' => (array_key_exists('image_title', $this->config)) ? $this->config['image_title'] : 'Upload More '.$this->config['plural'],
@@ -189,14 +235,15 @@
'singular' => $this->singular,
'plural' => $this->plural,
'multiple' => true,
+ 'destination' => 'post'
];
- if (!jvbCheck('single_image', $this->config)) {
- $uploadConfig['imageType'] = 'groupable';
+ if (!array_key_exists('single_image', $this->config) || $this->config['single_image'] === false) {
+ $uploadConfig['destination'] = 'post_group';
}
-
+ $uploadConfig['destination'] = 'post_group';
if (!jvbCheck('single_image', $this->config)) {
- $uploadConfig['group_title'] = 'Create '.$this->config['plural'];
- $uploadConfig['group_description'] = '<p>Drag images into groups. Each group becomes its own '.$this->singular.'.</p>
+ $uploadConfig['label'] = 'Create '.$this->config['plural'];
+ $uploadConfig['upload_text'] = '<p>Drag images into groups. Each group becomes its own '.$this->singular.'.</p>
<p>You can also select multiple images and click the "Add to Group" button.</p>
<p>If a '.$this->singular.' has multiple images, you can select the '.jvbIcon('star').' to set an image as the main one.</p>
<p>Images left ungrouped will become individual '.$this->plural.'</p>
@@ -205,7 +252,6 @@
$uploadConfig['description'] = 'Each image will become its own '.$this->singular.'.';
}
?>
- <button type="button" class="create-item row" title="Create New <?= $this->singular?>"><?=jvbIcon('add') ?><span class="screen-reader-text">Create New <?= $this->singular?></span></button>
<details open class="uploader">
<summary class="row btw"><?= $this->config['upload_title'] ?? 'Bulk Upload '.$this->plural?></summary>
<?php
@@ -243,9 +289,9 @@
ob_start();
?>
<div class="empty-state">
- <h3><?=jvbIcon($this->content)?>Nothing here<?=jvbIcon($this->content)?></h3>
+ <h3><?=jvbIcon($this->config['icon'])?>Nothing here<?=jvbIcon($this->config['icon'])?></h3>
<p>It doesn't look like you have any <?=$this->config['plural'] ?> yet.</p>
- <p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('add')?>" button to add one at a time.</small></p>
+ <p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('plus-square')?>" button to add one at a time.</small></p>
</div>
<?php
return ob_get_clean();
@@ -254,7 +300,7 @@
protected function renderFilters():void
{
?>
- <div class="all-filters col start">
+ <div class="all-filters col start" data-ignore>
<div class="search row start nowrap">
<span class="label">Search:</span>
<?= jvbSearch() ?>
@@ -273,7 +319,7 @@
$this->renderDateFilters();
?>
<button type="button" class="clear-filters row" hidden>
- <?= jvbIcon('close', ['title' => 'Clear']); ?>
+ <?= jvbIcon('x', ['title' => 'Clear']); ?>
Clear All Filters
</button>
</div>
@@ -294,8 +340,8 @@
'alphabetical' => 'Order alphabetically'
],
'order' => [
- 'asc' => 'In ascending order (Z-A, oldest to newest)',
- 'desc' => 'In descending order (A-Z, newest to oldest)'
+ 'sort-ascending' => 'In ascending order (Z-A, oldest to newest)',
+ 'sort-descending' => 'In descending order (A-Z, newest to oldest)'
]
];
@@ -306,10 +352,11 @@
<?php
$i = 0;
foreach ($option as $opt => $label) {
+ $icon = $opt === 'date' ? 'calendar' : $opt;
?>
<input id="<?=$opt?>" class="btn" type="radio" name="<?=$o?>" data-filter="<?=$o?>" value="<?=$opt?>"<?=$i===0 ? ' checked':''?>>
- <label for="<?=$opt?>" title="<?=$label?>"><?=jvbIcon($opt)?></label>
+ <label for="<?=$opt?>" title="<?=$label?>"><?=jvbIcon($icon)?></label>
<?php
$i++;
}
@@ -355,8 +402,8 @@
<?php
$views = [
- 'grid' => ['icon' => 'grid', 'label' => 'Grid View'],
- 'list' => ['icon' => 'list', 'label' => 'List View'],
+ 'grid' => ['icon' => 'squares-four', 'label' => 'Grid View'],
+ 'list' => ['icon' => 'rows', 'label' => 'List View'],
'table' => ['icon' => 'table', 'label' => 'Table View']
];
@@ -583,7 +630,7 @@
protected function renderModals():void
{
- $this->renderCreateModal();
+// $this->renderCreateModal();
$this->renderEditModal();
$this->renderBulkEditModal();
}
@@ -600,7 +647,8 @@
{
ob_start();
?>
- <form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>">
+ <form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>" data-autosave<?= ($this->isTimeline) ? ' data-timeline' : ''?>>
+ <?= jvbFormStatus() ?>
<input type="hidden" name="form-id" value="<?=uniqid('new-')?>" />
<input type="hidden" name="content" value="<?=$this->content?>" />
<div class="fields">
@@ -630,20 +678,50 @@
} else {
$tabs = false;
}
+
+
$fields = $this->fields;
+ if (!$this->isTimeline) {
+ $first = ['post_thumbnail', 'post_title', 'price'];
- $first = ['post_thumbnail', 'post_title', 'price'];
- foreach ($first as $f) {
- if (array_key_exists($f, $fields)) {
- if ($tabs) {
- $tabs['basic']['content'] .= $this->meta->render('form', $f, $fields[$f], false, true);
- } else {
- $this->meta->render('form', $f, $fields[$f]);
+ foreach ($first as $f) {
+ if (array_key_exists($f, $fields)) {
+ if ($tabs) {
+ $tabs['basic']['content'] .= $this->meta->render('form', $f, $fields[$f], false, true);
+ } else {
+ $this->meta->render('form', $f, $fields[$f]);
+ }
+
+ unset($fields[$f]);
}
-
- unset($fields[$f]);
}
}
+
+ if ($this->isTimeline) {
+ $temp = array_filter($fields, function ($field) {
+ return in_array($field, $this->timelineUniqueFields);
+ }, ARRAY_FILTER_USE_KEY);
+
+ $config = [
+ 'type' => 'gallery',
+ 'subtype' => 'timeline',
+ 'data' => 'timeline',
+ 'label' => 'Progression',
+ 'fields' => $temp
+ ];
+ $content = '';
+ foreach ($fields as $slug=> $field) {
+ if (in_array($slug, $this->timelineSharedFields)) {
+ $content .= $this->form->render($slug, null, $field, false, true);
+ }
+ }
+
+
+ $content .= $this->meta->render('form', 'timeline', $config, false,true);
+
+ $tabs['progression']['content'] = $content;
+ $fields = $this->nonTimelineFields;
+ }
foreach ($fields as $n => $config) {
if ($tabs) {
$section = (array_key_exists('section', $config)) ? $config['section'] : 'basic';
@@ -651,10 +729,8 @@
} else {
$this->meta->render('form', $n, $config);
}
-
}
-
if ($tabs) {
jvbRenderTabs($tabs);
}
@@ -665,6 +741,28 @@
return ob_get_clean();
}
+
+ protected function renderRowFields():void
+ {
+ $fields = $this->fields;
+
+ // Render priority fields first
+ $first = ['post_thumbnail', 'post_title', 'price'];
+ foreach ($first as $f) {
+ if (array_key_exists($f, $fields)) {
+ $this->meta->render('form', $f, $fields[$f]);
+ unset($fields[$f]);
+ }
+ }
+
+ // Render remaining fields
+ foreach ($fields as $name => $config) {
+ if (!array_key_exists('hidden', $config) || !$config['hidden']) {
+ $this->meta->render('form', $name, $config);
+ }
+ }
+ }
+
protected function getApplicableStatuses(string $prefix) {
foreach ($this->statuses as $status => $config) {
if ($status === 'all') {
@@ -674,7 +772,7 @@
if ($status === 'future') {
$status = 'publish';
$config = [
- 'icon' => 'publish',
+ 'icon' => 'eye',
'label' => 'Live',
];
} else {
@@ -711,6 +809,7 @@
ob_start();
?>
<form class="bulk-edit-form" data-save="content" data-form-id="bulk-edit-<?=$this->content?>">
+ <?= jvbFormStatus() ?>
<div class="selected"></div>
<p class="description">You can unselect items by clicking the image here.</p>
<p class="hint"><strong>IMPORTANT: </strong> Whatever changes you make here will be applied to all selected <?=$this->plural?>.</p>
@@ -774,6 +873,15 @@
$this->renderGridView();
$this->renderTableView();
$this->renderTableRow();
+ if ($this->isTimeline) {
+ $temp = array_filter($this->fields, function ($field) {
+ return in_array($field, $this->timelineUniqueFields);
+ }, ARRAY_FILTER_USE_KEY);
+ $form = new MetaForm();
+ echo '<template class="timelineItem">';
+ $form->renderImagePreview(null,['fields' => $temp]);
+ echo '</template>';
+ }
echo jvbGetEmptyStateTemplate();
echo jvbGetGalleryPreviewTemplate();
@@ -809,11 +917,11 @@
?>
<div class="item-actions">
<button type="button" class="action" data-action="edit" title="Edit <?= $this->singular ?>">
- <?=jvbIcon('edit')?>
+ <?=jvbIcon('pencil-simple')?>
<span class="screen-reader-text">Edit <?= $this->singular ?></span>
</button>
<button type="button" class="action" data-action="trash" title="Scrap <?= $this->singular ?>">
- <?=jvbIcon('delete')?>
+ <?=jvbIcon('trash')?>
<span class="screen-reader-text">Scrap <?= $this->singular ?></span>
</button>
<!-- <button type="button" class="action" data-action="toggle-status">-->
@@ -878,13 +986,17 @@
protected function renderTableView():void
{
+ if ($this->isTimeline) {
+ $this->renderTimelineTableView();
+ return;
+ }
?>
<template class="contentTable">
<form class="table"
data-save="content"
data-content="<?= esc_attr($this->content) ?>"
data-form-id="content-table-<?= esc_attr($this->content) ?>">
-
+ <?= jvbFormStatus() ?>
<?= $this->renderTableActions() ?>
<table>
@@ -906,13 +1018,17 @@
* Render table row template
*/
protected function renderTableRow(): void {
+ if ($this->isTimeline) {
+ $this->renderTimelineTableGroup();
+ return;
+ }
?>
<template class="tableView">
<tr class="item">
<td class="select">
<?= $this->renderItemSelect() ?>
</td>
- <td class="status">
+ <td class="status" data-field="post_status">
<?= $this->renderStatusRadios() ?>
</td>
<?php
@@ -938,6 +1054,102 @@
</template>
<?php
}
+
+ protected function renderTimelineTableView():void
+ {
+ ?>
+ <template class="contentTable">
+ <form class="table"
+ data-save="content"
+ data-content="<?= esc_attr($this->content) ?>"
+ data-form-id="content-table-<?= esc_attr($this->content) ?>">
+ <?= jvbFormStatus() ?>
+ <?= $this->renderTableActions() ?>
+
+ <table>
+ <thead>
+ <?= $this->renderTimelineTableHeader() ?>
+ </thead>
+ <!-- Rows are inserted as tbody groups -->
+ <tfoot>
+ <?= $this->renderTimelineTableHeader() ?>
+ </tfoot>
+ </table>
+ </form>
+ </template>
+ <?php
+ }
+
+ protected function renderTimelineTableGroup():void
+ {
+ $makeDetails = [
+ 'group',
+ 'repeater',
+ 'checkbox',
+ 'radio'
+ ];
+ ?>
+ <template class="tableView">
+ <tbody class="item">
+ <tr class="shared">
+ <td class="select">
+ <?= $this->renderItemSelect() ?>
+ </td>
+ <td class="show-post_status field" data-field="post_status">
+ <?= $this->renderStatusRadios() ?>
+ </td>
+ <?php
+ foreach ($this->fields as $name => $config) {
+ if(array_key_exists('hidden', $config) || $name === 'post_status') {
+ continue;
+ }
+ if (!in_array($name, $this->timelineSharedFields)) {
+ echo '<td></td>';
+ continue;
+ }
+ $makeThisDetailed = (in_array($config['type'], $makeDetails));
+ ?>
+ <td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+ <?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+ <?php $this->meta->render('form', $name, $config); ?>
+ <?= $makeThisDetailed ? '</details>' : '' ?>
+ </td>
+ <?php
+ }
+
+ ?>
+ </tr>
+ <tr class="timeline-point">
+ <td class="select">
+ <button class="drag-handle" title="Drag to reorder" aria-label="Drag to reorder this timeline point"><?= jvbIcon('dots-six') ?></button>
+ </td>
+ <td class="show-post_status field" data-field="post_status">
+ <?= $this->renderStatusRadios() ?>
+ </td>
+ <?php
+ foreach ($this->fields as $name => $config) {
+ if(array_key_exists('hidden', $config) || $name === 'post_status') {
+ continue;
+ }
+ if (!in_array($name, $this->timelineUniqueFields)) {
+ echo '<td></td>';
+ continue;
+ }
+ $makeThisDetailed = (in_array($config['type'], $makeDetails));
+ ?>
+ <td class="field show-<?= esc_attr($name) ?>" data-field="<?= esc_attr($name) ?>" data-field-type="<?=$config['type']?>"<?=(in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+ <?= $makeThisDetailed ? '<details><summary class="row btw">See Value</summary>' : '' ?>
+ <?php $this->meta->render('form', $name, $config); ?>
+ <?= $makeThisDetailed ? '</details>' : '' ?>
+ </td>
+ <?php
+ }
+ ?>
+ </tr>
+ </tbody>
+ </template>
+ <?php
+ }
/**
* Render status radio buttons
*/
@@ -952,7 +1164,7 @@
if ($status === 'future') {
$status = 'publish';
$config = [
- 'icon' => 'publish',
+ 'icon' => 'eye',
'label' => 'Live'
];
} elseif ($status === 'past') {
@@ -999,6 +1211,32 @@
return ob_get_clean();
}
+ protected function renderTimelineTableHeader(): string {
+ ob_start();
+
+ ?>
+ <tr>
+ <th scope="col" class="select-header">
+ <input type="checkbox" id="select-all" name="select-all">
+ <label for="select-all">All</label>
+ </th>
+ <th scope="col" class="show-post_status">
+ Status
+ </th>
+ <?php foreach ($this->fields as $name => $config):
+ if (array_key_exists('hidden', $config) || $name === 'post_status'){
+ continue;
+ }
+ ?>
+ <th scope="col" class="show-<?= esc_attr($name) ?>"<?= (in_array($name, $this->stuck)) ? ' data-stuck':''?>>
+ <?= esc_html($config['label']) ?>
+ </th>
+ <?php endforeach; ?>
+ </tr>
+ <?php
+ return ob_get_clean();
+ }
+
/**
* Render table action controls
*/
@@ -1010,16 +1248,29 @@
'vertical',
'TAB NAV:',
'',
- jvbIcon('down'),
- jvbIcon('right')
+ jvbIcon('caret-double-down'),
+ jvbIcon('caret-double-right')
) ?>
<button type="button" class="add-row" title="Add new row">
- <?= jvbIcon('add') ?>
+ <?= jvbIcon('plus-square') ?>
<span>Add Row</span>
</button>
</div>
<?php
return ob_get_clean();
}
+
+ public function createItem(array $actions):array
+ {
+ ob_start();
+ $this->renderCreateModal();
+ $content = ob_get_clean();
+ $create = [
+ 'button' => '<button type="button" class="create-item row" title="Create New '.$this->singular.'">'.jvbIcon('plus-square').'<span class="screen-reader-text">Create New '.$this->singular.'</span></button>',
+ 'content' => $content,
+ ];
+ $actions[] = $create;
+ return $actions;
+ }
}
--
Gitblit v1.10.0