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;
protected string $emptyState = '';
// 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 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;
}
public function icon(string $icon):void
{
$this->icon = $icon;
}
public function getIcon():string
{
return $this->icon;
}
/**
* Set content type information
*/
public function content(string $type, string $singular, string $plural): self {
$this->dataType = $type;
$registrar = Registrar::getInstance($type);
if ($registrar) {
$this->registrar = Registrar::getInstance($type)??null;
$this->sections = $this->registrar->getSections();
}
$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);
if ($registrar) {
$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 = $this->userIsVerified();
$label = ($label === '') ? Registrar::getInstance($taxonomy)->getPlural() : $label;
return [
'type' => 'taxonomy',
'label' => $label,
'taxonomy' => $taxonomy,
'createNew' => $isVerified,
'multiple' => true,
'mode' => 'append',
];
}
protected function userIsVerified():bool
{
$membership = Site::membership();
return !($membership && $membership->has('member_verified')) || current_user_can('skip_moderation');
}
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 || is_null($field['for_all'])){
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;
}
/**
* 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 {
$classes = array_merge(['dashboard-page', $this->dataType], $this->additionalClasses);
// ob_start();
?>
showHeader) {
$this->renderHeader();
}
$this->renderContent();
$this->renderModals();
$this->renderTemplates();
?>
= esc_html($this->title) ?>
description)) {
?>
= esc_html($this->description) ?>
uploaderConfig) {
$this->renderUploader();
}
do_action('jvb_crud_after_header', $this->dataType, $this);
}
/**
* Render uploader section
*/
protected function renderUploader(): void {
?>
= esc_html($this->uploaderConfig['label'] ?? 'Upload Files') ?>
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;
}
?>
Search:
= jvbSearch() ?>
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' : '';
?>
>
[
[
'slug' => 'date',
'label' => 'Date Created',
'icon' => 'calendar'
],
[
'slug' => 'alphabetical',
'label' => 'Alphabetically',
'icon' => 'alphabetical',
],
[
'slug' => 'date_modified',
'icon' => 'clock-clockwise',
'label' => 'Date Modified',
],
],
'order' => [
[
'slug' => 'desc',
'icon' => 'sort-descending',
'label' => 'Descending (A-Z, 1-10)'
],
[
'slug' => 'asc',
'icon' => 'sort-ascending',
'label' => 'Ascending (Z-A, 10-1)'
]
]
];
foreach ($order as $o => $option) {
?>
= ucfirst($o)?>:
>
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);
}
?>
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 (Site::has('membership') && $limit === 'user') {
$manager = new UserTermsManager();
return $manager->fetchUserTerms($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();
?>
= jvbDashIcon('columns') ?>
Toggle Columns
fields as $fieldName => $config):
if (array_key_exists('hidden', $config)){
continue;
}
?>
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 '';
echo Form::renderImagePreview(null, $temp);
echo '';
}
if (!array_key_exists('empty', $templates)) {
$state = apply_filters('jvbEmptyState', $this->renderEmptyState(), $this->dataType);
echo '' . $state . '';
}
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 ''.$state.'';
}
protected function renderEmptyState():string
{
return empty($this->emptyState) ? sprintf(
'
%sNothing here%s
It doesn\'t look like you have any %s yet.
Add many by uploading images above., or click the "%s" button to add one at a time.
',
jvbDashIcon($this->icon),
jvbDashIcon($this->icon),
$this->plural,
jvbDashIcon('plus-square')
) : $this->emptyState;
}
public function setEmptyState(string $state):void
{
$this->emptyState = $state;
}
protected function renderGalleryPreviewTemplate():void
{
echo '
';
}
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']) : '';
?>
= $this->renderItemActions(); ?>
= $this->renderItemSelect()?>
=$this->renderImage() ?>
= $this->renderItemActions()?>
isTimeline) {
$this->renderTimelineTableView();
return;
}
$permissions = '';
foreach ($this->caps as $cap) {
$permissions .= ' data-'.$cap;
}
?>
isTimeline) {
$this->renderTimelineTableGroup();
return;
}
?>
|
= $this->renderItemSelect() ?>
|
fields)){
?>
= $this->renderStatusRadios() ?>
|
fields as $name => $config):
if ((array_key_exists('hidden', $config) && $config['hidden'] === true) || $name === 'post_status'){
continue;
}
$makeThisDetailed = (in_array($config['type'], $makeDetails));
?>
stuck)) ? ' data-stuck':''?>>
caps)) {
echo $makeThisDetailed ? 'See Value' : '';
if (in_array($config['type'], ['selector', 'taxonomy', 'post'])) {
$config['autocomplete'] = true;
}
echo Form::render($name, '', $config);
echo $makeThisDetailed ? ' ' : '';
} else {
echo '';
}
?>
|
itemActions)) {
?>
= $this->renderItemActions(); ?>
|
|
= $this->renderItemSelect() ?>
|
= $this->renderStatusRadios() ?>
|
fields as $name => $config) {
if(array_key_exists('hidden', $config) || $name === 'post_status') {
continue;
}
if (!in_array($name, $this->timelineSharedFields)) {
echo ' | ';
continue;
}
$makeThisDetailed = (in_array($config['type'], $makeDetails));
?>
stuck)) ? ' data-stuck':''?>>
= $makeThisDetailed ? 'See Value' : '' ?>
= Form::render($name, '', $config); ?>
= $makeThisDetailed ? ' ' : '' ?>
|
|
|
= $this->renderStatusRadios() ?>
|
fields as $name => $config) {
if((array_key_exists('hidden', $config) && $config['hidden'] === true) || $name === 'post_status') {
continue;
}
if (!in_array($name, $this->timelineUniqueFields)) {
echo ' | ';
continue;
}
$makeThisDetailed = (in_array($config['type'], $makeDetails));
?>
stuck)) ? ' data-stuck':''?>>
= $makeThisDetailed ? 'See Value' : '' ?>
= Form::render($name, '', $config); ?>
= $makeThisDetailed ? ' ' : '' ?>
|
';
if (array_key_exists('post_status', $this->fields)){
$out .= '';
}
foreach ($this->fields as $name => $config) {
if ($name === 'post_status' || (array_key_exists('hidden', $config) && $config['hidden'] === true)) {
continue;
}
$out .= sprintf(
'%s | ',
esc_attr($name),
in_array($name, $this->stuck) ? ' data-stuck' : '',
esc_html($config['label'])
);
}
if (!empty($this->itemActions)) {
$out .= 'Actions | ';
}
$out .= '';
return $out;
}
protected function renderTimelineTableHeader(): string {
$out = '
|
Status
| ';
foreach ($this->fields as $name => $config) {
if ($name === 'post_status' || (array_key_exists('hidden', $config) && $config['hidden'] === true)) {
continue;
}
$out .= sprintf(
'%s | th>',
esc_attr($name),
in_array($name, $this->stuck) ? ' data-stuck':'',
esc_html($config['label'])
);
}
$out .= '
';
return $out;
}
/**
* Render table action controls
*/
protected function renderTableActions(): string {
ob_start();
?>
caps)) > 0) { ?>
= jvbRenderToggleTextField(
'vertical',
'TAB NAV:',
'',
jvbDashIcon('caret-double-down'),
jvbDashIcon('caret-double-right')
) ?>
';
foreach ($this->statuses as $status) {
if ($status === 'all' || !array_key_exists($status, $this->allowedStatuses)) continue;
$config = $this->allowedStatuses[$status];
$out .= sprintf(
'
',
esc_attr($status),
esc_attr($status),
esc_attr($status),
esc_html($config['label']),
jvbDashIcon($config['icon']),
esc_html($config['label'])
);
}
$out .= '
';
return $out;
}
/**
* Get default statuses
*/
protected function getDefaultStatuses(): array {
return [
'all' => [
'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',
'create',
'Creating New '.$this->singular,
str_replace('edit-form"', 'create-form" data-noautosave', $this->editForm())
);
}
protected function editForm():string
{
ob_start();
/*
singular,
$this->editForm()
);
}
protected function renderBulkEditModal():void
{
if (empty($this->bulkActions)) return;
ob_start();
/*
'.$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']);
}
public static function searchFilter():string
{
$self = new self();
$self->hasSearch = true;
ob_start();
$self->renderSearch();
return ob_get_clean();
}
public static function viewFilter(array $views):string
{
$self = new self();
$self->views = $views;
ob_start();
$self->renderViewControls();
return ob_get_clean();
}
public static function orderFilter(bool $random = false):string
{
$self = new self();
ob_start();
$self->renderOrderControls();
return ob_get_clean();
}
}