title('Your Referrals', 'Track and manage your referral links') * ->addFilter('date') * ->addFilter('status', ['active', 'pending', 'expired']) * ->addView('grid') * ->addView('table') * ->dataSource([$this, 'getReferrals']) * ->render(); */ class CRUDSkeleton { protected WP_User $user; protected int $user_id; // Core configuration protected string $title = ''; protected string $description = ''; protected string $dataType = ''; protected string $singular = ''; protected string $plural = ''; protected string $icon; // Capabilities protected array $caps = []; private array $allowedCaps = ['view','edit', 'create', 'delete']; protected bool $userCanPublish = false; // Features protected array $filters = []; protected array $taxonomies = []; protected array $views = []; protected array $defaultViews = ['grid', 'list', 'table']; protected string $defaultView = 'grid'; protected bool $hasSearch = false; protected array $itemActions = []; protected array $defaultItemActions = [ 'edit' => [ 'title' => 'Edit', 'icon' => 'pencil-simple' ], 'trash'=> [ 'title' => 'Scrap', 'icon' => 'trash' ] ]; protected bool $isTimeline = false; protected array $nonTimelineFields = []; protected array $timelineSharedFields = []; protected array $timelineUniqueFields = []; protected bool $isCalendar = false; protected bool $useCRUDjs = true; //Bulk Actions protected array $bulkActions = []; private array $allowedBulkActions = ['edit', 'publish', 'draft', 'copy', 'trash']; protected array $defaultBulkActions = [ 'edit' => 'Edit', 'publish' => 'Show', 'draft' => 'Hide', 'copy' => 'Duplicate', 'trash' => 'Scrap' ]; protected array $fields = []; protected array $sections = []; protected array $statuses = []; protected array $allowedStatuses = [ 'all' => [ 'label' => 'Everything', 'icon' => 'infinity' ], 'publish' => [ 'label' => 'Visible', 'icon' => 'eye', ], 'draft' => [ 'label' => 'Hidden', 'icon' => 'eye-slash' ], 'trash' => [ 'label' => 'Deleted', 'icon' => 'trash' ], 'future' => [ 'label' => 'Upcoming', 'icon' => 'clock-clockwise', ], 'past' => [ 'label' => 'Past', 'icon' => 'clock-counter-clockwise', ], 'repeat' => [ 'label' => 'Recurring', 'icon' => 'repeat', ] ]; protected array $defaultStatus = ['all', 'publish', 'draft', 'trash']; protected array $defaultCalendarStatus = ['all', 'future', 'past', 'repeat', 'draft', 'trash']; protected ?array $uploaderConfig = null; // Data protected $dataSourceCallback = null; protected array $templates = []; // UI Options protected array $stuck = []; // Fields that stick when scrolling protected bool $showHeader = true; protected bool $showBulkControls = true; protected bool $showFilters = true; protected array $customDateRanges = []; protected array $additionalClasses = []; protected Registrar $registrar; public function __construct() { $this->icon = jvbDefaultIcon(); $this->user = wp_get_current_user(); $this->user_id = $this->user->ID; } /** * Set the title and optional description */ public function title(string $title, string $description = ''): self { $this->title = $title; $this->description = $description; return $this; } /** * Set content type information */ public function content(string $type, string $singular, string $plural): self { $this->dataType = $type; $this->registrar = Registrar::getInstance($type); $this->singular = $singular; $this->plural = $plural; return $this; } /** * Add a filter to the interface * * @param string $type Built-in types: 'status', 'date', 'author', or custom * @param mixed $config Configuration array or callable */ public function addFilter(string $type, $config = []): self { if ($type === 'status' && empty($config)) { $config = $this->getDefaultStatuses(); } elseif ($type === 'date' && empty($config)) { $config = [ 'label' => 'Date', 'icon' => 'calendar' ]; } $this->filters[$type] = $config; return $this; } /** * Add a date filter * * @param string $field The field to filter on (default: 'post_date') * @param ?array $ranges Available date ranges */ public function addDateFilter(string $field = 'post_date', ?array $ranges = null): self { if ($ranges === null) { $ranges = ['today' => 'Today', 'week' => 'This Week', 'this-month' => 'This Month', 'last-month' => 'Last Month', 'quarter' => 'The Last 3 Months', 'past-year' => 'the Last Year', 'custom' => 'Custom Range']; } $this->filters['date'] = [ 'type' => 'date', 'field' => $field, 'ranges' => $ranges, 'label' => 'Date', 'icon' => 'calendar' ]; return $this; } public function addCustomDateRange(array $ranges):self { $ranges = array_filter($ranges); $ranges = array_filter($ranges, function($range) { return is_string($range); }); $this->customDateRanges = $ranges; return $this; } /** * Add taxonomy filters * * @param array $taxonomies Array of taxonomy slugs to filter by * @param string|null $limit 'user' to limit to current user's terms, null for all */ public function addTaxonomyFilter(array $taxonomies, ?string $limit = null): self { foreach($taxonomies as $taxonomy) { $registrar = Registrar::getInstance($taxonomy); $this->taxonomies[$taxonomy] = [ 'type' => 'taxonomy', 'taxonomy'=> $taxonomy, 'limit' => $limit, 'label' => $registrar->getPlural(), 'icon' => $registrar->getIcon() ]; } return $this; } protected function taxConfig(string $taxonomy, string $label = ''):array { $isVerified = jvbUserIsVerified(); $label = ($label === '') ? Registrar::getInstance($taxonomy)->getPlural() : $label; return [ 'type' => 'taxonomy', 'label' => $label, 'taxonomy' => $taxonomy, 'createNew' => $isVerified, 'multiple' => true, 'mode' => 'append', ]; } public function addSearch():self { $this->hasSearch = true; return $this; } /** * Add a view type (grid, table, list, timeline) */ public function addViews(?array $views = null):self { if (!$views) { $views = $this->defaultViews; } $this->views = $views; return $this; } /** * Set the default view */ public function defaultView(string $type): self { $this->defaultView = $type; return $this; } /***************************************************** * ITEM ACTIONS *****************************************************/ public function addItemActions(array $actions = ['edit', 'trash']):self { if(!empty($actions)) { $this->itemActions = $actions; } return $this; } public function defineItemAction(string $action, array $definition):self { $config = array_key_exists($action, $this->defaultItemActions) ? $this->defaultItemActions[$action] : []; $config = array_merge($config, $definition); $this->defaultItemActions[$action] = $config; return $this; } /** * Configure the uploader */ public function addUploader(array $config): self { $this->uploaderConfig = array_merge([ 'type' => 'upload', 'subtype' => 'image', 'mode' => 'selection', 'multiple' => true, 'label' => 'Upload Files', ], $config); return $this; } public function useCRUDjs(bool $use = true):self { $this->useCRUDjs = false; return $this; } public function setCalendar():self { $this->isCalendar = true; return $this; } public function setDefaultStatus():self { if ($this->isCalendar) { $this->statuses = $this->defaultCalendarStatus; }else { $this->statuses = $this->defaultStatus; } return $this; } /************************************************** * TIMELINE SHORTCUTS **************************************************/ public function setTimeline():self { $this->isTimeline = true; return $this; } public function maybeSetupTimeline():void { 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_title'); $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); } /************************************************** * CAPABILITIES * Changes output depends on capabilities. * View -> only lists data * Edit -> can edit data * Create -> can create data * delete -> can delete data *************************************************/ public function addCapabilities(?array $capabilities = null):self { if (!$capabilities) { $capabilities = $this->allowedCaps; } $capabilities = array_filter($capabilities, function ($cap) { return in_array($cap, $this->allowedCaps); }); $this->caps = $capabilities; return $this; } public function userCanPublish(bool $can = false):self { $this->userCanPublish = $can; return $this; } /************************************************** * BULK ACTIONS * addBulkActions() -> adds default bulk actions * addBulkActions(['edit','delete']) -> adds edit/delete * setActionLabel('edit', 'Modify') -> change the edit action's label to 'Modify' *************************************************/ public function addBulkActions(?array $actions = null):self { if ($actions === null) { $actions = array_keys($this->defaultBulkActions); } $actions = array_filter($actions, function($item) { return in_array($item, $this->allowedBulkActions); }); $temp =[]; foreach ($actions as $action) { $temp[$action] = $this->defaultBulkActions[$action]; } $this->bulkActions = $temp; return $this; } public function setActionLabel(string $key, string $label): self { if (array_key_exists($key, $this->bulkActions)) { $this->bulkActions[$key] = $label; } return $this; } /** * Add a single field */ public function addField(string $name, array $config): self { $this->fields[$name] = $config; return $this; } /** * Set all fields at once */ public function setFields(array $fields): self { $this->fields = $fields; $this->maybeSetupTimeline(); return $this; } /** * Add a section for organizing fields */ public function addSection(string $id, array $config): self { $this->sections[$id] = $config; return $this; } /** * Set custom statuses */ public function setStatuses(array $statuses): self { $this->statuses = $statuses; return $this; } /** * Mark fields that should stick when scrolling */ public function stickFields(array $fieldNames): self { $this->stuck = array_merge($this->stuck, $fieldNames); return $this; } /** * Set the data source callback * Callback should accept filters and return array of items */ public function dataSource(callable $callback): self { $this->dataSourceCallback = $callback; return $this; } /** * Add a custom template */ public function addTemplate(string $name, string $template): self { $this->templates[$name] = $template; return $this; } /** * Add CSS classes to the wrapper */ public function addClasses(array $classes): self { $this->additionalClasses = array_merge($this->additionalClasses, $classes); return $this; } /** * Toggle UI elements */ public function showHeader(bool $show = true): self { $this->showHeader = $show; return $this; } public function showBulkControls(bool $show = true): self { $this->showBulkControls = $show; return $this; } public function showFilters(bool $show = true): self { $this->showFilters = $show; return $this; } /** * Build the configuration array */ public function build(): array { return [ 'title' => $this->title, 'description' => $this->description, 'dataType' => $this->dataType, 'singular' => $this->singular, 'plural' => $this->plural, 'filters' => $this->filters, 'views' => $this->views, 'defaultView' => $this->defaultView, 'bulkActions' => $this->bulkActions, 'fields' => $this->fields, 'sections' => $this->sections, 'statuses' => $this->statuses, 'uploaderConfig' => $this->uploaderConfig, 'stuck' => $this->stuck, 'showHeader' => $this->showHeader, 'showBulkControls' => $this->showBulkControls, 'showFilters' => $this->showFilters, 'additionalClasses' => $this->additionalClasses, ]; } /** * Render the CRUD interface */ public function render(): void { $config = $this->build(); $classes = array_merge(['dashboard-page', $this->dataType], $this->additionalClasses); // ob_start(); ?>
showHeader) { $this->renderHeader(); } $this->renderContent(); $this->renderModals(); $this->renderTemplates(); ?>

title) ?>

description)) { ?>

description) ?>

uploaderConfig) { $this->renderUploader(); } do_action('jvb_crud_after_header', $this->dataType, $this); } /** * Render uploader section */ protected function renderUploader(): void { ?>
uploaderConfig['label'] ?? 'Upload Files') ?> dataType, '', $this->uploaderConfig ); ?>
useCRUDjs ? '' : ' data-ignore'; ?>
>
renderControlsAndFilters(); if ($this->showBulkControls) { $this->renderBulkActions(); } ?>
showFilters) { return; } ?>
Filters renderSearch(); $this->renderViewControls(); $this->renderStatusControls(); $this->renderOrderControls(); $this->renderFilters(); if (in_array('table', $this->views)) { $this->renderColumnSelector(); } ?>
hasSearch){ return; } ?> views) || count($this->views) === 1){ return; } ?>
View: ['icon' => 'squares-four', 'label' => 'Grid View'], 'list' => ['icon' => 'rows', 'label' => 'List View'], 'table' => ['icon' => 'table', 'label' => 'Table View'], ]; foreach ($this->views as $index => $view) { $first = $index === 0; ?> >
statuses) || count($this->statuses) === 1) { return; } ?>
Status: statuses as $status) { if (!array_key_exists($status, $this->allowedStatuses)) { continue; } $config = $this->allowedStatuses[$status]; $checked = ($i == 1) ? ' checked' : ''; ?> >
[ 'date' => 'Order by date created', 'alphabetical' => 'Order alphabetically' ], 'order' => [ 'sort-ascending' => 'In ascending order (Z-A, oldest to newest)', 'sort-descending' => 'In descending order (A-Z, newest to oldest)' ] ]; foreach ($order as $o => $option) { ?>
: $label) { $icon = $opt === 'date' ? 'calendar' : $opt; $value = $opt; $value = ($value === 'sort-ascending') ? 'asc' : $value; $value = ($value === 'sort-descending') ? 'desc' : $value; ?> >
showFilters || empty($this->filters)) { return; } ?>
Filters: filters as $key => $config) { $type = $config['type'] ?? $key; switch ($type) { case 'date': $this->renderDateFilter($config); break; default: // Custom filter - allow override do_action('jvb_crud_render_filter_' . $type, $config, $this); break; } } foreach ($this->taxonomies as $config) { $this->renderTaxonomyFilter($config); } ?>
customDateRanges)) { ob_start(); ?>
getCommonTerms($taxonomy, $limit); $label = $config['label'] ?? 'Categories'; $out = ''; if (!empty($terms)) { $out .= sprintf( '
'; } echo $out; } /** * Get common terms for taxonomy * @param string $taxonomy * @return array */ protected function getCommonTerms(string $taxonomy, ?string $limit = null):array { if ($limit) { if ($limit === 'user') { $manager = new UserTermsManager(); return $manager->getUserTerms($this->user_id, $taxonomy); } else { $limit = (int)$limit; } } $args = [ 'taxonomy' => jvbCheckBase($taxonomy), 'hide_empty' => true, 'orderby' => 'name', ]; if ($limit) { $args['number'] = $limit; } return get_terms($args); } protected function renderColumnSelector():void { ob_start(); ?> bulkActions)) { return; } ?>
caps as $cap) { switch ($cap) { case 'create': $this->renderCreateModal(); break; case 'edit': $this->renderEditModal(); if (!empty($this->bulkActions)) { $this->renderBulkEditModal(); } break; } } do_action('jvb_crud_render_modals', $this->dataType, $this); } /** * Render templates (can be overridden) */ protected function renderTemplates(): void { $templates = $this->templates; foreach ($this->views as $view) { if (array_key_exists($view, $templates)) { echo $templates[$view]; unset($templates[$view]); } else { switch ($view) { case 'table': $this->renderTableTemplate(); $this->renderTableRowTemplate(); break; case 'grid': $this->renderGridTemplate(); break; case 'list': $this->renderListTemplate(); break; } } } if ($this->isTimeline && !array_key_exists('timeline', $templates)) { $temp = array_filter($this->fields, function ($field) { return in_array($field, $this->timelineUniqueFields); }, ARRAY_FILTER_USE_KEY); echo ''; } if (!array_key_exists('empty', $templates)) { $state = apply_filters('jvbEmptyState', $this->renderEmptyState(), $this->dataType); echo ''; } if (!array_key_exists('galleryPreview', $templates)) { $this->renderGalleryPreviewTemplate(); } foreach ($templates as $name => $template) { echo $template; } do_action('jvb_crud_render_templates', $this->dataType, $this); } protected function renderEmptyStateTemplate():void { $state = apply_filters('jvbEmptyState', $this->renderEmptyState(), $this->dataType); echo ''; } protected function renderEmptyState():string { ob_start(); ?>

icon)?>Nothing hereicon)?>

It doesn't look like you have any plural ?> yet.

Add many by uploading images above., or click the "" button to add one at a time.

'; } protected function renderItemSelect():string { ob_start(); ?>
'; } protected function renderItemActions():string { if (empty($this->itemActions)) { return ''; } ob_start(); ?>
itemActions as $action) { $config = $this->defaultItemActions[$action]; $title = (array_key_exists('title', $config)) ? ' title="'.$config['title'].' '.$this->singular.'"' : ''; $icon = (array_key_exists('icon', $config)) ? jvbIcon($config['icon']) : ''; ?>
isTimeline) { $this->renderTimelineTableView(); return; } $permissions = ''; foreach ($this->caps as $cap) { $permissions .= ' data-'.$cap; } ?> isTimeline) { $this->renderTimelineTableGroup(); return; } ?> fields)) { ?> Status fields as $name => $config): if (array_key_exists('hidden', $config) || $name === 'status'){ continue; } ?> stuck)) ? ' data-stuck':''?>> itemActions)) { ?> Actions Status fields as $name => $config): if (array_key_exists('hidden', $config) || $name === 'post_status'){ continue; } ?> stuck)) ? ' data-stuck':''?>>
caps)) > 0) { ?>
statuses as $status): if ($status === 'all') continue; if (!array_key_exists($status, $this->allowedStatuses)) continue; $config = $this->allowedStatuses[$status]; ?>
[ 'icon' => 'infinity', 'label' => 'All', ], 'active' => [ 'icon' => 'check-circle', 'label' => 'Active', ], 'inactive' => [ 'icon' => 'x-circle', 'label' => 'Inactive', ], ]; } /** * Get field configuration */ public function getFields(): array { return $this->fields; } /** * Get configuration value */ public function get(string $key) { return $this->$key ?? null; } /*************************************************** * MODALS ***************************************************/ protected function renderCreateModal():void { echo jvbNewModal( 'create', 'Creating New '.$this->singular, str_replace('edit-form"', 'create-form" data-noautosave', $this->editForm()) ); } protected function editForm():string { ob_start(); ?>
isTimeline) ? ' data-timeline' : ''?>>
getStatusFieldConfig('edit-')); if (!empty($this->sections)) { $tabs = []; foreach ($this->sections as $config) { $slug = $config['slug']; $section = []; if (array_key_exists('icon', $config)) { $section = [ 'icon' => $config['icon'] ]; } $tabs[$slug] = array_merge([ 'title' => $config['label'], 'content' => '', 'description' => $config['description']??'', ], $section); } } else { $tabs = false; } $fields = $this->fields; if (!$this->isTimeline) { $first = ['post_thumbnail', 'post_title', 'price']; foreach ($first as $f) { if (array_key_exists($f, $fields)) { if ($tabs) { $tabs['basic']['content'] .= Form::render($f, '', $fields[$f]); } else { echo Form::render($f, '', $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' => 'upload', 'subtype' => 'timeline', 'data' => 'timeline', 'label' => 'Progression', 'fields' => $temp ]; $content = ''; foreach ($fields as $slug=> $field) { if (in_array($slug, $this->timelineSharedFields)) { if (in_array($field['type'], ['taxonomy', 'selector'])) { $field = array_merge($field, $this->taxConfig($field['taxonomy'], $field['label'])); } $content .= Form::render($slug, '', $field); } } $content .= Form::render('timeline', '', $config); $tabs['progression']['content'] = $content; $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 { jvbDump($config, $n); echo Form::render($n, '', $config); } } if ($tabs) { jvbRenderTabs($tabs); } ?>
singular, $this->editForm() ); } protected function renderBulkEditModal():void { if (empty($this->bulkActions)) return; ob_start(); ?>

You can unselect items by clicking the image here.

IMPORTANT: Whatever changes you make here will be applied to all selected plural?>.

getStatusFieldConfig('bulk-')); if (!empty($this->taxonomies)) { ?>
taxonomies as $taxonomy => $config) { echo Form::render('bulk-edit-'.$taxonomy, '', $this->taxConfig($taxonomy, $config['label'])); } ?>
fields; $fields = array_filter($fields, function ($field) { return array_key_exists('bulkEdit', $field); }); foreach ($fields as $fieldName => $config) { echo Form::render($fieldName, '', $config); } ?>
'.$this->plural, $form ); } 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) { 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; } } $disabled = ($status === 'publish' && !$this->userCanPublish) ? ' disabled' : ''; ?> > 'group']); } }