@tuturuuu/ui 0.25.2 → 0.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/biome.json +1 -1
  3. package/package.json +13 -12
  4. package/src/components/ui/chat/chat-sidebar-items.tsx +25 -19
  5. package/src/components/ui/chat/chat-utils.test.ts +88 -2
  6. package/src/components/ui/chat/chat-workspace.tsx +1 -0
  7. package/src/components/ui/chat/composer-attachment-chip.tsx +128 -0
  8. package/src/components/ui/chat/external-message-content.test.ts +41 -0
  9. package/src/components/ui/chat/external-message-content.ts +28 -0
  10. package/src/components/ui/chat/message-bubble.tsx +4 -2
  11. package/src/components/ui/chat/message-composer.tsx +121 -33
  12. package/src/components/ui/chat/message-links.test.tsx +14 -0
  13. package/src/components/ui/chat/utils.ts +39 -3
  14. package/src/components/ui/custom/education/courses/course-row-actions.tsx +1 -1
  15. package/src/components/ui/custom/education/modules/course-module-row-actions.tsx +1 -1
  16. package/src/components/ui/custom/structure.test.tsx +55 -1
  17. package/src/components/ui/custom/structure.tsx +10 -1
  18. package/src/components/ui/custom/tables/custom-data-table.tsx +3 -2
  19. package/src/components/ui/custom/tables/data-table-column-header.tsx +4 -3
  20. package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +4 -3
  21. package/src/components/ui/custom/tables/data-table-pagination.tsx +6 -6
  22. package/src/components/ui/custom/tables/data-table-toolbar.tsx +5 -5
  23. package/src/components/ui/custom/tables/data-table-view-options.tsx +4 -3
  24. package/src/components/ui/custom/tables/data-table.tsx +69 -30
  25. package/src/components/ui/finance/invoices/columns.test.tsx +3 -3
  26. package/src/components/ui/finance/invoices/columns.tsx +4 -2
  27. package/src/components/ui/finance/invoices/pending-columns.tsx +1 -1
  28. package/src/components/ui/finance/invoices/row-actions.tsx +1 -1
  29. package/src/components/ui/finance/transactions/categories/columns.test.tsx +5 -5
  30. package/src/components/ui/finance/transactions/categories/columns.tsx +4 -2
  31. package/src/components/ui/finance/transactions/categories/row-actions.tsx +1 -1
  32. package/src/components/ui/finance/transactions/columns.test.tsx +5 -5
  33. package/src/components/ui/finance/transactions/columns.tsx +4 -2
  34. package/src/components/ui/finance/transactions/row-actions.tsx +1 -1
  35. package/src/components/ui/finance/wallets/columns.tsx +4 -2
  36. package/src/components/ui/finance/wallets/row-actions.tsx +1 -1
@@ -1,16 +1,27 @@
1
1
  'use client';
2
2
 
3
- import { LoaderCircle, Paperclip, Send, Upload, X } from '@tuturuuu/icons';
3
+ import { LoaderCircle, Paperclip, Send } from '@tuturuuu/icons';
4
4
  import type { ChatAttachmentDraft } from '@tuturuuu/internal-api';
5
5
  import { cn } from '@tuturuuu/utils/format';
6
6
  import { useTranslations } from 'next-intl';
7
- import { type ClipboardEvent, type FormEvent, useRef, useState } from 'react';
7
+ import {
8
+ type ClipboardEvent,
9
+ type DragEvent,
10
+ type FormEvent,
11
+ useEffect,
12
+ useRef,
13
+ useState,
14
+ } from 'react';
8
15
  import { Button } from '../button';
9
16
  import { toast } from '../sonner';
10
17
  import { Textarea } from '../textarea';
18
+ import { ComposerAttachmentChip } from './composer-attachment-chip';
11
19
  import { formatFileSize } from './utils';
12
20
 
13
21
  const MAX_COMPOSER_ATTACHMENTS = 20;
22
+ // Mirrors MAX_AI_ATTACHMENT_BYTES on the server. Rejecting here means the user
23
+ // finds out before the upload instead of after it.
24
+ const MAX_ATTACHMENT_BYTES = 100 * 1024 * 1024;
14
25
 
15
26
  interface MessageComposerProps {
16
27
  allowAttachments?: boolean;
@@ -37,6 +48,32 @@ export function MessageComposer({
37
48
  const [content, setContent] = useState('');
38
49
  const [attachments, setAttachments] = useState<ChatAttachmentDraft[]>([]);
39
50
  const [uploadingCount, setUploadingCount] = useState(0);
51
+ const [isDraggingOver, setIsDraggingOver] = useState(false);
52
+ // Thumbnails come from the local File, so they render immediately and cost
53
+ // no round trip. Object URLs leak unless revoked, hence the cleanup below.
54
+ const [previewUrls, setPreviewUrls] = useState<Record<string, string>>({});
55
+ const previewUrlsRef = useRef(previewUrls);
56
+ previewUrlsRef.current = previewUrls;
57
+
58
+ useEffect(
59
+ () => () => {
60
+ for (const url of Object.values(previewUrlsRef.current)) {
61
+ URL.revokeObjectURL(url);
62
+ }
63
+ },
64
+ []
65
+ );
66
+
67
+ function releasePreview(path: string) {
68
+ setPreviewUrls((current) => {
69
+ const url = current[path];
70
+ if (!url) return current;
71
+ URL.revokeObjectURL(url);
72
+ const next = { ...current };
73
+ delete next[path];
74
+ return next;
75
+ });
76
+ }
40
77
 
41
78
  const busy = disabled || isSending || isUploading || uploadingCount > 0;
42
79
  const canSend = content.trim().length > 0 || attachments.length > 0;
@@ -50,6 +87,7 @@ export function MessageComposer({
50
87
 
51
88
  setContent('');
52
89
  setAttachments([]);
90
+ for (const draft of draftAttachments) releasePreview(draft.path);
53
91
 
54
92
  try {
55
93
  await onSend({
@@ -65,19 +103,47 @@ export function MessageComposer({
65
103
  async function handleFileCandidates(files: File[]) {
66
104
  if (!allowAttachments || disabled || files.length === 0) return;
67
105
 
106
+ const withinSizeLimit = files.filter(
107
+ (file) => file.size <= MAX_ATTACHMENT_BYTES
108
+ );
109
+ if (withinSizeLimit.length < files.length) {
110
+ toast.error(
111
+ t('attachment_too_large', {
112
+ size: formatFileSize(MAX_ATTACHMENT_BYTES),
113
+ })
114
+ );
115
+ }
116
+ if (withinSizeLimit.length === 0) return;
117
+
68
118
  const remainingSlots = Math.max(
69
119
  MAX_COMPOSER_ATTACHMENTS - attachments.length,
70
120
  0
71
121
  );
72
- const nextFiles = files.slice(0, remainingSlots);
73
- if (nextFiles.length === 0) return;
122
+ if (remainingSlots === 0) {
123
+ toast.error(
124
+ t('attachment_limit_reached', { count: MAX_COMPOSER_ATTACHMENTS })
125
+ );
126
+ return;
127
+ }
128
+
129
+ const nextFiles = withinSizeLimit.slice(0, remainingSlots);
130
+ if (nextFiles.length < withinSizeLimit.length) {
131
+ toast.error(
132
+ t('attachment_limit_reached', { count: MAX_COMPOSER_ATTACHMENTS })
133
+ );
134
+ }
74
135
 
75
136
  setUploadingCount((count) => count + nextFiles.length);
76
137
  const uploaded: ChatAttachmentDraft[] = [];
77
138
 
78
139
  for (const file of nextFiles) {
79
140
  try {
80
- uploaded.push(await onUploadFile(file));
141
+ const draft = await onUploadFile(file);
142
+ uploaded.push(draft);
143
+ if (file.type.startsWith('image/') || file.type.startsWith('video/')) {
144
+ const url = URL.createObjectURL(file);
145
+ setPreviewUrls((current) => ({ ...current, [draft.path]: url }));
146
+ }
81
147
  } catch {
82
148
  toast.error(t('upload_failed'));
83
149
  } finally {
@@ -94,6 +160,17 @@ export function MessageComposer({
94
160
  }
95
161
  }
96
162
 
163
+ function handleDrop(event: DragEvent<HTMLFormElement>) {
164
+ setIsDraggingOver(false);
165
+ if (!allowAttachments || disabled) return;
166
+
167
+ const dropped = Array.from(event.dataTransfer?.files ?? []);
168
+ if (dropped.length === 0) return;
169
+
170
+ event.preventDefault();
171
+ void handleFileCandidates(dropped);
172
+ }
173
+
97
174
  async function handleFiles(files: FileList | null) {
98
175
  if (!files?.length) return;
99
176
  await handleFileCandidates(Array.from(files));
@@ -122,38 +199,49 @@ export function MessageComposer({
122
199
  }
123
200
 
124
201
  return (
125
- <form className="border-t bg-background/95 p-3" onSubmit={handleSubmit}>
202
+ <form
203
+ className={cn(
204
+ 'relative border-t bg-background/95 p-3 transition-colors',
205
+ isDraggingOver && 'bg-primary/5 ring-2 ring-primary ring-inset'
206
+ )}
207
+ onDragLeave={(event) => {
208
+ // Only clear when the pointer actually leaves the composer, not when it
209
+ // crosses into a child element.
210
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
211
+ setIsDraggingOver(false);
212
+ }
213
+ }}
214
+ onDragOver={(event) => {
215
+ if (!allowAttachments || disabled) return;
216
+ if (!Array.from(event.dataTransfer?.types ?? []).includes('Files')) {
217
+ return;
218
+ }
219
+ event.preventDefault();
220
+ setIsDraggingOver(true);
221
+ }}
222
+ onDrop={handleDrop}
223
+ onSubmit={handleSubmit}
224
+ >
225
+ {isDraggingOver && (
226
+ <div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-sm bg-background/80 font-medium text-primary text-sm">
227
+ {t('drop_files_here')}
228
+ </div>
229
+ )}
126
230
  {attachments.length > 0 && (
127
231
  <div className="mb-2 flex flex-wrap gap-2">
128
232
  {attachments.map((attachment) => (
129
- <div
130
- className="flex min-w-0 max-w-full items-center gap-2 overflow-hidden rounded-md border bg-muted/40 px-2 py-1 text-sm"
233
+ <ComposerAttachmentChip
234
+ attachment={attachment}
131
235
  key={attachment.path}
132
- >
133
- <Upload className="size-4 shrink-0 text-muted-foreground" />
134
- <span className="min-w-0 truncate" title={attachment.filename}>
135
- {attachment.filename}
136
- </span>
137
- {attachment.sizeBytes ? (
138
- <span className="shrink-0 text-muted-foreground text-xs">
139
- {formatFileSize(attachment.sizeBytes)}
140
- </span>
141
- ) : null}
142
- <Button
143
- aria-label={t('remove_attachment')}
144
- className="size-6"
145
- onClick={() =>
146
- setAttachments((current) =>
147
- current.filter((item) => item.path !== attachment.path)
148
- )
149
- }
150
- size="icon"
151
- type="button"
152
- variant="ghost"
153
- >
154
- <X className="size-3.5" />
155
- </Button>
156
- </div>
236
+ onRemove={() => {
237
+ releasePreview(attachment.path);
238
+ setAttachments((current) =>
239
+ current.filter((item) => item.path !== attachment.path)
240
+ );
241
+ }}
242
+ previewUrl={previewUrls[attachment.path]}
243
+ removeLabel={t('remove_attachment')}
244
+ />
157
245
  ))}
158
246
  </div>
159
247
  )}
@@ -0,0 +1,14 @@
1
+ import { render } from '@testing-library/react';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { MessageText } from './message-links';
4
+
5
+ describe('MessageText', () => {
6
+ it('renders HTML-like message content as inert text', () => {
7
+ const { container } = render(
8
+ <MessageText content="<img src=x onerror=alert(1)>" />
9
+ );
10
+
11
+ expect(container.querySelector('img')).toBeNull();
12
+ expect(container.textContent).toBe('<img src=x onerror=alert(1)>');
13
+ });
14
+ });
@@ -4,6 +4,10 @@ import type {
4
4
  ChatMessage,
5
5
  ChatUserProfile,
6
6
  } from '@tuturuuu/internal-api';
7
+ import {
8
+ decodeExternalChatContent,
9
+ getChatMessageDisplayContent,
10
+ } from './external-message-content';
7
11
 
8
12
  export type ChatConversationScope = 'external' | 'personal' | 'workspaces';
9
13
  export type ChatConversationArchiveFilter = 'active' | 'all' | 'archived';
@@ -102,6 +106,12 @@ export function getChatConversationQueueDetails(
102
106
  ) {
103
107
  const latestMessage = conversation.latestMessage;
104
108
  const phone = readNonEmptyString(conversation.metadata.phone);
109
+ const previewContent = latestMessage
110
+ ? decodeExternalChatContent(
111
+ latestMessage.content,
112
+ conversation.metadata.externalChat === true
113
+ )
114
+ : null;
105
115
 
106
116
  return {
107
117
  deliveryState: latestMessage
@@ -110,7 +120,7 @@ export function getChatConversationQueueDetails(
110
120
  phone,
111
121
  preview: latestMessage?.deletedAt
112
122
  ? null
113
- : (readNonEmptyString(latestMessage?.content) ??
123
+ : (readNonEmptyString(previewContent) ??
114
124
  readNonEmptyString(latestMessage?.attachments[0]?.filename)),
115
125
  timestamp: latestMessage?.createdAt ?? conversation.updatedAt,
116
126
  };
@@ -210,7 +220,7 @@ export function getChatMessageSenderLabel(
210
220
  const displayName =
211
221
  readNonEmptyString(externalSender.displayName) ??
212
222
  readNonEmptyString(externalSender.name);
213
- if (displayName) return displayName;
223
+ if (displayName) return decodeExternalChatContent(displayName, true);
214
224
  }
215
225
 
216
226
  return message.metadata?.externalChat === true
@@ -234,9 +244,29 @@ export function getConversationTitle(
234
244
  channel?: string;
235
245
  chat?: string;
236
246
  direct?: string;
247
+ external?: string;
237
248
  group?: string;
238
249
  }
239
250
  ) {
251
+ if (conversation.metadata.externalChat === true) {
252
+ const profileTitle =
253
+ readNonEmptyString(conversation.metadata.displayName) ??
254
+ readNonEmptyString(conversation.metadata.name);
255
+ if (profileTitle) return decodeExternalChatContent(profileTitle, true);
256
+
257
+ const persistedTitle = readNonEmptyString(conversation.title);
258
+ if (persistedTitle && !isGenericExternalTitle(persistedTitle)) {
259
+ return decodeExternalChatContent(persistedTitle, true);
260
+ }
261
+
262
+ const reference = conversation.id
263
+ .replaceAll('-', '')
264
+ .slice(-6)
265
+ .toUpperCase();
266
+ const label = fallback?.external ?? fallback?.channel ?? 'External visitor';
267
+ return reference ? `${label} #${reference}` : label;
268
+ }
269
+
240
270
  if (conversation.title) return conversation.title;
241
271
 
242
272
  if (conversation.type === 'direct') {
@@ -257,6 +287,11 @@ export function getConversationTitle(
257
287
  return fallback?.chat ?? 'Untitled chat';
258
288
  }
259
289
 
290
+ function isGenericExternalTitle(title: string) {
291
+ const normalized = title.trim().toLowerCase();
292
+ return normalized === 'external visitor' || normalized === 'website visitor';
293
+ }
294
+
260
295
  export function getCurrentChatConversationMember(
261
296
  conversation: ChatConversation,
262
297
  currentUserId: string
@@ -412,7 +447,8 @@ export function getLastMessagePreview(
412
447
  if (message.deletedAt) return labels.messageDeleted ?? '';
413
448
  if (message.kind === 'system')
414
449
  return labels.systemEvent ?? labels.message ?? '';
415
- if (message.content.trim()) return message.content.trim();
450
+ const displayContent = getChatMessageDisplayContent(message).trim();
451
+ if (displayContent) return displayContent;
416
452
  if (message.attachments.length > 0) {
417
453
  return message.attachments[0]?.filename ?? labels.attachment ?? '';
418
454
  }
@@ -1,13 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { useMutation } from '@tanstack/react-query';
4
- import type { Row } from '@tanstack/react-table';
5
4
  import { Ellipsis } from '@tuturuuu/icons';
6
5
  import { deleteWorkspaceCourse } from '@tuturuuu/internal-api';
7
6
  import type { WorkspaceCourse } from '@tuturuuu/types';
8
7
  import { Button } from '@tuturuuu/ui/button';
9
8
  import { CourseForm } from '@tuturuuu/ui/custom/education/courses/course-form';
10
9
  import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger';
10
+ import type { Row } from '@tuturuuu/ui/custom/tables/data-table';
11
11
  import {
12
12
  DropdownMenu,
13
13
  DropdownMenuContent,
@@ -1,13 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { useMutation } from '@tanstack/react-query';
4
- import type { Row } from '@tanstack/react-table';
5
4
  import { Ellipsis } from '@tuturuuu/icons';
6
5
  import { deleteWorkspaceCourseModule } from '@tuturuuu/internal-api';
7
6
  import type { WorkspaceCourseModule } from '@tuturuuu/types';
8
7
  import { Button } from '@tuturuuu/ui/button';
9
8
  import { CourseModuleForm } from '@tuturuuu/ui/custom/education/modules/course-module-form';
10
9
  import ModifiableDialogTrigger from '@tuturuuu/ui/custom/modifiable-dialog-trigger';
10
+ import type { Row } from '@tuturuuu/ui/custom/tables/data-table';
11
11
  import {
12
12
  DropdownMenu,
13
13
  DropdownMenuContent,
@@ -1,5 +1,5 @@
1
1
  import '@testing-library/jest-dom';
2
- import { render, screen } from '@testing-library/react';
2
+ import { render, screen, within } from '@testing-library/react';
3
3
  import { describe, expect, it, vi } from 'vitest';
4
4
  import { Structure } from './structure';
5
5
 
@@ -30,4 +30,58 @@ describe('Structure', () => {
30
30
  'md:p-4'
31
31
  );
32
32
  });
33
+
34
+ it('keeps the account and notification controls in the collapsed footer', () => {
35
+ const { container } = render(
36
+ <Structure
37
+ actions={<span>Expanded account actions</span>}
38
+ isCollapsed
39
+ notificationPopover={<button type="button">Notifications</button>}
40
+ setIsCollapsed={vi.fn()}
41
+ userPopover={<button type="button">Account</button>}
42
+ >
43
+ <span>Page content</span>
44
+ </Structure>
45
+ );
46
+
47
+ const sidebar = container.querySelector('aside');
48
+ expect(sidebar).not.toBeNull();
49
+ const sidebarQueries = within(sidebar as HTMLElement);
50
+
51
+ expect(
52
+ sidebarQueries.getByRole('button', { name: 'Account' })
53
+ ).toBeVisible();
54
+ expect(
55
+ sidebarQueries.getByRole('button', { name: 'Notifications' })
56
+ ).toBeVisible();
57
+ expect(
58
+ sidebarQueries.queryByText('Expanded account actions')
59
+ ).not.toBeInTheDocument();
60
+ });
61
+
62
+ it('renders the combined account actions only once when expanded', () => {
63
+ const { container } = render(
64
+ <Structure
65
+ actions={<span>Expanded account actions</span>}
66
+ isCollapsed={false}
67
+ notificationPopover={<button type="button">Notifications</button>}
68
+ setIsCollapsed={vi.fn()}
69
+ userPopover={<button type="button">Account</button>}
70
+ >
71
+ <span>Page content</span>
72
+ </Structure>
73
+ );
74
+
75
+ const sidebar = container.querySelector('aside');
76
+ expect(sidebar).not.toBeNull();
77
+ const sidebarQueries = within(sidebar as HTMLElement);
78
+
79
+ expect(sidebarQueries.getByText('Expanded account actions')).toBeVisible();
80
+ expect(
81
+ sidebarQueries.queryByRole('button', { name: 'Notifications' })
82
+ ).not.toBeInTheDocument();
83
+ expect(
84
+ sidebarQueries.queryByRole('button', { name: 'Account' })
85
+ ).not.toBeInTheDocument();
86
+ });
33
87
  });
@@ -17,6 +17,7 @@ interface StructureProps {
17
17
  sidebarContent?: ReactNode;
18
18
  actions?: ReactNode;
19
19
  userPopover?: ReactNode;
20
+ notificationPopover?: ReactNode;
20
21
  sidebarUtility?: ReactNode;
21
22
  feedbackButton?: ReactNode;
22
23
  children: ReactNode;
@@ -40,6 +41,7 @@ export function Structure({
40
41
  sidebarContent,
41
42
  actions,
42
43
  userPopover,
44
+ notificationPopover,
43
45
  sidebarUtility,
44
46
  feedbackButton,
45
47
  children,
@@ -191,7 +193,14 @@ export function Structure({
191
193
  isCollapsed ? 'justify-center' : ''
192
194
  )}
193
195
  >
194
- {isCollapsed ? userPopover : actions}
196
+ {isCollapsed ? (
197
+ <div className="flex w-full flex-col items-center gap-1">
198
+ {userPopover}
199
+ {notificationPopover}
200
+ </div>
201
+ ) : (
202
+ actions
203
+ )}
195
204
  </div>
196
205
 
197
206
  {!hideSizeToggle && (
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { RowData } from '@tanstack/react-table';
3
4
  import {
4
5
  DataTable,
5
6
  type DataTableProps,
@@ -8,7 +9,7 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
8
9
  import { useTranslations } from 'next-intl';
9
10
  import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
10
11
 
11
- function CustomDataTableInner<TData, TValue>({
12
+ function CustomDataTableInner<TData extends RowData, TValue>({
12
13
  namespace,
13
14
  hideToolbar,
14
15
  hidePagination,
@@ -119,7 +120,7 @@ function CustomDataTableInner<TData, TValue>({
119
120
  );
120
121
  }
121
122
 
122
- export function CustomDataTable<TData, TValue>(
123
+ export function CustomDataTable<TData extends RowData, TValue>(
123
124
  props: DataTableProps<TData, TValue>
124
125
  ) {
125
126
  return (
@@ -1,4 +1,4 @@
1
- import type { Column } from '@tanstack/react-table';
1
+ import type { RowData } from '@tanstack/react-table';
2
2
  import { ArrowDown, ArrowUp, ChevronDown, EyeOff } from '@tuturuuu/icons';
3
3
  import { cn } from '@tuturuuu/utils/format';
4
4
  import type React from 'react';
@@ -10,15 +10,16 @@ import {
10
10
  DropdownMenuSeparator,
11
11
  DropdownMenuTrigger,
12
12
  } from '../../dropdown-menu';
13
+ import type { Column } from './data-table';
13
14
 
14
- interface DataTableColumnHeaderProps<TData, TValue>
15
+ interface DataTableColumnHeaderProps<TData extends RowData, TValue>
15
16
  extends React.HTMLAttributes<HTMLDivElement> {
16
17
  t: any;
17
18
  column: Column<TData, TValue>;
18
19
  title?: string;
19
20
  }
20
21
 
21
- export function DataTableColumnHeader<TData, TValue>({
22
+ export function DataTableColumnHeader<TData extends RowData, TValue>({
22
23
  t,
23
24
  column,
24
25
  title,
@@ -1,4 +1,4 @@
1
- import type { Column } from '@tanstack/react-table';
1
+ import type { RowData } from '@tanstack/react-table';
2
2
  import { Check, PlusCircle } from '@tuturuuu/icons';
3
3
  import { cn } from '@tuturuuu/utils/format';
4
4
  import type * as React from 'react';
@@ -15,8 +15,9 @@ import {
15
15
  } from '../../command';
16
16
  import { Popover, PopoverContent, PopoverTrigger } from '../../popover';
17
17
  import { Separator } from '../../separator';
18
+ import type { Column } from './data-table';
18
19
 
19
- interface DataTableFacetedFilterProps<TData, TValue> {
20
+ interface DataTableFacetedFilterProps<TData extends RowData, TValue> {
20
21
  column?: Column<TData, TValue>;
21
22
  title?: string;
22
23
  options: {
@@ -26,7 +27,7 @@ interface DataTableFacetedFilterProps<TData, TValue> {
26
27
  }[];
27
28
  }
28
29
 
29
- export function DataTableFacetedFilter<TData, TValue>({
30
+ export function DataTableFacetedFilter<TData extends RowData, TValue>({
30
31
  column,
31
32
  title,
32
33
  options,
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import type { Table } from '@tanstack/react-table';
3
+ import type { RowData } from '@tanstack/react-table';
4
4
  import {
5
5
  ArrowLeftToLine,
6
6
  ArrowRightToLine,
@@ -17,8 +17,9 @@ import {
17
17
  SelectValue,
18
18
  } from '../../select';
19
19
  import { Separator } from '../../separator';
20
+ import type { Table } from './data-table';
20
21
 
21
- interface DataTablePaginationProps<TData> {
22
+ interface DataTablePaginationProps<TData extends RowData> {
22
23
  table?: Table<TData>;
23
24
  count?: number | null;
24
25
  className?: string;
@@ -31,7 +32,7 @@ interface DataTablePaginationProps<TData> {
31
32
  setParams?: (params: { page?: number; pageSize?: string }) => void;
32
33
  }
33
34
 
34
- export function DataTablePagination<TData>({
35
+ export function DataTablePagination<TData extends RowData>({
35
36
  table,
36
37
  count,
37
38
  className,
@@ -46,9 +47,8 @@ export function DataTablePagination<TData>({
46
47
  // When setParams is provided, we're in server-side pagination mode
47
48
  const isServerSide = !!setParams;
48
49
 
49
- const pageIndex =
50
- pageIndexProp ?? table?.getState().pagination.pageIndex ?? 0;
51
- const pageSize = pageSizeProp ?? table?.getState().pagination.pageSize ?? 10;
50
+ const pageIndex = pageIndexProp ?? table?.state.pagination.pageIndex ?? 0;
51
+ const pageSize = pageSizeProp ?? table?.state.pagination.pageSize ?? 10;
52
52
  const pageCount = pageCountProp ?? table?.getPageCount() ?? 0;
53
53
 
54
54
  // filter duplicate and sort sizes
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
 
3
- import type { Table } from '@tanstack/react-table';
3
+ import type { RowData } from '@tanstack/react-table';
4
4
  import { Download, RotateCcw, Upload } from '@tuturuuu/icons';
5
5
  import { Dialog, DialogContent, DialogTrigger } from '@tuturuuu/ui/dialog';
6
6
  import type { ReactNode } from 'react';
7
7
  import { Button } from '../../button';
8
8
  import SearchBar from '../search-bar';
9
+ import type { Table } from './data-table';
9
10
  import { DataTableCreateButton } from './data-table-create-button';
10
11
  import { DataTableRefreshButton } from './data-table-refresh-button';
11
12
  import { DataTableViewOptions } from './data-table-view-options';
@@ -19,7 +20,7 @@ type DataTableTranslator = ((key: string) => string) & {
19
20
  has?: (key: string) => boolean;
20
21
  };
21
22
 
22
- interface DataTableToolbarProps<TData> {
23
+ interface DataTableToolbarProps<TData extends RowData> {
23
24
  hasData: boolean;
24
25
  newObjectTitle?: string;
25
26
  editContent?: ReactNode;
@@ -42,7 +43,7 @@ interface DataTableToolbarProps<TData> {
42
43
  resetParams: () => void;
43
44
  }
44
45
 
45
- export function DataTableToolbar<TData>({
46
+ export function DataTableToolbar<TData extends RowData>({
46
47
  hasData,
47
48
  newObjectTitle,
48
49
  editContent,
@@ -65,8 +66,7 @@ export function DataTableToolbar<TData>({
65
66
  const isFiltered =
66
67
  isFilteredProp !== undefined
67
68
  ? isFilteredProp
68
- : table.getState().columnFilters.length > 0 ||
69
- (defaultQuery?.length || 0) > 0;
69
+ : table.state.columnFilters.length > 0 || (defaultQuery?.length || 0) > 0;
70
70
 
71
71
  return (
72
72
  <div className="flex flex-col items-start justify-between gap-2 lg:flex-row">
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
4
- import type { Table } from '@tanstack/react-table';
4
+ import type { RowData } from '@tanstack/react-table';
5
5
  import { Settings2, UserCog } from '@tuturuuu/icons';
6
6
  import { Fragment } from 'react';
7
7
  import { Button } from '../../button';
@@ -13,15 +13,16 @@ import {
13
13
  DropdownMenuSeparator,
14
14
  } from '../../dropdown-menu';
15
15
  import { ScrollArea } from '../../scroll-area';
16
+ import type { Table } from './data-table';
16
17
 
17
- interface DataTableViewOptionsProps<TData> {
18
+ interface DataTableViewOptionsProps<TData extends RowData> {
18
19
  table: Table<TData>;
19
20
  extraColumns?: any[];
20
21
  namespace: string | undefined;
21
22
  t?: any;
22
23
  }
23
24
 
24
- export function DataTableViewOptions<TData>({
25
+ export function DataTableViewOptions<TData extends RowData>({
25
26
  t,
26
27
  namespace,
27
28
  table,