@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
2
2
|
import { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'
|
|
3
3
|
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
4
|
-
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
-
import { Role, RoleAcl } from '@open-mercato/core/modules/auth/data/entities'
|
|
4
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
+
import { Role, RoleAcl, UserAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'
|
|
6
6
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
7
|
|
|
8
8
|
type ActorAcl = {
|
|
@@ -33,6 +33,16 @@ type FeatureGrantCheckInput = GrantCheckContext & {
|
|
|
33
33
|
organizations?: string[] | null
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
type SuperAdminUserTargetInput = GrantCheckContext & {
|
|
37
|
+
targetUserId: string
|
|
38
|
+
actorIsSuperAdmin?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type SuperAdminRoleTargetInput = GrantCheckContext & {
|
|
42
|
+
targetRoleId: string
|
|
43
|
+
actorIsSuperAdmin?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
37
47
|
|
|
38
48
|
export async function assertActorCanGrantRoleTokens(input: RoleTokenGrantCheckInput): Promise<Role[]> {
|
|
@@ -99,6 +109,110 @@ export function normalizeGrantFeatureList(features: unknown): string[] {
|
|
|
99
109
|
return normalizeStringList(features)
|
|
100
110
|
}
|
|
101
111
|
|
|
112
|
+
export async function assertActorCanModifySuperAdminUserTarget(input: SuperAdminUserTargetInput): Promise<void> {
|
|
113
|
+
const actorIsSuperAdmin = await resolveActorIsSuperAdmin(input)
|
|
114
|
+
if (actorIsSuperAdmin) return
|
|
115
|
+
const targetIsSuperAdmin = await isUserEffectivelySuperAdmin(input.em, input.targetUserId)
|
|
116
|
+
if (targetIsSuperAdmin) {
|
|
117
|
+
throw forbidden('Only super administrators can modify super administrator accounts.')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function assertActorCanModifySuperAdminRoleTarget(input: SuperAdminRoleTargetInput): Promise<void> {
|
|
122
|
+
const actorIsSuperAdmin = await resolveActorIsSuperAdmin(input)
|
|
123
|
+
if (actorIsSuperAdmin) return
|
|
124
|
+
const targetIsSuperAdmin = await isRoleEffectivelySuperAdmin(input.em, input.targetRoleId)
|
|
125
|
+
if (targetIsSuperAdmin) {
|
|
126
|
+
throw forbidden('Only super administrators can modify super administrator roles.')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function resolveActorIsSuperAdmin(input: GrantCheckContext & { actorIsSuperAdmin?: boolean }): Promise<boolean> {
|
|
131
|
+
if (typeof input.actorIsSuperAdmin === 'boolean') return input.actorIsSuperAdmin
|
|
132
|
+
const acl = await loadActorAcl(input)
|
|
133
|
+
return acl.isSuperAdmin
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function isUserEffectivelySuperAdmin(em: EntityManager, userId: string): Promise<boolean> {
|
|
137
|
+
const directGrant = await em.findOne(
|
|
138
|
+
UserAcl,
|
|
139
|
+
{ user: userId as unknown, isSuperAdmin: true } as FilterQuery<UserAcl>,
|
|
140
|
+
)
|
|
141
|
+
if (directGrant && (directGrant as { isSuperAdmin?: boolean }).isSuperAdmin === true) return true
|
|
142
|
+
const links = await findWithDecryption(
|
|
143
|
+
em,
|
|
144
|
+
UserRole,
|
|
145
|
+
{ user: userId as unknown } as FilterQuery<UserRole>,
|
|
146
|
+
{ populate: ['role'] },
|
|
147
|
+
{ tenantId: null, organizationId: null },
|
|
148
|
+
)
|
|
149
|
+
const roleIds = (Array.isArray(links) ? links : [])
|
|
150
|
+
.map((link) => {
|
|
151
|
+
const role = (link as { role?: { id?: unknown } | string | null }).role
|
|
152
|
+
if (!role) return null
|
|
153
|
+
if (typeof role === 'string') return role
|
|
154
|
+
return role.id ? String(role.id) : null
|
|
155
|
+
})
|
|
156
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
157
|
+
if (!roleIds.length) return false
|
|
158
|
+
const roleGrant = await em.findOne(
|
|
159
|
+
RoleAcl,
|
|
160
|
+
{ role: { $in: roleIds } as unknown, isSuperAdmin: true } as FilterQuery<RoleAcl>,
|
|
161
|
+
)
|
|
162
|
+
return !!roleGrant && (roleGrant as { isSuperAdmin?: boolean }).isSuperAdmin === true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function isRoleEffectivelySuperAdmin(em: EntityManager, roleId: string): Promise<boolean> {
|
|
166
|
+
const grant = await em.findOne(
|
|
167
|
+
RoleAcl,
|
|
168
|
+
{ role: roleId as unknown, isSuperAdmin: true } as FilterQuery<RoleAcl>,
|
|
169
|
+
)
|
|
170
|
+
return !!grant && (grant as { isSuperAdmin?: boolean }).isSuperAdmin === true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function listSuperAdminUserIds(em: EntityManager, tenantId: string | null): Promise<Set<string>> {
|
|
174
|
+
const ids = new Set<string>()
|
|
175
|
+
const userAclFilter: Record<string, unknown> = { isSuperAdmin: true }
|
|
176
|
+
if (tenantId) userAclFilter.tenantId = tenantId
|
|
177
|
+
const userAcls = await em.find(UserAcl, userAclFilter as FilterQuery<UserAcl>)
|
|
178
|
+
for (const acl of userAcls) {
|
|
179
|
+
const userRef = (acl as { user?: { id?: unknown } | string | null }).user
|
|
180
|
+
const userId = userRef && typeof userRef === 'object'
|
|
181
|
+
? userRef.id
|
|
182
|
+
: userRef
|
|
183
|
+
if (userId) ids.add(String(userId))
|
|
184
|
+
}
|
|
185
|
+
const roleAcls = await em.find(
|
|
186
|
+
RoleAcl,
|
|
187
|
+
{ isSuperAdmin: true } as FilterQuery<RoleAcl>,
|
|
188
|
+
)
|
|
189
|
+
const roleIds = roleAcls
|
|
190
|
+
.map((acl) => {
|
|
191
|
+
const roleRef = (acl as { role?: { id?: unknown } | string | null }).role
|
|
192
|
+
if (!roleRef) return null
|
|
193
|
+
if (typeof roleRef === 'string') return roleRef
|
|
194
|
+
return roleRef.id ? String(roleRef.id) : null
|
|
195
|
+
})
|
|
196
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
197
|
+
if (roleIds.length) {
|
|
198
|
+
const links = await findWithDecryption(
|
|
199
|
+
em,
|
|
200
|
+
UserRole,
|
|
201
|
+
{ role: { $in: roleIds } as unknown } as FilterQuery<UserRole>,
|
|
202
|
+
{},
|
|
203
|
+
{ tenantId: null, organizationId: null },
|
|
204
|
+
)
|
|
205
|
+
for (const link of Array.isArray(links) ? links : []) {
|
|
206
|
+
const userRef = (link as { user?: { id?: unknown } | string | null }).user
|
|
207
|
+
const userId = userRef && typeof userRef === 'object'
|
|
208
|
+
? userRef.id
|
|
209
|
+
: userRef
|
|
210
|
+
if (userId) ids.add(String(userId))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return ids
|
|
214
|
+
}
|
|
215
|
+
|
|
102
216
|
async function loadActorAcl(input: GrantCheckContext): Promise<ActorAcl> {
|
|
103
217
|
const actorUserId = normalizeNullableString(input.actorUserId)
|
|
104
218
|
if (!actorUserId) throw forbidden('Not authorized to grant ACL privileges.')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComparisonOperator, LogicalOperator } from '../data/validators'
|
|
2
|
+
import { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'
|
|
2
3
|
import { getNestedValue, resolveSpecialValue } from './value-resolver'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -319,11 +320,8 @@ function endsWith(str: any, suffix: any): boolean {
|
|
|
319
320
|
return String(str).endsWith(String(suffix))
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
/**
|
|
323
|
-
* Security limits for regex operations
|
|
324
|
-
*/
|
|
325
|
-
const REGEX_TIMEOUT_MS = 100
|
|
326
323
|
const MAX_REGEX_LENGTH = 200
|
|
324
|
+
const MAX_REGEX_INPUT_LENGTH = 10_000
|
|
327
325
|
|
|
328
326
|
/**
|
|
329
327
|
* Check if string matches regex pattern (with ReDoS protection)
|
|
@@ -345,18 +343,16 @@ function matches(str: any, pattern: any): boolean {
|
|
|
345
343
|
return false
|
|
346
344
|
}
|
|
347
345
|
|
|
348
|
-
const regex = new RegExp(patternStr)
|
|
349
346
|
const testStr = String(str)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const startTime = Date.now()
|
|
353
|
-
const result = regex.test(testStr)
|
|
354
|
-
|
|
355
|
-
if (Date.now() - startTime > REGEX_TIMEOUT_MS) {
|
|
356
|
-
throw new Error('Regex execution timeout - potential ReDoS pattern detected')
|
|
347
|
+
if (testStr.length > MAX_REGEX_INPUT_LENGTH) {
|
|
348
|
+
return false
|
|
357
349
|
}
|
|
358
350
|
|
|
359
|
-
|
|
351
|
+
const result = testLinearRegex(patternStr, testStr, {
|
|
352
|
+
maxPatternLength: MAX_REGEX_LENGTH,
|
|
353
|
+
maxInputLength: MAX_REGEX_INPUT_LENGTH,
|
|
354
|
+
})
|
|
355
|
+
return result.ok ? result.matched : false
|
|
360
356
|
} catch (error) {
|
|
361
357
|
// Log the error for debugging but don't expose to user
|
|
362
358
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -77,7 +77,7 @@ export async function evaluate(
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const evaluationTime = Date.now() - startTime
|
|
80
|
+
const evaluationTime = Math.max(0, Date.now() - startTime)
|
|
81
81
|
|
|
82
82
|
// Determine if conditions were met (at least one rule matched)
|
|
83
83
|
const conditionsPassed = matchedRules.length > 0
|
|
@@ -140,7 +140,7 @@ export async function evaluateSingleRule(
|
|
|
140
140
|
// Evaluate conditions
|
|
141
141
|
const conditionsPassed = await evaluateConditions(rule.conditionExpression, data, context)
|
|
142
142
|
|
|
143
|
-
const evaluationTime = Date.now() - startTime
|
|
143
|
+
const evaluationTime = Math.max(0, Date.now() - startTime)
|
|
144
144
|
|
|
145
145
|
console.log('[RULE EVAL] Final result:', {
|
|
146
146
|
ruleId: rule.ruleId,
|
|
@@ -156,7 +156,7 @@ export async function evaluateSingleRule(
|
|
|
156
156
|
evaluationTime,
|
|
157
157
|
}
|
|
158
158
|
} catch (error) {
|
|
159
|
-
const evaluationTime = Date.now() - startTime
|
|
159
|
+
const evaluationTime = Math.max(0, Date.now() - startTime)
|
|
160
160
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
161
161
|
|
|
162
162
|
console.log('[RULE EVAL] ✗ ERROR:', errorMessage)
|
|
@@ -373,8 +373,12 @@ const DEAL_ANALYZER_PROMPT_SECTIONS: DealAnalyzerPromptSection[] = [
|
|
|
373
373
|
'2. customers.update_deal_stage — propose a stage move for a stalled high-value deal.',
|
|
374
374
|
' The runtime intercepts this via the pending-action gate; do NOT claim the change',
|
|
375
375
|
' is saved until the mutation-result-card arrives.',
|
|
376
|
+
' When moving to a named pipeline stage, first call customers.list_pipeline_stages',
|
|
377
|
+
' and pass the matching UUID as toPipelineStageId. Only use toStage for top-level',
|
|
378
|
+
' status slugs such as open, won, or lost.',
|
|
376
379
|
'Secondary read tools (use when you need more detail):',
|
|
377
380
|
'- customers.list_deals, customers.get_deal, customers.list_activities',
|
|
381
|
+
'- customers.list_pipeline_stages',
|
|
378
382
|
'- search.hybrid_search, meta.describe_agent',
|
|
379
383
|
].join('\n'),
|
|
380
384
|
},
|
|
@@ -415,7 +419,10 @@ const DEAL_ANALYZER_PROMPT_SECTIONS: DealAnalyzerPromptSection[] = [
|
|
|
415
419
|
'Lead with a one-paragraph summary of the deal portfolio health, then emit one',
|
|
416
420
|
'deal card per at-risk deal (sorted by health score ascending). After the cards,',
|
|
417
421
|
'propose exactly one stage move for the highest-value stalled deal and call',
|
|
418
|
-
'customers.update_deal_stage.
|
|
422
|
+
'customers.update_deal_stage. If the recommended stage is a pipeline stage label',
|
|
423
|
+
'rather than a status slug, resolve it with customers.list_pipeline_stages first;',
|
|
424
|
+
'do not ask the operator to paste a stage id unless no matching stage exists.',
|
|
425
|
+
'Then finish with a short conclusion naming the',
|
|
419
426
|
'highest-value stalled deal, the recommended move, and the approval status.',
|
|
420
427
|
].join('\n'),
|
|
421
428
|
},
|
|
@@ -431,6 +438,7 @@ function compileDealAnalyzerPrompt(): string {
|
|
|
431
438
|
const DEAL_ANALYZER_ALLOWED_TOOLS: readonly string[] = [
|
|
432
439
|
'customers.analyze_deals',
|
|
433
440
|
'customers.update_deal_stage',
|
|
441
|
+
'customers.list_pipeline_stages',
|
|
434
442
|
'customers.list_deals',
|
|
435
443
|
'customers.get_deal',
|
|
436
444
|
'customers.list_activities',
|
|
@@ -447,6 +455,7 @@ function buildDealAnalyzerPrepareStep() {
|
|
|
447
455
|
'customers.list_deals',
|
|
448
456
|
'customers.get_deal',
|
|
449
457
|
'customers.list_activities',
|
|
458
|
+
'customers.list_pipeline_stages',
|
|
450
459
|
'search.hybrid_search',
|
|
451
460
|
'meta.describe_agent',
|
|
452
461
|
'meta.update_task_plan',
|
|
@@ -26,6 +26,10 @@ function buildScope(ctx: CustomersToolContext, tenantId: string) {
|
|
|
26
26
|
|
|
27
27
|
const getSettingsInput = z.object({}).passthrough()
|
|
28
28
|
|
|
29
|
+
const listPipelineStagesInput = z.object({
|
|
30
|
+
pipelineId: z.string().uuid().optional().describe('Optional pipeline id to restrict returned stages.'),
|
|
31
|
+
}).passthrough()
|
|
32
|
+
|
|
29
33
|
const getSettingsTool: CustomersAiToolDefinition = {
|
|
30
34
|
name: 'customers.get_settings',
|
|
31
35
|
displayName: 'Get customers module settings',
|
|
@@ -116,6 +120,63 @@ const getSettingsTool: CustomersAiToolDefinition = {
|
|
|
116
120
|
},
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
|
|
123
|
+
const listPipelineStagesTool: CustomersAiToolDefinition = {
|
|
124
|
+
name: 'customers.list_pipeline_stages',
|
|
125
|
+
displayName: 'List customer pipeline stages',
|
|
126
|
+
description:
|
|
127
|
+
'Return customer deal pipelines and their pipeline stages, including stage ids. Use this before moving a deal to a named pipeline stage.',
|
|
128
|
+
inputSchema: listPipelineStagesInput,
|
|
129
|
+
requiredFeatures: ['customers.pipelines.view'],
|
|
130
|
+
tags: ['read', 'customers', 'pipelines'],
|
|
131
|
+
isMutation: false,
|
|
132
|
+
handler: async (rawInput, ctx) => {
|
|
133
|
+
const { tenantId } = assertTenantScope(ctx)
|
|
134
|
+
const input = listPipelineStagesInput.parse(rawInput)
|
|
135
|
+
const em = resolveEm(ctx)
|
|
136
|
+
const where: Record<string, unknown> = { tenantId }
|
|
137
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
138
|
+
const stageWhere: Record<string, unknown> = { ...where }
|
|
139
|
+
if (input.pipelineId) stageWhere.pipelineId = input.pipelineId
|
|
140
|
+
const [pipelines, stages] = await Promise.all([
|
|
141
|
+
findWithDecryption<CustomerPipeline>(
|
|
142
|
+
em,
|
|
143
|
+
CustomerPipeline,
|
|
144
|
+
where as any,
|
|
145
|
+
{ orderBy: { createdAt: 'asc' } as any } as any,
|
|
146
|
+
buildScope(ctx, tenantId),
|
|
147
|
+
),
|
|
148
|
+
findWithDecryption<CustomerPipelineStage>(
|
|
149
|
+
em,
|
|
150
|
+
CustomerPipelineStage,
|
|
151
|
+
stageWhere as any,
|
|
152
|
+
{ orderBy: { pipelineId: 'asc', order: 'asc' } as any } as any,
|
|
153
|
+
buildScope(ctx, tenantId),
|
|
154
|
+
),
|
|
155
|
+
])
|
|
156
|
+
return {
|
|
157
|
+
pipelines: pipelines
|
|
158
|
+
.filter((row) => row.tenantId === tenantId)
|
|
159
|
+
.map((row) => ({
|
|
160
|
+
id: row.id,
|
|
161
|
+
name: row.name,
|
|
162
|
+
isDefault: !!row.isDefault,
|
|
163
|
+
organizationId: row.organizationId ?? null,
|
|
164
|
+
tenantId: row.tenantId ?? null,
|
|
165
|
+
})),
|
|
166
|
+
pipelineStages: stages
|
|
167
|
+
.filter((row) => row.tenantId === tenantId)
|
|
168
|
+
.map((row) => ({
|
|
169
|
+
id: row.id,
|
|
170
|
+
pipelineId: row.pipelineId,
|
|
171
|
+
label: row.label,
|
|
172
|
+
order: row.order,
|
|
173
|
+
organizationId: row.organizationId ?? null,
|
|
174
|
+
tenantId: row.tenantId ?? null,
|
|
175
|
+
})),
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const settingsAiTools: CustomersAiToolDefinition[] = [getSettingsTool, listPipelineStagesTool]
|
|
120
181
|
|
|
121
182
|
export default settingsAiTools
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import { format } from 'date-fns'
|
|
3
2
|
import type { ActivityType } from './fieldConfig'
|
|
4
3
|
|
|
5
4
|
export type RsvpStatus = 'pending' | 'accepted' | 'declined' | 'tentative'
|
|
@@ -63,6 +62,18 @@ const DEFAULT_REMINDER_MINUTES: Record<ActivityType, number> = {
|
|
|
63
62
|
email: 15,
|
|
64
63
|
}
|
|
65
64
|
|
|
65
|
+
function padDatePart(value: number): string {
|
|
66
|
+
return String(value).padStart(2, '0')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatLocalDateInput(date: Date): string {
|
|
70
|
+
return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatLocalTimeInput(date: Date): string {
|
|
74
|
+
return `${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
interface UseScheduleFormStateParams {
|
|
67
78
|
open: boolean
|
|
68
79
|
editData: ScheduleActivityEditData | null | undefined
|
|
@@ -71,7 +82,7 @@ interface UseScheduleFormStateParams {
|
|
|
71
82
|
export function useScheduleFormState({ open, editData }: UseScheduleFormStateParams) {
|
|
72
83
|
const [activityType, setActivityType] = React.useState<ActivityType>('meeting')
|
|
73
84
|
const [title, setTitle] = React.useState('')
|
|
74
|
-
const [date, setDate] = React.useState(() =>
|
|
85
|
+
const [date, setDate] = React.useState(() => formatLocalDateInput(new Date()))
|
|
75
86
|
const [startTime, setStartTime] = React.useState('10:00')
|
|
76
87
|
const [duration, setDuration] = React.useState(30)
|
|
77
88
|
const [allDay, setAllDay] = React.useState(false)
|
|
@@ -101,15 +112,15 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
101
112
|
// For historical activities the canonical timestamp is `occurredAt`; for
|
|
102
113
|
// planned/future ones it's `scheduledAt`. Without this fallback editing a
|
|
103
114
|
// past activity prefilled to "today" instead of its actual moment (#1807).
|
|
104
|
-
//
|
|
105
|
-
//
|
|
115
|
+
// Keep seed values in the user's local timezone, matching the cluster-E
|
|
116
|
+
// local-day convention.
|
|
106
117
|
const sourceTimestamp = editData.occurredAt ?? editData.scheduledAt ?? null
|
|
107
118
|
const seedDate = sourceTimestamp ? new Date(sourceTimestamp) : new Date()
|
|
108
119
|
const seedDateValid = !Number.isNaN(seedDate.getTime())
|
|
109
120
|
const fallbackNow = new Date()
|
|
110
121
|
const dateForForm = seedDateValid ? seedDate : fallbackNow
|
|
111
|
-
setDate(
|
|
112
|
-
setStartTime(
|
|
122
|
+
setDate(formatLocalDateInput(dateForForm))
|
|
123
|
+
setStartTime(formatLocalTimeInput(dateForForm))
|
|
113
124
|
setDuration(editData.durationMinutes ?? 30)
|
|
114
125
|
setAllDay(editData.allDay ?? false)
|
|
115
126
|
setDescription(editData.body ?? '')
|
|
@@ -170,7 +181,7 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
170
181
|
// Create mode: reset all fields
|
|
171
182
|
setActivityType('meeting')
|
|
172
183
|
setTitle('')
|
|
173
|
-
setDate(
|
|
184
|
+
setDate(formatLocalDateInput(new Date()))
|
|
174
185
|
setStartTime('10:00')
|
|
175
186
|
setDuration(30)
|
|
176
187
|
setAllDay(false)
|
|
@@ -1,54 +1,46 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
-
import { sql } from '@mikro-orm/postgresql'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// The previous implementation probed `information_schema.columns` on every
|
|
4
|
+
// first call via `em.getKysely()` to decide whether to add a `deletedAt: null`
|
|
5
|
+
// filter. When that probe ran inside an already-active MikroORM transaction it
|
|
6
|
+
// shared the transaction's connection — and any failure (including transient
|
|
7
|
+
// connection-pool state, an aborted TX from a sibling query, or pg returning
|
|
8
|
+
// 25P02) propagated back into the host transaction, poisoning every following
|
|
9
|
+
// query with `current transaction is aborted, commands ignored until end of
|
|
10
|
+
// transaction block`. That is what produced the standalone-CI failure burst
|
|
11
|
+
// (loadPersonCompanyLinks → 25P02 → customers.people.create → 500) and the
|
|
12
|
+
// sync_excel worker `failedCount: 1` regression.
|
|
13
|
+
//
|
|
14
|
+
// The `deleted_at` column on `customer_person_company_links` has been part of
|
|
15
|
+
// the committed module snapshot since `Migration20260415095203` (April 2026)
|
|
16
|
+
// and is referenced by `Migration20260417140000` (partial unique index uses
|
|
17
|
+
// `where deleted_at is null`). Any environment running these migrations
|
|
18
|
+
// already has the column. So the probe is no longer load-bearing — it only
|
|
19
|
+
// adds risk. We now unconditionally include `deletedAt: null` in the filter.
|
|
20
|
+
// If a downstream DB is genuinely un-migrated, the underlying `em.find` will
|
|
21
|
+
// surface the authentic `column "deleted_at" of relation
|
|
22
|
+
// "customer_person_company_links" does not exist` (pg 42703) pointing the
|
|
23
|
+
// operator at the real fix (`yarn db:migrate`).
|
|
24
|
+
//
|
|
25
|
+
// `customerPersonCompanyLinksSupportDeletedAt` and
|
|
26
|
+
// `warnMissingCustomerPersonCompanyLinksDeletedAt` are kept as exported
|
|
27
|
+
// no-op-equivalents so external callers (and the published @open-mercato/core
|
|
28
|
+
// API surface) don't break.
|
|
6
29
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export async function customerPersonCompanyLinksSupportDeletedAt(em: EntityManager): Promise<boolean> {
|
|
11
|
-
if (typeof (em as { getKysely?: unknown }).getKysely !== 'function') {
|
|
12
|
-
return true
|
|
13
|
-
}
|
|
14
|
-
if (!supportsDeletedAtColumnPromise) {
|
|
15
|
-
const db = em.getKysely<any>() as any
|
|
16
|
-
const probe: Promise<boolean> = db
|
|
17
|
-
.selectFrom('information_schema.columns')
|
|
18
|
-
.select(['column_name'])
|
|
19
|
-
.where(sql<boolean>`table_schema = current_schema()`)
|
|
20
|
-
.where('table_name', '=', PERSON_COMPANY_LINKS_TABLE)
|
|
21
|
-
.where('column_name', '=', PERSON_COMPANY_LINKS_DELETED_AT_COLUMN)
|
|
22
|
-
.executeTakeFirst()
|
|
23
|
-
.then((row: unknown) => !!row)
|
|
24
|
-
.catch(() => false)
|
|
25
|
-
supportsDeletedAtColumnPromise = probe
|
|
26
|
-
return probe
|
|
27
|
-
}
|
|
28
|
-
return supportsDeletedAtColumnPromise
|
|
30
|
+
export async function customerPersonCompanyLinksSupportDeletedAt(_em: EntityManager): Promise<boolean> {
|
|
31
|
+
return true
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
export function warnMissingCustomerPersonCompanyLinksDeletedAt(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
warnedAboutMissingDeletedAtColumn = true
|
|
36
|
-
console.warn(
|
|
37
|
-
`[${source}] missing ${PERSON_COMPANY_LINKS_TABLE}.${PERSON_COMPANY_LINKS_DELETED_AT_COLUMN}; ` +
|
|
38
|
-
'continuing without link soft-delete filtering. Run yarn db:migrate.',
|
|
39
|
-
)
|
|
34
|
+
export function warnMissingCustomerPersonCompanyLinksDeletedAt(_source: string): void {
|
|
35
|
+
// No-op: column is guaranteed present by Migration20260415095203 and the
|
|
36
|
+
// module snapshot. Kept for backward-compatible exports.
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
export async function withActiveCustomerPersonCompanyLinkFilter<T extends Record<string, unknown>>(
|
|
43
|
-
|
|
40
|
+
_em: EntityManager,
|
|
44
41
|
where: T,
|
|
45
|
-
|
|
42
|
+
_source: string,
|
|
46
43
|
): Promise<T & { deletedAt?: null }> {
|
|
47
|
-
const supportsDeletedAt = await customerPersonCompanyLinksSupportDeletedAt(em)
|
|
48
|
-
if (!supportsDeletedAt) {
|
|
49
|
-
warnMissingCustomerPersonCompanyLinksDeletedAt(source)
|
|
50
|
-
return { ...where }
|
|
51
|
-
}
|
|
52
44
|
return { ...where, deletedAt: null }
|
|
53
45
|
}
|
|
54
46
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Idempotent backstop for `customer_person_company_links.deleted_at`.
|
|
5
|
+
*
|
|
6
|
+
* The column was originally added by `Migration20260415095203` and is
|
|
7
|
+
* referenced by `Migration20260417140000`'s partial unique index
|
|
8
|
+
* (`where "deleted_at" is null`). It is part of the committed
|
|
9
|
+
* `migrations/.snapshot-open-mercato.json`.
|
|
10
|
+
*
|
|
11
|
+
* However, the runtime defensive probe that lived in
|
|
12
|
+
* `packages/core/src/modules/customers/lib/personCompanyLinkTable.ts`
|
|
13
|
+
* historically existed because an aborted-transaction state on a
|
|
14
|
+
* shared connection could (very rarely) make `loadPersonCompanyLinks`
|
|
15
|
+
* fail in a way that *looked* like the column was missing — and the
|
|
16
|
+
* cached fallback then silently dropped the `deletedAt: null` filter.
|
|
17
|
+
* That probe is removed in the same change as this migration (no
|
|
18
|
+
* Kysely query is ever issued from inside an active MikroORM TX);
|
|
19
|
+
* this migration is the matching belt-and-suspenders guarantee that
|
|
20
|
+
* the column is physically present so the unconditional `deletedAt:
|
|
21
|
+
* null` filter cannot 42703.
|
|
22
|
+
*
|
|
23
|
+
* Postgres-native `add column if not exists` is used so this re-applies
|
|
24
|
+
* cleanly on already-migrated databases without raising.
|
|
25
|
+
*/
|
|
26
|
+
export class Migration20260521175027 extends Migration {
|
|
27
|
+
|
|
28
|
+
override async up(): Promise<void> {
|
|
29
|
+
this.addSql(`alter table "customer_person_company_links" add column if not exists "deleted_at" timestamptz null;`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override async down(): Promise<void> {
|
|
33
|
+
// No-op on the way down: dropping the column would break
|
|
34
|
+
// `Migration20260417140000`'s partial unique index, which is the
|
|
35
|
+
// authoritative owner of the column going forward. The original
|
|
36
|
+
// `Migration20260415095203` down still drops it for full rollback.
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
@@ -32,6 +32,7 @@ type UserResponse = {
|
|
|
32
32
|
type BaseProps = {
|
|
33
33
|
tenantId?: string | null
|
|
34
34
|
organizationId?: string | null
|
|
35
|
+
preserveOnTenantChange?: boolean
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
type RoleProps = BaseProps & {
|
|
@@ -92,7 +93,7 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
92
93
|
(widget: WidgetCatalogItem) => resolveWidgetText(t, widget.id, 'description', widget.description || ''),
|
|
93
94
|
[t],
|
|
94
95
|
)
|
|
95
|
-
const { kind, targetId, tenantId, organizationId } = props
|
|
96
|
+
const { kind, targetId, tenantId, organizationId, preserveOnTenantChange = false } = props
|
|
96
97
|
const [catalog, setCatalog] = React.useState<WidgetCatalogItem[]>([])
|
|
97
98
|
const [loading, setLoading] = React.useState(true)
|
|
98
99
|
const [saving, setSaving] = React.useState(false)
|
|
@@ -136,10 +137,16 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
136
137
|
setCatalog(mapped)
|
|
137
138
|
}, [])
|
|
138
139
|
|
|
139
|
-
const
|
|
140
|
+
const tenantIdRef = React.useRef(tenantId)
|
|
141
|
+
React.useEffect(() => { tenantIdRef.current = tenantId }, [tenantId])
|
|
142
|
+
const organizationIdRef = React.useRef(organizationId)
|
|
143
|
+
React.useEffect(() => { organizationIdRef.current = organizationId }, [organizationId])
|
|
144
|
+
const hasMountedRef = React.useRef(false)
|
|
145
|
+
|
|
146
|
+
const loadRoleData = React.useCallback(async (forTenantId: string | null | undefined, forOrganizationId: string | null | undefined) => {
|
|
140
147
|
const params = new URLSearchParams({ roleId: targetId })
|
|
141
|
-
if (
|
|
142
|
-
if (
|
|
148
|
+
if (forTenantId) params.set('tenantId', forTenantId)
|
|
149
|
+
if (forOrganizationId) params.set('organizationId', forOrganizationId)
|
|
143
150
|
const data = await readApiResultOrThrow<RoleResponse>(
|
|
144
151
|
`/api/dashboards/roles/widgets?${params.toString()}`,
|
|
145
152
|
undefined,
|
|
@@ -151,12 +158,12 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
151
158
|
setMode('override')
|
|
152
159
|
setOriginalMode('override')
|
|
153
160
|
setEffective(ids)
|
|
154
|
-
}, [
|
|
161
|
+
}, [targetId])
|
|
155
162
|
|
|
156
|
-
const loadUserData = React.useCallback(async () => {
|
|
163
|
+
const loadUserData = React.useCallback(async (forTenantId: string | null | undefined, forOrganizationId: string | null | undefined) => {
|
|
157
164
|
const params = new URLSearchParams({ userId: targetId })
|
|
158
|
-
if (
|
|
159
|
-
if (
|
|
165
|
+
if (forTenantId) params.set('tenantId', forTenantId)
|
|
166
|
+
if (forOrganizationId) params.set('organizationId', forOrganizationId)
|
|
160
167
|
const data = await readApiResultOrThrow<UserResponse>(
|
|
161
168
|
`/api/dashboards/users/widgets?${params.toString()}`,
|
|
162
169
|
undefined,
|
|
@@ -168,7 +175,7 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
168
175
|
setMode(data.mode || 'inherit')
|
|
169
176
|
setOriginalMode(data.mode || 'inherit')
|
|
170
177
|
setEffective(Array.isArray(data.effectiveWidgetIds) ? data.effectiveWidgetIds : [])
|
|
171
|
-
}, [
|
|
178
|
+
}, [targetId])
|
|
172
179
|
|
|
173
180
|
React.useEffect(() => {
|
|
174
181
|
let cancelled = false
|
|
@@ -177,21 +184,40 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
177
184
|
setError(null)
|
|
178
185
|
try {
|
|
179
186
|
await loadCatalog()
|
|
180
|
-
if (kind === 'role') await loadRoleData()
|
|
181
|
-
else await loadUserData()
|
|
187
|
+
if (kind === 'role') await loadRoleData(tenantIdRef.current, organizationIdRef.current)
|
|
188
|
+
else await loadUserData(tenantIdRef.current, organizationIdRef.current)
|
|
182
189
|
} catch (err) {
|
|
183
190
|
console.error('Failed to load widget visibility data', err)
|
|
184
191
|
if (!cancelled) {
|
|
185
192
|
setError(tRef.current('dashboards.widgets.error.load', 'Unable to load widget configuration.'))
|
|
186
193
|
}
|
|
187
194
|
} finally {
|
|
188
|
-
if (!cancelled)
|
|
195
|
+
if (!cancelled) {
|
|
196
|
+
setLoading(false)
|
|
197
|
+
hasMountedRef.current = true
|
|
198
|
+
}
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
201
|
load()
|
|
192
202
|
return () => { cancelled = true }
|
|
193
203
|
}, [kind, loadCatalog, loadRoleData, loadUserData])
|
|
194
204
|
|
|
205
|
+
React.useEffect(() => {
|
|
206
|
+
if (!hasMountedRef.current) return
|
|
207
|
+
if (preserveOnTenantChange) return
|
|
208
|
+
let cancelled = false
|
|
209
|
+
async function refetch() {
|
|
210
|
+
try {
|
|
211
|
+
if (kind === 'role') await loadRoleData(tenantId, organizationId)
|
|
212
|
+
else await loadUserData(tenantId, organizationId)
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('Failed to reload widget visibility data', err)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
refetch()
|
|
218
|
+
return () => { cancelled = true }
|
|
219
|
+
}, [tenantId, organizationId, kind, loadRoleData, loadUserData, preserveOnTenantChange])
|
|
220
|
+
|
|
195
221
|
const toggle = React.useCallback((id: string) => {
|
|
196
222
|
setSelected((prev) => (prev.includes(id) ? prev.filter((value) => value !== id) : [...prev, id]))
|
|
197
223
|
}, [])
|
|
@@ -44,6 +44,8 @@ export async function GET(req: Request) {
|
|
|
44
44
|
description: integration.description ?? null,
|
|
45
45
|
providerKey: integration.providerKey ?? null,
|
|
46
46
|
direction: adapter.direction,
|
|
47
|
+
runMode: adapter.runMode ?? 'generic',
|
|
48
|
+
canStartRun: adapter.runMode !== 'provider',
|
|
47
49
|
supportedEntities: adapter.supportedEntities,
|
|
48
50
|
hasCredentials: Boolean(credentials),
|
|
49
51
|
isEnabled: state.isEnabled,
|