@tuturuuu/utils 0.0.3 → 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 +120 -1
  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,241 @@
1
+ /**
2
+ * Utility functions for task management and calculations
3
+ */
4
+ import type { Active, Over } from '@dnd-kit/core';
5
+ import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
6
+
7
+ interface Task {
8
+ id: string;
9
+ name: string;
10
+ description?: string;
11
+ priority?: TaskPriority | null;
12
+ created_at?: string;
13
+ updated_at?: string;
14
+ end_date?: string | null;
15
+ boardId: string;
16
+ boardName: string;
17
+ listName: string;
18
+ listStatus?: string;
19
+ archived?: boolean;
20
+ completed_at?: string;
21
+ closed_at?: string;
22
+ finished_at?: string;
23
+ done_at?: string;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ interface BoardMetrics {
28
+ id: string;
29
+ totalTasks: number;
30
+ completedTasks: number;
31
+ overdueTasks: number;
32
+ highPriorityTasks: number;
33
+ progressPercentage: number;
34
+ }
35
+
36
+ interface DraggableData {
37
+ type: 'Task' | 'Column';
38
+ task?: unknown;
39
+ column?: unknown;
40
+ }
41
+
42
+ export function hasDraggableData(
43
+ element: Active | Over | null
44
+ ): element is Active | Over {
45
+ if (!element) return false;
46
+ if (!element.data?.current) return false;
47
+
48
+ const data = element.data.current as DraggableData;
49
+
50
+ if (data.type === 'Task' && !data.task) return false;
51
+ if (data.type === 'Column' && !data.column) return false;
52
+
53
+ return data.type === 'Task' || data.type === 'Column';
54
+ }
55
+
56
+ /**
57
+ * Calculate the number of days a task is overdue
58
+ * @param dueDate The task's due date
59
+ * @returns Number of days the task is overdue
60
+ */
61
+ export function calculateOverdueDays(dueDate: string | Date): number {
62
+ const due = new Date(dueDate);
63
+ return Math.ceil((Date.now() - due.getTime()) / (1000 * 60 * 60 * 24));
64
+ }
65
+
66
+ /**
67
+ * Safe utility to get completion date from various possible fields
68
+ * @param task Task object with potential completion date fields
69
+ * @returns Date object or null if no valid completion date found
70
+ */
71
+ export function getTaskCompletionDate(task: Task): Date | null {
72
+ const possibleFields = [
73
+ 'updated_at',
74
+ 'completed_at',
75
+ 'closed_at',
76
+ 'finished_at',
77
+ 'done_at',
78
+ ];
79
+
80
+ // First try to get explicit completion dates
81
+ for (const field of possibleFields) {
82
+ const dateStr = task[field];
83
+ if (dateStr) {
84
+ const date = new Date(dateStr as string);
85
+ if (!Number.isNaN(date.getTime()) && date.getTime() > 0) {
86
+ return date;
87
+ }
88
+ }
89
+ }
90
+
91
+ // Fallback: if task is completed but no completion date, use updated_at or created_at as estimate
92
+ if (
93
+ task.listStatus === 'done' ||
94
+ task.listStatus === 'closed' ||
95
+ task.archived
96
+ ) {
97
+ // Try updated_at first (might indicate when status was changed)
98
+ if (task.updated_at) {
99
+ const date = new Date(task.updated_at);
100
+ if (!Number.isNaN(date.getTime()) && date.getTime() > 0) {
101
+ return date;
102
+ }
103
+ }
104
+
105
+ // Last resort: use creation date (not ideal but better than nothing)
106
+ if (task.created_at) {
107
+ const date = new Date(task.created_at);
108
+ if (!Number.isNaN(date.getTime()) && date.getTime() > 0) {
109
+ return date;
110
+ }
111
+ }
112
+ }
113
+
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Get status color for task visualization
119
+ * @param status Task status string
120
+ * @returns CSS class name for background color
121
+ */
122
+ export function getStatusColor(status: string): string {
123
+ switch (status) {
124
+ case 'done':
125
+ case 'closed':
126
+ return 'bg-green-500';
127
+ case 'review':
128
+ return 'bg-orange-500';
129
+ case 'active':
130
+ return 'bg-blue-500';
131
+ default:
132
+ return 'bg-gray-400';
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Filter tasks based on board selection and status
138
+ * @param tasks Array of tasks to filter
139
+ * @param boardId Board ID to filter by (null for all boards)
140
+ * @param statusFilter Status filter to apply
141
+ * @returns Filtered array of tasks
142
+ */
143
+ export function filterTasks(
144
+ tasks: Task[],
145
+ boardId: string | null,
146
+ statusFilter: 'all' | 'not_started' | 'active' | 'review' | 'done' | 'closed'
147
+ ): Task[] {
148
+ let filtered = tasks;
149
+
150
+ // Filter by board
151
+ if (boardId) {
152
+ filtered = filtered.filter((task) => task.boardId === boardId);
153
+ }
154
+
155
+ // Filter by status
156
+ if (statusFilter !== 'all') {
157
+ filtered = filtered.filter((task) => {
158
+ const taskStatus = task.listStatus || 'not_started';
159
+ return taskStatus === statusFilter;
160
+ });
161
+ }
162
+
163
+ return filtered;
164
+ }
165
+
166
+ /**
167
+ * Group tasks by their status
168
+ * @param tasks Array of tasks to group
169
+ * @returns Object with tasks grouped by status
170
+ */
171
+ export function groupTasksByStatus(tasks: Task[]): Record<string, Task[]> {
172
+ const groups: Record<string, Task[]> = {
173
+ not_started: [],
174
+ active: [],
175
+ done: [],
176
+ closed: [],
177
+ };
178
+
179
+ tasks.forEach((task) => {
180
+ if (task.archived || task.listStatus === 'done') {
181
+ groups.done?.push(task);
182
+ } else if (task.listStatus === 'closed') {
183
+ groups.closed?.push(task);
184
+ } else if (task.listStatus === 'active') {
185
+ groups.active?.push(task);
186
+ } else {
187
+ groups.not_started?.push(task);
188
+ }
189
+ });
190
+
191
+ return groups;
192
+ }
193
+
194
+ /**
195
+ * Calculate filtered metrics for boards based on selection
196
+ * @param data Array of board data
197
+ * @param selectedBoard Board ID to filter by (null for all boards)
198
+ * @returns Aggregated metrics
199
+ */
200
+ export function getFilteredMetrics(
201
+ data: BoardMetrics[],
202
+ selectedBoard: string | null
203
+ ) {
204
+ const filteredData = selectedBoard
205
+ ? data.filter((board) => board.id === selectedBoard)
206
+ : data;
207
+
208
+ const totalTasks = filteredData.reduce(
209
+ (sum, board) => sum + board.totalTasks,
210
+ 0
211
+ );
212
+ const totalCompleted = filteredData.reduce(
213
+ (sum, board) => sum + board.completedTasks,
214
+ 0
215
+ );
216
+ const totalOverdue = filteredData.reduce(
217
+ (sum, board) => sum + board.overdueTasks,
218
+ 0
219
+ );
220
+ const totalHighPriority = filteredData.reduce(
221
+ (sum, board) => sum + board.highPriorityTasks,
222
+ 0
223
+ );
224
+ const avgProgress =
225
+ filteredData.length > 0
226
+ ? Math.round(
227
+ filteredData.reduce(
228
+ (sum, board) => sum + board.progressPercentage,
229
+ 0
230
+ ) / filteredData.length
231
+ )
232
+ : 0;
233
+
234
+ return {
235
+ totalTasks,
236
+ totalCompleted,
237
+ totalOverdue,
238
+ totalHighPriority,
239
+ avgProgress,
240
+ };
241
+ }
@@ -0,0 +1,62 @@
1
+ import type { SupportedColor } from '@tuturuuu/types/primitives/SupportedColors';
2
+ import type { TaskBoardStatus } from '@tuturuuu/types/primitives/TaskBoard';
3
+
4
+ export const TASK_BOARD_STATUSES = [
5
+ 'documents',
6
+ 'not_started',
7
+ 'active',
8
+ 'review',
9
+ 'done',
10
+ 'closed',
11
+ ] as const satisfies readonly TaskBoardStatus[];
12
+
13
+ export const TASK_BOARD_WORKFLOW_STATUSES = [
14
+ 'not_started',
15
+ 'active',
16
+ 'review',
17
+ 'done',
18
+ 'closed',
19
+ ] as const satisfies readonly TaskBoardStatus[];
20
+
21
+ export function isTaskBoardStatus(
22
+ status: string | null | undefined
23
+ ): status is TaskBoardStatus {
24
+ return TASK_BOARD_STATUSES.includes(status as TaskBoardStatus);
25
+ }
26
+
27
+ export function isTaskBoardCompletedStatus(status: string | null | undefined) {
28
+ return status === 'done';
29
+ }
30
+
31
+ export function isTaskBoardResolvedStatus(status: string | null | undefined) {
32
+ return (
33
+ status === 'review' ||
34
+ isTaskBoardCompletedStatus(status) ||
35
+ status === 'closed'
36
+ );
37
+ }
38
+
39
+ export function isTaskBoardTerminalStatus(status: string | null | undefined) {
40
+ return status === 'done' || status === 'closed';
41
+ }
42
+
43
+ export function getDefaultTaskListColorForStatus(
44
+ status: string | null | undefined
45
+ ): SupportedColor {
46
+ switch (status) {
47
+ case 'documents':
48
+ return 'CYAN';
49
+ case 'not_started':
50
+ return 'GRAY';
51
+ case 'active':
52
+ return 'BLUE';
53
+ case 'review':
54
+ return 'ORANGE';
55
+ case 'done':
56
+ return 'GREEN';
57
+ case 'closed':
58
+ return 'PURPLE';
59
+ default:
60
+ return 'GRAY';
61
+ }
62
+ }
@@ -0,0 +1,82 @@
1
+ import type {
2
+ TaskUserOverride,
3
+ TaskWithRelations,
4
+ UserBoardListOverride,
5
+ } from '@tuturuuu/types';
6
+
7
+ /**
8
+ * When self_managed is true, applies personal overrides to task fields.
9
+ * Returns a new task object with effective values; never mutates the original.
10
+ */
11
+ export function resolveEffectiveValues(
12
+ task: TaskWithRelations,
13
+ overrides: TaskUserOverride | null | undefined
14
+ ): TaskWithRelations {
15
+ if (!overrides?.self_managed) return task;
16
+
17
+ return {
18
+ ...task,
19
+ priority:
20
+ overrides.priority_override !== null
21
+ ? overrides.priority_override
22
+ : task.priority,
23
+ end_date:
24
+ overrides.due_date_override !== null
25
+ ? overrides.due_date_override
26
+ : task.end_date,
27
+ estimation_points:
28
+ overrides.estimation_override !== null
29
+ ? overrides.estimation_override
30
+ : task.estimation_points,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Checks whether a task should be hidden from My Tasks based on:
36
+ * 1. Personal completion (completed_at is set)
37
+ * 2. Personal unassignment (personally_unassigned is true)
38
+ * 3. Board/list override status being 'done' or 'closed'
39
+ */
40
+ export function isPersonallyHidden(
41
+ task: TaskWithRelations,
42
+ overrides: TaskUserOverride | null | undefined,
43
+ boardListOverrides: UserBoardListOverride[]
44
+ ): boolean {
45
+ // Check personal completion
46
+ if (overrides?.completed_at) return true;
47
+
48
+ // Check personal unassignment
49
+ if (overrides?.personally_unassigned) return true;
50
+
51
+ // Check board-level override
52
+ const boardId = task.list?.board?.id;
53
+ if (boardId) {
54
+ const boardOverride = boardListOverrides.find(
55
+ (o) => o.scope_type === 'board' && o.board_id === boardId
56
+ );
57
+ if (
58
+ boardOverride &&
59
+ (boardOverride.personal_status === 'done' ||
60
+ boardOverride.personal_status === 'closed')
61
+ ) {
62
+ return true;
63
+ }
64
+ }
65
+
66
+ // Check list-level override
67
+ const listId = task.list?.id;
68
+ if (listId) {
69
+ const listOverride = boardListOverrides.find(
70
+ (o) => o.scope_type === 'list' && o.list_id === listId
71
+ );
72
+ if (
73
+ listOverride &&
74
+ (listOverride.personal_status === 'done' ||
75
+ listOverride.personal_status === 'closed')
76
+ ) {
77
+ return true;
78
+ }
79
+ }
80
+
81
+ return false;
82
+ }