@open-mercato/core 0.10.1-develop.7836.1.0ef0f453c6 → 0.10.1-develop.7847.1.7d83dc5794
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/api_keys/api/keys/route.js +15 -1
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +16 -1
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/users/page.js +13 -2
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/catalog/commands/products.js +4 -2
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles.js +25 -2
- package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users.js +24 -7
- package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +77 -15
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +12 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +2 -1
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/lib/adminOrganizationScope.js +46 -0
- package/dist/modules/customer_accounts/lib/adminOrganizationScope.js.map +7 -0
- package/dist/modules/customers/api/customer-dictionary-entries/route.js +86 -0
- package/dist/modules/customers/api/customer-dictionary-entries/route.js.map +7 -0
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +29 -7
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/cli.js +28 -110
- package/dist/modules/customers/cli.js.map +3 -3
- package/dist/modules/customers/components/DictionarySettings.js +1 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +1 -1
- package/dist/modules/customers/components/detail/ManageTagsDialog.js +1 -1
- package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskDialog.js +14 -1
- package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskForm.js +1 -16
- package/dist/modules/customers/components/detail/TaskForm.js.map +2 -2
- package/dist/modules/customers/components/detail/TasksSection.js +38 -10
- package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js +12 -5
- package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/useInteractions.js +3 -13
- package/dist/modules/customers/components/detail/hooks/useInteractions.js.map +2 -2
- package/dist/modules/customers/lib/dictionaryDefaults.js +134 -0
- package/dist/modules/customers/lib/dictionaryDefaults.js.map +7 -0
- package/dist/modules/customers/lib/dictionaryLabels.js +59 -0
- package/dist/modules/customers/lib/dictionaryLabels.js.map +7 -0
- package/dist/modules/customers/lib/interactionCompatibility.js +1 -0
- package/dist/modules/customers/lib/interactionCompatibility.js.map +2 -2
- package/dist/modules/customers/translations.js +9 -0
- package/dist/modules/customers/translations.js.map +7 -0
- package/dist/modules/directory/api/organization-branding/route.js +6 -9
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/backend/directory/branding/page.js +15 -3
- package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
- package/dist/modules/eudr/index.js +13 -0
- package/dist/modules/eudr/index.js.map +2 -2
- package/dist/modules/staff/api/timesheets/access-requests/route.js +11 -5
- package/dist/modules/staff/api/timesheets/access-requests/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-projects/route.js +19 -12
- package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +21 -12
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +14 -9
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js +31 -10
- package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js +64 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js.map +7 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js +13 -0
- package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js.map +7 -0
- package/dist/modules/sync_excel/index.js +11 -1
- package/dist/modules/sync_excel/index.js.map +2 -2
- package/dist/modules/translations/di.js +2 -0
- package/dist/modules/translations/di.js.map +2 -2
- package/dist/modules/wms/api/listEnrichers.js +3 -1
- package/dist/modules/wms/api/listEnrichers.js.map +2 -2
- package/dist/modules/wms/index.js +1 -1
- package/dist/modules/wms/index.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/api_keys/api/keys/route.ts +15 -1
- package/src/modules/api_keys/i18n/de.json +1 -0
- package/src/modules/api_keys/i18n/en.json +1 -0
- package/src/modules/api_keys/i18n/es.json +1 -0
- package/src/modules/api_keys/i18n/ko.json +1 -0
- package/src/modules/api_keys/i18n/pl.json +1 -0
- package/src/modules/auth/api/users/route.ts +18 -1
- package/src/modules/auth/backend/users/page.tsx +14 -2
- package/src/modules/auth/i18n/de.json +1 -0
- package/src/modules/auth/i18n/en.json +1 -0
- package/src/modules/auth/i18n/es.json +1 -0
- package/src/modules/auth/i18n/ko.json +1 -0
- package/src/modules/auth/i18n/pl.json +1 -0
- package/src/modules/catalog/commands/products.ts +4 -1
- package/src/modules/customer_accounts/api/admin/roles.ts +26 -2
- package/src/modules/customer_accounts/api/admin/users.ts +26 -7
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +96 -13
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +9 -1
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/i18n/de.json +10 -0
- package/src/modules/customer_accounts/i18n/en.json +10 -0
- package/src/modules/customer_accounts/i18n/es.json +10 -0
- package/src/modules/customer_accounts/i18n/ko.json +10 -0
- package/src/modules/customer_accounts/i18n/pl.json +10 -0
- package/src/modules/customer_accounts/lib/adminOrganizationScope.ts +71 -0
- package/src/modules/customers/api/customer-dictionary-entries/route.ts +82 -0
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +29 -6
- package/src/modules/customers/cli.ts +23 -124
- package/src/modules/customers/components/DictionarySettings.tsx +1 -1
- package/src/modules/customers/components/detail/ManageTagsDialog.tsx +1 -1
- package/src/modules/customers/components/detail/TaskDialog.tsx +15 -1
- package/src/modules/customers/components/detail/TaskForm.tsx +1 -17
- package/src/modules/customers/components/detail/TasksSection.tsx +44 -14
- package/src/modules/customers/components/detail/hooks/useCustomerDictionary.ts +14 -8
- package/src/modules/customers/components/detail/hooks/useInteractions.ts +3 -4
- package/src/modules/customers/i18n/de.json +81 -0
- package/src/modules/customers/i18n/en.json +81 -0
- package/src/modules/customers/i18n/es.json +81 -0
- package/src/modules/customers/i18n/ko.json +81 -0
- package/src/modules/customers/i18n/pl.json +81 -0
- package/src/modules/customers/lib/dictionaryDefaults.ts +137 -0
- package/src/modules/customers/lib/dictionaryLabels.ts +73 -0
- package/src/modules/customers/lib/interactionCompatibility.ts +1 -0
- package/src/modules/customers/translations.ts +5 -0
- package/src/modules/directory/api/organization-branding/route.ts +5 -8
- package/src/modules/directory/backend/directory/branding/page.tsx +15 -3
- package/src/modules/eudr/index.ts +13 -0
- package/src/modules/staff/api/timesheets/access-requests/route.ts +13 -14
- package/src/modules/staff/api/timesheets/time-projects/route.ts +22 -13
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +25 -14
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +14 -13
- package/src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx +30 -8
- package/src/modules/staff/lib/time-tracking-ui/projectAccessScope.ts +102 -0
- package/src/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.ts +15 -0
- package/src/modules/sync_excel/index.ts +10 -0
- package/src/modules/translations/di.ts +2 -0
- package/src/modules/wms/api/listEnrichers.ts +5 -2
- package/src/modules/wms/index.ts +1 -1
|
@@ -33,6 +33,8 @@ import { formatCurrency } from '@open-mercato/ui/utils/format'
|
|
|
33
33
|
import { KpiCard, Sparkline } from '@open-mercato/ui/backend/charts'
|
|
34
34
|
import { ProjectTeamDrawer } from '../../../../../lib/time-tracking-ui/ProjectTeamDrawer'
|
|
35
35
|
import { NoProjectAccess } from '../../../../../lib/time-tracking-ui/NoProjectAccess'
|
|
36
|
+
import { useProjectAccessScope } from '../../../../../lib/time-tracking-ui/projectAccessScope'
|
|
37
|
+
import { readResolvedProjectAccessScope } from '../../../../../lib/time-tracking-ui/projectAccessScopeHeaders'
|
|
36
38
|
import { ProjectBudgetCell } from '../../../../../lib/timesheets-projects-ui/ProjectBudgetCell'
|
|
37
39
|
import { computeBudgetBurn } from '../../../../../lib/timesheets-projects/budgetBurn'
|
|
38
40
|
import { resolveProjectColorHex } from '../../../../../lib/timesheets-ui/colors'
|
|
@@ -177,6 +179,7 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
|
|
|
177
179
|
const translateRef = React.useRef(t)
|
|
178
180
|
React.useEffect(() => { translateRef.current = t }, [t])
|
|
179
181
|
const scopeVersion = useOrganizationScopeVersion()
|
|
182
|
+
const { scopeKey: projectAccessScopeKey, resolvedScope, resolveScope } = useProjectAccessScope(projectId)
|
|
180
183
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
181
184
|
const router = useRouter()
|
|
182
185
|
const pathname = usePathname()
|
|
@@ -186,13 +189,9 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
|
|
|
186
189
|
const [loading, setLoading] = React.useState(true)
|
|
187
190
|
const [error, setError] = React.useState<string | null>(null)
|
|
188
191
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
const accessDenied = accessDeniedProjectId !== null && accessDeniedProjectId === projectId
|
|
193
|
-
// The id the last finished load was for. Until it matches the route, the page
|
|
194
|
-
// is still loading — never the previous project's error or empty state.
|
|
195
|
-
const [settledProjectId, setSettledProjectId] = React.useState<string | null>(null)
|
|
192
|
+
const [accessDeniedScopeKey, setAccessDeniedScopeKey] = React.useState<string | null>(null)
|
|
193
|
+
const [loadedScopeKey, setLoadedScopeKey] = React.useState<string | null>(null)
|
|
194
|
+
const accessDenied = accessDeniedScopeKey === projectAccessScopeKey
|
|
196
195
|
|
|
197
196
|
const [employees, setEmployees] = React.useState<EmployeeAssignment[]>([])
|
|
198
197
|
const [employeesLoading, setEmployeesLoading] = React.useState(false)
|
|
@@ -407,13 +406,14 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
|
|
|
407
406
|
const call = await apiCall<ProjectResponse>(
|
|
408
407
|
`/api/staff/timesheets/time-projects?${queryParams.toString()}`,
|
|
409
408
|
)
|
|
409
|
+
if (!cancelled) resolveScope(readResolvedProjectAccessScope(call.response))
|
|
410
410
|
// Screen 17: the route answers 404 with this discriminator for a project
|
|
411
411
|
// the caller is not a member of, without naming it.
|
|
412
412
|
if (call.status === 404 && readDenialReason(call.result) === NO_PROJECT_ACCESS_REASON) {
|
|
413
|
-
if (!cancelled)
|
|
413
|
+
if (!cancelled) setAccessDeniedScopeKey(projectAccessScopeKey)
|
|
414
414
|
return
|
|
415
415
|
}
|
|
416
|
-
if (!cancelled)
|
|
416
|
+
if (!cancelled) setAccessDeniedScopeKey(null)
|
|
417
417
|
if (!call.ok) {
|
|
418
418
|
throw new Error(translateRef.current('staff.timesheets.projects.errors.load', 'Failed to load project.'))
|
|
419
419
|
}
|
|
@@ -426,18 +426,19 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
|
|
|
426
426
|
if (!cancelled) setProject(record)
|
|
427
427
|
} catch (loadError) {
|
|
428
428
|
if (!cancelled) {
|
|
429
|
+
setAccessDeniedScopeKey(null)
|
|
429
430
|
setError(loadError instanceof Error ? loadError.message : translateRef.current('staff.timesheets.projects.errors.load', 'Failed to load project.'))
|
|
430
431
|
}
|
|
431
432
|
} finally {
|
|
432
433
|
if (!cancelled) {
|
|
434
|
+
setLoadedScopeKey(projectAccessScopeKey)
|
|
433
435
|
setLoading(false)
|
|
434
|
-
setSettledProjectId(projectId ?? null)
|
|
435
436
|
}
|
|
436
437
|
}
|
|
437
438
|
}
|
|
438
439
|
loadProject()
|
|
439
440
|
return () => { cancelled = true }
|
|
440
|
-
}, [projectId, scopeVersion])
|
|
441
|
+
}, [projectId, scopeVersion, projectAccessScopeKey, resolveScope])
|
|
441
442
|
|
|
442
443
|
// --- Load employees with name resolution ---
|
|
443
444
|
const loadEmployees = React.useCallback(async () => {
|
|
@@ -709,10 +710,10 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
|
|
|
709
710
|
// resolving late) keeps the guard mounted instead of resetting a request the
|
|
710
711
|
// user has just sent.
|
|
711
712
|
if (accessDenied) {
|
|
712
|
-
return <NoProjectAccess timeProjectId={projectId} />
|
|
713
|
+
return <NoProjectAccess timeProjectId={projectId} resolvedScope={resolvedScope} />
|
|
713
714
|
}
|
|
714
715
|
|
|
715
|
-
if (loading ||
|
|
716
|
+
if (loading || loadedScopeKey !== projectAccessScopeKey) {
|
|
716
717
|
return (
|
|
717
718
|
<Page>
|
|
718
719
|
<PageBody>
|
|
@@ -10,6 +10,8 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
|
10
10
|
import { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
11
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
12
12
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
13
|
+
import { useProjectAccessScope } from './projectAccessScope'
|
|
14
|
+
import type { ResolvedProjectAccessScope } from './projectAccessScopeHeaders'
|
|
13
15
|
|
|
14
16
|
export const MY_WORK_HREF = '/backend/staff/time-tracking'
|
|
15
17
|
export const ACCESS_REQUEST_ENDPOINT = '/api/staff/timesheets/access-requests'
|
|
@@ -22,36 +24,52 @@ export type AccessRequestButtonProps = {
|
|
|
22
24
|
* project exists to a user who has no right to it.
|
|
23
25
|
*/
|
|
24
26
|
timeProjectId?: string | null
|
|
27
|
+
resolvedScope?: ResolvedProjectAccessScope | null
|
|
25
28
|
label: string
|
|
26
29
|
sentLabel: string
|
|
27
30
|
variant?: 'default' | 'outline'
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
export function AccessRequestButton({
|
|
33
|
+
export function AccessRequestButton(props: AccessRequestButtonProps) {
|
|
34
|
+
const { scopeKey, isCurrentScope } = useProjectAccessScope(props.timeProjectId, props.resolvedScope)
|
|
35
|
+
return <ScopedAccessRequestButton key={scopeKey} {...props} isCurrentScope={isCurrentScope} />
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ScopedAccessRequestButton({
|
|
31
39
|
timeProjectId,
|
|
32
40
|
label,
|
|
33
41
|
sentLabel,
|
|
34
42
|
variant = 'outline',
|
|
35
|
-
|
|
43
|
+
isCurrentScope,
|
|
44
|
+
}: AccessRequestButtonProps & { isCurrentScope: () => boolean }) {
|
|
36
45
|
const t = useT()
|
|
37
46
|
const [submitting, setSubmitting] = React.useState(false)
|
|
38
47
|
const [sent, setSent] = React.useState(false)
|
|
48
|
+
const mounted = React.useRef(true)
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
mounted.current = true
|
|
51
|
+
return () => { mounted.current = false }
|
|
52
|
+
}, [])
|
|
39
53
|
const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({
|
|
40
54
|
contextId: ACCESS_REQUEST_CONTEXT_ID,
|
|
41
55
|
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
42
56
|
})
|
|
43
57
|
|
|
44
58
|
const requestAccess = React.useCallback(async () => {
|
|
59
|
+
const isCurrent = () => mounted.current && isCurrentScope()
|
|
60
|
+
if (!isCurrent()) return
|
|
45
61
|
const payload = timeProjectId ? { timeProjectId } : {}
|
|
46
62
|
setSubmitting(true)
|
|
47
63
|
try {
|
|
48
64
|
await runMutation({
|
|
49
|
-
operation: () =>
|
|
50
|
-
|
|
65
|
+
operation: () => {
|
|
66
|
+
if (!isCurrent()) throw new Error('[internal] Access request scope changed')
|
|
67
|
+
return apiCallOrThrow(ACCESS_REQUEST_ENDPOINT, {
|
|
51
68
|
method: 'POST',
|
|
52
69
|
headers: { 'content-type': 'application/json' },
|
|
53
70
|
body: JSON.stringify(payload),
|
|
54
|
-
})
|
|
71
|
+
})
|
|
72
|
+
},
|
|
55
73
|
context: {
|
|
56
74
|
formId: ACCESS_REQUEST_CONTEXT_ID,
|
|
57
75
|
resourceKind: 'staff.timesheets.access_request',
|
|
@@ -59,6 +77,7 @@ export function AccessRequestButton({
|
|
|
59
77
|
},
|
|
60
78
|
mutationPayload: payload,
|
|
61
79
|
})
|
|
80
|
+
if (!isCurrent()) return
|
|
62
81
|
setSent(true)
|
|
63
82
|
flash(
|
|
64
83
|
t(
|
|
@@ -68,14 +87,15 @@ export function AccessRequestButton({
|
|
|
68
87
|
'success',
|
|
69
88
|
)
|
|
70
89
|
} catch {
|
|
90
|
+
if (!isCurrent()) return
|
|
71
91
|
flash(
|
|
72
92
|
t('staff.time_tracking.access.request.error', 'Could not send the access request.'),
|
|
73
93
|
'error',
|
|
74
94
|
)
|
|
75
95
|
} finally {
|
|
76
|
-
setSubmitting(false)
|
|
96
|
+
if (isCurrent()) setSubmitting(false)
|
|
77
97
|
}
|
|
78
|
-
}, [retryLastMutation, runMutation, t, timeProjectId])
|
|
98
|
+
}, [isCurrentScope, retryLastMutation, runMutation, t, timeProjectId])
|
|
79
99
|
|
|
80
100
|
return (
|
|
81
101
|
<Button type="button" variant={variant} onClick={requestAccess} disabled={submitting || sent}>
|
|
@@ -87,6 +107,7 @@ export function AccessRequestButton({
|
|
|
87
107
|
|
|
88
108
|
export type NoProjectAccessProps = {
|
|
89
109
|
timeProjectId?: string | null
|
|
110
|
+
resolvedScope?: ResolvedProjectAccessScope | null
|
|
90
111
|
/** Where the "Back to My work" action returns to. */
|
|
91
112
|
myWorkHref?: string
|
|
92
113
|
}
|
|
@@ -97,7 +118,7 @@ export type NoProjectAccessProps = {
|
|
|
97
118
|
* access" and "access was revoked" (note 2); navigation stays untouched because
|
|
98
119
|
* the caller still has other projects (note 3).
|
|
99
120
|
*/
|
|
100
|
-
export function NoProjectAccess({ timeProjectId, myWorkHref = MY_WORK_HREF }: NoProjectAccessProps) {
|
|
121
|
+
export function NoProjectAccess({ timeProjectId, resolvedScope, myWorkHref = MY_WORK_HREF }: NoProjectAccessProps) {
|
|
101
122
|
const t = useT()
|
|
102
123
|
return (
|
|
103
124
|
<Page>
|
|
@@ -121,6 +142,7 @@ export function NoProjectAccess({ timeProjectId, myWorkHref = MY_WORK_HREF }: No
|
|
|
121
142
|
</Button>
|
|
122
143
|
<AccessRequestButton
|
|
123
144
|
timeProjectId={timeProjectId}
|
|
145
|
+
resolvedScope={resolvedScope}
|
|
124
146
|
label={t('staff.time_tracking.access.denied.request', 'Request access')}
|
|
125
147
|
sentLabel={t('staff.time_tracking.access.request.sent', 'Request sent')}
|
|
126
148
|
/>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
5
|
+
import {
|
|
6
|
+
parseSelectedOrganizationCookie,
|
|
7
|
+
parseSelectedTenantCookie,
|
|
8
|
+
} from '@open-mercato/core/modules/directory/utils/scopeCookies'
|
|
9
|
+
import type { ResolvedProjectAccessScope } from './projectAccessScopeHeaders'
|
|
10
|
+
|
|
11
|
+
export type ProjectAccessScope = {
|
|
12
|
+
projectId: string | null
|
|
13
|
+
tenantId: string | null
|
|
14
|
+
organizationId: string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ProjectAccessScopeState = {
|
|
18
|
+
/** Stable while the scope stays the same; changes when the project, tenant or organization changes. */
|
|
19
|
+
scopeKey: string
|
|
20
|
+
/** Reads the scope cookies again, so it also catches a change that has not been rendered yet. */
|
|
21
|
+
isCurrentScope: () => boolean
|
|
22
|
+
resolvedScope: ResolvedProjectAccessScope | null
|
|
23
|
+
resolveScope: (scope: ResolvedProjectAccessScope | null) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ProjectAccessScopeBinding = {
|
|
27
|
+
scope: ProjectAccessScope
|
|
28
|
+
generation: number
|
|
29
|
+
resolvedScope: ResolvedProjectAccessScope | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readProjectAccessScope(timeProjectId?: string | null): ProjectAccessScope {
|
|
33
|
+
const cookieHeader = typeof document === 'undefined' ? null : document.cookie
|
|
34
|
+
return {
|
|
35
|
+
projectId: timeProjectId ?? null,
|
|
36
|
+
tenantId: parseSelectedTenantCookie(cookieHeader),
|
|
37
|
+
organizationId: parseSelectedOrganizationCookie(cookieHeader),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function bindProjectAccessScope(
|
|
42
|
+
bound: ProjectAccessScope,
|
|
43
|
+
current: ProjectAccessScope,
|
|
44
|
+
resolvedScope?: ResolvedProjectAccessScope | null,
|
|
45
|
+
): ProjectAccessScope | null {
|
|
46
|
+
const continues = (boundValue: string | null, currentValue: string | null, resolvedValue?: string) =>
|
|
47
|
+
boundValue === currentValue || (boundValue === null && currentValue === resolvedValue)
|
|
48
|
+
if (bound.projectId !== current.projectId) return null
|
|
49
|
+
if (!continues(bound.tenantId, current.tenantId, resolvedScope?.tenantId)) return null
|
|
50
|
+
if (!continues(bound.organizationId, current.organizationId, resolvedScope?.organizationId)) return null
|
|
51
|
+
return current
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isSameProjectAccessScope(left: ProjectAccessScope, right: ProjectAccessScope): boolean {
|
|
55
|
+
return left.projectId === right.projectId
|
|
56
|
+
&& left.tenantId === right.tenantId
|
|
57
|
+
&& left.organizationId === right.organizationId
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function rebindProjectAccessScope(
|
|
61
|
+
binding: ProjectAccessScopeBinding,
|
|
62
|
+
current: ProjectAccessScope,
|
|
63
|
+
): ProjectAccessScopeBinding {
|
|
64
|
+
const continued = bindProjectAccessScope(binding.scope, current, binding.resolvedScope)
|
|
65
|
+
if (!continued) return { scope: current, generation: binding.generation + 1, resolvedScope: null }
|
|
66
|
+
if (isSameProjectAccessScope(continued, binding.scope)) return binding
|
|
67
|
+
return { ...binding, scope: continued }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function useProjectAccessScope(
|
|
71
|
+
timeProjectId?: string | null,
|
|
72
|
+
initialResolvedScope?: ResolvedProjectAccessScope | null,
|
|
73
|
+
): ProjectAccessScopeState {
|
|
74
|
+
useOrganizationScopeVersion()
|
|
75
|
+
const current = readProjectAccessScope(timeProjectId)
|
|
76
|
+
const [binding, setBinding] = React.useState<ProjectAccessScopeBinding>(() => ({
|
|
77
|
+
scope: current,
|
|
78
|
+
generation: 0,
|
|
79
|
+
resolvedScope: initialResolvedScope ?? null,
|
|
80
|
+
}))
|
|
81
|
+
const nextBinding = rebindProjectAccessScope(binding, current)
|
|
82
|
+
if (nextBinding !== binding) setBinding(nextBinding)
|
|
83
|
+
const boundScope = React.useRef(nextBinding)
|
|
84
|
+
boundScope.current = nextBinding
|
|
85
|
+
const generation = nextBinding.generation
|
|
86
|
+
const isCurrentScope = React.useCallback(() => {
|
|
87
|
+
const active = boundScope.current
|
|
88
|
+
return active.generation === generation
|
|
89
|
+
&& bindProjectAccessScope(active.scope, readProjectAccessScope(timeProjectId), active.resolvedScope) !== null
|
|
90
|
+
}, [generation, timeProjectId])
|
|
91
|
+
const resolveScope = React.useCallback((resolvedScope: ResolvedProjectAccessScope | null) => {
|
|
92
|
+
setBinding((previous) => {
|
|
93
|
+
if (previous.generation !== generation) return previous
|
|
94
|
+
if (!bindProjectAccessScope(previous.scope, readProjectAccessScope(timeProjectId), resolvedScope)) return previous
|
|
95
|
+
const changed = previous.resolvedScope !== null && resolvedScope !== null
|
|
96
|
+
&& (previous.resolvedScope.tenantId !== resolvedScope.tenantId
|
|
97
|
+
|| previous.resolvedScope.organizationId !== resolvedScope.organizationId)
|
|
98
|
+
return { ...previous, resolvedScope, generation: previous.generation + (changed ? 1 : 0) }
|
|
99
|
+
})
|
|
100
|
+
}, [generation, timeProjectId])
|
|
101
|
+
return { scopeKey: String(generation), isCurrentScope, resolvedScope: nextBinding.resolvedScope, resolveScope }
|
|
102
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const PROJECT_SCOPE_TENANT_HEADER = 'x-om-project-scope-tenant-id'
|
|
2
|
+
export const PROJECT_SCOPE_ORGANIZATION_HEADER = 'x-om-project-scope-organization-id'
|
|
3
|
+
|
|
4
|
+
export type ResolvedProjectAccessScope = {
|
|
5
|
+
tenantId: string
|
|
6
|
+
organizationId: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function readResolvedProjectAccessScope(
|
|
10
|
+
response: { headers?: Pick<Headers, 'get'> },
|
|
11
|
+
): ResolvedProjectAccessScope | null {
|
|
12
|
+
const tenantId = response.headers?.get(PROJECT_SCOPE_TENANT_HEADER)
|
|
13
|
+
const organizationId = response.headers?.get(PROJECT_SCOPE_ORGANIZATION_HEADER)
|
|
14
|
+
return tenantId && organizationId ? { tenantId, organizationId } : null
|
|
15
|
+
}
|
|
@@ -2,6 +2,16 @@ export const metadata = {
|
|
|
2
2
|
id: 'sync_excel',
|
|
3
3
|
title: 'Excel / CSV Import',
|
|
4
4
|
description: 'File-upload-based CSV import foundation built on top of the data sync hub.',
|
|
5
|
+
/**
|
|
6
|
+
* `customers` is a HARD dependency: `lib/adapters/customers.ts` reads `E.customers.*` at module scope.
|
|
7
|
+
*
|
|
8
|
+
* `E` only carries a key for an ENABLED module, so with `customers` switched off that read is
|
|
9
|
+
* `undefined.customer_entity` and it throws while the adapter is being imported — not inside a request, where
|
|
10
|
+
* it could be traced. Declaring it has the generator refuse the combination before anybody starts the app.
|
|
11
|
+
*
|
|
12
|
+
* `data_sync` is the hub this is built on, named in the description and now in the manifest too.
|
|
13
|
+
*/
|
|
14
|
+
requires: ['customers', 'data_sync'],
|
|
5
15
|
}
|
|
6
16
|
|
|
7
17
|
export default metadata
|
|
@@ -2,6 +2,7 @@ import { registerTranslatableFields } from '@open-mercato/shared/lib/localizatio
|
|
|
2
2
|
import { registerTranslationOverlayPlugin } from '@open-mercato/shared/lib/localization/overlay-plugin'
|
|
3
3
|
import { registerSupportedLocalesResolver } from '@open-mercato/shared/lib/i18n/locale-registry'
|
|
4
4
|
import { translatableFields as catalogFields } from '../catalog/translations'
|
|
5
|
+
import { translatableFields as customerFields } from '../customers/translations'
|
|
5
6
|
import { translatableFields as dictionaryFields } from '../dictionaries/translations'
|
|
6
7
|
import { translatableFields as entitiesFields } from '../entities/translations'
|
|
7
8
|
import { translatableFields as resourcesFields } from '../resources/translations'
|
|
@@ -19,6 +20,7 @@ registerSupportedLocalesResolver(resolveTenantSupportedLocales)
|
|
|
19
20
|
|
|
20
21
|
export function register() {
|
|
21
22
|
registerTranslatableFields(catalogFields)
|
|
23
|
+
registerTranslatableFields(customerFields)
|
|
22
24
|
registerTranslatableFields(dictionaryFields)
|
|
23
25
|
registerTranslatableFields(entitiesFields)
|
|
24
26
|
registerTranslatableFields(resourcesFields)
|
|
@@ -277,7 +277,8 @@ const CUSTOMER_FIELDS = ['id', 'display_name']
|
|
|
277
277
|
/**
|
|
278
278
|
* Decorate list items with `vendor_name` when they carry a `vendor_id` column
|
|
279
279
|
* referencing `customers.customer_entity` by UUID only (no ORM cross-module
|
|
280
|
-
* relations).
|
|
280
|
+
* relations). No-op when the optional `customers` module is not enabled.
|
|
281
|
+
* Mutates `payload.items` in place.
|
|
281
282
|
*/
|
|
282
283
|
export async function attachVendorLabelsToListItems(
|
|
283
284
|
payload: AnyListPayload,
|
|
@@ -285,11 +286,13 @@ export async function attachVendorLabelsToListItems(
|
|
|
285
286
|
): Promise<void> {
|
|
286
287
|
const items = Array.isArray(payload?.items) ? payload.items : []
|
|
287
288
|
if (items.length === 0) return
|
|
289
|
+
const customerEntityId = E.customers?.customer_entity
|
|
290
|
+
if (!customerEntityId) return
|
|
288
291
|
const ids = uniqueIdsFromKeys(items, ['vendor_id'])
|
|
289
292
|
if (ids.length === 0) return
|
|
290
293
|
const map = (await batchLoadById(
|
|
291
294
|
ctx,
|
|
292
|
-
|
|
295
|
+
customerEntityId,
|
|
293
296
|
ids,
|
|
294
297
|
CUSTOMER_FIELDS,
|
|
295
298
|
'vendor',
|
package/src/modules/wms/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const metadata: ModuleInfo = {
|
|
|
9
9
|
author: 'Open Mercato Team',
|
|
10
10
|
license: 'MIT',
|
|
11
11
|
ejectable: true,
|
|
12
|
-
requires: ['catalog', 'sales', 'feature_toggles'],
|
|
12
|
+
requires: ['catalog', 'customers', 'sales', 'feature_toggles'],
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export { features } from './acl'
|