Skip to main content

Pages And Tables

QoreResource includes default index, create, detail and edit pages. Each page is a node response, so you can keep the default behaviour or replace a layout with your own nodes.

Page Switches

Override these when a resource should not expose one of the standard pages:

public function hasCreatePage(): bool
{
return false;
}

public function hasEditPage(): bool
{
return false;
}

Available switches:

  • hasIndexPage(): bool
  • hasCreatePage(): bool
  • hasDetailPage(): bool
  • hasEditPage(): bool
  • hasTableSettings(): bool
  • isExportEnabled(): bool

The controllers and generated URLs respect these switches, but policies still decide access for the current user.

Page Layouts

The default layouts are intentionally small:

  • index renders getResourceTable()->toNode();
  • create renders a card with getCreateForm();
  • detail renders a detail card and relation table tabs;
  • edit renders a card with getEditForm($model).

Use formFieldLayout(array $fields): ?Node when create and edit should share a custom field arrangement but the default page chrome, card, submit footer and form behaviour should remain intact. Return null to use the default vertical field layout.

Override the layout method when the default structure is not enough:

use Illuminate\Database\Eloquent\Model;
use Qore\Next\System\Node\Node;
use Qore\Next\System\Node\SizeType;
use Qore\Next\System\Nodes\CardNode;
use Qore\Next\System\Nodes\FlexNode;
use Qore\Next\System\Nodes\TextNode;

public function detailLayout(Model $model): Node
{
return (new FlexNode)
->setIsVertical()
->setGap(SizeType::MIDDLE)
->addChild(
(new CardNode(__('common.summary')))
->addChild(new TextNode($model->description))
)
->addChild($this->getDefaultDetailLayoutCard($model));
}

Use getDefaultDetailLayoutCard($model) when you want to add content around the standard detail table instead of replacing it completely.

Fields Per Page

Field visibility controls which fields appear on each page. The resource collects those fields through:

  • getFieldsForIndex(): Field[]
  • getFieldsForCreate(): Field[]
  • getFieldsForDetail(Model $model): Field[]
  • getFieldsForEdit(Model $model): Field[]

Use field-level visibility when the decision belongs to the field:

TextField::make('internal_note')
->setIsShownOnIndex(fn () => false)
->setIsShownOnDetail(fn (Product $product) => $product->has_notes);

Override the resource layout when the decision belongs to the page composition.

Index Query

indexQuery() is the base query for the resource index, exports, relation options and other resource-owned lists.

use Illuminate\Database\Eloquent\Builder;

public function indexQuery(): Builder
{
return parent::indexQuery()
->where('tenant_id', tenant()->getKey())
->with('owner');
}

Keep authorization in policies. Use indexQuery() for application scoping, default eager loading and default ordering that belongs to this resource.

Index Statistics

indexStatistics() optionally adds StatisticNode blocks above the resource index table. Use it for small summary metrics that belong with the list, such as totals, revenue, pending work or status counts.

use Qore\Next\System\Nodes\StatisticNode;

/**
* @return StatisticNode[]
*/
public function indexStatistics(): array
{
return [
new StatisticNode(__('common.products.total'), $this->indexQuery()->count()),
(new StatisticNode(__('common.products.active'), $this->indexQuery()->where('is_active', true)->count()))
->setIcon('check-circle'),
];
}

Return an empty array when the resource should not show index statistics. The default indexLayout() renders these statistics above getResourceTable()->toNode().

Index Tabs

indexTabs() adds tabs above the index table. Tabs are represented by TableTab classes and replace the table query when selected.

namespace App\Resources\Tabs;

use App\Models\Product;
use Illuminate\Database\Eloquent\Builder;
use Qore\Next\System\Node\Table\TableTab;

class ActiveProductsTab extends TableTab
{
public function label(): string
{
return __('common.active');
}

public function query(): Builder
{
return Product::query()->where('is_active', true);
}
}

Register tabs on the resource:

use App\Resources\Tabs\ActiveProductsTab;

public function indexTabs(): array
{
return [
new ActiveProductsTab,
];
}

Use tabs for common, mutually exclusive table scopes. Use normal filters for ad hoc searching and filtering. If the tab should share resource scoping or eager loading, call the same query-building logic from the tab's query() method.

Table Behaviour

The default index table comes from getResourceTable():

public function getResourceTable(): ResourceTable
{
return new ResourceTable(
id: "{$this->getName()}_index",
resource: $this,
exportEndpoint: $this->isExportEnabled() ? "resources/{$this->getName()}/export" : null,
);
}

Override table-related methods for common behaviour:

use Illuminate\Database\Eloquent\Model;
use Qore\Next\System\Node\Table\TableSortOrder;

public function getInitialSortField(): ?string
{
return 'created_at';
}

public function getInitialSortOrder(): TableSortOrder
{
return TableSortOrder::DESC;
}

public function getTableRowNavigateUrl(Model $model): ?string
{
return $this->getDetailUrl($model->getKey());
}

public function getTableRowModalUrl(Model $model): ?string
{
return null;
}

Use getTableRowNavigateUrl() for normal drill-down behaviour. Use getTableRowModalUrl() when a row should open a modal node endpoint instead of navigating.

Use tableRowClass() and tableColumnClass() when the resource index needs conditional styling per row or per cell. Return CSS utility classes from the closure:

use Closure;
use Illuminate\Database\Eloquent\Model;
use Qore\Next\System\Field\Field;

/**
* @return Closure(Model): string|null
*/
public function tableRowClass(): ?Closure
{
return fn (Model $model) => $model->is_archived ? 'text-red-100 dark:text-red-900' : null;
}

/**
* @return Closure(Model, Field): string|null
*/
public function tableColumnClass(): ?Closure
{
return fn (Model $model, Field $field) => $field->getName() === 'status' && $model->is_overdue
? 'text-red-500 text-weight-medium'
: null;
}

Export

Exports use the index fields by default:

public function getFieldNamesForExport(): array
{
return array_keys($this->getFieldsForIndex());
}

Override getExportQuery() when export needs a different query than the visible index table. Override getFieldNamesForExport() when some index fields should not be exported or extra export-only fields should be included.