{"version":3,"file":"news.es.mjs","names":[],"sources":["../src/news/acknowledgeArticle.ts","../src/news/channelToOption.ts","../src/news/fetchArticleById.ts","../src/news/fetchChannel.ts","../src/news/isArticlePublished.ts","../src/news/keepPublishedArticle.ts","../src/news/fetchArticles.ts","../src/news/fetchChannels.ts","../src/news/fetchSpaceName.ts","../src/news/createNewsApi.ts"],"sourcesContent":["import { ApiError } from '../api/ApiError'\nimport { getStaffbaseCsrfToken } from '../host/getStaffbaseCsrfToken'\nimport type { NewsApiConfig } from '../types/news/NewsApiConfig'\n\n/**\n * Acknowledges a post for the current user.\n *\n * Fails fast (and reports via onError) when no CSRF token is available, since a\n * tokenless POST is guaranteed to be rejected and would otherwise surface as a\n * silent failure. On a non-OK response it reports the status and body, then\n * throws a typed ApiError so callers can branch on `status`.\n * @param {NewsApiConfig} config - The injected service configuration.\n * @param {string} id - The post id to acknowledge.\n * @returns {Promise<void>} Resolves when the acknowledgement succeeds.\n * @throws {ApiError | Error} On a missing CSRF token or a failed request.\n */\nexport const acknowledgeArticle = async (\n  config: NewsApiConfig,\n  id: string,\n): Promise<void> => {\n  const csrfToken = (config.getCsrfToken ?? getStaffbaseCsrfToken)()\n\n  if (!csrfToken) {\n    const error = new Error(\n      `Cannot acknowledge article ${id}: missing CSRF token`,\n    )\n    config.onError?.('Acknowledgment aborted:', error)\n    throw error\n  }\n\n  const response = await fetch(\n    `${config.apiUrl}/posts/${id}/acknowledgements`,\n    {\n      method: 'POST',\n      headers: {\n        'X-Csrf-Token': csrfToken,\n        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',\n      },\n      // Include the session cookie for authentication.\n      credentials: 'include',\n    },\n  )\n\n  if (!response.ok) {\n    const detail = await response.text().catch(() => '')\n    config.onError?.(\n      `Failed to acknowledge article ${id}:`,\n      response.statusText,\n      detail,\n    )\n    throw new ApiError(\n      response.status,\n      `Failed to acknowledge article ${id}: ${response.statusText}`,\n    )\n  }\n}\n","import type { Channel } from '../types/content/Channel'\nimport type { DropdownOption } from '../types/content/DropdownOption'\n\n/**\n * Maps a channel to a selector option, choosing its title.\n *\n * Prefers the title in `defaultLanguage` but falls back to any available\n * localized title, so a channel localized only in another language is never\n * dropped. Returns null when the channel has no usable title.\n * @param {Channel} channel - The channel from the API.\n * @param {string} defaultLanguage - The preferred locale for the title.\n * @returns {DropdownOption | null} The option, or null when untitled.\n */\nexport const channelToOption = (\n  channel: Channel,\n  defaultLanguage: string,\n): DropdownOption | null => {\n  const localization = channel?.config?.localization ?? {}\n  const title =\n    localization[defaultLanguage]?.title ||\n    Object.values(localization).find((entry) => entry?.title)?.title\n\n  return title ? { id: channel.id, title, spaceId: channel.spaceID } : null\n}\n","import { fetchJson } from '../api/fetchJson'\nimport type { NewsApiConfig } from '../types/news/NewsApiConfig'\nimport type { Post } from '../types/news/Post'\n\n/**\n * Fetches a single post by id.\n * @template TArticle The post shape returned (defaults to Post).\n * @param {NewsApiConfig} config - The injected service configuration.\n * @param {string} articleId - The post id.\n * @returns {Promise<TArticle>} The post.\n */\nexport const fetchArticleById = <TArticle = Post>(\n  config: NewsApiConfig,\n  articleId: string,\n): Promise<TArticle> =>\n  fetchJson<TArticle>(`${config.apiUrl}/posts/${articleId}`)\n","import { fetchJson } from '../api/fetchJson'\nimport type { Channel } from '../types/content/Channel'\nimport type { NewsApiConfig } from '../types/news/NewsApiConfig'\n\n/**\n * Fetches a single channel by id.\n * @param {NewsApiConfig} config - The injected service configuration.\n * @param {string} channelId - The channel id.\n * @returns {Promise<Channel>} The channel.\n */\nexport const fetchChannel = (\n  config: NewsApiConfig,\n  channelId: string,\n): Promise<Channel> =>\n  fetchJson<Channel>(`${config.apiUrl}/channels/${channelId}`)\n","import type { Post } from '../types/news/Post'\n\n/**\n * Decides whether a post is within its published window right now.\n *\n * A post counts as published only once its `published` date has passed and\n * before any `unpublished` date. A missing or future `published` date, or a\n * reached `unpublished` date, makes it unpublished.\n * @param {Pick<Post, 'published' | 'unpublished'>} article - The post's publish window.\n * @returns {boolean} True when currently published.\n */\nexport const isArticlePublished = (\n  article: Pick<Post, 'published' | 'unpublished'>,\n): boolean => {\n  const now = new Date()\n  const publishedDate = article.published ? new Date(article.published) : null\n  const unpublishedDate = article.unpublished\n    ? new Date(article.unpublished)\n    : null\n\n  if (!publishedDate || now < publishedDate) return false\n  if (unpublishedDate && now >= unpublishedDate) return false\n\n  return true\n}\n","import type { Post } from '../types/news/Post'\nimport { isArticlePublished } from './isArticlePublished'\n\n/**\n * Pagination mapper that keeps only currently-published posts (drops others by\n * returning null), preserving the post type. Constrained to the publish-window\n * fields so widget view-models that narrow other fields still qualify.\n * @template TArticle The post shape (must carry the publish-window fields).\n * @param {TArticle} article - The post to test.\n * @returns {TArticle | null} The post when published, otherwise null.\n */\nexport const keepPublishedArticle = <\n  TArticle extends Pick<Post, 'published' | 'unpublished'>,\n>(\n  article: TArticle,\n): TArticle | null => (isArticlePublished(article) ? article : null)\n","import { fetchAllPaginated } from '../api/fetchAllPaginated'\nimport type { FetchArticlesOptions } from '../types/news/FetchArticlesOptions'\nimport type { NewsApiConfig } from '../types/news/NewsApiConfig'\nimport type { Post } from '../types/news/Post'\nimport { fetchChannel } from './fetchChannel'\nimport { keepPublishedArticle } from './keepPublishedArticle'\n\n/**\n * Fetches a channel's posts, paginated and (by default) filtered to the\n * currently-published window.\n *\n * `scope` selects the client or channel posts endpoint. With\n * `requireChannelPublished`, an unpublished channel short-circuits to an empty\n * list. With `includeUnpublished`, the per-post publication filter is skipped\n * (used by configuration selectors that list every article).\n * @template TArticle The post shape returned (defaults to Post).\n * @param {NewsApiConfig} config - The injected service configuration.\n * @param {string} channelId - The channel id.\n * @param {FetchArticlesOptions} [options] - Endpoint and filtering options.\n * @returns {Promise<TArticle[]>} The posts.\n */\nexport const fetchArticles = async <\n  TArticle extends Pick<Post, 'published' | 'unpublished'> = Post,\n>(\n  config: NewsApiConfig,\n  channelId: string,\n  options: FetchArticlesOptions = {},\n): Promise<TArticle[]> => {\n  const {\n    scope = 'channel',\n    requireChannelPublished = false,\n    includeUnpublished = false,\n    includeDrafts = false,\n  } = options\n\n  if (requireChannelPublished) {\n    const channel = await fetchChannel(config, channelId)\n    const publishedDate = channel.published ? new Date(channel.published) : null\n    if (!publishedDate || new Date() < publishedDate) return []\n  }\n\n  const segment =\n    scope === 'client'\n      ? `client/channels/${channelId}/posts`\n      : `channels/${channelId}/posts`\n\n  return fetchAllPaginated<TArticle>(`${config.apiUrl}/${segment}`, {\n    includeDrafts,\n    maxPages: config.maxPages,\n    onError: config.onError,\n    mapItem: includeUnpublished ? undefined : keepPublishedArticle,\n  })\n}\n","import { fetchAllPaginated } from '../api/fetchAllPaginated'\nimport type { Channel } from '../types/content/Channel'\nimport type { DropdownOption } from '../types/content/DropdownOption'\nimport type { NewsApiConfig } from '../types/news/NewsApiConfig'\nimport { channelToOption } from './channelToOption'\n\n/**\n * Fetches every article channel as a selector option, paginated.\n *\n * Titles come from channelToOption (prefer default language, fall back to any),\n * so a channel localized only in another language is never silently dropped.\n * `spaceId` is always included for grouping/enrichment; `spaceName` is left to\n * fetchSpaceName so the list paints without blocking on per-space lookups.\n * @param {NewsApiConfig} config - The injected service configuration.\n * @returns {Promise<DropdownOption[]>} The channel options.\n */\nexport const fetchChannels = async (\n  config: NewsApiConfig,\n): Promise<DropdownOption[]> => {\n  const channels = await fetchAllPaginated<Channel>(\n    `${config.apiUrl}/channels?contentType=articles`,\n    {\n      includeDrafts: true,\n      maxPages: config.maxPages,\n      onError: config.onError,\n    },\n  )\n\n  return channels\n    .map((channel) => channelToOption(channel, config.defaultLanguage))\n    .filter((option): option is DropdownOption => option !== null)\n}\n","import { fetchJson } from '../api/fetchJson'\nimport type { NewsApiConfig } from '../types/news/NewsApiConfig'\n\n/**\n * Process-lifetime memo of resolved space names, keyed by API URL + space id so\n * distinct hosts never collide. Avoids re-fetching the same space across the\n * many channels that share it.\n */\nconst spaceNameCache = new Map<string, string>()\n\n/**\n * Resolves a space's display name by id, caching the result.\n * @param {NewsApiConfig} config - The injected service configuration.\n * @param {string} spaceId - The space id.\n * @returns {Promise<string>} The space name (empty string when unknown).\n */\nexport const fetchSpaceName = async (\n  config: NewsApiConfig,\n  spaceId: string,\n): Promise<string> => {\n  const key = `${config.apiUrl}|${spaceId}`\n  const cached = spaceNameCache.get(key)\n  if (cached !== undefined) return cached\n\n  const space = await fetchJson<{ id: string; name: string }>(\n    `${config.apiUrl}/spaces/${spaceId}`,\n  )\n  const name = space?.name ?? ''\n  spaceNameCache.set(key, name)\n\n  return name\n}\n","import type { FetchArticlesOptions } from '../types/news/FetchArticlesOptions'\nimport type { NewsApi } from '../types/news/NewsApi'\nimport type { NewsApiConfig } from '../types/news/NewsApiConfig'\nimport type { Post } from '../types/news/Post'\nimport { acknowledgeArticle } from './acknowledgeArticle'\nimport { fetchArticleById } from './fetchArticleById'\nimport { fetchArticles } from './fetchArticles'\nimport { fetchChannel } from './fetchChannel'\nimport { fetchChannels } from './fetchChannels'\nimport { fetchSpaceName } from './fetchSpaceName'\n\n/**\n * Builds a news service bound to one host configuration.\n *\n * Replaces the per-widget `apiService` modules that the alerts,\n * unacknowledged-bulletins and global-content widgets each maintained: a fix or\n * improvement here (CSRF fail-fast, typed errors, robust title fallback) now\n * reaches every consumer at once. Each returned method is a thin binding over a\n * single-purpose function in this module.\n * @param {NewsApiConfig} config - The injected, host-specific configuration.\n * @returns {NewsApi} The bound news service.\n */\nexport const createNewsApi = (config: NewsApiConfig): NewsApi => ({\n  fetchChannels() {\n    return fetchChannels(config)\n  },\n  fetchChannel(channelId: string) {\n    return fetchChannel(config, channelId)\n  },\n  fetchSpaceName(spaceId: string) {\n    return fetchSpaceName(config, spaceId)\n  },\n  fetchArticles<\n    TArticle extends Pick<Post, 'published' | 'unpublished'> = Post,\n  >(channelId: string, options?: FetchArticlesOptions) {\n    return fetchArticles<TArticle>(config, channelId, options)\n  },\n  fetchArticleById<TArticle = Post>(articleId: string) {\n    return fetchArticleById<TArticle>(config, articleId)\n  },\n  acknowledgeArticle(id: string) {\n    return acknowledgeArticle(config, id)\n  },\n})\n"],"mappings":"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"}