mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: grid row page detail redesign (#2351)
* chore: grid row page detail update * chore: update row_detail.dart Co-authored-by: Alex Wallen <wallenstephen@outlook.com> * 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 <wallenstephen@outlook.com> Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
parent
ac07c259f1
commit
77d58a81fd
@ -308,7 +308,8 @@
|
|||||||
"textPlaceholder": "Empty",
|
"textPlaceholder": "Empty",
|
||||||
"copyProperty": "Copied property to clipboard",
|
"copyProperty": "Copied property to clipboard",
|
||||||
"count": "Count",
|
"count": "Count",
|
||||||
"newRow": "New row"
|
"newRow": "New row",
|
||||||
|
"action": "Action"
|
||||||
},
|
},
|
||||||
"selectOption": {
|
"selectOption": {
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
|
@ -76,10 +76,7 @@ class CellController<T, D> extends Equatable {
|
|||||||
_cellListener?.start(
|
_cellListener?.start(
|
||||||
onCellChanged: (result) {
|
onCellChanged: (result) {
|
||||||
result.fold(
|
result.fold(
|
||||||
(_) {
|
(_) => _loadData(),
|
||||||
_cellCache.remove(_cacheKey);
|
|
||||||
_loadData();
|
|
||||||
},
|
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -115,7 +115,7 @@ class DatabaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addListener({
|
void setListener({
|
||||||
DatabaseCallbacks? onDatabaseChanged,
|
DatabaseCallbacks? onDatabaseChanged,
|
||||||
LayoutCallbacks? onLayoutChanged,
|
LayoutCallbacks? onLayoutChanged,
|
||||||
GroupCallbacks? onGroupChanged,
|
GroupCallbacks? onGroupChanged,
|
||||||
@ -211,6 +211,11 @@ class DatabaseController {
|
|||||||
await _databaseViewBackendSvc.closeView();
|
await _databaseViewBackendSvc.closeView();
|
||||||
await fieldController.dispose();
|
await fieldController.dispose();
|
||||||
await groupListener.stop();
|
await groupListener.stop();
|
||||||
|
await _viewCache.dispose();
|
||||||
|
_databaseCallbacks = null;
|
||||||
|
_groupCallbacks = null;
|
||||||
|
_layoutCallbacks = null;
|
||||||
|
_calendarLayoutCallbacks = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadGroups() async {
|
Future<void> _loadGroups() async {
|
||||||
@ -251,7 +256,7 @@ class DatabaseController {
|
|||||||
_databaseCallbacks?.onRowsCreated?.call(ids);
|
_databaseCallbacks?.onRowsCreated?.call(ids);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
_viewCache.addListener(callbacks);
|
_viewCache.setListener(callbacks);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenOnFieldsChanged() {
|
void _listenOnFieldsChanged() {
|
||||||
|
@ -111,9 +111,10 @@ class DatabaseViewCache {
|
|||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
await _databaseViewListener.stop();
|
await _databaseViewListener.stop();
|
||||||
await _rowCache.dispose();
|
await _rowCache.dispose();
|
||||||
|
_callbacks = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addListener(DatabaseViewCallbacks callbacks) {
|
void setListener(DatabaseViewCallbacks callbacks) {
|
||||||
_callbacks = callbacks;
|
_callbacks = callbacks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_databaseController.addListener(
|
_databaseController.setListener(
|
||||||
onDatabaseChanged: onDatabaseChanged,
|
onDatabaseChanged: onDatabaseChanged,
|
||||||
onGroupChanged: onGroupChanged,
|
onGroupChanged: onGroupChanged,
|
||||||
);
|
);
|
||||||
|
@ -78,7 +78,7 @@ class BoardContent extends StatefulWidget {
|
|||||||
|
|
||||||
class _BoardContentState extends State<BoardContent> {
|
class _BoardContentState extends State<BoardContent> {
|
||||||
late AppFlowyBoardScrollController scrollManager;
|
late AppFlowyBoardScrollController scrollManager;
|
||||||
final cardConfiguration = CardConfiguration<String>();
|
final renderHook = RowCardRenderHook<String>();
|
||||||
|
|
||||||
final config = const AppFlowyBoardConfig(
|
final config = const AppFlowyBoardConfig(
|
||||||
groupBackgroundColor: Color(0xffF7F8FC),
|
groupBackgroundColor: Color(0xffF7F8FC),
|
||||||
@ -87,7 +87,7 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
scrollManager = AppFlowyBoardScrollController();
|
scrollManager = AppFlowyBoardScrollController();
|
||||||
cardConfiguration.addSelectOptionHook((options, groupId) {
|
renderHook.addSelectOptionHook((options, groupId, _) {
|
||||||
// The cell should hide if the option id is equal to the groupId.
|
// The cell should hide if the option id is equal to the groupId.
|
||||||
final isInGroup =
|
final isInGroup =
|
||||||
options.where((element) => element.id == groupId).isNotEmpty;
|
options.where((element) => element.id == groupId).isNotEmpty;
|
||||||
@ -254,15 +254,15 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
key: ValueKey(groupItemId),
|
key: ValueKey(groupItemId),
|
||||||
margin: config.cardPadding,
|
margin: config.cardPadding,
|
||||||
decoration: _makeBoxDecoration(context),
|
decoration: _makeBoxDecoration(context),
|
||||||
child: Card<String>(
|
child: RowCard<String>(
|
||||||
row: rowPB,
|
row: rowPB,
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
cardData: groupData.group.groupId,
|
cardData: groupData.group.groupId,
|
||||||
fieldId: groupItem.fieldInfo.id,
|
groupingFieldId: groupItem.fieldInfo.id,
|
||||||
isEditing: isEditing,
|
isEditing: isEditing,
|
||||||
cellBuilder: cellBuilder,
|
cellBuilder: cellBuilder,
|
||||||
configuration: cardConfiguration,
|
renderHook: renderHook,
|
||||||
openCard: (context) => _openCard(
|
openCard: (context) => _openCard(
|
||||||
viewId,
|
viewId,
|
||||||
fieldController,
|
fieldController,
|
||||||
|
@ -55,6 +55,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
createEvent: (DateTime date, String title) async {
|
createEvent: (DateTime date, String title) async {
|
||||||
await _createEvent(date, title);
|
await _createEvent(date, title);
|
||||||
},
|
},
|
||||||
|
didCreateEvent: (CalendarEventData<CalendarDayEvent> event) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
createdEvent: event,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
updateCalendarLayoutSetting:
|
updateCalendarLayoutSetting:
|
||||||
(CalendarLayoutSettingsPB layoutSetting) async {
|
(CalendarLayoutSettingsPB layoutSetting) async {
|
||||||
await _updateCalendarLayoutSetting(layoutSetting);
|
await _updateCalendarLayoutSetting(layoutSetting);
|
||||||
@ -74,14 +81,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
didReceiveNewEvent: (CalendarEventData<CalendarDayEvent> event) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
allEvents: [...state.allEvents, event],
|
|
||||||
newEvent: event,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
didDeleteEvents: (List<String> deletedRowIds) {
|
didDeleteEvents: (List<String> deletedRowIds) {
|
||||||
var events = [...state.allEvents];
|
var events = [...state.allEvents];
|
||||||
events.retainWhere(
|
events.retainWhere(
|
||||||
@ -94,11 +93,25 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
didReceiveEvent: (CalendarEventData<CalendarDayEvent> event) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
allEvents: [...state.allEvents, event],
|
||||||
|
newEvent: event,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _databaseController.dispose();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
FieldInfo? _getCalendarFieldInfo(String fieldId) {
|
FieldInfo? _getCalendarFieldInfo(String fieldId) {
|
||||||
final fieldInfos = _databaseController.fieldController.fieldInfos;
|
final fieldInfos = _databaseController.fieldController.fieldInfos;
|
||||||
final index = fieldInfos.indexWhere(
|
final index = fieldInfos.indexWhere(
|
||||||
@ -142,17 +155,27 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
final dateField = _getCalendarFieldInfo(settings.layoutFieldId);
|
final dateField = _getCalendarFieldInfo(settings.layoutFieldId);
|
||||||
final titleField = _getTitleFieldInfo();
|
final titleField = _getTitleFieldInfo();
|
||||||
if (dateField != null && titleField != null) {
|
if (dateField != null && titleField != null) {
|
||||||
final result = await _databaseController.createRow(
|
final newRow = await _databaseController.createRow(
|
||||||
withCells: (builder) {
|
withCells: (builder) {
|
||||||
builder.insertDate(dateField, date);
|
builder.insertDate(dateField, date);
|
||||||
builder.insertText(titleField, title);
|
builder.insertText(titleField, title);
|
||||||
},
|
},
|
||||||
|
).then(
|
||||||
|
(result) => result.fold(
|
||||||
|
(newRow) => newRow,
|
||||||
|
(err) {
|
||||||
|
Log.error(err);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.fold(
|
if (newRow != null) {
|
||||||
(newRow) {},
|
final event = await _loadEvent(newRow.id);
|
||||||
(err) => Log.error(err),
|
if (event != null && !isClosed) {
|
||||||
);
|
add(CalendarEvent.didCreateEvent(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -247,7 +270,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
for (final id in ids) {
|
for (final id in ids) {
|
||||||
final event = await _loadEvent(id);
|
final event = await _loadEvent(id);
|
||||||
if (event != null && !isClosed) {
|
if (event != null && !isClosed) {
|
||||||
add(CalendarEvent.didReceiveNewEvent(event));
|
add(CalendarEvent.didReceiveEvent(event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -275,7 +298,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
onCalendarLayoutChanged: _didReceiveNewLayoutField,
|
onCalendarLayoutChanged: _didReceiveNewLayoutField,
|
||||||
);
|
);
|
||||||
|
|
||||||
_databaseController.addListener(
|
_databaseController.setListener(
|
||||||
onDatabaseChanged: onDatabaseChanged,
|
onDatabaseChanged: onDatabaseChanged,
|
||||||
onLayoutChanged: onLayoutChanged,
|
onLayoutChanged: onLayoutChanged,
|
||||||
onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
|
onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
|
||||||
@ -318,10 +341,15 @@ class CalendarEvent with _$CalendarEvent {
|
|||||||
) = _DidUpdateEvent;
|
) = _DidUpdateEvent;
|
||||||
|
|
||||||
// Called after creating a new event
|
// Called after creating a new event
|
||||||
const factory CalendarEvent.didReceiveNewEvent(
|
const factory CalendarEvent.didCreateEvent(
|
||||||
CalendarEventData<CalendarDayEvent> event,
|
CalendarEventData<CalendarDayEvent> event,
|
||||||
) = _DidReceiveNewEvent;
|
) = _DidReceiveNewEvent;
|
||||||
|
|
||||||
|
// Called when receive a new event
|
||||||
|
const factory CalendarEvent.didReceiveEvent(
|
||||||
|
CalendarEventData<CalendarDayEvent> event,
|
||||||
|
) = _DidReceiveEvent;
|
||||||
|
|
||||||
// Called when deleting events
|
// Called when deleting events
|
||||||
const factory CalendarEvent.didDeleteEvents(List<String> rowIds) =
|
const factory CalendarEvent.didDeleteEvents(List<String> rowIds) =
|
||||||
_DidDeleteEvents;
|
_DidDeleteEvents;
|
||||||
@ -349,6 +377,7 @@ class CalendarState with _$CalendarState {
|
|||||||
required Option<DatabasePB> database,
|
required Option<DatabasePB> database,
|
||||||
required Events allEvents,
|
required Events allEvents,
|
||||||
required Events initialEvents,
|
required Events initialEvents,
|
||||||
|
CalendarEventData<CalendarDayEvent>? createdEvent,
|
||||||
CalendarEventData<CalendarDayEvent>? newEvent,
|
CalendarEventData<CalendarDayEvent>? newEvent,
|
||||||
required List<String> deleteEventIds,
|
required List<String> deleteEventIds,
|
||||||
CalendarEventData<CalendarDayEvent>? updateEvent,
|
CalendarEventData<CalendarDayEvent>? updateEvent,
|
||||||
@ -391,5 +420,6 @@ class CalendarDayEvent {
|
|||||||
final CellIdentifier cellId;
|
final CellIdentifier cellId;
|
||||||
|
|
||||||
String get eventId => cellId.rowId;
|
String get eventId => cellId.rowId;
|
||||||
|
String get fieldId => cellId.fieldId;
|
||||||
CalendarDayEvent({required this.cellId, required this.event});
|
CalendarDayEvent({required this.cellId, required this.event});
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
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/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/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/cell_builder.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/image.dart';
|
import 'package:flowy_infra/image.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../grid/presentation/layout/sizes.dart';
|
import '../../grid/presentation/layout/sizes.dart';
|
||||||
|
import '../../widgets/row/cells/select_option_cell/extension.dart';
|
||||||
import '../application/calendar_bloc.dart';
|
import '../application/calendar_bloc.dart';
|
||||||
|
|
||||||
class CalendarDayCard extends StatelessWidget {
|
class CalendarDayCard extends StatelessWidget {
|
||||||
@ -23,11 +23,10 @@ class CalendarDayCard extends StatelessWidget {
|
|||||||
final bool isInMonth;
|
final bool isInMonth;
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
final RowCache _rowCache;
|
final RowCache _rowCache;
|
||||||
final CardCellBuilder _cellBuilder;
|
|
||||||
final List<CalendarDayEvent> events;
|
final List<CalendarDayEvent> events;
|
||||||
final void Function(DateTime) onCreateEvent;
|
final void Function(DateTime) onCreateEvent;
|
||||||
|
|
||||||
CalendarDayCard({
|
const CalendarDayCard({
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
required this.isToday,
|
required this.isToday,
|
||||||
required this.isInMonth,
|
required this.isInMonth,
|
||||||
@ -37,7 +36,6 @@ class CalendarDayCard extends StatelessWidget {
|
|||||||
required this.events,
|
required this.events,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : _rowCache = rowCache,
|
}) : _rowCache = rowCache,
|
||||||
_cellBuilder = CardCellBuilder(rowCache.cellCache),
|
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -50,42 +48,38 @@ class CalendarDayCard extends StatelessWidget {
|
|||||||
return ChangeNotifierProvider(
|
return ChangeNotifierProvider(
|
||||||
create: (_) => _CardEnterNotifier(),
|
create: (_) => _CardEnterNotifier(),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final children = events.map((event) {
|
List<GestureDetector> cards = _buildCards(context);
|
||||||
return _DayEventCell(
|
|
||||||
event: event,
|
Widget? multipleCards;
|
||||||
viewId: viewId,
|
if (cards.isNotEmpty) {
|
||||||
onClick: () => _showRowDetailPage(event, context),
|
multipleCards = Flexible(
|
||||||
child: _cellBuilder.buildCell(
|
child: ListView.separated(
|
||||||
cellId: event.cellId,
|
itemBuilder: (BuildContext context, int index) {
|
||||||
styles: {FieldType.RichText: TextCardCellStyle(10)},
|
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(
|
final child = Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
_Header(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
date: date,
|
||||||
child: _Header(
|
isInMonth: isInMonth,
|
||||||
date: date,
|
isToday: isToday,
|
||||||
isInMonth: isInMonth,
|
onCreate: () => onCreateEvent(date),
|
||||||
isToday: isToday,
|
|
||||||
onCreate: () => onCreateEvent(date),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Add a separator between the header and the content.
|
||||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||||
Flexible(
|
|
||||||
child: ListView.separated(
|
// Use SizedBox instead of ListView if there are no cards.
|
||||||
itemBuilder: (BuildContext context, int index) {
|
multipleCards ?? const SizedBox(),
|
||||||
return children[index];
|
|
||||||
},
|
|
||||||
itemCount: children.length,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
separatorBuilder: (BuildContext context, int index) =>
|
|
||||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -96,7 +90,7 @@ class CalendarDayCard extends StatelessWidget {
|
|||||||
onEnter: (p) => notifyEnter(context, true),
|
onEnter: (p) => notifyEnter(context, true),
|
||||||
onExit: (p) => notifyEnter(context, false),
|
onExit: (p) => notifyEnter(context, false),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -105,6 +99,113 @@ class CalendarDayCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<GestureDetector> _buildCards(BuildContext context) {
|
||||||
|
final children = events.map((CalendarDayEvent event) {
|
||||||
|
final cellBuilder = CardCellBuilder<String>(_rowCache.cellCache);
|
||||||
|
final rowInfo = _rowCache.getRow(event.eventId);
|
||||||
|
|
||||||
|
final renderHook = RowCardRenderHook<String>();
|
||||||
|
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<String>(
|
||||||
|
// 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) {
|
void _showRowDetailPage(CalendarDayEvent event, BuildContext context) {
|
||||||
final dataController = RowController(
|
final dataController = RowController(
|
||||||
rowId: event.cellId.rowId,
|
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 {
|
class _Header extends StatelessWidget {
|
||||||
final bool isToday;
|
final bool isToday;
|
||||||
final bool isInMonth;
|
final bool isInMonth;
|
||||||
@ -191,12 +256,16 @@ class _Header extends StatelessWidget {
|
|||||||
isInMonth: isInMonth,
|
isInMonth: isInMonth,
|
||||||
date: date,
|
date: date,
|
||||||
);
|
);
|
||||||
return Row(
|
|
||||||
children: [
|
return Padding(
|
||||||
if (notifier.onEnter) _NewEventButton(onClick: onCreate),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
const Spacer(),
|
child: Row(
|
||||||
badge,
|
children: [
|
||||||
],
|
if (notifier.onEnter) _NewEventButton(onClick: onCreate),
|
||||||
|
const Spacer(),
|
||||||
|
badge,
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -85,13 +85,20 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
BlocListener<CalendarBloc, CalendarState>(
|
||||||
|
listenWhen: (p, c) => p.createdEvent != c.createdEvent,
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.createdEvent != null) {
|
||||||
|
_showRowDetailPage(state.createdEvent!.event!, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
BlocListener<CalendarBloc, CalendarState>(
|
BlocListener<CalendarBloc, CalendarState>(
|
||||||
listenWhen: (p, c) => p.newEvent != c.newEvent,
|
listenWhen: (p, c) => p.newEvent != c.newEvent,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.newEvent != null) {
|
if (state.newEvent != null) {
|
||||||
_eventController.add(state.newEvent!);
|
_eventController.add(state.newEvent!);
|
||||||
}
|
}
|
||||||
_showRowDetailPage(state.newEvent!.event!, context);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -120,7 +127,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
child: MonthView(
|
child: MonthView(
|
||||||
key: _calendarState,
|
key: _calendarState,
|
||||||
controller: _eventController,
|
controller: _eventController,
|
||||||
cellAspectRatio: .9,
|
cellAspectRatio: .6,
|
||||||
startDay: _weekdayFromInt(firstDayOfWeek),
|
startDay: _weekdayFromInt(firstDayOfWeek),
|
||||||
borderColor: Theme.of(context).dividerColor,
|
borderColor: Theme.of(context).dividerColor,
|
||||||
headerBuilder: _headerNavigatorBuilder,
|
headerBuilder: _headerNavigatorBuilder,
|
||||||
|
@ -87,7 +87,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
|
databaseController.setListener(onDatabaseChanged: onDatabaseChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openGrid(Emitter<GridState> emit) async {
|
Future<void> _openGrid(Emitter<GridState> emit) async {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@ -7,31 +8,39 @@ import '../../../application/row/row_data_controller.dart';
|
|||||||
part 'row_detail_bloc.freezed.dart';
|
part 'row_detail_bloc.freezed.dart';
|
||||||
|
|
||||||
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||||
|
final RowBackendService rowService;
|
||||||
final RowController dataController;
|
final RowController dataController;
|
||||||
|
|
||||||
RowDetailBloc({
|
RowDetailBloc({
|
||||||
required this.dataController,
|
required this.dataController,
|
||||||
}) : super(RowDetailState.initial()) {
|
}) : rowService = RowBackendService(viewId: dataController.viewId),
|
||||||
|
super(RowDetailState.initial()) {
|
||||||
on<RowDetailEvent>(
|
on<RowDetailEvent>(
|
||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.map(
|
await event.when(
|
||||||
initial: (_Initial value) async {
|
initial: () async {
|
||||||
await _startListening();
|
await _startListening();
|
||||||
final cells = dataController.loadData();
|
final cells = dataController.loadData();
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
|
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
didReceiveCellDatas: (_DidReceiveCellDatas value) {
|
didReceiveCellDatas: (cells) {
|
||||||
emit(state.copyWith(gridCells: value.gridCells));
|
emit(state.copyWith(gridCells: cells));
|
||||||
},
|
},
|
||||||
deleteField: (_DeleteField value) {
|
deleteField: (fieldId) {
|
||||||
final fieldService = FieldBackendService(
|
final fieldService = FieldBackendService(
|
||||||
viewId: dataController.viewId,
|
viewId: dataController.viewId,
|
||||||
fieldId: value.fieldId,
|
fieldId: fieldId,
|
||||||
);
|
);
|
||||||
fieldService.deleteField();
|
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<RowDetailEvent, RowDetailState> {
|
|||||||
class RowDetailEvent with _$RowDetailEvent {
|
class RowDetailEvent with _$RowDetailEvent {
|
||||||
const factory RowDetailEvent.initial() = _Initial;
|
const factory RowDetailEvent.initial() = _Initial;
|
||||||
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
|
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(
|
const factory RowDetailEvent.didReceiveCellDatas(
|
||||||
List<CellIdentifier> gridCells,
|
List<CellIdentifier> gridCells,
|
||||||
) = _DidReceiveCellDatas;
|
) = _DidReceiveCellDatas;
|
||||||
|
@ -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/application/row/row_cache.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.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/accessory.dart';
|
||||||
import 'container/card_container.dart';
|
import 'container/card_container.dart';
|
||||||
|
|
||||||
class Card<CustomCardData> extends StatefulWidget {
|
/// Edit a database row with card style widget
|
||||||
|
class RowCard<CustomCardData> extends StatefulWidget {
|
||||||
final RowPB row;
|
final RowPB row;
|
||||||
final String viewId;
|
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 CustomCardData? cardData;
|
||||||
final bool isEditing;
|
final bool isEditing;
|
||||||
final RowCache rowCache;
|
final RowCache rowCache;
|
||||||
final CardCellBuilder<CustomCardData> cellBuilder;
|
|
||||||
final void Function(BuildContext) openCard;
|
|
||||||
final VoidCallback onStartEditing;
|
|
||||||
final VoidCallback onEndEditing;
|
|
||||||
final CardConfiguration<CustomCardData>? configuration;
|
|
||||||
|
|
||||||
const Card({
|
/// The [CardCellBuilder] is used to build the card cells.
|
||||||
|
final CardCellBuilder<CustomCardData> 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<CustomCardData>? renderHook;
|
||||||
|
|
||||||
|
final RowCardStyleConfiguration styleConfiguration;
|
||||||
|
|
||||||
|
const RowCard({
|
||||||
required this.row,
|
required this.row,
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
required this.fieldId,
|
this.groupingFieldId,
|
||||||
required this.isEditing,
|
required this.isEditing,
|
||||||
required this.rowCache,
|
required this.rowCache,
|
||||||
required this.cellBuilder,
|
required this.cellBuilder,
|
||||||
@ -37,15 +55,19 @@ class Card<CustomCardData> extends StatefulWidget {
|
|||||||
required this.onStartEditing,
|
required this.onStartEditing,
|
||||||
required this.onEndEditing,
|
required this.onEndEditing,
|
||||||
this.cardData,
|
this.cardData,
|
||||||
this.configuration,
|
this.styleConfiguration = const RowCardStyleConfiguration(
|
||||||
|
showAccessory: true,
|
||||||
|
),
|
||||||
|
this.renderHook,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<Card<CustomCardData>> createState() => _CardState<CustomCardData>();
|
State<RowCard<CustomCardData>> createState() =>
|
||||||
|
_RowCardState<CustomCardData>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CardState<T> extends State<Card<T>> {
|
class _RowCardState<T> extends State<RowCard<T>> {
|
||||||
late CardBloc _cardBloc;
|
late CardBloc _cardBloc;
|
||||||
late EditableRowNotifier rowNotifier;
|
late EditableRowNotifier rowNotifier;
|
||||||
late PopoverController popoverController;
|
late PopoverController popoverController;
|
||||||
@ -56,15 +78,15 @@ class _CardState<T> extends State<Card<T>> {
|
|||||||
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
|
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
|
||||||
_cardBloc = CardBloc(
|
_cardBloc = CardBloc(
|
||||||
viewId: widget.viewId,
|
viewId: widget.viewId,
|
||||||
groupFieldId: widget.fieldId,
|
groupFieldId: widget.groupingFieldId,
|
||||||
isEditing: widget.isEditing,
|
isEditing: widget.isEditing,
|
||||||
row: widget.row,
|
row: widget.row,
|
||||||
rowCache: widget.rowCache,
|
rowCache: widget.rowCache,
|
||||||
)..add(const BoardCardEvent.initial());
|
)..add(const RowCardEvent.initial());
|
||||||
|
|
||||||
rowNotifier.isEditing.addListener(() {
|
rowNotifier.isEditing.addListener(() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value));
|
_cardBloc.add(RowCardEvent.setIsEditing(rowNotifier.isEditing.value));
|
||||||
|
|
||||||
if (rowNotifier.isEditing.value) {
|
if (rowNotifier.isEditing.value) {
|
||||||
widget.onStartEditing();
|
widget.onStartEditing();
|
||||||
@ -81,7 +103,7 @@ class _CardState<T> extends State<Card<T>> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: _cardBloc,
|
value: _cardBloc,
|
||||||
child: BlocBuilder<CardBloc, BoardCardState>(
|
child: BlocBuilder<CardBloc, RowCardState>(
|
||||||
buildWhen: (previous, current) {
|
buildWhen: (previous, current) {
|
||||||
// Rebuild when:
|
// Rebuild when:
|
||||||
// 1.If the length of the cells is not the same
|
// 1.If the length of the cells is not the same
|
||||||
@ -106,21 +128,26 @@ class _CardState<T> extends State<Card<T>> {
|
|||||||
context,
|
context,
|
||||||
popoverContext,
|
popoverContext,
|
||||||
),
|
),
|
||||||
child: BoardCardContainer(
|
child: RowCardContainer(
|
||||||
buildAccessoryWhen: () => state.isEditing == false,
|
buildAccessoryWhen: () => state.isEditing == false,
|
||||||
accessoryBuilder: (context) {
|
accessoryBuilder: (context) {
|
||||||
return [
|
if (widget.styleConfiguration.showAccessory == false) {
|
||||||
_CardEditOption(rowNotifier: rowNotifier),
|
return [];
|
||||||
_CardMoreOption(),
|
} else {
|
||||||
];
|
return [
|
||||||
|
_CardEditOption(rowNotifier: rowNotifier),
|
||||||
|
_CardMoreOption(),
|
||||||
|
];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
openAccessory: _handleOpenAccessory,
|
openAccessory: _handleOpenAccessory,
|
||||||
openCard: (context) => widget.openCard(context),
|
openCard: (context) => widget.openCard(context),
|
||||||
child: _CardContent<T>(
|
child: _CardContent<T>(
|
||||||
rowNotifier: rowNotifier,
|
rowNotifier: rowNotifier,
|
||||||
cellBuilder: widget.cellBuilder,
|
cellBuilder: widget.cellBuilder,
|
||||||
|
styleConfiguration: widget.styleConfiguration,
|
||||||
cells: state.cells,
|
cells: state.cells,
|
||||||
cardConfiguration: widget.configuration,
|
renderHook: widget.renderHook,
|
||||||
cardData: widget.cardData,
|
cardData: widget.cardData,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -166,15 +193,17 @@ class _CardState<T> extends State<Card<T>> {
|
|||||||
class _CardContent<CustomCardData> extends StatelessWidget {
|
class _CardContent<CustomCardData> extends StatelessWidget {
|
||||||
final CardCellBuilder<CustomCardData> cellBuilder;
|
final CardCellBuilder<CustomCardData> cellBuilder;
|
||||||
final EditableRowNotifier rowNotifier;
|
final EditableRowNotifier rowNotifier;
|
||||||
final List<BoardCellEquatable> cells;
|
final List<CellIdentifier> cells;
|
||||||
final CardConfiguration<CustomCardData>? cardConfiguration;
|
final RowCardRenderHook<CustomCardData>? renderHook;
|
||||||
final CustomCardData? cardData;
|
final CustomCardData? cardData;
|
||||||
|
final RowCardStyleConfiguration styleConfiguration;
|
||||||
const _CardContent({
|
const _CardContent({
|
||||||
required this.rowNotifier,
|
required this.rowNotifier,
|
||||||
required this.cellBuilder,
|
required this.cellBuilder,
|
||||||
required this.cells,
|
required this.cells,
|
||||||
required this.cardData,
|
required this.cardData,
|
||||||
this.cardConfiguration,
|
required this.styleConfiguration,
|
||||||
|
this.renderHook,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -188,30 +217,30 @@ class _CardContent<CustomCardData> extends StatelessWidget {
|
|||||||
|
|
||||||
List<Widget> _makeCells(
|
List<Widget> _makeCells(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<BoardCellEquatable> cells,
|
List<CellIdentifier> cells,
|
||||||
) {
|
) {
|
||||||
final List<Widget> children = [];
|
final List<Widget> children = [];
|
||||||
// Remove all the cell listeners.
|
// Remove all the cell listeners.
|
||||||
rowNotifier.unbind();
|
rowNotifier.unbind();
|
||||||
|
|
||||||
cells.asMap().forEach(
|
cells.asMap().forEach(
|
||||||
(int index, BoardCellEquatable cell) {
|
(int index, CellIdentifier cell) {
|
||||||
final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
|
final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
|
||||||
final cellNotifier = EditableCardNotifier(isEditing: isEditing);
|
final cellNotifier = EditableCardNotifier(isEditing: isEditing);
|
||||||
|
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
// Only use the first cell to receive user's input when click the edit
|
// Only use the first cell to receive user's input when click the edit
|
||||||
// button
|
// button
|
||||||
rowNotifier.bindCell(cell.identifier, cellNotifier);
|
rowNotifier.bindCell(cell, cellNotifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
final child = Padding(
|
final child = Padding(
|
||||||
key: cell.identifier.key(),
|
key: cell.key(),
|
||||||
padding: const EdgeInsets.only(left: 4, right: 4),
|
padding: styleConfiguration.cellPadding,
|
||||||
child: cellBuilder.buildCell(
|
child: cellBuilder.buildCell(
|
||||||
cellId: cell.identifier,
|
cellId: cell,
|
||||||
cellNotifier: cellNotifier,
|
cellNotifier: cellNotifier,
|
||||||
cardConfiguration: cardConfiguration,
|
renderHook: renderHook,
|
||||||
cardData: cardData,
|
cardData: cardData,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -265,3 +294,13 @@ class _CardEditOption extends StatelessWidget with CardAccessory {
|
|||||||
@override
|
@override
|
||||||
AccessoryType get type => AccessoryType.edit;
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -12,9 +11,9 @@ import '../../application/row/row_service.dart';
|
|||||||
|
|
||||||
part 'card_bloc.freezed.dart';
|
part 'card_bloc.freezed.dart';
|
||||||
|
|
||||||
class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
class CardBloc extends Bloc<RowCardEvent, RowCardState> {
|
||||||
final RowPB row;
|
final RowPB row;
|
||||||
final String groupFieldId;
|
final String? groupFieldId;
|
||||||
final RowBackendService _rowBackendSvc;
|
final RowBackendService _rowBackendSvc;
|
||||||
final RowCache _rowCache;
|
final RowCache _rowCache;
|
||||||
VoidCallback? _rowCallback;
|
VoidCallback? _rowCallback;
|
||||||
@ -28,13 +27,13 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
|||||||
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||||
_rowCache = rowCache,
|
_rowCache = rowCache,
|
||||||
super(
|
super(
|
||||||
BoardCardState.initial(
|
RowCardState.initial(
|
||||||
row,
|
row,
|
||||||
_makeCells(groupFieldId, rowCache.loadGridCells(row.id)),
|
_makeCells(groupFieldId, rowCache.loadGridCells(row.id)),
|
||||||
isEditing,
|
isEditing,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
on<BoardCardEvent>(
|
on<RowCardEvent>(
|
||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
initial: () async {
|
initial: () async {
|
||||||
@ -69,7 +68,7 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
|||||||
return RowInfo(
|
return RowInfo(
|
||||||
viewId: _rowBackendSvc.viewId,
|
viewId: _rowBackendSvc.viewId,
|
||||||
fields: UnmodifiableListView(
|
fields: UnmodifiableListView(
|
||||||
state.cells.map((cell) => cell.identifier.fieldInfo).toList(),
|
state.cells.map((cell) => cell.fieldInfo).toList(),
|
||||||
),
|
),
|
||||||
rowPB: state.rowPB,
|
rowPB: state.rowPB,
|
||||||
);
|
);
|
||||||
@ -81,70 +80,58 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
|||||||
onCellUpdated: (cellMap, reason) {
|
onCellUpdated: (cellMap, reason) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
final cells = _makeCells(groupFieldId, cellMap);
|
final cells = _makeCells(groupFieldId, cellMap);
|
||||||
add(BoardCardEvent.didReceiveCells(cells, reason));
|
add(RowCardEvent.didReceiveCells(cells, reason));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<BoardCellEquatable> _makeCells(
|
List<CellIdentifier> _makeCells(
|
||||||
String groupFieldId,
|
String? groupFieldId,
|
||||||
CellByFieldId originalCellMap,
|
CellByFieldId originalCellMap,
|
||||||
) {
|
) {
|
||||||
List<BoardCellEquatable> cells = [];
|
List<CellIdentifier> cells = [];
|
||||||
for (final entry in originalCellMap.entries) {
|
for (final entry in originalCellMap.entries) {
|
||||||
// Filter out the cell if it's fieldId equal to the groupFieldId
|
// Filter out the cell if it's fieldId equal to the groupFieldId
|
||||||
if (entry.value.fieldId != groupFieldId) {
|
if (groupFieldId != null) {
|
||||||
cells.add(BoardCellEquatable(entry.value));
|
if (entry.value.fieldId == groupFieldId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cells.add(entry.value);
|
||||||
}
|
}
|
||||||
return cells;
|
return cells;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class BoardCardEvent with _$BoardCardEvent {
|
class RowCardEvent with _$RowCardEvent {
|
||||||
const factory BoardCardEvent.initial() = _InitialRow;
|
const factory RowCardEvent.initial() = _InitialRow;
|
||||||
const factory BoardCardEvent.setIsEditing(bool isEditing) = _IsEditing;
|
const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing;
|
||||||
const factory BoardCardEvent.didReceiveCells(
|
const factory RowCardEvent.didReceiveCells(
|
||||||
List<BoardCellEquatable> cells,
|
List<CellIdentifier> cells,
|
||||||
RowsChangedReason reason,
|
RowsChangedReason reason,
|
||||||
) = _DidReceiveCells;
|
) = _DidReceiveCells;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class BoardCardState with _$BoardCardState {
|
class RowCardState with _$RowCardState {
|
||||||
const factory BoardCardState({
|
const factory RowCardState({
|
||||||
required RowPB rowPB,
|
required RowPB rowPB,
|
||||||
required List<BoardCellEquatable> cells,
|
required List<CellIdentifier> cells,
|
||||||
required bool isEditing,
|
required bool isEditing,
|
||||||
RowsChangedReason? changeReason,
|
RowsChangedReason? changeReason,
|
||||||
}) = _BoardCardState;
|
}) = _RowCardState;
|
||||||
|
|
||||||
factory BoardCardState.initial(
|
factory RowCardState.initial(
|
||||||
RowPB rowPB,
|
RowPB rowPB,
|
||||||
List<BoardCellEquatable> cells,
|
List<CellIdentifier> cells,
|
||||||
bool isEditing,
|
bool isEditing,
|
||||||
) =>
|
) =>
|
||||||
BoardCardState(
|
RowCardState(
|
||||||
rowPB: rowPB,
|
rowPB: rowPB,
|
||||||
cells: cells,
|
cells: cells,
|
||||||
isEditing: isEditing,
|
isEditing: isEditing,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class BoardCellEquatable extends Equatable {
|
|
||||||
final CellIdentifier identifier;
|
|
||||||
|
|
||||||
const BoardCellEquatable(this.identifier);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props {
|
|
||||||
return [
|
|
||||||
identifier.fieldInfo.id,
|
|
||||||
identifier.fieldInfo.fieldType,
|
|
||||||
identifier.fieldInfo.visibility,
|
|
||||||
identifier.fieldInfo.width,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -22,7 +22,7 @@ class CardCellBuilder<CustomCardData> {
|
|||||||
CustomCardData? cardData,
|
CustomCardData? cardData,
|
||||||
required CellIdentifier cellId,
|
required CellIdentifier cellId,
|
||||||
EditableCardNotifier? cellNotifier,
|
EditableCardNotifier? cellNotifier,
|
||||||
CardConfiguration<CustomCardData>? cardConfiguration,
|
RowCardRenderHook<CustomCardData>? renderHook,
|
||||||
Map<FieldType, CardCellStyle>? styles,
|
Map<FieldType, CardCellStyle>? styles,
|
||||||
}) {
|
}) {
|
||||||
final cellControllerBuilder = CellControllerBuilder(
|
final cellControllerBuilder = CellControllerBuilder(
|
||||||
@ -39,20 +39,21 @@ class CardCellBuilder<CustomCardData> {
|
|||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
case FieldType.DateTime:
|
case FieldType.DateTime:
|
||||||
return DateCardCell(
|
return DateCardCell<CustomCardData>(
|
||||||
|
renderHook: renderHook?.renderHook[FieldType.DateTime],
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
case FieldType.SingleSelect:
|
case FieldType.SingleSelect:
|
||||||
return SelectOptionCardCell<CustomCardData>(
|
return SelectOptionCardCell<CustomCardData>(
|
||||||
renderHook: cardConfiguration?.renderHook[FieldType.SingleSelect],
|
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
cardData: cardData,
|
cardData: cardData,
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
case FieldType.MultiSelect:
|
case FieldType.MultiSelect:
|
||||||
return SelectOptionCardCell<CustomCardData>(
|
return SelectOptionCardCell<CustomCardData>(
|
||||||
renderHook: cardConfiguration?.renderHook[FieldType.MultiSelect],
|
renderHook: renderHook?.renderHook[FieldType.MultiSelect],
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
cardData: cardData,
|
cardData: cardData,
|
||||||
editableNotifier: cellNotifier,
|
editableNotifier: cellNotifier,
|
||||||
@ -69,9 +70,11 @@ class CardCellBuilder<CustomCardData> {
|
|||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
case FieldType.RichText:
|
case FieldType.RichText:
|
||||||
return TextCardCell(
|
return TextCardCell<CustomCardData>(
|
||||||
|
renderHook: renderHook?.renderHook[FieldType.RichText],
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
editableNotifier: cellNotifier,
|
editableNotifier: cellNotifier,
|
||||||
|
cardData: cardData,
|
||||||
style: isStyleOrNull<TextCardCellStyle>(style),
|
style: isStyleOrNull<TextCardCellStyle>(style),
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
|
@ -1,26 +1,59 @@
|
|||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
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/field_entities.pbenum.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
typedef CellRenderHook<C, T> = Widget? Function(C cellData, T cardData);
|
typedef CellRenderHook<C, CustomCardData> = Widget? Function(
|
||||||
|
C cellData,
|
||||||
|
CustomCardData cardData,
|
||||||
|
BuildContext buildContext,
|
||||||
|
);
|
||||||
typedef RenderHookByFieldType<C> = Map<FieldType, CellRenderHook<dynamic, C>>;
|
typedef RenderHookByFieldType<C> = Map<FieldType, CellRenderHook<dynamic, C>>;
|
||||||
|
|
||||||
class CardConfiguration<CustomCardData> {
|
class RowCardRenderHook<CustomCardData> {
|
||||||
final RenderHookByFieldType<CustomCardData> renderHook = {};
|
final RenderHookByFieldType<CustomCardData> renderHook = {};
|
||||||
CardConfiguration();
|
RowCardRenderHook();
|
||||||
|
|
||||||
|
/// Add render hook for the FieldType.SingleSelect and FieldType.MultiSelect
|
||||||
void addSelectOptionHook(
|
void addSelectOptionHook(
|
||||||
CellRenderHook<List<SelectOptionPB>, CustomCardData> hook,
|
CellRenderHook<List<SelectOptionPB>, CustomCardData?> hook,
|
||||||
) {
|
) {
|
||||||
selectOptionHook(cellData, cardData) {
|
final hookFn = _typeSafeHook<List<SelectOptionPB>>(hook);
|
||||||
if (cellData is List<SelectOptionPB>) {
|
renderHook[FieldType.SingleSelect] = hookFn;
|
||||||
hook(cellData, cardData);
|
renderHook[FieldType.MultiSelect] = hookFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addTextFieldHook(
|
||||||
|
CellRenderHook<String, CustomCardData?> hook,
|
||||||
|
) {
|
||||||
|
renderHook[FieldType.RichText] = _typeSafeHook<String>(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addDateFieldHook(
|
||||||
|
CellRenderHook<DateCellDataPB, CustomCardData?> hook,
|
||||||
|
) {
|
||||||
|
renderHook[FieldType.DateTime] = _typeSafeHook<DateCellDataPB>(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
CellRenderHook<dynamic, CustomCardData> _typeSafeHook<C>(
|
||||||
|
CellRenderHook<C, CustomCardData?> 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;
|
return hookFn;
|
||||||
renderHook[FieldType.MultiSelect] = selectOptionHook;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,13 +44,16 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
|
|||||||
: svgWidget('editor/editor_uncheck');
|
: svgWidget('editor/editor_uncheck');
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: FlowyIconButton(
|
child: Padding(
|
||||||
iconPadding: EdgeInsets.zero,
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
icon: icon,
|
child: FlowyIconButton(
|
||||||
width: 20,
|
iconPadding: EdgeInsets.zero,
|
||||||
onPressed: () => context
|
icon: icon,
|
||||||
.read<CheckboxCardCellBloc>()
|
width: 20,
|
||||||
.add(const CheckboxCardCellEvent.select()),
|
onPressed: () => context
|
||||||
|
.read<CheckboxCardCellBloc>()
|
||||||
|
.add(const CheckboxCardCellEvent.select()),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -7,11 +7,13 @@ import '../bloc/date_card_cell_bloc.dart';
|
|||||||
import '../define.dart';
|
import '../define.dart';
|
||||||
import 'card_cell.dart';
|
import 'card_cell.dart';
|
||||||
|
|
||||||
class DateCardCell extends CardCell {
|
class DateCardCell<CustomCardData> extends CardCell {
|
||||||
final CellControllerBuilder cellControllerBuilder;
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
final CellRenderHook<dynamic, CustomCardData>? renderHook;
|
||||||
|
|
||||||
const DateCardCell({
|
const DateCardCell({
|
||||||
required this.cellControllerBuilder,
|
required this.cellControllerBuilder,
|
||||||
|
this.renderHook,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -42,6 +44,15 @@ class _DateCardCellState extends State<DateCardCell> {
|
|||||||
if (state.dateStr.isEmpty) {
|
if (state.dateStr.isEmpty) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
} else {
|
} else {
|
||||||
|
Widget? custom = widget.renderHook?.call(
|
||||||
|
state.data,
|
||||||
|
widget.cardData,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
if (custom != null) {
|
||||||
|
return custom;
|
||||||
|
}
|
||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
@ -11,17 +11,18 @@ import 'card_cell.dart';
|
|||||||
|
|
||||||
class SelectOptionCardCellStyle extends CardCellStyle {}
|
class SelectOptionCardCellStyle extends CardCellStyle {}
|
||||||
|
|
||||||
class SelectOptionCardCell<T> extends CardCell<T, SelectOptionCardCellStyle>
|
class SelectOptionCardCell<CustomCardData>
|
||||||
|
extends CardCell<CustomCardData, SelectOptionCardCellStyle>
|
||||||
with EditableCell {
|
with EditableCell {
|
||||||
final CellControllerBuilder cellControllerBuilder;
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
final CellRenderHook<List<SelectOptionPB>, T>? renderHook;
|
final CellRenderHook<List<SelectOptionPB>, CustomCardData>? renderHook;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final EditableCardNotifier? editableNotifier;
|
final EditableCardNotifier? editableNotifier;
|
||||||
|
|
||||||
SelectOptionCardCell({
|
SelectOptionCardCell({
|
||||||
required this.cellControllerBuilder,
|
required this.cellControllerBuilder,
|
||||||
required T? cardData,
|
required CustomCardData? cardData,
|
||||||
this.renderHook,
|
this.renderHook,
|
||||||
this.editableNotifier,
|
this.editableNotifier,
|
||||||
Key? key,
|
Key? key,
|
||||||
@ -57,6 +58,7 @@ class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
|
|||||||
Widget? custom = widget.renderHook?.call(
|
Widget? custom = widget.renderHook?.call(
|
||||||
state.selectedOptions,
|
state.selectedOptions,
|
||||||
widget.cardData,
|
widget.cardData,
|
||||||
|
context,
|
||||||
);
|
);
|
||||||
if (custom != null) {
|
if (custom != null) {
|
||||||
return custom;
|
return custom;
|
||||||
|
@ -14,18 +14,21 @@ class TextCardCellStyle extends CardCellStyle {
|
|||||||
TextCardCellStyle(this.fontSize);
|
TextCardCellStyle(this.fontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextCardCell extends CardCell<String, TextCardCellStyle>
|
class TextCardCell<CustomCardData>
|
||||||
with EditableCell {
|
extends CardCell<CustomCardData, TextCardCellStyle> with EditableCell {
|
||||||
@override
|
@override
|
||||||
final EditableCardNotifier? editableNotifier;
|
final EditableCardNotifier? editableNotifier;
|
||||||
final CellControllerBuilder cellControllerBuilder;
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
final CellRenderHook<String, CustomCardData>? renderHook;
|
||||||
|
|
||||||
const TextCardCell({
|
const TextCardCell({
|
||||||
required this.cellControllerBuilder,
|
required this.cellControllerBuilder,
|
||||||
|
required CustomCardData? cardData,
|
||||||
this.editableNotifier,
|
this.editableNotifier,
|
||||||
|
this.renderHook,
|
||||||
TextCardCellStyle? style,
|
TextCardCellStyle? style,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key, style: style);
|
}) : super(key: key, style: style, cardData: cardData);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TextCardCell> createState() => _TextCardCellState();
|
State<TextCardCell> createState() => _TextCardCellState();
|
||||||
@ -104,6 +107,16 @@ class _TextCardCellState extends State<TextCardCell> {
|
|||||||
return previous != current;
|
return previous != current;
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
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 &&
|
if (state.content.isEmpty &&
|
||||||
state.enableEdit == false &&
|
state.enableEdit == false &&
|
||||||
focusWhenInit == false) {
|
focusWhenInit == false) {
|
||||||
|
@ -4,13 +4,13 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
|
|
||||||
import 'accessory.dart';
|
import 'accessory.dart';
|
||||||
|
|
||||||
class BoardCardContainer extends StatelessWidget {
|
class RowCardContainer extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final CardAccessoryBuilder? accessoryBuilder;
|
final CardAccessoryBuilder? accessoryBuilder;
|
||||||
final bool Function()? buildAccessoryWhen;
|
final bool Function()? buildAccessoryWhen;
|
||||||
final void Function(BuildContext) openCard;
|
final void Function(BuildContext) openCard;
|
||||||
final void Function(AccessoryType) openAccessory;
|
final void Function(AccessoryType) openAccessory;
|
||||||
const BoardCardContainer({
|
const RowCardContainer({
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.openCard,
|
required this.openCard,
|
||||||
required this.openAccessory,
|
required this.openAccessory,
|
||||||
|
@ -43,83 +43,84 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RowDetailPageState extends State<RowDetailPage> {
|
class _RowDetailPageState extends State<RowDetailPage> {
|
||||||
final padding = const EdgeInsets.symmetric(
|
|
||||||
horizontal: 40,
|
|
||||||
vertical: 20,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyDialog(
|
return FlowyDialog(
|
||||||
child: BlocProvider(
|
child: BlocProvider(
|
||||||
create: (context) {
|
create: (context) {
|
||||||
final bloc = RowDetailBloc(
|
return RowDetailBloc(dataController: widget.dataController)
|
||||||
dataController: widget.dataController,
|
..add(const RowDetailEvent.initial());
|
||||||
);
|
|
||||||
bloc.add(const RowDetailEvent.initial());
|
|
||||||
return bloc;
|
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: ListView(
|
||||||
padding: padding,
|
children: [
|
||||||
child: Column(
|
// using ListView here for future expansion:
|
||||||
children: [
|
// - header and cover image
|
||||||
const _Header(),
|
// - lower rich text area
|
||||||
Expanded(
|
IntrinsicHeight(child: _responsiveRowInfo()),
|
||||||
child: _PropertyColumn(
|
const Divider(height: 1.0)
|
||||||
cellBuilder: widget.cellBuilder,
|
],
|
||||||
viewId: widget.dataController.viewId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class _Header extends StatelessWidget {
|
Widget _responsiveRowInfo() {
|
||||||
const _Header({Key? key}) : super(key: key);
|
final rowDataColumn = _PropertyColumn(
|
||||||
|
cellBuilder: widget.cellBuilder,
|
||||||
@override
|
viewId: widget.dataController.viewId,
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: 30,
|
|
||||||
child: Row(
|
|
||||||
children: const [Spacer(), _CloseButton()],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
final rowOptionColumn = _RowOptionColumn(
|
||||||
}
|
viewId: widget.dataController.viewId,
|
||||||
|
rowId: widget.dataController.rowId,
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
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 {
|
class _PropertyColumn extends StatelessWidget {
|
||||||
final String viewId;
|
final String viewId;
|
||||||
final GridCellBuilder cellBuilder;
|
final GridCellBuilder cellBuilder;
|
||||||
final ScrollController _scrollController;
|
const _PropertyColumn({
|
||||||
_PropertyColumn({
|
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
required this.cellBuilder,
|
required this.cellBuilder,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : _scrollController = ScrollController(),
|
}) : super(key: key);
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -127,63 +128,34 @@ class _PropertyColumn extends StatelessWidget {
|
|||||||
buildWhen: (previous, current) => previous.gridCells != current.gridCells,
|
buildWhen: (previous, current) => previous.gridCells != current.gridCells,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _wrapScrollbar(buildPropertyCells(state))),
|
...state.gridCells
|
||||||
const VSpace(10),
|
.map(
|
||||||
_CreatePropertyButton(
|
(cell) => Padding(
|
||||||
viewId: viewId,
|
padding: const EdgeInsets.only(bottom: 4.0),
|
||||||
onClosed: _scrollToNewProperty,
|
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 {
|
class _CreatePropertyButton extends StatefulWidget {
|
||||||
final String viewId;
|
final String viewId;
|
||||||
final VoidCallback onClosed;
|
|
||||||
|
|
||||||
const _CreatePropertyButton({
|
const _CreatePropertyButton({
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
required this.onClosed,
|
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -207,10 +179,8 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
|
|||||||
controller: popoverController,
|
controller: popoverController,
|
||||||
direction: PopoverDirection.topWithLeftAligned,
|
direction: PopoverDirection.topWithLeftAligned,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
onClose: widget.onClosed,
|
child: SizedBox(
|
||||||
child: Container(
|
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: _makeBoxDecoration(context),
|
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
LocaleKeys.grid_field_newProperty.tr(),
|
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 {
|
class _PropertyCell extends StatefulWidget {
|
||||||
@ -377,3 +339,69 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
|||||||
}
|
}
|
||||||
throw UnimplementedError;
|
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<RowDetailBloc>().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<RowDetailBloc>().add(RowDetailEvent.duplicateRow(rowId));
|
||||||
|
FlowyOverlay.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -283,7 +283,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
|
|||||||
return 3.0;
|
return 3.0;
|
||||||
}),
|
}),
|
||||||
crossAxisMargin: 0.0,
|
crossAxisMargin: 0.0,
|
||||||
mainAxisMargin: 0.0,
|
mainAxisMargin: 6.0,
|
||||||
radius: Corners.s10Radius,
|
radius: Corners.s10Radius,
|
||||||
),
|
),
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
@ -313,7 +313,9 @@ pub(crate) async fn duplicate_row_handler(
|
|||||||
) -> Result<(), FlowyError> {
|
) -> Result<(), FlowyError> {
|
||||||
let params: RowIdParams = data.into_inner().try_into()?;
|
let params: RowIdParams = data.into_inner().try_into()?;
|
||||||
let editor = manager.get_database_editor(¶ms.view_id).await?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,11 +248,11 @@ pub fn insert_checkbox_cell(is_check: bool, field_rev: &FieldRevision) -> CellRe
|
|||||||
CellRevision::new(data)
|
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 {
|
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,
|
time: None,
|
||||||
include_time: Some(false),
|
include_time: Some(date_cell_data.include_time),
|
||||||
is_utc: true,
|
is_utc: true,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -520,7 +520,31 @@ impl DatabaseEditor {
|
|||||||
self.database_views.subscribe_view_changed(view_id).await
|
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::<HashMap<String, String>>();
|
||||||
|
|
||||||
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,8 +157,7 @@ impl FromCellString for DateCellData {
|
|||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
let result: DateCellData = serde_json::from_str(s).unwrap();
|
Ok(serde_json::from_str::<DateCellData>(s).unwrap_or_default())
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ use crate::services::cell::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::entities::FieldType;
|
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 database_model::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -52,12 +52,12 @@ impl RowRevisionBuilder {
|
|||||||
FieldType::RichText => builder.insert_text_cell(&field_id, cell_data),
|
FieldType::RichText => builder.insert_text_cell(&field_id, cell_data),
|
||||||
FieldType::Number => {
|
FieldType::Number => {
|
||||||
if let Ok(num) = cell_data.parse::<i64>() {
|
if let Ok(num) = cell_data.parse::<i64>() {
|
||||||
builder.insert_date_cell(&field_id, num)
|
builder.insert_number_cell(&field_id, num)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
FieldType::DateTime => {
|
FieldType::DateTime => {
|
||||||
if let Ok(timestamp) = cell_data.parse::<i64>() {
|
if let Ok(date_cell_data) = DateCellData::from_cell_str(&cell_data) {
|
||||||
builder.insert_date_cell(&field_id, timestamp)
|
builder.insert_date_cell(&field_id, date_cell_data)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
FieldType::MultiSelect | FieldType::SingleSelect => {
|
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()) {
|
match self.field_rev_map.get(&field_id.to_owned()) {
|
||||||
None => tracing::warn!("Can't find the date field with id: {}", field_id),
|
None => tracing::warn!("Can't find the date field with id: {}", field_id),
|
||||||
Some(field_rev) => {
|
Some(field_rev) => {
|
||||||
self
|
self.payload.cell_by_field_id.insert(
|
||||||
.payload
|
field_id.to_owned(),
|
||||||
.cell_by_field_id
|
insert_date_cell(date_cell_data, field_rev),
|
||||||
.insert(field_id.to_owned(), insert_date_cell(timestamp, field_rev));
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user