@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +29 -7
  3. package/biome.json +1 -1
  4. package/package.json +48 -54
  5. package/src/components/ui/calendar-app/hooks/use-calendar-settings.test.ts +32 -0
  6. package/src/components/ui/custom/combobox.tsx +4 -0
  7. package/src/components/ui/custom/nav-link.test.tsx +47 -0
  8. package/src/components/ui/custom/nav-link.tsx +28 -3
  9. package/src/components/ui/custom/workspace-access/adapters.test.ts +89 -1
  10. package/src/components/ui/custom/workspace-access/adapters.ts +67 -2
  11. package/src/components/ui/custom/workspace-access/types.ts +13 -0
  12. package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.test.tsx +79 -0
  13. package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.tsx +125 -0
  14. package/src/components/ui/custom/workspace-access/workspace-access-invite-access-picker.tsx +108 -0
  15. package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.test.tsx +134 -0
  16. package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +94 -113
  17. package/src/components/ui/custom/workspace-access/workspace-access-invite-pos-panel.tsx +75 -0
  18. package/src/components/ui/custom/workspace-access/workspace-access-invite-role-picker.tsx +151 -0
  19. package/src/components/ui/custom/workspace-access/workspace-access-labels.ts +1 -1
  20. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +27 -2
  21. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +10 -0
  22. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +102 -3
  23. package/src/components/ui/custom/workspace-access/workspace-access-role-options.test.ts +35 -0
  24. package/src/components/ui/custom/workspace-access/workspace-access-role-options.ts +21 -0
  25. package/src/components/ui/custom/workspace-select-invitations.tsx +2 -1
  26. package/src/components/ui/legacy/polls/poll-display.test.tsx +118 -0
  27. package/src/components/ui/legacy/polls/poll-display.tsx +8 -0
  28. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -1
  29. package/src/hooks/use-board-actions.test.ts +23 -0
  30. package/src/hooks/use-board-actions.ts +48 -35
  31. package/src/hooks/use-calendar-sync.tsx +12 -12
  32. package/src/hooks/use-notifications.ts +4 -8
  33. package/src/lib/calendar-settings-resolver.ts +1 -200
  34. 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 { Button, Card } from '@tuturuuu/ui'
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
- <Button variant="primary">Click me</Button>
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
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "root": false,
3
- "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json",
3
+ "$schema": "https://biomejs.dev/schemas/2.5.8/schema.json",
4
4
  "extends": "//"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuturuuu/ui",
3
- "version": "0.26.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.22.1",
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.29.2",
64
- "@tiptap/extension-collaboration": "3.29.2",
65
- "@tiptap/extension-collaboration-caret": "3.29.2",
66
- "@tiptap/extension-color": "3.29.2",
67
- "@tiptap/extension-drag-handle": "3.29.2",
68
- "@tiptap/extension-drag-handle-react": "3.29.2",
69
- "@tiptap/extension-highlight": "3.29.2",
70
- "@tiptap/extension-horizontal-rule": "3.29.2",
71
- "@tiptap/extension-image": "3.29.2",
72
- "@tiptap/extension-link": "3.29.2",
73
- "@tiptap/extension-list": "3.29.2",
74
- "@tiptap/extension-node-range": "3.29.2",
75
- "@tiptap/extension-placeholder": "3.29.2",
76
- "@tiptap/extension-strike": "3.29.2",
77
- "@tiptap/extension-subscript": "3.29.2",
78
- "@tiptap/extension-superscript": "3.29.2",
79
- "@tiptap/extension-table": "3.29.2",
80
- "@tiptap/extension-table-cell": "3.29.2",
81
- "@tiptap/extension-table-header": "3.29.2",
82
- "@tiptap/extension-table-row": "3.29.2",
83
- "@tiptap/extension-text-align": "3.29.2",
84
- "@tiptap/extension-text-style": "3.29.2",
85
- "@tiptap/extension-youtube": "3.29.2",
86
- "@tiptap/pm": "3.29.2",
87
- "@tiptap/react": "3.29.2",
88
- "@tiptap/starter-kit": "3.29.2",
89
- "@tuturuuu/ai": "0.8.1",
90
- "@tuturuuu/apis": "0.11.0",
91
- "@tuturuuu/hooks": "0.1.0",
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.29.0",
93
+ "@tuturuuu/internal-api": "0.30.0",
94
94
  "@tuturuuu/supabase": "0.5.0",
95
- "@tuturuuu/utils": "0.24.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.0.0",
107
+ "framer-motion": "^13.1.0",
108
108
  "gsap": "^3.15.0",
109
109
  "html-entities": "^2.6.0",
110
- "html2canvas-pro": "^2.3.3",
110
+ "html2canvas-pro": "^2.3.6",
111
111
  "input-otp": "^1.4.2",
112
- "katex": "^0.18.1",
112
+ "katex": "^0.18.4",
113
113
  "lodash": "4.18.1",
114
114
  "moment": "^2.30.1",
115
- "motion": "^13.0.0",
116
- "next": "^16.3.0",
117
- "next-intl": "^4.13.5",
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.0.0",
128
- "react-hook-form": "^7.84.0",
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.3",
137
- "sonner": "^2.0.7",
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.5",
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.0.0",
154
- "@testing-library/jest-dom": "^7.0.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.1.2",
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.25",
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 (isAbsoluteHttpUrl(target) && url.origin !== base) return null;
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
  });