@tuturuuu/ui 0.8.0 → 0.9.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/biome.json +1 -1
- package/package.json +73 -71
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +1 -1
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
- package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
- package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
- package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/carousel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
- package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
- package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/command.tsx +1 -1
- package/src/components/ui/context-menu.tsx +5 -1
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
- package/src/components/ui/custom/combobox.test.tsx +195 -0
- package/src/components/ui/custom/combobox.tsx +273 -156
- package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
- package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
- package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
- package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
- package/src/components/ui/custom/theme-toggle.tsx +1 -1
- package/src/components/ui/custom/workspace-select.tsx +8 -3
- package/src/components/ui/dialog.test.tsx +52 -0
- package/src/components/ui/dialog.tsx +6 -2
- package/src/components/ui/dropdown-menu.tsx +5 -1
- package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
- package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
- package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
- package/src/components/ui/finance/debts/debts-page.tsx +15 -2
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
- package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
- package/src/components/ui/finance/transactions/form-types.ts +1 -0
- package/src/components/ui/finance/transactions/form.tsx +2 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
- package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
- package/src/components/ui/finance/wallets/form.test.tsx +51 -3
- package/src/components/ui/finance/wallets/form.tsx +15 -4
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
- package/src/components/ui/input-otp.tsx +1 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
- package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
- package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
- package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
- package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
- package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
- package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
- package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
- package/src/components/ui/navigation-menu.tsx +1 -1
- package/src/components/ui/pagination.tsx +1 -1
- package/src/components/ui/radio-group.tsx +1 -1
- package/src/components/ui/select.tsx +5 -1
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +61 -0
- package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
- package/src/components/ui/storefront/cart-summary.tsx +93 -154
- package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
- package/src/components/ui/storefront/listing-card.tsx +1 -1
- package/src/components/ui/storefront/merch-sections.tsx +70 -0
- package/src/components/ui/storefront/product-detail.tsx +1 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
- package/src/components/ui/storefront/storefront-surface.tsx +101 -166
- package/src/components/ui/storefront/types.ts +4 -0
- package/src/components/ui/storefront/utils.ts +6 -0
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
- package/src/components/ui/text-editor/background-color-extension.ts +62 -0
- package/src/components/ui/text-editor/color-controls.tsx +284 -0
- package/src/components/ui/text-editor/editor.tsx +69 -14
- package/src/components/ui/text-editor/extensions.ts +8 -2
- package/src/components/ui/text-editor/highlight-extension.ts +22 -0
- package/src/components/ui/text-editor/tool-bar.tsx +9 -16
- package/src/components/ui/toast.tsx +1 -1
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
- package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
- package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
- package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
- package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
- package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
- package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
- package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
- package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
- package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
- package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
- package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
- package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
- package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
- package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
- package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
- package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
- package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
- package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
- package/src/declarations.d.ts +1 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
- package/src/hooks/use-calendar-sync.tsx +247 -243
- package/src/hooks/use-calendar.tsx +323 -138
- package/src/hooks/use-task-actions.ts +24 -0
- package/src/hooks/use-user-workspace-config.ts +75 -0
- package/src/hooks/use-workspace-currency.ts +8 -3
- package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
|
@@ -4,7 +4,6 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
4
4
|
import {
|
|
5
5
|
Building2,
|
|
6
6
|
Calendar as CalendarIcon,
|
|
7
|
-
Check,
|
|
8
7
|
Filter,
|
|
9
8
|
Flag,
|
|
10
9
|
Globe2,
|
|
@@ -30,25 +29,17 @@ import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
|
|
|
30
29
|
import { Avatar, AvatarFallback, AvatarImage } from '@tuturuuu/ui/avatar';
|
|
31
30
|
import { Badge } from '@tuturuuu/ui/badge';
|
|
32
31
|
import { Button } from '@tuturuuu/ui/button';
|
|
33
|
-
import { Calendar } from '@tuturuuu/ui/calendar';
|
|
34
32
|
import { Checkbox } from '@tuturuuu/ui/checkbox';
|
|
35
|
-
import {
|
|
36
|
-
DropdownMenu,
|
|
37
|
-
DropdownMenuContent,
|
|
38
|
-
DropdownMenuItem,
|
|
39
|
-
DropdownMenuLabel,
|
|
40
|
-
DropdownMenuSeparator,
|
|
41
|
-
DropdownMenuSub,
|
|
42
|
-
DropdownMenuSubContent,
|
|
43
|
-
DropdownMenuSubTrigger,
|
|
44
|
-
DropdownMenuTrigger,
|
|
45
|
-
} from '@tuturuuu/ui/dropdown-menu';
|
|
33
|
+
import { Combobox } from '@tuturuuu/ui/custom/combobox';
|
|
46
34
|
import { useWorkspaceMembers } from '@tuturuuu/ui/hooks/use-workspace-members';
|
|
35
|
+
import { Input } from '@tuturuuu/ui/input';
|
|
36
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
|
|
47
37
|
import { ScrollArea } from '@tuturuuu/ui/scroll-area';
|
|
38
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
|
|
48
39
|
import { cn } from '@tuturuuu/utils/format';
|
|
49
40
|
import { getInitials } from '@tuturuuu/utils/name-helper';
|
|
50
41
|
import { useTranslations } from 'next-intl';
|
|
51
|
-
import { useMemo, useState } from 'react';
|
|
42
|
+
import { type ReactNode, useMemo, useState } from 'react';
|
|
52
43
|
import type {
|
|
53
44
|
SortOption,
|
|
54
45
|
TaskAssignee,
|
|
@@ -192,6 +183,85 @@ const PRIORITIES: { value: TaskPriority; labelKey: string; color: string }[] = [
|
|
|
192
183
|
{ value: 'low', labelKey: 'tasks.priority_low', color: 'text-dynamic-gray' },
|
|
193
184
|
];
|
|
194
185
|
|
|
186
|
+
function formatShortDate(date: Date) {
|
|
187
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
188
|
+
day: 'numeric',
|
|
189
|
+
month: 'short',
|
|
190
|
+
}).format(date);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatDateInputValue(date: Date | undefined) {
|
|
194
|
+
if (!date) return '';
|
|
195
|
+
|
|
196
|
+
const year = date.getFullYear();
|
|
197
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
198
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
199
|
+
|
|
200
|
+
return `${year}-${month}-${day}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseDateInputValue(value: string) {
|
|
204
|
+
if (!value) return undefined;
|
|
205
|
+
|
|
206
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
207
|
+
if (!year || !month || !day) return undefined;
|
|
208
|
+
|
|
209
|
+
return new Date(year, month - 1, day);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function FilterPickerField({
|
|
213
|
+
badge,
|
|
214
|
+
children,
|
|
215
|
+
icon,
|
|
216
|
+
label,
|
|
217
|
+
}: {
|
|
218
|
+
badge?: ReactNode;
|
|
219
|
+
children: ReactNode;
|
|
220
|
+
icon: ReactNode;
|
|
221
|
+
label: string;
|
|
222
|
+
}) {
|
|
223
|
+
return (
|
|
224
|
+
<div className="space-y-1.5">
|
|
225
|
+
<div className="flex min-w-0 items-center gap-2 text-muted-foreground text-xs">
|
|
226
|
+
{icon}
|
|
227
|
+
<span className="min-w-0 flex-1 truncate font-medium">{label}</span>
|
|
228
|
+
{badge}
|
|
229
|
+
</div>
|
|
230
|
+
{children}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function FilterSection({
|
|
236
|
+
badge,
|
|
237
|
+
children,
|
|
238
|
+
className,
|
|
239
|
+
icon,
|
|
240
|
+
testId,
|
|
241
|
+
title,
|
|
242
|
+
}: {
|
|
243
|
+
badge?: ReactNode;
|
|
244
|
+
children: ReactNode;
|
|
245
|
+
className?: string;
|
|
246
|
+
icon: ReactNode;
|
|
247
|
+
testId?: string;
|
|
248
|
+
title: string;
|
|
249
|
+
}) {
|
|
250
|
+
return (
|
|
251
|
+
<section
|
|
252
|
+
className={cn('rounded-md border bg-background p-2.5', className)}
|
|
253
|
+
data-testid={testId}
|
|
254
|
+
>
|
|
255
|
+
<div className="mb-2 flex min-w-0 items-center gap-2 text-muted-foreground text-xs">
|
|
256
|
+
{icon}
|
|
257
|
+
<span className="min-w-0 flex-1 truncate font-semibold">{title}</span>
|
|
258
|
+
{badge}
|
|
259
|
+
</div>
|
|
260
|
+
{children}
|
|
261
|
+
</section>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
195
265
|
export function TaskFilter({
|
|
196
266
|
wsId,
|
|
197
267
|
currentUserId,
|
|
@@ -320,59 +390,115 @@ export function TaskFilter({
|
|
|
320
390
|
staleTime: 60_000,
|
|
321
391
|
});
|
|
322
392
|
|
|
323
|
-
const
|
|
324
|
-
const
|
|
393
|
+
const sourceScopeOptions = SOURCE_SCOPE_OPTIONS.map((scope) => {
|
|
394
|
+
const Icon = SOURCE_SCOPE_ICONS[scope];
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
value: scope,
|
|
398
|
+
label: t(`ws-tasks.filter_source_scope_${scope}` as any),
|
|
399
|
+
icon: <Icon className="h-4 w-4 text-muted-foreground" />,
|
|
400
|
+
};
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const sourceWorkspaceOptions = sourceWorkspaces.map((workspace) => ({
|
|
404
|
+
value: workspace.id,
|
|
405
|
+
label: workspace.name ?? workspace.id,
|
|
406
|
+
icon: <Building2 className="h-4 w-4 text-muted-foreground" />,
|
|
407
|
+
}));
|
|
408
|
+
|
|
409
|
+
const sourceBoardOptions = sourceBoards.map((board) => ({
|
|
410
|
+
value: board.id,
|
|
411
|
+
label: board.name ?? t('common.untitled'),
|
|
412
|
+
description: board.workspaceName,
|
|
413
|
+
searchValue: `${board.name ?? ''} ${board.workspaceName}`,
|
|
414
|
+
icon: <LayoutDashboard className="h-4 w-4 text-muted-foreground" />,
|
|
415
|
+
}));
|
|
416
|
+
|
|
417
|
+
const assigneeOptions = availableAssignees.map((assignee) => {
|
|
418
|
+
const label =
|
|
419
|
+
assignee.display_name ||
|
|
420
|
+
assignee.email ||
|
|
421
|
+
assignee.id ||
|
|
422
|
+
t('common.untitled');
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
value: assignee.id,
|
|
426
|
+
label,
|
|
427
|
+
description:
|
|
428
|
+
assignee.display_name && assignee.email ? assignee.email : undefined,
|
|
429
|
+
searchValue: `${label} ${assignee.email ?? ''}`,
|
|
430
|
+
icon: (
|
|
431
|
+
<Avatar className="h-5 w-5 border">
|
|
432
|
+
{assignee.avatar_url && <AvatarImage src={assignee.avatar_url} />}
|
|
433
|
+
<AvatarFallback className="font-medium text-[9px]">
|
|
434
|
+
{getInitials(label)}
|
|
435
|
+
</AvatarFallback>
|
|
436
|
+
</Avatar>
|
|
437
|
+
),
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const labelOptions = availableLabels.map((label) => ({
|
|
442
|
+
value: label.id,
|
|
443
|
+
label: label.name,
|
|
444
|
+
icon: (
|
|
445
|
+
<span
|
|
446
|
+
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
447
|
+
style={getColorStyles(label.color)}
|
|
448
|
+
/>
|
|
449
|
+
),
|
|
450
|
+
}));
|
|
451
|
+
|
|
452
|
+
const projectOptions = availableProjects.map((project) => ({
|
|
453
|
+
value: project.id,
|
|
454
|
+
label: project.name,
|
|
455
|
+
icon: <Hash className="h-4 w-4 text-muted-foreground" />,
|
|
456
|
+
}));
|
|
457
|
+
|
|
458
|
+
const priorityOptions = PRIORITIES.map((priority) => ({
|
|
459
|
+
value: priority.value,
|
|
460
|
+
label: t(priority.labelKey as any),
|
|
461
|
+
icon: <Flag className={cn(priority.color, 'h-4 w-4')} />,
|
|
462
|
+
}));
|
|
463
|
+
|
|
464
|
+
const setLabelIds = (labelIds: string[]) => {
|
|
325
465
|
onFiltersChange({
|
|
326
466
|
...filters,
|
|
327
|
-
labels:
|
|
328
|
-
? filters.labels.filter((l) => l.id !== label.id)
|
|
329
|
-
: [...filters.labels, label],
|
|
467
|
+
labels: availableLabels.filter((label) => labelIds.includes(label.id)),
|
|
330
468
|
});
|
|
331
469
|
};
|
|
332
470
|
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
: [...filters.assignees, assignee];
|
|
340
|
-
|
|
341
|
-
// Determine if "Assigned to me" should be checked:
|
|
342
|
-
// - If toggling current user: sync with their selection state
|
|
343
|
-
// - If adding a non-current-user: uncheck "Assigned to me"
|
|
344
|
-
// - If removing someone while current user remains: keep "Assigned to me" if only current user left
|
|
345
|
-
const shouldIncludeMyTasks = isCurrentUser
|
|
346
|
-
? !isSelected
|
|
347
|
-
: newAssignees.length === 1 && newAssignees[0]?.id === currentUserId;
|
|
471
|
+
const setAssigneeIds = (assigneeIds: string[]) => {
|
|
472
|
+
const newAssignees = availableAssignees.filter((assignee) =>
|
|
473
|
+
assigneeIds.includes(assignee.id)
|
|
474
|
+
);
|
|
475
|
+
const shouldIncludeMyTasks =
|
|
476
|
+
newAssignees.length === 1 && newAssignees[0]?.id === currentUserId;
|
|
348
477
|
|
|
349
478
|
onFiltersChange({
|
|
350
479
|
...filters,
|
|
351
480
|
assignees: newAssignees,
|
|
352
481
|
includeMyTasks: shouldIncludeMyTasks,
|
|
353
|
-
// Auto-deselect "Unassigned" when selecting any assignee
|
|
354
482
|
includeUnassigned:
|
|
355
483
|
newAssignees.length > 0 ? false : filters.includeUnassigned,
|
|
356
484
|
});
|
|
357
485
|
};
|
|
358
486
|
|
|
359
|
-
const
|
|
360
|
-
const isSelected = filters.projects.some((p) => p.id === project.id);
|
|
487
|
+
const setProjectIds = (projectIds: string[]) => {
|
|
361
488
|
onFiltersChange({
|
|
362
489
|
...filters,
|
|
363
|
-
projects:
|
|
364
|
-
|
|
365
|
-
|
|
490
|
+
projects: availableProjects.filter((project) =>
|
|
491
|
+
projectIds.includes(project.id)
|
|
492
|
+
),
|
|
366
493
|
});
|
|
367
494
|
};
|
|
368
495
|
|
|
369
|
-
const
|
|
370
|
-
const isSelected = filters.priorities.includes(priority);
|
|
496
|
+
const setPriorityValues = (priorities: string[]) => {
|
|
371
497
|
onFiltersChange({
|
|
372
498
|
...filters,
|
|
373
|
-
priorities:
|
|
374
|
-
|
|
375
|
-
|
|
499
|
+
priorities: priorities.filter((priority): priority is TaskPriority =>
|
|
500
|
+
PRIORITIES.some((option) => option.value === priority)
|
|
501
|
+
),
|
|
376
502
|
});
|
|
377
503
|
};
|
|
378
504
|
|
|
@@ -389,38 +515,33 @@ export function TaskFilter({
|
|
|
389
515
|
});
|
|
390
516
|
};
|
|
391
517
|
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const nextWorkspaceIds = isSelected
|
|
395
|
-
? selectedSourceWorkspaceIds.filter((id) => id !== workspace.id)
|
|
396
|
-
: [...selectedSourceWorkspaceIds, workspace.id];
|
|
397
|
-
const deselectedBoardIds = new Set(
|
|
398
|
-
sourceBoards
|
|
399
|
-
.filter((board) => board.workspaceId === workspace.id)
|
|
400
|
-
.map((board) => board.id)
|
|
401
|
-
);
|
|
518
|
+
const setSourceWorkspaceIds = (nextWorkspaceIds: string[]) => {
|
|
519
|
+
const selectedWorkspaces = new Set(nextWorkspaceIds);
|
|
402
520
|
|
|
403
521
|
onFiltersChange({
|
|
404
522
|
...filters,
|
|
405
523
|
sourceScope: 'external_specific',
|
|
406
524
|
sourceWorkspaceIds: nextWorkspaceIds,
|
|
407
|
-
sourceBoardIds:
|
|
408
|
-
|
|
409
|
-
|
|
525
|
+
sourceBoardIds: selectedSourceBoardIds.filter((boardId) => {
|
|
526
|
+
const board = sourceBoards.find((item) => item.id === boardId);
|
|
527
|
+
return !board || selectedWorkspaces.has(board.workspaceId);
|
|
528
|
+
}),
|
|
410
529
|
});
|
|
411
530
|
};
|
|
412
531
|
|
|
413
|
-
const
|
|
414
|
-
const
|
|
532
|
+
const setSourceBoardIds = (nextBoardIds: string[]) => {
|
|
533
|
+
const workspaceIds = new Set(selectedSourceWorkspaceIds);
|
|
534
|
+
for (const board of sourceBoards.filter((board) =>
|
|
535
|
+
nextBoardIds.includes(board.id)
|
|
536
|
+
)) {
|
|
537
|
+
workspaceIds.add(board.workspaceId);
|
|
538
|
+
}
|
|
539
|
+
|
|
415
540
|
onFiltersChange({
|
|
416
541
|
...filters,
|
|
417
542
|
sourceScope: 'external_specific',
|
|
418
|
-
sourceBoardIds:
|
|
419
|
-
|
|
420
|
-
: [...selectedSourceBoardIds, board.id],
|
|
421
|
-
sourceWorkspaceIds: selectedSourceWorkspaceIds.includes(board.workspaceId)
|
|
422
|
-
? selectedSourceWorkspaceIds
|
|
423
|
-
: [...selectedSourceWorkspaceIds, board.workspaceId],
|
|
543
|
+
sourceBoardIds: nextBoardIds,
|
|
544
|
+
sourceWorkspaceIds: Array.from(workspaceIds),
|
|
424
545
|
});
|
|
425
546
|
};
|
|
426
547
|
|
|
@@ -477,510 +598,399 @@ export function TaskFilter({
|
|
|
477
598
|
selectedSourceWorkspaceIds.length + selectedSourceBoardIds.length
|
|
478
599
|
)
|
|
479
600
|
: 0);
|
|
601
|
+
const sourceFilterCount = isSourceFilterActive
|
|
602
|
+
? Math.max(
|
|
603
|
+
1,
|
|
604
|
+
selectedSourceWorkspaceIds.length + selectedSourceBoardIds.length
|
|
605
|
+
)
|
|
606
|
+
: 0;
|
|
607
|
+
const quickFilterCount =
|
|
608
|
+
(filters.includeMyTasks ? 1 : 0) + (filters.includeUnassigned ? 1 : 0);
|
|
609
|
+
const peopleFilterCount = assigneeCount;
|
|
610
|
+
const detailFilterCount =
|
|
611
|
+
filters.labels.length + filters.projects.length + filters.priorities.length;
|
|
612
|
+
const dueDateSummary = filters.dueDateRange
|
|
613
|
+
? [
|
|
614
|
+
filters.dueDateRange.from
|
|
615
|
+
? formatShortDate(filters.dueDateRange.from)
|
|
616
|
+
: null,
|
|
617
|
+
filters.dueDateRange.to
|
|
618
|
+
? formatShortDate(filters.dueDateRange.to)
|
|
619
|
+
: null,
|
|
620
|
+
]
|
|
621
|
+
.filter(Boolean)
|
|
622
|
+
.join(' - ') || t('common.due_date')
|
|
623
|
+
: t('common.due_date');
|
|
480
624
|
|
|
481
625
|
return (
|
|
482
626
|
<div className="flex flex-wrap items-center gap-1 sm:gap-1.5">
|
|
483
|
-
<
|
|
484
|
-
<
|
|
485
|
-
<
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
{hasFilters && (
|
|
496
|
-
<Badge
|
|
497
|
-
variant="secondary"
|
|
498
|
-
className="h-3.5 px-1 text-[9px] sm:h-4 sm:text-[10px]"
|
|
627
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
628
|
+
<Tooltip delayDuration={0}>
|
|
629
|
+
<TooltipTrigger asChild>
|
|
630
|
+
<PopoverTrigger asChild>
|
|
631
|
+
<Button
|
|
632
|
+
size="xs"
|
|
633
|
+
variant="outline"
|
|
634
|
+
aria-label={t('common.filters')}
|
|
635
|
+
className={cn(
|
|
636
|
+
'relative h-7 w-7 px-0 text-muted-foreground transition-colors hover:text-foreground sm:h-8 sm:w-8',
|
|
637
|
+
hasFilters && 'border-primary/50 bg-primary/5 text-foreground'
|
|
638
|
+
)}
|
|
499
639
|
>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
)}
|
|
503
|
-
</Button>
|
|
504
|
-
</DropdownMenuTrigger>
|
|
505
|
-
<DropdownMenuContent className="w-70 sm:w-[320px]" align="start">
|
|
506
|
-
<ScrollArea className="max-h-[70vh] sm:max-h-100">
|
|
507
|
-
{/* My Tasks */}
|
|
508
|
-
{currentUserId && (
|
|
509
|
-
<>
|
|
510
|
-
<DropdownMenuLabel className="flex items-center gap-2 py-2 font-semibold text-muted-foreground text-xs uppercase tracking-wide">
|
|
511
|
-
<User className="h-3.5 w-3.5" />
|
|
512
|
-
{t('common.quick_filters')}
|
|
513
|
-
</DropdownMenuLabel>
|
|
514
|
-
<div className="space-y-1 px-2 pb-2">
|
|
515
|
-
<label className="flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-2 text-sm hover:bg-accent">
|
|
516
|
-
<Checkbox
|
|
517
|
-
checked={filters.includeMyTasks}
|
|
518
|
-
onCheckedChange={(checked) => {
|
|
519
|
-
const currentUser = availableAssignees.find(
|
|
520
|
-
(a) => a.id === currentUserId
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
onFiltersChange({
|
|
524
|
-
...filters,
|
|
525
|
-
includeMyTasks: !!checked,
|
|
526
|
-
// Replace all assignees with only current user when checked
|
|
527
|
-
assignees:
|
|
528
|
-
checked && currentUser ? [currentUser] : [],
|
|
529
|
-
// Auto-deselect "Unassigned" when selecting "Assigned to me"
|
|
530
|
-
includeUnassigned: checked
|
|
531
|
-
? false
|
|
532
|
-
: filters.includeUnassigned,
|
|
533
|
-
});
|
|
534
|
-
}}
|
|
535
|
-
/>
|
|
536
|
-
<UserStar className="h-4 w-4 text-dynamic-yellow" />
|
|
537
|
-
<span>{t('common.assigned_to_me')}</span>
|
|
538
|
-
</label>
|
|
539
|
-
<label className="flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-2 text-sm hover:bg-accent">
|
|
540
|
-
<Checkbox
|
|
541
|
-
checked={filters.includeUnassigned}
|
|
542
|
-
onCheckedChange={(checked) =>
|
|
543
|
-
onFiltersChange({
|
|
544
|
-
...filters,
|
|
545
|
-
includeUnassigned: !!checked,
|
|
546
|
-
// Auto-deselect all assignees when selecting "Unassigned"
|
|
547
|
-
includeMyTasks: checked
|
|
548
|
-
? false
|
|
549
|
-
: filters.includeMyTasks,
|
|
550
|
-
assignees: checked ? [] : filters.assignees,
|
|
551
|
-
})
|
|
552
|
-
}
|
|
553
|
-
/>
|
|
554
|
-
<UserX className="h-4 w-4 text-dynamic-red" />
|
|
555
|
-
<span>{t('common.unassigned')}</span>
|
|
556
|
-
</label>
|
|
557
|
-
</div>
|
|
558
|
-
<DropdownMenuSeparator />
|
|
559
|
-
</>
|
|
560
|
-
)}
|
|
561
|
-
|
|
562
|
-
{/* Source Scope */}
|
|
563
|
-
<DropdownMenuSub>
|
|
564
|
-
<DropdownMenuSubTrigger className="gap-2 py-2.5">
|
|
565
|
-
<ListFilter className="h-4 w-4 text-muted-foreground" />
|
|
566
|
-
<span className="flex-1">
|
|
567
|
-
{t('ws-tasks.filter_source_scope')}
|
|
568
|
-
</span>
|
|
569
|
-
{isSourceFilterActive && (
|
|
640
|
+
<Filter className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
|
|
641
|
+
{hasFilters && (
|
|
570
642
|
<Badge
|
|
571
643
|
variant="secondary"
|
|
572
|
-
className="h-4 min-w-
|
|
644
|
+
className="absolute -top-1 -right-1 h-4 min-w-4 justify-center px-1 text-[9px]"
|
|
573
645
|
>
|
|
574
|
-
{
|
|
575
|
-
? Math.max(
|
|
576
|
-
1,
|
|
577
|
-
selectedSourceWorkspaceIds.length +
|
|
578
|
-
selectedSourceBoardIds.length
|
|
579
|
-
)
|
|
580
|
-
: 1}
|
|
646
|
+
{filterCount}
|
|
581
647
|
</Badge>
|
|
582
648
|
)}
|
|
583
|
-
</
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
649
|
+
</Button>
|
|
650
|
+
</PopoverTrigger>
|
|
651
|
+
</TooltipTrigger>
|
|
652
|
+
<TooltipContent>{t('common.filters')}</TooltipContent>
|
|
653
|
+
</Tooltip>
|
|
654
|
+
<PopoverContent
|
|
655
|
+
className="max-h-[min(82dvh,40rem)] w-[min(26rem,calc(100vw-1rem))] overflow-hidden p-0"
|
|
656
|
+
align="end"
|
|
657
|
+
collisionPadding={8}
|
|
658
|
+
sideOffset={6}
|
|
659
|
+
>
|
|
660
|
+
<ScrollArea className="h-[min(76dvh,36rem)]">
|
|
661
|
+
<div className="space-y-3 p-3">
|
|
662
|
+
{currentUserId && (
|
|
663
|
+
<FilterSection
|
|
664
|
+
icon={<User className="h-3.5 w-3.5" />}
|
|
665
|
+
title={t('common.quick_filters')}
|
|
666
|
+
badge={
|
|
667
|
+
quickFilterCount ? (
|
|
668
|
+
<Badge variant="secondary">{quickFilterCount}</Badge>
|
|
669
|
+
) : null
|
|
670
|
+
}
|
|
671
|
+
testId="task-filter-section-quick"
|
|
672
|
+
>
|
|
673
|
+
<div className="grid gap-1 sm:grid-cols-2">
|
|
674
|
+
<label className="flex min-w-0 cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent">
|
|
675
|
+
<Checkbox
|
|
676
|
+
checked={filters.includeMyTasks}
|
|
677
|
+
onCheckedChange={(checked) => {
|
|
678
|
+
const currentUser = availableAssignees.find(
|
|
679
|
+
(assignee) => assignee.id === currentUserId
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
onFiltersChange({
|
|
683
|
+
...filters,
|
|
684
|
+
includeMyTasks: !!checked,
|
|
685
|
+
assignees:
|
|
686
|
+
checked && currentUser ? [currentUser] : [],
|
|
687
|
+
includeUnassigned: checked
|
|
688
|
+
? false
|
|
689
|
+
: filters.includeUnassigned,
|
|
690
|
+
});
|
|
594
691
|
}}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
{t('ws-tasks.filter_no_workspaces_available')}
|
|
621
|
-
</div>
|
|
622
|
-
) : (
|
|
623
|
-
sourceWorkspaces.map((workspace) => (
|
|
624
|
-
<DropdownMenuItem
|
|
625
|
-
key={workspace.id}
|
|
626
|
-
onClick={(event) => {
|
|
627
|
-
event.preventDefault();
|
|
628
|
-
toggleSourceWorkspace(workspace);
|
|
629
|
-
}}
|
|
630
|
-
className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-2"
|
|
631
|
-
>
|
|
632
|
-
<Checkbox
|
|
633
|
-
checked={selectedSourceWorkspaceIds.includes(
|
|
634
|
-
workspace.id
|
|
635
|
-
)}
|
|
636
|
-
className="pointer-events-none"
|
|
637
|
-
/>
|
|
638
|
-
<Building2 className="h-4 w-4 text-muted-foreground" />
|
|
639
|
-
<span className="truncate font-medium text-sm">
|
|
640
|
-
{workspace.name}
|
|
641
|
-
</span>
|
|
642
|
-
</DropdownMenuItem>
|
|
643
|
-
))
|
|
644
|
-
)}
|
|
645
|
-
</div>
|
|
646
|
-
|
|
647
|
-
<DropdownMenuLabel className="flex items-center gap-2 py-2 font-semibold text-muted-foreground text-xs uppercase tracking-wide">
|
|
648
|
-
<LayoutDashboard className="h-3.5 w-3.5" />
|
|
649
|
-
{t('ws-tasks.filter_boards')}
|
|
650
|
-
</DropdownMenuLabel>
|
|
651
|
-
<div className="max-h-56 overflow-y-auto px-1 pb-1">
|
|
652
|
-
{selectedSourceWorkspaceIds.length === 0 ? (
|
|
653
|
-
<div className="px-3 py-2 text-muted-foreground text-xs">
|
|
654
|
-
{t('ws-tasks.filter_select_source_prompt')}
|
|
655
|
-
</div>
|
|
656
|
-
) : sourceBoardsLoading ? (
|
|
657
|
-
<div className="px-3 py-2 text-muted-foreground text-xs">
|
|
658
|
-
{t('common.loading')}
|
|
659
|
-
</div>
|
|
660
|
-
) : sourceBoards.length === 0 ? (
|
|
661
|
-
<div className="px-3 py-2 text-muted-foreground text-xs">
|
|
662
|
-
{t('ws-tasks.filter_no_boards_for_workspaces')}
|
|
663
|
-
</div>
|
|
664
|
-
) : (
|
|
665
|
-
sourceBoards.map((board) => (
|
|
666
|
-
<DropdownMenuItem
|
|
667
|
-
key={board.id}
|
|
668
|
-
onClick={(event) => {
|
|
669
|
-
event.preventDefault();
|
|
670
|
-
toggleSourceBoard(board);
|
|
671
|
-
}}
|
|
672
|
-
className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-2"
|
|
673
|
-
>
|
|
674
|
-
<Checkbox
|
|
675
|
-
checked={selectedSourceBoardIds.includes(
|
|
676
|
-
board.id
|
|
677
|
-
)}
|
|
678
|
-
className="pointer-events-none"
|
|
679
|
-
/>
|
|
680
|
-
<LayoutDashboard className="h-4 w-4 text-muted-foreground" />
|
|
681
|
-
<div className="min-w-0 flex-1">
|
|
682
|
-
<div className="truncate font-medium text-sm">
|
|
683
|
-
{board.name}
|
|
684
|
-
</div>
|
|
685
|
-
<div className="truncate text-muted-foreground text-xs">
|
|
686
|
-
{board.workspaceName}
|
|
687
|
-
</div>
|
|
688
|
-
</div>
|
|
689
|
-
</DropdownMenuItem>
|
|
690
|
-
))
|
|
691
|
-
)}
|
|
692
|
-
</div>
|
|
693
|
-
</>
|
|
694
|
-
)}
|
|
695
|
-
</DropdownMenuSubContent>
|
|
696
|
-
</DropdownMenuSub>
|
|
697
|
-
|
|
698
|
-
{/* Assignees */}
|
|
699
|
-
<DropdownMenuSub>
|
|
700
|
-
<DropdownMenuSubTrigger className="gap-2 py-2.5">
|
|
701
|
-
<Users className="h-4 w-4 text-muted-foreground" />
|
|
702
|
-
<span className="flex-1">{t('common.assignees')}</span>
|
|
703
|
-
{filters.assignees.length > 0 && (
|
|
704
|
-
<Badge
|
|
705
|
-
variant="secondary"
|
|
706
|
-
className="h-4 min-w-5 justify-center px-1 text-[10px]"
|
|
707
|
-
>
|
|
708
|
-
{filters.assignees.length}
|
|
709
|
-
</Badge>
|
|
710
|
-
)}
|
|
711
|
-
</DropdownMenuSubTrigger>
|
|
712
|
-
<DropdownMenuSubContent className="w-65 p-0">
|
|
713
|
-
{availableAssignees.length === 0 ? (
|
|
714
|
-
<div className="p-4 text-center text-muted-foreground text-sm">
|
|
715
|
-
{t('common.no_members_found')}
|
|
692
|
+
/>
|
|
693
|
+
<UserStar className="h-4 w-4 shrink-0 text-dynamic-yellow" />
|
|
694
|
+
<span className="min-w-0 truncate">
|
|
695
|
+
{t('common.assigned_to_me')}
|
|
696
|
+
</span>
|
|
697
|
+
</label>
|
|
698
|
+
<label className="flex min-w-0 cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent">
|
|
699
|
+
<Checkbox
|
|
700
|
+
checked={filters.includeUnassigned}
|
|
701
|
+
onCheckedChange={(checked) =>
|
|
702
|
+
onFiltersChange({
|
|
703
|
+
...filters,
|
|
704
|
+
includeUnassigned: !!checked,
|
|
705
|
+
includeMyTasks: checked
|
|
706
|
+
? false
|
|
707
|
+
: filters.includeMyTasks,
|
|
708
|
+
assignees: checked ? [] : filters.assignees,
|
|
709
|
+
})
|
|
710
|
+
}
|
|
711
|
+
/>
|
|
712
|
+
<UserX className="h-4 w-4 shrink-0 text-dynamic-red" />
|
|
713
|
+
<span className="min-w-0 truncate">
|
|
714
|
+
{t('common.unassigned')}
|
|
715
|
+
</span>
|
|
716
|
+
</label>
|
|
716
717
|
</div>
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
{
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
718
|
+
</FilterSection>
|
|
719
|
+
)}
|
|
720
|
+
|
|
721
|
+
<FilterSection
|
|
722
|
+
icon={<ListFilter className="h-3.5 w-3.5" />}
|
|
723
|
+
title={t('ws-tasks.filter_source_scope')}
|
|
724
|
+
badge={
|
|
725
|
+
isSourceFilterActive ? (
|
|
726
|
+
<Badge variant="secondary">{sourceFilterCount}</Badge>
|
|
727
|
+
) : null
|
|
728
|
+
}
|
|
729
|
+
>
|
|
730
|
+
<Combobox
|
|
731
|
+
mode="single"
|
|
732
|
+
options={sourceScopeOptions}
|
|
733
|
+
selected={sourceScope}
|
|
734
|
+
onChange={(value) => setSourceScope(value as TaskSourceScope)}
|
|
735
|
+
placeholder={t('ws-tasks.filter_source_scope')}
|
|
736
|
+
searchPlaceholder={t('common.search_tasks')}
|
|
737
|
+
className="[&_button]:h-9"
|
|
738
|
+
/>
|
|
739
|
+
{sourceScope === 'external_specific' && (
|
|
740
|
+
<div className="mt-2 grid gap-2 sm:grid-cols-2">
|
|
741
|
+
<FilterPickerField
|
|
742
|
+
icon={<Building2 className="h-3.5 w-3.5" />}
|
|
743
|
+
label={t('ws-tasks.filter_workspaces')}
|
|
744
|
+
badge={
|
|
745
|
+
selectedSourceWorkspaceIds.length ? (
|
|
746
|
+
<Badge variant="secondary">
|
|
747
|
+
{selectedSourceWorkspaceIds.length}
|
|
748
|
+
</Badge>
|
|
749
|
+
) : null
|
|
750
|
+
}
|
|
751
|
+
>
|
|
752
|
+
<Combobox
|
|
753
|
+
mode="multiple"
|
|
754
|
+
options={sourceWorkspaceOptions}
|
|
755
|
+
selected={selectedSourceWorkspaceIds}
|
|
756
|
+
onChange={(value) =>
|
|
757
|
+
setSourceWorkspaceIds(value as string[])
|
|
758
|
+
}
|
|
759
|
+
placeholder={t('ws-tasks.filter_workspaces')}
|
|
760
|
+
searchPlaceholder={t('common.search_tasks')}
|
|
761
|
+
emptyText={t('ws-tasks.filter_no_workspaces_available')}
|
|
762
|
+
className="[&_button]:h-9"
|
|
763
|
+
/>
|
|
764
|
+
</FilterPickerField>
|
|
765
|
+
|
|
766
|
+
<FilterPickerField
|
|
767
|
+
icon={<LayoutDashboard className="h-3.5 w-3.5" />}
|
|
768
|
+
label={t('ws-tasks.filter_boards')}
|
|
769
|
+
badge={
|
|
770
|
+
selectedSourceBoardIds.length ? (
|
|
771
|
+
<Badge variant="secondary">
|
|
772
|
+
{selectedSourceBoardIds.length}
|
|
773
|
+
</Badge>
|
|
774
|
+
) : null
|
|
775
|
+
}
|
|
776
|
+
>
|
|
777
|
+
<Combobox
|
|
778
|
+
mode="multiple"
|
|
779
|
+
options={sourceBoardOptions}
|
|
780
|
+
selected={selectedSourceBoardIds}
|
|
781
|
+
onChange={(value) =>
|
|
782
|
+
setSourceBoardIds(value as string[])
|
|
783
|
+
}
|
|
784
|
+
placeholder={
|
|
785
|
+
selectedSourceWorkspaceIds.length
|
|
786
|
+
? t('ws-tasks.filter_boards')
|
|
787
|
+
: t('ws-tasks.filter_select_source_prompt')
|
|
788
|
+
}
|
|
789
|
+
searchPlaceholder={t('common.search_boards')}
|
|
790
|
+
emptyText={
|
|
791
|
+
sourceBoardsLoading
|
|
792
|
+
? t('common.loading')
|
|
793
|
+
: t('ws-tasks.filter_no_boards_for_workspaces')
|
|
794
|
+
}
|
|
795
|
+
disabled={selectedSourceWorkspaceIds.length === 0}
|
|
796
|
+
className="[&_button]:h-9"
|
|
797
|
+
/>
|
|
798
|
+
</FilterPickerField>
|
|
755
799
|
</div>
|
|
756
800
|
)}
|
|
757
|
-
</
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
801
|
+
</FilterSection>
|
|
802
|
+
|
|
803
|
+
<FilterSection
|
|
804
|
+
icon={<Users className="h-3.5 w-3.5" />}
|
|
805
|
+
title={t('common.people')}
|
|
806
|
+
badge={
|
|
807
|
+
peopleFilterCount ? (
|
|
808
|
+
<Badge variant="secondary">{peopleFilterCount}</Badge>
|
|
809
|
+
) : null
|
|
810
|
+
}
|
|
811
|
+
>
|
|
812
|
+
<Combobox
|
|
813
|
+
mode="multiple"
|
|
814
|
+
options={assigneeOptions}
|
|
815
|
+
selected={filters.assignees.map((assignee) => assignee.id)}
|
|
816
|
+
onChange={(value) => setAssigneeIds(value as string[])}
|
|
817
|
+
placeholder={t('common.assignees')}
|
|
818
|
+
searchPlaceholder={t('common.search_members')}
|
|
819
|
+
emptyText={t('common.no_members_found')}
|
|
820
|
+
className="[&_button]:h-9"
|
|
821
|
+
/>
|
|
822
|
+
</FilterSection>
|
|
823
|
+
|
|
824
|
+
<FilterSection
|
|
825
|
+
icon={<Tag className="h-3.5 w-3.5" />}
|
|
826
|
+
title={t('common.details')}
|
|
827
|
+
badge={
|
|
828
|
+
detailFilterCount ? (
|
|
829
|
+
<Badge variant="secondary">{detailFilterCount}</Badge>
|
|
830
|
+
) : null
|
|
831
|
+
}
|
|
832
|
+
>
|
|
833
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
834
|
+
<FilterPickerField
|
|
835
|
+
icon={<Tag className="h-3.5 w-3.5" />}
|
|
836
|
+
label={t('common.labels')}
|
|
837
|
+
badge={
|
|
838
|
+
filters.labels.length ? (
|
|
839
|
+
<Badge variant="secondary">
|
|
840
|
+
{filters.labels.length}
|
|
841
|
+
</Badge>
|
|
842
|
+
) : null
|
|
843
|
+
}
|
|
769
844
|
>
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
<
|
|
789
|
-
|
|
790
|
-
(l) => l.id === label.id
|
|
791
|
-
)}
|
|
792
|
-
className="pointer-events-none"
|
|
793
|
-
/>
|
|
794
|
-
<Badge
|
|
795
|
-
style={getColorStyles(label.color)}
|
|
796
|
-
className="border-0 font-medium text-xs"
|
|
797
|
-
>
|
|
798
|
-
{label.name}
|
|
845
|
+
<Combobox
|
|
846
|
+
mode="multiple"
|
|
847
|
+
options={labelOptions}
|
|
848
|
+
selected={filters.labels.map((label) => label.id)}
|
|
849
|
+
onChange={(value) => setLabelIds(value as string[])}
|
|
850
|
+
placeholder={t('common.labels')}
|
|
851
|
+
searchPlaceholder={t('common.search_labels')}
|
|
852
|
+
emptyText={t('common.no_labels_found')}
|
|
853
|
+
className="[&_button]:h-9"
|
|
854
|
+
/>
|
|
855
|
+
</FilterPickerField>
|
|
856
|
+
|
|
857
|
+
{availableProjects.length > 0 && (
|
|
858
|
+
<FilterPickerField
|
|
859
|
+
icon={<Hash className="h-3.5 w-3.5" />}
|
|
860
|
+
label={t('common.projects')}
|
|
861
|
+
badge={
|
|
862
|
+
filters.projects.length ? (
|
|
863
|
+
<Badge variant="secondary">
|
|
864
|
+
{filters.projects.length}
|
|
799
865
|
</Badge>
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
</div>
|
|
803
|
-
</div>
|
|
804
|
-
)}
|
|
805
|
-
</DropdownMenuSubContent>
|
|
806
|
-
</DropdownMenuSub>
|
|
807
|
-
|
|
808
|
-
{/* Projects */}
|
|
809
|
-
{availableProjects.length > 0 && (
|
|
810
|
-
<DropdownMenuSub>
|
|
811
|
-
<DropdownMenuSubTrigger className="gap-2 py-2.5">
|
|
812
|
-
<Hash className="h-4 w-4 text-muted-foreground" />
|
|
813
|
-
<span className="flex-1">{t('common.projects')}</span>
|
|
814
|
-
{filters.projects.length > 0 && (
|
|
815
|
-
<Badge
|
|
816
|
-
variant="secondary"
|
|
817
|
-
className="h-4 min-w-5 justify-center px-1 text-[10px]"
|
|
866
|
+
) : null
|
|
867
|
+
}
|
|
818
868
|
>
|
|
819
|
-
|
|
820
|
-
|
|
869
|
+
<Combobox
|
|
870
|
+
mode="multiple"
|
|
871
|
+
options={projectOptions}
|
|
872
|
+
selected={filters.projects.map((project) => project.id)}
|
|
873
|
+
onChange={(value) => setProjectIds(value as string[])}
|
|
874
|
+
placeholder={t('common.projects')}
|
|
875
|
+
searchPlaceholder={t('common.search_projects')}
|
|
876
|
+
emptyText={t('common.empty')}
|
|
877
|
+
className="[&_button]:h-9"
|
|
878
|
+
/>
|
|
879
|
+
</FilterPickerField>
|
|
821
880
|
)}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
checked={filters.projects.some(
|
|
834
|
-
(p) => p.id === project.id
|
|
835
|
-
)}
|
|
836
|
-
className="pointer-events-none"
|
|
837
|
-
/>
|
|
838
|
-
<span className="font-medium text-sm">
|
|
839
|
-
{project.name}
|
|
840
|
-
</span>
|
|
841
|
-
</DropdownMenuItem>
|
|
842
|
-
))}
|
|
843
|
-
</div>
|
|
844
|
-
</div>
|
|
845
|
-
</DropdownMenuSubContent>
|
|
846
|
-
</DropdownMenuSub>
|
|
847
|
-
)}
|
|
848
|
-
|
|
849
|
-
{/* Priority */}
|
|
850
|
-
<DropdownMenuSub>
|
|
851
|
-
<DropdownMenuSubTrigger className="gap-2 py-2.5">
|
|
852
|
-
<Flag className="h-4 w-4 text-muted-foreground" />
|
|
853
|
-
<span className="flex-1">{t('common.priority')}</span>
|
|
854
|
-
{filters.priorities.length > 0 && (
|
|
855
|
-
<Badge
|
|
856
|
-
variant="secondary"
|
|
857
|
-
className="h-4 min-w-5 justify-center px-1 text-[10px]"
|
|
881
|
+
|
|
882
|
+
<FilterPickerField
|
|
883
|
+
icon={<Flag className="h-3.5 w-3.5" />}
|
|
884
|
+
label={t('common.priority')}
|
|
885
|
+
badge={
|
|
886
|
+
filters.priorities.length ? (
|
|
887
|
+
<Badge variant="secondary">
|
|
888
|
+
{filters.priorities.length}
|
|
889
|
+
</Badge>
|
|
890
|
+
) : null
|
|
891
|
+
}
|
|
858
892
|
>
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
893
|
+
<Combobox
|
|
894
|
+
mode="multiple"
|
|
895
|
+
options={priorityOptions}
|
|
896
|
+
selected={filters.priorities}
|
|
897
|
+
onChange={(value) => setPriorityValues(value as string[])}
|
|
898
|
+
placeholder={t('common.priority')}
|
|
899
|
+
searchPlaceholder={t('common.search_tasks')}
|
|
900
|
+
className="[&_button]:h-9"
|
|
901
|
+
/>
|
|
902
|
+
</FilterPickerField>
|
|
903
|
+
</div>
|
|
904
|
+
</FilterSection>
|
|
905
|
+
|
|
906
|
+
<FilterSection
|
|
907
|
+
icon={<CalendarIcon className="h-3.5 w-3.5" />}
|
|
908
|
+
title={t('common.due_date')}
|
|
909
|
+
badge={
|
|
910
|
+
filters.dueDateRange ? (
|
|
911
|
+
<Badge variant="secondary">1</Badge>
|
|
912
|
+
) : null
|
|
913
|
+
}
|
|
914
|
+
>
|
|
915
|
+
<div className="space-y-2">
|
|
916
|
+
<div className="flex min-w-0 items-center gap-2 rounded-md border bg-muted/20 px-2 py-1.5 text-muted-foreground text-xs">
|
|
917
|
+
<CalendarIcon className="h-3.5 w-3.5 shrink-0" />
|
|
918
|
+
<span className="min-w-0 truncate">{dueDateSummary}</span>
|
|
919
|
+
</div>
|
|
920
|
+
<div className="grid min-w-0 grid-cols-1 gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_2.25rem]">
|
|
921
|
+
<Input
|
|
922
|
+
aria-label={t('common.from')}
|
|
923
|
+
className="h-9 min-w-0"
|
|
924
|
+
type="date"
|
|
925
|
+
value={formatDateInputValue(filters.dueDateRange?.from)}
|
|
926
|
+
onChange={(event) => {
|
|
927
|
+
const nextFrom = parseDateInputValue(
|
|
928
|
+
event.target.value
|
|
929
|
+
);
|
|
930
|
+
const nextTo = filters.dueDateRange?.to;
|
|
931
|
+
|
|
932
|
+
onFiltersChange({
|
|
933
|
+
...filters,
|
|
934
|
+
dueDateRange:
|
|
935
|
+
nextFrom || nextTo
|
|
936
|
+
? { from: nextFrom, to: nextTo }
|
|
937
|
+
: null,
|
|
938
|
+
});
|
|
939
|
+
}}
|
|
940
|
+
/>
|
|
941
|
+
<Input
|
|
942
|
+
aria-label={t('common.to')}
|
|
943
|
+
className="h-9 min-w-0"
|
|
944
|
+
type="date"
|
|
945
|
+
value={formatDateInputValue(filters.dueDateRange?.to)}
|
|
946
|
+
onChange={(event) => {
|
|
947
|
+
const nextFrom = filters.dueDateRange?.from;
|
|
948
|
+
const nextTo = parseDateInputValue(event.target.value);
|
|
949
|
+
|
|
950
|
+
onFiltersChange({
|
|
951
|
+
...filters,
|
|
952
|
+
dueDateRange:
|
|
953
|
+
nextFrom || nextTo
|
|
954
|
+
? { from: nextFrom, to: nextTo }
|
|
955
|
+
: null,
|
|
956
|
+
});
|
|
957
|
+
}}
|
|
958
|
+
/>
|
|
959
|
+
<Button
|
|
960
|
+
type="button"
|
|
961
|
+
variant="outline"
|
|
962
|
+
size="icon"
|
|
963
|
+
className="h-9 w-full sm:w-9"
|
|
964
|
+
aria-label={t('common.clear')}
|
|
965
|
+
onClick={() =>
|
|
966
|
+
onFiltersChange({
|
|
967
|
+
...filters,
|
|
968
|
+
dueDateRange: null,
|
|
969
|
+
})
|
|
970
|
+
}
|
|
971
|
+
disabled={!filters.dueDateRange}
|
|
870
972
|
>
|
|
871
|
-
<
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
/>
|
|
875
|
-
<Flag className={cn(priority.color, 'h-4 w-4')} />
|
|
876
|
-
<span className="font-medium text-sm">
|
|
877
|
-
{t(priority.labelKey as any)}
|
|
878
|
-
</span>
|
|
879
|
-
</DropdownMenuItem>
|
|
880
|
-
))}
|
|
973
|
+
<X className="h-4 w-4" />
|
|
974
|
+
</Button>
|
|
975
|
+
</div>
|
|
881
976
|
</div>
|
|
882
|
-
</
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
|
|
889
|
-
<span className="flex-1">{t('common.due_date')}</span>
|
|
890
|
-
{filters.dueDateRange && (
|
|
891
|
-
<Check className="h-3.5 w-3.5 text-primary" />
|
|
892
|
-
)}
|
|
893
|
-
</DropdownMenuSubTrigger>
|
|
894
|
-
<DropdownMenuSubContent className="w-auto p-0">
|
|
895
|
-
<Calendar
|
|
896
|
-
mode="range"
|
|
897
|
-
selected={
|
|
898
|
-
filters.dueDateRange
|
|
899
|
-
? {
|
|
900
|
-
from: filters.dueDateRange.from,
|
|
901
|
-
to: filters.dueDateRange.to,
|
|
902
|
-
}
|
|
903
|
-
: undefined
|
|
904
|
-
}
|
|
905
|
-
onSelect={(range) =>
|
|
906
|
-
onFiltersChange({
|
|
907
|
-
...filters,
|
|
908
|
-
dueDateRange: range
|
|
909
|
-
? { from: range.from, to: range.to }
|
|
910
|
-
: null,
|
|
911
|
-
})
|
|
912
|
-
}
|
|
913
|
-
numberOfMonths={1}
|
|
914
|
-
className="rounded-md border-0"
|
|
915
|
-
/>
|
|
916
|
-
</DropdownMenuSubContent>
|
|
917
|
-
</DropdownMenuSub>
|
|
918
|
-
|
|
919
|
-
{/* Clear All */}
|
|
920
|
-
{hasFilters && (
|
|
921
|
-
<>
|
|
922
|
-
<DropdownMenuSeparator />
|
|
923
|
-
<DropdownMenuItem
|
|
977
|
+
</FilterSection>
|
|
978
|
+
|
|
979
|
+
{hasFilters && (
|
|
980
|
+
<Button
|
|
981
|
+
type="button"
|
|
982
|
+
variant="ghost"
|
|
924
983
|
onClick={clearAllFilters}
|
|
925
|
-
className="gap-2 text-dynamic-red/80
|
|
984
|
+
className="h-8 w-full justify-start gap-2 text-dynamic-red/80 hover:text-dynamic-red"
|
|
926
985
|
>
|
|
927
986
|
<X className="h-4 w-4" />
|
|
928
987
|
{t('common.clear_all_filters')}
|
|
929
|
-
</
|
|
930
|
-
|
|
931
|
-
|
|
988
|
+
</Button>
|
|
989
|
+
)}
|
|
990
|
+
</div>
|
|
932
991
|
</ScrollArea>
|
|
933
|
-
</
|
|
934
|
-
</
|
|
935
|
-
|
|
936
|
-
{/* Active filter chips */}
|
|
937
|
-
{/* {filters.labels.map((label) => (
|
|
938
|
-
<Badge
|
|
939
|
-
key={label.id}
|
|
940
|
-
style={getColorStyles(label.color)}
|
|
941
|
-
className="h-5 cursor-pointer border-0 px-1.5 text-[10px] hover:opacity-80 sm:h-6 sm:px-2 sm:text-xs"
|
|
942
|
-
onClick={() => toggleLabel(label)}
|
|
943
|
-
>
|
|
944
|
-
{label.name}
|
|
945
|
-
<X className="ml-0.5 h-2.5 w-2.5 sm:ml-1 sm:h-3 sm:w-3" />
|
|
946
|
-
</Badge>
|
|
947
|
-
))}
|
|
948
|
-
{filters.assignees.map((assignee) => (
|
|
949
|
-
<Badge
|
|
950
|
-
key={assignee.id}
|
|
951
|
-
variant="secondary"
|
|
952
|
-
className="h-5 cursor-pointer px-1.5 text-[10px] hover:opacity-80 sm:h-6 sm:px-2 sm:text-xs"
|
|
953
|
-
onClick={() => toggleAssignee(assignee)}
|
|
954
|
-
>
|
|
955
|
-
{assignee.display_name || assignee.email}
|
|
956
|
-
<X className="ml-0.5 h-2.5 w-2.5 sm:ml-1 sm:h-3 sm:w-3" />
|
|
957
|
-
</Badge>
|
|
958
|
-
))}
|
|
959
|
-
{filters.projects.map((project) => (
|
|
960
|
-
<Badge
|
|
961
|
-
key={project.id}
|
|
962
|
-
variant="outline"
|
|
963
|
-
className="h-5 cursor-pointer px-1.5 text-[10px] hover:opacity-80 sm:h-6 sm:px-2 sm:text-xs"
|
|
964
|
-
onClick={() => toggleProject(project)}
|
|
965
|
-
>
|
|
966
|
-
{project.name}
|
|
967
|
-
<X className="ml-0.5 h-2.5 w-2.5 sm:ml-1 sm:h-3 sm:w-3" />
|
|
968
|
-
</Badge>
|
|
969
|
-
))}
|
|
970
|
-
{filters.priorities.map((priority) => {
|
|
971
|
-
const priorityConfig = PRIORITIES.find((p) => p.value === priority);
|
|
972
|
-
return (
|
|
973
|
-
<Badge
|
|
974
|
-
key={priority}
|
|
975
|
-
variant="outline"
|
|
976
|
-
className="h-5 cursor-pointer px-1.5 text-[10px] hover:opacity-80 sm:h-6 sm:px-2 sm:text-xs"
|
|
977
|
-
onClick={() => togglePriority(priority)}
|
|
978
|
-
>
|
|
979
|
-
{priorityConfig?.label}
|
|
980
|
-
<X className="ml-0.5 h-2.5 w-2.5 sm:ml-1 sm:h-3 sm:w-3" />
|
|
981
|
-
</Badge>
|
|
982
|
-
);
|
|
983
|
-
})} */}
|
|
992
|
+
</PopoverContent>
|
|
993
|
+
</Popover>
|
|
984
994
|
</div>
|
|
985
995
|
);
|
|
986
996
|
}
|