Admin Component Style Guide
Conventions for building pages under resources/js/pages/Admin/*. Follow these so new modules look and behave like the existing User / Role / ActivityLog / Settings / Files pages.
Page layout
resources/js/pages/Admin/<Module>/
├── Index.vue # Listing (SkDatatable)
├── Create.vue # Optional — only if create is a dedicated route
├── Edit.vue # Optional — only if edit is a dedicated route
└── components/
└── <Module>Form.vue # SkForm wrapper reused by Create/Edit
Most modules fit inside Index.vue with dialog-based create/edit (see Users/Index.vue). Prefer dedicated Create.vue / Edit.vue only when the form cannot fit in a dialog (multi-step, media, long forms).
Form flows — pick one, don't mix
| Case | Use | Endpoint shape | Why |
|---|---|---|---|
| Admin CRUD from a dialog or an Inertia page | SkForm + FormBuilder |
Inertia controller action returning back() / redirect() |
SkForm wraps Inertia's useForm internally; validation errors auto-populate from Laravel FormRequest responses; success/cancel events drive dialog lifecycle |
| Programmatic one-shot API call (row action, bulk op, non-form mutation) | useApi() |
Controller returning to_api() / ApiResponse |
Returns unwrapped data, throws ApiError with envelope, shows toast on failure |
| Reading a small JSON payload (select options, stats widget) | useApi().get() |
ApiResponse::success(...) |
Same as above; skip if the data is already delivered by Inertia props |
Never call useApi() to submit an SkForm. Never hand-roll fetch() — it bypasses the toast/error pipeline.
Datatable
- Frontend:
SkDatatable+DB.table<T>()builder (see datatable.md) - Backend:
DatatableQueryBuilder::for(Model::class)in a*DatatableQueryclass underapp/Domain/<Module>/Queries/ - Pass a unique
refresh-keyso dialogs can calluseRefreshBus().refresh(key)after mutations
Rich-text content
For any user-authored HTML (welcome messages, notice banners, marketing copy, rich descriptions) use FB.editor() paired with App\Support\HtmlSanitizer on the backend. Sanitise on write through a FormRequest prepareForValidation hook and again on read in the controller before sharing to Inertia; render with v-html inside an sk-prose container. Never expose raw editor HTML straight from the DB to the browser — see the FormBuilder Editor field API.
Dialogs & confirmations
AppDialogfrom@lvntr/components/ui/AppDialog.vuefor modal formsuseConfirm()for destructive actions (delete, disable). Wireacceptto the mutation.useDialog()for imperative dialog state if you don't want a template-levelv-model
Feedback
- Mutation success toast: emit from the component OR use Laravel's flash session (handled by
useFlash()which auto-fires on Inertia response) - Mutation failure toast: already handled by
useApi()/SkForm— do not add a second toast - Validation errors:
SkFormsurfaces them inline; don't duplicate in toasts
Permission guards
- In template:
v-can="'users.update'"/v-role="'admin'"directives - In script:
const { can, hasRole } = useCan() - Server-side checks stay authoritative — client checks are UX only
Definitions (enums / dropdowns)
-
Load via
useDefinition()in the pageonMounted, not inside a loop -
When a mutation invalidates a definition set (e.g. role list changed), call
useDefinition().invalidate('roles')or wire it to the refresh bus:const { invalidate } = useDefinition(); const bus = useRefreshBus(); bus.on('roles-updated', () => invalidate('roles'));
Routes from the frontend
Always use Wayfinder-generated functions — never hardcode /admin/... strings.
import users from '@/routes/users';
DB.table<UserRow>().route(users.dtApi.url());
Quick checklist for a new admin module
- Domain scaffold:
php artisan make:sk-domain <Name> - Backend:
*DatatableQuery+ FormRequest + Actions + Events - Frontend
Index.vue:SkDatatablewithrefresh-key, actions wired to dialogs <Module>Form.vue:SkFormwith FormBuilder config; emitsuccess/cancel- Permissions: add resource entries to
config/permission-resources.php, runsk:seed-permissions --fresh - Translations:
lang/{en,tr}/<module>.php - Tests: Feature test for list + create + update + delete