@open-mercato/core 0.5.1-develop.2797.c1d2a513ed → 0.5.1-develop.2802.9223828f7f

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.
@@ -0,0 +1,235 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
6
+ import { flash } from "@open-mercato/ui/backend/FlashMessages";
7
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
8
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
9
+ import { toErrorMessage } from "./message-detail/utils.js";
10
+ const MESSAGE_BULK_REQUESTS = {
11
+ markRead: {
12
+ method: "PUT",
13
+ buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/read`,
14
+ successKey: "messages.bulk.flash.markReadSuccess",
15
+ successFallback: "{count} messages marked as read.",
16
+ errorKey: "messages.errors.stateChangeFailed",
17
+ errorFallback: "Failed to update message state."
18
+ },
19
+ markUnread: {
20
+ method: "DELETE",
21
+ buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/read`,
22
+ successKey: "messages.bulk.flash.markUnreadSuccess",
23
+ successFallback: "{count} messages marked as unread.",
24
+ errorKey: "messages.errors.stateChangeFailed",
25
+ errorFallback: "Failed to update message state."
26
+ },
27
+ archive: {
28
+ method: "PUT",
29
+ buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/archive`,
30
+ successKey: "messages.bulk.flash.archiveSuccess",
31
+ successFallback: "{count} messages archived.",
32
+ errorKey: "messages.errors.stateChangeFailed",
33
+ errorFallback: "Failed to update message state."
34
+ },
35
+ delete: {
36
+ method: "DELETE",
37
+ buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}`,
38
+ successKey: "messages.bulk.flash.deleteSuccess",
39
+ successFallback: "{count} messages deleted.",
40
+ errorKey: "messages.errors.deleteFailed",
41
+ errorFallback: "Failed to delete message."
42
+ }
43
+ };
44
+ function normalizeSelectionScopeValue(value) {
45
+ if (value == null) return void 0;
46
+ if (typeof value === "string") {
47
+ const trimmed = value.trim();
48
+ return trimmed.length > 0 ? trimmed : void 0;
49
+ }
50
+ if (Array.isArray(value)) {
51
+ const normalized = value.map((item) => normalizeSelectionScopeValue(item)).filter((item) => item !== void 0);
52
+ return normalized.length > 0 ? normalized : void 0;
53
+ }
54
+ if (typeof value === "object") {
55
+ const normalizedEntries = Object.entries(value).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)).map(([key, nestedValue]) => [key, normalizeSelectionScopeValue(nestedValue)]).filter(([, nestedValue]) => nestedValue !== void 0);
56
+ if (normalizedEntries.length === 0) return void 0;
57
+ return Object.fromEntries(normalizedEntries);
58
+ }
59
+ return value;
60
+ }
61
+ function buildMessageSelectionScopeKey(folder, page, search, filterValues) {
62
+ return JSON.stringify({
63
+ folder,
64
+ page,
65
+ search: search.trim(),
66
+ filters: normalizeSelectionScopeValue(filterValues) ?? {}
67
+ });
68
+ }
69
+ async function runWithConcurrency(items, limit, worker) {
70
+ if (items.length === 0) return [];
71
+ const results = new Array(items.length);
72
+ let nextIndex = 0;
73
+ const runWorker = async () => {
74
+ while (nextIndex < items.length) {
75
+ const currentIndex = nextIndex;
76
+ nextIndex += 1;
77
+ try {
78
+ await worker(items[currentIndex]);
79
+ results[currentIndex] = { status: "fulfilled", value: void 0 };
80
+ } catch (error) {
81
+ results[currentIndex] = { status: "rejected", reason: error };
82
+ }
83
+ }
84
+ };
85
+ await Promise.all(
86
+ Array.from({ length: Math.min(limit, items.length) }, () => runWorker())
87
+ );
88
+ return results;
89
+ }
90
+ function useMessagesInboxBulkActions({
91
+ folder,
92
+ page,
93
+ search,
94
+ filterValues
95
+ }) {
96
+ const t = useT();
97
+ const queryClient = useQueryClient();
98
+ const { confirm, ConfirmDialogElement } = useConfirmDialog();
99
+ const { runMutation, retryLastMutation } = useGuardedMutation({
100
+ contextId: "messages-inbox-bulk-actions"
101
+ });
102
+ const selectionScopeKey = React.useMemo(
103
+ () => buildMessageSelectionScopeKey(folder, page, search, filterValues),
104
+ [filterValues, folder, page, search]
105
+ );
106
+ const injectionContext = React.useMemo(
107
+ () => ({
108
+ folder,
109
+ page,
110
+ search: search.trim(),
111
+ filters: filterValues,
112
+ retryLastMutation
113
+ }),
114
+ [filterValues, folder, page, retryLastMutation, search]
115
+ );
116
+ const executeBulkAction = React.useCallback(async (actionId, selectedRows) => {
117
+ const messageIds = selectedRows.map((row) => row.id).filter((id) => id.trim().length > 0);
118
+ if (messageIds.length === 0) return false;
119
+ if (actionId === "delete") {
120
+ const confirmed = await confirm({
121
+ title: t("messages.bulk.delete.title", "Delete {count} messages?", { count: messageIds.length }),
122
+ description: t("messages.bulk.delete.description", "This removes the selected messages from your view."),
123
+ confirmText: t("messages.actions.delete", "Delete"),
124
+ variant: "destructive"
125
+ });
126
+ if (!confirmed) return false;
127
+ }
128
+ const requestConfig = MESSAGE_BULK_REQUESTS[actionId];
129
+ try {
130
+ const summary = await runMutation({
131
+ operation: async () => {
132
+ const results = await runWithConcurrency(messageIds, 5, async (messageId) => {
133
+ const call = await apiCall(requestConfig.buildUrl(messageId), {
134
+ method: requestConfig.method
135
+ });
136
+ if (!call.ok) {
137
+ throw new Error(
138
+ toErrorMessage(call.result) ?? t(requestConfig.errorKey, requestConfig.errorFallback)
139
+ );
140
+ }
141
+ });
142
+ const failed = results.filter((result) => result.status === "rejected").length;
143
+ const succeeded = results.length - failed;
144
+ if (succeeded > 0) {
145
+ await queryClient.invalidateQueries({ queryKey: ["messages", "list"] });
146
+ }
147
+ return {
148
+ action: actionId,
149
+ total: messageIds.length,
150
+ succeeded,
151
+ failed
152
+ };
153
+ },
154
+ context: {
155
+ actionId,
156
+ messageIds,
157
+ folder,
158
+ page,
159
+ search: search.trim(),
160
+ filters: filterValues,
161
+ retryLastMutation
162
+ },
163
+ mutationPayload: {
164
+ actionId,
165
+ messageIds
166
+ }
167
+ });
168
+ if (summary.succeeded === 0) {
169
+ flash(
170
+ t("messages.bulk.flash.failed", "Failed to process {count} messages.", { count: summary.failed }),
171
+ "error"
172
+ );
173
+ return false;
174
+ }
175
+ if (summary.failed > 0) {
176
+ flash(
177
+ t("messages.bulk.flash.partial", "{succeeded} of {total} messages processed; {failed} failed.", {
178
+ succeeded: summary.succeeded,
179
+ total: summary.total,
180
+ failed: summary.failed
181
+ }),
182
+ "warning"
183
+ );
184
+ return true;
185
+ }
186
+ flash(
187
+ t(requestConfig.successKey, requestConfig.successFallback, { count: summary.succeeded }),
188
+ "success"
189
+ );
190
+ return true;
191
+ } catch (error) {
192
+ flash(
193
+ error instanceof Error ? error.message : t(requestConfig.errorKey, requestConfig.errorFallback),
194
+ "error"
195
+ );
196
+ return false;
197
+ }
198
+ }, [confirm, filterValues, folder, page, queryClient, retryLastMutation, runMutation, search, t]);
199
+ const bulkActions = React.useMemo(
200
+ () => folder === "inbox" ? [
201
+ {
202
+ id: "messages-mark-read",
203
+ label: t("messages.actions.markRead", "Mark read"),
204
+ onExecute: (selectedRows) => executeBulkAction("markRead", selectedRows)
205
+ },
206
+ {
207
+ id: "messages-mark-unread",
208
+ label: t("messages.actions.markUnread", "Mark unread"),
209
+ onExecute: (selectedRows) => executeBulkAction("markUnread", selectedRows)
210
+ },
211
+ {
212
+ id: "messages-archive",
213
+ label: t("messages.actions.archive", "Archive"),
214
+ onExecute: (selectedRows) => executeBulkAction("archive", selectedRows)
215
+ },
216
+ {
217
+ id: "messages-delete",
218
+ label: t("messages.actions.delete", "Delete"),
219
+ destructive: true,
220
+ onExecute: (selectedRows) => executeBulkAction("delete", selectedRows)
221
+ }
222
+ ] : void 0,
223
+ [executeBulkAction, folder, t]
224
+ );
225
+ return {
226
+ bulkActions,
227
+ selectionScopeKey,
228
+ injectionContext,
229
+ ConfirmDialogElement
230
+ };
231
+ }
232
+ export {
233
+ useMessagesInboxBulkActions
234
+ };
235
+ //# sourceMappingURL=useMessagesInboxBulkActions.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/messages/components/useMessagesInboxBulkActions.ts"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { BulkAction } from '@open-mercato/ui/backend/DataTable'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { toErrorMessage } from './message-detail/utils'\n\nexport type MessageFolder = 'inbox' | 'sent' | 'drafts' | 'archived' | 'all'\n\ntype MessageBulkActionId = 'markRead' | 'markUnread' | 'archive' | 'delete'\n\ntype BulkExecutionSummary = {\n action: MessageBulkActionId\n total: number\n succeeded: number\n failed: number\n}\n\ntype MessageBulkRequestConfig = {\n method: 'PUT' | 'DELETE'\n buildUrl: (messageId: string) => string\n successKey: string\n successFallback: string\n errorKey: string\n errorFallback: string\n}\n\ntype MessageInboxBulkMutationContext = {\n actionId: MessageBulkActionId\n messageIds: string[]\n folder: MessageFolder\n page: number\n search: string\n filters: FilterValues\n retryLastMutation: () => Promise<boolean>\n}\n\ntype UseMessagesInboxBulkActionsInput = {\n folder: MessageFolder\n page: number\n search: string\n filterValues: FilterValues\n}\n\ntype MessageInboxBulkRow = {\n id: string\n}\n\nconst MESSAGE_BULK_REQUESTS: Record<MessageBulkActionId, MessageBulkRequestConfig> = {\n markRead: {\n method: 'PUT',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/read`,\n successKey: 'messages.bulk.flash.markReadSuccess',\n successFallback: '{count} messages marked as read.',\n errorKey: 'messages.errors.stateChangeFailed',\n errorFallback: 'Failed to update message state.',\n },\n markUnread: {\n method: 'DELETE',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/read`,\n successKey: 'messages.bulk.flash.markUnreadSuccess',\n successFallback: '{count} messages marked as unread.',\n errorKey: 'messages.errors.stateChangeFailed',\n errorFallback: 'Failed to update message state.',\n },\n archive: {\n method: 'PUT',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/archive`,\n successKey: 'messages.bulk.flash.archiveSuccess',\n successFallback: '{count} messages archived.',\n errorKey: 'messages.errors.stateChangeFailed',\n errorFallback: 'Failed to update message state.',\n },\n delete: {\n method: 'DELETE',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}`,\n successKey: 'messages.bulk.flash.deleteSuccess',\n successFallback: '{count} messages deleted.',\n errorKey: 'messages.errors.deleteFailed',\n errorFallback: 'Failed to delete message.',\n },\n}\n\nfunction normalizeSelectionScopeValue(value: unknown): unknown {\n if (value == null) return undefined\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n }\n if (Array.isArray(value)) {\n const normalized = value\n .map((item) => normalizeSelectionScopeValue(item))\n .filter((item) => item !== undefined)\n return normalized.length > 0 ? normalized : undefined\n }\n if (typeof value === 'object') {\n const normalizedEntries = Object.entries(value)\n .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))\n .map(([key, nestedValue]) => [key, normalizeSelectionScopeValue(nestedValue)] as const)\n .filter(([, nestedValue]) => nestedValue !== undefined)\n if (normalizedEntries.length === 0) return undefined\n return Object.fromEntries(normalizedEntries)\n }\n return value\n}\n\nfunction buildMessageSelectionScopeKey(\n folder: MessageFolder,\n page: number,\n search: string,\n filterValues: FilterValues,\n): string {\n return JSON.stringify({\n folder,\n page,\n search: search.trim(),\n filters: normalizeSelectionScopeValue(filterValues) ?? {},\n })\n}\n\nasync function runWithConcurrency<TItem>(\n items: TItem[],\n limit: number,\n worker: (item: TItem) => Promise<void>,\n): Promise<PromiseSettledResult<void>[]> {\n if (items.length === 0) return []\n\n const results: PromiseSettledResult<void>[] = new Array(items.length)\n let nextIndex = 0\n\n const runWorker = async () => {\n while (nextIndex < items.length) {\n const currentIndex = nextIndex\n nextIndex += 1\n try {\n await worker(items[currentIndex])\n results[currentIndex] = { status: 'fulfilled', value: undefined }\n } catch (error) {\n results[currentIndex] = { status: 'rejected', reason: error }\n }\n }\n }\n\n await Promise.all(\n Array.from({ length: Math.min(limit, items.length) }, () => runWorker()),\n )\n\n return results\n}\n\nexport function useMessagesInboxBulkActions<T extends MessageInboxBulkRow>({\n folder,\n page,\n search,\n filterValues,\n}: UseMessagesInboxBulkActionsInput): {\n bulkActions: BulkAction<T>[] | undefined\n selectionScopeKey: string\n injectionContext: Record<string, unknown>\n ConfirmDialogElement: React.ReactNode\n} {\n const t = useT()\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const { runMutation, retryLastMutation } = useGuardedMutation<MessageInboxBulkMutationContext>({\n contextId: 'messages-inbox-bulk-actions',\n })\n\n const selectionScopeKey = React.useMemo(\n () => buildMessageSelectionScopeKey(folder, page, search, filterValues),\n [filterValues, folder, page, search],\n )\n const injectionContext = React.useMemo<Record<string, unknown>>(\n () => ({\n folder,\n page,\n search: search.trim(),\n filters: filterValues,\n retryLastMutation,\n }),\n [filterValues, folder, page, retryLastMutation, search],\n )\n\n const executeBulkAction = React.useCallback(async (\n actionId: MessageBulkActionId,\n selectedRows: T[],\n ): Promise<boolean> => {\n const messageIds = selectedRows.map((row) => row.id).filter((id) => id.trim().length > 0)\n if (messageIds.length === 0) return false\n\n if (actionId === 'delete') {\n const confirmed = await confirm({\n title: t('messages.bulk.delete.title', 'Delete {count} messages?', { count: messageIds.length }),\n description: t('messages.bulk.delete.description', 'This removes the selected messages from your view.'),\n confirmText: t('messages.actions.delete', 'Delete'),\n variant: 'destructive',\n })\n if (!confirmed) return false\n }\n\n const requestConfig = MESSAGE_BULK_REQUESTS[actionId]\n\n try {\n const summary = await runMutation<BulkExecutionSummary>({\n operation: async () => {\n const results = await runWithConcurrency(messageIds, 5, async (messageId) => {\n const call = await apiCall<{ ok?: boolean }>(requestConfig.buildUrl(messageId), {\n method: requestConfig.method,\n })\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t(requestConfig.errorKey, requestConfig.errorFallback),\n )\n }\n })\n\n const failed = results.filter((result) => result.status === 'rejected').length\n const succeeded = results.length - failed\n\n if (succeeded > 0) {\n await queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })\n }\n\n return {\n action: actionId,\n total: messageIds.length,\n succeeded,\n failed,\n }\n },\n context: {\n actionId,\n messageIds,\n folder,\n page,\n search: search.trim(),\n filters: filterValues,\n retryLastMutation,\n },\n mutationPayload: {\n actionId,\n messageIds,\n },\n })\n\n if (summary.succeeded === 0) {\n flash(\n t('messages.bulk.flash.failed', 'Failed to process {count} messages.', { count: summary.failed }),\n 'error',\n )\n return false\n }\n\n if (summary.failed > 0) {\n flash(\n t('messages.bulk.flash.partial', '{succeeded} of {total} messages processed; {failed} failed.', {\n succeeded: summary.succeeded,\n total: summary.total,\n failed: summary.failed,\n }),\n 'warning',\n )\n return true\n }\n\n flash(\n t(requestConfig.successKey, requestConfig.successFallback, { count: summary.succeeded }),\n 'success',\n )\n return true\n } catch (error) {\n flash(\n error instanceof Error\n ? error.message\n : t(requestConfig.errorKey, requestConfig.errorFallback),\n 'error',\n )\n return false\n }\n }, [confirm, filterValues, folder, page, queryClient, retryLastMutation, runMutation, search, t])\n\n const bulkActions = React.useMemo<BulkAction<T>[] | undefined>(\n () => folder === 'inbox'\n ? [\n {\n id: 'messages-mark-read',\n label: t('messages.actions.markRead', 'Mark read'),\n onExecute: (selectedRows: T[]) => executeBulkAction('markRead', selectedRows),\n },\n {\n id: 'messages-mark-unread',\n label: t('messages.actions.markUnread', 'Mark unread'),\n onExecute: (selectedRows: T[]) => executeBulkAction('markUnread', selectedRows),\n },\n {\n id: 'messages-archive',\n label: t('messages.actions.archive', 'Archive'),\n onExecute: (selectedRows: T[]) => executeBulkAction('archive', selectedRows),\n },\n {\n id: 'messages-delete',\n label: t('messages.actions.delete', 'Delete'),\n destructive: true,\n onExecute: (selectedRows: T[]) => executeBulkAction('delete', selectedRows),\n },\n ]\n : undefined,\n [executeBulkAction, folder, t],\n )\n\n return {\n bulkActions,\n selectionScopeKey,\n injectionContext,\n ConfirmDialogElement,\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.5.1-develop.2797.c1d2a513ed",
3
+ "version": "0.5.1-develop.2802.9223828f7f",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -237,10 +237,10 @@
237
237
  "ts-pattern": "^5.0.0"
238
238
  },
239
239
  "peerDependencies": {
240
- "@open-mercato/shared": "0.5.1-develop.2797.c1d2a513ed"
240
+ "@open-mercato/shared": "0.5.1-develop.2802.9223828f7f"
241
241
  },
242
242
  "devDependencies": {
243
- "@open-mercato/shared": "0.5.1-develop.2797.c1d2a513ed",
243
+ "@open-mercato/shared": "0.5.1-develop.2802.9223828f7f",
244
244
  "@testing-library/dom": "^10.4.1",
245
245
  "@testing-library/jest-dom": "^6.9.1",
246
246
  "@testing-library/react": "^16.3.1",
@@ -642,7 +642,6 @@ const setPassword: ModuleCli = {
642
642
  },
643
643
  }
644
644
 
645
- // Export the full CLI list
646
645
  const syncRoleAcls: ModuleCli = {
647
646
  command: 'sync-role-acls',
648
647
  async run(rest) {
@@ -705,4 +704,5 @@ const syncRoleAcls: ModuleCli = {
705
704
  },
706
705
  }
707
706
 
707
+ // Export the full CLI list
708
708
  export default [addUser, seedRoles, syncRoleAcls, rotateEncryptionKey, addOrganization, setupApp, listOrganizations, listTenants, listUsers, setPassword]
@@ -16,8 +16,8 @@ import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
16
16
  import { Archive, ChevronDown, FilePenLine, Inbox, Layers, Send } from 'lucide-react'
17
17
  import { getMessageUiComponentRegistry } from './utils/typeUiRegistry'
18
18
  import { DefaultMessageListItem } from './defaults/DefaultMessageListItem'
19
-
20
- type MessageFolder = 'inbox' | 'sent' | 'drafts' | 'archived' | 'all'
19
+ import { toErrorMessage } from './message-detail/utils'
20
+ import { useMessagesInboxBulkActions, type MessageFolder } from './useMessagesInboxBulkActions'
21
21
 
22
22
  type MessageListItem = {
23
23
  id: string
@@ -63,29 +63,6 @@ type UserListItem = {
63
63
  name?: string | null
64
64
  }
65
65
 
66
- function toErrorMessage(payload: unknown): string | null {
67
- if (!payload) return null
68
- if (typeof payload === 'string') return payload
69
- if (Array.isArray(payload)) {
70
- for (const item of payload) {
71
- const nested = toErrorMessage(item)
72
- if (nested) return nested
73
- }
74
- return null
75
- }
76
- if (typeof payload === 'object') {
77
- const record = payload as Record<string, unknown>
78
- return (
79
- toErrorMessage(record.error)
80
- ?? toErrorMessage(record.message)
81
- ?? toErrorMessage(record.detail)
82
- ?? toErrorMessage(record.details)
83
- ?? null
84
- )
85
- }
86
- return null
87
- }
88
-
89
66
  export function MessagesInboxPageClient() {
90
67
  const router = useRouter()
91
68
  const t = useT()
@@ -108,6 +85,12 @@ export function MessagesInboxPageClient() {
108
85
  const pageSize = 20
109
86
  const folderMenuRef = React.useRef<HTMLDivElement | null>(null)
110
87
  const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])
88
+ const { bulkActions, selectionScopeKey, injectionContext, ConfirmDialogElement } = useMessagesInboxBulkActions<MessageListItem>({
89
+ folder,
90
+ page,
91
+ search,
92
+ filterValues,
93
+ })
111
94
 
112
95
  const listQuery = useQuery({
113
96
  queryKey: [
@@ -393,6 +376,8 @@ export function MessagesInboxPageClient() {
393
376
  title={t('messages.title', 'Messages')}
394
377
  columns={columns}
395
378
  data={rows}
379
+ bulkActions={bulkActions}
380
+ selectionScopeKey={selectionScopeKey}
396
381
  searchValue={search}
397
382
  onSearchChange={(value) => {
398
383
  setSearch(value)
@@ -409,6 +394,7 @@ export function MessagesInboxPageClient() {
409
394
  setFilterValues({})
410
395
  setPage(1)
411
396
  }}
397
+ injectionContext={injectionContext}
412
398
  isLoading={listQuery.isLoading || listQuery.isFetching}
413
399
  pagination={{
414
400
  page,
@@ -443,12 +429,14 @@ export function MessagesInboxPageClient() {
443
429
  const Icon = option.icon
444
430
  const isActive = option.id === folder
445
431
  return (
446
- <button
432
+ <Button
447
433
  key={option.id}
448
434
  type="button"
435
+ variant="ghost"
436
+ size="sm"
449
437
  role="menuitemradio"
450
438
  aria-checked={isActive}
451
- className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm hover:bg-accent ${isActive ? 'bg-accent/60' : ''}`}
439
+ className={`w-full justify-start h-auto px-2 py-1.5 text-sm font-normal ${isActive ? 'bg-accent/60' : ''}`}
452
440
  onClick={() => {
453
441
  setFolder(option.id)
454
442
  setPage(1)
@@ -457,7 +445,7 @@ export function MessagesInboxPageClient() {
457
445
  >
458
446
  <Icon className="h-4 w-4" aria-hidden />
459
447
  <span>{option.label}</span>
460
- </button>
448
+ </Button>
461
449
  )
462
450
  })}
463
451
  </div>
@@ -471,9 +459,9 @@ export function MessagesInboxPageClient() {
471
459
  onRowClick={(row) => {
472
460
  router.push(`/backend/messages/${row.id}`)
473
461
  }}
474
- perspective={{ tableId: 'messages.inbox' }}
475
462
  embedded
476
463
  />
464
+ {ConfirmDialogElement}
477
465
  </div>
478
466
  )
479
467
  }