@open-mercato/core 0.7.1-develop.7137.1.26575786c0 → 0.7.1-develop.7148.1.3076e5ccf7
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/backend/users/page.js +4 -4
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +17 -4
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/personAdapter.js +1 -1
- package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.js +16 -0
- package/dist/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.js.map +7 -0
- package/dist/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.js +16 -0
- package/dist/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.js.map +7 -0
- package/dist/modules/integrations/backend/integrations/[id]/page.js +16 -3
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/useIntegrationCredentialsFeatureAccess.js +42 -0
- package/dist/modules/integrations/backend/integrations/useIntegrationCredentialsFeatureAccess.js.map +7 -0
- package/dist/modules/messages/api/route.js +2 -1
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +11 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/lib/calculations.js +22 -0
- package/dist/modules/sales/lib/calculations.js.map +2 -2
- package/dist/modules/sales/lib/lineSnapshots.js +11 -1
- package/dist/modules/sales/lib/lineSnapshots.js.map +2 -2
- package/dist/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.js +16 -0
- package/dist/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/backend/users/page.tsx +4 -4
- package/src/modules/auth/i18n/de.json +4 -0
- package/src/modules/auth/i18n/en.json +4 -0
- package/src/modules/auth/i18n/es.json +4 -0
- package/src/modules/auth/i18n/ko.json +4 -0
- package/src/modules/auth/i18n/pl.json +4 -0
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +17 -4
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +2 -1
- package/src/modules/customers/i18n/de.json +5 -0
- package/src/modules/customers/i18n/en.json +5 -0
- package/src/modules/customers/i18n/es.json +5 -0
- package/src/modules/customers/i18n/ko.json +5 -0
- package/src/modules/customers/i18n/pl.json +5 -0
- package/src/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.ts +22 -0
- package/src/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.ts +20 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +23 -2
- package/src/modules/integrations/backend/integrations/useIntegrationCredentialsFeatureAccess.ts +50 -0
- package/src/modules/integrations/i18n/de.json +1 -0
- package/src/modules/integrations/i18n/en.json +1 -0
- package/src/modules/integrations/i18n/es.json +1 -0
- package/src/modules/integrations/i18n/ko.json +1 -0
- package/src/modules/integrations/i18n/pl.json +1 -0
- package/src/modules/messages/api/route.ts +2 -1
- package/src/modules/sales/commands/documents.ts +14 -0
- package/src/modules/sales/lib/calculations.ts +41 -0
- package/src/modules/sales/lib/lineSnapshots.ts +23 -0
- package/src/modules/sales/lib/types.ts +9 -0
- package/src/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.ts +21 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customers/components/linking/adapters/personAdapter.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Building2, CalendarDays, Link2, Mail, Phone } from 'lucide-react'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport type {\n LinkEntityAdapter,\n LinkEntityFilterOption,\n LinkEntityOption,\n LinkEntityRowContext,\n LinkEntitySearchPage,\n} from '../LinkEntityDialog'\n\nexport type PersonLinkSettings = {\n role: string | null\n isPrimary: boolean\n}\n\ntype PersonDetails = {\n contactEmails: string[]\n contactPhones: string[]\n linkedCompanies: Array<{ id: string; name: string; isPrimary?: boolean }>\n recentActivity: { title: string; occurredAt: string | null; summary?: string | null } | null\n}\n\ntype PersonAdapterOptions = {\n dialogTitle: string\n dialogSubtitle?: string\n sectionLabel?: string\n searchPlaceholder: string\n searchEmptyHint: string\n selectedEmptyHint: string\n confirmButtonLabel: string\n excludeLinkedCompanyId?: string\n excludeIds?: string[]\n defaultAvatarIcon?: React.ReactNode\n pageSize?: number\n addNew?: LinkEntityAdapter<PersonDetails, PersonLinkSettings>['addNew']\n showLinkSettings?: boolean\n roleOptions?: Array<{ id: string; label: string }>\n headerIcon?: React.ReactNode\n}\n\nconst DEFAULT_PAGE_SIZE = 20\n\ntype RecentActivityPayload = {\n title?: string | null\n occurredAt?: string | null\n summary?: string | null\n description?: string | null\n} | null\n\nfunction normalizePersonRecord(record: Record<string, unknown>): LinkEntityOption | null {\n const id = typeof record.id === 'string' ? record.id : null\n if (!id) return null\n const displayName =\n typeof record.displayName === 'string' && record.displayName.trim().length\n ? record.displayName.trim()\n : typeof record.display_name === 'string' && record.display_name.trim().length\n ? record.display_name.trim()\n : null\n if (!displayName) return null\n const subtitleSource =\n typeof record.primaryEmail === 'string' && record.primaryEmail.trim().length\n ? record.primaryEmail.trim()\n : typeof record.primary_email === 'string' && record.primary_email.trim().length\n ? record.primary_email.trim()\n : typeof record.jobTitle === 'string' && record.jobTitle.trim().length\n ? record.jobTitle.trim()\n : typeof record.job_title === 'string' && record.job_title.trim().length\n ? record.job_title.trim()\n : null\n const role =\n typeof record.roleValue === 'string' && record.roleValue.trim().length\n ? record.roleValue.trim()\n : typeof record.role === 'string' && record.role.trim().length\n ? record.role.trim()\n : null\n const jobTitle =\n typeof record.jobTitle === 'string'\n ? record.jobTitle\n : typeof record.job_title === 'string'\n ? record.job_title\n : null\n const alreadyLinked =\n record.alreadyLinked === true ||\n record.already_linked === true ||\n record.excludedByLink === true\n return {\n id,\n label: displayName,\n subtitle: subtitleSource,\n meta: {\n role,\n jobTitle,\n alreadyLinked,\n primaryEmail:\n typeof record.primaryEmail === 'string'\n ? record.primaryEmail\n : typeof record.primary_email === 'string'\n ? record.primary_email\n : null,\n primaryPhone:\n typeof record.primaryPhone === 'string'\n ? record.primaryPhone\n : typeof record.primary_phone === 'string'\n ? record.primary_phone\n : null,\n },\n }\n}\n\nasync function fetchPersonDetails(id: string): Promise<PersonDetails> {\n try {\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/customers/people/${encodeURIComponent(id)}`,\n )\n const emails: string[] = []\n const phones: string[] = []\n if (typeof payload.primaryEmail === 'string' && payload.primaryEmail.trim().length) {\n emails.push(payload.primaryEmail.trim())\n } else if (typeof payload.primary_email === 'string' && payload.primary_email.trim().length) {\n emails.push(payload.primary_email.trim())\n }\n if (typeof payload.primaryPhone === 'string' && payload.primaryPhone.trim().length) {\n phones.push(payload.primaryPhone.trim())\n } else if (typeof payload.primary_phone === 'string' && payload.primary_phone.trim().length) {\n phones.push(payload.primary_phone.trim())\n }\n const companies: Array<{ id: string; name: string; isPrimary?: boolean }> = []\n const rawCompanies = Array.isArray(payload.companies) ? payload.companies : []\n rawCompanies.forEach((entry) => {\n if (!entry || typeof entry !== 'object') return\n const record = entry as Record<string, unknown>\n const companyId = typeof record.id === 'string' ? record.id : null\n const name =\n typeof record.displayName === 'string'\n ? record.displayName\n : typeof record.display_name === 'string'\n ? record.display_name\n : null\n if (!companyId || !name) return\n companies.push({ id: companyId, name, isPrimary: Boolean(record.isPrimary ?? record.is_primary) })\n })\n let recentActivity: PersonDetails['recentActivity'] = null\n const activities = Array.isArray(payload.interactions)\n ? (payload.interactions as RecentActivityPayload[])\n : Array.isArray(payload.activities)\n ? (payload.activities as RecentActivityPayload[])\n : Array.isArray((payload.plannedActivitiesPreview as unknown[]))\n ? (payload.plannedActivitiesPreview as RecentActivityPayload[])\n : []\n if (activities.length > 0) {\n const entry = activities[0]\n if (entry && typeof entry === 'object') {\n recentActivity = {\n title: (entry.title ?? '').toString() || 'Interaction',\n occurredAt: typeof entry.occurredAt === 'string' ? entry.occurredAt : null,\n summary:\n typeof entry.summary === 'string'\n ? entry.summary\n : typeof entry.description === 'string'\n ? entry.description\n : null,\n }\n }\n }\n return { contactEmails: emails, contactPhones: phones, linkedCompanies: companies, recentActivity }\n } catch {\n return { contactEmails: [], contactPhones: [], linkedCompanies: [], recentActivity: null }\n }\n}\n\nfunction formatRelative(dateString: string | null): string {\n if (!dateString) return ''\n try {\n const date = new Date(dateString)\n const now = Date.now()\n const diffMs = now - date.getTime()\n const diffDays = Math.floor(diffMs / 86_400_000)\n if (diffDays <= 0) return 'today'\n if (diffDays === 1) return 'yesterday'\n if (diffDays < 30) return `${diffDays} days ago`\n if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`\n return `${Math.floor(diffDays / 365)} years ago`\n } catch {\n return ''\n }\n}\n\nexport function createPersonLinkAdapter(\n options: PersonAdapterOptions,\n): LinkEntityAdapter<PersonDetails, PersonLinkSettings> {\n const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE\n const roleOptions = options.roleOptions ?? []\n\n const roleKeywords = new Map<string, string[]>(\n roleOptions.map((role) => [\n role.id,\n // build keyword list: id (as words), label words, lowercased\n [\n role.id.replace(/_/g, ' ').toLowerCase(),\n role.label.toLowerCase(),\n ],\n ]),\n )\n\n const filters:\n | {\n options: LinkEntityFilterOption[]\n defaultId?: string\n clientFilter?: (option: LinkEntityOption, filterId: string) => boolean\n }\n | undefined =\n roleOptions.length > 0\n ? {\n options: [\n { id: 'all', label: 'All' },\n ...roleOptions.map((role) => ({ id: role.id, label: role.label })),\n ],\n defaultId: 'all',\n clientFilter: (option, filterId) => {\n if (!filterId || filterId === 'all') return true\n const meta = (option.meta ?? {}) as {\n role?: string | null\n jobTitle?: string | null\n }\n const keywords = roleKeywords.get(filterId) ?? []\n const fields = [\n meta.role?.toLowerCase() ?? '',\n meta.jobTitle?.toLowerCase() ?? '',\n option.subtitle?.toLowerCase() ?? '',\n ]\n return keywords.some((keyword) =>\n fields.some((field) => field.includes(keyword)),\n )\n },\n }\n : undefined\n\n const searchPage = async (\n query: string,\n page: number,\n _filterId?: string,\n ): Promise<LinkEntitySearchPage> => {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'name',\n sortDir: 'asc',\n })\n if (query.trim().length > 0) {\n params.set('search', query.trim())\n }\n if (options.excludeLinkedCompanyId) {\n params.set('excludeLinkedCompanyId', options.excludeLinkedCompanyId)\n }\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/customers/people?${params.toString()}`,\n )\n const rawItems = Array.isArray(payload.items) ? payload.items : []\n const exclude = new Set(options.excludeIds ?? [])\n const items = rawItems\n .map((candidate) =>\n candidate && typeof candidate === 'object'\n ? normalizePersonRecord(candidate as Record<string, unknown>)\n : null,\n )\n .filter((entry): entry is LinkEntityOption => entry !== null && !exclude.has(entry.id))\n const totalPages = typeof payload.totalPages === 'number' ? payload.totalPages : 1\n const total = typeof payload.total === 'number' ? payload.total : undefined\n return { items, totalPages, total }\n }\n\n const fetchByIds = async (ids: string[]): Promise<LinkEntityOption[]> => {\n const uniqueIds = Array.from(new Set(ids.map((value) => value.trim()).filter(Boolean)))\n if (!uniqueIds.length) return []\n const params = new URLSearchParams({\n ids: uniqueIds.join(','),\n pageSize: String(Math.max(uniqueIds.length, 1)),\n })\n try {\n const payload = await readApiResultOrThrow<{ items?: Record<string, unknown>[] }>(\n `/api/customers/people?${params.toString()}`,\n )\n const rawItems = Array.isArray(payload.items) ? payload.items : []\n const byId = new Map<string, LinkEntityOption>()\n rawItems.forEach((record) => {\n if (!record || typeof record !== 'object') return\n const option = normalizePersonRecord(record as Record<string, unknown>)\n if (option) byId.set(option.id, option)\n })\n return uniqueIds.map((id) => byId.get(id) ?? { id, label: id, subtitle: null })\n } catch {\n return uniqueIds.map((id) => ({ id, label: id, subtitle: null }))\n }\n }\n\n const renderRow = (option: LinkEntityOption, ctx: LinkEntityRowContext): React.ReactNode => {\n const meta = (option.meta ?? {}) as {\n role?: string | null\n jobTitle?: string | null\n primaryEmail?: string | null\n }\n const rolePill = meta.role ?? meta.jobTitle ?? null\n return (\n <>\n <Avatar label={option.label} variant=\"monochrome\" size=\"md\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex min-w-0 flex-wrap items-center gap-2\">\n <span className=\"truncate text-sm font-semibold text-foreground\">\n {option.label}\n </span>\n {rolePill ? (\n <span className=\"inline-flex items-center rounded-sm bg-muted px-1.5 py-0.5 text-xs font-semibold text-muted-foreground\">\n {rolePill}\n </span>\n ) : null}\n </div>\n {option.subtitle ? (\n <div className=\"mt-0.5 truncate text-xs text-muted-foreground\">\n {option.subtitle}\n </div>\n ) : null}\n </div>\n <span\n role=\"checkbox\"\n aria-checked={ctx.selected}\n aria-label={`Select ${option.label}`}\n className={cn(\n 'inline-flex size-5 shrink-0 items-center justify-center rounded-full border',\n ctx.selected\n ? 'border-foreground bg-foreground text-background'\n : 'border-border bg-background',\n )}\n >\n {ctx.selected ? (\n <svg viewBox=\"0 0 12 12\" className=\"size-3\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M2.5 6.5l2.5 2.5 4.5-5\" />\n </svg>\n ) : null}\n </span>\n </>\n )\n }\n\n const renderPreview = (\n option: LinkEntityOption,\n details?: PersonDetails,\n ): React.ReactNode => {\n const meta = (option.meta ?? {}) as { jobTitle?: string | null; primaryPhone?: string | null; primaryEmail?: string | null }\n const email = details?.contactEmails?.[0] ?? meta.primaryEmail ?? null\n const phone = details?.contactPhones?.[0] ?? meta.primaryPhone ?? null\n const companies = details?.linkedCompanies ?? []\n const activity = details?.recentActivity ?? null\n const subtitle = option.subtitle ?? meta.jobTitle ?? null\n return (\n <div className=\"flex flex-col gap-3.5 rounded-xl border border-border/70 bg-card p-4\">\n <div className=\"flex items-center gap-3\">\n <Avatar label={option.label} variant=\"monochrome\" size=\"lg\" />\n <div className=\"min-w-0\">\n <div className=\"truncate text-base font-bold text-foreground\">{option.label}</div>\n {subtitle ? (\n <div className=\"mt-0.5 truncate text-xs text-muted-foreground\">{subtitle}</div>\n ) : null}\n </div>\n </div>\n\n {email || phone ? (\n <>\n <div className=\"h-px w-full bg-border/70\" />\n <div className=\"text-overline font-semibold uppercase tracking-wide text-muted-foreground\">\n Contact\n </div>\n {email ? (\n <div className=\"flex items-center gap-2.5 text-xs text-foreground\">\n <Mail className=\"size-3.5 text-muted-foreground\" />\n <span className=\"truncate\">{email}</span>\n </div>\n ) : null}\n {phone ? (\n <div className=\"flex items-center gap-2.5 text-xs text-foreground\">\n <Phone className=\"size-3.5 text-muted-foreground\" />\n <span className=\"truncate\">{phone}</span>\n </div>\n ) : null}\n </>\n ) : null}\n\n {companies.length > 0 ? (\n <>\n <div className=\"h-px w-full bg-border/70\" />\n <div className=\"text-overline font-semibold uppercase tracking-wide text-muted-foreground\">\n Linked companies ({companies.length})\n </div>\n <div className=\"flex flex-col gap-1.5\">\n {companies.map((company) => (\n <div\n key={company.id}\n className=\"flex items-center gap-2 rounded-md bg-muted/70 px-2.5 py-1.5 text-xs font-semibold text-foreground\"\n >\n <Building2 className=\"size-3 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate\">{company.name}</span>\n {company.isPrimary ? (\n <span className=\"inline-flex items-center rounded-full bg-primary px-2 py-0.5 text-xs font-bold text-primary-foreground\">\n PRIMARY\n </span>\n ) : null}\n </div>\n ))}\n </div>\n </>\n ) : null}\n\n {activity ? (\n <>\n <div className=\"h-px w-full bg-border/70\" />\n <div className=\"text-overline font-semibold uppercase tracking-wide text-muted-foreground\">\n Recent activity\n </div>\n <div className=\"flex items-start gap-2\">\n <CalendarDays className=\"mt-0.5 size-3 text-muted-foreground\" />\n <div className=\"min-w-0\">\n <div className=\"truncate text-xs font-semibold text-foreground\">\n {activity.title}\n {activity.occurredAt ? (\n <span className=\"text-muted-foreground\"> \u00B7 {formatRelative(activity.occurredAt)}</span>\n ) : null}\n </div>\n {activity.summary ? (\n <div className=\"mt-0.5 truncate text-xs text-muted-foreground\">\n {activity.summary}\n </div>\n ) : null}\n </div>\n </div>\n </>\n ) : null}\n </div>\n )\n }\n\n const renderLinkSettings = options.showLinkSettings\n ? (\n settings: PersonLinkSettings,\n onChange: (next: PersonLinkSettings) => void,\n ): React.ReactNode => {\n const currentRole = settings.role ?? ''\n return (\n <div className=\"flex flex-col gap-3\">\n {roleOptions.length > 0 ? (\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-semibold text-muted-foreground\">Role at company</span>\n <select\n value={currentRole}\n onChange={(event) => onChange({ ...settings, role: event.target.value || null })}\n className=\"h-9 rounded-md border border-border bg-card px-3 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring/40\"\n >\n <option value=\"\">\u2014</option>\n {roleOptions.map((role) => (\n <option key={role.id} value={role.id}>\n {role.label}\n </option>\n ))}\n </select>\n </label>\n ) : null}\n <label className=\"flex cursor-pointer items-start gap-2\">\n <input\n type=\"checkbox\"\n checked={Boolean(settings.isPrimary)}\n onChange={(event) => onChange({ ...settings, isPrimary: event.target.checked })}\n className=\"mt-0.5 size-4 shrink-0 rounded-sm border border-border accent-foreground\"\n />\n <span className=\"min-w-0\">\n <span className=\"block text-xs font-semibold text-foreground\">\n Set as PRIMARY contact\n </span>\n <span className=\"mt-0.5 block text-xs text-muted-foreground\">\n Will appear at the top of company contacts\n </span>\n </span>\n </label>\n </div>\n )\n }\n : undefined\n\n return {\n kind: 'person',\n dialogTitle: options.dialogTitle,\n dialogSubtitle: options.dialogSubtitle,\n sectionLabel: options.sectionLabel ?? 'MATCHING CONTACTS',\n searchPlaceholder: options.searchPlaceholder,\n searchEmptyHint: options.searchEmptyHint,\n selectedEmptyHint: options.selectedEmptyHint,\n confirmButtonLabel: options.confirmButtonLabel,\n defaultAvatarIcon: options.defaultAvatarIcon,\n headerIcon: options.headerIcon ?? <Link2 className=\"size-5\" />,\n filters,\n renderRow,\n renderPreview,\n renderLinkSettings,\n initialLinkSettings: options.showLinkSettings ? { role: null, isPrimary: false } : undefined,\n fetchDetails: fetchPersonDetails,\n searchPage,\n fetchByIds,\n addNew: options.addNew,\n }\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Building2, CalendarDays, Link2, Mail, Phone } from 'lucide-react'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Avatar } from '@open-mercato/ui/primitives/avatar'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport type {\n LinkEntityAdapter,\n LinkEntityFilterOption,\n LinkEntityOption,\n LinkEntityRowContext,\n LinkEntitySearchPage,\n} from '../LinkEntityDialog'\n\nexport type PersonLinkSettings = {\n role: string | null\n isPrimary: boolean\n}\n\ntype PersonDetails = {\n contactEmails: string[]\n contactPhones: string[]\n linkedCompanies: Array<{ id: string; name: string; isPrimary?: boolean }>\n recentActivity: { title: string; occurredAt: string | null; summary?: string | null } | null\n}\n\ntype PersonAdapterOptions = {\n dialogTitle: string\n dialogSubtitle?: string\n sectionLabel?: string\n searchPlaceholder: string\n searchEmptyHint: string\n selectedEmptyHint: string\n confirmButtonLabel: string\n excludeLinkedCompanyId?: string\n excludeIds?: string[]\n defaultAvatarIcon?: React.ReactNode\n pageSize?: number\n addNew?: LinkEntityAdapter<PersonDetails, PersonLinkSettings>['addNew']\n showLinkSettings?: boolean\n roleOptions?: Array<{ id: string; label: string }>\n allFilterLabel?: string\n headerIcon?: React.ReactNode\n}\n\nconst DEFAULT_PAGE_SIZE = 20\n\ntype RecentActivityPayload = {\n title?: string | null\n occurredAt?: string | null\n summary?: string | null\n description?: string | null\n} | null\n\nfunction normalizePersonRecord(record: Record<string, unknown>): LinkEntityOption | null {\n const id = typeof record.id === 'string' ? record.id : null\n if (!id) return null\n const displayName =\n typeof record.displayName === 'string' && record.displayName.trim().length\n ? record.displayName.trim()\n : typeof record.display_name === 'string' && record.display_name.trim().length\n ? record.display_name.trim()\n : null\n if (!displayName) return null\n const subtitleSource =\n typeof record.primaryEmail === 'string' && record.primaryEmail.trim().length\n ? record.primaryEmail.trim()\n : typeof record.primary_email === 'string' && record.primary_email.trim().length\n ? record.primary_email.trim()\n : typeof record.jobTitle === 'string' && record.jobTitle.trim().length\n ? record.jobTitle.trim()\n : typeof record.job_title === 'string' && record.job_title.trim().length\n ? record.job_title.trim()\n : null\n const role =\n typeof record.roleValue === 'string' && record.roleValue.trim().length\n ? record.roleValue.trim()\n : typeof record.role === 'string' && record.role.trim().length\n ? record.role.trim()\n : null\n const jobTitle =\n typeof record.jobTitle === 'string'\n ? record.jobTitle\n : typeof record.job_title === 'string'\n ? record.job_title\n : null\n const alreadyLinked =\n record.alreadyLinked === true ||\n record.already_linked === true ||\n record.excludedByLink === true\n return {\n id,\n label: displayName,\n subtitle: subtitleSource,\n meta: {\n role,\n jobTitle,\n alreadyLinked,\n primaryEmail:\n typeof record.primaryEmail === 'string'\n ? record.primaryEmail\n : typeof record.primary_email === 'string'\n ? record.primary_email\n : null,\n primaryPhone:\n typeof record.primaryPhone === 'string'\n ? record.primaryPhone\n : typeof record.primary_phone === 'string'\n ? record.primary_phone\n : null,\n },\n }\n}\n\nasync function fetchPersonDetails(id: string): Promise<PersonDetails> {\n try {\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/customers/people/${encodeURIComponent(id)}`,\n )\n const emails: string[] = []\n const phones: string[] = []\n if (typeof payload.primaryEmail === 'string' && payload.primaryEmail.trim().length) {\n emails.push(payload.primaryEmail.trim())\n } else if (typeof payload.primary_email === 'string' && payload.primary_email.trim().length) {\n emails.push(payload.primary_email.trim())\n }\n if (typeof payload.primaryPhone === 'string' && payload.primaryPhone.trim().length) {\n phones.push(payload.primaryPhone.trim())\n } else if (typeof payload.primary_phone === 'string' && payload.primary_phone.trim().length) {\n phones.push(payload.primary_phone.trim())\n }\n const companies: Array<{ id: string; name: string; isPrimary?: boolean }> = []\n const rawCompanies = Array.isArray(payload.companies) ? payload.companies : []\n rawCompanies.forEach((entry) => {\n if (!entry || typeof entry !== 'object') return\n const record = entry as Record<string, unknown>\n const companyId = typeof record.id === 'string' ? record.id : null\n const name =\n typeof record.displayName === 'string'\n ? record.displayName\n : typeof record.display_name === 'string'\n ? record.display_name\n : null\n if (!companyId || !name) return\n companies.push({ id: companyId, name, isPrimary: Boolean(record.isPrimary ?? record.is_primary) })\n })\n let recentActivity: PersonDetails['recentActivity'] = null\n const activities = Array.isArray(payload.interactions)\n ? (payload.interactions as RecentActivityPayload[])\n : Array.isArray(payload.activities)\n ? (payload.activities as RecentActivityPayload[])\n : Array.isArray((payload.plannedActivitiesPreview as unknown[]))\n ? (payload.plannedActivitiesPreview as RecentActivityPayload[])\n : []\n if (activities.length > 0) {\n const entry = activities[0]\n if (entry && typeof entry === 'object') {\n recentActivity = {\n title: (entry.title ?? '').toString() || 'Interaction',\n occurredAt: typeof entry.occurredAt === 'string' ? entry.occurredAt : null,\n summary:\n typeof entry.summary === 'string'\n ? entry.summary\n : typeof entry.description === 'string'\n ? entry.description\n : null,\n }\n }\n }\n return { contactEmails: emails, contactPhones: phones, linkedCompanies: companies, recentActivity }\n } catch {\n return { contactEmails: [], contactPhones: [], linkedCompanies: [], recentActivity: null }\n }\n}\n\nfunction formatRelative(dateString: string | null): string {\n if (!dateString) return ''\n try {\n const date = new Date(dateString)\n const now = Date.now()\n const diffMs = now - date.getTime()\n const diffDays = Math.floor(diffMs / 86_400_000)\n if (diffDays <= 0) return 'today'\n if (diffDays === 1) return 'yesterday'\n if (diffDays < 30) return `${diffDays} days ago`\n if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`\n return `${Math.floor(diffDays / 365)} years ago`\n } catch {\n return ''\n }\n}\n\nexport function createPersonLinkAdapter(\n options: PersonAdapterOptions,\n): LinkEntityAdapter<PersonDetails, PersonLinkSettings> {\n const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE\n const roleOptions = options.roleOptions ?? []\n\n const roleKeywords = new Map<string, string[]>(\n roleOptions.map((role) => [\n role.id,\n // build keyword list: id (as words), label words, lowercased\n [\n role.id.replace(/_/g, ' ').toLowerCase(),\n role.label.toLowerCase(),\n ],\n ]),\n )\n\n const filters:\n | {\n options: LinkEntityFilterOption[]\n defaultId?: string\n clientFilter?: (option: LinkEntityOption, filterId: string) => boolean\n }\n | undefined =\n roleOptions.length > 0\n ? {\n options: [\n { id: 'all', label: options.allFilterLabel ?? 'All' },\n ...roleOptions.map((role) => ({ id: role.id, label: role.label })),\n ],\n defaultId: 'all',\n clientFilter: (option, filterId) => {\n if (!filterId || filterId === 'all') return true\n const meta = (option.meta ?? {}) as {\n role?: string | null\n jobTitle?: string | null\n }\n const keywords = roleKeywords.get(filterId) ?? []\n const fields = [\n meta.role?.toLowerCase() ?? '',\n meta.jobTitle?.toLowerCase() ?? '',\n option.subtitle?.toLowerCase() ?? '',\n ]\n return keywords.some((keyword) =>\n fields.some((field) => field.includes(keyword)),\n )\n },\n }\n : undefined\n\n const searchPage = async (\n query: string,\n page: number,\n _filterId?: string,\n ): Promise<LinkEntitySearchPage> => {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'name',\n sortDir: 'asc',\n })\n if (query.trim().length > 0) {\n params.set('search', query.trim())\n }\n if (options.excludeLinkedCompanyId) {\n params.set('excludeLinkedCompanyId', options.excludeLinkedCompanyId)\n }\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/customers/people?${params.toString()}`,\n )\n const rawItems = Array.isArray(payload.items) ? payload.items : []\n const exclude = new Set(options.excludeIds ?? [])\n const items = rawItems\n .map((candidate) =>\n candidate && typeof candidate === 'object'\n ? normalizePersonRecord(candidate as Record<string, unknown>)\n : null,\n )\n .filter((entry): entry is LinkEntityOption => entry !== null && !exclude.has(entry.id))\n const totalPages = typeof payload.totalPages === 'number' ? payload.totalPages : 1\n const total = typeof payload.total === 'number' ? payload.total : undefined\n return { items, totalPages, total }\n }\n\n const fetchByIds = async (ids: string[]): Promise<LinkEntityOption[]> => {\n const uniqueIds = Array.from(new Set(ids.map((value) => value.trim()).filter(Boolean)))\n if (!uniqueIds.length) return []\n const params = new URLSearchParams({\n ids: uniqueIds.join(','),\n pageSize: String(Math.max(uniqueIds.length, 1)),\n })\n try {\n const payload = await readApiResultOrThrow<{ items?: Record<string, unknown>[] }>(\n `/api/customers/people?${params.toString()}`,\n )\n const rawItems = Array.isArray(payload.items) ? payload.items : []\n const byId = new Map<string, LinkEntityOption>()\n rawItems.forEach((record) => {\n if (!record || typeof record !== 'object') return\n const option = normalizePersonRecord(record as Record<string, unknown>)\n if (option) byId.set(option.id, option)\n })\n return uniqueIds.map((id) => byId.get(id) ?? { id, label: id, subtitle: null })\n } catch {\n return uniqueIds.map((id) => ({ id, label: id, subtitle: null }))\n }\n }\n\n const renderRow = (option: LinkEntityOption, ctx: LinkEntityRowContext): React.ReactNode => {\n const meta = (option.meta ?? {}) as {\n role?: string | null\n jobTitle?: string | null\n primaryEmail?: string | null\n }\n const rolePill = meta.role ?? meta.jobTitle ?? null\n return (\n <>\n <Avatar label={option.label} variant=\"monochrome\" size=\"md\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex min-w-0 flex-wrap items-center gap-2\">\n <span className=\"truncate text-sm font-semibold text-foreground\">\n {option.label}\n </span>\n {rolePill ? (\n <span className=\"inline-flex items-center rounded-sm bg-muted px-1.5 py-0.5 text-xs font-semibold text-muted-foreground\">\n {rolePill}\n </span>\n ) : null}\n </div>\n {option.subtitle ? (\n <div className=\"mt-0.5 truncate text-xs text-muted-foreground\">\n {option.subtitle}\n </div>\n ) : null}\n </div>\n <span\n role=\"checkbox\"\n aria-checked={ctx.selected}\n aria-label={`Select ${option.label}`}\n className={cn(\n 'inline-flex size-5 shrink-0 items-center justify-center rounded-full border',\n ctx.selected\n ? 'border-foreground bg-foreground text-background'\n : 'border-border bg-background',\n )}\n >\n {ctx.selected ? (\n <svg viewBox=\"0 0 12 12\" className=\"size-3\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5}>\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M2.5 6.5l2.5 2.5 4.5-5\" />\n </svg>\n ) : null}\n </span>\n </>\n )\n }\n\n const renderPreview = (\n option: LinkEntityOption,\n details?: PersonDetails,\n ): React.ReactNode => {\n const meta = (option.meta ?? {}) as { jobTitle?: string | null; primaryPhone?: string | null; primaryEmail?: string | null }\n const email = details?.contactEmails?.[0] ?? meta.primaryEmail ?? null\n const phone = details?.contactPhones?.[0] ?? meta.primaryPhone ?? null\n const companies = details?.linkedCompanies ?? []\n const activity = details?.recentActivity ?? null\n const subtitle = option.subtitle ?? meta.jobTitle ?? null\n return (\n <div className=\"flex flex-col gap-3.5 rounded-xl border border-border/70 bg-card p-4\">\n <div className=\"flex items-center gap-3\">\n <Avatar label={option.label} variant=\"monochrome\" size=\"lg\" />\n <div className=\"min-w-0\">\n <div className=\"truncate text-base font-bold text-foreground\">{option.label}</div>\n {subtitle ? (\n <div className=\"mt-0.5 truncate text-xs text-muted-foreground\">{subtitle}</div>\n ) : null}\n </div>\n </div>\n\n {email || phone ? (\n <>\n <div className=\"h-px w-full bg-border/70\" />\n <div className=\"text-overline font-semibold uppercase tracking-wide text-muted-foreground\">\n Contact\n </div>\n {email ? (\n <div className=\"flex items-center gap-2.5 text-xs text-foreground\">\n <Mail className=\"size-3.5 text-muted-foreground\" />\n <span className=\"truncate\">{email}</span>\n </div>\n ) : null}\n {phone ? (\n <div className=\"flex items-center gap-2.5 text-xs text-foreground\">\n <Phone className=\"size-3.5 text-muted-foreground\" />\n <span className=\"truncate\">{phone}</span>\n </div>\n ) : null}\n </>\n ) : null}\n\n {companies.length > 0 ? (\n <>\n <div className=\"h-px w-full bg-border/70\" />\n <div className=\"text-overline font-semibold uppercase tracking-wide text-muted-foreground\">\n Linked companies ({companies.length})\n </div>\n <div className=\"flex flex-col gap-1.5\">\n {companies.map((company) => (\n <div\n key={company.id}\n className=\"flex items-center gap-2 rounded-md bg-muted/70 px-2.5 py-1.5 text-xs font-semibold text-foreground\"\n >\n <Building2 className=\"size-3 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate\">{company.name}</span>\n {company.isPrimary ? (\n <span className=\"inline-flex items-center rounded-full bg-primary px-2 py-0.5 text-xs font-bold text-primary-foreground\">\n PRIMARY\n </span>\n ) : null}\n </div>\n ))}\n </div>\n </>\n ) : null}\n\n {activity ? (\n <>\n <div className=\"h-px w-full bg-border/70\" />\n <div className=\"text-overline font-semibold uppercase tracking-wide text-muted-foreground\">\n Recent activity\n </div>\n <div className=\"flex items-start gap-2\">\n <CalendarDays className=\"mt-0.5 size-3 text-muted-foreground\" />\n <div className=\"min-w-0\">\n <div className=\"truncate text-xs font-semibold text-foreground\">\n {activity.title}\n {activity.occurredAt ? (\n <span className=\"text-muted-foreground\"> \u00B7 {formatRelative(activity.occurredAt)}</span>\n ) : null}\n </div>\n {activity.summary ? (\n <div className=\"mt-0.5 truncate text-xs text-muted-foreground\">\n {activity.summary}\n </div>\n ) : null}\n </div>\n </div>\n </>\n ) : null}\n </div>\n )\n }\n\n const renderLinkSettings = options.showLinkSettings\n ? (\n settings: PersonLinkSettings,\n onChange: (next: PersonLinkSettings) => void,\n ): React.ReactNode => {\n const currentRole = settings.role ?? ''\n return (\n <div className=\"flex flex-col gap-3\">\n {roleOptions.length > 0 ? (\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-semibold text-muted-foreground\">Role at company</span>\n <select\n value={currentRole}\n onChange={(event) => onChange({ ...settings, role: event.target.value || null })}\n className=\"h-9 rounded-md border border-border bg-card px-3 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring/40\"\n >\n <option value=\"\">\u2014</option>\n {roleOptions.map((role) => (\n <option key={role.id} value={role.id}>\n {role.label}\n </option>\n ))}\n </select>\n </label>\n ) : null}\n <label className=\"flex cursor-pointer items-start gap-2\">\n <input\n type=\"checkbox\"\n checked={Boolean(settings.isPrimary)}\n onChange={(event) => onChange({ ...settings, isPrimary: event.target.checked })}\n className=\"mt-0.5 size-4 shrink-0 rounded-sm border border-border accent-foreground\"\n />\n <span className=\"min-w-0\">\n <span className=\"block text-xs font-semibold text-foreground\">\n Set as PRIMARY contact\n </span>\n <span className=\"mt-0.5 block text-xs text-muted-foreground\">\n Will appear at the top of company contacts\n </span>\n </span>\n </label>\n </div>\n )\n }\n : undefined\n\n return {\n kind: 'person',\n dialogTitle: options.dialogTitle,\n dialogSubtitle: options.dialogSubtitle,\n sectionLabel: options.sectionLabel ?? 'MATCHING CONTACTS',\n searchPlaceholder: options.searchPlaceholder,\n searchEmptyHint: options.searchEmptyHint,\n selectedEmptyHint: options.selectedEmptyHint,\n confirmButtonLabel: options.confirmButtonLabel,\n defaultAvatarIcon: options.defaultAvatarIcon,\n headerIcon: options.headerIcon ?? <Link2 className=\"size-5\" />,\n filters,\n renderRow,\n renderPreview,\n renderLinkSettings,\n initialLinkSettings: options.showLinkSettings ? { role: null, isPrimary: false } : undefined,\n fetchDetails: fetchPersonDetails,\n searchPage,\n fetchByIds,\n addNew: options.addNew,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
import { declareQueryIndexReindex } from "@open-mercato/shared/lib/query/migration-reindex";
|
|
3
|
+
const queryIndexReindexEntityTypes = declareQueryIndexReindex([
|
|
4
|
+
"customers:customer_dictionary_entry"
|
|
5
|
+
]);
|
|
6
|
+
class Migration20260901120000_reindex_pipeline_stage_colors extends Migration {
|
|
7
|
+
up() {
|
|
8
|
+
}
|
|
9
|
+
down() {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
Migration20260901120000_reindex_pipeline_stage_colors,
|
|
14
|
+
queryIndexReindexEntityTypes
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=Migration20260901120000_reindex_pipeline_stage_colors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\nimport { declareQueryIndexReindex } from '@open-mercato/shared/lib/query/migration-reindex';\n\n/**\n * Catch-up reindex for `Migration20260519120000_pipeline_stage_color_tones`, which rewrote\n * `customer_dictionary_entries.color` in raw SQL and never notified the query index. On every\n * install that already applied it, `entity_indexes.doc` for `customers:customer_dictionary_entry`\n * still holds the legacy hex chip (`#facc15`, `#a855f7`, \u2026) instead of the semantic tone, and so\n * do the `search_tokens` rows derived from that document. Both migrations are forward-only, so a\n * follow-up migration is the only route to repair existing installs.\n *\n * This migration executes no SQL \u2014 the declaration below is its entire payload.\n */\nexport const queryIndexReindexEntityTypes = declareQueryIndexReindex([\n 'customers:customer_dictionary_entry',\n]);\n\nexport class Migration20260901120000_reindex_pipeline_stage_colors extends Migration {\n override up(): void | Promise<void> {}\n\n override down(): void | Promise<void> {}\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAC1B,SAAS,gCAAgC;AAYlC,MAAM,+BAA+B,yBAAyB;AAAA,EACnE;AACF,CAAC;AAEM,MAAM,8DAA8D,UAAU;AAAA,EAC1E,KAA2B;AAAA,EAAC;AAAA,EAE5B,OAA6B;AAAA,EAAC;AACzC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
import { declareQueryIndexReindex } from "@open-mercato/shared/lib/query/migration-reindex";
|
|
3
|
+
const queryIndexReindexEntityTypes = declareQueryIndexReindex([
|
|
4
|
+
"dictionaries:dictionary_entry"
|
|
5
|
+
]);
|
|
6
|
+
class Migration20260901120000_reindex_dictionary_entries extends Migration {
|
|
7
|
+
up() {
|
|
8
|
+
}
|
|
9
|
+
down() {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
Migration20260901120000_reindex_dictionary_entries,
|
|
14
|
+
queryIndexReindexEntityTypes
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=Migration20260901120000_reindex_dictionary_entries.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\nimport { declareQueryIndexReindex } from '@open-mercato/shared/lib/query/migration-reindex';\n\n/**\n * Catch-up reindex for `Migration20260410171544`, which added `position` and `is_default` to\n * `dictionary_entries` and backfilled them in raw SQL. Documents written before that migration\n * were never rebuilt, so `entity_indexes.doc` for `dictionaries:dictionary_entry` is missing both\n * keys entirely rather than holding a stale value \u2014 the same absent emit, a different symptom.\n *\n * This migration executes no SQL \u2014 the declaration below is its entire payload.\n */\nexport const queryIndexReindexEntityTypes = declareQueryIndexReindex([\n 'dictionaries:dictionary_entry',\n]);\n\nexport class Migration20260901120000_reindex_dictionary_entries extends Migration {\n override up(): void | Promise<void> {}\n\n override down(): void | Promise<void> {}\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAC1B,SAAS,gCAAgC;AAUlC,MAAM,+BAA+B,yBAAyB;AAAA,EACnE;AACF,CAAC;AAEM,MAAM,2DAA2D,UAAU;AAAA,EACvE,KAA2B;AAAA,EAAC;AAAA,EAE5B,OAA6B;AAAA,EAAC;AACzC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -51,7 +51,9 @@ import {
|
|
|
51
51
|
buildIntegrationCredentialSavePayload
|
|
52
52
|
} from "../credential-secret-fields.js";
|
|
53
53
|
import { isValidCredentialUrl } from "../../../lib/credentials-field-validation.js";
|
|
54
|
+
import { useIntegrationCredentialsFeatureAccess } from "../useIntegrationCredentialsFeatureAccess.js";
|
|
54
55
|
const UNSUPPORTED_CREDENTIAL_FIELD_TYPES = /* @__PURE__ */ new Set(["oauth", "ssh_keypair"]);
|
|
56
|
+
const credentialsRequestHeaders = { "x-om-forbidden-redirect": "0" };
|
|
55
57
|
function isEditableCredentialField(field) {
|
|
56
58
|
return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type);
|
|
57
59
|
}
|
|
@@ -305,6 +307,10 @@ function IntegrationDetailPage({ params }) {
|
|
|
305
307
|
const [isRefreshingRunActivity, setIsRefreshingRunActivity] = React.useState(false);
|
|
306
308
|
const [activeTab, setActiveTab] = React.useState("credentials");
|
|
307
309
|
const credentialsFormId = React.useId();
|
|
310
|
+
const {
|
|
311
|
+
isLoading: isLoadingCredentialsAccess,
|
|
312
|
+
canManageCredentials
|
|
313
|
+
} = useIntegrationCredentialsFeatureAccess();
|
|
308
314
|
const resolveCurrentIntegrationId = React.useCallback(() => {
|
|
309
315
|
return integrationId ?? (typeof window !== "undefined" ? resolvePathnameId(window.location.pathname) : void 0);
|
|
310
316
|
}, [integrationId]);
|
|
@@ -347,7 +353,7 @@ function IntegrationDetailPage({ params }) {
|
|
|
347
353
|
if (!currentIntegrationId) return;
|
|
348
354
|
const call = await apiCall(
|
|
349
355
|
`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`,
|
|
350
|
-
|
|
356
|
+
{ headers: credentialsRequestHeaders },
|
|
351
357
|
{ fallback: null }
|
|
352
358
|
);
|
|
353
359
|
if (call.ok && call.result) {
|
|
@@ -783,7 +789,7 @@ function IntegrationDetailPage({ params }) {
|
|
|
783
789
|
];
|
|
784
790
|
const StateIcon = resolvedState?.isEnabled ? CheckCircle2 : XCircle;
|
|
785
791
|
const stateBadgeClass = resolvedState?.isEnabled ? "border-status-success-border bg-status-success-bg text-status-success-text" : "border-status-neutral-border bg-status-neutral-bg text-status-neutral-text";
|
|
786
|
-
const showCredentialActions = showCredentialsTab && activeTab === "credentials" && credentialFormFields.length > 0;
|
|
792
|
+
const showCredentialActions = showCredentialsTab && activeTab === "credentials" && credentialFormFields.length > 0 && canManageCredentials;
|
|
787
793
|
React.useEffect(() => {
|
|
788
794
|
setActiveTab(resolveRequestedIntegrationDetailTab(searchParams?.get("tab"), visibleTabIds));
|
|
789
795
|
}, [searchParams, visibleTabIds]);
|
|
@@ -941,7 +947,14 @@ function IntegrationDetailPage({ params }) {
|
|
|
941
947
|
] }),
|
|
942
948
|
showCredentialsTab ? /* @__PURE__ */ jsx(TabsContent, { value: "credentials", className: "mt-0", children: /* @__PURE__ */ jsxs("section", { className: "space-y-4 rounded-lg border bg-card p-6", children: [
|
|
943
949
|
detail.bundle ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-status-info-border bg-status-info-bg p-3 text-sm text-status-info-text", children: t("integrations.detail.credentials.bundleShared", { bundle: detail.bundle.title }) }) : null,
|
|
944
|
-
credentialFormFields.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("integrations.detail.credentials.notConfigured") }) : /* @__PURE__ */ jsx(
|
|
950
|
+
credentialFormFields.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("integrations.detail.credentials.notConfigured") }) : isLoadingCredentialsAccess ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-8", children: /* @__PURE__ */ jsx(Spinner, {}) }) : !canManageCredentials ? /* @__PURE__ */ jsx(
|
|
951
|
+
EmptyState,
|
|
952
|
+
{
|
|
953
|
+
size: "sm",
|
|
954
|
+
icon: /* @__PURE__ */ jsx(Key, { className: "h-8 w-8", "aria-hidden": "true" }),
|
|
955
|
+
title: t("integrations.detail.credentials.noPermission", "You do not have permission to manage credentials for this integration.")
|
|
956
|
+
}
|
|
957
|
+
) : /* @__PURE__ */ jsx(
|
|
945
958
|
CrudForm,
|
|
946
959
|
{
|
|
947
960
|
formId: credentialsFormId,
|