@open-mercato/core 0.6.7-develop.6706.1.b3a4c759bb → 0.6.7-develop.6726.1.983ae8a07e
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/api/admin/nav.js +14 -4
- package/dist/modules/auth/api/admin/nav.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +8 -19
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +31 -14
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/customers/api/utils.js +12 -18
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +17 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +7 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/cli.js +11 -4
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +4 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +14 -10
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/batch/route.js +4 -2
- package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/route.js +20 -2
- package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
- package/dist/modules/dashboards/lib/aggregations.js +76 -40
- package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
- package/dist/modules/dashboards/lib/exactDecimal.js +69 -0
- package/dist/modules/dashboards/lib/exactDecimal.js.map +7 -0
- package/dist/modules/dashboards/services/widgetDataService.js +228 -5
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/feature_toggles/components/FeatureGuard.js +3 -2
- package/dist/modules/feature_toggles/components/FeatureGuard.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/staleTime.js +5 -0
- package/dist/modules/feature_toggles/components/hooks/staleTime.js.map +7 -0
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/route.js +16 -1
- package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js +32 -0
- package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js.map +7 -0
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +0 -5
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
- package/dist/modules/integrations/widgets/injection/external-ids/widget.js +14 -0
- package/dist/modules/integrations/widgets/injection/external-ids/widget.js.map +7 -0
- package/dist/modules/messages/lib/searchLookup.js +8 -16
- package/dist/modules/messages/lib/searchLookup.js.map +2 -2
- package/dist/modules/query_index/api/openapi.js +13 -1
- package/dist/modules/query_index/api/openapi.js.map +2 -2
- package/dist/modules/query_index/api/status.js +165 -105
- package/dist/modules/query_index/api/status.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +128 -41
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/query_index/lib/batch.js +11 -4
- package/dist/modules/query_index/lib/batch.js.map +2 -2
- package/dist/modules/query_index/lib/document.js +12 -5
- package/dist/modules/query_index/lib/document.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +36 -4
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +1 -1
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +7 -4
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +14 -3
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +22 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/useSalesChannelsEnabled.js +2 -35
- package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +2 -2
- package/dist/modules/sales/data/validators.js +25 -4
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/admin/nav.ts +24 -4
- package/src/modules/auth/api/users/route.ts +8 -28
- package/src/modules/auth/lib/backendChrome.tsx +51 -13
- package/src/modules/customers/api/utils.ts +12 -31
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +35 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +10 -1
- package/src/modules/customers/cli.ts +11 -4
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +4 -0
- package/src/modules/customers/components/formConfig.tsx +12 -8
- package/src/modules/dashboards/api/widgets/data/batch/route.ts +4 -0
- package/src/modules/dashboards/api/widgets/data/route.ts +21 -0
- package/src/modules/dashboards/lib/aggregations.ts +122 -43
- package/src/modules/dashboards/lib/exactDecimal.ts +96 -0
- package/src/modules/dashboards/services/widgetDataService.ts +342 -4
- package/src/modules/feature_toggles/components/FeatureGuard.tsx +9 -2
- package/src/modules/feature_toggles/components/hooks/staleTime.ts +5 -0
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.ts +13 -1
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagJson.ts +12 -3
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagNumber.ts +10 -1
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagString.ts +10 -1
- package/src/modules/inbox_ops/api/proposals/route.ts +20 -1
- package/src/modules/inbox_ops/subscribers/search-reindex-proposal-created.ts +39 -0
- package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +0 -5
- package/src/modules/integrations/widgets/injection/external-ids/widget.ts +13 -0
- package/src/modules/messages/lib/searchLookup.ts +11 -34
- package/src/modules/query_index/api/openapi.ts +12 -0
- package/src/modules/query_index/api/status.ts +238 -136
- package/src/modules/query_index/components/QueryIndexesTable.tsx +192 -46
- package/src/modules/query_index/i18n/de.json +5 -3
- package/src/modules/query_index/i18n/en.json +5 -3
- package/src/modules/query_index/i18n/es.json +5 -3
- package/src/modules/query_index/i18n/pl.json +5 -3
- package/src/modules/query_index/lib/batch.ts +11 -4
- package/src/modules/query_index/lib/document.ts +35 -4
- package/src/modules/query_index/lib/engine.ts +53 -5
- package/src/modules/query_index/lib/indexer.ts +1 -1
- package/src/modules/query_index/lib/search-tokens.ts +13 -4
- package/src/modules/sales/api/documents/factory.ts +28 -3
- package/src/modules/sales/commands/documents.ts +26 -2
- package/src/modules/sales/components/useSalesChannelsEnabled.ts +2 -48
- package/src/modules/sales/data/validators.ts +50 -5
|
@@ -22,6 +22,7 @@ import { resolveRegisteredLucideIconNode } from '@open-mercato/ui/backend/icons/
|
|
|
22
22
|
import { profilePathPrefixes, profileSections } from './profile-sections'
|
|
23
23
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
24
24
|
import { filterGrantsByEnabledModules } from '@open-mercato/shared/security/enabledModulesRegistry'
|
|
25
|
+
import { getNavGroupOrderOverride } from '@open-mercato/shared/modules/overrides'
|
|
25
26
|
import {
|
|
26
27
|
getSelectedOrganizationFromRequest,
|
|
27
28
|
resolveFeatureCheckContext,
|
|
@@ -147,19 +148,35 @@ async function serializeNavItem(item: AdminNavItem): Promise<ResolvedNavItem> {
|
|
|
147
148
|
}
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
const defaultGroupOrder = [
|
|
152
|
+
'customers.nav.group',
|
|
153
|
+
'catalog.nav.group',
|
|
154
|
+
'customers~sales.nav.group',
|
|
155
|
+
'wms.nav.group',
|
|
156
|
+
'resources.nav.group',
|
|
157
|
+
'staff.nav.group',
|
|
158
|
+
'entities.nav.group',
|
|
159
|
+
'directory.nav.group',
|
|
160
|
+
'attachments.nav.group',
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Group ids ranked ahead of everything else, most significant first.
|
|
165
|
+
*
|
|
166
|
+
* An app may prepend its own ids via `overrides.nav.groupOrder` in `modules.ts`; ids it does not name
|
|
167
|
+
* keep the ordering they have today. With no override configured this returns `defaultGroupOrder`
|
|
168
|
+
* itself, so ordering is unchanged for every existing install.
|
|
169
|
+
*/
|
|
170
|
+
function resolveGroupOrder(): string[] {
|
|
171
|
+
const override = getNavGroupOrderOverride()
|
|
172
|
+
if (!override || override.length === 0) return defaultGroupOrder
|
|
173
|
+
const overridden = new Set(override)
|
|
174
|
+
return [...override, ...defaultGroupOrder.filter((id) => !overridden.has(id))]
|
|
175
|
+
}
|
|
176
|
+
|
|
150
177
|
function normalizeGroupWeights(groups: NavGroupWithWeight[]): NavGroupWithWeight[] {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
'catalog.nav.group',
|
|
154
|
-
'customers~sales.nav.group',
|
|
155
|
-
'wms.nav.group',
|
|
156
|
-
'resources.nav.group',
|
|
157
|
-
'staff.nav.group',
|
|
158
|
-
'entities.nav.group',
|
|
159
|
-
'directory.nav.group',
|
|
160
|
-
'attachments.nav.group',
|
|
161
|
-
]
|
|
162
|
-
const groupOrderIndex = new Map(defaultGroupOrder.map((id, index) => [id, index]))
|
|
178
|
+
const groupOrder = resolveGroupOrder()
|
|
179
|
+
const groupOrderIndex = new Map(groupOrder.map((id, index) => [id, index]))
|
|
163
180
|
groups.sort((a, b) => {
|
|
164
181
|
const aIndex = groupOrderIndex.get(a.id)
|
|
165
182
|
const bIndex = groupOrderIndex.get(b.id)
|
|
@@ -171,7 +188,7 @@ function normalizeGroupWeights(groups: NavGroupWithWeight[]): NavGroupWithWeight
|
|
|
171
188
|
if (a.weight !== b.weight) return a.weight - b.weight
|
|
172
189
|
return a.name.localeCompare(b.name)
|
|
173
190
|
})
|
|
174
|
-
const defaultGroupCount =
|
|
191
|
+
const defaultGroupCount = groupOrder.length
|
|
175
192
|
groups.forEach((group, index) => {
|
|
176
193
|
const rank = groupOrderIndex.get(group.id)
|
|
177
194
|
const fallbackWeight = typeof group.weight === 'number' ? group.weight : 10_000
|
|
@@ -275,6 +292,11 @@ export async function resolveBackendChromePayload({
|
|
|
275
292
|
|
|
276
293
|
let scopedOrganizationId: string | null = auth.orgId ?? null
|
|
277
294
|
let scopedTenantId: string | null = auth.tenantId ?? null
|
|
295
|
+
// The organization the caller actually *selected*, as distinct from the one the scope resolver fell
|
|
296
|
+
// back to. `resolveFeatureCheckContext` resolves `organizationId` to `auth.orgId` when no concrete
|
|
297
|
+
// organization is selected — which is precisely what an all-organizations view produces — so the
|
|
298
|
+
// resolved id cannot answer "which organization am I viewing".
|
|
299
|
+
let concretelySelectedOrganizationId: string | null = null
|
|
278
300
|
let allowNavigation = true
|
|
279
301
|
|
|
280
302
|
try {
|
|
@@ -287,12 +309,14 @@ export async function resolveBackendChromePayload({
|
|
|
287
309
|
})
|
|
288
310
|
scopedOrganizationId = organizationId
|
|
289
311
|
scopedTenantId = scope.tenantId ?? auth.tenantId ?? null
|
|
312
|
+
concretelySelectedOrganizationId = scope.selectedId ?? null
|
|
290
313
|
if (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length === 0) {
|
|
291
314
|
allowNavigation = false
|
|
292
315
|
}
|
|
293
316
|
} catch {
|
|
294
317
|
scopedOrganizationId = auth.orgId ?? null
|
|
295
318
|
scopedTenantId = auth.tenantId ?? null
|
|
319
|
+
concretelySelectedOrganizationId = null
|
|
296
320
|
}
|
|
297
321
|
|
|
298
322
|
const acl = allowNavigation
|
|
@@ -411,6 +435,11 @@ export async function resolveBackendChromePayload({
|
|
|
411
435
|
?? (fallbackOrganizationId && !isAllOrganizationsSelection(fallbackOrganizationId) ? fallbackOrganizationId : null)
|
|
412
436
|
|
|
413
437
|
let brand: BackendChromePayload['brand'] = null
|
|
438
|
+
// Resolved here rather than left to callers. `brand` only populates when the organization has a
|
|
439
|
+
// logo, so it is a branding channel, not a dependable "which organization am I viewing" source.
|
|
440
|
+
// Without this field every downstream app has to fetch `/api/directory/organization-switcher` and
|
|
441
|
+
// walk its tree for the selected id. The row is already loaded below, so the name costs nothing.
|
|
442
|
+
let currentOrganization: BackendChromePayload['currentOrganization'] = null
|
|
414
443
|
if (brandOrganizationId && scopedTenantId) {
|
|
415
444
|
try {
|
|
416
445
|
const organization = await findOneWithDecryption(
|
|
@@ -420,6 +449,12 @@ export async function resolveBackendChromePayload({
|
|
|
420
449
|
undefined,
|
|
421
450
|
{ tenantId: scopedTenantId, organizationId: brandOrganizationId },
|
|
422
451
|
)
|
|
452
|
+
// Only when a concrete organization was selected. Under an all-organizations view
|
|
453
|
+
// `brandOrganizationId` still resolves (to the caller's own organization, which is what keeps
|
|
454
|
+
// branding working), so gating on the loaded row alone would misreport the scope.
|
|
455
|
+
if (organization && concretelySelectedOrganizationId === brandOrganizationId) {
|
|
456
|
+
currentOrganization = { id: String(organization.id), name: organization.name }
|
|
457
|
+
}
|
|
423
458
|
if (organization?.logoUrl) {
|
|
424
459
|
brand = {
|
|
425
460
|
name: organization.name,
|
|
@@ -430,7 +465,9 @@ export async function resolveBackendChromePayload({
|
|
|
430
465
|
}
|
|
431
466
|
}
|
|
432
467
|
} catch {
|
|
468
|
+
// Fail soft, as before: a failed organization lookup must not take down the nav payload.
|
|
433
469
|
brand = null
|
|
470
|
+
currentOrganization = null
|
|
434
471
|
}
|
|
435
472
|
}
|
|
436
473
|
|
|
@@ -443,5 +480,6 @@ export async function resolveBackendChromePayload({
|
|
|
443
480
|
grantedFeatures,
|
|
444
481
|
roles: Array.isArray(auth.roles) ? auth.roles : [],
|
|
445
482
|
brand,
|
|
483
|
+
currentOrganization,
|
|
446
484
|
}
|
|
447
485
|
}
|
|
@@ -4,8 +4,7 @@ import { sql } from 'kysely'
|
|
|
4
4
|
import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
|
|
5
5
|
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
6
6
|
import type { QueryCustomFieldSource, QueryJoinEdge, QueryEngine } from '@open-mercato/shared/lib/query/types'
|
|
7
|
-
import {
|
|
8
|
-
import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
|
|
7
|
+
import { findEntityIdsBySearchTokensCompat, type SearchTokenDatabase } from '@open-mercato/shared/lib/search/tokenLookup'
|
|
9
8
|
import { SortDir } from '@open-mercato/shared/lib/query/types'
|
|
10
9
|
|
|
11
10
|
const { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({
|
|
@@ -106,36 +105,18 @@ async function findSearchTokenEntityIds({
|
|
|
106
105
|
fields,
|
|
107
106
|
query,
|
|
108
107
|
}: SearchTokenMatchInput): Promise<string[] | null> {
|
|
109
|
-
const trimmed = query.trim()
|
|
110
|
-
if (!trimmed) return null
|
|
111
|
-
|
|
112
|
-
const tokens = tokenizeText(trimmed, resolveSearchConfig())
|
|
113
|
-
if (!tokens.hashes.length) return []
|
|
114
|
-
|
|
115
108
|
const em = ctx.container.resolve('em') as EntityManager
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
searchQuery = searchQuery.where(sql<boolean>`tenant_id is not distinct from ${ctx.auth?.tenantId ?? null}`)
|
|
128
|
-
}
|
|
129
|
-
if (ctx.selectedOrganizationId) {
|
|
130
|
-
searchQuery = searchQuery.where('organization_id', '=', ctx.selectedOrganizationId)
|
|
131
|
-
} else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {
|
|
132
|
-
searchQuery = searchQuery.where('organization_id', 'in', ctx.organizationIds)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const rows = await searchQuery.execute() as Array<{ entity_id?: unknown }>
|
|
136
|
-
return rows
|
|
137
|
-
.map((row) => (typeof row.entity_id === 'string' ? row.entity_id : null))
|
|
138
|
-
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
109
|
+
return findEntityIdsBySearchTokensCompat({
|
|
110
|
+
db: em.getKysely<SearchTokenDatabase>(),
|
|
111
|
+
entityType,
|
|
112
|
+
query,
|
|
113
|
+
fields,
|
|
114
|
+
scope: {
|
|
115
|
+
tenantId: ctx.auth?.tenantId !== undefined ? ctx.auth?.tenantId ?? null : undefined,
|
|
116
|
+
organizationId: ctx.selectedOrganizationId ?? undefined,
|
|
117
|
+
organizationIds: ctx.organizationIds,
|
|
118
|
+
},
|
|
119
|
+
})
|
|
139
120
|
}
|
|
140
121
|
|
|
141
122
|
async function mapScopedEntityIds({
|
package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuarde
|
|
|
18
18
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
19
19
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
20
20
|
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
21
|
+
import { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
21
22
|
|
|
22
23
|
// Deal status values written by the kanban flow. These intentionally do NOT include 'closed'
|
|
23
24
|
// because the rest of the app only persists 'loose' for lost deals (see StatusFilterPopover
|
|
@@ -56,7 +57,18 @@ export type QuickDealCompanyOption = {
|
|
|
56
57
|
label: string
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Registered component id for the kanban quick-add dialog. Downstream apps can
|
|
62
|
+
* target it from `widgets/components.ts` to replace/wrap the dialog or to
|
|
63
|
+
* transform its props — e.g. `defaultProbability: null` so quick-create stops
|
|
64
|
+
* seeding a probability that overrides pipeline automation (#4329).
|
|
65
|
+
*/
|
|
66
|
+
export const QUICK_DEAL_DIALOG_COMPONENT_ID = 'dialog:customers.deals.quickDeal'
|
|
67
|
+
|
|
68
|
+
/** Probability seeded into the quick-add form when the host passes no override. */
|
|
69
|
+
export const DEFAULT_QUICK_DEAL_PROBABILITY = 25
|
|
70
|
+
|
|
71
|
+
export type QuickDealDialogProps = {
|
|
60
72
|
open: boolean
|
|
61
73
|
context: QuickDealContext | null
|
|
62
74
|
onClose: () => void
|
|
@@ -64,6 +76,13 @@ type QuickDealDialogProps = {
|
|
|
64
76
|
currentUserId?: string
|
|
65
77
|
currentUserLabel?: string
|
|
66
78
|
companies?: QuickDealCompanyOption[]
|
|
79
|
+
/**
|
|
80
|
+
* Probability pre-filled in the collapsed "More details" group. `null` leaves
|
|
81
|
+
* the field empty, so quick-create sends no `probability` at all and whatever
|
|
82
|
+
* the pipeline/automation derives for the target stage stands. Defaults to
|
|
83
|
+
* `DEFAULT_QUICK_DEAL_PROBABILITY` to preserve the historic behavior.
|
|
84
|
+
*/
|
|
85
|
+
defaultProbability?: number | null
|
|
67
86
|
/**
|
|
68
87
|
* Tenant currency list. When omitted (e.g. dictionary not yet loaded), we render a
|
|
69
88
|
* conservative fallback so the dialog still works end-to-end. The default selection is
|
|
@@ -94,6 +113,7 @@ export function QuickDealDialog({
|
|
|
94
113
|
currentUserLabel,
|
|
95
114
|
companies = [],
|
|
96
115
|
currencies,
|
|
116
|
+
defaultProbability = DEFAULT_QUICK_DEAL_PROBABILITY,
|
|
97
117
|
}: QuickDealDialogProps): React.ReactElement | null {
|
|
98
118
|
const t = useT()
|
|
99
119
|
// Re-mount CrudForm whenever the dialog opens so cleared state is consistently fresh.
|
|
@@ -157,11 +177,11 @@ export function QuickDealDialog({
|
|
|
157
177
|
valueCurrency: defaultCurrencyCode,
|
|
158
178
|
companyId: '',
|
|
159
179
|
status: 'open',
|
|
160
|
-
probability:
|
|
180
|
+
probability: defaultProbability,
|
|
161
181
|
expectedCloseAt: '',
|
|
162
182
|
description: '',
|
|
163
183
|
}),
|
|
164
|
-
[defaultCurrencyCode],
|
|
184
|
+
[defaultCurrencyCode, defaultProbability],
|
|
165
185
|
)
|
|
166
186
|
|
|
167
187
|
const fields = React.useMemo<CrudField[]>(
|
|
@@ -405,4 +425,16 @@ export function QuickDealDialog({
|
|
|
405
425
|
)
|
|
406
426
|
}
|
|
407
427
|
|
|
428
|
+
// Register so downstream apps can reach the dialog from `widgets/components.ts`
|
|
429
|
+
// (replace / wrap / propsTransform) instead of forking the whole kanban page
|
|
430
|
+
// just to change a default (#4329).
|
|
431
|
+
registerComponent<QuickDealDialogProps>({
|
|
432
|
+
id: QUICK_DEAL_DIALOG_COMPONENT_ID,
|
|
433
|
+
component: QuickDealDialog,
|
|
434
|
+
metadata: {
|
|
435
|
+
module: 'customers',
|
|
436
|
+
description: 'Kanban quick-add deal dialog (pipeline board "+ deal").',
|
|
437
|
+
},
|
|
438
|
+
})
|
|
439
|
+
|
|
408
440
|
export default QuickDealDialog
|
|
@@ -64,9 +64,11 @@ import { CurrencyFilterPopover } from './components/CurrencyFilterPopover'
|
|
|
64
64
|
import { AddStageLane } from './components/AddStageLane'
|
|
65
65
|
import type { DealCardData } from './components/DealCard'
|
|
66
66
|
import {
|
|
67
|
-
QuickDealDialog,
|
|
67
|
+
QuickDealDialog as DefaultQuickDealDialog,
|
|
68
|
+
QUICK_DEAL_DIALOG_COMPONENT_ID,
|
|
68
69
|
type QuickDealContext,
|
|
69
70
|
type QuickDealCompanyOption,
|
|
71
|
+
type QuickDealDialogProps,
|
|
70
72
|
} from './components/QuickDealDialog'
|
|
71
73
|
import { AddStageDialog, type AddStageContext } from './components/AddStageDialog'
|
|
72
74
|
import { StatusFilterPopover } from './components/StatusFilterPopover'
|
|
@@ -80,6 +82,7 @@ import {
|
|
|
80
82
|
type ActivityComposerContext,
|
|
81
83
|
} from './components/ActivityComposerDialog'
|
|
82
84
|
import { BulkActionsBar } from './components/BulkActionsBar'
|
|
85
|
+
import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
|
|
83
86
|
import { ChangeStageDialog } from './components/ChangeStageDialog'
|
|
84
87
|
import { ChangeOwnerDialog } from './components/ChangeOwnerDialog'
|
|
85
88
|
import { buildCrudExportUrl, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
@@ -410,6 +413,12 @@ function sortDeals(deals: DealCardData[], option: SortOption): DealCardData[] {
|
|
|
410
413
|
|
|
411
414
|
export default function DealsKanbanPage(): React.ReactElement {
|
|
412
415
|
const t = useT()
|
|
416
|
+
// Resolved through the component registry so downstream apps can replace,
|
|
417
|
+
// wrap, or props-transform the quick-add dialog without forking this page.
|
|
418
|
+
const QuickDealDialog = useRegisteredComponent<QuickDealDialogProps>(
|
|
419
|
+
QUICK_DEAL_DIALOG_COMPONENT_ID,
|
|
420
|
+
DefaultQuickDealDialog,
|
|
421
|
+
)
|
|
413
422
|
const router = useRouter()
|
|
414
423
|
const scopeVersion = useOrganizationScopeVersion()
|
|
415
424
|
const queryClient = useQueryClient()
|
|
@@ -12,6 +12,7 @@ import { E as CoreEntities } from '#generated/entities.ids.generated'
|
|
|
12
12
|
import { createProgressBar } from '@open-mercato/shared/lib/cli/progress'
|
|
13
13
|
import { buildIndexDocument, type IndexCustomFieldValue } from '@open-mercato/core/modules/query_index/lib/document'
|
|
14
14
|
import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
|
|
15
|
+
import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
|
|
15
16
|
import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
|
|
16
17
|
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
17
18
|
import {
|
|
@@ -1994,6 +1995,7 @@ async function seedCustomerStressTest(
|
|
|
1994
1995
|
updated_at: Date
|
|
1995
1996
|
deleted_at: null
|
|
1996
1997
|
}> = []
|
|
1998
|
+
const searchConfig = resolveSearchConfig()
|
|
1997
1999
|
for (const [entityType, bucket] of pendingIndexDocs.entries()) {
|
|
1998
2000
|
for (const entry of bucket.values()) {
|
|
1999
2001
|
if (!entry.baseRow || Object.keys(entry.baseRow).length === 0) continue
|
|
@@ -2002,10 +2004,15 @@ async function seedCustomerStressTest(
|
|
|
2002
2004
|
entity_id: entry.recordId,
|
|
2003
2005
|
organization_id: entry.organizationId,
|
|
2004
2006
|
tenant_id: entry.tenantId,
|
|
2005
|
-
doc: buildIndexDocument(
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2007
|
+
doc: buildIndexDocument(
|
|
2008
|
+
entry.baseRow,
|
|
2009
|
+
entry.customFields,
|
|
2010
|
+
{
|
|
2011
|
+
organizationId: entry.organizationId,
|
|
2012
|
+
tenantId: entry.tenantId,
|
|
2013
|
+
},
|
|
2014
|
+
{ entityType, config: searchConfig },
|
|
2015
|
+
),
|
|
2009
2016
|
index_version: 1,
|
|
2010
2017
|
created_at: entry.createdAt,
|
|
2011
2018
|
updated_at: entry.updatedAt,
|
|
@@ -99,6 +99,8 @@ interface ScheduleActivityDialogProps {
|
|
|
99
99
|
onActivityCreated?: () => void
|
|
100
100
|
/** When provided, dialog opens in edit mode with pre-filled data */
|
|
101
101
|
editData?: ScheduleActivityEditData | null
|
|
102
|
+
/** Default country ISO2 code for phone number input when value is empty/unparsed */
|
|
103
|
+
defaultCountryIso2?: string
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
export function ScheduleActivityDialog({
|
|
@@ -111,6 +113,7 @@ export function ScheduleActivityDialog({
|
|
|
111
113
|
entityType,
|
|
112
114
|
onActivityCreated,
|
|
113
115
|
editData,
|
|
116
|
+
defaultCountryIso2,
|
|
114
117
|
}: ScheduleActivityDialogProps) {
|
|
115
118
|
const t = useT()
|
|
116
119
|
const state = useScheduleFormState({ open, editData: editData ?? null })
|
|
@@ -689,6 +692,7 @@ export function ScheduleActivityDialog({
|
|
|
689
692
|
externalError={callPhoneError}
|
|
690
693
|
invalidLabel={callPhoneInvalidMessage}
|
|
691
694
|
minDigits={7}
|
|
695
|
+
defaultCountryIso2={defaultCountryIso2}
|
|
692
696
|
/>
|
|
693
697
|
</div>
|
|
694
698
|
) : (
|
|
@@ -418,7 +418,7 @@ const companyDictionaryFieldDefinitions: DictionaryFieldDefinition[] = [
|
|
|
418
418
|
},
|
|
419
419
|
]
|
|
420
420
|
|
|
421
|
-
const createPrimaryPhoneField = (t: Translator): CrudField => ({
|
|
421
|
+
const createPrimaryPhoneField = (t: Translator, defaultCountryIso2?: string): CrudField => ({
|
|
422
422
|
id: 'primaryPhone',
|
|
423
423
|
label: t('customers.people.form.primaryPhone'),
|
|
424
424
|
type: 'custom',
|
|
@@ -447,6 +447,7 @@ const createPrimaryPhoneField = (t: Translator): CrudField => ({
|
|
|
447
447
|
invalidLabel={t('customers.people.form.primaryPhone.invalid', 'Enter a valid phone number with country code (e.g. +1 212 555 1234)')}
|
|
448
448
|
minDigits={7}
|
|
449
449
|
onDuplicateLookup={!disabled && !error ? duplicateLookup : undefined}
|
|
450
|
+
defaultCountryIso2={defaultCountryIso2}
|
|
450
451
|
/>
|
|
451
452
|
)
|
|
452
453
|
},
|
|
@@ -869,7 +870,8 @@ export const createDisplayNameSection = (t: Translator) =>
|
|
|
869
870
|
)
|
|
870
871
|
}
|
|
871
872
|
|
|
872
|
-
export const createPersonFormFields = (t: Translator): CrudField[] => {
|
|
873
|
+
export const createPersonFormFields = (t: Translator, options?: { defaultCountryIso2?: string }): CrudField[] => {
|
|
874
|
+
const defaultCountryIso2 = options?.defaultCountryIso2
|
|
873
875
|
const contactSection = createSectionHeadingField('__contactInformationSection', t('customers.people.form.sections.contactInformation'))
|
|
874
876
|
const companySection = createSectionHeadingField('__companyInformationSection', t('customers.people.form.sections.companyInformation'))
|
|
875
877
|
const dictionaryFields: CrudField[] = dictionaryFieldDefinitions.map((definition) => ({
|
|
@@ -930,7 +932,7 @@ export const createPersonFormFields = (t: Translator): CrudField[] => {
|
|
|
930
932
|
},
|
|
931
933
|
contactSection,
|
|
932
934
|
createPrimaryEmailField(t),
|
|
933
|
-
createPrimaryPhoneField(t),
|
|
935
|
+
createPrimaryPhoneField(t, defaultCountryIso2),
|
|
934
936
|
companySection,
|
|
935
937
|
{
|
|
936
938
|
id: 'companyEntityId',
|
|
@@ -1225,7 +1227,8 @@ export const createCompanyFormSchema = () =>
|
|
|
1225
1227
|
})
|
|
1226
1228
|
.passthrough()
|
|
1227
1229
|
|
|
1228
|
-
export const createCompanyFormFields = (t: Translator): CrudField[] => {
|
|
1230
|
+
export const createCompanyFormFields = (t: Translator, options?: { defaultCountryIso2?: string }): CrudField[] => {
|
|
1231
|
+
const defaultCountryIso2 = options?.defaultCountryIso2
|
|
1229
1232
|
const dictionaryFields: CrudField[] = companyDictionaryFieldDefinitions.map((definition) => ({
|
|
1230
1233
|
id: definition.id,
|
|
1231
1234
|
label: t(definition.labelKey),
|
|
@@ -1270,6 +1273,7 @@ export const createCompanyFormFields = (t: Translator): CrudField[] => {
|
|
|
1270
1273
|
placeholder={t('customers.companies.form.primaryPhonePlaceholder', '+1 555 123 4567')}
|
|
1271
1274
|
invalidLabel={t('customers.people.form.primaryPhone.invalid', 'Enter a valid phone number with country code (e.g. +1 212 555 1234)')}
|
|
1272
1275
|
minDigits={7}
|
|
1276
|
+
defaultCountryIso2={defaultCountryIso2}
|
|
1273
1277
|
/>
|
|
1274
1278
|
),
|
|
1275
1279
|
} as CrudField,
|
|
@@ -1651,8 +1655,8 @@ const buildIndustryLabels = (t: Translator): DictionarySelectLabels => ({
|
|
|
1651
1655
|
manageTitle: t('customers.people.form.dictionary.manage'),
|
|
1652
1656
|
})
|
|
1653
1657
|
|
|
1654
|
-
export const createCompanyEditFields = (t: Translator): CrudField[] => {
|
|
1655
|
-
const baseFields = createCompanyFormFields(t)
|
|
1658
|
+
export const createCompanyEditFields = (t: Translator, options?: { defaultCountryIso2?: string }): CrudField[] => {
|
|
1659
|
+
const baseFields = createCompanyFormFields(t, options)
|
|
1656
1660
|
const industryLabels = buildIndustryLabels(t)
|
|
1657
1661
|
|
|
1658
1662
|
return baseFields.map((field) => {
|
|
@@ -1676,8 +1680,8 @@ export const createCompanyEditFields = (t: Translator): CrudField[] => {
|
|
|
1676
1680
|
})
|
|
1677
1681
|
}
|
|
1678
1682
|
|
|
1679
|
-
export const createPersonEditFields = (t: Translator): CrudField[] => {
|
|
1680
|
-
const baseFields = createPersonFormFields(t)
|
|
1683
|
+
export const createPersonEditFields = (t: Translator, options?: { defaultCountryIso2?: string }): CrudField[] => {
|
|
1684
|
+
const baseFields = createPersonFormFields(t, options)
|
|
1681
1685
|
return [
|
|
1682
1686
|
...baseFields,
|
|
1683
1687
|
{
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
createWidgetDataService,
|
|
10
10
|
type WidgetDataRequest,
|
|
11
11
|
WidgetDataValidationError,
|
|
12
|
+
WidgetDataScanLimitError,
|
|
13
|
+
WidgetDataEncryptionUnavailableError,
|
|
12
14
|
} from '../../../../services/widgetDataService'
|
|
13
15
|
import { runWidgetDataBatch } from '../../../../lib/widgetDataBatch'
|
|
14
16
|
import type { AnalyticsRegistry } from '../../../../services/analyticsRegistry'
|
|
@@ -125,6 +127,8 @@ export async function POST(req: Request) {
|
|
|
125
127
|
fetchOne: (request) => service.fetchWidgetData(request),
|
|
126
128
|
describeError: (error) =>
|
|
127
129
|
error instanceof WidgetDataValidationError
|
|
130
|
+
|| error instanceof WidgetDataScanLimitError
|
|
131
|
+
|| error instanceof WidgetDataEncryptionUnavailableError
|
|
128
132
|
? error.message
|
|
129
133
|
: 'An error occurred while processing your request',
|
|
130
134
|
})
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
createWidgetDataService,
|
|
9
9
|
type WidgetDataRequest,
|
|
10
10
|
WidgetDataValidationError,
|
|
11
|
+
WidgetDataScanLimitError,
|
|
12
|
+
WidgetDataEncryptionUnavailableError,
|
|
11
13
|
} from '../../../services/widgetDataService'
|
|
12
14
|
import type { AnalyticsRegistry } from '../../../services/analyticsRegistry'
|
|
13
15
|
import { runApiInterceptorsBefore } from '@open-mercato/shared/lib/crud/interceptor-runner'
|
|
@@ -129,6 +131,15 @@ export async function POST(req: Request) {
|
|
|
129
131
|
if (err instanceof WidgetDataValidationError) {
|
|
130
132
|
return NextResponse.json({ error: err.message }, { status: 400 })
|
|
131
133
|
}
|
|
134
|
+
// The dataset is too large to group in application code; surface why rather than a masked 500.
|
|
135
|
+
if (err instanceof WidgetDataScanLimitError) {
|
|
136
|
+
return NextResponse.json({ error: err.message }, { status: 422 })
|
|
137
|
+
}
|
|
138
|
+
// Encryption is configured but currently unresolvable. Refusing the request keeps ciphertext and
|
|
139
|
+
// silently-wrong buckets out of the response, and 503 tells the client it is worth retrying.
|
|
140
|
+
if (err instanceof WidgetDataEncryptionUnavailableError) {
|
|
141
|
+
return NextResponse.json({ error: err.message }, { status: 503 })
|
|
142
|
+
}
|
|
132
143
|
return NextResponse.json(
|
|
133
144
|
{ error: 'An error occurred while processing your request' },
|
|
134
145
|
{ status: 500 },
|
|
@@ -156,8 +167,18 @@ const widgetDataPostDoc: OpenApiMethodDoc = {
|
|
|
156
167
|
errors: [
|
|
157
168
|
{ status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },
|
|
158
169
|
{ status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },
|
|
170
|
+
{
|
|
171
|
+
status: 422,
|
|
172
|
+
description: 'Too many rows to group an encrypted field in application code',
|
|
173
|
+
schema: dashboardsErrorSchema,
|
|
174
|
+
},
|
|
159
175
|
{ status: 403, description: 'Missing analytics.view feature', schema: dashboardsErrorSchema },
|
|
160
176
|
{ status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },
|
|
177
|
+
{
|
|
178
|
+
status: 503,
|
|
179
|
+
description: 'Encryption is configured but the group source cannot currently be resolved',
|
|
180
|
+
schema: dashboardsErrorSchema,
|
|
181
|
+
},
|
|
161
182
|
],
|
|
162
183
|
}
|
|
163
184
|
|