Skip to main content

Creating custom tables

There might be a lot of cases where you might want to create your own custom table. For example, you might need a table where you display all active sessions for a user on the user detail page.

Getting started

Let's start by adding a Tab on the User Resource:

Back-end

Creating the tab:

class SessionManagement extends Tab
{
public function title(): string
{
return __('Session management');
}

public function component(): string
{
return 'SessionManagement';
}
}

Registering the tab:

public function tabs(): TabCollection
{
return new TabCollection(
(new SessionManagement())
);
}

Front-end

Creating the component:


<template>
<div>
My custom tab
</div>
</template>

<script>
export default {
props: ['model']
}
</script>

Registering the component:

import SessionManagement from 'components/tabs/session_management/SessionManagement'

Vue.component('SessionManagement', SessionManagement)

Creating the table

Back-end

We can create a table by extending the Qore base Table:

use Qore\System\Http\Table\Table;

class SessionManagementTable extends Table
{
public bool $isSortable = true;

public string $sortDirection = 'descending';

public string $sortColumn = 'created_at';

private Model $user;

public function __construct(Request $request, User $model)
{
$this->user = $model;

parent::__construct($request);
}

public function columns(): ColumnCollection
{
return new ColumnCollection(
Column::make(__('Device'), 'device'),
Column::make(__('Location'), 'location'),
DateTime::make(__('Logged in'), 'created_at')
->sortable(true)
->filterable(true),
DateTime::make(__('Logged out'), 'deleted_at')
->sortable(true)
->filterable(true)
);
}

public function query(): Builder
{
return $this->user
->sessions()
->withTrashed()
->getQuery();
}

protected function responseArguments(): array
{
return [
'active_session_id' => $this->request->session()->getId()
];
}

protected function postProcess($item, ColumnCollection $columns, $type = 'display'): mixed
{
$process = parent::postProcess($item, $columns);

$process->agent = $item->agent;
$process->last_activity = $item->last_activity ? apply_timezone($item->last_activity) : null;
$process->last_activity_formatted = $item->last_activity ? date_time_presenter($item->last_activity) : null;
$process->created_at = apply_timezone($item->created_at);
$process->created_at_formatted = date_time_presenter($item->created_at);
$process->deleted_at = $item->deleted_at ? apply_timezone($item->deleted_at) : null;
$process->deleted_at_formatted = $item->deleted_at ? date_time_presenter($item->deleted_at) : null;

return $process;
}
}

As you can see, you can supply your own query, columns, default sorts and more.

The responseArguments defines all the data that should be sent back in the JSON response. In the postProcess method you may customize the data for each row. In this example we need the actual unformatted dates for the front-end.

Front-end

Qore provides a base-table component which may be used in your template:


<template>
<div>
<base-table
ref="table"
:endpoint="`users/${model.id}/session-management`"
@ready="loaded"
/>
</div>

</template>

<script>

export default {
props: ['model'],
data() {
return {
activeSessionId: null
}
},
mounted() {
this.$refs.table.reload()
},
methods: {
loaded(data) {
this.activeSessionId = data.active_session_id
}
}
}
</script>
caution

The <base-table> component will add (filters, sorting etc.) parameters to the URL by default. This might sometimes lead to conflict/unexpected behaviour. In that case please add the prop: disable-history

Creating the controller

In the front-end we do a request towards: users/${model.id}/session-management. Let's register a route:

Route::get('users/{userId}/session-management', [SessionManagementController::class, 'index']);

And our controller:

class SessionManagementController extends Controller
{
use AuthorizesRequests;

public function index(Request $request, $userId): JsonResponse
{
$user = User::findOrFail($userId);

$this->authorize('sessionManagement', $user);

return (new SessionManagementTable($request, $user))->make();
}
}

Data should now be be displayed in the table.

Using custom slots

You may use custom slots to define how a column should be shown in the front-end. For example, you may want to show an icon for the user device:


<template>
<div>
<base-table
ref="table"
:endpoint="`users/${model.id}/session-management`"
@ready="loaded"
>
<template v-slot:body-cell-device="{ props }">
<device-index
:row="props.row"/>
</template>
</base-table>
</div>

</template>

<script>

import DeviceIndex from './DeviceIndex.vue'

export default {
components: {
DeviceIndex
},
props: ['model']
}
</script>

The DeviceIndex component:

DeviceIndex.vue
<template>
<q-item class="q-pa-xs">
<q-item-section
avatar
class="q-pr-sm"
>
<q-icon
:name="icon"/>
</q-item-section>
<q-item-section>
<q-item-label>{{row.agent.browser}}, {{row.agent.platform}}</q-item-label>
<q-item-label
v-if="row.agent.browser_version"
caption
>{{$t('browser')}}: {{row.agent.browser_version}}
</q-item-label>
</q-item-section>
</q-item>
</template>

<script>
export default {
props: {
row: {
required: true,
type: Object
}
},
computed: {
icon() {
const type = this.row.agent.device_type

if (type === 'desktop') {
return 'desktop_windows'
}

if (type === 'phone') {
return 'smartphone'
}

if (type === 'tablet') {
return 'tablet'
}

return type
}
}
}
</script>

Exporting

Exporting is disabled by default on custom tables, however you can enable it by following these steps.

The BaseTable exposes its features (refresh, filters, settings, export etc.) via a Portal. You will have to add the following to your table:

  <div class="flex justify-end">
<portal-target :name="`actions-${someTableName}`" />
</div>

<base-table
endpoint="/test/table"
export-endpoint="/test/export"
:name="someTableName"
/>

The name (simply a string) is required for the portal to work.

Next as you can see we have a export-endpoint:

Route::get('/test/table', [\App\Http\Controllers\TestController::class, 'table']);
Route::post('/test/export', [\App\Http\Controllers\TestController::class, 'export']);

The export function will look something like this:

public function export(Request $request): mixed
{
$table = new TestTable($request);

return $table->export();
}

Note that exporting will be done synchronously and all records will be exported. Downloaded exports will also be added to the /processes page.

BaseTable props

Sometimes you want to disable specific features in a table.

Below are a some props you may want to edit:

/**
* The endpoint for the table
*/
endpoint: {
type: String,
required: true
}
/**
* Default sort column
*/
sortColumn: {
type: String
}
/**
* Default sort direction
*/
sortDirection: {
type: String
}
/**
* Whether rows can be clicked on
*/
clickable: {
type: Boolean,
default: false
}
/**
* If given, the name will be used in the store/database
* for history and caching
*/
name: {
required: false,
type: String
}
/**
* The resource.
*/
resource: {
required: false,
type: Object,
default: () => { }
}
/**
* Only for resources.
* Whether rows can be selected
*/
selectable: {
required: false,
type: Boolean,
default: false
}
/**
* Only for resources.
* The table index tabs
*/
tabs: {
required: false,
type: Array,
default: () => []
}
/**.
* Whether table exporting should be disabled
*/
disableExporting: {
type: Boolean,
default: false
}
/**.
* Whether table loader should be disabled
*/
disableLoader: {
type: Boolean,
default: false
}
/**
* Only for resources.
* Whether table filters should be enabled
*/
disableFilters: {
type: Boolean,
default: false
}
/**
* Only for resources.
* Whether table data should be stored in the store
*/
disableHistory: {
type: Boolean,
default: false
}
/**
* Used fixed layout for columns so columns have a fixed width
* This is true for resource index tables by default
*/
fixedLayout: {
type: Boolean,
default: function () { return this.isColumnEditable }
}
/**
* Make last column as wide as possible
*/
fillEmptySpace: {
type: Boolean,
default: true
}
/**
* Whether rows have on-hover actions
*/
hasRowHoverActions: {
type: Boolean,
default: false
}
/**
* Extra arguments to send within pagination
*/
appendToPagination: {
type: Object,
default: () => { }
}
/**
* Whether to have the footer sticky to the bottom of the page
*/
stickyBottom: {
type: Boolean,
default: false
}
/**
* Endpoint to use for exporting
*/
exportEndpoint: {
type: String,
required: false
}
/**
* Endpoint to use for export columns
*/
exportColumnsEndpoint: {
type: String,
required: false
}
/**
* File name to use for exporting
*/
exportFileName: {
type: String,
required: false
}