@emberai-engg/task-board 0.4.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/README.md +7 -2
- package/dist/index.d.mts +36 -4
- package/dist/index.d.ts +36 -4
- package/dist/index.js +447 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +483 -83
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +45 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1525,7 +1525,8 @@ function MentionTextarea({
|
|
|
1525
1525
|
placeholder = "",
|
|
1526
1526
|
rows = 2,
|
|
1527
1527
|
className = "",
|
|
1528
|
-
disabled = false
|
|
1528
|
+
disabled = false,
|
|
1529
|
+
autoFocus = false
|
|
1529
1530
|
}) {
|
|
1530
1531
|
const { service, features } = useTaskBoardContext();
|
|
1531
1532
|
const textareaRef = useRef6(null);
|
|
@@ -1635,7 +1636,8 @@ function MentionTextarea({
|
|
|
1635
1636
|
rows,
|
|
1636
1637
|
className,
|
|
1637
1638
|
placeholder,
|
|
1638
|
-
disabled
|
|
1639
|
+
disabled,
|
|
1640
|
+
autoFocus
|
|
1639
1641
|
}
|
|
1640
1642
|
),
|
|
1641
1643
|
mentionQuery !== null && mentionUsers.length > 0 && /* @__PURE__ */ jsxs11("div", { className: "absolute bottom-full left-0 mb-1 w-64 bg-white border border-neutral-200 rounded-lg shadow-lg z-50 py-1 max-h-48 overflow-y-auto", children: [
|
|
@@ -2427,7 +2429,7 @@ function TaskBoard({
|
|
|
2427
2429
|
}
|
|
2428
2430
|
|
|
2429
2431
|
// src/components/TaskDetailView.tsx
|
|
2430
|
-
import { useCallback as useCallback11, useEffect as
|
|
2432
|
+
import { useCallback as useCallback11, useEffect as useEffect18, useMemo as useMemo3, useRef as useRef16, useState as useState19 } from "react";
|
|
2431
2433
|
|
|
2432
2434
|
// src/utils/threads.ts
|
|
2433
2435
|
function deriveThreads(comments, attachments) {
|
|
@@ -2459,7 +2461,8 @@ function deriveThreads(comments, attachments) {
|
|
|
2459
2461
|
author_name: r.author_name,
|
|
2460
2462
|
content: r.content,
|
|
2461
2463
|
created_at: r.created_at,
|
|
2462
|
-
is_internal: !!r.is_internal
|
|
2464
|
+
is_internal: !!r.is_internal,
|
|
2465
|
+
edited: !!r.edited
|
|
2463
2466
|
}));
|
|
2464
2467
|
const threadAttachments = (c.attachment_ids || []).map((aid) => attachById.get(aid)).filter((a) => Boolean(a));
|
|
2465
2468
|
return {
|
|
@@ -2473,6 +2476,7 @@ function deriveThreads(comments, attachments) {
|
|
|
2473
2476
|
is_internal: !!c.is_internal,
|
|
2474
2477
|
status: c.thread_status === "complete" ? "complete" : "active",
|
|
2475
2478
|
rawContent: c.content,
|
|
2479
|
+
edited: !!c.edited,
|
|
2476
2480
|
anchor: c.anchor || null,
|
|
2477
2481
|
attachments: threadAttachments,
|
|
2478
2482
|
replies
|
|
@@ -2496,7 +2500,7 @@ function timeAgo(dateStr) {
|
|
|
2496
2500
|
}
|
|
2497
2501
|
|
|
2498
2502
|
// src/components/DescriptionSection.tsx
|
|
2499
|
-
import { useState as useState10 } from "react";
|
|
2503
|
+
import { useEffect as useEffect10, useRef as useRef10, useState as useState10 } from "react";
|
|
2500
2504
|
|
|
2501
2505
|
// src/components/MarkdownView.tsx
|
|
2502
2506
|
import React10 from "react";
|
|
@@ -2921,10 +2925,13 @@ function DescriptionSection({
|
|
|
2921
2925
|
onChange,
|
|
2922
2926
|
status,
|
|
2923
2927
|
onStatusChange,
|
|
2924
|
-
saving
|
|
2928
|
+
saving,
|
|
2929
|
+
anchors,
|
|
2930
|
+
onAnchorClick
|
|
2925
2931
|
}) {
|
|
2926
2932
|
const [editing, setEditing] = useState10(false);
|
|
2927
2933
|
const [openHelp, setOpenHelp] = useState10(null);
|
|
2934
|
+
const readRef = useRef10(null);
|
|
2928
2935
|
const startEdit = () => setEditing(true);
|
|
2929
2936
|
const cancel = () => setEditing(false);
|
|
2930
2937
|
const handleCommit = (md) => {
|
|
@@ -2932,6 +2939,52 @@ function DescriptionSection({
|
|
|
2932
2939
|
setEditing(false);
|
|
2933
2940
|
};
|
|
2934
2941
|
const hasContent = value.trim().length > 0;
|
|
2942
|
+
useEffect10(() => {
|
|
2943
|
+
if (editing) return;
|
|
2944
|
+
const root = readRef.current;
|
|
2945
|
+
if (!root) return;
|
|
2946
|
+
root.querySelectorAll("mark.eb-tb-annot").forEach((m) => {
|
|
2947
|
+
const text = m.textContent || "";
|
|
2948
|
+
m.replaceWith(document.createTextNode(text));
|
|
2949
|
+
});
|
|
2950
|
+
root.normalize();
|
|
2951
|
+
if (!anchors || anchors.length === 0) return;
|
|
2952
|
+
for (const anchor of anchors) {
|
|
2953
|
+
const snippet = anchor.snippet?.trim();
|
|
2954
|
+
if (!snippet) continue;
|
|
2955
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
2956
|
+
let node = walker.nextNode();
|
|
2957
|
+
while (node) {
|
|
2958
|
+
const txt = node.textContent || "";
|
|
2959
|
+
const idx = txt.indexOf(snippet);
|
|
2960
|
+
if (idx !== -1) {
|
|
2961
|
+
const range = document.createRange();
|
|
2962
|
+
try {
|
|
2963
|
+
range.setStart(node, idx);
|
|
2964
|
+
range.setEnd(node, idx + snippet.length);
|
|
2965
|
+
const mark = document.createElement("mark");
|
|
2966
|
+
mark.className = "eb-tb-annot";
|
|
2967
|
+
mark.dataset.threadId = anchor.threadId;
|
|
2968
|
+
range.surroundContents(mark);
|
|
2969
|
+
} catch {
|
|
2970
|
+
}
|
|
2971
|
+
break;
|
|
2972
|
+
}
|
|
2973
|
+
node = walker.nextNode();
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
}, [anchors, value, editing]);
|
|
2977
|
+
const handleReadClick = (e) => {
|
|
2978
|
+
const target = e.target;
|
|
2979
|
+
const mark = target?.closest("mark.eb-tb-annot");
|
|
2980
|
+
if (mark && mark.dataset.threadId && onAnchorClick) {
|
|
2981
|
+
e.preventDefault();
|
|
2982
|
+
e.stopPropagation();
|
|
2983
|
+
onAnchorClick(mark.dataset.threadId);
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
startEdit();
|
|
2987
|
+
};
|
|
2935
2988
|
return /* @__PURE__ */ jsxs16("section", { className: "group", "data-section": sectionKey, children: [
|
|
2936
2989
|
/* @__PURE__ */ jsxs16("div", { className: "flex items-center justify-between gap-3 mb-3", children: [
|
|
2937
2990
|
/* @__PURE__ */ jsx18("h2", { className: "text-[15px] font-semibold text-neutral-900 tracking-tight", children: label }),
|
|
@@ -2985,11 +3038,19 @@ function DescriptionSection({
|
|
|
2985
3038
|
saving
|
|
2986
3039
|
}
|
|
2987
3040
|
) : /* @__PURE__ */ jsx18(
|
|
2988
|
-
"
|
|
3041
|
+
"div",
|
|
2989
3042
|
{
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3043
|
+
ref: readRef,
|
|
3044
|
+
role: "button",
|
|
3045
|
+
tabIndex: 0,
|
|
3046
|
+
onClick: handleReadClick,
|
|
3047
|
+
onKeyDown: (e) => {
|
|
3048
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
3049
|
+
e.preventDefault();
|
|
3050
|
+
startEdit();
|
|
3051
|
+
}
|
|
3052
|
+
},
|
|
3053
|
+
className: "w-full text-left rounded-lg border border-transparent hover:border-neutral-200 hover:bg-neutral-50/60 transition-colors px-3.5 py-2.5 -mx-3.5 -my-2.5 cursor-text focus:outline-none focus-visible:ring-2 focus-visible:ring-[#FF5E00]/20",
|
|
2993
3054
|
children: hasContent ? /* @__PURE__ */ jsx18(MarkdownView, { content: value, className: "text-[13px]" }) : /* @__PURE__ */ jsx18("p", { className: "text-[13px] text-neutral-400 italic", children: placeholder })
|
|
2994
3055
|
}
|
|
2995
3056
|
)
|
|
@@ -2997,7 +3058,7 @@ function DescriptionSection({
|
|
|
2997
3058
|
}
|
|
2998
3059
|
|
|
2999
3060
|
// src/components/OutstandingQuestionsSection.tsx
|
|
3000
|
-
import { useEffect as
|
|
3061
|
+
import { useEffect as useEffect11, useRef as useRef11, useState as useState11 } from "react";
|
|
3001
3062
|
import { Fragment as Fragment3, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
3002
3063
|
function timeAgo2(dateStr) {
|
|
3003
3064
|
const d = parseDate(dateStr);
|
|
@@ -3024,9 +3085,9 @@ function OutstandingQuestionsSection({
|
|
|
3024
3085
|
const [adding, setAdding] = useState11(false);
|
|
3025
3086
|
const [newText, setNewText] = useState11("");
|
|
3026
3087
|
const [posting, setPosting] = useState11(false);
|
|
3027
|
-
const filterRef =
|
|
3028
|
-
const newRef =
|
|
3029
|
-
|
|
3088
|
+
const filterRef = useRef11(null);
|
|
3089
|
+
const newRef = useRef11(null);
|
|
3090
|
+
useEffect11(() => {
|
|
3030
3091
|
if (!filterMenuOpen) return;
|
|
3031
3092
|
const onClick = (e) => {
|
|
3032
3093
|
if (filterRef.current && !filterRef.current.contains(e.target)) {
|
|
@@ -3318,7 +3379,7 @@ function QuestionItem({
|
|
|
3318
3379
|
}
|
|
3319
3380
|
|
|
3320
3381
|
// src/components/AttachmentsSection.tsx
|
|
3321
|
-
import { useEffect as
|
|
3382
|
+
import { useEffect as useEffect12, useRef as useRef12, useState as useState12 } from "react";
|
|
3322
3383
|
import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3323
3384
|
function formatBytes(bytes) {
|
|
3324
3385
|
if (!bytes && bytes !== 0) return "";
|
|
@@ -3348,10 +3409,10 @@ function AttachmentsSection({
|
|
|
3348
3409
|
const [linkTitle, setLinkTitle] = useState12("");
|
|
3349
3410
|
const [busy, setBusy] = useState12(false);
|
|
3350
3411
|
const [error, setError] = useState12("");
|
|
3351
|
-
const fileInputRef =
|
|
3352
|
-
const imageInputRef =
|
|
3353
|
-
const menuRef =
|
|
3354
|
-
|
|
3412
|
+
const fileInputRef = useRef12(null);
|
|
3413
|
+
const imageInputRef = useRef12(null);
|
|
3414
|
+
const menuRef = useRef12(null);
|
|
3415
|
+
useEffect12(() => {
|
|
3355
3416
|
if (!menuOpen) return;
|
|
3356
3417
|
const onClick = (e) => {
|
|
3357
3418
|
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
@@ -3690,7 +3751,7 @@ function AttachmentsSection({
|
|
|
3690
3751
|
}
|
|
3691
3752
|
|
|
3692
3753
|
// src/components/ThreadsPanel.tsx
|
|
3693
|
-
import { useEffect as
|
|
3754
|
+
import { useEffect as useEffect14, useRef as useRef15, useState as useState15 } from "react";
|
|
3694
3755
|
|
|
3695
3756
|
// src/components/ActivityList.tsx
|
|
3696
3757
|
import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
@@ -3884,7 +3945,7 @@ function ThreadCard({ thread, onOpen, onAnchorClick, shimmer }) {
|
|
|
3884
3945
|
}
|
|
3885
3946
|
|
|
3886
3947
|
// src/components/ThreadComposer.tsx
|
|
3887
|
-
import { useRef as
|
|
3948
|
+
import { useRef as useRef13, useState as useState13 } from "react";
|
|
3888
3949
|
import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
3889
3950
|
function ThreadComposer({
|
|
3890
3951
|
attachments,
|
|
@@ -3989,7 +4050,8 @@ function ThreadComposer({
|
|
|
3989
4050
|
onChange: setBody,
|
|
3990
4051
|
rows: 4,
|
|
3991
4052
|
placeholder: "Write something\u2026 (type @ to mention)",
|
|
3992
|
-
className: "w-full rounded-lg border border-neutral-200 px-3 py-2 text-[12px] text-neutral-800 min-h-[100px] focus:outline-none focus:ring-1 focus:ring-neutral-300 focus:border-neutral-300 resize-none"
|
|
4053
|
+
className: "w-full rounded-lg border border-neutral-200 px-3 py-2 text-[12px] text-neutral-800 min-h-[100px] focus:outline-none focus:ring-1 focus:ring-neutral-300 focus:border-neutral-300 resize-none",
|
|
4054
|
+
autoFocus: true
|
|
3993
4055
|
}
|
|
3994
4056
|
),
|
|
3995
4057
|
/* @__PURE__ */ jsx24(
|
|
@@ -4049,8 +4111,8 @@ function ComposerAttachments({
|
|
|
4049
4111
|
const [linkOpen, setLinkOpen] = useState13(false);
|
|
4050
4112
|
const [linkUrl, setLinkUrl] = useState13("");
|
|
4051
4113
|
const [linkName, setLinkName] = useState13("");
|
|
4052
|
-
const imageRef =
|
|
4053
|
-
const fileRef =
|
|
4114
|
+
const imageRef = useRef13(null);
|
|
4115
|
+
const fileRef = useRef13(null);
|
|
4054
4116
|
const selected = attachmentIds.map((id) => attachments.find((a) => a.id === id)).filter((a) => Boolean(a));
|
|
4055
4117
|
const upload = async (file) => {
|
|
4056
4118
|
setBusy(true);
|
|
@@ -4215,13 +4277,17 @@ function ComposerAttachments({
|
|
|
4215
4277
|
}
|
|
4216
4278
|
|
|
4217
4279
|
// src/components/ThreadDetailView.tsx
|
|
4218
|
-
import { useEffect as
|
|
4280
|
+
import { useEffect as useEffect13, useRef as useRef14, useState as useState14 } from "react";
|
|
4219
4281
|
import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
4220
4282
|
function ThreadDetailView({
|
|
4221
4283
|
thread,
|
|
4284
|
+
currentUsername,
|
|
4222
4285
|
onBack,
|
|
4223
4286
|
onReply,
|
|
4224
4287
|
onUpdateThread,
|
|
4288
|
+
onEditMessage,
|
|
4289
|
+
onDeleteMessage,
|
|
4290
|
+
onDeleteThread,
|
|
4225
4291
|
onAnchorClick,
|
|
4226
4292
|
isInternalUser
|
|
4227
4293
|
}) {
|
|
@@ -4231,10 +4297,29 @@ function ThreadDetailView({
|
|
|
4231
4297
|
const [posting, setPosting] = useState14(false);
|
|
4232
4298
|
const [editingTitle, setEditingTitle] = useState14(false);
|
|
4233
4299
|
const [titleDraft, setTitleDraft] = useState14(thread.title);
|
|
4234
|
-
const
|
|
4235
|
-
|
|
4300
|
+
const [editingMessageId, setEditingMessageId] = useState14(null);
|
|
4301
|
+
const [editDraft, setEditDraft] = useState14("");
|
|
4302
|
+
const [savingEdit, setSavingEdit] = useState14(false);
|
|
4303
|
+
const [headerMenuOpen, setHeaderMenuOpen] = useState14(false);
|
|
4304
|
+
const headerMenuRef = useRef14(null);
|
|
4305
|
+
const messagesEndRef = useRef14(null);
|
|
4306
|
+
useEffect13(() => {
|
|
4236
4307
|
setTitleDraft(thread.title);
|
|
4237
4308
|
}, [thread.title]);
|
|
4309
|
+
useEffect13(() => {
|
|
4310
|
+
setEditingMessageId(null);
|
|
4311
|
+
setEditDraft("");
|
|
4312
|
+
}, [thread.id]);
|
|
4313
|
+
useEffect13(() => {
|
|
4314
|
+
if (!headerMenuOpen) return;
|
|
4315
|
+
const onClick = (e) => {
|
|
4316
|
+
if (headerMenuRef.current && !headerMenuRef.current.contains(e.target)) {
|
|
4317
|
+
setHeaderMenuOpen(false);
|
|
4318
|
+
}
|
|
4319
|
+
};
|
|
4320
|
+
document.addEventListener("mousedown", onClick);
|
|
4321
|
+
return () => document.removeEventListener("mousedown", onClick);
|
|
4322
|
+
}, [headerMenuOpen]);
|
|
4238
4323
|
const submit = async () => {
|
|
4239
4324
|
if (!body.trim()) return;
|
|
4240
4325
|
setPosting(true);
|
|
@@ -4262,7 +4347,40 @@ function ThreadDetailView({
|
|
|
4262
4347
|
const next = thread.status === "complete" ? "active" : "complete";
|
|
4263
4348
|
await onUpdateThread({ thread_status: next });
|
|
4264
4349
|
};
|
|
4350
|
+
const startEditMessage = (id, content) => {
|
|
4351
|
+
setEditingMessageId(id);
|
|
4352
|
+
setEditDraft(content);
|
|
4353
|
+
};
|
|
4354
|
+
const cancelEditMessage = () => {
|
|
4355
|
+
setEditingMessageId(null);
|
|
4356
|
+
setEditDraft("");
|
|
4357
|
+
};
|
|
4358
|
+
const commitEditMessage = async () => {
|
|
4359
|
+
const id = editingMessageId;
|
|
4360
|
+
const next = editDraft.trim();
|
|
4361
|
+
if (!id || !next) {
|
|
4362
|
+
cancelEditMessage();
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
setSavingEdit(true);
|
|
4366
|
+
try {
|
|
4367
|
+
await onEditMessage(id, next);
|
|
4368
|
+
cancelEditMessage();
|
|
4369
|
+
} finally {
|
|
4370
|
+
setSavingEdit(false);
|
|
4371
|
+
}
|
|
4372
|
+
};
|
|
4373
|
+
const handleDeleteMessage = async (id, isThreadRoot) => {
|
|
4374
|
+
const msg = isThreadRoot ? "Delete this thread and all of its replies? This cannot be undone." : "Delete this message? This cannot be undone.";
|
|
4375
|
+
if (!confirm(msg)) return;
|
|
4376
|
+
if (isThreadRoot) {
|
|
4377
|
+
await onDeleteThread(id);
|
|
4378
|
+
} else {
|
|
4379
|
+
await onDeleteMessage(id);
|
|
4380
|
+
}
|
|
4381
|
+
};
|
|
4265
4382
|
const isComplete = thread.status === "complete";
|
|
4383
|
+
const isThreadAuthor = thread.author_id === currentUsername;
|
|
4266
4384
|
return /* @__PURE__ */ jsxs23(Fragment7, { children: [
|
|
4267
4385
|
/* @__PURE__ */ jsxs23("div", { className: "shrink-0 bg-white px-4 py-3 border-b border-neutral-100", children: [
|
|
4268
4386
|
/* @__PURE__ */ jsxs23("div", { className: "flex items-start justify-between gap-4", children: [
|
|
@@ -4277,21 +4395,71 @@ function ThreadDetailView({
|
|
|
4277
4395
|
]
|
|
4278
4396
|
}
|
|
4279
4397
|
),
|
|
4280
|
-
/* @__PURE__ */
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
/* @__PURE__ */
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
/* @__PURE__ */
|
|
4291
|
-
|
|
4398
|
+
/* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-1.5 shrink-0", children: [
|
|
4399
|
+
/* @__PURE__ */ jsx25(
|
|
4400
|
+
"button",
|
|
4401
|
+
{
|
|
4402
|
+
type: "button",
|
|
4403
|
+
onClick: toggleStatus,
|
|
4404
|
+
className: "inline-flex items-center justify-center gap-1.5 text-[12px] font-medium text-neutral-600 hover:text-neutral-900 px-3 h-8 rounded-xl border border-neutral-200 bg-white hover:bg-neutral-50",
|
|
4405
|
+
children: isComplete ? /* @__PURE__ */ jsxs23(Fragment7, { children: [
|
|
4406
|
+
/* @__PURE__ */ jsx25(RotateCcwIcon, { size: 12, strokeWidth: 1.5 }),
|
|
4407
|
+
"Reopen"
|
|
4408
|
+
] }) : /* @__PURE__ */ jsxs23(Fragment7, { children: [
|
|
4409
|
+
/* @__PURE__ */ jsx25(CheckCircle2Icon, { size: 12, strokeWidth: 1.5 }),
|
|
4410
|
+
"Mark complete"
|
|
4411
|
+
] })
|
|
4412
|
+
}
|
|
4413
|
+
),
|
|
4414
|
+
/* @__PURE__ */ jsxs23("div", { ref: headerMenuRef, className: "relative", children: [
|
|
4415
|
+
/* @__PURE__ */ jsx25(
|
|
4416
|
+
"button",
|
|
4417
|
+
{
|
|
4418
|
+
type: "button",
|
|
4419
|
+
onClick: () => setHeaderMenuOpen((v) => !v),
|
|
4420
|
+
"aria-label": "Thread actions",
|
|
4421
|
+
className: "w-8 h-8 flex items-center justify-center rounded-xl border border-neutral-200 bg-white text-neutral-500 hover:text-neutral-900 hover:border-neutral-300 transition-colors",
|
|
4422
|
+
children: /* @__PURE__ */ jsx25(MoreVerticalIcon, { size: 14, strokeWidth: 1.5 })
|
|
4423
|
+
}
|
|
4424
|
+
),
|
|
4425
|
+
headerMenuOpen && /* @__PURE__ */ jsxs23("div", { className: "absolute right-0 top-full mt-1 w-44 bg-white border border-neutral-200 rounded-xl shadow-lg py-1 z-40", children: [
|
|
4426
|
+
isThreadAuthor && /* @__PURE__ */ jsxs23(
|
|
4427
|
+
"button",
|
|
4428
|
+
{
|
|
4429
|
+
type: "button",
|
|
4430
|
+
onClick: () => {
|
|
4431
|
+
setHeaderMenuOpen(false);
|
|
4432
|
+
setEditingTitle(true);
|
|
4433
|
+
},
|
|
4434
|
+
className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2.5",
|
|
4435
|
+
children: [
|
|
4436
|
+
/* @__PURE__ */ jsx25(PencilIcon, { size: 14, strokeWidth: 1.5 }),
|
|
4437
|
+
"Edit title"
|
|
4438
|
+
]
|
|
4439
|
+
}
|
|
4440
|
+
),
|
|
4441
|
+
/* @__PURE__ */ jsxs23(
|
|
4442
|
+
"button",
|
|
4443
|
+
{
|
|
4444
|
+
type: "button",
|
|
4445
|
+
onClick: async () => {
|
|
4446
|
+
setHeaderMenuOpen(false);
|
|
4447
|
+
if (!confirm(
|
|
4448
|
+
"Delete this thread and all of its replies? This cannot be undone."
|
|
4449
|
+
))
|
|
4450
|
+
return;
|
|
4451
|
+
await onDeleteThread(thread.id);
|
|
4452
|
+
},
|
|
4453
|
+
className: "w-full text-left px-3 py-2 text-[12px] text-red-600 hover:bg-red-50 flex items-center gap-2.5",
|
|
4454
|
+
children: [
|
|
4455
|
+
/* @__PURE__ */ jsx25(TrashIcon, { size: 14, strokeWidth: 1.5 }),
|
|
4456
|
+
"Delete thread"
|
|
4457
|
+
]
|
|
4458
|
+
}
|
|
4459
|
+
)
|
|
4292
4460
|
] })
|
|
4293
|
-
}
|
|
4294
|
-
)
|
|
4461
|
+
] })
|
|
4462
|
+
] })
|
|
4295
4463
|
] }),
|
|
4296
4464
|
editingTitle ? /* @__PURE__ */ jsx25(
|
|
4297
4465
|
"input",
|
|
@@ -4333,27 +4501,60 @@ function ThreadDetailView({
|
|
|
4333
4501
|
) })
|
|
4334
4502
|
] }),
|
|
4335
4503
|
/* @__PURE__ */ jsxs23("div", { className: "flex-1 overflow-y-auto p-3 flex flex-col gap-4", children: [
|
|
4336
|
-
/* @__PURE__ */ jsx25(
|
|
4504
|
+
editingMessageId === thread.id ? /* @__PURE__ */ jsx25(
|
|
4505
|
+
ThreadMessageEditor,
|
|
4506
|
+
{
|
|
4507
|
+
authorName: thread.author_name,
|
|
4508
|
+
isInternal: thread.is_internal,
|
|
4509
|
+
value: editDraft,
|
|
4510
|
+
onChange: setEditDraft,
|
|
4511
|
+
onCommit: commitEditMessage,
|
|
4512
|
+
onCancel: cancelEditMessage,
|
|
4513
|
+
saving: savingEdit
|
|
4514
|
+
}
|
|
4515
|
+
) : /* @__PURE__ */ jsx25(
|
|
4337
4516
|
ThreadMessage,
|
|
4338
4517
|
{
|
|
4339
4518
|
authorName: thread.author_name,
|
|
4340
4519
|
createdAt: thread.created_at,
|
|
4341
4520
|
content: thread.rawContent,
|
|
4342
4521
|
isInternal: thread.is_internal,
|
|
4343
|
-
|
|
4522
|
+
edited: thread.edited,
|
|
4523
|
+
isOwn: isThreadAuthor,
|
|
4524
|
+
internalLabel,
|
|
4525
|
+
onEdit: () => startEditMessage(thread.id, thread.rawContent),
|
|
4526
|
+
onDelete: () => handleDeleteMessage(thread.id, true)
|
|
4344
4527
|
}
|
|
4345
4528
|
),
|
|
4346
|
-
thread.replies.map(
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4529
|
+
thread.replies.map(
|
|
4530
|
+
(r) => editingMessageId === r.id ? /* @__PURE__ */ jsx25(
|
|
4531
|
+
ThreadMessageEditor,
|
|
4532
|
+
{
|
|
4533
|
+
authorName: r.author_name,
|
|
4534
|
+
isInternal: r.is_internal,
|
|
4535
|
+
value: editDraft,
|
|
4536
|
+
onChange: setEditDraft,
|
|
4537
|
+
onCommit: commitEditMessage,
|
|
4538
|
+
onCancel: cancelEditMessage,
|
|
4539
|
+
saving: savingEdit
|
|
4540
|
+
},
|
|
4541
|
+
r.id
|
|
4542
|
+
) : /* @__PURE__ */ jsx25(
|
|
4543
|
+
ThreadMessage,
|
|
4544
|
+
{
|
|
4545
|
+
authorName: r.author_name,
|
|
4546
|
+
createdAt: r.created_at,
|
|
4547
|
+
content: r.content,
|
|
4548
|
+
isInternal: r.is_internal,
|
|
4549
|
+
edited: r.edited,
|
|
4550
|
+
isOwn: r.author_id === currentUsername,
|
|
4551
|
+
internalLabel,
|
|
4552
|
+
onEdit: () => startEditMessage(r.id, r.content),
|
|
4553
|
+
onDelete: () => handleDeleteMessage(r.id, false)
|
|
4554
|
+
},
|
|
4555
|
+
r.id
|
|
4556
|
+
)
|
|
4557
|
+
),
|
|
4357
4558
|
/* @__PURE__ */ jsx25("div", { ref: messagesEndRef })
|
|
4358
4559
|
] }),
|
|
4359
4560
|
/* @__PURE__ */ jsxs23("div", { className: "shrink-0 p-3 border-t border-neutral-100 bg-white", children: [
|
|
@@ -4399,10 +4600,27 @@ function ThreadMessage({
|
|
|
4399
4600
|
createdAt,
|
|
4400
4601
|
content,
|
|
4401
4602
|
isInternal,
|
|
4402
|
-
|
|
4603
|
+
edited = false,
|
|
4604
|
+
isOwn = false,
|
|
4605
|
+
internalLabel,
|
|
4606
|
+
onEdit,
|
|
4607
|
+
onDelete
|
|
4403
4608
|
}) {
|
|
4404
4609
|
const initials = getInitials(authorName || "?");
|
|
4405
|
-
|
|
4610
|
+
const [menuOpen, setMenuOpen] = useState14(false);
|
|
4611
|
+
const menuRef = useRef14(null);
|
|
4612
|
+
useEffect13(() => {
|
|
4613
|
+
if (!menuOpen) return;
|
|
4614
|
+
const onClick = (e) => {
|
|
4615
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
4616
|
+
setMenuOpen(false);
|
|
4617
|
+
}
|
|
4618
|
+
};
|
|
4619
|
+
document.addEventListener("mousedown", onClick);
|
|
4620
|
+
return () => document.removeEventListener("mousedown", onClick);
|
|
4621
|
+
}, [menuOpen]);
|
|
4622
|
+
const showActions = isOwn && (onEdit || onDelete);
|
|
4623
|
+
return /* @__PURE__ */ jsxs23("div", { className: "group flex gap-2.5", children: [
|
|
4406
4624
|
/* @__PURE__ */ jsx25(
|
|
4407
4625
|
"div",
|
|
4408
4626
|
{
|
|
@@ -4414,15 +4632,130 @@ function ThreadMessage({
|
|
|
4414
4632
|
/* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2 mb-0.5 flex-wrap", children: [
|
|
4415
4633
|
/* @__PURE__ */ jsx25("span", { className: "text-[13px] font-medium text-neutral-900", children: authorName }),
|
|
4416
4634
|
/* @__PURE__ */ jsx25("span", { className: "text-[10px] text-neutral-400", children: timeAgo(createdAt) }),
|
|
4635
|
+
edited && /* @__PURE__ */ jsx25("span", { className: "text-[10px] text-neutral-400 italic", children: "(edited)" }),
|
|
4417
4636
|
isInternal && /* @__PURE__ */ jsxs23("span", { className: "text-[10px] text-neutral-500 bg-neutral-100 px-1.5 py-0.5 rounded inline-flex items-center gap-1", children: [
|
|
4418
4637
|
/* @__PURE__ */ jsx25(LockIcon, { size: 10, strokeWidth: 1.5 }),
|
|
4419
4638
|
internalLabel
|
|
4420
|
-
] })
|
|
4639
|
+
] }),
|
|
4640
|
+
showActions && /* @__PURE__ */ jsxs23(
|
|
4641
|
+
"div",
|
|
4642
|
+
{
|
|
4643
|
+
ref: menuRef,
|
|
4644
|
+
className: "ml-auto relative opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity",
|
|
4645
|
+
children: [
|
|
4646
|
+
/* @__PURE__ */ jsx25(
|
|
4647
|
+
"button",
|
|
4648
|
+
{
|
|
4649
|
+
type: "button",
|
|
4650
|
+
onClick: () => setMenuOpen((v) => !v),
|
|
4651
|
+
"aria-label": "Message actions",
|
|
4652
|
+
className: "w-6 h-6 flex items-center justify-center rounded text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100",
|
|
4653
|
+
children: /* @__PURE__ */ jsx25(MoreVerticalIcon, { size: 12, strokeWidth: 1.5 })
|
|
4654
|
+
}
|
|
4655
|
+
),
|
|
4656
|
+
menuOpen && /* @__PURE__ */ jsxs23("div", { className: "absolute right-0 top-full mt-1 w-36 bg-white border border-neutral-200 rounded-xl shadow-lg py-1 z-30", children: [
|
|
4657
|
+
onEdit && /* @__PURE__ */ jsxs23(
|
|
4658
|
+
"button",
|
|
4659
|
+
{
|
|
4660
|
+
type: "button",
|
|
4661
|
+
onClick: () => {
|
|
4662
|
+
setMenuOpen(false);
|
|
4663
|
+
onEdit();
|
|
4664
|
+
},
|
|
4665
|
+
className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2",
|
|
4666
|
+
children: [
|
|
4667
|
+
/* @__PURE__ */ jsx25(PencilIcon, { size: 12, strokeWidth: 1.5 }),
|
|
4668
|
+
"Edit"
|
|
4669
|
+
]
|
|
4670
|
+
}
|
|
4671
|
+
),
|
|
4672
|
+
onDelete && /* @__PURE__ */ jsxs23(
|
|
4673
|
+
"button",
|
|
4674
|
+
{
|
|
4675
|
+
type: "button",
|
|
4676
|
+
onClick: () => {
|
|
4677
|
+
setMenuOpen(false);
|
|
4678
|
+
onDelete();
|
|
4679
|
+
},
|
|
4680
|
+
className: "w-full text-left px-3 py-2 text-[12px] text-red-600 hover:bg-red-50 flex items-center gap-2",
|
|
4681
|
+
children: [
|
|
4682
|
+
/* @__PURE__ */ jsx25(TrashIcon, { size: 12, strokeWidth: 1.5 }),
|
|
4683
|
+
"Delete"
|
|
4684
|
+
]
|
|
4685
|
+
}
|
|
4686
|
+
)
|
|
4687
|
+
] })
|
|
4688
|
+
]
|
|
4689
|
+
}
|
|
4690
|
+
)
|
|
4421
4691
|
] }),
|
|
4422
4692
|
/* @__PURE__ */ jsx25("div", { className: "text-[12px] text-neutral-700 leading-relaxed whitespace-pre-wrap", children: /* @__PURE__ */ jsx25(MentionText, { text: content }) })
|
|
4423
4693
|
] })
|
|
4424
4694
|
] });
|
|
4425
4695
|
}
|
|
4696
|
+
function ThreadMessageEditor({
|
|
4697
|
+
authorName,
|
|
4698
|
+
isInternal,
|
|
4699
|
+
value,
|
|
4700
|
+
onChange,
|
|
4701
|
+
onCommit,
|
|
4702
|
+
onCancel,
|
|
4703
|
+
saving
|
|
4704
|
+
}) {
|
|
4705
|
+
const initials = getInitials(authorName || "?");
|
|
4706
|
+
return /* @__PURE__ */ jsxs23("div", { className: "flex gap-2.5", children: [
|
|
4707
|
+
/* @__PURE__ */ jsx25(
|
|
4708
|
+
"div",
|
|
4709
|
+
{
|
|
4710
|
+
className: `w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-semibold shrink-0 ${isInternal ? "bg-neutral-200 text-neutral-700" : "bg-[#FF5E00] text-white"}`,
|
|
4711
|
+
children: initials
|
|
4712
|
+
}
|
|
4713
|
+
),
|
|
4714
|
+
/* @__PURE__ */ jsxs23("div", { className: "flex-1 min-w-0 flex flex-col gap-2", children: [
|
|
4715
|
+
/* @__PURE__ */ jsx25(
|
|
4716
|
+
MentionTextarea,
|
|
4717
|
+
{
|
|
4718
|
+
value,
|
|
4719
|
+
onChange,
|
|
4720
|
+
onKeyDown: (e) => {
|
|
4721
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
4722
|
+
e.preventDefault();
|
|
4723
|
+
onCommit();
|
|
4724
|
+
} else if (e.key === "Escape") {
|
|
4725
|
+
e.preventDefault();
|
|
4726
|
+
onCancel();
|
|
4727
|
+
}
|
|
4728
|
+
},
|
|
4729
|
+
rows: 3,
|
|
4730
|
+
placeholder: "Edit message\u2026",
|
|
4731
|
+
className: "w-full rounded-lg border border-neutral-200 px-3 py-2 text-[12px] text-neutral-800 min-h-[80px] focus:outline-none focus:ring-1 focus:ring-neutral-300 focus:border-neutral-300 resize-none",
|
|
4732
|
+
autoFocus: true
|
|
4733
|
+
}
|
|
4734
|
+
),
|
|
4735
|
+
/* @__PURE__ */ jsxs23("div", { className: "flex items-center justify-end gap-2", children: [
|
|
4736
|
+
/* @__PURE__ */ jsx25(
|
|
4737
|
+
"button",
|
|
4738
|
+
{
|
|
4739
|
+
type: "button",
|
|
4740
|
+
onClick: onCancel,
|
|
4741
|
+
className: "text-[12px] font-medium text-neutral-500 hover:text-neutral-900 px-2 h-7 rounded hover:bg-neutral-50",
|
|
4742
|
+
children: "Cancel"
|
|
4743
|
+
}
|
|
4744
|
+
),
|
|
4745
|
+
/* @__PURE__ */ jsx25(
|
|
4746
|
+
"button",
|
|
4747
|
+
{
|
|
4748
|
+
type: "button",
|
|
4749
|
+
onClick: onCommit,
|
|
4750
|
+
disabled: saving || !value.trim(),
|
|
4751
|
+
className: "inline-flex items-center justify-center px-3 h-7 rounded-lg text-[12px] font-medium text-white bg-[#FF5E00] hover:bg-[#E05200] disabled:opacity-50",
|
|
4752
|
+
children: saving ? "Saving..." : "Save"
|
|
4753
|
+
}
|
|
4754
|
+
)
|
|
4755
|
+
] })
|
|
4756
|
+
] })
|
|
4757
|
+
] });
|
|
4758
|
+
}
|
|
4426
4759
|
|
|
4427
4760
|
// src/components/ThreadsPanel.tsx
|
|
4428
4761
|
import { Fragment as Fragment8, jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
@@ -4430,6 +4763,7 @@ function ThreadsPanel({
|
|
|
4430
4763
|
threads,
|
|
4431
4764
|
activity,
|
|
4432
4765
|
attachments,
|
|
4766
|
+
currentUsername,
|
|
4433
4767
|
open,
|
|
4434
4768
|
onToggle,
|
|
4435
4769
|
openThreadId,
|
|
@@ -4442,6 +4776,9 @@ function ThreadsPanel({
|
|
|
4442
4776
|
onCreateThread,
|
|
4443
4777
|
onCreateReply,
|
|
4444
4778
|
onUpdateThread,
|
|
4779
|
+
onEditMessage,
|
|
4780
|
+
onDeleteMessage,
|
|
4781
|
+
onDeleteThread,
|
|
4445
4782
|
onUploadAttachment,
|
|
4446
4783
|
onAddLinkAttachment
|
|
4447
4784
|
}) {
|
|
@@ -4449,15 +4786,15 @@ function ThreadsPanel({
|
|
|
4449
4786
|
const [composerOpen, setComposerOpen] = useState15(false);
|
|
4450
4787
|
const [filter, setFilter] = useState15("active");
|
|
4451
4788
|
const [filterMenuOpen, setFilterMenuOpen] = useState15(false);
|
|
4452
|
-
const filterRef =
|
|
4453
|
-
|
|
4789
|
+
const filterRef = useRef15(null);
|
|
4790
|
+
useEffect14(() => {
|
|
4454
4791
|
if (pendingAnchor) {
|
|
4455
4792
|
setComposerOpen(true);
|
|
4456
4793
|
setTab("threads");
|
|
4457
4794
|
onOpenThread(null);
|
|
4458
4795
|
}
|
|
4459
4796
|
}, [pendingAnchor, onOpenThread]);
|
|
4460
|
-
|
|
4797
|
+
useEffect14(() => {
|
|
4461
4798
|
if (!filterMenuOpen) return;
|
|
4462
4799
|
const onClick = (e) => {
|
|
4463
4800
|
if (filterRef.current && !filterRef.current.contains(e.target)) {
|
|
@@ -4469,7 +4806,7 @@ function ThreadsPanel({
|
|
|
4469
4806
|
}, [filterMenuOpen]);
|
|
4470
4807
|
const filteredThreads = threads.filter((t) => t.status === filter);
|
|
4471
4808
|
const openThread = openThreadId ? threads.find((t) => t.id === openThreadId) : null;
|
|
4472
|
-
|
|
4809
|
+
useEffect14(() => {
|
|
4473
4810
|
if (openThreadId && !threads.some((t) => t.id === openThreadId)) {
|
|
4474
4811
|
onOpenThread(null);
|
|
4475
4812
|
}
|
|
@@ -4617,9 +4954,13 @@ function ThreadsPanel({
|
|
|
4617
4954
|
ThreadDetailView,
|
|
4618
4955
|
{
|
|
4619
4956
|
thread: openThread,
|
|
4957
|
+
currentUsername,
|
|
4620
4958
|
onBack: () => onOpenThread(null),
|
|
4621
4959
|
onReply: (content, isInternal) => onCreateReply(openThread.id, content, isInternal),
|
|
4622
4960
|
onUpdateThread: (body) => onUpdateThread(openThread.id, body),
|
|
4961
|
+
onEditMessage,
|
|
4962
|
+
onDeleteMessage,
|
|
4963
|
+
onDeleteThread,
|
|
4623
4964
|
onAnchorClick,
|
|
4624
4965
|
isInternalUser
|
|
4625
4966
|
}
|
|
@@ -4661,7 +5002,7 @@ function HighlightBubble({ bubble, onComment }) {
|
|
|
4661
5002
|
}
|
|
4662
5003
|
|
|
4663
5004
|
// src/hooks/useTaskQuestions.ts
|
|
4664
|
-
import { useCallback as useCallback8, useEffect as
|
|
5005
|
+
import { useCallback as useCallback8, useEffect as useEffect15, useState as useState16 } from "react";
|
|
4665
5006
|
function useTaskQuestions(taskId, initial) {
|
|
4666
5007
|
const { service } = useTaskBoardContext();
|
|
4667
5008
|
const [questions, setQuestions] = useState16(initial ?? []);
|
|
@@ -4676,7 +5017,7 @@ function useTaskQuestions(taskId, initial) {
|
|
|
4676
5017
|
setLoading(false);
|
|
4677
5018
|
}
|
|
4678
5019
|
}, [service, taskId]);
|
|
4679
|
-
|
|
5020
|
+
useEffect15(() => {
|
|
4680
5021
|
if (initial !== void 0) return;
|
|
4681
5022
|
if (!taskId) return;
|
|
4682
5023
|
refresh();
|
|
@@ -4744,7 +5085,7 @@ function useTaskQuestions(taskId, initial) {
|
|
|
4744
5085
|
}
|
|
4745
5086
|
|
|
4746
5087
|
// src/hooks/useTaskAttachments.ts
|
|
4747
|
-
import { useCallback as useCallback9, useEffect as
|
|
5088
|
+
import { useCallback as useCallback9, useEffect as useEffect16, useState as useState17 } from "react";
|
|
4748
5089
|
function useTaskAttachments(taskId, initial) {
|
|
4749
5090
|
const { service } = useTaskBoardContext();
|
|
4750
5091
|
const [attachments, setAttachments] = useState17(initial ?? []);
|
|
@@ -4759,7 +5100,7 @@ function useTaskAttachments(taskId, initial) {
|
|
|
4759
5100
|
setLoading(false);
|
|
4760
5101
|
}
|
|
4761
5102
|
}, [service, taskId]);
|
|
4762
|
-
|
|
5103
|
+
useEffect16(() => {
|
|
4763
5104
|
if (initial !== void 0) return;
|
|
4764
5105
|
if (!taskId) return;
|
|
4765
5106
|
refresh();
|
|
@@ -4794,11 +5135,11 @@ function useTaskAttachments(taskId, initial) {
|
|
|
4794
5135
|
}
|
|
4795
5136
|
|
|
4796
5137
|
// src/hooks/useHighlightAnchor.ts
|
|
4797
|
-
import { useCallback as useCallback10, useEffect as
|
|
5138
|
+
import { useCallback as useCallback10, useEffect as useEffect17, useState as useState18 } from "react";
|
|
4798
5139
|
function useHighlightAnchor() {
|
|
4799
5140
|
const [bubble, setBubble] = useState18(null);
|
|
4800
5141
|
const [pendingAnchor, setPendingAnchor] = useState18(null);
|
|
4801
|
-
|
|
5142
|
+
useEffect17(() => {
|
|
4802
5143
|
const onMouseUp = () => {
|
|
4803
5144
|
const sel = window.getSelection();
|
|
4804
5145
|
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) {
|
|
@@ -4856,13 +5197,21 @@ function useHighlightAnchor() {
|
|
|
4856
5197
|
return anchor;
|
|
4857
5198
|
}, [bubble]);
|
|
4858
5199
|
const focusAnchor = useCallback10((anchor) => {
|
|
4859
|
-
const
|
|
4860
|
-
if (!
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
5200
|
+
const sectionEl = document.querySelector(`[data-section="${anchor.section}"]`);
|
|
5201
|
+
if (!sectionEl) return;
|
|
5202
|
+
sectionEl.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
5203
|
+
const markEl = Array.from(sectionEl.querySelectorAll("mark.eb-tb-annot")).find(
|
|
5204
|
+
(m) => (m.textContent || "").includes(anchor.snippet)
|
|
5205
|
+
) || sectionEl.querySelector("mark.eb-tb-annot");
|
|
5206
|
+
if (markEl) {
|
|
5207
|
+
markEl.classList.add("eb-tb-annot-pulse");
|
|
5208
|
+
setTimeout(() => markEl.classList.remove("eb-tb-annot-pulse"), 1500);
|
|
5209
|
+
} else {
|
|
5210
|
+
sectionEl.classList.add("ring-2", "ring-amber-300", "rounded-lg", "transition-all");
|
|
5211
|
+
setTimeout(() => {
|
|
5212
|
+
sectionEl.classList.remove("ring-2", "ring-amber-300", "rounded-lg");
|
|
5213
|
+
}, 1500);
|
|
5214
|
+
}
|
|
4866
5215
|
}, []);
|
|
4867
5216
|
return {
|
|
4868
5217
|
bubble,
|
|
@@ -4908,10 +5257,10 @@ function TaskDetailView({
|
|
|
4908
5257
|
const [pendingTags, setPendingTags] = useState19([]);
|
|
4909
5258
|
const [showOtherTagInput, setShowOtherTagInput] = useState19(false);
|
|
4910
5259
|
const [linkCopied, setLinkCopied] = useState19(false);
|
|
4911
|
-
const statusRef =
|
|
4912
|
-
const priorityRef =
|
|
4913
|
-
const moreRef =
|
|
4914
|
-
const tagsRef =
|
|
5260
|
+
const statusRef = useRef16(null);
|
|
5261
|
+
const priorityRef = useRef16(null);
|
|
5262
|
+
const moreRef = useRef16(null);
|
|
5263
|
+
const tagsRef = useRef16(null);
|
|
4915
5264
|
const [projectTaskIds, setProjectTaskIds] = useState19([]);
|
|
4916
5265
|
const [openThreadId, setOpenThreadId] = useState19(null);
|
|
4917
5266
|
const [shimmeringThreadIds, setShimmeringThreadIds] = useState19(/* @__PURE__ */ new Set());
|
|
@@ -4955,10 +5304,10 @@ function TaskDetailView({
|
|
|
4955
5304
|
setLoading(false);
|
|
4956
5305
|
}
|
|
4957
5306
|
}, [service, taskId]);
|
|
4958
|
-
|
|
5307
|
+
useEffect18(() => {
|
|
4959
5308
|
fetchTask();
|
|
4960
5309
|
}, [fetchTask]);
|
|
4961
|
-
|
|
5310
|
+
useEffect18(() => {
|
|
4962
5311
|
if (!task || !onNavigateToTask) return;
|
|
4963
5312
|
let cancelled = false;
|
|
4964
5313
|
(async () => {
|
|
@@ -4980,7 +5329,7 @@ function TaskDetailView({
|
|
|
4980
5329
|
cancelled = true;
|
|
4981
5330
|
};
|
|
4982
5331
|
}, [task, columns, service, onNavigateToTask]);
|
|
4983
|
-
|
|
5332
|
+
useEffect18(() => {
|
|
4984
5333
|
const onClick = (e) => {
|
|
4985
5334
|
const target = e.target;
|
|
4986
5335
|
if (statusOpen && statusRef.current && !statusRef.current.contains(target)) setStatusOpen(false);
|
|
@@ -4994,7 +5343,7 @@ function TaskDetailView({
|
|
|
4994
5343
|
const questions = useTaskQuestions(taskId, initialQuestions);
|
|
4995
5344
|
const attachments = useTaskAttachments(taskId, initialAttachments);
|
|
4996
5345
|
const highlight = useHighlightAnchor();
|
|
4997
|
-
|
|
5346
|
+
useEffect18(() => {
|
|
4998
5347
|
if (highlight.pendingAnchor && !panelOpen) {
|
|
4999
5348
|
setPanelOpen(true);
|
|
5000
5349
|
try {
|
|
@@ -5118,6 +5467,28 @@ function TaskDetailView({
|
|
|
5118
5467
|
} catch {
|
|
5119
5468
|
}
|
|
5120
5469
|
};
|
|
5470
|
+
const handleEditMessage = async (messageId, content) => {
|
|
5471
|
+
try {
|
|
5472
|
+
await service.editComment(taskId, messageId, { content });
|
|
5473
|
+
await fetchTask();
|
|
5474
|
+
} catch {
|
|
5475
|
+
}
|
|
5476
|
+
};
|
|
5477
|
+
const handleDeleteMessage = async (messageId) => {
|
|
5478
|
+
try {
|
|
5479
|
+
await service.deleteComment(taskId, messageId);
|
|
5480
|
+
await fetchTask();
|
|
5481
|
+
} catch {
|
|
5482
|
+
}
|
|
5483
|
+
};
|
|
5484
|
+
const handleDeleteThread = async (threadId) => {
|
|
5485
|
+
try {
|
|
5486
|
+
await service.deleteComment(taskId, threadId);
|
|
5487
|
+
setOpenThreadId(null);
|
|
5488
|
+
await fetchTask();
|
|
5489
|
+
} catch {
|
|
5490
|
+
}
|
|
5491
|
+
};
|
|
5121
5492
|
const project = useMemo3(() => {
|
|
5122
5493
|
if (!task) return null;
|
|
5123
5494
|
return projects.find((p) => p.slug === task.project_slug) ?? null;
|
|
@@ -5126,6 +5497,29 @@ function TaskDetailView({
|
|
|
5126
5497
|
() => deriveThreads(comments, attachments.attachments),
|
|
5127
5498
|
[comments, attachments.attachments]
|
|
5128
5499
|
);
|
|
5500
|
+
const anchorsBySection = useMemo3(() => {
|
|
5501
|
+
const map = {};
|
|
5502
|
+
for (const t of threads) {
|
|
5503
|
+
if (!t.anchor) continue;
|
|
5504
|
+
const key = t.anchor.section;
|
|
5505
|
+
if (!map[key]) map[key] = [];
|
|
5506
|
+
map[key].push({ snippet: t.anchor.snippet, threadId: t.id });
|
|
5507
|
+
}
|
|
5508
|
+
return map;
|
|
5509
|
+
}, [threads]);
|
|
5510
|
+
const openThreadFromAnchor = useCallback11(
|
|
5511
|
+
(threadId) => {
|
|
5512
|
+
setOpenThreadId(threadId);
|
|
5513
|
+
if (!panelOpen) {
|
|
5514
|
+
setPanelOpen(true);
|
|
5515
|
+
try {
|
|
5516
|
+
window.localStorage.setItem(PANEL_OPEN_KEY, "true");
|
|
5517
|
+
} catch {
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
},
|
|
5521
|
+
[panelOpen]
|
|
5522
|
+
);
|
|
5129
5523
|
const statusCol = columns.find((c) => c.key === taskStatus);
|
|
5130
5524
|
const priorityStyle = getPriorityStyle(priority);
|
|
5131
5525
|
const currentIdx = task ? projectTaskIds.indexOf(task.id) : -1;
|
|
@@ -5501,7 +5895,9 @@ function TaskDetailView({
|
|
|
5501
5895
|
onChange: (v) => handleSectionChange(section.key, v),
|
|
5502
5896
|
status,
|
|
5503
5897
|
onStatusChange: (s) => handleSectionStatusChange(section.key, s),
|
|
5504
|
-
saving
|
|
5898
|
+
saving,
|
|
5899
|
+
anchors: anchorsBySection[section.key],
|
|
5900
|
+
onAnchorClick: openThreadFromAnchor
|
|
5505
5901
|
},
|
|
5506
5902
|
section.key
|
|
5507
5903
|
);
|
|
@@ -5536,6 +5932,7 @@ function TaskDetailView({
|
|
|
5536
5932
|
threads,
|
|
5537
5933
|
activity,
|
|
5538
5934
|
attachments: attachments.attachments,
|
|
5935
|
+
currentUsername: user.username,
|
|
5539
5936
|
open: panelOpen,
|
|
5540
5937
|
onToggle: togglePanel,
|
|
5541
5938
|
openThreadId,
|
|
@@ -5548,6 +5945,9 @@ function TaskDetailView({
|
|
|
5548
5945
|
onCreateThread: handleCreateThread,
|
|
5549
5946
|
onCreateReply: handleCreateReply,
|
|
5550
5947
|
onUpdateThread: handleUpdateThread,
|
|
5948
|
+
onEditMessage: handleEditMessage,
|
|
5949
|
+
onDeleteMessage: handleDeleteMessage,
|
|
5950
|
+
onDeleteThread: handleDeleteThread,
|
|
5551
5951
|
onUploadAttachment: attachments.uploadFile,
|
|
5552
5952
|
onAddLinkAttachment: (url, name) => attachments.addLink({ url, name })
|
|
5553
5953
|
}
|