@tuturuuu/utils 0.0.2 → 0.6.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 (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +122 -3
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,313 @@
1
+ import moment from 'moment';
2
+
3
+ export function timetzToTime(timetz: string) {
4
+ // Find the position of the '+' or '-' that indicates the start of the offset
5
+ const offsetPos = Math.max(timetz.lastIndexOf('+'), timetz.lastIndexOf('-'));
6
+
7
+ // Split the input string into the time and offset parts
8
+ const time = timetz.substring(0, offsetPos);
9
+ const offsetStr = timetz.substring(offsetPos);
10
+
11
+ // Split the time into hours and minutes
12
+ const [hourStr, minuteStr] = time.split(':');
13
+
14
+ // Parse the hour, minute, and offset as integers
15
+ const hour = parseInt(hourStr ?? '0', 10);
16
+ const minute = parseInt(minuteStr ?? '0', 10);
17
+ const offset = parseInt(offsetStr, 10);
18
+
19
+ // Get the current date and time
20
+ const date = new Date();
21
+
22
+ // Get the current user's timezone offset in hours
23
+ const currentUserOffset = -date.getTimezoneOffset() / 60;
24
+
25
+ // Calculate the difference between the user's timezone and the offset
26
+ const offsetDiff = currentUserOffset - offset;
27
+
28
+ // Set the hour and minute to the input time, adjusted by the offset difference
29
+ date.setHours(hour + offsetDiff);
30
+ date.setMinutes(minute);
31
+
32
+ // Format the hour and minute with leading zeros if necessary
33
+ const hourFormatted = date.getHours().toString().padStart(2, '0');
34
+ const minuteFormatted = date.getMinutes().toString().padStart(2, '0');
35
+
36
+ // Return the time in the user's timezone
37
+ return `${hourFormatted}:${minuteFormatted}`;
38
+ }
39
+
40
+ export function timetzToHour(timetz?: string) {
41
+ if (!timetz) return undefined;
42
+ const [hourStr] = timetzToTime(timetz).split(':');
43
+ return parseInt(hourStr ?? '0', 10);
44
+ }
45
+
46
+ export function compareTimetz(timetz1: string, timetz2: string) {
47
+ const time1 = timetzToTime(timetz1);
48
+ const time2 = timetzToTime(timetz2);
49
+ return time1.localeCompare(time2);
50
+ }
51
+
52
+ export function minTimetz(timetz1: string, timetz2: string) {
53
+ return compareTimetz(timetz1, timetz2) < 0 ? timetz1 : timetz2;
54
+ }
55
+
56
+ export function maxTimetz(timetz1: string, timetz2: string) {
57
+ return compareTimetz(timetz1, timetz2) > 0 ? timetz1 : timetz2;
58
+ }
59
+
60
+ /**
61
+ * Parses timezone offset from a time string and returns a formatted offset string
62
+ * @param timeString - String like "11:00:00+07:00" or "14:30:00-05:30"
63
+ * @returns Formatted offset string like "+07:00" or "-05:30"
64
+ */
65
+ export function parseTimezoneOffset(timeString: string): string {
66
+ if (!timeString) return '';
67
+
68
+ // Find the last occurrence of '+' or '-' which indicates the timezone offset
69
+ const lastPlusIndex = timeString.lastIndexOf('+');
70
+ const lastMinusIndex = timeString.lastIndexOf('-');
71
+
72
+ // If no offset found, return empty string
73
+ if (lastPlusIndex === -1 && lastMinusIndex === -1) {
74
+ return '';
75
+ }
76
+
77
+ // Determine which offset to use (take the last one if both exist)
78
+ const offsetIndex = Math.max(lastPlusIndex, lastMinusIndex);
79
+
80
+ // Extract the offset part
81
+ const offsetPart = timeString.substring(offsetIndex);
82
+
83
+ // Handle cases where the offset is already in HH:MM format like "+05:30"
84
+ if (offsetPart.includes(':')) {
85
+ // Already in HH:MM format, return as-is
86
+ return offsetPart;
87
+ }
88
+
89
+ // Handle legacy decimal format like "+5.5" (for backward compatibility)
90
+ const offset = parseFloat(offsetPart);
91
+
92
+ // Handle NaN case
93
+ if (Number.isNaN(offset)) {
94
+ return '';
95
+ }
96
+
97
+ // Convert decimal hours to HH:MM format
98
+ const hours = Math.floor(Math.abs(offset));
99
+ const minutes = Math.round((Math.abs(offset) - hours) * 60);
100
+
101
+ // Format as HH:MM
102
+ const formattedHours = hours.toString().padStart(2, '0');
103
+ const formattedMinutes = minutes.toString().padStart(2, '0');
104
+
105
+ const sign = offset >= 0 ? '+' : '-';
106
+ return `${sign}${formattedHours}:${formattedMinutes}`;
107
+ }
108
+
109
+ /**
110
+ * Formats timezone offset for display in UI
111
+ * @param timeString - String like "11:00:00+07" or "14:30:00-05"
112
+ * @returns Formatted string like "UTC+07:00" or "UTC-05:30"
113
+ */
114
+ export function formatTimezoneOffset(timeString: string): string {
115
+ if (!timeString) return '';
116
+
117
+ const offset = parseTimezoneOffset(timeString);
118
+ if (!offset) return '';
119
+
120
+ return `UTC${offset}`;
121
+ }
122
+
123
+ export type DateRangeOption = 'present' | 'past' | 'future';
124
+ export type DateRangeUnit =
125
+ | 'day'
126
+ | 'week'
127
+ | 'month'
128
+ | 'year'
129
+ | 'all'
130
+ | 'custom';
131
+
132
+ export type DateRange = [Date | null, Date | null];
133
+
134
+ export const getDateRange = (
135
+ unit: DateRangeUnit,
136
+ option: DateRangeOption
137
+ ): DateRange => {
138
+ const start = moment();
139
+ const end = moment();
140
+
141
+ switch (unit) {
142
+ case 'day':
143
+ switch (option) {
144
+ case 'present':
145
+ start.startOf('day');
146
+ end.endOf('day');
147
+ break;
148
+
149
+ case 'past':
150
+ start.subtract(1, 'day').startOf('day');
151
+ end.subtract(1, 'day').endOf('day');
152
+ break;
153
+
154
+ case 'future':
155
+ start.add(1, 'day').startOf('day');
156
+ end.add(1, 'day').endOf('day');
157
+ break;
158
+ }
159
+ break;
160
+
161
+ case 'week':
162
+ switch (option) {
163
+ case 'present':
164
+ start.startOf('week');
165
+ end.endOf('week');
166
+ break;
167
+
168
+ case 'past':
169
+ start.subtract(1, 'week').startOf('week');
170
+ end.subtract(1, 'week').endOf('week');
171
+ break;
172
+
173
+ case 'future':
174
+ start.add(1, 'week').startOf('week');
175
+ end.add(1, 'week').endOf('week');
176
+ break;
177
+ }
178
+ break;
179
+
180
+ case 'month':
181
+ switch (option) {
182
+ case 'present':
183
+ start.startOf('month');
184
+ end.endOf('month');
185
+ break;
186
+
187
+ case 'past':
188
+ start.subtract(1, 'month').startOf('month');
189
+ end.subtract(1, 'month').endOf('month');
190
+ break;
191
+
192
+ case 'future':
193
+ start.add(1, 'month').startOf('month');
194
+ end.add(1, 'month').endOf('month');
195
+ break;
196
+ }
197
+ break;
198
+
199
+ case 'year':
200
+ switch (option) {
201
+ case 'present':
202
+ start.startOf('year');
203
+ end.endOf('year');
204
+ break;
205
+
206
+ case 'past':
207
+ start.subtract(1, 'year').startOf('year');
208
+ end.subtract(1, 'year').endOf('year');
209
+ break;
210
+
211
+ case 'future':
212
+ start.add(1, 'year').startOf('year');
213
+ end.add(1, 'year').endOf('year');
214
+ break;
215
+ }
216
+ break;
217
+
218
+ case 'all':
219
+ return [null, null];
220
+
221
+ case 'custom': {
222
+ throw new Error('Not implemented yet: "custom" case');
223
+ }
224
+ }
225
+
226
+ return [start.toDate(), end.toDate()];
227
+ };
228
+
229
+ export const getDateRangeUnits = (
230
+ t: (key: string) => string
231
+ ): {
232
+ label: string;
233
+ value: DateRangeUnit;
234
+ }[] => {
235
+ return [
236
+ { label: t('date_helper.day'), value: 'day' },
237
+ { label: t('date_helper.week'), value: 'week' },
238
+ { label: t('date_helper.month'), value: 'month' },
239
+ { label: t('date_helper.year'), value: 'year' },
240
+ { label: t('date_helper.all'), value: 'all' },
241
+ { label: t('date_helper.custom'), value: 'custom' },
242
+ ];
243
+ };
244
+
245
+ export const getDateRangeOptions = (
246
+ unit: DateRangeUnit,
247
+ t: (key: string) => string
248
+ ): {
249
+ label: string;
250
+ value: DateRangeOption;
251
+ }[] => {
252
+ switch (unit) {
253
+ case 'day':
254
+ return [
255
+ { label: t('date_helper.today'), value: 'present' },
256
+ { label: t('date_helper.yesterday'), value: 'past' },
257
+ { label: t('date_helper.tomorrow'), value: 'future' },
258
+ ];
259
+
260
+ case 'week':
261
+ return [
262
+ { label: t('date_helper.this-week'), value: 'present' },
263
+ { label: t('date_helper.last-week'), value: 'past' },
264
+ { label: t('date_helper.next-week'), value: 'future' },
265
+ ];
266
+
267
+ case 'month':
268
+ return [
269
+ { label: t('date_helper.this_month'), value: 'present' },
270
+ { label: t('date_helper.last-month'), value: 'past' },
271
+ { label: t('date_helper.next-month'), value: 'future' },
272
+ ];
273
+
274
+ case 'year':
275
+ return [
276
+ { label: t('date_helper.this-year'), value: 'present' },
277
+ { label: t('date_helper.last-year'), value: 'past' },
278
+ { label: t('date_helper.next-year'), value: 'future' },
279
+ ];
280
+
281
+ case 'all':
282
+ return [{ label: t('date_helper.all'), value: 'present' }];
283
+
284
+ default:
285
+ return [];
286
+ }
287
+ };
288
+
289
+ export function calculateDuration(startDate: Date, endDate: Date): string {
290
+ // Calculate the difference in milliseconds
291
+ const diff = endDate.getTime() - startDate.getTime();
292
+
293
+ // Convert to seconds
294
+ const seconds = Math.floor(diff / 1000);
295
+
296
+ if (seconds < 60) {
297
+ return `${seconds} seconds`;
298
+ }
299
+
300
+ // Calculate hours, minutes, seconds
301
+ const hours = Math.floor(seconds / 3600);
302
+ const minutes = Math.floor((seconds % 3600) / 60);
303
+ const remainingSeconds = seconds % 60;
304
+
305
+ // Format the result based on the duration
306
+ if (hours > 0) {
307
+ return `${hours}h ${minutes}m ${remainingSeconds}s`;
308
+ } else if (minutes > 0) {
309
+ return `${minutes}m ${remainingSeconds}s`;
310
+ } else {
311
+ return `${seconds}s`;
312
+ }
313
+ }
@@ -0,0 +1,264 @@
1
+ import type { Editor } from '@tiptap/core';
2
+
3
+ export interface ConvertToTaskOptions {
4
+ editor: Editor;
5
+ listId: string;
6
+ listName: string;
7
+ createTask: (params: { name: string; listId: string }) => Promise<{
8
+ id: string;
9
+ name: string;
10
+ display_number?: number;
11
+ priority?: string;
12
+ listColor?: string;
13
+ assignees?: string;
14
+ workspaceId?: string | null;
15
+ }>;
16
+ wrapInParagraph?: boolean;
17
+ }
18
+
19
+ export interface ConvertToTaskResult {
20
+ success: boolean;
21
+ taskId?: string;
22
+ taskName?: string;
23
+ error?: {
24
+ type: 'no_selection' | 'empty_content' | 'no_lists' | 'unknown';
25
+ message: string;
26
+ description?: string;
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Converts selected text or a list item in a TipTap editor to a task and replaces it with a mention.
32
+ *
33
+ * This function supports two modes:
34
+ * 1. Selection mode: If text is highlighted/selected, uses the selected text as the task name
35
+ * and replaces the selection with a mention to the new task.
36
+ * 2. List item mode (fallback): If no text is selected but cursor is in a list item,
37
+ * uses the entire list item text and replaces the list item with a mention.
38
+ *
39
+ * @param options - Configuration options for the conversion
40
+ * @returns Result object indicating success or failure with error details
41
+ */
42
+ export async function convertListItemToTask(
43
+ options: ConvertToTaskOptions
44
+ ): Promise<ConvertToTaskResult> {
45
+ const {
46
+ editor,
47
+ listId,
48
+ listName: _listName, // Kept for backwards compatibility but unused
49
+ createTask,
50
+ wrapInParagraph = false,
51
+ } = options;
52
+
53
+ const { state } = editor;
54
+ const { selection } = state;
55
+ const { from, to, empty } = selection;
56
+
57
+ // Mode 1: Text is selected - use the selected text as task name
58
+ if (!empty) {
59
+ const selectedText = state.doc.textBetween(from, to, ' ').trim();
60
+
61
+ if (!selectedText) {
62
+ return {
63
+ success: false,
64
+ error: {
65
+ type: 'empty_content',
66
+ message: 'Empty selection',
67
+ description: 'Select some text to convert to a task',
68
+ },
69
+ };
70
+ }
71
+
72
+ try {
73
+ // Create new task using the selected text
74
+ const newTask = await createTask({
75
+ name: selectedText,
76
+ listId,
77
+ });
78
+
79
+ // Replace the selected text with a mention to the new task
80
+ const tr = state.tr;
81
+
82
+ // Delete the selected text
83
+ tr.delete(from, to);
84
+
85
+ // Check if mention node exists in schema
86
+ if (state.schema.nodes.mention) {
87
+ // Create mention node with correct attributes:
88
+ // - displayName: ticket number (e.g., "123" for #123)
89
+ // - subtitle: task name
90
+ const mentionNode = state.schema.nodes.mention.create({
91
+ entityId: newTask.id,
92
+ entityType: 'task',
93
+ displayName: newTask.display_number
94
+ ? String(newTask.display_number)
95
+ : newTask.name,
96
+ avatarUrl: null,
97
+ subtitle: newTask.name,
98
+ // Add task-specific attributes if present
99
+ priority: newTask.priority || null,
100
+ listColor: newTask.listColor || null,
101
+ assignees: newTask.assignees || null,
102
+ workspaceId: newTask.workspaceId ?? null,
103
+ });
104
+
105
+ // Insert mention at the selection start position
106
+ tr.insert(from, mentionNode);
107
+
108
+ // Add a space after the mention for better UX
109
+ const spacePos = from + mentionNode.nodeSize;
110
+ tr.insertText(' ', spacePos);
111
+ }
112
+
113
+ // Apply transaction
114
+ editor.view.dispatch(tr);
115
+
116
+ return {
117
+ success: true,
118
+ taskId: newTask.id,
119
+ taskName: newTask.name,
120
+ };
121
+ } catch (error) {
122
+ console.error('Failed to convert selection to task:', error);
123
+ return {
124
+ success: false,
125
+ error: {
126
+ type: 'unknown',
127
+ message: 'Failed to create task',
128
+ description: 'An error occurred while creating the task',
129
+ },
130
+ };
131
+ }
132
+ }
133
+
134
+ // Mode 2: No selection - fall back to list item mode
135
+ const { $from } = selection;
136
+
137
+ // Get the current node (could be listItem, taskItem, or paragraph inside them)
138
+ let currentNode = $from.parent;
139
+ const depth = $from.depth;
140
+
141
+ // Safety check: depth must be at least 1 to have a valid position
142
+ if (depth < 1) {
143
+ return {
144
+ success: false,
145
+ error: {
146
+ type: 'no_selection',
147
+ message: 'No text selected',
148
+ description:
149
+ 'Select some text or move your cursor to a list item to convert it to a task',
150
+ },
151
+ };
152
+ }
153
+
154
+ let nodePos = $from.before(depth);
155
+
156
+ // If we're inside a paragraph, get the parent list item
157
+ if (currentNode.type.name === 'paragraph') {
158
+ if (depth < 2) {
159
+ return {
160
+ success: false,
161
+ error: {
162
+ type: 'no_selection',
163
+ message: 'No text selected',
164
+ description:
165
+ 'Select some text or move your cursor to a list item to convert it to a task',
166
+ },
167
+ };
168
+ }
169
+ currentNode = $from.node(depth - 1);
170
+ nodePos = $from.before(depth - 1);
171
+ }
172
+
173
+ // Check if we're in a list item or task item
174
+ const validNodeTypes = ['listItem', 'taskItem'];
175
+ if (!validNodeTypes.includes(currentNode.type.name)) {
176
+ return {
177
+ success: false,
178
+ error: {
179
+ type: 'no_selection',
180
+ message: 'No text selected',
181
+ description:
182
+ 'Select some text or move your cursor to a list item to convert it to a task',
183
+ },
184
+ };
185
+ }
186
+
187
+ // Extract text content from the node
188
+ const textContent = currentNode.textContent.trim();
189
+ if (!textContent) {
190
+ return {
191
+ success: false,
192
+ error: {
193
+ type: 'empty_content',
194
+ message: 'Empty list item',
195
+ description: 'Add some text before converting to a task',
196
+ },
197
+ };
198
+ }
199
+
200
+ try {
201
+ // Create new task using the provided function
202
+ const newTask = await createTask({
203
+ name: textContent,
204
+ listId,
205
+ });
206
+
207
+ // Replace the list item with a mention to the new task
208
+ const tr = state.tr;
209
+
210
+ // Delete the list item node
211
+ tr.delete(nodePos, nodePos + currentNode.nodeSize);
212
+
213
+ // Check if mention node exists in schema
214
+ if (state.schema.nodes.mention) {
215
+ // Create mention node with correct attributes:
216
+ // - displayName: ticket number (e.g., "123" for #123)
217
+ // - subtitle: task name
218
+ const mentionNode = state.schema.nodes.mention.create({
219
+ entityId: newTask.id,
220
+ entityType: 'task',
221
+ displayName: newTask.display_number
222
+ ? String(newTask.display_number)
223
+ : newTask.name,
224
+ avatarUrl: null,
225
+ subtitle: newTask.name,
226
+ // Add task-specific attributes if present
227
+ priority: newTask.priority || null,
228
+ listColor: newTask.listColor || null,
229
+ assignees: newTask.assignees || null,
230
+ workspaceId: newTask.workspaceId ?? null,
231
+ });
232
+
233
+ // Wrap in paragraph if requested and paragraph node exists
234
+ if (wrapInParagraph && state.schema.nodes.paragraph) {
235
+ const paragraphNode = state.schema.nodes.paragraph.create(null, [
236
+ mentionNode,
237
+ state.schema.text(' '),
238
+ ]);
239
+ tr.insert(nodePos, paragraphNode);
240
+ } else {
241
+ tr.insert(nodePos, mentionNode);
242
+ }
243
+ }
244
+
245
+ // Apply transaction
246
+ editor.view.dispatch(tr);
247
+
248
+ return {
249
+ success: true,
250
+ taskId: newTask.id,
251
+ taskName: newTask.name,
252
+ };
253
+ } catch (error) {
254
+ console.error('Failed to convert item to task:', error);
255
+ return {
256
+ success: false,
257
+ error: {
258
+ type: 'unknown',
259
+ message: 'Failed to create task',
260
+ description: 'An error occurred while creating the task',
261
+ },
262
+ };
263
+ }
264
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ type ConvertToTaskOptions,
3
+ type ConvertToTaskResult,
4
+ convertListItemToTask,
5
+ } from './convert-to-task';