Aksiyon Butonları (ui:button)

Eklenti, panelin belirli bir slot'una (şu an paket detayı) bir buton yerleştirir. Tenant tıklayınca ya eklentinin iframe sayfasını modal açar ya da senin action ucuna imzalı bir istek gider. Action Hook'ların aksine NON-BLOCKING'tir: çekirdek akışı durdurmaz, sonucu yalnız toast gösterir.

Editör hazır. Manifest editöründe "Aksiyon Butonları" kartından buton ekleyebilirsin; type:"hook" hedefi olan actionUrl'i ise "Uç noktalar" kartındaki "Action path" alanından girersin. Ayrıntı: Action Ucu (actionUrl).

Nasıl çalışır

Panel — Paket detay ekranı
   └─ [ikon] "Kuryeye Gönder"   ◄── ui:button (slot: packet.detail.actions)
          │ tıkla  (varsa: onay diyaloğu)
          ├─ action.type = "page"  → eklenti iframe'in MODAL açılır
          │                          (context: target:{type,id} — App Bridge + session token)
          │
          ├─ action.type = "form"  → restoyeni native form render eder → kullanıcı doldurur
          │                          → /action'a formData ile gider (aşağıdaki POST + "formData")
          │
          └─ action.type = "hook"  → senin /action ucuna imzalı SENKRON POST
                                      { type:"action", hook, tenantId, slot, target:{type,id}, actor:{userId,role} }
                                          │
                                          ▼
                                 sen → { success, message, level, display }  →  kullanıcıya TOAST

Action Hook'tan farkı

  • Aksiyon butonu: kullanıcı bir butona basar; NON-BLOCKING; yanıt { success, message, level, display } → toast.
  • Action Hook: bir çekirdek işlem (örn. masa kapatma) öncesinde otomatik tetiklenir; BLOCKING; yanıt { decision, message, attach } → işlemi durdurur/sürdürür.

Manifest şeması — manifest.buttons[]

AlanTipZorunluAçıklama
idstringEklenti içinde benzersiz.
slotenumYerleşim (whitelist). Geçerli değerler aşağıda.
labeli18n objButon metni (text-only).
iconenumİkon whitelist'i (aşağıda).
action.typehook | page | formTıklama davranışı.
action.hookstringtype=hook/form ise zorunlu — action adı.
action.pageIdstringtype=page ise zorunlu — mevcut bir ui:page id'si.
action.formobjecttype=form ise zorunlu — bir Declarative Form'u referans alır (formId) ve içeriğini gömer: { formId, title?, fields, submitLabel? }. Alanlar (key/type/label/required/maxLength/options) forms[]'te tanımlanır.
confirmi18n objTıklamadan önce onay diyaloğu metni.
"manifest": {
  "requestedScopes": ["ui:button"],            // type:"page"→AYRICA "ui:page", type:"form"→AYRICA "ui:form"
  "actionUrl": "https://acme.com/api/action",  // type:"hook"/"form" hedefi (ops.; yoksa webhook)
  "buttons": [
    {
      "id": "send-to-courier",                 // eklenti içi benzersiz
      "slot": "packet.detail.actions",         // BUTTON_SLOTS whitelist
      "label": { "tr": "Kuryeye Gönder", "en": "Send to courier" },
      "icon": "truck",                         // BUTTON_ICONS whitelist (opsiyonel)
      "action": {
        "type": "hook",                        // "hook" | "page" | "form"
        "hook": "packet.sendToCourier"         // type:hook/form → action adı
        // "pageId": "tracking"                // type:page → mevcut bir ui:page id'si (modal açılır)
        // type:form → "form": { "formId":"courierForm", "title":{…}, "fields":[…], "submitLabel":{…} }
        //             (formId = manifest.forms[] içindeki bir form; portal içeriğini gömer)
      },
      "confirm": { "tr": "Bu paketi kuryeye göndereyim mi?" }   // opsiyonel onay
    }
  ]
}

İzinli slot değerleri:

  • packet.detail.actions — Paket detayı — işlemler (dropdown)

İzinli icon değerleri:

truckboxprintcheckxmarkpaper-planebelltagmap-pin

Tıklama davranışı

  • type: "page" — eklentinin pageId sayfası modal/drawer olarak açılır. Bağlam App Bridge getContext'i ile uniform gelir ({ target:{type,id} }); backend'inde session token'ı doğrula. (iframe origin pinli — güvenlik.)
  • type: "hook" — senin action ucuna (manifest actionUrl, yoksa ana webhook) imzalı senkron POST gelir; kısa timeout içinde { success, message, level, display } dön. Sonuç toast olarak gösterilir, akış bloklanmaz.
  • type: "form" — buton, eklediğin bir Declarative Form'u referans alır (action.form.formId); portal o formun şemasını (title/fields/submitLabel) gömer. restoyeni native render eder, kullanıcı doldurur, değerler formData olarak action isteğine eklenir (hook ile birlikte). ui:form scope gerekir. Alan tipleri kataloğun formFieldTypes'ından (drift-free: GET /plugin-meta/catalog).

Rol yetkisi — actor (manager / staff)

Buton tüm tenant kullanıcılarına görünür — platform butonu role göre gizlemez. Kim bastığı action body'sinde imzalı actor ile gelir; yetkiyi her zaman sunucu tarafında (action handler'ında) uygula.

AlanTipZorunluAçıklama
actor.userIdstringKullanıcının kalıcı/opak uid'si (PII değil). users/get id'si + diğer event'lerin actor.userId'si ile aynı.
actor.role"manager" | "staff"Kaba rol: manager (işletme yönetimi yetkisi) veya staff (diğer tüm çalışanlar). Restomenum'un tam izin modelinin özetidir; ham yetki listesi gizlilik gereği sızmaz.

İşi yapmadan önce actor.role'ü kontrol et; reddi success:false + mesaj ile dön (handler örneği bir üstteki Referans /action'da):

Buton görünürlüğü yetki değildir. İmzayı doğrulamadan actor'a güvenme; yetkiyi mutlaka sunucuda uygula.

İnce yetki (per-user) — opsiyonel

role ikilidir (manager/staff). Daha ince kontrol için kendi izin tablonu userId'ye göre tut:

// İnce yetki (per-user) — role ikili (manager/staff). Daha ince kontrol için userId tablosu:
const ALLOWED = new Set(['lZz6Bq…', 'aB12…']);   // bu işi yapabilen userId'ler (kendi ayar sayfandan yönet)
if (!ALLOWED.has(actor?.userId))
  return res.json({ success: false, level: 'error', message: 'Yetkiniz yok' });
// Kullanıcı ADI göstermek istersen: GET /plugin-api/users/get (users:read + PII consent) → { id, name }
// actor.userId ↔ users/get'teki id BİREBİR eşleşir.
Aynı actor başka nerede? actor:{userId,role} yalnız butonlarda değil — declarative form gönderiminde ve before-hook (table.close) gate'inde de aynı şekilde gelir. Rol/kullanıcı mantığını tek yerde kurup buton + form + hook için tekrar kullan.

Referans — /action (Node)

// /action — Aksiyon butonu "hook" hedefi (NON-BLOCKING). Restomenum HMAC imzalı SENKRON POST eder.
// Restomenum → sana:  { type:"action", hook, tenantId, slot, target:{type,id},
//                       actor:{ userId, role }, formData?, occurredAt, id }   // actor İMZALI → güvenilir
// Sen → Restomenum:   { success: true, message: "Kuryeye gönderildi", level: "success", display: "toast" }
import express from 'express';
const app = express();

app.post('/action', express.raw({ type: '*/*' }), async (req, res) => {
  // 1) imzayı doğrula (webhook ile AYNI imza şeması — raw body); başarısızsa res.sendStatus(401)
  // 2) target.id'nin bu tenant'a (tenantId) ait olduğunu kendi tarafında doğrula
  const { hook, tenantId, target, actor } = JSON.parse(req.body.toString('utf8'));

  // hook ile hangi buton, target ile hangi nesne (target.type: packet)
  if (hook !== 'packet.sendToCourier')
    return res.json({ success: false, level: 'error', display: 'popup', message: 'Bilinmeyen işlem' });

  // 3) ROL YETKİSİ — buton herkese görünür; yetkiyi SUNUCUDA uygula (actor imzalı → güvenilir)
  if (actor?.role !== 'manager')
    return res.json({ success: false, level: 'error', display: 'popup', message: 'Bu işlem yalnız yöneticiler içindir.' });

  const ok = await sendToCourier(tenantId, target.id);   // senin işin
  // başarı → toast (hızlı bilgi), hata → popup (önemli sonuç)
  res.json({ success: ok, level: ok ? 'success' : 'error', display: ok ? 'toast' : 'popup', message: ok ? 'Kuryeye gönderildi' : 'Gönderilemedi' });
});

// message DÜZ METİNDİR (HTML değil). level=renk, display=sunum (toast/popup). Akışı BLOKLAMAZ:
// decision / allow-deny / failMode / receipt YOK — bu yönüyle Action Hook'tan ayrılır. Tam sözleşme: /docs/action-url

Adımlar

  1. ui:button scope'unu iste (type=page ise ayrıca ui:page + bir sayfa).
  2. Manifest'te manifest.buttons[] tanımla: slot, label, icon, action, (opsiyonel) confirm.
  3. type=hook ise actionUrl ucunu yaz: imzayı doğrula, target.id'yi tenant'a göre teyit et, işini yap, { success, message, level, display } dön.
İstek imzalıdır (webhook ile aynı şema) — her zaman doğrula. target.id'nin gerçekten istek sahibi tenant'a (tenantId) ait olduğunu kendi tarafında kontrol et (cross-tenant erişimi engelle). Buton yalnız aktif + bağlı ve ui:button'lı eklentiden render edilir.