@tuturuuu/utils 0.8.0 → 0.10.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 +21 -0
- package/biome.json +1 -1
- package/package.json +17 -15
- package/src/__tests__/api-proxy-guard.test.ts +728 -71
- package/src/__tests__/app-url.test.ts +2 -0
- package/src/__tests__/task-description-codec.test.ts +110 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +10 -45
- package/src/abuse-protection/__tests__/edge-trust.test.ts +99 -0
- package/src/abuse-protection/__tests__/index.test.ts +136 -0
- package/src/abuse-protection/backend-rate-limit.ts +5 -4
- package/src/abuse-protection/edge-trust.ts +141 -6
- package/src/abuse-protection/index.ts +73 -40
- package/src/api-proxy-guard.ts +202 -43
- package/src/currencies.test.ts +33 -0
- package/src/currencies.ts +28 -5
- package/src/effect.test.ts +247 -0
- package/src/effect.ts +259 -0
- package/src/internal-domains.ts +24 -0
- package/src/platform-release.test.ts +1 -1
- package/src/platform-release.ts +1 -1
- package/src/portless.ts +2 -0
- package/src/task-description-codec.ts +457 -0
- package/src/task-helper/task-operations.ts +3 -0
- package/src/yjs-task-description.ts +13 -21
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import type { JSONContent } from '@tiptap/core';
|
|
3
|
+
import { Node as ProseMirrorNode } from 'prosemirror-model';
|
|
4
|
+
import {
|
|
5
|
+
hasMeaningfulTaskDescriptionContent,
|
|
6
|
+
parseTaskDescriptionContent,
|
|
7
|
+
taskDescriptionSchema,
|
|
8
|
+
} from './task-description-content';
|
|
9
|
+
import { getDescriptionText } from './text-helper';
|
|
10
|
+
import {
|
|
11
|
+
convertJsonContentToYjsState,
|
|
12
|
+
convertYjsStateToJsonContent,
|
|
13
|
+
} from './yjs-helper';
|
|
14
|
+
|
|
15
|
+
export type TaskDescriptionInputFormat = 'json' | 'markdown' | 'text';
|
|
16
|
+
export type TaskDescriptionYjsFormat = 'bytes-json' | 'yjs-base64';
|
|
17
|
+
|
|
18
|
+
export interface TaskDescriptionPayload {
|
|
19
|
+
content: JSONContent;
|
|
20
|
+
description: string | null;
|
|
21
|
+
description_yjs_state: number[] | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type InlineMark = NonNullable<JSONContent['marks']>[number];
|
|
25
|
+
type ListKind = 'bulletList' | 'orderedList' | 'taskList';
|
|
26
|
+
|
|
27
|
+
const MARK_PATTERNS = [
|
|
28
|
+
{ mark: 'code', pattern: /`([^`]+)`/u },
|
|
29
|
+
{ mark: 'bold', pattern: /\*\*([^*]+)\*\*/u },
|
|
30
|
+
{ mark: 'strike', pattern: /~~(.+?)~~/u },
|
|
31
|
+
{ mark: 'italic', pattern: /\*([^*\n]+)\*/u },
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
function textNode(text: string, marks?: JSONContent['marks']): JSONContent[] {
|
|
35
|
+
if (!text) return [];
|
|
36
|
+
return marks?.length
|
|
37
|
+
? [{ marks, text, type: 'text' }]
|
|
38
|
+
: [{ text, type: 'text' }];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function withMark(type: InlineMark['type'], attrs?: InlineMark['attrs']) {
|
|
42
|
+
const mark: InlineMark = { type };
|
|
43
|
+
if (attrs && Object.keys(attrs).length > 0) {
|
|
44
|
+
mark.attrs = attrs;
|
|
45
|
+
}
|
|
46
|
+
return mark;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findEarliestInlineToken(text: string):
|
|
50
|
+
| {
|
|
51
|
+
attrs?: Record<string, unknown>;
|
|
52
|
+
before: string;
|
|
53
|
+
inner: string;
|
|
54
|
+
mark: InlineMark['type'];
|
|
55
|
+
rest: string;
|
|
56
|
+
}
|
|
57
|
+
| undefined {
|
|
58
|
+
const linkMatch = /\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/u.exec(text);
|
|
59
|
+
let best:
|
|
60
|
+
| {
|
|
61
|
+
attrs?: Record<string, unknown>;
|
|
62
|
+
before: string;
|
|
63
|
+
inner: string;
|
|
64
|
+
index: number;
|
|
65
|
+
mark: InlineMark['type'];
|
|
66
|
+
rest: string;
|
|
67
|
+
}
|
|
68
|
+
| undefined;
|
|
69
|
+
|
|
70
|
+
if (linkMatch?.index !== undefined) {
|
|
71
|
+
best = {
|
|
72
|
+
attrs: {
|
|
73
|
+
href: linkMatch[2],
|
|
74
|
+
rel: null,
|
|
75
|
+
target: null,
|
|
76
|
+
title: linkMatch[3] ?? null,
|
|
77
|
+
},
|
|
78
|
+
before: text.slice(0, linkMatch.index),
|
|
79
|
+
index: linkMatch.index,
|
|
80
|
+
inner: linkMatch[1] ?? '',
|
|
81
|
+
mark: 'link',
|
|
82
|
+
rest: text.slice(linkMatch.index + linkMatch[0].length),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const { mark, pattern } of MARK_PATTERNS) {
|
|
87
|
+
const match = pattern.exec(text);
|
|
88
|
+
if (!match?.[0] || match.index === undefined) continue;
|
|
89
|
+
if (best && match.index >= best.index) continue;
|
|
90
|
+
|
|
91
|
+
best = {
|
|
92
|
+
before: text.slice(0, match.index),
|
|
93
|
+
index: match.index,
|
|
94
|
+
inner: match[1] ?? '',
|
|
95
|
+
mark,
|
|
96
|
+
rest: text.slice(match.index + match[0].length),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return best;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseInlineMarkdown(
|
|
104
|
+
text: string,
|
|
105
|
+
marks: JSONContent['marks'] = []
|
|
106
|
+
): JSONContent[] {
|
|
107
|
+
const token = findEarliestInlineToken(text);
|
|
108
|
+
if (!token) return textNode(text, marks);
|
|
109
|
+
|
|
110
|
+
return [
|
|
111
|
+
...textNode(token.before, marks),
|
|
112
|
+
...parseInlineMarkdown(token.inner, [
|
|
113
|
+
...marks,
|
|
114
|
+
withMark(token.mark, token.attrs),
|
|
115
|
+
]),
|
|
116
|
+
...parseInlineMarkdown(token.rest, marks),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function paragraphFromText(text: string, markdown = false): JSONContent {
|
|
121
|
+
const lines = text.split('\n');
|
|
122
|
+
const content: JSONContent[] = [];
|
|
123
|
+
|
|
124
|
+
for (const [index, line] of lines.entries()) {
|
|
125
|
+
if (index > 0) {
|
|
126
|
+
content.push({ type: 'hardBreak' });
|
|
127
|
+
}
|
|
128
|
+
content.push(...(markdown ? parseInlineMarkdown(line) : textNode(line)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return content.length > 0
|
|
132
|
+
? { content, type: 'paragraph' }
|
|
133
|
+
: { type: 'paragraph' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function codeBlockFromLines(lines: string[], language?: string): JSONContent {
|
|
137
|
+
return {
|
|
138
|
+
attrs: { language: language || null },
|
|
139
|
+
content:
|
|
140
|
+
lines.length > 0 ? [{ text: lines.join('\n'), type: 'text' }] : undefined,
|
|
141
|
+
type: 'codeBlock',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isHorizontalRule(line: string) {
|
|
146
|
+
return /^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/u.test(line);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getListLine(line: string):
|
|
150
|
+
| {
|
|
151
|
+
checked?: boolean;
|
|
152
|
+
kind: ListKind;
|
|
153
|
+
start?: number;
|
|
154
|
+
text: string;
|
|
155
|
+
}
|
|
156
|
+
| undefined {
|
|
157
|
+
const taskMatch = /^\s*[-*+]\s+\[([ xX])\]\s+(.+)$/u.exec(line);
|
|
158
|
+
if (taskMatch) {
|
|
159
|
+
return {
|
|
160
|
+
checked: taskMatch[1]?.toLowerCase() === 'x',
|
|
161
|
+
kind: 'taskList',
|
|
162
|
+
text: taskMatch[2] ?? '',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const bulletMatch = /^\s*[-*+]\s+(.+)$/u.exec(line);
|
|
167
|
+
if (bulletMatch) {
|
|
168
|
+
return { kind: 'bulletList', text: bulletMatch[1] ?? '' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const orderedMatch = /^\s*(\d+)[.)]\s+(.+)$/u.exec(line);
|
|
172
|
+
if (orderedMatch) {
|
|
173
|
+
return {
|
|
174
|
+
kind: 'orderedList',
|
|
175
|
+
start: Number.parseInt(orderedMatch[1] ?? '1', 10),
|
|
176
|
+
text: orderedMatch[2] ?? '',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isBlockStart(line: string) {
|
|
182
|
+
return (
|
|
183
|
+
/^```/u.test(line) ||
|
|
184
|
+
/^#{1,6}\s+/u.test(line) ||
|
|
185
|
+
/^>\s?/u.test(line) ||
|
|
186
|
+
isHorizontalRule(line) ||
|
|
187
|
+
Boolean(getListLine(line))
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createListBlock(
|
|
192
|
+
kind: ListKind,
|
|
193
|
+
entries: Array<{ checked?: boolean; text: string }>,
|
|
194
|
+
start?: number
|
|
195
|
+
): JSONContent {
|
|
196
|
+
if (kind === 'taskList') {
|
|
197
|
+
return {
|
|
198
|
+
content: entries.map((entry) => ({
|
|
199
|
+
attrs: { checked: entry.checked === true },
|
|
200
|
+
content: [paragraphFromText(entry.text, true)],
|
|
201
|
+
type: 'taskItem',
|
|
202
|
+
})),
|
|
203
|
+
type: 'taskList',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
attrs: kind === 'orderedList' ? { start: start ?? 1 } : undefined,
|
|
209
|
+
content: entries.map((entry) => ({
|
|
210
|
+
content: [paragraphFromText(entry.text, true)],
|
|
211
|
+
type: 'listItem',
|
|
212
|
+
})),
|
|
213
|
+
type: kind,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseMarkdownBlocks(markdown: string): JSONContent[] {
|
|
218
|
+
const lines = markdown.replace(/\r\n?/gu, '\n').split('\n');
|
|
219
|
+
const blocks: JSONContent[] = [];
|
|
220
|
+
let index = 0;
|
|
221
|
+
|
|
222
|
+
while (index < lines.length) {
|
|
223
|
+
const line = lines[index] ?? '';
|
|
224
|
+
|
|
225
|
+
if (!line.trim()) {
|
|
226
|
+
index += 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const fence = /^```([^\s`]*)\s*$/u.exec(line);
|
|
231
|
+
if (fence) {
|
|
232
|
+
const codeLines: string[] = [];
|
|
233
|
+
index += 1;
|
|
234
|
+
while (index < lines.length && !/^```\s*$/u.test(lines[index] ?? '')) {
|
|
235
|
+
codeLines.push(lines[index] ?? '');
|
|
236
|
+
index += 1;
|
|
237
|
+
}
|
|
238
|
+
if (index < lines.length) index += 1;
|
|
239
|
+
blocks.push(codeBlockFromLines(codeLines, fence[1]));
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const heading = /^(#{1,6})\s+(.+)$/u.exec(line);
|
|
244
|
+
if (heading) {
|
|
245
|
+
blocks.push({
|
|
246
|
+
attrs: { level: heading[1]?.length ?? 1, textAlign: null },
|
|
247
|
+
content: parseInlineMarkdown(heading[2] ?? ''),
|
|
248
|
+
type: 'heading',
|
|
249
|
+
});
|
|
250
|
+
index += 1;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (isHorizontalRule(line)) {
|
|
255
|
+
blocks.push({ type: 'horizontalRule' });
|
|
256
|
+
index += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (/^>\s?/u.test(line)) {
|
|
261
|
+
const quoted: string[] = [];
|
|
262
|
+
while (index < lines.length && /^>\s?/u.test(lines[index] ?? '')) {
|
|
263
|
+
quoted.push((lines[index] ?? '').replace(/^>\s?/u, ''));
|
|
264
|
+
index += 1;
|
|
265
|
+
}
|
|
266
|
+
const content = parseMarkdownBlocks(quoted.join('\n'));
|
|
267
|
+
blocks.push({
|
|
268
|
+
content: content.length > 0 ? content : [{ type: 'paragraph' }],
|
|
269
|
+
type: 'blockquote',
|
|
270
|
+
});
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const listLine = getListLine(line);
|
|
275
|
+
if (listLine) {
|
|
276
|
+
const entries: Array<{ checked?: boolean; text: string }> = [];
|
|
277
|
+
const listKind = listLine.kind;
|
|
278
|
+
const start = listLine.start;
|
|
279
|
+
|
|
280
|
+
while (index < lines.length) {
|
|
281
|
+
const item = getListLine(lines[index] ?? '');
|
|
282
|
+
if (!item || item.kind !== listKind) break;
|
|
283
|
+
entries.push({ checked: item.checked, text: item.text });
|
|
284
|
+
index += 1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
blocks.push(createListBlock(listKind, entries, start));
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const paragraphLines = [line];
|
|
292
|
+
index += 1;
|
|
293
|
+
while (
|
|
294
|
+
index < lines.length &&
|
|
295
|
+
(lines[index] ?? '').trim() &&
|
|
296
|
+
!isBlockStart(lines[index] ?? '')
|
|
297
|
+
) {
|
|
298
|
+
paragraphLines.push(lines[index] ?? '');
|
|
299
|
+
index += 1;
|
|
300
|
+
}
|
|
301
|
+
blocks.push(paragraphFromText(paragraphLines.join('\n'), true));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return blocks;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function createPlainTextTaskDescriptionContent(text: string) {
|
|
308
|
+
const normalizedText = text.replace(/\r\n?/gu, '\n');
|
|
309
|
+
const paragraphs = normalizedText.split(/\n{2,}/u);
|
|
310
|
+
const content = paragraphs.map((paragraph) =>
|
|
311
|
+
paragraphFromText(paragraph, false)
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
content: content.length > 0 ? content : [{ type: 'paragraph' }],
|
|
316
|
+
type: 'doc',
|
|
317
|
+
} satisfies JSONContent;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function parseMarkdownTaskDescriptionContent(markdown: string) {
|
|
321
|
+
const content = parseMarkdownBlocks(markdown);
|
|
322
|
+
return {
|
|
323
|
+
content: content.length > 0 ? content : [{ type: 'paragraph' }],
|
|
324
|
+
type: 'doc',
|
|
325
|
+
} satisfies JSONContent;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function normalizeTaskDescriptionContent(
|
|
329
|
+
content: JSONContent
|
|
330
|
+
): JSONContent {
|
|
331
|
+
const transformed = parseTaskDescriptionContent(JSON.stringify(content));
|
|
332
|
+
return ProseMirrorNode.fromJSON(
|
|
333
|
+
taskDescriptionSchema,
|
|
334
|
+
transformed
|
|
335
|
+
).toJSON() as JSONContent;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function parseTaskDescriptionJson(input: string): JSONContent {
|
|
339
|
+
const parsed = JSON.parse(input) as unknown;
|
|
340
|
+
if (
|
|
341
|
+
!parsed ||
|
|
342
|
+
typeof parsed !== 'object' ||
|
|
343
|
+
Array.isArray(parsed) ||
|
|
344
|
+
(parsed as JSONContent).type !== 'doc'
|
|
345
|
+
) {
|
|
346
|
+
throw new Error('Task description JSON must be a TipTap doc object');
|
|
347
|
+
}
|
|
348
|
+
return normalizeTaskDescriptionContent(parsed as JSONContent);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function parseTaskDescriptionInput(
|
|
352
|
+
input: string,
|
|
353
|
+
format: TaskDescriptionInputFormat = 'text'
|
|
354
|
+
): JSONContent {
|
|
355
|
+
if (format === 'json') {
|
|
356
|
+
return parseTaskDescriptionJson(input);
|
|
357
|
+
}
|
|
358
|
+
if (format === 'markdown') {
|
|
359
|
+
return normalizeTaskDescriptionContent(
|
|
360
|
+
parseMarkdownTaskDescriptionContent(input)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return normalizeTaskDescriptionContent(
|
|
364
|
+
createPlainTextTaskDescriptionContent(input)
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function parsePersistedTaskDescriptionContent({
|
|
369
|
+
description,
|
|
370
|
+
description_yjs_state,
|
|
371
|
+
}: {
|
|
372
|
+
description?: string | null;
|
|
373
|
+
description_yjs_state?: number[] | null;
|
|
374
|
+
}): JSONContent {
|
|
375
|
+
if (description_yjs_state && description_yjs_state.length > 0) {
|
|
376
|
+
return decodeTaskDescriptionYjsState(description_yjs_state);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
return normalizeTaskDescriptionContent(
|
|
381
|
+
parseTaskDescriptionContent(description ?? '')
|
|
382
|
+
);
|
|
383
|
+
} catch {
|
|
384
|
+
return normalizeTaskDescriptionContent(
|
|
385
|
+
createPlainTextTaskDescriptionContent(
|
|
386
|
+
getDescriptionText(description ?? '').trim()
|
|
387
|
+
)
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function encodeTaskDescriptionYjsState(content: JSONContent): number[] {
|
|
393
|
+
return Array.from(
|
|
394
|
+
convertJsonContentToYjsState(
|
|
395
|
+
normalizeTaskDescriptionContent(content),
|
|
396
|
+
taskDescriptionSchema
|
|
397
|
+
)
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function decodeTaskDescriptionYjsState(
|
|
402
|
+
yjsState: number[] | Uint8Array
|
|
403
|
+
): JSONContent {
|
|
404
|
+
return convertYjsStateToJsonContent(
|
|
405
|
+
yjsState instanceof Uint8Array ? yjsState : Uint8Array.from(yjsState),
|
|
406
|
+
taskDescriptionSchema
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function taskDescriptionYjsStateToBase64(
|
|
411
|
+
yjsState: number[] | Uint8Array | null | undefined
|
|
412
|
+
): string | null {
|
|
413
|
+
if (yjsState == null) return null;
|
|
414
|
+
const bytes =
|
|
415
|
+
yjsState instanceof Uint8Array ? yjsState : Uint8Array.from(yjsState);
|
|
416
|
+
return Buffer.from(bytes).toString('base64');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function taskDescriptionYjsStateFromBase64(input: string): number[] {
|
|
420
|
+
return Array.from(Buffer.from(input.trim(), 'base64'));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function taskDescriptionYjsStateFromBytesJson(input: string): number[] {
|
|
424
|
+
const parsed = JSON.parse(input) as unknown;
|
|
425
|
+
if (
|
|
426
|
+
!Array.isArray(parsed) ||
|
|
427
|
+
parsed.some((value) => !Number.isInteger(value) || value < 0 || value > 255)
|
|
428
|
+
) {
|
|
429
|
+
throw new Error('Yjs bytes JSON must be an array of byte values');
|
|
430
|
+
}
|
|
431
|
+
return parsed as number[];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function taskDescriptionPlainText(content: JSONContent): string {
|
|
435
|
+
return getDescriptionText(normalizeTaskDescriptionContent(content));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function createTaskDescriptionPayload(
|
|
439
|
+
input: string,
|
|
440
|
+
format: TaskDescriptionInputFormat = 'text'
|
|
441
|
+
): TaskDescriptionPayload {
|
|
442
|
+
const content = parseTaskDescriptionInput(input, format);
|
|
443
|
+
|
|
444
|
+
if (!hasMeaningfulTaskDescriptionContent(content)) {
|
|
445
|
+
return {
|
|
446
|
+
content,
|
|
447
|
+
description: null,
|
|
448
|
+
description_yjs_state: null,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
content,
|
|
454
|
+
description: JSON.stringify(content),
|
|
455
|
+
description_yjs_state: encodeTaskDescriptionYjsState(content),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
@@ -165,6 +165,9 @@ export async function invalidateTaskCaches(
|
|
|
165
165
|
promises.push(
|
|
166
166
|
queryClient.invalidateQueries({ queryKey: ['task_lists', boardId] })
|
|
167
167
|
);
|
|
168
|
+
promises.push(
|
|
169
|
+
queryClient.invalidateQueries({ queryKey: ['kanban-deadline-tasks'] })
|
|
170
|
+
);
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
promises.push(
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { JSONContent } from '@tiptap/core';
|
|
2
|
+
import {
|
|
3
|
+
decodeTaskDescriptionYjsState,
|
|
4
|
+
encodeTaskDescriptionYjsState,
|
|
5
|
+
parsePersistedTaskDescriptionContent,
|
|
6
|
+
parseTaskDescriptionInput,
|
|
7
|
+
} from './task-description-codec';
|
|
2
8
|
import {
|
|
3
9
|
hasMeaningfulTaskDescriptionContent,
|
|
4
10
|
isValidTaskDescriptionContent,
|
|
5
|
-
parseTaskDescriptionContent,
|
|
6
11
|
taskDescriptionSchema,
|
|
7
12
|
} from './task-description-content';
|
|
8
13
|
import { getDescriptionText } from './text-helper';
|
|
9
|
-
import {
|
|
10
|
-
convertJsonContentToYjsState,
|
|
11
|
-
convertYjsStateToJsonContent,
|
|
12
|
-
} from './yjs-helper';
|
|
13
14
|
|
|
14
15
|
export { isValidTaskDescriptionContent, taskDescriptionSchema };
|
|
15
16
|
|
|
@@ -21,10 +22,7 @@ export function isValidTaskDescriptionYjsState(
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
try {
|
|
24
|
-
|
|
25
|
-
Uint8Array.from(yjsState),
|
|
26
|
-
taskDescriptionSchema
|
|
27
|
-
);
|
|
25
|
+
decodeTaskDescriptionYjsState(yjsState);
|
|
28
26
|
return true;
|
|
29
27
|
} catch {
|
|
30
28
|
return false;
|
|
@@ -32,11 +30,11 @@ export function isValidTaskDescriptionYjsState(
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
function encodeYjsState(content: JSONContent): number[] | null {
|
|
35
|
-
const encoded =
|
|
33
|
+
const encoded = encodeTaskDescriptionYjsState(content);
|
|
36
34
|
if (encoded.length === 0) {
|
|
37
35
|
return null;
|
|
38
36
|
}
|
|
39
|
-
return
|
|
37
|
+
return encoded;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
/**
|
|
@@ -51,7 +49,9 @@ export function deriveTaskDescriptionYjsState(
|
|
|
51
49
|
return null;
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
const content =
|
|
52
|
+
const content = parsePersistedTaskDescriptionContent({
|
|
53
|
+
description: normalizedDescription,
|
|
54
|
+
});
|
|
55
55
|
if (!hasMeaningfulTaskDescriptionContent(content)) {
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
@@ -65,15 +65,7 @@ export function deriveTaskDescriptionYjsState(
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
try {
|
|
68
|
-
return encodeYjsState(
|
|
69
|
-
type: 'doc',
|
|
70
|
-
content: [
|
|
71
|
-
{
|
|
72
|
-
type: 'paragraph',
|
|
73
|
-
content: [{ type: 'text', text: fallbackText }],
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
});
|
|
68
|
+
return encodeYjsState(parseTaskDescriptionInput(fallbackText, 'text'));
|
|
77
69
|
} catch {
|
|
78
70
|
return null;
|
|
79
71
|
}
|