@tuturuuu/utils 0.0.3 → 0.6.1
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 +313 -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,374 @@
|
|
|
1
|
+
import type { JSONContent } from '@tiptap/react';
|
|
2
|
+
|
|
3
|
+
/** Priority type matching the codebase convention */
|
|
4
|
+
export type TaskPriorityType = 'critical' | 'high' | 'normal' | 'low';
|
|
5
|
+
|
|
6
|
+
/** Icon component type (generic to avoid lucide-react dependency) */
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
export type IconComponent = React.ComponentType<any>;
|
|
9
|
+
|
|
10
|
+
/** Snapshot of a task at a point in history */
|
|
11
|
+
export interface TaskSnapshot {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string | JSONContent | null;
|
|
15
|
+
priority: TaskPriorityType | null;
|
|
16
|
+
start_date: string | null;
|
|
17
|
+
end_date: string | null;
|
|
18
|
+
estimation_points: number | null;
|
|
19
|
+
list_id: string;
|
|
20
|
+
list_name?: string | null;
|
|
21
|
+
completed: boolean;
|
|
22
|
+
assignees: SnapshotAssignee[];
|
|
23
|
+
labels: SnapshotLabel[];
|
|
24
|
+
projects: SnapshotProject[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SnapshotAssignee {
|
|
28
|
+
id: string;
|
|
29
|
+
user_id?: string;
|
|
30
|
+
display_name: string | null;
|
|
31
|
+
avatar_url?: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SnapshotLabel {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
color?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SnapshotProject {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Current task state for comparison */
|
|
46
|
+
export interface CurrentTaskState {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
description: string | JSONContent | null;
|
|
50
|
+
priority: TaskPriorityType | null;
|
|
51
|
+
start_date: string | null;
|
|
52
|
+
end_date: string | null;
|
|
53
|
+
estimation_points: number | null;
|
|
54
|
+
list_id: string;
|
|
55
|
+
list_name?: string | null;
|
|
56
|
+
completed: boolean;
|
|
57
|
+
assignees?: Array<{ id?: string; user_id?: string }>;
|
|
58
|
+
labels?: Array<{ id: string }>;
|
|
59
|
+
projects?: Array<{ id: string }>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Field that can be compared and reverted */
|
|
63
|
+
export type ComparableField =
|
|
64
|
+
| 'name'
|
|
65
|
+
| 'description'
|
|
66
|
+
| 'priority'
|
|
67
|
+
| 'start_date'
|
|
68
|
+
| 'end_date'
|
|
69
|
+
| 'estimation_points'
|
|
70
|
+
| 'list_id'
|
|
71
|
+
| 'completed'
|
|
72
|
+
| 'assignees'
|
|
73
|
+
| 'labels'
|
|
74
|
+
| 'projects';
|
|
75
|
+
|
|
76
|
+
export const ALL_COMPARABLE_FIELDS: ComparableField[] = [
|
|
77
|
+
'name',
|
|
78
|
+
'description',
|
|
79
|
+
'priority',
|
|
80
|
+
'start_date',
|
|
81
|
+
'end_date',
|
|
82
|
+
'estimation_points',
|
|
83
|
+
'list_id',
|
|
84
|
+
'completed',
|
|
85
|
+
'assignees',
|
|
86
|
+
'labels',
|
|
87
|
+
'projects',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/** Field display configuration */
|
|
91
|
+
export interface FieldDisplayInfo {
|
|
92
|
+
label: string;
|
|
93
|
+
icon: IconComponent;
|
|
94
|
+
category: 'core' | 'dates' | 'relationships';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Deep compare two JSON content objects for equality
|
|
99
|
+
*/
|
|
100
|
+
export function isDescriptionEqual(
|
|
101
|
+
a: string | JSONContent | null | undefined,
|
|
102
|
+
b: string | JSONContent | null | undefined
|
|
103
|
+
): boolean {
|
|
104
|
+
// Both null/undefined
|
|
105
|
+
if (!a && !b) return true;
|
|
106
|
+
// One is null/undefined
|
|
107
|
+
if (!a || !b) return false;
|
|
108
|
+
|
|
109
|
+
// Convert strings to objects if needed
|
|
110
|
+
const objA = typeof a === 'string' ? tryParseJson(a) : a;
|
|
111
|
+
const objB = typeof b === 'string' ? tryParseJson(b) : b;
|
|
112
|
+
|
|
113
|
+
// If parsing failed, compare as strings
|
|
114
|
+
if (objA === null || objB === null) {
|
|
115
|
+
return String(a) === String(b);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Deep comparison
|
|
119
|
+
return JSON.stringify(objA) === JSON.stringify(objB);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Try to parse a JSON string, return null if invalid
|
|
124
|
+
*/
|
|
125
|
+
function tryParseJson(str: string): JSONContent | null {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(str);
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Compare two arrays by extracting IDs
|
|
135
|
+
*/
|
|
136
|
+
function arraysEqualById(
|
|
137
|
+
a: Array<{ id: string }> | undefined,
|
|
138
|
+
b: Array<{ id: string }> | undefined
|
|
139
|
+
): boolean {
|
|
140
|
+
const aIds = new Set((a || []).map((item) => item.id));
|
|
141
|
+
const bIds = new Set((b || []).map((item) => item.id));
|
|
142
|
+
|
|
143
|
+
if (aIds.size !== bIds.size) return false;
|
|
144
|
+
|
|
145
|
+
for (const id of aIds) {
|
|
146
|
+
if (!bIds.has(id)) return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Compare snapshot with current task state and return changed fields
|
|
154
|
+
*/
|
|
155
|
+
export function getChangedFields(
|
|
156
|
+
snapshot: TaskSnapshot,
|
|
157
|
+
current: CurrentTaskState
|
|
158
|
+
): ComparableField[] {
|
|
159
|
+
const changedFields: ComparableField[] = [];
|
|
160
|
+
|
|
161
|
+
// Core text field
|
|
162
|
+
if (snapshot.name !== current.name) {
|
|
163
|
+
changedFields.push('name');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Description (deep comparison)
|
|
167
|
+
if (!isDescriptionEqual(snapshot.description, current.description)) {
|
|
168
|
+
changedFields.push('description');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Priority
|
|
172
|
+
if (snapshot.priority !== current.priority) {
|
|
173
|
+
changedFields.push('priority');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Dates
|
|
177
|
+
if (
|
|
178
|
+
normalizeDate(snapshot.start_date) !== normalizeDate(current.start_date)
|
|
179
|
+
) {
|
|
180
|
+
changedFields.push('start_date');
|
|
181
|
+
}
|
|
182
|
+
if (normalizeDate(snapshot.end_date) !== normalizeDate(current.end_date)) {
|
|
183
|
+
changedFields.push('end_date');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Estimation points
|
|
187
|
+
if (snapshot.estimation_points !== current.estimation_points) {
|
|
188
|
+
changedFields.push('estimation_points');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// List/status
|
|
192
|
+
if (snapshot.list_id !== current.list_id) {
|
|
193
|
+
changedFields.push('list_id');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Completed status
|
|
197
|
+
if (snapshot.completed !== current.completed) {
|
|
198
|
+
changedFields.push('completed');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Relationships
|
|
202
|
+
const snapshotAssigneeIds =
|
|
203
|
+
snapshot.assignees?.map((a) => ({ id: a.user_id || a.id })) || [];
|
|
204
|
+
const currentAssigneeIds =
|
|
205
|
+
current.assignees?.map((a) => ({ id: a.user_id || a.id || '' })) || [];
|
|
206
|
+
if (!arraysEqualById(snapshotAssigneeIds, currentAssigneeIds)) {
|
|
207
|
+
changedFields.push('assignees');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!arraysEqualById(snapshot.labels, current.labels)) {
|
|
211
|
+
changedFields.push('labels');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!arraysEqualById(snapshot.projects, current.projects)) {
|
|
215
|
+
changedFields.push('projects');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return changedFields;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Normalize date string for comparison (to ISO date string or null)
|
|
223
|
+
*/
|
|
224
|
+
function normalizeDate(date: string | null | undefined): string | null {
|
|
225
|
+
if (!date) return null;
|
|
226
|
+
try {
|
|
227
|
+
return new Date(date).toISOString();
|
|
228
|
+
} catch {
|
|
229
|
+
return date;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get display information for a field
|
|
235
|
+
* Note: Icons are passed as a parameter to avoid importing lucide-react in utils package
|
|
236
|
+
*/
|
|
237
|
+
export function getFieldDisplayInfo(
|
|
238
|
+
fieldName: ComparableField,
|
|
239
|
+
t: (key: string, options?: { defaultValue?: string }) => string,
|
|
240
|
+
icons: {
|
|
241
|
+
FileText: IconComponent;
|
|
242
|
+
Flag: IconComponent;
|
|
243
|
+
Clock: IconComponent;
|
|
244
|
+
Calendar: IconComponent;
|
|
245
|
+
Target: IconComponent;
|
|
246
|
+
Layers: IconComponent;
|
|
247
|
+
CheckCircle2: IconComponent;
|
|
248
|
+
Users: IconComponent;
|
|
249
|
+
Tag: IconComponent;
|
|
250
|
+
FolderKanban: IconComponent;
|
|
251
|
+
}
|
|
252
|
+
): FieldDisplayInfo {
|
|
253
|
+
const fieldConfig: Record<ComparableField, FieldDisplayInfo> = {
|
|
254
|
+
name: {
|
|
255
|
+
label: t('field.name', { defaultValue: 'Name' }),
|
|
256
|
+
icon: icons.FileText,
|
|
257
|
+
category: 'core',
|
|
258
|
+
},
|
|
259
|
+
description: {
|
|
260
|
+
label: t('field.description', { defaultValue: 'Description' }),
|
|
261
|
+
icon: icons.FileText,
|
|
262
|
+
category: 'core',
|
|
263
|
+
},
|
|
264
|
+
priority: {
|
|
265
|
+
label: t('field.priority', { defaultValue: 'Priority' }),
|
|
266
|
+
icon: icons.Flag,
|
|
267
|
+
category: 'core',
|
|
268
|
+
},
|
|
269
|
+
start_date: {
|
|
270
|
+
label: t('field.start_date', { defaultValue: 'Start Date' }),
|
|
271
|
+
icon: icons.Clock,
|
|
272
|
+
category: 'dates',
|
|
273
|
+
},
|
|
274
|
+
end_date: {
|
|
275
|
+
label: t('field.end_date', { defaultValue: 'Due Date' }),
|
|
276
|
+
icon: icons.Calendar,
|
|
277
|
+
category: 'dates',
|
|
278
|
+
},
|
|
279
|
+
estimation_points: {
|
|
280
|
+
label: t('field.estimation', { defaultValue: 'Estimation' }),
|
|
281
|
+
icon: icons.Target,
|
|
282
|
+
category: 'core',
|
|
283
|
+
},
|
|
284
|
+
list_id: {
|
|
285
|
+
label: t('field.list', { defaultValue: 'List' }),
|
|
286
|
+
icon: icons.Layers,
|
|
287
|
+
category: 'core',
|
|
288
|
+
},
|
|
289
|
+
completed: {
|
|
290
|
+
label: t('field.completed', { defaultValue: 'Completed' }),
|
|
291
|
+
icon: icons.CheckCircle2,
|
|
292
|
+
category: 'core',
|
|
293
|
+
},
|
|
294
|
+
assignees: {
|
|
295
|
+
label: t('field.assignees', { defaultValue: 'Assignees' }),
|
|
296
|
+
icon: icons.Users,
|
|
297
|
+
category: 'relationships',
|
|
298
|
+
},
|
|
299
|
+
labels: {
|
|
300
|
+
label: t('field.labels', { defaultValue: 'Labels' }),
|
|
301
|
+
icon: icons.Tag,
|
|
302
|
+
category: 'relationships',
|
|
303
|
+
},
|
|
304
|
+
projects: {
|
|
305
|
+
label: t('field.projects', { defaultValue: 'Projects' }),
|
|
306
|
+
icon: icons.FolderKanban,
|
|
307
|
+
category: 'relationships',
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
return fieldConfig[fieldName];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get priority label for a priority value
|
|
316
|
+
*/
|
|
317
|
+
export function getPriorityLabel(
|
|
318
|
+
priority: TaskPriorityType | null,
|
|
319
|
+
t: (key: string, options?: { defaultValue?: string }) => string
|
|
320
|
+
): string {
|
|
321
|
+
if (priority === null) return t('priority.none', { defaultValue: 'None' });
|
|
322
|
+
|
|
323
|
+
const labels: Record<TaskPriorityType, string> = {
|
|
324
|
+
low: t('priority.low', { defaultValue: 'Low' }),
|
|
325
|
+
normal: t('priority.normal', { defaultValue: 'Normal' }),
|
|
326
|
+
high: t('priority.high', { defaultValue: 'High' }),
|
|
327
|
+
critical: t('priority.critical', { defaultValue: 'Critical' }),
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return labels[priority] || String(priority);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Format a date for display
|
|
335
|
+
*/
|
|
336
|
+
export function formatDateForDisplay(
|
|
337
|
+
date: string | null | undefined,
|
|
338
|
+
locale: string = 'en'
|
|
339
|
+
): string {
|
|
340
|
+
if (!date) return '-';
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
return new Date(date).toLocaleDateString(locale, {
|
|
344
|
+
year: 'numeric',
|
|
345
|
+
month: 'short',
|
|
346
|
+
day: 'numeric',
|
|
347
|
+
hour: '2-digit',
|
|
348
|
+
minute: '2-digit',
|
|
349
|
+
});
|
|
350
|
+
} catch {
|
|
351
|
+
return date;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get a summary of relationship changes
|
|
357
|
+
*/
|
|
358
|
+
export function getRelationshipDiff<T extends { id: string; name?: string }>(
|
|
359
|
+
snapshotItems: T[],
|
|
360
|
+
currentItems: T[]
|
|
361
|
+
): {
|
|
362
|
+
added: T[];
|
|
363
|
+
removed: T[];
|
|
364
|
+
unchanged: T[];
|
|
365
|
+
} {
|
|
366
|
+
const snapshotIds = new Set(snapshotItems.map((i) => i.id));
|
|
367
|
+
const currentIds = new Set(currentItems.map((i) => i.id));
|
|
368
|
+
|
|
369
|
+
const added = currentItems.filter((i) => !snapshotIds.has(i.id));
|
|
370
|
+
const removed = snapshotItems.filter((i) => !currentIds.has(i.id));
|
|
371
|
+
const unchanged = snapshotItems.filter((i) => currentIds.has(i.id));
|
|
372
|
+
|
|
373
|
+
return { added, removed, unchanged };
|
|
374
|
+
}
|
package/src/text-diff.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as Diff from 'diff';
|
|
2
|
+
|
|
3
|
+
export interface DiffChange {
|
|
4
|
+
type: 'added' | 'removed' | 'unchanged';
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface DiffStats {
|
|
9
|
+
added: number;
|
|
10
|
+
removed: number;
|
|
11
|
+
unchanged: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Computes a line-level diff between two text strings.
|
|
16
|
+
* Returns an array of changes with their type (added/removed/unchanged).
|
|
17
|
+
*/
|
|
18
|
+
export function computeLineDiff(
|
|
19
|
+
oldText: string,
|
|
20
|
+
newText: string
|
|
21
|
+
): DiffChange[] {
|
|
22
|
+
const changes = Diff.diffLines(oldText, newText);
|
|
23
|
+
return changes.map((change) => ({
|
|
24
|
+
type: change.added ? 'added' : change.removed ? 'removed' : 'unchanged',
|
|
25
|
+
value: change.value,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Computes a word-level diff between two text strings.
|
|
31
|
+
* Useful for smaller changes within lines.
|
|
32
|
+
*/
|
|
33
|
+
export function computeWordDiff(
|
|
34
|
+
oldText: string,
|
|
35
|
+
newText: string
|
|
36
|
+
): DiffChange[] {
|
|
37
|
+
const changes = Diff.diffWords(oldText, newText);
|
|
38
|
+
return changes.map((change) => ({
|
|
39
|
+
type: change.added ? 'added' : change.removed ? 'removed' : 'unchanged',
|
|
40
|
+
value: change.value,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Computes statistics from a diff result.
|
|
46
|
+
* Counts the number of lines added, removed, and unchanged.
|
|
47
|
+
*/
|
|
48
|
+
export function getDiffStats(diff: DiffChange[]): DiffStats {
|
|
49
|
+
return diff.reduce(
|
|
50
|
+
(acc, d) => {
|
|
51
|
+
const lines = d.value.split('\n').filter(Boolean).length;
|
|
52
|
+
acc[d.type] += lines;
|
|
53
|
+
return acc;
|
|
54
|
+
},
|
|
55
|
+
{ added: 0, removed: 0, unchanged: 0 }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks if a diff has any meaningful changes.
|
|
61
|
+
*/
|
|
62
|
+
export function hasDiffChanges(diff: DiffChange[]): boolean {
|
|
63
|
+
return diff.some((d) => d.type !== 'unchanged');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Formats a diff for display, adding line prefixes.
|
|
68
|
+
*/
|
|
69
|
+
export function formatDiffForDisplay(diff: DiffChange[]): string {
|
|
70
|
+
return diff
|
|
71
|
+
.map((d) => {
|
|
72
|
+
const prefix =
|
|
73
|
+
d.type === 'added' ? '+ ' : d.type === 'removed' ? '- ' : ' ';
|
|
74
|
+
return d.value
|
|
75
|
+
.split('\n')
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.map((line) => prefix + line)
|
|
78
|
+
.join('\n');
|
|
79
|
+
})
|
|
80
|
+
.join('\n');
|
|
81
|
+
}
|