@tuturuuu/ui 0.26.0 → 0.27.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 +36 -0
- package/README.md +29 -7
- package/biome.json +1 -1
- package/package.json +48 -54
- package/src/components/ui/calendar-app/hooks/use-calendar-settings.test.ts +32 -0
- package/src/components/ui/custom/combobox.tsx +4 -0
- package/src/components/ui/custom/nav-link.test.tsx +47 -0
- package/src/components/ui/custom/nav-link.tsx +28 -3
- package/src/components/ui/custom/workspace-access/adapters.test.ts +89 -1
- package/src/components/ui/custom/workspace-access/adapters.ts +67 -2
- package/src/components/ui/custom/workspace-access/types.ts +13 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.test.tsx +79 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.tsx +125 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-access-picker.tsx +108 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.test.tsx +134 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +94 -113
- package/src/components/ui/custom/workspace-access/workspace-access-invite-pos-panel.tsx +75 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-role-picker.tsx +151 -0
- package/src/components/ui/custom/workspace-access/workspace-access-labels.ts +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +27 -2
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +10 -0
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +102 -3
- package/src/components/ui/custom/workspace-access/workspace-access-role-options.test.ts +35 -0
- package/src/components/ui/custom/workspace-access/workspace-access-role-options.ts +21 -0
- package/src/components/ui/custom/workspace-select-invitations.tsx +2 -1
- package/src/components/ui/legacy/polls/poll-display.test.tsx +118 -0
- package/src/components/ui/legacy/polls/poll-display.tsx +8 -0
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -1
- package/src/hooks/use-board-actions.test.ts +23 -0
- package/src/hooks/use-board-actions.ts +48 -35
- package/src/hooks/use-calendar-sync.tsx +12 -12
- package/src/hooks/use-notifications.ts +4 -8
- package/src/lib/calendar-settings-resolver.ts +1 -200
- package/src/readme-contract.test.tsx +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.27.0](https://github.com/tutur3u/platform/compare/ui-v0.26.1...ui-v0.27.0) (2026-08-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **tasks:** add board lifecycle actions ([3295657](https://github.com/tutur3u/platform/commit/329565797b8bb3cefdf8f3812e6f5391576bf394))
|
|
9
|
+
* **workspaces:** assign roles to pending invites ([#5125](https://github.com/tutur3u/platform/issues/5125)) ([d4052fd](https://github.com/tutur3u/platform/commit/d4052fd40de66bda4e9535740bbcfd5a8da19123))
|
|
10
|
+
* **workspaces:** revamp invitation access flow ([7ea94af](https://github.com/tutur3u/platform/commit/7ea94afc2c5e14af1c83d1478ad9c006268b13c0))
|
|
11
|
+
* **workspaces:** support multi-role invitations ([f6f78ba](https://github.com/tutur3u/platform/commit/f6f78bac6c2120fd70c09e85c075c4206be4f897))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **contacts:** preserve server referral search results ([8d0941d](https://github.com/tutur3u/platform/commit/8d0941de693fb784fd2487582d905f2cbd0fb16e))
|
|
17
|
+
* **tasks:** allow board lifecycle actions ([9a6bc0c](https://github.com/tutur3u/platform/commit/9a6bc0ce31c4191bed81607fdb5c9ee2af8e1d04))
|
|
18
|
+
* **workspaces:** manage roles for pending invites ([2466f6c](https://github.com/tutur3u/platform/commit/2466f6cbbd447207d87eb0e0d78b3c713b02b739))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Performance Improvements
|
|
22
|
+
|
|
23
|
+
* **vercel:** reduce realtime fallback polling ([e9e44a4](https://github.com/tutur3u/platform/commit/e9e44a4eb17c971beb9e1b737c4580466e427be5))
|
|
24
|
+
* **vercel:** reduce realtime fallback polling ([#5126](https://github.com/tutur3u/platform/issues/5126)) ([fde2692](https://github.com/tutur3u/platform/commit/fde2692ff73a45616a40b93de9fff30ffc941fd2))
|
|
25
|
+
|
|
26
|
+
## [0.26.1](https://github.com/tutur3u/platform/compare/ui-v0.26.0...ui-v0.26.1) (2026-08-11)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* **vercel:** harden navigation prefetch policy ([b8cf61c](https://github.com/tutur3u/platform/commit/b8cf61c86727817faff1f28da768fee4b3e52e68))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Performance Improvements
|
|
35
|
+
|
|
36
|
+
* **vercel:** reduce speculative app requests ([423e240](https://github.com/tutur3u/platform/commit/423e2401c16b2cad4785ba46a08bf5b3d1c8a749))
|
|
37
|
+
* **vercel:** reduce speculative app requests ([#5116](https://github.com/tutur3u/platform/issues/5116)) ([9e2b59b](https://github.com/tutur3u/platform/commit/9e2b59bcaa0bbc7a593ff91e2d2331aca4ab6595))
|
|
38
|
+
|
|
3
39
|
## [0.26.0](https://github.com/tutur3u/platform/compare/ui-v0.25.3...ui-v0.26.0) (2026-08-09)
|
|
4
40
|
|
|
5
41
|
|
package/README.md
CHANGED
|
@@ -6,12 +6,11 @@ Shared UI components and design system for Tuturuuu Platform.
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install @tuturuuu/ui
|
|
9
|
-
# or
|
|
10
|
-
yarn add @tuturuuu/ui
|
|
11
|
-
# or
|
|
12
|
-
bun add @tuturuuu/ui
|
|
13
9
|
```
|
|
14
10
|
|
|
11
|
+
Using another package manager? Run `pnpm add @tuturuuu/ui`,
|
|
12
|
+
`yarn add @tuturuuu/ui`, or `bun add @tuturuuu/ui` instead.
|
|
13
|
+
|
|
15
14
|
The public package depends on other published Tuturuuu packages, including
|
|
16
15
|
`@tuturuuu/apis` and `@tuturuuu/ai`. Release those dependencies first, then
|
|
17
16
|
publish UI from the same production commit so standalone apps can install
|
|
@@ -19,18 +18,41 @@ publish UI from the same production commit so standalone apps can install
|
|
|
19
18
|
|
|
20
19
|
## Usage
|
|
21
20
|
|
|
21
|
+
Import the shared stylesheet once from your app's root layout or entry point:
|
|
22
|
+
|
|
22
23
|
```typescript
|
|
23
|
-
import
|
|
24
|
+
import '@tuturuuu/ui/globals.css';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then import each component from its public subpath:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
31
|
+
import {
|
|
32
|
+
Card,
|
|
33
|
+
CardContent,
|
|
34
|
+
CardHeader,
|
|
35
|
+
CardTitle,
|
|
36
|
+
} from '@tuturuuu/ui/card';
|
|
24
37
|
|
|
25
38
|
export default function MyComponent() {
|
|
26
39
|
return (
|
|
27
40
|
<Card>
|
|
28
|
-
<
|
|
41
|
+
<CardHeader>
|
|
42
|
+
<CardTitle>Welcome to Tuturuuu</CardTitle>
|
|
43
|
+
</CardHeader>
|
|
44
|
+
<CardContent>
|
|
45
|
+
<Button>Get started</Button>
|
|
46
|
+
</CardContent>
|
|
29
47
|
</Card>
|
|
30
|
-
)
|
|
48
|
+
);
|
|
31
49
|
}
|
|
32
50
|
```
|
|
33
51
|
|
|
52
|
+
The package exports TypeScript and TSX source files. Configure your framework
|
|
53
|
+
or bundler to transpile `@tuturuuu/ui` and its published Tuturuuu dependencies
|
|
54
|
+
when it does not transpile package source automatically.
|
|
55
|
+
|
|
34
56
|
## Features
|
|
35
57
|
|
|
36
58
|
- Fully accessible components
|
package/biome.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,43 +56,43 @@
|
|
|
56
56
|
"@streamdown/math": "^1.0.2",
|
|
57
57
|
"@streamdown/mermaid": "^1.0.2",
|
|
58
58
|
"@tanstack/react-hotkeys": "^0.10.0",
|
|
59
|
-
"@tanstack/react-pacer": "^0.
|
|
59
|
+
"@tanstack/react-pacer": "^0.23.0",
|
|
60
60
|
"@tanstack/react-query": "^5.101.2",
|
|
61
61
|
"@tanstack/react-table": "^9.0.0",
|
|
62
62
|
"@tanstack/react-virtual": "^3.14.9",
|
|
63
|
-
"@tiptap/core": "3.
|
|
64
|
-
"@tiptap/extension-collaboration": "3.
|
|
65
|
-
"@tiptap/extension-collaboration-caret": "3.
|
|
66
|
-
"@tiptap/extension-color": "3.
|
|
67
|
-
"@tiptap/extension-drag-handle": "3.
|
|
68
|
-
"@tiptap/extension-drag-handle-react": "3.
|
|
69
|
-
"@tiptap/extension-highlight": "3.
|
|
70
|
-
"@tiptap/extension-horizontal-rule": "3.
|
|
71
|
-
"@tiptap/extension-image": "3.
|
|
72
|
-
"@tiptap/extension-link": "3.
|
|
73
|
-
"@tiptap/extension-list": "3.
|
|
74
|
-
"@tiptap/extension-node-range": "3.
|
|
75
|
-
"@tiptap/extension-placeholder": "3.
|
|
76
|
-
"@tiptap/extension-strike": "3.
|
|
77
|
-
"@tiptap/extension-subscript": "3.
|
|
78
|
-
"@tiptap/extension-superscript": "3.
|
|
79
|
-
"@tiptap/extension-table": "3.
|
|
80
|
-
"@tiptap/extension-table-cell": "3.
|
|
81
|
-
"@tiptap/extension-table-header": "3.
|
|
82
|
-
"@tiptap/extension-table-row": "3.
|
|
83
|
-
"@tiptap/extension-text-align": "3.
|
|
84
|
-
"@tiptap/extension-text-style": "3.
|
|
85
|
-
"@tiptap/extension-youtube": "3.
|
|
86
|
-
"@tiptap/pm": "3.
|
|
87
|
-
"@tiptap/react": "3.
|
|
88
|
-
"@tiptap/starter-kit": "3.
|
|
89
|
-
"@tuturuuu/ai": "0.8.
|
|
90
|
-
"@tuturuuu/apis": "0.11.
|
|
91
|
-
"@tuturuuu/hooks": "0.1.
|
|
63
|
+
"@tiptap/core": "3.30.0",
|
|
64
|
+
"@tiptap/extension-collaboration": "3.30.0",
|
|
65
|
+
"@tiptap/extension-collaboration-caret": "3.30.0",
|
|
66
|
+
"@tiptap/extension-color": "3.30.0",
|
|
67
|
+
"@tiptap/extension-drag-handle": "3.30.0",
|
|
68
|
+
"@tiptap/extension-drag-handle-react": "3.30.0",
|
|
69
|
+
"@tiptap/extension-highlight": "3.30.0",
|
|
70
|
+
"@tiptap/extension-horizontal-rule": "3.30.0",
|
|
71
|
+
"@tiptap/extension-image": "3.30.0",
|
|
72
|
+
"@tiptap/extension-link": "3.30.0",
|
|
73
|
+
"@tiptap/extension-list": "3.30.0",
|
|
74
|
+
"@tiptap/extension-node-range": "3.30.0",
|
|
75
|
+
"@tiptap/extension-placeholder": "3.30.0",
|
|
76
|
+
"@tiptap/extension-strike": "3.30.0",
|
|
77
|
+
"@tiptap/extension-subscript": "3.30.0",
|
|
78
|
+
"@tiptap/extension-superscript": "3.30.0",
|
|
79
|
+
"@tiptap/extension-table": "3.30.0",
|
|
80
|
+
"@tiptap/extension-table-cell": "3.30.0",
|
|
81
|
+
"@tiptap/extension-table-header": "3.30.0",
|
|
82
|
+
"@tiptap/extension-table-row": "3.30.0",
|
|
83
|
+
"@tiptap/extension-text-align": "3.30.0",
|
|
84
|
+
"@tiptap/extension-text-style": "3.30.0",
|
|
85
|
+
"@tiptap/extension-youtube": "3.30.0",
|
|
86
|
+
"@tiptap/pm": "3.30.0",
|
|
87
|
+
"@tiptap/react": "3.30.0",
|
|
88
|
+
"@tiptap/starter-kit": "3.30.0",
|
|
89
|
+
"@tuturuuu/ai": "0.8.2",
|
|
90
|
+
"@tuturuuu/apis": "0.11.2",
|
|
91
|
+
"@tuturuuu/hooks": "0.1.1",
|
|
92
92
|
"@tuturuuu/icons": "0.1.0",
|
|
93
|
-
"@tuturuuu/internal-api": "0.
|
|
93
|
+
"@tuturuuu/internal-api": "0.30.0",
|
|
94
94
|
"@tuturuuu/supabase": "0.5.0",
|
|
95
|
-
"@tuturuuu/utils": "0.
|
|
95
|
+
"@tuturuuu/utils": "0.25.0",
|
|
96
96
|
"@types/debug": "^4.1.13",
|
|
97
97
|
"browser-image-compression": "^2.0.2",
|
|
98
98
|
"class-variance-authority": "^0.7.1",
|
|
@@ -104,17 +104,17 @@
|
|
|
104
104
|
"debug": "^4.4.3",
|
|
105
105
|
"embla-carousel-react": "^8.6.0",
|
|
106
106
|
"eventemitter3": "^5.0.4",
|
|
107
|
-
"framer-motion": "^13.
|
|
107
|
+
"framer-motion": "^13.1.0",
|
|
108
108
|
"gsap": "^3.15.0",
|
|
109
109
|
"html-entities": "^2.6.0",
|
|
110
|
-
"html2canvas-pro": "^2.3.
|
|
110
|
+
"html2canvas-pro": "^2.3.6",
|
|
111
111
|
"input-otp": "^1.4.2",
|
|
112
|
-
"katex": "^0.18.
|
|
112
|
+
"katex": "^0.18.4",
|
|
113
113
|
"lodash": "4.18.1",
|
|
114
114
|
"moment": "^2.30.1",
|
|
115
|
-
"motion": "^13.
|
|
116
|
-
"next": "^16.3.
|
|
117
|
-
"next-intl": "^4.13.
|
|
115
|
+
"motion": "^13.1.0",
|
|
116
|
+
"next": "^16.3.1",
|
|
117
|
+
"next-intl": "^4.13.6",
|
|
118
118
|
"next-themes": "^0.4.6",
|
|
119
119
|
"nuqs": "^2.9.5",
|
|
120
120
|
"prosemirror-state": "^1.4.4",
|
|
@@ -124,8 +124,8 @@
|
|
|
124
124
|
"react-colorful": "^5.8.0",
|
|
125
125
|
"react-day-picker": "^10.0.1",
|
|
126
126
|
"react-dom": "^19.2.8",
|
|
127
|
-
"react-dropzone": "^20.
|
|
128
|
-
"react-hook-form": "^7.
|
|
127
|
+
"react-dropzone": "^20.1.0",
|
|
128
|
+
"react-hook-form": "^7.85.0",
|
|
129
129
|
"react-markdown": "^10.1.0",
|
|
130
130
|
"react-papaparse": "^4.4.0",
|
|
131
131
|
"react-pdf": "^10.4.1",
|
|
@@ -133,13 +133,13 @@
|
|
|
133
133
|
"react-resizable-panels": "^4.12.2",
|
|
134
134
|
"react-syntax-highlighter": "^16.1.1",
|
|
135
135
|
"recharts": "3.10.1",
|
|
136
|
-
"slot-text": "^0.3.
|
|
137
|
-
"sonner": "^2.0.
|
|
136
|
+
"slot-text": "^0.3.4",
|
|
137
|
+
"sonner": "^2.0.8",
|
|
138
138
|
"streamdown": "^2.5.0",
|
|
139
139
|
"tailwind-scrollbar": "^4.0.2",
|
|
140
140
|
"tailwind-scrollbar-hide": "^4.0.0",
|
|
141
141
|
"tailwindcss-animate": "^1.0.7",
|
|
142
|
-
"tiptap-extension-resize-image": "^1.4.
|
|
142
|
+
"tiptap-extension-resize-image": "^1.4.6",
|
|
143
143
|
"use-debounce": "^10.1.1",
|
|
144
144
|
"vaul": "^1.1.2",
|
|
145
145
|
"y-protocols": "^1.0.7",
|
|
@@ -150,14 +150,14 @@
|
|
|
150
150
|
"@tailwindcss/postcss": "^4.3.3",
|
|
151
151
|
"@tailwindcss/typography": "^0.5.20",
|
|
152
152
|
"@tanstack/react-query": "^5.101.4",
|
|
153
|
-
"@tanstack/react-table": "^9.
|
|
154
|
-
"@testing-library/jest-dom": "^7.0.
|
|
153
|
+
"@tanstack/react-table": "^9.1.2",
|
|
154
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
155
155
|
"@testing-library/react": "^16.3.2",
|
|
156
156
|
"@tuturuuu/types": "0.27.0",
|
|
157
157
|
"@tuturuuu/typescript-config": "0.1.1",
|
|
158
158
|
"@types/html2canvas": "^1.0.0",
|
|
159
159
|
"@types/lodash": "^4.17.25",
|
|
160
|
-
"@types/node": "^26.
|
|
160
|
+
"@types/node": "^26.2.0",
|
|
161
161
|
"@types/react": "^19.2.18",
|
|
162
162
|
"@types/react-dom": "^19.2.4",
|
|
163
163
|
"@types/react-resizable": "^4.0.0",
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
"@vitejs/plugin-react": "^6.0.5",
|
|
167
167
|
"autoprefixer": "^10.5.4",
|
|
168
168
|
"jsdom": "^30.0.1",
|
|
169
|
-
"postcss": "^8.5.
|
|
169
|
+
"postcss": "^8.5.26",
|
|
170
170
|
"postcss-load-config": "^6.0.1",
|
|
171
171
|
"tailwindcss": "^4.3.3",
|
|
172
172
|
"typescript": "7.0.2",
|
|
@@ -396,12 +396,6 @@
|
|
|
396
396
|
},
|
|
397
397
|
"./calendar-app/hooks/*": "./src/components/ui/calendar-app/hooks/*.ts",
|
|
398
398
|
"./calendar-app/components/*": "./src/components/ui/calendar-app/components/*.tsx",
|
|
399
|
-
"./calendar-app/components/sidebar/*": "./src/components/ui/calendar-app/components/sidebar/*.tsx",
|
|
400
|
-
"./calendar-app/components/time-tracker": {
|
|
401
|
-
"types": "./src/components/ui/calendar-app/components/time-tracker/index.tsx",
|
|
402
|
-
"import": "./src/components/ui/calendar-app/components/time-tracker/index.tsx"
|
|
403
|
-
},
|
|
404
|
-
"./calendar-app/components/time-tracker/*": "./src/components/ui/calendar-app/components/time-tracker/*.tsx",
|
|
405
399
|
"./custom/icon-picker": {
|
|
406
400
|
"types": "./src/components/ui/custom/icon-picker/index.ts",
|
|
407
401
|
"import": "./src/components/ui/custom/icon-picker/index.ts"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveFirstDayOfWeek as resolveCanonicalFirstDayOfWeek,
|
|
3
|
+
resolveTimezone as resolveCanonicalTimezone,
|
|
4
|
+
} from '@tuturuuu/utils/calendar-settings-resolver';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
resolveFirstDayOfWeek,
|
|
8
|
+
resolveTimezone,
|
|
9
|
+
} from '../../../../lib/calendar-settings-resolver';
|
|
10
|
+
|
|
11
|
+
describe('shared calendar settings resolver integration', () => {
|
|
12
|
+
it('keeps the UI compatibility path on the canonical resolver', () => {
|
|
13
|
+
expect(resolveTimezone).toBe(resolveCanonicalTimezone);
|
|
14
|
+
expect(resolveFirstDayOfWeek).toBe(resolveCanonicalFirstDayOfWeek);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('prefers saved user values and preserves locale fallback', () => {
|
|
18
|
+
expect(
|
|
19
|
+
resolveTimezone(
|
|
20
|
+
{ timezone: 'Asia/Ho_Chi_Minh' },
|
|
21
|
+
{ timezone: 'Europe/London' }
|
|
22
|
+
)
|
|
23
|
+
).toBe('Asia/Ho_Chi_Minh');
|
|
24
|
+
expect(
|
|
25
|
+
resolveFirstDayOfWeek(
|
|
26
|
+
{ first_day_of_week: 'auto' },
|
|
27
|
+
{ first_day_of_week: 'auto' },
|
|
28
|
+
'vi-VN'
|
|
29
|
+
)
|
|
30
|
+
).toBe('monday');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -101,6 +101,8 @@ interface ComboboxProps {
|
|
|
101
101
|
onCreate?: (value: string) => unknown | Promise<unknown>;
|
|
102
102
|
/** Callback when search input changes */
|
|
103
103
|
onSearchChange?: (value: string) => void;
|
|
104
|
+
/** Whether Command should filter the provided options locally */
|
|
105
|
+
shouldFilter?: boolean;
|
|
104
106
|
/** Callback when the popover opens or closes */
|
|
105
107
|
onOpenChange?: (open: boolean) => void;
|
|
106
108
|
/** Whether there are more options to load */
|
|
@@ -148,6 +150,7 @@ export function Combobox({
|
|
|
148
150
|
useFirstValueAsDefault = false,
|
|
149
151
|
onCreate,
|
|
150
152
|
onSearchChange,
|
|
153
|
+
shouldFilter = true,
|
|
151
154
|
onOpenChange,
|
|
152
155
|
hasMore = false,
|
|
153
156
|
onLoadMore,
|
|
@@ -543,6 +546,7 @@ export function Combobox({
|
|
|
543
546
|
sideOffset={4}
|
|
544
547
|
>
|
|
545
548
|
<Command
|
|
549
|
+
shouldFilter={shouldFilter}
|
|
546
550
|
filter={(value, search) => {
|
|
547
551
|
if (value.startsWith(actionValuePrefix)) return 1;
|
|
548
552
|
if (value.startsWith(createValuePrefix)) return 1;
|
|
@@ -7,10 +7,12 @@ import { NavLink } from './nav-link';
|
|
|
7
7
|
|
|
8
8
|
const navigationState = vi.hoisted(() => ({
|
|
9
9
|
pathname: '/personal/tasks',
|
|
10
|
+
prefetch: vi.fn(),
|
|
10
11
|
}));
|
|
11
12
|
|
|
12
13
|
vi.mock('next/navigation', () => ({
|
|
13
14
|
usePathname: () => navigationState.pathname,
|
|
15
|
+
useRouter: () => ({ prefetch: navigationState.prefetch }),
|
|
14
16
|
}));
|
|
15
17
|
|
|
16
18
|
vi.mock('next-intl', () => ({
|
|
@@ -51,9 +53,54 @@ function renderNavLink(
|
|
|
51
53
|
describe('NavLink', () => {
|
|
52
54
|
beforeEach(() => {
|
|
53
55
|
navigationState.pathname = '/personal/tasks';
|
|
56
|
+
navigationState.prefetch.mockReset();
|
|
54
57
|
vi.restoreAllMocks();
|
|
55
58
|
});
|
|
56
59
|
|
|
60
|
+
it('prefetches internal routes only after navigation intent', () => {
|
|
61
|
+
renderNavLink({
|
|
62
|
+
href: '/personal/tasks/boards',
|
|
63
|
+
title: 'Boards',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const link = screen.getByRole('link', { name: 'Boards' });
|
|
67
|
+
expect(navigationState.prefetch).not.toHaveBeenCalled();
|
|
68
|
+
|
|
69
|
+
fireEvent.mouseEnter(link);
|
|
70
|
+
fireEvent.focus(link);
|
|
71
|
+
|
|
72
|
+
expect(navigationState.prefetch).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(navigationState.prefetch).toHaveBeenCalledWith(
|
|
74
|
+
'/personal/tasks/boards'
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not prefetch external routes', () => {
|
|
79
|
+
renderNavLink({
|
|
80
|
+
external: true,
|
|
81
|
+
href: 'https://docs.tuturuuu.com',
|
|
82
|
+
title: 'Docs',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
fireEvent.mouseEnter(screen.getByRole('link', { name: 'Docs' }));
|
|
86
|
+
|
|
87
|
+
expect(navigationState.prefetch).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it.each([
|
|
91
|
+
['protocol-relative', '//docs.tuturuuu.com/guide'],
|
|
92
|
+
['email', 'mailto:support@tuturuuu.com'],
|
|
93
|
+
['telephone', 'tel:+84123456789'],
|
|
94
|
+
])('does not prefetch %s URLs', (_label, href) => {
|
|
95
|
+
renderNavLink({ href, title: 'External destination' });
|
|
96
|
+
|
|
97
|
+
fireEvent.mouseEnter(
|
|
98
|
+
screen.getByRole('link', { name: 'External destination' })
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(navigationState.prefetch).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
57
104
|
it('matches wildcard aliases even when the primary route is exact', () => {
|
|
58
105
|
navigationState.pathname = '/personal/tasks/boards/board-1';
|
|
59
106
|
|
|
@@ -12,9 +12,9 @@ import {
|
|
|
12
12
|
import { getWorkspaceConfigIdList } from '@tuturuuu/internal-api';
|
|
13
13
|
import { cn } from '@tuturuuu/utils/format';
|
|
14
14
|
import Link from 'next/link';
|
|
15
|
-
import { usePathname } from 'next/navigation';
|
|
15
|
+
import { usePathname, useRouter } from 'next/navigation';
|
|
16
16
|
import { useTranslations } from 'next-intl';
|
|
17
|
-
import { useState } from 'react';
|
|
17
|
+
import { useRef, useState } from 'react';
|
|
18
18
|
import { Badge } from '../badge';
|
|
19
19
|
import { Button } from '../button';
|
|
20
20
|
import {
|
|
@@ -58,7 +58,7 @@ function getComparablePath(
|
|
|
58
58
|
? new URL(target)
|
|
59
59
|
: new URL(target, base);
|
|
60
60
|
|
|
61
|
-
if (
|
|
61
|
+
if (url.origin !== base) return null;
|
|
62
62
|
|
|
63
63
|
return url.pathname;
|
|
64
64
|
} catch {
|
|
@@ -103,6 +103,7 @@ export function NavLink({
|
|
|
103
103
|
}: NavLinkProps) {
|
|
104
104
|
const t = useTranslations();
|
|
105
105
|
const pathname = usePathname();
|
|
106
|
+
const router = useRouter();
|
|
106
107
|
const {
|
|
107
108
|
title,
|
|
108
109
|
icon,
|
|
@@ -338,6 +339,27 @@ export function NavLink({
|
|
|
338
339
|
})()
|
|
339
340
|
: href;
|
|
340
341
|
|
|
342
|
+
const prefetchedHrefRef = useRef<string | null>(null);
|
|
343
|
+
const prefetchOnIntent = () => {
|
|
344
|
+
if (
|
|
345
|
+
!effectiveHref ||
|
|
346
|
+
newTab ||
|
|
347
|
+
link.external ||
|
|
348
|
+
isDisabled ||
|
|
349
|
+
isResolvingHref ||
|
|
350
|
+
hasSubMenu ||
|
|
351
|
+
archivedItems.length > 0 ||
|
|
352
|
+
!getComparablePath(effectiveHref)
|
|
353
|
+
) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (prefetchedHrefRef.current === effectiveHref) return;
|
|
358
|
+
|
|
359
|
+
prefetchedHrefRef.current = effectiveHref;
|
|
360
|
+
router.prefetch(effectiveHref);
|
|
361
|
+
};
|
|
362
|
+
|
|
341
363
|
const commonProps = {
|
|
342
364
|
className: cn(
|
|
343
365
|
'group/navlink flex w-full cursor-pointer items-center justify-between rounded-md p-2 font-medium text-sm',
|
|
@@ -388,6 +410,8 @@ export function NavLink({
|
|
|
388
410
|
onClick();
|
|
389
411
|
}
|
|
390
412
|
},
|
|
413
|
+
onFocus: prefetchOnIntent,
|
|
414
|
+
onMouseEnter: prefetchOnIntent,
|
|
391
415
|
};
|
|
392
416
|
|
|
393
417
|
const linkElement =
|
|
@@ -425,6 +449,7 @@ export function NavLink({
|
|
|
425
449
|
) : effectiveHref && !isDisabled ? (
|
|
426
450
|
<Link
|
|
427
451
|
href={effectiveHref}
|
|
452
|
+
prefetch={false}
|
|
428
453
|
{...commonProps}
|
|
429
454
|
target={newTab ? '_blank' : '_self'}
|
|
430
455
|
>
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
createStandardWorkspaceAccessAdapter,
|
|
4
|
+
mergeWorkspaceInvitationRoles,
|
|
4
5
|
normalizeWorkspaceAccessRole,
|
|
5
6
|
} from './adapters';
|
|
6
7
|
|
|
8
|
+
const mocks = vi.hoisted(() => ({
|
|
9
|
+
inviteWorkspaceMember: vi.fn().mockResolvedValue({ message: 'success' }),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('@tuturuuu/internal-api/workspaces', async (importOriginal) => ({
|
|
13
|
+
...(await importOriginal<
|
|
14
|
+
typeof import('@tuturuuu/internal-api/workspaces')
|
|
15
|
+
>()),
|
|
16
|
+
inviteWorkspaceMember: mocks.inviteWorkspaceMember,
|
|
17
|
+
}));
|
|
18
|
+
|
|
7
19
|
describe('workspace access adapters', () => {
|
|
8
20
|
it('normalizes role permissions into the shared access role shape', () => {
|
|
9
21
|
expect(
|
|
@@ -37,4 +49,80 @@ describe('workspace access adapters', () => {
|
|
|
37
49
|
createStandardWorkspaceAccessAdapter().updateMemberProfile
|
|
38
50
|
).toBeTypeOf('function');
|
|
39
51
|
});
|
|
52
|
+
|
|
53
|
+
it('hydrates pending email and registered-user invitations with their assigned roles', () => {
|
|
54
|
+
const members = mergeWorkspaceInvitationRoles(
|
|
55
|
+
[
|
|
56
|
+
{
|
|
57
|
+
default_permissions: [],
|
|
58
|
+
email: 'pending@example.com',
|
|
59
|
+
id: null,
|
|
60
|
+
is_creator: false,
|
|
61
|
+
pending: true,
|
|
62
|
+
roles: [],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
default_permissions: [],
|
|
66
|
+
email: null,
|
|
67
|
+
id: 'user-2',
|
|
68
|
+
is_creator: false,
|
|
69
|
+
pending: true,
|
|
70
|
+
roles: [],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
default_permissions: [],
|
|
74
|
+
email: 'joined@example.com',
|
|
75
|
+
id: 'user-3',
|
|
76
|
+
is_creator: false,
|
|
77
|
+
pending: false,
|
|
78
|
+
roles: [{ id: 'role-owner', name: 'Owner', permissions: [] }],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
{
|
|
83
|
+
email: 'Pending@Example.com',
|
|
84
|
+
roles: [
|
|
85
|
+
{ id: 'role-editor', name: 'Editor' },
|
|
86
|
+
{ id: 'role-reviewer', name: 'Reviewer' },
|
|
87
|
+
],
|
|
88
|
+
userId: null,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
email: null,
|
|
92
|
+
roles: [{ id: 'role-reviewer', name: 'Reviewer' }],
|
|
93
|
+
userId: 'user-2',
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(members[0]?.roles).toEqual([
|
|
99
|
+
{ id: 'role-editor', name: 'Editor', permissions: [] },
|
|
100
|
+
{ id: 'role-reviewer', name: 'Reviewer', permissions: [] },
|
|
101
|
+
]);
|
|
102
|
+
expect(members[1]?.roles).toEqual([
|
|
103
|
+
{ id: 'role-reviewer', name: 'Reviewer', permissions: [] },
|
|
104
|
+
]);
|
|
105
|
+
expect(members[2]?.roles).toEqual([
|
|
106
|
+
{ id: 'role-owner', name: 'Owner', permissions: [] },
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('forwards multiple role assignments with every standard invite', async () => {
|
|
111
|
+
const adapter = createStandardWorkspaceAccessAdapter();
|
|
112
|
+
|
|
113
|
+
await adapter.inviteMembers('ws-1', {
|
|
114
|
+
accessPreset: 'member',
|
|
115
|
+
emails: ['editor@example.com'],
|
|
116
|
+
memberType: 'MEMBER',
|
|
117
|
+
roleIds: ['role-editor', 'role-reviewer'],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(mocks.inviteWorkspaceMember).toHaveBeenCalledWith('ws-1', {
|
|
121
|
+
accessPreset: 'member',
|
|
122
|
+
confirmDefaultAdminMigration: undefined,
|
|
123
|
+
email: 'editor@example.com',
|
|
124
|
+
memberType: 'MEMBER',
|
|
125
|
+
roleIds: ['role-editor', 'role-reviewer'],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
40
128
|
});
|