@alliance-droid/status-feedback-system 4.2.3 → 4.3.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/CHANGELOG.md +15 -1
- package/README.md +266 -264
- package/dist/api.d.ts +9 -2
- package/dist/api.js +4 -2
- package/dist/components/DevFeedback.svelte +7 -7
- package/dist/components/FeedbackButton.svelte +8 -8
- package/dist/components/FeedbackForm.svelte +8 -8
- package/dist/components/StatusBanner.svelte +21 -21
- package/dist/components/StatusPage.svelte +16 -16
- package/dist/components/board/FeedbackDetail.svelte +22 -6
- package/dist/components/board/FeedbackItem.svelte +26 -5
- package/dist/components/board/FeedbackList.svelte +2 -2
- package/dist/components/board/FeedbackSidebar.svelte +11 -5
- package/dist/components/board/StatusBadgeMenu.svelte +32 -14
- package/dist/components/board/capabilities.d.ts +4 -0
- package/dist/components/board/capabilities.js +10 -0
- package/dist/components/board/state.svelte.d.ts +3 -3
- package/dist/components/board/state.svelte.js +32 -59
- package/dist/components/board/status-state.svelte.d.ts +10 -0
- package/dist/components/board/status-state.svelte.js +41 -0
- package/dist/sveltekit/server.js +9 -4
- package/package.json +70 -70
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* <FeedbackSidebar {board} />
|
|
7
7
|
* <FeedbackList {board} />
|
|
8
8
|
*/
|
|
9
|
+
import { createBoardStatusState } from './status-state.svelte.js';
|
|
9
10
|
import { FEEDBACK_CATEGORY_ICONS } from '../../types.js';
|
|
10
11
|
import { createStatusClient, } from '../../api.js';
|
|
11
12
|
export function createFeedbackBoard(options) {
|
|
@@ -25,10 +26,8 @@ export function createFeedbackBoard(options) {
|
|
|
25
26
|
let votedItems = $state({});
|
|
26
27
|
let voteErrors = $state({});
|
|
27
28
|
let loadMoreError = $state('');
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let statusPrivilege = $state(false);
|
|
31
|
-
let privilegeProbed = false;
|
|
29
|
+
const statusState = createBoardStatusState(client, getAuthToken);
|
|
30
|
+
let requestVersion = 0;
|
|
32
31
|
// Response cache
|
|
33
32
|
const responseCache = {};
|
|
34
33
|
// Track in-flight votes to prevent duplicate requests. A plain Set is correct
|
|
@@ -36,6 +35,9 @@ export function createFeedbackBoard(options) {
|
|
|
36
35
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
37
36
|
const pendingVotes = new Set();
|
|
38
37
|
async function load() {
|
|
38
|
+
const version = ++requestVersion;
|
|
39
|
+
loadingMore = false;
|
|
40
|
+
loadMoreError = '';
|
|
39
41
|
loading = true;
|
|
40
42
|
error = '';
|
|
41
43
|
try {
|
|
@@ -45,22 +47,28 @@ export function createFeedbackBoard(options) {
|
|
|
45
47
|
sort: sortBy,
|
|
46
48
|
limit: pageSize,
|
|
47
49
|
offset: 0,
|
|
48
|
-
});
|
|
50
|
+
}, await statusState.resolveToken());
|
|
51
|
+
if (version !== requestVersion)
|
|
52
|
+
return;
|
|
49
53
|
items = data.items;
|
|
50
54
|
total = data.total;
|
|
51
55
|
categories = data.categories;
|
|
52
|
-
|
|
56
|
+
statusState.reset(data.canManageStatus ?? false);
|
|
53
57
|
}
|
|
54
58
|
catch (e) {
|
|
59
|
+
if (version !== requestVersion)
|
|
60
|
+
return;
|
|
55
61
|
error = e instanceof Error ? e.message : 'Failed to load';
|
|
56
62
|
}
|
|
57
63
|
finally {
|
|
58
|
-
|
|
64
|
+
if (version === requestVersion)
|
|
65
|
+
loading = false;
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
async function loadMore() {
|
|
62
|
-
if (loadingMore || items.length >= total)
|
|
69
|
+
if (loading || loadingMore || items.length >= total)
|
|
63
70
|
return;
|
|
71
|
+
const version = requestVersion;
|
|
64
72
|
loadingMore = true;
|
|
65
73
|
loadMoreError = '';
|
|
66
74
|
try {
|
|
@@ -70,15 +78,21 @@ export function createFeedbackBoard(options) {
|
|
|
70
78
|
sort: sortBy,
|
|
71
79
|
limit: pageSize,
|
|
72
80
|
offset: items.length,
|
|
73
|
-
});
|
|
81
|
+
}, await statusState.resolveToken());
|
|
82
|
+
if (version !== requestVersion)
|
|
83
|
+
return;
|
|
74
84
|
items = [...items, ...data.items];
|
|
75
85
|
total = data.total;
|
|
86
|
+
statusState.reset(data.canManageStatus ?? false);
|
|
76
87
|
}
|
|
77
88
|
catch (e) {
|
|
89
|
+
if (version !== requestVersion)
|
|
90
|
+
return;
|
|
78
91
|
loadMoreError = e instanceof Error ? e.message : 'Failed to load more';
|
|
79
92
|
}
|
|
80
93
|
finally {
|
|
81
|
-
|
|
94
|
+
if (version === requestVersion)
|
|
95
|
+
loadingMore = false;
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
// Auto-reload when filters change
|
|
@@ -167,51 +181,8 @@ export function createFeedbackBoard(options) {
|
|
|
167
181
|
if (item)
|
|
168
182
|
item.responseCount += 1;
|
|
169
183
|
}
|
|
170
|
-
async function resolveToken() {
|
|
171
|
-
if (!getAuthToken)
|
|
172
|
-
return null;
|
|
173
|
-
try {
|
|
174
|
-
return (await getAuthToken()) ?? null;
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async function loadTransitions(itemId) {
|
|
181
|
-
const token = await resolveToken();
|
|
182
|
-
if (!token)
|
|
183
|
-
return null;
|
|
184
|
-
try {
|
|
185
|
-
const result = await client.getBoardTransitions(itemId, token);
|
|
186
|
-
transitions = { ...transitions, [itemId]: result };
|
|
187
|
-
// The backend tells us whether this user may manage status (200 either
|
|
188
|
-
// way — no 403 console noise for ordinary signed-in users).
|
|
189
|
-
statusPrivilege = result.canManage;
|
|
190
|
-
return result;
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
// 401 (no/invalid token) or network error → no control.
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* One-time probe so the UI can gate the status control by *actual* privilege
|
|
199
|
-
* (valid token + qualifying role + project access), not merely token presence.
|
|
200
|
-
* Loads the first item's transitions once; a non-null result confirms the
|
|
201
|
-
* backend authorized the user. No token / no items / a 403 leaves it `false`.
|
|
202
|
-
*/
|
|
203
|
-
async function ensureStatusPrivilege() {
|
|
204
|
-
if (privilegeProbed || !getAuthToken)
|
|
205
|
-
return;
|
|
206
|
-
const sample = items[0];
|
|
207
|
-
if (!sample)
|
|
208
|
-
return;
|
|
209
|
-
privilegeProbed = true;
|
|
210
|
-
// loadTransitions sets statusPrivilege from the result's canManage flag.
|
|
211
|
-
await loadTransitions(sample.id);
|
|
212
|
-
}
|
|
213
184
|
async function setStatus(itemId, status, reason) {
|
|
214
|
-
const token = await resolveToken();
|
|
185
|
+
const token = await statusState.resolveToken();
|
|
215
186
|
if (!token)
|
|
216
187
|
throw new Error('Authentication required to set status');
|
|
217
188
|
const result = await client.updateBoardStatus(itemId, { status, reason }, token);
|
|
@@ -219,9 +190,11 @@ export function createFeedbackBoard(options) {
|
|
|
219
190
|
if (item) {
|
|
220
191
|
item.status = result.status;
|
|
221
192
|
item.publicStatus = result.publicStatus;
|
|
193
|
+
item.canChangeStatus = result.canChangeStatus;
|
|
194
|
+
item.needsInput = result.needsInput;
|
|
222
195
|
}
|
|
223
|
-
//
|
|
224
|
-
await
|
|
196
|
+
// Re-query to keep the current filter, counts, and pagination accurate.
|
|
197
|
+
await load();
|
|
225
198
|
}
|
|
226
199
|
return {
|
|
227
200
|
get client() { return client; },
|
|
@@ -249,9 +222,9 @@ export function createFeedbackBoard(options) {
|
|
|
249
222
|
getResponses,
|
|
250
223
|
submitFeedback,
|
|
251
224
|
submitResponse,
|
|
252
|
-
get canManageStatus() { return
|
|
253
|
-
get transitions() { return transitions; },
|
|
254
|
-
loadTransitions,
|
|
225
|
+
get canManageStatus() { return statusState.canManage; },
|
|
226
|
+
get transitions() { return statusState.transitions; },
|
|
227
|
+
loadTransitions: statusState.loadTransitions,
|
|
255
228
|
setStatus,
|
|
256
229
|
getCategoryIcon(name) {
|
|
257
230
|
return (categoryConfig[name]?.icon ||
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Server-authorized status capabilities and transition cache for a single board. */
|
|
2
|
+
import type { BoardTransitions, StatusClient } from '../../api.js';
|
|
3
|
+
import type { FeedbackBoardOptions } from './state.svelte.js';
|
|
4
|
+
export declare function createBoardStatusState(client: StatusClient, getAuthToken: FeedbackBoardOptions['getAuthToken']): {
|
|
5
|
+
readonly canManage: boolean;
|
|
6
|
+
readonly transitions: Record<string, BoardTransitions>;
|
|
7
|
+
resolveToken: () => Promise<string | null>;
|
|
8
|
+
loadTransitions: (itemId: string) => Promise<BoardTransitions | null>;
|
|
9
|
+
reset(allowed: boolean): void;
|
|
10
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function createBoardStatusState(client, getAuthToken) {
|
|
2
|
+
let canManage = $state(false);
|
|
3
|
+
let transitions = $state({});
|
|
4
|
+
let generation = 0;
|
|
5
|
+
async function resolveToken() {
|
|
6
|
+
try {
|
|
7
|
+
return (await getAuthToken?.()) ?? null;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function loadTransitions(itemId) {
|
|
14
|
+
const currentGeneration = generation;
|
|
15
|
+
const token = await resolveToken();
|
|
16
|
+
if (!token)
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const result = await client.getBoardTransitions(itemId, token);
|
|
20
|
+
if (currentGeneration !== generation)
|
|
21
|
+
return null;
|
|
22
|
+
transitions = { ...transitions, [itemId]: result };
|
|
23
|
+
canManage = result.canManage;
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
get canManage() { return canManage; },
|
|
32
|
+
get transitions() { return transitions; },
|
|
33
|
+
resolveToken,
|
|
34
|
+
loadTransitions,
|
|
35
|
+
reset(allowed) {
|
|
36
|
+
generation += 1;
|
|
37
|
+
canManage = allowed;
|
|
38
|
+
transitions = {};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/sveltekit/server.js
CHANGED
|
@@ -64,11 +64,15 @@ async function handleProxyRequest(event, options) {
|
|
|
64
64
|
if (route.forwardBearerToken) {
|
|
65
65
|
const authorization = event.request.headers.get('authorization');
|
|
66
66
|
if (!authorization?.toLowerCase().startsWith('bearer ')) {
|
|
67
|
-
if (route.
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
if (route.forwardBearerToken !== 'optional') {
|
|
68
|
+
if (route.operation === 'transitions')
|
|
69
|
+
return statusManagementUnavailable();
|
|
70
|
+
return feedbackError(401, 'Status feedback access token required');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
headers.set('Authorization', authorization);
|
|
70
75
|
}
|
|
71
|
-
headers.set('Authorization', authorization);
|
|
72
76
|
}
|
|
73
77
|
const method = event.request.method.toUpperCase();
|
|
74
78
|
let body;
|
|
@@ -137,6 +141,7 @@ function getRouteSpec(method, path) {
|
|
|
137
141
|
upstreamPath: '/api/board',
|
|
138
142
|
bodyKind: 'none',
|
|
139
143
|
allowedSearchParams: BOARD_QUERY_KEYS,
|
|
144
|
+
forwardBearerToken: 'optional',
|
|
140
145
|
};
|
|
141
146
|
}
|
|
142
147
|
if (normalizedMethod === 'POST' && normalizedPath === 'api/board/vote') {
|
package/package.json
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@alliance-droid/status-feedback-system",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
|
|
6
|
-
"svelte": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"svelte": "./dist/index.js"
|
|
12
|
-
},
|
|
13
|
-
"./sveltekit/server": {
|
|
14
|
-
"types": "./dist/sveltekit/server.d.ts",
|
|
15
|
-
"default": "./dist/sveltekit/server.js"
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
"files": [
|
|
19
|
-
"dist",
|
|
20
|
-
"!dist/**/*.test.*",
|
|
21
|
-
"CHANGELOG.md"
|
|
22
|
-
],
|
|
23
|
-
"scripts": {
|
|
24
|
-
"build": "svelte-package -o dist && publint",
|
|
25
|
-
"lint": "eslint src",
|
|
26
|
-
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
27
|
-
"test": "vitest run",
|
|
28
|
-
"test:watch": "vitest",
|
|
29
|
-
"prepublishOnly": "npm run build"
|
|
30
|
-
},
|
|
31
|
-
"peerDependencies": {
|
|
32
|
-
"@alliance-droid/svelte-component-library": ">=1.3.2",
|
|
33
|
-
"svelte": "^5.
|
|
34
|
-
},
|
|
35
|
-
"devDependencies": {
|
|
36
|
-
"@alliance-droid/svelte-component-library": "^1.
|
|
37
|
-
"@eslint/js": "^10.0.1",
|
|
38
|
-
"@sveltejs/package": "^2.
|
|
39
|
-
"@sveltejs/vite-plugin-svelte": "^6.
|
|
40
|
-
"@testing-library/jest-dom": "^6.9.1",
|
|
41
|
-
"@testing-library/svelte": "^5.
|
|
42
|
-
"eslint": "^10.
|
|
43
|
-
"eslint-plugin-svelte": "^3.
|
|
44
|
-
"globals": "^17.
|
|
45
|
-
"jsdom": "^29.
|
|
46
|
-
"publint": "^0.3.
|
|
47
|
-
"svelte": "^5.
|
|
48
|
-
"svelte-check": "^4.
|
|
49
|
-
"typescript": "^5.9.
|
|
50
|
-
"typescript-eslint": "^8.
|
|
51
|
-
"vite": "^7.3.
|
|
52
|
-
"vitest": "^4.
|
|
53
|
-
},
|
|
54
|
-
"keywords": [
|
|
55
|
-
"svelte",
|
|
56
|
-
"sveltekit",
|
|
57
|
-
"status-page",
|
|
58
|
-
"feedback",
|
|
59
|
-
"uptime"
|
|
60
|
-
],
|
|
61
|
-
"author": "Alliance Technical Group",
|
|
62
|
-
"license": "UNLICENSED",
|
|
63
|
-
"publishConfig": {
|
|
64
|
-
"access": "restricted"
|
|
65
|
-
},
|
|
66
|
-
"repository": {
|
|
67
|
-
"type": "git",
|
|
68
|
-
"url": "git+https://alliance.ghe.com/alliance/status-feedback-system.git"
|
|
69
|
-
}
|
|
70
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@alliance-droid/status-feedback-system",
|
|
3
|
+
"version": "4.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
|
|
6
|
+
"svelte": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"svelte": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./sveltekit/server": {
|
|
14
|
+
"types": "./dist/sveltekit/server.d.ts",
|
|
15
|
+
"default": "./dist/sveltekit/server.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"!dist/**/*.test.*",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "svelte-package -o dist && publint",
|
|
25
|
+
"lint": "eslint src",
|
|
26
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@alliance-droid/svelte-component-library": ">=1.3.2",
|
|
33
|
+
"svelte": "^5.55.7"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@alliance-droid/svelte-component-library": "^1.20.2",
|
|
37
|
+
"@eslint/js": "^10.0.1",
|
|
38
|
+
"@sveltejs/package": "^2.5.8",
|
|
39
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
40
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
41
|
+
"@testing-library/svelte": "^5.4.2",
|
|
42
|
+
"eslint": "^10.10.0",
|
|
43
|
+
"eslint-plugin-svelte": "^3.23.0",
|
|
44
|
+
"globals": "^17.12.0",
|
|
45
|
+
"jsdom": "^29.1.1",
|
|
46
|
+
"publint": "^0.3.24",
|
|
47
|
+
"svelte": "^5.57.0",
|
|
48
|
+
"svelte-check": "^4.7.6",
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"typescript-eslint": "^8.70.0",
|
|
51
|
+
"vite": "^7.3.6",
|
|
52
|
+
"vitest": "^4.1.11"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"svelte",
|
|
56
|
+
"sveltekit",
|
|
57
|
+
"status-page",
|
|
58
|
+
"feedback",
|
|
59
|
+
"uptime"
|
|
60
|
+
],
|
|
61
|
+
"author": "Alliance Technical Group",
|
|
62
|
+
"license": "UNLICENSED",
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "restricted"
|
|
65
|
+
},
|
|
66
|
+
"repository": {
|
|
67
|
+
"type": "git",
|
|
68
|
+
"url": "git+https://alliance.ghe.com/alliance/status-feedback-system.git"
|
|
69
|
+
}
|
|
70
|
+
}
|