@tuturuuu/ui 0.11.0 → 0.11.1
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 +9 -0
- package/package.json +47 -47
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +22 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +157 -4
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +178 -14
- package/src/components/ui/tu-do/templates/task-template-client.test.tsx +52 -0
- package/src/components/ui/tu-do/templates/task-template-client.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.1](https://github.com/tutur3u/platform/compare/ui-v0.11.0...ui-v0.11.1) (2026-07-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **cli:** preserve markdown tables in task descriptions ([9faf8ba](https://github.com/tutur3u/platform/commit/9faf8ba44b92565a9dfc2949c496654fc1fea9b3))
|
|
9
|
+
* **tasks:** refresh task template placeholders ([0c3c309](https://github.com/tutur3u/platform/commit/0c3c30935f67e3e64d548c8a13584fd597e887b8))
|
|
10
|
+
* **ui:** stabilize task property popover handoff ([8fb2e62](https://github.com/tutur3u/platform/commit/8fb2e62904d34a3ae7894bfbbb66c285fcf1a1bd))
|
|
11
|
+
|
|
3
12
|
## [0.11.0](https://github.com/tutur3u/platform/compare/ui-v0.10.0...ui-v0.11.0) (2026-06-29)
|
|
4
13
|
|
|
5
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ui",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,42 +24,42 @@
|
|
|
24
24
|
"@hookform/resolvers": "^5.4.0",
|
|
25
25
|
"@json-render/core": "^0.19.0",
|
|
26
26
|
"@json-render/react": "^0.19.0",
|
|
27
|
-
"@radix-ui/react-accordion": "^1.2.
|
|
28
|
-
"@radix-ui/react-alert-dialog": "^1.1.
|
|
29
|
-
"@radix-ui/react-aspect-ratio": "^1.1.
|
|
30
|
-
"@radix-ui/react-avatar": "^1.2.
|
|
31
|
-
"@radix-ui/react-checkbox": "^1.3.
|
|
32
|
-
"@radix-ui/react-collapsible": "^1.1.
|
|
33
|
-
"@radix-ui/react-context-menu": "^2.3.
|
|
34
|
-
"@radix-ui/react-dialog": "^1.1.
|
|
35
|
-
"@radix-ui/react-dropdown-menu": "^2.1.
|
|
36
|
-
"@radix-ui/react-hover-card": "^1.1.
|
|
37
|
-
"@radix-ui/react-label": "^2.1.
|
|
38
|
-
"@radix-ui/react-menubar": "^1.1.
|
|
39
|
-
"@radix-ui/react-navigation-menu": "^1.2.
|
|
40
|
-
"@radix-ui/react-popover": "^1.1.
|
|
41
|
-
"@radix-ui/react-progress": "^1.1.
|
|
42
|
-
"@radix-ui/react-radio-group": "^1.4.
|
|
43
|
-
"@radix-ui/react-scroll-area": "^1.2.
|
|
44
|
-
"@radix-ui/react-select": "^2.3.
|
|
45
|
-
"@radix-ui/react-separator": "^1.1.
|
|
46
|
-
"@radix-ui/react-slider": "^1.4.
|
|
27
|
+
"@radix-ui/react-accordion": "^1.2.15",
|
|
28
|
+
"@radix-ui/react-alert-dialog": "^1.1.18",
|
|
29
|
+
"@radix-ui/react-aspect-ratio": "^1.1.11",
|
|
30
|
+
"@radix-ui/react-avatar": "^1.2.1",
|
|
31
|
+
"@radix-ui/react-checkbox": "^1.3.6",
|
|
32
|
+
"@radix-ui/react-collapsible": "^1.1.15",
|
|
33
|
+
"@radix-ui/react-context-menu": "^2.3.2",
|
|
34
|
+
"@radix-ui/react-dialog": "^1.1.18",
|
|
35
|
+
"@radix-ui/react-dropdown-menu": "^2.1.19",
|
|
36
|
+
"@radix-ui/react-hover-card": "^1.1.18",
|
|
37
|
+
"@radix-ui/react-label": "^2.1.11",
|
|
38
|
+
"@radix-ui/react-menubar": "^1.1.19",
|
|
39
|
+
"@radix-ui/react-navigation-menu": "^1.2.17",
|
|
40
|
+
"@radix-ui/react-popover": "^1.1.18",
|
|
41
|
+
"@radix-ui/react-progress": "^1.1.11",
|
|
42
|
+
"@radix-ui/react-radio-group": "^1.4.2",
|
|
43
|
+
"@radix-ui/react-scroll-area": "^1.2.13",
|
|
44
|
+
"@radix-ui/react-select": "^2.3.2",
|
|
45
|
+
"@radix-ui/react-separator": "^1.1.11",
|
|
46
|
+
"@radix-ui/react-slider": "^1.4.2",
|
|
47
47
|
"@radix-ui/react-slot": "^1.3.0",
|
|
48
|
-
"@radix-ui/react-switch": "^1.3.
|
|
49
|
-
"@radix-ui/react-tabs": "^1.1.
|
|
50
|
-
"@radix-ui/react-toast": "^1.2.
|
|
51
|
-
"@radix-ui/react-toggle": "^1.1.
|
|
52
|
-
"@radix-ui/react-toggle-group": "^1.1.
|
|
53
|
-
"@radix-ui/react-tooltip": "^1.2.
|
|
48
|
+
"@radix-ui/react-switch": "^1.3.2",
|
|
49
|
+
"@radix-ui/react-tabs": "^1.1.16",
|
|
50
|
+
"@radix-ui/react-toast": "^1.2.18",
|
|
51
|
+
"@radix-ui/react-toggle": "^1.1.13",
|
|
52
|
+
"@radix-ui/react-toggle-group": "^1.1.14",
|
|
53
|
+
"@radix-ui/react-tooltip": "^1.2.11",
|
|
54
54
|
"@streamdown/cjk": "^1.0.3",
|
|
55
55
|
"@streamdown/code": "^1.1.1",
|
|
56
56
|
"@streamdown/math": "^1.0.2",
|
|
57
57
|
"@streamdown/mermaid": "^1.0.2",
|
|
58
58
|
"@tanstack/react-hotkeys": "^0.10.0",
|
|
59
59
|
"@tanstack/react-pacer": "^0.22.1",
|
|
60
|
-
"@tanstack/react-query": "^5.101.
|
|
60
|
+
"@tanstack/react-query": "^5.101.2",
|
|
61
61
|
"@tanstack/react-table": "^8.21.3",
|
|
62
|
-
"@tanstack/react-virtual": "^3.14.
|
|
62
|
+
"@tanstack/react-virtual": "^3.14.5",
|
|
63
63
|
"@tiptap/core": "3.27.1",
|
|
64
64
|
"@tiptap/extension-collaboration": "3.27.1",
|
|
65
65
|
"@tiptap/extension-collaboration-caret": "3.27.1",
|
|
@@ -88,9 +88,9 @@
|
|
|
88
88
|
"@tuturuuu/apis": "0.7.0",
|
|
89
89
|
"@tuturuuu/hooks": "0.0.2",
|
|
90
90
|
"@tuturuuu/icons": "0.0.6",
|
|
91
|
-
"@tuturuuu/internal-api": "0.
|
|
91
|
+
"@tuturuuu/internal-api": "0.13.0",
|
|
92
92
|
"@tuturuuu/supabase": "0.4.0",
|
|
93
|
-
"@tuturuuu/utils": "0.
|
|
93
|
+
"@tuturuuu/utils": "0.12.0",
|
|
94
94
|
"@types/debug": "^4.1.13",
|
|
95
95
|
"browser-image-compression": "^2.0.2",
|
|
96
96
|
"class-variance-authority": "^0.7.1",
|
|
@@ -102,18 +102,18 @@
|
|
|
102
102
|
"debug": "^4.4.3",
|
|
103
103
|
"embla-carousel-react": "^8.6.0",
|
|
104
104
|
"eventemitter3": "^5.0.4",
|
|
105
|
-
"framer-motion": "^12.
|
|
105
|
+
"framer-motion": "^12.42.2",
|
|
106
106
|
"gsap": "^3.15.0",
|
|
107
|
-
"html2canvas-pro": "^2.
|
|
107
|
+
"html2canvas-pro": "^2.2.1",
|
|
108
108
|
"input-otp": "^1.4.2",
|
|
109
109
|
"katex": "^0.17.0",
|
|
110
110
|
"lodash": "4.18.1",
|
|
111
111
|
"moment": "^2.30.1",
|
|
112
|
-
"motion": "^12.
|
|
113
|
-
"next": "^16.
|
|
114
|
-
"next-intl": "^4.13.
|
|
112
|
+
"motion": "^12.42.2",
|
|
113
|
+
"next": "^16.3.0-preview.5",
|
|
114
|
+
"next-intl": "^4.13.1",
|
|
115
115
|
"next-themes": "^0.4.6",
|
|
116
|
-
"nuqs": "^2.
|
|
116
|
+
"nuqs": "^2.9.0",
|
|
117
117
|
"prosemirror-state": "^1.4.4",
|
|
118
118
|
"qrcode.react": "^4.2.0",
|
|
119
119
|
"react": "^19.2.7",
|
|
@@ -126,9 +126,9 @@
|
|
|
126
126
|
"react-papaparse": "^4.4.0",
|
|
127
127
|
"react-pdf": "^10.4.1",
|
|
128
128
|
"react-resizable": "^4.0.2",
|
|
129
|
-
"react-resizable-panels": "^4.
|
|
129
|
+
"react-resizable-panels": "^4.12.0",
|
|
130
130
|
"react-syntax-highlighter": "^16.1.1",
|
|
131
|
-
"recharts": "3.9.
|
|
131
|
+
"recharts": "3.9.1",
|
|
132
132
|
"sonner": "^2.0.7",
|
|
133
133
|
"streamdown": "^2.5.0",
|
|
134
134
|
"tailwind-scrollbar": "^4.0.2",
|
|
@@ -143,34 +143,34 @@
|
|
|
143
143
|
"zod": "^4.4.3"
|
|
144
144
|
},
|
|
145
145
|
"devDependencies": {
|
|
146
|
-
"@tailwindcss/postcss": "^4.3.
|
|
146
|
+
"@tailwindcss/postcss": "^4.3.2",
|
|
147
147
|
"@tailwindcss/typography": "^0.5.20",
|
|
148
|
-
"@tanstack/react-query": "^5.101.
|
|
148
|
+
"@tanstack/react-query": "^5.101.2",
|
|
149
149
|
"@tanstack/react-table": "^8.21.3",
|
|
150
150
|
"@testing-library/jest-dom": "^6.9.1",
|
|
151
151
|
"@testing-library/react": "^16.3.2",
|
|
152
|
-
"@tuturuuu/types": "0.13.
|
|
152
|
+
"@tuturuuu/types": "0.13.1",
|
|
153
153
|
"@tuturuuu/typescript-config": "0.1.1",
|
|
154
154
|
"@types/html2canvas": "^1.0.0",
|
|
155
155
|
"@types/lodash": "^4.17.24",
|
|
156
|
-
"@types/node": "^26.0.
|
|
156
|
+
"@types/node": "^26.0.1",
|
|
157
157
|
"@types/react": "^19.2.17",
|
|
158
158
|
"@types/react-dom": "^19.2.3",
|
|
159
159
|
"@types/react-resizable": "^4.0.0",
|
|
160
160
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
161
161
|
"@types/uuid": "^11.0.0",
|
|
162
162
|
"@vitejs/plugin-react": "^6.0.3",
|
|
163
|
-
"autoprefixer": "^10.5.
|
|
163
|
+
"autoprefixer": "^10.5.2",
|
|
164
164
|
"jsdom": "^29.1.1",
|
|
165
|
-
"postcss": "^8.5.
|
|
165
|
+
"postcss": "^8.5.16",
|
|
166
166
|
"postcss-load-config": "^6.0.1",
|
|
167
|
-
"tailwindcss": "^4.3.
|
|
167
|
+
"tailwindcss": "^4.3.2",
|
|
168
168
|
"typescript": "7.0.1-rc",
|
|
169
169
|
"uuid": "^14.0.1",
|
|
170
170
|
"vitest": "^4.1.9"
|
|
171
171
|
},
|
|
172
172
|
"peerDependencies": {
|
|
173
|
-
"@tanstack/react-query": "^5.101.
|
|
173
|
+
"@tanstack/react-query": "^5.101.2",
|
|
174
174
|
"@tanstack/react-table": "^8.21.3"
|
|
175
175
|
},
|
|
176
176
|
"exports": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { generateHTML, generateJSON } from '@tiptap/core';
|
|
2
2
|
import type SupabaseProvider from '@tuturuuu/ui/hooks/supabase-provider';
|
|
3
|
+
import { parseTaskDescriptionInput } from '@tuturuuu/utils/task-description-codec';
|
|
3
4
|
import { describe, expect, it } from 'vitest';
|
|
4
5
|
import * as Y from 'yjs';
|
|
5
6
|
import { getEditorExtensions } from '../extensions';
|
|
@@ -157,6 +158,27 @@ describe('text editor extensions', () => {
|
|
|
157
158
|
expect(html).toContain('#FFF59D');
|
|
158
159
|
});
|
|
159
160
|
|
|
161
|
+
it('renders shared markdown-codec table JSON with editor table extensions', () => {
|
|
162
|
+
const extensions = getEditorExtensions({ readOnly: true });
|
|
163
|
+
const content = parseTaskDescriptionInput(
|
|
164
|
+
['| Field | Value |', '| --- | --- |', '| Owner | Platform |'].join('\n'),
|
|
165
|
+
'markdown'
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const html = generateHTML(content, extensions);
|
|
169
|
+
|
|
170
|
+
expect(html).toContain('<table');
|
|
171
|
+
expect(html).toContain('<th');
|
|
172
|
+
expect(html).toContain('<td');
|
|
173
|
+
expect(html).toContain('Platform');
|
|
174
|
+
|
|
175
|
+
const parsed = generateJSON(html, extensions);
|
|
176
|
+
expect(parsed.content?.[0]?.type).toBe('table');
|
|
177
|
+
expect(parsed.content?.[0]?.content?.[0]?.content?.[0]?.type).toBe(
|
|
178
|
+
'tableHeader'
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
160
182
|
it('round-trips task mention workspace metadata through HTML attrs', () => {
|
|
161
183
|
const renderOutput = (Mention.config as any).renderHTML({
|
|
162
184
|
HTMLAttributes: {
|
|
@@ -5,7 +5,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
|
|
|
5
5
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
|
|
6
6
|
import { cn } from '@tuturuuu/utils/format';
|
|
7
7
|
import { useTranslations } from 'next-intl';
|
|
8
|
-
import { useMemo, useState } from 'react';
|
|
8
|
+
import { type ComponentProps, useMemo, useState } from 'react';
|
|
9
9
|
import { CreateListDialog } from '../../create-list-dialog';
|
|
10
10
|
import { translateTaskListNameForDisplay } from '../../utils/translate-task-list-display-name';
|
|
11
11
|
import { TaskListPickerPanel } from './task-list-picker-panel';
|
|
@@ -23,7 +23,14 @@ interface TaskListSelectorProps {
|
|
|
23
23
|
compact?: boolean;
|
|
24
24
|
open?: boolean;
|
|
25
25
|
onOpenChange?: (open: boolean) => void;
|
|
26
|
+
onPopoverCloseAutoFocus?: ComponentProps<
|
|
27
|
+
typeof PopoverContent
|
|
28
|
+
>['onCloseAutoFocus'];
|
|
29
|
+
onPopoverInteractOutside?: ComponentProps<
|
|
30
|
+
typeof PopoverContent
|
|
31
|
+
>['onInteractOutside'];
|
|
26
32
|
onListChange: (listId: string) => void;
|
|
33
|
+
propertyPopoverId?: string;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export function TaskListSelector({
|
|
@@ -35,7 +42,10 @@ export function TaskListSelector({
|
|
|
35
42
|
compact = false,
|
|
36
43
|
open,
|
|
37
44
|
onOpenChange,
|
|
45
|
+
onPopoverCloseAutoFocus,
|
|
46
|
+
onPopoverInteractOutside,
|
|
38
47
|
onListChange,
|
|
48
|
+
propertyPopoverId,
|
|
39
49
|
}: TaskListSelectorProps) {
|
|
40
50
|
const t = useTranslations();
|
|
41
51
|
const [uncontrolledPopoverOpen, setUncontrolledPopoverOpen] = useState(false);
|
|
@@ -80,6 +90,7 @@ export function TaskListSelector({
|
|
|
80
90
|
const triggerButton = (
|
|
81
91
|
<button
|
|
82
92
|
type="button"
|
|
93
|
+
data-task-property-popover-trigger={propertyPopoverId}
|
|
83
94
|
disabled={disabled}
|
|
84
95
|
aria-label={compact ? triggerLabel : undefined}
|
|
85
96
|
className={cn(
|
|
@@ -111,7 +122,12 @@ export function TaskListSelector({
|
|
|
111
122
|
) : (
|
|
112
123
|
<PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
|
|
113
124
|
)}
|
|
114
|
-
<PopoverContent
|
|
125
|
+
<PopoverContent
|
|
126
|
+
align="start"
|
|
127
|
+
className="w-80 p-0"
|
|
128
|
+
onCloseAutoFocus={onPopoverCloseAutoFocus}
|
|
129
|
+
onInteractOutside={onPopoverInteractOutside}
|
|
130
|
+
>
|
|
115
131
|
<TaskListPickerPanel
|
|
116
132
|
selectedListId={selectedListId}
|
|
117
133
|
availableLists={availableLists}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import '@testing-library/jest-dom';
|
|
6
6
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
7
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
7
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
8
8
|
import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
|
|
9
|
+
import type { ComponentProps } from 'react';
|
|
9
10
|
import { describe, expect, it, vi } from 'vitest';
|
|
10
11
|
import { TaskPropertiesSection } from './task-properties-section';
|
|
11
12
|
|
|
@@ -25,7 +26,9 @@ vi.mock('@tuturuuu/ui/hooks/use-calendar-preferences', () => ({
|
|
|
25
26
|
}),
|
|
26
27
|
}));
|
|
27
28
|
|
|
28
|
-
function renderTaskPropertiesSection(
|
|
29
|
+
function renderTaskPropertiesSection(
|
|
30
|
+
overrides: Partial<ComponentProps<typeof TaskPropertiesSection>> = {}
|
|
31
|
+
) {
|
|
29
32
|
const props = {
|
|
30
33
|
wsId: 'ws-1',
|
|
31
34
|
boardId: 'board-1',
|
|
@@ -72,7 +75,13 @@ function renderTaskPropertiesSection() {
|
|
|
72
75
|
created_at: '2026-01-01T00:00:00.000Z',
|
|
73
76
|
},
|
|
74
77
|
],
|
|
75
|
-
taskProjects: [
|
|
78
|
+
taskProjects: [
|
|
79
|
+
{
|
|
80
|
+
id: 'project-1',
|
|
81
|
+
name: 'Launch',
|
|
82
|
+
status: 'active',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
76
85
|
workspaceMembers: [
|
|
77
86
|
{
|
|
78
87
|
id: 'user-1',
|
|
@@ -105,6 +114,7 @@ function renderTaskPropertiesSection() {
|
|
|
105
114
|
disabled: false,
|
|
106
115
|
isDraftMode: false,
|
|
107
116
|
variant: 'compact' as const,
|
|
117
|
+
...overrides,
|
|
108
118
|
};
|
|
109
119
|
|
|
110
120
|
const queryClient = new QueryClient({
|
|
@@ -124,13 +134,18 @@ function renderTaskPropertiesSection() {
|
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
describe('TaskPropertiesSection', () => {
|
|
127
|
-
it('keeps only one property popover open at a time', () => {
|
|
137
|
+
it('keeps only one property popover open at a time', async () => {
|
|
128
138
|
renderTaskPropertiesSection();
|
|
129
139
|
|
|
130
140
|
fireEvent.click(screen.getByLabelText('common.priority'));
|
|
131
141
|
expect(screen.getByText('tasks.priority_critical')).toBeInTheDocument();
|
|
132
142
|
|
|
133
143
|
fireEvent.click(screen.getByLabelText('common.labels'));
|
|
144
|
+
await waitFor(() =>
|
|
145
|
+
expect(
|
|
146
|
+
screen.queryByText('tasks.priority_critical')
|
|
147
|
+
).not.toBeInTheDocument()
|
|
148
|
+
);
|
|
134
149
|
expect(
|
|
135
150
|
screen.queryByText('tasks.priority_critical')
|
|
136
151
|
).not.toBeInTheDocument();
|
|
@@ -139,6 +154,11 @@ describe('TaskPropertiesSection', () => {
|
|
|
139
154
|
).toBeInTheDocument();
|
|
140
155
|
|
|
141
156
|
fireEvent.click(screen.getByLabelText('common.list_name_to_do'));
|
|
157
|
+
await waitFor(() =>
|
|
158
|
+
expect(
|
|
159
|
+
screen.queryByPlaceholderText('common.search_labels')
|
|
160
|
+
).not.toBeInTheDocument()
|
|
161
|
+
);
|
|
142
162
|
expect(
|
|
143
163
|
screen.queryByPlaceholderText('common.search_labels')
|
|
144
164
|
).not.toBeInTheDocument();
|
|
@@ -147,4 +167,137 @@ describe('TaskPropertiesSection', () => {
|
|
|
147
167
|
'true'
|
|
148
168
|
);
|
|
149
169
|
});
|
|
170
|
+
|
|
171
|
+
it('switches from priority to dates without closing the target popover', async () => {
|
|
172
|
+
renderTaskPropertiesSection();
|
|
173
|
+
|
|
174
|
+
fireEvent.click(screen.getByLabelText('common.priority'));
|
|
175
|
+
expect(screen.getByText('tasks.priority_critical')).toBeInTheDocument();
|
|
176
|
+
|
|
177
|
+
fireEvent.click(screen.getByLabelText('ws-task-boards.dialog.dates'));
|
|
178
|
+
|
|
179
|
+
await waitFor(() =>
|
|
180
|
+
expect(
|
|
181
|
+
screen.getByText('ws-task-boards.dialog.start_date')
|
|
182
|
+
).toBeInTheDocument()
|
|
183
|
+
);
|
|
184
|
+
expect(
|
|
185
|
+
screen.getByText('ws-task-boards.dialog.due_date')
|
|
186
|
+
).toBeInTheDocument();
|
|
187
|
+
expect(
|
|
188
|
+
screen.queryByText('tasks.priority_critical')
|
|
189
|
+
).not.toBeInTheDocument();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it.each([
|
|
193
|
+
{
|
|
194
|
+
triggerLabel: 'common.labels',
|
|
195
|
+
targetPlaceholder: 'common.search_labels',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
triggerLabel: 'common.projects',
|
|
199
|
+
targetPlaceholder: 'common.search_projects',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
triggerLabel: 'common.assignees',
|
|
203
|
+
targetPlaceholder: 'common.search_members',
|
|
204
|
+
},
|
|
205
|
+
])('switches from priority to $triggerLabel and keeps the target popover open', async ({
|
|
206
|
+
targetPlaceholder,
|
|
207
|
+
triggerLabel,
|
|
208
|
+
}) => {
|
|
209
|
+
renderTaskPropertiesSection();
|
|
210
|
+
|
|
211
|
+
fireEvent.click(screen.getByLabelText('common.priority'));
|
|
212
|
+
expect(screen.getByText('tasks.priority_critical')).toBeInTheDocument();
|
|
213
|
+
|
|
214
|
+
fireEvent.click(screen.getByLabelText(triggerLabel));
|
|
215
|
+
|
|
216
|
+
await waitFor(() =>
|
|
217
|
+
expect(screen.getByPlaceholderText(targetPlaceholder)).toBeInTheDocument()
|
|
218
|
+
);
|
|
219
|
+
expect(
|
|
220
|
+
screen.queryByText('tasks.priority_critical')
|
|
221
|
+
).not.toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('closes the priority popover after selecting a priority', () => {
|
|
225
|
+
const props = renderTaskPropertiesSection();
|
|
226
|
+
|
|
227
|
+
fireEvent.click(screen.getByLabelText('common.priority'));
|
|
228
|
+
fireEvent.click(screen.getByText('tasks.priority_high'));
|
|
229
|
+
|
|
230
|
+
expect(props.onPriorityChange).toHaveBeenCalledWith('high');
|
|
231
|
+
expect(
|
|
232
|
+
screen.queryByText('tasks.priority_critical')
|
|
233
|
+
).not.toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('keeps labels popover open after toggling a label', () => {
|
|
237
|
+
const props = renderTaskPropertiesSection();
|
|
238
|
+
|
|
239
|
+
fireEvent.click(screen.getByLabelText('common.labels'));
|
|
240
|
+
fireEvent.click(screen.getByText('Bug'));
|
|
241
|
+
|
|
242
|
+
expect(props.onLabelToggle).toHaveBeenCalledWith(
|
|
243
|
+
expect.objectContaining({ id: 'label-1' })
|
|
244
|
+
);
|
|
245
|
+
expect(
|
|
246
|
+
screen.getByPlaceholderText('common.search_labels')
|
|
247
|
+
).toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('keeps projects popover open after toggling a project', () => {
|
|
251
|
+
const props = renderTaskPropertiesSection();
|
|
252
|
+
|
|
253
|
+
fireEvent.click(screen.getByLabelText('common.projects'));
|
|
254
|
+
fireEvent.click(screen.getByText('Launch'));
|
|
255
|
+
|
|
256
|
+
expect(props.onProjectToggle).toHaveBeenCalledWith(
|
|
257
|
+
expect.objectContaining({ id: 'project-1' })
|
|
258
|
+
);
|
|
259
|
+
expect(
|
|
260
|
+
screen.getByPlaceholderText('common.search_projects')
|
|
261
|
+
).toBeInTheDocument();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('keeps assignees popover open after toggling an assignee', () => {
|
|
265
|
+
const props = renderTaskPropertiesSection();
|
|
266
|
+
|
|
267
|
+
fireEvent.click(screen.getByLabelText('common.assignees'));
|
|
268
|
+
fireEvent.click(screen.getByText('Taylor'));
|
|
269
|
+
|
|
270
|
+
expect(props.onAssigneeToggle).toHaveBeenCalledWith(
|
|
271
|
+
expect.objectContaining({ user_id: 'user-1' })
|
|
272
|
+
);
|
|
273
|
+
expect(
|
|
274
|
+
screen.getByPlaceholderText('common.search_members')
|
|
275
|
+
).toBeInTheDocument();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('closes the active popover when clicking outside', async () => {
|
|
279
|
+
renderTaskPropertiesSection();
|
|
280
|
+
|
|
281
|
+
fireEvent.click(screen.getByLabelText('common.labels'));
|
|
282
|
+
await waitFor(() =>
|
|
283
|
+
expect(
|
|
284
|
+
screen.getByPlaceholderText('common.search_labels')
|
|
285
|
+
).toBeInTheDocument()
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
289
|
+
fireEvent.pointerDown(document.body, {
|
|
290
|
+
button: 0,
|
|
291
|
+
ctrlKey: false,
|
|
292
|
+
pointerType: 'mouse',
|
|
293
|
+
});
|
|
294
|
+
fireEvent.mouseDown(document.body, { button: 0, ctrlKey: false });
|
|
295
|
+
fireEvent.click(document.body);
|
|
296
|
+
|
|
297
|
+
await waitFor(() =>
|
|
298
|
+
expect(
|
|
299
|
+
screen.queryByPlaceholderText('common.search_labels')
|
|
300
|
+
).not.toBeInTheDocument()
|
|
301
|
+
);
|
|
302
|
+
});
|
|
150
303
|
});
|
|
@@ -42,6 +42,10 @@ import { computeAccessibleLabelStyles } from '@tuturuuu/utils/label-colors';
|
|
|
42
42
|
import dayjs from 'dayjs';
|
|
43
43
|
import { useTranslations } from 'next-intl';
|
|
44
44
|
import {
|
|
45
|
+
type ComponentProps,
|
|
46
|
+
cloneElement,
|
|
47
|
+
isValidElement,
|
|
48
|
+
type ReactElement,
|
|
45
49
|
type ReactNode,
|
|
46
50
|
useCallback,
|
|
47
51
|
useEffect,
|
|
@@ -162,29 +166,75 @@ type TaskPropertyPopoverId =
|
|
|
162
166
|
| 'assignees'
|
|
163
167
|
| 'scheduling';
|
|
164
168
|
|
|
169
|
+
const TASK_PROPERTY_POPOVER_TRIGGER_ATTRIBUTE =
|
|
170
|
+
'data-task-property-popover-trigger';
|
|
171
|
+
|
|
172
|
+
const taskPropertyPopoverIds = new Set<TaskPropertyPopoverId>([
|
|
173
|
+
'priority',
|
|
174
|
+
'list',
|
|
175
|
+
'dates',
|
|
176
|
+
'estimation',
|
|
177
|
+
'labels',
|
|
178
|
+
'projects',
|
|
179
|
+
'assignees',
|
|
180
|
+
'scheduling',
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
function isTaskPropertyPopoverId(
|
|
184
|
+
value: string | null
|
|
185
|
+
): value is TaskPropertyPopoverId {
|
|
186
|
+
return !!value && taskPropertyPopoverIds.has(value as TaskPropertyPopoverId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getTaskPropertyPopoverIdFromTarget(
|
|
190
|
+
target: EventTarget | null
|
|
191
|
+
): TaskPropertyPopoverId | null {
|
|
192
|
+
if (typeof Element === 'undefined' || !(target instanceof Element)) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const trigger = target.closest(
|
|
197
|
+
`[${TASK_PROPERTY_POPOVER_TRIGGER_ATTRIBUTE}]`
|
|
198
|
+
);
|
|
199
|
+
const popoverId =
|
|
200
|
+
trigger?.getAttribute(TASK_PROPERTY_POPOVER_TRIGGER_ATTRIBUTE) ?? null;
|
|
201
|
+
|
|
202
|
+
return isTaskPropertyPopoverId(popoverId) ? popoverId : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
165
205
|
function TaskPropertyPopoverTrigger({
|
|
166
206
|
children,
|
|
167
207
|
compact,
|
|
168
208
|
label,
|
|
209
|
+
popoverId,
|
|
169
210
|
}: {
|
|
170
211
|
children: ReactNode;
|
|
171
212
|
compact: boolean;
|
|
172
213
|
label: ReactNode;
|
|
214
|
+
popoverId: TaskPropertyPopoverId;
|
|
173
215
|
}) {
|
|
216
|
+
const trigger = isValidElement(children)
|
|
217
|
+
? cloneElement(children as ReactElement<Record<string, unknown>>, {
|
|
218
|
+
[TASK_PROPERTY_POPOVER_TRIGGER_ATTRIBUTE]: popoverId,
|
|
219
|
+
})
|
|
220
|
+
: children;
|
|
221
|
+
|
|
174
222
|
if (!compact) {
|
|
175
|
-
return <PopoverTrigger asChild>{
|
|
223
|
+
return <PopoverTrigger asChild>{trigger}</PopoverTrigger>;
|
|
176
224
|
}
|
|
177
225
|
|
|
178
226
|
return (
|
|
179
227
|
<Tooltip>
|
|
180
228
|
<TooltipTrigger asChild>
|
|
181
|
-
<PopoverTrigger asChild>{
|
|
229
|
+
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
182
230
|
</TooltipTrigger>
|
|
183
231
|
<TooltipContent side="bottom">{label}</TooltipContent>
|
|
184
232
|
</Tooltip>
|
|
185
233
|
);
|
|
186
234
|
}
|
|
187
235
|
|
|
236
|
+
type TaskPropertyPopoverContentProps = ComponentProps<typeof PopoverContent>;
|
|
237
|
+
|
|
188
238
|
// Calendar hours type options
|
|
189
239
|
const getCalendarHoursOptions = (t: any) => [
|
|
190
240
|
{
|
|
@@ -422,16 +472,92 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
422
472
|
[activePopover]
|
|
423
473
|
);
|
|
424
474
|
|
|
475
|
+
const openPropertyPopover = useCallback(
|
|
476
|
+
(popoverId: TaskPropertyPopoverId) => {
|
|
477
|
+
setActivePopover(popoverId);
|
|
478
|
+
},
|
|
479
|
+
[]
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const closePropertyPopover = useCallback(
|
|
483
|
+
(popoverId: TaskPropertyPopoverId) => {
|
|
484
|
+
setActivePopover((currentPopover) =>
|
|
485
|
+
currentPopover === popoverId ? null : currentPopover
|
|
486
|
+
);
|
|
487
|
+
},
|
|
488
|
+
[]
|
|
489
|
+
);
|
|
490
|
+
|
|
425
491
|
const setPopoverOpen = useCallback(
|
|
426
492
|
(popoverId: TaskPropertyPopoverId, open: boolean) => {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return
|
|
430
|
-
}
|
|
493
|
+
if (open) {
|
|
494
|
+
openPropertyPopover(popoverId);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
closePropertyPopover(popoverId);
|
|
431
499
|
},
|
|
432
|
-
[]
|
|
500
|
+
[closePropertyPopover, openPropertyPopover]
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const handlePropertyPopoverCloseAutoFocus = useCallback<
|
|
504
|
+
NonNullable<TaskPropertyPopoverContentProps['onCloseAutoFocus']>
|
|
505
|
+
>((event) => {
|
|
506
|
+
event.preventDefault();
|
|
507
|
+
}, []);
|
|
508
|
+
|
|
509
|
+
const handlePropertyPopoverInteractOutside = useCallback<
|
|
510
|
+
NonNullable<TaskPropertyPopoverContentProps['onInteractOutside']>
|
|
511
|
+
>(
|
|
512
|
+
(event) => {
|
|
513
|
+
const targetPopoverId = getTaskPropertyPopoverIdFromTarget(event.target);
|
|
514
|
+
|
|
515
|
+
if (!targetPopoverId || targetPopoverId === activePopover) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
event.preventDefault();
|
|
520
|
+
|
|
521
|
+
const openTargetPopover = () => {
|
|
522
|
+
openPropertyPopover(targetPopoverId);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
if (typeof window !== 'undefined' && window.requestAnimationFrame) {
|
|
526
|
+
window.requestAnimationFrame(openTargetPopover);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
openTargetPopover();
|
|
531
|
+
},
|
|
532
|
+
[activePopover, openPropertyPopover]
|
|
433
533
|
);
|
|
434
534
|
|
|
535
|
+
const propertyPopoverContentProps = useMemo(
|
|
536
|
+
() => ({
|
|
537
|
+
onCloseAutoFocus: handlePropertyPopoverCloseAutoFocus,
|
|
538
|
+
onInteractOutside: handlePropertyPopoverInteractOutside,
|
|
539
|
+
}),
|
|
540
|
+
[handlePropertyPopoverCloseAutoFocus, handlePropertyPopoverInteractOutside]
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
useEffect(() => {
|
|
544
|
+
if (activePopover !== 'labels') {
|
|
545
|
+
setLabelSearchQuery('');
|
|
546
|
+
}
|
|
547
|
+
}, [activePopover]);
|
|
548
|
+
|
|
549
|
+
useEffect(() => {
|
|
550
|
+
if (activePopover !== 'projects') {
|
|
551
|
+
setProjectSearchQuery('');
|
|
552
|
+
}
|
|
553
|
+
}, [activePopover]);
|
|
554
|
+
|
|
555
|
+
useEffect(() => {
|
|
556
|
+
if (activePopover !== 'assignees') {
|
|
557
|
+
setAssigneeSearchQuery('');
|
|
558
|
+
}
|
|
559
|
+
}, [activePopover]);
|
|
560
|
+
|
|
435
561
|
const unselectedAvailableLabels = useMemo(
|
|
436
562
|
() =>
|
|
437
563
|
availableLabels.filter(
|
|
@@ -903,6 +1029,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
903
1029
|
? t(`tasks.priority_${priority}`)
|
|
904
1030
|
: t('common.priority')
|
|
905
1031
|
}
|
|
1032
|
+
popoverId="priority"
|
|
906
1033
|
>
|
|
907
1034
|
<button
|
|
908
1035
|
type="button"
|
|
@@ -932,7 +1059,11 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
932
1059
|
</span>
|
|
933
1060
|
</button>
|
|
934
1061
|
</TaskPropertyPopoverTrigger>
|
|
935
|
-
<PopoverContent
|
|
1062
|
+
<PopoverContent
|
|
1063
|
+
align="start"
|
|
1064
|
+
className="w-56 p-0"
|
|
1065
|
+
{...propertyPopoverContentProps}
|
|
1066
|
+
>
|
|
936
1067
|
<div className="p-1">
|
|
937
1068
|
{[
|
|
938
1069
|
{
|
|
@@ -1000,7 +1131,10 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1000
1131
|
compact={isCompact}
|
|
1001
1132
|
open={isPopoverOpen('list')}
|
|
1002
1133
|
onOpenChange={(open) => setPopoverOpen('list', open)}
|
|
1134
|
+
onPopoverCloseAutoFocus={handlePropertyPopoverCloseAutoFocus}
|
|
1135
|
+
onPopoverInteractOutside={handlePropertyPopoverInteractOutside}
|
|
1003
1136
|
onListChange={onListChange}
|
|
1137
|
+
propertyPopoverId="list"
|
|
1004
1138
|
/>
|
|
1005
1139
|
|
|
1006
1140
|
{/* Dates Badge */}
|
|
@@ -1015,6 +1149,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1015
1149
|
? `${startDate ? new Date(startDate).toLocaleDateString(t('common.locale', { defaultValue: 'en-US' }), { month: 'short', day: 'numeric' }) : t('ws-task-boards.dialog.no_start_date')} → ${endDate ? new Date(endDate).toLocaleDateString(t('common.locale', { defaultValue: 'en-US' }), { month: 'short', day: 'numeric' }) : t('ws-task-boards.dialog.no_due_date')}`
|
|
1016
1150
|
: t('ws-task-boards.dialog.dates')
|
|
1017
1151
|
}
|
|
1152
|
+
popoverId="dates"
|
|
1018
1153
|
>
|
|
1019
1154
|
<button
|
|
1020
1155
|
type="button"
|
|
@@ -1036,7 +1171,11 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1036
1171
|
</span>
|
|
1037
1172
|
</button>
|
|
1038
1173
|
</TaskPropertyPopoverTrigger>
|
|
1039
|
-
<PopoverContent
|
|
1174
|
+
<PopoverContent
|
|
1175
|
+
align="start"
|
|
1176
|
+
className="w-80 p-0"
|
|
1177
|
+
{...propertyPopoverContentProps}
|
|
1178
|
+
>
|
|
1040
1179
|
<div className="rounded-lg p-3.5">
|
|
1041
1180
|
<div className="space-y-3">
|
|
1042
1181
|
{/* Start Date */}
|
|
@@ -1165,6 +1304,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1165
1304
|
)
|
|
1166
1305
|
: t('ws-task-boards.dialog.estimate')
|
|
1167
1306
|
}
|
|
1307
|
+
popoverId="estimation"
|
|
1168
1308
|
>
|
|
1169
1309
|
<button
|
|
1170
1310
|
type="button"
|
|
@@ -1191,7 +1331,11 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1191
1331
|
</span>
|
|
1192
1332
|
</button>
|
|
1193
1333
|
</TaskPropertyPopoverTrigger>
|
|
1194
|
-
<PopoverContent
|
|
1334
|
+
<PopoverContent
|
|
1335
|
+
align="start"
|
|
1336
|
+
className="w-64 p-0"
|
|
1337
|
+
{...propertyPopoverContentProps}
|
|
1338
|
+
>
|
|
1195
1339
|
{!boardConfig?.estimation_type ? (
|
|
1196
1340
|
<EmptyStateCard
|
|
1197
1341
|
title={t('ws-task-boards.dialog.no_estimation_configured')}
|
|
@@ -1265,6 +1409,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1265
1409
|
count: selectedLabels.length,
|
|
1266
1410
|
})
|
|
1267
1411
|
}
|
|
1412
|
+
popoverId="labels"
|
|
1268
1413
|
>
|
|
1269
1414
|
<button
|
|
1270
1415
|
type="button"
|
|
@@ -1290,7 +1435,11 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1290
1435
|
</span>
|
|
1291
1436
|
</button>
|
|
1292
1437
|
</TaskPropertyPopoverTrigger>
|
|
1293
|
-
<PopoverContent
|
|
1438
|
+
<PopoverContent
|
|
1439
|
+
align="start"
|
|
1440
|
+
className="w-72 p-0"
|
|
1441
|
+
{...propertyPopoverContentProps}
|
|
1442
|
+
>
|
|
1294
1443
|
{availableLabels.length === 0 ? (
|
|
1295
1444
|
<EmptyStateCard
|
|
1296
1445
|
title={t('ws-task-boards.dialog.no_labels_configured')}
|
|
@@ -1415,6 +1564,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1415
1564
|
count: selectedProjects.length,
|
|
1416
1565
|
})
|
|
1417
1566
|
}
|
|
1567
|
+
popoverId="projects"
|
|
1418
1568
|
>
|
|
1419
1569
|
<button
|
|
1420
1570
|
type="button"
|
|
@@ -1440,7 +1590,11 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1440
1590
|
</span>
|
|
1441
1591
|
</button>
|
|
1442
1592
|
</TaskPropertyPopoverTrigger>
|
|
1443
|
-
<PopoverContent
|
|
1593
|
+
<PopoverContent
|
|
1594
|
+
align="start"
|
|
1595
|
+
className="w-72 p-0"
|
|
1596
|
+
{...propertyPopoverContentProps}
|
|
1597
|
+
>
|
|
1444
1598
|
{taskProjects.length === 0 ? (
|
|
1445
1599
|
<EmptyStateCard
|
|
1446
1600
|
title={t('ws-task-boards.dialog.no_projects_configured')}
|
|
@@ -1556,6 +1710,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1556
1710
|
count: selectedAssignees.length,
|
|
1557
1711
|
})
|
|
1558
1712
|
}
|
|
1713
|
+
popoverId="assignees"
|
|
1559
1714
|
>
|
|
1560
1715
|
<button
|
|
1561
1716
|
type="button"
|
|
@@ -1582,7 +1737,11 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1582
1737
|
</span>
|
|
1583
1738
|
</button>
|
|
1584
1739
|
</TaskPropertyPopoverTrigger>
|
|
1585
|
-
<PopoverContent
|
|
1740
|
+
<PopoverContent
|
|
1741
|
+
align="start"
|
|
1742
|
+
className="w-72 p-0"
|
|
1743
|
+
{...propertyPopoverContentProps}
|
|
1744
|
+
>
|
|
1586
1745
|
{workspaceMembers.length === 0 ? (
|
|
1587
1746
|
<div className="p-4 text-center text-muted-foreground text-sm">
|
|
1588
1747
|
{t('ws-task-boards.dialog.no_members_found')}
|
|
@@ -1679,6 +1838,7 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1679
1838
|
? formatDuration(totalMinutes, t)
|
|
1680
1839
|
: t('ws-task-boards.dialog.schedule')
|
|
1681
1840
|
}
|
|
1841
|
+
popoverId="scheduling"
|
|
1682
1842
|
>
|
|
1683
1843
|
<button
|
|
1684
1844
|
type="button"
|
|
@@ -1711,7 +1871,11 @@ export function TaskPropertiesSection(props: TaskPropertiesSectionProps) {
|
|
|
1711
1871
|
)}
|
|
1712
1872
|
</button>
|
|
1713
1873
|
</TaskPropertyPopoverTrigger>
|
|
1714
|
-
<PopoverContent
|
|
1874
|
+
<PopoverContent
|
|
1875
|
+
align="start"
|
|
1876
|
+
className="w-72 p-0"
|
|
1877
|
+
{...propertyPopoverContentProps}
|
|
1878
|
+
>
|
|
1715
1879
|
<div className="rounded-lg p-3">
|
|
1716
1880
|
<div className="space-y-3">
|
|
1717
1881
|
{/* Duration */}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { render, waitFor } from '@testing-library/react';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { listWorkspaceTaskTemplates } from './task-template-api';
|
|
7
|
+
import { TaskTemplateClient } from './task-template-client';
|
|
8
|
+
|
|
9
|
+
vi.mock('next-intl', () => ({
|
|
10
|
+
useTranslations: () => (key: string, values?: Record<string, string>) =>
|
|
11
|
+
values?.count ? `${key}:${values.count}` : key,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('@tuturuuu/internal-api/tasks', () => ({
|
|
15
|
+
listWorkspaceTaskBoards: vi.fn(async () => ({ boards: [] })),
|
|
16
|
+
listWorkspaceTaskLists: vi.fn(async () => ({ lists: [] })),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('./task-template-api', () => ({
|
|
20
|
+
createWorkspaceTaskTemplate: vi.fn(),
|
|
21
|
+
deleteWorkspaceTaskTemplate: vi.fn(),
|
|
22
|
+
instantiateWorkspaceTaskTemplate: vi.fn(),
|
|
23
|
+
listWorkspaceTaskTemplates: vi.fn(async () => ({ templates: [] })),
|
|
24
|
+
saveWorkspaceTaskTemplateFromTask: vi.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
function renderWithQueryClient(ui: ReactNode) {
|
|
28
|
+
const queryClient = new QueryClient({
|
|
29
|
+
defaultOptions: {
|
|
30
|
+
queries: {
|
|
31
|
+
retry: false,
|
|
32
|
+
staleTime: 30_000,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return render(
|
|
38
|
+
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('TaskTemplateClient', () => {
|
|
43
|
+
it('fetches task templates even when the server shell starts empty', async () => {
|
|
44
|
+
renderWithQueryClient(
|
|
45
|
+
<TaskTemplateClient initialTemplates={[]} wsId="ws-1" />
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
await waitFor(() => {
|
|
49
|
+
expect(listWorkspaceTaskTemplates).toHaveBeenCalledWith('ws-1');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|