@open-mercato/core 0.6.6-develop.6184.1.b7e55f8d61 → 0.6.6-develop.6201.1.8ceb502c4b
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/modules/auth/frontend/login.js +5 -5
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +3 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +26 -14
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +169 -149
- package/dist/modules/customers/api/deals/[id]/route.js.map +3 -3
- package/dist/modules/customers/components/detail/InlineEditors.js +9 -4
- package/dist/modules/customers/components/detail/InlineEditors.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +81 -37
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/detail-page-refresh.js +17 -0
- package/dist/modules/integrations/backend/integrations/detail-page-refresh.js.map +7 -0
- package/dist/modules/integrations/backend/integrations/page.js +45 -24
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/notifications/api/[id]/action/route.js +18 -3
- package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
- package/dist/modules/notifications/api/[id]/restore/route.js +19 -3
- package/dist/modules/notifications/api/[id]/restore/route.js.map +2 -2
- package/dist/modules/notifications/api/mark-all-read/route.js +18 -4
- package/dist/modules/notifications/api/mark-all-read/route.js.map +2 -2
- package/dist/modules/notifications/api/route.js +17 -4
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/api/settings/route.js +26 -5
- package/dist/modules/notifications/api/settings/route.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +64 -5
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +299 -297
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/workflows/components/EdgeEditDialog.js +120 -141
- package/dist/modules/workflows/components/EdgeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +114 -118
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +155 -260
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/frontend/login.tsx +5 -5
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +4 -3
- package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +27 -17
- package/src/modules/customers/api/deals/[id]/route.ts +208 -174
- package/src/modules/customers/components/detail/InlineEditors.tsx +6 -10
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +13 -9
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +78 -35
- package/src/modules/integrations/backend/integrations/detail-page-refresh.ts +36 -0
- package/src/modules/integrations/backend/integrations/page.tsx +42 -21
- package/src/modules/notifications/api/[id]/action/route.ts +17 -2
- package/src/modules/notifications/api/[id]/restore/route.ts +19 -3
- package/src/modules/notifications/api/mark-all-read/route.ts +18 -4
- package/src/modules/notifications/api/route.ts +16 -3
- package/src/modules/notifications/api/settings/route.ts +26 -5
- package/src/modules/notifications/lib/routeHelpers.ts +101 -4
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +351 -327
- package/src/modules/workflows/components/EdgeEditDialog.tsx +132 -157
- package/src/modules/workflows/components/NodeEditDialog.tsx +116 -121
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +172 -239
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
5
|
import { AtSign, Briefcase, Loader2, Pencil, X } from 'lucide-react'
|
|
6
|
-
import ReactMarkdown from 'react-markdown'
|
|
7
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
|
-
import
|
|
7
|
+
import { MarkdownContent } from '@open-mercato/ui/backend/markdown'
|
|
9
8
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
10
9
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
11
10
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
12
11
|
import { useQueryClient } from '@tanstack/react-query'
|
|
13
12
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
14
|
-
import remarkGfm from 'remark-gfm'
|
|
15
13
|
import { useEmailDuplicateCheck } from '../../backend/hooks/useEmailDuplicateCheck'
|
|
16
14
|
import { lookupPhoneDuplicate } from '../../utils/phoneDuplicates'
|
|
17
15
|
import {
|
|
@@ -172,8 +170,6 @@ export function InlineTextEditor(props: InlineFieldProps) {
|
|
|
172
170
|
export const InlineMultilineEditor = UiInlineMultilineEditor
|
|
173
171
|
export const InlineSelectEditor = UiInlineSelectEditor
|
|
174
172
|
|
|
175
|
-
const MARKDOWN_PREVIEW_PLUGINS: PluggableList = [remarkGfm]
|
|
176
|
-
|
|
177
173
|
function createSocialRenderDisplay(IconComponent: typeof Briefcase): NonNullable<InlineFieldProps['renderDisplay']> {
|
|
178
174
|
// eslint-disable-next-line react/display-name
|
|
179
175
|
return ({ value, emptyLabel }) => {
|
|
@@ -206,11 +202,11 @@ export const renderMultilineMarkdownDisplay: InlineMultilineDisplayRenderer = ({
|
|
|
206
202
|
return <span className="text-muted-foreground">{emptyLabel}</span>
|
|
207
203
|
}
|
|
208
204
|
return (
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
205
|
+
<MarkdownContent
|
|
206
|
+
format="markdown"
|
|
207
|
+
body={raw}
|
|
208
|
+
className="text-sm text-foreground [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs"
|
|
209
|
+
/>
|
|
214
210
|
)
|
|
215
211
|
}
|
|
216
212
|
|
|
@@ -47,6 +47,10 @@ import {
|
|
|
47
47
|
resolveIntegrationDetailWidgetSpotId,
|
|
48
48
|
resolveRequestedIntegrationDetailTab,
|
|
49
49
|
} from '../detail-page-widgets'
|
|
50
|
+
import {
|
|
51
|
+
refreshIntegrationDetailPanels,
|
|
52
|
+
refreshIntegrationRunActivityPanels,
|
|
53
|
+
} from '../detail-page-refresh'
|
|
50
54
|
import { isValidCredentialUrl } from '../../../lib/credentials-field-validation'
|
|
51
55
|
|
|
52
56
|
type CredentialField = IntegrationCredentialField
|
|
@@ -531,8 +535,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
531
535
|
spotId: detailWidgetSpotId,
|
|
532
536
|
})
|
|
533
537
|
const refreshDetail = React.useCallback(async () => {
|
|
534
|
-
await
|
|
535
|
-
await loadCredentials()
|
|
538
|
+
await refreshIntegrationDetailPanels({ loadDetail, loadCredentials })
|
|
536
539
|
}, [loadCredentials, loadDetail])
|
|
537
540
|
const refreshLogs = React.useCallback(async () => {
|
|
538
541
|
await loadLogs()
|
|
@@ -549,13 +552,14 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
549
552
|
}
|
|
550
553
|
if (options?.showLoading) setIsRefreshingRunActivity(true)
|
|
551
554
|
try {
|
|
552
|
-
const call = await
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
555
|
+
const [call] = await Promise.all([
|
|
556
|
+
apiCall<DataSyncRunDetail>(
|
|
557
|
+
`/api/data_sync/runs/${encodeURIComponent(runIdFromUrl)}`,
|
|
558
|
+
undefined,
|
|
559
|
+
{ fallback: null },
|
|
560
|
+
),
|
|
561
|
+
refreshIntegrationRunActivityPanels({ loadLogs, loadDetail }),
|
|
562
|
+
])
|
|
559
563
|
if (call.ok && call.result) {
|
|
560
564
|
setActiveRunDetail(call.result)
|
|
561
565
|
setActiveRunRefreshedAt(new Date().toISOString())
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
19
19
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
20
20
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
21
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
21
22
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
22
23
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
23
24
|
import type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
|
|
@@ -88,6 +89,14 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
88
89
|
const [isSavingCreds, setIsSavingCreds] = React.useState(false)
|
|
89
90
|
const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())
|
|
90
91
|
|
|
92
|
+
const mutationContextId = React.useMemo(
|
|
93
|
+
() => `integrations.bundle:${bundleId ?? 'unknown'}`,
|
|
94
|
+
[bundleId],
|
|
95
|
+
)
|
|
96
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({
|
|
97
|
+
contextId: mutationContextId,
|
|
98
|
+
})
|
|
99
|
+
|
|
91
100
|
const resolveCurrentBundleId = React.useCallback(() => {
|
|
92
101
|
return bundleId ?? (
|
|
93
102
|
typeof window !== 'undefined'
|
|
@@ -147,49 +156,83 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
|
|
|
147
156
|
const currentBundleId = resolveCurrentBundleId()
|
|
148
157
|
if (!currentBundleId) return
|
|
149
158
|
setIsSavingCreds(true)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
try {
|
|
160
|
+
const call = await runMutation({
|
|
161
|
+
mutationPayload: { bundleId: currentBundleId, credentials: credValues },
|
|
162
|
+
context: {
|
|
163
|
+
formId: mutationContextId,
|
|
164
|
+
operation: 'update',
|
|
165
|
+
actionId: 'save-credentials',
|
|
166
|
+
resourceKind: 'integrations.bundle',
|
|
167
|
+
resourceId: currentBundleId,
|
|
168
|
+
bundleId: currentBundleId,
|
|
169
|
+
retryLastMutation,
|
|
170
|
+
},
|
|
171
|
+
// TODO(#2373-B): thread updatedAt — integration detail/state response does not expose a record version yet
|
|
172
|
+
operation: () => withScopedApiRequestHeaders(
|
|
173
|
+
buildOptimisticLockHeader(undefined),
|
|
174
|
+
() => apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {
|
|
175
|
+
method: 'PUT',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({ credentials: credValues }),
|
|
178
|
+
}, { fallback: null }),
|
|
179
|
+
),
|
|
180
|
+
})
|
|
181
|
+
if (call.ok) {
|
|
182
|
+
flash(t('integrations.detail.credentials.saved'), 'success')
|
|
183
|
+
} else {
|
|
184
|
+
flash(t('integrations.detail.credentials.saveError'), 'error')
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
162
187
|
flash(t('integrations.detail.credentials.saveError'), 'error')
|
|
188
|
+
} finally {
|
|
189
|
+
setIsSavingCreds(false)
|
|
163
190
|
}
|
|
164
|
-
|
|
165
|
-
}, [resolveCurrentBundleId, credValues, t])
|
|
191
|
+
}, [resolveCurrentBundleId, runMutation, mutationContextId, retryLastMutation, credValues, t])
|
|
166
192
|
|
|
167
193
|
const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean) => {
|
|
168
194
|
setTogglingIds((prev) => new Set(prev).add(integrationId))
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
195
|
+
try {
|
|
196
|
+
const call = await runMutation({
|
|
197
|
+
mutationPayload: { integrationId, isEnabled: enabled },
|
|
198
|
+
context: {
|
|
199
|
+
formId: mutationContextId,
|
|
200
|
+
operation: 'update',
|
|
201
|
+
actionId: 'toggle-state',
|
|
202
|
+
resourceKind: 'integrations.integration',
|
|
203
|
+
resourceId: integrationId,
|
|
204
|
+
integrationId,
|
|
205
|
+
retryLastMutation,
|
|
206
|
+
},
|
|
207
|
+
// TODO(#2373-B): thread updatedAt — integration detail/state response does not expose a record version yet
|
|
208
|
+
operation: () => withScopedApiRequestHeaders(
|
|
209
|
+
buildOptimisticLockHeader(undefined),
|
|
210
|
+
() => apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {
|
|
211
|
+
method: 'PUT',
|
|
212
|
+
headers: { 'Content-Type': 'application/json' },
|
|
213
|
+
body: JSON.stringify({ isEnabled: enabled }),
|
|
214
|
+
}, { fallback: null }),
|
|
215
|
+
),
|
|
187
216
|
})
|
|
188
|
-
|
|
217
|
+
if (call.ok) {
|
|
218
|
+
setDetail((prev) => {
|
|
219
|
+
if (!prev) return prev
|
|
220
|
+
return {
|
|
221
|
+
...prev,
|
|
222
|
+
bundleIntegrations: prev.bundleIntegrations.map((item) =>
|
|
223
|
+
item.id === integrationId ? { ...item, isEnabled: enabled } : item,
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
} else {
|
|
228
|
+
flash(t('integrations.detail.stateError'), 'error')
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
189
231
|
flash(t('integrations.detail.stateError'), 'error')
|
|
232
|
+
} finally {
|
|
233
|
+
setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })
|
|
190
234
|
}
|
|
191
|
-
|
|
192
|
-
}, [t])
|
|
235
|
+
}, [runMutation, mutationContextId, retryLastMutation, t])
|
|
193
236
|
|
|
194
237
|
const handleBulkToggle = React.useCallback(async (enabled: boolean) => {
|
|
195
238
|
if (!detail) return
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type ShowLoadingOption = { showLoading?: boolean }
|
|
2
|
+
type IntegrationDetailLoader = (options?: ShowLoadingOption) => Promise<void>
|
|
3
|
+
type IntegrationPanelLoader = () => Promise<void>
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Refresh the integration detail and credential panels concurrently.
|
|
7
|
+
*
|
|
8
|
+
* Detail and credentials are served by independent endpoints with no data
|
|
9
|
+
* dependency between them, so the two requests start together instead of
|
|
10
|
+
* waiting for the detail load to resolve before credentials begins.
|
|
11
|
+
*/
|
|
12
|
+
export async function refreshIntegrationDetailPanels(loaders: {
|
|
13
|
+
loadDetail: IntegrationDetailLoader
|
|
14
|
+
loadCredentials: IntegrationPanelLoader
|
|
15
|
+
}): Promise<void> {
|
|
16
|
+
await Promise.all([
|
|
17
|
+
loaders.loadDetail({ showLoading: false }),
|
|
18
|
+
loaders.loadCredentials(),
|
|
19
|
+
])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Refresh the run-activity panels (logs and detail) concurrently.
|
|
24
|
+
*
|
|
25
|
+
* loadLogs and loadDetail hit independent endpoints and neither consumes the
|
|
26
|
+
* other's result, so the two reloads start together.
|
|
27
|
+
*/
|
|
28
|
+
export async function refreshIntegrationRunActivityPanels(loaders: {
|
|
29
|
+
loadLogs: IntegrationPanelLoader
|
|
30
|
+
loadDetail: IntegrationDetailLoader
|
|
31
|
+
}): Promise<void> {
|
|
32
|
+
await Promise.all([
|
|
33
|
+
loaders.loadLogs(),
|
|
34
|
+
loaders.loadDetail({ showLoading: false }),
|
|
35
|
+
])
|
|
36
|
+
}
|
|
@@ -9,6 +9,7 @@ import { Input } from '@open-mercato/ui/primitives/input'
|
|
|
9
9
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
10
10
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
11
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
12
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
12
13
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
13
14
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
14
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -145,6 +146,9 @@ export default function IntegrationsMarketplacePage() {
|
|
|
145
146
|
const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())
|
|
146
147
|
const scopeVersion = useOrganizationScopeVersion()
|
|
147
148
|
const t = useT()
|
|
149
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({
|
|
150
|
+
contextId: 'integrations.marketplace',
|
|
151
|
+
})
|
|
148
152
|
|
|
149
153
|
React.useEffect(() => {
|
|
150
154
|
const timer = setTimeout(() => setDebouncedSearch(searchInput.trim()), 300)
|
|
@@ -202,30 +206,47 @@ export default function IntegrationsMarketplacePage() {
|
|
|
202
206
|
|
|
203
207
|
const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean, updatedAt?: string | null) => {
|
|
204
208
|
setTogglingIds((prev) => new Set(prev).add(integrationId))
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
try {
|
|
210
|
+
const call = await runMutation({
|
|
211
|
+
mutationPayload: { integrationId, isEnabled: enabled },
|
|
212
|
+
context: {
|
|
213
|
+
formId: 'integrations.marketplace',
|
|
214
|
+
operation: 'update',
|
|
215
|
+
actionId: 'toggle-state',
|
|
216
|
+
resourceKind: 'integrations.integration',
|
|
217
|
+
resourceId: integrationId,
|
|
218
|
+
integrationId,
|
|
219
|
+
retryLastMutation,
|
|
220
|
+
},
|
|
221
|
+
operation: () => withScopedApiRequestHeaders(
|
|
222
|
+
buildOptimisticLockHeader(updatedAt),
|
|
223
|
+
() => apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {
|
|
224
|
+
method: 'PUT',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify({ isEnabled: enabled }),
|
|
227
|
+
}, { fallback: null }),
|
|
228
|
+
),
|
|
229
|
+
})
|
|
213
230
|
|
|
214
|
-
|
|
231
|
+
if (!call.ok) {
|
|
232
|
+
flash(t('integrations.detail.stateError'), 'error')
|
|
233
|
+
} else {
|
|
234
|
+
setData((prev) => {
|
|
235
|
+
if (!prev) return prev
|
|
236
|
+
return {
|
|
237
|
+
...prev,
|
|
238
|
+
items: prev.items.map((item) =>
|
|
239
|
+
item.id === integrationId ? { ...item, isEnabled: enabled } : item,
|
|
240
|
+
),
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
215
245
|
flash(t('integrations.detail.stateError'), 'error')
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (!prev) return prev
|
|
219
|
-
return {
|
|
220
|
-
...prev,
|
|
221
|
-
items: prev.items.map((item) =>
|
|
222
|
-
item.id === integrationId ? { ...item, isEnabled: enabled } : item,
|
|
223
|
-
),
|
|
224
|
-
}
|
|
225
|
-
})
|
|
246
|
+
} finally {
|
|
247
|
+
setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })
|
|
226
248
|
}
|
|
227
|
-
|
|
228
|
-
}, [t])
|
|
249
|
+
}, [retryLastMutation, runMutation, t])
|
|
229
250
|
|
|
230
251
|
const grouped = React.useMemo(() => {
|
|
231
252
|
if (!data) return { bundles: [] as Array<BundleItem & { integrations: IntegrationItem[] }>, standalone: [] as IntegrationItem[] }
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { executeActionSchema } from '../../../data/validators'
|
|
2
2
|
import { actionResultResponseSchema, errorResponseSchema } from '../../openapi'
|
|
3
3
|
import {
|
|
4
|
+
NOTIFICATION_RESOURCE_KIND,
|
|
4
5
|
notificationCrudErrorResponse,
|
|
5
6
|
notificationValidationErrorResponse,
|
|
6
7
|
resolveNotificationContext,
|
|
8
|
+
runGuardedNotificationWrite,
|
|
7
9
|
} from '../../../lib/routeHelpers'
|
|
8
10
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
11
|
|
|
@@ -13,7 +15,7 @@ export const metadata = {
|
|
|
13
15
|
|
|
14
16
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
15
17
|
const { id } = await params
|
|
16
|
-
const { service, scope } = await resolveNotificationContext(req)
|
|
18
|
+
const { service, scope, ctx } = await resolveNotificationContext(req)
|
|
17
19
|
|
|
18
20
|
const body = await req.json().catch(() => ({}))
|
|
19
21
|
const parsed = executeActionSchema.safeParse(body)
|
|
@@ -23,7 +25,20 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
23
25
|
const input = parsed.data
|
|
24
26
|
|
|
25
27
|
try {
|
|
26
|
-
const
|
|
28
|
+
const guarded = await runGuardedNotificationWrite(
|
|
29
|
+
ctx.container,
|
|
30
|
+
scope,
|
|
31
|
+
req,
|
|
32
|
+
{
|
|
33
|
+
resourceKind: NOTIFICATION_RESOURCE_KIND,
|
|
34
|
+
resourceId: id,
|
|
35
|
+
operation: 'custom',
|
|
36
|
+
payload: input as Record<string, unknown>,
|
|
37
|
+
},
|
|
38
|
+
() => service.executeAction(id, input, scope),
|
|
39
|
+
)
|
|
40
|
+
if (!guarded.ok) return guarded.response
|
|
41
|
+
const { notification, result } = guarded.result
|
|
27
42
|
|
|
28
43
|
const action = notification.actionData?.actions?.find((a) => a.id === input.actionId)
|
|
29
44
|
const href = action?.href?.replace('{sourceEntityId}', notification.sourceEntityId ?? '')
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { restoreNotificationSchema } from '../../../data/validators'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
NOTIFICATION_RESOURCE_KIND,
|
|
5
|
+
resolveNotificationContext,
|
|
6
|
+
runGuardedNotificationWrite,
|
|
7
|
+
} from '../../../lib/routeHelpers'
|
|
4
8
|
|
|
5
9
|
export const metadata = {
|
|
6
10
|
PUT: { requireAuth: true },
|
|
@@ -8,12 +12,24 @@ export const metadata = {
|
|
|
8
12
|
|
|
9
13
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
10
14
|
const { id } = await params
|
|
11
|
-
const { service, scope } = await resolveNotificationContext(req)
|
|
15
|
+
const { service, scope, ctx } = await resolveNotificationContext(req)
|
|
12
16
|
|
|
13
17
|
const body = await req.json().catch(() => ({}))
|
|
14
18
|
const input = restoreNotificationSchema.parse(body)
|
|
15
19
|
|
|
16
|
-
await
|
|
20
|
+
const guarded = await runGuardedNotificationWrite(
|
|
21
|
+
ctx.container,
|
|
22
|
+
scope,
|
|
23
|
+
req,
|
|
24
|
+
{
|
|
25
|
+
resourceKind: NOTIFICATION_RESOURCE_KIND,
|
|
26
|
+
resourceId: id,
|
|
27
|
+
operation: 'update',
|
|
28
|
+
payload: input as Record<string, unknown>,
|
|
29
|
+
},
|
|
30
|
+
() => service.restoreDismissed(id, input.status, scope),
|
|
31
|
+
)
|
|
32
|
+
if (!guarded.ok) return guarded.response
|
|
17
33
|
|
|
18
34
|
return Response.json({ ok: true })
|
|
19
35
|
}
|
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
NOTIFICATION_RESOURCE_KIND,
|
|
4
|
+
resolveNotificationContext,
|
|
5
|
+
runGuardedNotificationWrite,
|
|
6
|
+
} from '../../lib/routeHelpers'
|
|
3
7
|
|
|
4
8
|
export const metadata = {
|
|
5
9
|
PUT: { requireAuth: true },
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
export async function PUT(req: Request) {
|
|
9
|
-
const { service, scope } = await resolveNotificationContext(req)
|
|
13
|
+
const { service, scope, ctx } = await resolveNotificationContext(req)
|
|
10
14
|
|
|
11
|
-
const
|
|
15
|
+
const guarded = await runGuardedNotificationWrite(
|
|
16
|
+
ctx.container,
|
|
17
|
+
scope,
|
|
18
|
+
req,
|
|
19
|
+
{
|
|
20
|
+
resourceKind: NOTIFICATION_RESOURCE_KIND,
|
|
21
|
+
operation: 'update',
|
|
22
|
+
},
|
|
23
|
+
() => service.markAllAsRead(scope),
|
|
24
|
+
)
|
|
25
|
+
if (!guarded.ok) return guarded.response
|
|
12
26
|
|
|
13
|
-
return Response.json({ ok: true, count })
|
|
27
|
+
return Response.json({ ok: true, count: guarded.result })
|
|
14
28
|
}
|
|
15
29
|
|
|
16
30
|
export const openApi = {
|
|
@@ -4,9 +4,11 @@ import { Notification } from '../data/entities'
|
|
|
4
4
|
import { listNotificationsSchema, createNotificationSchema } from '../data/validators'
|
|
5
5
|
import { toNotificationDto } from '../lib/notificationMapper'
|
|
6
6
|
import {
|
|
7
|
+
NOTIFICATION_RESOURCE_KIND,
|
|
7
8
|
notificationCrudErrorResponse,
|
|
8
9
|
notificationValidationErrorResponse,
|
|
9
10
|
resolveNotificationContext,
|
|
11
|
+
runGuardedNotificationWrite,
|
|
10
12
|
} from '../lib/routeHelpers'
|
|
11
13
|
import {
|
|
12
14
|
buildNotificationsCrudOpenApi,
|
|
@@ -74,7 +76,7 @@ export async function GET(req: Request) {
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
export async function POST(req: Request) {
|
|
77
|
-
const { service, scope } = await resolveNotificationContext(req)
|
|
79
|
+
const { service, scope, ctx } = await resolveNotificationContext(req)
|
|
78
80
|
|
|
79
81
|
const body = await req.json().catch(() => ({}))
|
|
80
82
|
const parsed = createNotificationSchema.safeParse(body)
|
|
@@ -83,9 +85,20 @@ export async function POST(req: Request) {
|
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
try {
|
|
86
|
-
const
|
|
88
|
+
const guarded = await runGuardedNotificationWrite(
|
|
89
|
+
ctx.container,
|
|
90
|
+
scope,
|
|
91
|
+
req,
|
|
92
|
+
{
|
|
93
|
+
resourceKind: NOTIFICATION_RESOURCE_KIND,
|
|
94
|
+
operation: 'create',
|
|
95
|
+
payload: parsed.data as Record<string, unknown>,
|
|
96
|
+
},
|
|
97
|
+
() => service.create(parsed.data, scope),
|
|
98
|
+
)
|
|
99
|
+
if (!guarded.ok) return guarded.response
|
|
87
100
|
|
|
88
|
-
return Response.json({ id:
|
|
101
|
+
return Response.json({ id: guarded.result.id }, { status: 201 })
|
|
89
102
|
} catch (error) {
|
|
90
103
|
const errorResponse = notificationCrudErrorResponse(error)
|
|
91
104
|
if (errorResponse) return errorResponse
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
resolveNotificationDeliveryConfig,
|
|
14
14
|
saveNotificationDeliveryConfig,
|
|
15
15
|
} from '../../lib/deliveryConfig'
|
|
16
|
+
import {
|
|
17
|
+
NOTIFICATION_SETTINGS_RESOURCE_KIND,
|
|
18
|
+
runGuardedNotificationWrite,
|
|
19
|
+
} from '../../lib/routeHelpers'
|
|
16
20
|
|
|
17
21
|
export const metadata = {
|
|
18
22
|
GET: { requireAuth: true, requireFeatures: ['notifications.manage'] },
|
|
@@ -67,11 +71,28 @@ export async function POST(req: Request) {
|
|
|
67
71
|
|
|
68
72
|
const container = await createRequestContainer()
|
|
69
73
|
try {
|
|
70
|
-
await
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const guarded = await runGuardedNotificationWrite(
|
|
75
|
+
container,
|
|
76
|
+
{
|
|
77
|
+
tenantId: auth.tenantId ?? '',
|
|
78
|
+
organizationId: auth.orgId ?? null,
|
|
79
|
+
userId: auth.sub ?? null,
|
|
80
|
+
},
|
|
81
|
+
req,
|
|
82
|
+
{
|
|
83
|
+
resourceKind: NOTIFICATION_SETTINGS_RESOURCE_KIND,
|
|
84
|
+
operation: 'update',
|
|
85
|
+
payload: parsed.data as Record<string, unknown>,
|
|
86
|
+
},
|
|
87
|
+
async () => {
|
|
88
|
+
await saveNotificationDeliveryConfig(container, parsed.data)
|
|
89
|
+
return resolveNotificationDeliveryConfig(container, {
|
|
90
|
+
defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
if (!guarded.ok) return guarded.response
|
|
95
|
+
return NextResponse.json({ ok: true, settings: guarded.result })
|
|
75
96
|
} catch (error) {
|
|
76
97
|
return NextResponse.json(
|
|
77
98
|
{ error: error instanceof Error ? error.message : t('api.errors.internal', 'Internal error') },
|