@tuturuuu/ui 0.37.0 → 0.37.2
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 +26 -0
- package/package.json +7 -5
- package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +181 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +24 -38
- 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/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 +293 -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 +87 -359
- package/src/components/ui/date-time-picker-timezone.test.tsx +258 -0
- package/src/components/ui/date-time-picker-values.ts +205 -0
- package/src/components/ui/date-time-picker.tsx +70 -167
- package/src/components/ui/legacy/calendar/agenda-view.tsx +29 -94
- package/src/components/ui/legacy/calendar/all-day-color.test.tsx +48 -0
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +29 -41
- package/src/components/ui/legacy/calendar/calendar-cell-color.test.tsx +70 -0
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +4 -5
- package/src/components/ui/legacy/calendar/calendar-content.test.tsx +82 -3
- package/src/components/ui/legacy/calendar/calendar-content.tsx +37 -34
- package/src/components/ui/legacy/calendar/calendar-day-bridge.test.tsx +94 -0
- package/src/components/ui/legacy/calendar/calendar-day-display.test.tsx +106 -0
- package/src/components/ui/legacy/calendar/calendar-header.tsx +14 -5
- package/src/components/ui/legacy/calendar/calendar-matrix.tsx +4 -9
- package/src/components/ui/legacy/calendar/calendar-views.test.tsx +120 -6
- package/src/components/ui/legacy/calendar/day-title.tsx +6 -6
- package/src/components/ui/legacy/calendar/dynamic-island.test.tsx +140 -0
- package/src/components/ui/legacy/calendar/dynamic-island.tsx +27 -24
- package/src/components/ui/legacy/calendar/event-card.test.tsx +206 -7
- package/src/components/ui/legacy/calendar/event-card.tsx +27 -49
- package/src/components/ui/legacy/calendar/event-modal.tsx +6 -26
- package/src/components/ui/legacy/calendar/event-picker-bounds.ts +16 -0
- package/src/components/ui/legacy/calendar/event-preview-popover.test.tsx +139 -0
- package/src/components/ui/legacy/calendar/event-preview-popover.tsx +13 -17
- package/src/components/ui/legacy/calendar/event-preview-time.ts +41 -0
- package/src/components/ui/legacy/calendar/location-timeline.tsx +9 -17
- package/src/components/ui/legacy/calendar/month-calendar.tsx +20 -10
- 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/time-indicator-line.tsx +3 -20
- package/src/components/ui/legacy/calendar/time-indicator.tsx +11 -3
- package/src/components/ui/legacy/calendar/weekday-bar.tsx +2 -8
- package/src/components/ui/legacy/calendar/year-calendar.tsx +27 -39
- package/src/components/ui/resizable.tsx +2 -1
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +45 -0
- package/src/hooks/__tests__/use-visible-workspaces.test.tsx +76 -0
- package/src/hooks/__tests__/use-workspace-visibility.test.tsx +155 -0
- package/src/hooks/__tests__/workspace-actor-lifetime.test.tsx +103 -0
- package/src/hooks/calendar-event-lookup.test.ts +39 -0
- package/src/hooks/calendar-event-lookup.ts +22 -8
- package/src/hooks/calendar-sync-range.test.ts +92 -0
- package/src/hooks/calendar-sync-range.ts +40 -0
- package/src/hooks/use-calendar-clock.ts +49 -0
- package/src/hooks/use-calendar-day-zone.tsx +12 -0
- package/src/hooks/use-calendar-sync.tsx +42 -80
- package/src/hooks/use-calendar.tsx +3 -3
- package/src/hooks/use-visible-workspaces.ts +57 -0
- package/src/hooks/use-workspace-user.ts +15 -3
- package/src/hooks/use-workspace-visibility.ts +234 -0
- package/src/lib/calendar-day.ts +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.37.2](https://github.com/tutur3u/platform/compare/ui-v0.37.1...ui-v0.37.2) (2026-10-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **calendar:** bind source color hydration to persisted identity ([00269c3](https://github.com/tutur3u/platform/commit/00269c315cd2ac8e1c9607f450100db8537cada2))
|
|
9
|
+
* **calendar:** preserve month hover color contrast ([6fd591c](https://github.com/tutur3u/platform/commit/6fd591c46f4c90d6f4c685c63b140728dd0339fe))
|
|
10
|
+
* **calendar:** preserve opaque rendering cues and hydrate source colors ([33847dd](https://github.com/tutur3u/platform/commit/33847ddcd8a3cf2b5cef8ebf50ca4c1700424a27))
|
|
11
|
+
* **calendar:** preserve resolved contrast during event hover ([79001b3](https://github.com/tutur3u/platform/commit/79001b32ad1c813119f8f7df934e7142bbbd7585))
|
|
12
|
+
* **calendar:** render opaque readable event colors ([#5694](https://github.com/tutur3u/platform/issues/5694)) ([6e3d81d](https://github.com/tutur3u/platform/commit/6e3d81d6fdd6f8155f12e36ede58d31fde0ac362))
|
|
13
|
+
* **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))
|
|
14
|
+
|
|
15
|
+
## [0.37.1](https://github.com/tutur3u/platform/compare/ui-v0.37.0...ui-v0.37.1) (2026-10-01)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **calendar:** align calendar-day navigation and zoned query ranges ([e81a8be](https://github.com/tutur3u/platform/commit/e81a8be8edf2e46b2653bfcf30bc118d12984e88))
|
|
21
|
+
* **calendar:** align date navigation and zoned query ranges ([#5654](https://github.com/tutur3u/platform/issues/5654)) ([f40288c](https://github.com/tutur3u/platform/commit/f40288c7cc817f71ca30f6ee5331efbf726c4ada))
|
|
22
|
+
* **calendar:** format clock options without browser dates ([757709e](https://github.com/tutur3u/platform/commit/757709ecbf1070cfa3248ece54f9b9c66cd000c7))
|
|
23
|
+
* **calendar:** honor resolved timezone in event picker ([eae3a25](https://github.com/tutur3u/platform/commit/eae3a25ec443fbb1fe51acf1c642a6b44e6c80f8)) ([#5648](https://github.com/tutur3u/platform/issues/5648)) ([7ccc4b3](https://github.com/tutur3u/platform/commit/7ccc4b356411744f5b90e55c35c1e6f759981d42))
|
|
24
|
+
* **calendar:** preserve explicit time bounds and guard DST rollover ([56d746b](https://github.com/tutur3u/platform/commit/56d746b91e8c4d48c6c97ad88a32cab74f8457cc))
|
|
25
|
+
* **calendar:** respect timezone in picker bounds and previews ([47720c7](https://github.com/tutur3u/platform/commit/47720c72d09545cd86d3d3814d4fb83f53848890))
|
|
26
|
+
* **calendar:** type-check picker regression import ([60aef11](https://github.com/tutur3u/platform/commit/60aef114c7ad9b3b327befaee37798db1a98b9b6))
|
|
27
|
+
* **calendar:** use calendar timezone for picker bounds and previews ([#5653](https://github.com/tutur3u/platform/issues/5653)) ([ea912c0](https://github.com/tutur3u/platform/commit/ea912c0fd89bf54ca073ae9c8b219a3696db4d19))
|
|
28
|
+
|
|
3
29
|
## [0.37.0](https://github.com/tutur3u/platform/compare/ui-v0.36.1...ui-v0.37.0) (2026-09-29)
|
|
4
30
|
|
|
5
31
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -91,9 +91,9 @@
|
|
|
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.48.0",
|
|
95
95
|
"@tuturuuu/supabase": "0.5.2",
|
|
96
|
-
"@tuturuuu/utils": "0.33.
|
|
96
|
+
"@tuturuuu/utils": "0.33.3",
|
|
97
97
|
"@types/debug": "^4.1.13",
|
|
98
98
|
"browser-image-compression": "^2.0.2",
|
|
99
99
|
"class-variance-authority": "^0.7.1",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"use-debounce": "^10.1.1",
|
|
145
145
|
"vaul": "^1.1.2",
|
|
146
146
|
"y-protocols": "^1.0.7",
|
|
147
|
-
"yjs": "^13.6.
|
|
147
|
+
"yjs": "^13.6.33",
|
|
148
148
|
"zod": "^4.6.2"
|
|
149
149
|
},
|
|
150
150
|
"devDependencies": {
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
"@tanstack/react-table": "^9.2.4",
|
|
155
155
|
"@testing-library/jest-dom": "^7.0.1",
|
|
156
156
|
"@testing-library/react": "^16.3.3",
|
|
157
|
-
"@tuturuuu/types": "0.33.
|
|
157
|
+
"@tuturuuu/types": "0.33.1",
|
|
158
158
|
"@tuturuuu/typescript-config": "0.1.2",
|
|
159
159
|
"@types/html2canvas": "^1.0.0",
|
|
160
160
|
"@types/lodash": "^4.17.25",
|
|
@@ -226,6 +226,8 @@
|
|
|
226
226
|
"./hooks/*": "./src/hooks/*.ts",
|
|
227
227
|
"./hooks/time-blocking-provider": "./src/hooks/time-blocking-provider.tsx",
|
|
228
228
|
"./hooks/use-at-bottom": "./src/hooks/use-at-bottom.tsx",
|
|
229
|
+
"./hooks/use-calendar-day-zone": "./src/hooks/use-calendar-day-zone.tsx",
|
|
230
|
+
"./lib/calendar-day": "./src/lib/calendar-day.ts",
|
|
229
231
|
"./hooks/use-calendar-preferences": "./src/hooks/use-calendar-preferences.tsx",
|
|
230
232
|
"./hooks/use-calendar-sync": "./src/hooks/use-calendar-sync.tsx",
|
|
231
233
|
"./hooks/use-calendar": "./src/hooks/use-calendar.tsx",
|
|
@@ -0,0 +1,181 @@
|
|
|
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 { WorkspacePickerContent } from '../workspace-picker-content';
|
|
7
|
+
|
|
8
|
+
vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key }));
|
|
9
|
+
vi.mock('../workspace-select-icon', () => ({
|
|
10
|
+
WorkspaceIcon: () => <span aria-hidden="true" />,
|
|
11
|
+
}));
|
|
12
|
+
const one: InternalApiWorkspaceSummary = {
|
|
13
|
+
id: 'a',
|
|
14
|
+
name: 'Repeated name',
|
|
15
|
+
personal: false,
|
|
16
|
+
avatar_url: null,
|
|
17
|
+
logo_url: null,
|
|
18
|
+
tier: 'PRO',
|
|
19
|
+
};
|
|
20
|
+
const two: InternalApiWorkspaceSummary = { ...one, id: 'b' };
|
|
21
|
+
const other: InternalApiWorkspaceSummary = {
|
|
22
|
+
...one,
|
|
23
|
+
id: 'z',
|
|
24
|
+
name: 'Another workspace',
|
|
25
|
+
};
|
|
26
|
+
function visibility(overrides = {}) {
|
|
27
|
+
return {
|
|
28
|
+
known: true,
|
|
29
|
+
hiddenIds: [],
|
|
30
|
+
pending: new Set(),
|
|
31
|
+
isError: false,
|
|
32
|
+
updateError: null,
|
|
33
|
+
setHidden: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
refetch: vi.fn(),
|
|
35
|
+
...overrides,
|
|
36
|
+
} as unknown as ReturnType<typeof useWorkspaceVisibility>;
|
|
37
|
+
}
|
|
38
|
+
describe('workspace sheet rendered identities', () => {
|
|
39
|
+
it('keeps floating create and search actions separate from workspace selection', () => {
|
|
40
|
+
const onCreate = vi.fn();
|
|
41
|
+
const onJoin = vi.fn();
|
|
42
|
+
const onSelect = vi.fn();
|
|
43
|
+
render(
|
|
44
|
+
<Dialog open>
|
|
45
|
+
<WorkspacePickerContent
|
|
46
|
+
workspaces={[one]}
|
|
47
|
+
visibility={visibility()}
|
|
48
|
+
onSelect={onSelect}
|
|
49
|
+
onCreate={onCreate}
|
|
50
|
+
onJoin={onJoin}
|
|
51
|
+
/>
|
|
52
|
+
</Dialog>
|
|
53
|
+
);
|
|
54
|
+
const create = screen.getByRole('button', {
|
|
55
|
+
name: 'create_workspace_action',
|
|
56
|
+
});
|
|
57
|
+
const search = screen.getByRole('button', { name: 'search_workspace' });
|
|
58
|
+
expect(create.parentElement).toBe(search.parentElement);
|
|
59
|
+
expect(create.parentElement).toHaveClass('absolute');
|
|
60
|
+
fireEvent.click(create);
|
|
61
|
+
fireEvent.click(
|
|
62
|
+
screen.getByRole('button', { name: 'join_workspace_action' })
|
|
63
|
+
);
|
|
64
|
+
expect(onCreate).toHaveBeenCalledOnce();
|
|
65
|
+
expect(onJoin).toHaveBeenCalledOnce();
|
|
66
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
it('focuses search when opened and clears the current filtered query', async () => {
|
|
69
|
+
render(
|
|
70
|
+
<Dialog open>
|
|
71
|
+
<WorkspacePickerContent
|
|
72
|
+
workspaces={[one, other]}
|
|
73
|
+
visibility={visibility()}
|
|
74
|
+
onSelect={vi.fn()}
|
|
75
|
+
/>
|
|
76
|
+
</Dialog>
|
|
77
|
+
);
|
|
78
|
+
fireEvent.click(screen.getByRole('button', { name: 'search_workspace' }));
|
|
79
|
+
const input = screen.getByRole('textbox', { name: 'search_workspace' });
|
|
80
|
+
await waitFor(() => expect(document.activeElement).toBe(input));
|
|
81
|
+
fireEvent.change(input, { target: { value: 'Repeated' } });
|
|
82
|
+
expect(screen.queryByText('Another workspace')).toBeNull();
|
|
83
|
+
fireEvent.click(screen.getByRole('button', { name: 'clear_search' }));
|
|
84
|
+
expect(input).toHaveValue('');
|
|
85
|
+
expect(document.activeElement).toBe(input);
|
|
86
|
+
expect(screen.getByText('Another workspace')).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
it('provides fullscreen scrollable modal content with trapped focus and Escape close', async () => {
|
|
89
|
+
const onOpenChange = vi.fn();
|
|
90
|
+
render(
|
|
91
|
+
<>
|
|
92
|
+
<input aria-label="Outside picker" />
|
|
93
|
+
<Dialog open onOpenChange={onOpenChange}>
|
|
94
|
+
<WorkspacePickerContent
|
|
95
|
+
workspaces={[one]}
|
|
96
|
+
visibility={visibility()}
|
|
97
|
+
onSelect={vi.fn()}
|
|
98
|
+
/>
|
|
99
|
+
</Dialog>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
const dialog = screen.getByRole('dialog');
|
|
103
|
+
expect(dialog).toHaveClass('inset-0', 'h-dvh', 'overflow-hidden');
|
|
104
|
+
const row = screen.getByRole('button', { name: 'Repeated name PRO' });
|
|
105
|
+
expect(row.closest('.overflow-y-auto')).not.toBeNull();
|
|
106
|
+
const outside = screen.getByLabelText('Outside picker');
|
|
107
|
+
outside.focus();
|
|
108
|
+
await waitFor(() =>
|
|
109
|
+
expect(dialog.contains(document.activeElement)).toBe(true)
|
|
110
|
+
);
|
|
111
|
+
fireEvent.keyDown(document.activeElement ?? dialog, { key: 'Escape' });
|
|
112
|
+
await waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false));
|
|
113
|
+
});
|
|
114
|
+
it('selects refreshed filtered objects with duplicate names and reordered input', () => {
|
|
115
|
+
const onSelect = vi.fn();
|
|
116
|
+
const state = visibility();
|
|
117
|
+
const { rerender } = render(
|
|
118
|
+
<Dialog open>
|
|
119
|
+
<WorkspacePickerContent
|
|
120
|
+
workspaces={[other, two, one]}
|
|
121
|
+
visibility={state}
|
|
122
|
+
onSelect={onSelect}
|
|
123
|
+
/>
|
|
124
|
+
</Dialog>
|
|
125
|
+
);
|
|
126
|
+
fireEvent.click(screen.getByRole('button', { name: 'search_workspace' }));
|
|
127
|
+
fireEvent.change(screen.getByRole('textbox'), {
|
|
128
|
+
target: { value: 'Repeated' },
|
|
129
|
+
});
|
|
130
|
+
expect(screen.queryByText('Another workspace')).toBeNull();
|
|
131
|
+
const updated = { ...two, tier: 'ENTERPRISE' as const };
|
|
132
|
+
rerender(
|
|
133
|
+
<Dialog open>
|
|
134
|
+
<WorkspacePickerContent
|
|
135
|
+
workspaces={[one, other, updated]}
|
|
136
|
+
visibility={state}
|
|
137
|
+
onSelect={onSelect}
|
|
138
|
+
/>
|
|
139
|
+
</Dialog>
|
|
140
|
+
);
|
|
141
|
+
fireEvent.click(
|
|
142
|
+
screen.getByRole('button', { name: 'Repeated name ENTERPRISE' })
|
|
143
|
+
);
|
|
144
|
+
expect(onSelect).toHaveBeenCalledWith(updated);
|
|
145
|
+
});
|
|
146
|
+
it('restores only the hidden ID without selecting a current/default scope', () => {
|
|
147
|
+
const state = visibility({ hiddenIds: ['b'] });
|
|
148
|
+
const onSelect = vi.fn();
|
|
149
|
+
render(
|
|
150
|
+
<Dialog open>
|
|
151
|
+
<WorkspacePickerContent
|
|
152
|
+
restoreOnly
|
|
153
|
+
workspaces={[one, two, other]}
|
|
154
|
+
visibility={state}
|
|
155
|
+
onSelect={onSelect}
|
|
156
|
+
/>
|
|
157
|
+
</Dialog>
|
|
158
|
+
);
|
|
159
|
+
fireEvent.click(
|
|
160
|
+
screen.getByRole('button', { name: 'restore_workspace: Repeated name' })
|
|
161
|
+
);
|
|
162
|
+
expect(state.setHidden).toHaveBeenCalledWith('b', false);
|
|
163
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
164
|
+
expect(screen.queryByText('Another workspace')).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
it('does not expose unfiltered choices while the private list is unknown', () => {
|
|
167
|
+
render(
|
|
168
|
+
<Dialog open>
|
|
169
|
+
<WorkspacePickerContent
|
|
170
|
+
workspaces={[one, two]}
|
|
171
|
+
visibility={visibility({ known: false, isError: true })}
|
|
172
|
+
onSelect={vi.fn()}
|
|
173
|
+
/>
|
|
174
|
+
</Dialog>
|
|
175
|
+
);
|
|
176
|
+
expect(screen.queryByText('Repeated name')).toBeNull();
|
|
177
|
+
expect(screen.getByRole('status').textContent).toBe(
|
|
178
|
+
'hidden_workspaces_load_error'
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -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,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
4
|
+
import { listWorkspaces } from '@tuturuuu/internal-api/workspaces';
|
|
5
|
+
import { useTranslations } from 'next-intl';
|
|
6
|
+
import {
|
|
7
|
+
useWorkspaceActor,
|
|
8
|
+
useWorkspaceVisibility,
|
|
9
|
+
} from '../../../hooks/use-workspace-visibility';
|
|
10
|
+
import { Button } from '../button';
|
|
11
|
+
import { Dialog, DialogTrigger } from '../dialog';
|
|
12
|
+
import { WorkspacePickerContent } from './workspace-picker-content';
|
|
13
|
+
|
|
14
|
+
/** Private Settings restore editor; restoring never changes current/default scope. */
|
|
15
|
+
export function HiddenWorkspacesSettings() {
|
|
16
|
+
const t = useTranslations('common');
|
|
17
|
+
const actor = useWorkspaceActor();
|
|
18
|
+
const visibility = useWorkspaceVisibility();
|
|
19
|
+
const list = useQuery({
|
|
20
|
+
queryKey: ['workspace-ui-list', actor?.actorId],
|
|
21
|
+
enabled: Boolean(actor),
|
|
22
|
+
queryFn: async () => {
|
|
23
|
+
actor!.assertActive();
|
|
24
|
+
const result = await listWorkspaces();
|
|
25
|
+
actor!.assertActive();
|
|
26
|
+
return result;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
return (
|
|
30
|
+
<Dialog>
|
|
31
|
+
<DialogTrigger asChild>
|
|
32
|
+
<Button variant="outline" disabled={!actor}>
|
|
33
|
+
{t('hidden_workspaces')}
|
|
34
|
+
</Button>
|
|
35
|
+
</DialogTrigger>
|
|
36
|
+
<WorkspacePickerContent
|
|
37
|
+
listLoading={list.isLoading}
|
|
38
|
+
listError={list.error}
|
|
39
|
+
onRetryList={() => {
|
|
40
|
+
void list.refetch();
|
|
41
|
+
}}
|
|
42
|
+
restoreOnly
|
|
43
|
+
workspaces={list.data ?? []}
|
|
44
|
+
visibility={visibility}
|
|
45
|
+
onSelect={() => {}}
|
|
46
|
+
/>
|
|
47
|
+
</Dialog>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Measure the fixed mobile header including safe-area padding and wrapped rows. */
|
|
2
|
+
export function observeMobileHeaderHeight(nav: HTMLElement, main: HTMLElement) {
|
|
3
|
+
const update = () => {
|
|
4
|
+
main.style.setProperty(
|
|
5
|
+
'--mobile-nav-height',
|
|
6
|
+
`${nav.getBoundingClientRect().height}px`
|
|
7
|
+
);
|
|
8
|
+
};
|
|
9
|
+
update();
|
|
10
|
+
const observer = new ResizeObserver(update);
|
|
11
|
+
observer.observe(nav);
|
|
12
|
+
return () => {
|
|
13
|
+
observer.disconnect();
|
|
14
|
+
main.style.removeProperty('--mobile-nav-height');
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -4,7 +4,14 @@ import { ChevronLeft, ChevronRight, Menu, X } from '@tuturuuu/icons';
|
|
|
4
4
|
import { Button } from '@tuturuuu/ui/button';
|
|
5
5
|
import { TooltipProvider } from '@tuturuuu/ui/tooltip';
|
|
6
6
|
import { cn } from '@tuturuuu/utils/format';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
useEffect,
|
|
11
|
+
useLayoutEffect,
|
|
12
|
+
useRef,
|
|
13
|
+
} from 'react';
|
|
14
|
+
import { observeMobileHeaderHeight } from './mobile-header-height';
|
|
8
15
|
|
|
9
16
|
interface StructureProps {
|
|
10
17
|
sidebarLabels?: { open: string; close: string };
|
|
@@ -22,6 +29,7 @@ interface StructureProps {
|
|
|
22
29
|
sidebarUtility?: ReactNode;
|
|
23
30
|
feedbackButton?: ReactNode;
|
|
24
31
|
children: ReactNode;
|
|
32
|
+
contentFullBleed?: boolean;
|
|
25
33
|
onMouseEnter?: () => void;
|
|
26
34
|
onMouseLeave?: () => void;
|
|
27
35
|
sidebarHidden?: boolean;
|
|
@@ -47,6 +55,7 @@ export function Structure({
|
|
|
47
55
|
sidebarUtility,
|
|
48
56
|
feedbackButton,
|
|
49
57
|
children,
|
|
58
|
+
contentFullBleed = false,
|
|
50
59
|
onMouseEnter,
|
|
51
60
|
onMouseLeave,
|
|
52
61
|
sidebarHidden = false,
|
|
@@ -55,6 +64,15 @@ export function Structure({
|
|
|
55
64
|
sidebarHeaderClassName,
|
|
56
65
|
sidebarHeaderHeight,
|
|
57
66
|
}: StructureProps) {
|
|
67
|
+
const mobileNavRef = useRef<HTMLElement>(null);
|
|
68
|
+
const mainRef = useRef<HTMLElement>(null);
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
if (!contentFullBleed || sidebarHidden) return;
|
|
71
|
+
const nav = mobileNavRef.current;
|
|
72
|
+
const main = mainRef.current;
|
|
73
|
+
if (nav && main) return observeMobileHeaderHeight(nav, main);
|
|
74
|
+
}, [contentFullBleed, sidebarHidden]);
|
|
75
|
+
|
|
58
76
|
const sidebarStyle = {
|
|
59
77
|
'--sidebar-collapsed-width': sidebarCollapsedWidth,
|
|
60
78
|
'--sidebar-expanded-width': sidebarExpandedWidth,
|
|
@@ -86,7 +104,10 @@ export function Structure({
|
|
|
86
104
|
return (
|
|
87
105
|
<>
|
|
88
106
|
{!sidebarHidden && (
|
|
89
|
-
<nav
|
|
107
|
+
<nav
|
|
108
|
+
ref={mobileNavRef}
|
|
109
|
+
className="safe-top safe-x fixed inset-x-0 top-0 z-30 max-sm:border-b md:hidden"
|
|
110
|
+
>
|
|
90
111
|
<div className="bg-background/50 p-2 font-semibold backdrop-blur-md md:px-8 lg:px-16 xl:px-32">
|
|
91
112
|
<div className="relative flex items-center justify-between gap-2 md:gap-4">
|
|
92
113
|
<div className="flex w-full items-center gap-2">
|
|
@@ -236,8 +257,12 @@ export function Structure({
|
|
|
236
257
|
{/* Main content area - overflow-y-auto removed to prevent double scrollbars */}
|
|
237
258
|
{/* Body element now handles page-level scrolling */}
|
|
238
259
|
<main
|
|
260
|
+
ref={mainRef}
|
|
239
261
|
className={cn(
|
|
240
|
-
'relative flex
|
|
262
|
+
'relative flex flex-col overflow-x-clip transition-all duration-300 ease-in-out',
|
|
263
|
+
contentFullBleed
|
|
264
|
+
? 'h-dvh min-h-0 overflow-y-hidden'
|
|
265
|
+
: 'h-full min-h-screen overflow-y-visible',
|
|
241
266
|
sidebarHidden
|
|
242
267
|
? 'md:pl-0'
|
|
243
268
|
: isCollapsed || overlayOnExpand
|
|
@@ -249,8 +274,15 @@ export function Structure({
|
|
|
249
274
|
{header && <div className="mb-4 hidden md:block">{header}</div>}
|
|
250
275
|
<div
|
|
251
276
|
className={cn(
|
|
252
|
-
'safe-bottom relative
|
|
253
|
-
|
|
277
|
+
'safe-bottom relative w-full',
|
|
278
|
+
contentFullBleed
|
|
279
|
+
? sidebarHidden
|
|
280
|
+
? 'flex min-h-0 flex-1 flex-col'
|
|
281
|
+
: 'flex min-h-0 flex-1 flex-col pt-[var(--mobile-nav-height,calc(4.25rem+env(safe-area-inset-top,0px)))] md:pt-0'
|
|
282
|
+
: [
|
|
283
|
+
'h-full p-2 md:p-4 md:pt-4',
|
|
284
|
+
sidebarHidden ? 'pt-2' : 'pt-17 pl-2',
|
|
285
|
+
]
|
|
254
286
|
)}
|
|
255
287
|
>
|
|
256
288
|
{children}
|
|
@@ -175,7 +175,8 @@ export function Filter({
|
|
|
175
175
|
className={cn('h-8 border-dashed px-1', className)}
|
|
176
176
|
disabled={disabled}
|
|
177
177
|
>
|
|
178
|
-
{selectedSize > 0
|
|
178
|
+
{selectedSize > 0 &&
|
|
179
|
+
options.some((option) => selectedValues.has(option.value)) ? null : (
|
|
179
180
|
<>
|
|
180
181
|
{icon} {title}
|
|
181
182
|
</>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from 'next-intl';
|
|
4
|
+
import type { ComponentProps } from 'react';
|
|
5
|
+
import { useWorkspaceVisibility } from '../../../hooks/use-workspace-visibility';
|
|
6
|
+
import { Button } from '../button';
|
|
7
|
+
import { Filter } from './user-filters';
|
|
8
|
+
|
|
9
|
+
/** Filter choices only; never change the underlying authorized report/data query. */
|
|
10
|
+
export function VisibleWorkspaceFilter(props: ComponentProps<typeof Filter>) {
|
|
11
|
+
const t = useTranslations('common');
|
|
12
|
+
const visibility = useWorkspaceVisibility();
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<Filter
|
|
16
|
+
{...props}
|
|
17
|
+
options={
|
|
18
|
+
visibility.known
|
|
19
|
+
? props.options?.filter(
|
|
20
|
+
(option) => !visibility.hiddenIds.includes(option.value)
|
|
21
|
+
)
|
|
22
|
+
: []
|
|
23
|
+
}
|
|
24
|
+
/>
|
|
25
|
+
{!visibility.known && visibility.isError && (
|
|
26
|
+
<span role="alert">
|
|
27
|
+
{t('hidden_workspaces_load_error')}
|
|
28
|
+
<Button
|
|
29
|
+
size="sm"
|
|
30
|
+
variant="ghost"
|
|
31
|
+
onClick={() => {
|
|
32
|
+
void visibility.refetch();
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{t('retry')}
|
|
36
|
+
</Button>
|
|
37
|
+
</span>
|
|
38
|
+
)}
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
}
|