@ai-matrx/associations 0.3.0 → 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/CHANGELOG.md +116 -0
- package/README.md +3 -3
- package/dist/core/index.cjs +315 -3
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +85 -3
- package/dist/core/index.d.ts +85 -3
- package/dist/core/index.js +315 -3
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +11 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +156 -3
- package/dist/index.d.ts +156 -3
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +1270 -39
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +336 -5
- package/dist/react/index.d.ts +336 -5
- package/dist/react/index.js +1273 -5
- package/dist/react/index.js.map +1 -1
- package/package.json +3 -3
package/dist/react/index.js
CHANGED
|
@@ -13,6 +13,7 @@ function AssociationsProvider({
|
|
|
13
13
|
capture,
|
|
14
14
|
pickerOverrides,
|
|
15
15
|
entityDoors,
|
|
16
|
+
authorDisplay,
|
|
16
17
|
children
|
|
17
18
|
}) {
|
|
18
19
|
const notifierWarned = useRef(false);
|
|
@@ -26,7 +27,8 @@ function AssociationsProvider({
|
|
|
26
27
|
...windowShell !== void 0 ? { windowShell } : {},
|
|
27
28
|
...capture !== void 0 ? { capture } : {},
|
|
28
29
|
...pickerOverrides !== void 0 ? { pickerOverrides } : {},
|
|
29
|
-
...entityDoors !== void 0 ? { entityDoors } : {}
|
|
30
|
+
...entityDoors !== void 0 ? { entityDoors } : {},
|
|
31
|
+
...authorDisplay !== void 0 ? { authorDisplay } : {}
|
|
30
32
|
},
|
|
31
33
|
children
|
|
32
34
|
}
|
|
@@ -503,6 +505,7 @@ var ENTITY_TYPE_TOKENS = [
|
|
|
503
505
|
"platform_actor_session",
|
|
504
506
|
"platform_actor_token",
|
|
505
507
|
"platform_actor_token_event",
|
|
508
|
+
"platform_continued_access",
|
|
506
509
|
"platform_outcome_event",
|
|
507
510
|
"platform_outsider_consumer",
|
|
508
511
|
"platform_saved_view",
|
|
@@ -531,6 +534,7 @@ var ENTITY_TYPE_TOKENS = [
|
|
|
531
534
|
"research_tag",
|
|
532
535
|
"research_template",
|
|
533
536
|
"research_topic",
|
|
537
|
+
"route_manifest_entry",
|
|
534
538
|
"rulebook",
|
|
535
539
|
"sandbox_instance",
|
|
536
540
|
"sch_agent_task",
|
|
@@ -609,6 +613,7 @@ var ENTITY_TYPE_TOKENS = [
|
|
|
609
613
|
"seo_topic",
|
|
610
614
|
"seo_web_analytics_daily",
|
|
611
615
|
"shared_canvas_item",
|
|
616
|
+
"short_link",
|
|
612
617
|
"skill",
|
|
613
618
|
"skill_render_definition",
|
|
614
619
|
"sms_consent",
|
|
@@ -763,6 +768,12 @@ var IDLE_CATEGORIES = Object.freeze({
|
|
|
763
768
|
fetchedAt: null,
|
|
764
769
|
error: null
|
|
765
770
|
});
|
|
771
|
+
var IDLE_COMMENTS = Object.freeze({
|
|
772
|
+
status: "idle",
|
|
773
|
+
comments: Object.freeze([]),
|
|
774
|
+
fetchedAt: null,
|
|
775
|
+
error: null
|
|
776
|
+
});
|
|
766
777
|
|
|
767
778
|
// src/react/hooks/useAssociations.ts
|
|
768
779
|
var noopSubscribe = () => () => {
|
|
@@ -1247,6 +1258,72 @@ function currentUserIdOrNull(store) {
|
|
|
1247
1258
|
}
|
|
1248
1259
|
}
|
|
1249
1260
|
|
|
1261
|
+
// src/react/hooks/useComments.ts
|
|
1262
|
+
import { useEffect as useEffect7, useRef as useRef6, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
1263
|
+
var noopSubscribe3 = () => () => {
|
|
1264
|
+
};
|
|
1265
|
+
function useComments(args) {
|
|
1266
|
+
const { token, id, autoLoad = true } = args;
|
|
1267
|
+
const store = useAssociationsStore();
|
|
1268
|
+
const key = token && id ? associationsKey(token, id) : null;
|
|
1269
|
+
const entry = useSyncExternalStore3(
|
|
1270
|
+
key ? (cb) => store.subscribeComments(key, cb) : noopSubscribe3,
|
|
1271
|
+
() => store.getComments(token, id ?? ""),
|
|
1272
|
+
() => store.getComments(token, id ?? "")
|
|
1273
|
+
);
|
|
1274
|
+
const loadedKey = useRef6(null);
|
|
1275
|
+
useEffect7(() => {
|
|
1276
|
+
if (!autoLoad || !key || !id) return;
|
|
1277
|
+
if (loadedKey.current === key) return;
|
|
1278
|
+
loadedKey.current = key;
|
|
1279
|
+
void store.loadComments(token, id);
|
|
1280
|
+
}, [autoLoad, store, key, token, id]);
|
|
1281
|
+
let currentUserId = null;
|
|
1282
|
+
try {
|
|
1283
|
+
currentUserId = store.identity.requireUserId();
|
|
1284
|
+
} catch {
|
|
1285
|
+
currentUserId = null;
|
|
1286
|
+
}
|
|
1287
|
+
return {
|
|
1288
|
+
comments: entry.comments,
|
|
1289
|
+
status: entry.status,
|
|
1290
|
+
error: entry.error,
|
|
1291
|
+
fetchedAt: entry.fetchedAt,
|
|
1292
|
+
add: async (body, opts) => {
|
|
1293
|
+
if (!id) return { ok: false, error: "Missing entity id" };
|
|
1294
|
+
return store.addComment({
|
|
1295
|
+
entityType: token,
|
|
1296
|
+
entityId: id,
|
|
1297
|
+
body,
|
|
1298
|
+
...opts?.parentId !== void 0 ? { parentId: opts.parentId } : {},
|
|
1299
|
+
...opts?.orgId !== void 0 ? { orgId: opts.orgId } : {}
|
|
1300
|
+
});
|
|
1301
|
+
},
|
|
1302
|
+
edit: async (commentId, body) => {
|
|
1303
|
+
if (!id) return { ok: false, error: "Missing entity id" };
|
|
1304
|
+
return store.editComment({
|
|
1305
|
+
entityType: token,
|
|
1306
|
+
entityId: id,
|
|
1307
|
+
id: commentId,
|
|
1308
|
+
body
|
|
1309
|
+
});
|
|
1310
|
+
},
|
|
1311
|
+
remove: async (commentId) => {
|
|
1312
|
+
if (!id) return { ok: false, error: "Missing entity id" };
|
|
1313
|
+
return store.deleteComment({
|
|
1314
|
+
entityType: token,
|
|
1315
|
+
entityId: id,
|
|
1316
|
+
id: commentId
|
|
1317
|
+
});
|
|
1318
|
+
},
|
|
1319
|
+
reload: async () => {
|
|
1320
|
+
if (!id) return;
|
|
1321
|
+
await store.loadComments(token, id, { force: true });
|
|
1322
|
+
},
|
|
1323
|
+
currentUserId
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1250
1327
|
// src/react/hooks/useAssociationEntitySelect.ts
|
|
1251
1328
|
import { useState as useState5 } from "react";
|
|
1252
1329
|
function useAssociationEntitySelectAdapter(args) {
|
|
@@ -1418,6 +1495,27 @@ function ExternalLinkIcon(props) {
|
|
|
1418
1495
|
/* @__PURE__ */ jsx3("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
|
|
1419
1496
|
] });
|
|
1420
1497
|
}
|
|
1498
|
+
function ChevronDownIcon(props) {
|
|
1499
|
+
return /* @__PURE__ */ jsx3("svg", { ...base(props), children: /* @__PURE__ */ jsx3("path", { d: "m6 9 6 6 6-6" }) });
|
|
1500
|
+
}
|
|
1501
|
+
function ChevronsUpDownIcon(props) {
|
|
1502
|
+
return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
|
|
1503
|
+
/* @__PURE__ */ jsx3("path", { d: "m7 15 5 5 5-5" }),
|
|
1504
|
+
/* @__PURE__ */ jsx3("path", { d: "m7 9 5-5 5 5" })
|
|
1505
|
+
] });
|
|
1506
|
+
}
|
|
1507
|
+
function CornerDownRightIcon(props) {
|
|
1508
|
+
return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
|
|
1509
|
+
/* @__PURE__ */ jsx3("polyline", { points: "15 10 20 15 15 20" }),
|
|
1510
|
+
/* @__PURE__ */ jsx3("path", { d: "M4 4v7a4 4 0 0 0 4 4h12" })
|
|
1511
|
+
] });
|
|
1512
|
+
}
|
|
1513
|
+
function TagIcon(props) {
|
|
1514
|
+
return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
|
|
1515
|
+
/* @__PURE__ */ jsx3("path", { d: "M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" }),
|
|
1516
|
+
/* @__PURE__ */ jsx3("circle", { cx: "7.5", cy: "7.5", r: ".5", fill: "currentColor" })
|
|
1517
|
+
] });
|
|
1518
|
+
}
|
|
1421
1519
|
function DefaultEntityIcon(props) {
|
|
1422
1520
|
return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
|
|
1423
1521
|
/* @__PURE__ */ jsx3("path", { d: "M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" }),
|
|
@@ -1969,7 +2067,7 @@ function UniversalAssociationPicker(props) {
|
|
|
1969
2067
|
}
|
|
1970
2068
|
|
|
1971
2069
|
// src/react/components/AttachedItemsSheet.tsx
|
|
1972
|
-
import { useEffect as
|
|
2070
|
+
import { useEffect as useEffect8, useState as useState8 } from "react";
|
|
1973
2071
|
import { cn as cn5 } from "@ai-matrx/design-system";
|
|
1974
2072
|
|
|
1975
2073
|
// src/react/components/entityDoors.tsx
|
|
@@ -2089,7 +2187,7 @@ function AttachedItemsBody({
|
|
|
2089
2187
|
const [titleError, setTitleError] = useState8(null);
|
|
2090
2188
|
const [busyId, setBusyId] = useState8(null);
|
|
2091
2189
|
const idKey = links.map((l) => l.resourceId).sort().join(",");
|
|
2092
|
-
|
|
2190
|
+
useEffect8(() => {
|
|
2093
2191
|
if (!enabled) return;
|
|
2094
2192
|
const ids = idKey ? idKey.split(",") : [];
|
|
2095
2193
|
if (ids.length === 0) {
|
|
@@ -2774,7 +2872,7 @@ function titleize(token) {
|
|
|
2774
2872
|
}
|
|
2775
2873
|
|
|
2776
2874
|
// src/react/components/AssociationCaptureToolbar.tsx
|
|
2777
|
-
import { useRef as
|
|
2875
|
+
import { useRef as useRef7, useState as useState11 } from "react";
|
|
2778
2876
|
import { Button, cn as cn9, Input as Input3 } from "@ai-matrx/design-system";
|
|
2779
2877
|
import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2780
2878
|
function AssociationCaptureToolbar({
|
|
@@ -2794,7 +2892,7 @@ function AssociationCaptureToolbar({
|
|
|
2794
2892
|
const store = useAssociationsStore();
|
|
2795
2893
|
const { capture } = useAssociationsUiPorts();
|
|
2796
2894
|
const notifier = useNotifier();
|
|
2797
|
-
const fileInputRef =
|
|
2895
|
+
const fileInputRef = useRef7(null);
|
|
2798
2896
|
const [isUploading, setIsUploading] = useState11(false);
|
|
2799
2897
|
const [isDragOver, setIsDragOver] = useState11(false);
|
|
2800
2898
|
const [isPicking, setIsPicking] = useState11(false);
|
|
@@ -3106,11 +3204,1174 @@ function CaptureToolbarAction({
|
|
|
3106
3204
|
}
|
|
3107
3205
|
);
|
|
3108
3206
|
}
|
|
3207
|
+
|
|
3208
|
+
// src/react/components/AssociationEntitySelect.tsx
|
|
3209
|
+
import { useState as useState12 } from "react";
|
|
3210
|
+
import {
|
|
3211
|
+
cn as cn10,
|
|
3212
|
+
Command,
|
|
3213
|
+
CommandEmpty,
|
|
3214
|
+
CommandGroup,
|
|
3215
|
+
CommandInput,
|
|
3216
|
+
CommandItem,
|
|
3217
|
+
CommandList,
|
|
3218
|
+
EditableLabel,
|
|
3219
|
+
Popover,
|
|
3220
|
+
PopoverContent,
|
|
3221
|
+
PopoverTrigger
|
|
3222
|
+
} from "@ai-matrx/design-system";
|
|
3223
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3224
|
+
function AssociationEntitySelect({
|
|
3225
|
+
token,
|
|
3226
|
+
adapter,
|
|
3227
|
+
className,
|
|
3228
|
+
labelClassName,
|
|
3229
|
+
align = "end",
|
|
3230
|
+
showIcon = true,
|
|
3231
|
+
renameActivation = "click",
|
|
3232
|
+
emptyLabel,
|
|
3233
|
+
iconClassName,
|
|
3234
|
+
createSlot
|
|
3235
|
+
}) {
|
|
3236
|
+
const store = useAssociationsStore();
|
|
3237
|
+
const { entityDoors } = useAssociationsUiPorts();
|
|
3238
|
+
const notifier = useNotifier();
|
|
3239
|
+
const info = store.registry.getEntityInfo(token);
|
|
3240
|
+
const InfoIcon = info.Icon ?? DefaultEntityIcon;
|
|
3241
|
+
const DoorControls = entityDoors?.DoorControls;
|
|
3242
|
+
const { items, activeId } = adapter;
|
|
3243
|
+
const active = items.find((i) => i.id === activeId) ?? null;
|
|
3244
|
+
const activeIndex = active ? items.indexOf(active) : -1;
|
|
3245
|
+
const [open, setOpen] = useState12(false);
|
|
3246
|
+
const [query, setQuery] = useState12("");
|
|
3247
|
+
const [creating, setCreating] = useState12(false);
|
|
3248
|
+
const [draftName, setDraftName] = useState12("");
|
|
3249
|
+
const [busy, setBusy] = useState12(false);
|
|
3250
|
+
const entityLabel = info.label.toLowerCase();
|
|
3251
|
+
const close = () => {
|
|
3252
|
+
setOpen(false);
|
|
3253
|
+
setQuery("");
|
|
3254
|
+
setCreating(false);
|
|
3255
|
+
setDraftName("");
|
|
3256
|
+
};
|
|
3257
|
+
const create = async (title) => {
|
|
3258
|
+
const name = title.trim();
|
|
3259
|
+
if (!name || busy || !adapter.createAndAttach) return;
|
|
3260
|
+
setBusy(true);
|
|
3261
|
+
const id = await adapter.createAndAttach(name);
|
|
3262
|
+
setBusy(false);
|
|
3263
|
+
if (id) close();
|
|
3264
|
+
else notifier.error(`Couldn't create the ${entityLabel}`);
|
|
3265
|
+
};
|
|
3266
|
+
return /* @__PURE__ */ jsxs11(
|
|
3267
|
+
"div",
|
|
3268
|
+
{
|
|
3269
|
+
className: cn10(
|
|
3270
|
+
"group/entity-ref flex min-w-0 items-center gap-0.5",
|
|
3271
|
+
className
|
|
3272
|
+
),
|
|
3273
|
+
children: [
|
|
3274
|
+
showIcon ? /* @__PURE__ */ jsx13(
|
|
3275
|
+
InfoIcon,
|
|
3276
|
+
{
|
|
3277
|
+
className: cn10(
|
|
3278
|
+
"size-3.5 shrink-0",
|
|
3279
|
+
iconClassName ?? "text-muted-foreground"
|
|
3280
|
+
),
|
|
3281
|
+
"aria-hidden": true
|
|
3282
|
+
}
|
|
3283
|
+
) : null,
|
|
3284
|
+
active ? /* @__PURE__ */ jsx13(
|
|
3285
|
+
EditableLabel,
|
|
3286
|
+
{
|
|
3287
|
+
value: active.title,
|
|
3288
|
+
onCommit: async (next) => {
|
|
3289
|
+
const ok = await adapter.rename(active.id, next);
|
|
3290
|
+
if (!ok) {
|
|
3291
|
+
notifier.error(`Couldn't rename the ${entityLabel}`);
|
|
3292
|
+
throw new Error("rename failed");
|
|
3293
|
+
}
|
|
3294
|
+
},
|
|
3295
|
+
commitMode: "await",
|
|
3296
|
+
activation: renameActivation,
|
|
3297
|
+
ariaLabel: info.label,
|
|
3298
|
+
className: cn10("max-w-[14rem]", labelClassName),
|
|
3299
|
+
displayClassName: "text-xs font-medium text-foreground",
|
|
3300
|
+
inputClassName: "text-xs font-medium"
|
|
3301
|
+
}
|
|
3302
|
+
) : null,
|
|
3303
|
+
active && DoorControls ? /* @__PURE__ */ jsx13(DoorControls, { token, id: active.id, name: active.title }) : null,
|
|
3304
|
+
!active ? /* @__PURE__ */ jsx13(
|
|
3305
|
+
"span",
|
|
3306
|
+
{
|
|
3307
|
+
className: cn10(
|
|
3308
|
+
"truncate px-1 text-xs font-medium text-muted-foreground",
|
|
3309
|
+
labelClassName
|
|
3310
|
+
),
|
|
3311
|
+
children: adapter.loading ? "\u2026" : emptyLabel ?? info.labelPlural
|
|
3312
|
+
}
|
|
3313
|
+
) : null,
|
|
3314
|
+
/* @__PURE__ */ jsxs11(
|
|
3315
|
+
Popover,
|
|
3316
|
+
{
|
|
3317
|
+
open,
|
|
3318
|
+
onOpenChange: (next) => next ? setOpen(true) : close(),
|
|
3319
|
+
children: [
|
|
3320
|
+
/* @__PURE__ */ jsx13(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs11(
|
|
3321
|
+
"button",
|
|
3322
|
+
{
|
|
3323
|
+
type: "button",
|
|
3324
|
+
className: "inline-flex h-6 shrink-0 items-center gap-0.5 rounded-md px-1 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
|
|
3325
|
+
title: `Switch or add ${info.labelPlural.toLowerCase()}`,
|
|
3326
|
+
"aria-label": `Switch or add ${info.labelPlural.toLowerCase()}`,
|
|
3327
|
+
children: [
|
|
3328
|
+
items.length > 1 ? /* @__PURE__ */ jsxs11("span", { className: "tabular-nums", children: [
|
|
3329
|
+
activeIndex >= 0 ? activeIndex + 1 : "\u2014",
|
|
3330
|
+
"/",
|
|
3331
|
+
items.length
|
|
3332
|
+
] }) : null,
|
|
3333
|
+
/* @__PURE__ */ jsx13(ChevronDownIcon, { className: "size-3 opacity-60" })
|
|
3334
|
+
]
|
|
3335
|
+
}
|
|
3336
|
+
) }),
|
|
3337
|
+
/* @__PURE__ */ jsx13(PopoverContent, { className: "w-60 p-0", align, children: /* @__PURE__ */ jsxs11(Command, { children: [
|
|
3338
|
+
items.length > 5 ? /* @__PURE__ */ jsx13(
|
|
3339
|
+
CommandInput,
|
|
3340
|
+
{
|
|
3341
|
+
value: query,
|
|
3342
|
+
onValueChange: setQuery,
|
|
3343
|
+
placeholder: `Search ${info.labelPlural.toLowerCase()}\u2026`
|
|
3344
|
+
}
|
|
3345
|
+
) : null,
|
|
3346
|
+
/* @__PURE__ */ jsxs11(CommandList, { children: [
|
|
3347
|
+
/* @__PURE__ */ jsx13(CommandEmpty, { children: "No match." }),
|
|
3348
|
+
items.length > 0 ? /* @__PURE__ */ jsx13(CommandGroup, { children: items.map((item) => /* @__PURE__ */ jsxs11(
|
|
3349
|
+
CommandItem,
|
|
3350
|
+
{
|
|
3351
|
+
value: `${item.title} ${item.id}`,
|
|
3352
|
+
onSelect: () => {
|
|
3353
|
+
if (item.id !== activeId)
|
|
3354
|
+
void adapter.setActive(item.id);
|
|
3355
|
+
close();
|
|
3356
|
+
},
|
|
3357
|
+
className: "group/entity-ref group gap-2",
|
|
3358
|
+
children: [
|
|
3359
|
+
/* @__PURE__ */ jsx13(
|
|
3360
|
+
CheckIcon,
|
|
3361
|
+
{
|
|
3362
|
+
className: cn10(
|
|
3363
|
+
"size-3.5 shrink-0",
|
|
3364
|
+
item.id === activeId ? "opacity-100" : "opacity-0"
|
|
3365
|
+
)
|
|
3366
|
+
}
|
|
3367
|
+
),
|
|
3368
|
+
/* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate", children: item.title }),
|
|
3369
|
+
DoorControls ? /* @__PURE__ */ jsx13(
|
|
3370
|
+
"span",
|
|
3371
|
+
{
|
|
3372
|
+
className: "inline-flex shrink-0 items-center",
|
|
3373
|
+
onPointerDown: (e) => {
|
|
3374
|
+
e.preventDefault();
|
|
3375
|
+
e.stopPropagation();
|
|
3376
|
+
},
|
|
3377
|
+
children: /* @__PURE__ */ jsx13(
|
|
3378
|
+
DoorControls,
|
|
3379
|
+
{
|
|
3380
|
+
token,
|
|
3381
|
+
id: item.id,
|
|
3382
|
+
name: item.title
|
|
3383
|
+
}
|
|
3384
|
+
)
|
|
3385
|
+
}
|
|
3386
|
+
) : null,
|
|
3387
|
+
adapter.detach && item.id !== activeId ? /* @__PURE__ */ jsx13(
|
|
3388
|
+
"button",
|
|
3389
|
+
{
|
|
3390
|
+
type: "button",
|
|
3391
|
+
onPointerDown: (e) => {
|
|
3392
|
+
e.preventDefault();
|
|
3393
|
+
e.stopPropagation();
|
|
3394
|
+
},
|
|
3395
|
+
onClick: (e) => {
|
|
3396
|
+
e.stopPropagation();
|
|
3397
|
+
void adapter.detach?.(item.id);
|
|
3398
|
+
},
|
|
3399
|
+
className: "grid size-5 shrink-0 place-items-center rounded text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-destructive group-hover:opacity-100",
|
|
3400
|
+
title: `Remove this ${entityLabel} from here (does not delete it)`,
|
|
3401
|
+
"aria-label": `Remove ${item.title}`,
|
|
3402
|
+
children: /* @__PURE__ */ jsx13(XIcon, { className: "size-3" })
|
|
3403
|
+
}
|
|
3404
|
+
) : null
|
|
3405
|
+
]
|
|
3406
|
+
},
|
|
3407
|
+
item.id
|
|
3408
|
+
)) }) : null
|
|
3409
|
+
] }),
|
|
3410
|
+
createSlot !== void 0 ? /* @__PURE__ */ jsx13("div", { className: "border-t border-border p-1", children: typeof createSlot === "function" ? createSlot(close) : createSlot }) : null,
|
|
3411
|
+
createSlot === void 0 && adapter.createAndAttach ? /* @__PURE__ */ jsx13("div", { className: "border-t border-border p-1", children: creating ? /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1 px-1 py-0.5", children: [
|
|
3412
|
+
/* @__PURE__ */ jsx13(
|
|
3413
|
+
"input",
|
|
3414
|
+
{
|
|
3415
|
+
autoFocus: true,
|
|
3416
|
+
type: "text",
|
|
3417
|
+
value: draftName,
|
|
3418
|
+
disabled: busy,
|
|
3419
|
+
placeholder: `New ${entityLabel} name\u2026`,
|
|
3420
|
+
onChange: (e) => setDraftName(e.target.value),
|
|
3421
|
+
onKeyDown: (e) => {
|
|
3422
|
+
if (e.key === "Enter") {
|
|
3423
|
+
e.preventDefault();
|
|
3424
|
+
void create(draftName);
|
|
3425
|
+
} else if (e.key === "Escape") {
|
|
3426
|
+
e.preventDefault();
|
|
3427
|
+
e.stopPropagation();
|
|
3428
|
+
setCreating(false);
|
|
3429
|
+
setDraftName("");
|
|
3430
|
+
}
|
|
3431
|
+
},
|
|
3432
|
+
className: "h-6 w-full min-w-0 rounded-sm bg-transparent px-1 text-base outline-none focus:bg-background focus:ring-1 focus:ring-ring md:text-xs",
|
|
3433
|
+
"aria-label": `New ${entityLabel} name`
|
|
3434
|
+
}
|
|
3435
|
+
),
|
|
3436
|
+
/* @__PURE__ */ jsx13(
|
|
3437
|
+
"button",
|
|
3438
|
+
{
|
|
3439
|
+
type: "button",
|
|
3440
|
+
disabled: busy || !draftName.trim(),
|
|
3441
|
+
onClick: () => void create(draftName),
|
|
3442
|
+
className: "inline-flex h-6 shrink-0 items-center rounded-md px-1.5 text-[10px] font-medium text-primary transition-colors hover:bg-accent disabled:opacity-50",
|
|
3443
|
+
children: busy ? /* @__PURE__ */ jsx13(SpinnerIcon, { className: "size-3 animate-spin" }) : "Create"
|
|
3444
|
+
}
|
|
3445
|
+
)
|
|
3446
|
+
] }) : /* @__PURE__ */ jsxs11(
|
|
3447
|
+
"button",
|
|
3448
|
+
{
|
|
3449
|
+
type: "button",
|
|
3450
|
+
disabled: busy,
|
|
3451
|
+
onClick: () => {
|
|
3452
|
+
if (query.trim()) void create(query);
|
|
3453
|
+
else setCreating(true);
|
|
3454
|
+
},
|
|
3455
|
+
className: "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
|
|
3456
|
+
children: [
|
|
3457
|
+
busy ? /* @__PURE__ */ jsx13(SpinnerIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx13(PlusIcon, { className: "size-3.5" }),
|
|
3458
|
+
query.trim() ? `Create "${query.trim()}"` : `New ${info.label}`
|
|
3459
|
+
]
|
|
3460
|
+
}
|
|
3461
|
+
) }) : null
|
|
3462
|
+
] }) })
|
|
3463
|
+
]
|
|
3464
|
+
}
|
|
3465
|
+
)
|
|
3466
|
+
]
|
|
3467
|
+
}
|
|
3468
|
+
);
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
// src/react/components/CategorySelect.tsx
|
|
3472
|
+
import { useState as useState13 } from "react";
|
|
3473
|
+
import {
|
|
3474
|
+
CreatablePicker,
|
|
3475
|
+
Select,
|
|
3476
|
+
SelectContent,
|
|
3477
|
+
SelectItem,
|
|
3478
|
+
SelectTrigger,
|
|
3479
|
+
SelectValue
|
|
3480
|
+
} from "@ai-matrx/design-system";
|
|
3481
|
+
|
|
3482
|
+
// src/core/categoryHierarchy.ts
|
|
3483
|
+
function buildCategoryHierarchy(categories) {
|
|
3484
|
+
const byId = new Map(categories.map((category) => [category.id, category]));
|
|
3485
|
+
const hasHierarchy = categories.some(
|
|
3486
|
+
(category) => category.parentId !== null && byId.has(category.parentId)
|
|
3487
|
+
);
|
|
3488
|
+
if (!hasHierarchy) {
|
|
3489
|
+
return {
|
|
3490
|
+
items: categories.map((category) => ({
|
|
3491
|
+
category,
|
|
3492
|
+
depth: 0,
|
|
3493
|
+
parent: null,
|
|
3494
|
+
displayName: category.name
|
|
3495
|
+
})),
|
|
3496
|
+
roots: categories.filter((category) => category.parentId === null),
|
|
3497
|
+
hasHierarchy: false
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
const roots = categories.filter(
|
|
3501
|
+
(category) => category.parentId === null || !byId.has(category.parentId)
|
|
3502
|
+
);
|
|
3503
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
3504
|
+
for (const category of categories) {
|
|
3505
|
+
if (!category.parentId || !byId.has(category.parentId)) continue;
|
|
3506
|
+
const children = childrenByParent.get(category.parentId) ?? [];
|
|
3507
|
+
children.push(category);
|
|
3508
|
+
childrenByParent.set(category.parentId, children);
|
|
3509
|
+
}
|
|
3510
|
+
const items = [];
|
|
3511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3512
|
+
for (const root of roots) {
|
|
3513
|
+
items.push({
|
|
3514
|
+
category: root,
|
|
3515
|
+
depth: 0,
|
|
3516
|
+
parent: null,
|
|
3517
|
+
displayName: root.name
|
|
3518
|
+
});
|
|
3519
|
+
seen.add(root.id);
|
|
3520
|
+
for (const child of childrenByParent.get(root.id) ?? []) {
|
|
3521
|
+
items.push({
|
|
3522
|
+
category: child,
|
|
3523
|
+
depth: 1,
|
|
3524
|
+
parent: root,
|
|
3525
|
+
displayName: `${root.name} / ${child.name}`
|
|
3526
|
+
});
|
|
3527
|
+
seen.add(child.id);
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
for (const category of categories) {
|
|
3531
|
+
if (seen.has(category.id)) continue;
|
|
3532
|
+
items.push({
|
|
3533
|
+
category,
|
|
3534
|
+
depth: 0,
|
|
3535
|
+
parent: null,
|
|
3536
|
+
displayName: category.name
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
return {
|
|
3540
|
+
items,
|
|
3541
|
+
roots: categories.filter((category) => category.parentId === null),
|
|
3542
|
+
hasHierarchy: true
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
// src/react/components/CategorySelect.tsx
|
|
3547
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3548
|
+
var NONE = "__none__";
|
|
3549
|
+
var ROOT_PARENT = "__root__";
|
|
3550
|
+
function plural(noun) {
|
|
3551
|
+
return /[^aeiou]y$/i.test(noun) ? `${noun.slice(0, -1)}ies` : `${noun}s`;
|
|
3552
|
+
}
|
|
3553
|
+
function CategorySelect({
|
|
3554
|
+
dimension,
|
|
3555
|
+
value,
|
|
3556
|
+
onChange,
|
|
3557
|
+
placeholder,
|
|
3558
|
+
allowNone = true,
|
|
3559
|
+
disabled,
|
|
3560
|
+
allowCreate = true,
|
|
3561
|
+
orgId,
|
|
3562
|
+
noun = "category"
|
|
3563
|
+
}) {
|
|
3564
|
+
const store = useAssociationsStore();
|
|
3565
|
+
const notifier = useNotifier();
|
|
3566
|
+
const {
|
|
3567
|
+
categories,
|
|
3568
|
+
status,
|
|
3569
|
+
error,
|
|
3570
|
+
create: createCategory,
|
|
3571
|
+
reload
|
|
3572
|
+
} = useCategories({ dimension });
|
|
3573
|
+
const hierarchy = buildCategoryHierarchy(categories);
|
|
3574
|
+
const [createParentId, setCreateParentId] = useState13(ROOT_PARENT);
|
|
3575
|
+
const options = [
|
|
3576
|
+
...allowNone ? [{ value: NONE, label: "None", keywords: "none clear empty" }] : [],
|
|
3577
|
+
...hierarchy.hasHierarchy ? hierarchy.items.map(({ category, depth, parent }) => ({
|
|
3578
|
+
value: category.id,
|
|
3579
|
+
label: category.name,
|
|
3580
|
+
// The type-ahead matches the parent's name too, so someone who knows
|
|
3581
|
+
// the branch but not the leaf still finds it.
|
|
3582
|
+
keywords: parent ? parent.name : "",
|
|
3583
|
+
render: depth === 0 ? /* @__PURE__ */ jsx14("span", { className: "font-medium", children: category.name }) : /* @__PURE__ */ jsxs12("span", { className: "flex items-center gap-1.5 pl-3 text-muted-foreground", children: [
|
|
3584
|
+
/* @__PURE__ */ jsx14(CornerDownRightIcon, { className: "h-3.5 w-3.5 shrink-0" }),
|
|
3585
|
+
/* @__PURE__ */ jsx14("span", { className: "text-foreground", children: category.name })
|
|
3586
|
+
] })
|
|
3587
|
+
})) : categories.map((category) => ({
|
|
3588
|
+
value: category.id,
|
|
3589
|
+
label: category.name
|
|
3590
|
+
}))
|
|
3591
|
+
];
|
|
3592
|
+
const selected = value ? hierarchy.items.find((item) => item.category.id === value)?.displayName ?? categories.find((category) => category.id === value)?.name ?? null : null;
|
|
3593
|
+
const create = async (typed) => {
|
|
3594
|
+
const name = typed.trim();
|
|
3595
|
+
if (!name) return null;
|
|
3596
|
+
const existing = categories.find(
|
|
3597
|
+
(category) => category.name.toLowerCase() === name.toLowerCase()
|
|
3598
|
+
);
|
|
3599
|
+
if (existing) return existing.id;
|
|
3600
|
+
let resolvedOrgId = orgId ?? null;
|
|
3601
|
+
if (!resolvedOrgId && store.identity.ensureOrgId) {
|
|
3602
|
+
try {
|
|
3603
|
+
resolvedOrgId = await store.identity.ensureOrgId();
|
|
3604
|
+
} catch (e) {
|
|
3605
|
+
store.errorSink({
|
|
3606
|
+
code: "ensure_org_failed",
|
|
3607
|
+
message: "[CategorySelect] identity.ensureOrgId threw",
|
|
3608
|
+
context: { error: e instanceof Error ? e.message : String(e) }
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
if (!resolvedOrgId) {
|
|
3613
|
+
notifier.error(`Select an organization before creating a ${noun}.`);
|
|
3614
|
+
return null;
|
|
3615
|
+
}
|
|
3616
|
+
const parentId = hierarchy.hasHierarchy && createParentId !== ROOT_PARENT ? createParentId : null;
|
|
3617
|
+
const result = await createCategory({ name, orgId: resolvedOrgId, parentId });
|
|
3618
|
+
if (!result.ok || !result.id) {
|
|
3619
|
+
notifier.error(`Couldn't create that ${noun}`, {
|
|
3620
|
+
...result.error != null ? { description: result.error } : {}
|
|
3621
|
+
});
|
|
3622
|
+
return null;
|
|
3623
|
+
}
|
|
3624
|
+
await reload();
|
|
3625
|
+
setCreateParentId(ROOT_PARENT);
|
|
3626
|
+
const under = parentId ? categories.find((category) => category.id === parentId)?.name : null;
|
|
3627
|
+
notifier.success(
|
|
3628
|
+
`\u201C${name}\u201D is now one of your ${plural(noun)}${under ? ` \u2014 it sits under ${under}, and every screen that offers ${plural(noun)} offers it.` : ` \u2014 every screen that offers ${plural(noun)} offers it from now on.`}`
|
|
3629
|
+
);
|
|
3630
|
+
return result.id;
|
|
3631
|
+
};
|
|
3632
|
+
return /* @__PURE__ */ jsxs12("div", { children: [
|
|
3633
|
+
/* @__PURE__ */ jsx14(
|
|
3634
|
+
CreatablePicker,
|
|
3635
|
+
{
|
|
3636
|
+
value: value ?? (allowNone ? NONE : null),
|
|
3637
|
+
onSelect: (next) => onChange(next === NONE ? null : next),
|
|
3638
|
+
options,
|
|
3639
|
+
placeholder,
|
|
3640
|
+
searchPlaceholder: allowCreate ? `Search or add a ${noun}\u2026` : `Search ${plural(noun)}\u2026`,
|
|
3641
|
+
noun,
|
|
3642
|
+
ariaLabel: placeholder,
|
|
3643
|
+
className: "text-sm",
|
|
3644
|
+
...disabled !== void 0 ? { disabled } : {},
|
|
3645
|
+
loading: status === "loading",
|
|
3646
|
+
emptyLabel: `No ${noun} matches that.`,
|
|
3647
|
+
...allowCreate ? { onCreate: create } : {},
|
|
3648
|
+
...allowCreate && hierarchy.hasHierarchy ? {
|
|
3649
|
+
createExtra: /* @__PURE__ */ jsxs12(Select, { value: createParentId, onValueChange: setCreateParentId, children: [
|
|
3650
|
+
/* @__PURE__ */ jsx14(
|
|
3651
|
+
SelectTrigger,
|
|
3652
|
+
{
|
|
3653
|
+
className: "h-7 text-xs",
|
|
3654
|
+
"aria-label": `Put the new ${noun} under`,
|
|
3655
|
+
children: /* @__PURE__ */ jsx14(SelectValue, {})
|
|
3656
|
+
}
|
|
3657
|
+
),
|
|
3658
|
+
/* @__PURE__ */ jsxs12(SelectContent, { children: [
|
|
3659
|
+
/* @__PURE__ */ jsx14(SelectItem, { value: ROOT_PARENT, className: "text-xs", children: "Top level" }),
|
|
3660
|
+
hierarchy.items.filter((item) => item.depth === 0).map((item) => /* @__PURE__ */ jsxs12(
|
|
3661
|
+
SelectItem,
|
|
3662
|
+
{
|
|
3663
|
+
value: item.category.id,
|
|
3664
|
+
className: "text-xs",
|
|
3665
|
+
children: [
|
|
3666
|
+
"Under ",
|
|
3667
|
+
item.category.name
|
|
3668
|
+
]
|
|
3669
|
+
},
|
|
3670
|
+
item.category.id
|
|
3671
|
+
))
|
|
3672
|
+
] })
|
|
3673
|
+
] })
|
|
3674
|
+
} : {}
|
|
3675
|
+
}
|
|
3676
|
+
),
|
|
3677
|
+
selected === null && value !== null ? (
|
|
3678
|
+
// A stored id whose row this reader cannot see is not "nothing chosen":
|
|
3679
|
+
// saying so beats a blank box that looks like an unsaved field.
|
|
3680
|
+
/* @__PURE__ */ jsxs12("p", { className: "mt-1 text-xs text-muted-foreground", children: [
|
|
3681
|
+
"This ",
|
|
3682
|
+
noun,
|
|
3683
|
+
" is no longer available to you."
|
|
3684
|
+
] })
|
|
3685
|
+
) : null,
|
|
3686
|
+
error ? /* @__PURE__ */ jsx14("p", { className: "mt-1 text-xs text-destructive", children: error }) : null
|
|
3687
|
+
] });
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
// src/react/components/CategoryTagPicker.tsx
|
|
3691
|
+
import { useState as useState14 } from "react";
|
|
3692
|
+
import {
|
|
3693
|
+
Badge,
|
|
3694
|
+
Button as Button2,
|
|
3695
|
+
cn as cn11,
|
|
3696
|
+
Command as Command2,
|
|
3697
|
+
CommandEmpty as CommandEmpty2,
|
|
3698
|
+
CommandGroup as CommandGroup2,
|
|
3699
|
+
CommandInput as CommandInput2,
|
|
3700
|
+
CommandItem as CommandItem2,
|
|
3701
|
+
CommandList as CommandList2,
|
|
3702
|
+
Popover as Popover2,
|
|
3703
|
+
PopoverContent as PopoverContent2,
|
|
3704
|
+
PopoverTrigger as PopoverTrigger2,
|
|
3705
|
+
Select as Select2,
|
|
3706
|
+
SelectContent as SelectContent2,
|
|
3707
|
+
SelectItem as SelectItem2,
|
|
3708
|
+
SelectTrigger as SelectTrigger2,
|
|
3709
|
+
SelectValue as SelectValue2
|
|
3710
|
+
} from "@ai-matrx/design-system";
|
|
3711
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3712
|
+
var ROOT_PARENT2 = "__root__";
|
|
3713
|
+
function CategoryTagPicker({
|
|
3714
|
+
entityType,
|
|
3715
|
+
entityId,
|
|
3716
|
+
dimension,
|
|
3717
|
+
edgeRole,
|
|
3718
|
+
orgId,
|
|
3719
|
+
addLabel = "Add tag",
|
|
3720
|
+
icon: Icon = TagIcon,
|
|
3721
|
+
allowCreate = true,
|
|
3722
|
+
hierarchical,
|
|
3723
|
+
emptyText = "No categories yet."
|
|
3724
|
+
}) {
|
|
3725
|
+
const store = useAssociationsStore();
|
|
3726
|
+
const notifier = useNotifier();
|
|
3727
|
+
const [open, setOpen] = useState14(false);
|
|
3728
|
+
const [creating, setCreating] = useState14(false);
|
|
3729
|
+
const [writing, setWriting] = useState14(false);
|
|
3730
|
+
const [search, setSearch] = useState14("");
|
|
3731
|
+
const [createParentId, setCreateParentId] = useState14(ROOT_PARENT2);
|
|
3732
|
+
const {
|
|
3733
|
+
categories,
|
|
3734
|
+
create: createCategory,
|
|
3735
|
+
reload: reloadCategories
|
|
3736
|
+
} = useCategories({ dimension });
|
|
3737
|
+
const { edges, setTargets } = useAssociations({
|
|
3738
|
+
type: entityType,
|
|
3739
|
+
id: entityId
|
|
3740
|
+
});
|
|
3741
|
+
const hierarchy = buildCategoryHierarchy(categories);
|
|
3742
|
+
const showHierarchy = hierarchical ?? hierarchy.hasHierarchy;
|
|
3743
|
+
const hierarchyById = new Map(
|
|
3744
|
+
hierarchy.items.map((item) => [item.category.id, item])
|
|
3745
|
+
);
|
|
3746
|
+
const selectedIds = new Set(
|
|
3747
|
+
edges.filter(
|
|
3748
|
+
(e) => e.direction === "outgoing" && e.otherType === "category" && e.role === edgeRole
|
|
3749
|
+
).map((e) => e.otherId)
|
|
3750
|
+
);
|
|
3751
|
+
const selected = categories.filter((c) => selectedIds.has(c.id));
|
|
3752
|
+
const toggle = async (categoryId) => {
|
|
3753
|
+
if (writing) return;
|
|
3754
|
+
setWriting(true);
|
|
3755
|
+
try {
|
|
3756
|
+
const next = selectedIds.has(categoryId) ? [...selectedIds].filter((id) => id !== categoryId) : [...selectedIds, categoryId];
|
|
3757
|
+
const res = await setTargets({
|
|
3758
|
+
targetType: "category",
|
|
3759
|
+
targetIds: next,
|
|
3760
|
+
...orgId != null ? { orgId } : {},
|
|
3761
|
+
role: edgeRole
|
|
3762
|
+
});
|
|
3763
|
+
if (!res.ok) {
|
|
3764
|
+
store.errorSink({
|
|
3765
|
+
code: "category_tag_set_targets_failed",
|
|
3766
|
+
message: "[CategoryTagPicker] setTargets failed",
|
|
3767
|
+
context: { entityType, entityId, edgeRole, error: res.error }
|
|
3768
|
+
});
|
|
3769
|
+
notifier.error(res.error ?? "Could not update categories");
|
|
3770
|
+
}
|
|
3771
|
+
} finally {
|
|
3772
|
+
setWriting(false);
|
|
3773
|
+
}
|
|
3774
|
+
};
|
|
3775
|
+
const createAndAttach = async () => {
|
|
3776
|
+
const name = search.trim();
|
|
3777
|
+
if (!name) return;
|
|
3778
|
+
setCreating(true);
|
|
3779
|
+
try {
|
|
3780
|
+
let resolvedOrgId = orgId ?? null;
|
|
3781
|
+
if (!resolvedOrgId && store.identity.ensureOrgId) {
|
|
3782
|
+
try {
|
|
3783
|
+
resolvedOrgId = await store.identity.ensureOrgId();
|
|
3784
|
+
} catch (e) {
|
|
3785
|
+
store.errorSink({
|
|
3786
|
+
code: "ensure_org_failed",
|
|
3787
|
+
message: "[CategoryTagPicker] identity.ensureOrgId threw",
|
|
3788
|
+
context: { error: e instanceof Error ? e.message : String(e) }
|
|
3789
|
+
});
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
if (!resolvedOrgId) {
|
|
3793
|
+
store.errorSink({
|
|
3794
|
+
code: "ensure_org_missing",
|
|
3795
|
+
message: "[CategoryTagPicker] no org for the created category \u2014 pass `orgId` or bind `identity.ensureOrgId` on the store config",
|
|
3796
|
+
context: { entityType, entityId, dimension }
|
|
3797
|
+
});
|
|
3798
|
+
notifier.error(
|
|
3799
|
+
"Could not create the category \u2014 no organization is selected"
|
|
3800
|
+
);
|
|
3801
|
+
return;
|
|
3802
|
+
}
|
|
3803
|
+
const parentId = showHierarchy && createParentId !== ROOT_PARENT2 ? createParentId : null;
|
|
3804
|
+
const createRes = await createCategory({
|
|
3805
|
+
name,
|
|
3806
|
+
orgId: resolvedOrgId,
|
|
3807
|
+
parentId
|
|
3808
|
+
});
|
|
3809
|
+
if (!createRes.ok || !createRes.id) {
|
|
3810
|
+
store.errorSink({
|
|
3811
|
+
code: "category_tag_create_failed",
|
|
3812
|
+
message: "[CategoryTagPicker] create failed",
|
|
3813
|
+
context: { dimension, error: createRes.error }
|
|
3814
|
+
});
|
|
3815
|
+
notifier.error(createRes.error ?? "Could not create the category");
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
await reloadCategories();
|
|
3819
|
+
await toggle(createRes.id);
|
|
3820
|
+
setSearch("");
|
|
3821
|
+
setCreateParentId(ROOT_PARENT2);
|
|
3822
|
+
} finally {
|
|
3823
|
+
setCreating(false);
|
|
3824
|
+
}
|
|
3825
|
+
};
|
|
3826
|
+
const filtered = categories.filter(
|
|
3827
|
+
(c) => c.name.toLowerCase().includes(search.trim().toLowerCase())
|
|
3828
|
+
);
|
|
3829
|
+
const filteredHierarchy = hierarchy.items.filter(
|
|
3830
|
+
(item) => item.displayName.toLowerCase().includes(search.trim().toLowerCase())
|
|
3831
|
+
);
|
|
3832
|
+
const exactMatch = categories.some(
|
|
3833
|
+
(c) => c.name.toLowerCase() === search.trim().toLowerCase()
|
|
3834
|
+
);
|
|
3835
|
+
return /* @__PURE__ */ jsxs13("div", { className: "flex flex-wrap items-center gap-1.5", children: [
|
|
3836
|
+
selected.map((c) => /* @__PURE__ */ jsxs13(
|
|
3837
|
+
Badge,
|
|
3838
|
+
{
|
|
3839
|
+
variant: "secondary",
|
|
3840
|
+
className: "gap-1 pr-1 text-xs font-medium",
|
|
3841
|
+
children: [
|
|
3842
|
+
showHierarchy ? hierarchyById.get(c.id)?.displayName ?? c.name : c.name,
|
|
3843
|
+
/* @__PURE__ */ jsx15(
|
|
3844
|
+
"button",
|
|
3845
|
+
{
|
|
3846
|
+
type: "button",
|
|
3847
|
+
onClick: () => void toggle(c.id),
|
|
3848
|
+
className: "ml-0.5 rounded-full p-0.5 hover:bg-muted-foreground/20",
|
|
3849
|
+
"aria-label": `Remove ${c.name}`,
|
|
3850
|
+
children: /* @__PURE__ */ jsx15(XIcon, { className: "h-2.5 w-2.5" })
|
|
3851
|
+
}
|
|
3852
|
+
)
|
|
3853
|
+
]
|
|
3854
|
+
},
|
|
3855
|
+
c.id
|
|
3856
|
+
)),
|
|
3857
|
+
/* @__PURE__ */ jsxs13(Popover2, { open, onOpenChange: setOpen, children: [
|
|
3858
|
+
/* @__PURE__ */ jsx15(PopoverTrigger2, { asChild: true, children: /* @__PURE__ */ jsxs13(
|
|
3859
|
+
Button2,
|
|
3860
|
+
{
|
|
3861
|
+
variant: "outline",
|
|
3862
|
+
size: "sm",
|
|
3863
|
+
className: "h-7 gap-1 px-2 text-xs",
|
|
3864
|
+
children: [
|
|
3865
|
+
/* @__PURE__ */ jsx15(Icon, { className: "h-3.5 w-3.5" }),
|
|
3866
|
+
selected.length === 0 ? addLabel : "Edit",
|
|
3867
|
+
/* @__PURE__ */ jsx15(ChevronsUpDownIcon, { className: "h-3 w-3 opacity-50" })
|
|
3868
|
+
]
|
|
3869
|
+
}
|
|
3870
|
+
) }),
|
|
3871
|
+
/* @__PURE__ */ jsx15(PopoverContent2, { className: "w-56 p-0", align: "start", children: /* @__PURE__ */ jsxs13(Command2, { shouldFilter: false, children: [
|
|
3872
|
+
/* @__PURE__ */ jsx15(
|
|
3873
|
+
CommandInput2,
|
|
3874
|
+
{
|
|
3875
|
+
placeholder: allowCreate ? "Search or create..." : "Search...",
|
|
3876
|
+
value: search,
|
|
3877
|
+
onValueChange: setSearch
|
|
3878
|
+
}
|
|
3879
|
+
),
|
|
3880
|
+
/* @__PURE__ */ jsxs13(CommandList2, { children: [
|
|
3881
|
+
/* @__PURE__ */ jsx15(CommandEmpty2, { className: "px-2 py-3 text-xs text-muted-foreground", children: search.trim() ? "No match." : emptyText }),
|
|
3882
|
+
/* @__PURE__ */ jsx15(CommandGroup2, { children: showHierarchy ? filteredHierarchy.map(({ category, depth, parent }) => /* @__PURE__ */ jsxs13(
|
|
3883
|
+
CommandItem2,
|
|
3884
|
+
{
|
|
3885
|
+
value: category.id,
|
|
3886
|
+
keywords: parent ? [parent.name, category.name] : [],
|
|
3887
|
+
onSelect: () => void toggle(category.id),
|
|
3888
|
+
className: cn11("text-xs", depth === 1 && "pl-6"),
|
|
3889
|
+
children: [
|
|
3890
|
+
/* @__PURE__ */ jsx15(
|
|
3891
|
+
CheckIcon,
|
|
3892
|
+
{
|
|
3893
|
+
className: cn11(
|
|
3894
|
+
"mr-2 h-3.5 w-3.5",
|
|
3895
|
+
selectedIds.has(category.id) ? "opacity-100" : "opacity-0"
|
|
3896
|
+
)
|
|
3897
|
+
}
|
|
3898
|
+
),
|
|
3899
|
+
depth === 1 ? /* @__PURE__ */ jsx15(CornerDownRightIcon, { className: "mr-1.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }) : null,
|
|
3900
|
+
/* @__PURE__ */ jsx15("span", { className: cn11(depth === 0 && "font-medium"), children: category.name })
|
|
3901
|
+
]
|
|
3902
|
+
},
|
|
3903
|
+
category.id
|
|
3904
|
+
)) : filtered.map((c) => /* @__PURE__ */ jsxs13(
|
|
3905
|
+
CommandItem2,
|
|
3906
|
+
{
|
|
3907
|
+
value: c.id,
|
|
3908
|
+
onSelect: () => void toggle(c.id),
|
|
3909
|
+
className: "text-xs",
|
|
3910
|
+
children: [
|
|
3911
|
+
/* @__PURE__ */ jsx15(
|
|
3912
|
+
CheckIcon,
|
|
3913
|
+
{
|
|
3914
|
+
className: cn11(
|
|
3915
|
+
"mr-2 h-3.5 w-3.5",
|
|
3916
|
+
selectedIds.has(c.id) ? "opacity-100" : "opacity-0"
|
|
3917
|
+
)
|
|
3918
|
+
}
|
|
3919
|
+
),
|
|
3920
|
+
c.name
|
|
3921
|
+
]
|
|
3922
|
+
},
|
|
3923
|
+
c.id
|
|
3924
|
+
)) }),
|
|
3925
|
+
allowCreate && search.trim() && !exactMatch ? /* @__PURE__ */ jsxs13(CommandGroup2, { children: [
|
|
3926
|
+
showHierarchy ? /* @__PURE__ */ jsxs13("div", { className: "space-y-1.5 px-2 pb-2", children: [
|
|
3927
|
+
/* @__PURE__ */ jsx15("p", { className: "text-[11px] font-medium text-muted-foreground", children: "Place in" }),
|
|
3928
|
+
/* @__PURE__ */ jsxs13(
|
|
3929
|
+
Select2,
|
|
3930
|
+
{
|
|
3931
|
+
value: createParentId,
|
|
3932
|
+
onValueChange: setCreateParentId,
|
|
3933
|
+
children: [
|
|
3934
|
+
/* @__PURE__ */ jsx15(SelectTrigger2, { size: "sm", className: "bg-background", children: /* @__PURE__ */ jsx15(SelectValue2, {}) }),
|
|
3935
|
+
/* @__PURE__ */ jsxs13(SelectContent2, { children: [
|
|
3936
|
+
/* @__PURE__ */ jsx15(SelectItem2, { value: ROOT_PARENT2, children: "Top level" }),
|
|
3937
|
+
hierarchy.roots.map((root) => /* @__PURE__ */ jsx15(SelectItem2, { value: root.id, children: root.name }, root.id))
|
|
3938
|
+
] })
|
|
3939
|
+
]
|
|
3940
|
+
}
|
|
3941
|
+
)
|
|
3942
|
+
] }) : null,
|
|
3943
|
+
/* @__PURE__ */ jsxs13(
|
|
3944
|
+
CommandItem2,
|
|
3945
|
+
{
|
|
3946
|
+
value: `__create__${search}`,
|
|
3947
|
+
onSelect: () => void createAndAttach(),
|
|
3948
|
+
disabled: creating,
|
|
3949
|
+
className: "text-xs text-primary",
|
|
3950
|
+
children: [
|
|
3951
|
+
creating ? /* @__PURE__ */ jsx15(SpinnerIcon, { className: "mr-2 h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx15(Icon, { className: "mr-2 h-3.5 w-3.5" }),
|
|
3952
|
+
"Create \u201C",
|
|
3953
|
+
search.trim(),
|
|
3954
|
+
"\u201D"
|
|
3955
|
+
]
|
|
3956
|
+
}
|
|
3957
|
+
)
|
|
3958
|
+
] }) : null
|
|
3959
|
+
] })
|
|
3960
|
+
] }) })
|
|
3961
|
+
] })
|
|
3962
|
+
] });
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
// src/react/components/CommentThread.tsx
|
|
3966
|
+
import { useState as useState15 } from "react";
|
|
3967
|
+
import { Button as Button3, Skeleton as Skeleton2, cn as cn12 } from "@ai-matrx/design-system";
|
|
3968
|
+
|
|
3969
|
+
// src/react/relativeTime.ts
|
|
3970
|
+
var UNITS = [
|
|
3971
|
+
{ unit: "year", ms: 365 * 24 * 60 * 60 * 1e3 },
|
|
3972
|
+
{ unit: "month", ms: 30 * 24 * 60 * 60 * 1e3 },
|
|
3973
|
+
{ unit: "week", ms: 7 * 24 * 60 * 60 * 1e3 },
|
|
3974
|
+
{ unit: "day", ms: 24 * 60 * 60 * 1e3 },
|
|
3975
|
+
{ unit: "hour", ms: 60 * 60 * 1e3 },
|
|
3976
|
+
{ unit: "minute", ms: 60 * 1e3 }
|
|
3977
|
+
];
|
|
3978
|
+
function formatRelativeTime(iso, now = Date.now()) {
|
|
3979
|
+
const t = Date.parse(iso);
|
|
3980
|
+
if (Number.isNaN(t)) return iso;
|
|
3981
|
+
const delta = t - now;
|
|
3982
|
+
const magnitude = Math.abs(delta);
|
|
3983
|
+
if (magnitude < 60 * 1e3) return "just now";
|
|
3984
|
+
const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
|
|
3985
|
+
for (const { unit, ms } of UNITS) {
|
|
3986
|
+
if (magnitude >= ms) {
|
|
3987
|
+
return rtf.format(Math.trunc(delta / ms), unit);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
return "just now";
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
// src/react/components/CommentThread.tsx
|
|
3994
|
+
import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3995
|
+
var AUTHOR_DOOR_TOKEN = "user";
|
|
3996
|
+
function resolveAuthor(comment, authorDisplay) {
|
|
3997
|
+
const base2 = {
|
|
3998
|
+
userId: comment.createdBy,
|
|
3999
|
+
email: comment.author.email,
|
|
4000
|
+
displayName: comment.author.displayName,
|
|
4001
|
+
avatarUrl: comment.author.avatarUrl
|
|
4002
|
+
};
|
|
4003
|
+
const enriched = authorDisplay ? authorDisplay(base2) : null;
|
|
4004
|
+
const name = enriched?.displayName ?? base2.displayName ?? base2.email ?? "Unknown user";
|
|
4005
|
+
return {
|
|
4006
|
+
name,
|
|
4007
|
+
avatarUrl: enriched?.avatarUrl !== void 0 && enriched?.avatarUrl !== null ? enriched.avatarUrl : base2.avatarUrl,
|
|
4008
|
+
userId: comment.createdBy
|
|
4009
|
+
};
|
|
4010
|
+
}
|
|
4011
|
+
function AuthorAvatar({ author }) {
|
|
4012
|
+
if (author.avatarUrl) {
|
|
4013
|
+
return /* @__PURE__ */ jsx16(
|
|
4014
|
+
"img",
|
|
4015
|
+
{
|
|
4016
|
+
src: author.avatarUrl,
|
|
4017
|
+
alt: "",
|
|
4018
|
+
className: "h-6 w-6 shrink-0 rounded-full object-cover"
|
|
4019
|
+
}
|
|
4020
|
+
);
|
|
4021
|
+
}
|
|
4022
|
+
return /* @__PURE__ */ jsx16(
|
|
4023
|
+
"span",
|
|
4024
|
+
{
|
|
4025
|
+
"aria-hidden": true,
|
|
4026
|
+
className: "flex h-6 w-6 shrink-0 select-none items-center justify-center rounded-full bg-muted text-[11px] font-medium uppercase text-muted-foreground",
|
|
4027
|
+
children: author.name.trim().charAt(0) || "?"
|
|
4028
|
+
}
|
|
4029
|
+
);
|
|
4030
|
+
}
|
|
4031
|
+
function AuthorName({
|
|
4032
|
+
author,
|
|
4033
|
+
entityDoors
|
|
4034
|
+
}) {
|
|
4035
|
+
const DoorRef2 = entityDoors?.EntityRef;
|
|
4036
|
+
if (DoorRef2 && author.userId) {
|
|
4037
|
+
return /* @__PURE__ */ jsx16(
|
|
4038
|
+
DoorRef2,
|
|
4039
|
+
{
|
|
4040
|
+
token: AUTHOR_DOOR_TOKEN,
|
|
4041
|
+
id: author.userId,
|
|
4042
|
+
name: author.name,
|
|
4043
|
+
showIcon: false,
|
|
4044
|
+
className: "text-sm font-medium text-foreground"
|
|
4045
|
+
}
|
|
4046
|
+
);
|
|
4047
|
+
}
|
|
4048
|
+
return /* @__PURE__ */ jsx16("span", { className: "text-sm font-medium text-foreground", children: author.name });
|
|
4049
|
+
}
|
|
4050
|
+
function CommentComposer({
|
|
4051
|
+
onSubmit,
|
|
4052
|
+
placeholder = "Write a comment\u2026",
|
|
4053
|
+
submitLabel = "Comment",
|
|
4054
|
+
initialValue = "",
|
|
4055
|
+
trailing,
|
|
4056
|
+
autoFocus = false
|
|
4057
|
+
}) {
|
|
4058
|
+
const notifier = useNotifier();
|
|
4059
|
+
const [body, setBody] = useState15(initialValue);
|
|
4060
|
+
const [submitting, setSubmitting] = useState15(false);
|
|
4061
|
+
const [error, setError] = useState15(null);
|
|
4062
|
+
async function submit(e) {
|
|
4063
|
+
e?.preventDefault();
|
|
4064
|
+
const trimmed = body.trim();
|
|
4065
|
+
if (!trimmed || submitting) return;
|
|
4066
|
+
setSubmitting(true);
|
|
4067
|
+
setError(null);
|
|
4068
|
+
const res = await onSubmit(trimmed);
|
|
4069
|
+
setSubmitting(false);
|
|
4070
|
+
if (!res.ok) {
|
|
4071
|
+
const msg = res.error ?? "Could not save the comment";
|
|
4072
|
+
setError(msg);
|
|
4073
|
+
notifier.error(msg);
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
setBody("");
|
|
4077
|
+
}
|
|
4078
|
+
return /* @__PURE__ */ jsxs14("form", { onSubmit: submit, className: "flex flex-col gap-1.5", children: [
|
|
4079
|
+
/* @__PURE__ */ jsx16(
|
|
4080
|
+
"textarea",
|
|
4081
|
+
{
|
|
4082
|
+
value: body,
|
|
4083
|
+
onChange: (e) => setBody(e.target.value),
|
|
4084
|
+
onKeyDown: (e) => {
|
|
4085
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void submit();
|
|
4086
|
+
},
|
|
4087
|
+
placeholder,
|
|
4088
|
+
rows: 2,
|
|
4089
|
+
autoFocus,
|
|
4090
|
+
"aria-label": placeholder,
|
|
4091
|
+
className: "w-full resize-y rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
4092
|
+
}
|
|
4093
|
+
),
|
|
4094
|
+
error && /* @__PURE__ */ jsx16("p", { role: "alert", className: "text-xs text-destructive", children: error }),
|
|
4095
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
|
|
4096
|
+
/* @__PURE__ */ jsxs14(Button3, { type: "submit", size: "sm", disabled: !body.trim() || submitting, children: [
|
|
4097
|
+
submitting && /* @__PURE__ */ jsx16(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
|
|
4098
|
+
submitLabel
|
|
4099
|
+
] }),
|
|
4100
|
+
trailing
|
|
4101
|
+
] })
|
|
4102
|
+
] });
|
|
4103
|
+
}
|
|
4104
|
+
function CommentItem({
|
|
4105
|
+
comment,
|
|
4106
|
+
childrenOf,
|
|
4107
|
+
depth,
|
|
4108
|
+
thread,
|
|
4109
|
+
orgId
|
|
4110
|
+
}) {
|
|
4111
|
+
const { authorDisplay, entityDoors } = useAssociationsUiPorts();
|
|
4112
|
+
const notifier = useNotifier();
|
|
4113
|
+
const [replying, setReplying] = useState15(false);
|
|
4114
|
+
const [editing, setEditing] = useState15(false);
|
|
4115
|
+
const [confirmingDelete, setConfirmingDelete] = useState15(false);
|
|
4116
|
+
const [deleting, setDeleting] = useState15(false);
|
|
4117
|
+
const author = resolveAuthor(comment, authorDisplay);
|
|
4118
|
+
const isOwn = thread.currentUserId !== null && comment.createdBy === thread.currentUserId;
|
|
4119
|
+
const edited = comment.updatedAt !== comment.createdAt;
|
|
4120
|
+
const replies = childrenOf.get(comment.id) ?? [];
|
|
4121
|
+
async function confirmDelete() {
|
|
4122
|
+
setDeleting(true);
|
|
4123
|
+
const res = await thread.remove(comment.id);
|
|
4124
|
+
setDeleting(false);
|
|
4125
|
+
setConfirmingDelete(false);
|
|
4126
|
+
if (!res.ok) {
|
|
4127
|
+
notifier.error(res.error ?? "Could not delete the comment");
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
return /* @__PURE__ */ jsxs14(
|
|
4131
|
+
"div",
|
|
4132
|
+
{
|
|
4133
|
+
"data-comment-id": comment.id,
|
|
4134
|
+
className: cn12(
|
|
4135
|
+
"flex flex-col gap-1",
|
|
4136
|
+
depth > 0 && "border-l border-border pl-3"
|
|
4137
|
+
),
|
|
4138
|
+
children: [
|
|
4139
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
|
|
4140
|
+
/* @__PURE__ */ jsx16(AuthorAvatar, { author }),
|
|
4141
|
+
/* @__PURE__ */ jsx16(AuthorName, { author, entityDoors }),
|
|
4142
|
+
/* @__PURE__ */ jsx16(
|
|
4143
|
+
"span",
|
|
4144
|
+
{
|
|
4145
|
+
className: "text-xs text-muted-foreground",
|
|
4146
|
+
title: comment.createdAt,
|
|
4147
|
+
children: formatRelativeTime(comment.createdAt)
|
|
4148
|
+
}
|
|
4149
|
+
),
|
|
4150
|
+
edited && /* @__PURE__ */ jsx16(
|
|
4151
|
+
"span",
|
|
4152
|
+
{
|
|
4153
|
+
className: "text-xs text-muted-foreground",
|
|
4154
|
+
title: comment.updatedAt,
|
|
4155
|
+
children: "(edited)"
|
|
4156
|
+
}
|
|
4157
|
+
)
|
|
4158
|
+
] }),
|
|
4159
|
+
editing ? /* @__PURE__ */ jsx16("div", { className: "pl-8", children: /* @__PURE__ */ jsx16(
|
|
4160
|
+
CommentComposer,
|
|
4161
|
+
{
|
|
4162
|
+
initialValue: comment.body,
|
|
4163
|
+
submitLabel: "Save",
|
|
4164
|
+
placeholder: "Edit your comment\u2026",
|
|
4165
|
+
autoFocus: true,
|
|
4166
|
+
onSubmit: async (body) => {
|
|
4167
|
+
const res = await thread.edit(comment.id, body);
|
|
4168
|
+
if (res.ok) setEditing(false);
|
|
4169
|
+
return res;
|
|
4170
|
+
},
|
|
4171
|
+
trailing: /* @__PURE__ */ jsx16(
|
|
4172
|
+
Button3,
|
|
4173
|
+
{
|
|
4174
|
+
type: "button",
|
|
4175
|
+
size: "sm",
|
|
4176
|
+
variant: "ghost",
|
|
4177
|
+
onClick: () => setEditing(false),
|
|
4178
|
+
children: "Cancel"
|
|
4179
|
+
}
|
|
4180
|
+
)
|
|
4181
|
+
}
|
|
4182
|
+
) }) : /* @__PURE__ */ jsx16("p", { className: "whitespace-pre-wrap pl-8 text-sm text-foreground", children: comment.body }),
|
|
4183
|
+
!editing && /* @__PURE__ */ jsx16("div", { className: "flex items-center gap-1 pl-7", children: confirmingDelete ? /* @__PURE__ */ jsxs14(Fragment5, { children: [
|
|
4184
|
+
/* @__PURE__ */ jsx16("span", { className: "text-xs text-destructive", children: "Deletes this comment for everyone." }),
|
|
4185
|
+
/* @__PURE__ */ jsxs14(
|
|
4186
|
+
Button3,
|
|
4187
|
+
{
|
|
4188
|
+
type: "button",
|
|
4189
|
+
size: "sm",
|
|
4190
|
+
variant: "destructive",
|
|
4191
|
+
disabled: deleting,
|
|
4192
|
+
onClick: () => void confirmDelete(),
|
|
4193
|
+
children: [
|
|
4194
|
+
deleting && /* @__PURE__ */ jsx16(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
|
|
4195
|
+
"Delete"
|
|
4196
|
+
]
|
|
4197
|
+
}
|
|
4198
|
+
),
|
|
4199
|
+
/* @__PURE__ */ jsx16(
|
|
4200
|
+
Button3,
|
|
4201
|
+
{
|
|
4202
|
+
type: "button",
|
|
4203
|
+
size: "sm",
|
|
4204
|
+
variant: "ghost",
|
|
4205
|
+
disabled: deleting,
|
|
4206
|
+
onClick: () => setConfirmingDelete(false),
|
|
4207
|
+
children: "Cancel"
|
|
4208
|
+
}
|
|
4209
|
+
)
|
|
4210
|
+
] }) : /* @__PURE__ */ jsxs14(Fragment5, { children: [
|
|
4211
|
+
/* @__PURE__ */ jsx16(
|
|
4212
|
+
"button",
|
|
4213
|
+
{
|
|
4214
|
+
type: "button",
|
|
4215
|
+
onClick: () => setReplying((v) => !v),
|
|
4216
|
+
className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
|
|
4217
|
+
children: "Reply"
|
|
4218
|
+
}
|
|
4219
|
+
),
|
|
4220
|
+
isOwn && /* @__PURE__ */ jsxs14(Fragment5, { children: [
|
|
4221
|
+
/* @__PURE__ */ jsx16(
|
|
4222
|
+
"button",
|
|
4223
|
+
{
|
|
4224
|
+
type: "button",
|
|
4225
|
+
onClick: () => setEditing(true),
|
|
4226
|
+
className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
|
|
4227
|
+
children: "Edit"
|
|
4228
|
+
}
|
|
4229
|
+
),
|
|
4230
|
+
/* @__PURE__ */ jsx16(
|
|
4231
|
+
"button",
|
|
4232
|
+
{
|
|
4233
|
+
type: "button",
|
|
4234
|
+
onClick: () => setConfirmingDelete(true),
|
|
4235
|
+
className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-destructive",
|
|
4236
|
+
children: "Delete"
|
|
4237
|
+
}
|
|
4238
|
+
)
|
|
4239
|
+
] })
|
|
4240
|
+
] }) }),
|
|
4241
|
+
replying && /* @__PURE__ */ jsxs14("div", { className: "flex items-start gap-2 pl-8 pt-1", children: [
|
|
4242
|
+
/* @__PURE__ */ jsx16(CornerDownRightIcon, { className: "mt-2 h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
|
|
4243
|
+
/* @__PURE__ */ jsx16("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx16(
|
|
4244
|
+
CommentComposer,
|
|
4245
|
+
{
|
|
4246
|
+
submitLabel: "Reply",
|
|
4247
|
+
placeholder: `Reply to ${author.name}\u2026`,
|
|
4248
|
+
autoFocus: true,
|
|
4249
|
+
onSubmit: async (body) => {
|
|
4250
|
+
const res = await thread.add(body, {
|
|
4251
|
+
parentId: comment.id,
|
|
4252
|
+
orgId: orgId ?? null
|
|
4253
|
+
});
|
|
4254
|
+
if (res.ok) setReplying(false);
|
|
4255
|
+
return res;
|
|
4256
|
+
},
|
|
4257
|
+
trailing: /* @__PURE__ */ jsx16(
|
|
4258
|
+
Button3,
|
|
4259
|
+
{
|
|
4260
|
+
type: "button",
|
|
4261
|
+
size: "sm",
|
|
4262
|
+
variant: "ghost",
|
|
4263
|
+
onClick: () => setReplying(false),
|
|
4264
|
+
children: "Cancel"
|
|
4265
|
+
}
|
|
4266
|
+
)
|
|
4267
|
+
}
|
|
4268
|
+
) })
|
|
4269
|
+
] }),
|
|
4270
|
+
replies.length > 0 && /* @__PURE__ */ jsx16("div", { className: "flex flex-col gap-3 pl-8 pt-1", children: replies.map((reply) => /* @__PURE__ */ jsx16(
|
|
4271
|
+
CommentItem,
|
|
4272
|
+
{
|
|
4273
|
+
comment: reply,
|
|
4274
|
+
childrenOf,
|
|
4275
|
+
depth: depth + 1,
|
|
4276
|
+
thread,
|
|
4277
|
+
orgId
|
|
4278
|
+
},
|
|
4279
|
+
reply.id
|
|
4280
|
+
)) })
|
|
4281
|
+
]
|
|
4282
|
+
}
|
|
4283
|
+
);
|
|
4284
|
+
}
|
|
4285
|
+
function buildCommentTree(comments) {
|
|
4286
|
+
const byId = new Set(comments.map((c) => c.id));
|
|
4287
|
+
const roots = [];
|
|
4288
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
4289
|
+
for (const c of comments) {
|
|
4290
|
+
if (c.parentId && byId.has(c.parentId)) {
|
|
4291
|
+
const bucket = childrenOf.get(c.parentId);
|
|
4292
|
+
if (bucket) bucket.push(c);
|
|
4293
|
+
else childrenOf.set(c.parentId, [c]);
|
|
4294
|
+
} else {
|
|
4295
|
+
roots.push(c);
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
return { roots, childrenOf };
|
|
4299
|
+
}
|
|
4300
|
+
function CommentThread({
|
|
4301
|
+
token,
|
|
4302
|
+
id,
|
|
4303
|
+
orgId,
|
|
4304
|
+
showHeader = true,
|
|
4305
|
+
className
|
|
4306
|
+
}) {
|
|
4307
|
+
const thread = useComments({ token, id });
|
|
4308
|
+
const { roots, childrenOf } = buildCommentTree(thread.comments);
|
|
4309
|
+
const loading = (thread.status === "loading" || thread.status === "idle") && thread.comments.length === 0;
|
|
4310
|
+
return /* @__PURE__ */ jsxs14("section", { className: cn12("flex flex-col gap-3", className), children: [
|
|
4311
|
+
showHeader && /* @__PURE__ */ jsxs14("header", { className: "flex items-center gap-2", children: [
|
|
4312
|
+
/* @__PURE__ */ jsx16("h3", { className: "text-sm font-semibold text-foreground", children: "Comments" }),
|
|
4313
|
+
/* @__PURE__ */ jsx16("span", { className: "text-xs text-muted-foreground tabular-nums", children: thread.comments.length }),
|
|
4314
|
+
/* @__PURE__ */ jsx16(
|
|
4315
|
+
"button",
|
|
4316
|
+
{
|
|
4317
|
+
type: "button",
|
|
4318
|
+
onClick: () => void thread.reload(),
|
|
4319
|
+
"aria-label": "Refresh comments",
|
|
4320
|
+
title: "Refresh comments",
|
|
4321
|
+
className: "ml-auto flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
|
|
4322
|
+
children: thread.status === "loading" ? /* @__PURE__ */ jsx16(SpinnerIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx16(RefreshIcon, { className: "h-3.5 w-3.5" })
|
|
4323
|
+
}
|
|
4324
|
+
)
|
|
4325
|
+
] }),
|
|
4326
|
+
thread.error && /* @__PURE__ */ jsxs14(
|
|
4327
|
+
"div",
|
|
4328
|
+
{
|
|
4329
|
+
role: "alert",
|
|
4330
|
+
className: "flex items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive",
|
|
4331
|
+
children: [
|
|
4332
|
+
/* @__PURE__ */ jsx16("span", { className: "min-w-0 flex-1", children: thread.error }),
|
|
4333
|
+
/* @__PURE__ */ jsx16(
|
|
4334
|
+
Button3,
|
|
4335
|
+
{
|
|
4336
|
+
type: "button",
|
|
4337
|
+
size: "sm",
|
|
4338
|
+
variant: "outline",
|
|
4339
|
+
disabled: thread.status === "loading",
|
|
4340
|
+
onClick: () => void thread.reload(),
|
|
4341
|
+
children: "Retry"
|
|
4342
|
+
}
|
|
4343
|
+
)
|
|
4344
|
+
]
|
|
4345
|
+
}
|
|
4346
|
+
),
|
|
4347
|
+
loading ? /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-3", "aria-hidden": true, "data-testid": "comments-loading", children: [
|
|
4348
|
+
/* @__PURE__ */ jsx16(Skeleton2, { className: "h-12 w-full" }),
|
|
4349
|
+
/* @__PURE__ */ jsx16(Skeleton2, { className: "h-12 w-4/5" })
|
|
4350
|
+
] }) : roots.length === 0 && !thread.error ? /* @__PURE__ */ jsx16("p", { className: "text-sm text-muted-foreground", children: "No comments yet." }) : /* @__PURE__ */ jsx16("div", { className: "flex flex-col gap-4", children: roots.map((comment) => /* @__PURE__ */ jsx16(
|
|
4351
|
+
CommentItem,
|
|
4352
|
+
{
|
|
4353
|
+
comment,
|
|
4354
|
+
childrenOf,
|
|
4355
|
+
depth: 0,
|
|
4356
|
+
thread,
|
|
4357
|
+
orgId
|
|
4358
|
+
},
|
|
4359
|
+
comment.id
|
|
4360
|
+
)) }),
|
|
4361
|
+
/* @__PURE__ */ jsx16(
|
|
4362
|
+
CommentComposer,
|
|
4363
|
+
{
|
|
4364
|
+
onSubmit: (body) => thread.add(body, { orgId: orgId ?? null })
|
|
4365
|
+
}
|
|
4366
|
+
)
|
|
4367
|
+
] });
|
|
4368
|
+
}
|
|
3109
4369
|
export {
|
|
3110
4370
|
AssociationCandidateBody,
|
|
3111
4371
|
AssociationCaptureToolbar,
|
|
3112
4372
|
AssociationCard,
|
|
3113
4373
|
AssociationCardGrid,
|
|
4374
|
+
AssociationEntitySelect,
|
|
3114
4375
|
AssociationList,
|
|
3115
4376
|
AssociationPicker,
|
|
3116
4377
|
AssociationWindow,
|
|
@@ -3118,12 +4379,18 @@ export {
|
|
|
3118
4379
|
AttachedItemsSheet,
|
|
3119
4380
|
CONTENT_ROLES,
|
|
3120
4381
|
CaptureToolbarAction,
|
|
4382
|
+
CategorySelect,
|
|
4383
|
+
CategoryTagPicker,
|
|
4384
|
+
CommentComposer,
|
|
4385
|
+
CommentThread,
|
|
3121
4386
|
DefaultEntityIcon,
|
|
3122
4387
|
DoorRef,
|
|
3123
4388
|
PrimaryEntityProvider,
|
|
3124
4389
|
UniversalAssociationPicker,
|
|
3125
4390
|
UnresolvedRef,
|
|
3126
4391
|
attachedKey,
|
|
4392
|
+
buildCommentTree,
|
|
4393
|
+
formatRelativeTime,
|
|
3127
4394
|
getContentRoleMeta,
|
|
3128
4395
|
useAssociationCandidates,
|
|
3129
4396
|
useAssociationEntitySelectAdapter,
|
|
@@ -3131,6 +4398,7 @@ export {
|
|
|
3131
4398
|
useAssociationsStore,
|
|
3132
4399
|
useAssociationsUiPorts,
|
|
3133
4400
|
useCategories,
|
|
4401
|
+
useComments,
|
|
3134
4402
|
useContainerLinks,
|
|
3135
4403
|
useContainerLinksAdapter,
|
|
3136
4404
|
useEntityRelationships,
|