@open-mercato/ui 0.6.2-develop.3461.1.605f31c2c9 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/ai/AiChat.js +130 -22
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiChatSessions.js +73 -3
- package/dist/ai/AiChatSessions.js.map +2 -2
- package/dist/ai/conversation-store.js +149 -0
- package/dist/ai/conversation-store.js.map +7 -0
- package/dist/ai/parts/MutationPreviewCard.js +4 -3
- package/dist/ai/parts/MutationPreviewCard.js.map +2 -2
- package/dist/ai/parts/MutationResultCard.js +9 -16
- package/dist/ai/parts/MutationResultCard.js.map +2 -2
- package/dist/ai/useAiChat.js +80 -4
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/AuthSessionGuard.js +99 -0
- package/dist/backend/AuthSessionGuard.js.map +7 -0
- package/dist/backend/CrudForm.js +2 -1
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/FilterOverlay.js +1 -1
- package/dist/backend/FilterOverlay.js.map +1 -1
- package/dist/backend/dashboard/widgetRegistry.js +2 -1
- package/dist/backend/dashboard/widgetRegistry.js.map +2 -2
- package/dist/backend/date-range/dateRanges.js +16 -18
- package/dist/backend/date-range/dateRanges.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js +3 -1
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +1 -1
- package/dist/backend/detail/InlineEditors.js.map +1 -1
- package/dist/backend/injection/useAppEvent.js +2 -7
- package/dist/backend/injection/useAppEvent.js.map +2 -2
- package/dist/backend/injection/widgetRegistry.js +2 -1
- package/dist/backend/injection/widgetRegistry.js.map +2 -2
- package/dist/backend/messages/useMessageCompose.js +62 -5
- package/dist/backend/messages/useMessageCompose.js.map +2 -2
- package/dist/backend/messages/useMessageComposeOperations.js +64 -5
- package/dist/backend/messages/useMessageComposeOperations.js.map +2 -2
- package/dist/backend/schedule/ScheduleCalendar.js +11 -1
- package/dist/backend/schedule/ScheduleCalendar.js.map +2 -2
- package/dist/backend/schedule/ScheduleToolbar.js +11 -1
- package/dist/backend/schedule/ScheduleToolbar.js.map +2 -2
- package/dist/primitives/calendar.js +1 -1
- package/dist/primitives/calendar.js.map +1 -1
- package/dist/primitives/date-format.js +1 -1
- package/dist/primitives/date-format.js.map +1 -1
- package/dist/primitives/date-picker-helpers.js +7 -9
- package/dist/primitives/date-picker-helpers.js.map +2 -2
- package/dist/primitives/date-picker.js +1 -1
- package/dist/primitives/date-picker.js.map +2 -2
- package/dist/primitives/date-range-picker.js +1 -1
- package/dist/primitives/date-range-picker.js.map +2 -2
- package/jest.config.cjs +4 -1
- package/package.json +6 -7
- package/src/ai/AiChat.tsx +138 -22
- package/src/ai/AiChatSessions.tsx +89 -9
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +1 -1
- package/src/ai/__tests__/AiChat.conversation.test.tsx +86 -0
- package/src/ai/__tests__/AiChat.test.tsx +17 -2
- package/src/ai/__tests__/conversation-store.test.ts +29 -0
- package/src/ai/conversation-store.ts +246 -0
- package/src/ai/parts/MutationPreviewCard.tsx +25 -20
- package/src/ai/parts/MutationResultCard.tsx +15 -27
- package/src/ai/parts/__tests__/MutationPreviewCard.test.tsx +26 -1
- package/src/ai/parts/__tests__/MutationResultCard.test.tsx +16 -1
- package/src/ai/useAiChat.ts +97 -8
- package/src/backend/AuthSessionGuard.tsx +114 -0
- package/src/backend/CrudForm.tsx +3 -2
- package/src/backend/FilterOverlay.tsx +1 -1
- package/src/backend/__tests__/AppShell.test.tsx +1 -1
- package/src/backend/__tests__/AuthSessionGuard.test.tsx +150 -0
- package/src/backend/__tests__/SidebarCustomizationEditor.test.tsx +8 -1
- package/src/backend/dashboard/widgetRegistry.ts +2 -1
- package/src/backend/date-range/dateRanges.ts +16 -18
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/backend/detail/InlineEditors.tsx +1 -1
- package/src/backend/injection/__tests__/useAppEvent.test.ts +16 -0
- package/src/backend/injection/useAppEvent.ts +2 -9
- package/src/backend/injection/widgetRegistry.ts +2 -1
- package/src/backend/messages/__tests__/MessageComposer.test.tsx +77 -0
- package/src/backend/messages/useMessageCompose.ts +64 -5
- package/src/backend/messages/useMessageComposeOperations.ts +70 -4
- package/src/backend/schedule/ScheduleCalendar.tsx +11 -1
- package/src/backend/schedule/ScheduleToolbar.tsx +11 -1
- package/src/primitives/calendar.tsx +2 -2
- package/src/primitives/date-format.ts +2 -2
- package/src/primitives/date-picker-helpers.ts +7 -9
- package/src/primitives/date-picker.tsx +2 -2
- package/src/primitives/date-range-picker.tsx +2 -2
|
@@ -533,7 +533,7 @@ export function AttachmentMetadataDialog({ open, onOpenChange, item, availableTa
|
|
|
533
533
|
{item.fileName}
|
|
534
534
|
</div>
|
|
535
535
|
<div className="text-xs text-muted-foreground">
|
|
536
|
-
{formatFileSize(item.fileSize)} \u2022 {item.partitionTitle ?? item.partitionCode}
|
|
536
|
+
{formatFileSize(item.fileSize)} {'\u2022'} {item.partitionTitle ?? item.partitionCode}
|
|
537
537
|
</div>
|
|
538
538
|
</div>
|
|
539
539
|
{downloadUrl ? (
|
|
@@ -17,7 +17,7 @@ import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
|
17
17
|
import { DatePicker } from '@open-mercato/ui/primitives/date-picker'
|
|
18
18
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
19
19
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
20
|
-
import { format } from 'date-fns'
|
|
20
|
+
import { format } from 'date-fns/format'
|
|
21
21
|
import { LoadingMessage } from './LoadingMessage'
|
|
22
22
|
import { mapCrudServerErrorToFormErrors } from '../utils/serverErrors'
|
|
23
23
|
import { MarkdownPreview } from '../markdown'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { matchesPattern } from '../useAppEvent'
|
|
2
|
+
|
|
3
|
+
describe('matchesPattern', () => {
|
|
4
|
+
it('matches DOM event wildcard patterns without regex execution', () => {
|
|
5
|
+
expect(matchesPattern('*', 'customers.person.created')).toBe(true)
|
|
6
|
+
expect(matchesPattern('customers.person.*', 'customers.person.created')).toBe(true)
|
|
7
|
+
expect(matchesPattern('customers.*.created', 'customers.person.created')).toBe(true)
|
|
8
|
+
expect(matchesPattern('sales.*', 'customers.person.created')).toBe(false)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('treats regex metacharacters as literal event characters', () => {
|
|
12
|
+
expect(matchesPattern('customers.(person).*', 'customers.(person).created')).toBe(true)
|
|
13
|
+
expect(matchesPattern('customers.+.*', 'customers.person.created')).toBe(false)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import { useEffect, useRef } from 'react'
|
|
3
3
|
import type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'
|
|
4
|
+
import { matchWildcardPattern } from '@open-mercato/shared/lib/patterns/wildcard'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* DOM Event Bridge event name.
|
|
@@ -23,15 +24,7 @@ export const APP_EVENT_DOM_NAME = 'om:event'
|
|
|
23
24
|
* matchesPattern('*', 'anything.here') // true
|
|
24
25
|
*/
|
|
25
26
|
export function matchesPattern(pattern: string, eventId: string): boolean {
|
|
26
|
-
|
|
27
|
-
if (!pattern.includes('*')) return pattern === eventId
|
|
28
|
-
const escapedPattern = pattern
|
|
29
|
-
.replace(/[|\\{}()[\]^$+?.]/g, '\\$&')
|
|
30
|
-
.replace(/\*/g, '.*')
|
|
31
|
-
const regex = new RegExp(
|
|
32
|
-
'^' + escapedPattern + '$',
|
|
33
|
-
)
|
|
34
|
-
return regex.test(eventId)
|
|
27
|
+
return matchWildcardPattern(eventId, pattern)
|
|
35
28
|
}
|
|
36
29
|
|
|
37
30
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ModuleInjectionWidgetEntry } from '@open-mercato/shared/modules/registry'
|
|
2
2
|
import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
|
|
3
|
+
import { applyInjectionWidgetOverridesToEntries } from '@open-mercato/shared/modules/overrides'
|
|
3
4
|
|
|
4
5
|
type Entry = ModuleInjectionWidgetEntry
|
|
5
6
|
|
|
@@ -10,7 +11,7 @@ export function registerInjectionWidgets(entries: Entry[]) {
|
|
|
10
11
|
if (_injectionWidgetEntries !== null && process.env.NODE_ENV === 'development') {
|
|
11
12
|
console.debug('[Bootstrap] Injection widgets re-registered (this may occur during HMR)')
|
|
12
13
|
}
|
|
13
|
-
_injectionWidgetEntries = entries
|
|
14
|
+
_injectionWidgetEntries = applyInjectionWidgetOverridesToEntries(entries)
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function getInjectionWidgets(): Entry[] {
|
|
@@ -121,6 +121,78 @@ describe('MessageComposer draft flow', () => {
|
|
|
121
121
|
expect(flash).toHaveBeenCalledWith('Draft saved.', 'success')
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
+
it('sends an existing draft via PATCH with isDraft=false', async () => {
|
|
125
|
+
renderWithProviders(
|
|
126
|
+
<MessageComposer
|
|
127
|
+
inline
|
|
128
|
+
variant="compose"
|
|
129
|
+
messageId="draft-1"
|
|
130
|
+
defaultValues={{
|
|
131
|
+
recipients: ['11111111-1111-4111-8111-111111111111'],
|
|
132
|
+
subject: 'Existing draft',
|
|
133
|
+
body: 'Existing draft body',
|
|
134
|
+
}}
|
|
135
|
+
/>,
|
|
136
|
+
{ dict: {} },
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Send' }))
|
|
140
|
+
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(apiCall).toHaveBeenCalledWith(
|
|
143
|
+
'/api/messages/draft-1',
|
|
144
|
+
expect.objectContaining({
|
|
145
|
+
method: 'PATCH',
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const updateRequest = (apiCall as jest.Mock).mock.calls.find(
|
|
151
|
+
(call) => call[0] === '/api/messages/draft-1' && call[1]?.method === 'PATCH',
|
|
152
|
+
)
|
|
153
|
+
const payload = JSON.parse(updateRequest?.[1]?.body ?? '{}') as Record<string, unknown>
|
|
154
|
+
expect(payload.isDraft).toBe(false)
|
|
155
|
+
expect(payload.recipients).toEqual([{ userId: '11111111-1111-4111-8111-111111111111', type: 'to' }])
|
|
156
|
+
expect(flash).toHaveBeenCalledWith('Message sent.', 'success')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('saves an existing draft via PATCH without a draft transition flag', async () => {
|
|
160
|
+
renderWithProviders(
|
|
161
|
+
<MessageComposer
|
|
162
|
+
inline
|
|
163
|
+
variant="compose"
|
|
164
|
+
messageId="draft-1"
|
|
165
|
+
defaultValues={{
|
|
166
|
+
subject: 'Existing draft',
|
|
167
|
+
body: 'Existing draft body',
|
|
168
|
+
}}
|
|
169
|
+
/>,
|
|
170
|
+
{ dict: {} },
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Save draft' }))
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(apiCall).toHaveBeenCalledWith(
|
|
177
|
+
'/api/messages/draft-1',
|
|
178
|
+
expect.objectContaining({
|
|
179
|
+
method: 'PATCH',
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const updateRequest = (apiCall as jest.Mock).mock.calls.find(
|
|
185
|
+
(call) => call[0] === '/api/messages/draft-1' && call[1]?.method === 'PATCH',
|
|
186
|
+
)
|
|
187
|
+
const payload = JSON.parse(updateRequest?.[1]?.body ?? '{}') as Record<string, unknown>
|
|
188
|
+
expect(payload).not.toHaveProperty('isDraft')
|
|
189
|
+
expect(payload).toEqual(expect.objectContaining({
|
|
190
|
+
subject: 'Existing draft',
|
|
191
|
+
body: 'Existing draft body',
|
|
192
|
+
}))
|
|
193
|
+
expect(flash).toHaveBeenCalledWith('Draft saved.', 'success')
|
|
194
|
+
})
|
|
195
|
+
|
|
124
196
|
it('keeps the send action disabled after a successful compose submit until the composer resets', async () => {
|
|
125
197
|
let resolveMessagePost!: (value: {
|
|
126
198
|
ok: boolean
|
|
@@ -342,6 +414,11 @@ describe('MessageComposer draft flow', () => {
|
|
|
342
414
|
await waitFor(() => {
|
|
343
415
|
expect(apiCall).toHaveBeenCalledWith(
|
|
344
416
|
expect.stringMatching(/^\/api\/auth\/users\?/),
|
|
417
|
+
expect.objectContaining({
|
|
418
|
+
headers: expect.objectContaining({
|
|
419
|
+
'x-om-forbidden-redirect': '0',
|
|
420
|
+
}),
|
|
421
|
+
}),
|
|
345
422
|
)
|
|
346
423
|
})
|
|
347
424
|
})
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
useComposeSendOperation,
|
|
17
17
|
useForwardSubmitOperation,
|
|
18
18
|
useReplySubmitOperation,
|
|
19
|
+
useSendDraftOperation,
|
|
20
|
+
useUpdateDraftOperation,
|
|
19
21
|
} from './useMessageComposeOperations'
|
|
20
22
|
|
|
21
23
|
function toErrorMessage(payload: unknown): string | null {
|
|
@@ -410,7 +412,17 @@ export function useMessageCompose({
|
|
|
410
412
|
params.set('pageSize', '100')
|
|
411
413
|
// Recipient lookup is filtered in TagsInput because incremental auth user search is unreliable.
|
|
412
414
|
|
|
413
|
-
const call = await apiCall<{ items?: UserListItem[] }>(
|
|
415
|
+
const call = await apiCall<{ items?: UserListItem[] }>(
|
|
416
|
+
`/api/auth/users?${params.toString()}`,
|
|
417
|
+
{
|
|
418
|
+
headers: {
|
|
419
|
+
'x-om-forbidden-redirect': '0',
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
).catch(() => null)
|
|
423
|
+
if (!call) {
|
|
424
|
+
return []
|
|
425
|
+
}
|
|
414
426
|
if (!call.ok) {
|
|
415
427
|
return []
|
|
416
428
|
}
|
|
@@ -477,6 +489,7 @@ export function useMessageCompose({
|
|
|
477
489
|
|
|
478
490
|
const composeDraftOperation = useComposeDraftOperation({
|
|
479
491
|
t,
|
|
492
|
+
messageId,
|
|
480
493
|
messageType,
|
|
481
494
|
priority,
|
|
482
495
|
visibility,
|
|
@@ -514,16 +527,59 @@ export function useMessageCompose({
|
|
|
514
527
|
sendViaEmail,
|
|
515
528
|
})
|
|
516
529
|
|
|
530
|
+
const sendDraftOperation = useSendDraftOperation({
|
|
531
|
+
t,
|
|
532
|
+
messageId: messageId ?? '',
|
|
533
|
+
messageType,
|
|
534
|
+
priority,
|
|
535
|
+
visibility,
|
|
536
|
+
externalEmail,
|
|
537
|
+
recipientIds,
|
|
538
|
+
subject,
|
|
539
|
+
body,
|
|
540
|
+
bodyFormat,
|
|
541
|
+
sendViaEmail,
|
|
542
|
+
contextObject,
|
|
543
|
+
defaultValues,
|
|
544
|
+
contextActionOptions,
|
|
545
|
+
normalizedRequiredActionMode,
|
|
546
|
+
shouldShowContextActions,
|
|
547
|
+
contextActionRequired,
|
|
548
|
+
contextActionType,
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
const updateDraftOperation = useUpdateDraftOperation({
|
|
552
|
+
t,
|
|
553
|
+
messageId: messageId ?? '',
|
|
554
|
+
messageType,
|
|
555
|
+
priority,
|
|
556
|
+
visibility,
|
|
557
|
+
externalEmail,
|
|
558
|
+
recipientIds,
|
|
559
|
+
subject,
|
|
560
|
+
body,
|
|
561
|
+
bodyFormat,
|
|
562
|
+
sendViaEmail,
|
|
563
|
+
contextObject,
|
|
564
|
+
defaultValues,
|
|
565
|
+
contextActionOptions,
|
|
566
|
+
normalizedRequiredActionMode,
|
|
567
|
+
shouldShowContextActions,
|
|
568
|
+
contextActionRequired,
|
|
569
|
+
contextActionType,
|
|
570
|
+
})
|
|
571
|
+
|
|
517
572
|
const handleSubmit = React.useCallback(async ({ saveAsDraft = false }: { saveAsDraft?: boolean } = {}) => {
|
|
518
573
|
if (submitting) return false
|
|
519
574
|
|
|
520
575
|
setSubmitError(null)
|
|
521
576
|
|
|
577
|
+
const isEditingExistingDraft = variant === 'compose' && Boolean(messageId)
|
|
522
578
|
const isComposeDraftSubmit = saveAsDraft && variant === 'compose'
|
|
523
579
|
const operation = isComposeDraftSubmit
|
|
524
|
-
? composeDraftOperation
|
|
580
|
+
? (isEditingExistingDraft ? updateDraftOperation : composeDraftOperation)
|
|
525
581
|
: variant === 'compose'
|
|
526
|
-
? composeSendOperation
|
|
582
|
+
? (isEditingExistingDraft ? sendDraftOperation : composeSendOperation)
|
|
527
583
|
: variant === 'reply'
|
|
528
584
|
? replyOperation
|
|
529
585
|
: forwardOperation
|
|
@@ -556,10 +612,10 @@ export function useMessageCompose({
|
|
|
556
612
|
}
|
|
557
613
|
|
|
558
614
|
if (!shouldReturnFalse) {
|
|
559
|
-
const { endpoint, payload } = operation.buildRequest({ attachmentIds: nextAttachmentIds })
|
|
615
|
+
const { endpoint, method, payload } = operation.buildRequest({ attachmentIds: nextAttachmentIds })
|
|
560
616
|
|
|
561
617
|
const call = await apiCall<{ id?: string }>(endpoint, {
|
|
562
|
-
method
|
|
618
|
+
method,
|
|
563
619
|
headers: { 'Content-Type': 'application/json' },
|
|
564
620
|
body: JSON.stringify(payload),
|
|
565
621
|
})
|
|
@@ -612,11 +668,14 @@ export function useMessageCompose({
|
|
|
612
668
|
forwardOperation,
|
|
613
669
|
inline,
|
|
614
670
|
loadAttachmentIds,
|
|
671
|
+
messageId,
|
|
615
672
|
onOpenChange,
|
|
616
673
|
onSuccess,
|
|
617
674
|
replyOperation,
|
|
675
|
+
sendDraftOperation,
|
|
618
676
|
submitting,
|
|
619
677
|
t,
|
|
678
|
+
updateDraftOperation,
|
|
620
679
|
variant,
|
|
621
680
|
])
|
|
622
681
|
|
|
@@ -16,6 +16,7 @@ type MessageRecipient = {
|
|
|
16
16
|
|
|
17
17
|
type SubmitRequest = {
|
|
18
18
|
endpoint: string
|
|
19
|
+
method: 'POST' | 'PATCH'
|
|
19
20
|
payload: Record<string, unknown>
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -67,6 +68,7 @@ type ForwardOperationParams = {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
type DraftOperationParams = Omit<ComposeOperationParams, 'createableMessageTypes'>
|
|
71
|
+
& { messageId?: string }
|
|
70
72
|
|
|
71
73
|
function isValidEmailAddress(value: string): boolean {
|
|
72
74
|
const email = value.trim()
|
|
@@ -182,6 +184,7 @@ export function useComposeSendOperation(params: ComposeOperationParams): BaseOpe
|
|
|
182
184
|
},
|
|
183
185
|
buildRequest: ({ attachmentIds }) => ({
|
|
184
186
|
endpoint: '/api/messages',
|
|
187
|
+
method: 'POST',
|
|
185
188
|
payload: buildComposePayload(params, { attachmentIds, isDraft: false }),
|
|
186
189
|
}),
|
|
187
190
|
successMessage: params.t('messages.flash.sentSuccess', 'Message sent.'),
|
|
@@ -192,10 +195,24 @@ export function useComposeSendOperation(params: ComposeOperationParams): BaseOpe
|
|
|
192
195
|
export function useComposeDraftOperation(params: DraftOperationParams): BaseOperation {
|
|
193
196
|
return React.useMemo(() => ({
|
|
194
197
|
validate: () => null,
|
|
195
|
-
buildRequest: ({ attachmentIds }) =>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
buildRequest: ({ attachmentIds }) => {
|
|
199
|
+
const payload = buildComposePayload(params, { attachmentIds, isDraft: true })
|
|
200
|
+
if (!params.messageId) {
|
|
201
|
+
return {
|
|
202
|
+
endpoint: '/api/messages',
|
|
203
|
+
method: 'POST',
|
|
204
|
+
payload,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const draftUpdatePayload = { ...payload }
|
|
209
|
+
delete draftUpdatePayload.isDraft
|
|
210
|
+
return {
|
|
211
|
+
endpoint: `/api/messages/${encodeURIComponent(params.messageId)}`,
|
|
212
|
+
method: 'PATCH',
|
|
213
|
+
payload: draftUpdatePayload,
|
|
214
|
+
}
|
|
215
|
+
},
|
|
199
216
|
successMessage: params.t('messages.flash.draftSaved', 'Draft saved.'),
|
|
200
217
|
requiresAttachmentRefresh: true,
|
|
201
218
|
}), [params])
|
|
@@ -214,6 +231,7 @@ export function useReplySubmitOperation(params: ReplyOperationParams): BaseOpera
|
|
|
214
231
|
},
|
|
215
232
|
buildRequest: ({ attachmentIds }) => ({
|
|
216
233
|
endpoint: `/api/messages/${encodeURIComponent(params.messageId ?? '')}/reply`,
|
|
234
|
+
method: 'POST',
|
|
217
235
|
payload: {
|
|
218
236
|
body: params.body,
|
|
219
237
|
bodyFormat: params.bodyFormat,
|
|
@@ -244,6 +262,7 @@ export function useForwardSubmitOperation(params: ForwardOperationParams): BaseO
|
|
|
244
262
|
},
|
|
245
263
|
buildRequest: () => ({
|
|
246
264
|
endpoint: `/api/messages/${encodeURIComponent(params.messageId ?? '')}/forward`,
|
|
265
|
+
method: 'POST',
|
|
247
266
|
payload: {
|
|
248
267
|
recipients: mapRecipients(params.recipientIds),
|
|
249
268
|
body: params.body,
|
|
@@ -256,4 +275,51 @@ export function useForwardSubmitOperation(params: ForwardOperationParams): BaseO
|
|
|
256
275
|
}), [params])
|
|
257
276
|
}
|
|
258
277
|
|
|
278
|
+
type SendDraftOperationParams = DraftOperationParams & { messageId: string }
|
|
279
|
+
|
|
280
|
+
export function useSendDraftOperation(params: SendDraftOperationParams): BaseOperation {
|
|
281
|
+
return React.useMemo(() => ({
|
|
282
|
+
validate: () => {
|
|
283
|
+
if (params.visibility !== 'public' && params.recipientIds.length === 0) {
|
|
284
|
+
return params.t('messages.errors.noRecipients', 'Please add at least one recipient.')
|
|
285
|
+
}
|
|
286
|
+
if (params.visibility === 'public' && !isValidEmailAddress(params.externalEmail.trim())) {
|
|
287
|
+
return params.t('messages.errors.noExternalEmail', 'Please enter a valid external email.')
|
|
288
|
+
}
|
|
289
|
+
if (!params.subject.trim()) {
|
|
290
|
+
return params.t('messages.errors.noSubject', 'Please enter a subject.')
|
|
291
|
+
}
|
|
292
|
+
if (!params.body.trim()) {
|
|
293
|
+
return params.t('messages.errors.noBody', 'Please enter a message.')
|
|
294
|
+
}
|
|
295
|
+
return null
|
|
296
|
+
},
|
|
297
|
+
buildRequest: ({ attachmentIds }) => ({
|
|
298
|
+
endpoint: `/api/messages/${encodeURIComponent(params.messageId)}`,
|
|
299
|
+
method: 'PATCH' as const,
|
|
300
|
+
payload: buildComposePayload(params, { attachmentIds, isDraft: false }),
|
|
301
|
+
}),
|
|
302
|
+
successMessage: params.t('messages.flash.sentSuccess', 'Message sent.'),
|
|
303
|
+
requiresAttachmentRefresh: true,
|
|
304
|
+
}), [params])
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
type UpdateDraftOperationParams = DraftOperationParams & { messageId: string }
|
|
308
|
+
|
|
309
|
+
export function useUpdateDraftOperation(params: UpdateDraftOperationParams): BaseOperation {
|
|
310
|
+
return React.useMemo(() => ({
|
|
311
|
+
validate: () => null,
|
|
312
|
+
buildRequest: ({ attachmentIds }) => {
|
|
313
|
+
const { isDraft: _omit, ...payload } = buildComposePayload(params, { attachmentIds, isDraft: true })
|
|
314
|
+
return {
|
|
315
|
+
endpoint: `/api/messages/${encodeURIComponent(params.messageId)}`,
|
|
316
|
+
method: 'PATCH' as const,
|
|
317
|
+
payload,
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
successMessage: params.t('messages.flash.draftSaved', 'Draft saved.'),
|
|
321
|
+
requiresAttachmentRefresh: true,
|
|
322
|
+
}), [params])
|
|
323
|
+
}
|
|
324
|
+
|
|
259
325
|
export type MessageComposeSubmitOperation = BaseOperation
|
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Calendar, dateFnsLocalizer, type View, type SlotInfo } from 'react-big-calendar'
|
|
5
|
-
import { addDays
|
|
5
|
+
import { addDays } from 'date-fns/addDays'
|
|
6
|
+
import { differenceInCalendarDays } from 'date-fns/differenceInCalendarDays'
|
|
7
|
+
import { endOfDay } from 'date-fns/endOfDay'
|
|
8
|
+
import { endOfMonth } from 'date-fns/endOfMonth'
|
|
9
|
+
import { endOfWeek } from 'date-fns/endOfWeek'
|
|
10
|
+
import { format } from 'date-fns/format'
|
|
11
|
+
import { getDay } from 'date-fns/getDay'
|
|
12
|
+
import { parse } from 'date-fns/parse'
|
|
13
|
+
import { startOfDay } from 'date-fns/startOfDay'
|
|
14
|
+
import { startOfMonth } from 'date-fns/startOfMonth'
|
|
15
|
+
import { startOfWeek } from 'date-fns/startOfWeek'
|
|
6
16
|
import { enUS } from 'date-fns/locale/en-US'
|
|
7
17
|
import type { ScheduleItem, ScheduleRange, ScheduleSlot, ScheduleViewMode } from './types'
|
|
8
18
|
import { Button } from '../../primitives/button'
|
|
@@ -6,7 +6,17 @@ import { Input } from '../../primitives/input'
|
|
|
6
6
|
import type { ScheduleRange, ScheduleViewMode } from './types'
|
|
7
7
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
8
8
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
9
|
-
import { addDays
|
|
9
|
+
import { addDays } from 'date-fns/addDays'
|
|
10
|
+
import { addMonths } from 'date-fns/addMonths'
|
|
11
|
+
import { addWeeks } from 'date-fns/addWeeks'
|
|
12
|
+
import { differenceInCalendarDays } from 'date-fns/differenceInCalendarDays'
|
|
13
|
+
import { endOfDay } from 'date-fns/endOfDay'
|
|
14
|
+
import { endOfMonth } from 'date-fns/endOfMonth'
|
|
15
|
+
import { endOfWeek } from 'date-fns/endOfWeek'
|
|
16
|
+
import { format } from 'date-fns/format'
|
|
17
|
+
import { startOfDay } from 'date-fns/startOfDay'
|
|
18
|
+
import { startOfMonth } from 'date-fns/startOfMonth'
|
|
19
|
+
import { startOfWeek } from 'date-fns/startOfWeek'
|
|
10
20
|
import { enUS } from 'date-fns/locale/en-US'
|
|
11
21
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
12
22
|
|
|
@@ -4,8 +4,8 @@ import * as React from 'react'
|
|
|
4
4
|
import { DayPicker, useDayPicker } from 'react-day-picker'
|
|
5
5
|
import type { DayPickerProps, CalendarMonth } from 'react-day-picker'
|
|
6
6
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
7
|
-
import { format } from 'date-fns'
|
|
8
|
-
import type { Locale } from 'date-fns'
|
|
7
|
+
import { format } from 'date-fns/format'
|
|
8
|
+
import type { Locale } from 'date-fns/locale'
|
|
9
9
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
10
10
|
|
|
11
11
|
export type CalendarProps = DayPickerProps
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
subYears,
|
|
9
|
-
} from 'date-fns'
|
|
1
|
+
import { endOfDay } from 'date-fns/endOfDay'
|
|
2
|
+
import { startOfDay } from 'date-fns/startOfDay'
|
|
3
|
+
import { startOfMonth } from 'date-fns/startOfMonth'
|
|
4
|
+
import { startOfYear } from 'date-fns/startOfYear'
|
|
5
|
+
import { subDays } from 'date-fns/subDays'
|
|
6
|
+
import { subMonths } from 'date-fns/subMonths'
|
|
7
|
+
import { subYears } from 'date-fns/subYears'
|
|
10
8
|
import type { DateRange } from '../backend/date-range/dateRanges'
|
|
11
9
|
|
|
12
10
|
export type DateRangePresetItem = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import type { Locale } from 'date-fns'
|
|
4
|
+
import type { Locale } from 'date-fns/locale'
|
|
5
5
|
import { CalendarIcon } from 'lucide-react'
|
|
6
6
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
7
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -233,7 +233,7 @@ export function DatePicker({
|
|
|
233
233
|
onSelect={handleDaySelect}
|
|
234
234
|
locale={locale}
|
|
235
235
|
disabled={disabledMatcher}
|
|
236
|
-
|
|
236
|
+
autoFocus
|
|
237
237
|
/>
|
|
238
238
|
{withTime && (
|
|
239
239
|
<div className="flex items-center gap-2 border-t px-3 py-2">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import type { Locale } from 'date-fns'
|
|
4
|
+
import type { Locale } from 'date-fns/locale'
|
|
5
5
|
import { CalendarIcon } from 'lucide-react'
|
|
6
6
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
7
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -244,7 +244,7 @@ export function DateRangePicker({
|
|
|
244
244
|
locale={locale}
|
|
245
245
|
disabled={disabledMatcher}
|
|
246
246
|
numberOfMonths={numberOfMonths}
|
|
247
|
-
|
|
247
|
+
autoFocus
|
|
248
248
|
/>
|
|
249
249
|
</div>
|
|
250
250
|
</div>
|