@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 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
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "root": false,
3
- "$schema": "https://biomejs.dev/schemas/2.5.8/schema.json",
3
+ "$schema": "https://biomejs.dev/schemas/2.5.9/schema.json",
4
4
  "extends": "//"
5
5
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/utils",
3
3
  "license": "MIT",
4
- "version": "0.25.3",
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.0",
24
- "@tiptap/react": "3.30.0",
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.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.21",
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.1",
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.1",
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.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.5",
53
+ "@vitejs/plugin-react": "^6.1.0",
54
54
  "typescript": "7.0.2",
55
- "vite": "^8.2.1",
56
- "vitest": "^4.1.10"
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.3.14"
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.0'); // x-release-please-version
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', () => {
@@ -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.0'; // x-release-please-version
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
+ }
@@ -11,3 +11,4 @@ export * from './task-helper/sort-keys';
11
11
  export * from './task-helper/task-hooks-basic';
12
12
  export * from './task-helper/task-hooks-move';
13
13
  export * from './task-helper/task-operations';
14
+ export * from './task-helper/task-sort';