@@ -11,6 +11,7 @@ import {
1111} from '../utils/pagination.util.js' ;
1212import { ControllerResponse } from '../types/common.types.js' ;
1313import { formatCommentsList } from './atlassian.comments.formatter.js' ;
14+ import { formatInlineCommentsList } from './atlassian.inline-comments.formatter.js' ;
1415import { DEFAULT_PAGE_SIZE } from '../utils/defaults.util.js' ;
1516import { adfToMarkdown } from '../utils/adf.util.js' ;
1617import {
@@ -49,6 +50,41 @@ interface ListPageCommentsOptions {
4950 bodyFormat ?: 'storage' | 'view' | 'atlas_doc_format' ;
5051}
5152
53+ /**
54+ * Interface for list inline comments options
55+ */
56+ interface ListInlineCommentsOptions {
57+ /**
58+ * The ID of the page to get inline comments for
59+ */
60+ pageId : string ;
61+
62+ /**
63+ * Include resolved inline comments
64+ */
65+ includeResolved ?: boolean ;
66+
67+ /**
68+ * Sort order for inline comments
69+ */
70+ sortBy ?: 'created' | 'position' ;
71+
72+ /**
73+ * Maximum number of results to return
74+ */
75+ limit ?: number ;
76+
77+ /**
78+ * Starting point for pagination
79+ */
80+ start ?: number ;
81+
82+ /**
83+ * Body format (storage, view, atlas_doc_format)
84+ */
85+ bodyFormat ?: 'storage' | 'view' | 'atlas_doc_format' ;
86+ }
87+
5288/**
5389 * Extended interface for a comment with converted markdown content
5490 */
@@ -206,7 +242,184 @@ async function listPageComments(
206242 }
207243}
208244
245+ /**
246+ * List inline comments only for a specific Confluence page
247+ *
248+ * @param options - Options for listing inline comments
249+ * @returns Controller response with formatted inline comments and pagination info
250+ */
251+ async function listInlineComments (
252+ options : ListInlineCommentsOptions ,
253+ ) : Promise < ControllerResponse > {
254+ const methodLogger = logger . forMethod ( 'listInlineComments' ) ;
255+ try {
256+ // Apply defaults and prepare service parameters
257+ const {
258+ pageId,
259+ includeResolved = false ,
260+ sortBy = 'position' ,
261+ limit = DEFAULT_PAGE_SIZE ,
262+ start = 0 ,
263+ bodyFormat = 'atlas_doc_format' ,
264+ } = options ;
265+
266+ methodLogger . debug ( 'Listing inline comments for page' , {
267+ pageId,
268+ includeResolved,
269+ sortBy,
270+ limit,
271+ start,
272+ bodyFormat,
273+ } ) ;
274+
275+ // Get all comments first with a higher limit to ensure we capture inline comments
276+ // since we'll filter them locally
277+ const allCommentsData = await atlassianCommentsService . listPageComments (
278+ {
279+ pageId,
280+ limit : 250 , // Get more comments to filter inline ones
281+ start : 0 , // Always start from beginning for inline filtering
282+ bodyFormat,
283+ } ,
284+ ) ;
285+
286+ methodLogger . debug ( 'Retrieved all comments for filtering' , {
287+ totalComments : allCommentsData . results . length ,
288+ pageId,
289+ } ) ;
290+
291+ // Filter for inline comments only
292+ const inlineCommentsRaw = allCommentsData . results . filter ( ( comment ) => {
293+ const isInline = comment . extensions ?. location === 'inline' ;
294+
295+ // Apply resolved filter if needed
296+ if ( ! includeResolved && comment . status !== 'current' ) {
297+ return false ;
298+ }
299+
300+ return isInline ;
301+ } ) ;
302+
303+ methodLogger . debug ( 'Filtered inline comments' , {
304+ inlineCount : inlineCommentsRaw . length ,
305+ totalCount : allCommentsData . results . length ,
306+ includeResolved,
307+ } ) ;
308+
309+ // Convert ADF content to Markdown and extract highlighted text for inline comments
310+ const convertedComments : CommentWithContext [ ] = inlineCommentsRaw . map (
311+ ( comment ) => {
312+ let markdownBody =
313+ '*Content format not supported or unavailable*' ;
314+
315+ // Convert comment body from ADF to Markdown
316+ if ( comment . body ?. atlas_doc_format ?. value ) {
317+ try {
318+ markdownBody = adfToMarkdown (
319+ comment . body . atlas_doc_format . value ,
320+ ) ;
321+ methodLogger . debug (
322+ `Successfully converted ADF to Markdown for inline comment ${ comment . id } ` ,
323+ ) ;
324+ } catch ( conversionError ) {
325+ methodLogger . error (
326+ `ADF conversion failed for inline comment ${ comment . id } ` ,
327+ conversionError ,
328+ ) ;
329+ // Keep default error message
330+ }
331+ } else {
332+ methodLogger . warn (
333+ `No ADF content available for inline comment ${ comment . id } ` ,
334+ ) ;
335+ }
336+
337+ // Extract the highlighted text for inline comments
338+ let highlightedText : string | undefined = undefined ;
339+ if ( comment . extensions ?. inlineProperties ) {
340+ // Safely access inlineProperties fields with type checking
341+ const props = comment . extensions
342+ . inlineProperties as InlineProperties ;
343+
344+ // Try different properties that might contain the highlighted text
345+ highlightedText =
346+ props . originalSelection || props . textContext ;
347+
348+ // If not found in standard properties, check for custom properties
349+ if ( ! highlightedText && 'selectionText' in props ) {
350+ highlightedText = String ( props . selectionText || '' ) ;
351+ }
352+
353+ if ( highlightedText ) {
354+ methodLogger . debug (
355+ `Found highlighted text for inline comment ${ comment . id } : ${ highlightedText . substring ( 0 , 50 ) } ${ highlightedText . length > 50 ? '...' : '' } ` ,
356+ ) ;
357+ } else {
358+ methodLogger . warn (
359+ `No highlighted text found for inline comment ${ comment . id } ` ,
360+ ) ;
361+ }
362+ }
363+
364+ // Return comment with added context
365+ return {
366+ ...comment ,
367+ convertedMarkdownBody : markdownBody ,
368+ highlightedText,
369+ } ;
370+ } ,
371+ ) ;
372+
373+ // Sort inline comments by requested order
374+ if ( sortBy === 'position' ) {
375+ convertedComments . sort ( ( a , b ) => {
376+ // Sort by marker position or container ID if available
377+ const aPos = a . extensions ?. inlineProperties ?. markerRef || a . id ;
378+ const bPos = b . extensions ?. inlineProperties ?. markerRef || b . id ;
379+ return String ( aPos ) . localeCompare ( String ( bPos ) ) ;
380+ } ) ;
381+ } else if ( sortBy === 'created' ) {
382+ // Sort by ID as a proxy for creation order (newer IDs = later created)
383+ convertedComments . sort ( ( a , b ) => a . id . localeCompare ( b . id ) ) ;
384+ }
385+
386+ // Apply pagination after filtering and sorting
387+ const paginatedComments = convertedComments . slice ( start , start + limit ) ;
388+
389+ methodLogger . debug ( 'Applied pagination to inline comments' , {
390+ totalInline : convertedComments . length ,
391+ start,
392+ limit,
393+ returned : paginatedComments . length ,
394+ } ) ;
395+
396+ // Format the inline comments for display
397+ const baseUrl = allCommentsData . _links ?. base || '' ;
398+ const formattedContent = formatInlineCommentsList (
399+ paginatedComments ,
400+ pageId ,
401+ baseUrl ,
402+ convertedComments . length ,
403+ start ,
404+ limit ,
405+ ) ;
406+
407+ return {
408+ content : formattedContent ,
409+ } ;
410+ } catch ( error ) {
411+ // Handle errors
412+ throw handleControllerError ( error , {
413+ entityType : 'InlineComment' ,
414+ operation : 'list' ,
415+ source : 'controllers/atlassian.comments.controller.ts@listInlineComments' ,
416+ additionalInfo : { pageId : options . pageId } ,
417+ } ) ;
418+ }
419+ }
420+
209421// Export controller functions
210422export const atlassianCommentsController = {
211423 listPageComments,
424+ listInlineComments,
212425} ;
0 commit comments