Skip to content

Páginas personalizadas ​

Cuando una pantalla no encaja en un Resource ni en una Page (un calendario, un asistente de varios pasos, un editor visual...), CustomPage la registra en la navegación del panel y su contenido es un componente Vue que escribes tú. Arrel te da el kit de componentes de su interfaz para que la pantalla se vea como el resto del panel.

Una página personalizada tiene tres piezas:

  1. La entrada de navegación, en PHP (CustomPage).
  2. El componente Vue, en tu frontend.
  3. Los endpoints que el componente consume, en tu aplicación.

La entrada de navegación ​

php
Panel::make('admin')
    ->customPages([
        CustomPage::make('pass-list', 'Pass list')
            ->icon('clipboard-document-check')
            ->group('Attendance')
            ->sort(10)
            ->visible(fn (): bool => auth()->user()->can('attendance.create'))
            ->badge(fn (): int => Absence::pending()->count()),
    ])

CustomPage::make($slug, $label) reserva la URL /{prefix}/{panel}/custom/{slug} y la entrada de menú.

MétodoDescripción
icon(string $icon)Icono de la navegación. Véase la lista.
group(string $group)Grupo de la navegación. Las páginas y los Resources que comparten nombre de grupo se muestran juntos.
sort(int $sort)Posición dentro de la navegación, de menor a mayor.
visible(Closure $callback)Si devuelve falso, la entrada no aparece. Se evalúa en cada petición, por usuario.
badge(Closure $count)Un recuento que se muestra en el enlace. Solo se muestra si es mayor que cero.

visible() solo oculta el enlace. Los endpoints que alimentan la página deben hacer su propia autorización (véase Endpoints propios).

El componente ​

Una página se registra desde JavaScript, con un script que cargas en el panel con Panel::scripts(). Hay dos maneras de hacerlo. La recomendada es registerMountablePage.

registerMountablePage: tu aplicación Vue ​

Arrel te da un contenedor vacío y tú montas en él tu propia aplicación Vue, con tu build, tus dependencias y tu versión de Vue:

ts
import '@arrel/ui.css'
import { createApp, type App } from 'vue'
import PassListPage from './PassListPage.vue'

const mounted = new WeakMap<HTMLElement, App>()

window.Arrel.registerMountablePage('pass-list', {
    mount(container, props) {
        const app = createApp(PassListPage, props)
        app.mount(container)
        mounted.set(container, app)
    },
    unmount(container) {
        mounted.get(container)?.unmount()
        mounted.delete(container)
    },
})

props contiene panel (el identificador del panel) y slug (el de la página). Arrel nunca reconcilia los vnodes de tu componente, así que no hay ningún riesgo de tener dos instancias de Vue mezcladas.

registerPage: componentes dentro del árbol de Arrel ​

registerPage monta el componente dentro del árbol de Arrel, con su instancia de Vue. Esta es la versión runtime only, sin compilador de plantillas: hacen falta h() o componentes precompilados.

js
const { h } = window.Arrel.Vue

window.Arrel.registerPage('about', {
    render() {
        return h('div', [h('h1', 'About')])
    },
})

window.Arrel.Vue expone h, ref, reactive, computed, watch, onMounted y onBeforeUnmount.

registerMountablePageregisterPage
Instancia de VueLa tuya, separadaLa de Arrel
PlantillasCualquiera, con tu buildSolo h() o componentes precompilados
Ciclo de vidaLo gestionas tú (mount() y unmount())Lo gestiona Arrel

La API global window.Arrel ​

MiembroDescripción
panelEl identificador del panel actual.
prefixEl prefijo configurado en config('arrel.prefix').
url(path = '')Construye una URL dentro del panel: Arrel.url('posts').
navigate(path)Navega dentro de la SPA sin recargar la página. Devuelve una promesa.
refreshNavigation()Vuelve a cargar la navegación, por ejemplo para refrescar un badge() después de una acción.
registerPage(slug, component)Registra un componente Vue de Arrel.
registerMountablePage(slug, page)Registra una página montable.
VueFunciones de Vue para registerPage.

El kit de componentes (@arrel/ui) ​

El paquete publica el kit de componentes de su propia interfaz, para que tus páginas tengan los mismos botones, campos y estados. No se inyecta nada en la página: lo incorporas a tu build.

ComponenteProps principales
Badgecolor: gray, success, warning, danger o info.
Buttonvariant: primary, secondary o danger. type, disabled, href, newTab.
DatePickerv-model, displayFormat, min, max, isDisabled(iso), clearable, invalid.
Iconname, size.
Inputv-model, type, placeholder, invalid.
Labelfor.
RichEditorv-model, field, error.
Selectv-model, options, nullable, placeholder, invalid.
Textareav-model, rows, placeholder, invalid.
Togglev-model.

Además de los componentes, exporta requestJson, ApiError, formatDate y t.

  • requestJson<T>(url, init) hace un fetch con la sesión y la cabecera CSRF que Sanctum espera, y lanza un ApiError si la respuesta no es correcta. Para una respuesta 422, error.validationErrors tiene los errores de validación por campo.
  • formatDate(value, format) formatea una fecha con la sintaxis de date() de PHP.
  • t(key) devuelve un texto de la interfaz, ya traducido con Panel::translations().

Conectarlo a tu build ​

El kit se distribuye compilado en dist/ui dentro del paquete, con Vue como dependencia externa (es el Vue de tu build). Define un alias en Vite:

js
// vite.config.js
import path from 'node:path'

const arrel = path.resolve(__dirname, 'vendor/arrel/arrel')

export default defineConfig({
    resolve: {
        alias: {
            '@arrel/ui.css': `${arrel}/dist/ui/arrel-ui.css`,
            '@arrel/ui': `${arrel}/dist/ui/arrel-ui.js`,
        },
    },
})

Y, para TypeScript, la ruta a los tipos:

json
{
    "compilerOptions": {
        "paths": {
            "@arrel/ui": ["./vendor/arrel/arrel/dist/ui/types/ui.d.ts"]
        }
    }
}

Importa @arrel/ui.css una sola vez en la entrada de tus páginas, para que los componentes lleven los estilos de Arrel.

Endpoints propios ​

Los datos de una página personalizada viven en tu aplicación, no en Arrel. Para que hereden la autenticación, el segundo factor y la identificación del tenant, móntalos con la misma pila de middleware que Arrel:

php
Route::middleware([
    ...config('arrel.middleware'),
    ForceJsonResponse::class,
    EnsureFrontendRequestsAreStateful::class,
    'web',
    'auth:sanctum',
    EnsureEmailIsVerified::class,
    EnsureTwoFactorIsSetUp::class,
    ...config('arrel.authenticated_middleware'),
])->group(function (): void {
    Route::get('/arrel-app/pass-list', PassListController::class);
});

Dentro, autoriza cada petición con tus Policies o con Gate. Que visible() oculte la entrada no impide que alguien llame al endpoint a mano.

Una vez una acción de la página cambia un recuento, llama a window.Arrel.refreshNavigation() para que el badge() se refresque.