Orbito - Dokumentacja modułówOrbito - Dokumentacja modułów
Wprowadzenie
Pierwsze kroki
API
Komunikacja
Widoki
Wdrożenie
Społeczność
For AI agents
Wprowadzenie
Pierwsze kroki
API
Komunikacja
Widoki
Wdrożenie
Społeczność
For AI agents
  • Widoki

    • Widoki stron modułu
    • OrbitoViewEngine (OVE)
    • Widoki HTML i Vue
    • Rozszerzanie widoków przez hooki

Rozszerzanie widoków przez hooki

Moduł może w dowolnym miejscu swojej strony pozostawić zaczep widokowy – punkt, w którym inne moduły dokładają własne sekcje UI. Zaczep to blok OVE type: dynamic ze schemaFrom, który woła metodę modułu-właściciela. Metoda emituje hook views.<nazwaStrony>, zbiera fragmenty widoku zwrócone przez moduły rozszerzające i skleja je w jeden root widget – ten widget wraca do przeglądarki i zostaje wyrenderowany w miejscu bloku.

Dwie rzeczy są tu istotne:

  • Zależność jest jednostronna. Właściciel strony nie zna modułów rozszerzających – nie wywołuje ich metod, nie ma ich w zależnościach i działa poprawnie, gdy żaden z nich nie jest zainstalowany. Wystarczy zainstalować nowy moduł rejestrujący hook, żeby na stronie pojawiła się jego sekcja.
  • Fragment to dane, nie kod. Moduł zwraca opis widoku OVE, a nie HTML. Zanim schemat zostanie wyrenderowany, przechodzi walidację schemaGuard: dozwolone są wyłącznie znane typy widoków i znane akcje, typy wstawiające surowy kod strony (html, cssbox) wymagają jawnej zgody allow, a rejestrowanie komponentów (components) jest zabronione.

Dalej opisane są dwa kontrakty core.orders, które trzymają się tej samej zasady jednostronnej zależności: flagi na liście zamówień (hook orders.flags) i powrót z karty zamówienia (zmienna ordersReturnTarget).

Jak to działa

Strona OVE właściciela              Metoda właściciela                 Moduł rozszerzający
────────────────────────            ────────────────────────           ────────────────────────
type: dynamic
  method: getOrderDetailsBlock ───►  requirePermission / pobranie
  schemaFrom: view                   zamówienia
                                     Core.execHook(
                                       'views.orderDetails',
                                       { orderId, order })      ─────►  registerHook('views.orderDetails', handler)
                                                                ◄─────  { key, title, order, view }
                                     walidacja + sortowanie
                                     po `order`, sklejenie
                                     w root `flex`
◄─────────────────────────────────   { view, fragments }
render (schemaGuard)

Konwencja nazewnictwa hooków widokowych

Hooki widokowe przyjmują prefiks views. i opisują stronę, na której się pojawiają:

views.<nazwaStrony>

Przykłady:

views.orderDetails      – strona szczegółów zamówienia
views.customerProfile   – profil klienta
views.productEdit       – edycja produktu

Właścicielem nazwy i kontraktu jest moduł, który hook emituje – to on decyduje, co trafia do handlerów i jak wygląda fragment.

Dokumentuj swoje zaczepy

Jeśli Twój moduł wystawia zaczepy widokowe, opisz je w dokumentacji razem ze strukturą obiektu przekazywanego do handlera. Umożliwi to innym deweloperom tworzenie rozszerzeń bez zaglądania do kodu.

Kontrakt fragmentu

Handler dostaje dane zaczepu (dla views.orderDetails jest to { orderId, order }) i zwraca jeden fragment, tablicę fragmentów albo null/undefined, gdy nie ma nic do pokazania:

{
    key: 'core.shipping',   // stabilny identyfikator (zwykle kod modułu)
    title: 'Przesyłki',     // opcjonalny; diagnostyka i logi – nagłówek ma sam fragment (karta)
    order: 20,              // kolejność sklejania rosnąco; brak → 100
    view: { type: 'card' }  // czysty root widget OVE
}

Zasady po stronie emitenta:

SytuacjaZachowanie
Brak key (string) lub brak view.type (string)Fragment odrzucony, ostrzeżenie w logu
Powtórzony keyWygrywa pierwszy fragment, kolejny odrzucony (log)
Brak orderDomyślnie 100
Równe orderZostaje kolejność odpowiedzi hooka
null / undefinedPoprawny brak fragmentu – bez ostrzeżenia
Brak jakiegokolwiek fragmentuMetoda zwraca view: null, a DynamicView pokazuje emptyMessage

Wyjątek w handlerze to pusta odpowiedź

Protokół hooka nie ma kanału błędów – wyjątek rzucony w handlerze daje pustą odpowiedź, więc fragment po prostu zniknie ze strony bez śladu w logach modułu. Dlatego cała treść handlera powinna być w try/catch, a błąd trzeba zalogować samemu i zwrócić null.

Przykład – strona szczegółów zamówienia

Moduł core/orders – właściciel widoku

Na stronie order-details-v2.yaml miejsce na fragmenty innych modułów to jeden blok dynamiczny:

- type: dynamic
  id: orderDetailsBlock
  module: core/orders
  method: getOrderDetailsBlock
  params:
    orderId: '{{ $page.urlParams.orderId }}'
  schemaFrom: view
  failSoft: false
  loadingMessage: Ładowanie modułów zamówienia...
  emptyMessage: Brak modułów rozszerzających kartę zamówienia
  fetchErrorMessage: Nie udało się załadować bloku modułów zamówienia
  schemaErrorMessage: Moduły zwróciły niepoprawny widok

schemaFrom: view mówi silnikowi, że pod kluczem view w odpowiedzi metody znajduje się schemat widoku (zamiast lokalnego template).

Metoda getOrderDetailsBlock sprawdza uprawnienie, pobiera zamówienie, emituje hook i skleja fragmenty:

const { Core, DB, Log } = require('@orbito/api-lib');
const Order = require('../src/models/Order');
const { PERMISSIONS, requirePermission } = require('../src/register-permissions');
const {
    ORDER_DETAILS_HOOK,          // 'views.orderDetails'
    normalizeFragments,          // walidacja, deduplikacja, sortowanie
    buildOrderDetailsView,       // sklejenie w root widget
} = require('../src/lib/order-details-block');

module.exports = {
    description: 'Buduje blok karty zamówienia z fragmentów dostarczonych przez inne moduły',

    params: {
        orderId: { type: 'string', required: true },
    },

    handler: async function(params, ctx) {
        requirePermission(PERMISSIONS.ORDERS_VIEW, ctx);

        const order = await Order.findOne({ _id: new DB.ObjectId(params.orderId) });
        if (!order) {
            throw new Error('Order not found');
        }

        // Handlery dostają surowy dokument zamówienia – fragmenty i tak wołają
        // własne metody modułu po `orderId`.
        const responses = await Core.execHook(ORDER_DETAILS_HOOK, {
            orderId: String(order._id),
            order,
        });

        const fragments = normalizeFragments(responses, (message, details) => Log.warn(message, details));

        return {
            view: buildOrderDetailsView(fragments),
            fragments: fragments.map(({ key, title, order: position }) => ({ key, title, order: position })),
        };
    },
};

Sklejony widok to zwykła kolumna – fragmenty renderują się jeden pod drugim:

{
    type: 'flex',
    id: 'orderDetailsBlock',
    direction: 'column',
    gap: 3,
    childs: fragments.map(fragment => fragment.view),
}

Moduł core/shipping – rozszerzający widok

Moduł przesyłek rejestruje handler w index.js:

const { Core } = require('@orbito/api-lib');
const { orderDetailsViewHandler } = require('./src/hooks/order-details-view');

// Zależność jest jednostronna: `core.orders` emituje hook i skleja odpowiedzi,
// nie znając tego modułu.
Core.registerHook('views.orderDetails', orderDetailsViewHandler);

Sam handler nie buduje widoku w kodzie – ładuje schemat z pliku YAML i podstawia w nim placeholder @orderId:

const path = require('path');
const { Log } = require('@orbito/api-lib');
const { loadViewSchema, bindPlaceholders } = require('../lib/view-schema');

const SLOT_VIEW_FILE = path.join(__dirname, '..', '..', 'pages', 'slots', 'order-details.yaml');

async function orderDetailsViewHandler(payload) {
    try {
        const orderId = payload?.orderId;
        if (!orderId) {
            return null;
        }

        return {
            key: 'core.shipping',
            title: 'Przesyłki',
            order: 20,
            view: bindPlaceholders(loadViewSchema(SLOT_VIEW_FILE), { orderId }),
        };
    } catch (err) {
        Log.error('Shipping: nie udało się zbudować fragmentu widoku zamówienia', {
            orderId: payload?.orderId,
            error: err?.message,
        });
        return null;
    }
}

module.exports = { orderDetailsViewHandler };

Fragment (pages/slots/order-details.yaml) to czysty root widget OVE – karta z listą przesyłek, wołająca wyłącznie metody własnego modułu:

type: card
id: shpCard
title: Przesyłki
content:
  - type: list
    id: shpList
    module: core/shipping
    method: shipments.getByOrderId
    params:
      orderId: '@orderId'
    emptyMessage: Brak przesyłek dla tego zamówienia
    loadingMessage: Ładowanie przesyłek...
    template:
      type: text
      id: shpListRowTitle.$
      content: '{{ shpList.$.carrier }} · {{ shpList.$.trackNo }}'

'@orderId' jest podstawiane przez handler przy każdym wywołaniu hooka. Fragment nie ma dostępu do $page.urlParams właściciela, więc wszystkie dane kontekstowe muszą przyjść tą drogą.

Dobre praktyki

  • Prefiksuj identyfikatory. Fragmenty kilku modułów renderują się na jednej stronie i współdzielą przestrzeń zmiennych. Nadawaj id widoków i nazwom zmiennych strony prefiks kodu modułu (shpList, shpManualModal, invDocsList), żeby nie kolidowały z innymi fragmentami ani ze stroną właściciela.
  • Wołaj tylko własne metody. W module: fragmentu wpisuj wyłącznie własny moduł – inaczej wracasz do zależności między modułami, którą zaczep miał usunąć.
  • Uprawnienia sprawdzaj dwustronnie. Właściciel sprawdza dostęp do całego bloku (np. ORDERS_VIEW w metodzie emitującej hook), a moduł rozszerzający – w każdej metodzie, którą fragment wywołuje z przeglądarki.
  • Nie licz na kolejność rejestracji modułów. Pozycję fragmentu ustala pole order, nie moment załadowania modułu.

Różnice względem poprzedniej wersji

  • Nie ma już handlerów stron (Pages.registerPage z callbackiem budującym widok). Strony to statyczne pliki OVE, a treść dynamiczna wchodzi przez blok type: dynamic wołający metodę modułu.
  • Hooki widokowe nazywają się views.<nazwaStrony> zamiast view:<nazwaStrony>.
  • Handler zwraca fragment { key, title, order, view }, a nie { type: 'ove', template }; view to czysty root widget OVE.
  • Filtrowanie pustych odpowiedzi, deduplikacja key i sortowanie należą do emitenta – moduł rozszerzający zwraca po prostu null, gdy nie ma nic do pokazania.

Flagi na liście zamówień – hook orders.flags

Ten sam wzorzec obsługuje kolumnę FLAGI na liście zamówień (core/orders, strona orders). Różnica jest jedna: moduł rozszerzający nie zwraca fragmentu OVE, tylko opis flagi – dane, z których emitent sam buduje ikonę w wierszu i treść dymka ze szczegółami.

Hook jest emitowany raz na stronę wyników metody search (≤ 100 zamówień) oraz raz dla pojedynczego zamówienia, gdy operator otwiera dymek flagi (metoda flags.details). Handler robi więc jedno zapytanie do bazy na całą stronę listy, a nie jedno na zamówienie.

Argument hooka

{
    orderIds: ['<id>', ...],
    // lekka projekcja zamówień – tyle, ile trzeba do oceny własnej flagi
    orders: [{
        _id: '<id>',
        invoice: { isNeeded: true },
        isPaid: true,
        isPacked: false,
        delivery: { isCod: false },
    }],
}

Nie wołaj emitenta z wnętrza handlera

Projekcja orders istnieje po to, żeby handler nie musiał wołać z powrotem core/orders. Wywołanie zwrotne w trakcie obsługi hooka może zakleszczyć kontenery obu modułów.

Kontrakt odpowiedzi

Handler zwraca jeden opis flagi, tablicę opisów albo null/undefined, gdy nie ma nic do pokazania:

{
    key: 'shipment',              // stabilny, unikalny identyfikator (wymagany)
    label: 'Przesyłka',           // etykieta dla operatora (wymagana)
    icon: 'truck-delivery',       // ikona Tabler bez prefiksu `ti-` (wymagana)
    order: 30,                    // kolejność w wierszu rosnąco; brak → 100
    inactive: {                   // opcjonalny opis stanu „nic się nie wydarzyło”
        title: 'Brak przesyłki',
        summary: 'Nie utworzono przesyłki dla zamówienia',
    },
    states: {                     // stan per zamówienie – tylko dla tych, które go mają
        '<orderId>': {
            active: true,
            tone: 'success',      // success | info | warning | muted
            title: 'Przesyłka utworzona',
            summary: 'Kurier: DPD\nNumer przesyłki: 0001234567',
        },
    },
}

Zasady po stronie emitenta:

SytuacjaZachowanie
null / undefinedPoprawny brak flagi – bez ostrzeżenia
Brak key, label albo icon (niepuste stringi)Opis odrzucony, Log.warn('Orders: odrzucono flagę zamówienia', { key, reason })
Powtórzony keyWygrywa pierwszy opis, kolejny odrzucony (log)
key zarezerwowany (payment, cod, packed, photo)Opis odrzucony – to flagi natywne core.orders
Brak orderDomyślnie 100; flagi natywne mają 10, 20, 40, 50, więc zewnętrzne wchodzą między nie własnym order
Równe orderNajpierw flagi natywne, dalej kolejność odpowiedzi hooka
Brak wpisu w states dla zamówieniaFlaga nieaktywna: tone: muted, title/summary z inactive (albo sama label)
tone spoza listymuted
Awaria hooka (wyjątek, timeout)Lista zamówień zostaje z samymi flagami natywnymi, powód trafia do Log.warn

Wiersz metody search dostaje gotową tablicę posortowaną po order: flags: [{ key, label, icon, active, tone, title, summary, imageDataUrl }].

Szczegóły są eager

title i summary przychodzą w tej samej odpowiedzi hooka, co sama flaga – nie ma drugiego kontraktu „szczegóły flagi” i żadnych wywołań zwrotnych przy otwieraniu dymka. Handler i tak ma jedno zapytanie $in po orderIds, a opis to ok. 100 B na zamówienie.

Cały handler w try/catch

Jak przy hookach widokowych: wyjątek w handlerze daje pustą odpowiedź bez śladu w logach modułu. Błąd loguj sam i zwracaj null – lista zamówień pokaże się wtedy bez Twojej flagi.

Przykład – flaga „Przesyłka” w core/shipping

const { Core, Log } = require('@orbito/api-lib');
const Shipment = require('./src/models/Shipment');

// Emitent (`core.orders`) woła hook raz na stronę wyników, a tam `perPage` jest ≤ 100.
const MAX_ORDER_IDS = 100;

Core.registerHook('orders.flags', async payload => {
    try {
        const orderIds = (payload?.orderIds || [])
            .filter(orderId => typeof orderId === 'string' && orderId.trim())
            .slice(0, MAX_ORDER_IDS);

        if (!orderIds.length) {
            return null;
        }

        // Jedno zapytanie na całą stronę listy.
        const shipments = await Shipment.find({ orderId: { $in: orderIds } }, 1000, 0, { createdAt: -1 });

        const states = {};
        for (const shipment of shipments) {
            const orderId = String(shipment.orderId || '');
            if (!orderId || states[orderId]) {
                continue;
            }

            states[orderId] = {
                active: true,
                tone: shipment.status === 'error' ? 'warning' : 'success',
                title: 'Przesyłka utworzona',
                summary: `Kurier: ${shipment.carrier}\nNumer przesyłki: ${shipment.trackNo}`,
            };
        }

        return {
            key: 'shipment',
            label: 'Przesyłka',
            icon: 'truck-delivery',
            order: 30,
            inactive: {
                title: 'Brak przesyłki',
                summary: 'Nie utworzono przesyłki dla zamówienia',
            },
            states,
        };
    } catch (err) {
        Log.error('Shipping: nie udało się zbudować flagi przesyłki', { error: err?.message });
        return null;
    }
});

Zamówienia bez wpisu w states nie wymagają żadnej obsługi – emitent pokazuje im szarą flagę zbudowaną z inactive.

Powrót z karty zamówienia – zmienna ordersReturnTarget

Moduł, który linkuje ze swojej listy do karty zamówienia (navigate do core/orders, strona details/<orderId>), zostawia przed nawigacją cel powrotu w zmiennej globalnej ordersReturnTarget. Karta zamówienia renderuje z niej przycisk „Wróć do …” i dzięki temu nie zna żadnego z modułów, które do niej prowadzą.

{
    module: 'core/shipping',   // moduł, do którego wraca przycisk
    page: 'shipments',         // strona tego modułu
    label: 'przesyłek',        // dopełniacz do tytułu „Wróć do przesyłek”
}

Kto co robi:

RolaKiedyCo ustawia
Moduł linkującyTuż przed navigate do karty zamówieniasetGlobalVar ordersReturnTarget z obiektem { module, page, label }
Moduł linkującyW onLoad własnej listysetGlobalVar ordersReturnTarget → null (żeby cel nie został po poprzedniej wizycie)
core.ordersPo kliknięciu przycisku „Wróć do …”Kasuje zmienną (null) i nawiguje pod module/page z celu
core.ordersGdy zmienna jest pustaPokazuje domyślny przycisk „Wróć do zamówień”

Ustawienie celu w akcji OVE to dwa kroki w jednym onClick:

onClick:
  - action: setGlobalVar
    key: ordersReturnTarget
    value:
      module: core/shipping
      page: shipments
      label: przesyłek
  - action: navigate
    module: core/orders
    page: details/{{ shpShipmentsTable.$.orderId }}

A zerowanie przy wejściu na własną listę – jeden krok w onLoad:

onLoad:
  - action: setGlobalVar
    key: ordersReturnTarget
    value: null

Zmienna globalna żyje dłużej niż strona

ordersReturnTarget jest wspólna dla całego panelu, więc bez zerowania przycisk powrotu prowadziłby do listy odwiedzonej kilka nawigacji wcześniej. Zeruj ją zawsze, gdy operator wchodzi na Twoją listę.

Podsumowanie

RolaMechanizmZadanie
Właściciel stronytype: dynamic + schemaFromWskazuje metodę, która zbuduje blok
Właściciel stronyCore.execHook('views.<strona>', data)Zbiera fragmenty, waliduje je i skleja w jeden root widget
Moduł rozszerzającyCore.registerHook('views.<strona>', handler)Zwraca fragment { key, title, order, view } albo null
Właściciel listyCore.execHook('orders.flags', { orderIds, orders })Zbiera opisy flag, waliduje je i skleja z flagami natywnymi
Moduł rozszerzającyCore.registerHook('orders.flags', handler)Zwraca opis flagi { key, label, icon, order, inactive, states } albo null
Moduł linkujący do zamówieniasetGlobalVar ordersReturnTargetZostawia cel powrotu { module, page, label } dla przycisku „Wróć do …”
Ostatnio zaktualizowano:: 22.09.2026, 08:28
Contributors: Wiktor Koźmiński, Claude Fable 5.1
Prev
Widoki HTML i Vue