@tuturuuu/ui 0.6.1 → 0.7.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 +25 -0
- package/README.md +3 -3
- package/biome.json +1 -1
- package/package.json +8 -8
- package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
- package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
- package/src/components/ui/calendar.test.tsx +24 -0
- package/src/components/ui/calendar.tsx +1 -0
- package/src/components/ui/date-time-picker.tsx +352 -234
- package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
- package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
- package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
- package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
- package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
- package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
- package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
- package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
- package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
- package/src/components/ui/finance/transactions/form-types.ts +3 -0
- package/src/components/ui/finance/transactions/form.test.tsx +105 -22
- package/src/components/ui/finance/transactions/form.tsx +116 -20
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
- package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
- package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
- package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
- package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
- package/src/components/ui/optional-time-picker.tsx +95 -0
- package/src/components/ui/quick-command-center.test.tsx +90 -0
- package/src/components/ui/quick-command-center.tsx +190 -0
- package/src/components/ui/storefront/cart-summary.tsx +18 -27
- package/src/components/ui/storefront/hero-panel.tsx +22 -13
- package/src/components/ui/storefront/storefront-surface.test.tsx +8 -4
- package/src/components/ui/storefront/storefront-surface.tsx +84 -41
- package/src/components/ui/storefront/types.ts +2 -0
- package/src/components/ui/storefront/utils.ts +21 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const internalApiMocks = vi.hoisted(() => ({
|
|
4
|
+
abortWorkspaceTaskDescriptionChunks: vi.fn(),
|
|
5
|
+
appendWorkspaceTaskDescriptionChunk: vi.fn(),
|
|
6
|
+
beginWorkspaceTaskDescriptionChunks: vi.fn(),
|
|
7
|
+
commitWorkspaceTaskDescriptionChunks: vi.fn(),
|
|
8
|
+
createWorkspaceTaskProject: vi.fn(),
|
|
9
|
+
getWorkspaceTask: vi.fn(),
|
|
10
|
+
getWorkspaceTaskDescription: vi.fn(),
|
|
11
|
+
updateWorkspaceTask: vi.fn(),
|
|
12
|
+
updateWorkspaceTaskDescription: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('@tuturuuu/internal-api/tasks', () => internalApiMocks);
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
shouldChunkTaskDescriptionPayload,
|
|
19
|
+
updateWorkspaceTaskDescription,
|
|
20
|
+
} from './task-api';
|
|
21
|
+
|
|
22
|
+
describe('task-api description persistence', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.resetAllMocks();
|
|
25
|
+
internalApiMocks.abortWorkspaceTaskDescriptionChunks.mockResolvedValue({
|
|
26
|
+
success: true,
|
|
27
|
+
});
|
|
28
|
+
internalApiMocks.appendWorkspaceTaskDescriptionChunk.mockResolvedValue({
|
|
29
|
+
success: true,
|
|
30
|
+
});
|
|
31
|
+
internalApiMocks.beginWorkspaceTaskDescriptionChunks.mockResolvedValue({
|
|
32
|
+
session_id: 'chunk-session-1',
|
|
33
|
+
});
|
|
34
|
+
internalApiMocks.commitWorkspaceTaskDescriptionChunks.mockResolvedValue({
|
|
35
|
+
description: 'persisted',
|
|
36
|
+
description_yjs_state: [1, 2, 3],
|
|
37
|
+
});
|
|
38
|
+
internalApiMocks.updateWorkspaceTaskDescription.mockResolvedValue({
|
|
39
|
+
description: 'small',
|
|
40
|
+
description_yjs_state: [1, 2, 3],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('uses the direct description update for small payloads', async () => {
|
|
45
|
+
const payload = {
|
|
46
|
+
description: JSON.stringify({
|
|
47
|
+
type: 'doc',
|
|
48
|
+
content: [{ type: 'paragraph', content: [{ text: 'Small' }] }],
|
|
49
|
+
}),
|
|
50
|
+
description_yjs_state: [1, 2, 3],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
await expect(
|
|
54
|
+
updateWorkspaceTaskDescription('ws-1', 'task-1', payload)
|
|
55
|
+
).resolves.toEqual({
|
|
56
|
+
description: 'small',
|
|
57
|
+
description_yjs_state: [1, 2, 3],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(
|
|
61
|
+
internalApiMocks.updateWorkspaceTaskDescription
|
|
62
|
+
).toHaveBeenCalledWith('ws-1', 'task-1', payload);
|
|
63
|
+
expect(
|
|
64
|
+
internalApiMocks.beginWorkspaceTaskDescriptionChunks
|
|
65
|
+
).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('uploads large description updates through ordered chunks', async () => {
|
|
69
|
+
const yjsState = Array.from({ length: 220_000 }, (_, index) => index % 256);
|
|
70
|
+
const payload = {
|
|
71
|
+
description: JSON.stringify({
|
|
72
|
+
type: 'doc',
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: 'paragraph',
|
|
76
|
+
content: [{ type: 'text', text: 'Large paste' }],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
}),
|
|
80
|
+
description_yjs_state: yjsState,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
expect(shouldChunkTaskDescriptionPayload(payload)).toBe(true);
|
|
84
|
+
|
|
85
|
+
await updateWorkspaceTaskDescription('ws-1', 'task-1', payload);
|
|
86
|
+
|
|
87
|
+
expect(
|
|
88
|
+
internalApiMocks.updateWorkspaceTaskDescription
|
|
89
|
+
).not.toHaveBeenCalled();
|
|
90
|
+
expect(
|
|
91
|
+
internalApiMocks.beginWorkspaceTaskDescriptionChunks
|
|
92
|
+
).toHaveBeenCalledWith(
|
|
93
|
+
'ws-1',
|
|
94
|
+
'task-1',
|
|
95
|
+
expect.objectContaining({
|
|
96
|
+
description: expect.objectContaining({
|
|
97
|
+
chunk_count: 1,
|
|
98
|
+
}),
|
|
99
|
+
description_yjs_state: expect.objectContaining({
|
|
100
|
+
chunk_count: expect.any(Number),
|
|
101
|
+
}),
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const appendCalls =
|
|
106
|
+
internalApiMocks.appendWorkspaceTaskDescriptionChunk.mock.calls;
|
|
107
|
+
expect(appendCalls.length).toBeGreaterThan(2);
|
|
108
|
+
expect(appendCalls[0]).toEqual([
|
|
109
|
+
'ws-1',
|
|
110
|
+
'task-1',
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
chunk_index: 0,
|
|
113
|
+
field: 'description',
|
|
114
|
+
session_id: 'chunk-session-1',
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
expect(
|
|
118
|
+
appendCalls
|
|
119
|
+
.filter((call) => call[2].field === 'description_yjs_state')
|
|
120
|
+
.map((call) => call[2].chunk_index)
|
|
121
|
+
).toEqual(
|
|
122
|
+
appendCalls
|
|
123
|
+
.filter((call) => call[2].field === 'description_yjs_state')
|
|
124
|
+
.map((_, index) => index)
|
|
125
|
+
);
|
|
126
|
+
expect(
|
|
127
|
+
internalApiMocks.commitWorkspaceTaskDescriptionChunks
|
|
128
|
+
).toHaveBeenCalledWith('ws-1', 'task-1', 'chunk-session-1');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('falls back to chunked upload when a direct save hits the proxy body limit', async () => {
|
|
132
|
+
internalApiMocks.updateWorkspaceTaskDescription.mockRejectedValueOnce({
|
|
133
|
+
status: 413,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await updateWorkspaceTaskDescription('ws-1', 'task-1', {
|
|
137
|
+
description: 'small enough to try directly',
|
|
138
|
+
description_yjs_state: [1, 2, 3],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(
|
|
142
|
+
internalApiMocks.beginWorkspaceTaskDescriptionChunks
|
|
143
|
+
).toHaveBeenCalled();
|
|
144
|
+
expect(
|
|
145
|
+
internalApiMocks.commitWorkspaceTaskDescriptionChunks
|
|
146
|
+
).toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('aborts the chunk session when an append fails', async () => {
|
|
150
|
+
internalApiMocks.appendWorkspaceTaskDescriptionChunk.mockRejectedValueOnce(
|
|
151
|
+
new Error('network down')
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await expect(
|
|
155
|
+
updateWorkspaceTaskDescription('ws-1', 'task-1', {
|
|
156
|
+
description: 'x'.repeat(10),
|
|
157
|
+
description_yjs_state: Array.from(
|
|
158
|
+
{ length: 220_000 },
|
|
159
|
+
(_, index) => index % 256
|
|
160
|
+
),
|
|
161
|
+
})
|
|
162
|
+
).rejects.toThrow('network down');
|
|
163
|
+
|
|
164
|
+
expect(
|
|
165
|
+
internalApiMocks.abortWorkspaceTaskDescriptionChunks
|
|
166
|
+
).toHaveBeenCalledWith('ws-1', 'task-1', 'chunk-session-1');
|
|
167
|
+
expect(
|
|
168
|
+
internalApiMocks.commitWorkspaceTaskDescriptionChunks
|
|
169
|
+
).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
|
+
abortWorkspaceTaskDescriptionChunks,
|
|
3
|
+
appendWorkspaceTaskDescriptionChunk,
|
|
4
|
+
beginWorkspaceTaskDescriptionChunks,
|
|
5
|
+
commitWorkspaceTaskDescriptionChunks,
|
|
2
6
|
createWorkspaceTaskProject,
|
|
3
7
|
getWorkspaceTask,
|
|
8
|
+
getWorkspaceTaskDescription,
|
|
9
|
+
updateWorkspaceTaskDescription as updateWorkspaceTaskDescriptionViaApi,
|
|
4
10
|
updateWorkspaceTask as updateWorkspaceTaskViaApi,
|
|
11
|
+
type WorkspaceTaskDescriptionChunkField,
|
|
12
|
+
type WorkspaceTaskDescriptionChunkFields,
|
|
13
|
+
type WorkspaceTaskDescriptionResponse,
|
|
14
|
+
type WorkspaceTaskDescriptionUpdatePayload,
|
|
5
15
|
type WorkspaceTaskUpdatePayload,
|
|
6
16
|
} from '@tuturuuu/internal-api/tasks';
|
|
7
17
|
import type { WorkspaceTaskLabel } from '../types';
|
|
8
18
|
|
|
19
|
+
const TASK_DESCRIPTION_DIRECT_BODY_LIMIT_BYTES = 192 * 1024;
|
|
20
|
+
const TASK_DESCRIPTION_CHUNK_TEXT_LENGTH = 96 * 1024;
|
|
21
|
+
const TASK_DESCRIPTION_TOO_LARGE_MESSAGE =
|
|
22
|
+
'Description content is too large. Please shorten it or split it into smaller documents.';
|
|
23
|
+
const textEncoder = new TextEncoder();
|
|
24
|
+
|
|
9
25
|
async function getErrorMessage(
|
|
10
26
|
response: Response,
|
|
11
27
|
fallback: string,
|
|
@@ -38,59 +54,207 @@ export async function fetchWorkspaceTask(wsId: string, taskId: string) {
|
|
|
38
54
|
return getWorkspaceTask(wsId, taskId);
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
interface TaskDescriptionResponse {
|
|
42
|
-
description: string | null;
|
|
43
|
-
description_yjs_state: number[] | null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
57
|
export async function fetchWorkspaceTaskDescription(
|
|
47
58
|
wsId: string,
|
|
48
59
|
taskId: string
|
|
49
60
|
) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
method: 'GET',
|
|
53
|
-
cache: 'no-store',
|
|
54
|
-
});
|
|
61
|
+
return getWorkspaceTaskDescription(wsId, taskId);
|
|
62
|
+
}
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
export async function updateWorkspaceTaskDescription(
|
|
65
|
+
wsId: string,
|
|
66
|
+
taskId: string,
|
|
67
|
+
payload: WorkspaceTaskDescriptionUpdatePayload
|
|
68
|
+
) {
|
|
69
|
+
if (shouldChunkTaskDescriptionPayload(payload)) {
|
|
70
|
+
return updateWorkspaceTaskDescriptionChunked(wsId, taskId, payload);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
return await updateWorkspaceTaskDescriptionViaApi(wsId, taskId, payload);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (isPayloadTooLargeError(error)) {
|
|
77
|
+
return updateWorkspaceTaskDescriptionChunked(wsId, taskId, payload);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw normalizeTaskDescriptionError(
|
|
81
|
+
error,
|
|
82
|
+
'Failed to update task description'
|
|
61
83
|
);
|
|
62
84
|
}
|
|
85
|
+
}
|
|
63
86
|
|
|
64
|
-
|
|
87
|
+
export function shouldChunkTaskDescriptionPayload(
|
|
88
|
+
payload: WorkspaceTaskDescriptionUpdatePayload
|
|
89
|
+
) {
|
|
90
|
+
return getJsonByteLength(payload) > TASK_DESCRIPTION_DIRECT_BODY_LIMIT_BYTES;
|
|
65
91
|
}
|
|
66
92
|
|
|
67
|
-
export async function
|
|
93
|
+
export async function updateWorkspaceTaskDescriptionChunked(
|
|
68
94
|
wsId: string,
|
|
69
95
|
taskId: string,
|
|
70
|
-
payload:
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
payload: WorkspaceTaskDescriptionUpdatePayload
|
|
97
|
+
): Promise<WorkspaceTaskDescriptionResponse> {
|
|
98
|
+
const chunksByField = buildTaskDescriptionChunks(payload);
|
|
99
|
+
const fields = buildTaskDescriptionChunkFields(chunksByField);
|
|
100
|
+
|
|
101
|
+
if (Object.keys(fields).length === 0) {
|
|
102
|
+
return updateWorkspaceTaskDescriptionViaApi(wsId, taskId, payload);
|
|
73
103
|
}
|
|
74
|
-
) {
|
|
75
|
-
const requestPath = `/api/v1/workspaces/${wsId}/tasks/${taskId}/description`;
|
|
76
|
-
const response = await fetch(requestPath, {
|
|
77
|
-
method: 'PATCH',
|
|
78
|
-
headers: {
|
|
79
|
-
'Content-Type': 'application/json',
|
|
80
|
-
},
|
|
81
|
-
body: JSON.stringify(payload),
|
|
82
|
-
cache: 'no-store',
|
|
83
|
-
});
|
|
84
104
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
const { session_id: sessionId } = await beginWorkspaceTaskDescriptionChunks(
|
|
106
|
+
wsId,
|
|
107
|
+
taskId,
|
|
108
|
+
fields
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
for (const field of ['description', 'description_yjs_state'] as const) {
|
|
113
|
+
const chunks = chunksByField[field];
|
|
114
|
+
if (!chunks) continue;
|
|
115
|
+
|
|
116
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
117
|
+
await appendWorkspaceTaskDescriptionChunk(wsId, taskId, {
|
|
118
|
+
chunk: chunks[index] ?? '',
|
|
119
|
+
chunk_index: index,
|
|
120
|
+
field,
|
|
121
|
+
session_id: sessionId,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return await commitWorkspaceTaskDescriptionChunks(wsId, taskId, sessionId);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
await abortWorkspaceTaskDescriptionChunks(wsId, taskId, sessionId).catch(
|
|
129
|
+
() => undefined
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
throw normalizeTaskDescriptionError(
|
|
133
|
+
error,
|
|
134
|
+
'Failed to update task description'
|
|
90
135
|
);
|
|
91
136
|
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getJsonByteLength(value: unknown) {
|
|
140
|
+
return textEncoder.encode(JSON.stringify(value)).byteLength;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isPayloadTooLargeError(error: unknown) {
|
|
144
|
+
return (
|
|
145
|
+
typeof error === 'object' &&
|
|
146
|
+
error !== null &&
|
|
147
|
+
'status' in error &&
|
|
148
|
+
(error as { status?: unknown }).status === 413
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeTaskDescriptionError(error: unknown, fallback: string) {
|
|
153
|
+
if (isPayloadTooLargeError(error)) {
|
|
154
|
+
return new Error(TASK_DESCRIPTION_TOO_LARGE_MESSAGE);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return error instanceof Error ? error : new Error(fallback);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function splitStringIntoChunks(value: string) {
|
|
161
|
+
const chunks: string[] = [];
|
|
162
|
+
|
|
163
|
+
for (
|
|
164
|
+
let index = 0;
|
|
165
|
+
index < value.length;
|
|
166
|
+
index += TASK_DESCRIPTION_CHUNK_TEXT_LENGTH
|
|
167
|
+
) {
|
|
168
|
+
chunks.push(value.slice(index, index + TASK_DESCRIPTION_CHUNK_TEXT_LENGTH));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return chunks.length > 0 ? chunks : [''];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function bytesToBase64(bytes: number[]) {
|
|
175
|
+
const byteArray = Uint8Array.from(bytes);
|
|
176
|
+
|
|
177
|
+
if (typeof btoa === 'function') {
|
|
178
|
+
let binary = '';
|
|
179
|
+
const step = 0x8000;
|
|
180
|
+
|
|
181
|
+
for (let index = 0; index < byteArray.length; index += step) {
|
|
182
|
+
binary += String.fromCharCode(...byteArray.subarray(index, index + step));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return btoa(binary);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const nodeBuffer = (
|
|
189
|
+
globalThis as typeof globalThis & {
|
|
190
|
+
Buffer?: {
|
|
191
|
+
from: (value: Uint8Array) => {
|
|
192
|
+
toString: (encoding: 'base64') => string;
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
).Buffer;
|
|
197
|
+
|
|
198
|
+
if (!nodeBuffer) {
|
|
199
|
+
throw new Error('Unable to encode task description state.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return nodeBuffer.from(byteArray).toString('base64');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildTaskDescriptionChunks(
|
|
206
|
+
payload: WorkspaceTaskDescriptionUpdatePayload
|
|
207
|
+
): Partial<Record<WorkspaceTaskDescriptionChunkField, string[] | null>> {
|
|
208
|
+
const chunks: Partial<
|
|
209
|
+
Record<WorkspaceTaskDescriptionChunkField, string[] | null>
|
|
210
|
+
> = {};
|
|
211
|
+
|
|
212
|
+
if ('description' in payload) {
|
|
213
|
+
chunks.description =
|
|
214
|
+
payload.description === null
|
|
215
|
+
? null
|
|
216
|
+
: splitStringIntoChunks(payload.description ?? '');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if ('description_yjs_state' in payload) {
|
|
220
|
+
chunks.description_yjs_state =
|
|
221
|
+
payload.description_yjs_state === null
|
|
222
|
+
? null
|
|
223
|
+
: splitStringIntoChunks(
|
|
224
|
+
bytesToBase64(payload.description_yjs_state ?? [])
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return chunks;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function buildTaskDescriptionChunkFields(
|
|
232
|
+
chunksByField: Partial<
|
|
233
|
+
Record<WorkspaceTaskDescriptionChunkField, string[] | null>
|
|
234
|
+
>
|
|
235
|
+
) {
|
|
236
|
+
const fields: WorkspaceTaskDescriptionChunkFields = {};
|
|
237
|
+
|
|
238
|
+
for (const field of ['description', 'description_yjs_state'] as const) {
|
|
239
|
+
const chunks = chunksByField[field];
|
|
240
|
+
if (chunks === undefined) continue;
|
|
241
|
+
|
|
242
|
+
if (chunks === null) {
|
|
243
|
+
fields[field] = {
|
|
244
|
+
chunk_count: 0,
|
|
245
|
+
is_null: true,
|
|
246
|
+
total_length: 0,
|
|
247
|
+
};
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fields[field] = {
|
|
252
|
+
chunk_count: chunks.length,
|
|
253
|
+
total_length: chunks.join('').length,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
92
256
|
|
|
93
|
-
return
|
|
257
|
+
return fields;
|
|
94
258
|
}
|
|
95
259
|
|
|
96
260
|
export async function createWorkspaceLabel(
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
updateTaskDescriptionCaches,
|
|
35
35
|
} from '../utils';
|
|
36
36
|
import {
|
|
37
|
+
shouldChunkTaskDescriptionPayload,
|
|
37
38
|
updateWorkspaceTask,
|
|
38
39
|
updateWorkspaceTaskDescription,
|
|
39
40
|
} from './task-api';
|
|
@@ -945,9 +946,17 @@ export async function handleCreateTask({
|
|
|
945
946
|
];
|
|
946
947
|
}
|
|
947
948
|
|
|
949
|
+
const createDescriptionPayload = {
|
|
950
|
+
description: descriptionString || '',
|
|
951
|
+
description_yjs_state: descriptionYjsState ?? undefined,
|
|
952
|
+
};
|
|
953
|
+
const shouldDeferDescription = shouldChunkTaskDescriptionPayload(
|
|
954
|
+
createDescriptionPayload
|
|
955
|
+
);
|
|
956
|
+
|
|
948
957
|
const taskData: Partial<Task> = {
|
|
949
958
|
name: name.trim(),
|
|
950
|
-
description: descriptionString || '',
|
|
959
|
+
description: shouldDeferDescription ? '' : descriptionString || '',
|
|
951
960
|
priority: priority,
|
|
952
961
|
start_date: startDate ? startDate.toISOString() : undefined,
|
|
953
962
|
end_date: endDate ? endDate.toISOString() : undefined,
|
|
@@ -957,7 +966,9 @@ export async function handleCreateTask({
|
|
|
957
966
|
};
|
|
958
967
|
const newTask = await createTask(wsId, selectedListId, {
|
|
959
968
|
...taskData,
|
|
960
|
-
description_yjs_state:
|
|
969
|
+
description_yjs_state: shouldDeferDescription
|
|
970
|
+
? undefined
|
|
971
|
+
: (descriptionYjsState ?? undefined),
|
|
961
972
|
label_ids: selectedLabels.map((label) => label.id),
|
|
962
973
|
assignee_ids: desiredAssignees
|
|
963
974
|
.map((assignee) => assignee.user_id || assignee.id)
|
|
@@ -965,6 +976,14 @@ export async function handleCreateTask({
|
|
|
965
976
|
project_ids: selectedProjects.map((project) => project.id),
|
|
966
977
|
});
|
|
967
978
|
|
|
979
|
+
if (shouldDeferDescription) {
|
|
980
|
+
await updateWorkspaceTaskDescription(wsId, newTask.id, {
|
|
981
|
+
description: descriptionString,
|
|
982
|
+
description_yjs_state: descriptionYjsState,
|
|
983
|
+
});
|
|
984
|
+
newTask.description = descriptionString ?? undefined;
|
|
985
|
+
}
|
|
986
|
+
|
|
968
987
|
// Save per-user scheduling settings for the creator (if any were provided)
|
|
969
988
|
const hasAnySchedulingValue =
|
|
970
989
|
totalDuration != null ||
|