-
Notifications
You must be signed in to change notification settings - Fork 0
INT-05: Add note-style import and web clip intake #809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Chris0Jeky
merged 16 commits into
main
from
int/note-style-import-and-clip-followthrough
Apr 12, 2026
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
02de93e
Add MarkdownImport and WebClip capture source values
Chris0Jeky 51b2bec
Add MarkdownImport and WebClip to frontend CaptureSource type
Chris0Jeky 9ef8b56
Add NoteImportDtos for markdown import and web clip intake
Chris0Jeky d1ccb7b
Add INoteImportService interface for note-style import
Chris0Jeky 4f58d37
Add NoteImportService for markdown and web clip intake
Chris0Jeky 80ff111
Add NoteImportController with markdown and web clip endpoints
Chris0Jeky 7b68632
Register INoteImportService in DI container
Chris0Jeky 6f89eb4
Add TypeScript types for note import API
Chris0Jeky 57c671e
Add noteImportApi client for markdown and web clip endpoints
Chris0Jeky 570e9cd
Extend ExportImportView with Markdown and Web Clip tabs
Chris0Jeky 5f48ada
Add NoteImportService unit tests
Chris0Jeky 75fbb88
Add noteImportApi frontend unit tests
Chris0Jeky ade9794
Fix silent success on total import failure and sourceRef inconsistency
Chris0Jeky f81749b
Guard proposal decisions with EF concurrency
Chris0Jeky f311fc5
Address PR #809 review comments for note import
Chris0Jeky d166667
Merge branch 'main' into int/note-style-import-and-clip-followthrough
Chris0Jeky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
backend/src/Taskdeck.Api/Controllers/NoteImportController.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.AspNetCore.RateLimiting; | ||
| using Taskdeck.Api.Contracts; | ||
| using Taskdeck.Api.Extensions; | ||
| using Taskdeck.Api.RateLimiting; | ||
| using Taskdeck.Application.DTOs; | ||
| using Taskdeck.Application.Interfaces; | ||
| using Taskdeck.Application.Services; | ||
|
|
||
| namespace Taskdeck.Api.Controllers; | ||
|
|
||
| /// <summary> | ||
| /// Note-style import endpoints for markdown files and web clips. | ||
| /// All imported content routes through the standard capture pipeline — | ||
| /// no direct board mutations occur here. | ||
| /// </summary> | ||
| [ApiController] | ||
| [Authorize] | ||
| [Route("api/import/notes")] | ||
| [Produces("application/json")] | ||
| public class NoteImportController : AuthenticatedControllerBase | ||
| { | ||
| private readonly INoteImportService _noteImportService; | ||
|
|
||
| public NoteImportController( | ||
| INoteImportService noteImportService, | ||
| IUserContext userContext) | ||
| : base(userContext) | ||
| { | ||
| _noteImportService = noteImportService; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Import a markdown file. The content is parsed into sections and | ||
| /// each section becomes a capture item in the standard pipeline. | ||
| /// </summary> | ||
| /// <param name="dto">Markdown import request with filename and content.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Import result with created capture item IDs.</returns> | ||
| /// <response code="200">Markdown imported successfully — capture items created.</response> | ||
| /// <response code="400">Validation error (empty content, oversized file, etc.).</response> | ||
| /// <response code="401">Authentication required.</response> | ||
| /// <response code="429">Rate limit exceeded.</response> | ||
| [HttpPost("markdown")] | ||
| [EnableRateLimiting(RateLimitingPolicyNames.NoteImportPerUser)] | ||
| [ProducesResponseType(typeof(NoteImportResultDto), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status400BadRequest)] | ||
| [ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status401Unauthorized)] | ||
| [ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status429TooManyRequests)] | ||
| public async Task<IActionResult> ImportMarkdown( | ||
| [FromBody] MarkdownImportRequestDto? dto, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| if (!TryGetCurrentUserId(out var userId, out var errorResult)) | ||
| return errorResult!; | ||
|
|
||
| if (dto == null) | ||
| { | ||
| return BadRequest(new ApiErrorResponse( | ||
| "VALIDATION_ERROR", | ||
| "Request body is required")); | ||
| } | ||
|
|
||
| var result = await _noteImportService.ImportMarkdownAsync(userId, dto, cancellationToken); | ||
| return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Import a web clip (URL + content snippet). Creates a single capture | ||
| /// item with the URL preserved as source provenance. | ||
| /// </summary> | ||
| /// <param name="dto">Web clip import request with URL, content, and optional title.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Import result with created capture item ID.</returns> | ||
| /// <response code="200">Web clip imported successfully — capture item created.</response> | ||
| /// <response code="400">Validation error (invalid URL, empty content, etc.).</response> | ||
| /// <response code="401">Authentication required.</response> | ||
| /// <response code="429">Rate limit exceeded.</response> | ||
| [HttpPost("webclip")] | ||
| [EnableRateLimiting(RateLimitingPolicyNames.NoteImportPerUser)] | ||
| [ProducesResponseType(typeof(NoteImportResultDto), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status400BadRequest)] | ||
| [ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status401Unauthorized)] | ||
| [ProducesResponseType(typeof(ApiErrorResponse), StatusCodes.Status429TooManyRequests)] | ||
| public async Task<IActionResult> ImportWebClip( | ||
| [FromBody] WebClipImportRequestDto? dto, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| if (!TryGetCurrentUserId(out var userId, out var errorResult)) | ||
| return errorResult!; | ||
|
|
||
| if (dto == null) | ||
| { | ||
| return BadRequest(new ApiErrorResponse( | ||
| "VALIDATION_ERROR", | ||
| "Request body is required")); | ||
| } | ||
|
|
||
| var result = await _noteImportService.ImportWebClipAsync(userId, dto, cancellationToken); | ||
| return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| namespace Taskdeck.Application.DTOs; | ||
|
|
||
| /// <summary> | ||
| /// Request to import a markdown file as one or more capture items. | ||
| /// The content is parsed, split into logical sections, and routed | ||
| /// through the standard capture pipeline. | ||
| /// </summary> | ||
| public sealed record MarkdownImportRequestDto( | ||
| string FileName, | ||
| string Content, | ||
| Guid? BoardId = null); | ||
|
|
||
| /// <summary> | ||
| /// Request to import a web clip (URL + content snippet) as a capture item. | ||
| /// The content is routed through the standard capture pipeline with | ||
| /// source provenance preserved. | ||
| /// </summary> | ||
| public sealed record WebClipImportRequestDto( | ||
| string Url, | ||
| string Content, | ||
| string? Title = null, | ||
| Guid? BoardId = null); | ||
|
|
||
| /// <summary> | ||
| /// Result of a note-style import operation. | ||
| /// </summary> | ||
| public sealed record NoteImportResultDto( | ||
| int ItemsCreated, | ||
| IReadOnlyList<NoteImportItemResultDto> Items, | ||
| IReadOnlyList<string>? Warnings = null, | ||
| IReadOnlyList<NoteImportItemErrorDto>? Errors = null); | ||
|
|
||
| /// <summary> | ||
| /// Error detail for a section that failed to import. | ||
| /// </summary> | ||
| public sealed record NoteImportItemErrorDto( | ||
| int SectionIndex, | ||
| string? Heading, | ||
| string ErrorCode, | ||
| string ErrorMessage); | ||
|
|
||
| /// <summary> | ||
| /// Result for a single capture item created from a note import. | ||
| /// </summary> | ||
| public sealed record NoteImportItemResultDto( | ||
| Guid CaptureItemId, | ||
| string TextExcerpt, | ||
| string SourceType, | ||
| string? SourceRef); |
28 changes: 28 additions & 0 deletions
28
backend/src/Taskdeck.Application/Services/INoteImportService.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| using Taskdeck.Application.DTOs; | ||
| using Taskdeck.Domain.Common; | ||
|
|
||
| namespace Taskdeck.Application.Services; | ||
|
|
||
| /// <summary> | ||
| /// Handles note-style import (markdown files, web clips) by routing | ||
| /// imported content through the standard capture pipeline. | ||
| /// No direct board mutations — all content enters the capture → triage → proposal flow. | ||
| /// </summary> | ||
| public interface INoteImportService | ||
| { | ||
| /// <summary> | ||
| /// Parses a markdown file and creates capture items for each logical section. | ||
| /// </summary> | ||
| Task<Result<NoteImportResultDto>> ImportMarkdownAsync( | ||
| Guid userId, | ||
| MarkdownImportRequestDto request, | ||
| CancellationToken cancellationToken = default); | ||
|
|
||
| /// <summary> | ||
| /// Creates a capture item from a web clip (URL + content snippet). | ||
| /// </summary> | ||
| Task<Result<NoteImportResultDto>> ImportWebClipAsync( | ||
| Guid userId, | ||
| WebClipImportRequestDto request, | ||
| CancellationToken cancellationToken = default); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ImportMarkdowncan enqueue up to 50 capture items in a single request, but it uses the same fixed-window rate limit policy as single-item capture writes. This effectively multiplies the allowed write throughput per permit. Consider a dedicated policy for note imports and/or enforcing a stricter per-request max section count based on the configured capture write rate.