@open-mercato/core 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/helpers/integration/queue-runner.js +15 -3
- package/dist/helpers/integration/queue-runner.js.map +2 -2
- package/dist/helpers/integration/queue.js +1 -1
- package/dist/helpers/integration/queue.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +24 -1
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +37 -1
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/roles/route.js +36 -2
- package/dist/modules/auth/api/roles/route.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +36 -1
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +101 -13
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +32 -14
- package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/create/page.js +1 -1
- package/dist/modules/auth/backend/users/create/page.js.map +2 -2
- package/dist/modules/auth/backend/users/roleOptions.js +1 -1
- package/dist/modules/auth/backend/users/roleOptions.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +8 -0
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/auth/commands/users.js +3 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/components/AclEditor.js +53 -26
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/auth/frontend/login.js +35 -0
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/auth/lib/grantChecks.js +97 -2
- package/dist/modules/auth/lib/grantChecks.js.map +2 -2
- package/dist/modules/business_rules/lib/expression-evaluator.js +9 -7
- package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-evaluator.js +3 -3
- package/dist/modules/business_rules/lib/rule-evaluator.js.map +2 -2
- package/dist/modules/customers/ai-agents.js +10 -1
- package/dist/modules/customers/ai-agents.js.map +2 -2
- package/dist/modules/customers/ai-tools/settings-pack.js +55 -1
- package/dist/modules/customers/ai-tools/settings-pack.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +13 -5
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/lib/personCompanyLinkTable.js +4 -30
- package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260521175027.js +12 -0
- package/dist/modules/customers/migrations/Migration20260521175027.js.map +7 -0
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +41 -12
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/data_sync/api/options.js +2 -0
- package/dist/modules/data_sync/api/options.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +77 -58
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +3 -2
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +8 -3
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +15 -13
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/data/validators.js +9 -6
- package/dist/modules/integrations/data/validators.js.map +2 -2
- package/dist/modules/messages/api/route.js +2 -2
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +42 -2
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +70 -89
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/defaults/DefaultMessageListItem.js +8 -6
- package/dist/modules/messages/components/defaults/DefaultMessageListItem.js.map +2 -2
- package/dist/modules/messages/components/inboxFilters.js +133 -0
- package/dist/modules/messages/components/inboxFilters.js.map +7 -0
- package/dist/modules/messages/components/messageListLabels.js +14 -0
- package/dist/modules/messages/components/messageListLabels.js.map +7 -0
- package/dist/modules/messages/components/useMessagesInboxBulkActions.js +47 -23
- package/dist/modules/messages/components/useMessagesInboxBulkActions.js.map +2 -2
- package/dist/modules/messages/data/validators.js +8 -6
- package/dist/modules/messages/data/validators.js.map +2 -2
- package/dist/modules/progress/api/jobs/route.js +26 -16
- package/dist/modules/progress/api/jobs/route.js.map +2 -2
- package/dist/modules/query_index/di.js +8 -6
- package/dist/modules/query_index/di.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +12 -10
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/subscribers/upsert_one.js +7 -1
- package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
- package/dist/modules/sales/api/order-adjustments/route.js.map +2 -2
- package/dist/modules/sales/api/quote-adjustments/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +104 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/data/validators.js +20 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
- package/dist/modules/sync_excel/api/import/route.js +126 -116
- package/dist/modules/sync_excel/api/import/route.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +1 -0
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +9 -4
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +23 -11
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/jest.config.cjs +4 -1
- package/package.json +15 -16
- package/src/helpers/integration/queue-runner.ts +21 -2
- package/src/helpers/integration/queue.ts +1 -1
- package/src/helpers/integration/salesUi.ts +26 -1
- package/src/modules/auth/api/roles/acl/route.ts +39 -1
- package/src/modules/auth/api/roles/route.ts +38 -2
- package/src/modules/auth/api/users/acl/route.ts +41 -1
- package/src/modules/auth/api/users/route.ts +105 -12
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +35 -13
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
- package/src/modules/auth/backend/users/create/page.tsx +1 -1
- package/src/modules/auth/backend/users/roleOptions.ts +2 -1
- package/src/modules/auth/commands/roles.ts +8 -0
- package/src/modules/auth/commands/users.ts +3 -0
- package/src/modules/auth/components/AclEditor.tsx +50 -24
- package/src/modules/auth/frontend/login.tsx +40 -0
- package/src/modules/auth/lib/grantChecks.ts +116 -2
- package/src/modules/business_rules/lib/expression-evaluator.ts +9 -13
- package/src/modules/business_rules/lib/rule-evaluator.ts +3 -3
- package/src/modules/customers/ai-agents.ts +10 -1
- package/src/modules/customers/ai-tools/settings-pack.ts +62 -1
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +18 -7
- package/src/modules/customers/lib/personCompanyLinkTable.ts +33 -41
- package/src/modules/customers/migrations/Migration20260521175027.ts +39 -0
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +38 -12
- package/src/modules/data_sync/api/options.ts +2 -0
- package/src/modules/data_sync/api/run.ts +80 -61
- package/src/modules/data_sync/backend/data-sync/page.tsx +10 -0
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +17 -3
- package/src/modules/data_sync/i18n/de.json +3 -0
- package/src/modules/data_sync/i18n/en.json +3 -0
- package/src/modules/data_sync/i18n/es.json +3 -0
- package/src/modules/data_sync/i18n/pl.json +3 -0
- package/src/modules/data_sync/lib/adapter.ts +9 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +15 -13
- package/src/modules/integrations/data/validators.ts +5 -4
- package/src/modules/messages/api/route.ts +2 -2
- package/src/modules/messages/commands/messages.ts +54 -2
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +80 -92
- package/src/modules/messages/components/defaults/DefaultMessageListItem.tsx +9 -5
- package/src/modules/messages/components/inboxFilters.ts +162 -0
- package/src/modules/messages/components/messageListLabels.ts +29 -0
- package/src/modules/messages/components/useMessagesInboxBulkActions.ts +58 -26
- package/src/modules/messages/data/validators.ts +7 -5
- package/src/modules/messages/i18n/de.json +7 -4
- package/src/modules/messages/i18n/en.json +7 -4
- package/src/modules/messages/i18n/es.json +7 -4
- package/src/modules/messages/i18n/pl.json +7 -4
- package/src/modules/progress/api/jobs/route.ts +30 -20
- package/src/modules/query_index/di.ts +9 -7
- package/src/modules/query_index/lib/indexer.ts +13 -11
- package/src/modules/query_index/subscribers/upsert_one.ts +9 -1
- package/src/modules/sales/api/order-adjustments/route.ts +2 -1
- package/src/modules/sales/api/quote-adjustments/route.ts +2 -1
- package/src/modules/sales/commands/documents.ts +116 -0
- package/src/modules/sales/data/validators.ts +38 -0
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/sales/lib/makeSalesLineRoute.ts +2 -1
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/sync_excel/api/import/route.ts +141 -131
- package/src/modules/sync_excel/lib/adapters/customers.ts +1 -0
- package/src/modules/translations/components/TranslationManager.tsx +9 -4
- package/src/modules/workflows/lib/activity-executor.ts +4 -3
- package/src/modules/workflows/lib/event-trigger-service.ts +24 -11
- package/tsconfig.json +1 -0
|
@@ -20,76 +20,95 @@ export const openApi = {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export async function POST(req: Request) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
try {
|
|
24
|
+
const auth = await getAuthFromRequest(req)
|
|
25
|
+
if (!auth?.tenantId || !auth.orgId) {
|
|
26
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
27
|
+
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const payload = await readJsonSafe(req)
|
|
30
|
+
const parsed = runSyncSchema.safeParse(payload)
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })
|
|
33
|
+
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const container = await createRequestContainer()
|
|
36
|
+
const syncRunService = container.resolve('dataSyncRunService') as SyncRunService
|
|
37
|
+
const progressService = container.resolve('progressService') as ProgressService
|
|
38
|
+
const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const scope = {
|
|
41
|
+
organizationId: auth.orgId as string,
|
|
42
|
+
tenantId: auth.tenantId,
|
|
43
|
+
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const integration = getIntegration(parsed.data.integrationId)
|
|
46
|
+
if (!integration?.providerKey) {
|
|
47
|
+
return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
|
|
48
|
+
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const adapter = getDataSyncAdapter(integration.providerKey)
|
|
51
|
+
if (!adapter) {
|
|
52
|
+
return NextResponse.json({ error: 'No registered sync adapter for provider' }, { status: 404 })
|
|
53
|
+
}
|
|
54
|
+
if (adapter.runMode === 'provider') {
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{
|
|
57
|
+
error: 'This integration must be started from its provider-specific import flow.',
|
|
58
|
+
settingsPath: `/backend/integrations/${encodeURIComponent(parsed.data.integrationId)}`,
|
|
59
|
+
},
|
|
60
|
+
{ status: 422 },
|
|
61
|
+
)
|
|
62
|
+
}
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
if (!adapter.supportedEntities.includes(parsed.data.entityType)) {
|
|
65
|
+
return NextResponse.json({ error: 'Unsupported entity type for this integration' }, { status: 422 })
|
|
66
|
+
}
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
const integrationEnabled = await integrationStateService.isEnabled(parsed.data.integrationId, scope)
|
|
69
|
+
if (!integrationEnabled) {
|
|
70
|
+
return NextResponse.json({ error: 'Integration is disabled' }, { status: 409 })
|
|
71
|
+
}
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
const overlap = await syncRunService.findRunningOverlap(
|
|
74
|
+
parsed.data.integrationId,
|
|
75
|
+
parsed.data.entityType,
|
|
76
|
+
parsed.data.direction,
|
|
77
|
+
scope,
|
|
78
|
+
)
|
|
79
|
+
if (overlap) {
|
|
80
|
+
return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })
|
|
81
|
+
}
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
const cursor = parsed.data.fullSync
|
|
84
|
+
? null
|
|
85
|
+
: await syncRunService.resolveCursor(parsed.data.integrationId, parsed.data.entityType, parsed.data.direction, scope)
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
const { run, progressJob } = await startDataSyncRun({
|
|
88
|
+
syncRunService,
|
|
89
|
+
progressService,
|
|
90
|
+
scope: {
|
|
91
|
+
...scope,
|
|
92
|
+
userId: auth.sub,
|
|
93
|
+
},
|
|
94
|
+
input: {
|
|
95
|
+
integrationId: parsed.data.integrationId,
|
|
96
|
+
entityType: parsed.data.entityType,
|
|
97
|
+
direction: parsed.data.direction,
|
|
98
|
+
cursor,
|
|
99
|
+
triggeredBy: parsed.data.triggeredBy ?? auth.sub,
|
|
100
|
+
batchSize: parsed.data.batchSize,
|
|
101
|
+
},
|
|
102
|
+
})
|
|
93
103
|
|
|
94
|
-
|
|
104
|
+
return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
107
|
+
const stack = error instanceof Error ? error.stack : undefined
|
|
108
|
+
console.error('[data_sync.run] unhandled error', { message, stack })
|
|
109
|
+
return NextResponse.json(
|
|
110
|
+
{ error: 'Failed to start data sync run.', message, stack },
|
|
111
|
+
{ status: 500 },
|
|
112
|
+
)
|
|
113
|
+
}
|
|
95
114
|
}
|
|
@@ -65,6 +65,8 @@ type SyncOption = {
|
|
|
65
65
|
description?: string | null
|
|
66
66
|
providerKey?: string | null
|
|
67
67
|
direction: 'import' | 'export' | 'bidirectional'
|
|
68
|
+
runMode?: 'generic' | 'provider'
|
|
69
|
+
canStartRun?: boolean
|
|
68
70
|
supportedEntities: string[]
|
|
69
71
|
hasCredentials: boolean
|
|
70
72
|
isEnabled: boolean
|
|
@@ -602,6 +604,7 @@ export default function SyncRunsDashboardPage() {
|
|
|
602
604
|
selectedIntegration
|
|
603
605
|
&& selectedEntityType
|
|
604
606
|
&& selectedIntegration.isEnabled
|
|
607
|
+
&& selectedIntegration.canStartRun !== false
|
|
605
608
|
&& selectedIntegration.hasCredentials,
|
|
606
609
|
)
|
|
607
610
|
const hasSavedSchedule = Boolean(scheduleEditor.id)
|
|
@@ -975,6 +978,13 @@ export default function SyncRunsDashboardPage() {
|
|
|
975
978
|
</AlertDescription>
|
|
976
979
|
</Alert>
|
|
977
980
|
) : null}
|
|
981
|
+
{selectedIntegration && selectedIntegration.canStartRun === false ? (
|
|
982
|
+
<Alert variant="info">
|
|
983
|
+
<AlertDescription>
|
|
984
|
+
{t('data_sync.dashboard.start.providerManaged', 'This integration starts sync runs from its own setup flow. Open the integration settings page to continue.')}
|
|
985
|
+
</AlertDescription>
|
|
986
|
+
</Alert>
|
|
987
|
+
) : null}
|
|
978
988
|
</CardContent>
|
|
979
989
|
</Card>
|
|
980
990
|
|
|
@@ -32,6 +32,8 @@ type SyncOption = {
|
|
|
32
32
|
integrationId: string
|
|
33
33
|
title: string
|
|
34
34
|
direction: 'import' | 'export' | 'bidirectional'
|
|
35
|
+
runMode?: 'generic' | 'provider'
|
|
36
|
+
canStartRun?: boolean
|
|
35
37
|
supportedEntities: string[]
|
|
36
38
|
hasCredentials: boolean
|
|
37
39
|
isEnabled: boolean
|
|
@@ -204,6 +206,10 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
204
206
|
flash(t('data_sync.integrationTab.credentialsMissing', 'Configure credentials before starting a sync.'), 'error')
|
|
205
207
|
return
|
|
206
208
|
}
|
|
209
|
+
if (option?.canStartRun === false) {
|
|
210
|
+
flash(t('data_sync.integrationTab.providerManaged', 'Start this integration from its provider-specific setup flow.'), 'error')
|
|
211
|
+
return
|
|
212
|
+
}
|
|
207
213
|
|
|
208
214
|
setRunningKey(scheduleKey)
|
|
209
215
|
try {
|
|
@@ -231,7 +237,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
231
237
|
} finally {
|
|
232
238
|
setRunningKey(null)
|
|
233
239
|
}
|
|
234
|
-
}, [props.hasCredentials, props.integrationId, props.isEnabled, schedules, t])
|
|
240
|
+
}, [option?.canStartRun, props.hasCredentials, props.integrationId, props.isEnabled, schedules, t])
|
|
235
241
|
|
|
236
242
|
const handleSaveSchedule = React.useCallback(async (entityType: string, direction: 'import' | 'export', scheduleKey: string) => {
|
|
237
243
|
const scheduleState = schedules[scheduleKey] ?? buildDefaultScheduleState(entityType)
|
|
@@ -369,6 +375,14 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
369
375
|
</Alert>
|
|
370
376
|
) : null}
|
|
371
377
|
|
|
378
|
+
{option.canStartRun === false ? (
|
|
379
|
+
<Alert variant="info">
|
|
380
|
+
<AlertDescription>
|
|
381
|
+
{t('data_sync.integrationTab.providerManagedNotice', 'This provider needs its own setup flow before a run can start. Use the provider tab on this page instead of generic schedules.')}
|
|
382
|
+
</AlertDescription>
|
|
383
|
+
</Alert>
|
|
384
|
+
) : null}
|
|
385
|
+
|
|
372
386
|
{rows.length === 0 ? (
|
|
373
387
|
<Alert variant="info">
|
|
374
388
|
<AlertDescription>
|
|
@@ -469,7 +483,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
469
483
|
type="button"
|
|
470
484
|
size="sm"
|
|
471
485
|
onClick={() => void handleStartSync(row.entityType, row.direction, row.key)}
|
|
472
|
-
disabled={controlsDisabled || !props.isEnabled || !props.hasCredentials}
|
|
486
|
+
disabled={controlsDisabled || !props.isEnabled || !props.hasCredentials || option.canStartRun === false}
|
|
473
487
|
>
|
|
474
488
|
{isRunning ? <Spinner className="mr-2 h-4 w-4" /> : <Play className="mr-2 h-4 w-4" />}
|
|
475
489
|
{isRunning
|
|
@@ -481,7 +495,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
481
495
|
size="sm"
|
|
482
496
|
variant="outline"
|
|
483
497
|
onClick={() => void handleSaveSchedule(row.entityType, row.direction, row.key)}
|
|
484
|
-
disabled={controlsDisabled}
|
|
498
|
+
disabled={controlsDisabled || option.canStartRun === false}
|
|
485
499
|
>
|
|
486
500
|
{isSaving ? <Spinner className="mr-2 h-4 w-4" /> : <Save className="mr-2 h-4 w-4" />}
|
|
487
501
|
{isSaving
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"data_sync.dashboard.start.fullSync": "Run as full sync",
|
|
55
55
|
"data_sync.dashboard.start.fullSyncHelp": "Ignore the saved cursor and process the entire source again for this run.",
|
|
56
56
|
"data_sync.dashboard.start.invalidBatchSize": "Batch size must be between 1 and 1000.",
|
|
57
|
+
"data_sync.dashboard.start.providerManaged": "Diese Integration startet Synchronisierungsläufe über ihren eigenen Einrichtungsablauf. Öffne die Integrationseinstellungen, um fortzufahren.",
|
|
57
58
|
"data_sync.dashboard.start.runNowDescription": "Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.",
|
|
58
59
|
"data_sync.dashboard.start.runNowFootnote": "Manual runs show progress immediately and land on the run detail page after launch.",
|
|
59
60
|
"data_sync.dashboard.start.runNowTitle": "Run once now",
|
|
@@ -83,6 +84,8 @@
|
|
|
83
84
|
"data_sync.integrationTab.integrationDisabledNotice": "The integration is disabled. You can save schedules now, but runs will stay blocked until the integration is enabled.",
|
|
84
85
|
"data_sync.integrationTab.loadError": "Failed to load sync schedules.",
|
|
85
86
|
"data_sync.integrationTab.notAvailable": "This integration is not registered as a data sync provider.",
|
|
87
|
+
"data_sync.integrationTab.providerManaged": "Starte diese Integration über ihren anbieterspezifischen Einrichtungsablauf.",
|
|
88
|
+
"data_sync.integrationTab.providerManagedNotice": "Dieser Anbieter benötigt einen eigenen Einrichtungsablauf, bevor ein Lauf gestartet werden kann. Verwende den Anbieter-Tab auf dieser Seite statt der generischen Zeitpläne.",
|
|
86
89
|
"data_sync.integrationTab.refresh": "Refresh",
|
|
87
90
|
"data_sync.integrationTab.runError": "Failed to start sync.",
|
|
88
91
|
"data_sync.integrationTab.runNowHelp": "Run now uses the full-sync checkbox from the same row and starts progress tracking immediately in Data Sync.",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"data_sync.dashboard.start.fullSync": "Run as full sync",
|
|
55
55
|
"data_sync.dashboard.start.fullSyncHelp": "Ignore the saved cursor and process the entire source again for this run.",
|
|
56
56
|
"data_sync.dashboard.start.invalidBatchSize": "Batch size must be between 1 and 1000.",
|
|
57
|
+
"data_sync.dashboard.start.providerManaged": "This integration starts sync runs from its own setup flow. Open the integration settings page to continue.",
|
|
57
58
|
"data_sync.dashboard.start.runNowDescription": "Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.",
|
|
58
59
|
"data_sync.dashboard.start.runNowFootnote": "Manual runs show progress immediately and land on the run detail page after launch.",
|
|
59
60
|
"data_sync.dashboard.start.runNowTitle": "Run once now",
|
|
@@ -83,6 +84,8 @@
|
|
|
83
84
|
"data_sync.integrationTab.integrationDisabledNotice": "The integration is disabled. You can save schedules now, but runs will stay blocked until the integration is enabled.",
|
|
84
85
|
"data_sync.integrationTab.loadError": "Failed to load sync schedules.",
|
|
85
86
|
"data_sync.integrationTab.notAvailable": "This integration is not registered as a data sync provider.",
|
|
87
|
+
"data_sync.integrationTab.providerManaged": "Start this integration from its provider-specific setup flow.",
|
|
88
|
+
"data_sync.integrationTab.providerManagedNotice": "This provider needs its own setup flow before a run can start. Use the provider tab on this page instead of generic schedules.",
|
|
86
89
|
"data_sync.integrationTab.refresh": "Refresh",
|
|
87
90
|
"data_sync.integrationTab.runError": "Failed to start sync.",
|
|
88
91
|
"data_sync.integrationTab.runNowHelp": "Run now uses the full-sync checkbox from the same row and starts progress tracking immediately in Data Sync.",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"data_sync.dashboard.start.fullSync": "Run as full sync",
|
|
55
55
|
"data_sync.dashboard.start.fullSyncHelp": "Ignore the saved cursor and process the entire source again for this run.",
|
|
56
56
|
"data_sync.dashboard.start.invalidBatchSize": "Batch size must be between 1 and 1000.",
|
|
57
|
+
"data_sync.dashboard.start.providerManaged": "Esta integración inicia las sincronizaciones desde su propio flujo de configuración. Abre la página de ajustes de la integración para continuar.",
|
|
57
58
|
"data_sync.dashboard.start.runNowDescription": "Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.",
|
|
58
59
|
"data_sync.dashboard.start.runNowFootnote": "Manual runs show progress immediately and land on the run detail page after launch.",
|
|
59
60
|
"data_sync.dashboard.start.runNowTitle": "Run once now",
|
|
@@ -83,6 +84,8 @@
|
|
|
83
84
|
"data_sync.integrationTab.integrationDisabledNotice": "The integration is disabled. You can save schedules now, but runs will stay blocked until the integration is enabled.",
|
|
84
85
|
"data_sync.integrationTab.loadError": "Failed to load sync schedules.",
|
|
85
86
|
"data_sync.integrationTab.notAvailable": "This integration is not registered as a data sync provider.",
|
|
87
|
+
"data_sync.integrationTab.providerManaged": "Inicia esta integración desde su flujo de configuración específico del proveedor.",
|
|
88
|
+
"data_sync.integrationTab.providerManagedNotice": "Este proveedor necesita su propio flujo de configuración antes de poder iniciar una ejecución. Usa la pestaña del proveedor en esta página en lugar de los calendarios genéricos.",
|
|
86
89
|
"data_sync.integrationTab.refresh": "Refresh",
|
|
87
90
|
"data_sync.integrationTab.runError": "Failed to start sync.",
|
|
88
91
|
"data_sync.integrationTab.runNowHelp": "Run now uses the full-sync checkbox from the same row and starts progress tracking immediately in Data Sync.",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"data_sync.dashboard.start.fullSync": "Run as full sync",
|
|
55
55
|
"data_sync.dashboard.start.fullSyncHelp": "Ignore the saved cursor and process the entire source again for this run.",
|
|
56
56
|
"data_sync.dashboard.start.invalidBatchSize": "Batch size must be between 1 and 1000.",
|
|
57
|
+
"data_sync.dashboard.start.providerManaged": "Ta integracja uruchamia synchronizacje z własnego przepływu konfiguracji. Otwórz ustawienia integracji, aby kontynuować.",
|
|
57
58
|
"data_sync.dashboard.start.runNowDescription": "Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.",
|
|
58
59
|
"data_sync.dashboard.start.runNowFootnote": "Manual runs show progress immediately and land on the run detail page after launch.",
|
|
59
60
|
"data_sync.dashboard.start.runNowTitle": "Run once now",
|
|
@@ -83,6 +84,8 @@
|
|
|
83
84
|
"data_sync.integrationTab.integrationDisabledNotice": "The integration is disabled. You can save schedules now, but runs will stay blocked until the integration is enabled.",
|
|
84
85
|
"data_sync.integrationTab.loadError": "Failed to load sync schedules.",
|
|
85
86
|
"data_sync.integrationTab.notAvailable": "This integration is not registered as a data sync provider.",
|
|
87
|
+
"data_sync.integrationTab.providerManaged": "Uruchom tę integrację z jej przepływu konfiguracji dostawcy.",
|
|
88
|
+
"data_sync.integrationTab.providerManagedNotice": "Ten dostawca wymaga własnego przepływu konfiguracji, zanim będzie można uruchomić synchronizację. Użyj karty dostawcy na tej stronie zamiast ogólnych harmonogramów.",
|
|
86
89
|
"data_sync.integrationTab.refresh": "Refresh",
|
|
87
90
|
"data_sync.integrationTab.runError": "Failed to start sync.",
|
|
88
91
|
"data_sync.integrationTab.runNowHelp": "Run now uses the full-sync checkbox from the same row and starts progress tracking immediately in Data Sync.",
|
|
@@ -93,6 +93,15 @@ export interface DataSyncAdapter {
|
|
|
93
93
|
readonly providerKey: string
|
|
94
94
|
readonly direction: 'import' | 'export' | 'bidirectional'
|
|
95
95
|
readonly supportedEntities: string[]
|
|
96
|
+
/**
|
|
97
|
+
* How a run may be started.
|
|
98
|
+
*
|
|
99
|
+
* - `generic` (default): `/api/data_sync/run` has enough information to
|
|
100
|
+
* create and enqueue the run.
|
|
101
|
+
* - `provider`: the provider owns a prerequisite flow before a run can be
|
|
102
|
+
* enqueued, such as uploading a CSV and linking that upload to the run.
|
|
103
|
+
*/
|
|
104
|
+
readonly runMode?: 'generic' | 'provider'
|
|
96
105
|
readonly operationalTelemetry?: boolean
|
|
97
106
|
|
|
98
107
|
streamImport?(input: StreamImportInput): AsyncIterable<ImportBatch>
|
|
@@ -442,15 +442,16 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
442
442
|
)
|
|
443
443
|
}, [integrationId])
|
|
444
444
|
|
|
445
|
-
const loadDetail = React.useCallback(async () => {
|
|
445
|
+
const loadDetail = React.useCallback(async (options?: { showLoading?: boolean }) => {
|
|
446
|
+
const showLoading = options?.showLoading ?? true
|
|
446
447
|
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
447
448
|
if (!currentIntegrationId) {
|
|
448
|
-
setIsLoading(false)
|
|
449
|
-
setError(t('integrations.detail.loadError', 'Failed to load integration'))
|
|
449
|
+
if (showLoading) setIsLoading(false)
|
|
450
|
+
if (showLoading) setError(t('integrations.detail.loadError', 'Failed to load integration'))
|
|
450
451
|
return
|
|
451
452
|
}
|
|
452
|
-
setError(null)
|
|
453
|
-
setIsLoading(true)
|
|
453
|
+
if (showLoading) setError(null)
|
|
454
|
+
if (showLoading) setIsLoading(true)
|
|
454
455
|
try {
|
|
455
456
|
const call = await apiCall<IntegrationDetail>(
|
|
456
457
|
`/api/integrations/${encodeURIComponent(currentIntegrationId)}`,
|
|
@@ -458,15 +459,16 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
458
459
|
{ fallback: null },
|
|
459
460
|
)
|
|
460
461
|
if (!call.ok || !call.result) {
|
|
461
|
-
setError(t('integrations.detail.loadError', 'Failed to load integration'))
|
|
462
|
-
setIsLoading(false)
|
|
462
|
+
if (showLoading) setError(t('integrations.detail.loadError', 'Failed to load integration'))
|
|
463
|
+
if (showLoading) setIsLoading(false)
|
|
463
464
|
return
|
|
464
465
|
}
|
|
465
466
|
setDetail(call.result)
|
|
466
|
-
|
|
467
|
+
setError(null)
|
|
468
|
+
if (showLoading) setIsLoading(false)
|
|
467
469
|
} catch {
|
|
468
|
-
setError(t('integrations.detail.loadError', 'Failed to load integration'))
|
|
469
|
-
setIsLoading(false)
|
|
470
|
+
if (showLoading) setError(t('integrations.detail.loadError', 'Failed to load integration'))
|
|
471
|
+
if (showLoading) setIsLoading(false)
|
|
470
472
|
}
|
|
471
473
|
}, [resolveCurrentIntegrationId, t])
|
|
472
474
|
|
|
@@ -522,14 +524,14 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
522
524
|
spotId: detailWidgetSpotId,
|
|
523
525
|
})
|
|
524
526
|
const refreshDetail = React.useCallback(async () => {
|
|
525
|
-
await loadDetail()
|
|
527
|
+
await loadDetail({ showLoading: false })
|
|
526
528
|
await loadCredentials()
|
|
527
529
|
}, [loadCredentials, loadDetail])
|
|
528
530
|
const refreshLogs = React.useCallback(async () => {
|
|
529
531
|
await loadLogs()
|
|
530
532
|
}, [loadLogs])
|
|
531
533
|
const refreshHealthSnapshot = React.useCallback(async () => {
|
|
532
|
-
await loadDetail()
|
|
534
|
+
await loadDetail({ showLoading: false })
|
|
533
535
|
}, [loadDetail])
|
|
534
536
|
const runIdFromUrl = searchParams?.get('runId') ?? null
|
|
535
537
|
const refreshRunActivity = React.useCallback(async (options?: { showLoading?: boolean }) => {
|
|
@@ -546,7 +548,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
546
548
|
{ fallback: null },
|
|
547
549
|
)
|
|
548
550
|
await loadLogs()
|
|
549
|
-
await loadDetail()
|
|
551
|
+
await loadDetail({ showLoading: false })
|
|
550
552
|
if (call.ok && call.result) {
|
|
551
553
|
setActiveRunDetail(call.result)
|
|
552
554
|
setActiveRunRefreshedAt(new Date().toISOString())
|
|
@@ -40,14 +40,15 @@ export const listIntegrationLogsQuerySchema = z.object({
|
|
|
40
40
|
|
|
41
41
|
export type ListIntegrationLogsQuery = z.infer<typeof listIntegrationLogsQuerySchema>
|
|
42
42
|
|
|
43
|
-
const optionalBooleanQuery = z.
|
|
44
|
-
|
|
43
|
+
const optionalBooleanQuery = z.preprocess(
|
|
44
|
+
(value) => {
|
|
45
45
|
if (value === undefined || value === '' || value === null) return undefined
|
|
46
46
|
if (value === true || value === 'true' || value === '1') return true
|
|
47
47
|
if (value === false || value === 'false' || value === '0') return false
|
|
48
48
|
return value
|
|
49
|
-
}
|
|
50
|
-
|
|
49
|
+
},
|
|
50
|
+
z.boolean().optional(),
|
|
51
|
+
)
|
|
51
52
|
|
|
52
53
|
export const integrationMarketplaceHealthStatusSchema = z.enum(['healthy', 'degraded', 'unhealthy', 'unconfigured'])
|
|
53
54
|
|
|
@@ -168,13 +168,13 @@ export async function GET(req: Request) {
|
|
|
168
168
|
eb.selectFrom('attachments')
|
|
169
169
|
.select(sql<number>`1`.as('one'))
|
|
170
170
|
.where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)
|
|
171
|
-
.
|
|
171
|
+
.where(sql<boolean>`attachments.record_id = m.id::text`)
|
|
172
172
|
)
|
|
173
173
|
const notExistsFn = (eb: any) => eb.not(eb.exists(
|
|
174
174
|
eb.selectFrom('attachments')
|
|
175
175
|
.select(sql<number>`1`.as('one'))
|
|
176
176
|
.where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)
|
|
177
|
-
.
|
|
177
|
+
.where(sql<boolean>`attachments.record_id = m.id::text`)
|
|
178
178
|
))
|
|
179
179
|
q = input.hasAttachments ? q.where(existsFn) : q.where(notExistsFn)
|
|
180
180
|
}
|
|
@@ -2,7 +2,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
|
|
4
4
|
import { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
5
|
-
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
6
6
|
import { Message, MessageObject, MessageRecipient, type MessageActionData } from '../data/entities'
|
|
7
7
|
import { emitMessagesEvent } from '../events'
|
|
8
8
|
import {
|
|
@@ -368,6 +368,20 @@ const updateDraftCommand: CommandHandler<unknown, { ok: true; id: string }> = {
|
|
|
368
368
|
if (message.senderUserId !== input.userId) throw new Error('Access denied')
|
|
369
369
|
if (!message.isDraft) throw new Error('Only draft messages can be edited')
|
|
370
370
|
|
|
371
|
+
const isSending = input.isDraft === false
|
|
372
|
+
const preloadedRecipients = isSending && !input.recipients
|
|
373
|
+
? await findWithDecryption(
|
|
374
|
+
em,
|
|
375
|
+
MessageRecipient,
|
|
376
|
+
{ messageId: message.id, deletedAt: null },
|
|
377
|
+
undefined,
|
|
378
|
+
{
|
|
379
|
+
tenantId: input.tenantId,
|
|
380
|
+
organizationId: input.organizationId,
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
: null
|
|
384
|
+
|
|
371
385
|
const nextMessageType = input.type ?? message.type
|
|
372
386
|
if (input.objects) {
|
|
373
387
|
const objectValidationError = validateMessageObjectsForType(nextMessageType, input.objects)
|
|
@@ -454,12 +468,50 @@ const updateDraftCommand: CommandHandler<unknown, { ok: true; id: string }> = {
|
|
|
454
468
|
)
|
|
455
469
|
}
|
|
456
470
|
|
|
471
|
+
if (isSending) {
|
|
472
|
+
const finalVisibility = input.visibility ?? message.visibility
|
|
473
|
+
const finalSubject = input.subject ?? message.subject
|
|
474
|
+
const finalBody = input.body ?? message.body
|
|
475
|
+
const finalRecipientCount = input.recipients
|
|
476
|
+
? input.recipients.length
|
|
477
|
+
: (preloadedRecipients?.length ?? 0)
|
|
478
|
+
|
|
479
|
+
if (finalVisibility !== 'public' && finalRecipientCount === 0) {
|
|
480
|
+
throw new Error('at least one recipient is required')
|
|
481
|
+
}
|
|
482
|
+
if (!finalSubject?.trim()) throw new Error('subject is required')
|
|
483
|
+
if (!finalBody?.trim()) throw new Error('body is required')
|
|
484
|
+
|
|
485
|
+
message.isDraft = false
|
|
486
|
+
message.status = 'sent'
|
|
487
|
+
message.sentAt = new Date()
|
|
488
|
+
if (!message.threadId) message.threadId = message.id
|
|
489
|
+
}
|
|
490
|
+
|
|
457
491
|
await em.flush()
|
|
458
492
|
await emitMessageIndexUpsert(ctx.container, {
|
|
459
493
|
messageId: message.id,
|
|
460
494
|
tenantId: input.tenantId,
|
|
461
495
|
organizationId: input.organizationId,
|
|
462
496
|
})
|
|
497
|
+
|
|
498
|
+
if (isSending) {
|
|
499
|
+
const recipientUserIds = input.recipients
|
|
500
|
+
? input.recipients.map((r) => r.userId)
|
|
501
|
+
: (preloadedRecipients ?? []).map((r) => r.recipientUserId)
|
|
502
|
+
const resolvedVisibility = input.visibility ?? message.visibility
|
|
503
|
+
const resolvedSendViaEmail = input.sendViaEmail ?? message.sendViaEmail
|
|
504
|
+
await emitMessageSentEvent(ctx.container, {
|
|
505
|
+
messageId: message.id,
|
|
506
|
+
senderUserId: input.userId,
|
|
507
|
+
recipientUserIds,
|
|
508
|
+
sendViaEmail: resolvedVisibility === 'public' ? true : resolvedSendViaEmail,
|
|
509
|
+
externalEmail: message.externalEmail ?? null,
|
|
510
|
+
tenantId: input.tenantId,
|
|
511
|
+
organizationId: input.organizationId,
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
|
|
463
515
|
return { ok: true, id: message.id }
|
|
464
516
|
},
|
|
465
517
|
async captureAfter(rawInput, _result, ctx) {
|
|
@@ -473,7 +525,7 @@ const updateDraftCommand: CommandHandler<unknown, { ok: true; id: string }> = {
|
|
|
473
525
|
buildLog: async ({ input, snapshots }) => {
|
|
474
526
|
const parsed = updateDraftCommandSchema.parse(input)
|
|
475
527
|
return {
|
|
476
|
-
actionLabel: 'Update draft message',
|
|
528
|
+
actionLabel: parsed.isDraft === false ? 'Send draft message' : 'Update draft message',
|
|
477
529
|
resourceKind: 'messages.message',
|
|
478
530
|
resourceId: parsed.messageId,
|
|
479
531
|
tenantId: parsed.tenantId,
|