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.
type:"hook" hedefi olan actionUrl'i ise "Uç noktalar" kartındaki "Action path" alanından girersin. Ayrıntı: Action Ucu (actionUrl).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{ success, message, level, display } → toast.{ decision, message, attach } → işlemi durdurur/sürdürür.| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| id | string | ✓ | Eklenti içinde benzersiz. |
| slot | enum | ✓ | Yerleşim (whitelist). Geçerli değerler aşağıda. |
| label | i18n obj | ✓ | Buton metni (text-only). |
| icon | enum | – | İkon whitelist'i (aşağıda). |
| action.type | hook | page | form | ✓ | Tıklama davranışı. |
| action.hook | string | – | type=hook/form ise zorunlu — action adı. |
| action.pageId | string | – | type=page ise zorunlu — mevcut bir ui:page id'si. |
| action.form | object | – | type=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. |
| confirm | i18n obj | – | Tı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
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.)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.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).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.
| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| actor.userId | string | ✓ | Kullanı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):
actor'a güvenme; yetkiyi mutlaka sunucuda uygula.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.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.// /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 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.