@alliance-droid/status-feedback-system 3.1.0 → 4.0.1

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.
@@ -0,0 +1,11 @@
1
+ import type { BoardItem } from '../../api.js';
2
+ import type { FeedbackBoardState } from './state.svelte.js';
3
+ interface Props {
4
+ board: FeedbackBoardState;
5
+ item: BoardItem;
6
+ /** Eagerly load transitions on mount (detail view). The list leaves this off. */
7
+ loadOnMount?: boolean;
8
+ }
9
+ declare const StatusBadgeMenu: import("svelte").Component<Props, {}, "">;
10
+ type StatusBadgeMenu = ReturnType<typeof StatusBadgeMenu>;
11
+ export default StatusBadgeMenu;
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Reactive state manager for FeedbackBoard composable components.
2
+ * Reactive state manager for the composable feedback board components.
3
3
  *
4
4
  * Usage:
5
5
  * const board = createFeedbackBoard({ apiUrl, apiKey });
6
6
  * <FeedbackSidebar {board} />
7
7
  * <FeedbackList {board} />
8
8
  */
9
- import type { FeedbackResponse } from '../../types.js';
10
- import { type BoardItem, type BoardCategory, type StatusClient } from '../../api.js';
9
+ import { type FeedbackResponse } from '../../types.js';
10
+ import { type BoardItem, type BoardCategory, type BoardTransitions, type StatusClient } from '../../api.js';
11
11
  export interface CategoryConfig {
12
12
  label?: string;
13
13
  icon?: string;
@@ -19,6 +19,14 @@ export interface FeedbackBoardOptions {
19
19
  categoryConfig?: Record<string, CategoryConfig>;
20
20
  /** Authenticated user email. Required for submitting feedback and replies. */
21
21
  userEmail?: string;
22
+ /**
23
+ * Returns the current user's Alliance OIDC **access token** — whose audience
24
+ * covers `status-feedback-admin` (an id token is rejected by the backend) — or
25
+ * null if they aren't signed in / not privileged. When provided, privileged
26
+ * users (Admin / ProductOwner) can set feedback status from the board. Called
27
+ * on demand so the host controls token refresh.
28
+ */
29
+ getAuthToken?: () => string | Promise<string | null>;
22
30
  /** Items per page. Default: 50 */
23
31
  pageSize?: number;
24
32
  }
@@ -32,6 +40,8 @@ export interface FeedbackBoardState {
32
40
  readonly loading: boolean;
33
41
  readonly loadingMore: boolean;
34
42
  readonly error: string;
43
+ /** Error from the most recent `loadMore()`, or '' — cleared on the next attempt. */
44
+ readonly loadMoreError: string;
35
45
  readonly hasMore: boolean;
36
46
  activeCategory: string | null;
37
47
  statusFilter: string;
@@ -49,6 +59,17 @@ export interface FeedbackBoardState {
49
59
  pastedImage?: string | null;
50
60
  }): Promise<void>;
51
61
  submitResponse(itemId: string, message: string, pastedImage?: string | null): Promise<void>;
62
+ /** Whether a token provider was configured (status controls may be available). */
63
+ readonly canManageStatus: boolean;
64
+ /** Cached allowed transitions per item id, populated by loadTransitions. */
65
+ readonly transitions: Record<string, BoardTransitions>;
66
+ /**
67
+ * Fetch the status transitions the current user may make on an item. Returns
68
+ * null when there's no token or the user isn't privileged for this item.
69
+ */
70
+ loadTransitions(itemId: string): Promise<BoardTransitions | null>;
71
+ /** Apply a status change to an item (privileged). Throws on failure. */
72
+ setStatus(itemId: string, status: string, reason?: string): Promise<void>;
52
73
  getCategoryIcon(name: string): string;
53
74
  getCategoryLabel(name: string): string;
54
75
  getCategoryDescription(name: string): string;
@@ -1,14 +1,15 @@
1
1
  /**
2
- * Reactive state manager for FeedbackBoard composable components.
2
+ * Reactive state manager for the composable feedback board components.
3
3
  *
4
4
  * Usage:
5
5
  * const board = createFeedbackBoard({ apiUrl, apiKey });
6
6
  * <FeedbackSidebar {board} />
7
7
  * <FeedbackList {board} />
8
8
  */
9
- import { createStatusClient } from '../../api.js';
9
+ import { FEEDBACK_CATEGORY_ICONS } from '../../types.js';
10
+ import { createStatusClient, } from '../../api.js';
10
11
  export function createFeedbackBoard(options) {
11
- const { apiUrl, apiKey, categoryConfig = {}, userEmail, pageSize = 50 } = options;
12
+ const { apiUrl, apiKey, categoryConfig = {}, userEmail, getAuthToken, pageSize = 50 } = options;
12
13
  const client = createStatusClient({ apiUrl, apiKey });
13
14
  const resolvedEmail = userEmail?.trim() || null;
14
15
  // Reactive state
@@ -23,9 +24,13 @@ export function createFeedbackBoard(options) {
23
24
  let sortBy = $state('votes');
24
25
  let votedItems = $state({});
25
26
  let voteErrors = $state({});
27
+ let loadMoreError = $state('');
28
+ let transitions = $state({});
26
29
  // Response cache
27
30
  const responseCache = {};
28
- // Track in-flight votes to prevent duplicate requests
31
+ // Track in-flight votes to prevent duplicate requests. A plain Set is correct
32
+ // here — an internal dedup guard, not reactive UI state.
33
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
29
34
  const pendingVotes = new Set();
30
35
  async function load() {
31
36
  loading = true;
@@ -53,6 +58,7 @@ export function createFeedbackBoard(options) {
53
58
  if (loadingMore || items.length >= total)
54
59
  return;
55
60
  loadingMore = true;
61
+ loadMoreError = '';
56
62
  try {
57
63
  const data = await client.getBoard({
58
64
  category: activeCategory || undefined,
@@ -64,8 +70,8 @@ export function createFeedbackBoard(options) {
64
70
  items = [...items, ...data.items];
65
71
  total = data.total;
66
72
  }
67
- catch {
68
- // Silently fail — user can retry
73
+ catch (e) {
74
+ loadMoreError = e instanceof Error ? e.message : 'Failed to load more';
69
75
  }
70
76
  finally {
71
77
  loadingMore = false;
@@ -85,7 +91,8 @@ export function createFeedbackBoard(options) {
85
91
  const prev = votedItems[itemId];
86
92
  // Clear any prior error for this item
87
93
  if (voteErrors[itemId]) {
88
- const { [itemId]: _, ...rest } = voteErrors;
94
+ const rest = { ...voteErrors };
95
+ delete rest[itemId];
89
96
  voteErrors = rest;
90
97
  }
91
98
  try {
@@ -105,8 +112,9 @@ export function createFeedbackBoard(options) {
105
112
  voteErrors = { ...voteErrors, [itemId]: 'Vote failed' };
106
113
  // Auto-dismiss after 3 seconds
107
114
  setTimeout(() => {
108
- const { [itemId]: _, ...rest } = voteErrors;
109
- voteErrors = rest;
115
+ const next = { ...voteErrors };
116
+ delete next[itemId];
117
+ voteErrors = next;
110
118
  }, 3000);
111
119
  }
112
120
  finally {
@@ -155,6 +163,43 @@ export function createFeedbackBoard(options) {
155
163
  if (item)
156
164
  item.responseCount += 1;
157
165
  }
166
+ async function resolveToken() {
167
+ if (!getAuthToken)
168
+ return null;
169
+ try {
170
+ return (await getAuthToken()) ?? null;
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+ async function loadTransitions(itemId) {
177
+ const token = await resolveToken();
178
+ if (!token)
179
+ return null;
180
+ try {
181
+ const result = await client.getBoardTransitions(itemId, token);
182
+ transitions = { ...transitions, [itemId]: result };
183
+ return result;
184
+ }
185
+ catch {
186
+ // 401/403 → not signed in or not privileged for this item: no control.
187
+ return null;
188
+ }
189
+ }
190
+ async function setStatus(itemId, status, reason) {
191
+ const token = await resolveToken();
192
+ if (!token)
193
+ throw new Error('Authentication required to set status');
194
+ const result = await client.updateBoardStatus(itemId, { status, reason }, token);
195
+ const item = items.find((i) => i.id === itemId);
196
+ if (item) {
197
+ item.status = result.status;
198
+ item.publicStatus = result.publicStatus;
199
+ }
200
+ // Allowed targets depend on the new status — refresh the cache.
201
+ await loadTransitions(itemId);
202
+ }
158
203
  return {
159
204
  get client() { return client; },
160
205
  get categoryConfig() { return categoryConfig; },
@@ -165,6 +210,7 @@ export function createFeedbackBoard(options) {
165
210
  get loading() { return loading; },
166
211
  get loadingMore() { return loadingMore; },
167
212
  get error() { return error; },
213
+ get loadMoreError() { return loadMoreError; },
168
214
  get hasMore() { return items.length < total; },
169
215
  get activeCategory() { return activeCategory; },
170
216
  set activeCategory(v) { activeCategory = v; },
@@ -180,8 +226,14 @@ export function createFeedbackBoard(options) {
180
226
  getResponses,
181
227
  submitFeedback,
182
228
  submitResponse,
229
+ get canManageStatus() { return !!getAuthToken; },
230
+ get transitions() { return transitions; },
231
+ loadTransitions,
232
+ setStatus,
183
233
  getCategoryIcon(name) {
184
- return categoryConfig[name]?.icon || 'fa-solid fa-circle';
234
+ return (categoryConfig[name]?.icon ||
235
+ FEEDBACK_CATEGORY_ICONS[name.toLowerCase()] ||
236
+ 'fa-solid fa-circle');
185
237
  },
186
238
  getCategoryLabel(name) {
187
239
  return categoryConfig[name]?.label || name;
package/dist/index.d.ts CHANGED
@@ -15,7 +15,6 @@ export { default as StatusPage } from './components/StatusPage.svelte';
15
15
  export { default as StatusBanner } from './components/StatusBanner.svelte';
16
16
  export { default as FeedbackForm } from './components/FeedbackForm.svelte';
17
17
  export { default as DevFeedback } from './components/DevFeedback.svelte';
18
- export { default as FeedbackBoard } from './components/FeedbackBoard.svelte';
19
18
  export { default as FeedbackButton } from './components/FeedbackButton.svelte';
20
19
  export { createFeedbackBoard, FeedbackSidebar, FeedbackList, FeedbackItem, FeedbackDetail, FeedbackSubmitForm, } from './components/board/index.js';
21
20
  export type { FeedbackBoardState, FeedbackBoardOptions, CategoryConfig } from './components/board/index.js';
package/dist/index.js CHANGED
@@ -16,7 +16,6 @@ export { default as StatusPage } from './components/StatusPage.svelte';
16
16
  export { default as StatusBanner } from './components/StatusBanner.svelte';
17
17
  export { default as FeedbackForm } from './components/FeedbackForm.svelte';
18
18
  export { default as DevFeedback } from './components/DevFeedback.svelte';
19
- export { default as FeedbackBoard } from './components/FeedbackBoard.svelte';
20
19
  export { default as FeedbackButton } from './components/FeedbackButton.svelte';
21
20
  // ── Composable Board Components ──
22
21
  export { createFeedbackBoard, FeedbackSidebar, FeedbackList, FeedbackItem, FeedbackDetail, FeedbackSubmitForm, } from './components/board/index.js';
package/dist/types.d.ts CHANGED
@@ -13,6 +13,18 @@ export type IncidentSeverity = 'minor' | 'major' | 'critical';
13
13
  export type FeedbackStatus = string;
14
14
  /** Coarse, consumer-facing status buckets the board API returns as `publicStatus`. */
15
15
  export type PublicStatus = 'open' | 'planned' | 'in_progress' | 'done' | 'declined';
16
+ /**
17
+ * The categories a user can file feedback under. The feedback system owns this
18
+ * list — consumers don't supply their own.
19
+ */
20
+ export declare const FEEDBACK_CATEGORIES: readonly ["Bug", "Feature Request"];
21
+ export type FeedbackCategory = (typeof FEEDBACK_CATEGORIES)[number];
22
+ /**
23
+ * Default Font Awesome icon per category, keyed by the lower-cased category
24
+ * name so it matches regardless of how the value was stored (e.g. "Bug" vs
25
+ * "bug"). A consumer's `categoryConfig` icon takes precedence over these.
26
+ */
27
+ export declare const FEEDBACK_CATEGORY_ICONS: Record<string, string>;
16
28
  export interface Project {
17
29
  id: string;
18
30
  name: string;
package/dist/types.js CHANGED
@@ -1,4 +1,17 @@
1
1
  /**
2
2
  * Shared types for the status-feedback-system package
3
3
  */
4
- export {};
4
+ /**
5
+ * The categories a user can file feedback under. The feedback system owns this
6
+ * list — consumers don't supply their own.
7
+ */
8
+ export const FEEDBACK_CATEGORIES = ['Bug', 'Feature Request'];
9
+ /**
10
+ * Default Font Awesome icon per category, keyed by the lower-cased category
11
+ * name so it matches regardless of how the value was stored (e.g. "Bug" vs
12
+ * "bug"). A consumer's `categoryConfig` icon takes precedence over these.
13
+ */
14
+ export const FEEDBACK_CATEGORY_ICONS = {
15
+ bug: 'fa-solid fa-bug',
16
+ 'feature request': 'fa-solid fa-lightbulb',
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alliance-droid/status-feedback-system",
3
- "version": "3.1.0",
3
+ "version": "4.0.1",
4
4
  "type": "module",
5
5
  "description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
6
6
  "svelte": "./dist/index.js",
@@ -1,110 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * All-in-one feedback board component.
4
- * Composes the board sub-components (FeedbackSidebar, FeedbackList,
5
- * FeedbackSubmitForm) with a shared FeedbackBoardState.
6
- */
7
- import { untrack } from 'svelte';
8
- import { Button } from '@alliance-droid/svelte-component-library';
9
- import type { BoardItem } from '../api.js';
10
- import { createFeedbackBoard, type CategoryConfig, type FeedbackBoardState } from './board/index.js';
11
- import FeedbackSidebar from './board/FeedbackSidebar.svelte';
12
- import FeedbackList from './board/FeedbackList.svelte';
13
- import FeedbackDetail from './board/FeedbackDetail.svelte';
14
- import FeedbackSubmitForm from './board/FeedbackSubmitForm.svelte';
15
-
16
- interface Props {
17
- apiUrl: string;
18
- apiKey: string;
19
- /** Authenticated user email. Required for submitting feedback and replies. */
20
- userEmail?: string;
21
- /** Category labels and icons — order determines sidebar order */
22
- categoryConfig?: Record<string, CategoryConfig>;
23
- /** Title shown at the top */
24
- title?: string;
25
- /** Show the submit form inline */
26
- showSubmit?: boolean;
27
- /** Available categories for the submit form */
28
- submitCategories?: string[];
29
- }
30
-
31
- let {
32
- apiUrl,
33
- apiKey,
34
- userEmail,
35
- categoryConfig = {},
36
- title = 'Feedback Board',
37
- showSubmit = true,
38
- submitCategories = [],
39
- }: Props = $props();
40
-
41
- let board: FeedbackBoardState = $derived(
42
- createFeedbackBoard({ apiUrl, apiKey, userEmail, categoryConfig })
43
- );
44
-
45
- let showForm = $state(false);
46
- let selectedItem = $state<BoardItem | null>(null);
47
-
48
- function selectItem(item: BoardItem) {
49
- selectedItem = item;
50
- }
51
-
52
- function clearSelection() {
53
- selectedItem = null;
54
- }
55
- </script>
56
-
57
- <div class="flex gap-6 min-h-[400px]">
58
- <!-- Sidebar -->
59
- <aside class="w-56 flex-shrink-0">
60
- <FeedbackSidebar {board} />
61
- </aside>
62
-
63
- <!-- Main -->
64
- <main class="flex-1 min-w-0">
65
- <div class="flex items-start justify-between mb-4">
66
- <div>
67
- {#if board.activeCategory}
68
- <h2 class="text-xl font-bold flex items-center gap-2">
69
- <i class={board.getCategoryIcon(board.activeCategory)}></i>
70
- {board.getCategoryLabel(board.activeCategory)}
71
- </h2>
72
- {#if board.getCategoryDescription(board.activeCategory)}
73
- <p class="text-sm text-foreground-secondary mt-1">{board.getCategoryDescription(board.activeCategory)}</p>
74
- {/if}
75
- {:else if title}
76
- <h2 class="text-xl font-bold">{title}</h2>
77
- {/if}
78
- </div>
79
- {#if showSubmit}
80
- <Button variant="primary" onclick={() => (showForm = !showForm)}>
81
- <i class="fa-solid fa-plus mr-1.5"></i> Submit
82
- </Button>
83
- {/if}
84
- </div>
85
-
86
- <!-- Submit form -->
87
- {#if showForm}
88
- <div class="mb-4">
89
- <FeedbackSubmitForm {board} categories={submitCategories} />
90
- </div>
91
- {/if}
92
-
93
- <!-- Items / Detail -->
94
- {#if selectedItem}
95
- <div class="mb-4">
96
- <button
97
- onclick={clearSelection}
98
- class="text-sm text-foreground-secondary hover:text-foreground transition-colors mb-3"
99
- >
100
- <i class="fa-solid fa-arrow-left mr-1" aria-hidden="true"></i> Back to list
101
- </button>
102
- <div class="rounded-xl border border-border bg-surface p-5">
103
- <FeedbackDetail {board} item={selectedItem} />
104
- </div>
105
- </div>
106
- {:else}
107
- <FeedbackList {board} onSelect={selectItem} />
108
- {/if}
109
- </main>
110
- </div>
@@ -1,18 +0,0 @@
1
- import { type CategoryConfig } from './board/index.js';
2
- interface Props {
3
- apiUrl: string;
4
- apiKey: string;
5
- /** Authenticated user email. Required for submitting feedback and replies. */
6
- userEmail?: string;
7
- /** Category labels and icons — order determines sidebar order */
8
- categoryConfig?: Record<string, CategoryConfig>;
9
- /** Title shown at the top */
10
- title?: string;
11
- /** Show the submit form inline */
12
- showSubmit?: boolean;
13
- /** Available categories for the submit form */
14
- submitCategories?: string[];
15
- }
16
- declare const FeedbackBoard: import("svelte").Component<Props, {}, "">;
17
- type FeedbackBoard = ReturnType<typeof FeedbackBoard>;
18
- export default FeedbackBoard;