@open-mercato/core 0.8.1-develop.7275.1.f772b944d4 → 0.8.1-develop.7295.1.d0e0e33014
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/users/acl/route.js.map +2 -2
- package/dist/modules/auth/components/AclEditor.js +21 -3
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +6 -4
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js +6 -4
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +6 -4
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +5 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +6 -4
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +6 -4
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +9 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +4 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/lookups.js +7 -3
- package/dist/modules/customers/components/calendar/editor/lookups.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +5 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/RoleAssignmentRow.js +4 -2
- package/dist/modules/customers/components/detail/RoleAssignmentRow.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +8 -5
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +10 -7
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +4 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js +9 -0
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/index.js +3 -1
- package/dist/modules/customers/components/detail/schedule/index.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +11 -4
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +17 -9
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/data/validators.js +12 -7
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/assignableStaff.js +103 -22
- package/dist/modules/customers/lib/assignableStaff.js.map +2 -2
- package/dist/modules/customers/setup.js +10 -2
- package/dist/modules/customers/setup.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +5 -0
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +5 -0
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +29 -5
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +48 -11
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/data_sync/data/validators.js +13 -2
- package/dist/modules/data_sync/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/schedule-value.js +30 -0
- package/dist/modules/data_sync/lib/schedule-value.js.map +7 -0
- package/dist/modules/eudr/data/validators.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +3 -1
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +2 -1
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js.map +2 -2
- package/dist/modules/portal/lib/navigation.js +3 -4
- package/dist/modules/portal/lib/navigation.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +155 -15
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/page.js +122 -86
- package/dist/modules/staff/backend/staff/time-tracking/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +38 -17
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js +45 -18
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js.map +2 -2
- package/dist/modules/staff/extension-points.js +16 -0
- package/dist/modules/staff/extension-points.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js +3 -4
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js +3 -4
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js +14 -13
- package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js +60 -41
- package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js.map +2 -2
- package/dist/modules/warranty_claims/api/ai/assess/route.js +2 -2
- package/dist/modules/warranty_claims/api/ai/assess/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js +3 -4
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js.map +3 -3
- package/dist/modules/warranty_claims/api/ai/suggest/route.js +3 -3
- package/dist/modules/warranty_claims/api/ai/suggest/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/assign/route.js +3 -3
- package/dist/modules/warranty_claims/api/assign/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/credit-memo/route.js +2 -2
- package/dist/modules/warranty_claims/api/credit-memo/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/entitlement/route.js +2 -2
- package/dist/modules/warranty_claims/api/entitlement/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/events/route.js +4 -4
- package/dist/modules/warranty_claims/api/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/external/claims/route.js +3 -3
- package/dist/modules/warranty_claims/api/external/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/events/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/receiving/route.js +2 -2
- package/dist/modules/warranty_claims/api/receiving/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/replacement-order/route.js +2 -2
- package/dist/modules/warranty_claims/api/replacement-order/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/return-label/route.js +2 -2
- package/dist/modules/warranty_claims/api/return-label/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/risk/route.js +2 -2
- package/dist/modules/warranty_claims/api/risk/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/sales-return/route.js +2 -2
- package/dist/modules/warranty_claims/api/sales-return/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/settings-general/route.js +4 -4
- package/dist/modules/warranty_claims/api/settings-general/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/stats/route.js +2 -2
- package/dist/modules/warranty_claims/api/stats/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/submit/route.js +3 -3
- package/dist/modules/warranty_claims/api/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/transition/route.js +3 -3
- package/dist/modules/warranty_claims/api/transition/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js +3 -3
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +2 -2
- package/dist/modules/warranty_claims/backend/page.js +4 -2
- package/dist/modules/warranty_claims/backend/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +9 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js +4 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +3 -4
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js +3 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +3 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +2 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -1
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js +2 -2
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +56 -1
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/users/acl/route.ts +4 -3
- package/src/modules/auth/components/AclEditor.tsx +38 -2
- package/src/modules/auth/i18n/de.json +5 -1
- package/src/modules/auth/i18n/en.json +5 -1
- package/src/modules/auth/i18n/es.json +5 -1
- package/src/modules/auth/i18n/ko.json +5 -1
- package/src/modules/auth/i18n/pl.json +5 -1
- package/src/modules/customers/backend/customers/companies/page.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapView.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/page.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +5 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +6 -4
- package/src/modules/customers/backend/customers/people/page.tsx +6 -4
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +9 -2
- package/src/modules/customers/components/calendar/editor/hooks.ts +4 -2
- package/src/modules/customers/components/calendar/editor/lookups.ts +12 -4
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +4 -1
- package/src/modules/customers/components/detail/RoleAssignmentRow.tsx +4 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +15 -7
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +16 -7
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +4 -2
- package/src/modules/customers/components/detail/schedule/fieldConfig.ts +15 -0
- package/src/modules/customers/components/detail/schedule/index.ts +1 -1
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +16 -9
- package/src/modules/customers/components/formConfig.tsx +35 -12
- package/src/modules/customers/data/validators.ts +14 -8
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/lib/assignableStaff.ts +172 -34
- package/src/modules/customers/setup.ts +8 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +5 -0
- package/src/modules/data_sync/api/schedules/route.ts +5 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +48 -4
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +61 -2
- package/src/modules/data_sync/data/validators.ts +26 -2
- package/src/modules/data_sync/i18n/de.json +2 -0
- package/src/modules/data_sync/i18n/en.json +2 -0
- package/src/modules/data_sync/i18n/es.json +2 -0
- package/src/modules/data_sync/i18n/ko.json +2 -0
- package/src/modules/data_sync/i18n/pl.json +2 -0
- package/src/modules/data_sync/lib/schedule-value.ts +45 -0
- package/src/modules/eudr/data/validators.ts +3 -4
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +5 -4
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +3 -1
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +2 -1
- package/src/modules/portal/frontend/[orgSlug]/portal/page.tsx +5 -5
- package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.tsx +5 -4
- package/src/modules/portal/i18n/de.json +1 -0
- package/src/modules/portal/i18n/en.json +1 -0
- package/src/modules/portal/i18n/es.json +1 -0
- package/src/modules/portal/i18n/ko.json +1 -0
- package/src/modules/portal/i18n/pl.json +1 -0
- package/src/modules/portal/lib/navigation.ts +8 -3
- package/src/modules/query_index/lib/engine.ts +238 -17
- package/src/modules/staff/backend/staff/time-tracking/page.tsx +161 -130
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +55 -43
- package/src/modules/staff/backend/staff/time-tracking/reports/[id]/page.tsx +61 -38
- package/src/modules/staff/extension-points.ts +16 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.tsx +3 -4
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/page.tsx +3 -4
- package/src/modules/staff/i18n/de.json +3 -0
- package/src/modules/staff/i18n/en.json +3 -0
- package/src/modules/staff/i18n/es.json +3 -0
- package/src/modules/staff/i18n/ko.json +3 -0
- package/src/modules/staff/i18n/pl.json +3 -0
- package/src/modules/staff/lib/time-tracking-ui/ReportSheet.tsx +32 -29
- package/src/modules/staff/lib/time-tracking-ui/TaskListView.tsx +89 -79
- package/src/modules/warranty_claims/api/ai/assess/route.ts +2 -2
- package/src/modules/warranty_claims/api/ai/draft-reply/route.ts +2 -3
- package/src/modules/warranty_claims/api/ai/suggest/route.ts +3 -3
- package/src/modules/warranty_claims/api/assign/route.ts +3 -3
- package/src/modules/warranty_claims/api/credit-memo/route.ts +2 -2
- package/src/modules/warranty_claims/api/entitlement/route.ts +2 -2
- package/src/modules/warranty_claims/api/events/route.ts +4 -4
- package/src/modules/warranty_claims/api/external/claims/route.ts +3 -3
- package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/claims/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/events/route.ts +2 -2
- package/src/modules/warranty_claims/api/receiving/route.ts +2 -2
- package/src/modules/warranty_claims/api/replacement-order/route.ts +2 -2
- package/src/modules/warranty_claims/api/return-label/route.ts +2 -2
- package/src/modules/warranty_claims/api/risk/route.ts +2 -2
- package/src/modules/warranty_claims/api/sales-return/route.ts +2 -2
- package/src/modules/warranty_claims/api/settings-general/route.ts +4 -4
- package/src/modules/warranty_claims/api/stats/route.ts +2 -2
- package/src/modules/warranty_claims/api/submit/route.ts +3 -3
- package/src/modules/warranty_claims/api/transition/route.ts +3 -3
- package/src/modules/warranty_claims/api/vendor-recovery/route.ts +3 -3
- package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +2 -2
- package/src/modules/warranty_claims/backend/page.tsx +4 -2
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +9 -2
- package/src/modules/warranty_claims/backend/warranty_claims/settings/page.tsx +4 -2
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +5 -4
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.tsx +5 -2
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +5 -2
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -1
- package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.tsx +2 -2
- package/src/modules/workflows/i18n/de.json +1 -1
- package/src/modules/workflows/i18n/en.json +1 -1
- package/src/modules/workflows/i18n/es.json +1 -1
- package/src/modules/workflows/i18n/ko.json +1 -1
- package/src/modules/workflows/i18n/pl.json +1 -1
- package/src/modules/workflows/lib/event-trigger-service.ts +139 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/portal/frontend/%5BorgSlug%5D/portal/invite/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useCallback, useMemo, useState, useEffect } from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport Link from 'next/link'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { SearchX } from 'lucide-react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'\nimport { navigateWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'\n\ntype Props = { params: { orgSlug: string } }\n\nexport default function PortalInvitePage({ params }: Props) {\n const t = useT()\n const orgSlug = params.orgSlug\n const { tenant } = usePortalContext()\n const searchParams = useSearchParams()\n\n const [displayName, setDisplayName] = useState('')\n const [password, setPassword] = useState('')\n const [confirmPassword, setConfirmPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const [token, setToken] = useState<string | null>(null)\n\n useEffect(() => {\n const tokenParam = searchParams.get('token')\n if (!tokenParam) {\n setError(t('portal.invite.error.noToken', 'Invalid or missing invitation token.'))\n } else {\n setToken(tokenParam)\n }\n }, [searchParams, t])\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent) => {\n event.preventDefault()\n setError(null)\n\n if (!token) {\n setError(t('portal.invite.error.noToken', 'Invalid or missing invitation token.'))\n return\n }\n\n if (!displayName.trim()) {\n setError(t('portal.invite.error.displayNameRequired', 'Display name is required.'))\n return\n }\n\n if (password !== confirmPassword) {\n setError(t('portal.invite.error.passwordMismatch', 'Passwords do not match.'))\n return\n }\n\n if (password.length < 8) {\n setError(t('portal.invite.error.passwordTooShort', 'Password must be at least 8 characters long.'))\n return\n }\n\n setSubmitting(true)\n try {\n const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/invitations/accept', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token, password, displayName: displayName.trim() }),\n })\n\n if (result.ok && result.result?.ok) {\n navigateWithPageReload(`/${orgSlug}/portal/dashboard`)\n return\n }\n\n if (result.status === 400) {\n setError(t('portal.invite.error.invalidToken', 'Invalid or expired invitation.'))\n } else {\n setError(result.result?.error || t('portal.invite.error.generic', 'Invitation acceptance failed. Please try again.'))\n }\n } catch {\n setError(t('portal.invite.error.generic', 'Invitation acceptance failed. Please try again.'))\n } finally {\n setSubmitting(false)\n }\n },\n [token, password, confirmPassword, displayName, orgSlug, t],\n )\n\n const injectionContext = useMemo(\n () => ({ orgSlug }),\n [orgSlug],\n )\n\n if (tenant.loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (tenant.error) {\n return (\n <div className=\"mx-auto w-full max-w-md py-12\">\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={<SearchX className=\"h-6 w-6\" aria-hidden />}\n title={t('portal.org.invalid', 'Organization not found.')}\n />\n </div>\n )\n }\n\n return (\n <div className=\"mx-auto w-full max-w-sm\">\n <div className=\"mb-8 text-center\">\n <h1 className=\"text-2xl font-bold tracking-tight\">{t('portal.invite.title', 'Accept Invitation')}</h1>\n <p className=\"mt-1.5 text-sm text-muted-foreground\">{t('portal.invite.description', 'Create your portal account to accept this invitation.')}</p>\n </div>\n\n <InjectionSpot spotId={PortalInjectionSpots.pageBefore('invite')} context={injectionContext} />\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\">\n {error ? (\n <Alert status=\"error\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"invite-display-name\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.invite.displayName', 'Display Name')}</Label>\n <Input id=\"invite-display-name\" autoComplete=\"name\" required value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={submitting || !token} className=\"rounded-lg\" />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"invite-password\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.invite.password', 'Password')}</Label>\n <PasswordInput id=\"invite-password\" autoComplete=\"new-password\" required placeholder={t('portal.invite.password.placeholder', '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting || !token} className=\"rounded-lg\" />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"invite-confirm-password\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.invite.confirmPassword', 'Confirm Password')}</Label>\n <PasswordInput id=\"invite-confirm-password\" autoComplete=\"new-password\" required placeholder={t('portal.invite.confirmPassword.placeholder', '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022')} value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} disabled={submitting || !token} className=\"rounded-lg\" />\n </div>\n\n <Button type=\"submit\" disabled={submitting || !token} className=\"mt-1 w-full rounded-lg\">\n {submitting ? t('portal.invite.submitting', 'Accepting invitation...') : t('portal.invite.submit', 'Accept Invitation')}\n </Button>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n {t('portal.invite.backToLogin', 'Already have an account?')}{' '}\n <Link href={`/${orgSlug}/portal/login`} className=\"font-medium text-foreground underline underline-offset-4 hover:opacity-80\">\n {t('portal.invite.loginLink', 'Sign in')}\n </Link>\n </p>\n </form>\n\n <InjectionSpot spotId={PortalInjectionSpots.pageAfter('invite')} context={injectionContext} />\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useCallback, useMemo, useState, useEffect } from 'react'\nimport { useSearchParams } from 'next/navigation'\nimport Link from 'next/link'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { SearchX } from 'lucide-react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\nimport { PortalInjectionSpots } from '@open-mercato/ui/backend/injection/spotIds'\nimport { navigateWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'\n\ntype Props = { params: { orgSlug: string } }\n\nexport default function PortalInvitePage({ params }: Props) {\n const t = useT()\n const orgSlug = params.orgSlug\n const { tenant } = usePortalContext()\n const searchParams = useSearchParams()\n\n const [displayName, setDisplayName] = useState('')\n const [password, setPassword] = useState('')\n const [confirmPassword, setConfirmPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const [token, setToken] = useState<string | null>(null)\n\n useEffect(() => {\n const tokenParam = searchParams.get('token')\n if (!tokenParam) {\n setError(t('portal.invite.error.noToken', 'Invalid or missing invitation token.'))\n } else {\n setToken(tokenParam)\n }\n }, [searchParams, t])\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent) => {\n event.preventDefault()\n setError(null)\n\n if (!token) {\n setError(t('portal.invite.error.noToken', 'Invalid or missing invitation token.'))\n return\n }\n\n if (!displayName.trim()) {\n setError(t('portal.invite.error.displayNameRequired', 'Display name is required.'))\n return\n }\n\n if (password !== confirmPassword) {\n setError(t('portal.invite.error.passwordMismatch', 'Passwords do not match.'))\n return\n }\n\n if (password.length < 8) {\n setError(t('portal.invite.error.passwordTooShort', 'Password must be at least 8 characters long.'))\n return\n }\n\n setSubmitting(true)\n try {\n const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/invitations/accept', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token, password, displayName: displayName.trim() }),\n })\n\n if (result.ok && result.result?.ok) {\n navigateWithPageReload(`/${orgSlug}/portal/dashboard`)\n return\n }\n\n if (result.status === 409) {\n setError(t('portal.invite.error.accountExists', 'This email address already has a portal account. Sign in below instead of accepting the invitation.'))\n } else if (result.status === 400) {\n setError(t('portal.invite.error.invalidToken', 'Invalid or expired invitation.'))\n } else {\n setError(result.result?.error || t('portal.invite.error.generic', 'Invitation acceptance failed. Please try again.'))\n }\n } catch {\n setError(t('portal.invite.error.generic', 'Invitation acceptance failed. Please try again.'))\n } finally {\n setSubmitting(false)\n }\n },\n [token, password, confirmPassword, displayName, orgSlug, t],\n )\n\n const injectionContext = useMemo(\n () => ({ orgSlug }),\n [orgSlug],\n )\n\n if (tenant.loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (tenant.error) {\n return (\n <div className=\"mx-auto w-full max-w-md py-12\">\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={<SearchX className=\"h-6 w-6\" aria-hidden />}\n title={t('portal.org.invalid', 'Organization not found.')}\n />\n </div>\n )\n }\n\n return (\n <div className=\"mx-auto w-full max-w-sm\">\n <div className=\"mb-8 text-center\">\n <h1 className=\"text-2xl font-bold tracking-tight\">{t('portal.invite.title', 'Accept Invitation')}</h1>\n <p className=\"mt-1.5 text-sm text-muted-foreground\">{t('portal.invite.description', 'Create your portal account to accept this invitation.')}</p>\n </div>\n\n <InjectionSpot spotId={PortalInjectionSpots.pageBefore('invite')} context={injectionContext} />\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\">\n {error ? (\n <Alert status=\"error\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"invite-display-name\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.invite.displayName', 'Display Name')}</Label>\n <Input id=\"invite-display-name\" autoComplete=\"name\" required value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={submitting || !token} className=\"rounded-lg\" />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"invite-password\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.invite.password', 'Password')}</Label>\n <PasswordInput id=\"invite-password\" autoComplete=\"new-password\" required placeholder={t('portal.invite.password.placeholder', '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting || !token} className=\"rounded-lg\" />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"invite-confirm-password\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.invite.confirmPassword', 'Confirm Password')}</Label>\n <PasswordInput id=\"invite-confirm-password\" autoComplete=\"new-password\" required placeholder={t('portal.invite.confirmPassword.placeholder', '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022')} value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} disabled={submitting || !token} className=\"rounded-lg\" />\n </div>\n\n <Button type=\"submit\" disabled={submitting || !token} className=\"mt-1 w-full rounded-lg\">\n {submitting ? t('portal.invite.submitting', 'Accepting invitation...') : t('portal.invite.submit', 'Accept Invitation')}\n </Button>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n {t('portal.invite.backToLogin', 'Already have an account?')}{' '}\n <Link href={`/${orgSlug}/portal/login`} className=\"font-medium text-foreground underline underline-offset-4 hover:opacity-80\">\n {t('portal.invite.loginLink', 'Sign in')}\n </Link>\n </p>\n </form>\n\n <InjectionSpot spotId={PortalInjectionSpots.pageAfter('invite')} context={injectionContext} />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,6 +13,7 @@ import { EmptyState } from "@open-mercato/ui/primitives/empty-state";
|
|
|
13
13
|
import { SearchX } from "lucide-react";
|
|
14
14
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
15
15
|
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
16
|
+
import { navigateWithPageReload } from "@open-mercato/core/modules/portal/lib/navigation";
|
|
16
17
|
import { usePortalContext } from "@open-mercato/ui/portal/PortalContext";
|
|
17
18
|
import { InjectionSpot } from "@open-mercato/ui/backend/injection/InjectionSpot";
|
|
18
19
|
function PortalLoginPage({ params }) {
|
|
@@ -40,7 +41,7 @@ function PortalLoginPage({ params }) {
|
|
|
40
41
|
body: JSON.stringify({ email, password, organizationId: tenant.organizationId })
|
|
41
42
|
});
|
|
42
43
|
if (result.ok && result.result?.ok) {
|
|
43
|
-
|
|
44
|
+
navigateWithPageReload(`/${orgSlug}/portal/dashboard`);
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
47
|
if (result.status === 401) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/portal/frontend/%5BorgSlug%5D/portal/login/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useCallback, useMemo, useState } from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/portal/extension-points'\nimport Link from 'next/link'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { SearchX } from 'lucide-react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\n\ntype Props = { params: { orgSlug: string } }\n\nexport default function PortalLoginPage({ params }: Props) {\n const t = useT()\n const orgSlug = params.orgSlug\n const { tenant } = usePortalContext()\n\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent) => {\n event.preventDefault()\n setError(null)\n\n if (!tenant.organizationId) {\n setError(t('portal.org.invalid', 'Organization not found.'))\n return\n }\n\n setSubmitting(true)\n try {\n const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/login', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ email, password, organizationId: tenant.organizationId }),\n })\n\n if (result.ok && result.result?.ok) {\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useCallback, useMemo, useState } from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/portal/extension-points'\nimport Link from 'next/link'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { EmailInput } from '@open-mercato/ui/primitives/email-input'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { SearchX } from 'lucide-react'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { navigateWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\n\ntype Props = { params: { orgSlug: string } }\n\nexport default function PortalLoginPage({ params }: Props) {\n const t = useT()\n const orgSlug = params.orgSlug\n const { tenant } = usePortalContext()\n\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent) => {\n event.preventDefault()\n setError(null)\n\n if (!tenant.organizationId) {\n setError(t('portal.org.invalid', 'Organization not found.'))\n return\n }\n\n setSubmitting(true)\n try {\n const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/login', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ email, password, organizationId: tenant.organizationId }),\n })\n\n if (result.ok && result.result?.ok) {\n navigateWithPageReload(`/${orgSlug}/portal/dashboard`)\n return\n }\n\n if (result.status === 401) {\n // The login API intentionally returns a single generic 401 for unknown, inactive,\n // locked, and unverified accounts so account existence cannot be enumerated.\n // Lockout/deactivation guidance is delivered out-of-band (e.g. email), never here.\n setError(t('portal.login.error.invalidCredentials', 'Invalid email or password.'))\n } else {\n setError(result.result?.error || t('portal.login.error.generic', 'Login failed. Please try again.'))\n }\n } catch {\n setError(t('portal.login.error.generic', 'Login failed. Please try again.'))\n } finally {\n setSubmitting(false)\n }\n },\n [email, password, tenant.organizationId, orgSlug, t],\n )\n\n const injectionContext = useMemo(\n () => ({ orgSlug }),\n [orgSlug],\n )\n\n if (tenant.loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (tenant.error) {\n return (\n <div className=\"mx-auto w-full max-w-md py-12\">\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={<SearchX className=\"h-6 w-6\" aria-hidden />}\n title={t('portal.org.invalid', 'Organization not found.')}\n />\n </div>\n )\n }\n\n return (\n <div className=\"mx-auto w-full max-w-sm\">\n <div className=\"mb-8 text-center\">\n <h1 className=\"text-2xl font-bold tracking-tight\">{t('portal.login.title', 'Sign In')}</h1>\n <p className=\"mt-1.5 text-sm text-muted-foreground\">{t('portal.login.description', 'Enter your credentials to access the portal.')}</p>\n </div>\n\n <InjectionSpot spotId={extensionPoints.hosts.loginBefore.spotId} context={injectionContext} />\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\">\n {error ? (\n <Alert status=\"error\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"login-email\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.login.email', 'Email')}</Label>\n <EmailInput id=\"login-email\" required placeholder={t('portal.login.email.placeholder', 'you@example.com')} value={email} onChange={(e) => setEmail(e.target.value)} disabled={submitting} className=\"rounded-lg\" />\n </div>\n\n <div className=\"flex flex-col gap-1.5\">\n <Label htmlFor=\"login-password\" className=\"text-overline font-semibold uppercase tracking-wider text-muted-foreground/70\">{t('portal.login.password', 'Password')}</Label>\n <PasswordInput id=\"login-password\" autoComplete=\"current-password\" required placeholder={t('portal.login.password.placeholder', '\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022')} value={password} onChange={(e) => setPassword(e.target.value)} disabled={submitting} className=\"rounded-lg\" />\n </div>\n\n <Button type=\"submit\" disabled={submitting} className=\"mt-1 w-full rounded-lg\">\n {submitting ? t('portal.login.submitting', 'Signing in...') : t('portal.login.submit', 'Sign In')}\n </Button>\n\n <p className=\"text-center text-sm text-muted-foreground\">\n {t('portal.login.noAccount', \"Don't have an account?\")}{' '}\n <Link href={`/${orgSlug}/portal/signup`} className=\"font-medium text-foreground underline underline-offset-4 hover:opacity-80\">\n {t('portal.login.signupLink', 'Sign up')}\n </Link>\n </p>\n </form>\n\n <InjectionSpot spotId={extensionPoints.hosts.loginAfter.spotId} context={injectionContext} />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useMemo } from "react";
|
|
4
4
|
import { extensionPoints } from "@open-mercato/core/modules/portal/extension-points";
|
|
5
5
|
import Link from "next/link";
|
|
6
|
-
import {
|
|
6
|
+
import { replaceWithPageReload } from "@open-mercato/core/modules/portal/lib/navigation";
|
|
7
7
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
8
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
9
9
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
@@ -14,14 +14,13 @@ import { PortalFeatureCard } from "@open-mercato/ui/portal/components/PortalFeat
|
|
|
14
14
|
import { InjectionSpot } from "@open-mercato/ui/backend/injection/InjectionSpot";
|
|
15
15
|
function PortalLandingPage({ params }) {
|
|
16
16
|
const t = useT();
|
|
17
|
-
const router = useRouter();
|
|
18
17
|
const orgSlug = params.orgSlug;
|
|
19
18
|
const { auth, tenant } = usePortalContext();
|
|
20
19
|
useEffect(() => {
|
|
21
20
|
if (!auth.loading && auth.user) {
|
|
22
|
-
|
|
21
|
+
replaceWithPageReload(`/${orgSlug}/portal/dashboard`);
|
|
23
22
|
}
|
|
24
|
-
}, [auth.loading, auth.user,
|
|
23
|
+
}, [auth.loading, auth.user, orgSlug]);
|
|
25
24
|
const injectionContext = useMemo(
|
|
26
25
|
() => ({ orgSlug }),
|
|
27
26
|
[orgSlug]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/portal/frontend/%5BorgSlug%5D/portal/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useEffect, useMemo } from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/portal/extension-points'\nimport Link from 'next/link'\nimport {
|
|
5
|
-
"mappings": ";AAmCmE,SAoB/D,UApB+D,KAiC3D,YAjC2D;AAlCnE,SAAS,WAAW,eAAe;AACnC,SAAS,uBAAuB;AAChC,OAAO,UAAU;AACjB,SAAS,
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useEffect, useMemo } from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/portal/extension-points'\nimport Link from 'next/link'\nimport { replaceWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { SearchX, ShoppingBag, User, Shield } from 'lucide-react'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { PortalFeatureCard } from '@open-mercato/ui/portal/components/PortalFeatureCard'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\n\ntype Props = { params: { orgSlug: string } }\n\nexport default function PortalLandingPage({ params }: Props) {\n const t = useT()\n const orgSlug = params.orgSlug\n const { auth, tenant } = usePortalContext()\n\n // Redirect authenticated users to dashboard. This crosses the public/authenticated\n // boundary, so it must be a full page load \u2014 see `replaceWithPageReload`.\n useEffect(() => {\n if (!auth.loading && auth.user) {\n replaceWithPageReload(`/${orgSlug}/portal/dashboard`)\n }\n }, [auth.loading, auth.user, orgSlug])\n\n const injectionContext = useMemo(\n () => ({ orgSlug }),\n [orgSlug],\n )\n\n if (auth.loading || tenant.loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (tenant.error) {\n return (\n <div className=\"mx-auto w-full max-w-md py-12\">\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={<SearchX className=\"h-6 w-6\" aria-hidden />}\n title={t('portal.org.invalid', 'Organization not found.')}\n />\n </div>\n )\n }\n\n // Authenticated user \u2014 redirect is in progress\n if (auth.user) return null\n\n return (\n <>\n <InjectionSpot spotId={extensionPoints.hosts.homeBefore.spotId} context={injectionContext} />\n\n <section className=\"flex flex-col items-center gap-5 py-8 text-center sm:py-16\">\n <p className=\"text-overline font-semibold uppercase tracking-widest text-muted-foreground/60\">\n {t('portal.nav.home', 'Customer Portal')}\n </p>\n <h1 className=\"max-w-2xl text-4xl font-bold tracking-tight text-foreground sm:text-5xl lg:text-6xl\">\n {t('portal.landing.hero.title', 'Welcome to your portal')}\n </h1>\n <p className=\"max-w-lg text-base leading-relaxed text-muted-foreground sm:text-lg\">\n {t('portal.landing.hero.description', 'Access your account, manage orders, and stay up to date.')}\n </p>\n <div className=\"mt-2 flex items-center gap-3\">\n <Button asChild size=\"lg\" className=\"rounded-lg px-6 text-sm\">\n <Link href={`/${orgSlug}/portal/login`}>{t('portal.landing.cta.login', 'Sign In')}</Link>\n </Button>\n <Button asChild variant=\"outline\" size=\"lg\" className=\"rounded-lg px-6 text-sm\">\n <Link href={`/${orgSlug}/portal/signup`}>{t('portal.landing.cta.signup', 'Create Account')}</Link>\n </Button>\n </div>\n </section>\n\n <section className=\"grid gap-4 sm:grid-cols-3\">\n <PortalFeatureCard\n icon={<ShoppingBag className=\"size-5\" />}\n title={t('portal.landing.feature.orders', 'Orders & Invoices')}\n description={t('portal.landing.feature.orders.description', 'Track your orders, download invoices, and view delivery status in real time.')}\n />\n <PortalFeatureCard\n icon={<User className=\"size-5\" />}\n title={t('portal.landing.feature.account', 'Account Management')}\n description={t('portal.landing.feature.account.description', 'Update your profile, manage team members, and configure your preferences.')}\n />\n <PortalFeatureCard\n icon={<Shield className=\"size-5\" />}\n title={t('portal.landing.feature.security', 'Secure Access')}\n description={t('portal.landing.feature.security.description', 'Role-based permissions, session management, and full audit trail.')}\n />\n </section>\n\n <InjectionSpot spotId={extensionPoints.hosts.homeAfter.spotId} context={injectionContext} />\n </>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo } from "react";
|
|
4
4
|
import { extensionPoints } from "@open-mercato/core/modules/portal/extension-points";
|
|
5
|
-
import {
|
|
5
|
+
import { replaceWithPageReload } from "@open-mercato/core/modules/portal/lib/navigation";
|
|
6
6
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
7
|
import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
8
8
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
@@ -12,14 +12,13 @@ import { PortalCard, PortalCardHeader, PortalStatRow, PortalCardDivider } from "
|
|
|
12
12
|
import { InjectionSpot } from "@open-mercato/ui/backend/injection/InjectionSpot";
|
|
13
13
|
function PortalProfilePage({ params }) {
|
|
14
14
|
const t = useT();
|
|
15
|
-
const router = useRouter();
|
|
16
15
|
const { auth } = usePortalContext();
|
|
17
16
|
const { user, roles, resolvedFeatures, isPortalAdmin, loading } = auth;
|
|
18
17
|
useEffect(() => {
|
|
19
18
|
if (!loading && !user) {
|
|
20
|
-
|
|
19
|
+
replaceWithPageReload(`/${params.orgSlug}/portal/login`);
|
|
21
20
|
}
|
|
22
|
-
}, [loading, user,
|
|
21
|
+
}, [loading, user, params.orgSlug]);
|
|
23
22
|
const injectionContext = useMemo(
|
|
24
23
|
() => ({ orgSlug: params.orgSlug, user, roles, resolvedFeatures, isPortalAdmin }),
|
|
25
24
|
[params.orgSlug, user, roles, resolvedFeatures, isPortalAdmin]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/portal/frontend/%5BorgSlug%5D/portal/profile/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { useEffect, useMemo } from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/portal/extension-points'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useEffect, useMemo } from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/portal/extension-points'\nimport { replaceWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { usePortalContext } from '@open-mercato/ui/portal/PortalContext'\nimport { PortalPageHeader } from '@open-mercato/ui/portal/components/PortalPageHeader'\nimport { PortalCard, PortalCardHeader, PortalStatRow, PortalCardDivider } from '@open-mercato/ui/portal/components/PortalCard'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\n\ntype Props = { params: { orgSlug: string } }\n\nexport default function PortalProfilePage({ params }: Props) {\n const t = useT()\n const { auth } = usePortalContext()\n const { user, roles, resolvedFeatures, isPortalAdmin, loading } = auth\n\n // Leaving an authenticated page for the login page crosses the public/authenticated\n // boundary, so it must be a full page load \u2014 see `replaceWithPageReload`.\n useEffect(() => {\n if (!loading && !user) {\n replaceWithPageReload(`/${params.orgSlug}/portal/login`)\n }\n }, [loading, user, params.orgSlug])\n\n const injectionContext = useMemo(\n () => ({ orgSlug: params.orgSlug, user, roles, resolvedFeatures, isPortalAdmin }),\n [params.orgSlug, user, roles, resolvedFeatures, isPortalAdmin],\n )\n\n if (loading) {\n return <div className=\"flex items-center justify-center py-20\"><Spinner /></div>\n }\n\n if (!user) return null\n\n const formatDate = (dateString: string | null) => {\n if (!dateString) return t('portal.dashboard.never', 'Never')\n return new Date(dateString).toLocaleDateString(undefined, {\n year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',\n })\n }\n\n return (\n <div className=\"flex flex-col gap-8\">\n <PortalPageHeader\n label={t('portal.profile.label', 'Account')}\n title={t('portal.profile.title', 'Profile')}\n />\n\n <InjectionSpot spotId={extensionPoints.hosts.profileBefore.spotId} context={injectionContext} />\n\n <div className=\"grid gap-5 md:grid-cols-2\">\n <PortalCard>\n <PortalCardHeader label={t('portal.dashboard.profile', 'Profile')} title={user.displayName} />\n <div className=\"flex flex-col\">\n <PortalStatRow\n label={t('portal.dashboard.email', 'Email')}\n value={\n <span className=\"flex items-center gap-2\">\n <span className=\"truncate\">{user.email}</span>\n <Badge variant={user.emailVerified ? 'default' : 'outline'} className=\"shrink-0 text-overline\">\n {user.emailVerified ? t('portal.dashboard.emailVerified', 'Verified') : t('portal.dashboard.emailNotVerified', 'Unverified')}\n </Badge>\n </span>\n }\n />\n <PortalCardDivider />\n <PortalStatRow label={t('portal.dashboard.lastLogin', 'Last login')} value={formatDate(user.lastLoginAt)} />\n <PortalCardDivider />\n <PortalStatRow label={t('portal.dashboard.memberSince', 'Member since')} value={formatDate(user.createdAt)} />\n {isPortalAdmin ? (\n <>\n <PortalCardDivider />\n <PortalStatRow label={t('portal.dashboard.roles', 'Role')} value={<Badge className=\"text-overline\">{t('portal.dashboard.portalAdmin', 'Portal Admin')}</Badge>} />\n </>\n ) : null}\n </div>\n </PortalCard>\n\n <PortalCard>\n <PortalCardHeader label={t('portal.dashboard.roles', 'Roles')} title={`${roles.length} assigned`} />\n {roles.length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {roles.map((role) => (\n <span key={role.id} className=\"inline-flex items-center rounded-lg border px-3 py-1.5 text-xs font-medium\">{role.name}</span>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">{t('portal.dashboard.noRoles', 'No roles assigned')}</p>\n )}\n </PortalCard>\n\n <PortalCard className=\"md:col-span-2\">\n <PortalCardHeader label={t('portal.dashboard.permissions', 'Permissions')} title={`${resolvedFeatures.length} features`} />\n {resolvedFeatures.length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {resolvedFeatures.map((feature) => (\n <span key={feature} className=\"inline-flex items-center rounded-md border bg-muted/50 px-2 py-1 font-mono text-overline text-muted-foreground\">{feature}</span>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">{t('portal.dashboard.noPermissions', 'No permissions')}</p>\n )}\n </PortalCard>\n </div>\n\n <InjectionSpot spotId={extensionPoints.hosts.profileAfter.spotId} context={injectionContext} />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
window.location.assign(path);
|
|
3
|
-
}
|
|
1
|
+
import { navigateWithPageReload, replaceWithPageReload } from "@open-mercato/shared/lib/navigation/pageReload";
|
|
4
2
|
export {
|
|
5
|
-
navigateWithPageReload
|
|
3
|
+
navigateWithPageReload,
|
|
4
|
+
replaceWithPageReload
|
|
6
5
|
};
|
|
7
6
|
//# sourceMappingURL=navigation.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/portal/lib/navigation.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * Portal-local import path for the boundary-crossing navigation helpers.\n *\n * The implementation lives in `@open-mercato/shared/lib/navigation/pageReload` so `@open-mercato/ui`\n * \u2014 which does not depend on `@open-mercato/core` \u2014 can reach it from the portal shell and hooks.\n * This re-export keeps the published import path stable for existing callers.\n */\nexport { navigateWithPageReload, replaceWithPageReload } from '@open-mercato/shared/lib/navigation/pageReload'\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,wBAAwB,6BAA6B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,12 @@ import { sql } from "kysely";
|
|
|
5
5
|
import { readCoverageSnapshot, refreshCoverageSnapshot } from "./coverage.js";
|
|
6
6
|
import { createProfiler, shouldEnableProfiler } from "@open-mercato/shared/lib/profiler";
|
|
7
7
|
import { decryptIndexDocCustomFields } from "@open-mercato/shared/lib/encryption/indexDoc";
|
|
8
|
+
import {
|
|
9
|
+
buildCustomFieldKindIndex,
|
|
10
|
+
createCustomFieldKindMapResolver,
|
|
11
|
+
emptyCustomFieldKindIndex,
|
|
12
|
+
mergeCustomFieldKindIndexes
|
|
13
|
+
} from "@open-mercato/shared/lib/custom-fields/kinds";
|
|
8
14
|
import { parseBooleanToken, parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
|
|
9
15
|
import {
|
|
10
16
|
applyJoinFilters,
|
|
@@ -21,10 +27,12 @@ import {
|
|
|
21
27
|
hasSearchFilter
|
|
22
28
|
} from "@open-mercato/shared/lib/search/availability";
|
|
23
29
|
import { tokenizeText } from "@open-mercato/shared/lib/search/tokenize";
|
|
30
|
+
import { buildContainmentPatterns } from "@open-mercato/shared/lib/search/containment";
|
|
24
31
|
import { runBeforeQueryPipeline, runAfterQueryPipeline } from "@open-mercato/shared/lib/query/query-extension-runner";
|
|
25
32
|
import { warnOnCiphertextLikeFallback } from "@open-mercato/shared/lib/query/ciphertext-search-warning";
|
|
26
33
|
import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from "@open-mercato/shared/lib/query/encrypted-sort";
|
|
27
34
|
import { resolveListCountCap } from "@open-mercato/shared/lib/query/count-cap";
|
|
35
|
+
import { resolveCfDefIndexOrgCandidates } from "@open-mercato/shared/lib/crud/custom-field-definition-index";
|
|
28
36
|
import { mapWithConcurrency } from "@open-mercato/shared/lib/query/bounded-decrypt";
|
|
29
37
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
30
38
|
import { parseNumberWithDefault } from "@open-mercato/shared/lib/number";
|
|
@@ -43,6 +51,7 @@ const CF_FILTER_SUPPORTED_OPS = /* @__PURE__ */ new Set([
|
|
|
43
51
|
"lt",
|
|
44
52
|
"lte"
|
|
45
53
|
]);
|
|
54
|
+
const NUMERIC_CF_SORT_KINDS = /* @__PURE__ */ new Set(["integer", "float"]);
|
|
46
55
|
const DECRYPT_CONCURRENCY = 8;
|
|
47
56
|
const AUTO_REINDEX_DEBOUNCE_DEFAULT_MS = 3e4;
|
|
48
57
|
const AUTO_REINDEX_DEBOUNCE_MAX_SCOPES = 1e4;
|
|
@@ -138,7 +147,8 @@ class HybridQueryEngine {
|
|
|
138
147
|
this.eventBusResolver = eventBusResolver;
|
|
139
148
|
this.vectorServiceResolver = vectorServiceResolver;
|
|
140
149
|
this.encryptionResolver = encryptionResolver;
|
|
141
|
-
this.
|
|
150
|
+
this.customFieldDefsCache = /* @__PURE__ */ new Map();
|
|
151
|
+
this.customFieldKindCache = /* @__PURE__ */ new Map();
|
|
142
152
|
this.columnCache = /* @__PURE__ */ new Map();
|
|
143
153
|
this.customEntityCache = /* @__PURE__ */ new Map();
|
|
144
154
|
this.debugVerbosity = null;
|
|
@@ -584,6 +594,15 @@ class HybridQueryEngine {
|
|
|
584
594
|
fallbackOrgId
|
|
585
595
|
);
|
|
586
596
|
const requiresPlaintextSort = encryptedSortFields.size > 0;
|
|
597
|
+
const cfSortKeys = Array.from(new Set(
|
|
598
|
+
resolvedSorts.filter((sort) => sort.field.startsWith("cf:")).map((sort) => sort.field.slice(3))
|
|
599
|
+
));
|
|
600
|
+
const cfSortKinds = cfSortKeys.length ? await this.resolveCustomFieldSortKinds(
|
|
601
|
+
indexSources.map((source) => String(source.entityId)),
|
|
602
|
+
opts.tenantId ?? null,
|
|
603
|
+
resolveCfDefIndexOrgCandidates(opts.organizationIds, fallbackOrgId),
|
|
604
|
+
cfSortKeys
|
|
605
|
+
) : /* @__PURE__ */ new Map();
|
|
587
606
|
const applyBaseScope = (q) => {
|
|
588
607
|
let next = q;
|
|
589
608
|
if (orgScope && hasOrganizationColumn) {
|
|
@@ -929,7 +948,13 @@ class HybridQueryEngine {
|
|
|
929
948
|
const textExpr = this.buildCfTextExprSql(fieldName, indexSources);
|
|
930
949
|
if (textExpr) {
|
|
931
950
|
const direction = sql.raw(coerceSortDirection(s.dir));
|
|
932
|
-
|
|
951
|
+
const kind = cfSortKinds.get(fieldName.slice(3));
|
|
952
|
+
let sortExpr = textExpr;
|
|
953
|
+
if (kind && NUMERIC_CF_SORT_KINDS.has(kind)) {
|
|
954
|
+
const jsonExpr = this.buildCfJsonExprSql(fieldName, indexSources);
|
|
955
|
+
sortExpr = jsonExpr ? sql`CASE WHEN jsonb_typeof(${jsonExpr}) = 'number' THEN (${textExpr})::numeric END` : textExpr;
|
|
956
|
+
}
|
|
957
|
+
next = next.orderBy(sql`${sortExpr} ${direction} NULLS LAST`);
|
|
933
958
|
}
|
|
934
959
|
} else {
|
|
935
960
|
const baseField = resolveBaseColumn(fieldName);
|
|
@@ -937,6 +962,10 @@ class HybridQueryEngine {
|
|
|
937
962
|
next = next.orderBy(qualify(baseField), s.dir ?? SortDir.Asc);
|
|
938
963
|
}
|
|
939
964
|
}
|
|
965
|
+
const lastSortField = resolvedSorts.length ? String(resolvedSorts[resolvedSorts.length - 1].field) : null;
|
|
966
|
+
if (resolvedSorts.length && lastSortField !== "id") {
|
|
967
|
+
next = next.orderBy(qualify("id"), SortDir.Asc);
|
|
968
|
+
}
|
|
940
969
|
return next;
|
|
941
970
|
};
|
|
942
971
|
const page = opts.page?.page ?? 1;
|
|
@@ -968,6 +997,16 @@ class HybridQueryEngine {
|
|
|
968
997
|
const total = counted.total;
|
|
969
998
|
const listCountCapWarning = counted.warning;
|
|
970
999
|
const dekKeyCache = /* @__PURE__ */ new Map();
|
|
1000
|
+
let customFieldKinds = null;
|
|
1001
|
+
const resolveCustomFieldKinds = () => {
|
|
1002
|
+
if (!customFieldKinds) {
|
|
1003
|
+
customFieldKinds = this.resolveCustomFieldKindIndex(
|
|
1004
|
+
Array.from(new Set(indexSources.map((src) => String(src.entityId)))),
|
|
1005
|
+
opts.tenantId ?? null
|
|
1006
|
+
).then((index) => createCustomFieldKindMapResolver(index));
|
|
1007
|
+
}
|
|
1008
|
+
return customFieldKinds;
|
|
1009
|
+
};
|
|
971
1010
|
const decryptRow = async (item) => {
|
|
972
1011
|
let next = item;
|
|
973
1012
|
if (encSvc?.decryptEntityPayload) {
|
|
@@ -986,14 +1025,17 @@ class HybridQueryEngine {
|
|
|
986
1025
|
}
|
|
987
1026
|
if (encSvc) {
|
|
988
1027
|
try {
|
|
1028
|
+
const cfScope = {
|
|
1029
|
+
tenantId: next?.tenant_id ?? next?.tenantId ?? opts.tenantId ?? null,
|
|
1030
|
+
organizationId: next?.organization_id ?? next?.organizationId ?? fallbackOrgId ?? null
|
|
1031
|
+
};
|
|
1032
|
+
const resolveKinds = await resolveCustomFieldKinds();
|
|
989
1033
|
next = await decryptIndexDocCustomFields(
|
|
990
1034
|
next,
|
|
991
|
-
|
|
992
|
-
tenantId: next?.tenant_id ?? next?.tenantId ?? opts.tenantId ?? null,
|
|
993
|
-
organizationId: next?.organization_id ?? next?.organizationId ?? null
|
|
994
|
-
},
|
|
1035
|
+
cfScope,
|
|
995
1036
|
encSvc,
|
|
996
|
-
dekKeyCache
|
|
1037
|
+
dekKeyCache,
|
|
1038
|
+
resolveKinds(cfScope.organizationId, cfScope.tenantId)
|
|
997
1039
|
);
|
|
998
1040
|
} catch {
|
|
999
1041
|
}
|
|
@@ -1535,6 +1577,12 @@ class HybridQueryEngine {
|
|
|
1535
1577
|
}
|
|
1536
1578
|
return searchRuntime?.encryptedFields != null && isEncryptedLikeField(searchRuntime.encryptedFields, fieldName) ? sql`false` : sql`true`;
|
|
1537
1579
|
}
|
|
1580
|
+
if ((filter.op === "like" || filter.op === "ilike") && typeof filter.value === "string" && searchRuntime?.enabled && searchRuntime.encryptedFields != null) {
|
|
1581
|
+
const patterns = buildContainmentPatterns(filter.value);
|
|
1582
|
+
if (patterns.length > 1) {
|
|
1583
|
+
return eb.and(patterns.map((pattern) => eb(qualify(baseField), filter.op, pattern)));
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1538
1586
|
return this.buildColumnFilterExpression(eb, qualify(baseField), filter.op, filter.value);
|
|
1539
1587
|
}
|
|
1540
1588
|
buildColumnFilterExpression(eb, column, op, value) {
|
|
@@ -1738,29 +1786,113 @@ class HybridQueryEngine {
|
|
|
1738
1786
|
if (present) baseTableExistsCache.set(table, true);
|
|
1739
1787
|
return present;
|
|
1740
1788
|
}
|
|
1741
|
-
async
|
|
1789
|
+
async resolveCustomFieldDefs(entityIds, tenantId) {
|
|
1742
1790
|
if (!entityIds.length) return [];
|
|
1743
1791
|
const cacheKey = this.customFieldKeysCacheKey(entityIds, tenantId);
|
|
1744
1792
|
const now = Date.now();
|
|
1745
|
-
const cached = this.
|
|
1793
|
+
const cached = this.customFieldDefsCache.get(cacheKey);
|
|
1746
1794
|
if (cached && cached.expiresAt > now) return cached.value.slice();
|
|
1747
1795
|
const db = this.getDb();
|
|
1748
|
-
const rows = await db.selectFrom("custom_field_defs").select("key").where("entity_id", "in", entityIds).where("is_active", "=", true).where((eb) => eb.or([
|
|
1796
|
+
const rows = await db.selectFrom("custom_field_defs").select(["entity_id", "key", "kind", "organization_id", "tenant_id", "config_json", "updated_at"]).where("entity_id", "in", entityIds).where("is_active", "=", true).where((eb) => eb.or([
|
|
1749
1797
|
eb("tenant_id", "=", tenantId),
|
|
1750
1798
|
eb("tenant_id", "is", null)
|
|
1751
1799
|
])).execute();
|
|
1752
|
-
const
|
|
1800
|
+
const result = [];
|
|
1753
1801
|
for (const row of rows) {
|
|
1754
1802
|
const key = row.key;
|
|
1755
|
-
|
|
1756
|
-
|
|
1803
|
+
const normalized = typeof key === "string" ? key.trim() : key == null ? "" : String(key);
|
|
1804
|
+
if (!normalized.length) continue;
|
|
1805
|
+
const entityId = typeof row.entity_id === "string" ? row.entity_id : String(row.entity_id ?? "");
|
|
1806
|
+
result.push({
|
|
1807
|
+
entityId,
|
|
1808
|
+
key: normalized,
|
|
1809
|
+
kind: row.kind,
|
|
1810
|
+
organizationId: typeof row.organization_id === "string" ? row.organization_id : null,
|
|
1811
|
+
tenantId: typeof row.tenant_id === "string" ? row.tenant_id : null,
|
|
1812
|
+
configJson: row.config_json ?? null,
|
|
1813
|
+
updatedAt: row.updated_at ?? null
|
|
1814
|
+
});
|
|
1757
1815
|
}
|
|
1758
|
-
const result = Array.from(keys);
|
|
1759
1816
|
if (this.customFieldKeysTtlMs > 0) {
|
|
1760
|
-
this.
|
|
1817
|
+
this.customFieldDefsCache.set(cacheKey, { expiresAt: now + this.customFieldKeysTtlMs, value: result });
|
|
1761
1818
|
}
|
|
1762
1819
|
return result.slice();
|
|
1763
1820
|
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Resolve the declared `kind` for a set of `cf:` sort keys so `applySort` can
|
|
1823
|
+
* cast numeric kinds instead of ordering by jsonb text (#5674). A same-org
|
|
1824
|
+
* definition wins over a tenant-wide one, which wins over a global one, for
|
|
1825
|
+
* the same key — a same-key definition scoped to a *different* organization
|
|
1826
|
+
* in the tenant never gets to decide the kind (#5674 review).
|
|
1827
|
+
*/
|
|
1828
|
+
async resolveCustomFieldSortKinds(entityIds, tenantId, organizationIds, keys) {
|
|
1829
|
+
if (!entityIds.length || !keys.length) return /* @__PURE__ */ new Map();
|
|
1830
|
+
const sortedOrgIds = organizationIds.slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
1831
|
+
const cacheKey = `${this.customFieldKeysCacheKey(entityIds, tenantId)}|org:${sortedOrgIds.join(",")}|${keys.slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join(",")}`;
|
|
1832
|
+
const now = Date.now();
|
|
1833
|
+
const cached = this.customFieldKindCache.get(cacheKey);
|
|
1834
|
+
if (cached && cached.expiresAt > now) return new Map(cached.value);
|
|
1835
|
+
const db = this.getDb();
|
|
1836
|
+
const rows = await db.selectFrom("custom_field_defs").select(["key", "kind", "organization_id", "tenant_id"]).where("entity_id", "in", entityIds).where("key", "in", keys).where("is_active", "=", true).where((eb) => eb.or([
|
|
1837
|
+
eb("tenant_id", "=", tenantId),
|
|
1838
|
+
eb("tenant_id", "is", null)
|
|
1839
|
+
])).where((eb) => sortedOrgIds.length ? eb.or([
|
|
1840
|
+
eb("organization_id", "is", null),
|
|
1841
|
+
eb("organization_id", "in", sortedOrgIds)
|
|
1842
|
+
]) : eb("organization_id", "is", null)).execute();
|
|
1843
|
+
const winners = /* @__PURE__ */ new Map();
|
|
1844
|
+
for (const row of rows) {
|
|
1845
|
+
const key = typeof row.key === "string" ? row.key : String(row.key);
|
|
1846
|
+
const kind = typeof row.kind === "string" ? row.kind : null;
|
|
1847
|
+
if (!kind) continue;
|
|
1848
|
+
const specificity = row.organization_id != null ? 2 : row.tenant_id != null ? 1 : 0;
|
|
1849
|
+
const existing = winners.get(key);
|
|
1850
|
+
if (!existing || specificity >= existing.specificity) winners.set(key, { kind, specificity });
|
|
1851
|
+
}
|
|
1852
|
+
const result = /* @__PURE__ */ new Map();
|
|
1853
|
+
for (const [key, entry] of winners) result.set(key, entry.kind);
|
|
1854
|
+
if (this.customFieldKeysTtlMs > 0) {
|
|
1855
|
+
this.customFieldKindCache.set(cacheKey, { expiresAt: now + this.customFieldKeysTtlMs, value: new Map(result) });
|
|
1856
|
+
}
|
|
1857
|
+
return result;
|
|
1858
|
+
}
|
|
1859
|
+
async resolveAvailableCustomFieldKeys(entityIds, tenantId) {
|
|
1860
|
+
const defs = await this.resolveCustomFieldDefs(entityIds, tenantId);
|
|
1861
|
+
return Array.from(new Set(defs.map((def) => def.key)));
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Kind lookup for the `cf:`/`cf_` keys of an index document, so encrypted string-typed
|
|
1865
|
+
* custom fields keep their type on read instead of being `JSON.parse`d back into numbers
|
|
1866
|
+
* or booleans (issue #5968). Backed by the same TTL cache as the key lookup, so this adds
|
|
1867
|
+
* no round trip on a warm cache and fails open to an empty index on a cold-cache error.
|
|
1868
|
+
*
|
|
1869
|
+
* The index keeps every scope's definition per key rather than collapsing to one kind:
|
|
1870
|
+
* the caller resolves the winner against each row's own `organization_id`, the same scope
|
|
1871
|
+
* the row is decrypted with, so an organization's override never types another
|
|
1872
|
+
* organization's value.
|
|
1873
|
+
*
|
|
1874
|
+
* `entityIds` MUST arrive in `indexSources` order: `buildCfJsonExprSql` reads a `cf` value
|
|
1875
|
+
* with `coalesce(source0, source1, …)`, so a key defined by several sources has to be typed
|
|
1876
|
+
* by the same source the value came from — hence the priority-ordered merge rather than
|
|
1877
|
+
* whatever order the rows happen to come back in.
|
|
1878
|
+
*/
|
|
1879
|
+
async resolveCustomFieldKindIndex(entityIds, tenantId) {
|
|
1880
|
+
try {
|
|
1881
|
+
const defs = await this.resolveCustomFieldDefs(entityIds, tenantId);
|
|
1882
|
+
const byEntityId = /* @__PURE__ */ new Map();
|
|
1883
|
+
for (const def of defs) {
|
|
1884
|
+
const group = byEntityId.get(def.entityId) ?? [];
|
|
1885
|
+
group.push(def);
|
|
1886
|
+
byEntityId.set(def.entityId, group);
|
|
1887
|
+
}
|
|
1888
|
+
return mergeCustomFieldKindIndexes(
|
|
1889
|
+
entityIds.map((entityId) => buildCustomFieldKindIndex(byEntityId.get(entityId) ?? []))
|
|
1890
|
+
);
|
|
1891
|
+
} catch (err) {
|
|
1892
|
+
logger.warn("Failed to resolve custom field kinds", { entityIds, err });
|
|
1893
|
+
return emptyCustomFieldKindIndex();
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1764
1896
|
async entityHasActiveCustomFields(entityId, tenantId) {
|
|
1765
1897
|
try {
|
|
1766
1898
|
const keys = await this.resolveAvailableCustomFieldKeys([entityId], tenantId);
|
|
@@ -2135,6 +2267,14 @@ class HybridQueryEngine {
|
|
|
2135
2267
|
return q;
|
|
2136
2268
|
}
|
|
2137
2269
|
const col = column;
|
|
2270
|
+
if ((filter.op === "like" || filter.op === "ilike") && typeof filter.value === "string" && search?.enabled && search.encryptedFields != null) {
|
|
2271
|
+
const patterns = buildContainmentPatterns(filter.value);
|
|
2272
|
+
if (patterns.length > 1) {
|
|
2273
|
+
let next = q;
|
|
2274
|
+
for (const pattern of patterns) next = next.where(col, filter.op, pattern);
|
|
2275
|
+
return next;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2138
2278
|
switch (filter.op) {
|
|
2139
2279
|
case "eq":
|
|
2140
2280
|
return filter.value === null ? q.where(col, "is", null) : q.where(col, "=", filter.value);
|