Produkty Objednávky Zákazníci Dokumentace
Nepřihlášen Uživatel 1 Uživatel 2 Uživatel 3
Část I · Začínáme

1. Instalace

Šest kroků. Tabulky v databázi si knihovna vytvoří sama.

  1. Do composer.json přidat:
    "repositories": [
    	{"type": "vcs", "url": "git@gitlab.xart.cz:xart/devel/app-packages/utils.git"},
    	{"type": "vcs", "url": "git@gitlab.xart.cz:xart/devel/app-packages/console.git"},
    	{"type": "vcs", "url": "git@gitlab.xart.cz:xart/devel/app-packages/grid.git"}
    ]
  2. Spustit:
    composer require xart/grid
    npm install xart-grid
  3. Do konfigurace přidat:
    extensions:
    	grid: Xart\Grid\Extension
  4. Do frontendu přidat:
    import 'xart-grid';
    import 'xart-grid/css';
  5. Zaregistrovat potomka Xart\Grid\User\UserProvider s metodou loadUser(string $id): ?UserDTO. Bez něj neprojde kompilace kontejneru; nemáte-li uživatele, vracejte null.
  6. Presentery s gridem dědit z BasePresenter (xart/utils).

Poznámky

  • Vyžaduje PHP 8.1+, Nette 3.2, MySQL, bootstrap 5.3, naja 3.3, ikony Tabler.
  • Naja musí být tatáž instance jako ve zbytku aplikace (ve Vite přes dedupe).
  • UserDTO: povinné id a jméno; volitelné imageUrl, color, online, profileUrl.
  • Nepřihlášenému drží nastavení session, profily nemá.

Ukázka

Jediný grid v dokumentaci, kde je vidět všechno naráz. Data se nikam nezapisují.

Objednávky

Co je na ukázce vidět

Sloupce s presetyČíslo, částka, datum, ano/ne – část III
Enum sloupceStav a Platba z PHP enumu i s ikonou a barvou – kapitola 8
AvatarV datech je jen id; jméno a fotku dodá UserProvider – kapitola 10
Systémový sloupecStáří se dopočítá v rows() – kapitola 11
Obarvené řádkyNezaplacené objednávky – kapitola 16
Hromadná akceZatržené řádky sečte tlačítko v liště – kapitola 37
Řádkový modálDetail otevře běžnou Nette komponentu – kapitola 36.2
Nabídka ZobrazeníSkrývání sloupců, délka stránky – kapitoly 12, 26 a 43

Zdrojový kód

php app/Modules/AdminModule/Components/Docs/Start/TourGridControl/TourGridControl.php
<?php

declare(strict_types=1);

namespace App\Components\Docs\Start\TourGridControl;

use App\Components\Docs\Start\OrderDetailControl\OrderDetailControl;
use App\Enum\OrderPayment;
use App\Enum\OrderStatus;
use DateTimeImmutable;
use DateTimeInterface;
use Override;
use Xart\Grid\Button\ButtonManager;
use Xart\Grid\Color\ColorScheme;
use Xart\Grid\Column\ColumnManager;
use Xart\Grid\Column\Enum\GridEnum;
use Xart\Grid\Control\GridControl;
use Xart\Grid\DataOutput\Row;
use Xart\Grid\DataSource\DataSource;
use Xart\Grid\DataSource\SQL\SQLDataSource;
use Xart\Grid\Settings\SettingsManager;
use Xart\Utils\Presenter\BasePresenter;

/**
 * Dokumentace, kapitola 1 – Instalace.
 *
 * Jediný grid v celé dokumentaci, který schválně ukazuje všechno najednou: sloupce s presety, enum
 * s pilulkou, avatar, systémový sloupec, obarvené řádky, hromadnou akci i řádkový modál. Ostatní
 * kapitoly mají vždy jen to jedno, o čem jsou.
 *
 * Nic tady nic nepřepisuje – hromadná akce jen spočítá, co má vybráno. Dokumentaci si prochází
 * kdekdo a demo data mají zůstat, jak jsou.
 */
class TourGridControl extends GridControl
{
    public function createDataSource(): DataSource
    {
        return new SQLDataSource(__DIR__ . '/TourGridControl.sql');
    }


    public function columns(ColumnManager $cm): void
    {
        $cm->add('number', 'Číslo');

        // Avatar: v buňce je id zákazníka, jméno i fotku si grid vyzvedne u UserProvideru.
        $cm->addAvatar('id_customer', 'Zákazník');

        $cm->addEnum('status', 'Stav', OrderStatus::class)
            ->showInCell(GridEnum::Badge, GridEnum::Icon, GridEnum::Label)
            ->showInFilter(GridEnum::Label, GridEnum::Color);

        $cm->addEnum('payment', 'Platba', OrderPayment::class)
            ->showInCell(GridEnum::Icon, GridEnum::Label);

        $cm->addNumber('total_price', 'Celkem', 2, 'Kč');
        $cm->addCheck('paid', 'Zaplaceno');
        $cm->addDatetime('created_at', 'Vytvořeno', true);

        // Systémový sloupec – hodnota se nebere z databáze, dopočítá se v rows().
        $cm->addSystem('age', 'Stáří');
    }


    public function buttons(ButtonManager $bm): void
    {
        $bm->addBulkButton('Sečíst vybrané', $this->sumSelected(...))
            ->setIconClasses('ti ti-sum')
            ->setType('secondary');
    }


    public function rows(Row $row): void
    {
        // Systémový sloupec se plní tady – v datech pro něj nic není.
        $created = $row['created_at']->defaultValue;
        $row['age'] = $created instanceof DateTimeInterface
            ? (new DateTimeImmutable)->diff($created)->days . ' dní'
            : '';

        // Nezaplacená objednávka se podbarví, ať je v seznamu vidět.
        if (!$row['paid']->defaultValue) {
            $row->setColor(ColorScheme::orange()->subtle());
        }

        $row['total_price']->addClass('fw-bold');

        $detail = $row->addModalButton('Detail', fn(Row $row): OrderDetailControl => $this->detail($row));
        $detail->setPreset('detail');
        $detail->modal
            ->setTitle('Detail objednávky')
            ->setSize('lg');
    }


    /**
     * Obsluha hromadného tlačítka: dostane zatržené řádky a odpoví hláškou. Grid se po ní
     * nepřenačítá – nic se nezměnilo, takže by to bylo jen zbytečné natažení dat.
     */
    private function sumSelected(): void
    {
        $rows = $this->getSelectedRows();
        $sum = 0.0;
        foreach ($rows as $row) {
            $sum += (float) $row['total_price']->defaultValue;
        }

        $presenter = $this->getPresenter();
        assert($presenter instanceof BasePresenter);
        $presenter->payloadSuccess(sprintf(
            'Vybrané objednávky: %d, celkem %s Kč.',
            count($rows),
            number_format($sum, 2, ',', ' '),
        ));
        $presenter->sendPayload();
    }


    /** Obsah modálu si bereme z presenteru – jen jako jeho komponenta umí odkazy i vlastní signály. */
    private function detail(Row $row): OrderDetailControl
    {
        $detail = $this->getPresenter()->getComponent('orderDetail');
        assert($detail instanceof OrderDetailControl);

        return $detail->setOrderId((int) $row['id']->defaultValue);
    }


    #[Override]
    public function settings(): SettingsManager
    {
        $sm = new SettingsManager;
        $sm->setTitle('Objednávky');
        return $sm;
    }


    public function render(): void
    {
        $this->template->render(__DIR__ . '/TourGridControl.latte');
    }
}
sql app/Modules/AdminModule/Components/Docs/Start/TourGridControl/TourGridControl.sql
SELECT
    id,
    number,
    id_customer,
    status,
    payment,
    total_price / 100 AS total_price,
    (paid_at IS NOT NULL) AS paid,
    created_at
FROM `order`
php app/Helpers/DemoUserProvider.php
<?php

declare(strict_types=1);

namespace App\Helpers;

use Nette\Application\LinkGenerator;
use Nette\Database\Explorer;
use Xart\Grid\User\UserDTO;
use Xart\Grid\User\UserProvider;


final class DemoUserProvider extends UserProvider
{
    public function __construct(
        private Explorer $db,
        private LinkGenerator $linkGenerator,
    ) {
    }


    protected function loadUser(string $id): ?UserDTO
    {
        $row = $this->db->query('SELECT * FROM customer WHERE id = ?', $id)->fetch();
        if ($row === null) {
            return null;
        }

        $numericId = (int) $id;

        return new UserDTO(
            $id,
            (string) $row->first_name,
            (string) $row->last_name,
            // Demo: fotku má jen půlka lidí, ať je vidět i vykreslení iniciál.
            $numericId % 2 === 0 ? 'https://i.pravatar.cc/256?u=' . urlencode($id) : null,
            null,
            $numericId % 4 === 0 ? $numericId % 3 === 0 : null,
            $numericId % 3 === 0 ? $this->linkGenerator->link('Customer:Form', [$id]) : null,
        );
    }
}
sql vendor/xart/grid/backend/State/grid_state.sql
CREATE TABLE IF NOT EXISTS `grid_state` (
	`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
	`id_user` INT NOT NULL,
	`grid_id` VARCHAR(255) NOT NULL,
	`name` VARCHAR(100) NOT NULL DEFAULT '',
	`state` MEDIUMTEXT NOT NULL,
	`share_hash` CHAR(32) NULL DEFAULT NULL,
	`id_profile` INT UNSIGNED NULL DEFAULT NULL,
	`created_at` DATETIME NOT NULL,
	`updated_at` DATETIME NOT NULL,
	PRIMARY KEY (`id`),
	UNIQUE KEY `grid_state_user_grid_name` (`id_user`, `grid_id`, `name`),
	UNIQUE KEY `grid_state_share_hash` (`share_hash`),
	KEY `grid_state_profile_idx` (`id_profile`),
	CONSTRAINT `grid_state_profile_fk` FOREIGN KEY (`id_profile`) REFERENCES `grid_state` (`id`) ON DELETE SET NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;