@tuturuuu/ui 0.37.1 → 0.38.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 +40 -0
- package/package.json +10 -7
- package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +267 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +24 -38
- package/src/components/ui/custom/education-todo.test.tsx +131 -0
- package/src/components/ui/custom/education-todo.tsx +191 -0
- package/src/components/ui/custom/hidden-workspaces-settings.tsx +49 -0
- package/src/components/ui/custom/mobile-header-height.ts +16 -0
- package/src/components/ui/custom/satellite-notification-polling.test.tsx +61 -0
- package/src/components/ui/custom/satellite-notification-popover.tsx +7 -3
- package/src/components/ui/custom/structure.tsx +37 -5
- package/src/components/ui/custom/user-filters.tsx +2 -1
- package/src/components/ui/custom/visible-workspace-filter.tsx +41 -0
- package/src/components/ui/custom/workspace-picker-content.tsx +356 -0
- package/src/components/ui/custom/workspace-select-helpers.ts +11 -0
- package/src/components/ui/custom/workspace-select-invitations.tsx +3 -0
- package/src/components/ui/custom/workspace-select.tsx +95 -359
- package/src/components/ui/legacy/calendar/calendar-draft-recovery.ts +34 -0
- package/src/components/ui/legacy/calendar/event-modal.tsx +77 -137
- package/src/components/ui/legacy/calendar/event-save-payload.test.ts +40 -0
- package/src/components/ui/legacy/calendar/event-save-payload.ts +79 -0
- package/src/components/ui/legacy/calendar/event-source-selection.test.ts +95 -0
- package/src/components/ui/legacy/calendar/event-source-selection.ts +78 -0
- package/src/components/ui/legacy/calendar/provider-color-picker.test.tsx +147 -0
- package/src/components/ui/legacy/calendar/provider-color-picker.tsx +115 -0
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +35 -1
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +23 -11
- package/src/components/ui/legacy/calendar/settings/calendar-sync-dashboard.tsx +4 -23
- package/src/components/ui/legacy/calendar/settings/sync-logs-table.tsx +3 -6
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +119 -0
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +141 -0
- package/src/components/ui/popover.tsx +3 -1
- package/src/components/ui/resizable.tsx +2 -1
- package/src/components/ui/text-editor/__tests__/collaboration-carets.test.ts +142 -0
- package/src/components/ui/text-editor/__tests__/markdown-document.test.ts +50 -0
- package/src/components/ui/text-editor/clipboard-serialization.ts +10 -0
- package/src/components/ui/text-editor/collaboration-carets.ts +56 -0
- package/src/components/ui/text-editor/editor-readonly-transition.test.tsx +2 -0
- package/src/components/ui/text-editor/editor.tsx +8 -3
- package/src/components/ui/text-editor/extensions.ts +8 -2
- package/src/components/ui/text-editor/markdown-paste-extension.ts +1 -1
- package/src/components/ui/text-editor/markdown.ts +11 -0
- package/src/components/ui/text-editor/tool-bar.tsx +43 -44
- package/src/hooks/__tests__/collaboration-awareness.test.ts +106 -0
- package/src/hooks/__tests__/supabase-provider.test.ts +9 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +12 -2
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +31 -1
- package/src/hooks/__tests__/use-visible-workspaces.test.tsx +76 -0
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +3 -3
- package/src/hooks/__tests__/use-workspace-visibility.test.tsx +155 -0
- package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +108 -0
- package/src/hooks/__tests__/workspace-actor-lifetime.test.tsx +103 -0
- package/src/hooks/calendar-creation-request.test.ts +62 -0
- package/src/hooks/calendar-creation-request.ts +34 -0
- package/src/hooks/calendar-event-write-payload.ts +24 -0
- package/src/hooks/notification-subscription.ts +5 -2
- package/src/hooks/supabase-provider.ts +38 -72
- package/src/hooks/use-calendar.tsx +21 -45
- package/src/hooks/use-exchange-rates.ts +4 -15
- package/src/hooks/use-visible-workspaces.ts +57 -0
- package/src/hooks/use-workspace-presence.ts +9 -9
- package/src/hooks/use-workspace-user.ts +15 -3
- package/src/hooks/use-workspace-visibility.ts +234 -0
- package/src/hooks/use-yjs-collaboration.ts +45 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.38.0](https://github.com/tutur3u/platform/compare/ui-v0.37.2...ui-v0.38.0) (2026-10-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **calendar:** support full Google color choices and scoped edits ([#5661](https://github.com/tutur3u/platform/issues/5661)) ([8cafac5](https://github.com/tutur3u/platform/commit/8cafac53438b504eafa8bcc8213d18448e4a17bc))
|
|
9
|
+
* **education:** add personal to do destinations to Teach and Learn ([9587942](https://github.com/tutur3u/platform/commit/9587942deeebcb6aca3a774f4289178771bcabd6))
|
|
10
|
+
* **education:** add personal To do in Teach and Learn ([#5785](https://github.com/tutur3u/platform/issues/5785)) ([0a5e302](https://github.com/tutur3u/platform/commit/0a5e302fa9b09c140c717751005bbda23a1dc0b1))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **calendar:** gate uncertain creation retries on durable capability ([4a0a088](https://github.com/tutur3u/platform/commit/4a0a0883ddfff464be0880afd13758908e65203f))
|
|
16
|
+
* **calendar:** recover edited drafts and align provider color parity ([d97142a](https://github.com/tutur3u/platform/commit/d97142abe22188b6a31a4a6b0e7b3a97d35d07f5))
|
|
17
|
+
* **finance:** authenticate exchange rates with satellite sessions ([33368f1](https://github.com/tutur3u/platform/commit/33368f1c00f86f71b4a66702899af262a90122e2)) ([#5760](https://github.com/tutur3u/platform/issues/5760)) ([759b1e5](https://github.com/tutur3u/platform/commit/759b1e52f8867aced2d64288af43b861d5ebccdc))
|
|
18
|
+
* **identity:** integrate main and order unapplied creator migrations ([5512e24](https://github.com/tutur3u/platform/commit/5512e2414abaa0c78b68cbdd3186a1d4bab861e7))
|
|
19
|
+
* **notifications:** preserve Colab polling policy ([ed09b9d](https://github.com/tutur3u/platform/commit/ed09b9ddaabd9b1865ca11e0459f23b0141137df))
|
|
20
|
+
* **ui:** close workspace popover without callback ([b0693b8](https://github.com/tutur3u/platform/commit/b0693b869721b9333d81d791554864291bec769d))
|
|
21
|
+
* **ui:** default browser workspace selectors to dropdowns ([9a8da02](https://github.com/tutur3u/platform/commit/9a8da02ceb7c03ee7eb05fb6cdc729144da6192a))
|
|
22
|
+
* **ui:** refine workspace dropdown recovery and viewport bounds ([06eeee7](https://github.com/tutur3u/platform/commit/06eeee77a26c1fc632907b72a5e0218d5d3fb49d))
|
|
23
|
+
* **web:** restore anchored workspace dropdown ([2df0661](https://github.com/tutur3u/platform/commit/2df0661f911eaec319cacca5d29242804d0e76ee))
|
|
24
|
+
* **web:** restore standard workspace dropdown ([#5739](https://github.com/tutur3u/platform/issues/5739)) ([f84ae74](https://github.com/tutur3u/platform/commit/f84ae741f2abab2a21aadfacda56bd7eba623466))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Performance Improvements
|
|
28
|
+
|
|
29
|
+
* **notifications:** reduce idle API polling ([32ee541](https://github.com/tutur3u/platform/commit/32ee5410e51d900698a06724b440b63df363fee9)) ([#5773](https://github.com/tutur3u/platform/issues/5773)) ([55089e0](https://github.com/tutur3u/platform/commit/55089e06ed6728f4bd6543787dcae48de3cfd734))
|
|
30
|
+
|
|
31
|
+
## [0.37.2](https://github.com/tutur3u/platform/compare/ui-v0.37.1...ui-v0.37.2) (2026-10-02)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Bug Fixes
|
|
35
|
+
|
|
36
|
+
* **calendar:** bind source color hydration to persisted identity ([00269c3](https://github.com/tutur3u/platform/commit/00269c315cd2ac8e1c9607f450100db8537cada2))
|
|
37
|
+
* **calendar:** preserve month hover color contrast ([6fd591c](https://github.com/tutur3u/platform/commit/6fd591c46f4c90d6f4c685c63b140728dd0339fe))
|
|
38
|
+
* **calendar:** preserve opaque rendering cues and hydrate source colors ([33847dd](https://github.com/tutur3u/platform/commit/33847ddcd8a3cf2b5cef8ebf50ca4c1700424a27))
|
|
39
|
+
* **calendar:** preserve resolved contrast during event hover ([79001b3](https://github.com/tutur3u/platform/commit/79001b32ad1c813119f8f7df934e7142bbbd7585))
|
|
40
|
+
* **calendar:** render opaque readable event colors ([#5694](https://github.com/tutur3u/platform/issues/5694)) ([6e3d81d](https://github.com/tutur3u/platform/commit/6e3d81d6fdd6f8155f12e36ede58d31fde0ac362))
|
|
41
|
+
* **calendar:** restore timer restart and metadata color refresh ([58c17ff](https://github.com/tutur3u/platform/commit/58c17ff6a89624678cc207cd9551bc54f15f53d2)) ([#5722](https://github.com/tutur3u/platform/issues/5722)) ([8954e54](https://github.com/tutur3u/platform/commit/8954e5476fd04ada4cb846e909fa26b8a669cda3))
|
|
42
|
+
|
|
3
43
|
## [0.37.1](https://github.com/tutur3u/platform/compare/ui-v0.37.0...ui-v0.37.1) (2026-10-01)
|
|
4
44
|
|
|
5
45
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -87,13 +87,14 @@
|
|
|
87
87
|
"@tiptap/pm": "3.31.3",
|
|
88
88
|
"@tiptap/react": "3.31.3",
|
|
89
89
|
"@tiptap/starter-kit": "3.31.3",
|
|
90
|
-
"@tuturuuu/ai": "0.
|
|
90
|
+
"@tuturuuu/ai": "0.17.0",
|
|
91
91
|
"@tuturuuu/apis": "0.13.0",
|
|
92
92
|
"@tuturuuu/hooks": "0.1.2",
|
|
93
93
|
"@tuturuuu/icons": "0.1.1",
|
|
94
|
-
"@tuturuuu/internal-api": "0.
|
|
94
|
+
"@tuturuuu/internal-api": "0.49.0",
|
|
95
|
+
"@tuturuuu/realtime": "0.10.0",
|
|
95
96
|
"@tuturuuu/supabase": "0.5.2",
|
|
96
|
-
"@tuturuuu/utils": "0.
|
|
97
|
+
"@tuturuuu/utils": "0.34.0",
|
|
97
98
|
"@types/debug": "^4.1.13",
|
|
98
99
|
"browser-image-compression": "^2.0.2",
|
|
99
100
|
"class-variance-authority": "^0.7.1",
|
|
@@ -144,7 +145,7 @@
|
|
|
144
145
|
"use-debounce": "^10.1.1",
|
|
145
146
|
"vaul": "^1.1.2",
|
|
146
147
|
"y-protocols": "^1.0.7",
|
|
147
|
-
"yjs": "^13.6.
|
|
148
|
+
"yjs": "^13.6.33",
|
|
148
149
|
"zod": "^4.6.2"
|
|
149
150
|
},
|
|
150
151
|
"devDependencies": {
|
|
@@ -154,7 +155,7 @@
|
|
|
154
155
|
"@tanstack/react-table": "^9.2.4",
|
|
155
156
|
"@testing-library/jest-dom": "^7.0.1",
|
|
156
157
|
"@testing-library/react": "^16.3.3",
|
|
157
|
-
"@tuturuuu/types": "0.
|
|
158
|
+
"@tuturuuu/types": "0.34.0",
|
|
158
159
|
"@tuturuuu/typescript-config": "0.1.2",
|
|
159
160
|
"@types/html2canvas": "^1.0.0",
|
|
160
161
|
"@types/lodash": "^4.17.25",
|
|
@@ -179,6 +180,7 @@
|
|
|
179
180
|
"@tanstack/react-table": "^9.0.0"
|
|
180
181
|
},
|
|
181
182
|
"exports": {
|
|
183
|
+
"./custom/education-todo": "./src/components/ui/custom/education-todo.tsx",
|
|
182
184
|
"./globals.css": "./src/globals.css",
|
|
183
185
|
"./marketing": "./src/components/ui/marketing/index.ts",
|
|
184
186
|
"./postcss.config": "./postcss.config.mjs",
|
|
@@ -497,7 +499,8 @@
|
|
|
497
499
|
"./custom/satellite-notification-popover": "./src/components/ui/custom/satellite-notification-popover.tsx",
|
|
498
500
|
"./custom/notification-runtime": "./src/components/ui/custom/notification-runtime.tsx",
|
|
499
501
|
"./finance/invoices/query-invalidation": "./src/components/ui/finance/invoices/query-invalidation.ts",
|
|
500
|
-
"./finance/shared/charts/use-finance-confidential-visibility": "./src/components/ui/finance/shared/charts/use-finance-confidential-visibility.ts"
|
|
502
|
+
"./finance/shared/charts/use-finance-confidential-visibility": "./src/components/ui/finance/shared/charts/use-finance-confidential-visibility.ts",
|
|
503
|
+
"./text-editor/collaboration-carets": "./src/components/ui/text-editor/collaboration-carets.ts"
|
|
501
504
|
},
|
|
502
505
|
"packageManager": "bun@1.4.1"
|
|
503
506
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import type { InternalApiWorkspaceSummary } from '@tuturuuu/types';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import type { useWorkspaceVisibility } from '../../../../hooks/use-workspace-visibility';
|
|
5
|
+
import { Dialog } from '../../dialog';
|
|
6
|
+
import { Popover, PopoverTrigger } from '../../popover';
|
|
7
|
+
import { WorkspacePickerContent } from '../workspace-picker-content';
|
|
8
|
+
|
|
9
|
+
vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key }));
|
|
10
|
+
vi.mock('../workspace-select-icon', () => ({
|
|
11
|
+
WorkspaceIcon: () => <span aria-hidden="true" />,
|
|
12
|
+
}));
|
|
13
|
+
const one: InternalApiWorkspaceSummary = {
|
|
14
|
+
id: 'a',
|
|
15
|
+
name: 'Repeated name',
|
|
16
|
+
personal: false,
|
|
17
|
+
avatar_url: null,
|
|
18
|
+
logo_url: null,
|
|
19
|
+
tier: 'PRO',
|
|
20
|
+
};
|
|
21
|
+
const two: InternalApiWorkspaceSummary = { ...one, id: 'b' };
|
|
22
|
+
const other: InternalApiWorkspaceSummary = {
|
|
23
|
+
...one,
|
|
24
|
+
id: 'z',
|
|
25
|
+
name: 'Another workspace',
|
|
26
|
+
};
|
|
27
|
+
function visibility(overrides = {}) {
|
|
28
|
+
return {
|
|
29
|
+
known: true,
|
|
30
|
+
hiddenIds: [],
|
|
31
|
+
pending: new Set(),
|
|
32
|
+
isError: false,
|
|
33
|
+
updateError: null,
|
|
34
|
+
setHidden: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
refetch: vi.fn(),
|
|
36
|
+
...overrides,
|
|
37
|
+
} as unknown as ReturnType<typeof useWorkspaceVisibility>;
|
|
38
|
+
}
|
|
39
|
+
describe('workspace sheet rendered identities', () => {
|
|
40
|
+
it('keeps floating create and search actions separate from workspace selection', () => {
|
|
41
|
+
const onCreate = vi.fn();
|
|
42
|
+
const onJoin = vi.fn();
|
|
43
|
+
const onSelect = vi.fn();
|
|
44
|
+
render(
|
|
45
|
+
<Dialog open>
|
|
46
|
+
<WorkspacePickerContent
|
|
47
|
+
workspaces={[one]}
|
|
48
|
+
visibility={visibility()}
|
|
49
|
+
onSelect={onSelect}
|
|
50
|
+
onCreate={onCreate}
|
|
51
|
+
onJoin={onJoin}
|
|
52
|
+
/>
|
|
53
|
+
</Dialog>
|
|
54
|
+
);
|
|
55
|
+
const create = screen.getByRole('button', {
|
|
56
|
+
name: 'create_workspace_action',
|
|
57
|
+
});
|
|
58
|
+
const search = screen.getByRole('button', { name: 'search_workspace' });
|
|
59
|
+
expect(create.parentElement).toBe(search.parentElement);
|
|
60
|
+
expect(create.parentElement).toHaveClass('absolute');
|
|
61
|
+
fireEvent.click(create);
|
|
62
|
+
fireEvent.click(
|
|
63
|
+
screen.getByRole('button', { name: 'join_workspace_action' })
|
|
64
|
+
);
|
|
65
|
+
expect(onCreate).toHaveBeenCalledOnce();
|
|
66
|
+
expect(onJoin).toHaveBeenCalledOnce();
|
|
67
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
it('focuses search when opened and clears the current filtered query', async () => {
|
|
70
|
+
render(
|
|
71
|
+
<Dialog open>
|
|
72
|
+
<WorkspacePickerContent
|
|
73
|
+
workspaces={[one, other]}
|
|
74
|
+
visibility={visibility()}
|
|
75
|
+
onSelect={vi.fn()}
|
|
76
|
+
/>
|
|
77
|
+
</Dialog>
|
|
78
|
+
);
|
|
79
|
+
fireEvent.click(screen.getByRole('button', { name: 'search_workspace' }));
|
|
80
|
+
const input = screen.getByRole('textbox', { name: 'search_workspace' });
|
|
81
|
+
await waitFor(() => expect(document.activeElement).toBe(input));
|
|
82
|
+
fireEvent.change(input, { target: { value: 'Repeated' } });
|
|
83
|
+
expect(screen.queryByText('Another workspace')).toBeNull();
|
|
84
|
+
fireEvent.click(screen.getByRole('button', { name: 'clear_search' }));
|
|
85
|
+
expect(input).toHaveValue('');
|
|
86
|
+
expect(document.activeElement).toBe(input);
|
|
87
|
+
expect(screen.getByText('Another workspace')).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
it('provides fullscreen scrollable modal content with trapped focus and Escape close', async () => {
|
|
90
|
+
const onOpenChange = vi.fn();
|
|
91
|
+
render(
|
|
92
|
+
<>
|
|
93
|
+
<input aria-label="Outside picker" />
|
|
94
|
+
<Dialog open onOpenChange={onOpenChange}>
|
|
95
|
+
<WorkspacePickerContent
|
|
96
|
+
workspaces={[one]}
|
|
97
|
+
visibility={visibility()}
|
|
98
|
+
onSelect={vi.fn()}
|
|
99
|
+
/>
|
|
100
|
+
</Dialog>
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
const dialog = screen.getByRole('dialog');
|
|
104
|
+
expect(dialog).toHaveClass('inset-0', 'h-dvh', 'overflow-hidden');
|
|
105
|
+
const row = screen.getByRole('button', { name: 'Repeated name PRO' });
|
|
106
|
+
expect(row.closest('.overflow-y-auto')).not.toBeNull();
|
|
107
|
+
const outside = screen.getByLabelText('Outside picker');
|
|
108
|
+
outside.focus();
|
|
109
|
+
await waitFor(() =>
|
|
110
|
+
expect(dialog.contains(document.activeElement)).toBe(true)
|
|
111
|
+
);
|
|
112
|
+
fireEvent.keyDown(document.activeElement ?? dialog, { key: 'Escape' });
|
|
113
|
+
await waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false));
|
|
114
|
+
});
|
|
115
|
+
it('selects refreshed filtered objects with duplicate names and reordered input', () => {
|
|
116
|
+
const onSelect = vi.fn();
|
|
117
|
+
const state = visibility();
|
|
118
|
+
const { rerender } = render(
|
|
119
|
+
<Dialog open>
|
|
120
|
+
<WorkspacePickerContent
|
|
121
|
+
workspaces={[other, two, one]}
|
|
122
|
+
visibility={state}
|
|
123
|
+
onSelect={onSelect}
|
|
124
|
+
/>
|
|
125
|
+
</Dialog>
|
|
126
|
+
);
|
|
127
|
+
fireEvent.click(screen.getByRole('button', { name: 'search_workspace' }));
|
|
128
|
+
fireEvent.change(screen.getByRole('textbox'), {
|
|
129
|
+
target: { value: 'Repeated' },
|
|
130
|
+
});
|
|
131
|
+
expect(screen.queryByText('Another workspace')).toBeNull();
|
|
132
|
+
const updated = { ...two, tier: 'ENTERPRISE' as const };
|
|
133
|
+
rerender(
|
|
134
|
+
<Dialog open>
|
|
135
|
+
<WorkspacePickerContent
|
|
136
|
+
workspaces={[one, other, updated]}
|
|
137
|
+
visibility={state}
|
|
138
|
+
onSelect={onSelect}
|
|
139
|
+
/>
|
|
140
|
+
</Dialog>
|
|
141
|
+
);
|
|
142
|
+
fireEvent.click(
|
|
143
|
+
screen.getByRole('button', { name: 'Repeated name ENTERPRISE' })
|
|
144
|
+
);
|
|
145
|
+
expect(onSelect).toHaveBeenCalledWith(updated);
|
|
146
|
+
});
|
|
147
|
+
it('restores only the hidden ID without selecting a current/default scope', () => {
|
|
148
|
+
const state = visibility({ hiddenIds: ['b'] });
|
|
149
|
+
const onSelect = vi.fn();
|
|
150
|
+
render(
|
|
151
|
+
<Dialog open>
|
|
152
|
+
<WorkspacePickerContent
|
|
153
|
+
restoreOnly
|
|
154
|
+
workspaces={[one, two, other]}
|
|
155
|
+
visibility={state}
|
|
156
|
+
onSelect={onSelect}
|
|
157
|
+
/>
|
|
158
|
+
</Dialog>
|
|
159
|
+
);
|
|
160
|
+
fireEvent.click(
|
|
161
|
+
screen.getByRole('button', { name: 'restore_workspace: Repeated name' })
|
|
162
|
+
);
|
|
163
|
+
expect(state.setHidden).toHaveBeenCalledWith('b', false);
|
|
164
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
165
|
+
expect(screen.queryByText('Another workspace')).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
it('does not expose unfiltered choices while the private list is unknown', () => {
|
|
168
|
+
render(
|
|
169
|
+
<Dialog open>
|
|
170
|
+
<WorkspacePickerContent
|
|
171
|
+
workspaces={[one, two]}
|
|
172
|
+
visibility={visibility({ known: false, isError: true })}
|
|
173
|
+
onSelect={vi.fn()}
|
|
174
|
+
/>
|
|
175
|
+
</Dialog>
|
|
176
|
+
);
|
|
177
|
+
expect(screen.queryByText('Repeated name')).toBeNull();
|
|
178
|
+
expect(screen.getByRole('status').textContent).toBe(
|
|
179
|
+
'hidden_workspaces_load_error'
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('workspace browser dropdown', () => {
|
|
185
|
+
it('keeps search and Hidden recovery in an anchored scrolling dropdown', async () => {
|
|
186
|
+
const onSelect = vi.fn();
|
|
187
|
+
const onDefault = vi.fn();
|
|
188
|
+
const onCreate = vi.fn();
|
|
189
|
+
const state = visibility({ hiddenIds: [other.id] });
|
|
190
|
+
render(
|
|
191
|
+
<Popover defaultOpen>
|
|
192
|
+
<PopoverTrigger>Choose workspace</PopoverTrigger>
|
|
193
|
+
<WorkspacePickerContent
|
|
194
|
+
presentation="dropdown"
|
|
195
|
+
workspaces={[one, other]}
|
|
196
|
+
visibility={state}
|
|
197
|
+
onSelect={onSelect}
|
|
198
|
+
onDefault={onDefault}
|
|
199
|
+
onCreate={onCreate}
|
|
200
|
+
/>
|
|
201
|
+
</Popover>
|
|
202
|
+
);
|
|
203
|
+
const dropdown = screen.getByRole('dialog', { name: 'workspaces' });
|
|
204
|
+
expect(dropdown).toHaveAttribute('data-slot', 'popover-content');
|
|
205
|
+
expect(dropdown).not.toHaveClass('inset-0');
|
|
206
|
+
expect(dropdown).not.toHaveClass('h-dvh');
|
|
207
|
+
expect(dropdown).toHaveClass(
|
|
208
|
+
'max-h-[min(32rem,var(--radix-popover-content-available-height))]'
|
|
209
|
+
);
|
|
210
|
+
expect(dropdown).toHaveClass('overflow-hidden', 'flex-col');
|
|
211
|
+
const input = screen.getByRole('textbox', { name: 'search_workspace' });
|
|
212
|
+
const row = screen.getByRole('button', { name: 'Repeated name PRO' });
|
|
213
|
+
expect(row.closest('.overflow-y-auto')).not.toBeNull();
|
|
214
|
+
expect(screen.queryByText('Another workspace')).toBeNull();
|
|
215
|
+
fireEvent.click(row);
|
|
216
|
+
expect(onSelect).toHaveBeenCalledWith(one);
|
|
217
|
+
fireEvent.click(screen.getByRole('button', { name: 'default_workspace' }));
|
|
218
|
+
expect(onDefault).toHaveBeenCalledWith(one.id);
|
|
219
|
+
const create = screen.getByRole('button', {
|
|
220
|
+
name: 'create_workspace_action',
|
|
221
|
+
});
|
|
222
|
+
const createFooter = create.parentElement;
|
|
223
|
+
expect(createFooter).not.toHaveClass('absolute');
|
|
224
|
+
fireEvent.click(create);
|
|
225
|
+
expect(onCreate).toHaveBeenCalledOnce();
|
|
226
|
+
fireEvent.change(input, { target: { value: 'missing' } });
|
|
227
|
+
expect(
|
|
228
|
+
screen.queryByRole('button', { name: 'Repeated name PRO' })
|
|
229
|
+
).toBeNull();
|
|
230
|
+
fireEvent.click(screen.getByRole('button', { name: 'hidden_workspaces' }));
|
|
231
|
+
expect(input).toHaveValue('');
|
|
232
|
+
expect(dropdown).toHaveAccessibleName('hidden_workspaces');
|
|
233
|
+
expect(createFooter).toHaveClass('hidden');
|
|
234
|
+
fireEvent.click(
|
|
235
|
+
screen.getByRole('button', {
|
|
236
|
+
name: 'restore_workspace: Another workspace',
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
expect(state.setHidden).toHaveBeenCalledWith(other.id, false);
|
|
240
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
241
|
+
fireEvent.keyDown(dropdown, { key: 'Escape' });
|
|
242
|
+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it.each([false, true])(
|
|
247
|
+
'closes its popover with an optional callback: %s',
|
|
248
|
+
async (withCallback) => {
|
|
249
|
+
const onClose = vi.fn();
|
|
250
|
+
render(
|
|
251
|
+
<Popover defaultOpen>
|
|
252
|
+
<PopoverTrigger>Choose workspace</PopoverTrigger>
|
|
253
|
+
<WorkspacePickerContent
|
|
254
|
+
presentation="dropdown"
|
|
255
|
+
workspaces={[one]}
|
|
256
|
+
visibility={visibility()}
|
|
257
|
+
onSelect={vi.fn()}
|
|
258
|
+
onDefault={vi.fn()}
|
|
259
|
+
onClose={withCallback ? onClose : undefined}
|
|
260
|
+
/>
|
|
261
|
+
</Popover>
|
|
262
|
+
);
|
|
263
|
+
fireEvent.click(screen.getByRole('button', { name: 'close' }));
|
|
264
|
+
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
265
|
+
expect(onClose).toHaveBeenCalledTimes(withCallback ? 1 : 0);
|
|
266
|
+
}
|
|
267
|
+
);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
buildWorkspaceSetupHandoffUrl,
|
|
7
7
|
mergeWorkspaceSelectWorkspaces,
|
|
8
8
|
normalizeWorkspaceSwitchPath,
|
|
9
|
+
resolveGuestWorkspaceLanding,
|
|
9
10
|
resolveWorkspaceAvatarUrl,
|
|
10
11
|
} from '../workspace-select-helpers';
|
|
11
12
|
|
|
@@ -126,44 +127,6 @@ describe('mergeWorkspaceSelectWorkspaces', () => {
|
|
|
126
127
|
/<AvatarFallback[\s\S]*<AvatarImage/u
|
|
127
128
|
);
|
|
128
129
|
});
|
|
129
|
-
|
|
130
|
-
it('lays out the short create and join actions in the command item grid', () => {
|
|
131
|
-
const workspaceSelectSource = readFileSync(
|
|
132
|
-
join(process.cwd(), 'src/components/ui/custom/workspace-select.tsx'),
|
|
133
|
-
'utf8'
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
expect(workspaceSelectSource).toContain(
|
|
137
|
-
'[&_[cmdk-group-items]]:grid-cols-2'
|
|
138
|
-
);
|
|
139
|
-
expect(workspaceSelectSource).toContain(
|
|
140
|
-
"t('common.create_workspace_action')"
|
|
141
|
-
);
|
|
142
|
-
expect(workspaceSelectSource).toContain(
|
|
143
|
-
"t('common.join_workspace_action')"
|
|
144
|
-
);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('supports a modal popover so nested settings pickers remain scrollable', () => {
|
|
148
|
-
const workspaceSelectSource = readFileSync(
|
|
149
|
-
join(process.cwd(), 'src/components/ui/custom/workspace-select.tsx'),
|
|
150
|
-
'utf8'
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
expect(workspaceSelectSource).toContain('popoverModal = false');
|
|
154
|
-
expect(workspaceSelectSource).toContain('<Popover modal={popoverModal}');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('focuses workspace search as soon as the picker opens', () => {
|
|
158
|
-
const workspaceSelectSource = readFileSync(
|
|
159
|
-
join(process.cwd(), 'src/components/ui/custom/workspace-select.tsx'),
|
|
160
|
-
'utf8'
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
expect(workspaceSelectSource).toContain(
|
|
164
|
-
'<CommandInput autoFocus placeholder="Search workspace..." />'
|
|
165
|
-
);
|
|
166
|
-
});
|
|
167
130
|
});
|
|
168
131
|
|
|
169
132
|
describe('normalizeWorkspaceSwitchPath', () => {
|
|
@@ -200,3 +163,26 @@ describe('normalizeWorkspaceSwitchPath', () => {
|
|
|
200
163
|
).toBe('/workspace-1/users');
|
|
201
164
|
});
|
|
202
165
|
});
|
|
166
|
+
|
|
167
|
+
it.each([null, ''])(
|
|
168
|
+
'preserves the boards fallback for a guest without a landing path (%s)',
|
|
169
|
+
(guest_landing_path) => {
|
|
170
|
+
expect(
|
|
171
|
+
resolveGuestWorkspaceLanding({ access_type: 'guest', guest_landing_path })
|
|
172
|
+
).toBe('/tasks/boards');
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
it('preserves explicit guest landing while members retain normal path resolution', () => {
|
|
176
|
+
expect(
|
|
177
|
+
resolveGuestWorkspaceLanding({
|
|
178
|
+
access_type: 'guest',
|
|
179
|
+
guest_landing_path: '/tasks/boards/shared-board',
|
|
180
|
+
})
|
|
181
|
+
).toBe('/tasks/boards/shared-board');
|
|
182
|
+
expect(
|
|
183
|
+
resolveGuestWorkspaceLanding({
|
|
184
|
+
access_type: 'member',
|
|
185
|
+
guest_landing_path: null,
|
|
186
|
+
})
|
|
187
|
+
).toBeNull();
|
|
188
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { EducationTodo } from './education-todo';
|
|
4
|
+
|
|
5
|
+
const state = vi.hoisted(() => ({
|
|
6
|
+
query: vi.fn(),
|
|
7
|
+
refetch: vi.fn(),
|
|
8
|
+
error: false,
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('@tanstack/react-query', () => ({
|
|
11
|
+
useQuery: (options: unknown) => {
|
|
12
|
+
state.query(options);
|
|
13
|
+
return {
|
|
14
|
+
isError: state.error,
|
|
15
|
+
isPending: false,
|
|
16
|
+
refetch: state.refetch,
|
|
17
|
+
data: {
|
|
18
|
+
count: 30,
|
|
19
|
+
totalPages: 2,
|
|
20
|
+
data: [
|
|
21
|
+
{
|
|
22
|
+
id: 'done',
|
|
23
|
+
title: 'Completed synthetic work',
|
|
24
|
+
courseId: 'course',
|
|
25
|
+
completed: true,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'open',
|
|
29
|
+
title: 'Open synthetic work',
|
|
30
|
+
courseId: 'course',
|
|
31
|
+
completed: false,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
vi.mock('@tuturuuu/internal-api/teach', () => ({ listEducationTodo: vi.fn() }));
|
|
39
|
+
const translate = (key: string) => key;
|
|
40
|
+
describe('personal education work controls', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.clearAllMocks();
|
|
43
|
+
state.error = false;
|
|
44
|
+
});
|
|
45
|
+
it('separates actor caches, paginates and resets page on category switch', () => {
|
|
46
|
+
const { rerender } = render(
|
|
47
|
+
<EducationTodo
|
|
48
|
+
app="learn"
|
|
49
|
+
wsId="workspace"
|
|
50
|
+
actorId="actor-one"
|
|
51
|
+
t={translate}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
expect(state.query).toHaveBeenLastCalledWith(
|
|
55
|
+
expect.objectContaining({
|
|
56
|
+
queryKey: [
|
|
57
|
+
'education-todo',
|
|
58
|
+
'learn',
|
|
59
|
+
'workspace',
|
|
60
|
+
'actor-one',
|
|
61
|
+
'tutoring',
|
|
62
|
+
1,
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
fireEvent.click(screen.getByRole('button', { name: 'next' }));
|
|
67
|
+
expect(state.query).toHaveBeenLastCalledWith(
|
|
68
|
+
expect.objectContaining({
|
|
69
|
+
queryKey: [
|
|
70
|
+
'education-todo',
|
|
71
|
+
'learn',
|
|
72
|
+
'workspace',
|
|
73
|
+
'actor-one',
|
|
74
|
+
'tutoring',
|
|
75
|
+
2,
|
|
76
|
+
],
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
fireEvent.click(screen.getByRole('button', { name: 'assignments' }));
|
|
80
|
+
expect(screen.queryByText('Completed synthetic work')).toBeNull();
|
|
81
|
+
expect(screen.getByText('Open synthetic work')).toBeTruthy();
|
|
82
|
+
expect(state.query).toHaveBeenLastCalledWith(
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
queryKey: [
|
|
85
|
+
'education-todo',
|
|
86
|
+
'learn',
|
|
87
|
+
'workspace',
|
|
88
|
+
'actor-one',
|
|
89
|
+
'assignments',
|
|
90
|
+
1,
|
|
91
|
+
],
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
fireEvent.click(screen.getByRole('checkbox'));
|
|
95
|
+
expect(screen.getByText('Completed synthetic work')).toBeTruthy();
|
|
96
|
+
rerender(
|
|
97
|
+
<EducationTodo
|
|
98
|
+
app="learn"
|
|
99
|
+
wsId="workspace"
|
|
100
|
+
actorId="actor-two"
|
|
101
|
+
t={translate}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
expect(state.query).toHaveBeenLastCalledWith(
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
queryKey: [
|
|
107
|
+
'education-todo',
|
|
108
|
+
'learn',
|
|
109
|
+
'workspace',
|
|
110
|
+
'actor-two',
|
|
111
|
+
'assignments',
|
|
112
|
+
1,
|
|
113
|
+
],
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
it('offers retry on errors without rendering stale assigned rows', () => {
|
|
118
|
+
state.error = true;
|
|
119
|
+
render(
|
|
120
|
+
<EducationTodo
|
|
121
|
+
app="teach"
|
|
122
|
+
wsId="workspace"
|
|
123
|
+
actorId="actor"
|
|
124
|
+
t={translate}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
expect(screen.queryByText('Open synthetic work')).toBeNull();
|
|
128
|
+
fireEvent.click(screen.getByRole('button', { name: 'retry' }));
|
|
129
|
+
expect(state.refetch).toHaveBeenCalledOnce();
|
|
130
|
+
});
|
|
131
|
+
});
|