import { requestJson } from '@/lib/api/client/request' import * as selectorContracts from '@/lib/api/contracts/selectors' import { fetchOAuthToken } from '@/hooks/selectors/helpers' import { ensureCredential, ensureDomain, SELECTOR_STALE } from '@/hooks/selectors/providers/shared' import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types' function formatConfluenceSpaceLabel(space: { name: string; key: string; status?: string }): string { const base = `${space.name} (${space.key})` return space.status === 'archived' ? `${base} — archived` : base } function toSpaceOption(space: { name: string; key: string; status?: string }): { id: string label: string } { return { id: space.key, label: formatConfluenceSpaceLabel(space) } } export const confluenceSelectors = { 'confluence.spaces': { key: 'confluence.spaces', contracts: [selectorContracts.confluenceSpacesSelectorContract], staleTime: SELECTOR_STALE, getQueryKey: ({ context }: SelectorQueryArgs) => [ 'selectors', 'confluence.spaces', context.oauthCredential ?? 'none', context.domain ?? 'none', ], enabled: ({ context }) => Boolean(context.oauthCredential && context.domain), /** * Drives pagination through {@link useSelectorOptions}, which drains every * page via this callback. No `fetchList` — the paged path supersedes it. */ fetchPage: async ({ context, cursor, signal }) => { const credentialId = ensureCredential(context, 'confluence.spaces') const domain = ensureDomain(context, 'confluence.spaces') const data = await requestJson(selectorContracts.confluenceSpacesSelectorContract, { body: { credential: credentialId, workflowId: context.workflowId, domain, cursor, }, signal, }) return { items: (data.spaces || []).map(toSpaceOption), nextCursor: data.nextCursor, } }, /** * Resolves a single space label. Hits only the first page — the dropdown's * `fetchPage` stream populates the options cache for spaces beyond page 1, * and `useSelectorOptionMap` merges them in. Walking all pages here would * double API load since the stream is already running in parallel. */ fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => { if (!detailId) return null const credentialId = ensureCredential(context, 'confluence.spaces') const domain = ensureDomain(context, 'confluence.spaces') const data = await requestJson(selectorContracts.confluenceSpacesSelectorContract, { body: { credential: credentialId, workflowId: context.workflowId, domain, }, signal, }) const space = (data.spaces || []).find((s) => s.key === detailId) ?? null if (!space) return null return toSpaceOption(space) }, }, 'confluence.pages': { key: 'confluence.pages', contracts: [ selectorContracts.confluencePagesSelectorContract, selectorContracts.confluencePageSelectorContract, ], staleTime: SELECTOR_STALE, getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'confluence.pages', context.oauthCredential ?? 'none', context.domain ?? 'none', search ?? '', ], enabled: ({ context }) => Boolean(context.oauthCredential && context.domain), fetchList: async ({ context, search, signal }: SelectorQueryArgs) => { const credentialId = ensureCredential(context, 'confluence.pages') const domain = ensureDomain(context, 'confluence.pages') const bundle = await fetchOAuthToken(credentialId, context.workflowId) if (!bundle) { throw new Error('Missing Confluence access token') } const data = await requestJson(selectorContracts.confluencePagesSelectorContract, { body: { domain, accessToken: bundle.accessToken, cloudId: bundle.cloudId, title: search, }, signal, }) return (data.files || []).map((file) => ({ id: file.id, label: file.name, })) }, fetchById: async ({ context, detailId, signal }: SelectorQueryArgs) => { if (!detailId) return null const credentialId = ensureCredential(context, 'confluence.pages') const domain = ensureDomain(context, 'confluence.pages') const bundle = await fetchOAuthToken(credentialId, context.workflowId) if (!bundle) { throw new Error('Missing Confluence access token') } const data = await requestJson(selectorContracts.confluencePageSelectorContract, { body: { domain, accessToken: bundle.accessToken, cloudId: bundle.cloudId, pageId: detailId, }, signal, }) return { id: data.id, label: data.title } }, }, } satisfies Record< Extract, SelectorDefinition >