@open-mercato/core 0.6.3-develop.3857.1.da89d7530c → 0.6.3-develop.3881.1.0b590ac4eb
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/attachments/api/file/[id]/route.js +7 -2
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -4
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/audit_logs/services/accessLogService.js +127 -8
- package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
- package/dist/modules/auth/di.js +17 -3
- package/dist/modules/auth/di.js.map +2 -2
- package/dist/modules/auth/services/rbacDefaultCache.js +110 -0
- package/dist/modules/auth/services/rbacDefaultCache.js.map +7 -0
- package/dist/modules/currencies/api/currencies/route.js +3 -4
- package/dist/modules/currencies/api/currencies/route.js.map +2 -2
- package/dist/modules/currencies/api/exchange-rates/route.js +3 -4
- package/dist/modules/currencies/api/exchange-rates/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +26 -24
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +26 -0
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +7 -0
- package/dist/modules/directory/utils/organizationScope.js +85 -0
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/create/page.js +4 -2
- package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +20 -3
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/components/ActivitiesEditor.js +34 -1
- package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +153 -17
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/StepsEditor.js +31 -0
- package/dist/modules/workflows/components/StepsEditor.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraph.js +3 -2
- package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
- package/dist/modules/workflows/components/nodes/WaitForTimerNode.js +54 -0
- package/dist/modules/workflows/components/nodes/WaitForTimerNode.js.map +7 -0
- package/dist/modules/workflows/components/nodes/index.js +3 -1
- package/dist/modules/workflows/components/nodes/index.js.map +2 -2
- package/dist/modules/workflows/data/validators.js +117 -0
- package/dist/modules/workflows/data/validators.js.map +2 -2
- package/dist/modules/workflows/di.js +5 -1
- package/dist/modules/workflows/di.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +42 -1
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-queue-types.js.map +2 -2
- package/dist/modules/workflows/lib/activity-worker-handler.js +24 -0
- package/dist/modules/workflows/lib/activity-worker-handler.js.map +2 -2
- package/dist/modules/workflows/lib/duration.js +32 -0
- package/dist/modules/workflows/lib/duration.js.map +7 -0
- package/dist/modules/workflows/lib/event-logger.js +1 -0
- package/dist/modules/workflows/lib/event-logger.js.map +2 -2
- package/dist/modules/workflows/lib/format-validation-error.js +12 -0
- package/dist/modules/workflows/lib/format-validation-error.js.map +7 -0
- package/dist/modules/workflows/lib/graph-utils.js +6 -3
- package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
- package/dist/modules/workflows/lib/node-type-icons.js +9 -5
- package/dist/modules/workflows/lib/node-type-icons.js.map +2 -2
- package/dist/modules/workflows/lib/signal-handler.js +55 -23
- package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +79 -29
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/timer-handler.js +159 -0
- package/dist/modules/workflows/lib/timer-handler.js.map +7 -0
- package/dist/modules/workflows/lib/workflow-executor.js +1 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/workers/workflow-activities.worker.js +20 -4
- package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/api/file/[id]/route.ts +7 -2
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -4
- package/src/modules/audit_logs/services/accessLogService.ts +179 -15
- package/src/modules/auth/di.ts +26 -3
- package/src/modules/auth/services/rbacDefaultCache.ts +145 -0
- package/src/modules/currencies/api/currencies/route.ts +3 -4
- package/src/modules/currencies/api/exchange-rates/route.ts +3 -4
- package/src/modules/customers/api/people/route.ts +27 -25
- package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +39 -0
- package/src/modules/directory/utils/organizationScope.ts +121 -0
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +3 -2
- package/src/modules/workflows/backend/definitions/create/page.tsx +4 -2
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +18 -1
- package/src/modules/workflows/components/ActivitiesEditor.tsx +40 -0
- package/src/modules/workflows/components/NodeEditDialog.tsx +218 -30
- package/src/modules/workflows/components/StepsEditor.tsx +36 -0
- package/src/modules/workflows/components/WorkflowGraph.tsx +2 -1
- package/src/modules/workflows/components/nodes/WaitForTimerNode.tsx +70 -0
- package/src/modules/workflows/components/nodes/index.ts +3 -0
- package/src/modules/workflows/data/validators.ts +121 -0
- package/src/modules/workflows/di.ts +4 -0
- package/src/modules/workflows/i18n/de.json +10 -1
- package/src/modules/workflows/i18n/en.json +10 -1
- package/src/modules/workflows/i18n/es.json +10 -1
- package/src/modules/workflows/i18n/pl.json +10 -1
- package/src/modules/workflows/lib/activity-executor.ts +86 -2
- package/src/modules/workflows/lib/activity-queue-types.ts +18 -11
- package/src/modules/workflows/lib/activity-worker-handler.ts +29 -0
- package/src/modules/workflows/lib/duration.ts +51 -0
- package/src/modules/workflows/lib/event-logger.ts +1 -0
- package/src/modules/workflows/lib/format-validation-error.ts +30 -0
- package/src/modules/workflows/lib/graph-utils.ts +3 -0
- package/src/modules/workflows/lib/node-type-icons.ts +6 -2
- package/src/modules/workflows/lib/signal-handler.ts +62 -24
- package/src/modules/workflows/lib/step-handler.ts +107 -50
- package/src/modules/workflows/lib/timer-handler.ts +213 -0
- package/src/modules/workflows/lib/workflow-executor.ts +1 -1
- package/src/modules/workflows/workers/workflow-activities.worker.ts +33 -7
|
@@ -427,37 +427,39 @@ const crud = makeCrudRoute({
|
|
|
427
427
|
tenantId: ctx.auth?.tenantId ?? null,
|
|
428
428
|
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
429
429
|
}
|
|
430
|
-
const
|
|
431
|
-
em,
|
|
432
|
-
CustomerEntity,
|
|
433
|
-
{
|
|
434
|
-
id: { $in: ids },
|
|
435
|
-
deletedAt: null,
|
|
436
|
-
kind: 'person',
|
|
437
|
-
} as FilterQuery<CustomerEntity>,
|
|
438
|
-
undefined,
|
|
439
|
-
decryptionScope,
|
|
440
|
-
)
|
|
441
|
-
const entitiesById = new Map<string, CustomerEntity>()
|
|
442
|
-
for (const entity of entities) {
|
|
443
|
-
entitiesById.set(entity.id, entity)
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const where: Record<string, unknown> = {
|
|
430
|
+
const profileWhere: Record<string, unknown> = {
|
|
447
431
|
entity: { $in: ids },
|
|
448
432
|
tenantId: ctx.auth?.tenantId ?? null,
|
|
449
433
|
}
|
|
450
434
|
if (ctx.selectedOrganizationId) {
|
|
451
|
-
|
|
435
|
+
profileWhere.organizationId = ctx.selectedOrganizationId
|
|
452
436
|
}
|
|
453
437
|
|
|
454
|
-
const profiles = await
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
438
|
+
const [entities, profiles] = await Promise.all([
|
|
439
|
+
findWithDecryption(
|
|
440
|
+
em,
|
|
441
|
+
CustomerEntity,
|
|
442
|
+
{
|
|
443
|
+
id: { $in: ids },
|
|
444
|
+
deletedAt: null,
|
|
445
|
+
kind: 'person',
|
|
446
|
+
} as FilterQuery<CustomerEntity>,
|
|
447
|
+
undefined,
|
|
448
|
+
decryptionScope,
|
|
449
|
+
),
|
|
450
|
+
findWithDecryption(
|
|
451
|
+
em,
|
|
452
|
+
CustomerPersonProfile,
|
|
453
|
+
profileWhere as FilterQuery<CustomerPersonProfile>,
|
|
454
|
+
{ populate: ['entity', 'company'] },
|
|
455
|
+
decryptionScope,
|
|
456
|
+
),
|
|
457
|
+
])
|
|
458
|
+
|
|
459
|
+
const entitiesById = new Map<string, CustomerEntity>()
|
|
460
|
+
for (const entity of entities) {
|
|
461
|
+
entitiesById.set(entity.id, entity)
|
|
462
|
+
}
|
|
461
463
|
|
|
462
464
|
const profilesByEntityId = new Map<string, CustomerPersonProfile>()
|
|
463
465
|
for (const profile of profiles) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Invalidate the OrganizationScope cache when an organization mutates.
|
|
2
|
+
//
|
|
3
|
+
// resolveOrganizationScopeForRequest caches its result with a short TTL
|
|
4
|
+
// (default 60s, OM_ORG_SCOPE_CACHE_TTL_MS). When an organization is
|
|
5
|
+
// created/updated/deleted, the cached scope for users of the affected
|
|
6
|
+
// tenant may be stale (visibility set or descendant tree changed). We
|
|
7
|
+
// drop every cache entry tagged for that tenant; the TTL is the backstop
|
|
8
|
+
// for races where the event fires after a request reads the cache.
|
|
9
|
+
|
|
10
|
+
type CacheService = {
|
|
11
|
+
deleteByTags(tags: string[]): Promise<number>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const metadata = {
|
|
15
|
+
event: 'directory.organization.*',
|
|
16
|
+
persistent: false,
|
|
17
|
+
id: 'directory:invalidate-org-scope-cache',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default async function handle(
|
|
21
|
+
payload: unknown,
|
|
22
|
+
ctx: { resolve: <T = unknown>(name: string) => T },
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const data = (payload ?? {}) as Record<string, unknown>
|
|
25
|
+
const tenantId = typeof data.tenantId === 'string' ? data.tenantId : null
|
|
26
|
+
if (!tenantId) return
|
|
27
|
+
let cache: CacheService | null = null
|
|
28
|
+
try {
|
|
29
|
+
cache = ctx.resolve<CacheService>('cache')
|
|
30
|
+
} catch {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if (!cache) return
|
|
34
|
+
try {
|
|
35
|
+
await cache.deleteByTags([`org-scope:tenant:${tenantId}`])
|
|
36
|
+
} catch {
|
|
37
|
+
// best-effort; TTL is the backstop.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -5,6 +5,7 @@ import { Organization } from '@open-mercato/core/modules/directory/data/entities
|
|
|
5
5
|
import { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'
|
|
6
6
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
7
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
8
|
+
import type { CacheStrategy } from '@open-mercato/cache'
|
|
8
9
|
import { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'
|
|
9
10
|
|
|
10
11
|
export { parseSelectedOrganizationCookie, parseSelectedTenantCookie }
|
|
@@ -16,6 +17,90 @@ export type OrganizationScope = {
|
|
|
16
17
|
tenantId: string | null
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
// Phase 4 — short-TTL cache for resolveOrganizationScopeForRequest.
|
|
21
|
+
// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,
|
|
22
|
+
// requestedTenant) between membership changes; caching it bypasses 1
|
|
23
|
+
// SELECT on `organizations` per CRUD request. TTL is short (60s default)
|
|
24
|
+
// to keep staleness bounded for membership/visibility changes. Tag-based
|
|
25
|
+
// invalidation kicks the cache when user_organizations or organizations
|
|
26
|
+
// mutate (wired via invalidateOrganizationScopeCacheFor).
|
|
27
|
+
const ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'
|
|
28
|
+
// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)
|
|
29
|
+
// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`
|
|
30
|
+
// (or any positive integer) to opt in once cross-request safety is re-verified.
|
|
31
|
+
const ORG_SCOPE_DEFAULT_TTL_MS = 0
|
|
32
|
+
|
|
33
|
+
function resolveOrgScopeTtlMs(): number {
|
|
34
|
+
const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS
|
|
35
|
+
if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS
|
|
36
|
+
const parsed = Number(raw)
|
|
37
|
+
if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS
|
|
38
|
+
return parsed
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildOrgScopeCacheKey(parts: {
|
|
42
|
+
userId: string
|
|
43
|
+
effectiveTenantId: string
|
|
44
|
+
selectedOrgId: string | null
|
|
45
|
+
requestedTenantId: string | null
|
|
46
|
+
}): string {
|
|
47
|
+
const selected = parts.selectedOrgId ?? 'none'
|
|
48
|
+
const requested = parts.requestedTenantId ?? 'none'
|
|
49
|
+
return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {
|
|
53
|
+
return [
|
|
54
|
+
`${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${parts.userId}`,
|
|
55
|
+
`${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${parts.effectiveTenantId}`,
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isValidCachedScope(value: unknown): value is OrganizationScope {
|
|
60
|
+
if (typeof value !== 'object' || value === null) return false
|
|
61
|
+
const record = value as Partial<OrganizationScope>
|
|
62
|
+
const idOk = (v: unknown) => v === null || typeof v === 'string'
|
|
63
|
+
const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))
|
|
64
|
+
return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {
|
|
68
|
+
if (!container) return null
|
|
69
|
+
try {
|
|
70
|
+
const c = container.resolve('cache') as CacheStrategy | undefined
|
|
71
|
+
if (c && typeof c.get === 'function' && typeof c.set === 'function') return c
|
|
72
|
+
} catch {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function invalidateOrganizationScopeCacheForUser(
|
|
79
|
+
container: AwilixContainer,
|
|
80
|
+
userId: string,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const cache = resolveCacheFromContainer(container)
|
|
83
|
+
if (!cache?.deleteByTags) return
|
|
84
|
+
try {
|
|
85
|
+
await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`])
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.warn('[org-scope:cache] invalidate user failed', err)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function invalidateOrganizationScopeCacheForTenant(
|
|
92
|
+
container: AwilixContainer,
|
|
93
|
+
tenantId: string,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const cache = resolveCacheFromContainer(container)
|
|
96
|
+
if (!cache?.deleteByTags) return
|
|
97
|
+
try {
|
|
98
|
+
await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`])
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn('[org-scope:cache] invalidate tenant failed', err)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
19
104
|
function normalizeOrganizationId(value: unknown): string | null {
|
|
20
105
|
if (typeof value !== 'string') return null
|
|
21
106
|
const trimmed = value.trim()
|
|
@@ -270,6 +355,31 @@ export async function resolveOrganizationScopeForRequest({
|
|
|
270
355
|
}
|
|
271
356
|
|
|
272
357
|
const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)
|
|
358
|
+
const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0
|
|
359
|
+
? rawSelected.trim()
|
|
360
|
+
: null
|
|
361
|
+
|
|
362
|
+
const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null
|
|
363
|
+
const ttlMs = resolveOrgScopeTtlMs()
|
|
364
|
+
const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null
|
|
365
|
+
const cacheKey = userId
|
|
366
|
+
? buildOrgScopeCacheKey({
|
|
367
|
+
userId,
|
|
368
|
+
effectiveTenantId,
|
|
369
|
+
selectedOrgId: normalizedSelectedId,
|
|
370
|
+
requestedTenantId: requestedTenantId ?? null,
|
|
371
|
+
})
|
|
372
|
+
: null
|
|
373
|
+
|
|
374
|
+
if (cache && cacheKey && typeof cache.get === 'function') {
|
|
375
|
+
try {
|
|
376
|
+
const cached = await cache.get(cacheKey)
|
|
377
|
+
if (isValidCachedScope(cached)) return cached
|
|
378
|
+
} catch (err) {
|
|
379
|
+
console.warn('[org-scope:cache] read failed', err)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
273
383
|
const baseScope = await resolveOrganizationScope({
|
|
274
384
|
em,
|
|
275
385
|
rbac,
|
|
@@ -278,6 +388,17 @@ export async function resolveOrganizationScopeForRequest({
|
|
|
278
388
|
tenantId: effectiveTenantId,
|
|
279
389
|
})
|
|
280
390
|
|
|
391
|
+
if (cache && cacheKey && userId && typeof cache.set === 'function') {
|
|
392
|
+
try {
|
|
393
|
+
await cache.set(cacheKey, baseScope, {
|
|
394
|
+
ttl: ttlMs,
|
|
395
|
+
tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),
|
|
396
|
+
})
|
|
397
|
+
} catch (err) {
|
|
398
|
+
console.warn('[org-scope:cache] write failed', err)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
281
402
|
return baseScope
|
|
282
403
|
}
|
|
283
404
|
|
|
@@ -10,6 +10,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
10
10
|
import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
11
11
|
import { apiFetch } from '@open-mercato/ui/backend/utils/api'
|
|
12
12
|
import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
13
|
+
import { formatWorkflowValidationError } from '../../../lib/format-validation-error'
|
|
13
14
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
14
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
15
16
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
@@ -94,8 +95,8 @@ export default function EditWorkflowDefinitionPage() {
|
|
|
94
95
|
body: JSON.stringify(payload),
|
|
95
96
|
})
|
|
96
97
|
if (!response.ok) {
|
|
97
|
-
const errorBody = await readJsonSafe<{ error?: string }>(response, null)
|
|
98
|
-
throw new Error(errorBody
|
|
98
|
+
const errorBody = await readJsonSafe<{ error?: string; details?: Array<{ path?: Array<string | number>; message?: string }> }>(response, null)
|
|
99
|
+
throw new Error(formatWorkflowValidationError(errorBody, t('workflows.errors.updateFailed')))
|
|
99
100
|
}
|
|
100
101
|
return response
|
|
101
102
|
},
|
|
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
|
|
|
5
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
6
|
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
7
7
|
import { apiFetch } from '@open-mercato/ui/backend/utils/api'
|
|
8
|
+
import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
8
9
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
9
10
|
import {
|
|
10
11
|
workflowDefinitionFormSchema,
|
|
@@ -18,6 +19,7 @@ import { StepsEditor } from '../../../components/StepsEditor'
|
|
|
18
19
|
import { TransitionsEditor } from '../../../components/TransitionsEditor'
|
|
19
20
|
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
20
21
|
import { Zap } from 'lucide-react'
|
|
22
|
+
import { formatWorkflowValidationError } from '../../../lib/format-validation-error'
|
|
21
23
|
|
|
22
24
|
export default function CreateWorkflowDefinitionPage() {
|
|
23
25
|
const router = useRouter()
|
|
@@ -33,8 +35,8 @@ export default function CreateWorkflowDefinitionPage() {
|
|
|
33
35
|
})
|
|
34
36
|
|
|
35
37
|
if (!response.ok) {
|
|
36
|
-
const
|
|
37
|
-
throw new Error(
|
|
38
|
+
const errorBody = await readJsonSafe<{ error?: string; details?: Array<{ path?: Array<string | number>; message?: string }> }>(response, null)
|
|
39
|
+
throw new Error(formatWorkflowValidationError(errorBody, t('workflows.errors.createFailed')))
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
router.push('/backend/definitions')
|
|
@@ -989,7 +989,7 @@ export default function VisualEditorPage() {
|
|
|
989
989
|
<p className="mb-3 text-xs text-muted-foreground">{t('workflows.visualEditor.tapToAdd')}</p>
|
|
990
990
|
|
|
991
991
|
<div className="flex gap-2 overflow-x-auto pb-1">
|
|
992
|
-
{(['start', 'userTask', 'automated', 'waitForSignal', 'subWorkflow', 'end'] as const).map((nodeType) => {
|
|
992
|
+
{(['start', 'userTask', 'automated', 'waitForSignal', 'waitForTimer', 'subWorkflow', 'end'] as const).map((nodeType) => {
|
|
993
993
|
const Icon = NODE_TYPE_ICONS[nodeType]
|
|
994
994
|
return (
|
|
995
995
|
<button
|
|
@@ -1078,6 +1078,21 @@ export default function VisualEditorPage() {
|
|
|
1078
1078
|
<div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.waitForSignal.description}</div>
|
|
1079
1079
|
</button>
|
|
1080
1080
|
|
|
1081
|
+
{/* WAIT_FOR_TIMER Step */}
|
|
1082
|
+
<button
|
|
1083
|
+
onClick={() => handleAddNode('waitForTimer')}
|
|
1084
|
+
className="group relative w-full cursor-pointer rounded-xl border-2 border-border bg-background px-4 py-3 text-left transition-all hover:border-muted-foreground/30 hover:shadow-md"
|
|
1085
|
+
>
|
|
1086
|
+
<div className={`absolute right-2 top-2 ${NODE_TYPE_COLORS.waitForTimer} opacity-60 transition-opacity group-hover:opacity-100`}>
|
|
1087
|
+
{(() => {
|
|
1088
|
+
const Icon = NODE_TYPE_ICONS.waitForTimer
|
|
1089
|
+
return <Icon className="h-4 w-4" />
|
|
1090
|
+
})()}
|
|
1091
|
+
</div>
|
|
1092
|
+
<div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.waitForTimer.title}</div>
|
|
1093
|
+
<div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.waitForTimer.description}</div>
|
|
1094
|
+
</button>
|
|
1095
|
+
|
|
1081
1096
|
{/* SUB_WORKFLOW Step */}
|
|
1082
1097
|
<button
|
|
1083
1098
|
onClick={() => handleAddNode('subWorkflow')}
|
|
@@ -1182,6 +1197,7 @@ function getDefaultLabel(nodeType: string): string {
|
|
|
1182
1197
|
automated: 'New Automated Task',
|
|
1183
1198
|
decision: 'Decision Point',
|
|
1184
1199
|
waitForSignal: 'Wait for Signal',
|
|
1200
|
+
waitForTimer: 'Wait for Timer',
|
|
1185
1201
|
}
|
|
1186
1202
|
return labels[nodeType] || 'New Step'
|
|
1187
1203
|
}
|
|
@@ -1194,6 +1210,7 @@ function getDefaultBadge(nodeType: string): string {
|
|
|
1194
1210
|
automated: 'Automated',
|
|
1195
1211
|
decision: 'Decision',
|
|
1196
1212
|
waitForSignal: 'Wait for Signal',
|
|
1213
|
+
waitForTimer: 'Wait for Timer',
|
|
1197
1214
|
}
|
|
1198
1215
|
return badges[nodeType] || 'Task'
|
|
1199
1216
|
}
|
|
@@ -273,6 +273,45 @@ export function ActivitiesEditor({ value = [], onChange, error }: ActivitiesEdit
|
|
|
273
273
|
</div>
|
|
274
274
|
</div>
|
|
275
275
|
|
|
276
|
+
{activity.activityType === 'WAIT' && (
|
|
277
|
+
<div className="space-y-3">
|
|
278
|
+
<div>
|
|
279
|
+
<Label htmlFor={`activity-${index}-duration`} className="text-xs">
|
|
280
|
+
{t('workflows.activities.waitDuration')}
|
|
281
|
+
</Label>
|
|
282
|
+
<Input
|
|
283
|
+
id={`activity-${index}-duration`}
|
|
284
|
+
value={activity.config?.duration || ''}
|
|
285
|
+
onChange={(e) => updateActivity(index, 'config', { ...activity.config, duration: e.target.value, until: undefined })}
|
|
286
|
+
placeholder={t('workflows.activities.waitDurationPlaceholder')}
|
|
287
|
+
disabled={!!activity.config?.until}
|
|
288
|
+
className="mt-1"
|
|
289
|
+
/>
|
|
290
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
291
|
+
{t('workflows.activities.waitDurationDescription')}
|
|
292
|
+
</p>
|
|
293
|
+
</div>
|
|
294
|
+
<div className="text-xs text-center text-muted-foreground">{t('workflows.activities.waitOr')}</div>
|
|
295
|
+
<div>
|
|
296
|
+
<Label htmlFor={`activity-${index}-until`} className="text-xs">
|
|
297
|
+
{t('workflows.activities.waitUntil')}
|
|
298
|
+
</Label>
|
|
299
|
+
<Input
|
|
300
|
+
id={`activity-${index}-until`}
|
|
301
|
+
type="datetime-local"
|
|
302
|
+
value={activity.config?.until ? activity.config.until.slice(0, 16) : ''}
|
|
303
|
+
onChange={(e) => updateActivity(index, 'config', { ...activity.config, until: e.target.value ? new Date(e.target.value).toISOString() : undefined, duration: undefined })}
|
|
304
|
+
disabled={!!activity.config?.duration}
|
|
305
|
+
className="mt-1"
|
|
306
|
+
/>
|
|
307
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
308
|
+
{t('workflows.activities.waitUntilDescription')}
|
|
309
|
+
</p>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{activity.activityType !== 'WAIT' && (
|
|
276
315
|
<div>
|
|
277
316
|
<Label htmlFor={`activity-${index}-config`} className="text-xs">
|
|
278
317
|
{t('workflows.activities.config')} (JSON)
|
|
@@ -293,6 +332,7 @@ export function ActivitiesEditor({ value = [], onChange, error }: ActivitiesEdit
|
|
|
293
332
|
className="mt-1 font-mono text-xs"
|
|
294
333
|
/>
|
|
295
334
|
</div>
|
|
335
|
+
)}
|
|
296
336
|
</div>
|
|
297
337
|
</div>
|
|
298
338
|
))}
|