syncfusion / flutter-widgets

Syncfusion Flutter widgets libraries include high quality UI widgets and file-format packages to help you create rich, high-quality applications for iOS, Android, and web from a single code base.
1.55k stars 757 forks source link

SFCalendar Special Regions Resize Problems #2022

Open Kuromory opened 1 month ago

Kuromory commented 1 month ago

Bug description

When I have A SpecialRegion (TimeRegion) with example lunch (12:00 - 13:00) and also an appointment that is like this:

Same goes for the other direction (13:00 - 14:00) to (13:00 - 15:00)

Steps to reproduce

  1. create a TimeRegion with interactable = false (example lunch: 12:00 - 13:00)
  2. create an Appointment before or after the TimeRegion that has either the same start or end time (example 10:00 - 12:00)
  3. enable resize in the sfcalender widget and use the timelineview
  4. resize it horizontal to the left

Code sample

Code sample ```dart import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; void main() { return runApp(const CalendarApp()); } /// The app which hosts the home page which contains the calendar on it. class CalendarApp extends StatelessWidget { const CalendarApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp(title: 'Calendar Demo', home: MyHomePage()); } } /// The hove page which hosts the calendar class MyHomePage extends StatefulWidget { /// Creates the home page to display teh calendar widget. const MyHomePage({super.key}); @override // ignore: library_private_types_in_public_api _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { final now = DateTime.now(); @override Widget build(BuildContext context) { return Scaffold( body: SfCalendar( view: CalendarView.timelineWeek, dataSource: MeetingDataSource(_getDataSource()), allowAppointmentResize: true, onAppointmentResizeEnd: _onResizeEnd, specialRegions: [ TimeRegion( text: 'lunch', startTime: DateTime(now.year, now.month, now.day, 12), endTime: DateTime(now.year, now.month, now.day, 13), enablePointerInteraction: false, // only is a bug when it is false ), ], ), ); } List _getDataSource() { final List meetings = []; final DateTime today = DateTime.now(); final DateTime startTime = DateTime(today.year, today.month, today.day, 9); final DateTime endTime = DateTime(today.year, today.month, today.day, 12); meetings.add( Meeting('Conference', startTime, endTime, const Color(0xFF0F8644), false), ); meetings.add( Meeting( 'Conference 2', endTime.add(const Duration(hours: 1)), endTime.add(const Duration(hours: 2)), const Color(0xFF0F8644), false), ); return meetings; } void _onResizeEnd(AppointmentResizeEndDetails details) { final appointment = details.appointment as Meeting; debugPrint('resize end ${appointment.from} - ${appointment.to}'); } } /// An object to set the appointment collection data source to calendar, which /// used to map the custom appointment data to the calendar appointment, and /// allows to add, remove or reset the appointment collection. class MeetingDataSource extends CalendarDataSource { /// Creates a meeting data source, which used to set the appointment /// collection to the calendar MeetingDataSource(List source) { appointments = source; } @override DateTime getStartTime(int index) { return _getMeetingData(index).from; } @override DateTime getEndTime(int index) { return _getMeetingData(index).to; } @override String getSubject(int index) { return _getMeetingData(index).eventName; } @override Color getColor(int index) { return _getMeetingData(index).background; } @override bool isAllDay(int index) { return _getMeetingData(index).isAllDay; } Meeting _getMeetingData(int index) { final dynamic meeting = appointments![index]; late final Meeting meetingData; if (meeting is Meeting) { meetingData = meeting; } return meetingData; } @override Meeting convertAppointmentToObject( Meeting customData, Appointment appointment) { return Meeting( customData.eventName, customData.from, customData.to, customData.background, customData.isAllDay, ); } } /// Custom business object class which contains properties to hold the detailed /// information about the event data which will be rendered in calendar. class Meeting { /// Creates a meeting class with required details. Meeting(this.eventName, this.from, this.to, this.background, this.isAllDay); /// Event name which is equivalent to subject property of [Appointment]. String eventName; /// From which is equivalent to start time property of [Appointment]. DateTime from; /// To which is equivalent to end time property of [Appointment]. DateTime to; /// Background which is equivalent to color property of [Appointment]. Color background; /// IsAllDay which is equivalent to isAllDay property of [Appointment]. bool isAllDay; } ```

Screenshots or Video

Screenshots / Video demonstration no media

Stack Traces

Stack Traces no stacktrace

On which target platforms have you observed this bug?

Windows

Flutter Doctor output

Doctor output ```console [√] Flutter (Channel stable, 3.24.0, on Microsoft Windows [Version 10.0.22631.3880], locale de-CH) • Flutter version 3.24.0 on channel stable at D:\programming tools\flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 80c2e84975 (13 days ago), 2024-07-30 23:06:49 +0700 • Engine revision b8800d88be • Dart version 3.5.0 • DevTools version 2.37.2 [√] Windows Version (Installed version of Windows is version 10 or higher) [!] Android toolchain - develop for Android devices (Android SDK version 33.0.2) • Android SDK at C:\Users\Kuromory\AppData\Local\Android\sdk • Platform android-33, build-tools 33.0.2 X No Java Development Kit (JDK) found; You must have the environment variable JAVA_HOME set and the java binary in your PATH. You can download the JDK from https://www.oracle.com/technetwork/java/javase/downloads/. [√] Chrome - develop for the web • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe [√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.9.0) • Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community • Visual Studio Community 2022 version 17.9.34607.119 • Windows 10 SDK version 10.0.22621.0 [!] Android Studio (not installed) • Android Studio not found; download from https://developer.android.com/studio/index.html (or visit https://flutter.dev/to/windows-android-setup for detailed instructions). [√] VS Code (version 1.90.2) • VS Code at C:\Users\Kuromory\AppData\Local\Programs\Microsoft VS Code • Flutter extension version 3.90.0 [√] Connected device (3 available) • Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.22631.3880] • Chrome (web) • chrome • web-javascript • Google Chrome 126.0.6478.63 • Edge (web) • edge • web-javascript • Microsoft Edge 126.0.2592.87 [√] Network resources • All expected network resources are available. ! Doctor found issues in 2 categories. ```
PreethikaSelvam commented 1 week ago

Hi @Kuromory,

We have analyzed your code snippet and found that the issue with appointment resizing in your code was due to inconsistent handling of Appointment and Meeting types. The MeetingDataSource was not correctly mapping Meeting objects to Appointment objects, and the onAppointmentResizeEnd callback was not processing resize events properly.

We resolved this by updating the _onResizeEnd method to correctly identify and handle both Appointment and Meeting types, ensuring accurate logging and display. The MeetingDataSource was revised to consistently map Meeting objects to Appointment objects with proper type checking, and we have adjusted the convertAppointmentToObject method to maintain custom data when converting between types. These changes ensure that appointment resizing functions correctly. We have shared a modified code snippet and a sample for your reference.

Code snippet:

final DateTime now = DateTime.now();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SfCalendar(
        view: CalendarView.timelineWeek,
        dataSource: MeetingDataSource(_getDataSource()),
        allowAppointmentResize: true,
        onAppointmentResizeEnd: _onResizeEnd,
        specialRegions: [
          TimeRegion(
            text: 'Lunch',
            startTime: DateTime(now.year, now.month, now.day, 12),
            endTime: DateTime(now.year, now.month, now.day, 13),
          ),
        ],
      ),
    );
  }

  List<Meeting> _getDataSource() {
    final List<Meeting> meetings = <Meeting>[];
    final DateTime today = DateTime.now();
    final DateTime startTime = DateTime(today.year, today.month, today.day, 9);
    final DateTime endTime = DateTime(today.year, today.month, today.day, 12);
    meetings.add(
      Meeting('Conference', startTime, endTime, const Color(0xFF0F8644), false),
    );
    meetings.add(
      Meeting(
        'Conference 2',
        endTime.add(const Duration(hours: 1)),
        endTime.add(const Duration(hours: 2)),
        const Color(0xFF0F8644),
        false,
      ),
    );
    return meetings;
  }

  void _onResizeEnd(AppointmentResizeEndDetails appointmentResizeEndDetails) {
    final dynamic appointment = appointmentResizeEndDetails.appointment;

    if (appointment is Appointment) {
      debugPrint(
          'Resize end: ${appointment.startTime} - ${appointment.endTime}');
      print('Appointment: ${appointment.subject}');
    } else if (appointment is Meeting) {
      debugPrint('Resize end: ${appointment.from} - ${appointment.to}');
      print('Appointment: ${appointment.eventName}');
    }
  }
}

class MeetingDataSource extends CalendarDataSource {
  MeetingDataSource(List<Meeting> source) {
    appointments = source
        .map((meeting) => Appointment(
              startTime: meeting.from,
              endTime: meeting.to,
              subject: meeting.eventName,
              color: meeting.background,
              isAllDay: meeting.isAllDay,
            ))
        .toList();
  }

  @override
  DateTime getStartTime(int index) {
    final dynamic appointment = appointments![index];
    if (appointment is Appointment) {
      return appointment.startTime;
    } else if (appointment is Meeting) {
      return appointment.from;
    }
    throw Exception('Invalid appointment type');
  }

  @override
  DateTime getEndTime(int index) {
    final dynamic appointment = appointments![index];
    if (appointment is Appointment) {
      return appointment.endTime;
    } else if (appointment is Meeting) {
      return appointment.to;
    }
    throw Exception('Invalid appointment type');
  }

  @override
  String getSubject(int index) {
    final dynamic appointment = appointments![index];
    if (appointment is Appointment) {
      return appointment.subject;
    } else if (appointment is Meeting) {
      return appointment.eventName;
    }
    throw Exception('Invalid appointment type');
  }

  @override
  Color getColor(int index) {
    final dynamic appointment = appointments![index];
    if (appointment is Appointment) {
      return appointment.color;
    } else if (appointment is Meeting) {
      return appointment.background;
    }
    throw Exception('Invalid appointment type');
  }

  @override
  bool isAllDay(int index) {
    final dynamic appointment = appointments![index];
    if (appointment is Appointment) {
      return appointment.isAllDay;
    } else if (appointment is Meeting) {
      return appointment.isAllDay;
    }
    throw Exception('Invalid appointment type');
  }

  @override
  Object? convertAppointmentToObject(
      Object? customData, Appointment appointment) {
    if (customData is Meeting) {
      return Meeting(
        customData.eventName,
        appointment.startTime,
        appointment.endTime,
        customData.background,
        customData.isAllDay,
      );
    }
    throw Exception('Invalid customData type');
  }
}

class Meeting {
  Meeting(this.eventName, this.from, this.to, this.background, this.isAllDay);

  String eventName;
  DateTime from;
  DateTime to;
  Color background;
  bool isAllDay;
}

Output:

gh2022-ezgif com-video-to-gif-converter

If you still face any issue after modifying the code, we kindly request you to try replicating the reported issue in the test sample attached below. Please revert to us so that we can assist you in a better way.

Regards, Preethika Selvam. gh2022.zip

Kuromory commented 1 week ago

Hey @PreethikaSelvam

Thank you for your time and reply :)

I have stated that the issue is only when I have the TimeRegion flag of enablePointerInteraction to false. I saw that my code snipped missed it somehow and changed it so I after again testing with only this code snipped I can assure you that the problem still exists...

PreethikaSelvam commented 5 days ago

Hi @Kuromory,

According to our current implementation, the enablePointerInteraction property of TimeRegion is used to enable or disable touch interaction. By default, its value is set to true. When the enablePointerInteraction property of TimeRegion is set to false, touch interaction is disabled, and no further interaction will occur. Consequently, you cannot perform the resize event. If you need to resize the appointment, you must enable the enablePointerInteraction property of TimeRegion by setting it to true. We have shared user guide documentation for your reference.

UG: https://help.syncfusion.com/flutter/calendar/timeslot-views#selection-restriction-in-timeslots

If you still encounter the issue after enabling the enablePointerInteraction property of the TimeRegion, we kindly request that you try to replicate the reported issue in the below attached test sample and revert us so that it will help us assist you in a better way.

Regards, Preethika Selvam. gh2022.zip

Kuromory commented 5 days ago

hey @PreethikaSelvam

I see where we did diffrent turns, I used the details of the AppointmentResizeEndDetails in onResizeEnd for the change which only works when it does not stay at the side of a TimeRegion... here is the updated code you will see Confrence 3 you can resize but Confrence 1 and 2 don't work...

import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';

void main() {
  return runApp(const CalendarApp());
}

/// The app which hosts the home page which contains the calendar on it.
class CalendarApp extends StatelessWidget {
  const CalendarApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(title: 'Calendar Demo', home: MyHomePage());
  }
}

/// The hove page which hosts the calendar
class MyHomePage extends StatefulWidget {
  /// Creates the home page to display teh calendar widget.
  const MyHomePage({super.key});

  @override
  // ignore: library_private_types_in_public_api
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final now = DateTime.now();
  final _dataSource = MeetingDataSource(_getDataSource());
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SfCalendar(
        view: CalendarView.timelineWeek,
        dataSource: _dataSource,
        allowAppointmentResize: true,
        onAppointmentResizeEnd: _onResizeEnd,
        specialRegions: [
          TimeRegion(
            text: 'lunch',
            startTime: DateTime(now.year, now.month, now.day, 12),
            endTime: DateTime(now.year, now.month, now.day, 13),
            enablePointerInteraction: false,
          ),
        ],
      ),
    );
  }

  static List<Meeting> _getDataSource() {
    final List<Meeting> meetings = <Meeting>[];
    final DateTime today = DateTime.now();
    final DateTime startTime = DateTime(today.year, today.month, today.day, 9);
    final DateTime endTime = DateTime(today.year, today.month, today.day, 12);
    meetings.add(
      Meeting('Conference', startTime, endTime, const Color(0xFF0F8644), false),
    );
    meetings.add(
      Meeting(
        'Conference 2',
        endTime.add(const Duration(hours: 1)),
        endTime.add(const Duration(hours: 2)),
        const Color(0xFF0F8644),
        false,
      ),
    );
    meetings.add(
      Meeting(
        'Conference 3',
        endTime.add(const Duration(hours: 4)),
        endTime.add(const Duration(hours: 6)),
        const Color(0xFF0F8644),
        false,
      ),
    );
    return meetings;
  }

  void _onResizeEnd(AppointmentResizeEndDetails details) {
    final appointment = details.appointment as Meeting;
    debugPrint('resize start ${details.startTime} - ${details.endTime}');
    debugPrint('resize end ${appointment.from} - ${appointment.to}');
    appointment.from = details.startTime ?? appointment.from;
    appointment.to = details.endTime ?? appointment.to;
    _dataSource.notifyListeners(
        CalendarDataSourceAction.reset, _dataSource.appointments!);
  }
}

/// An object to set the appointment collection data source to calendar, which
/// used to map the custom appointment data to the calendar appointment, and
/// allows to add, remove or reset the appointment collection.
class MeetingDataSource extends CalendarDataSource<Meeting> {
  /// Creates a meeting data source, which used to set the appointment
  /// collection to the calendar
  MeetingDataSource(List<Meeting> source) {
    appointments = source;
  }

  @override
  DateTime getStartTime(int index) {
    return _getMeetingData(index).from;
  }

  @override
  DateTime getEndTime(int index) {
    return _getMeetingData(index).to;
  }

  @override
  String getSubject(int index) {
    return _getMeetingData(index).eventName;
  }

  @override
  Color getColor(int index) {
    return _getMeetingData(index).background;
  }

  @override
  bool isAllDay(int index) {
    return _getMeetingData(index).isAllDay;
  }

  Meeting _getMeetingData(int index) {
    final dynamic meeting = appointments![index];
    late final Meeting meetingData;
    if (meeting is Meeting) {
      meetingData = meeting;
    }

    return meetingData;
  }

  @override
  Meeting convertAppointmentToObject(
      Meeting customData, Appointment appointment) {
    return Meeting(
      customData.eventName,
      customData.from,
      customData.to,
      customData.background,
      customData.isAllDay,
    );
  }
}

/// Custom business object class which contains properties to hold the detailed
/// information about the event data which will be rendered in calendar.
class Meeting {
  /// Creates a meeting class with required details.
  Meeting(this.eventName, this.from, this.to, this.background, this.isAllDay);

  /// Event name which is equivalent to subject property of [Appointment].
  String eventName;

  /// From which is equivalent to start time property of [Appointment].
  DateTime from;

  /// To which is equivalent to end time property of [Appointment].
  DateTime to;

  /// Background which is equivalent to color property of [Appointment].
  Color background;

  /// IsAllDay which is equivalent to isAllDay property of [Appointment].
  bool isAllDay;
}