Skip to content

fix: preserve event duration when dragging ranged calendar events#8869

Open
hkarmoush wants to merge 1 commit into
AppFlowy-IO:mainfrom
hkarmoush:fix/calendar-range-event-drag-8859
Open

fix: preserve event duration when dragging ranged calendar events#8869
hkarmoush wants to merge 1 commit into
AppFlowy-IO:mainfrom
hkarmoush:fix/calendar-range-event-drag-8859

Conversation

@hkarmoush

@hkarmoush hkarmoush commented Jul 18, 2026

Copy link
Copy Markdown

Feature Preview

N/A (bug fix, not a new feature).

fixes #8859

What's wrong

Dragging a Calendar card that has an End Date toggled does nothing — the UI shows the drop is accepted, but the card snaps back and the row is unchanged. Cards without an End Date drag/drop fine.

Root cause

DateTypeOption::apply_changeset (in date_type_option.rs) rejects a changeset on a ranged (is_range) date cell unless timestamp and end_timestamp are supplied together — a guard added in #6582 to catch accidental partial updates coming from the date-picker widget.

The calendar drag handler (MoveCalendarEventPBmove_calendar_event_handler) only ever sends timestamp. For a ranged event this trips that guard and the whole changeset is silently dropped, which matches the reported symptom exactly.

Fix

Added DatabaseEditor::move_calendar_event(), which loads the cell being moved first. If it's ranged, it shifts end_timestamp by the same delta as the new start timestamp, so the event keeps its original duration (matching the Notion-like behavior shown in the issue) instead of tripping the single-field-update guard. Non-ranged cells are unaffected — only timestamp is sent, same as before.

Tests

Added two integration tests in flowy-database2/tests/database/cell_test/test.rs:

  • a 2-day ranged event shifts both start and end together when dragged
  • a single-date event is unaffected (end stays unset)

PR Checklist

  • My code adheres to AppFlowy's Conventions
  • I've listed at least one issue that this PR fixes in the description above.
  • I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes.
  • All existing tests are passing. (This environment doesn't have the Rust toolchain installed, so I could not run the suite locally — verified by manual code tracing against existing call sites instead. Leaving this open pending CI.)

Summary by Sourcery

Preserve the duration of ranged calendar events when they are moved, and route calendar drag operations through a new database editor helper that adjusts end dates consistently.

Bug Fixes:

  • Fix ranged calendar events failing to move when dragged by ensuring both start and end timestamps are updated together.

Enhancements:

  • Add a dedicated DatabaseEditor::move_calendar_event helper to encapsulate calendar event movement logic and duration preservation.

Tests:

  • Add integration tests to verify that dragging a ranged calendar event shifts both start and end timestamps while keeping the duration, and that single-date events remain without an end date when moved.

Dragging a calendar card with an End Date toggled silently did nothing.
DateTypeOption::apply_changeset() rejects a changeset for an is_range
date cell unless timestamp and end_timestamp are both present or both
absent (added in AppFlowy-IO#6582 to catch accidental partial updates from the
date picker). MoveCalendarEventPB only ever set timestamp, so the
whole update was dropped for ranged events.

Add DatabaseEditor::move_calendar_event(), which loads the existing
cell first and, when it's a ranged date, shifts end_timestamp by the
same delta as the new start timestamp so the event keeps its original
duration instead of collapsing to a single day.

Fixes AppFlowy-IO#8859

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements a dedicated DatabaseEditor::move_calendar_event API so that dragging calendar events updates both start and end timestamps for ranged date cells while keeping single-date behavior unchanged, and wires the calendar event handler plus tests to validate the new behavior.

Sequence diagram for moving a ranged calendar event

sequenceDiagram
  actor User
  participant UI as CalendarUI
  participant Handler as move_calendar_event_handler
  participant Editor as DatabaseEditor
  participant DB as Database

  User->>UI: Drag calendar event to new date
  UI->>Handler: MoveCalendarEventPB(timestamp, cell_path)
  Handler->>Editor: get_database_editor_with_view_id(view_id)
  Handler->>Editor: move_calendar_event(view_id, row_id, field_id, new_timestamp)
  Editor->>DB: get_cell(field_id, row_id)
  DB-->>Editor: Cell
  Editor->>Editor: DateCellData::from(Cell)
  Editor->>Editor: [if is_range] compute end_timestamp
  Editor->>DB: update_cell_with_changeset(view_id, row_id, field_id, DateCellChangeset)
  DB-->>Editor: Ok
  Editor-->>Handler: Ok
  Handler-->>UI: Ok
  UI-->>User: Event moved (duration preserved)
Loading

File-Level Changes

Change Details Files
Add a DatabaseEditor helper that moves calendar events and preserves duration for ranged date cells.
  • Introduce move_calendar_event(view_id, row_id, field_id, new_timestamp) on DatabaseEditor.
  • Load existing date cell via get_cell and convert to DateCellData to inspect is_range, timestamp, and end_timestamp.
  • For ranged cells with both timestamps set, compute a shifted end_timestamp by applying the same delta as the start timestamp.
  • Build a DateCellChangeset with the new timestamp and optional shifted end_timestamp and apply it via update_cell_with_changeset.
frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs
Update the calendar move event handler to use the new helper instead of issuing a raw DateCellChangeset with only timestamp.
  • Remove direct construction of DateCellChangeset in move_calendar_event_handler.
  • Call DatabaseEditor::move_calendar_event with the view, row, field, and target timestamp from MoveCalendarEventPB.
  • Keep error handling and manager/editor acquisition logic unchanged.
frontend/rust-lib/flowy-database2/src/event_handler.rs
Add integration tests verifying that moving calendar events shifts both start and end for ranged dates and leaves non-ranged dates single.
  • Add a test that seeds a ranged date cell with start and end two days apart, drags it forward one day via move_calendar_event, and asserts both start and end moved by one day while remaining a range.
  • Add a test that seeds a single-date cell without an end_timestamp, drags it forward one day, and asserts timestamp updated while end_timestamp remains None.
frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#8859 Allow calendar cards with an End Date (ranged events) to be successfully dragged and dropped without snapping back.
#8859 When dragging a ranged calendar event, preserve its duration by shifting the end date by the same amount as the start date.

Possibly linked issues


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

@hkarmoush
hkarmoush marked this pull request as ready for review July 20, 2026 15:21

@sourcery-ai sourcery-ai 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.

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

  • In DatabaseEditor::move_calendar_event, the get_cell result is only mapped and then used via filter/and_then, which both operate on Result rather than Option; consider using let old = DateCellData::from(&self.get_cell(field_id, row_id).await?); and then deriving an Option<i64> for end_timestamp from old to both fix compilation and make the error propagation behavior explicit.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `DatabaseEditor::move_calendar_event`, the `get_cell` result is only mapped and then used via `filter`/`and_then`, which both operate on `Result` rather than `Option`; consider using `let old = DateCellData::from(&self.get_cell(field_id, row_id).await?);` and then deriving an `Option<i64>` for `end_timestamp` from `old` to both fix compilation and make the error propagation behavior explicit.

## Individual Comments

### Comment 1
<location path="frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs" line_range="1405-1408" />
<code_context>
+  /// Moves a calendar event to `new_timestamp`. If the event's date cell spans a range
+  /// (start + end date), the end date is shifted by the same amount as the start date so the
+  /// event keeps its original duration, instead of collapsing to a single day.
+  #[tracing::instrument(level = "trace", skip_all)]
+  pub async fn move_calendar_event(
+    &self,
+    view_id: &str,
+    row_id: &RowId,
+    field_id: &str,
+    new_timestamp: i64,
+  ) -> FlowyResult<()> {
+    let old_cell_data = self
+      .get_cell(field_id, row_id)
+      .await
+      .map(|cell| DateCellData::from(&cell));
+    let end_timestamp = old_cell_data
</code_context>
<issue_to_address>
**suggestion:** Including `err` in the tracing instrumentation for `move_calendar_event` would make failures easier to diagnose.

`move_calendar_event` returns a `FlowyResult<()>` and can fail (e.g., during cell access or update). Adding `err` to the `#[tracing::instrument]` attribute, as in `notify_did_insert_database_field`, would auto-log these errors and keep diagnostics consistent for calendar move operations.

```suggestion
  /// Moves a calendar event to `new_timestamp`. If the event's date cell spans a range
  /// (start + end date), the end date is shifted by the same amount as the start date so the
  /// event keeps its original duration, instead of collapsing to a single day.
  #[tracing::instrument(level = "trace", skip_all, err)]
```
</issue_to_address>

### Comment 2
<location path="frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs" line_range="208-209" />
<code_context>
   }
 }
+
+#[tokio::test]
+async fn move_calendar_event_shifts_ranged_end_date_test() {
+  let test = DatabaseCellTest::new().await;
+  let date_field = test.get_first_field(FieldType::DateTime).await;
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for moving ranged events backward in time to cover negative deltas

Since `move_calendar_event` derives `end_timestamp` from `new_timestamp - old_timestamp`, please also add a test that moves a ranged event backward (e.g., `new_start = start - 86400`) and verifies both `timestamp` and `end_timestamp` shift back by one day. This will cover negative deltas and better protect against regressions in the delta application logic.
</issue_to_address>

### Comment 3
<location path="frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs" line_range="239-243" />
<code_context>
+    .await
+    .unwrap();
+
+  let cell = test.editor.get_cell(&date_field.id, &row_id).await.unwrap();
+  let cell_data = DateCellData::from(&cell);
+  assert_eq!(cell_data.timestamp, Some(new_start));
+  assert_eq!(cell_data.end_timestamp, Some(end + 86400));
</code_context>
<issue_to_address>
**suggestion (testing):** Consider also asserting the `is_range` flag to fully validate the ranged behavior

Also assert that `cell_data.is_range` remains `true` after the move to verify the event is still treated as a ranged date cell and not collapsed into a single-date value.

```suggestion
  let cell = test.editor.get_cell(&date_field.id, &row_id).await.unwrap();
  let cell_data = DateCellData::from(&cell);
  assert_eq!(cell_data.timestamp, Some(new_start));
  assert_eq!(cell_data.end_timestamp, Some(end + 86400));
  assert!(cell_data.is_range, "Moved calendar event should remain a ranged date cell");
}
```
</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 on lines +1405 to +1408
/// Moves a calendar event to `new_timestamp`. If the event's date cell spans a range
/// (start + end date), the end date is shifted by the same amount as the start date so the
/// event keeps its original duration, instead of collapsing to a single day.
#[tracing::instrument(level = "trace", skip_all)]

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.

suggestion: Including err in the tracing instrumentation for move_calendar_event would make failures easier to diagnose.

move_calendar_event returns a FlowyResult<()> and can fail (e.g., during cell access or update). Adding err to the #[tracing::instrument] attribute, as in notify_did_insert_database_field, would auto-log these errors and keep diagnostics consistent for calendar move operations.

Suggested change
/// Moves a calendar event to `new_timestamp`. If the event's date cell spans a range
/// (start + end date), the end date is shifted by the same amount as the start date so the
/// event keeps its original duration, instead of collapsing to a single day.
#[tracing::instrument(level = "trace", skip_all)]
/// Moves a calendar event to `new_timestamp`. If the event's date cell spans a range
/// (start + end date), the end date is shifted by the same amount as the start date so the
/// event keeps its original duration, instead of collapsing to a single day.
#[tracing::instrument(level = "trace", skip_all, err)]

Comment on lines +208 to +209
#[tokio::test]
async fn move_calendar_event_shifts_ranged_end_date_test() {

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.

suggestion (testing): Add a test for moving ranged events backward in time to cover negative deltas

Since move_calendar_event derives end_timestamp from new_timestamp - old_timestamp, please also add a test that moves a ranged event backward (e.g., new_start = start - 86400) and verifies both timestamp and end_timestamp shift back by one day. This will cover negative deltas and better protect against regressions in the delta application logic.

Comment on lines +239 to +243
let cell = test.editor.get_cell(&date_field.id, &row_id).await.unwrap();
let cell_data = DateCellData::from(&cell);
assert_eq!(cell_data.timestamp, Some(new_start));
assert_eq!(cell_data.end_timestamp, Some(end + 86400));
}

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.

suggestion (testing): Consider also asserting the is_range flag to fully validate the ranged behavior

Also assert that cell_data.is_range remains true after the move to verify the event is still treated as a ranged date cell and not collapsed into a single-date value.

Suggested change
let cell = test.editor.get_cell(&date_field.id, &row_id).await.unwrap();
let cell_data = DateCellData::from(&cell);
assert_eq!(cell_data.timestamp, Some(new_start));
assert_eq!(cell_data.end_timestamp, Some(end + 86400));
}
let cell = test.editor.get_cell(&date_field.id, &row_id).await.unwrap();
let cell_data = DateCellData::from(&cell);
assert_eq!(cell_data.timestamp, Some(new_start));
assert_eq!(cell_data.end_timestamp, Some(end + 86400));
assert!(cell_data.is_range, "Moved calendar event should remain a ranged date cell");
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Cards with End Date toggled can not be dragged and dropped across Calendar view

1 participant