@alliance-droid/status-feedback-system 1.3.7 → 1.3.9
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/dist/api.d.ts +6 -0
- package/dist/api.js +6 -0
- package/dist/components/board/FeedbackDetail.svelte +92 -6
- package/dist/components/board/FeedbackItem.svelte +19 -14
- package/dist/components/board/FeedbackSubmitForm.svelte +4 -2
- package/dist/components/board/FeedbackSubmitForm.svelte.d.ts +2 -0
- package/dist/components/board/state.svelte.d.ts +6 -5
- package/dist/components/board/state.svelte.js +10 -0
- package/dist/components/dev/DevContextMenu.svelte +2 -3
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface BoardItem {
|
|
|
39
39
|
title: string | null;
|
|
40
40
|
category: string | null;
|
|
41
41
|
message: string;
|
|
42
|
+
email: string | null;
|
|
42
43
|
status: string;
|
|
43
44
|
votes: number;
|
|
44
45
|
adminResponse: string | null;
|
|
@@ -87,6 +88,11 @@ export interface StatusClient {
|
|
|
87
88
|
getResponses(feedbackId: string): Promise<{
|
|
88
89
|
responses: FeedbackResponse[];
|
|
89
90
|
}>;
|
|
91
|
+
/** Submit a public response to a feedback item */
|
|
92
|
+
submitResponse(feedbackId: string, data: {
|
|
93
|
+
message: string;
|
|
94
|
+
author?: string;
|
|
95
|
+
}): Promise<FeedbackResponse>;
|
|
90
96
|
}
|
|
91
97
|
/**
|
|
92
98
|
* Create a status API client instance
|
package/dist/api.js
CHANGED
|
@@ -96,5 +96,11 @@ export function createStatusClient(config) {
|
|
|
96
96
|
async getResponses(feedbackId) {
|
|
97
97
|
return request(`/api/board/${feedbackId}/responses`);
|
|
98
98
|
},
|
|
99
|
+
async submitResponse(feedbackId, data) {
|
|
100
|
+
return request(`/api/board/${feedbackId}/responses`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: JSON.stringify(data),
|
|
103
|
+
});
|
|
104
|
+
},
|
|
99
105
|
};
|
|
100
106
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
|
-
import { Badge, Spinner } from '@alliance-droid/svelte-component-library';
|
|
3
|
+
import { Badge, Spinner, Button, Textarea, Input } from '@alliance-droid/svelte-component-library';
|
|
4
4
|
import type { BoardItem } from '../../api.js';
|
|
5
|
+
import type { FeedbackResponse } from '../../types.js';
|
|
5
6
|
import type { FeedbackBoardState } from './state.svelte.js';
|
|
6
7
|
import { timeAgo, getStatusBadgeVariant } from './utils.js';
|
|
7
8
|
|
|
@@ -12,13 +13,42 @@
|
|
|
12
13
|
|
|
13
14
|
let { board, item }: Props = $props();
|
|
14
15
|
|
|
15
|
-
let responses = $state<
|
|
16
|
+
let responses = $state<FeedbackResponse[]>([]);
|
|
16
17
|
let loadingResponses = $state(true);
|
|
17
18
|
|
|
19
|
+
// Response form state
|
|
20
|
+
let replyMessage = $state('');
|
|
21
|
+
let replyAuthor = $state('');
|
|
22
|
+
let submittingReply = $state(false);
|
|
23
|
+
let replyError = $state('');
|
|
24
|
+
let replySuccess = $state(false);
|
|
25
|
+
|
|
18
26
|
onMount(async () => {
|
|
19
27
|
responses = await board.getResponses(item.id);
|
|
20
28
|
loadingResponses = false;
|
|
21
29
|
});
|
|
30
|
+
|
|
31
|
+
async function handleSubmitReply() {
|
|
32
|
+
if (!replyMessage.trim()) return;
|
|
33
|
+
submittingReply = true;
|
|
34
|
+
replyError = '';
|
|
35
|
+
try {
|
|
36
|
+
await board.submitResponse(item.id, {
|
|
37
|
+
message: replyMessage.trim(),
|
|
38
|
+
author: replyAuthor.trim() || undefined,
|
|
39
|
+
});
|
|
40
|
+
// Refresh responses
|
|
41
|
+
responses = await board.getResponses(item.id);
|
|
42
|
+
replyMessage = '';
|
|
43
|
+
replyAuthor = '';
|
|
44
|
+
replySuccess = true;
|
|
45
|
+
setTimeout(() => { replySuccess = false; }, 2000);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
replyError = err instanceof Error ? err.message : 'Failed to submit';
|
|
48
|
+
} finally {
|
|
49
|
+
submittingReply = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
22
52
|
</script>
|
|
23
53
|
|
|
24
54
|
<div class="space-y-4">
|
|
@@ -38,6 +68,14 @@
|
|
|
38
68
|
{/if}
|
|
39
69
|
</div>
|
|
40
70
|
|
|
71
|
+
<!-- Submitter email -->
|
|
72
|
+
{#if item.email}
|
|
73
|
+
<p class="text-sm text-foreground-tertiary flex items-center gap-1">
|
|
74
|
+
<i class="fa-solid fa-envelope" aria-hidden="true"></i>
|
|
75
|
+
{item.email}
|
|
76
|
+
</p>
|
|
77
|
+
{/if}
|
|
78
|
+
|
|
41
79
|
<!-- Title + Message -->
|
|
42
80
|
{#if item.title}
|
|
43
81
|
<h3 class="text-lg font-semibold text-foreground">{item.title}</h3>
|
|
@@ -71,12 +109,18 @@
|
|
|
71
109
|
<h4 class="text-sm font-semibold text-foreground">
|
|
72
110
|
<i class="fa-solid fa-comments mr-1" aria-hidden="true"></i> Responses
|
|
73
111
|
</h4>
|
|
74
|
-
{#each responses as resp}
|
|
112
|
+
{#each responses as resp (resp.id)}
|
|
75
113
|
<div class="rounded-lg bg-surface-secondary p-3">
|
|
76
114
|
<div class="flex items-center justify-between mb-1">
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
115
|
+
{#if !resp.author}
|
|
116
|
+
<span class="text-xs font-semibold text-primary">
|
|
117
|
+
<i class="fa-solid fa-shield mr-1" aria-hidden="true"></i>Team
|
|
118
|
+
</span>
|
|
119
|
+
{:else}
|
|
120
|
+
<span class="text-xs font-semibold text-foreground-secondary">
|
|
121
|
+
<i class="fa-solid fa-user mr-1" aria-hidden="true"></i>{resp.author}
|
|
122
|
+
</span>
|
|
123
|
+
{/if}
|
|
80
124
|
<span class="text-[0.6875rem] text-foreground-tertiary">{timeAgo(resp.createdAt)}</span>
|
|
81
125
|
</div>
|
|
82
126
|
<p class="text-sm text-foreground-secondary leading-relaxed whitespace-pre-wrap">{resp.message}</p>
|
|
@@ -84,4 +128,46 @@
|
|
|
84
128
|
{/each}
|
|
85
129
|
</div>
|
|
86
130
|
{/if}
|
|
131
|
+
|
|
132
|
+
<!-- Reply form -->
|
|
133
|
+
<div class="border-t border-border pt-4 space-y-2">
|
|
134
|
+
<h4 class="text-sm font-semibold text-foreground">
|
|
135
|
+
<i class="fa-solid fa-reply mr-1" aria-hidden="true"></i> Add a Response
|
|
136
|
+
</h4>
|
|
137
|
+
{#if replySuccess}
|
|
138
|
+
<p class="text-sm text-green-400 font-semibold">
|
|
139
|
+
<i class="fa-solid fa-check-circle mr-1" aria-hidden="true"></i> Response submitted!
|
|
140
|
+
</p>
|
|
141
|
+
{:else}
|
|
142
|
+
<Textarea
|
|
143
|
+
value={replyMessage}
|
|
144
|
+
oninput={(e) => (replyMessage = (e.currentTarget as HTMLTextAreaElement).value)}
|
|
145
|
+
placeholder="Share your thoughts..."
|
|
146
|
+
rows={3}
|
|
147
|
+
/>
|
|
148
|
+
<div class="flex gap-2 items-center">
|
|
149
|
+
<div class="flex-1">
|
|
150
|
+
<Input
|
|
151
|
+
bind:value={replyAuthor}
|
|
152
|
+
placeholder="Your name (optional)"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
<Button
|
|
156
|
+
variant="primary"
|
|
157
|
+
size="sm"
|
|
158
|
+
disabled={submittingReply || !replyMessage.trim()}
|
|
159
|
+
loading={submittingReply}
|
|
160
|
+
onclick={handleSubmitReply}
|
|
161
|
+
>
|
|
162
|
+
Submit
|
|
163
|
+
</Button>
|
|
164
|
+
</div>
|
|
165
|
+
{#if replyError}
|
|
166
|
+
<p class="text-sm text-red-400">
|
|
167
|
+
<i class="fa-solid fa-exclamation-circle mr-1" aria-hidden="true"></i>
|
|
168
|
+
{replyError}
|
|
169
|
+
</p>
|
|
170
|
+
{/if}
|
|
171
|
+
{/if}
|
|
172
|
+
</div>
|
|
87
173
|
</div>
|
|
@@ -33,9 +33,7 @@
|
|
|
33
33
|
<div class="flex gap-3 p-4">
|
|
34
34
|
<!-- Vote column -->
|
|
35
35
|
<div class="flex flex-col items-center gap-0.5 min-w-[2.5rem]" role="group" aria-label="Vote on this feedback">
|
|
36
|
-
|
|
37
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
38
|
-
<div onclick={(e) => e.stopPropagation()}>
|
|
36
|
+
<div role="presentation" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
|
|
39
37
|
<IconButton
|
|
40
38
|
size="sm"
|
|
41
39
|
variant={board.votedItems[item.id] === 'up' ? 'primary' : 'neutral'}
|
|
@@ -51,9 +49,7 @@
|
|
|
51
49
|
<i class="fa-solid fa-exclamation-circle" aria-hidden="true"></i>
|
|
52
50
|
</span>
|
|
53
51
|
{/if}
|
|
54
|
-
|
|
55
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
56
|
-
<div onclick={(e) => e.stopPropagation()}>
|
|
52
|
+
<div role="presentation" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
|
|
57
53
|
<IconButton
|
|
58
54
|
size="sm"
|
|
59
55
|
variant={board.votedItems[item.id] === 'down' ? 'primary' : 'neutral'}
|
|
@@ -83,17 +79,26 @@
|
|
|
83
79
|
<p class="text-sm text-foreground-secondary mt-1.5 leading-relaxed line-clamp-2">{item.message}</p>
|
|
84
80
|
{/if}
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
<
|
|
82
|
+
{#if item.email}
|
|
83
|
+
<p class="text-[0.8125rem] text-foreground-tertiary mt-1.5 flex items-center gap-1">
|
|
84
|
+
<i class="fa-solid fa-envelope" aria-hidden="true"></i>
|
|
85
|
+
{item.email}
|
|
86
|
+
</p>
|
|
87
|
+
{/if}
|
|
88
|
+
|
|
89
|
+
<div class="flex items-center justify-between mt-2 text-[0.8125rem] text-foreground-tertiary">
|
|
90
|
+
<div class="flex items-center gap-3">
|
|
91
|
+
<span>{timeAgo(item.createdAt)}</span>
|
|
92
|
+
{#if item.adminResponse}
|
|
93
|
+
<span class="text-xs text-primary">
|
|
94
|
+
<i class="fa-solid fa-shield" aria-hidden="true"></i> Official response
|
|
95
|
+
</span>
|
|
96
|
+
{/if}
|
|
97
|
+
</div>
|
|
88
98
|
{#if item.responseCount > 0}
|
|
89
99
|
<span class="flex items-center gap-1" aria-label="{item.responseCount} {item.responseCount === 1 ? 'response' : 'responses'}">
|
|
90
100
|
<i class="fa-solid fa-comment" aria-hidden="true"></i>
|
|
91
|
-
{item.responseCount}
|
|
92
|
-
</span>
|
|
93
|
-
{/if}
|
|
94
|
-
{#if item.adminResponse}
|
|
95
|
-
<span class="text-xs text-primary">
|
|
96
|
-
<i class="fa-solid fa-shield" aria-hidden="true"></i> Official response
|
|
101
|
+
{item.responseCount}
|
|
97
102
|
</span>
|
|
98
103
|
{/if}
|
|
99
104
|
</div>
|
|
@@ -6,14 +6,16 @@
|
|
|
6
6
|
board: FeedbackBoardState;
|
|
7
7
|
/** Available category options for the dropdown */
|
|
8
8
|
categories?: string[];
|
|
9
|
+
/** Pre-fill the email field (e.g. from the logged-in user's session) */
|
|
10
|
+
userEmail?: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
let { board, categories = [] }: Props = $props();
|
|
13
|
+
let { board, categories = [], userEmail }: Props = $props();
|
|
12
14
|
|
|
13
15
|
let title = $state('');
|
|
14
16
|
let message = $state('');
|
|
15
17
|
let category = $state('');
|
|
16
|
-
let email = $
|
|
18
|
+
let email = $derived(userEmail ?? '');
|
|
17
19
|
let submitting = $state(false);
|
|
18
20
|
let success = $state(false);
|
|
19
21
|
let error = $state('');
|
|
@@ -3,6 +3,8 @@ interface Props {
|
|
|
3
3
|
board: FeedbackBoardState;
|
|
4
4
|
/** Available category options for the dropdown */
|
|
5
5
|
categories?: string[];
|
|
6
|
+
/** Pre-fill the email field (e.g. from the logged-in user's session) */
|
|
7
|
+
userEmail?: string;
|
|
6
8
|
}
|
|
7
9
|
declare const FeedbackSubmitForm: import("svelte").Component<Props, {}, "">;
|
|
8
10
|
type FeedbackSubmitForm = ReturnType<typeof FeedbackSubmitForm>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* <FeedbackSidebar {board} />
|
|
7
7
|
* <FeedbackList {board} />
|
|
8
8
|
*/
|
|
9
|
+
import type { FeedbackResponse } from '../../types.js';
|
|
9
10
|
import { type BoardItem, type BoardCategory, type StatusClient } from '../../api.js';
|
|
10
11
|
export interface CategoryConfig {
|
|
11
12
|
label?: string;
|
|
@@ -37,17 +38,17 @@ export interface FeedbackBoardState {
|
|
|
37
38
|
reload(): Promise<void>;
|
|
38
39
|
loadMore(): Promise<void>;
|
|
39
40
|
vote(itemId: string, direction: 'up' | 'down'): Promise<void>;
|
|
40
|
-
getResponses(itemId: string): Promise<
|
|
41
|
-
message: string;
|
|
42
|
-
author: string | null;
|
|
43
|
-
createdAt: number;
|
|
44
|
-
}[]>;
|
|
41
|
+
getResponses(itemId: string): Promise<FeedbackResponse[]>;
|
|
45
42
|
submitFeedback(data: {
|
|
46
43
|
title?: string;
|
|
47
44
|
message: string;
|
|
48
45
|
category?: string;
|
|
49
46
|
email?: string;
|
|
50
47
|
}): Promise<void>;
|
|
48
|
+
submitResponse(itemId: string, data: {
|
|
49
|
+
message: string;
|
|
50
|
+
author?: string;
|
|
51
|
+
}): Promise<void>;
|
|
51
52
|
getCategoryIcon(name: string): string;
|
|
52
53
|
getCategoryLabel(name: string): string;
|
|
53
54
|
getCategoryDescription(name: string): string;
|
|
@@ -123,6 +123,15 @@ export function createFeedbackBoard(options) {
|
|
|
123
123
|
});
|
|
124
124
|
await load();
|
|
125
125
|
}
|
|
126
|
+
async function submitResponse(itemId, data) {
|
|
127
|
+
await client.submitResponse(itemId, data);
|
|
128
|
+
// Invalidate cache so next getResponses fetches fresh data
|
|
129
|
+
delete responseCache[itemId];
|
|
130
|
+
// Update response count on the item
|
|
131
|
+
const item = items.find((i) => i.id === itemId);
|
|
132
|
+
if (item)
|
|
133
|
+
item.responseCount += 1;
|
|
134
|
+
}
|
|
126
135
|
return {
|
|
127
136
|
get client() { return client; },
|
|
128
137
|
get categoryConfig() { return categoryConfig; },
|
|
@@ -146,6 +155,7 @@ export function createFeedbackBoard(options) {
|
|
|
146
155
|
vote,
|
|
147
156
|
getResponses,
|
|
148
157
|
submitFeedback,
|
|
158
|
+
submitResponse,
|
|
149
159
|
getCategoryIcon(name) {
|
|
150
160
|
return categoryConfig[name]?.icon || 'fa-solid fa-circle';
|
|
151
161
|
},
|
|
@@ -94,18 +94,17 @@
|
|
|
94
94
|
}
|
|
95
95
|
</script>
|
|
96
96
|
|
|
97
|
-
<!-- svelte-ignore a11y_no_static_element_interactions — context menu positioned at cursor -->
|
|
98
97
|
<!-- popover="manual" promotes to browser top layer (above <dialog> modals) -->
|
|
99
98
|
<div
|
|
100
99
|
bind:this={popoverEl}
|
|
100
|
+
role="presentation"
|
|
101
101
|
popover="manual"
|
|
102
102
|
class="sf-ctx-popover"
|
|
103
103
|
style="left: {adjustedX}px; top: {adjustedY}px;"
|
|
104
104
|
onmousedown={(e) => e.stopPropagation()}
|
|
105
105
|
onclick={(e) => e.stopPropagation()}
|
|
106
|
+
onkeydown={(e) => e.stopPropagation()}
|
|
106
107
|
>
|
|
107
|
-
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
108
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
109
108
|
<div class="sf-ctx-menu" role="menu" tabindex="-1" aria-label="Feedback actions" onkeydown={handleKeydown}>
|
|
110
109
|
<button class="sf-ctx-item" role="menuitem" onclick={onReportBug}>
|
|
111
110
|
<i class="fa-solid fa-bug text-red-400" aria-hidden="true"></i>
|
package/package.json
CHANGED