@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,240 @@
1
+ import type { JSONContent } from '@tiptap/core';
2
+ import { Node as ProseMirrorNode, Schema } from 'prosemirror-model';
3
+ import { getDescriptionText } from './text-helper';
4
+
5
+ export const taskDescriptionSchema = new Schema({
6
+ nodes: {
7
+ doc: { content: 'block+' },
8
+ paragraph: {
9
+ group: 'block',
10
+ content: 'inline*',
11
+ attrs: { textAlign: { default: null } },
12
+ },
13
+ heading: {
14
+ group: 'block',
15
+ content: 'inline*',
16
+ attrs: {
17
+ level: { default: 1 },
18
+ textAlign: { default: null },
19
+ },
20
+ },
21
+ blockquote: {
22
+ group: 'block',
23
+ content: 'block+',
24
+ },
25
+ codeBlock: {
26
+ group: 'block',
27
+ content: 'text*',
28
+ marks: '',
29
+ attrs: { language: { default: null } },
30
+ code: true,
31
+ },
32
+ bulletList: {
33
+ group: 'block',
34
+ content: 'listItem+',
35
+ },
36
+ orderedList: {
37
+ group: 'block',
38
+ content: 'listItem+',
39
+ attrs: { start: { default: 1 } },
40
+ },
41
+ listItem: {
42
+ content: 'paragraph block*',
43
+ defining: true,
44
+ },
45
+ taskList: {
46
+ group: 'block',
47
+ content: 'taskItem+',
48
+ },
49
+ taskItem: {
50
+ content: 'paragraph block*',
51
+ attrs: { checked: { default: false } },
52
+ defining: true,
53
+ },
54
+ table: {
55
+ group: 'block',
56
+ content: 'tableRow+',
57
+ },
58
+ tableRow: {
59
+ content: '(tableCell | tableHeader)+',
60
+ },
61
+ tableCell: {
62
+ content: 'block+',
63
+ attrs: {
64
+ colspan: { default: 1 },
65
+ rowspan: { default: 1 },
66
+ colwidth: { default: null },
67
+ },
68
+ },
69
+ tableHeader: {
70
+ content: 'block+',
71
+ attrs: {
72
+ colspan: { default: 1 },
73
+ rowspan: { default: 1 },
74
+ colwidth: { default: null },
75
+ },
76
+ },
77
+ horizontalRule: {
78
+ group: 'block',
79
+ },
80
+ imageResize: {
81
+ group: 'block',
82
+ atom: true,
83
+ attrs: {
84
+ src: { default: null },
85
+ alt: { default: null },
86
+ title: { default: null },
87
+ width: { default: null },
88
+ height: { default: null },
89
+ },
90
+ },
91
+ video: {
92
+ group: 'block',
93
+ atom: true,
94
+ attrs: {
95
+ src: { default: null },
96
+ },
97
+ },
98
+ youtube: {
99
+ group: 'block',
100
+ atom: true,
101
+ attrs: {
102
+ src: { default: null },
103
+ videoId: { default: null },
104
+ },
105
+ },
106
+ mention: {
107
+ group: 'inline',
108
+ inline: true,
109
+ atom: true,
110
+ attrs: {
111
+ id: { default: null },
112
+ label: { default: null },
113
+ userId: { default: null },
114
+ displayName: { default: null },
115
+ entityId: { default: null },
116
+ entityType: { default: null },
117
+ avatarUrl: { default: null },
118
+ subtitle: { default: null },
119
+ priority: { default: null },
120
+ listColor: { default: null },
121
+ assignees: { default: null },
122
+ workspaceId: { default: null },
123
+ },
124
+ },
125
+ text: {
126
+ group: 'inline',
127
+ },
128
+ hardBreak: {
129
+ group: 'inline',
130
+ inline: true,
131
+ selectable: false,
132
+ },
133
+ },
134
+ marks: {
135
+ bold: {},
136
+ italic: {},
137
+ strike: {},
138
+ underline: {},
139
+ code: {},
140
+ subscript: {},
141
+ superscript: {},
142
+ textStyle: {
143
+ attrs: {
144
+ color: { default: null },
145
+ },
146
+ },
147
+ highlight: {
148
+ attrs: {
149
+ color: { default: null },
150
+ },
151
+ },
152
+ link: {
153
+ attrs: {
154
+ href: { default: null },
155
+ target: { default: null },
156
+ rel: { default: null },
157
+ class: { default: null },
158
+ title: { default: null },
159
+ },
160
+ inclusive: false,
161
+ },
162
+ },
163
+ });
164
+
165
+ /**
166
+ * Transforms legacy 'image' nodes to 'imageResize' nodes for schema compatibility.
167
+ * The web app uses tiptap-extension-resize-image which registers 'imageResize' only.
168
+ */
169
+ function transformImageNodes(content: JSONContent): JSONContent {
170
+ if (typeof content !== 'object' || content === null) {
171
+ return content;
172
+ }
173
+
174
+ if (Array.isArray(content)) {
175
+ return content.map(transformImageNodes) as JSONContent;
176
+ }
177
+
178
+ const transformed: JSONContent = { ...content };
179
+
180
+ if (transformed.type === 'image') {
181
+ transformed.type = 'imageResize';
182
+ }
183
+
184
+ if (Array.isArray(transformed.content)) {
185
+ transformed.content = transformed.content.map(transformImageNodes);
186
+ }
187
+
188
+ return transformed;
189
+ }
190
+
191
+ export function parseTaskDescriptionContent(description: string): JSONContent {
192
+ try {
193
+ const parsed = JSON.parse(description);
194
+ if (
195
+ parsed &&
196
+ typeof parsed === 'object' &&
197
+ !Array.isArray(parsed) &&
198
+ parsed.type === 'doc'
199
+ ) {
200
+ return transformImageNodes(parsed as JSONContent);
201
+ }
202
+ } catch {
203
+ // Fall through to plain-text conversion.
204
+ }
205
+
206
+ return {
207
+ type: 'doc',
208
+ content: [
209
+ {
210
+ type: 'paragraph',
211
+ content: [{ type: 'text', text: description }],
212
+ },
213
+ ],
214
+ };
215
+ }
216
+
217
+ export function hasMeaningfulTaskDescriptionContent(
218
+ content: JSONContent
219
+ ): boolean {
220
+ return getDescriptionText(content).trim().length > 0;
221
+ }
222
+
223
+ export function isValidTaskDescriptionContent(
224
+ description: string | null | undefined
225
+ ): boolean {
226
+ const normalizedDescription = description?.trim();
227
+ if (!normalizedDescription) {
228
+ return true;
229
+ }
230
+
231
+ try {
232
+ ProseMirrorNode.fromJSON(
233
+ taskDescriptionSchema,
234
+ parseTaskDescriptionContent(normalizedDescription)
235
+ );
236
+ return true;
237
+ } catch {
238
+ return false;
239
+ }
240
+ }
@@ -0,0 +1,193 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import type { InternalApiClientOptions } from '@tuturuuu/internal-api/client';
3
+ import {
4
+ createWorkspaceTaskBoard,
5
+ getWorkspaceTaskBoard as getWorkspaceTaskBoardFromApi,
6
+ listWorkspaceLabels,
7
+ updateWorkspaceTaskBoard,
8
+ updateWorkspaceTaskList,
9
+ } from '@tuturuuu/internal-api/tasks';
10
+ import type { Database, WorkspaceTaskBoard } from '@tuturuuu/types';
11
+ import { getMutationApiOptions } from './shared';
12
+
13
+ export async function getTaskBoard(
14
+ boardId: string,
15
+ workspaceId?: string,
16
+ options?: InternalApiClientOptions
17
+ ) {
18
+ if (!workspaceId) {
19
+ return null;
20
+ }
21
+
22
+ const payload = await getWorkspaceTaskBoardFromApi(
23
+ workspaceId,
24
+ boardId,
25
+ options
26
+ );
27
+
28
+ return (payload.board ?? null) as WorkspaceTaskBoard | null;
29
+ }
30
+
31
+ export async function createBoardWithTemplate(
32
+ wsId: string,
33
+ name: string,
34
+ templateId?: string,
35
+ icon?: Database['public']['Enums']['platform_icon'] | null,
36
+ options?: InternalApiClientOptions
37
+ ) {
38
+ const payload = await createWorkspaceTaskBoard(
39
+ wsId,
40
+ {
41
+ name,
42
+ template_id: templateId,
43
+ icon: icon ?? null,
44
+ },
45
+ options
46
+ );
47
+
48
+ return payload.board as WorkspaceTaskBoard;
49
+ }
50
+
51
+ export async function deleteTaskList(
52
+ wsId: string,
53
+ boardId: string,
54
+ listId: string
55
+ ) {
56
+ const options = await getMutationApiOptions();
57
+ const { list } = await updateWorkspaceTaskList(
58
+ wsId,
59
+ boardId,
60
+ listId,
61
+ { deleted: true },
62
+ options
63
+ );
64
+
65
+ return list;
66
+ }
67
+
68
+ export function useCreateBoardWithTemplate(wsId: string) {
69
+ const queryClient = useQueryClient();
70
+
71
+ return useMutation({
72
+ mutationFn: async ({
73
+ name,
74
+ templateId,
75
+ icon,
76
+ }: {
77
+ name: string;
78
+ templateId?: string;
79
+ icon?: Database['public']['Enums']['platform_icon'] | null;
80
+ }) => {
81
+ const baseUrl =
82
+ typeof window !== 'undefined' ? window.location.origin : undefined;
83
+ return createBoardWithTemplate(wsId, name, templateId, icon, {
84
+ baseUrl: baseUrl ?? undefined,
85
+ });
86
+ },
87
+ onSuccess: () => {
88
+ queryClient.invalidateQueries({ queryKey: ['boards', wsId] });
89
+ },
90
+ onError: (error) => {
91
+ console.error('Error creating board:', error);
92
+ },
93
+ });
94
+ }
95
+
96
+ export function useUpdateBoardWithTemplate(wsId: string) {
97
+ const queryClient = useQueryClient();
98
+
99
+ return useMutation({
100
+ mutationFn: async ({
101
+ boardId,
102
+ name,
103
+ icon,
104
+ }: {
105
+ boardId: string;
106
+ name: string;
107
+ icon: string | null;
108
+ }) => {
109
+ return updateWorkspaceTaskBoard(
110
+ wsId,
111
+ boardId,
112
+ {
113
+ name,
114
+ icon: icon as Database['public']['Enums']['platform_icon'] | null,
115
+ },
116
+ {
117
+ baseUrl:
118
+ typeof window !== 'undefined' ? window.location.origin : undefined,
119
+ }
120
+ );
121
+ },
122
+ onSuccess: () => {
123
+ queryClient.invalidateQueries({ queryKey: ['boards', wsId] });
124
+ },
125
+ onError: (error) => {
126
+ console.error('Error updating board:', error);
127
+ },
128
+ });
129
+ }
130
+
131
+ export interface WorkspaceLabel {
132
+ id: string;
133
+ name: string;
134
+ color: string;
135
+ created_at: string;
136
+ ws_id: string;
137
+ }
138
+
139
+ export function useWorkspaceLabels(wsId: string | null | undefined) {
140
+ return useQuery({
141
+ queryKey: ['workspace-labels', wsId],
142
+ queryFn: async () => {
143
+ if (!wsId) return [];
144
+
145
+ const labels = await listWorkspaceLabels(wsId);
146
+ return labels as WorkspaceLabel[];
147
+ },
148
+ enabled: Boolean(wsId),
149
+ staleTime: 5 * 60 * 1000,
150
+ refetchOnWindowFocus: false,
151
+ });
152
+ }
153
+
154
+ export interface BoardConfig {
155
+ id: string;
156
+ estimation_type: string | null;
157
+ extended_estimation: boolean;
158
+ allow_zero_estimates: boolean;
159
+ ws_id: string;
160
+ ticket_prefix: string | null;
161
+ }
162
+
163
+ export function useBoardConfig(
164
+ boardId: string | null | undefined,
165
+ wsId: string | null | undefined
166
+ ) {
167
+ return useQuery({
168
+ queryKey: ['board-config', wsId, boardId],
169
+ queryFn: async () => {
170
+ if (!boardId || !wsId) {
171
+ return null;
172
+ }
173
+
174
+ const payload = await getWorkspaceTaskBoardFromApi(wsId, boardId);
175
+ const board = payload.board;
176
+ if (!board) {
177
+ return null;
178
+ }
179
+
180
+ return {
181
+ id: board.id,
182
+ estimation_type: board.estimation_type ?? null,
183
+ extended_estimation: board.extended_estimation ?? false,
184
+ allow_zero_estimates: board.allow_zero_estimates ?? false,
185
+ ws_id: board.ws_id,
186
+ ticket_prefix: board.ticket_prefix ?? null,
187
+ } as BoardConfig;
188
+ },
189
+ enabled: Boolean(boardId && wsId),
190
+ staleTime: 10 * 60 * 1000,
191
+ refetchOnWindowFocus: false,
192
+ });
193
+ }