@tuturuuu/utils 0.25.3 → 0.25.5
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 +15 -0
- package/biome.json +1 -1
- package/package.json +14 -14
- package/src/next-config.test.ts +5 -0
- package/src/next-config.ts +1 -0
- package/src/platform-release.test.ts +1 -1
- package/src/platform-release.ts +1 -1
- package/src/task-helper/task-sort.test.ts +78 -0
- package/src/task-helper/task-sort.ts +162 -0
- package/src/task-helper.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.5](https://github.com/tutur3u/platform/compare/utils-v0.25.4...utils-v0.25.5) (2026-08-25)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **contacts:** proxy feedbacks before dynamic users ([910b79e](https://github.com/tutur3u/platform/commit/910b79ef5a53e31b865cb1290e2f2b29d19c1702))
|
|
9
|
+
|
|
10
|
+
## [0.25.4](https://github.com/tutur3u/platform/compare/utils-v0.25.3...utils-v0.25.4) (2026-08-21)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **ci:** complete dependency and Rust validation ([61e4c12](https://github.com/tutur3u/platform/commit/61e4c12ec3c90707dde63ed7469854519b3688c8))
|
|
16
|
+
* **tasks:** repair resource CRUD and sorting ([de345a4](https://github.com/tutur3u/platform/commit/de345a40e2948415eaec6f2965dfd83154d83935))
|
|
17
|
+
|
|
3
18
|
## [0.25.3](https://github.com/tutur3u/platform/compare/utils-v0.25.2...utils-v0.25.3) (2026-08-20)
|
|
4
19
|
|
|
5
20
|
|
package/biome.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/utils",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "0.25.
|
|
4
|
+
"version": "0.25.5",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tutur3u/platform",
|
|
@@ -19,41 +19,41 @@
|
|
|
19
19
|
"@dnd-kit/core": "^6.3.1",
|
|
20
20
|
"@noble/ciphers": "^2.3.0",
|
|
21
21
|
"@noble/hashes": "^2.3.0",
|
|
22
|
-
"@tanstack/react-query": "^5.
|
|
23
|
-
"@tiptap/core": "3.30.
|
|
24
|
-
"@tiptap/react": "3.30.
|
|
22
|
+
"@tanstack/react-query": "^5.102.1",
|
|
23
|
+
"@tiptap/core": "3.30.2",
|
|
24
|
+
"@tiptap/react": "3.30.2",
|
|
25
25
|
"@tuturuuu/google": "0.1.0",
|
|
26
26
|
"@tuturuuu/icons": "0.1.0",
|
|
27
|
-
"@tuturuuu/internal-api": "0.
|
|
28
|
-
"@tuturuuu/supabase": "0.5.
|
|
27
|
+
"@tuturuuu/internal-api": "0.32.0",
|
|
28
|
+
"@tuturuuu/supabase": "0.5.1",
|
|
29
29
|
"@upstash/ratelimit": "^2.0.8",
|
|
30
30
|
"@upstash/redis": "^1.38.2",
|
|
31
31
|
"clsx": "^2.1.1",
|
|
32
32
|
"date-fns": "^4.4.0",
|
|
33
|
-
"dayjs": "^1.11.
|
|
33
|
+
"dayjs": "^1.11.23",
|
|
34
34
|
"diff": "^9.0.0",
|
|
35
35
|
"effect": "^3.22.1",
|
|
36
36
|
"moment": "^2.30.1",
|
|
37
|
-
"next": "^16.3.
|
|
37
|
+
"next": "^16.3.2",
|
|
38
38
|
"prosemirror-model": "^1.25.11",
|
|
39
39
|
"react": "^19.2.8",
|
|
40
40
|
"react-dom": "^19.2.8",
|
|
41
41
|
"tailwind-merge": "^3.6.0",
|
|
42
|
-
"uuid": "^14.0.
|
|
42
|
+
"uuid": "^14.0.2",
|
|
43
43
|
"y-prosemirror": "^1.3.7",
|
|
44
44
|
"yjs": "^13.6.32",
|
|
45
45
|
"zod": "^4.4.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@tuturuuu/types": "0.27.0",
|
|
49
|
-
"@tuturuuu/typescript-config": "0.1.
|
|
49
|
+
"@tuturuuu/typescript-config": "0.1.2",
|
|
50
50
|
"@types/diff": "^8.0.0",
|
|
51
51
|
"@types/node": "^26.2.0",
|
|
52
52
|
"@types/react": "^19.2.18",
|
|
53
|
-
"@vitejs/plugin-react": "^6.0
|
|
53
|
+
"@vitejs/plugin-react": "^6.1.0",
|
|
54
54
|
"typescript": "7.0.2",
|
|
55
|
-
"vite": "^8.2.
|
|
56
|
-
"vitest": "^4.1.
|
|
55
|
+
"vite": "^8.2.2",
|
|
56
|
+
"vitest": "^4.1.11"
|
|
57
57
|
},
|
|
58
58
|
"exports": {
|
|
59
59
|
"./timezones": "./src/timezones.json",
|
|
@@ -77,5 +77,5 @@
|
|
|
77
77
|
"./encryption": "./src/encryption/index.ts",
|
|
78
78
|
"./finance": "./src/finance/index.ts"
|
|
79
79
|
},
|
|
80
|
-
"packageManager": "bun@1.
|
|
80
|
+
"packageManager": "bun@1.4.0"
|
|
81
81
|
}
|
package/src/next-config.test.ts
CHANGED
|
@@ -290,6 +290,11 @@ describe('createTuturuuuWebWorkspaceApiRewrites', () => {
|
|
|
290
290
|
destination:
|
|
291
291
|
'https://tuturuuu.com/api/v1/workspaces/:wsId/settings/permissions',
|
|
292
292
|
},
|
|
293
|
+
{
|
|
294
|
+
source: '/api/v1/workspaces/:wsId/users/feedbacks',
|
|
295
|
+
destination:
|
|
296
|
+
'https://tuturuuu.com/api/v1/workspaces/:wsId/users/feedbacks',
|
|
297
|
+
},
|
|
293
298
|
]);
|
|
294
299
|
});
|
|
295
300
|
});
|
package/src/next-config.ts
CHANGED
|
@@ -16,6 +16,7 @@ export const TUTURUUU_NEXT_OPTIMIZE_PACKAGE_IMPORTS = [
|
|
|
16
16
|
export const TUTURUUU_WEB_WORKSPACE_API_RESERVED_PATHS = [
|
|
17
17
|
'/api/workspaces/invitations',
|
|
18
18
|
'/api/v1/workspaces/:wsId/settings/permissions',
|
|
19
|
+
'/api/v1/workspaces/:wsId/users/feedbacks',
|
|
19
20
|
] as const;
|
|
20
21
|
|
|
21
22
|
type NextImageConfig = NonNullable<NextConfig['images']>;
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
describe('platform release metadata', () => {
|
|
9
9
|
it('uses the centralized shared browser app version', () => {
|
|
10
|
-
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.
|
|
10
|
+
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.41.0'); // x-release-please-version
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it('preserves the source timestamp in builtAt and derives a short hash', () => {
|
package/src/platform-release.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
|
|
2
2
|
|
|
3
|
-
export const TUTURUUU_PLATFORM_VERSION = '0.
|
|
3
|
+
export const TUTURUUU_PLATFORM_VERSION = '0.41.0'; // x-release-please-version
|
|
4
4
|
|
|
5
5
|
export type PlatformBuildMetadataInput = {
|
|
6
6
|
/** ISO timestamp of the source commit used for the build, not deployment time. */
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
parseTaskSortBy,
|
|
4
|
+
type SortableTask,
|
|
5
|
+
sortTasksByCriterion,
|
|
6
|
+
} from './task-sort';
|
|
7
|
+
|
|
8
|
+
const task = (
|
|
9
|
+
id: string,
|
|
10
|
+
values: Partial<SortableTask> = {}
|
|
11
|
+
): SortableTask => ({
|
|
12
|
+
id,
|
|
13
|
+
name: id,
|
|
14
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
15
|
+
...values,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('sortTasksByCriterion', () => {
|
|
19
|
+
it('accepts only supported server and client sort criteria', () => {
|
|
20
|
+
expect(parseTaskSortBy('priority-high')).toBe('priority-high');
|
|
21
|
+
expect(parseTaskSortBy('unknown')).toBeUndefined();
|
|
22
|
+
expect(parseTaskSortBy(null)).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('sorts every priority direction with unset priorities last', () => {
|
|
26
|
+
const tasks = [
|
|
27
|
+
task('none'),
|
|
28
|
+
task('high', { priority: 'high' }),
|
|
29
|
+
task('low', { priority: 'low' }),
|
|
30
|
+
task('critical', { priority: 'critical' }),
|
|
31
|
+
task('normal', { priority: 'normal' }),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
expect(
|
|
35
|
+
sortTasksByCriterion(tasks, 'priority-high').map(({ id }) => id)
|
|
36
|
+
).toEqual(['critical', 'high', 'normal', 'low', 'none']);
|
|
37
|
+
expect(
|
|
38
|
+
sortTasksByCriterion(tasks, 'priority-low').map(({ id }) => id)
|
|
39
|
+
).toEqual(['low', 'normal', 'high', 'critical', 'none']);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('keeps missing due dates and estimations last in both directions', () => {
|
|
43
|
+
const tasks = [
|
|
44
|
+
task('none'),
|
|
45
|
+
task('invalid', { end_date: 'not-a-date' }),
|
|
46
|
+
task('early', {
|
|
47
|
+
end_date: '2026-01-02T00:00:00.000Z',
|
|
48
|
+
estimation_points: 1,
|
|
49
|
+
}),
|
|
50
|
+
task('late', {
|
|
51
|
+
end_date: '2026-01-03T00:00:00.000Z',
|
|
52
|
+
estimation_points: 8,
|
|
53
|
+
}),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
sortTasksByCriterion(tasks, 'due-date-desc').map(({ id }) => id)
|
|
58
|
+
).toEqual(['late', 'early', 'invalid', 'none']);
|
|
59
|
+
expect(
|
|
60
|
+
sortTasksByCriterion(tasks, 'estimation-low').map(({ id }) => id)
|
|
61
|
+
).toEqual(['early', 'late', 'invalid', 'none']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('uses newest creation time and then id as stable tie-breakers', () => {
|
|
65
|
+
const tasks = [
|
|
66
|
+
task('b', { priority: 'high' }),
|
|
67
|
+
task('c', {
|
|
68
|
+
priority: 'high',
|
|
69
|
+
created_at: '2026-01-02T00:00:00.000Z',
|
|
70
|
+
}),
|
|
71
|
+
task('a', { priority: 'high' }),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
expect(
|
|
75
|
+
sortTasksByCriterion(tasks, 'priority-high').map(({ id }) => id)
|
|
76
|
+
).toEqual(['c', 'a', 'b']);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
|
|
2
|
+
|
|
3
|
+
export type TaskSortBy =
|
|
4
|
+
| 'name-asc'
|
|
5
|
+
| 'name-desc'
|
|
6
|
+
| 'priority-high'
|
|
7
|
+
| 'priority-low'
|
|
8
|
+
| 'due-date-asc'
|
|
9
|
+
| 'due-date-desc'
|
|
10
|
+
| 'created-date-desc'
|
|
11
|
+
| 'created-date-asc'
|
|
12
|
+
| 'estimation-high'
|
|
13
|
+
| 'estimation-low';
|
|
14
|
+
|
|
15
|
+
export type SortableTask = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
priority?: TaskPriority | null;
|
|
19
|
+
end_date?: string | null;
|
|
20
|
+
created_at?: string | null;
|
|
21
|
+
estimation_points?: number | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function parseTaskSortBy(value: string | null) {
|
|
25
|
+
switch (value) {
|
|
26
|
+
case 'name-asc':
|
|
27
|
+
case 'name-desc':
|
|
28
|
+
case 'priority-high':
|
|
29
|
+
case 'priority-low':
|
|
30
|
+
case 'due-date-asc':
|
|
31
|
+
case 'due-date-desc':
|
|
32
|
+
case 'created-date-desc':
|
|
33
|
+
case 'created-date-asc':
|
|
34
|
+
case 'estimation-high':
|
|
35
|
+
case 'estimation-low':
|
|
36
|
+
return value satisfies TaskSortBy;
|
|
37
|
+
default:
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PRIORITY_RANK: Record<TaskPriority, number> = {
|
|
43
|
+
critical: 0,
|
|
44
|
+
high: 1,
|
|
45
|
+
normal: 2,
|
|
46
|
+
low: 3,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function compareNullable<T>(
|
|
50
|
+
a: T | null | undefined,
|
|
51
|
+
b: T | null | undefined,
|
|
52
|
+
compare: (left: T, right: T) => number
|
|
53
|
+
) {
|
|
54
|
+
if (a == null && b == null) return 0;
|
|
55
|
+
if (a == null) return 1;
|
|
56
|
+
if (b == null) return -1;
|
|
57
|
+
return compare(a, b);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compareText(a: string, b: string) {
|
|
61
|
+
const left = a.toLowerCase();
|
|
62
|
+
const right = b.toLowerCase();
|
|
63
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getTimestamp(value: string | null | undefined) {
|
|
67
|
+
if (!value) return null;
|
|
68
|
+
const timestamp = new Date(value).getTime();
|
|
69
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function compareCreatedFallback(a: SortableTask, b: SortableTask) {
|
|
73
|
+
const created = compareNullable(
|
|
74
|
+
getTimestamp(a.created_at),
|
|
75
|
+
getTimestamp(b.created_at),
|
|
76
|
+
(left, right) => right - left
|
|
77
|
+
);
|
|
78
|
+
return created || a.id.localeCompare(b.id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function compareTasksByCriterion(
|
|
82
|
+
a: SortableTask,
|
|
83
|
+
b: SortableTask,
|
|
84
|
+
sortBy: TaskSortBy
|
|
85
|
+
) {
|
|
86
|
+
let result = 0;
|
|
87
|
+
|
|
88
|
+
switch (sortBy) {
|
|
89
|
+
case 'name-asc':
|
|
90
|
+
result = compareText(a.name, b.name);
|
|
91
|
+
break;
|
|
92
|
+
case 'name-desc':
|
|
93
|
+
result = compareText(b.name, a.name);
|
|
94
|
+
break;
|
|
95
|
+
case 'priority-high':
|
|
96
|
+
result = compareNullable(
|
|
97
|
+
a.priority,
|
|
98
|
+
b.priority,
|
|
99
|
+
(left, right) => PRIORITY_RANK[left] - PRIORITY_RANK[right]
|
|
100
|
+
);
|
|
101
|
+
break;
|
|
102
|
+
case 'priority-low':
|
|
103
|
+
result = compareNullable(
|
|
104
|
+
a.priority,
|
|
105
|
+
b.priority,
|
|
106
|
+
(left, right) => PRIORITY_RANK[right] - PRIORITY_RANK[left]
|
|
107
|
+
);
|
|
108
|
+
break;
|
|
109
|
+
case 'due-date-asc':
|
|
110
|
+
result = compareNullable(
|
|
111
|
+
getTimestamp(a.end_date),
|
|
112
|
+
getTimestamp(b.end_date),
|
|
113
|
+
(left, right) => left - right
|
|
114
|
+
);
|
|
115
|
+
break;
|
|
116
|
+
case 'due-date-desc':
|
|
117
|
+
result = compareNullable(
|
|
118
|
+
getTimestamp(a.end_date),
|
|
119
|
+
getTimestamp(b.end_date),
|
|
120
|
+
(left, right) => right - left
|
|
121
|
+
);
|
|
122
|
+
break;
|
|
123
|
+
case 'created-date-asc':
|
|
124
|
+
result = compareNullable(
|
|
125
|
+
getTimestamp(a.created_at),
|
|
126
|
+
getTimestamp(b.created_at),
|
|
127
|
+
(left, right) => left - right
|
|
128
|
+
);
|
|
129
|
+
break;
|
|
130
|
+
case 'created-date-desc':
|
|
131
|
+
result = compareNullable(
|
|
132
|
+
getTimestamp(a.created_at),
|
|
133
|
+
getTimestamp(b.created_at),
|
|
134
|
+
(left, right) => right - left
|
|
135
|
+
);
|
|
136
|
+
break;
|
|
137
|
+
case 'estimation-high':
|
|
138
|
+
result = compareNullable(
|
|
139
|
+
a.estimation_points,
|
|
140
|
+
b.estimation_points,
|
|
141
|
+
(left, right) => right - left
|
|
142
|
+
);
|
|
143
|
+
break;
|
|
144
|
+
case 'estimation-low':
|
|
145
|
+
result = compareNullable(
|
|
146
|
+
a.estimation_points,
|
|
147
|
+
b.estimation_points,
|
|
148
|
+
(left, right) => left - right
|
|
149
|
+
);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result || compareCreatedFallback(a, b);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function sortTasksByCriterion<T extends SortableTask>(
|
|
157
|
+
tasks: readonly T[],
|
|
158
|
+
sortBy: TaskSortBy | null | undefined
|
|
159
|
+
) {
|
|
160
|
+
if (!sortBy) return [...tasks];
|
|
161
|
+
return [...tasks].sort((a, b) => compareTasksByCriterion(a, b, sortBy));
|
|
162
|
+
}
|
package/src/task-helper.ts
CHANGED