Skip to content

feat(tasks): enhance CSV import with tag support and Todoist format#236

Merged
ahmet-cetinkaya merged 2 commits into
mainfrom
feat/import-tags-ux-refactor
Feb 9, 2026
Merged

feat(tasks): enhance CSV import with tag support and Todoist format#236
ahmet-cetinkaya merged 2 commits into
mainfrom
feat/import-tags-ux-refactor

Conversation

@ahmet-cetinkaya

@ahmet-cetinkaya ahmet-cetinkaya commented Feb 9, 2026

Copy link
Copy Markdown
Owner

🚀 Motivation and Context

This PR enhances the CSV import functionality for tasks by adding support for tags. It implements Todoist-specific import logic with automatic tag creation and improves the overall UX structure of the import/export settings component.

⚙️ Implementation Details

  • Tag Support: Added support for importing tags from CSV files via TAGS and LABELS columns
  • Todoist Format: Implemented Todoist-specific import logic with tag extraction from content (@tag format)
  • Async Processing: Updated existing imports to use async processing for better performance
  • UI Improvements: Refactored import_export_settings component for better maintainability
  • Translations: Added new translation keys across all supported languages (22+ locales)
  • Testing: Added comprehensive test coverage for the new functionality
  • Error Handling: Improved error handling throughout the import process

📋 Checklist for Reviewer

  • Tests passed locally (skipped per user request)
  • Commit history is clean and descriptive
  • Documentation updated (translation keys added)
  • Code quality standards were met

🔗 Related

Closes #233

Add support for importing tags from CSV files with new TAGS and LABELS
columns. Implement automatic tag creation during import and update
existing imports to use async processing. Add Todoist-specific import
logic including tag extraction from content (@tag format) and improved
error handling. Update UI with new translation keys and improve
component structure for better maintainability.
@ahmet-cetinkaya ahmet-cetinkaya added the enhancement New feature or request label Feb 9, 2026
@ahmet-cetinkaya ahmet-cetinkaya self-assigned this Feb 9, 2026
@ahmet-cetinkaya ahmet-cetinkaya added the enhancement New feature or request label Feb 9, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @ahmet-cetinkaya, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly upgrades the task CSV import feature by introducing robust tag support and a specialized import path for Todoist users. It aims to provide a more flexible and user-friendly experience for bringing external task data into the application, ensuring that task metadata like tags are preserved and hierarchies are correctly interpreted. The changes also include performance enhancements through asynchronous processing and a cleaner, more informative user interface for import settings.

Highlights

  • Enhanced CSV Import with Tag Support: The CSV import functionality for tasks now supports importing tags. This includes dedicated 'TAGS' and 'LABELS' columns in generic CSVs, and automatic extraction of tags from task content (e.g., '@tag') for Todoist imports.
  • Todoist-Specific Import Logic: A new import type has been introduced specifically for Todoist CSV exports. This logic intelligently parses Todoist's content format to extract tags and handles task hierarchy, providing a more seamless import experience for Todoist users.
  • Asynchronous Processing for Imports: Existing and new import operations have been updated to utilize asynchronous processing, which improves performance and responsiveness during large CSV imports.
  • UI/UX Improvements for Import/Export Settings: The import_export_settings component has been refactored for better maintainability and user experience. This includes moving the external import page into its own dedicated widget and adding specific information cards for generic and Todoist import formats.
  • Improved Error Handling and Validation: Error handling throughout the import process has been enhanced, with more specific error messages and robust validation, including checks for file path traversal attempts and invalid data formats.
  • Comprehensive Test Coverage: New and updated unit tests have been added to cover the new tag import logic, Todoist-specific parsing, and various error handling scenarios, ensuring the reliability of the import feature.
  • Internationalization Updates: New translation keys have been added across all supported locales (22+ languages) to support the new import features and UI elements, ensuring a localized experience for users worldwide.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/lib/core/application/features/tasks/commands/import_tasks_command.dart
    • Added imports for SaveTagCommand and GetListTagsQuery to support tag management during import.
    • Refactored _ImportErrorIds and _ImportErrorMessages classes to be public (ImportErrorIds, ImportErrorMessages) for broader accessibility.
    • Updated all references to error ID and message constants to use the new public class names.
    • Modified _mapRowToSaveCommand, _mapTodoistRow, and _mapGenericRow methods to be asynchronous (Future<SaveTaskCommand?>) to handle tag creation/retrieval.
    • Implemented logic in _mapTodoistRow to extract tags from task content (e.g., @tag) and the 'LABELS' column, then create or retrieve corresponding tag IDs.
    • Implemented logic in _mapGenericRow to process a 'TAGS' column and create or retrieve corresponding tag IDs.
    • Added new private helper methods: _extractTodoistTags to parse tags from Todoist content, _cleanTodoistTitle to remove tags from the title, and _getOrCreateTagIds to handle tag creation/retrieval via the mediator.
    • Updated _mediator.send calls to explicitly specify generic types for improved type safety.
  • src/lib/presentation/ui/features/settings/components/import_export_settings.dart
    • Refactored the _buildExternalImportPage method by extracting its content into a new dedicated _ExternalImportPage StatefulWidget.
    • Removed _selectedTaskFilePath and _taskImportType state variables from _ImportExportActionsDialogState and moved them to the new _ExternalImportPageState.
    • Moved the _handleTaskFilePick and _handleExternalImportExecute methods to the _ExternalImportPageState to encapsulate external import logic.
    • Added an InformationCard to display Todoist-specific import instructions when the Todoist format is selected.
  • src/lib/presentation/ui/features/tasks/assets/locales/cs.yaml
    • Added 'Column 6: Štítky (Volitelné, oddělené čárkou)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/da.yaml
    • Added 'Kolonne 6: Tags (Valgfri, kommasepareret)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/de.yaml
    • Added 'Spalte 6: Tags (Optional, durch Kommas getrennt)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/el.yaml
    • Added 'Στήλη 6: Ετικέτες (Προαιρετικό, διαχωρισμένες με κόμμα)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/en.yaml
    • Added 'Column 6: Tags (Optional, comma-separated)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/es.yaml
    • Added 'Columna 6: Etiquetas (Opcional, separadas por comas)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/fi.yaml
    • Added 'Sarake 6: Tunnisteet (Valinnainen, pilkuilla erotettu)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/fr.yaml
    • Added 'Colonne 6 : Étiquettes (Optionnel, séparées par des virgules)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/it.yaml
    • Added 'Colonna 6: Tag (Opzionale, separati da virgole)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/ja.yaml
    • Added '列6: タグ(オプション、カンマ区切り)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/ko.yaml
    • Added '열 6: 태그 (선택 사항, 쉼표로 구분)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/nl.yaml
    • Added 'Kolom 6: Tags (Optioneel, door komma's gescheiden)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/no.yaml
    • Added 'Kolonne 6: Merker (Valgfritt, kommaseparert)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/pl.yaml
    • Added 'Kolumna 6: Tagi (Opcjonalne, oddzielone przecinkami)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/pt.yaml
    • Added 'Coluna 6: Tags (Opcional, separados por vírgulas)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/ro.yaml
    • Added 'Coloana 6: Etichete (Opțional, separate prin virgule)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/ru.yaml
    • Added 'Столбец 6: Теги (Необязательно, через запятую)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/sl.yaml
    • Added 'Stolpec 6: Oznake (Izbirno, ločene z vejicami)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/sv.yaml
    • Added 'Kolumn 6: Taggar (Valfritt, kommaseparerad)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/tr.yaml
    • Added 'Sütun 6: Etiketler (İsteğe Bağlı, virgülle ayrılmış)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/uk.yaml
    • Added 'Стовпець 6: Теги (Необов'язково, через кому)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/assets/locales/zh.yaml
    • Added '第 6 列:标签(可选,逗号分隔)' to the generic import description.
    • Added todoist_info_title and todoist_info_description for Todoist CSV export instructions.
  • src/lib/presentation/ui/features/tasks/constants/task_translation_keys.dart
    • Added new translation keys: importTodoistInfoTitle and importTodoistInfoDescription.
  • src/test/core/application/features/tasks/commands/import_tasks_command_test.dart
    • Added imports for SaveTagCommand and GetListTagsQuery for mocking tag-related operations.
    • Introduced reset(mockMediator) calls before each test to ensure test isolation and prevent interference between tests.
    • Updated when(mockMediator.send) calls to use argThat(predicate<SaveTaskCommand>((r) => r is SaveTaskCommand)) for more precise argument matching.
    • Added new test cases for Todoist CSV import with labels and content tags, including mocking the creation and retrieval of tags.
    • Added new test cases for generic CSV import that includes a 'TAGS' column.
    • Adjusted verify calls to correctly assert on SaveTaskCommand instances, including the new tagIdsToAdd property.
  • src/test/presentation/ui/features/settings/components/import_export_settings_test.dart
    • New file: Added a widget test to verify the UI behavior of ImportExportSettings.
    • The test specifically checks that the correct information text (generic vs. Todoist) is displayed based on the selected import format.
Activity
  • The author has implemented support for importing tags from CSV files, including a specific format for Todoist exports.
  • The import process now uses asynchronous operations for better performance.
  • The user interface for import/export settings has been refactored for improved maintainability and user experience.
  • New translation keys have been added to support the updated features across all supported languages.
  • Comprehensive test coverage has been added for the new functionality, including tag handling and error scenarios.
  • The author has confirmed that tests passed locally, commit history is clean, documentation is updated, and code quality standards are met, indicating a thorough self-review process.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@sourcery-ai

sourcery-ai Bot commented Feb 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors the settings import/export UI to a dedicated external import page while extending the task CSV import pipeline to support tags (generic TAGS column and Todoist LABELS/@tags), tightening error handling, and significantly expanding unit tests and translations for the new behavior.

Sequence diagram for the enhanced CSV task import with tags and Todoist format

sequenceDiagram
    actor User
    participant ExternalImportPage as _ExternalImportPage
    participant FileService as IFileService
    participant AsyncErrorHandler
    participant Mediator
    participant ImportHandler as ImportTasksCommandHandler
    participant TagQuery as GetListTagsQueryHandler
    participant TagSave as SaveTagCommandHandler
    participant TaskSave as SaveTaskCommandHandler
    participant Overlay as OverlayNotificationHelper

    User->>ExternalImportPage: tap import tasks
    ExternalImportPage->>FileService: pickFile(csv)
    FileService-->>ExternalImportPage: filePath
    ExternalImportPage->>ExternalImportPage: setState(selectedTaskFilePath)

    User->>ExternalImportPage: press import button
    ExternalImportPage->>Overlay: showLoading(importInProgress)
    ExternalImportPage->>AsyncErrorHandler: execute(operation: importTasks)

    AsyncErrorHandler->>Mediator: send(ImportTasksCommand)
    Mediator->>ImportHandler: handle(ImportTasksCommand)

    loop rows in CSV
        ImportHandler->>ImportHandler: _mapRowToSaveCommand(row, type, colIndices, parentId)
        alt type == todoist
            ImportHandler->>ImportHandler: _extractTodoistTags(content)
            ImportHandler->>ImportHandler: _cleanTodoistTitle(content)
            ImportHandler->>ImportHandler: _getOrCreateTagIds(combinedTags)
        else type == generic
            ImportHandler->>ImportHandler: _getOrCreateTagIds(TAGS)
        end

        loop tags
            ImportHandler->>Mediator: send(GetListTagsQuery)
            Mediator->>TagQuery: handle(GetListTagsQuery)
            TagQuery-->>Mediator: GetListTagsQueryResponse
            alt tag exists (case-insensitive)
                Mediator-->>ImportHandler: existing Tag.id
            else create new tag
                ImportHandler->>Mediator: send(SaveTagCommand)
                Mediator->>TagSave: handle(SaveTagCommand)
                TagSave-->>Mediator: SaveTagCommandResponse(id)
                Mediator-->>ImportHandler: new tag id
            end
        end

        ImportHandler->>Mediator: send(SaveTaskCommand with tagIdsToAdd)
        Mediator->>TaskSave: handle(SaveTaskCommand)
        TaskSave-->>Mediator: SaveTaskCommandResponse(id)
        Mediator-->>ImportHandler: SaveTaskCommandResponse
        ImportHandler->>ImportHandler: update parentIdsByIndent
    end

    ImportHandler-->>Mediator: ImportTasksCommandResponse
    Mediator-->>AsyncErrorHandler: ImportTasksCommandResponse

    alt import success or partial success
        AsyncErrorHandler-->>ExternalImportPage: response
        ExternalImportPage->>Overlay: hideNotification
        ExternalImportPage->>Overlay: showSuccess(message)
        ExternalImportPage->>ExternalImportPage: Navigator.pop()
    else error in AsyncErrorHandler
        AsyncErrorHandler-->>ExternalImportPage: null
        ExternalImportPage->>Overlay: showError(message)
    end

    ExternalImportPage->>ExternalImportPage: setState(_isProcessing = false)
Loading

Updated class diagram for ImportTasksCommandHandler and external import UI

classDiagram
    direction LR

    class ImportErrorIds {
      <<static>> String fileReadError
      <<static>> String csvParseError
      <<static>> String missingRequiredColumn
      <<static>> String dateParseError
      <<static>> String invalidFilePath
      <<static>> String mediatorError
    }

    class ImportErrorMessages {
      <<static>> String fileNotFound
      <<static>> String fileTooLarge
      <<static>> String fileReadError
      <<static>> String csvParseError
      <<static>> String missingRequiredColumn
      <<static>> String mediatorError
      <<static>> String rowImportError
      <<static>> String invalidFilePath
      <<static>> String emptyFilePath
      <<static>> String negativeCount
    }

    class TaskImportType {
      <<enum>>
      generic
      todoist
    }

    class ImportTasksCommand {
      +String filePath
      +TaskImportType importType
      +ImportTasksCommand(String filePath, TaskImportType importType)
    }

    class ImportTasksCommandResponse {
      +int successCount
      +int failureCount
      +List~String~ errors
      +ImportTasksCommandResponse(int successCount, int failureCount, List~String~ errors)
    }

    class ImportTasksCommandHandler {
      -Mediator _mediator
      +ImportTasksCommandHandler(Mediator mediator)
      +Future~ImportTasksCommandResponse~ handle(ImportTasksCommand request)
      -String~null~ _validateFilePath(String filePath)
      -Map~String,int~ _getColumnIndices(TaskImportType type, List~dynamic~ header)
      -int _getIndent(List~dynamic~ row, int~null~ indentIndex, TaskImportType type)
      -Future~SaveTaskCommand~ _mapRowToSaveCommand(List~dynamic~ row, TaskImportType type, Map~String,int~ colIndices, String~null~ parentId)
      -Future~SaveTaskCommand~ _mapTodoistRow(List~dynamic~ row, Map~String,int~ idx, String~null~ parentId)
      -Future~SaveTaskCommand~ _mapGenericRow(List~dynamic~ row, Map~String,int~ idx)
      -List~String~ _extractTodoistTags(String content)
      -String _cleanTodoistTitle(String content)
      -EisenhowerPriority _mapTodoistPriority(int todoistPriority)
      -Future~List~String~~ _getOrCreateTagIds(String~null~ tagsString)
      -DateTime~null~ _parseDate(String dateStr)
      -void _addError(List~String~ errors, String message)
    }

    class SaveTaskCommand {
      +String~null~ id
      +String title
      +String~null~ description
      +EisenhowerPriority~null~ priority
      +DateTime~null~ plannedDate
      +DateTime~null~ deadlineDate
      +String~null~ parentTaskId
      +List~String~~null~ tagIdsToAdd
    }

    class SaveTaskCommandResponse {
      +String id
    }

    class GetListTagsQuery {
      +int pageIndex
      +int pageSize
      +String~null~ search
    }

    class GetListTagsQueryResponse {
      +List~Tag~ items
    }

    class SaveTagCommand {
      +String~null~ id
      +String name
      +TagType type
    }

    class SaveTagCommandResponse {
      +String id
    }

    class Tag {
      +String id
      +String name
      +TagType type
    }

    class TagType {
      <<enum>>
      label
      other
    }

    class _ExternalImportPage {
      +_ExternalImportPage()
      +State createState()
    }

    class _ExternalImportPageState {
      -ITranslationService _translationService
      -IFileService _fileService
      -String~null~ _selectedTaskFilePath
      -TaskImportType _taskImportType
      -bool _isProcessing
      +Future~void~ _handleTaskFilePick(BuildContext context)
      +Future~void~ _handleExternalImportExecute(BuildContext context)
      +Widget build(BuildContext context)
    }

    class _ImportExportActionsDialogState {
      -GlobalKey~NavigatorState~ _navigatorKey
      -String~null~ _selectedFilePath
      -ExportDataFileOptions~null~ _selectedExportOption
      -bool _isProcessing
      +Widget _buildExternalImportPage(BuildContext context)
    }

    class ITranslationService {
      +String translate(String key)
      +String translate(String key, Map~String,String~ namedArgs)
    }

    class IFileService {
      +Future~String~ pickFile(List~String~ allowedExtensions, String dialogTitle)
    }

    class Mediator {
      +Future~TResponse~ send~TRequest,TResponse~(TRequest request)
    }

    ImportTasksCommand ..> ImportErrorMessages : uses
    ImportTasksCommandResponse ..> ImportErrorMessages : validates

    ImportTasksCommandHandler ..> ImportTasksCommand : handles
    ImportTasksCommandHandler ..> ImportTasksCommandResponse : returns
    ImportTasksCommandHandler ..> SaveTaskCommand : creates
    ImportTasksCommandHandler ..> GetListTagsQuery : queries
    ImportTasksCommandHandler ..> SaveTagCommand : creates
    ImportTasksCommandHandler ..> ImportErrorIds : logs
    ImportTasksCommandHandler ..> ImportErrorMessages : logs
    ImportTasksCommandHandler ..> Mediator : uses

    SaveTagCommandResponse ..> Tag : refers
    GetListTagsQueryResponse ..> Tag : contains

    _ImportExportActionsDialogState --> _ExternalImportPage : builds
    _ExternalImportPage --> _ExternalImportPageState : creates state

    _ExternalImportPageState ..> ITranslationService : uses
    _ExternalImportPageState ..> IFileService : uses
    _ExternalImportPageState ..> Mediator : sends ImportTasksCommand
    _ExternalImportPageState ..> ImportTasksCommand : constructs
    _ExternalImportPageState ..> TaskImportType : selects format

    Tag --> TagType : has type
    SaveTagCommand --> TagType : sets type
    SaveTaskCommand --> Tag : via tagIdsToAdd
Loading

File-Level Changes

Change Details Files
Refactor external task import UI into its own stateful widget and add Todoist-specific help text.
  • Replace inline external-import page in _ImportExportActionsDialog with a dedicated _ExternalImportPage stateful widget.
  • Move file-picking and import execution logic (including loading and success notifications) from the dialog state into the new page state.
  • Extend the external import page to show either generic CSV info or new Todoist CSV info cards depending on the selected TaskImportType, and wire translations for new Todoist info keys.
src/lib/presentation/ui/features/settings/components/import_export_settings.dart
src/lib/presentation/ui/features/tasks/constants/task_translation_keys.dart
src/lib/presentation/ui/features/tasks/assets/locales/en.yaml
src/lib/presentation/ui/features/tasks/assets/locales/de.yaml
src/lib/presentation/ui/features/tasks/assets/locales/es.yaml
src/lib/presentation/ui/features/tasks/assets/locales/fr.yaml
src/lib/presentation/ui/features/tasks/assets/locales/it.yaml
src/lib/presentation/ui/features/tasks/assets/locales/pt.yaml
src/lib/presentation/ui/features/tasks/assets/locales/tr.yaml
src/lib/presentation/ui/features/tasks/assets/locales/ja.yaml
src/lib/presentation/ui/features/tasks/assets/locales/zh.yaml
src/lib/presentation/ui/features/tasks/assets/locales/ru.yaml
src/lib/presentation/ui/features/tasks/assets/locales/uk.yaml
src/lib/presentation/ui/features/tasks/assets/locales/pl.yaml
src/lib/presentation/ui/features/tasks/assets/locales/cs.yaml
src/lib/presentation/ui/features/tasks/assets/locales/da.yaml
src/lib/presentation/ui/features/tasks/assets/locales/fi.yaml
src/lib/presentation/ui/features/tasks/assets/locales/nl.yaml
src/lib/presentation/ui/features/tasks/assets/locales/no.yaml
src/lib/presentation/ui/features/tasks/assets/locales/ro.yaml
src/lib/presentation/ui/features/tasks/assets/locales/sl.yaml
src/lib/presentation/ui/features/tasks/assets/locales/sv.yaml
src/lib/presentation/ui/features/tasks/assets/locales/ko.yaml
Extend ImportTasksCommandHandler to support tag extraction/creation and improve robustness and error typing.
  • Promote internal import error ID/message classes to public so they can be referenced externally and standardize their usage across the handler.
  • Make the row-to-command mapping pipeline async, routing through new async helpers for Todoist and generic formats.
  • For Todoist CSV, parse @tags from CONTENT, merge them with LABELS column values, normalize them into a unique set, and resolve them to tag IDs via _getOrCreateTagIds before attaching to SaveTaskCommand.tagIdsToAdd.
  • For generic CSV, parse a TAGS column and resolve its comma-separated names to tag IDs via _getOrCreateTagIds before attaching to SaveTaskCommand.tagIdsToAdd.
  • Introduce _getOrCreateTagIds which uses GetListTagsQuery to find existing tags case-insensitively and falls back to creating new TagType.label entries via SaveTagCommand when none match.
  • Adjust mediator calls to be fully typed (send<Command,Response>) and improve error logging paths (including date parse, mediator, and row import errors) while preserving max-error capping and file-path validation logic.
src/lib/core/application/features/tasks/commands/import_tasks_command.dart
Broaden and tighten unit tests for ImportTasksCommandHandler including tag behavior and error handling.
  • Reset mock mediator before each test to avoid cross-test interference and switch to predicate-based argument matching so expectations assert on command shape instead of call order.
  • Add Todoist CSV tests that verify hierarchy mapping, priority mapping, mixed and non-sequential indents, missing CONTENT handling, and that non-task rows are skipped.
  • Add new tests that cover Todoist tag import: @tags in CONTENT plus LABELS column, verifying SaveTagCommand invocations and that SaveTaskCommand receives the collected tag IDs.
  • Add new tests for generic CSV tag import via TAGS column with tag creation and attachment to SaveTaskCommand.
  • Strengthen error-path tests, including file-not-found, mediator exceptions, missing CONTENT columns, path traversal detection, and enforcing the capped error list size.
src/test/core/application/features/tasks/commands/import_tasks_command_test.dart
Add widget test coverage for the updated settings import/export UI behavior around external imports.
  • Introduce a widget test that boots ImportExportSettings with mocked container services and verifies navigation through Import -> External Apps.
  • Assert that the external import page initially shows the generic import info description and not the Todoist description when first opened (validating default selection wiring).
  • Lay groundwork (commented-out interactions) for later extending the test to actually change the dropdown selection to Todoist once overlay interaction is resolved.
src/test/presentation/ui/features/settings/components/import_export_settings_test.dart

Possibly linked issues

  • #: PR implements CSV tag column and Todoist @tag/label import exactly as requested in the issue.
  • #Importing tasks: The PR implements and extends CSV-based task import requested in the issue, including generic and Todoist formats.
  • #[FEEDBACK]: The PR adds Todoist CSV import functionality and in‑app instructions, resolving the user’s question about importing from Todoist.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In _ExternalImportPage, the back button now uses Navigator.of(context).pop() instead of the dialog’s _navigatorKey.currentState?.pop(), which may close the entire dialog instead of only popping the inner route; consider using the same navigator key routing as before to preserve the previous navigation behavior.
  • The new tag resolution logic in _getOrCreateTagIds performs a mediator query and possibly a save for every tag name encountered, which could be expensive for large imports; consider caching tagName → tagId within a single import run to avoid repeated lookups and creations for the same tag.
  • The Logger.info('DEBUG: tags=$tags'); line in _getOrCreateTagIds looks like leftover debug logging; consider removing it or replacing it with a more structured, appropriately leveled log message if it’s needed in production.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_ExternalImportPage`, the back button now uses `Navigator.of(context).pop()` instead of the dialog’s `_navigatorKey.currentState?.pop()`, which may close the entire dialog instead of only popping the inner route; consider using the same navigator key routing as before to preserve the previous navigation behavior.
- The new tag resolution logic in `_getOrCreateTagIds` performs a mediator query and possibly a save for every tag name encountered, which could be expensive for large imports; consider caching tagName → tagId within a single import run to avoid repeated lookups and creations for the same tag.
- The `Logger.info('DEBUG: tags=$tags');` line in `_getOrCreateTagIds` looks like leftover debug logging; consider removing it or replacing it with a more structured, appropriately leveled log message if it’s needed in production.

## Individual Comments

### Comment 1
<location> `src/lib/core/application/features/tasks/commands/import_tasks_command.dart:499-508` </location>
<code_context>
+  Future<List<String>?> _getOrCreateTagIds(String? tagsString) async {
</code_context>

<issue_to_address>
**suggestion (performance):** Tag resolution does repeated per-tag queries without caching, which may become slow on larger imports.

Each tag in each row triggers a `GetListTagsQuery` (with `pageSize: 1`) and possibly `SaveTagCommand`, so imports with many rows/reused tags do O(rows * tags-per-row) mediator calls. Within a single import run, cache tagName → id (e.g., a `Map<String, String>` in the handler) so once a tag is resolved/created you reuse its id for subsequent rows, avoiding redundant queries and speeding up large imports.

Suggested implementation:

```
    final List<String> tagIds = [];
    for (final tagName in tags) {
      // Reuse cached tag id within a single import run if available
      if (_tagIdCache.containsKey(tagName)) {
        tagIds.add(_tagIdCache[tagName]!);
        continue;
      }

      // Search for existing tag
      final query = GetListTagsQuery(

```

1. Add a cache field to the command handler class that owns `_getOrCreateTagIds`, for example:
   - `final Map<String, String> _tagIdCache = {};`
   Place it as a private field next to the other dependencies/fields (e.g., near the mediator).
2. Wherever in `_getOrCreateTagIds` you currently:
   - Resolve an existing tag id from the `GetListTagsQuery` result, and
   - Create a new tag via `SaveTagCommand` (or similar),
   add lines to populate the cache, e.g.:
   - After finding an existing tag: `_tagIdCache[tagName] = resolvedTagId;`
   - After creating a new tag: `_tagIdCache[tagName] = createdTagId;`
3. Ensure `_tagIdCache` lives for the duration of a single import run (i.e., per handler instance or per import command execution) so that multiple rows share the same cache, but it is not reused across independent imports.
</issue_to_address>

### Comment 2
<location> `src/lib/core/application/features/tasks/commands/import_tasks_command.dart:503` </location>
<code_context>
+    if (tagsString == null || tagsString.trim().isEmpty) return null;
+
+    final tags = tagsString.split(',').map((t) => t.trim()).where((t) => t.isNotEmpty).toList();
+    Logger.info('DEBUG: tags=$tags');
+    if (tags.isEmpty) return null;
+
</code_context>

<issue_to_address>
**🚨 suggestion (security):** The hardcoded 'DEBUG' log message may be too noisy for production and doesn’t add much value.

This logs the tags for every imported row, which will bloat logs and may leak user data. If this isn’t strictly temporary, please remove it or hide it behind a dedicated debug/trace flag.

Suggested implementation:

```
  Future<List<String>?> _getOrCreateTagIds(String? tagsString) async {
    if (tagsString == null || tagsString.trim().isEmpty) return null;

    final tags = tagsString.split(',').map((t) => t.trim()).where((t) => t.isNotEmpty).toList();

```

```
    final tags = tagsString.split(',').map((t) => t.trim()).where((t) => t.isNotEmpty).toList();
    if (tags.isEmpty) return null;

```
</issue_to_address>

### Comment 3
<location> `src/lib/core/application/features/tasks/commands/import_tasks_command.dart:429-431` </location>
<code_context>
     );
   }

+  List<String> _extractTodoistTags(String content) {
+    final tagRegex = RegExp(r'@(\S+)');
+    return tagRegex.allMatches(content).map((m) => m.group(1)!).toList();
+  }
+
</code_context>

<issue_to_address>
**issue:** Todoist tag extraction may include trailing punctuation or other separators in the tag names.

Because `@(
\S+)` matches all non-whitespace after `@`, content like `"Buy milk @groceries,"` yields the tag `"groceries,"` (including the comma), which is then used for lookup/creation. If punctuation commonly follows tags, consider either tightening the regex (e.g., stop at punctuation) or normalizing the captured value by stripping trailing punctuation before using it as a tag name.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/lib/core/application/features/tasks/commands/import_tasks_command.dart Outdated
Comment thread src/lib/core/application/features/tasks/commands/import_tasks_command.dart Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly enhances the CSV import functionality by adding support for tags for both generic and Todoist-specific formats. The implementation includes parsing tags from CSV columns and from the task content for Todoist imports, with logic to create new tags if they don't exist. The UI for import/export has been refactored into a separate, more maintainable widget, which is a great improvement. The test suite has also been expanded to cover the new tag functionality and to improve existing test cases.

My main feedback is on the implementation of the tag creation logic, where a piece of code could be refactored for better readability and maintainability. Overall, this is a solid contribution that adds valuable functionality and improves the codebase.

Comment on lines +517 to +527
final existingTag = response.items.isEmpty
? null
: response.items.firstWhere(
(t) => t.name.toLowerCase() == tagName.toLowerCase(),
orElse: () => response.items.first, // Fallback to first if only one result and it's close?
// Actually firstWhere with orElse is safer.
);

// Re-verify if the name matches exactly (case-insensitive)
if (existingTag != null && existingTag.name.toLowerCase() == tagName.toLowerCase()) {
tagIds.add(existingTag.id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic to find an existing tag is a bit complex and hard to read due to the orElse clause and the subsequent re-verification. The comments also indicate some uncertainty. This can be simplified for better clarity and maintainability by using cast and firstWhere with an orElse that returns null.

      final existingTag = response.items.cast<TagListItem?>().firstWhere(
        (t) => t!.name.toLowerCase() == tagName.toLowerCase(),
        orElse: () => null,
      );

      if (existingTag != null) {
        tagIds.add(existingTag.id);

Add in-memory cache for resolved tag IDs during CSV import operations.
This prevents redundant database queries for tags that appear multiple
times across imported rows, significantly improving performance on
large imports.

- Add _tagIdCache field to ImportTasksCommandHandler
- Cache tag IDs by normalized name (lowercase)
- Populate cache after successful lookup or creation
- Check cache before querying for existing tags

Closes issue raised in PR review about repeated per-tag queries.
@ahmet-cetinkaya ahmet-cetinkaya merged commit e1e8a11 into main Feb 9, 2026
4 checks passed
@ahmet-cetinkaya ahmet-cetinkaya added P1 - Strategic High-impact features / Critical regressions impact-critical bug Something isn't working labels Feb 17, 2026
@ahmet-cetinkaya ahmet-cetinkaya deleted the feat/import-tags-ux-refactor branch May 22, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request P1 - Strategic High-impact features / Critical regressions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tags in csv task import

1 participant