@open-mercato/core 0.6.4-develop.4113.1.5e87922616 → 0.6.4-develop.4133.1.48fc6c8f7b
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/lib/sessionIntegrity.js +16 -13
- package/dist/modules/auth/lib/sessionIntegrity.js.map +2 -2
- package/dist/modules/customers/api/utils.js +14 -9
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/batch/route.js +137 -0
- package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +7 -0
- package/dist/modules/dashboards/api/widgets/data/route.js +1 -75
- package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/schema.js +85 -0
- package/dist/modules/dashboards/api/widgets/data/schema.js.map +7 -0
- package/dist/modules/dashboards/lib/widgetDataBatch.js +49 -0
- package/dist/modules/dashboards/lib/widgetDataBatch.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +6 -14
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +33 -20
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/lib/sessionIntegrity.ts +37 -16
- package/src/modules/customers/api/utils.ts +17 -11
- package/src/modules/dashboards/api/widgets/data/batch/route.ts +168 -0
- package/src/modules/dashboards/api/widgets/data/route.ts +1 -90
- package/src/modules/dashboards/api/widgets/data/schema.ts +90 -0
- package/src/modules/dashboards/lib/widgetDataBatch.ts +89 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +6 -16
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +6 -16
- package/src/modules/directory/utils/organizationScope.ts +51 -20
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
|
-
import {
|
|
5
|
+
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
6
|
import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { LineChart, type LineChartDataItem } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import {
|
|
@@ -22,7 +22,7 @@ import { DEFAULT_SETTINGS, hydrateSettings, type RevenueTrendSettings } from './
|
|
|
22
22
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
23
23
|
import { formatCurrencyCompact } from '../../../lib/formatters'
|
|
24
24
|
|
|
25
|
-
async function fetchRevenueTrendData(settings: RevenueTrendSettings): Promise<WidgetDataResponse> {
|
|
25
|
+
async function fetchRevenueTrendData(settings: RevenueTrendSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
26
26
|
const body = {
|
|
27
27
|
entityType: 'sales:orders',
|
|
28
28
|
metric: {
|
|
@@ -39,18 +39,7 @@ async function fetchRevenueTrendData(settings: RevenueTrendSettings): Promise<Wi
|
|
|
39
39
|
},
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
method: 'POST',
|
|
44
|
-
headers: { 'Content-Type': 'application/json' },
|
|
45
|
-
body: JSON.stringify(body),
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
if (!call.ok) {
|
|
49
|
-
const errorMsg = (call.result as Record<string, unknown>)?.error
|
|
50
|
-
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch revenue trend data')
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return call.result as WidgetDataResponse
|
|
42
|
+
return fetchWidgetData<WidgetDataResponse>(body)
|
|
54
43
|
}
|
|
55
44
|
|
|
56
45
|
function formatDate(dateStr: string | null, granularity: DateGranularity, locale?: string): string {
|
|
@@ -125,12 +114,13 @@ const RevenueTrendWidget: React.FC<DashboardWidgetComponentProps<RevenueTrendSet
|
|
|
125
114
|
const [loading, setLoading] = React.useState(true)
|
|
126
115
|
const [error, setError] = React.useState<string | null>(null)
|
|
127
116
|
|
|
117
|
+
const fetchWidgetData = useWidgetData()
|
|
128
118
|
const refresh = React.useCallback(async () => {
|
|
129
119
|
onRefreshStateChange?.(true)
|
|
130
120
|
setLoading(true)
|
|
131
121
|
setError(null)
|
|
132
122
|
try {
|
|
133
|
-
const result = await fetchRevenueTrendData(hydrated)
|
|
123
|
+
const result = await fetchRevenueTrendData(hydrated, fetchWidgetData)
|
|
134
124
|
const sortedData = [...result.data].sort((a, b) => {
|
|
135
125
|
const aTime = new Date(a.groupKey as string || 0).getTime()
|
|
136
126
|
const bTime = new Date(b.groupKey as string || 0).getTime()
|
|
@@ -148,7 +138,7 @@ const RevenueTrendWidget: React.FC<DashboardWidgetComponentProps<RevenueTrendSet
|
|
|
148
138
|
setLoading(false)
|
|
149
139
|
onRefreshStateChange?.(false)
|
|
150
140
|
}
|
|
151
|
-
}, [hydrated, locale, onRefreshStateChange, t])
|
|
141
|
+
}, [hydrated, fetchWidgetData, locale, onRefreshStateChange, t])
|
|
152
142
|
|
|
153
143
|
React.useEffect(() => {
|
|
154
144
|
refresh().catch(() => {})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
|
-
import {
|
|
5
|
+
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import { DateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'
|
|
@@ -11,7 +11,7 @@ import { DEFAULT_SETTINGS, hydrateSettings, type SalesByRegionSettings } from '.
|
|
|
11
11
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
12
12
|
import { formatCurrencyCompact } from '../../../lib/formatters'
|
|
13
13
|
|
|
14
|
-
async function fetchSalesByRegionData(settings: SalesByRegionSettings): Promise<WidgetDataResponse> {
|
|
14
|
+
async function fetchSalesByRegionData(settings: SalesByRegionSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
15
15
|
const body = {
|
|
16
16
|
entityType: 'sales:orders',
|
|
17
17
|
metric: {
|
|
@@ -28,18 +28,7 @@ async function fetchSalesByRegionData(settings: SalesByRegionSettings): Promise<
|
|
|
28
28
|
},
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
method: 'POST',
|
|
33
|
-
headers: { 'Content-Type': 'application/json' },
|
|
34
|
-
body: JSON.stringify(body),
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
if (!call.ok) {
|
|
38
|
-
const errorMsg = (call.result as Record<string, unknown>)?.error
|
|
39
|
-
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch sales by region data')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return call.result as WidgetDataResponse
|
|
31
|
+
return fetchWidgetData<WidgetDataResponse>(body)
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
const SalesByRegionWidget: React.FC<DashboardWidgetComponentProps<SalesByRegionSettings>> = ({
|
|
@@ -55,12 +44,13 @@ const SalesByRegionWidget: React.FC<DashboardWidgetComponentProps<SalesByRegionS
|
|
|
55
44
|
const [loading, setLoading] = React.useState(true)
|
|
56
45
|
const [error, setError] = React.useState<string | null>(null)
|
|
57
46
|
|
|
47
|
+
const fetchWidgetData = useWidgetData()
|
|
58
48
|
const refresh = React.useCallback(async () => {
|
|
59
49
|
onRefreshStateChange?.(true)
|
|
60
50
|
setLoading(true)
|
|
61
51
|
setError(null)
|
|
62
52
|
try {
|
|
63
|
-
const result = await fetchSalesByRegionData(hydrated)
|
|
53
|
+
const result = await fetchSalesByRegionData(hydrated, fetchWidgetData)
|
|
64
54
|
const chartData = result.data.map((item) => ({
|
|
65
55
|
region: String(item.groupKey || t('dashboards.analytics.labels.unknown', 'Unknown')),
|
|
66
56
|
Revenue: item.value ?? 0,
|
|
@@ -73,7 +63,7 @@ const SalesByRegionWidget: React.FC<DashboardWidgetComponentProps<SalesByRegionS
|
|
|
73
63
|
setLoading(false)
|
|
74
64
|
onRefreshStateChange?.(false)
|
|
75
65
|
}
|
|
76
|
-
}, [hydrated, onRefreshStateChange, t])
|
|
66
|
+
}, [hydrated, fetchWidgetData, onRefreshStateChange, t])
|
|
77
67
|
|
|
78
68
|
React.useEffect(() => {
|
|
79
69
|
refresh().catch(() => {})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
|
-
import {
|
|
5
|
+
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { TopNTable, type TopNTableColumn } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import { DateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'
|
|
@@ -17,7 +17,7 @@ type CustomerRow = {
|
|
|
17
17
|
revenue: number
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
async function fetchTopCustomersData(settings: TopCustomersSettings): Promise<WidgetDataResponse> {
|
|
20
|
+
async function fetchTopCustomersData(settings: TopCustomersSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
21
21
|
const body = {
|
|
22
22
|
entityType: 'sales:orders',
|
|
23
23
|
metric: {
|
|
@@ -35,18 +35,7 @@ async function fetchTopCustomersData(settings: TopCustomersSettings): Promise<Wi
|
|
|
35
35
|
},
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: { 'Content-Type': 'application/json' },
|
|
41
|
-
body: JSON.stringify(body),
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
if (!call.ok) {
|
|
45
|
-
const errorMsg = (call.result as Record<string, unknown>)?.error
|
|
46
|
-
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch top customers data')
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return call.result as WidgetDataResponse
|
|
38
|
+
return fetchWidgetData<WidgetDataResponse>(body)
|
|
50
39
|
}
|
|
51
40
|
|
|
52
41
|
function formatCustomerName(name: string | null, unknownLabel: string): string {
|
|
@@ -90,12 +79,13 @@ const TopCustomersWidget: React.FC<DashboardWidgetComponentProps<TopCustomersSet
|
|
|
90
79
|
[t, unknownLabel],
|
|
91
80
|
)
|
|
92
81
|
|
|
82
|
+
const fetchWidgetData = useWidgetData()
|
|
93
83
|
const refresh = React.useCallback(async () => {
|
|
94
84
|
onRefreshStateChange?.(true)
|
|
95
85
|
setLoading(true)
|
|
96
86
|
setError(null)
|
|
97
87
|
try {
|
|
98
|
-
const result = await fetchTopCustomersData(hydrated)
|
|
88
|
+
const result = await fetchTopCustomersData(hydrated, fetchWidgetData)
|
|
99
89
|
const tableData: CustomerRow[] = result.data.map((item, index) => ({
|
|
100
90
|
rank: index + 1,
|
|
101
91
|
customerId: item.groupLabel || String(item.groupKey || t('dashboards.analytics.labels.unknown', 'Unknown')),
|
|
@@ -109,7 +99,7 @@ const TopCustomersWidget: React.FC<DashboardWidgetComponentProps<TopCustomersSet
|
|
|
109
99
|
setLoading(false)
|
|
110
100
|
onRefreshStateChange?.(false)
|
|
111
101
|
}
|
|
112
|
-
}, [hydrated, onRefreshStateChange, t])
|
|
102
|
+
}, [hydrated, fetchWidgetData, onRefreshStateChange, t])
|
|
113
103
|
|
|
114
104
|
React.useEffect(() => {
|
|
115
105
|
refresh().catch(() => {})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
5
|
-
import {
|
|
5
|
+
import { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'
|
|
8
8
|
import { DateRangeSelect, InlineDateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'
|
|
@@ -18,7 +18,7 @@ import { DEFAULT_SETTINGS, hydrateSettings, type TopProductsSettings } from './c
|
|
|
18
18
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
19
19
|
import { formatCurrencyCompact } from '../../../lib/formatters'
|
|
20
20
|
|
|
21
|
-
async function fetchTopProductsData(settings: TopProductsSettings): Promise<WidgetDataResponse> {
|
|
21
|
+
async function fetchTopProductsData(settings: TopProductsSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
22
22
|
const body = {
|
|
23
23
|
entityType: 'sales:order_lines',
|
|
24
24
|
metric: {
|
|
@@ -36,18 +36,7 @@ async function fetchTopProductsData(settings: TopProductsSettings): Promise<Widg
|
|
|
36
36
|
},
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
method: 'POST',
|
|
41
|
-
headers: { 'Content-Type': 'application/json' },
|
|
42
|
-
body: JSON.stringify(body),
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
if (!call.ok) {
|
|
46
|
-
const errorMsg = (call.result as Record<string, unknown>)?.error
|
|
47
|
-
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch top products data')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return call.result as WidgetDataResponse
|
|
39
|
+
return fetchWidgetData<WidgetDataResponse>(body)
|
|
51
40
|
}
|
|
52
41
|
|
|
53
42
|
function truncateLabel(
|
|
@@ -82,6 +71,7 @@ const TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSetti
|
|
|
82
71
|
const [error, setError] = React.useState<string | null>(null)
|
|
83
72
|
const fetchingRef = React.useRef(false)
|
|
84
73
|
|
|
74
|
+
const fetchWidgetData = useWidgetData()
|
|
85
75
|
const refresh = React.useCallback(async () => {
|
|
86
76
|
if (fetchingRef.current) return
|
|
87
77
|
fetchingRef.current = true
|
|
@@ -89,7 +79,7 @@ const TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSetti
|
|
|
89
79
|
setLoading(true)
|
|
90
80
|
setError(null)
|
|
91
81
|
try {
|
|
92
|
-
const result = await fetchTopProductsData(hydrated)
|
|
82
|
+
const result = await fetchTopProductsData(hydrated, fetchWidgetData)
|
|
93
83
|
const chartData = result.data.map((item, index) => ({
|
|
94
84
|
name: truncateLabel(item.groupLabel ?? item.groupKey ?? `Product ${index + 1}`, t),
|
|
95
85
|
Revenue: item.value ?? 0,
|
|
@@ -103,7 +93,7 @@ const TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSetti
|
|
|
103
93
|
onRefreshStateChange?.(false)
|
|
104
94
|
fetchingRef.current = false
|
|
105
95
|
}
|
|
106
|
-
}, [hydrated, onRefreshStateChange, t])
|
|
96
|
+
}, [hydrated, fetchWidgetData, onRefreshStateChange, t])
|
|
107
97
|
|
|
108
98
|
React.useEffect(() => {
|
|
109
99
|
refresh().catch(() => {})
|
|
@@ -131,29 +131,56 @@ export function getSelectedTenantFromRequest(
|
|
|
131
131
|
return parseSelectedTenantCookie(header)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const unique = Array.from(new Set(
|
|
134
|
+
function normalizeOrganizationIds(ids: string[]): string[] {
|
|
135
|
+
return Array.from(new Set(
|
|
137
136
|
ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {
|
|
138
137
|
if (!value) return false
|
|
139
138
|
if (isAllOrganizationsSelection(value)) return false
|
|
140
139
|
return true
|
|
141
140
|
})
|
|
142
141
|
))
|
|
143
|
-
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Map each organization id to itself plus its persisted descendant ids. Only
|
|
145
|
+
// orgs that exist for the tenant and are not soft-deleted are included, so an
|
|
146
|
+
// unknown/inaccessible id simply has no entry (matching the per-id query that
|
|
147
|
+
// returned an empty set for it).
|
|
148
|
+
type OrgDescendantMap = Map<string, string[]>
|
|
149
|
+
|
|
150
|
+
// Issue #2228 — single round-trip for org-scope resolution. Instead of issuing
|
|
151
|
+
// one `organizations` SELECT per `collectWithDescendants` call (up to 3-4
|
|
152
|
+
// sequential queries per request: accessible set, fallback set, selected set),
|
|
153
|
+
// gather every candidate id up front and fetch their descendant expansions in
|
|
154
|
+
// one `em.find(Organization, { id: $in })`. Expansion then happens in-memory.
|
|
155
|
+
async function loadOrgDescendantMap(em: EntityManager, tenantId: string, ids: string[]): Promise<OrgDescendantMap> {
|
|
156
|
+
const unique = normalizeOrganizationIds(ids)
|
|
157
|
+
if (!unique.length) return new Map()
|
|
144
158
|
const filter: FilterQuery<Organization> = {
|
|
145
159
|
tenant: tenantId,
|
|
146
160
|
id: { $in: unique },
|
|
147
161
|
deletedAt: null,
|
|
148
162
|
}
|
|
149
163
|
const orgs = await em.find(Organization, filter)
|
|
150
|
-
const
|
|
164
|
+
const map: OrgDescendantMap = new Map()
|
|
151
165
|
for (const org of orgs) {
|
|
152
166
|
const id = String(org.id)
|
|
153
|
-
|
|
167
|
+
const expansion = [id]
|
|
154
168
|
if (Array.isArray(org.descendantIds)) {
|
|
155
|
-
for (const desc of org.descendantIds)
|
|
169
|
+
for (const desc of org.descendantIds) expansion.push(String(desc))
|
|
156
170
|
}
|
|
171
|
+
map.set(id, expansion)
|
|
172
|
+
}
|
|
173
|
+
return map
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function expandWithDescendants(map: OrgDescendantMap, ids: string[]): Set<string> {
|
|
177
|
+
const set = new Set<string>()
|
|
178
|
+
for (const value of ids) {
|
|
179
|
+
const id = normalizeOrganizationId(value)
|
|
180
|
+
if (!id || isAllOrganizationsSelection(id)) continue
|
|
181
|
+
const expansion = map.get(id)
|
|
182
|
+
if (!expansion) continue
|
|
183
|
+
for (const entry of expansion) set.add(entry)
|
|
157
184
|
}
|
|
158
185
|
return set
|
|
159
186
|
}
|
|
@@ -214,14 +241,18 @@ export async function resolveOrganizationScope({
|
|
|
214
241
|
|
|
215
242
|
const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null
|
|
216
243
|
const fallbackOrgId = accountOrgId ?? null
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
244
|
+
|
|
245
|
+
// Every id that could be expanded below — accessible set, fallback (account)
|
|
246
|
+
// org, and the requested selection — is known up front, so fetch them all in
|
|
247
|
+
// a single `organizations` query and expand from the in-memory map.
|
|
248
|
+
const candidateIds = [
|
|
249
|
+
...(accessibleList ?? []),
|
|
250
|
+
...(fallbackOrgId ? [fallbackOrgId] : []),
|
|
251
|
+
...(normalizedSelectedId ? [normalizedSelectedId] : []),
|
|
252
|
+
]
|
|
253
|
+
const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds)
|
|
254
|
+
const loadFallbackSet = (): Set<string> | null =>
|
|
255
|
+
fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null
|
|
225
256
|
|
|
226
257
|
let allowedSet: Set<string> | null = null
|
|
227
258
|
if (accessibleList === null) {
|
|
@@ -229,11 +260,11 @@ export async function resolveOrganizationScope({
|
|
|
229
260
|
} else if (accessibleList.length === 0) {
|
|
230
261
|
allowedSet = new Set()
|
|
231
262
|
} else {
|
|
232
|
-
allowedSet =
|
|
263
|
+
allowedSet = expandWithDescendants(orgDescendants, accessibleList)
|
|
233
264
|
}
|
|
234
265
|
|
|
235
266
|
if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {
|
|
236
|
-
const computed =
|
|
267
|
+
const computed = loadFallbackSet()
|
|
237
268
|
if (computed && computed.size > 0) {
|
|
238
269
|
allowedSet = computed
|
|
239
270
|
}
|
|
@@ -256,17 +287,17 @@ export async function resolveOrganizationScope({
|
|
|
256
287
|
|
|
257
288
|
let filterSet: Set<string> | null = null
|
|
258
289
|
if (effectiveSelected) {
|
|
259
|
-
filterSet =
|
|
290
|
+
filterSet = expandWithDescendants(orgDescendants, [effectiveSelected])
|
|
260
291
|
} else if (allowedSet !== null) {
|
|
261
292
|
filterSet = allowedSet
|
|
262
293
|
} else if (widenToAllOrgs) {
|
|
263
294
|
filterSet = null
|
|
264
295
|
} else if (auth.orgId) {
|
|
265
|
-
filterSet =
|
|
296
|
+
filterSet = loadFallbackSet()
|
|
266
297
|
}
|
|
267
298
|
|
|
268
299
|
if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {
|
|
269
|
-
const computed =
|
|
300
|
+
const computed = loadFallbackSet()
|
|
270
301
|
if (computed && computed.size > 0) {
|
|
271
302
|
filterSet = computed
|
|
272
303
|
if (!effectiveSelected) {
|