@agent-native/core-corpus 0.210.2 → 0.211.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.
Files changed (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
  3. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
  4. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
  5. package/corpus/templates/analytics/CHANGELOG.md +18 -0
  6. package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
  7. package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
  8. package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
  9. package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
  10. package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
  11. package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
  12. package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
  13. package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
  14. package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
  15. package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
  16. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  17. package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
  18. package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
  19. package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
  20. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
  21. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
  22. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
  23. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
  24. package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
  25. package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
  26. package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
  27. package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
  28. package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
  37. package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
  38. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  39. package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
  40. package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
  41. package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
  42. package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
  43. package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
  44. package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
  45. package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
  46. package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
  47. package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
  48. package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
  49. package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
  51. package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
  52. package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
  53. package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
  54. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
  55. package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
  56. package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
  57. package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
  58. package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
  59. package/corpus/templates/analytics/server/plugins/db.ts +9 -1
  60. package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
  61. package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
  62. package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
  63. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
  64. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
  65. package/corpus/templates/calendar/actions/delete-event.ts +1 -3
  66. package/corpus/templates/calendar/actions/delete-events.ts +1 -2
  67. package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
  68. package/corpus/templates/calendar/actions/update-event.ts +2 -6
  69. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  70. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
  71. package/corpus/templates/clips/CHANGELOG.md +11 -24
  72. package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
  73. package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
  74. package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
  75. package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
  76. package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
  77. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  78. package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
  79. package/corpus/templates/clips/app/root.tsx +22 -10
  80. package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
  81. package/corpus/templates/clips/app/routes/import.tsx +115 -110
  82. package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
  88. package/corpus/templates/clips/desktop/src/app.tsx +29 -186
  89. package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
  90. package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
  91. package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
  92. package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
  93. package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
  94. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
  95. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
  96. package/corpus/templates/content/CHANGELOG.md +12 -0
  97. package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
  98. package/corpus/templates/content/actions/_database-navigation.ts +3 -3
  99. package/corpus/templates/content/actions/_database-utils.ts +10 -22
  100. package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
  101. package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
  102. package/corpus/templates/content/actions/export-document.ts +2 -14
  103. package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
  104. package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
  105. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
  106. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
  107. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
  108. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
  109. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
  110. package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
  111. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
  112. package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
  113. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
  114. package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
  115. package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
  116. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
  117. package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
  118. package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
  119. package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
  120. package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
  121. package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
  122. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
  123. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
  124. package/corpus/templates/content/app/global.css +23 -0
  125. package/corpus/templates/content/app/i18n-data.ts +1 -0
  126. package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
  127. package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
  128. package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-19-edits-made-from-a-block-s-menu-or-a-table-s-row-and-column-m.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
  132. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
  133. package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
  134. package/corpus/templates/content/e2e/helpers.ts +10 -0
  135. package/corpus/templates/content/shared/api.ts +18 -12
  136. package/corpus/templates/content/shared/database-query.ts +107 -13
  137. package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
  138. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
  139. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
  140. package/corpus/templates/design/CHANGELOG.md +2 -0
  141. package/corpus/templates/design/actions/delete-file.ts +1 -4
  142. package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
  143. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
  144. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
  145. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
  146. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
  147. package/corpus/templates/design/changelog/2026-10-19-reply-and-edit-on-a-comment-in-the-sidebar-or-on-a-canvas-pi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
  149. package/corpus/templates/design/e2e/global-setup.ts +185 -85
  150. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
  151. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  152. package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
  153. package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
  154. package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
  155. package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
  156. package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
  157. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
  158. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  159. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  160. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  161. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
  162. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
  163. package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
  164. package/corpus/templates/mail/AGENTS.md +1 -1
  165. package/corpus/templates/mail/CHANGELOG.md +14 -0
  166. package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
  167. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
  168. package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
  169. package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
  170. package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
  171. package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
  172. package/corpus/templates/mail/app/root.tsx +10 -2
  173. package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
  174. package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
  175. package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
  176. package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
  177. package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
  178. package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
  179. package/corpus/templates/mail/server/handlers/emails.ts +9 -0
  180. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
  181. package/corpus/templates/plan/CHANGELOG.md +4 -0
  182. package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
  183. package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
  184. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
  185. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
  186. package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
  187. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
  188. package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
  189. package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
  190. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
  191. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
  192. package/package.json +1 -1
@@ -0,0 +1,515 @@
1
+ import { useSession } from "@agent-native/core/client/hooks";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
4
+ import type { ContentDatabaseItem, DocumentProperty } from "@shared/api";
5
+ import { commentPropertyTarget } from "@shared/comment-property-target";
6
+ import {
7
+ IconLayoutSidebarRight,
8
+ IconMessage,
9
+ IconMessagePlus,
10
+ } from "@tabler/icons-react";
11
+ import { useQueryClient } from "@tanstack/react-query";
12
+ import {
13
+ type MutableRefObject,
14
+ useContext,
15
+ useEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ } from "react";
20
+ import { toast } from "sonner";
21
+
22
+ import {
23
+ Tooltip,
24
+ TooltipContent,
25
+ TooltipTrigger,
26
+ } from "@/components/ui/tooltip";
27
+ import {
28
+ type CommentThread,
29
+ collectionRowsShowingDocumentFilter,
30
+ useComments,
31
+ } from "@/hooks/use-comments";
32
+ import { cn } from "@/lib/utils";
33
+
34
+ import {
35
+ CommentDraftProvider,
36
+ useCommentPanelSession,
37
+ } from "../comment-drafts";
38
+ import { CommentIconButton } from "../CommentRow";
39
+ import {
40
+ CommentsSidebar,
41
+ useCommentReplyDrafts,
42
+ usePendingCommentDraft,
43
+ } from "../CommentsSidebar";
44
+ import { deviceCannotHover } from "./ContentTable";
45
+ import {
46
+ DatabaseItemCommentsContext,
47
+ databaseCommentMarkerClassName,
48
+ } from "./database-item-comments";
49
+
50
+ type CellCommentCard = {
51
+ /** `start` opens on an empty composer, `read` on the cell's open threads. */
52
+ mode: "start" | "read";
53
+ key: number;
54
+ };
55
+
56
+ const NO_THREADS: CommentThread[] = [];
57
+
58
+ // Cards holding unsent text. While one does, no other cell opens a card,
59
+ // which would replace the row's saved draft with an empty one.
60
+ const cardsHoldingText = new Set<MutableRefObject<boolean>>();
61
+
62
+ /** Who can start, and who can resolve, Comments on a row's Page. */
63
+ export function databaseItemCommentAccess(item: ContentDatabaseItem) {
64
+ const { document } = item;
65
+ if (document.source?.mode === "local-files") {
66
+ return { canComment: false, canResolve: false };
67
+ }
68
+ const role = document.accessRole;
69
+ return {
70
+ canComment:
71
+ document.canComment ??
72
+ (role === "owner" ||
73
+ role === "admin" ||
74
+ role === "editor" ||
75
+ role === "commenter"),
76
+ canResolve: document.canEdit === true,
77
+ };
78
+ }
79
+
80
+ function isEditableTarget(target: EventTarget | null) {
81
+ return (
82
+ target instanceof Element &&
83
+ !!target.closest("[contenteditable='true'], input, textarea")
84
+ );
85
+ }
86
+
87
+ /**
88
+ * A table cell's Comments: a marker when its Property has open threads, and a
89
+ * button on hover or focus that starts one. Both open a card on the cell, so
90
+ * reading, replying, and starting a thread happen without leaving the table.
91
+ * Without a provider, cells show no Comment controls.
92
+ */
93
+ export function DatabaseCellComments({
94
+ item,
95
+ property,
96
+ rowSelected = false,
97
+ besideEditButton = false,
98
+ }: {
99
+ item: ContentDatabaseItem;
100
+ property: DocumentProperty;
101
+ /**
102
+ * Touch screens can't hover, so there the Comment button shows on a
103
+ * selected row's cells, like the row's selection controls.
104
+ */
105
+ rowSelected?: boolean;
106
+ /** The cell's value editor keeps its own button at the cell's end. */
107
+ besideEditButton?: boolean;
108
+ }) {
109
+ const t = useT();
110
+ const { session } = useSession();
111
+ const openComments = useContext(DatabaseItemCommentsContext);
112
+ const [card, setCard] = useState<CellCommentCard | null>(null);
113
+ const cardKeyRef = useRef(0);
114
+ const openerRef = useRef<HTMLButtonElement | null>(null);
115
+ const controlsRef = useRef<HTMLSpanElement>(null);
116
+ const startRef = useRef<HTMLButtonElement>(null);
117
+ // Set while the card holds unsent text, so a stray click or Escape keeps it.
118
+ const keepOpenRef = useRef(false);
119
+ // Set when focus belongs elsewhere once the card closes: wherever the user
120
+ // clicked, or the row's peek, which closes if focus returns to the table.
121
+ const skipFocusReturnRef = useRef(false);
122
+ const [layer, setLayer] = useState<HTMLDivElement | null>(null);
123
+ const [overflowing, setOverflowing] = useState(false);
124
+ useEffect(() => {
125
+ if (!layer || typeof ResizeObserver === "undefined") return;
126
+ const measure = () =>
127
+ setOverflowing(layer.scrollHeight > layer.clientHeight + 1);
128
+ const observer = new ResizeObserver(measure);
129
+ observer.observe(layer);
130
+ for (const child of layer.children) observer.observe(child);
131
+ measure();
132
+ return () => observer.disconnect();
133
+ }, [layer, card?.key]);
134
+
135
+ if (!openComments) return null;
136
+ const { canComment, canResolve } = databaseItemCommentAccess(item);
137
+ const propertyId = property.definition.id;
138
+ const name = property.definition.name;
139
+ const summary = item.comments?.properties[propertyId];
140
+ if (!summary && !canComment && !card) return null;
141
+
142
+ const open = (mode: CellCommentCard["mode"], opener: HTMLButtonElement) => {
143
+ // A card holding unsent text stays as it is, and no other card opens.
144
+ if (cardsHoldingText.size > 0) return;
145
+ if (card?.mode === mode) {
146
+ close();
147
+ return;
148
+ }
149
+ // Switching an open card to a new comment remounts only the card, so
150
+ // the composer takes focus once the clicked button lets go of it.
151
+ if (card && mode === "start") opener.blur();
152
+ openerRef.current = opener;
153
+ skipFocusReturnRef.current = false;
154
+ setCard({ mode, key: ++cardKeyRef.current });
155
+ };
156
+ const close = () => setCard(null);
157
+ // Once its last thread is resolved, the marker that opened the card goes
158
+ // away, so focus returns to the cell's Comment button instead.
159
+ const settle = () => {
160
+ const start = startRef.current;
161
+ // A touch screen hides it outside a selected row.
162
+ if (start && (start.checkVisibility?.() ?? true)) {
163
+ openerRef.current = start;
164
+ }
165
+ close();
166
+ };
167
+ const startLabel = t("comments.commentOnProperty", { name });
168
+ const markerLabel = summary
169
+ ? t("comments.openPropertyComments", { count: summary.open, name })
170
+ : "";
171
+
172
+ return (
173
+ <PopoverPrimitive.Root
174
+ open={!!card}
175
+ onOpenChange={(next) => {
176
+ if (!next) close();
177
+ }}
178
+ >
179
+ <PopoverPrimitive.Anchor asChild>
180
+ <span
181
+ className="pointer-events-none absolute inset-0"
182
+ aria-hidden="true"
183
+ />
184
+ </PopoverPrimitive.Anchor>
185
+ <span
186
+ ref={controlsRef}
187
+ className="relative ms-auto flex min-h-6 shrink-0 items-center"
188
+ >
189
+ {canComment ? (
190
+ <Tooltip>
191
+ <TooltipTrigger asChild>
192
+ <button
193
+ ref={startRef}
194
+ type="button"
195
+ className={cn(
196
+ "pointer-events-none absolute end-full top-0 flex size-6 items-center justify-center rounded bg-background text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-focus-within/cell:pointer-events-auto group-focus-within/cell:opacity-100 group-hover/cell:pointer-events-auto group-hover/cell:opacity-100 [@media(any-hover:none)]:hidden",
197
+ besideEditButton ? "me-6" : "me-0.5",
198
+ card?.mode === "start" && "pointer-events-auto opacity-100",
199
+ rowSelected &&
200
+ "[@media(any-hover:none)]:pointer-events-auto [@media(any-hover:none)]:flex [@media(any-hover:none)]:opacity-100",
201
+ )}
202
+ aria-label={startLabel}
203
+ aria-haspopup="dialog"
204
+ aria-expanded={card?.mode === "start"}
205
+ data-database-cell-comment-start
206
+ onClick={(event) => {
207
+ event.stopPropagation();
208
+ // A card is cramped under a phone's keyboard, so touch
209
+ // screens write the comment in the row's peek.
210
+ if (deviceCannotHover()) {
211
+ if (keepOpenRef.current) return;
212
+ // The peek closes if focus returns to the table.
213
+ skipFocusReturnRef.current = true;
214
+ close();
215
+ openComments(item, null, propertyId);
216
+ return;
217
+ }
218
+ open("start", event.currentTarget);
219
+ }}
220
+ >
221
+ <IconMessagePlus className="size-3.5" aria-hidden="true" />
222
+ </button>
223
+ </TooltipTrigger>
224
+ <TooltipContent>{startLabel}</TooltipContent>
225
+ </Tooltip>
226
+ ) : null}
227
+ {summary ? (
228
+ <Tooltip>
229
+ <TooltipTrigger asChild>
230
+ <button
231
+ type="button"
232
+ className={databaseCommentMarkerClassName}
233
+ aria-label={markerLabel}
234
+ aria-haspopup="dialog"
235
+ aria-expanded={card?.mode === "read"}
236
+ data-database-cell-comments={summary.open}
237
+ onClick={(event) => {
238
+ event.stopPropagation();
239
+ open("read", event.currentTarget);
240
+ }}
241
+ >
242
+ <IconMessage className="size-3.5" aria-hidden="true" />
243
+ {summary.open > 1 ? summary.open : null}
244
+ </button>
245
+ </TooltipTrigger>
246
+ <TooltipContent>{markerLabel}</TooltipContent>
247
+ </Tooltip>
248
+ ) : null}
249
+ </span>
250
+ <PopoverPrimitive.Portal>
251
+ <PopoverPrimitive.Content
252
+ ref={setLayer}
253
+ align="end"
254
+ sideOffset={4}
255
+ collisionPadding={12}
256
+ className="database-cell-comment-card"
257
+ data-database-cell-comment-card={card?.mode}
258
+ data-overflowing={overflowing ? "" : undefined}
259
+ onOpenAutoFocus={(event) => {
260
+ if (card?.mode !== "start") return;
261
+ // The composer takes focus once it mounts, but only from the page.
262
+ event.preventDefault();
263
+ if (globalThis.document.activeElement instanceof HTMLElement) {
264
+ globalThis.document.activeElement.blur();
265
+ }
266
+ }}
267
+ onCloseAutoFocus={(event) => {
268
+ event.preventDefault();
269
+ const opener = openerRef.current;
270
+ if (!skipFocusReturnRef.current && opener?.isConnected) {
271
+ opener.focus({ preventScroll: true });
272
+ }
273
+ }}
274
+ onEscapeKeyDown={(event) => {
275
+ // A composer's Escape closes its menus or cancels an empty draft.
276
+ if (keepOpenRef.current || isEditableTarget(event.target)) {
277
+ event.preventDefault();
278
+ }
279
+ }}
280
+ onInteractOutside={(event) => {
281
+ // The cell's own buttons switch or close the card when clicked.
282
+ if (
283
+ keepOpenRef.current ||
284
+ (event.target instanceof Node &&
285
+ controlsRef.current?.contains(event.target))
286
+ ) {
287
+ event.preventDefault();
288
+ } else {
289
+ skipFocusReturnRef.current = true;
290
+ }
291
+ }}
292
+ >
293
+ {card ? (
294
+ <CommentDraftProvider
295
+ documentId={item.document.id}
296
+ currentUserEmail={session?.email}
297
+ currentUserOrgId={session?.orgId}
298
+ >
299
+ <DatabaseCellCommentCard
300
+ key={card.key}
301
+ item={item}
302
+ property={property}
303
+ mode={card.mode}
304
+ canComment={canComment}
305
+ canResolve={canResolve}
306
+ currentUserEmail={session?.email ?? undefined}
307
+ currentUserOrgId={session?.orgId ?? undefined}
308
+ keepOpenRef={keepOpenRef}
309
+ onClose={close}
310
+ onSettled={settle}
311
+ onOpenRow={(threadId) => {
312
+ // The peek returns focus to the cell when it closes.
313
+ skipFocusReturnRef.current = true;
314
+ openerRef.current?.focus({ preventScroll: true });
315
+ close();
316
+ openComments(item, threadId);
317
+ }}
318
+ />
319
+ </CommentDraftProvider>
320
+ ) : null}
321
+ </PopoverPrimitive.Content>
322
+ </PopoverPrimitive.Portal>
323
+ </PopoverPrimitive.Root>
324
+ );
325
+ }
326
+
327
+ function DatabaseCellCommentCard({
328
+ item,
329
+ property,
330
+ mode,
331
+ canComment,
332
+ canResolve,
333
+ currentUserEmail,
334
+ currentUserOrgId,
335
+ keepOpenRef,
336
+ onClose,
337
+ onSettled,
338
+ onOpenRow,
339
+ }: {
340
+ item: ContentDatabaseItem;
341
+ property: DocumentProperty;
342
+ mode: CellCommentCard["mode"];
343
+ canComment: boolean;
344
+ canResolve: boolean;
345
+ currentUserEmail?: string;
346
+ currentUserOrgId?: string;
347
+ keepOpenRef: MutableRefObject<boolean>;
348
+ onClose: () => void;
349
+ onSettled: () => void;
350
+ onOpenRow: (threadId: string) => void;
351
+ }) {
352
+ const t = useT();
353
+ const queryClient = useQueryClient();
354
+ const documentId = item.document.id;
355
+ const propertyId = property.definition.id;
356
+ const { data: threads, isLoading, refetch } = useComments(documentId);
357
+ // The marker can be newer than this row's cached threads, so a card opened
358
+ // from it reads the threads again before deciding the cell has none.
359
+ const [listedSinceOpen, setListedSinceOpen] = useState(mode === "start");
360
+ const replyDrafts = useCommentReplyDrafts(documentId, currentUserEmail);
361
+ const {
362
+ pendingComment,
363
+ setPendingComment,
364
+ changePendingComment,
365
+ completePendingComment,
366
+ } = usePendingCommentDraft(documentId);
367
+ // After posting, the card shows the new thread rather than every thread.
368
+ const [postedThreadId, setPostedThreadId] = useState<string | null>(null);
369
+
370
+ useEffect(() => {
371
+ if (mode === "read") {
372
+ const marked = item.comments?.properties[propertyId]?.open ?? 0;
373
+ void refetch()
374
+ .then(({ data, error }) => {
375
+ // With nothing cached either, the card closes; say why.
376
+ if (!data && error) {
377
+ toast.error(t("empty.genericError"), {
378
+ description: error instanceof Error ? error.message : undefined,
379
+ });
380
+ return;
381
+ }
382
+ const open = data?.filter(
383
+ (thread) =>
384
+ thread.property?.propertyId === propertyId && !thread.resolved,
385
+ ).length;
386
+ // A marker that no longer matches refreshes the row's counts.
387
+ if (open !== undefined && open !== marked) {
388
+ void queryClient.invalidateQueries({
389
+ ...collectionRowsShowingDocumentFilter(documentId),
390
+ refetchType: "active",
391
+ });
392
+ }
393
+ })
394
+ .finally(() => setListedSinceOpen(true));
395
+ return;
396
+ }
397
+ setPendingComment({
398
+ quotedText: "",
399
+ offsetTop: 0,
400
+ property: commentPropertyTarget(property),
401
+ databaseId: item.databaseId,
402
+ });
403
+ // Runs once per card; the card remounts each time it opens.
404
+ // eslint-disable-next-line react-hooks/exhaustive-deps
405
+ }, []);
406
+
407
+ const propertyThreads = useMemo(
408
+ () =>
409
+ threads?.filter((thread) => thread.property?.propertyId === propertyId) ??
410
+ NO_THREADS,
411
+ [threads, propertyId],
412
+ );
413
+ const openThreads = propertyThreads.filter(
414
+ (thread) =>
415
+ !thread.resolved &&
416
+ (!postedThreadId || thread.threadId === postedThreadId),
417
+ );
418
+ const postedThread = postedThreadId
419
+ ? propertyThreads.find((thread) => thread.threadId === postedThreadId)
420
+ : undefined;
421
+ // A thread being resolved stays on screen until the server confirms it,
422
+ // so a failed resolve leaves it in the card with its error.
423
+ const { isResolving } = useCommentPanelSession();
424
+ const resolvingIds = propertyThreads
425
+ .filter((thread) => isResolving(thread.threadId))
426
+ .map((thread) => thread.threadId)
427
+ .join(" ");
428
+ const shownThreads = useMemo(() => {
429
+ if (!resolvingIds) return propertyThreads;
430
+ const resolving = new Set(resolvingIds.split(" "));
431
+ return propertyThreads.map((thread) =>
432
+ thread.resolved && resolving.has(thread.threadId)
433
+ ? { ...thread, resolved: false }
434
+ : thread,
435
+ );
436
+ }, [propertyThreads, resolvingIds]);
437
+ // Resolving the last thread, or the one just posted, closes the card, as
438
+ // does a read that found none or failed.
439
+ const settled =
440
+ !pendingComment &&
441
+ !resolvingIds &&
442
+ (postedThreadId
443
+ ? postedThread?.resolved === true
444
+ : mode === "read" && listedSinceOpen && openThreads.length === 0);
445
+ useEffect(() => {
446
+ if (settled) onSettled();
447
+ }, [settled, onSettled]);
448
+
449
+ const openReplyThreadId = replyDrafts.openReply?.threadId ?? null;
450
+ // Text still being written or sent; a failed send puts it back.
451
+ const unsent =
452
+ !!pendingComment?.submitting ||
453
+ !!pendingComment?.text.trim() ||
454
+ (!!openReplyThreadId && !!replyDrafts.get(openReplyThreadId).text.trim()) ||
455
+ propertyThreads.some((thread) => replyDrafts.isSubmitting(thread.threadId));
456
+ useEffect(() => {
457
+ keepOpenRef.current = unsent;
458
+ if (unsent) cardsHoldingText.add(keepOpenRef);
459
+ return () => {
460
+ keepOpenRef.current = false;
461
+ cardsHoldingText.delete(keepOpenRef);
462
+ };
463
+ }, [keepOpenRef, unsent]);
464
+
465
+ return (
466
+ <CommentsSidebar
467
+ compact
468
+ surface="popover"
469
+ presentation="inline"
470
+ alignToAnchors={false}
471
+ forceVisible
472
+ replyDrafts={replyDrafts}
473
+ documentId={documentId}
474
+ // While composing, the card shows only the new comment.
475
+ threads={pendingComment ? NO_THREADS : shownThreads}
476
+ visibleThreadId={postedThreadId}
477
+ documentProperties={item.properties}
478
+ isLoading={
479
+ !pendingComment &&
480
+ (isLoading || (!listedSinceOpen && openThreads.length === 0))
481
+ }
482
+ pendingComment={pendingComment}
483
+ onPendingChange={changePendingComment}
484
+ onPendingDone={(id, threadId) => {
485
+ if (!completePendingComment(id)) return;
486
+ if (threadId) setPostedThreadId(threadId);
487
+ else onClose();
488
+ }}
489
+ currentUserEmail={currentUserEmail}
490
+ currentUserOrgId={currentUserOrgId}
491
+ canComment={canComment}
492
+ canResolve={canResolve}
493
+ onClose={onClose}
494
+ renderThreadActions={(thread) =>
495
+ thread.threadId.startsWith("optimistic-") ? null : (
496
+ <Tooltip>
497
+ <TooltipTrigger asChild>
498
+ <CommentIconButton
499
+ aria-label={t("comments.openRow")}
500
+ data-database-cell-comment-open-row
501
+ onClick={(event) => {
502
+ event.stopPropagation();
503
+ onOpenRow(thread.threadId);
504
+ }}
505
+ >
506
+ <IconLayoutSidebarRight size={18} />
507
+ </CommentIconButton>
508
+ </TooltipTrigger>
509
+ <TooltipContent>{t("comments.openRow")}</TooltipContent>
510
+ </Tooltip>
511
+ )
512
+ }
513
+ />
514
+ );
515
+ }
@@ -11,17 +11,19 @@ import {
11
11
 
12
12
  /**
13
13
  * Opens a row with its Comments showing: one thread when `threadId` is set,
14
- * otherwise the list. Without a provider, rows show no Comment markers.
14
+ * a new comment on the Property `propertyId` names, otherwise the list.
15
+ * Without a provider, rows show no Comment markers.
15
16
  */
16
17
  export type OpenDatabaseItemComments = (
17
18
  item: ContentDatabaseItem,
18
19
  threadId: string | null,
20
+ propertyId?: string,
19
21
  ) => void;
20
22
 
21
23
  export const DatabaseItemCommentsContext =
22
24
  createContext<OpenDatabaseItemComments | null>(null);
23
25
 
24
- const markerClassName =
26
+ export const databaseCommentMarkerClassName =
25
27
  "flex h-6 min-w-6 shrink-0 items-center justify-center gap-0.5 rounded px-1 text-xs font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring comment-marker";
26
28
 
27
29
  /** The count of a row's open Comment threads, beside its name. */
@@ -43,7 +45,7 @@ export function DatabaseRowCommentCount({
43
45
  <TooltipTrigger asChild>
44
46
  <button
45
47
  type="button"
46
- className={markerClassName}
48
+ className={databaseCommentMarkerClassName}
47
49
  aria-label={label}
48
50
  data-database-row-comments={count}
49
51
  onClick={(event) => {
@@ -59,43 +61,3 @@ export function DatabaseRowCommentCount({
59
61
  </Tooltip>
60
62
  );
61
63
  }
62
-
63
- /** Marks a cell whose Property has open Comment threads. */
64
- export function DatabaseCellCommentMarker({
65
- item,
66
- propertyId,
67
- propertyName,
68
- }: {
69
- item: ContentDatabaseItem;
70
- propertyId: string;
71
- propertyName: string;
72
- }) {
73
- const t = useT();
74
- const openComments = useContext(DatabaseItemCommentsContext);
75
- const summary = item.comments?.properties[propertyId];
76
- if (!openComments || !summary) return null;
77
- const label = t("comments.openPropertyComments", {
78
- count: summary.open,
79
- name: propertyName,
80
- });
81
- return (
82
- <Tooltip>
83
- <TooltipTrigger asChild>
84
- <button
85
- type="button"
86
- className={markerClassName}
87
- aria-label={label}
88
- data-database-cell-comments={summary.open}
89
- onClick={(event) => {
90
- event.stopPropagation();
91
- openComments(item, summary.latestThreadId);
92
- }}
93
- >
94
- <IconMessage className="size-3.5" aria-hidden="true" />
95
- {summary.open > 1 ? summary.open : null}
96
- </button>
97
- </TooltipTrigger>
98
- <TooltipContent>{label}</TooltipContent>
99
- </Tooltip>
100
- );
101
- }