chore: Merge branch 'main' into develop

This commit is contained in:
nathan
2023-05-04 12:39:46 +08:00
146 changed files with 2576 additions and 1572 deletions

View File

@ -77,10 +77,7 @@ class CellController<T, D> extends Equatable {
_cellListener?.start(
onCellChanged: (result) {
result.fold(
(_) {
_cellCache.remove(_cacheKey);
_loadData();
},
(_) => _loadData(),
(err) => Log.error(err),
);
},
@ -174,8 +171,8 @@ class CellController<T, D> extends Equatable {
void _loadData() {
_saveDataOperation?.cancel();
_loadDataOperation?.cancel();
_loadDataOperation = Timer(const Duration(milliseconds: 10), () {
_cellDataLoader.loadData().then((data) {
if (data != null) {
@ -183,7 +180,6 @@ class CellController<T, D> extends Equatable {
} else {
_cellCache.remove(_cacheKey);
}
_cellDataNotifier?.value = data;
});
});

View File

@ -55,7 +55,7 @@ class CellControllerBuilder {
case FieldType.Number:
final cellDataLoader = CellDataLoader(
cellId: _cellId,
parser: StringCellDataParser(),
parser: NumberCellDataParser(),
reloadOnFieldChanged: true,
);
return NumberCellController(

View File

@ -27,7 +27,12 @@ class CellDataLoader<T> {
(result) => result.fold(
(CellPB cell) {
try {
return parser.parserData(cell.data);
// Return null the data of the cell is empty.
if (cell.data.isEmpty) {
return null;
} else {
return parser.parserData(cell.data);
}
} catch (e, s) {
Log.error('$parser parser cellData failed, $e');
Log.error('Stack trace \n $s');
@ -51,6 +56,13 @@ class StringCellDataParser implements CellDataParser<String> {
}
}
class NumberCellDataParser implements CellDataParser<String> {
@override
String? parserData(List<int> data) {
return utf8.decode(data);
}
}
class DateCellDataParser implements CellDataParser<DateCellDataPB> {
@override
DateCellDataPB? parserData(List<int> data) {

View File

@ -45,7 +45,12 @@ class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
Future<Option<FlowyError>> save(DateCellData data) {
var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
// This is a bit of a hack. This converts the data.date which is in
// UTC to Local but actually changes the timestamp instead of just
// changing the isUtc flag
final dateTime = DateTime(data.date.year, data.date.month, data.date.day);
final date = (dateTime.millisecondsSinceEpoch ~/ 1000).toString();
payload.date = date;
payload.isUtc = data.date.isUtc;
payload.includeTime = data.includeTime;

View File

@ -116,7 +116,7 @@ class DatabaseController {
}
}
void addListener({
void setListener({
DatabaseCallbacks? onDatabaseChanged,
LayoutCallbacks? onLayoutChanged,
GroupCallbacks? onGroupChanged,
@ -212,6 +212,11 @@ class DatabaseController {
await _databaseViewBackendSvc.closeView();
await fieldController.dispose();
await groupListener.stop();
await _viewCache.dispose();
_databaseCallbacks = null;
_groupCallbacks = null;
_layoutCallbacks = null;
_calendarLayoutCallbacks = null;
}
Future<void> _loadGroups() async {
@ -252,7 +257,7 @@ class DatabaseController {
_databaseCallbacks?.onRowsCreated?.call(ids);
},
);
_viewCache.addListener(callbacks);
_viewCache.setListener(callbacks);
}
void _listenOnFieldsChanged() {
@ -337,9 +342,10 @@ class RowDataBuilder {
_cellDataByFieldId[fieldInfo.field.id] = num.toString();
}
/// The date should use the UTC timezone. Becuase the backend uses UTC timezone to format the time string.
void insertDate(FieldInfo fieldInfo, DateTime date) {
assert(fieldInfo.fieldType == FieldType.DateTime);
final timestamp = (date.millisecondsSinceEpoch ~/ 1000);
final timestamp = (date.toUtc().millisecondsSinceEpoch ~/ 1000);
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
}

View File

@ -112,9 +112,10 @@ class DatabaseViewCache {
Future<void> dispose() async {
await _databaseViewListener.stop();
await _rowCache.dispose();
_callbacks = null;
}
void addListener(DatabaseViewCallbacks callbacks) {
void setListener(DatabaseViewCallbacks callbacks) {
_callbacks = callbacks;
}
}

View File

@ -237,7 +237,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
},
);
_databaseController.addListener(
_databaseController.setListener(
onDatabaseChanged: onDatabaseChanged,
onGroupChanged: onGroupChanged,
);

View File

@ -78,7 +78,7 @@ class BoardContent extends StatefulWidget {
class _BoardContentState extends State<BoardContent> {
late AppFlowyBoardScrollController scrollManager;
final cardConfiguration = CardConfiguration<String>();
final renderHook = RowCardRenderHook<String>();
final config = const AppFlowyBoardConfig(
groupBackgroundColor: Color(0xffF7F8FC),
@ -87,7 +87,7 @@ class _BoardContentState extends State<BoardContent> {
@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<BoardContent> {
key: ValueKey(groupItemId),
margin: config.cardPadding,
decoration: _makeBoxDecoration(context),
child: Card<String>(
child: RowCard<String>(
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,

View File

@ -47,7 +47,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
emit(state.copyWith(database: Some(database)));
},
didLoadAllEvents: (events) {
emit(state.copyWith(initialEvents: events, allEvents: events));
final calenderEvents = _calendarEventDataFromEventPBs(events);
emit(
state.copyWith(
initialEvents: calenderEvents,
allEvents: calenderEvents,
),
);
},
didReceiveNewLayoutField: (CalendarLayoutSettingPB layoutSettings) {
_loadAllEvents();
@ -56,6 +62,11 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
createEvent: (DateTime date, String title) async {
await _createEvent(date, title);
},
didCreateEvent: (CalendarEventData<CalendarDayEvent> event) {
emit(
state.copyWith(editEvent: event),
);
},
updateCalendarLayoutSetting:
(CalendarLayoutSettingPB layoutSetting) async {
await _updateCalendarLayoutSetting(layoutSetting);
@ -63,7 +74,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
didUpdateEvent: (CalendarEventData<CalendarDayEvent> eventData) {
var allEvents = [...state.allEvents];
final index = allEvents.indexWhere(
(element) => element.event!.cellId == eventData.event!.cellId,
(element) => element.event!.eventId == eventData.event!.eventId,
);
if (index != -1) {
allEvents[index] = eventData;
@ -71,22 +82,13 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
emit(
state.copyWith(
allEvents: allEvents,
updateEvent: eventData,
),
);
},
didReceiveNewEvent: (CalendarEventData<CalendarDayEvent> event) {
emit(
state.copyWith(
allEvents: [...state.allEvents, event],
newEvent: event,
),
);
},
didDeleteEvents: (List<RowId> deletedRowIds) {
var events = [...state.allEvents];
events.retainWhere(
(element) => !deletedRowIds.contains(element.event!.cellId.rowId),
(element) => !deletedRowIds.contains(element.event!.eventId),
);
emit(
state.copyWith(
@ -95,11 +97,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) {
final fieldInfos = _databaseController.fieldController.fieldInfos;
final index = fieldInfos.indexWhere(
@ -143,17 +159,27 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
final dateField = _getCalendarFieldInfo(settings.fieldId);
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));
}
}
}
},
);
@ -187,15 +213,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
result.fold(
(events) {
if (!isClosed) {
final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];
for (final eventPB in events.items) {
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
if (calendarEvent != null) {
calendarEvents.add(calendarEvent);
}
}
add(CalendarEvent.didLoadAllEvents(calendarEvents));
add(CalendarEvent.didLoadAllEvents(events.items));
}
},
(r) => Log.error(r),
@ -203,22 +221,32 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
});
}
List<CalendarEventData<CalendarDayEvent>> _calendarEventDataFromEventPBs(
List<CalendarEventPB> eventPBs,
) {
final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];
for (final eventPB in eventPBs) {
final event = _calendarEventDataFromEventPB(eventPB);
if (event != null) {
calendarEvents.add(event);
}
}
return calendarEvents;
}
CalendarEventData<CalendarDayEvent>? _calendarEventDataFromEventPB(
CalendarEventPB eventPB,
) {
final fieldInfo = fieldInfoByFieldId[eventPB.titleFieldId];
final fieldInfo = fieldInfoByFieldId[eventPB.dateFieldId];
if (fieldInfo != null) {
final cellId = CellIdentifier(
viewId: viewId,
rowId: eventPB.rowId,
fieldInfo: fieldInfo,
);
final eventData = CalendarDayEvent(
event: eventPB,
cellId: cellId,
eventId: eventPB.rowId,
dateFieldId: eventPB.dateFieldId,
);
// The timestamp is using UTC in the backend, so we need to convert it
// to local time.
final date = DateTime.fromMillisecondsSinceEpoch(
eventPB.timestamp.toInt() * 1000,
);
@ -243,25 +271,29 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
};
},
onRowsChanged: ((onRowsChanged, rowByRowId, reason) {}),
onRowsCreated: ((ids) async {
for (final id in ids) {
onRowsCreated: ((rowIds) async {
for (final id in rowIds) {
final event = await _loadEvent(id);
if (event != null && !isClosed) {
add(CalendarEvent.didReceiveNewEvent(event));
add(CalendarEvent.didReceiveEvent(event));
}
}
}),
onRowsDeleted: (ids) {
onRowsDeleted: (rowIds) {
if (isClosed) return;
add(CalendarEvent.didDeleteEvents(ids));
add(CalendarEvent.didDeleteEvents(rowIds));
},
onRowsUpdated: (ids) async {
onRowsUpdated: (rowIds) async {
if (isClosed) return;
for (final id in ids) {
for (final id in rowIds) {
final event = await _loadEvent(id);
if (event != null) {
add(CalendarEvent.didUpdateEvent(event));
if (event != null && isEventDayChanged(event)) {
if (isEventDayChanged(event)) {
add(CalendarEvent.didDeleteEvents([id]));
add(CalendarEvent.didReceiveEvent(event));
} else {
add(CalendarEvent.didUpdateEvent(event));
}
}
}
},
@ -276,7 +308,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
onCalendarLayoutChanged: _didReceiveNewLayoutField,
);
_databaseController.addListener(
_databaseController.setListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutChanged: onLayoutChanged,
onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
@ -296,6 +328,19 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
}
}
bool isEventDayChanged(
CalendarEventData<CalendarDayEvent> event,
) {
final index = state.allEvents.indexWhere(
(element) => element.event!.eventId == event.event!.eventId,
);
if (index != -1) {
return state.allEvents[index].date.day != event.date.day;
} else {
return false;
}
}
}
typedef Events = List<CalendarEventData<CalendarDayEvent>>;
@ -310,7 +355,7 @@ class CalendarEvent with _$CalendarEvent {
) = _ReceiveCalendarSettings;
// Called after loading all the current evnets
const factory CalendarEvent.didLoadAllEvents(Events events) =
const factory CalendarEvent.didLoadAllEvents(List<CalendarEventPB> events) =
_ReceiveCalendarEvents;
// Called when specific event was updated
@ -319,10 +364,15 @@ class CalendarEvent with _$CalendarEvent {
) = _DidUpdateEvent;
// Called after creating a new event
const factory CalendarEvent.didReceiveNewEvent(
const factory CalendarEvent.didCreateEvent(
CalendarEventData<CalendarDayEvent> event,
) = _DidReceiveNewEvent;
// Called when receive a new event
const factory CalendarEvent.didReceiveEvent(
CalendarEventData<CalendarDayEvent> event,
) = _DidReceiveEvent;
// Called when deleting events
const factory CalendarEvent.didDeleteEvents(List<RowId> rowIds) =
_DidDeleteEvents;
@ -348,11 +398,13 @@ class CalendarEvent with _$CalendarEvent {
class CalendarState with _$CalendarState {
const factory CalendarState({
required Option<DatabasePB> database,
// events by row id
required Events allEvents,
required Events initialEvents,
CalendarEventData<CalendarDayEvent>? editEvent,
CalendarEventData<CalendarDayEvent>? newEvent,
required List<RowId> deleteEventIds,
CalendarEventData<CalendarDayEvent>? updateEvent,
required List<String> deleteEventIds,
required Option<CalendarLayoutSettingPB> settings,
required DatabaseLoadingState loadingState,
required Option<FlowyError> noneOrError,
@ -389,8 +441,12 @@ class CalendarEditingRow {
class CalendarDayEvent {
final CalendarEventPB event;
final CellIdentifier cellId;
final String dateFieldId;
final String eventId;
RowId get eventId => cellId.rowId;
CalendarDayEvent({required this.cellId, required this.event});
CalendarDayEvent({
required this.dateFieldId,
required this.eventId,
required this.event,
});
}

View File

@ -1,7 +1,10 @@
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/card/cells/number_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/url_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-database2/field_entities.pbenum.dart';
@ -10,11 +13,11 @@ 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 +26,10 @@ class CalendarDayCard extends StatelessWidget {
final bool isInMonth;
final DateTime date;
final RowCache _rowCache;
final CardCellBuilder _cellBuilder;
final List<CalendarDayEvent> events;
final void Function(DateTime) onCreateEvent;
CalendarDayCard({
const CalendarDayCard({
required this.viewId,
required this.isToday,
required this.isInMonth,
@ -37,7 +39,6 @@ class CalendarDayCard extends StatelessWidget {
required this.events,
Key? key,
}) : _rowCache = rowCache,
_cellBuilder = CardCellBuilder(rowCache.cellCache),
super(key: key);
@override
@ -49,65 +50,183 @@ 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)},
builder: (context, child) {
Widget? multipleCards;
if (events.isNotEmpty) {
multipleCards = Flexible(
child: ListView.separated(
itemBuilder: (BuildContext context, int index) =>
_buildCard(context, events[index]),
itemCount: events.length,
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
),
);
}).toList();
}
final child = Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(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(),
],
);
return Container(
color: backgroundColor,
child: MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: child,
child: GestureDetector(
onDoubleTap: () => onCreateEvent(date),
child: MouseRegion(
cursor: SystemMouseCursors.basic,
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: child,
),
),
),
);
}),
},
);
}
GestureDetector _buildCard(BuildContext context, CalendarDayEvent event) {
final styles = <FieldType, CardCellStyle>{
FieldType.Number: NumberCardCellStyle(10),
FieldType.URL: URLCardCellStyle(10),
};
final cellBuilder = CardCellBuilder<String>(
_rowCache.cellCache,
styles: styles,
);
final rowInfo = _rowCache.getRow(event.eventId);
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((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.addDateCellHook((cellData, cardData, _) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 3,
child: FlowyText.regular(
cellData.date,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: FlowyText.regular(
cellData.time,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
)
],
),
),
);
});
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.dateFieldId,
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: MouseRegion(
cursor: SystemMouseCursors.click,
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,
),
),
);
}
void _showRowDetailPage(CalendarDayEvent event, BuildContext context) {
final dataController = RowController(
rowId: event.cellId.rowId,
rowId: event.eventId,
viewId: viewId,
rowCache: _rowCache,
);
@ -133,42 +252,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 +274,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,
],
),
);
},
);
@ -215,10 +302,8 @@ class _NewEventButton extends StatelessWidget {
return FlowyIconButton(
onPressed: onClick,
iconPadding: EdgeInsets.zero,
icon: svgWidget(
"home/add",
color: Theme.of(context).iconTheme.color,
),
icon: const FlowySvg(name: "home/add"),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 22,
);
}
@ -237,31 +322,38 @@ class _DayBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color dayTextColor = Theme.of(context).colorScheme.onSurface;
String dayString = date.day == 1
? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
: date.day.toString();
Color dayTextColor = Theme.of(context).colorScheme.onBackground;
String monthString =
DateFormat("MMM ", context.locale.toLanguageTag()).format(date);
String dayString = date.day.toString();
if (isToday) {
dayTextColor = Theme.of(context).colorScheme.onPrimary;
}
if (!isInMonth) {
dayTextColor = Theme.of(context).disabledColor;
}
if (isToday) {
dayTextColor = Theme.of(context).colorScheme.onPrimary;
}
Widget day = Container(
decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: Corners.s6Border,
),
padding: GridSize.typeOptionContentInsets,
child: FlowyText.medium(
dayString,
color: dayTextColor,
),
return Row(
children: [
if (date.day == 1) FlowyText.medium(monthString),
Container(
decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: Corners.s6Border,
),
width: isToday ? 26 : null,
height: isToday ? 26 : null,
padding: GridSize.typeOptionContentInsets,
child: Center(
child: FlowyText.medium(
dayString,
color: dayTextColor,
),
),
),
],
);
return day;
}
}

View File

@ -9,6 +9,9 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../application/row/row_data_controller.dart';
import '../../widgets/row/cell_builder.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
import 'layout/sizes.dart';
import 'toolbar/calendar_toolbar.dart';
@ -70,19 +73,16 @@ class _CalendarPageState extends State<CalendarPage> {
},
),
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.updateEvent != c.updateEvent,
listenWhen: (p, c) => p.editEvent != c.editEvent,
listener: (context, state) {
if (state.updateEvent != null) {
_eventController.removeWhere(
(element) =>
state.updateEvent!.event!.eventId ==
element.event!.eventId,
);
_eventController.add(state.updateEvent!);
if (state.editEvent != null) {
_showEditEventPage(state.editEvent!.event!, context);
}
},
),
BlocListener<CalendarBloc, CalendarState>(
// Event create by click the + button or double click on the
// calendar
listenWhen: (p, c) => p.newEvent != c.newEvent,
listener: (context, state) {
if (state.newEvent != null) {
@ -116,7 +116,7 @@ class _CalendarPageState extends State<CalendarPage> {
child: MonthView(
key: _calendarState,
controller: _eventController,
cellAspectRatio: .9,
cellAspectRatio: .6,
startDay: _weekdayFromInt(firstDayOfWeek),
borderColor: Theme.of(context).dividerColor,
headerBuilder: _headerNavigatorBuilder,
@ -137,7 +137,7 @@ class _CalendarPageState extends State<CalendarPage> {
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: svgWidget('home/arrow_left'),
icon: const FlowySvg(name: 'home/arrow_left'),
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.previousPage(),
@ -155,7 +155,7 @@ class _CalendarPageState extends State<CalendarPage> {
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: svgWidget('home/arrow_right'),
icon: const FlowySvg(name: 'home/arrow_right'),
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.nextPage(),
@ -185,7 +185,12 @@ class _CalendarPageState extends State<CalendarPage> {
isInMonth,
) {
final events = calenderEvents.map((value) => value.event!).toList();
// Sort the events by timestamp. Because the database view is not
// reserving the order of the events. Reserving the order of the rows/events
// is implemnted in the develop branch(WIP). Will be replaced with that.
events.sort(
(a, b) => a.event.timestamp.compareTo(b.event.timestamp),
);
return CalendarDayCard(
viewId: widget.view.id,
isToday: isToday,
@ -208,4 +213,24 @@ class _CalendarPageState extends State<CalendarPage> {
// MonthView places the first day of week on the second column for some reason.
return WeekDays.values[(dayOfWeek + 1) % 7];
}
void _showEditEventPage(CalendarDayEvent event, BuildContext context) {
final dataController = RowController(
rowId: event.eventId,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: _calendarBloc.rowCache.cellCache,
),
dataController: dataController,
);
},
);
}
}

View File

@ -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 {

View File

@ -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<RowDetailEvent, RowDetailState> {
final RowBackendService rowService;
final RowController dataController;
RowDetailBloc({
required this.dataController,
}) : super(RowDetailState.initial()) {
}) : rowService = RowBackendService(viewId: dataController.viewId),
super(RowDetailState.initial()) {
on<RowDetailEvent>(
(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<RowDetailEvent, RowDetailState> {
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<CellIdentifier> gridCells,
) = _DidReceiveCellDatas;

View File

@ -147,8 +147,6 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
widget.popoverMutex.listenOnPopoverChanged(() {
if (focusNode.hasFocus) {
focusNode.unfocus();
} else {
focusNode.requestFocus();
}
});
@ -205,6 +203,7 @@ class _DeleteFieldButton extends StatelessWidget {
builder: (context, state) {
final enable = !state.canDelete && !state.isGroupField;
Widget button = FlowyButton(
disable: !enable,
text: FlowyText.medium(
LocaleKeys.grid_field_delete.tr(),
color: enable ? null : Theme.of(context).disabledColor,

View File

@ -1,6 +1,5 @@
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/material.dart';
@ -58,15 +57,12 @@ class FieldTypeCell extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
fieldType.title(),
color: AFThemeExtension.of(context).textColor,
),
onTap: () => onSelectField(fieldType),
leftIcon: svgWidget(
fieldType.iconName(),
color: Theme.of(context).iconTheme.color,
leftIcon: FlowySvg(
name: fieldType.iconName(),
),
),
);

View File

@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' show Either;
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@ -113,20 +112,12 @@ class _SwitchFieldButton extends StatelessWidget {
Widget _buildMoreButton(BuildContext context) {
final bloc = context.read<FieldTypeOptionEditBloc>();
return FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
bloc.state.field.fieldType.title(),
color: AFThemeExtension.of(context).textColor,
),
margin: GridSize.typeOptionContentInsets,
leftIcon: svgWidget(
bloc.state.field.fieldType.iconName(),
color: Theme.of(context).iconTheme.color,
),
rightIcon: svgWidget(
"grid/more",
color: Theme.of(context).iconTheme.color,
),
leftIcon: FlowySvg(name: bloc.state.field.fieldType.iconName()),
rightIcon: const FlowySvg(name: 'grid/more'),
);
}
}

View File

@ -186,6 +186,7 @@ class CreateFieldButton extends StatelessWidget {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithRightAligned,
asBarrier: true,
margin: EdgeInsets.zero,
constraints: BoxConstraints.loose(const Size(240, 600)),
child: FlowyButton(
radius: BorderRadius.zero,

View File

@ -1,7 +1,5 @@
import 'package:appflowy/plugins/database_view/application/field/type_option/date_bloc.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart' hide DateFormat;
import 'package:appflowy/generated/locale_keys.g.dart';
@ -54,7 +52,6 @@ class DateTypeOptionWidget extends TypeOptionWidget {
const TypeOptionSeparator(),
_renderDateFormatButton(context, state.typeOption.dateFormat),
_renderTimeFormatButton(context, state.typeOption.timeFormat),
const _IncludeTimeButton(),
];
return ListView.separated(
@ -191,44 +188,6 @@ class TimeFormatButton extends StatelessWidget {
}
}
class _IncludeTimeButton extends StatelessWidget {
const _IncludeTimeButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocSelector<DateTypeOptionBloc, DateTypeOptionState, bool>(
selector: (state) => state.typeOption.includeTime,
builder: (context, includeTime) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: SizedBox(
height: GridSize.popoverItemHeight,
child: Padding(
padding: GridSize.typeOptionContentInsets,
child: Row(
children: [
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
const Spacer(),
Toggle(
value: includeTime,
onChanged: (value) {
context
.read<DateTypeOptionBloc>()
.add(DateTypeOptionEvent.includeTime(!value));
},
style: ToggleStyle.big,
padding: EdgeInsets.zero,
),
],
),
),
),
);
},
);
}
}
class DateFormatList extends StatelessWidget {
final DateFormatPB selectedFormat;
final Function(DateFormatPB format) onSelected;
@ -280,7 +239,7 @@ class DateFormatCell extends StatelessWidget {
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = svgWidget("grid/checkmark");
checkmark = const FlowySvg(name: 'grid/checkmark');
}
return SizedBox(
@ -364,7 +323,7 @@ class TimeFormatCell extends StatelessWidget {
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = svgWidget("grid/checkmark");
checkmark = const FlowySvg(name: 'grid/checkmark');
}
return SizedBox(

View File

@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -60,15 +59,10 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
final selectNumUnitButton = SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
margin: GridSize.typeOptionContentInsets,
rightIcon: svgWidget(
"grid/more",
color: AFThemeExtension.of(context).textColor,
),
rightIcon: const FlowySvg(name: 'grid/more'),
text: FlowyText.regular(
state.typeOption.format.title(),
color: AFThemeExtension.of(context).textColor,
),
),
);
@ -79,7 +73,6 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
alignment: Alignment.centerLeft,
child: FlowyText.medium(
LocaleKeys.grid_field_numberFormat.tr(),
color: AFThemeExtension.of(context).textColor,
),
);
return Padding(
@ -188,7 +181,9 @@ class NumberFormatCell extends StatelessWidget {
Widget build(BuildContext context) {
Widget? checkmark;
if (isSelected) {
checkmark = svgWidget("grid/checkmark");
checkmark = const FlowySvg(
name: 'grid/checkmark',
);
}
return SizedBox(

View File

@ -105,15 +105,10 @@ class _DeleteTag extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
LocaleKeys.grid_selectOption_deleteTag.tr(),
color: AFThemeExtension.of(context).textColor,
),
leftIcon: svgWidget(
"grid/delete",
color: Theme.of(context).iconTheme.color,
),
leftIcon: const FlowySvg(name: 'grid/delete'),
onTap: () {
context
.read<EditSelectOptionBloc>()
@ -226,7 +221,11 @@ class _SelectOptionColorCell extends StatelessWidget {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(color.optionName()),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
color.optionName(),
color: AFThemeExtension.of(context).textColor,
),
leftIcon: colorIcon,
rightIcon: checkmark,
onTap: () {

View File

@ -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-database2/row_entities.pb.dart';
@ -13,23 +14,40 @@ import 'card_cell_builder.dart';
import 'container/accessory.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 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<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.viewId,
required this.fieldId,
this.groupingFieldId,
required this.isEditing,
required this.rowCache,
required this.cellBuilder,
@ -37,15 +55,19 @@ class Card<CustomCardData> 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<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 EditableRowNotifier rowNotifier;
late PopoverController popoverController;
@ -56,15 +78,15 @@ class _CardState<T> extends State<Card<T>> {
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<T> extends State<Card<T>> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cardBloc,
child: BlocBuilder<CardBloc, BoardCardState>(
child: BlocBuilder<CardBloc, RowCardState>(
buildWhen: (previous, current) {
// Rebuild when:
// 1.If the length of the cells is not the same
@ -106,21 +128,26 @@ class _CardState<T> extends State<Card<T>> {
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<T>(
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<T> extends State<Card<T>> {
class _CardContent<CustomCardData> extends StatelessWidget {
final CardCellBuilder<CustomCardData> cellBuilder;
final EditableRowNotifier rowNotifier;
final List<BoardCellEquatable> cells;
final CardConfiguration<CustomCardData>? cardConfiguration;
final List<CellIdentifier> cells;
final RowCardRenderHook<CustomCardData>? 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<CustomCardData> extends StatelessWidget {
List<Widget> _makeCells(
BuildContext context,
List<BoardCellEquatable> cells,
List<CellIdentifier> cells,
) {
final List<Widget> 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),
});
}

View File

@ -1,5 +1,4 @@
import 'dart:collection';
import 'package:equatable/equatable.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/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<BoardCardEvent, BoardCardState> {
class CardBloc extends Bloc<RowCardEvent, RowCardState> {
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<BoardCardEvent, BoardCardState> {
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
_rowCache = rowCache,
super(
BoardCardState.initial(
RowCardState.initial(
row,
_makeCells(groupFieldId, rowCache.loadGridCells(row.id)),
isEditing,
),
) {
on<BoardCardEvent>(
on<RowCardEvent>(
(event, emit) async {
await event.when(
initial: () async {
@ -69,7 +68,7 @@ class CardBloc extends Bloc<BoardCardEvent, BoardCardState> {
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<BoardCardEvent, BoardCardState> {
onCellUpdated: (cellMap, reason) {
if (!isClosed) {
final cells = _makeCells(groupFieldId, cellMap);
add(BoardCardEvent.didReceiveCells(cells, reason));
add(RowCardEvent.didReceiveCells(cells, reason));
}
},
);
}
}
List<BoardCellEquatable> _makeCells(
String groupFieldId,
List<CellIdentifier> _makeCells(
String? groupFieldId,
CellByFieldId originalCellMap,
) {
List<BoardCellEquatable> cells = [];
List<CellIdentifier> 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<BoardCellEquatable> cells,
class RowCardEvent with _$RowCardEvent {
const factory RowCardEvent.initial() = _InitialRow;
const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing;
const factory RowCardEvent.didReceiveCells(
List<CellIdentifier> cells,
RowsChangedReason reason,
) = _DidReceiveCells;
}
@freezed
class BoardCardState with _$BoardCardState {
const factory BoardCardState({
class RowCardState with _$RowCardState {
const factory RowCardState({
required RowPB rowPB,
required List<BoardCellEquatable> cells,
required List<CellIdentifier> cells,
required bool isEditing,
RowsChangedReason? changeReason,
}) = _BoardCardState;
}) = _RowCardState;
factory BoardCardState.initial(
factory RowCardState.initial(
RowPB rowPB,
List<BoardCellEquatable> cells,
List<CellIdentifier> cells,
bool isEditing,
) =>
BoardCardState(
RowCardState(
rowPB: rowPB,
cells: cells,
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,
];
}
}

View File

@ -15,15 +15,15 @@ import 'cells/url_card_cell.dart';
// T represents as the Generic card data
class CardCellBuilder<CustomCardData> {
final CellCache cellCache;
final Map<FieldType, CardCellStyle>? styles;
CardCellBuilder(this.cellCache);
CardCellBuilder(this.cellCache, {this.styles});
Widget buildCell({
CustomCardData? cardData,
required CellIdentifier cellId,
EditableCardNotifier? cellNotifier,
CardConfiguration<CustomCardData>? cardConfiguration,
Map<FieldType, CardCellStyle>? styles,
RowCardRenderHook<CustomCardData>? renderHook,
}) {
final cellControllerBuilder = CellControllerBuilder(
cellId: cellId,
@ -39,20 +39,21 @@ class CardCellBuilder<CustomCardData> {
key: key,
);
case FieldType.DateTime:
return DateCardCell(
return DateCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.DateTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.SingleSelect:
return SelectOptionCardCell<CustomCardData>(
renderHook: cardConfiguration?.renderHook[FieldType.SingleSelect],
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
cellControllerBuilder: cellControllerBuilder,
cardData: cardData,
key: key,
);
case FieldType.MultiSelect:
return SelectOptionCardCell<CustomCardData>(
renderHook: cardConfiguration?.renderHook[FieldType.MultiSelect],
renderHook: renderHook?.renderHook[FieldType.MultiSelect],
cellControllerBuilder: cellControllerBuilder,
cardData: cardData,
editableNotifier: cellNotifier,
@ -64,19 +65,24 @@ class CardCellBuilder<CustomCardData> {
key: key,
);
case FieldType.Number:
return NumberCardCell(
return NumberCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.Number],
style: isStyleOrNull<NumberCardCellStyle>(style),
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.RichText:
return TextCardCell(
return TextCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.RichText],
cellControllerBuilder: cellControllerBuilder,
editableNotifier: cellNotifier,
cardData: cardData,
style: isStyleOrNull<TextCardCellStyle>(style),
key: key,
);
case FieldType.URL:
return URLCardCell(
return URLCardCell<CustomCardData>(
style: isStyleOrNull<URLCardCellStyle>(style),
cellControllerBuilder: cellControllerBuilder,
key: key,
);

View File

@ -1,27 +1,72 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/log.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>>;
class CardConfiguration<CustomCardData> {
/// The [RowCardRenderHook] is used to customize the rendering of the
/// card cell. Each cell has itw own field type. So the [renderHook]
/// is a map of [FieldType] to [CellRenderHook].
class RowCardRenderHook<CustomCardData> {
final RenderHookByFieldType<CustomCardData> renderHook = {};
CardConfiguration();
RowCardRenderHook();
/// Add render hook for the FieldType.SingleSelect and FieldType.MultiSelect
void addSelectOptionHook(
CellRenderHook<List<SelectOptionPB>, CustomCardData> hook,
CellRenderHook<List<SelectOptionPB>, CustomCardData?> hook,
) {
selectOptionHook(cellData, cardData) {
if (cellData is List<SelectOptionPB>) {
hook(cellData, cardData);
final hookFn = _typeSafeHook<List<SelectOptionPB>>(hook);
renderHook[FieldType.SingleSelect] = hookFn;
renderHook[FieldType.MultiSelect] = hookFn;
}
/// Add a render hook for the [FieldType.RichText]
void addTextCellHook(
CellRenderHook<String, CustomCardData?> hook,
) {
renderHook[FieldType.RichText] = _typeSafeHook<String>(hook);
}
/// Add a render hook for the [FieldType.Number]
void addNumberCellHook(
CellRenderHook<String, CustomCardData?> hook,
) {
renderHook[FieldType.Number] = _typeSafeHook<String>(hook);
}
/// Add a render hook for the [FieldType.Date]
void addDateCellHook(
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;
renderHook[FieldType.MultiSelect] = selectOptionHook;
return hookFn;
}
}

View File

@ -44,13 +44,16 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
: svgWidget('editor/editor_uncheck');
return Align(
alignment: Alignment.centerLeft,
child: FlowyIconButton(
iconPadding: EdgeInsets.zero,
icon: icon,
width: 20,
onPressed: () => context
.read<CheckboxCardCellBloc>()
.add(const CheckboxCardCellEvent.select()),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: FlowyIconButton(
iconPadding: EdgeInsets.zero,
icon: icon,
width: 20,
onPressed: () => context
.read<CheckboxCardCellBloc>()
.add(const CheckboxCardCellEvent.select()),
),
),
);
},

View File

@ -7,11 +7,13 @@ import '../bloc/date_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class DateCardCell extends CardCell {
class DateCardCell<CustomCardData> extends CardCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<dynamic, CustomCardData>? renderHook;
const DateCardCell({
required this.cellControllerBuilder,
this.renderHook,
Key? key,
}) : super(key: key);
@ -42,6 +44,15 @@ class _DateCardCellState extends State<DateCardCell> {
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(

View File

@ -7,13 +7,24 @@ import '../bloc/number_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class NumberCardCell extends CardCell {
class NumberCardCellStyle extends CardCellStyle {
final double fontSize;
NumberCardCellStyle(this.fontSize);
}
class NumberCardCell<CustomCardData>
extends CardCell<CustomCardData, NumberCardCellStyle> {
final CellRenderHook<String, CustomCardData>? renderHook;
final CellControllerBuilder cellControllerBuilder;
const NumberCardCell({
required this.cellControllerBuilder,
CustomCardData? cardData,
NumberCardCellStyle? style,
this.renderHook,
Key? key,
}) : super(key: key);
}) : super(key: key, style: style, cardData: cardData);
@override
State<NumberCardCell> createState() => _NumberCardCellState();
@ -42,6 +53,15 @@ class _NumberCardCellState extends State<NumberCardCell> {
if (state.content.isEmpty) {
return const SizedBox();
} else {
Widget? custom = widget.renderHook?.call(
state.content,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Align(
alignment: Alignment.centerLeft,
child: Padding(
@ -50,7 +70,7 @@ class _NumberCardCellState extends State<NumberCardCell> {
),
child: FlowyText.medium(
state.content,
fontSize: 14,
fontSize: widget.style?.fontSize ?? 14,
),
),
);

View File

@ -11,17 +11,18 @@ import 'card_cell.dart';
class SelectOptionCardCellStyle extends CardCellStyle {}
class SelectOptionCardCell<T> extends CardCell<T, SelectOptionCardCellStyle>
class SelectOptionCardCell<CustomCardData>
extends CardCell<CustomCardData, SelectOptionCardCellStyle>
with EditableCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<List<SelectOptionPB>, T>? renderHook;
final CellRenderHook<List<SelectOptionPB>, 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<SelectOptionCardCell> {
Widget? custom = widget.renderHook?.call(
state.selectedOptions,
widget.cardData,
context,
);
if (custom != null) {
return custom;

View File

@ -14,18 +14,21 @@ class TextCardCellStyle extends CardCellStyle {
TextCardCellStyle(this.fontSize);
}
class TextCardCell extends CardCell<String, TextCardCellStyle>
with EditableCell {
class TextCardCell<CustomCardData>
extends CardCell<CustomCardData, TextCardCellStyle> with EditableCell {
@override
final EditableCardNotifier? editableNotifier;
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<String, CustomCardData>? 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<TextCardCell> createState() => _TextCardCellState();
@ -104,6 +107,16 @@ class _TextCardCellState extends State<TextCardCell> {
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) {

View File

@ -8,13 +8,21 @@ import '../bloc/url_card_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class URLCardCell extends CardCell {
class URLCardCellStyle extends CardCellStyle {
final double fontSize;
URLCardCellStyle(this.fontSize);
}
class URLCardCell<CustomCardData>
extends CardCell<CustomCardData, URLCardCellStyle> {
final CellControllerBuilder cellControllerBuilder;
const URLCardCell({
required this.cellControllerBuilder,
URLCardCellStyle? style,
Key? key,
}) : super(key: key);
}) : super(key: key, style: style);
@override
State<URLCardCell> createState() => _URLCardCellState();
@ -55,7 +63,7 @@ class _URLCardCellState extends State<URLCardCell> {
style: Theme.of(context)
.textTheme
.bodyMedium!
.size(FontSizes.s14)
.size(widget.style?.fontSize ?? FontSizes.s14)
.textColor(Theme.of(context).colorScheme.primary)
.underline,
),

View File

@ -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,

View File

@ -20,7 +20,7 @@ class ChecklistProgressBar extends StatelessWidget {
percent: percent,
padding: EdgeInsets.zero,
progressColor: Theme.of(context).colorScheme.primary,
backgroundColor: AFThemeExtension.of(context).progressBarBGcolor,
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
barRadius: const Radius.circular(5),
);
}

View File

@ -289,10 +289,7 @@ Option<DateCellData> calDataFromCellData(DateCellDataPB? cellData) {
Option<DateCellData> dateData = none();
if (cellData != null) {
final timestamp = cellData.timestamp * 1000;
final date = DateTime.fromMillisecondsSinceEpoch(
timestamp.toInt(),
isUtc: true,
);
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
dateData = Some(
DateCellData(
date: date,

View File

@ -1,11 +1,11 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'number_cell_bloc.freezed.dart';
//
class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
final NumberCellController cellController;
void Function()? _onCellChangedFn;
@ -22,17 +22,18 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
didReceiveCellUpdate: (cellContent) {
emit(state.copyWith(cellContent: cellContent ?? ""));
},
updateCell: (text) {
updateCell: (text) async {
if (state.cellContent != text) {
emit(state.copyWith(cellContent: text));
cellController.saveCellData(
text,
onFinish: (result) {
result.fold(
() {},
(err) => Log.error(err),
);
},
await cellController.saveCellData(text);
// If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string.
// So for every cell data that will be formatted in the backend.
// It needs to get the formatted data after saving.
add(
NumberCellEvent.didReceiveCellUpdate(
cellController.getCellData(),
),
);
}
},

View File

@ -8,9 +8,13 @@ import '../../cell_builder.dart';
class GridTextCellStyle extends GridCellStyle {
String? placeholder;
TextStyle? textStyle;
bool? autofocus;
GridTextCellStyle({
this.placeholder,
this.textStyle,
this.autofocus,
});
}
@ -66,7 +70,9 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
controller: _controller,
focusNode: focusNode,
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
style: widget.cellStyle?.textStyle ??
Theme.of(context).textTheme.bodyMedium,
autofocus: widget.cellStyle?.autofocus ?? false,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
top: GridSize.cellContentInsets.top,

View File

@ -3,6 +3,7 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -43,83 +44,85 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
}
class _RowDetailPageState extends State<RowDetailPage> {
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),
const SizedBox(height: 10),
],
),
),
);
}
}
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 +130,61 @@ 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,
_RowTitle(
cellId: state.gridCells
.firstWhereOrNull((e) => e.fieldInfo.isPrimary),
cellBuilder: cellBuilder,
),
const VSpace(20),
...state.gridCells
.where((element) => !element.fieldInfo.isPrimary)
.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);
},
class _RowTitle extends StatelessWidget {
final CellIdentifier? cellId;
final GridCellBuilder cellBuilder;
const _RowTitle({this.cellId, required this.cellBuilder, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
if (cellId == null) {
return const SizedBox();
}
final style = GridTextCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
textStyle: Theme.of(context).textTheme.titleLarge,
autofocus: true,
);
}
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,
);
});
return cellBuilder.build(cellId!, style: style);
}
}
class _CreatePropertyButton extends StatefulWidget {
final String viewId;
final VoidCallback onClosed;
const _CreatePropertyButton({
required this.viewId,
required this.onClosed,
Key? key,
}) : super(key: key);
@ -206,10 +207,9 @@ class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
constraints: BoxConstraints.loose(const Size(240, 200)),
controller: popoverController,
direction: PopoverDirection.topWithLeftAligned,
onClose: widget.onClosed,
child: Container(
margin: EdgeInsets.zero,
child: SizedBox(
height: 40,
decoration: _makeBoxDecoration(context),
child: FlowyButton(
text: FlowyText.medium(
LocaleKeys.grid_field_newProperty.tr(),
@ -243,14 +243,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 {
@ -376,3 +368,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<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);
},
),
);
}
}