@tuturuuu/utils 0.25.3 → 0.25.4
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 +8 -0
- package/biome.json +1 -1
- package/package.json +12 -12
- 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,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.4](https://github.com/tutur3u/platform/compare/utils-v0.25.3...utils-v0.25.4) (2026-08-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **ci:** complete dependency and Rust validation ([61e4c12](https://github.com/tutur3u/platform/commit/61e4c12ec3c90707dde63ed7469854519b3688c8))
|
|
9
|
+
* **tasks:** repair resource CRUD and sorting ([de345a4](https://github.com/tutur3u/platform/commit/de345a40e2948415eaec6f2965dfd83154d83935))
|
|
10
|
+
|
|
3
11
|
## [0.25.3](https://github.com/tutur3u/platform/compare/utils-v0.25.2...utils-v0.25.3) (2026-08-20)
|
|
4
12
|
|
|
5
13
|
|
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.4",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tutur3u/platform",
|
|
@@ -20,40 +20,40 @@
|
|
|
20
20
|
"@noble/ciphers": "^2.3.0",
|
|
21
21
|
"@noble/hashes": "^2.3.0",
|
|
22
22
|
"@tanstack/react-query": "^5.101.4",
|
|
23
|
-
"@tiptap/core": "3.30.
|
|
24
|
-
"@tiptap/react": "3.30.
|
|
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
27
|
"@tuturuuu/internal-api": "0.31.1",
|
|
28
|
-
"@tuturuuu/supabase": "0.5.
|
|
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
|
}
|
|
@@ -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.40.
|
|
10
|
+
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.40.2'); // 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.40.
|
|
3
|
+
export const TUTURUUU_PLATFORM_VERSION = '0.40.2'; // 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