From 77d58a81fdfe9f78e20246432ad2ca214eca2934 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sun, 30 Apr 2023 20:50:24 +0800 Subject: [PATCH] chore: grid row page detail redesign (#2351) * chore: grid row page detail update * chore: update row_detail.dart Co-authored-by: Alex Wallen * chore: more adaptive and code cleanup * feat: duplicate row * feat: duplicate calendar event * fix: ci * feat: show other options * fix: show include time * fix: add key in RowCard to avoid incorrect data when open the row page --------- Co-authored-by: Alex Wallen Co-authored-by: nathan --- .../assets/translations/en.json | 3 +- .../application/cell/cell_controller.dart | 5 +- .../application/database_controller.dart | 9 +- .../application/view/view_cache.dart | 3 +- .../board/application/board_bloc.dart | 2 +- .../board/presentation/board_page.dart | 10 +- .../calendar/application/calendar_bloc.dart | 62 +++-- .../calendar/presentation/calendar_day.dart | 223 ++++++++++------ .../calendar/presentation/calendar_page.dart | 11 +- .../grid/application/grid_bloc.dart | 2 +- .../grid/application/row/row_detail_bloc.dart | 25 +- .../database_view/widgets/card/card.dart | 103 +++++--- .../database_view/widgets/card/card_bloc.dart | 67 ++--- .../widgets/card/card_cell_builder.dart | 13 +- .../widgets/card/cells/card_cell.dart | 51 +++- .../card/cells/checkbox_card_cell.dart | 17 +- .../widgets/card/cells/date_card_cell.dart | 13 +- .../card/cells/select_option_card_cell.dart | 8 +- .../widgets/card/cells/text_card_cell.dart | 19 +- .../card/container/card_container.dart | 4 +- .../database_view/widgets/row/row_detail.dart | 248 ++++++++++-------- .../lib/workspace/application/appearance.dart | 2 +- .../flowy-database/src/event_handler.rs | 4 +- .../src/services/cell/cell_operation.rs | 6 +- .../src/services/database/database_editor.rs | 26 +- .../date_type_option_entities.rs | 3 +- .../src/services/row/row_builder.rs | 18 +- 27 files changed, 611 insertions(+), 346 deletions(-) diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index df13c48c96..ce5b2ef9e4 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -308,7 +308,8 @@ "textPlaceholder": "Empty", "copyProperty": "Copied property to clipboard", "count": "Count", - "newRow": "New row" + "newRow": "New row", + "action": "Action" }, "selectOption": { "create": "Create", diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart index 804d0b8b81..ff391713be 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart @@ -76,10 +76,7 @@ class CellController extends Equatable { _cellListener?.start( onCellChanged: (result) { result.fold( - (_) { - _cellCache.remove(_cacheKey); - _loadData(); - }, + (_) => _loadData(), (err) => Log.error(err), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart index 15976a74f3..648621e869 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -115,7 +115,7 @@ class DatabaseController { } } - void addListener({ + void setListener({ DatabaseCallbacks? onDatabaseChanged, LayoutCallbacks? onLayoutChanged, GroupCallbacks? onGroupChanged, @@ -211,6 +211,11 @@ class DatabaseController { await _databaseViewBackendSvc.closeView(); await fieldController.dispose(); await groupListener.stop(); + await _viewCache.dispose(); + _databaseCallbacks = null; + _groupCallbacks = null; + _layoutCallbacks = null; + _calendarLayoutCallbacks = null; } Future _loadGroups() async { @@ -251,7 +256,7 @@ class DatabaseController { _databaseCallbacks?.onRowsCreated?.call(ids); }, ); - _viewCache.addListener(callbacks); + _viewCache.setListener(callbacks); } void _listenOnFieldsChanged() { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart index 5d8251c82f..1bdc96219f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart @@ -111,9 +111,10 @@ class DatabaseViewCache { Future dispose() async { await _databaseViewListener.stop(); await _rowCache.dispose(); + _callbacks = null; } - void addListener(DatabaseViewCallbacks callbacks) { + void setListener(DatabaseViewCallbacks callbacks) { _callbacks = callbacks; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index eab6093a4a..5c2fa3edd1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -236,7 +236,7 @@ class BoardBloc extends Bloc { }, ); - _databaseController.addListener( + _databaseController.setListener( onDatabaseChanged: onDatabaseChanged, onGroupChanged: onGroupChanged, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index f50213b73a..bf4ee07f99 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -78,7 +78,7 @@ class BoardContent extends StatefulWidget { class _BoardContentState extends State { late AppFlowyBoardScrollController scrollManager; - final cardConfiguration = CardConfiguration(); + final renderHook = RowCardRenderHook(); final config = const AppFlowyBoardConfig( groupBackgroundColor: Color(0xffF7F8FC), @@ -87,7 +87,7 @@ class _BoardContentState extends State { @override void initState() { scrollManager = AppFlowyBoardScrollController(); - cardConfiguration.addSelectOptionHook((options, groupId) { + renderHook.addSelectOptionHook((options, groupId, _) { // The cell should hide if the option id is equal to the groupId. final isInGroup = options.where((element) => element.id == groupId).isNotEmpty; @@ -254,15 +254,15 @@ class _BoardContentState extends State { key: ValueKey(groupItemId), margin: config.cardPadding, decoration: _makeBoxDecoration(context), - child: Card( + child: RowCard( row: rowPB, viewId: viewId, rowCache: rowCache, cardData: groupData.group.groupId, - fieldId: groupItem.fieldInfo.id, + groupingFieldId: groupItem.fieldInfo.id, isEditing: isEditing, cellBuilder: cellBuilder, - configuration: cardConfiguration, + renderHook: renderHook, openCard: (context) => _openCard( viewId, fieldController, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart index 76bb1655ae..ab59d14596 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart @@ -55,6 +55,13 @@ class CalendarBloc extends Bloc { createEvent: (DateTime date, String title) async { await _createEvent(date, title); }, + didCreateEvent: (CalendarEventData event) { + emit( + state.copyWith( + createdEvent: event, + ), + ); + }, updateCalendarLayoutSetting: (CalendarLayoutSettingsPB layoutSetting) async { await _updateCalendarLayoutSetting(layoutSetting); @@ -74,14 +81,6 @@ class CalendarBloc extends Bloc { ), ); }, - didReceiveNewEvent: (CalendarEventData event) { - emit( - state.copyWith( - allEvents: [...state.allEvents, event], - newEvent: event, - ), - ); - }, didDeleteEvents: (List deletedRowIds) { var events = [...state.allEvents]; events.retainWhere( @@ -94,11 +93,25 @@ class CalendarBloc extends Bloc { ), ); }, + didReceiveEvent: (CalendarEventData event) { + emit( + state.copyWith( + allEvents: [...state.allEvents, event], + newEvent: event, + ), + ); + }, ); }, ); } + @override + Future close() async { + await _databaseController.dispose(); + return super.close(); + } + FieldInfo? _getCalendarFieldInfo(String fieldId) { final fieldInfos = _databaseController.fieldController.fieldInfos; final index = fieldInfos.indexWhere( @@ -142,17 +155,27 @@ class CalendarBloc extends Bloc { final dateField = _getCalendarFieldInfo(settings.layoutFieldId); final titleField = _getTitleFieldInfo(); if (dateField != null && titleField != null) { - final result = await _databaseController.createRow( + final newRow = await _databaseController.createRow( withCells: (builder) { builder.insertDate(dateField, date); builder.insertText(titleField, title); }, + ).then( + (result) => result.fold( + (newRow) => newRow, + (err) { + Log.error(err); + return null; + }, + ), ); - return result.fold( - (newRow) {}, - (err) => Log.error(err), - ); + if (newRow != null) { + final event = await _loadEvent(newRow.id); + if (event != null && !isClosed) { + add(CalendarEvent.didCreateEvent(event)); + } + } } }, ); @@ -247,7 +270,7 @@ class CalendarBloc extends Bloc { for (final id in ids) { final event = await _loadEvent(id); if (event != null && !isClosed) { - add(CalendarEvent.didReceiveNewEvent(event)); + add(CalendarEvent.didReceiveEvent(event)); } } }), @@ -275,7 +298,7 @@ class CalendarBloc extends Bloc { onCalendarLayoutChanged: _didReceiveNewLayoutField, ); - _databaseController.addListener( + _databaseController.setListener( onDatabaseChanged: onDatabaseChanged, onLayoutChanged: onLayoutChanged, onCalendarLayoutChanged: onCalendarLayoutFieldChanged, @@ -318,10 +341,15 @@ class CalendarEvent with _$CalendarEvent { ) = _DidUpdateEvent; // Called after creating a new event - const factory CalendarEvent.didReceiveNewEvent( + const factory CalendarEvent.didCreateEvent( CalendarEventData event, ) = _DidReceiveNewEvent; + // Called when receive a new event + const factory CalendarEvent.didReceiveEvent( + CalendarEventData event, + ) = _DidReceiveEvent; + // Called when deleting events const factory CalendarEvent.didDeleteEvents(List rowIds) = _DidDeleteEvents; @@ -349,6 +377,7 @@ class CalendarState with _$CalendarState { required Option database, required Events allEvents, required Events initialEvents, + CalendarEventData? createdEvent, CalendarEventData? newEvent, required List deleteEventIds, CalendarEventData? updateEvent, @@ -391,5 +420,6 @@ class CalendarDayEvent { final CellIdentifier cellId; String get eventId => cellId.rowId; + String get fieldId => cellId.fieldId; CalendarDayEvent({required this.cellId, required this.event}); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart index e5a4597d76..56f3e3fcc7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart @@ -1,20 +1,20 @@ import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card.dart'; import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; -import 'package:appflowy/plugins/database_view/widgets/card/cells/text_card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../grid/presentation/layout/sizes.dart'; +import '../../widgets/row/cells/select_option_cell/extension.dart'; import '../application/calendar_bloc.dart'; class CalendarDayCard extends StatelessWidget { @@ -23,11 +23,10 @@ class CalendarDayCard extends StatelessWidget { final bool isInMonth; final DateTime date; final RowCache _rowCache; - final CardCellBuilder _cellBuilder; final List events; final void Function(DateTime) onCreateEvent; - CalendarDayCard({ + const CalendarDayCard({ required this.viewId, required this.isToday, required this.isInMonth, @@ -37,7 +36,6 @@ class CalendarDayCard extends StatelessWidget { required this.events, Key? key, }) : _rowCache = rowCache, - _cellBuilder = CardCellBuilder(rowCache.cellCache), super(key: key); @override @@ -50,42 +48,38 @@ class CalendarDayCard extends StatelessWidget { return ChangeNotifierProvider( create: (_) => _CardEnterNotifier(), builder: (context, child) { - final children = events.map((event) { - return _DayEventCell( - event: event, - viewId: viewId, - onClick: () => _showRowDetailPage(event, context), - child: _cellBuilder.buildCell( - cellId: event.cellId, - styles: {FieldType.RichText: TextCardCellStyle(10)}, + List cards = _buildCards(context); + + Widget? multipleCards; + if (cards.isNotEmpty) { + multipleCards = Flexible( + child: ListView.separated( + itemBuilder: (BuildContext context, int index) { + return cards[index]; + }, + itemCount: cards.length, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + separatorBuilder: (BuildContext context, int index) => + VSpace(GridSize.typeOptionSeparatorHeight), ), ); - }).toList(); + } final child = Column( mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _Header( - date: date, - isInMonth: isInMonth, - isToday: isToday, - onCreate: () => onCreateEvent(date), - ), + _Header( + date: date, + isInMonth: isInMonth, + isToday: isToday, + onCreate: () => onCreateEvent(date), ), + + // Add a separator between the header and the content. VSpace(GridSize.typeOptionSeparatorHeight), - Flexible( - child: ListView.separated( - itemBuilder: (BuildContext context, int index) { - return children[index]; - }, - itemCount: children.length, - padding: const EdgeInsets.symmetric(horizontal: 8.0), - separatorBuilder: (BuildContext context, int index) => - VSpace(GridSize.typeOptionSeparatorHeight), - ), - ), + + // Use SizedBox instead of ListView if there are no cards. + multipleCards ?? const SizedBox(), ], ); @@ -96,7 +90,7 @@ class CalendarDayCard extends StatelessWidget { onEnter: (p) => notifyEnter(context, true), onExit: (p) => notifyEnter(context, false), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.symmetric(vertical: 2.0), child: child, ), ), @@ -105,6 +99,113 @@ class CalendarDayCard extends StatelessWidget { ); } + List _buildCards(BuildContext context) { + final children = events.map((CalendarDayEvent event) { + final cellBuilder = CardCellBuilder(_rowCache.cellCache); + final rowInfo = _rowCache.getRow(event.eventId); + + final renderHook = RowCardRenderHook(); + renderHook.addTextFieldHook((cellData, primaryFieldId, _) { + if (cellData.isEmpty) { + return const SizedBox(); + } + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.medium( + cellData, + textAlign: TextAlign.left, + fontSize: 11, + maxLines: null, // Enable multiple lines + ), + ); + }); + + renderHook.addDateFieldHook((cellData, cardData, _) { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + FlowyText.regular( + cellData.date, + fontSize: 10, + color: Theme.of(context).hintColor, + ), + const Spacer(), + FlowyText.regular( + cellData.time, + fontSize: 10, + color: Theme.of(context).hintColor, + ) + ], + ), + ), + ); + }); + + renderHook.addSelectOptionHook((selectedOptions, cardData, _) { + final children = selectedOptions.map( + (option) { + return SelectOptionTag.fromOption( + context: context, + option: option, + ); + }, + ).toList(); + + return IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: SizedBox.expand( + child: Wrap(spacing: 4, runSpacing: 4, children: children), + ), + ), + ); + }); + + // renderHook.addDateFieldHook((cellData, cardData) { + + final card = RowCard( + // Add the key here to make sure the card is rebuilt when the cells + // in this row are updated. + key: ValueKey(event.eventId), + row: rowInfo!.rowPB, + viewId: viewId, + rowCache: _rowCache, + cardData: event.fieldId, + isEditing: false, + cellBuilder: cellBuilder, + openCard: (context) => _showRowDetailPage(event, context), + styleConfiguration: const RowCardStyleConfiguration( + showAccessory: false, + cellPadding: EdgeInsets.zero, + ), + renderHook: renderHook, + onStartEditing: () {}, + onEndEditing: () {}, + ); + + return GestureDetector( + onTap: () => _showRowDetailPage(event, context), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + border: Border.fromBorderSide( + BorderSide( + color: Theme.of(context).dividerColor, + width: 1.5, + ), + ), + borderRadius: Corners.s6Border, + ), + child: card, + ), + ); + }).toList(); + return children; + } + void _showRowDetailPage(CalendarDayEvent event, BuildContext context) { final dataController = RowController( rowId: event.cellId.rowId, @@ -133,42 +234,6 @@ class CalendarDayCard extends StatelessWidget { } } -class _DayEventCell extends StatelessWidget { - final String viewId; - final CalendarDayEvent event; - final VoidCallback onClick; - final Widget child; - const _DayEventCell({ - required this.viewId, - required this.event, - required this.onClick, - required this.child, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return FlowyHover( - child: GestureDetector( - onTap: onClick, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide( - color: Theme.of(context).dividerColor, - width: 1.0, - ), - ), - borderRadius: Corners.s6Border, - ), - child: child, - ), - ), - ); - } -} - class _Header extends StatelessWidget { final bool isToday; final bool isInMonth; @@ -191,12 +256,16 @@ class _Header extends StatelessWidget { isInMonth: isInMonth, date: date, ); - return Row( - children: [ - if (notifier.onEnter) _NewEventButton(onClick: onCreate), - const Spacer(), - badge, - ], + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + if (notifier.onEnter) _NewEventButton(onClick: onCreate), + const Spacer(), + badge, + ], + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart index f8e521acf4..724041fe7c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -85,13 +85,20 @@ class _CalendarPageState extends State { } }, ), + BlocListener( + listenWhen: (p, c) => p.createdEvent != c.createdEvent, + listener: (context, state) { + if (state.createdEvent != null) { + _showRowDetailPage(state.createdEvent!.event!, context); + } + }, + ), BlocListener( listenWhen: (p, c) => p.newEvent != c.newEvent, listener: (context, state) { if (state.newEvent != null) { _eventController.add(state.newEvent!); } - _showRowDetailPage(state.newEvent!.event!, context); }, ), ], @@ -120,7 +127,7 @@ class _CalendarPageState extends State { child: MonthView( key: _calendarState, controller: _eventController, - cellAspectRatio: .9, + cellAspectRatio: .6, startDay: _weekdayFromInt(firstDayOfWeek), borderColor: Theme.of(context).dividerColor, headerBuilder: _headerNavigatorBuilder, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart index 962745e165..3ea6e03cf7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -87,7 +87,7 @@ class GridBloc extends Bloc { } }, ); - databaseController.addListener(onDatabaseChanged: onDatabaseChanged); + databaseController.setListener(onDatabaseChanged: onDatabaseChanged); } Future _openGrid(Emitter emit) async { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart index 0bff716fa7..988cf21b00 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; @@ -7,31 +8,39 @@ import '../../../application/row/row_data_controller.dart'; part 'row_detail_bloc.freezed.dart'; class RowDetailBloc extends Bloc { + final RowBackendService rowService; final RowController dataController; RowDetailBloc({ required this.dataController, - }) : super(RowDetailState.initial()) { + }) : rowService = RowBackendService(viewId: dataController.viewId), + super(RowDetailState.initial()) { on( (event, emit) async { - await event.map( - initial: (_Initial value) async { + await event.when( + initial: () async { await _startListening(); final cells = dataController.loadData(); if (!isClosed) { add(RowDetailEvent.didReceiveCellDatas(cells.values.toList())); } }, - didReceiveCellDatas: (_DidReceiveCellDatas value) { - emit(state.copyWith(gridCells: value.gridCells)); + didReceiveCellDatas: (cells) { + emit(state.copyWith(gridCells: cells)); }, - deleteField: (_DeleteField value) { + deleteField: (fieldId) { final fieldService = FieldBackendService( viewId: dataController.viewId, - fieldId: value.fieldId, + fieldId: fieldId, ); fieldService.deleteField(); }, + deleteRow: (rowId) async { + await rowService.deleteRow(rowId); + }, + duplicateRow: (String rowId) async { + await rowService.duplicateRow(rowId); + }, ); }, ); @@ -58,6 +67,8 @@ class RowDetailBloc extends Bloc { class RowDetailEvent with _$RowDetailEvent { const factory RowDetailEvent.initial() = _Initial; const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField; + const factory RowDetailEvent.deleteRow(String rowId) = _DeleteRow; + const factory RowDetailEvent.duplicateRow(String rowId) = _DuplicateRow; const factory RowDetailEvent.didReceiveCellDatas( List gridCells, ) = _DidReceiveCellDatas; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart index 1227ef8e3c..78fa3ce976 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart'; import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart'; @@ -13,23 +14,40 @@ import 'card_cell_builder.dart'; import 'container/accessory.dart'; import 'container/card_container.dart'; -class Card extends StatefulWidget { +/// Edit a database row with card style widget +class RowCard extends StatefulWidget { final RowPB row; final String viewId; - final String fieldId; + final String? groupingFieldId; + + /// Allows passing a custom card data object to the card. The card will be + /// returned in the [CardCellBuilder] and can be used to build the card. final CustomCardData? cardData; final bool isEditing; final RowCache rowCache; - final CardCellBuilder cellBuilder; - final void Function(BuildContext) openCard; - final VoidCallback onStartEditing; - final VoidCallback onEndEditing; - final CardConfiguration? configuration; - const Card({ + /// The [CardCellBuilder] is used to build the card cells. + final CardCellBuilder cellBuilder; + + /// Called when the user taps on the card. + final void Function(BuildContext) openCard; + + /// Called when the user starts editing the card. + final VoidCallback onStartEditing; + + /// Called when the user ends editing the card. + final VoidCallback onEndEditing; + + /// The [RowCardRenderHook] is used to render the card's cell. Other than + /// using the default cell builder. For example the [SelectOptionCardCell] + final RowCardRenderHook? renderHook; + + final RowCardStyleConfiguration styleConfiguration; + + const RowCard({ required this.row, required this.viewId, - required this.fieldId, + this.groupingFieldId, required this.isEditing, required this.rowCache, required this.cellBuilder, @@ -37,15 +55,19 @@ class Card extends StatefulWidget { required this.onStartEditing, required this.onEndEditing, this.cardData, - this.configuration, + this.styleConfiguration = const RowCardStyleConfiguration( + showAccessory: true, + ), + this.renderHook, Key? key, }) : super(key: key); @override - State> createState() => _CardState(); + State> createState() => + _RowCardState(); } -class _CardState extends State> { +class _RowCardState extends State> { late CardBloc _cardBloc; late EditableRowNotifier rowNotifier; late PopoverController popoverController; @@ -56,15 +78,15 @@ class _CardState extends State> { rowNotifier = EditableRowNotifier(isEditing: widget.isEditing); _cardBloc = CardBloc( viewId: widget.viewId, - groupFieldId: widget.fieldId, + groupFieldId: widget.groupingFieldId, isEditing: widget.isEditing, row: widget.row, rowCache: widget.rowCache, - )..add(const BoardCardEvent.initial()); + )..add(const RowCardEvent.initial()); rowNotifier.isEditing.addListener(() { if (!mounted) return; - _cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value)); + _cardBloc.add(RowCardEvent.setIsEditing(rowNotifier.isEditing.value)); if (rowNotifier.isEditing.value) { widget.onStartEditing(); @@ -81,7 +103,7 @@ class _CardState extends State> { Widget build(BuildContext context) { return BlocProvider.value( value: _cardBloc, - child: BlocBuilder( + child: BlocBuilder( buildWhen: (previous, current) { // Rebuild when: // 1.If the length of the cells is not the same @@ -106,21 +128,26 @@ class _CardState extends State> { context, popoverContext, ), - child: BoardCardContainer( + child: RowCardContainer( buildAccessoryWhen: () => state.isEditing == false, accessoryBuilder: (context) { - return [ - _CardEditOption(rowNotifier: rowNotifier), - _CardMoreOption(), - ]; + if (widget.styleConfiguration.showAccessory == false) { + return []; + } else { + return [ + _CardEditOption(rowNotifier: rowNotifier), + _CardMoreOption(), + ]; + } }, openAccessory: _handleOpenAccessory, openCard: (context) => widget.openCard(context), child: _CardContent( rowNotifier: rowNotifier, cellBuilder: widget.cellBuilder, + styleConfiguration: widget.styleConfiguration, cells: state.cells, - cardConfiguration: widget.configuration, + renderHook: widget.renderHook, cardData: widget.cardData, ), ), @@ -166,15 +193,17 @@ class _CardState extends State> { class _CardContent extends StatelessWidget { final CardCellBuilder cellBuilder; final EditableRowNotifier rowNotifier; - final List cells; - final CardConfiguration? cardConfiguration; + final List cells; + final RowCardRenderHook? renderHook; final CustomCardData? cardData; + final RowCardStyleConfiguration styleConfiguration; const _CardContent({ required this.rowNotifier, required this.cellBuilder, required this.cells, required this.cardData, - this.cardConfiguration, + required this.styleConfiguration, + this.renderHook, Key? key, }) : super(key: key); @@ -188,30 +217,30 @@ class _CardContent extends StatelessWidget { List _makeCells( BuildContext context, - List cells, + List cells, ) { final List children = []; // Remove all the cell listeners. rowNotifier.unbind(); cells.asMap().forEach( - (int index, BoardCellEquatable cell) { + (int index, CellIdentifier cell) { final isEditing = index == 0 ? rowNotifier.isEditing.value : false; final cellNotifier = EditableCardNotifier(isEditing: isEditing); if (index == 0) { // Only use the first cell to receive user's input when click the edit // button - rowNotifier.bindCell(cell.identifier, cellNotifier); + rowNotifier.bindCell(cell, cellNotifier); } final child = Padding( - key: cell.identifier.key(), - padding: const EdgeInsets.only(left: 4, right: 4), + key: cell.key(), + padding: styleConfiguration.cellPadding, child: cellBuilder.buildCell( - cellId: cell.identifier, + cellId: cell, cellNotifier: cellNotifier, - cardConfiguration: cardConfiguration, + renderHook: renderHook, cardData: cardData, ), ); @@ -265,3 +294,13 @@ class _CardEditOption extends StatelessWidget with CardAccessory { @override AccessoryType get type => AccessoryType.edit; } + +class RowCardStyleConfiguration { + final bool showAccessory; + final EdgeInsets cellPadding; + + const RowCardStyleConfiguration({ + this.showAccessory = true, + this.cellPadding = const EdgeInsets.only(left: 4, right: 4), + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart index f84e509913..e7bf08a3c5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart @@ -1,5 +1,4 @@ import 'dart:collection'; -import 'package:equatable/equatable.dart'; import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,9 +11,9 @@ import '../../application/row/row_service.dart'; part 'card_bloc.freezed.dart'; -class CardBloc extends Bloc { +class CardBloc extends Bloc { final RowPB row; - final String groupFieldId; + final String? groupFieldId; final RowBackendService _rowBackendSvc; final RowCache _rowCache; VoidCallback? _rowCallback; @@ -28,13 +27,13 @@ class CardBloc extends Bloc { }) : _rowBackendSvc = RowBackendService(viewId: viewId), _rowCache = rowCache, super( - BoardCardState.initial( + RowCardState.initial( row, _makeCells(groupFieldId, rowCache.loadGridCells(row.id)), isEditing, ), ) { - on( + on( (event, emit) async { await event.when( initial: () async { @@ -69,7 +68,7 @@ class CardBloc extends Bloc { return RowInfo( viewId: _rowBackendSvc.viewId, fields: UnmodifiableListView( - state.cells.map((cell) => cell.identifier.fieldInfo).toList(), + state.cells.map((cell) => cell.fieldInfo).toList(), ), rowPB: state.rowPB, ); @@ -81,70 +80,58 @@ class CardBloc extends Bloc { onCellUpdated: (cellMap, reason) { if (!isClosed) { final cells = _makeCells(groupFieldId, cellMap); - add(BoardCardEvent.didReceiveCells(cells, reason)); + add(RowCardEvent.didReceiveCells(cells, reason)); } }, ); } } -List _makeCells( - String groupFieldId, +List _makeCells( + String? groupFieldId, CellByFieldId originalCellMap, ) { - List cells = []; + List cells = []; for (final entry in originalCellMap.entries) { // Filter out the cell if it's fieldId equal to the groupFieldId - if (entry.value.fieldId != groupFieldId) { - cells.add(BoardCellEquatable(entry.value)); + if (groupFieldId != null) { + if (entry.value.fieldId == groupFieldId) { + continue; + } } + + cells.add(entry.value); } return cells; } @freezed -class BoardCardEvent with _$BoardCardEvent { - const factory BoardCardEvent.initial() = _InitialRow; - const factory BoardCardEvent.setIsEditing(bool isEditing) = _IsEditing; - const factory BoardCardEvent.didReceiveCells( - List cells, +class RowCardEvent with _$RowCardEvent { + const factory RowCardEvent.initial() = _InitialRow; + const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing; + const factory RowCardEvent.didReceiveCells( + List cells, RowsChangedReason reason, ) = _DidReceiveCells; } @freezed -class BoardCardState with _$BoardCardState { - const factory BoardCardState({ +class RowCardState with _$RowCardState { + const factory RowCardState({ required RowPB rowPB, - required List cells, + required List cells, required bool isEditing, RowsChangedReason? changeReason, - }) = _BoardCardState; + }) = _RowCardState; - factory BoardCardState.initial( + factory RowCardState.initial( RowPB rowPB, - List cells, + List cells, bool isEditing, ) => - BoardCardState( + RowCardState( rowPB: rowPB, cells: cells, isEditing: isEditing, ); } - -class BoardCellEquatable extends Equatable { - final CellIdentifier identifier; - - const BoardCellEquatable(this.identifier); - - @override - List get props { - return [ - identifier.fieldInfo.id, - identifier.fieldInfo.fieldType, - identifier.fieldInfo.visibility, - identifier.fieldInfo.width, - ]; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart index ddf2dee63a..84d67587a1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart @@ -22,7 +22,7 @@ class CardCellBuilder { CustomCardData? cardData, required CellIdentifier cellId, EditableCardNotifier? cellNotifier, - CardConfiguration? cardConfiguration, + RowCardRenderHook? renderHook, Map? styles, }) { final cellControllerBuilder = CellControllerBuilder( @@ -39,20 +39,21 @@ class CardCellBuilder { key: key, ); case FieldType.DateTime: - return DateCardCell( + return DateCardCell( + renderHook: renderHook?.renderHook[FieldType.DateTime], cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.SingleSelect: return SelectOptionCardCell( - renderHook: cardConfiguration?.renderHook[FieldType.SingleSelect], + renderHook: renderHook?.renderHook[FieldType.SingleSelect], cellControllerBuilder: cellControllerBuilder, cardData: cardData, key: key, ); case FieldType.MultiSelect: return SelectOptionCardCell( - renderHook: cardConfiguration?.renderHook[FieldType.MultiSelect], + renderHook: renderHook?.renderHook[FieldType.MultiSelect], cellControllerBuilder: cellControllerBuilder, cardData: cardData, editableNotifier: cellNotifier, @@ -69,9 +70,11 @@ class CardCellBuilder { key: key, ); case FieldType.RichText: - return TextCardCell( + return TextCardCell( + renderHook: renderHook?.renderHook[FieldType.RichText], cellControllerBuilder: cellControllerBuilder, editableNotifier: cellNotifier, + cardData: cardData, style: isStyleOrNull(style), key: key, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart index e5942c8ab3..5433f5f1b1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart @@ -1,26 +1,59 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; import 'package:flutter/material.dart'; -typedef CellRenderHook = Widget? Function(C cellData, T cardData); +typedef CellRenderHook = Widget? Function( + C cellData, + CustomCardData cardData, + BuildContext buildContext, +); typedef RenderHookByFieldType = Map>; -class CardConfiguration { +class RowCardRenderHook { final RenderHookByFieldType renderHook = {}; - CardConfiguration(); + RowCardRenderHook(); + /// Add render hook for the FieldType.SingleSelect and FieldType.MultiSelect void addSelectOptionHook( - CellRenderHook, CustomCardData> hook, + CellRenderHook, CustomCardData?> hook, ) { - selectOptionHook(cellData, cardData) { - if (cellData is List) { - hook(cellData, cardData); + final hookFn = _typeSafeHook>(hook); + renderHook[FieldType.SingleSelect] = hookFn; + renderHook[FieldType.MultiSelect] = hookFn; + } + + void addTextFieldHook( + CellRenderHook hook, + ) { + renderHook[FieldType.RichText] = _typeSafeHook(hook); + } + + void addDateFieldHook( + CellRenderHook hook, + ) { + renderHook[FieldType.DateTime] = _typeSafeHook(hook); + } + + CellRenderHook _typeSafeHook( + CellRenderHook hook, + ) { + hookFn(cellData, cardData, buildContext) { + if (cellData == null) { + return null; + } + + if (cellData is C) { + return hook(cellData, cardData, buildContext); + } else { + Log.debug("Unexpected cellData type: ${cellData.runtimeType}"); + return null; } } - renderHook[FieldType.SingleSelect] = selectOptionHook; - renderHook[FieldType.MultiSelect] = selectOptionHook; + return hookFn; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart index ad58b9259d..33fef71bf0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checkbox_card_cell.dart @@ -44,13 +44,16 @@ class _CheckboxCardCellState extends State { : svgWidget('editor/editor_uncheck'); return Align( alignment: Alignment.centerLeft, - child: FlowyIconButton( - iconPadding: EdgeInsets.zero, - icon: icon, - width: 20, - onPressed: () => context - .read() - .add(const CheckboxCardCellEvent.select()), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: FlowyIconButton( + iconPadding: EdgeInsets.zero, + icon: icon, + width: 20, + onPressed: () => context + .read() + .add(const CheckboxCardCellEvent.select()), + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart index c954cb9d7b..7eacdd3f1c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/date_card_cell.dart @@ -7,11 +7,13 @@ import '../bloc/date_card_cell_bloc.dart'; import '../define.dart'; import 'card_cell.dart'; -class DateCardCell extends CardCell { +class DateCardCell extends CardCell { final CellControllerBuilder cellControllerBuilder; + final CellRenderHook? renderHook; const DateCardCell({ required this.cellControllerBuilder, + this.renderHook, Key? key, }) : super(key: key); @@ -42,6 +44,15 @@ class _DateCardCellState extends State { if (state.dateStr.isEmpty) { return const SizedBox(); } else { + Widget? custom = widget.renderHook?.call( + state.data, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + return Align( alignment: Alignment.centerLeft, child: Padding( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart index 1a38f727ad..c80942f3d6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart @@ -11,17 +11,18 @@ import 'card_cell.dart'; class SelectOptionCardCellStyle extends CardCellStyle {} -class SelectOptionCardCell extends CardCell +class SelectOptionCardCell + extends CardCell with EditableCell { final CellControllerBuilder cellControllerBuilder; - final CellRenderHook, T>? renderHook; + final CellRenderHook, CustomCardData>? renderHook; @override final EditableCardNotifier? editableNotifier; SelectOptionCardCell({ required this.cellControllerBuilder, - required T? cardData, + required CustomCardData? cardData, this.renderHook, this.editableNotifier, Key? key, @@ -57,6 +58,7 @@ class _SelectOptionCardCellState extends State { Widget? custom = widget.renderHook?.call( state.selectedOptions, widget.cardData, + context, ); if (custom != null) { return custom; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart index 4eb9d9137e..2e497f0a43 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart @@ -14,18 +14,21 @@ class TextCardCellStyle extends CardCellStyle { TextCardCellStyle(this.fontSize); } -class TextCardCell extends CardCell - with EditableCell { +class TextCardCell + extends CardCell with EditableCell { @override final EditableCardNotifier? editableNotifier; final CellControllerBuilder cellControllerBuilder; + final CellRenderHook? renderHook; const TextCardCell({ required this.cellControllerBuilder, + required CustomCardData? cardData, this.editableNotifier, + this.renderHook, TextCardCellStyle? style, Key? key, - }) : super(key: key, style: style); + }) : super(key: key, style: style, cardData: cardData); @override State createState() => _TextCardCellState(); @@ -104,6 +107,16 @@ class _TextCardCellState extends State { return previous != current; }, builder: (context, state) { + // Returns a custom render widget + Widget? custom = widget.renderHook?.call( + state.content, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + if (state.content.isEmpty && state.enableEdit == false && focusWhenInit == false) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart index 6281aa80b7..01d6189307 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/container/card_container.dart @@ -4,13 +4,13 @@ import 'package:styled_widget/styled_widget.dart'; import 'accessory.dart'; -class BoardCardContainer extends StatelessWidget { +class RowCardContainer extends StatelessWidget { final Widget child; final CardAccessoryBuilder? accessoryBuilder; final bool Function()? buildAccessoryWhen; final void Function(BuildContext) openCard; final void Function(AccessoryType) openAccessory; - const BoardCardContainer({ + const RowCardContainer({ required this.child, required this.openCard, required this.openAccessory, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart index c557098175..9e65e8c30c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart @@ -43,83 +43,84 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { } class _RowDetailPageState extends State { - final padding = const EdgeInsets.symmetric( - horizontal: 40, - vertical: 20, - ); - @override Widget build(BuildContext context) { return FlowyDialog( child: BlocProvider( create: (context) { - final bloc = RowDetailBloc( - dataController: widget.dataController, - ); - bloc.add(const RowDetailEvent.initial()); - return bloc; + return RowDetailBloc(dataController: widget.dataController) + ..add(const RowDetailEvent.initial()); }, - child: Padding( - padding: padding, - child: Column( - children: [ - const _Header(), - Expanded( - child: _PropertyColumn( - cellBuilder: widget.cellBuilder, - viewId: widget.dataController.viewId, - ), - ), - ], - ), + child: ListView( + children: [ + // using ListView here for future expansion: + // - header and cover image + // - lower rich text area + IntrinsicHeight(child: _responsiveRowInfo()), + const Divider(height: 1.0) + ], ), ), ); } -} -class _Header extends StatelessWidget { - const _Header({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 30, - child: Row( - children: const [Spacer(), _CloseButton()], - ), + Widget _responsiveRowInfo() { + final rowDataColumn = _PropertyColumn( + cellBuilder: widget.cellBuilder, + viewId: widget.dataController.viewId, ); - } -} - -class _CloseButton extends StatelessWidget { - const _CloseButton({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - width: 24, - onPressed: () => FlowyOverlay.pop(context), - iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), - icon: svgWidget( - "home/close", - color: Theme.of(context).iconTheme.color, - ), + final rowOptionColumn = _RowOptionColumn( + viewId: widget.dataController.viewId, + rowId: widget.dataController.rowId, ); + if (MediaQuery.of(context).size.width > 800) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 4, + child: Padding( + padding: const EdgeInsets.fromLTRB(50, 50, 20, 20), + child: rowDataColumn, + ), + ), + const VerticalDivider(width: 1.0), + Flexible( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 50, 20, 20), + child: rowOptionColumn, + ), + ), + ], + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 50, 20, 20), + child: rowDataColumn, + ), + const Divider(height: 1.0), + Padding( + padding: const EdgeInsets.all(20), + child: rowOptionColumn, + ) + ], + ); + } } } class _PropertyColumn extends StatelessWidget { final String viewId; final GridCellBuilder cellBuilder; - final ScrollController _scrollController; - _PropertyColumn({ + const _PropertyColumn({ required this.viewId, required this.cellBuilder, Key? key, - }) : _scrollController = ScrollController(), - super(key: key); + }) : super(key: key); @override Widget build(BuildContext context) { @@ -127,63 +128,34 @@ class _PropertyColumn extends StatelessWidget { buildWhen: (previous, current) => previous.gridCells != current.gridCells, builder: (context, state) { return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(child: _wrapScrollbar(buildPropertyCells(state))), - const VSpace(10), - _CreatePropertyButton( - viewId: viewId, - onClosed: _scrollToNewProperty, - ), + ...state.gridCells + .map( + (cell) => Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: _PropertyCell( + cellId: cell, + cellBuilder: cellBuilder, + ), + ), + ) + .toList(), + const VSpace(20), + _CreatePropertyButton(viewId: viewId), ], ); }, ); } - - Widget buildPropertyCells(RowDetailState state) { - return ListView.separated( - controller: _scrollController, - itemCount: state.gridCells.length, - itemBuilder: (BuildContext context, int index) { - return _PropertyCell( - cellId: state.gridCells[index], - cellBuilder: cellBuilder, - ); - }, - separatorBuilder: (BuildContext context, int index) { - return const VSpace(2); - }, - ); - } - - Widget _wrapScrollbar(Widget child) { - return ScrollbarListStack( - axis: Axis.vertical, - controller: _scrollController, - barSize: GridSize.scrollBarSize, - autoHideScrollbar: false, - child: child, - ); - } - - void _scrollToNewProperty() { - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 250), - curve: Curves.ease, - ); - }); - } } class _CreatePropertyButton extends StatefulWidget { final String viewId; - final VoidCallback onClosed; const _CreatePropertyButton({ required this.viewId, - required this.onClosed, Key? key, }) : super(key: key); @@ -207,10 +179,8 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> { controller: popoverController, direction: PopoverDirection.topWithLeftAligned, margin: EdgeInsets.zero, - onClose: widget.onClosed, - child: Container( + child: SizedBox( height: 40, - decoration: _makeBoxDecoration(context), child: FlowyButton( text: FlowyText.medium( LocaleKeys.grid_field_newProperty.tr(), @@ -244,14 +214,6 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> { }, ); } - - BoxDecoration _makeBoxDecoration(BuildContext context) { - final borderSide = - BorderSide(color: Theme.of(context).dividerColor, width: 1.0); - return BoxDecoration( - border: Border(top: borderSide), - ); - } } class _PropertyCell extends StatefulWidget { @@ -377,3 +339,69 @@ GridCellStyle? _customCellStyle(FieldType fieldType) { } throw UnimplementedError; } + +class _RowOptionColumn extends StatelessWidget { + final String rowId; + const _RowOptionColumn({ + required String viewId, + required this.rowId, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: FlowyText(LocaleKeys.grid_row_action.tr()), + ), + const VSpace(15), + _DeleteButton(rowId: rowId), + _DuplicateButton(rowId: rowId), + ], + ); + } +} + +class _DeleteButton extends StatelessWidget { + final String rowId; + const _DeleteButton({required this.rowId, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()), + leftIcon: const FlowySvg(name: "home/trash"), + onTap: () { + context.read().add(RowDetailEvent.deleteRow(rowId)); + FlowyOverlay.pop(context); + }, + ), + ); + } +} + +class _DuplicateButton extends StatelessWidget { + final String rowId; + const _DuplicateButton({required this.rowId, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()), + leftIcon: const FlowySvg(name: "grid/duplicate"), + onTap: () { + context.read().add(RowDetailEvent.duplicateRow(rowId)); + FlowyOverlay.pop(context); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart index 714d9a951d..cff6f16307 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart @@ -283,7 +283,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState { return 3.0; }), crossAxisMargin: 0.0, - mainAxisMargin: 0.0, + mainAxisMargin: 6.0, radius: Corners.s10Radius, ), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, diff --git a/frontend/rust-lib/flowy-database/src/event_handler.rs b/frontend/rust-lib/flowy-database/src/event_handler.rs index 331d093024..d189c1f6fd 100644 --- a/frontend/rust-lib/flowy-database/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database/src/event_handler.rs @@ -313,7 +313,9 @@ pub(crate) async fn duplicate_row_handler( ) -> Result<(), FlowyError> { let params: RowIdParams = data.into_inner().try_into()?; let editor = manager.get_database_editor(¶ms.view_id).await?; - editor.duplicate_row(¶ms.row_id).await?; + editor + .duplicate_row(¶ms.view_id, ¶ms.row_id) + .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs index b519a2943e..054bd70820 100644 --- a/frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs @@ -248,11 +248,11 @@ pub fn insert_checkbox_cell(is_check: bool, field_rev: &FieldRevision) -> CellRe CellRevision::new(data) } -pub fn insert_date_cell(timestamp: i64, field_rev: &FieldRevision) -> CellRevision { +pub fn insert_date_cell(date_cell_data: DateCellData, field_rev: &FieldRevision) -> CellRevision { let cell_data = serde_json::to_string(&DateCellChangeset { - date: Some(timestamp.to_string()), + date: date_cell_data.timestamp.map(|t| t.to_string()), time: None, - include_time: Some(false), + include_time: Some(date_cell_data.include_time), is_utc: true, }) .unwrap(); diff --git a/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs index 84f5c26bf1..804a7a2654 100644 --- a/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database/src/services/database/database_editor.rs @@ -520,7 +520,31 @@ impl DatabaseEditor { self.database_views.subscribe_view_changed(view_id).await } - pub async fn duplicate_row(&self, _row_id: &str) -> FlowyResult<()> { + pub async fn duplicate_row(&self, view_id: &str, row_id: &str) -> FlowyResult<()> { + if let Some(row) = self.get_row_rev(row_id).await? { + let cell_data_by_field_id = row + .cells + .iter() + .map(|(field_id, cell)| { + ( + field_id.clone(), + TypeCellData::try_from(cell) + .map(|value| value.cell_str) + .unwrap_or_default(), + ) + }) + .collect::>(); + + tracing::trace!("cell_data_by_field_id :{:?}", cell_data_by_field_id); + let params = CreateRowParams { + view_id: view_id.to_string(), + start_row_id: Some(row.id.clone()), + group_id: None, + cell_data_by_field_id: Some(cell_data_by_field_id), + }; + + self.create_row(params).await?; + } Ok(()) } diff --git a/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option_entities.rs index 716ffa625a..375d1fa4ad 100644 --- a/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -157,8 +157,7 @@ impl FromCellString for DateCellData { where Self: Sized, { - let result: DateCellData = serde_json::from_str(s).unwrap(); - Ok(result) + Ok(serde_json::from_str::(s).unwrap_or_default()) } } diff --git a/frontend/rust-lib/flowy-database/src/services/row/row_builder.rs b/frontend/rust-lib/flowy-database/src/services/row/row_builder.rs index bc78c0b0af..8d4a4f95f2 100644 --- a/frontend/rust-lib/flowy-database/src/services/row/row_builder.rs +++ b/frontend/rust-lib/flowy-database/src/services/row/row_builder.rs @@ -4,7 +4,7 @@ use crate::services::cell::{ }; use crate::entities::FieldType; -use crate::services::field::{CheckboxCellData, SelectOptionIds}; +use crate::services::field::{CheckboxCellData, DateCellData, SelectOptionIds}; use database_model::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT}; use indexmap::IndexMap; use std::collections::HashMap; @@ -52,12 +52,12 @@ impl RowRevisionBuilder { FieldType::RichText => builder.insert_text_cell(&field_id, cell_data), FieldType::Number => { if let Ok(num) = cell_data.parse::() { - builder.insert_date_cell(&field_id, num) + builder.insert_number_cell(&field_id, num) } }, FieldType::DateTime => { - if let Ok(timestamp) = cell_data.parse::() { - builder.insert_date_cell(&field_id, timestamp) + if let Ok(date_cell_data) = DateCellData::from_cell_str(&cell_data) { + builder.insert_date_cell(&field_id, date_cell_data) } }, FieldType::MultiSelect | FieldType::SingleSelect => { @@ -132,14 +132,14 @@ impl RowRevisionBuilder { } } - pub fn insert_date_cell(&mut self, field_id: &str, timestamp: i64) { + pub fn insert_date_cell(&mut self, field_id: &str, date_cell_data: DateCellData) { match self.field_rev_map.get(&field_id.to_owned()) { None => tracing::warn!("Can't find the date field with id: {}", field_id), Some(field_rev) => { - self - .payload - .cell_by_field_id - .insert(field_id.to_owned(), insert_date_cell(timestamp, field_rev)); + self.payload.cell_by_field_id.insert( + field_id.to_owned(), + insert_date_cell(date_cell_data, field_rev), + ); }, } }