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#
- 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.
- Presenter do
app/UI/Admin/<Modul>/Presenters/XxxPresenter.php:final,extends BasePresenter,$translatorDomain, private property pro editovaný záznam, konstruktor sDataGridFactorya manažerem. - Akce:
actionEdit(int $id)načte do property a při nenalezení volá$this->error();actionAdd()property vynuluje;renderAdd()/renderEdit()nastavípageTitlea jazyky.renderAdd()končísetView('edit'). - Grid v
createComponentGrid()— zdroj dat je manager, ne dotaz. NezapomeňtesetIncludes()na to, co grid opravdu zobrazuje, asetItemsPerPage($this->getAdminItemsPerPage()). - Formulář do vlastního traitu
Presenters/Traits/Forms/XxxFormTrait.php(postup níž). - Šablony
Templates/<Presenter>/show.latteaedit.latte. - Řádek v
cms_admin_system_pagess dvojicípresenter(s úvodní dvojtečkou,:Admin:Blog:Tag) aaction, plus překlady vcms_admin_system_page_texts. - Položku menu a její vazbu na stránku
(
cms_admin_system_page_menu_item_relations). - Překladové klíče do
app/Locale/<modul>/<locale>/admin.<LOCALE>.neonpodadmin.<modul>.presenters.<presenter>— a rovnou ve všech jazycích. - RBAC klíč pro akce, které nemá vidět každý; podmíněné zobrazení v gridu přes
isAllowed(). - 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ží.
createComponentEditForm()vFormTraitpředá docreateForm()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;
}
- 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.
- 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;
}
- 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á |
- 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 polebackKey. 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#
AdminFormHelpersTraitdágetFormLanguages()astr();AdminEditFormComponentsTraitjazykové záložky a lištu tlačítek.- 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'));
}
- Při sestavování entity dohledejte překlad přes
getTranslation($langId)a chybějící založte i ssetParent(). includes: ['translations']patří do načtení vactionEdit()— bez toho formulář nabídne prázdno.- 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í panelshow active. Lišta k pilulce jazyka vykreslí i zaškrtávátkoactive_<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#
- Formulář patří k akci, která není
add/edit(vzor:sendEmailFormu faktur). - V presenteru přepište
formSubmitPrivilege()a vraťte klíč té akce podle jména formuláře; ostatní formuláře nechte naparent::formSubmitPrivilege($form):
protected function formSubmitPrivilege(\Nette\Forms\Form $form): string
{
return $form->getName() === 'sendEmailForm'
? ':Admin:Invoicer:Invoice:sendEmail'
: parent::formSubmitPrivilege($form);
}
- Klíč založte v RBAC a povolte roli
admin. - 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
- Do komponenty
use PrivilegeGuardTrait; use OwnerContextGuardTrait;. - Každý mutující signál začni
requirePrivilege(…)a hned za ním$ownerId = $this->requireContextOwner($ownerIdZAdresy). - V obsluze formuláře
requirePrivilege(…), paksubmittedOwnerId($values->advertId ?? null); přinullskonči bez uložení. - Položky načítej s filtrem na
contextOwnerId(), ne na id z požadavku. - V presenteru:
->setOwnerContext(fn(): ?int => $this->editedAdvert?->getId())vcreateComponent…(). 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 a dashboard#
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