@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.js
CHANGED
|
@@ -1659,7 +1659,8 @@ function MentionTextarea({
|
|
|
1659
1659
|
placeholder = "",
|
|
1660
1660
|
rows = 2,
|
|
1661
1661
|
className = "",
|
|
1662
|
-
disabled = false
|
|
1662
|
+
disabled = false,
|
|
1663
|
+
autoFocus = false
|
|
1663
1664
|
}) {
|
|
1664
1665
|
const { service, features } = useTaskBoardContext();
|
|
1665
1666
|
const textareaRef = (0, import_react11.useRef)(null);
|
|
@@ -1769,7 +1770,8 @@ function MentionTextarea({
|
|
|
1769
1770
|
rows,
|
|
1770
1771
|
className,
|
|
1771
1772
|
placeholder,
|
|
1772
|
-
disabled
|
|
1773
|
+
disabled,
|
|
1774
|
+
autoFocus
|
|
1773
1775
|
}
|
|
1774
1776
|
),
|
|
1775
1777
|
mentionQuery !== null && mentionUsers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("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: [
|
|
@@ -2593,7 +2595,8 @@ function deriveThreads(comments, attachments) {
|
|
|
2593
2595
|
author_name: r.author_name,
|
|
2594
2596
|
content: r.content,
|
|
2595
2597
|
created_at: r.created_at,
|
|
2596
|
-
is_internal: !!r.is_internal
|
|
2598
|
+
is_internal: !!r.is_internal,
|
|
2599
|
+
edited: !!r.edited
|
|
2597
2600
|
}));
|
|
2598
2601
|
const threadAttachments = (c.attachment_ids || []).map((aid) => attachById.get(aid)).filter((a) => Boolean(a));
|
|
2599
2602
|
return {
|
|
@@ -2607,6 +2610,7 @@ function deriveThreads(comments, attachments) {
|
|
|
2607
2610
|
is_internal: !!c.is_internal,
|
|
2608
2611
|
status: c.thread_status === "complete" ? "complete" : "active",
|
|
2609
2612
|
rawContent: c.content,
|
|
2613
|
+
edited: !!c.edited,
|
|
2610
2614
|
anchor: c.anchor || null,
|
|
2611
2615
|
attachments: threadAttachments,
|
|
2612
2616
|
replies
|
|
@@ -3055,10 +3059,13 @@ function DescriptionSection({
|
|
|
3055
3059
|
onChange,
|
|
3056
3060
|
status,
|
|
3057
3061
|
onStatusChange,
|
|
3058
|
-
saving
|
|
3062
|
+
saving,
|
|
3063
|
+
anchors,
|
|
3064
|
+
onAnchorClick
|
|
3059
3065
|
}) {
|
|
3060
3066
|
const [editing, setEditing] = (0, import_react16.useState)(false);
|
|
3061
3067
|
const [openHelp, setOpenHelp] = (0, import_react16.useState)(null);
|
|
3068
|
+
const readRef = (0, import_react16.useRef)(null);
|
|
3062
3069
|
const startEdit = () => setEditing(true);
|
|
3063
3070
|
const cancel = () => setEditing(false);
|
|
3064
3071
|
const handleCommit = (md) => {
|
|
@@ -3066,6 +3073,52 @@ function DescriptionSection({
|
|
|
3066
3073
|
setEditing(false);
|
|
3067
3074
|
};
|
|
3068
3075
|
const hasContent = value.trim().length > 0;
|
|
3076
|
+
(0, import_react16.useEffect)(() => {
|
|
3077
|
+
if (editing) return;
|
|
3078
|
+
const root = readRef.current;
|
|
3079
|
+
if (!root) return;
|
|
3080
|
+
root.querySelectorAll("mark.eb-tb-annot").forEach((m) => {
|
|
3081
|
+
const text = m.textContent || "";
|
|
3082
|
+
m.replaceWith(document.createTextNode(text));
|
|
3083
|
+
});
|
|
3084
|
+
root.normalize();
|
|
3085
|
+
if (!anchors || anchors.length === 0) return;
|
|
3086
|
+
for (const anchor of anchors) {
|
|
3087
|
+
const snippet = anchor.snippet?.trim();
|
|
3088
|
+
if (!snippet) continue;
|
|
3089
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
3090
|
+
let node = walker.nextNode();
|
|
3091
|
+
while (node) {
|
|
3092
|
+
const txt = node.textContent || "";
|
|
3093
|
+
const idx = txt.indexOf(snippet);
|
|
3094
|
+
if (idx !== -1) {
|
|
3095
|
+
const range = document.createRange();
|
|
3096
|
+
try {
|
|
3097
|
+
range.setStart(node, idx);
|
|
3098
|
+
range.setEnd(node, idx + snippet.length);
|
|
3099
|
+
const mark = document.createElement("mark");
|
|
3100
|
+
mark.className = "eb-tb-annot";
|
|
3101
|
+
mark.dataset.threadId = anchor.threadId;
|
|
3102
|
+
range.surroundContents(mark);
|
|
3103
|
+
} catch {
|
|
3104
|
+
}
|
|
3105
|
+
break;
|
|
3106
|
+
}
|
|
3107
|
+
node = walker.nextNode();
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
}, [anchors, value, editing]);
|
|
3111
|
+
const handleReadClick = (e) => {
|
|
3112
|
+
const target = e.target;
|
|
3113
|
+
const mark = target?.closest("mark.eb-tb-annot");
|
|
3114
|
+
if (mark && mark.dataset.threadId && onAnchorClick) {
|
|
3115
|
+
e.preventDefault();
|
|
3116
|
+
e.stopPropagation();
|
|
3117
|
+
onAnchorClick(mark.dataset.threadId);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
startEdit();
|
|
3121
|
+
};
|
|
3069
3122
|
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { className: "group", "data-section": sectionKey, children: [
|
|
3070
3123
|
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center justify-between gap-3 mb-3", children: [
|
|
3071
3124
|
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h2", { className: "text-[15px] font-semibold text-neutral-900 tracking-tight", children: label }),
|
|
@@ -3119,11 +3172,19 @@ function DescriptionSection({
|
|
|
3119
3172
|
saving
|
|
3120
3173
|
}
|
|
3121
3174
|
) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3122
|
-
"
|
|
3175
|
+
"div",
|
|
3123
3176
|
{
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3177
|
+
ref: readRef,
|
|
3178
|
+
role: "button",
|
|
3179
|
+
tabIndex: 0,
|
|
3180
|
+
onClick: handleReadClick,
|
|
3181
|
+
onKeyDown: (e) => {
|
|
3182
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
3183
|
+
e.preventDefault();
|
|
3184
|
+
startEdit();
|
|
3185
|
+
}
|
|
3186
|
+
},
|
|
3187
|
+
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",
|
|
3127
3188
|
children: hasContent ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MarkdownView, { content: value, className: "text-[13px]" }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[13px] text-neutral-400 italic", children: placeholder })
|
|
3128
3189
|
}
|
|
3129
3190
|
)
|
|
@@ -4123,7 +4184,8 @@ function ThreadComposer({
|
|
|
4123
4184
|
onChange: setBody,
|
|
4124
4185
|
rows: 4,
|
|
4125
4186
|
placeholder: "Write something\u2026 (type @ to mention)",
|
|
4126
|
-
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"
|
|
4187
|
+
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",
|
|
4188
|
+
autoFocus: true
|
|
4127
4189
|
}
|
|
4128
4190
|
),
|
|
4129
4191
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
@@ -4353,9 +4415,13 @@ var import_react20 = require("react");
|
|
|
4353
4415
|
var import_jsx_runtime25 = require("react/jsx-runtime");
|
|
4354
4416
|
function ThreadDetailView({
|
|
4355
4417
|
thread,
|
|
4418
|
+
currentUsername,
|
|
4356
4419
|
onBack,
|
|
4357
4420
|
onReply,
|
|
4358
4421
|
onUpdateThread,
|
|
4422
|
+
onEditMessage,
|
|
4423
|
+
onDeleteMessage,
|
|
4424
|
+
onDeleteThread,
|
|
4359
4425
|
onAnchorClick,
|
|
4360
4426
|
isInternalUser
|
|
4361
4427
|
}) {
|
|
@@ -4365,10 +4431,29 @@ function ThreadDetailView({
|
|
|
4365
4431
|
const [posting, setPosting] = (0, import_react20.useState)(false);
|
|
4366
4432
|
const [editingTitle, setEditingTitle] = (0, import_react20.useState)(false);
|
|
4367
4433
|
const [titleDraft, setTitleDraft] = (0, import_react20.useState)(thread.title);
|
|
4434
|
+
const [editingMessageId, setEditingMessageId] = (0, import_react20.useState)(null);
|
|
4435
|
+
const [editDraft, setEditDraft] = (0, import_react20.useState)("");
|
|
4436
|
+
const [savingEdit, setSavingEdit] = (0, import_react20.useState)(false);
|
|
4437
|
+
const [headerMenuOpen, setHeaderMenuOpen] = (0, import_react20.useState)(false);
|
|
4438
|
+
const headerMenuRef = (0, import_react20.useRef)(null);
|
|
4368
4439
|
const messagesEndRef = (0, import_react20.useRef)(null);
|
|
4369
4440
|
(0, import_react20.useEffect)(() => {
|
|
4370
4441
|
setTitleDraft(thread.title);
|
|
4371
4442
|
}, [thread.title]);
|
|
4443
|
+
(0, import_react20.useEffect)(() => {
|
|
4444
|
+
setEditingMessageId(null);
|
|
4445
|
+
setEditDraft("");
|
|
4446
|
+
}, [thread.id]);
|
|
4447
|
+
(0, import_react20.useEffect)(() => {
|
|
4448
|
+
if (!headerMenuOpen) return;
|
|
4449
|
+
const onClick = (e) => {
|
|
4450
|
+
if (headerMenuRef.current && !headerMenuRef.current.contains(e.target)) {
|
|
4451
|
+
setHeaderMenuOpen(false);
|
|
4452
|
+
}
|
|
4453
|
+
};
|
|
4454
|
+
document.addEventListener("mousedown", onClick);
|
|
4455
|
+
return () => document.removeEventListener("mousedown", onClick);
|
|
4456
|
+
}, [headerMenuOpen]);
|
|
4372
4457
|
const submit = async () => {
|
|
4373
4458
|
if (!body.trim()) return;
|
|
4374
4459
|
setPosting(true);
|
|
@@ -4396,7 +4481,40 @@ function ThreadDetailView({
|
|
|
4396
4481
|
const next = thread.status === "complete" ? "active" : "complete";
|
|
4397
4482
|
await onUpdateThread({ thread_status: next });
|
|
4398
4483
|
};
|
|
4484
|
+
const startEditMessage = (id, content) => {
|
|
4485
|
+
setEditingMessageId(id);
|
|
4486
|
+
setEditDraft(content);
|
|
4487
|
+
};
|
|
4488
|
+
const cancelEditMessage = () => {
|
|
4489
|
+
setEditingMessageId(null);
|
|
4490
|
+
setEditDraft("");
|
|
4491
|
+
};
|
|
4492
|
+
const commitEditMessage = async () => {
|
|
4493
|
+
const id = editingMessageId;
|
|
4494
|
+
const next = editDraft.trim();
|
|
4495
|
+
if (!id || !next) {
|
|
4496
|
+
cancelEditMessage();
|
|
4497
|
+
return;
|
|
4498
|
+
}
|
|
4499
|
+
setSavingEdit(true);
|
|
4500
|
+
try {
|
|
4501
|
+
await onEditMessage(id, next);
|
|
4502
|
+
cancelEditMessage();
|
|
4503
|
+
} finally {
|
|
4504
|
+
setSavingEdit(false);
|
|
4505
|
+
}
|
|
4506
|
+
};
|
|
4507
|
+
const handleDeleteMessage = async (id, isThreadRoot) => {
|
|
4508
|
+
const msg = isThreadRoot ? "Delete this thread and all of its replies? This cannot be undone." : "Delete this message? This cannot be undone.";
|
|
4509
|
+
if (!confirm(msg)) return;
|
|
4510
|
+
if (isThreadRoot) {
|
|
4511
|
+
await onDeleteThread(id);
|
|
4512
|
+
} else {
|
|
4513
|
+
await onDeleteMessage(id);
|
|
4514
|
+
}
|
|
4515
|
+
};
|
|
4399
4516
|
const isComplete = thread.status === "complete";
|
|
4517
|
+
const isThreadAuthor = thread.author_id === currentUsername;
|
|
4400
4518
|
return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
|
|
4401
4519
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "shrink-0 bg-white px-4 py-3 border-b border-neutral-100", children: [
|
|
4402
4520
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex items-start justify-between gap-4", children: [
|
|
@@ -4411,21 +4529,71 @@ function ThreadDetailView({
|
|
|
4411
4529
|
]
|
|
4412
4530
|
}
|
|
4413
4531
|
),
|
|
4414
|
-
/* @__PURE__ */ (0, import_jsx_runtime25.
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
/* @__PURE__ */ (0, import_jsx_runtime25.
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
/* @__PURE__ */ (0, import_jsx_runtime25.
|
|
4425
|
-
|
|
4532
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex items-center gap-1.5 shrink-0", children: [
|
|
4533
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4534
|
+
"button",
|
|
4535
|
+
{
|
|
4536
|
+
type: "button",
|
|
4537
|
+
onClick: toggleStatus,
|
|
4538
|
+
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",
|
|
4539
|
+
children: isComplete ? /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
|
|
4540
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(RotateCcwIcon, { size: 12, strokeWidth: 1.5 }),
|
|
4541
|
+
"Reopen"
|
|
4542
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
|
|
4543
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(CheckCircle2Icon, { size: 12, strokeWidth: 1.5 }),
|
|
4544
|
+
"Mark complete"
|
|
4545
|
+
] })
|
|
4546
|
+
}
|
|
4547
|
+
),
|
|
4548
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { ref: headerMenuRef, className: "relative", children: [
|
|
4549
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4550
|
+
"button",
|
|
4551
|
+
{
|
|
4552
|
+
type: "button",
|
|
4553
|
+
onClick: () => setHeaderMenuOpen((v) => !v),
|
|
4554
|
+
"aria-label": "Thread actions",
|
|
4555
|
+
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",
|
|
4556
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(MoreVerticalIcon, { size: 14, strokeWidth: 1.5 })
|
|
4557
|
+
}
|
|
4558
|
+
),
|
|
4559
|
+
headerMenuOpen && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("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: [
|
|
4560
|
+
isThreadAuthor && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
|
|
4561
|
+
"button",
|
|
4562
|
+
{
|
|
4563
|
+
type: "button",
|
|
4564
|
+
onClick: () => {
|
|
4565
|
+
setHeaderMenuOpen(false);
|
|
4566
|
+
setEditingTitle(true);
|
|
4567
|
+
},
|
|
4568
|
+
className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2.5",
|
|
4569
|
+
children: [
|
|
4570
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(PencilIcon, { size: 14, strokeWidth: 1.5 }),
|
|
4571
|
+
"Edit title"
|
|
4572
|
+
]
|
|
4573
|
+
}
|
|
4574
|
+
),
|
|
4575
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
|
|
4576
|
+
"button",
|
|
4577
|
+
{
|
|
4578
|
+
type: "button",
|
|
4579
|
+
onClick: async () => {
|
|
4580
|
+
setHeaderMenuOpen(false);
|
|
4581
|
+
if (!confirm(
|
|
4582
|
+
"Delete this thread and all of its replies? This cannot be undone."
|
|
4583
|
+
))
|
|
4584
|
+
return;
|
|
4585
|
+
await onDeleteThread(thread.id);
|
|
4586
|
+
},
|
|
4587
|
+
className: "w-full text-left px-3 py-2 text-[12px] text-red-600 hover:bg-red-50 flex items-center gap-2.5",
|
|
4588
|
+
children: [
|
|
4589
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(TrashIcon, { size: 14, strokeWidth: 1.5 }),
|
|
4590
|
+
"Delete thread"
|
|
4591
|
+
]
|
|
4592
|
+
}
|
|
4593
|
+
)
|
|
4426
4594
|
] })
|
|
4427
|
-
}
|
|
4428
|
-
)
|
|
4595
|
+
] })
|
|
4596
|
+
] })
|
|
4429
4597
|
] }),
|
|
4430
4598
|
editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4431
4599
|
"input",
|
|
@@ -4467,27 +4635,60 @@ function ThreadDetailView({
|
|
|
4467
4635
|
) })
|
|
4468
4636
|
] }),
|
|
4469
4637
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex-1 overflow-y-auto p-3 flex flex-col gap-4", children: [
|
|
4470
|
-
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4638
|
+
editingMessageId === thread.id ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4639
|
+
ThreadMessageEditor,
|
|
4640
|
+
{
|
|
4641
|
+
authorName: thread.author_name,
|
|
4642
|
+
isInternal: thread.is_internal,
|
|
4643
|
+
value: editDraft,
|
|
4644
|
+
onChange: setEditDraft,
|
|
4645
|
+
onCommit: commitEditMessage,
|
|
4646
|
+
onCancel: cancelEditMessage,
|
|
4647
|
+
saving: savingEdit
|
|
4648
|
+
}
|
|
4649
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4471
4650
|
ThreadMessage,
|
|
4472
4651
|
{
|
|
4473
4652
|
authorName: thread.author_name,
|
|
4474
4653
|
createdAt: thread.created_at,
|
|
4475
4654
|
content: thread.rawContent,
|
|
4476
4655
|
isInternal: thread.is_internal,
|
|
4477
|
-
|
|
4656
|
+
edited: thread.edited,
|
|
4657
|
+
isOwn: isThreadAuthor,
|
|
4658
|
+
internalLabel,
|
|
4659
|
+
onEdit: () => startEditMessage(thread.id, thread.rawContent),
|
|
4660
|
+
onDelete: () => handleDeleteMessage(thread.id, true)
|
|
4478
4661
|
}
|
|
4479
4662
|
),
|
|
4480
|
-
thread.replies.map(
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4663
|
+
thread.replies.map(
|
|
4664
|
+
(r) => editingMessageId === r.id ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4665
|
+
ThreadMessageEditor,
|
|
4666
|
+
{
|
|
4667
|
+
authorName: r.author_name,
|
|
4668
|
+
isInternal: r.is_internal,
|
|
4669
|
+
value: editDraft,
|
|
4670
|
+
onChange: setEditDraft,
|
|
4671
|
+
onCommit: commitEditMessage,
|
|
4672
|
+
onCancel: cancelEditMessage,
|
|
4673
|
+
saving: savingEdit
|
|
4674
|
+
},
|
|
4675
|
+
r.id
|
|
4676
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4677
|
+
ThreadMessage,
|
|
4678
|
+
{
|
|
4679
|
+
authorName: r.author_name,
|
|
4680
|
+
createdAt: r.created_at,
|
|
4681
|
+
content: r.content,
|
|
4682
|
+
isInternal: r.is_internal,
|
|
4683
|
+
edited: r.edited,
|
|
4684
|
+
isOwn: r.author_id === currentUsername,
|
|
4685
|
+
internalLabel,
|
|
4686
|
+
onEdit: () => startEditMessage(r.id, r.content),
|
|
4687
|
+
onDelete: () => handleDeleteMessage(r.id, false)
|
|
4688
|
+
},
|
|
4689
|
+
r.id
|
|
4690
|
+
)
|
|
4691
|
+
),
|
|
4491
4692
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { ref: messagesEndRef })
|
|
4492
4693
|
] }),
|
|
4493
4694
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "shrink-0 p-3 border-t border-neutral-100 bg-white", children: [
|
|
@@ -4533,10 +4734,27 @@ function ThreadMessage({
|
|
|
4533
4734
|
createdAt,
|
|
4534
4735
|
content,
|
|
4535
4736
|
isInternal,
|
|
4536
|
-
|
|
4737
|
+
edited = false,
|
|
4738
|
+
isOwn = false,
|
|
4739
|
+
internalLabel,
|
|
4740
|
+
onEdit,
|
|
4741
|
+
onDelete
|
|
4537
4742
|
}) {
|
|
4538
4743
|
const initials = getInitials(authorName || "?");
|
|
4539
|
-
|
|
4744
|
+
const [menuOpen, setMenuOpen] = (0, import_react20.useState)(false);
|
|
4745
|
+
const menuRef = (0, import_react20.useRef)(null);
|
|
4746
|
+
(0, import_react20.useEffect)(() => {
|
|
4747
|
+
if (!menuOpen) return;
|
|
4748
|
+
const onClick = (e) => {
|
|
4749
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
4750
|
+
setMenuOpen(false);
|
|
4751
|
+
}
|
|
4752
|
+
};
|
|
4753
|
+
document.addEventListener("mousedown", onClick);
|
|
4754
|
+
return () => document.removeEventListener("mousedown", onClick);
|
|
4755
|
+
}, [menuOpen]);
|
|
4756
|
+
const showActions = isOwn && (onEdit || onDelete);
|
|
4757
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "group flex gap-2.5", children: [
|
|
4540
4758
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4541
4759
|
"div",
|
|
4542
4760
|
{
|
|
@@ -4548,15 +4766,130 @@ function ThreadMessage({
|
|
|
4548
4766
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex items-center gap-2 mb-0.5 flex-wrap", children: [
|
|
4549
4767
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "text-[13px] font-medium text-neutral-900", children: authorName }),
|
|
4550
4768
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "text-[10px] text-neutral-400", children: timeAgo(createdAt) }),
|
|
4769
|
+
edited && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "text-[10px] text-neutral-400 italic", children: "(edited)" }),
|
|
4551
4770
|
isInternal && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { className: "text-[10px] text-neutral-500 bg-neutral-100 px-1.5 py-0.5 rounded inline-flex items-center gap-1", children: [
|
|
4552
4771
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(LockIcon, { size: 10, strokeWidth: 1.5 }),
|
|
4553
4772
|
internalLabel
|
|
4554
|
-
] })
|
|
4773
|
+
] }),
|
|
4774
|
+
showActions && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
|
|
4775
|
+
"div",
|
|
4776
|
+
{
|
|
4777
|
+
ref: menuRef,
|
|
4778
|
+
className: "ml-auto relative opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity",
|
|
4779
|
+
children: [
|
|
4780
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4781
|
+
"button",
|
|
4782
|
+
{
|
|
4783
|
+
type: "button",
|
|
4784
|
+
onClick: () => setMenuOpen((v) => !v),
|
|
4785
|
+
"aria-label": "Message actions",
|
|
4786
|
+
className: "w-6 h-6 flex items-center justify-center rounded text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100",
|
|
4787
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(MoreVerticalIcon, { size: 12, strokeWidth: 1.5 })
|
|
4788
|
+
}
|
|
4789
|
+
),
|
|
4790
|
+
menuOpen && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("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: [
|
|
4791
|
+
onEdit && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
|
|
4792
|
+
"button",
|
|
4793
|
+
{
|
|
4794
|
+
type: "button",
|
|
4795
|
+
onClick: () => {
|
|
4796
|
+
setMenuOpen(false);
|
|
4797
|
+
onEdit();
|
|
4798
|
+
},
|
|
4799
|
+
className: "w-full text-left px-3 py-2 text-[12px] text-neutral-700 hover:bg-neutral-50 flex items-center gap-2",
|
|
4800
|
+
children: [
|
|
4801
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(PencilIcon, { size: 12, strokeWidth: 1.5 }),
|
|
4802
|
+
"Edit"
|
|
4803
|
+
]
|
|
4804
|
+
}
|
|
4805
|
+
),
|
|
4806
|
+
onDelete && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
|
|
4807
|
+
"button",
|
|
4808
|
+
{
|
|
4809
|
+
type: "button",
|
|
4810
|
+
onClick: () => {
|
|
4811
|
+
setMenuOpen(false);
|
|
4812
|
+
onDelete();
|
|
4813
|
+
},
|
|
4814
|
+
className: "w-full text-left px-3 py-2 text-[12px] text-red-600 hover:bg-red-50 flex items-center gap-2",
|
|
4815
|
+
children: [
|
|
4816
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(TrashIcon, { size: 12, strokeWidth: 1.5 }),
|
|
4817
|
+
"Delete"
|
|
4818
|
+
]
|
|
4819
|
+
}
|
|
4820
|
+
)
|
|
4821
|
+
] })
|
|
4822
|
+
]
|
|
4823
|
+
}
|
|
4824
|
+
)
|
|
4555
4825
|
] }),
|
|
4556
4826
|
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "text-[12px] text-neutral-700 leading-relaxed whitespace-pre-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(MentionText, { text: content }) })
|
|
4557
4827
|
] })
|
|
4558
4828
|
] });
|
|
4559
4829
|
}
|
|
4830
|
+
function ThreadMessageEditor({
|
|
4831
|
+
authorName,
|
|
4832
|
+
isInternal,
|
|
4833
|
+
value,
|
|
4834
|
+
onChange,
|
|
4835
|
+
onCommit,
|
|
4836
|
+
onCancel,
|
|
4837
|
+
saving
|
|
4838
|
+
}) {
|
|
4839
|
+
const initials = getInitials(authorName || "?");
|
|
4840
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex gap-2.5", children: [
|
|
4841
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4842
|
+
"div",
|
|
4843
|
+
{
|
|
4844
|
+
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"}`,
|
|
4845
|
+
children: initials
|
|
4846
|
+
}
|
|
4847
|
+
),
|
|
4848
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex-1 min-w-0 flex flex-col gap-2", children: [
|
|
4849
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4850
|
+
MentionTextarea,
|
|
4851
|
+
{
|
|
4852
|
+
value,
|
|
4853
|
+
onChange,
|
|
4854
|
+
onKeyDown: (e) => {
|
|
4855
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
4856
|
+
e.preventDefault();
|
|
4857
|
+
onCommit();
|
|
4858
|
+
} else if (e.key === "Escape") {
|
|
4859
|
+
e.preventDefault();
|
|
4860
|
+
onCancel();
|
|
4861
|
+
}
|
|
4862
|
+
},
|
|
4863
|
+
rows: 3,
|
|
4864
|
+
placeholder: "Edit message\u2026",
|
|
4865
|
+
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",
|
|
4866
|
+
autoFocus: true
|
|
4867
|
+
}
|
|
4868
|
+
),
|
|
4869
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex items-center justify-end gap-2", children: [
|
|
4870
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4871
|
+
"button",
|
|
4872
|
+
{
|
|
4873
|
+
type: "button",
|
|
4874
|
+
onClick: onCancel,
|
|
4875
|
+
className: "text-[12px] font-medium text-neutral-500 hover:text-neutral-900 px-2 h-7 rounded hover:bg-neutral-50",
|
|
4876
|
+
children: "Cancel"
|
|
4877
|
+
}
|
|
4878
|
+
),
|
|
4879
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
4880
|
+
"button",
|
|
4881
|
+
{
|
|
4882
|
+
type: "button",
|
|
4883
|
+
onClick: onCommit,
|
|
4884
|
+
disabled: saving || !value.trim(),
|
|
4885
|
+
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",
|
|
4886
|
+
children: saving ? "Saving..." : "Save"
|
|
4887
|
+
}
|
|
4888
|
+
)
|
|
4889
|
+
] })
|
|
4890
|
+
] })
|
|
4891
|
+
] });
|
|
4892
|
+
}
|
|
4560
4893
|
|
|
4561
4894
|
// src/components/ThreadsPanel.tsx
|
|
4562
4895
|
var import_jsx_runtime26 = require("react/jsx-runtime");
|
|
@@ -4564,6 +4897,7 @@ function ThreadsPanel({
|
|
|
4564
4897
|
threads,
|
|
4565
4898
|
activity,
|
|
4566
4899
|
attachments,
|
|
4900
|
+
currentUsername,
|
|
4567
4901
|
open,
|
|
4568
4902
|
onToggle,
|
|
4569
4903
|
openThreadId,
|
|
@@ -4576,6 +4910,9 @@ function ThreadsPanel({
|
|
|
4576
4910
|
onCreateThread,
|
|
4577
4911
|
onCreateReply,
|
|
4578
4912
|
onUpdateThread,
|
|
4913
|
+
onEditMessage,
|
|
4914
|
+
onDeleteMessage,
|
|
4915
|
+
onDeleteThread,
|
|
4579
4916
|
onUploadAttachment,
|
|
4580
4917
|
onAddLinkAttachment
|
|
4581
4918
|
}) {
|
|
@@ -4751,9 +5088,13 @@ function ThreadsPanel({
|
|
|
4751
5088
|
ThreadDetailView,
|
|
4752
5089
|
{
|
|
4753
5090
|
thread: openThread,
|
|
5091
|
+
currentUsername,
|
|
4754
5092
|
onBack: () => onOpenThread(null),
|
|
4755
5093
|
onReply: (content, isInternal) => onCreateReply(openThread.id, content, isInternal),
|
|
4756
5094
|
onUpdateThread: (body) => onUpdateThread(openThread.id, body),
|
|
5095
|
+
onEditMessage,
|
|
5096
|
+
onDeleteMessage,
|
|
5097
|
+
onDeleteThread,
|
|
4757
5098
|
onAnchorClick,
|
|
4758
5099
|
isInternalUser
|
|
4759
5100
|
}
|
|
@@ -4990,13 +5331,21 @@ function useHighlightAnchor() {
|
|
|
4990
5331
|
return anchor;
|
|
4991
5332
|
}, [bubble]);
|
|
4992
5333
|
const focusAnchor = (0, import_react24.useCallback)((anchor) => {
|
|
4993
|
-
const
|
|
4994
|
-
if (!
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5334
|
+
const sectionEl = document.querySelector(`[data-section="${anchor.section}"]`);
|
|
5335
|
+
if (!sectionEl) return;
|
|
5336
|
+
sectionEl.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
5337
|
+
const markEl = Array.from(sectionEl.querySelectorAll("mark.eb-tb-annot")).find(
|
|
5338
|
+
(m) => (m.textContent || "").includes(anchor.snippet)
|
|
5339
|
+
) || sectionEl.querySelector("mark.eb-tb-annot");
|
|
5340
|
+
if (markEl) {
|
|
5341
|
+
markEl.classList.add("eb-tb-annot-pulse");
|
|
5342
|
+
setTimeout(() => markEl.classList.remove("eb-tb-annot-pulse"), 1500);
|
|
5343
|
+
} else {
|
|
5344
|
+
sectionEl.classList.add("ring-2", "ring-amber-300", "rounded-lg", "transition-all");
|
|
5345
|
+
setTimeout(() => {
|
|
5346
|
+
sectionEl.classList.remove("ring-2", "ring-amber-300", "rounded-lg");
|
|
5347
|
+
}, 1500);
|
|
5348
|
+
}
|
|
5000
5349
|
}, []);
|
|
5001
5350
|
return {
|
|
5002
5351
|
bubble,
|
|
@@ -5252,6 +5601,28 @@ function TaskDetailView({
|
|
|
5252
5601
|
} catch {
|
|
5253
5602
|
}
|
|
5254
5603
|
};
|
|
5604
|
+
const handleEditMessage = async (messageId, content) => {
|
|
5605
|
+
try {
|
|
5606
|
+
await service.editComment(taskId, messageId, { content });
|
|
5607
|
+
await fetchTask();
|
|
5608
|
+
} catch {
|
|
5609
|
+
}
|
|
5610
|
+
};
|
|
5611
|
+
const handleDeleteMessage = async (messageId) => {
|
|
5612
|
+
try {
|
|
5613
|
+
await service.deleteComment(taskId, messageId);
|
|
5614
|
+
await fetchTask();
|
|
5615
|
+
} catch {
|
|
5616
|
+
}
|
|
5617
|
+
};
|
|
5618
|
+
const handleDeleteThread = async (threadId) => {
|
|
5619
|
+
try {
|
|
5620
|
+
await service.deleteComment(taskId, threadId);
|
|
5621
|
+
setOpenThreadId(null);
|
|
5622
|
+
await fetchTask();
|
|
5623
|
+
} catch {
|
|
5624
|
+
}
|
|
5625
|
+
};
|
|
5255
5626
|
const project = (0, import_react25.useMemo)(() => {
|
|
5256
5627
|
if (!task) return null;
|
|
5257
5628
|
return projects.find((p) => p.slug === task.project_slug) ?? null;
|
|
@@ -5260,6 +5631,29 @@ function TaskDetailView({
|
|
|
5260
5631
|
() => deriveThreads(comments, attachments.attachments),
|
|
5261
5632
|
[comments, attachments.attachments]
|
|
5262
5633
|
);
|
|
5634
|
+
const anchorsBySection = (0, import_react25.useMemo)(() => {
|
|
5635
|
+
const map = {};
|
|
5636
|
+
for (const t of threads) {
|
|
5637
|
+
if (!t.anchor) continue;
|
|
5638
|
+
const key = t.anchor.section;
|
|
5639
|
+
if (!map[key]) map[key] = [];
|
|
5640
|
+
map[key].push({ snippet: t.anchor.snippet, threadId: t.id });
|
|
5641
|
+
}
|
|
5642
|
+
return map;
|
|
5643
|
+
}, [threads]);
|
|
5644
|
+
const openThreadFromAnchor = (0, import_react25.useCallback)(
|
|
5645
|
+
(threadId) => {
|
|
5646
|
+
setOpenThreadId(threadId);
|
|
5647
|
+
if (!panelOpen) {
|
|
5648
|
+
setPanelOpen(true);
|
|
5649
|
+
try {
|
|
5650
|
+
window.localStorage.setItem(PANEL_OPEN_KEY, "true");
|
|
5651
|
+
} catch {
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
},
|
|
5655
|
+
[panelOpen]
|
|
5656
|
+
);
|
|
5263
5657
|
const statusCol = columns.find((c) => c.key === taskStatus);
|
|
5264
5658
|
const priorityStyle = getPriorityStyle(priority);
|
|
5265
5659
|
const currentIdx = task ? projectTaskIds.indexOf(task.id) : -1;
|
|
@@ -5635,7 +6029,9 @@ function TaskDetailView({
|
|
|
5635
6029
|
onChange: (v) => handleSectionChange(section.key, v),
|
|
5636
6030
|
status,
|
|
5637
6031
|
onStatusChange: (s) => handleSectionStatusChange(section.key, s),
|
|
5638
|
-
saving
|
|
6032
|
+
saving,
|
|
6033
|
+
anchors: anchorsBySection[section.key],
|
|
6034
|
+
onAnchorClick: openThreadFromAnchor
|
|
5639
6035
|
},
|
|
5640
6036
|
section.key
|
|
5641
6037
|
);
|
|
@@ -5670,6 +6066,7 @@ function TaskDetailView({
|
|
|
5670
6066
|
threads,
|
|
5671
6067
|
activity,
|
|
5672
6068
|
attachments: attachments.attachments,
|
|
6069
|
+
currentUsername: user.username,
|
|
5673
6070
|
open: panelOpen,
|
|
5674
6071
|
onToggle: togglePanel,
|
|
5675
6072
|
openThreadId,
|
|
@@ -5682,6 +6079,9 @@ function TaskDetailView({
|
|
|
5682
6079
|
onCreateThread: handleCreateThread,
|
|
5683
6080
|
onCreateReply: handleCreateReply,
|
|
5684
6081
|
onUpdateThread: handleUpdateThread,
|
|
6082
|
+
onEditMessage: handleEditMessage,
|
|
6083
|
+
onDeleteMessage: handleDeleteMessage,
|
|
6084
|
+
onDeleteThread: handleDeleteThread,
|
|
5685
6085
|
onUploadAttachment: attachments.uploadFile,
|
|
5686
6086
|
onAddLinkAttachment: (url, name) => attachments.addLink({ url, name })
|
|
5687
6087
|
}
|