---
name: unopim-datagrid
description: >
Implement Unopim DataGrid classes for listing records in admin panels with
search, filter, sort, actions (edit/delete), and mass actions. Covers
DataGrid subdirectory structure (DataGrids/Section/), prepareQueryBuilder
with DB::table and wk_ prefix, addColumn with correct closure patterns,
addAction with function callbacks for url, addMassAction, bouncer()
permission checks, and Blade integration. Use this skill when creating a
DataGrid for credentials, mappings, export history, or any Unopim module
listing page.
version: "2.0.0"
tags: [unopim, datagrid, laravel, admin, listing, connector, integration]
---
# Unopim DataGrid
## Overview
All DataGrid patterns are derived from the WooCommerce connector reference:
`DataGrids/Credential/CredentialDataGrid.php`.
**Critical rules:**
- DataGrid lives in a **subdirectory**: `DataGrids/{Section}/{Name}DataGrid.php`
- Methods use **PHPDoc `@return`** only — no PHP return type hints on methods
- `closure` callbacks in columns use PHP arrow functions `fn ($row) =>`
- `url` callbacks in actions use **regular function**: `function ($row) { return ...; }`
- Table names use `wk_` prefix in `DB::table()`
- Use `bouncer()->hasPermission()` for action permission checks
- Status badges: `class="label-active"` (true) and `class="label-info text-gray-600 dark:text-gray-300"` (false)
**Admin UI rule (critical):**
- If the listing page includes form/filter controls in Blade, use UnoPim form components.
- Do not generate raw `<select>`, `<input>`, `<textarea>`, or `<label>` controls when `x-admin::form.*` components exist.
- Keep labels/placeholders/messages in translations.
---
## 1. Class Structure
```php
<?php
// src/DataGrids/Credential/CredentialDataGrid.php
// ^^^^^^^^^ — always a subdirectory named after the entity
namespace Webkul{ModuleName}DataGridsCredential;
use IlluminateSupportFacadesDB;
use WebkulDataGridDataGrid;
class CredentialDataGrid extends DataGrid
{
/**
* Prepare query builder.
*
* @return IlluminateDatabaseQueryBuilder
*/
public function prepareQueryBuilder()
{
// Use DB::table() with wk_ prefix — never Eloquent model here
$queryBuilder = DB::table('wk_{module}_credentials')
->select(
'id',
'label',
'apiUrl',
'consumerKey',
'status',
'created_at',
);
return $queryBuilder;
}
/**
* Add columns.
*
* @return void
*/
public function prepareColumns()
{
$this->addColumn([
'index' => 'label',
'label' => trans('{module-name}::app.credentials.datagrid.label'),
'type' => 'string',
'searchable' => true,
'filterable' => true,
'sortable' => true,
]);
$this->addColumn([
'index' => 'apiUrl',
'label' => trans('{module-name}::app.credentials.datagrid.api-url'),
'type' => 'string',
'searchable' => true,
'filterable' => true,
'sortable' => true,
]);
$this->addColumn([
'index' => 'consumerKey',
'label' => trans('{module-name}::app.credentials.datagrid.consumer-key'),
'type' => 'string',
'searchable' => false,
'filterable' => false,
'sortable' => false,
]);
// Boolean column: use arrow function `fn ($row) =>` for closure
// Status badges: label-active (true) vs label-info (false)
$this->addColumn([
'index' => 'status',
'label' => trans('{module-name}::app.credentials.datagrid.status'),
'type' => 'boolean',
'searchable' => true,
'filterable' => true,
'sortable' => false,
'closure' => fn ($row) => $row->status
? '<span class="label-active">' . trans('admin::app.common.yes') . '</span>'
: '<span class="label-info text-gray-600 dark:text-gray-300">' . trans('admin::app.common.no') . '</span>',
]);
}
/**
* Prepare actions.
*
* @return void
*/
public function prepareActions()
{
// Use bouncer()->hasPermission() for ACL checks
if (bouncer()->hasPermission('{module-slug}.credentials.edit')) {
$this->addAction([
'icon' => 'icon-edit',
'title' => trans('admin::app.catalog.attributes.index.datagrid.edit'),
'method' => 'GET',
// url uses regular function — NOT arrow function
'url' => function ($row) {
return route('{module-slug}.credentials.edit', $row->id);
},
]);
}
if (bouncer()->hasPermission('{module-slug}.credentials.delete')) {
$this->addAction([
'icon' => 'icon-delete',
'title' => trans('admin::app.catalog.attributes.index.datagrid.delete'),
'method' => 'DELETE',
'url' => function ($row) {
return route('{module-slug}.credentials.destroy', $row->id);
},
]);
}
}
/**
* Prepare mass actions.
*
* @return void
*/
public function prepareMassActions()
{
if (bouncer()->hasPermission('{module-slug}.credentials.mass-delete')) {
$this->addMassAction([
'title' => trans('admin::app.catalog.attributes.index.datagrid.delete'),
'url' => route('{module-slug}.credentials.mass_delete'),
'method' => 'POST',
'options' => ['actionType' => 'delete'],
]);
}
if (bouncer()->hasPermission('{module-slug}.credentials.mass-edit')) {
$this->addMassAction([
'title' => trans('admin::app.catalog.products.index.datagrid.update-status'),
'url' => route('{module-slug}.credentials.mass_update'),
'method' => 'POST',
'options' => [
[
'label' => trans('admin::app.catalog.products.index.datagrid.active'),
'value' => true,
],
[
'label' => trans('admin::app.catalog.products.index.datagrid.disable'),
'value' => false,
],
],
]);
}
}
}
```
---
## 2. Callback Patterns: closure vs url
| Location | Function style | Why |
|---|---|---|
| `addColumn` `closure` key | Arrow function `fn ($row) =>` | Short rendering expression |
| `addAction` `url` key | `function ($row) { return ...; }` | Must `return` the URL string |
| `addMassAction` `url` key | Plain string `route(...)` | Static URL, no row context |
### Column closure (arrow function OK):
```php
'closure' => fn ($row) => $row->status ? '<span class="label-active">Yes</span>' : '<span class="label-info ...">No</span>',
```
### Action URL (regular function required):
```php
'url' => function ($row) {
return route('{module-slug}.credentials.edit', $row->id);
},
```
### Mass action URL (static — no function):
```php
'url' => route('{module-slug}.credentials.mass_delete'),
```
---
## 3. Controller Integration
```php
public function index()
{
if (request()->ajax()) {
return app(CredentialDataGrid::class)->toJson();
}
return view('{module-name}::credentials.index');
}
```
---
## 4. Blade View
```blade
{{-- resources/views/credentials/index.blade.php --}}
@extends('admin::layouts.master')
@section('page_title')
{{ __( '{module-name}::app.credentials.index.title' ) }}
@stop
@section('content-wrapper')
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between gap-4">
<p class="text-xl font-bold text-gray-800 dark:text-white">
{{ __(' {module-name}::app.credentials.index.title') }}
</p>
@if (bouncer()->hasPermission('{module-slug}.credentials.create'))
<a
href="{{ route('{module-slug}.credentials.create') }}"
class="primary-button"
>
{{ __(' {module-name}::app.credentials.index.create-btn') }}
</a>
@endif
</div>
{{-- DataGrid component renders the table --}}
<x-admin::datagrid :src="route('{module-slug}.credentials.index')" />
</div>
@stop
```
---
## 5. MappingDataGrid Example
DataGrids for other entities follow the same pattern in their own subdirectory:
```php
<?php
// src/DataGrids/Mapping/MappingDataGrid.php
namespace Webkul{ModuleName}DataGridsMapping;
use IlluminateSupportFacadesDB;
use WebkulDataGridDataGrid;
class MappingDataGrid extends DataGrid
{
/**
* @return IlluminateDatabaseQueryBuilder
*/
public function prepareQueryBuilder()
{
return DB::table('wk_{module}_mappings as m')
->leftJoin('wk_{module}_credentials as c', 'm.credential_id', '=', 'c.id')
->select(
'm.id',
'm.sku',
'm.external_id',
'c.label as credential_label',
'm.created_at',
);
}
/**
* @return void
*/
public function prepareColumns()
{
$this->addColumn([
'index' => 'sku',
'label' => trans('{module-name}::app.mappings.datagrid.sku'),
'type' => 'string',
'searchable' => true,
'filterable' => true,
'sortable' => true,
]);
$this->addColumn([
'index' => 'external_id',
'label' => trans('{module-name}::app.mappings.datagrid.external-id'),
'type' => 'string',
'searchable' => true,
'filterable' => true,
'sortable' => true,
]);
$this->addColumn([
'index' => 'credential_label',
'label' => trans('{module-name}::app.mappings.datagrid.credential'),
'type' => 'string',
'searchable' => false,
'filterable' => false,
'sortable' => false,
]);
}
/**
* @return void
*/
public function prepareActions()
{
if (bouncer()->hasPermission('{module-slug}.mappings.delete')) {
$this->addAction([
'icon' => 'icon-delete',
'title' => trans('admin::app.catalog.attributes.index.datagrid.delete'),
'method' => 'DELETE',
'url' => function ($row) {
return route('{module-slug}.mappings.destroy', $row->id);
},
]);
}
}
}
```
---
## 6. Checklist
- [ ] DataGrid file location: `src/DataGrids/{Section}/{Name}DataGrid.php` (subdirectory)
- [ ] Extends `WebkulDataGridDataGrid`
- [ ] Methods have PHPDoc `@return` only — no PHP return type hints on methods
- [ ] `DB::table('wk_{module}_...')` — `wk_` prefix in query builder
- [ ] Column `closure` uses arrow function `fn ($row) =>`
- [ ] Action `url` uses regular `function ($row) { return route(...); }`
- [ ] Mass action `url` is plain `route(...)` string (no function)
- [ ] Boolean columns use `label-active` (true) and `label-info text-gray-600 dark:text-gray-300` (false)
- [ ] Permission checks use `bouncer()->hasPermission()`
- [ ] Controller calls `app(DataGridClass::class)->toJson()` for AJAX
- [ ] Blade uses `<x-admin::datagrid :src="route(...)" />`