@alliance-droid/status-feedback-system 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,8 @@ This is the **client package** — UI components that talk to a [status-feedback
14
14
  │ │ StatusPage │──┼───────┼──│ /api/status │ │
15
15
  │ │ FeedbackButton │ │ │ │ /api/feedback │ │
16
16
  │ │ DevFeedback │ │ │ │ /api/incidents │ │
17
- │ │ FeedbackBoard │ │ │ │ /api/board │ │
17
+ │ │ Feedback board │ │ │ │ /api/board │ │
18
+ │ │ (composable) │ │ │ │ │ │
18
19
  │ └───────────────────┘ │ │ └────────────────────┘ │
19
20
  │ │ │ │ │
20
21
  │ npm package │ │ MSSQL + Azure Blob │
@@ -152,32 +153,70 @@ Standalone feedback form — embed it wherever you need it.
152
153
  | `onSuccess` | `() => void` | — | Called after successful submission |
153
154
  | `onError` | `(error: string) => void` | — | Called on submission error |
154
155
 
155
- ### FeedbackBoard
156
+ ### Feedback board (composable)
156
157
 
157
- Public feedback board with voting, category filtering, and admin responses.
158
+ > 📖 **Full integration guide:** [docs/custom-feedback-integration.md](docs/custom-feedback-integration.md) — composing the parts into your own shell, status management via `getAuthToken`, list restyling, and the not-configured fallback.
159
+
160
+ The board is composed from parts around **one shared state** (`createFeedbackBoard`), so it drops into your own layout/shell — app rails, breadcrumb, a right-hand detail panel, whatever you have.
158
161
 
159
162
  ```svelte
160
- <FeedbackBoard
161
- apiUrl="https://status-admin.example.com"
162
- apiKey="sf_abc123..."
163
- title="Feature Requests"
164
- showSubmit={true}
165
- submitCategories={['Bug', 'Feature Request', 'Improvement']}
166
- categoryConfig={{
167
- Bug: { icon: 'fa-solid fa-bug', label: 'Bugs', description: 'Something broken' },
168
- 'Feature Request': { icon: 'fa-solid fa-lightbulb', label: 'Ideas' },
169
- }}
170
- />
163
+ <script lang="ts">
164
+ import {
165
+ createFeedbackBoard,
166
+ FeedbackSidebar,
167
+ FeedbackList,
168
+ FeedbackDetail,
169
+ FeedbackSubmitForm,
170
+ type BoardItem,
171
+ } from '@alliance-droid/status-feedback-system';
172
+
173
+ let { data } = $props();
174
+ const board = createFeedbackBoard({
175
+ apiUrl: 'https://status-admin.example.com',
176
+ apiKey: 'sf_abc123...',
177
+ userEmail: data.user?.email,
178
+ getAuthToken: () => auth.getAccessToken(), // optional — enables status management
179
+ categoryConfig: {
180
+ Bug: { icon: 'fa-solid fa-bug', label: 'Bugs' },
181
+ 'Feature Request': { icon: 'fa-solid fa-lightbulb', label: 'Ideas' },
182
+ },
183
+ });
184
+
185
+ let selected = $state<BoardItem | null>(null);
186
+ </script>
187
+
188
+ <YourSidebar><FeedbackSidebar {board} /></YourSidebar>
189
+ <YourMain><FeedbackList {board} onSelect={(i) => (selected = i)} /></YourMain>
190
+ <YourRail>
191
+ {#if selected}<FeedbackDetail {board} item={selected} />{/if}
192
+ </YourRail>
193
+ <!-- Render <FeedbackSubmitForm {board} /> wherever you want submission. -->
171
194
  ```
172
195
 
173
- | Prop | Type | Default | Description |
174
- |------|------|---------|-------------|
196
+ `createFeedbackBoard(options)` returns a reactive `FeedbackBoardState` (items, categories, filters, voting, `loadTransitions`/`setStatus`, …) shared by every sub-component.
197
+
198
+ | Option | Type | Default | Description |
199
+ |--------|------|---------|-------------|
175
200
  | `apiUrl` | `string` | required | Admin backend URL |
176
- | `apiKey` | `string` | required | Project API key |
177
- | `title` | `string` | `'Feedback Board'` | Header title |
178
- | `showSubmit` | `boolean` | `true` | Show inline submit form |
179
- | `submitCategories` | `string[]` | `[]` | Category options for submit form |
180
- | `categoryConfig` | `Record<string, { label?, icon?, description? }>` | `{}` | Customize sidebar categories |
201
+ | `apiKey` | `string` | required | Project API key (`sf_…`) |
202
+ | `userEmail` | `string` | — | Authenticated user (required to submit feedback/replies) |
203
+ | `categoryConfig` | `Record<string, { label?, icon?, description? }>` | `{}` | Relabel / add icons to sidebar categories |
204
+ | `getAuthToken` | `() => string \| Promise<string \| null>` | — | Bearer token so privileged users can set status (see below) |
205
+ | `pageSize` | `number` | `50` | Items per page |
206
+
207
+ Exported parts: `createFeedbackBoard`, `FeedbackSidebar`, `FeedbackList`, `FeedbackItem`, `FeedbackDetail`, `FeedbackSubmitForm`.
208
+
209
+ Categories are fixed by the system (**Bug**, **Feature Request**) — `categoryConfig` only relabels or adds icons.
210
+
211
+ #### Setting status (Admin / ProductOwner)
212
+
213
+ When you pass `getAuthToken`, a signed-in **Admin** or **ProductOwner** can change an item's status from the board (via the status badge → menu) — Open → Considering → Accepted, or a decline with a reason — no trip to the admin dashboard.
214
+
215
+ - `getAuthToken` must return the user's **Alliance OIDC access token** whose audience covers **`status-feedback-admin`** (an *id token*, whose `aud` is your app's client id, is rejected). It's called on demand; return `null` for anonymous / non-privileged users and the control simply won't appear.
216
+ - The token travels as `Authorization: Bearer …` to the admin backend, which **verifies it against the IdP** and derives the user's roles. Authorization (per-project access + which transitions a role may make, including reason prompts on declines/reopens) is enforced **server-side** by the same logic as the dashboard — the client never decides permissions.
217
+ - The available transitions are returned by the backend per item, so the menu always reflects exactly what that user may do.
218
+
219
+ > Security note: the project `apiKey` alone never grants status changes — it only identifies the project. Every mutation additionally requires a valid IdP token, so the public board key stays safe to embed.
181
220
 
182
221
  ### DevFeedback
183
222
 
@@ -253,10 +292,16 @@ import {
253
292
  ```typescript
254
293
  // Components
255
294
  import {
256
- StatusPage, StatusBanner, FeedbackForm, FeedbackButton,
257
- FeedbackBoard, DevFeedback,
258
- StatusIndicator, UptimeBar, IncidentTimeline, SystemCard,
295
+ StatusPage, StatusBanner, FeedbackForm, FeedbackButton, DevFeedback,
296
+ StatusIndicator, UptimeBar, IncidentTimeline, SystemCard, PasteImageInput,
297
+ } from '@alliance-droid/status-feedback-system';
298
+
299
+ // Feedback board (composable)
300
+ import {
301
+ createFeedbackBoard,
302
+ FeedbackSidebar, FeedbackList, FeedbackItem, FeedbackDetail, FeedbackSubmitForm,
259
303
  } from '@alliance-droid/status-feedback-system';
304
+ import type { FeedbackBoardState, FeedbackBoardOptions, CategoryConfig } from '@alliance-droid/status-feedback-system';
260
305
 
261
306
  // API Client
262
307
  import { createStatusClient } from '@alliance-droid/status-feedback-system';
package/dist/api.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * const { systems, incidents } = await client.getStatus();
13
13
  */
14
- import type { System, Incident, StatusHistoryEntry, Feedback, FeedbackResponse } from './types.js';
14
+ import type { System, Incident, StatusHistoryEntry, Feedback, FeedbackResponse, PublicStatus } from './types.js';
15
15
  export interface StatusClientConfig {
16
16
  /** Base URL of the status-feedback-admin app */
17
17
  apiUrl: string;
@@ -41,6 +41,8 @@ export interface BoardItem {
41
41
  message: string;
42
42
  email: string | null;
43
43
  status: string;
44
+ /** Coarse public bucket for display (server projection of the internal status). */
45
+ publicStatus: PublicStatus;
44
46
  votes: number;
45
47
  adminResponse: string | null;
46
48
  responseCount: number;
@@ -51,6 +53,30 @@ export interface BoardData {
51
53
  total: number;
52
54
  categories: BoardCategory[];
53
55
  }
56
+ /** A status a privileged user may move an item to, and whether it needs a reason. */
57
+ export interface BoardTransition {
58
+ /** Raw internal status value to submit. */
59
+ status: string;
60
+ /** Human label for the menu. */
61
+ display: string;
62
+ /** When true, the user must supply a reason to make this transition. */
63
+ requiresReason: boolean;
64
+ }
65
+ /** The current status of a board item plus the transitions the caller may make. */
66
+ export interface BoardTransitions {
67
+ current: {
68
+ status: string;
69
+ display: string;
70
+ publicStatus: PublicStatus;
71
+ };
72
+ targets: BoardTransition[];
73
+ }
74
+ /** Result of applying a status change from the board. */
75
+ export interface BoardStatusUpdate {
76
+ id: string;
77
+ status: string;
78
+ publicStatus: PublicStatus;
79
+ }
54
80
  export interface StatusClient {
55
81
  /** Get current system statuses */
56
82
  getStatus(): Promise<StatusData>;
@@ -80,6 +106,17 @@ export interface StatusClient {
80
106
  limit?: number;
81
107
  offset?: number;
82
108
  }): Promise<BoardData>;
109
+ /**
110
+ * Status transitions the token's user may make on a board item. Requires an
111
+ * Alliance id token; the backend returns an empty `targets` list for users
112
+ * without a qualifying role.
113
+ */
114
+ getBoardTransitions(feedbackId: string, token: string): Promise<BoardTransitions>;
115
+ /** Set a board item's status (privileged). Requires an Alliance id token. */
116
+ updateBoardStatus(feedbackId: string, data: {
117
+ status: string;
118
+ reason?: string;
119
+ }, token: string): Promise<BoardStatusUpdate>;
83
120
  /** Vote on a feedback item */
84
121
  vote(feedbackId: string, direction?: 'up' | 'down'): Promise<{
85
122
  votes: number;
package/dist/api.js CHANGED
@@ -87,6 +87,18 @@ export function createStatusClient(config) {
87
87
  const qs = params.toString();
88
88
  return request(`/api/board${qs ? `?${qs}` : ''}`);
89
89
  },
90
+ async getBoardTransitions(feedbackId, token) {
91
+ return request(`/api/board/feedback/${feedbackId}/transitions`, {
92
+ headers: { Authorization: `Bearer ${token}` },
93
+ });
94
+ },
95
+ async updateBoardStatus(feedbackId, data, token) {
96
+ return request(`/api/board/feedback/${feedbackId}/status`, {
97
+ method: 'PATCH',
98
+ headers: { Authorization: `Bearer ${token}` },
99
+ body: JSON.stringify(data),
100
+ });
101
+ },
90
102
  async vote(feedbackId, direction = 'up') {
91
103
  return request('/api/board/vote', {
92
104
  method: 'POST',
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { createStatusClient, type StatusClient } from '../api.js';
3
+ import { FEEDBACK_CATEGORIES } from '../types.js';
3
4
  import PasteImageInput from './shared/PasteImageInput.svelte';
4
5
  import {
5
6
  Card,
@@ -363,7 +364,7 @@
363
364
  await getClient().submitFeedback({
364
365
  title: formTitle.trim() || undefined,
365
366
  message: formMessage.trim(),
366
- category: formType === 'bug' ? 'bug' : 'feature',
367
+ category: formType === 'bug' ? FEEDBACK_CATEGORIES[0] : FEEDBACK_CATEGORIES[1],
367
368
  email: userEmail,
368
369
  pageUrl: window.location.href,
369
370
  metadata,