Panel actions

Add your own buttons and links to the feedback detail view — create a ticket, hand a feedback to an agent, open it in your tracker.

panelActions adds your own controls to the feedback detail view, in a row below Resolve and Delete. Use them to bridge a feedback into the tools you already run, without forking the panel.

initSiteping({
  endpoint: "/api/siteping",
  projectName: "my-project",
  panelActions: [
    {
      id: "create-ticket",
      label: "Create ticket",
      visible: (feedback) => feedback.type === "bug",
      onAction: async (feedback, { refresh }) => {
        await fetch("/api/tickets", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ feedbackId: feedback.id, message: feedback.message }),
        });
        await refresh(); // your endpoint moved the feedback to in_progress — show it
      },
    },
    {
      id: "github-issue",
      label: "File a GitHub issue",
      href: (feedback) =>
        `https://github.com/acme/site/issues/new?title=${encodeURIComponent(feedback.message.slice(0, 80))}`,
    },
  ],
});

Each action is either a button (onAction) or a link (href). TypeScript rejects an action with both, or with neither:

FieldTypeWhat it does
idstringRequired and unique. Exposed as data-action-id on the control
labelstringRequired. Rendered as plain text, never as HTML. Not translated by the widget, so localize it yourself
iconstringOptional SVG markup shown before the label — see Icons
visible(feedback) => booleanOptional. Return false to hide the action for that feedback
onAction(feedback, context) => void | Promise<void>Button. Runs on click
hrefstring or (feedback) => stringLink. Where it points — http:, https: or mailto: only

The types are exported from @siteping/widget: SitepingPanelAction (the union), SitepingPanelButtonAction, SitepingPanelLinkAction, SitepingPanelActionContext and SitepingPanelActionFeedback.

Running an action

While the promise returned by onAction is pending, the clicked button shows a spinner and every other button of the view — Resolve and Delete included — is disabled. The spinner keeps the button's accessible name and sets aria-busy. The view stays locked until the promise settles, even when refresh() re-renders it or the user comes back to that feedback, so an action never runs twice at once on the same feedback. The detail view stays open whatever the outcome.

The second argument gives you two helpers:

  • refresh() reloads the list and the markers, then re-renders the detail view with the updated feedback — or goes back to the list when it no longer matches the panel filters. Call it when your action changed the feedback server-side.
  • close() closes the panel.

Callbacks receive a frozen copy of the feedback, typed SitepingPanelActionFeedback: read-only all the way down, arrays and nested records included. Read anything you need. Writes are rejected — TypeScript flags them, and at runtime they throw in strict-mode code (and are silently ignored otherwise) — and could never change what the panel shows anyway. Type your own helpers with SitepingPanelActionFeedback: a FeedbackResponse parameter does not accept the frozen copy.

Links are real <a> elements: middle-click, "open in new tab" and screen readers behave as users expect. Web links open in a new tab with rel="noopener noreferrer", so the page under review never leaks as referrer. mailto: links open the mail client in place. Relative URLs resolve against the current page.

Any other scheme — javascript:, data:, file:… — is never rendered. A static href skips the action with a console warning; a computed one hides the action for that feedback and is reported through onError.

Errors

Whatever onAction throws or rejects with, and a visible or href function that throws, is always logged to the console as [siteping] Panel action failed:, then goes to onError when you set one — non-Error values are wrapped — and the buttons come back. The widget has no UI of its own for these errors, so the console entry makes sure a bug in your handler never fails silently. These are failures of your code, not of the feedback API, so they are not emitted on the public feedback:error event, and they can never be mistaken for a failed submission in the comment popup.

initSiteping({
  endpoint: "/api/siteping",
  projectName: "my-project",
  onError: (error) => showToast(error.message),
  panelActions: [
    {
      id: "send-to-agent",
      label: "Send to agent",
      onAction: async (feedback) => {
        const res = await fetch("/api/agent", { method: "POST", body: JSON.stringify(feedback) });
        if (!res.ok) throw new Error(`The agent refused the job (${res.status})`); // → onError
      },
    },
  ],
});

Validation

panelActions is read once, when the panel loads. An entry without a non-empty id and label, without exactly one of onAction / href, with an unsafe static href, or reusing an earlier id is skipped with a [siteping] console warning. The other actions still render.

Icons

icon takes SVG markup, drawn at 15 px before the label and hidden from assistive technologies (the label already names the control). The markup is parsed in an inert document and reduced to plain shapes — path, circle, rect, g, gradients, clip paths — keeping only their geometry and paint attributes. Scripts, foreignObject, styles, animations, links, event handlers and any attribute value that could load a resource are dropped; url(#id) references inside the icon are kept. Still, treat it as static markup you control. A string that is not an <svg> is ignored with a warning, and the action keeps its label.

React

With useSiteping, onAction, visible and a function href are read at call time, so they can close over fresh state (an auth token, the current user) without re-initializing the widget. The list itself — ids, labels, icons, static hrefs — is read at mount, like every other structural option.

Edit on GitHub

On this page