feedtack 0.3.0 → 0.4.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 +90 -0
- package/dist/index.d.ts +4 -1
- package/dist/react/index.d.ts +14 -3
- package/dist/react/index.js +165 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,96 @@ class MySupabaseAdapter implements FeedtackAdapter {
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
## Adapter recipes
|
|
78
|
+
|
|
79
|
+
The `FeedtackAdapter` interface has five methods. Here are copy-paste implementations for common backends.
|
|
80
|
+
|
|
81
|
+
### Disk / JSON files (Node.js)
|
|
82
|
+
|
|
83
|
+
Git-trackable feedback — each submission becomes a JSON file in `.feedback/`.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import type { FeedtackAdapter, FeedbackItem, FeedtackPayload } from 'feedtack'
|
|
87
|
+
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
88
|
+
import { join } from 'node:path'
|
|
89
|
+
|
|
90
|
+
const DIR = '.feedback'
|
|
91
|
+
|
|
92
|
+
class DiskAdapter implements FeedtackAdapter {
|
|
93
|
+
async submit(payload: FeedtackPayload) {
|
|
94
|
+
await mkdir(DIR, { recursive: true })
|
|
95
|
+
const item: FeedbackItem = { payload, replies: [], resolutions: [], archives: [] }
|
|
96
|
+
await writeFile(join(DIR, `${payload.id}.json`), JSON.stringify(item, null, 2))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async reply(feedbackId: string, reply: Omit<FeedbackItem['replies'][0], 'id' | 'feedbackId'>) {
|
|
100
|
+
const item = await this.read(feedbackId)
|
|
101
|
+
item.replies.push({ ...reply, id: crypto.randomUUID(), feedbackId })
|
|
102
|
+
await this.write(feedbackId, item)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async resolve(feedbackId: string, resolution: Omit<FeedbackItem['resolutions'][0], 'feedbackId'>) {
|
|
106
|
+
const item = await this.read(feedbackId)
|
|
107
|
+
item.resolutions.push({ ...resolution, feedbackId })
|
|
108
|
+
await this.write(feedbackId, item)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async archive(feedbackId: string, userId: string) {
|
|
112
|
+
const item = await this.read(feedbackId)
|
|
113
|
+
item.archives.push({ feedbackId, archivedBy: { id: userId, name: '', role: '' }, timestamp: new Date().toISOString() })
|
|
114
|
+
await this.write(feedbackId, item)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async loadFeedback() {
|
|
118
|
+
await mkdir(DIR, { recursive: true })
|
|
119
|
+
const files = (await readdir(DIR)).filter((f) => f.endsWith('.json'))
|
|
120
|
+
return Promise.all(files.map(async (f) => JSON.parse(await readFile(join(DIR, f), 'utf-8')) as FeedbackItem))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async read(id: string) { return JSON.parse(await readFile(join(DIR, `${id}.json`), 'utf-8')) as FeedbackItem }
|
|
124
|
+
private async write(id: string, item: FeedbackItem) { await writeFile(join(DIR, `${id}.json`), JSON.stringify(item, null, 2)) }
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Supabase
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import type { FeedtackAdapter, FeedbackItem, FeedtackFilter, FeedtackPayload } from 'feedtack'
|
|
132
|
+
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
133
|
+
|
|
134
|
+
class SupabaseAdapter implements FeedtackAdapter {
|
|
135
|
+
constructor(private supabase: SupabaseClient) {}
|
|
136
|
+
|
|
137
|
+
async submit(payload: FeedtackPayload) {
|
|
138
|
+
await this.supabase.from('feedtack_submissions').insert({ id: payload.id, data: payload })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async reply(feedbackId: string, reply: Omit<FeedbackItem['replies'][0], 'id' | 'feedbackId'>) {
|
|
142
|
+
await this.supabase.from('feedtack_replies').insert({ feedback_id: feedbackId, ...reply })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async resolve(feedbackId: string, resolution: Omit<FeedbackItem['resolutions'][0], 'feedbackId'>) {
|
|
146
|
+
await this.supabase.from('feedtack_resolutions').insert({ feedback_id: feedbackId, ...resolution })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async archive(feedbackId: string, userId: string) {
|
|
150
|
+
await this.supabase.from('feedtack_archives').insert({ feedback_id: feedbackId, user_id: userId })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async loadFeedback(filter?: FeedtackFilter): Promise<FeedbackItem[]> {
|
|
154
|
+
let query = this.supabase.from('feedtack_submissions').select('*, feedtack_replies(*), feedtack_resolutions(*), feedtack_archives(*)')
|
|
155
|
+
if (filter?.pathname) query = query.eq('data->>page->>pathname', filter.pathname)
|
|
156
|
+
const { data } = await query
|
|
157
|
+
return (data ?? []).map((row) => ({
|
|
158
|
+
payload: row.data,
|
|
159
|
+
replies: row.feedtack_replies ?? [],
|
|
160
|
+
resolutions: row.feedtack_resolutions ?? [],
|
|
161
|
+
archives: row.feedtack_archives ?? [],
|
|
162
|
+
}))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
77
167
|
## The payload
|
|
78
168
|
|
|
79
169
|
Every pin emits a versioned JSON payload:
|
package/dist/index.d.ts
CHANGED
|
@@ -50,7 +50,10 @@ declare class WebhookAdapter implements FeedtackAdapter {
|
|
|
50
50
|
declare function getViewportMeta(): FeedtackViewportMeta;
|
|
51
51
|
declare function getPageMeta(): FeedtackPageMeta;
|
|
52
52
|
declare function getDeviceMeta(): FeedtackDeviceMeta;
|
|
53
|
-
declare function getPinCoords(event:
|
|
53
|
+
declare function getPinCoords(event: {
|
|
54
|
+
clientX: number;
|
|
55
|
+
clientY: number;
|
|
56
|
+
}): {
|
|
54
57
|
x: number;
|
|
55
58
|
y: number;
|
|
56
59
|
xPct: number;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { F as FeedtackAdapter, o as FeedtackUser, n as FeedtackTheme
|
|
3
|
+
import { e as FeedbackItem, F as FeedtackAdapter, o as FeedtackUser, n as FeedtackTheme } from '../theme-C-uctIoI.js';
|
|
4
|
+
|
|
5
|
+
interface FeedtackFlushEvent {
|
|
6
|
+
pathname: string;
|
|
7
|
+
items: FeedbackItem[];
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
interface FeedtackClasses {
|
|
6
11
|
button?: string;
|
|
@@ -26,8 +31,14 @@ interface FeedtackProviderProps {
|
|
|
26
31
|
disabled?: boolean;
|
|
27
32
|
/** Render custom content inside a submitted pin marker. Receives the feedback item. */
|
|
28
33
|
renderPinIcon?: (item: FeedbackItem) => React.ReactNode;
|
|
34
|
+
/** Called with batched feedback when user leaves a page or goes idle */
|
|
35
|
+
onFlush?: (event: FeedtackFlushEvent) => void;
|
|
36
|
+
/** Idle timeout in ms before flushing (default 5 min) */
|
|
37
|
+
flushIdleMs?: number;
|
|
38
|
+
/** User roles that trigger re-scope on reply (default: any non-'agent' role) */
|
|
39
|
+
rescopeRoles?: string[];
|
|
29
40
|
}
|
|
30
|
-
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, renderPinIcon, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
|
|
41
|
+
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, renderPinIcon, onFlush, flushIdleMs, rescopeRoles, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
|
|
31
42
|
|
|
32
43
|
interface FeedtackContextValue {
|
|
33
44
|
activatePinMode: () => void;
|
|
@@ -38,4 +49,4 @@ interface FeedtackContextValue {
|
|
|
38
49
|
/** Hook for host app to programmatically control feedtack */
|
|
39
50
|
declare function useFeedtack(): FeedtackContextValue;
|
|
40
51
|
|
|
41
|
-
export { type FeedtackClasses, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, useFeedtack };
|
|
52
|
+
export { type FeedtackClasses, type FeedtackFlushEvent, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, useFeedtack };
|
package/dist/react/index.js
CHANGED
|
@@ -171,7 +171,7 @@ function ThreadPanel({
|
|
|
171
171
|
item.replies.map((r) => /* @__PURE__ */ jsxs2(
|
|
172
172
|
"div",
|
|
173
173
|
{
|
|
174
|
-
style: { borderTop: "1px solid
|
|
174
|
+
style: { borderTop: "1px solid var(--ft-border)", paddingTop: 8 },
|
|
175
175
|
children: [
|
|
176
176
|
/* @__PURE__ */ jsx2("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
|
|
177
177
|
/* @__PURE__ */ jsx2("p", { style: { fontSize: 12 }, children: r.body })
|
|
@@ -190,7 +190,11 @@ function ThreadPanel({
|
|
|
190
190
|
fontSize: 12,
|
|
191
191
|
padding: 6,
|
|
192
192
|
borderRadius: 6,
|
|
193
|
-
border: "1px solid
|
|
193
|
+
border: "1px solid var(--ft-border)",
|
|
194
|
+
background: "var(--ft-surface)",
|
|
195
|
+
color: "var(--ft-text)",
|
|
196
|
+
minHeight: 60,
|
|
197
|
+
resize: "vertical",
|
|
194
198
|
marginTop: 4
|
|
195
199
|
}
|
|
196
200
|
}
|
|
@@ -243,14 +247,14 @@ function ThreadPanel({
|
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
// src/react/useFeedtackState.ts
|
|
246
|
-
import { useCallback as
|
|
250
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useState as useState2 } from "react";
|
|
247
251
|
|
|
248
252
|
// src/react/useFeedtackDom.ts
|
|
249
253
|
import { useEffect, useRef } from "react";
|
|
250
254
|
|
|
251
255
|
// src/ui/styles.ts
|
|
252
256
|
var FEEDTACK_DEFAULT_TOKENS = `
|
|
253
|
-
#feedtack-root {
|
|
257
|
+
#feedtack-root, .feedtack-form, .feedtack-thread {
|
|
254
258
|
--ft-primary: #2563eb;
|
|
255
259
|
--ft-primary-hover: #1d4ed8;
|
|
256
260
|
--ft-bg: #ffffff;
|
|
@@ -532,21 +536,86 @@ function useFeedtackDom(theme, disabled) {
|
|
|
532
536
|
return rootRef;
|
|
533
537
|
}
|
|
534
538
|
|
|
539
|
+
// src/react/useFeedtackFlush.ts
|
|
540
|
+
import { useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
541
|
+
var DEFAULT_IDLE_MS = 5 * 60 * 1e3;
|
|
542
|
+
function useFeedtackFlush({
|
|
543
|
+
pathname,
|
|
544
|
+
feedbackItems,
|
|
545
|
+
onFlush,
|
|
546
|
+
flushIdleMs = DEFAULT_IDLE_MS,
|
|
547
|
+
disabled
|
|
548
|
+
}) {
|
|
549
|
+
const flushedRef = useRef2(/* @__PURE__ */ new Set());
|
|
550
|
+
const prevPathnameRef = useRef2(pathname);
|
|
551
|
+
const idleTimerRef = useRef2(null);
|
|
552
|
+
const flush = useCallback(
|
|
553
|
+
(path, items) => {
|
|
554
|
+
if (!onFlush || flushedRef.current.has(path)) return;
|
|
555
|
+
const pageItems = items.filter((i) => i.payload.page.pathname === path);
|
|
556
|
+
if (pageItems.length === 0) return;
|
|
557
|
+
flushedRef.current.add(path);
|
|
558
|
+
onFlush({ pathname: path, items: pageItems });
|
|
559
|
+
},
|
|
560
|
+
[onFlush]
|
|
561
|
+
);
|
|
562
|
+
useEffect2(() => {
|
|
563
|
+
if (disabled || !onFlush) return;
|
|
564
|
+
const prev = prevPathnameRef.current;
|
|
565
|
+
prevPathnameRef.current = pathname;
|
|
566
|
+
if (prev !== pathname) {
|
|
567
|
+
flush(prev, feedbackItems);
|
|
568
|
+
}
|
|
569
|
+
}, [pathname, feedbackItems, flush, onFlush, disabled]);
|
|
570
|
+
useEffect2(() => {
|
|
571
|
+
if (disabled || !onFlush || flushIdleMs <= 0) return;
|
|
572
|
+
const resetTimer = () => {
|
|
573
|
+
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
574
|
+
idleTimerRef.current = setTimeout(() => {
|
|
575
|
+
flush(pathname, feedbackItems);
|
|
576
|
+
}, flushIdleMs);
|
|
577
|
+
};
|
|
578
|
+
const events = ["mousemove", "keydown", "scroll", "touchstart"];
|
|
579
|
+
for (const e of events)
|
|
580
|
+
window.addEventListener(e, resetTimer, { passive: true });
|
|
581
|
+
resetTimer();
|
|
582
|
+
return () => {
|
|
583
|
+
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
584
|
+
for (const e of events) window.removeEventListener(e, resetTimer);
|
|
585
|
+
};
|
|
586
|
+
}, [pathname, feedbackItems, flush, onFlush, flushIdleMs, disabled]);
|
|
587
|
+
useEffect2(() => {
|
|
588
|
+
if (disabled || !onFlush) return;
|
|
589
|
+
const handleUnload = () => flush(pathname, feedbackItems);
|
|
590
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
591
|
+
return () => window.removeEventListener("beforeunload", handleUnload);
|
|
592
|
+
}, [pathname, feedbackItems, flush, onFlush, disabled]);
|
|
593
|
+
const clearFlushed = useCallback((path) => {
|
|
594
|
+
flushedRef.current.delete(path);
|
|
595
|
+
}, []);
|
|
596
|
+
return { clearFlushed };
|
|
597
|
+
}
|
|
598
|
+
|
|
535
599
|
// src/react/usePinMode.ts
|
|
536
|
-
import { useCallback, useEffect as
|
|
537
|
-
function usePinMode({
|
|
600
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useState } from "react";
|
|
601
|
+
function usePinMode({
|
|
602
|
+
hotkey,
|
|
603
|
+
onDeactivate,
|
|
604
|
+
disabled,
|
|
605
|
+
isModalOpen
|
|
606
|
+
}) {
|
|
538
607
|
const [isActive, setIsActive] = useState(false);
|
|
539
608
|
const [pendingPins, setPendingPins] = useState([]);
|
|
540
609
|
const [selectedColor, setSelectedColor] = useState(PIN_PALETTE[0]);
|
|
541
610
|
const [showForm, setShowForm] = useState(false);
|
|
542
|
-
const activate =
|
|
543
|
-
const deactivate =
|
|
611
|
+
const activate = useCallback2(() => setIsActive(true), []);
|
|
612
|
+
const deactivate = useCallback2(() => {
|
|
544
613
|
setIsActive(false);
|
|
545
614
|
setPendingPins([]);
|
|
546
615
|
setShowForm(false);
|
|
547
616
|
onDeactivate?.();
|
|
548
617
|
}, [onDeactivate]);
|
|
549
|
-
|
|
618
|
+
useEffect3(() => {
|
|
550
619
|
if (isActive) {
|
|
551
620
|
document.documentElement.classList.add("feedtack-crosshair");
|
|
552
621
|
} else {
|
|
@@ -554,7 +623,7 @@ function usePinMode({ hotkey, onDeactivate, disabled }) {
|
|
|
554
623
|
}
|
|
555
624
|
return () => document.documentElement.classList.remove("feedtack-crosshair");
|
|
556
625
|
}, [isActive]);
|
|
557
|
-
|
|
626
|
+
useEffect3(() => {
|
|
558
627
|
if (disabled) return;
|
|
559
628
|
const handler = (e) => {
|
|
560
629
|
if (e.key === hotkey.toUpperCase() && e.shiftKey) {
|
|
@@ -563,7 +632,7 @@ function usePinMode({ hotkey, onDeactivate, disabled }) {
|
|
|
563
632
|
if (e.key === "Escape") {
|
|
564
633
|
deactivate();
|
|
565
634
|
}
|
|
566
|
-
if (isActive && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
|
|
635
|
+
if (isActive && !isModalOpen && !showForm && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
|
|
567
636
|
e.preventDefault();
|
|
568
637
|
setSelectedColor((prev) => {
|
|
569
638
|
const idx = PIN_PALETTE.indexOf(prev);
|
|
@@ -574,32 +643,53 @@ function usePinMode({ hotkey, onDeactivate, disabled }) {
|
|
|
574
643
|
};
|
|
575
644
|
window.addEventListener("keydown", handler);
|
|
576
645
|
return () => window.removeEventListener("keydown", handler);
|
|
577
|
-
}, [hotkey, deactivate, isActive, disabled]);
|
|
578
|
-
const
|
|
579
|
-
(
|
|
580
|
-
if (!isActive) return;
|
|
581
|
-
const target = e.target;
|
|
646
|
+
}, [hotkey, deactivate, isActive, disabled, isModalOpen, showForm]);
|
|
647
|
+
const placePin = useCallback2(
|
|
648
|
+
(coords, target) => {
|
|
582
649
|
if (target.closest("#feedtack-root, .feedtack-form, .feedtack-color-picker"))
|
|
583
650
|
return;
|
|
584
|
-
e.preventDefault();
|
|
585
|
-
e.stopPropagation();
|
|
586
651
|
setPendingPins((prev) => [
|
|
587
652
|
...prev,
|
|
588
653
|
{
|
|
589
654
|
color: selectedColor,
|
|
590
|
-
...getPinCoords(
|
|
655
|
+
...getPinCoords(coords),
|
|
591
656
|
target: getTargetMeta(target)
|
|
592
657
|
}
|
|
593
658
|
]);
|
|
594
659
|
setShowForm(true);
|
|
595
660
|
},
|
|
596
|
-
[
|
|
661
|
+
[selectedColor]
|
|
597
662
|
);
|
|
598
|
-
|
|
663
|
+
const handlePageClick = useCallback2(
|
|
664
|
+
(e) => {
|
|
665
|
+
if (!isActive) return;
|
|
666
|
+
e.preventDefault();
|
|
667
|
+
e.stopPropagation();
|
|
668
|
+
placePin(e, e.target);
|
|
669
|
+
},
|
|
670
|
+
[isActive, placePin]
|
|
671
|
+
);
|
|
672
|
+
const handleTouchEnd = useCallback2(
|
|
673
|
+
(e) => {
|
|
674
|
+
if (!isActive) return;
|
|
675
|
+
const touch = e.changedTouches[0];
|
|
676
|
+
if (!touch) return;
|
|
677
|
+
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
678
|
+
if (!target) return;
|
|
679
|
+
e.preventDefault();
|
|
680
|
+
placePin(touch, target);
|
|
681
|
+
},
|
|
682
|
+
[isActive, placePin]
|
|
683
|
+
);
|
|
684
|
+
useEffect3(() => {
|
|
599
685
|
if (disabled) return;
|
|
600
686
|
document.addEventListener("click", handlePageClick, true);
|
|
601
|
-
|
|
602
|
-
|
|
687
|
+
document.addEventListener("touchend", handleTouchEnd, true);
|
|
688
|
+
return () => {
|
|
689
|
+
document.removeEventListener("click", handlePageClick, true);
|
|
690
|
+
document.removeEventListener("touchend", handleTouchEnd, true);
|
|
691
|
+
};
|
|
692
|
+
}, [handlePageClick, handleTouchEnd, disabled]);
|
|
603
693
|
return {
|
|
604
694
|
isActive,
|
|
605
695
|
activate,
|
|
@@ -618,11 +708,16 @@ function useFeedtackState({
|
|
|
618
708
|
hotkey,
|
|
619
709
|
theme,
|
|
620
710
|
onError,
|
|
621
|
-
disabled
|
|
711
|
+
disabled,
|
|
712
|
+
onFlush,
|
|
713
|
+
flushIdleMs,
|
|
714
|
+
rescopeRoles
|
|
622
715
|
}) {
|
|
623
716
|
useFeedtackDom(theme, disabled);
|
|
624
|
-
const [pathname, setPathname] = useState2(
|
|
625
|
-
|
|
717
|
+
const [pathname, setPathname] = useState2(
|
|
718
|
+
() => typeof window === "undefined" ? "/" : window.location.pathname
|
|
719
|
+
);
|
|
720
|
+
useEffect4(() => {
|
|
626
721
|
const update = () => setPathname(window.location.pathname);
|
|
627
722
|
const origPush = history.pushState.bind(history);
|
|
628
723
|
const origReplace = history.replaceState.bind(history);
|
|
@@ -649,7 +744,7 @@ function useFeedtackState({
|
|
|
649
744
|
const [loading, setLoading] = useState2(true);
|
|
650
745
|
const [openThreadId, setOpenThreadId] = useState2(null);
|
|
651
746
|
const [replyBody, setReplyBody] = useState2("");
|
|
652
|
-
const resetForm =
|
|
747
|
+
const resetForm = useCallback3(() => {
|
|
653
748
|
setComment("");
|
|
654
749
|
setSentiment(null);
|
|
655
750
|
setCommentError(false);
|
|
@@ -657,12 +752,20 @@ function useFeedtackState({
|
|
|
657
752
|
const pinMode = usePinMode({
|
|
658
753
|
hotkey,
|
|
659
754
|
disabled,
|
|
755
|
+
isModalOpen: openThreadId !== null,
|
|
660
756
|
onDeactivate: () => {
|
|
661
757
|
resetForm();
|
|
662
758
|
setOpenThreadId(null);
|
|
663
759
|
}
|
|
664
760
|
});
|
|
665
|
-
|
|
761
|
+
const { clearFlushed } = useFeedtackFlush({
|
|
762
|
+
pathname,
|
|
763
|
+
feedbackItems,
|
|
764
|
+
onFlush,
|
|
765
|
+
flushIdleMs,
|
|
766
|
+
disabled
|
|
767
|
+
});
|
|
768
|
+
useEffect4(() => {
|
|
666
769
|
setLoading(true);
|
|
667
770
|
adapter.loadFeedback({ pathname }).then(setFeedbackItems).catch((err) => onError?.(err)).finally(() => setLoading(false));
|
|
668
771
|
}, [adapter, onError, pathname]);
|
|
@@ -710,19 +813,26 @@ function useFeedtackState({
|
|
|
710
813
|
body,
|
|
711
814
|
timestamp: ts
|
|
712
815
|
});
|
|
713
|
-
updateItem(feedbackId, (item) =>
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
816
|
+
updateItem(feedbackId, (item) => {
|
|
817
|
+
const updated = {
|
|
818
|
+
...item,
|
|
819
|
+
replies: [
|
|
820
|
+
...item.replies,
|
|
821
|
+
{
|
|
822
|
+
id: generateId(),
|
|
823
|
+
feedbackId,
|
|
824
|
+
author: currentUser,
|
|
825
|
+
body,
|
|
826
|
+
timestamp: ts
|
|
827
|
+
}
|
|
828
|
+
]
|
|
829
|
+
};
|
|
830
|
+
const triggerRescope = rescopeRoles ? rescopeRoles.includes(currentUser.role) : currentUser.role !== "agent";
|
|
831
|
+
if (triggerRescope && updated.resolutions.length === 0 && onFlush) {
|
|
832
|
+
clearFlushed(pathname);
|
|
833
|
+
}
|
|
834
|
+
return updated;
|
|
835
|
+
});
|
|
726
836
|
setReplyBody("");
|
|
727
837
|
} catch (err) {
|
|
728
838
|
onError?.(err);
|
|
@@ -762,6 +872,9 @@ function useFeedtackState({
|
|
|
762
872
|
onError?.(err);
|
|
763
873
|
}
|
|
764
874
|
};
|
|
875
|
+
const isArchivedForUser = (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id);
|
|
876
|
+
const hasUnread = (item) => item.replies.length > 0;
|
|
877
|
+
const hasValidPins = (item) => Array.isArray(item.payload?.pins) && item.payload.pins.length > 0;
|
|
765
878
|
return {
|
|
766
879
|
...pinMode,
|
|
767
880
|
isPinModeActive: pinMode.isActive,
|
|
@@ -785,9 +898,9 @@ function useFeedtackState({
|
|
|
785
898
|
handleReply,
|
|
786
899
|
handleResolve,
|
|
787
900
|
handleArchive,
|
|
788
|
-
isArchivedForUser
|
|
789
|
-
hasUnread
|
|
790
|
-
hasValidPins
|
|
901
|
+
isArchivedForUser,
|
|
902
|
+
hasUnread,
|
|
903
|
+
hasValidPins
|
|
791
904
|
};
|
|
792
905
|
}
|
|
793
906
|
|
|
@@ -804,7 +917,10 @@ function FeedtackProvider({
|
|
|
804
917
|
sentimentLabels = {},
|
|
805
918
|
onError,
|
|
806
919
|
disabled = false,
|
|
807
|
-
renderPinIcon
|
|
920
|
+
renderPinIcon,
|
|
921
|
+
onFlush,
|
|
922
|
+
flushIdleMs,
|
|
923
|
+
rescopeRoles
|
|
808
924
|
}) {
|
|
809
925
|
const state = useFeedtackState({
|
|
810
926
|
adapter,
|
|
@@ -812,7 +928,10 @@ function FeedtackProvider({
|
|
|
812
928
|
hotkey,
|
|
813
929
|
theme,
|
|
814
930
|
onError,
|
|
815
|
-
disabled
|
|
931
|
+
disabled,
|
|
932
|
+
onFlush,
|
|
933
|
+
flushIdleMs,
|
|
934
|
+
rescopeRoles
|
|
816
935
|
});
|
|
817
936
|
const firstPin = state.pendingPins[0];
|
|
818
937
|
const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
|