@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.
- package/CHANGELOG.md +305 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +120 -1
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- 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
|
+
}
|