Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
UI vrstva / Admin presenter

Admin presenter#

Obrazovka administrace: výpis v DataGridu, pod ním přidání a úprava přes formulář. Základ dělá app/Core/Base/Admin/BasePresenter.php — tahle kapitola je o tom, co k němu musíte dopsat vy.

Co bázový presenter zařizuje sám#

startup() proběhne v tomhle pořadí a všechno z toho dostanete zadarmo:

Krok Co udělá
vypnutí query cache celý admin požadavek čte z databáze, ne z Redisu
kontexty nastavení, téma, lokalizace, jazyk, překladač
checkAuthentication() synchronizace JWT ↔ Nette session
authorize() RBAC — výchozí požadavek je privilegium :Admin
loadUserPrivileges() naplní oprávnění pro isAllowed() v šablonách a gridech
authorizeListing() u akce show vyžaduje klíč …:show, viz Brány čtení a zápisu
initializePage() dohledá AdminPage podle dvojice presenter + akce

Navíc: createForm() (formulář s tlačítky a návratem), překladač v šabloně, getAdminItemsPerPage() a režim Pickeru.

Vlastní klíč v authorize() odmítne i administrátora

loadUserPrivileges() běží až po authorize(). Uvnitř authorize() je pole oprávnění ještě prázdné a isAllowed() je fail-closed, takže checkPrivilege() tam vrátí 403 každému. Presenter, který bránu staví už v authorize() (obrazovka bez akce show, celá sekce pod jedním klíčem), si oprávnění načte sám:

protected function authorize(): void
{
    parent::authorize();          // `:Admin`
    $this->loadUserPrivileges();  // startup() to udělá až po nás
    $this->checkPrivilege(self::PRIVILEGE);
}

Druhé volání ze startup() jde z mezipaměti PrivilegeManager (jedna na požadavek), do databáze už nesahá. Vzor: CustomAssetPresenter, ThemeSettingPresenter.

Téma a jazyk se nastavují ještě před checkAuthentication(), takže běží i na anonymní přihlašovací stránce. Co čtou, musí být dostupné bez identity, viz vlastní obrazovka nastavení.

Admin požadavek má cache vypnutou schválně — nezapínejte ji zpátky

Vypíná se kontext celého požadavku, ne jen admin repository: v administraci běží i frontové managery (assety, téma, uživatel) a ty jdou přes repository, která cache povoluje. Bez toho by se do formuláře mohl dostat záznam z cache — a admin formulář je čtení, po kterém následuje zápis. Uložilo by se něco jiného, než co admin viděl.

Doména překladů se v administraci NEodvozuje z namespace

Na rozdíl od Front presenteru ji musíte deklarovat ručně: protected string $translatorDomain = 'admin.blog.presenters.tag';. Bez ní se v šabloně nepřeloží nic a createForm() přeskočí useDomainTranslator() — texty formuláře pak zůstanou jako holé klíče. Podrobně Překlady.

Jak vypadá hotový presenter#

Soubor presenteru drží jen data, akce a továrnu gridu. Formulář je v traitu.

final class TagPresenter extends BasePresenter
{
    use TagFormTrait;                 // createComponentEditForm + uložení
    use AdminFormHelpersTrait;        // getFormLanguages() + str()
    use AdminEditFormComponentsTrait; // jazykové záložky + tlačítka

    protected string $translatorDomain = 'admin.blog.presenters.tag';

    private ?Tag $editedTag = null;

    public function __construct(
        private readonly DataGridFactory $dataGridFactory,
        private readonly TagManager $tagManager,
    ) {
        parent::__construct();
    }

    public function actionEdit(int $id): void
    {
        $tag = $this->tagManager->findAll(
            filters: [new EqualFilter('id', $id)],
            limit: 1,
            includes: ['translations'],
        )->first();

        $this->editedTag = $tag instanceof Tag ? $tag : null;

        if ($this->editedTag === null) {
            $this->error('Štítek nenalezen.');
        }
    }

    public function renderAdd(): void
    {
        $this->template->pageTitle = 'form.headers.add';
        $this->template->languages = $this->getFormLanguages();
        $this->setView('edit');        // add i edit sdílejí jednu šablonu
    }
}
Akce Šablona Co dělá
show Templates/<Presenter>/show.latte výpis, jen {control grid}
add sdílí edit.latte přes setView('edit') prázdný formulář
edit Templates/<Presenter>/edit.latte formulář s výchozími hodnotami

Postup: zakládám nový admin presenter#

  1. Model musí být hotový dřív. Admin varianta entity, manager, service, mapper a repository — viz Konvence kódu, oddíl o entitě a manageru.
  2. Presenter do app/UI/Admin/<Modul>/Presenters/XxxPresenter.php: final, extends BasePresenter, $translatorDomain, private property pro editovaný záznam, konstruktor s DataGridFactory a manažerem.
  3. Akce: actionEdit(int $id) načte do property a při nenalezení volá $this->error(); actionAdd() property vynuluje; renderAdd() / renderEdit() nastaví pageTitle a jazyky. renderAdd() končí setView('edit').
  4. Grid v createComponentGrid() — zdroj dat je manager, ne dotaz. Nezapomeňte setIncludes() na to, co grid opravdu zobrazuje, a setItemsPerPage($this->getAdminItemsPerPage()).
  5. Formulář do vlastního traitu Presenters/Traits/Forms/XxxFormTrait.php (postup níž).
  6. Šablony Templates/<Presenter>/show.latte a edit.latte.
  7. Řádek v cms_admin_system_pages s dvojicí presenter (s úvodní dvojtečkou, :Admin:Blog:Tag) a action, plus překlady v cms_admin_system_page_texts.
  8. Položku menu a její vazbu na stránku (cms_admin_system_page_menu_item_relations).
  9. Překladové klíče do app/Locale/<modul>/<locale>/admin.<LOCALE>.neon pod admin.<modul>.presenters.<presenter> — a rovnou ve všech jazycích.
  10. RBAC klíč pro akce, které nemá vidět každý; podmíněné zobrazení v gridu přes isAllowed().
  11. Prokliknout: výpis, filtr, řazení, stránkování, přidání, úprava, smazání a návrat tlačítkem „Zrušit“.

Bez řádku v cms_admin_system_pages nemá stránka titulek ani menu

initializePage() hledá stránku podle přesné dvojice presenter + akce, přičemž sloupec presenter drží plný Nette název s úvodní dvojtečkou. Když řádek chybí, nic nespadne — jen zůstane prázdný titulek, prázdné meta tagy a v menu se nezvýrazní žádná položka. Vypadá to jako chyba šablony.

Hezká admin URL potřebuje překlad stránky, jinak spadne na obecnou routu

AdminRouteFactory zkouší nejdřív hezký tvar admin/<presenterHref>/<actionHref> a bez uloženého překladu vrátí null — použije se fallback admin/<modul>/<presenter>/<akce>. Adresa funguje, jen je ošklivá; nikde se to nehlásí.

Postup: přidávám formulář#

Kostru dělá createForm(), vy dodáte pole a callback, který uloží.

  1. createComponentEditForm() v FormTrait předá do createForm() callback, který vrací bool — úspěch, nebo ne:
protected function createComponentEditForm(): Form
{
    $form = $this->createForm(fn(ArrayHash $values): bool => $this->saveTagForm($values));

    $this->addTranslationFields($form);

    if ($this->getHttpRequest()->isMethod('GET') && $this->getSignal() === null) {
        $this->setTagFormDefaults($form);
    }

    return $form;
}
  1. Výchozí hodnoty plňte jen při běžném GET renderu. Podmínka výš je záměrná — po odeslání formuláře nebo při signálu by defaulty přepsaly to, co uživatel vyplnil.
  2. Callback uloží přes manager, vyhodnotí isSuccess(), pošle flash a při úspěchu nastaví $this->savedEntityId:
private function saveTagForm(ArrayHash $values): bool
{
    $tag = $this->buildTagEntity($values);

    $result = $this->tagManager->save($tag);
    if (!$result->isSuccess()) {
        $this->flashMessage($this->translatorDomain . '.form.messages.save.error', 'danger');
        return false;
    }

    $this->flashMessage($this->translatorDomain . '.form.messages.save.success', 'success');
    $this->savedEntityId = $tag->getId();

    return true;
}
  1. Tlačítka a redirecty už řešit nemusíte:
Tlačítko Chování
Uložit (save) uloží a vrátí se na stránku, ze které jste přišli
Použít (update) uloží a zůstane na editaci
Zrušit (cancel) jen odkaz zpět, nic neukládá
  1. Návrat „zpět“ je per formulář. Adresa předchozí stránky se při prvním GET renderu vezme z Referer, uloží do session pod náhodný klíč a klíč jde do skrytého pole backKey. Souběžné taby si proto návrat nepřepisují.

$savedEntityId nenastavené znamená DUPLIKÁT při druhém „Použít“

Na akci add vede update na redirect('this') — tedy zpátky na prázdný add formulář. Druhé kliknutí na „Použít“ tam založí další nový záznam. Base to ošetřuje přesměrováním na edit?id=…, ale jen když callback $savedEntityId vyplní.

Ukládá se JEN přes save a update

Nette pouští onSuccess pro každý validní submit. Base proto uvnitř kontroluje jméno stisknutého tlačítka a u ostatních se vrátí bez uložení. Když si přidáte vlastní submit (překreslení Pickeru, „přidat řádek“), nebude ukládat — a to je správně. Kdo to nečeká, hledá, proč se data neuložila.

Vlastní tlačítko, které z přidání přesměruje na editaci, musí nést backKey

Bez něj si editace vygeneruje nový klíč z Referer, tedy ze stránky přidání, a „Uložit“ i „Zrušit“ na editaci vrátí admina na přidání. Je-li tlačítko AJAXové, dej mu data-naja-force-redirect: naja pak udělá plné načtení editace místo výměny snippetů, která mezi layoutem přidání a editace selže. Vzor: ProductFormTrait::saveProductAndManageVariants().

Postup: přidávám vícejazyčná pole#

  1. AdminFormHelpersTrait dá getFormLanguages() a str(); AdminEditFormComponentsTrait jazykové záložky a lištu tlačítek.
  2. Jedna sada polí na jazyk, sufix _<langId>:
foreach ($this->getFormLanguages() as $language) {
    $langId = (int) $language->getId();

    $active = $form->addCheckbox('active_' . $langId, 'form.fields.active.label');

    $form->addText('name_' . $langId, 'form.fields.name.label')
        ->setNullable()
        ->addConditionOn($active, $form::EQUAL, true)
        ->setRequired($t('form.fields.name.required'));
}
  1. Při sestavování entity dohledejte překlad přes getTranslation($langId) a chybějící založte i s setParent().
  2. includes: ['translations'] patří do načtení v actionEdit() — bez toho formulář nabídne prázdno.
  3. V šabloně vykresli lištu jazyků {control adminLangNav $languages, $form} a pod ní panel na jazyk: id="lang-panel-{$lid}", aria-labelledby="lang-tab-{$lid}", první panel show active. Lišta k pilulce jazyka vykreslí i zaškrtávátko active_<langId>, v panelu ho znovu nevykresluj.
Parametr adminLangNav Kdy ho dát
třetí, $idPrefix ('set-', 'tariff-') na stránce je víc jazykových lišt (formulář + komponenta nebo modál); panely té sekce pak mají id="{$prefix}lang-panel-…"
čtvrtý, $withActive = false formulář nemá „aktivní překlad“ (jazykové varianty nastavení, Identita webu); lišta nese jen taby a pole active_<langId> být nemusí

Dvě lišty bez předpony si přepínají panely navzájem

Obě vyrobí lang-tab-<id> a lang-panel-<id>, takže klik na jazyk v jedné sekci přepne panel v druhé a pole jazyka se v té první neukáže. Předponu musí mít stejně lišta i panely.

Povinné pole v nezobrazené jazykové záložce zablokuje odeslání

Proto je setRequired() navěšené addConditionOn($active, …) — povinné je jen tehdy, když je ten překlad označený jako aktivní. Bez podmínky nejde formulář odeslat a prohlížeč přitom skáče na pole, které není vidět.

Nevykreslené pole SMAŽE hodnotu

Pole, které šablona nevykreslí, se neodešle — a uložení ho zapíše jako prázdné. Netýká se to jen addHidden(): totéž udělá n:if na poli formuláře. Data zmizí bez jediné chybové hlášky a přijde se na to podle stížnosti uživatele.

Výchozí hodnoty editace potřebují ToOne vazby v includes

Bez nich formulář nabídne prázdný výběr — a uložení tu vazbu zruší, protože prázdno je platná hodnota.

Brány čtení a zápisu#

Base hlídá práva na dvou místech, takže je v presenteru nepíšete znovu:

Brána Kdy běží Jaký klíč chce
authorizeListing() ve startup(), jen u akce show :Admin:<Modul>:<Presenter>:show
authorizeFormSubmit() v createForm() před uložením, jen u tlačítek save a update podle formSubmitPrivilege(), viz níž

Klíč pro uložení se odvozuje jen ze serveru (presenter + akce požadavku), nikdy z pole formuláře:

Akce požadavku Klíč
add / edit …:add / …:edit
jiná akce u presenteru, který má add nebo edit …:edit
presenter bez add/edit (jednostránkové nastavení) klíč aktuální akce, typicky …:show

Brána zápisu je nutná, protože odeslání formuláře je signál (?do=editForm-submit) a Nette ho zpracuje nad libovolnou akcí až po action*(). Kontrola v actionEdit() tedy nestačí: submit poslaný na výpis by prošel jen právem zobrazení.

Postup: formulář na vlastní akci#

  1. Formulář patří k akci, která není add/edit (vzor: sendEmailForm u faktur).
  2. V presenteru přepište formSubmitPrivilege() a vraťte klíč té akce podle jména formuláře; ostatní formuláře nechte na parent::formSubmitPrivilege($form):
protected function formSubmitPrivilege(\Nette\Forms\Form $form): string
{
    return $form->getName() === 'sendEmailForm'
        ? ':Admin:Invoicer:Invoice:sendEmail'
        : parent::formSubmitPrivilege($form);
}
  1. Klíč založte v RBAC a povolte roli admin.
  2. Vlastní ukládací tlačítko mimo save/update (např. „Nejprve uložit produkt“) zavolá $this->authorizeFormSubmit($button->getForm()) samo, jinak ukládá bez brány.

Každá obrazovka show potřebuje klíč v tabulce práv

Brána čtení je fail-closed: bez řádku …:show povoleného roli admin se výpis nezobrazí ani administrátorovi (403).

Výjimka v formSubmitPrivilege() vždy podle jména formuláře

Podmínka jen podle akce by stejné právo dala i ostatním formulářům presenteru, které se na tu akci dají poslat.

Brány v signálech a komponentách#

Obě brány výš hlídají jen výpis a formulář presenteru. Všechno ostatní, co mění data, si bránu píše samo: skryté tlačítko nic nechrání, ručně složená adresa ?do=<signál> akci provede.

Kde Brána Kdy ji volat
signál nebo vlastní akce presenteru checkPrivilege($key) / checkCurrentPrivilege() (PrivilegeCheckTrait) jako první příkaz metody
signál komponenty (handleAdd, handleDelete…) requirePrivilege($key) z PrivilegeGuardTrait jako první příkaz, před načtením dat
formulář komponenty (AdminComponentFormTrait) requirePrivilege($key) na začátku ukládacího callbacku, vzor FeedEdit::saveFeedForm()
zobrazení odkazu, dlaždice, tlačítka isAllowed($key) v presenteru a šabloně, isTargetAllowed($key) v komponentě nevyhazuje, vrací bool

checkPrivilege() je protected, z komponenty ji nezavoláte. PrivilegeGuardTrait se proto ptá presenteru přes veřejnou isAllowed(), která čte práva načtená jednou ve startup(), takže brána nestojí žádný dotaz navíc. Klíč stránky (:Admin:System:User:show) složí resolvePageTarget($adminPage) z cíle stránky, nikdy z názvu třídy.

Formulář komponenty bránu zápisu z base nedostane

AdminComponentFormTrait přebírá z createForm() návrat a rozlišení update/save, ale authorizeFormSubmit() nevolá. Bez requirePrivilege() v ukládacím callbacku uloží formulář každý, kdo smí stránku otevřít.

Brána komponenty je fail-closed

Presenter bez isAllowed() (mimo admin větev) i chybějící právo končí stejně: 403. Prázdný klíč v isTargetAllowed() znamená „ne“.

Položky editovaného záznamu: kontext vlastníka#

Komponenty s položkami jednoho záznamu (odpovědi a sledující inzerátu, platební položky inzerátu, profilu a příspěvku) dostávají id vlastníka v parametru signálu a v hidden poli. Obojí jde v prohlížeči přepsat. OwnerContextGuardTrait proto bere vlastníka ze serveru, z callbacku, který komponentě předá presenter:

Metoda Co dělá
setOwnerContext(fn() => $this->editedAdvert?->getId()) volá presenter v createComponent…(); callback se čte až při signálu nebo submitu, kdy action*() záznam načetl
requireContextOwner($ownerId) signál: id z adresy musí sedět s kontextem, jinak 403
submittedOwnerId($posted) submit: vrací id z kontextu, nebo null, když kontext chybí nebo hidden pole nesedí; při null se nic neuloží
contextOwnerId() id pro načtení položek; položka se hledá jen v rámci kontextu, cizí je „nenalezeno“

Postup: komponenta s položkami jednoho záznamu

  1. Do komponenty use PrivilegeGuardTrait; use OwnerContextGuardTrait;.
  2. Každý mutující signál začni requirePrivilege(…) a hned za ním $ownerId = $this->requireContextOwner($ownerIdZAdresy).
  3. V obsluze formuláře requirePrivilege(…), pak submittedOwnerId($values->advertId ?? null); při null skonči bez uložení.
  4. Položky načítej s filtrem na contextOwnerId(), ne na id z požadavku.
  5. V presenteru: ->setOwnerContext(fn(): ?int => $this->editedAdvert?->getId()) v createComponent…(). Vzor: app/UI/Admin/Bazaar/Presenters/AdvertPresenter.php.

Bez setOwnerContext() komponenta nefunguje vůbec

Kontext je fail-closed: bez něj komponenta nic neuloží, nesmaže ani neotevře. Bez téhle vazby by role s právem úprav přes komponentu jednoho inzerátu upravila nebo smazala položku cizího (stačilo podvrhnout id).

Chyby polí bez {inputError}#

Serverové pravidlo pole zastaví uložení i tam, kde šablona {inputError} nemá. Aby to uživatel viděl, createForm() v onError volá publishFieldErrors(): při AJAXu pošle chyby v payload.fieldErrors, při plném vykreslení je layout dá do data-server-field-errors na <body>. Skript fieldErrors.js je vloží k poli a odkryje jeho záložku; s {inputError} se nezdvojí. Komponentní formulář (AdminComponentFormTrait) volá totéž přes presenter.

Režim Pickeru#

Presenter umí posloužit jako obsah Pickeru: vykreslí svůj show grid jako výběrový modál v minimálním layoutu, bez přidání, editace a hromadných akcí. Zapíná se v továrně gridu:

if ($this->picker !== '') {
    $grid->setPickerMode($this->picker)->setPickerLabelProperty('translations.name');
}

Parametry jsou persistentní, aby přežily filtrování a stránkování uvnitř modálu. pickerExclude (a dvojice pickerExcludeSubtreeLft/Rgt u stromů) skryje „Vybrat“ u záznamů, které se vybrat nesmí — typicky aby stránka nemohla být sama sobě rodičem.

Bez setPickerLabelProperty() se popisek vezme z prvního sloupce, který není identifikátor

Někdy to vyjde, někdy ne — a když je ta property u části záznamů prázdná, uživatel po výběru uvidí prázdné pole. Když jediná property nestačí (je potřeba složit víc polí), použijte setPickerLabelCallback(), který dostane celou entitu. Podrobně Picker.

Drobečky v hlavičce layoutu#

Hlavička administrace (layouts/parts/shared/_header.latte, v obou admin šablonách) ukazuje drobečkovou navigaci obrazovky. Kreslí ji Menu::renderBreadcrumb() se šablonou MenuComponent/Templates/Default/Breadcrumb.latte:

{capture $breadcrumbHtml}{control adminSystemMenu:breadcrumb 1}{/capture}
{if trim((string) $breadcrumbHtml) !== ''}
    {$breadcrumbHtml}
{else}
    <div class="admin-header__title">…titulek stránky…</div>
{/if}

Cesta je Administrace › předci aktivní položky menu › aktivní položka › obrazovka:

Drobeček Odkud Odkaz
Administrace :Admin:System:Page:dashboard ano
předci a aktivní položka položky menu 1 podle vazby stránka → položka; oddělovače a položky bez názvu se přeskočí jen položka s cílem, složka bez obrazovky je text
obrazovka titulek stránky z cms_admin_system_pages, jen když aktivní položka vede jinam (add, edit …) ne, poslední drobeček nikdy není odkaz

Z titulku obrazovky se odřízne přípona za | a ze tvaru „Modul - akce“ se vezme poslední část s velkým prvním písmenem. Položka dashboardu se jako drobeček neopakuje.

Komponenta je táž instance jako levé menu: loadMenuState() načte položky, odkazy a aktivní stav jednou pro každé menu, takže drobečky nestojí dotaz navíc.

Obrazovka mimo menu má prostý titulek

Bez aktivní položky menu renderBreadcrumb() nevykreslí nic a hlavička spadne na titulek stránky ($metaTitle, $pageTitle). Chybějící drobečky na nové obrazovce proto znamenají chybějící vazbu stránky na položku menu, ne chybu šablony.

Menu administrace i widgety dashboardu se řídí tímtéž klíčem jako obrazovka, na kterou vedou: <presenter>:<akce> cílové stránky, třeba :Admin:System:UserGroup:show. Menu a obrazovka si tak nemůžou odporovat.

Menu (MenuComponent\Menu) ukáže položky, na jejichž cíl má uživatel právo. Načítá je přes Api, aktivní položku určí vazba stránka → položka. Zmizí v něm složka, které prořez práv odebral všechny děti, a sekční nadpis, pod kterým nezůstala žádná položka. Položka, jejíž presenter neexistuje (class_exists()), se vypíše jako text bez odkazu. Položka může nést params (JSON): dvě položky nad jedním presenterem se pak liší jen předvolbou. Klíče musí odpovídat persistentním parametrům cílového presenteru.

Dashboard (:Admin:System:Page:dashboard) je plocha s widgety. Rozložení si skládá každý uživatel sám, pro každou stránku zvlášť, a každý widget je komponenta ve složce Widgets/ svého modulu. Dřívější pevná pásma („Co čeká na vyřízení“ a dlaždice) žijí dál jako info boxy (jeden typ widgetu na kartu) a widget „Rychlé odkazy“. Jak widget vzniká, co deklaruje a jak se hlídá, popisuje kapitola Widgety dashboardu.

Klíč menu, který v tabulce práv neexistuje, skryje položku všem

Včetně administrátora. Hlídá to kontrola 1.6 v tests/Manual/Admin/privilege_enforcement_audit.php; bez ní se na to přijde až tím, že někomu zmizí položka z menu. Totéž platí pro klíče widgetů, ty hlídá DashboardWidgetRegistryTest.

Na co si dát pozor#

Přesměrování na surovou adresu flash zprávu zahodí

redirectUrl() místo redirect() znamená, že uživatel neuvidí ani potvrzení, ani chybu — akce proběhne a vypadá, že se nic nestalo. Nette přidává klíč flash session (_fid) jen při redirect()/forward(). Návrat na předchozí stránku proto jděte přes redirectToFormBack(), který _fid do surové adresy doplní sám.

Flash v modálním okně potřebuje vlastní kotvu

Bez ní se vykreslí mimo modál, kde ji za otevřeným oknem nikdo nevidí.

Po změně struktury formuláře je potřeba načíst stránku dvakrát

První načtení ještě jede podle staré podoby z mezipaměti. Nezakládejte na prvním pokusu závěr, že oprava nefunguje.

Administrace nesmí používat Front traity ani Front entity

Vypadá to jako úspora — obojí přece existuje. Jenže Front entita je výsledek hydratace odpovědi pro návštěvníka: nese jen to, co Front potřebuje, a mění se podle frontu. Admin, který na ni sáhne, se rozbije při úpravě, která s administrací nemá nic společného.

Kam sáhnout#

Chci Kde
co dělá základ app/Core/Base/Admin/BasePresenter.php
kostru formuláře a návrat zpět tamtéž → createForm(), resolveFormBackKey()
jazykové záložky a tlačítka app/Core/Traits/Admin/Presenters/AdminEditFormComponentsTrait.php
pomocníky formuláře app/Core/Traits/Admin/Presenters/AdminFormHelpersTrait.php
RBAC v administraci app/Core/Traits/Admin/Presenters/PrivilegeCheckTrait.php
brány komponent a kontext vlastníka app/Core/Traits/Admin/Components/PrivilegeGuardTrait.php, OwnerContextGuardTrait.php
načtení stránky a menu app/Core/Traits/Admin/Presenters/PageInitTrait.php
menu a jeho prořez podle práv app/UI/Admin/System/Components/MenuComponent/Menu.php
drobečky v hlavičce Menu::renderBreadcrumb(), …/MenuComponent/Templates/Default/Breadcrumb.latte, layouts/parts/shared/_header.latte
dashboard a jeho widgety app/UI/Admin/System/Components/DashboardWidgetsComponent/, app/UI/Admin/<Modul>/Widgets/, viz Widgety dashboardu
hezké admin adresy app/Core/Routers/Factories/AdminRouteFactory.php
hotový vzor app/UI/Admin/Blog/Presenters/TagPresenter.php + Traits/Forms/TagFormTrait.php

Navazující kapitoly: Práce s tabulkami · Widgety dashboardu · Picker · Formuláře · Překlady · Konvence kódu

Nastavení cookies