Skip to content
Source
Sign inSign up

threadActionMenu.logic.ts

269 lines7.7 KB

1import type { ContextMenuItem } from "@t3tools/contracts";
2import type { SnoozePreset } from "@t3tools/client-runtime/state/thread-settled";
3​
4export type ThreadActionMenuId =
5 | "find"
6 | "new-thread-on-branch"
7 | "filter-by-project"
8 | "turn-into-project"
9 | "project-settings"
10 | "pin"
11 | "unpin"
12 | "settle"
13 | "unsettle"
14 | "auto-settle"
15 | "auto-settle:enabled"
16 | "auto-settle:disabled"
17 | "snooze"
18 | `snooze:${string}`
19 | "unsnooze"
20 | "rename"
21 | "regenerate-title"
22 | "mark-unread"
23 | "share-guest"
24 | "copy"
25 | "copy-path"
26 | "copy-branch"
27 | "copy-thread"
28 | "copy-thread-id"
29 | "archive"
30 | "delete";
31​
32export type DraftActionMenuId =
33 | "copy"
34 | "copy-path"
35 | "copy-branch"
36 | "project-settings"
37 | "discard";
38​
39export function buildDraftActionMenuItems(options: {
40 readonly hasPath: boolean;
41 readonly hasBranch: boolean;
42 readonly hasProject: boolean;
43}): ReadonlyArray<ContextMenuItem<DraftActionMenuId>> {
44 return [
45 {
46 id: "copy",
47 label: "Copy",
48 icon: "copy",
49 disabled: !options.hasPath && !options.hasBranch,
50 children: [
51 ...(options.hasPath ? [{ id: "copy-path" as const, label: "Path", icon: "folder" }] : []),
52 ...(options.hasBranch
53 ? [{ id: "copy-branch" as const, label: "Branch", icon: "git-branch" }]
54 : []),
55 ],
56 },
57 ...(options.hasProject
58 ? [{ id: "project-settings" as const, label: "Project settings", icon: "settings" }]
59 : []),
60 {
61 id: "discard",
62 label: "Discard draft",
63 icon: "trash",
64 destructive: true,
65 separatorBefore: true,
66 },
67 ];
69​
70export interface ThreadActionMenuState {
71 readonly canOperate: boolean;
72 readonly canFind?: boolean;
73 readonly branch: string | null;
74 readonly projectFilter: {
75 readonly label: string;
76 readonly isActive: boolean;
77 } | null;
78 readonly isPinned: boolean;
79 readonly isSettled: boolean;
80 readonly autoSettleEnabled: boolean;
81 readonly isSnoozed: boolean;
82 readonly canSnoozeNow: boolean;
83 readonly isRegeneratingTitle: boolean;
84 /** Archive rejects attached providers, so disable it while running. */
85 readonly isRunning: boolean;
86 readonly supports: {
87 readonly settlement: boolean;
88 readonly autoSettleOptOut: boolean;
89 readonly snooze: boolean;
90 readonly pinning: boolean;
91 readonly titleRegeneration: boolean;
92 };
93 readonly snoozePresets: ReadonlyArray<SnoozePreset>;
94 /** Guest links need their own grant, so the caller checks it instead of `canOperate`. */
95 readonly canShareWithGuest?: boolean;
96 /** A No project thread with its own folder, on a server that can promote it. */
97 readonly canTurnIntoProject?: boolean;
99​
100/** Local navigation, read markers, and copying remain available to read-only clients. */
101export function threadActionRequiresOperate(action: ThreadActionMenuId): boolean {
102 return ![
103 "find",
104 "new-thread-on-branch",
105 "project-settings",
106 "mark-unread",
107 "share-guest",
108 "copy",
109 "copy-path",
110 "copy-branch",
111 "copy-thread",
112 "copy-thread-id",
113 ].includes(action);
115​
116export function buildThreadActionMenuItems(
117 state: ThreadActionMenuState,
118): ReadonlyArray<ContextMenuItem<ThreadActionMenuId>> {
119 const items: ReadonlyArray<ContextMenuItem<ThreadActionMenuId>> = [
120 ...(state.canFind
121 ? [{ id: "find" as const, label: "Find in thread", icon: "text-search" }]
122 : []),
123 ...(state.branch
124 ? [
125 {
126 id: "new-thread-on-branch" as const,
127 label: `New thread on ${state.branch}`,
128 icon: "message-square-plus",
129 },
130 ]
131 : []),
132 ...(state.supports.pinning
133 ? [
134 state.isPinned
135 ? { id: "unpin" as const, label: "Unpin thread", icon: "pin-off" }
136 : { id: "pin" as const, label: "Pin thread", icon: "pin" },
137 ]
138 : []),
139 ...(state.supports.settlement
140 ? [
141 state.isSettled
142 ? { id: "unsettle" as const, label: "Un-settle thread", icon: "circle-check" }
143 : { id: "settle" as const, label: "Settle thread", icon: "circle-check" },
144 ]
145 : []),
146 ...(state.supports.snooze
147 ? [
148 state.isSnoozed
149 ? { id: "unsnooze" as const, label: "Wake thread", icon: "clock" }
150 : {
151 id: "snooze" as const,
152 label: "Snooze",
153 icon: "clock",
154 disabled: !state.canSnoozeNow,
155 children: [
156 ...state.snoozePresets.map((preset) => ({
157 id: `snooze:${preset.id}` as const,
158 label: preset.label,
159 detail: preset.whenLabel,
160 })),
161 {
162 id: "snooze:custom" as const,
163 label: "Custom…",
164 separatorBefore: state.snoozePresets.length > 0,
165 },
166 ],
167 },
168 ]
169 : []),
170 { id: "rename", label: "Rename thread", icon: "pencil", separatorBefore: true },
171 ...(state.supports.titleRegeneration
172 ? [
173 {
174 id: "regenerate-title" as const,
175 label: state.isRegeneratingTitle ? "Regenerating…" : "Regenerate title",
176 icon: "refresh-cw",
177 disabled: state.isRegeneratingTitle,
178 },
179 ]
180 : []),
181 { id: "mark-unread", label: "Mark unread", icon: "mail-open" },
182 ...(state.canShareWithGuest
183 ? [{ id: "share-guest" as const, label: "Share with guest", icon: "user-plus" }]
184 : []),
185 ...(state.projectFilter
186 ? [
187 {
188 id: "filter-by-project" as const,
189 label: state.projectFilter.isActive
190 ? "Show all projects"
191 : `Filter by ${state.projectFilter.label}`,
192 icon: "folder-tree",
193 },
194 ]
195 : []),
196 ...(state.canTurnIntoProject
197 ? [
198 {
199 id: "turn-into-project" as const,
200 label: "Turn into project",
201 icon: "folder-plus",
202 disabled: state.isRunning,
203 },
204 ]
205 : []),
206 ...(state.supports.autoSettleOptOut
207 ? [
208 {
209 id: "auto-settle" as const,
210 label: "Auto-settle behavior",
211 icon: "timer",
212 children: [
213 {
214 id: "auto-settle:enabled" as const,
215 label: "Follow settings",
216 checked: state.autoSettleEnabled,
217 },
218 {
219 id: "auto-settle:disabled" as const,
220 label: "Never",
221 checked: !state.autoSettleEnabled,
222 },
223 ],
224 },
225 ]
226 : []),
227 {
228 id: "copy",
229 label: "Copy",
230 icon: "copy",
231 separatorBefore: true,
232 children: [
233 { id: "copy-path", label: "Path", icon: "folder" },
234 ...(state.branch
235 ? [{ id: "copy-branch" as const, label: "Branch", icon: "git-branch" }]
236 : []),
237 { id: "copy-thread", label: "Thread", icon: "message-square" },
238 { id: "copy-thread-id", label: "Thread ID", icon: "hash" },
239 ],
240 },
241 { id: "project-settings", label: "Project settings", icon: "settings" },
242 {
243 id: "archive",
244 label: "Archive thread",
245 icon: "archive",
246 disabled: state.isRunning,
247 separatorBefore: true,
248 },
249 {
250 id: "delete",
251 label: "Delete",
252 destructive: true,
253 icon: "trash",
254 },
255 ];
256 return state.canOperate
257 ? items
258 : items.map((item) =>
259 threadActionRequiresOperate(item.id)
260 ? {
261 ...item,
262 disabled: true,
263 ...(item.children
264 ? { children: item.children.map((child) => ({ ...child, disabled: true })) }
265 : {}),
266 }
267 : item,
268 );