Skip to main content

Page With A Table

A table page follows the same <Node> pattern as a custom page, but the backend returns an EloquentTableNode.

packages/framework/src/system/resources/js/pages/app/auth/ApiTokensPage.tsx is intentionally tiny:

import { Node } from 'qore-next'

export default function ApiTokensPage() {
return <Node endpoint='settings/api-tokens-page' />
}

The interesting work happens in ApiTokenController: it builds a page, adds a token creation form, and renders an EloquentTableNode for the current user's tokens.

Backend Route

Route::group(['prefix' => 'settings'], function () {
Route::post('api-tokens-page', [ApiTokenController::class, 'page']);
Route::post('api-tokens', [ApiTokenController::class, 'store'])
->name('settings.api-tokens.store');
Route::post('api-tokens/delete-page', [ApiTokenController::class, 'deletePage'])
->name('settings.api-tokens.delete-page');
Route::delete('api-tokens', [ApiTokenController::class, 'delete'])
->name('settings.api-tokens.delete');
});

The page endpoint returns nodes. The store and delete endpoints handle form submissions.

Page Layout

public function page(): JsonResponse
{
return qore()->page(__('qore::common.api_tokens.title'), function (PageNode $page) {
$page->addDefaultPadding();

$page->addFlex(function (FlexNode $flex) {
$flex->setIsVertical()
->setGap(SizeType::MIDDLE);

$flex->addCard(function (CardNode $card) {
$card->setTitle(__('qore::common.api_tokens.create'));
$card->addChild($this->getCreateTokenForm());
});

$flex->addCard(function (CardNode $card) {
$card->setTitle(__('qore::common.api_tokens.title'));
$card->addChild($this->getTokenTable());
});
});
})->toResponse();
}

The frontend still receives one node response, but the page can contain forms, copyable values, tables, modals and subnodes.

Table Node

use Laravel\Sanctum\PersonalAccessToken;
use Qore\Next\System\Fields\CreatedAtField;
use Qore\Next\System\Fields\DateField;
use Qore\Next\System\Fields\TextField;
use Qore\Next\System\Node\Table\TableSortOrder;
use Qore\Next\System\Nodes\EloquentTableNode;

private function getTokenTable(): EloquentTableNode
{
return (new EloquentTableNode(
id: 'api-tokens-table',
fields: [
TextField::make('name', __('qore::common.name')),
DateField::make('last_used_at', __('qore::common.api_tokens.last_used'))
->setHasTime(true),
DateField::make('expires_at', __('qore::common.api_tokens.expires_at'))
->setHasTime(true),
CreatedAtField::make(label: __('qore::common.api_tokens.created_at')),
],
query: PersonalAccessToken::query()
->whereMorphedTo('tokenable', user()),
bulkActions: [$this->getDeleteBulkAction()],
))
->setInitialSort('created_at', TableSortOrder::DESC)
->setHasSettings(false);
}

The table owns pagination, filtering, sorting and selected row state through the node lifecycle. Give it a stable id so user interactions and reloads target the same table.

Bulk Actions

Bulk actions pair a dropdown item with a modal. The modal can contain a SubNode, which lets the confirmation form load independently from the table.

private function getDeleteBulkAction(): TableBulkAction
{
$item = new DropdownItemNode(
label: __('qore::common.delete'),
icon: 'trash2',
overlayId: 'api-tokens-delete-modal',
isDanger: true
);

$modal = (new ModalNode('api-tokens-delete-modal', __('qore::common.delete')))
->addChild(new SubNode(
endpoint: route('settings.api-tokens.delete-page'),
closeOverlayOnFormSuccess: 'api-tokens-delete-modal',
reloadNodeByIdOnFormSuccess: 'settings/api-tokens-page',
id: 'api-tokens-delete-page',
));

return new TableBulkAction($item, $modal);
}

Use a resource when the table is a normal CRUD index. Use a custom table page when the table belongs to a special workflow, settings page or non-resource model.