feedtack 0.3.1 → 0.5.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/dist/index.d.ts +4 -1
- package/dist/react/index.d.ts +14 -3
- package/dist/react/index.js +371 -148
- package/package.json +1 -1
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
|
@@ -155,11 +155,13 @@ function ThreadPanel({
|
|
|
155
155
|
onResolve,
|
|
156
156
|
onArchive,
|
|
157
157
|
onClose,
|
|
158
|
-
className
|
|
158
|
+
className,
|
|
159
|
+
pinPosition
|
|
159
160
|
}) {
|
|
160
161
|
const pin = item.payload?.pins?.[0];
|
|
161
162
|
if (!pin) return null;
|
|
162
|
-
const
|
|
163
|
+
const { x, y } = pinPosition ?? pin;
|
|
164
|
+
const pos = getAnchoredPosition(x, y);
|
|
163
165
|
return /* @__PURE__ */ jsxs2(
|
|
164
166
|
"div",
|
|
165
167
|
{
|
|
@@ -191,6 +193,10 @@ function ThreadPanel({
|
|
|
191
193
|
padding: 6,
|
|
192
194
|
borderRadius: 6,
|
|
193
195
|
border: "1px solid var(--ft-border)",
|
|
196
|
+
background: "var(--ft-surface)",
|
|
197
|
+
color: "var(--ft-text)",
|
|
198
|
+
minHeight: 60,
|
|
199
|
+
resize: "vertical",
|
|
194
200
|
marginTop: 4
|
|
195
201
|
}
|
|
196
202
|
}
|
|
@@ -242,15 +248,199 @@ function ThreadPanel({
|
|
|
242
248
|
);
|
|
243
249
|
}
|
|
244
250
|
|
|
251
|
+
// src/react/useAnchoredPins.ts
|
|
252
|
+
import { useCallback, useEffect, useState } from "react";
|
|
253
|
+
function useAnchoredPins(items, pathname) {
|
|
254
|
+
const [positions, setPositions] = useState(
|
|
255
|
+
/* @__PURE__ */ new Map()
|
|
256
|
+
);
|
|
257
|
+
const resolve = useCallback(() => {
|
|
258
|
+
const next = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const item of items) {
|
|
260
|
+
if (item.payload.page.pathname !== pathname) continue;
|
|
261
|
+
const pin = item.payload.pins[0];
|
|
262
|
+
if (!pin) continue;
|
|
263
|
+
const pos = resolvePin(pin);
|
|
264
|
+
next.set(item.payload.id, pos);
|
|
265
|
+
}
|
|
266
|
+
setPositions(next);
|
|
267
|
+
}, [items, pathname]);
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
resolve();
|
|
270
|
+
}, [resolve]);
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
let raf = 0;
|
|
273
|
+
const handler = () => {
|
|
274
|
+
cancelAnimationFrame(raf);
|
|
275
|
+
raf = requestAnimationFrame(resolve);
|
|
276
|
+
};
|
|
277
|
+
window.addEventListener("resize", handler, { passive: true });
|
|
278
|
+
window.addEventListener("scroll", handler, { passive: true });
|
|
279
|
+
return () => {
|
|
280
|
+
cancelAnimationFrame(raf);
|
|
281
|
+
window.removeEventListener("resize", handler);
|
|
282
|
+
window.removeEventListener("scroll", handler);
|
|
283
|
+
};
|
|
284
|
+
}, [resolve]);
|
|
285
|
+
const getPosition = useCallback(
|
|
286
|
+
(itemId, fallbackPin) => {
|
|
287
|
+
return positions.get(itemId) ?? { x: fallbackPin.x, y: fallbackPin.y };
|
|
288
|
+
},
|
|
289
|
+
[positions]
|
|
290
|
+
);
|
|
291
|
+
return { getPosition };
|
|
292
|
+
}
|
|
293
|
+
function resolvePin(pin) {
|
|
294
|
+
const { target } = pin;
|
|
295
|
+
if (!target.selector) return { x: pin.x, y: pin.y };
|
|
296
|
+
let el = null;
|
|
297
|
+
try {
|
|
298
|
+
el = document.querySelector(target.selector);
|
|
299
|
+
} catch {
|
|
300
|
+
return { x: pin.x, y: pin.y };
|
|
301
|
+
}
|
|
302
|
+
if (!el) return { x: pin.x, y: pin.y };
|
|
303
|
+
const rect = el.getBoundingClientRect();
|
|
304
|
+
const origRect = target.boundingRect;
|
|
305
|
+
const ratioX = origRect.width > 0 ? (pin.x - origRect.x) / origRect.width : 0.5;
|
|
306
|
+
const ratioY = origRect.height > 0 ? (pin.y - origRect.y) / origRect.height : 0.5;
|
|
307
|
+
const x = rect.x + window.scrollX + ratioX * rect.width;
|
|
308
|
+
const y = rect.y + window.scrollY + ratioY * rect.height;
|
|
309
|
+
return { x, y };
|
|
310
|
+
}
|
|
311
|
+
|
|
245
312
|
// src/react/useFeedtackState.ts
|
|
246
|
-
import { useCallback as
|
|
313
|
+
import { useCallback as useCallback5, useEffect as useEffect5, useState as useState3 } from "react";
|
|
314
|
+
|
|
315
|
+
// src/react/useFeedtackActions.ts
|
|
316
|
+
import { useCallback as useCallback2 } from "react";
|
|
317
|
+
function useFeedtackActions(deps) {
|
|
318
|
+
const { adapter, currentUser, onError } = deps;
|
|
319
|
+
const updateItem = useCallback2(
|
|
320
|
+
(id, fn) => deps.setFeedbackItems(
|
|
321
|
+
(prev) => prev.map((i) => i.payload.id === id ? fn(i) : i)
|
|
322
|
+
),
|
|
323
|
+
[deps.setFeedbackItems]
|
|
324
|
+
);
|
|
325
|
+
const handleSubmit = useCallback2(async () => {
|
|
326
|
+
const comment = deps.getComment();
|
|
327
|
+
if (!comment.trim()) {
|
|
328
|
+
deps.setCommentError(true);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
deps.setSubmitting(true);
|
|
332
|
+
const payload = {
|
|
333
|
+
schemaVersion: SCHEMA_VERSION,
|
|
334
|
+
id: generateId(),
|
|
335
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
336
|
+
submittedBy: currentUser,
|
|
337
|
+
comment: comment.trim(),
|
|
338
|
+
sentiment: deps.getSentiment(),
|
|
339
|
+
pins: deps.getPendingPins().map((p, i) => ({ ...p, index: i + 1 })),
|
|
340
|
+
page: getPageMeta(),
|
|
341
|
+
viewport: getViewportMeta(),
|
|
342
|
+
device: getDeviceMeta()
|
|
343
|
+
};
|
|
344
|
+
try {
|
|
345
|
+
await adapter.submit(payload);
|
|
346
|
+
deps.setFeedbackItems((prev) => [
|
|
347
|
+
...prev,
|
|
348
|
+
{ payload, replies: [], resolutions: [], archives: [] }
|
|
349
|
+
]);
|
|
350
|
+
deps.deactivatePinMode();
|
|
351
|
+
} catch (err) {
|
|
352
|
+
onError?.(err);
|
|
353
|
+
} finally {
|
|
354
|
+
deps.setSubmitting(false);
|
|
355
|
+
}
|
|
356
|
+
}, [adapter, currentUser, onError, deps]);
|
|
357
|
+
const handleReply = useCallback2(
|
|
358
|
+
async (feedbackId) => {
|
|
359
|
+
const body = deps.getReplyBody().trim();
|
|
360
|
+
if (!body) return;
|
|
361
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
362
|
+
try {
|
|
363
|
+
await adapter.reply(feedbackId, {
|
|
364
|
+
author: currentUser,
|
|
365
|
+
body,
|
|
366
|
+
timestamp: ts
|
|
367
|
+
});
|
|
368
|
+
updateItem(feedbackId, (item) => {
|
|
369
|
+
const updated = {
|
|
370
|
+
...item,
|
|
371
|
+
replies: [
|
|
372
|
+
...item.replies,
|
|
373
|
+
{
|
|
374
|
+
id: generateId(),
|
|
375
|
+
feedbackId,
|
|
376
|
+
author: currentUser,
|
|
377
|
+
body,
|
|
378
|
+
timestamp: ts
|
|
379
|
+
}
|
|
380
|
+
]
|
|
381
|
+
};
|
|
382
|
+
const rescope = deps.shouldRescope?.(currentUser.role) ?? currentUser.role !== "agent";
|
|
383
|
+
if (rescope && updated.resolutions.length === 0 && deps.hasFlush) {
|
|
384
|
+
deps.clearFlushed?.(deps.getPathname());
|
|
385
|
+
}
|
|
386
|
+
return updated;
|
|
387
|
+
});
|
|
388
|
+
deps.setReplyBody("");
|
|
389
|
+
} catch (err) {
|
|
390
|
+
onError?.(err);
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
[adapter, currentUser, onError, updateItem, deps]
|
|
394
|
+
);
|
|
395
|
+
const handleResolve = useCallback2(
|
|
396
|
+
async (feedbackId) => {
|
|
397
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
398
|
+
try {
|
|
399
|
+
await adapter.resolve(feedbackId, {
|
|
400
|
+
resolvedBy: currentUser,
|
|
401
|
+
timestamp: ts
|
|
402
|
+
});
|
|
403
|
+
updateItem(feedbackId, (item) => ({
|
|
404
|
+
...item,
|
|
405
|
+
resolutions: [
|
|
406
|
+
...item.resolutions,
|
|
407
|
+
{ feedbackId, resolvedBy: currentUser, timestamp: ts }
|
|
408
|
+
]
|
|
409
|
+
}));
|
|
410
|
+
} catch (err) {
|
|
411
|
+
onError?.(err);
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
[adapter, currentUser, onError, updateItem]
|
|
415
|
+
);
|
|
416
|
+
const handleArchive = useCallback2(
|
|
417
|
+
async (feedbackId) => {
|
|
418
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
419
|
+
try {
|
|
420
|
+
await adapter.archive(feedbackId, currentUser.id);
|
|
421
|
+
updateItem(feedbackId, (item) => ({
|
|
422
|
+
...item,
|
|
423
|
+
archives: [
|
|
424
|
+
...item.archives,
|
|
425
|
+
{ feedbackId, archivedBy: currentUser, timestamp: ts }
|
|
426
|
+
]
|
|
427
|
+
}));
|
|
428
|
+
deps.setOpenThreadId(null);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
onError?.(err);
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
[adapter, currentUser, onError, updateItem, deps]
|
|
434
|
+
);
|
|
435
|
+
return { handleSubmit, handleReply, handleResolve, handleArchive };
|
|
436
|
+
}
|
|
247
437
|
|
|
248
438
|
// src/react/useFeedtackDom.ts
|
|
249
|
-
import { useEffect, useRef } from "react";
|
|
439
|
+
import { useEffect as useEffect2, useRef } from "react";
|
|
250
440
|
|
|
251
441
|
// src/ui/styles.ts
|
|
252
442
|
var FEEDTACK_DEFAULT_TOKENS = `
|
|
253
|
-
#feedtack-root {
|
|
443
|
+
#feedtack-root, .feedtack-form, .feedtack-thread {
|
|
254
444
|
--ft-primary: #2563eb;
|
|
255
445
|
--ft-primary-hover: #1d4ed8;
|
|
256
446
|
--ft-bg: #ffffff;
|
|
@@ -499,7 +689,7 @@ var FEEDTACK_STYLES = `
|
|
|
499
689
|
// src/react/useFeedtackDom.ts
|
|
500
690
|
function useFeedtackDom(theme, disabled) {
|
|
501
691
|
const rootRef = useRef(null);
|
|
502
|
-
|
|
692
|
+
useEffect2(() => {
|
|
503
693
|
if (disabled) return;
|
|
504
694
|
if (document.getElementById("feedtack-styles")) return;
|
|
505
695
|
const style = document.createElement("style");
|
|
@@ -510,7 +700,7 @@ function useFeedtackDom(theme, disabled) {
|
|
|
510
700
|
style.remove();
|
|
511
701
|
};
|
|
512
702
|
}, [disabled]);
|
|
513
|
-
|
|
703
|
+
useEffect2(() => {
|
|
514
704
|
if (disabled) return;
|
|
515
705
|
const root = document.createElement("div");
|
|
516
706
|
root.id = "feedtack-root";
|
|
@@ -520,7 +710,7 @@ function useFeedtackDom(theme, disabled) {
|
|
|
520
710
|
root.remove();
|
|
521
711
|
};
|
|
522
712
|
}, [disabled]);
|
|
523
|
-
|
|
713
|
+
useEffect2(() => {
|
|
524
714
|
if (disabled) return;
|
|
525
715
|
const root = document.getElementById("feedtack-root");
|
|
526
716
|
if (!root || !theme) return;
|
|
@@ -532,26 +722,86 @@ function useFeedtackDom(theme, disabled) {
|
|
|
532
722
|
return rootRef;
|
|
533
723
|
}
|
|
534
724
|
|
|
725
|
+
// src/react/useFeedtackFlush.ts
|
|
726
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef2 } from "react";
|
|
727
|
+
var DEFAULT_IDLE_MS = 5 * 60 * 1e3;
|
|
728
|
+
function useFeedtackFlush({
|
|
729
|
+
pathname,
|
|
730
|
+
feedbackItems,
|
|
731
|
+
onFlush,
|
|
732
|
+
flushIdleMs = DEFAULT_IDLE_MS,
|
|
733
|
+
disabled
|
|
734
|
+
}) {
|
|
735
|
+
const flushedRef = useRef2(/* @__PURE__ */ new Set());
|
|
736
|
+
const prevPathnameRef = useRef2(pathname);
|
|
737
|
+
const idleTimerRef = useRef2(null);
|
|
738
|
+
const flush = useCallback3(
|
|
739
|
+
(path, items) => {
|
|
740
|
+
if (!onFlush || flushedRef.current.has(path)) return;
|
|
741
|
+
const pageItems = items.filter((i) => i.payload.page.pathname === path);
|
|
742
|
+
if (pageItems.length === 0) return;
|
|
743
|
+
flushedRef.current.add(path);
|
|
744
|
+
onFlush({ pathname: path, items: pageItems });
|
|
745
|
+
},
|
|
746
|
+
[onFlush]
|
|
747
|
+
);
|
|
748
|
+
useEffect3(() => {
|
|
749
|
+
if (disabled || !onFlush) return;
|
|
750
|
+
const prev = prevPathnameRef.current;
|
|
751
|
+
prevPathnameRef.current = pathname;
|
|
752
|
+
if (prev !== pathname) {
|
|
753
|
+
flush(prev, feedbackItems);
|
|
754
|
+
}
|
|
755
|
+
}, [pathname, feedbackItems, flush, onFlush, disabled]);
|
|
756
|
+
useEffect3(() => {
|
|
757
|
+
if (disabled || !onFlush || flushIdleMs <= 0) return;
|
|
758
|
+
const resetTimer = () => {
|
|
759
|
+
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
760
|
+
idleTimerRef.current = setTimeout(() => {
|
|
761
|
+
flush(pathname, feedbackItems);
|
|
762
|
+
}, flushIdleMs);
|
|
763
|
+
};
|
|
764
|
+
const events = ["mousemove", "keydown", "scroll", "touchstart"];
|
|
765
|
+
for (const e of events)
|
|
766
|
+
window.addEventListener(e, resetTimer, { passive: true });
|
|
767
|
+
resetTimer();
|
|
768
|
+
return () => {
|
|
769
|
+
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
770
|
+
for (const e of events) window.removeEventListener(e, resetTimer);
|
|
771
|
+
};
|
|
772
|
+
}, [pathname, feedbackItems, flush, onFlush, flushIdleMs, disabled]);
|
|
773
|
+
useEffect3(() => {
|
|
774
|
+
if (disabled || !onFlush) return;
|
|
775
|
+
const handleUnload = () => flush(pathname, feedbackItems);
|
|
776
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
777
|
+
return () => window.removeEventListener("beforeunload", handleUnload);
|
|
778
|
+
}, [pathname, feedbackItems, flush, onFlush, disabled]);
|
|
779
|
+
const clearFlushed = useCallback3((path) => {
|
|
780
|
+
flushedRef.current.delete(path);
|
|
781
|
+
}, []);
|
|
782
|
+
return { clearFlushed };
|
|
783
|
+
}
|
|
784
|
+
|
|
535
785
|
// src/react/usePinMode.ts
|
|
536
|
-
import { useCallback, useEffect as
|
|
786
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useState as useState2 } from "react";
|
|
537
787
|
function usePinMode({
|
|
538
788
|
hotkey,
|
|
539
789
|
onDeactivate,
|
|
540
790
|
disabled,
|
|
541
791
|
isModalOpen
|
|
542
792
|
}) {
|
|
543
|
-
const [isActive, setIsActive] =
|
|
544
|
-
const [pendingPins, setPendingPins] =
|
|
545
|
-
const [selectedColor, setSelectedColor] =
|
|
546
|
-
const [showForm, setShowForm] =
|
|
547
|
-
const activate =
|
|
548
|
-
const deactivate =
|
|
793
|
+
const [isActive, setIsActive] = useState2(false);
|
|
794
|
+
const [pendingPins, setPendingPins] = useState2([]);
|
|
795
|
+
const [selectedColor, setSelectedColor] = useState2(PIN_PALETTE[0]);
|
|
796
|
+
const [showForm, setShowForm] = useState2(false);
|
|
797
|
+
const activate = useCallback4(() => setIsActive(true), []);
|
|
798
|
+
const deactivate = useCallback4(() => {
|
|
549
799
|
setIsActive(false);
|
|
550
800
|
setPendingPins([]);
|
|
551
801
|
setShowForm(false);
|
|
552
802
|
onDeactivate?.();
|
|
553
803
|
}, [onDeactivate]);
|
|
554
|
-
|
|
804
|
+
useEffect4(() => {
|
|
555
805
|
if (isActive) {
|
|
556
806
|
document.documentElement.classList.add("feedtack-crosshair");
|
|
557
807
|
} else {
|
|
@@ -559,7 +809,7 @@ function usePinMode({
|
|
|
559
809
|
}
|
|
560
810
|
return () => document.documentElement.classList.remove("feedtack-crosshair");
|
|
561
811
|
}, [isActive]);
|
|
562
|
-
|
|
812
|
+
useEffect4(() => {
|
|
563
813
|
if (disabled) return;
|
|
564
814
|
const handler = (e) => {
|
|
565
815
|
if (e.key === hotkey.toUpperCase() && e.shiftKey) {
|
|
@@ -580,31 +830,52 @@ function usePinMode({
|
|
|
580
830
|
window.addEventListener("keydown", handler);
|
|
581
831
|
return () => window.removeEventListener("keydown", handler);
|
|
582
832
|
}, [hotkey, deactivate, isActive, disabled, isModalOpen, showForm]);
|
|
583
|
-
const
|
|
584
|
-
(
|
|
585
|
-
if (!isActive) return;
|
|
586
|
-
const target = e.target;
|
|
833
|
+
const placePin = useCallback4(
|
|
834
|
+
(coords, target) => {
|
|
587
835
|
if (target.closest("#feedtack-root, .feedtack-form, .feedtack-color-picker"))
|
|
588
836
|
return;
|
|
589
|
-
e.preventDefault();
|
|
590
|
-
e.stopPropagation();
|
|
591
837
|
setPendingPins((prev) => [
|
|
592
838
|
...prev,
|
|
593
839
|
{
|
|
594
840
|
color: selectedColor,
|
|
595
|
-
...getPinCoords(
|
|
841
|
+
...getPinCoords(coords),
|
|
596
842
|
target: getTargetMeta(target)
|
|
597
843
|
}
|
|
598
844
|
]);
|
|
599
845
|
setShowForm(true);
|
|
600
846
|
},
|
|
601
|
-
[
|
|
847
|
+
[selectedColor]
|
|
602
848
|
);
|
|
603
|
-
|
|
849
|
+
const handlePageClick = useCallback4(
|
|
850
|
+
(e) => {
|
|
851
|
+
if (!isActive) return;
|
|
852
|
+
e.preventDefault();
|
|
853
|
+
e.stopPropagation();
|
|
854
|
+
placePin(e, e.target);
|
|
855
|
+
},
|
|
856
|
+
[isActive, placePin]
|
|
857
|
+
);
|
|
858
|
+
const handleTouchEnd = useCallback4(
|
|
859
|
+
(e) => {
|
|
860
|
+
if (!isActive) return;
|
|
861
|
+
const touch = e.changedTouches[0];
|
|
862
|
+
if (!touch) return;
|
|
863
|
+
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
864
|
+
if (!target) return;
|
|
865
|
+
e.preventDefault();
|
|
866
|
+
placePin(touch, target);
|
|
867
|
+
},
|
|
868
|
+
[isActive, placePin]
|
|
869
|
+
);
|
|
870
|
+
useEffect4(() => {
|
|
604
871
|
if (disabled) return;
|
|
605
872
|
document.addEventListener("click", handlePageClick, true);
|
|
606
|
-
|
|
607
|
-
|
|
873
|
+
document.addEventListener("touchend", handleTouchEnd, true);
|
|
874
|
+
return () => {
|
|
875
|
+
document.removeEventListener("click", handlePageClick, true);
|
|
876
|
+
document.removeEventListener("touchend", handleTouchEnd, true);
|
|
877
|
+
};
|
|
878
|
+
}, [handlePageClick, handleTouchEnd, disabled]);
|
|
608
879
|
return {
|
|
609
880
|
isActive,
|
|
610
881
|
activate,
|
|
@@ -623,11 +894,16 @@ function useFeedtackState({
|
|
|
623
894
|
hotkey,
|
|
624
895
|
theme,
|
|
625
896
|
onError,
|
|
626
|
-
disabled
|
|
897
|
+
disabled,
|
|
898
|
+
onFlush,
|
|
899
|
+
flushIdleMs,
|
|
900
|
+
rescopeRoles
|
|
627
901
|
}) {
|
|
628
902
|
useFeedtackDom(theme, disabled);
|
|
629
|
-
const [pathname, setPathname] =
|
|
630
|
-
|
|
903
|
+
const [pathname, setPathname] = useState3(
|
|
904
|
+
() => typeof window === "undefined" ? "/" : window.location.pathname
|
|
905
|
+
);
|
|
906
|
+
useEffect5(() => {
|
|
631
907
|
const update = () => setPathname(window.location.pathname);
|
|
632
908
|
const origPush = history.pushState.bind(history);
|
|
633
909
|
const origReplace = history.replaceState.bind(history);
|
|
@@ -646,15 +922,15 @@ function useFeedtackState({
|
|
|
646
922
|
history.replaceState = origReplace;
|
|
647
923
|
};
|
|
648
924
|
}, []);
|
|
649
|
-
const [comment, setComment] =
|
|
650
|
-
const [sentiment, setSentiment] =
|
|
651
|
-
const [commentError, setCommentError] =
|
|
652
|
-
const [submitting, setSubmitting] =
|
|
653
|
-
const [feedbackItems, setFeedbackItems] =
|
|
654
|
-
const [loading, setLoading] =
|
|
655
|
-
const [openThreadId, setOpenThreadId] =
|
|
656
|
-
const [replyBody, setReplyBody] =
|
|
657
|
-
const resetForm =
|
|
925
|
+
const [comment, setComment] = useState3("");
|
|
926
|
+
const [sentiment, setSentiment] = useState3(null);
|
|
927
|
+
const [commentError, setCommentError] = useState3(false);
|
|
928
|
+
const [submitting, setSubmitting] = useState3(false);
|
|
929
|
+
const [feedbackItems, setFeedbackItems] = useState3([]);
|
|
930
|
+
const [loading, setLoading] = useState3(true);
|
|
931
|
+
const [openThreadId, setOpenThreadId] = useState3(null);
|
|
932
|
+
const [replyBody, setReplyBody] = useState3("");
|
|
933
|
+
const resetForm = useCallback5(() => {
|
|
658
934
|
setComment("");
|
|
659
935
|
setSentiment(null);
|
|
660
936
|
setCommentError(false);
|
|
@@ -668,106 +944,44 @@ function useFeedtackState({
|
|
|
668
944
|
setOpenThreadId(null);
|
|
669
945
|
}
|
|
670
946
|
});
|
|
671
|
-
|
|
947
|
+
const { clearFlushed } = useFeedtackFlush({
|
|
948
|
+
pathname,
|
|
949
|
+
feedbackItems,
|
|
950
|
+
onFlush,
|
|
951
|
+
flushIdleMs,
|
|
952
|
+
disabled
|
|
953
|
+
});
|
|
954
|
+
useEffect5(() => {
|
|
672
955
|
setLoading(true);
|
|
673
956
|
adapter.loadFeedback({ pathname }).then(setFeedbackItems).catch((err) => onError?.(err)).finally(() => setLoading(false));
|
|
674
957
|
}, [adapter, onError, pathname]);
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
);
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
pinMode.deactivate();
|
|
703
|
-
} catch (err) {
|
|
704
|
-
onError?.(err);
|
|
705
|
-
} finally {
|
|
706
|
-
setSubmitting(false);
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
const handleReply = async (feedbackId) => {
|
|
710
|
-
if (!replyBody.trim()) return;
|
|
711
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
712
|
-
const body = replyBody.trim();
|
|
713
|
-
try {
|
|
714
|
-
await adapter.reply(feedbackId, {
|
|
715
|
-
author: currentUser,
|
|
716
|
-
body,
|
|
717
|
-
timestamp: ts
|
|
718
|
-
});
|
|
719
|
-
updateItem(feedbackId, (item) => ({
|
|
720
|
-
...item,
|
|
721
|
-
replies: [
|
|
722
|
-
...item.replies,
|
|
723
|
-
{
|
|
724
|
-
id: generateId(),
|
|
725
|
-
feedbackId,
|
|
726
|
-
author: currentUser,
|
|
727
|
-
body,
|
|
728
|
-
timestamp: ts
|
|
729
|
-
}
|
|
730
|
-
]
|
|
731
|
-
}));
|
|
732
|
-
setReplyBody("");
|
|
733
|
-
} catch (err) {
|
|
734
|
-
onError?.(err);
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
const handleResolve = async (feedbackId) => {
|
|
738
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
739
|
-
try {
|
|
740
|
-
await adapter.resolve(feedbackId, {
|
|
741
|
-
resolvedBy: currentUser,
|
|
742
|
-
timestamp: ts
|
|
743
|
-
});
|
|
744
|
-
updateItem(feedbackId, (item) => ({
|
|
745
|
-
...item,
|
|
746
|
-
resolutions: [
|
|
747
|
-
...item.resolutions,
|
|
748
|
-
{ feedbackId, resolvedBy: currentUser, timestamp: ts }
|
|
749
|
-
]
|
|
750
|
-
}));
|
|
751
|
-
} catch (err) {
|
|
752
|
-
onError?.(err);
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
const handleArchive = async (feedbackId) => {
|
|
756
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
757
|
-
try {
|
|
758
|
-
await adapter.archive(feedbackId, currentUser.id);
|
|
759
|
-
updateItem(feedbackId, (item) => ({
|
|
760
|
-
...item,
|
|
761
|
-
archives: [
|
|
762
|
-
...item.archives,
|
|
763
|
-
{ feedbackId, archivedBy: currentUser, timestamp: ts }
|
|
764
|
-
]
|
|
765
|
-
}));
|
|
766
|
-
setOpenThreadId(null);
|
|
767
|
-
} catch (err) {
|
|
768
|
-
onError?.(err);
|
|
769
|
-
}
|
|
770
|
-
};
|
|
958
|
+
const commentRef = () => comment;
|
|
959
|
+
const sentimentRef = () => sentiment;
|
|
960
|
+
const pinsRef = () => pinMode.pendingPins;
|
|
961
|
+
const replyRef = () => replyBody;
|
|
962
|
+
const pathRef = () => pathname;
|
|
963
|
+
const actions = useFeedtackActions({
|
|
964
|
+
adapter,
|
|
965
|
+
currentUser,
|
|
966
|
+
onError,
|
|
967
|
+
getComment: commentRef,
|
|
968
|
+
getSentiment: sentimentRef,
|
|
969
|
+
getPendingPins: pinsRef,
|
|
970
|
+
getReplyBody: replyRef,
|
|
971
|
+
getPathname: pathRef,
|
|
972
|
+
setCommentError,
|
|
973
|
+
setSubmitting,
|
|
974
|
+
setFeedbackItems,
|
|
975
|
+
setReplyBody,
|
|
976
|
+
setOpenThreadId,
|
|
977
|
+
deactivatePinMode: pinMode.deactivate,
|
|
978
|
+
clearFlushed,
|
|
979
|
+
shouldRescope: rescopeRoles ? (role) => rescopeRoles.includes(role) : void 0,
|
|
980
|
+
hasFlush: !!onFlush
|
|
981
|
+
});
|
|
982
|
+
const isArchivedForUser = (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id);
|
|
983
|
+
const hasUnread = (item) => item.replies.length > 0;
|
|
984
|
+
const hasValidPins = (item) => Array.isArray(item.payload?.pins) && item.payload.pins.length > 0;
|
|
771
985
|
return {
|
|
772
986
|
...pinMode,
|
|
773
987
|
isPinModeActive: pinMode.isActive,
|
|
@@ -787,13 +1001,10 @@ function useFeedtackState({
|
|
|
787
1001
|
setOpenThreadId,
|
|
788
1002
|
replyBody,
|
|
789
1003
|
setReplyBody,
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
isArchivedForUser: (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id),
|
|
795
|
-
hasUnread: (item) => item.replies.length > 0,
|
|
796
|
-
hasValidPins: (item) => Array.isArray(item.payload?.pins) && item.payload.pins.length > 0
|
|
1004
|
+
...actions,
|
|
1005
|
+
isArchivedForUser,
|
|
1006
|
+
hasUnread,
|
|
1007
|
+
hasValidPins
|
|
797
1008
|
};
|
|
798
1009
|
}
|
|
799
1010
|
|
|
@@ -810,7 +1021,10 @@ function FeedtackProvider({
|
|
|
810
1021
|
sentimentLabels = {},
|
|
811
1022
|
onError,
|
|
812
1023
|
disabled = false,
|
|
813
|
-
renderPinIcon
|
|
1024
|
+
renderPinIcon,
|
|
1025
|
+
onFlush,
|
|
1026
|
+
flushIdleMs,
|
|
1027
|
+
rescopeRoles
|
|
814
1028
|
}) {
|
|
815
1029
|
const state = useFeedtackState({
|
|
816
1030
|
adapter,
|
|
@@ -818,8 +1032,12 @@ function FeedtackProvider({
|
|
|
818
1032
|
hotkey,
|
|
819
1033
|
theme,
|
|
820
1034
|
onError,
|
|
821
|
-
disabled
|
|
1035
|
+
disabled,
|
|
1036
|
+
onFlush,
|
|
1037
|
+
flushIdleMs,
|
|
1038
|
+
rescopeRoles
|
|
822
1039
|
});
|
|
1040
|
+
const { getPosition } = useAnchoredPins(state.feedbackItems, state.pathname);
|
|
823
1041
|
const firstPin = state.pendingPins[0];
|
|
824
1042
|
const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
|
|
825
1043
|
const showButton = !adminOnly || currentUser.role === "admin";
|
|
@@ -904,6 +1122,7 @@ function FeedtackProvider({
|
|
|
904
1122
|
),
|
|
905
1123
|
!state.loading && state.feedbackItems.filter((item) => item.payload.page.pathname === state.pathname).filter((item) => !state.isArchivedForUser(item)).filter((item) => state.hasValidPins(item)).map((item) => {
|
|
906
1124
|
const pin = item.payload.pins[0];
|
|
1125
|
+
const pos = getPosition(item.payload.id, pin);
|
|
907
1126
|
return /* @__PURE__ */ jsxs3(
|
|
908
1127
|
"button",
|
|
909
1128
|
{
|
|
@@ -915,8 +1134,8 @@ function FeedtackProvider({
|
|
|
915
1134
|
),
|
|
916
1135
|
style: {
|
|
917
1136
|
background: pin.color,
|
|
918
|
-
left:
|
|
919
|
-
top:
|
|
1137
|
+
left: pos.x,
|
|
1138
|
+
top: pos.y,
|
|
920
1139
|
position: "absolute",
|
|
921
1140
|
cursor: "pointer"
|
|
922
1141
|
},
|
|
@@ -949,6 +1168,10 @@ function FeedtackProvider({
|
|
|
949
1168
|
onResolve: () => state.handleResolve(openItem.payload.id),
|
|
950
1169
|
onArchive: () => state.handleArchive(openItem.payload.id),
|
|
951
1170
|
onClose: () => state.setOpenThreadId(null),
|
|
1171
|
+
pinPosition: getPosition(
|
|
1172
|
+
openItem.payload.id,
|
|
1173
|
+
openItem.payload.pins[0]
|
|
1174
|
+
),
|
|
952
1175
|
className: classes.thread
|
|
953
1176
|
}
|
|
954
1177
|
),
|