feat: mobile detail page UI revamp (#4057)

* feat: card detail page ui revamp

* feat: redesign the cells

* chore: code cleanup

* chore: code review

* fix: merge issues

* chore: adjust cell ui size and padding

* fix: dart analyzer

* fix: integration test

* fix: test
This commit is contained in:
Richard Shiue 2023-12-02 02:03:30 +08:00 committed by GitHub
parent af07b53484
commit 5f94ba129e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1535 additions and 852 deletions

View File

@ -223,7 +223,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
final finder = find.descendant(
of: findCell,
matching: find.byWidgetPredicate(
(widget) => widget is SelectOptionTag && widget.name == content,
(widget) =>
widget is SelectOptionTag &&
(widget.name == content || widget.option?.name == content),
),
);
expect(finder, findsOneWidget);
@ -240,7 +242,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
final finder = find.descendant(
of: findCell,
matching: find.byWidgetPredicate(
(widget) => widget is SelectOptionTag && widget.name == content,
(widget) =>
widget is SelectOptionTag &&
(widget.name == content || widget.option?.name == content),
),
);
expect(finder, findsOneWidget);
@ -288,13 +292,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
skipOffstage: false,
);
final dateCellText = find.descendant(
of: findCell,
matching: find.byType(GridDateCellText),
);
final text = find.descendant(
of: dateCellText,
of: findCell,
matching: find.byWidgetPredicate(
(widget) {
if (widget is FlowyText) {
@ -446,7 +445,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
}) async {
final findRow = find.byType(GridRow);
final option = find.byWidgetPredicate(
(widget) => widget is SelectOptionTag && widget.name == name,
(widget) =>
widget is SelectOptionTag &&
(widget.name == name || widget.option?.name == name),
);
final cell = find.descendant(

View File

@ -3,17 +3,12 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/board.dart';
import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.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/card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -71,19 +66,12 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
listenWhen: (previous, current) =>
previous.recentAddedRowMeta != current.recentAddedRowMeta,
listener: (context, state) {
// when add a new card
// it push to the card detail screen of the new card
final rowCache = context.read<BoardBloc>().getRowCache()!;
context.push(
MobileCardDetailScreen.routeName,
MobileRowDetailPage.routeName,
extra: {
MobileCardDetailScreen.argRowController: RowController(
rowMeta: state.recentAddedRowMeta!,
viewId: state.viewId,
rowCache: rowCache,
),
MobileCardDetailScreen.argFieldController:
context.read<BoardBloc>().fieldController,
MobileRowDetailPage.argRowId: state.recentAddedRowMeta!.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
@ -166,7 +154,6 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox.shrink(key: ObjectKey(groupItem));
final cellCache = rowCache.cellCache;
final fieldController = boardBloc.fieldController;
final viewId = boardBloc.viewId;
final cellBuilder = CardCellBuilder<String>(cellCache);
@ -189,14 +176,16 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
isEditing: isEditing,
cellBuilder: cellBuilder,
renderHook: renderHook,
openCard: (context) => _openCard(
context: context,
viewId: viewId,
groupId: groupData.group.groupId,
fieldController: fieldController,
rowMeta: rowMeta,
rowCache: rowCache,
),
openCard: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowMeta.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
onStartEditing: () => boardBloc
.add(BoardEvent.startEditingRow(groupData.group, groupItem.row)),
onEndEditing: () =>
@ -230,55 +219,4 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
],
);
}
void _openCard({
required BuildContext context,
required String viewId,
required String groupId,
required FieldController fieldController,
required RowMetaPB rowMeta,
required RowCache rowCache,
}) {
final rowInfo = RowInfo(
viewId: viewId,
fields: UnmodifiableListView(fieldController.fieldInfos),
rowMeta: rowMeta,
rowId: rowMeta.id,
);
final dataController = RowController(
rowMeta: rowInfo.rowMeta,
viewId: rowInfo.viewId,
rowCache: rowCache,
groupId: groupId,
);
showModalBottomSheet(
isScrollControlled: true,
context: context,
// To avoid the appbar of [MobileCardDetailScreen] being covered by status bar.
useSafeArea: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(8),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
builder: (context) => DraggableScrollableSheet(
expand: false,
initialChildSize: 0.4,
minChildSize: 0.4,
snapSizes: const [0.4, 1],
snap: true,
builder: (context, scrollController) {
return MobileCardDetailScreen(
fieldController: fieldController,
rowController: dataController,
scrollController: scrollController,
isBottomSheet: true,
);
},
),
);
}
}

View File

@ -5,7 +5,6 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
@ -247,11 +246,6 @@ class MobileHiddenGroupItemList extends StatelessWidget {
...group.rows.map(
(item) {
final cellContext = rowCache.loadCells(item)[primaryField.id]!;
final rowController = RowController(
rowMeta: item,
viewId: viewId,
rowCache: rowCache,
);
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, _, __) {
return BlocBuilder<TextCellBloc, TextCellState>(
@ -289,19 +283,18 @@ class MobileHiddenGroupItemList extends StatelessWidget {
foregroundColor: Theme.of(context).colorScheme.onBackground,
visualDensity: VisualDensity.compact,
),
child: CardCellBuilder<String>(rowController.cellCache)
.buildCell(
child: CardCellBuilder<String>(rowCache.cellCache).buildCell(
cellContext: cellContext,
renderHook: renderHook,
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
),
onPressed: () {
context.push(
MobileCardDetailScreen.routeName,
MobileRowDetailPage.routeName,
extra: {
MobileCardDetailScreen.argRowController: rowController,
MobileCardDetailScreen.argFieldController:
context.read<BoardBloc>().fieldController,
MobileRowDetailPage.argRowId: item.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},

View File

@ -0,0 +1,91 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RowDetailCheckboxCell extends GridCellWidget {
RowDetailCheckboxCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
});
final CellControllerBuilder cellControllerBuilder;
@override
GridCellState<RowDetailCheckboxCell> createState() =>
_RowDetailCheckboxCellState();
}
class _RowDetailCheckboxCellState extends GridCellState<RowDetailCheckboxCell> {
late final CheckboxCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCellBloc(cellController: cellController)
..add(const CheckboxCellEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
builder: (context, state) {
return InkWell(
onTap: () => context
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
borderRadius: const BorderRadius.all(Radius.circular(14)),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: FlowySvg(
state.isSelected
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
color: Theme.of(context).colorScheme.onBackground,
blendMode: BlendMode.dst,
size: const Size.square(24),
),
),
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
void requestBeginFocus() {
_cellBloc.add(const CheckboxCellEvent.select());
}
@override
String? onCopy() => _cellBloc.state.isSelected ? "Yes" : "No";
}

View File

@ -0,0 +1,101 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RowDetailNumberCell extends GridCellWidget {
RowDetailNumberCell({
super.key,
required this.cellControllerBuilder,
this.hintText,
});
final CellControllerBuilder cellControllerBuilder;
final String? hintText;
@override
GridEditableTextCell<RowDetailNumberCell> createState() =>
_RowDetailNumberCellState();
}
class _RowDetailNumberCellState
extends GridEditableTextCell<RowDetailNumberCell> {
late final NumberCellBloc _cellBloc;
late final TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCellBloc(cellController: cellController)
..add(const NumberCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.cellContent);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: MultiBlocListener(
listeners: [
BlocListener<NumberCellBloc, NumberCellState>(
listenWhen: (p, c) => p.cellContent != c.cellContent,
listener: (context, state) => _controller.text = state.cellContent,
),
],
child: TextField(
controller: _controller,
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16),
decoration: InputDecoration(
enabledBorder:
_getInputBorder(color: Theme.of(context).colorScheme.outline),
focusedBorder:
_getInputBorder(color: Theme.of(context).colorScheme.primary),
hintText: widget.hintText,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
isCollapsed: true,
isDense: true,
constraints: const BoxConstraints(),
),
// close keyboard when tapping outside of the text field
onTapOutside: (event) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
);
}
InputBorder _getInputBorder({Color? color}) {
return OutlineInputBorder(
borderSide: BorderSide(color: color!),
borderRadius: const BorderRadius.all(Radius.circular(14)),
gapPadding: 0,
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Future<void> focusChanged() async {
if (mounted &&
!_cellBloc.isClosed &&
_controller.text != _cellBloc.state.cellContent) {
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
}
}
@override
String? onCopy() => _cellBloc.state.cellContent;
}

View File

@ -0,0 +1,108 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RowDetailTextCell extends GridCellWidget {
RowDetailTextCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
}) {
if (style != null) {
cellStyle = (style as GridTextCellStyle);
} else {
cellStyle = const GridTextCellStyle();
}
}
final CellControllerBuilder cellControllerBuilder;
late final GridTextCellStyle cellStyle;
@override
GridEditableTextCell<RowDetailTextCell> createState() =>
_RowDetailTextCellState();
}
class _RowDetailTextCellState extends GridEditableTextCell<RowDetailTextCell> {
late final TextCellBloc _cellBloc;
late final TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
child: TextField(
controller: _controller,
focusNode: focusNode,
style: widget.cellStyle.textStyle,
maxLines: null,
decoration: InputDecoration(
enabledBorder:
_getInputBorder(color: Theme.of(context).colorScheme.outline),
focusedBorder:
_getInputBorder(color: Theme.of(context).colorScheme.primary),
hintText: widget.cellStyle.placeholder,
contentPadding: widget.cellStyle.cellPadding ??
const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
isCollapsed: true,
isDense: true,
constraints: const BoxConstraints(minHeight: 48),
hintStyle: widget.cellStyle.textStyle
?.copyWith(color: Theme.of(context).hintColor),
),
onTapOutside: (event) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
);
}
InputBorder _getInputBorder({Color? color}) {
if (!widget.cellStyle.useRoundedBorder) {
return InputBorder.none;
}
return OutlineInputBorder(
borderSide: BorderSide(color: color!),
borderRadius: const BorderRadius.all(Radius.circular(14)),
gapPadding: 0,
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
String? onCopy() => _cellBloc.state.content;
@override
Future<void> focusChanged() {
_cellBloc.add(TextCellEvent.updateText(_controller.text));
return super.focusChanged();
}
}

View File

@ -0,0 +1,123 @@
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher_string.dart';
class RowDetailURLCell extends GridCellWidget {
RowDetailURLCell({
super.key,
required this.cellControllerBuilder,
this.hintText,
});
final CellControllerBuilder cellControllerBuilder;
final String? hintText;
@override
GridCellState<RowDetailURLCell> createState() => _RowDetailURLCellState();
}
class _RowDetailURLCellState extends GridCellState<RowDetailURLCell> {
late final URLCellBloc _cellBloc;
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as URLCellController;
_cellBloc = URLCellBloc(cellController: cellController)
..add(const URLCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocSelector<URLCellBloc, URLCellState, String>(
selector: (state) => state.content,
builder: (context, content) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () {
if (content.isEmpty) {
_showURLEditor(content);
return;
}
final shouldAddScheme = !['http', 'https']
.any((pattern) => content.startsWith(pattern));
final url = shouldAddScheme ? 'http://$content' : content;
canLaunchUrlString(url).then((value) => launchUrlString(url));
},
onLongPress: () => _showURLEditor(content),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
child: Text(
content.isEmpty ? widget.hintText ?? "" : content,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
decoration:
content.isEmpty ? null : TextDecoration.underline,
color: content.isEmpty
? Theme.of(context).hintColor
: Theme.of(context).colorScheme.primary,
),
),
),
),
);
},
),
);
}
void _showURLEditor(String content) {
showFlowyMobileBottomSheet(
context,
title: LocaleKeys.board_mobile_editURL.tr(),
builder: (_) {
final controller = TextEditingController(text: content);
return TextField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.url,
onEditingComplete: () {
_cellBloc.add(URLCellEvent.updateURL(controller.text));
context.pop();
},
);
},
);
}
@override
void requestBeginFocus() {
_focusNode.requestFocus();
}
}

View File

@ -1,18 +1,21 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/mobile_row_detail_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -20,169 +23,393 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
class MobileCardDetailScreen extends StatefulWidget {
const MobileCardDetailScreen({
import 'widgets/mobile_create_row_field_button.dart';
import 'widgets/mobile_row_property_list.dart';
class MobileRowDetailPage extends StatefulWidget {
const MobileRowDetailPage({
super.key,
required this.rowController,
this.scrollController,
this.isBottomSheet = false,
required this.fieldController,
required this.databaseController,
required this.rowId,
});
static const routeName = '/MobileCardDetailScreen';
static const argRowController = 'rowController';
static const argCellBuilder = 'cellBuilder';
static const argFieldController = 'fieldController';
static const routeName = '/MobileRowDetailPage';
static const argDatabaseController = 'databaseController';
static const argRowId = 'rowId';
final RowController rowController;
final ScrollController? scrollController;
final bool isBottomSheet;
final FieldController fieldController;
final DatabaseController databaseController;
final String rowId;
@override
State<MobileCardDetailScreen> createState() => _MobileCardDetailScreenState();
State<MobileRowDetailPage> createState() => _MobileRowDetailPageState();
}
class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
late final GridCellBuilder _cellBuilder;
class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
late final MobileRowDetailBloc _bloc;
late final PageController _pageController;
String get viewId => widget.databaseController.viewId;
RowCache get rowCache => widget.databaseController.rowCache;
FieldController get fieldController =>
widget.databaseController.fieldController;
@override
void initState() {
super.initState();
_cellBuilder = GridCellBuilder(
cellCache: widget.rowController.cellCache,
_bloc = MobileRowDetailBloc(
databaseController: widget.databaseController,
)..add(MobileRowDetailEvent.initial(widget.rowId));
final initialPage = rowCache.rowInfos
.indexWhere((rowInfo) => rowInfo.rowId == widget.rowId);
_pageController =
PageController(initialPage: initialPage == -1 ? 0 : initialPage);
}
@override
void dispose() {
_bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _bloc,
child: Scaffold(
appBar: _buildAppBar(),
body: BlocBuilder<MobileRowDetailBloc, MobileRowDetailState>(
buildWhen: (previous, current) =>
previous.rowInfos.length != current.rowInfos.length,
builder: (context, state) {
if (state.isLoading) {
return const SizedBox.shrink();
}
return PageView.builder(
controller: _pageController,
onPageChanged: (page) {
final rowId = _bloc.state.rowInfos[page].rowId;
_bloc.add(MobileRowDetailEvent.changeRowId(rowId));
},
itemCount: state.rowInfos.length,
itemBuilder: (context, index) {
if (state.rowInfos.isEmpty || state.currentRowId == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: MobileRowDetailPageContent(
databaseController: widget.databaseController,
rowMeta: state.rowInfos[index].rowMeta,
),
);
},
);
},
),
floatingActionButton: RowDetailFab(
onTapPrevious: () => _pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
),
onTapNext: () => _pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
),
),
),
);
}
AppBar _buildAppBar() {
return AppBar(
elevation: 0,
leading: IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.close),
),
actions: [
IconButton(
iconSize: 40,
icon: const FlowySvg(
FlowySvgs.details_horizontal_s,
size: Size.square(20),
),
padding: EdgeInsets.zero,
onPressed: () => _showCardActions(context),
),
],
);
}
void _showCardActions(BuildContext context) {
showFlowyMobileBottomSheet(
context,
title: LocaleKeys.board_cardActions.tr(),
builder: (_) => Row(
children: [
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.copy_s,
text: LocaleKeys.button_duplicate.tr(),
onTap: () {
final rowId = _bloc.state.currentRowId;
if (rowId == null) {
return;
}
RowBackendService.duplicateRow(viewId, rowId);
context
..pop()
..pop();
Fluttertoast.showToast(
msg: LocaleKeys.board_cardDuplicated.tr(),
gravity: ToastGravity.BOTTOM,
);
},
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_delete_m,
text: LocaleKeys.button_delete.tr(),
onTap: () {
final rowId = _bloc.state.currentRowId;
if (rowId == null) {
return;
}
RowBackendService.deleteRow(viewId, rowId);
context
..pop()
..pop();
Fluttertoast.showToast(
msg: LocaleKeys.board_cardDeleted.tr(),
gravity: ToastGravity.BOTTOM,
);
},
),
),
],
),
);
}
}
class RowDetailFab extends StatelessWidget {
const RowDetailFab({
super.key,
required this.onTapPrevious,
required this.onTapNext,
});
final VoidCallback onTapPrevious;
final VoidCallback onTapNext;
@override
Widget build(BuildContext context) {
return BlocBuilder<MobileRowDetailBloc, MobileRowDetailState>(
builder: (context, state) {
final rowCount = state.rowInfos.length;
final rowIndex = state.rowInfos.indexWhere(
(rowInfo) => rowInfo.rowId == state.currentRowId,
);
if (rowIndex == -1 || rowCount == 0) {
return const SizedBox.shrink();
}
final previousDisabled = rowIndex == 0;
final nextDisabled = rowIndex == rowCount - 1;
return Positioned(
bottom: 0,
right: 0,
child: IntrinsicWidth(
child: Container(
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(26),
boxShadow: const [
BoxShadow(
offset: Offset(0, 8),
blurRadius: 20,
spreadRadius: 0,
color: Color(0x191F2329),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox.square(
dimension: 48,
child: Material(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(26),
borderOnForeground: false,
child: InkWell(
borderRadius: BorderRadius.circular(26),
onTap: () {
if (!previousDisabled) {
onTapPrevious();
}
},
child: Icon(
Icons.chevron_left_outlined,
color: previousDisabled
? Theme.of(context).disabledColor
: null,
),
),
),
),
FlowyText.medium(
"${rowIndex + 1} / $rowCount",
fontSize: 14,
),
SizedBox.square(
dimension: 48,
child: Material(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(26),
borderOnForeground: false,
child: InkWell(
borderRadius: BorderRadius.circular(26),
onTap: () {
if (!nextDisabled) {
onTapNext();
}
},
child: Icon(
Icons.chevron_right_outlined,
color: nextDisabled
? Theme.of(context).disabledColor
: null,
),
),
),
),
],
),
),
),
);
},
);
}
}
class MobileRowDetailPageContent extends StatefulWidget {
const MobileRowDetailPageContent({
super.key,
required this.databaseController,
required this.rowMeta,
});
final DatabaseController databaseController;
final RowMetaPB rowMeta;
@override
State<MobileRowDetailPageContent> createState() =>
MobileRowDetailPageContentState();
}
class MobileRowDetailPageContentState
extends State<MobileRowDetailPageContent> {
late final RowController rowController;
late final MobileRowDetailPageCellBuilder cellBuilder;
String get viewId => widget.databaseController.viewId;
RowCache get rowCache => widget.databaseController.rowCache;
FieldController get fieldController =>
widget.databaseController.fieldController;
@override
void initState() {
super.initState();
rowController = RowController(
rowMeta: widget.rowMeta,
viewId: viewId,
rowCache: rowCache,
);
cellBuilder = MobileRowDetailPageCellBuilder(
cellCache: rowCache.cellCache,
);
}
@override
Widget build(BuildContext context) {
// TODO(yijing): fix context issue when navigating in bottom navigation bar
return BlocProvider(
create: (context) => RowDetailBloc(rowController: widget.rowController)
return BlocProvider<RowDetailBloc>(
create: (_) => RowDetailBloc(rowController: rowController)
..add(const RowDetailEvent.initial()),
child: Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () {
context.pop();
},
icon: Icon(
widget.isBottomSheet ? Icons.close : Icons.arrow_back,
),
),
title: Text(LocaleKeys.board_cardDetail.tr()),
actions: [
// appbar with duplicate and delete card features
BlocProvider<RowActionSheetBloc>(
create: (context) => RowActionSheetBloc(
viewId: widget.rowController.viewId,
rowId: widget.rowController.rowId,
groupId: widget.rowController.groupId,
),
child: Builder(
builder: (context) {
return IconButton(
onPressed: () {
showFlowyMobileBottomSheet(
context,
title: LocaleKeys.board_cardActions.tr(),
builder: (_) => Row(
children: [
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.copy_s,
text: LocaleKeys.button_duplicate.tr(),
onTap: () {
context.read<RowActionSheetBloc>().add(
const RowActionSheetEvent
.duplicateRow(),
);
context
..pop()
..pop();
Fluttertoast.showToast(
msg: LocaleKeys.board_cardDuplicated.tr(),
gravity: ToastGravity.CENTER,
);
},
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_delete_m,
text: LocaleKeys.button_delete.tr(),
onTap: () {
context.read<RowActionSheetBloc>().add(
const RowActionSheetEvent.deleteRow(),
);
context
..pop()
..pop();
Fluttertoast.showToast(
msg: LocaleKeys.board_cardDeleted.tr(),
gravity: ToastGravity.CENTER,
);
},
),
),
],
),
);
},
icon: const Icon(Icons.more_horiz),
);
},
),
),
],
),
body: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: ListView(
controller: widget.scrollController,
child: BlocBuilder<RowDetailBloc, RowDetailState>(
builder: (context, rowDetailState) {
return Column(
children: [
BlocProvider<RowBannerBloc>(
create: (context) => RowBannerBloc(
viewId: widget.rowController.viewId,
rowMeta: widget.rowController.rowMeta,
viewId: viewId,
rowMeta: rowController.rowMeta,
)..add(const RowBannerEvent.initial()),
child: BlocBuilder<RowBannerBloc, RowBannerState>(
builder: (context, state) {
// primaryField is the property cannot be deleted like card title
if (state.primaryField != null) {
final mobileStyle = GridTextCellStyle(
final cellStyle = GridTextCellStyle(
placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(),
textStyle: Theme.of(context).textTheme.titleLarge,
textStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontSize: 22),
cellPadding: const EdgeInsets.symmetric(vertical: 8),
useRoundedBorder: false,
);
// get the cell context for the card title
final cellContext = DatabaseCellContext(
viewId: widget.rowController.viewId,
rowMeta: widget.rowController.rowMeta,
viewId: viewId,
rowMeta: rowController.rowMeta,
fieldInfo: FieldInfo.initial(state.primaryField!),
);
return _cellBuilder.build(
return cellBuilder.build(
cellContext,
style: mobileStyle,
style: cellStyle,
);
}
return const SizedBox.shrink();
},
),
),
const VSpace(8),
// Card Properties
MobileRowPropertyList(
cellBuilder: _cellBuilder,
viewId: widget.rowController.viewId,
fieldController: widget.fieldController,
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
MobileRowPropertyList(
cellBuilder: cellBuilder,
viewId: viewId,
fieldController: fieldController,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (rowDetailState.numHiddenFields != 0)
const ToggleHiddenFieldsVisibilityButton(),
const VSpace(12),
MobileCreateRowFieldButton(
viewId: viewId,
fieldController: fieldController,
),
],
),
),
],
),
),
const Divider(),
],
),
),
);
},
),
);
}

View File

@ -20,34 +20,39 @@ class MobileCreateRowFieldButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TextButton.icon(
label: Text(
LocaleKeys.grid_field_newProperty.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).hintColor,
return ConstrainedBox(
constraints:
const BoxConstraints(maxHeight: 44, minWidth: double.infinity),
child: TextButton.icon(
style: Theme.of(context).textButtonTheme.style?.copyWith(
alignment: AlignmentDirectional.centerStart,
splashFactory: NoSplash.splashFactory,
padding: const MaterialStatePropertyAll(EdgeInsets.zero),
),
),
onPressed: () async {
final result = await TypeOptionBackendService.createFieldTypeOption(
viewId: viewId,
);
result.fold(
(typeOption) {
context.push(
MobileCreateRowFieldScreen.routeName,
extra: {
MobileCreateRowFieldScreen.argViewId: viewId,
MobileCreateRowFieldScreen.argTypeOption: typeOption,
MobileCreateRowFieldScreen.argFieldController: fieldController,
},
);
},
(r) => Log.error("Failed to create field type option: $r"),
);
},
icon: FlowySvg(
FlowySvgs.add_m,
color: Theme.of(context).hintColor,
label: Text(
LocaleKeys.grid_field_newProperty.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15),
),
onPressed: () async {
final result = await TypeOptionBackendService.createFieldTypeOption(
viewId: viewId,
);
result.fold(
(typeOption) {
context.push(
MobileCreateRowFieldScreen.routeName,
extra: {
MobileCreateRowFieldScreen.argViewId: viewId,
MobileCreateRowFieldScreen.argTypeOption: typeOption,
MobileCreateRowFieldScreen.argFieldController:
fieldController,
},
);
},
(r) => Log.error("Failed to create field type option: $r"),
);
},
icon: const FlowySvg(FlowySvgs.add_m),
),
);
}

View File

@ -1,24 +1,17 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/mobile_create_row_field_button.dart';
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
/// Display the row properties in a list. Only use this widget in the
/// [MobileCardDetailScreen].
class MobileRowPropertyList extends StatelessWidget {
const MobileRowPropertyList({
super.key,
@ -29,7 +22,7 @@ class MobileRowPropertyList extends StatelessWidget {
final String viewId;
final FieldController fieldController;
final GridCellBuilder cellBuilder;
final MobileRowDetailPageCellBuilder cellBuilder;
@override
Widget build(BuildContext context) {
@ -39,7 +32,7 @@ class MobileRowPropertyList extends StatelessWidget {
.where((element) => !element.fieldInfo.field.isPrimary)
.toList();
return ReorderableListView.builder(
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: visibleCells.length,
@ -48,64 +41,25 @@ class MobileRowPropertyList extends StatelessWidget {
cellContext: visibleCells[index],
fieldController: fieldController,
cellBuilder: cellBuilder,
index: index,
),
onReorder: (oldIndex, newIndex) {
// when reorderiing downwards, need to update index
if (oldIndex < newIndex) {
newIndex--;
}
final reorderedFieldId = visibleCells[oldIndex].fieldId;
final targetFieldId = visibleCells[newIndex].fieldId;
context.read<RowDetailBloc>().add(
RowDetailEvent.reorderField(
reorderedFieldId,
targetFieldId,
oldIndex,
newIndex,
),
);
},
footer: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (context.read<RowDetailBloc>().state.numHiddenFields != 0)
const ToggleHiddenFieldsVisibilityButton(),
const VSpace(8),
// add new field
MobileCreateRowFieldButton(
viewId: viewId,
fieldController: fieldController,
),
],
),
),
separatorBuilder: (_, __) => const VSpace(22),
);
},
);
}
}
// TODO(yijing): temperary locate here
// It may need to be share with other widgets
const cellHeight = 32.0;
class _PropertyCell extends StatefulWidget {
const _PropertyCell({
super.key,
required this.cellContext,
required this.fieldController,
required this.cellBuilder,
required this.index,
});
final DatabaseCellContext cellContext;
final FieldController fieldController;
final GridCellBuilder cellBuilder;
final int index;
final MobileRowDetailPageCellBuilder cellBuilder;
@override
State<StatefulWidget> createState() => _PropertyCellState();
@ -117,56 +71,29 @@ class _PropertyCellState extends State<_PropertyCell> {
final style = _customCellStyle(widget.cellContext.fieldType);
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
return ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
horizontalTitleGap: 4,
// FieldCellButton in Desktop
// TODO(yijing): adjust width with sreen size
leading: SizedBox(
width: 150,
height: cellHeight,
child: TextButton.icon(
icon: FlowySvg(
widget.cellContext.fieldInfo.field.fieldType.icon(),
color: Theme.of(context).colorScheme.onSurface,
),
label: Text(
widget.cellContext.fieldInfo.field.name,
// TODO(yijing): update text style
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
style: TextButton.styleFrom(
alignment: Alignment.centerLeft,
padding: EdgeInsets.zero,
),
// naivgator to field editor
onPressed: () => context.push(
CardPropertyEditScreen.routeName,
extra: {
CardPropertyEditScreen.argCellContext: widget.cellContext,
CardPropertyEditScreen.argFieldController: widget.fieldController,
CardPropertyEditScreen.argRowDetailBloc:
context.read<RowDetailBloc>(),
},
),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
FlowySvg(
widget.cellContext.fieldInfo.field.fieldType.icon(),
color: Theme.of(context).hintColor,
),
const HSpace(6),
Expanded(
child: FlowyText.regular(
widget.cellContext.fieldInfo.field.name,
overflow: TextOverflow.ellipsis,
fontSize: 14,
color: Theme.of(context).hintColor,
),
),
],
),
),
title: SizedBox(
width: double.infinity,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(),
//property values
child: AccessoryHover(
fieldType: widget.cellContext.fieldType,
child: cell,
),
),
),
const VSpace(6),
cell,
],
);
}
}
@ -181,19 +108,22 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
return DateCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
cellPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
useRoundedBorder: true,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return TimestampCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
cellPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
useRoundedBorder: true,
);
case FieldType.MultiSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
cellPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
useRoundedBorder: true,
);
case FieldType.Checklist:
return ChecklistCellStyle(
@ -208,20 +138,18 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
case FieldType.RichText:
return GridTextCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
useRoundedBorder: true,
);
case FieldType.SingleSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
cellPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
useRoundedBorder: true,
);
case FieldType.URL:
return GridURLCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
accessoryTypes: [
GridURLCellAccessoryType.copyURL,
GridURLCellAccessoryType.visitURL,
],
accessoryTypes: [],
);
}
throw UnimplementedError;

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -9,11 +10,17 @@ class MobileTextCell extends GridCellWidget {
MobileTextCell({
super.key,
required this.cellControllerBuilder,
this.hintText,
});
GridCellStyle? style,
}) {
if (style != null) {
cellStyle = (style as GridTextCellStyle);
} else {
cellStyle = const GridTextCellStyle();
}
}
final CellControllerBuilder cellControllerBuilder;
final String? hintText;
late final GridTextCellStyle cellStyle;
@override
GridEditableTextCell<MobileTextCell> createState() => _MobileTextCellState();
@ -49,15 +56,14 @@ class _MobileTextCellState extends GridEditableTextCell<MobileTextCell> {
child: TextField(
controller: _controller,
focusNode: focusNode,
// TODO(yijing): update text style
style: widget.cellStyle.textStyle,
decoration: InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: widget.hintText,
hintText: widget.cellStyle.placeholder,
contentPadding: EdgeInsets.zero,
isCollapsed: true,
),
// close keyboard when tapping outside of the text field
onTapOutside: (event) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
@ -76,9 +82,7 @@ class _MobileTextCellState extends GridEditableTextCell<MobileTextCell> {
@override
Future<void> focusChanged() {
_cellBloc.add(
TextCellEvent.updateText(_controller.text),
);
_cellBloc.add(TextCellEvent.updateText(_controller.text));
return super.focusChanged();
}
}

View File

@ -80,10 +80,9 @@ class _MobileCalendarEventsScreenState
const VSpace(10),
..._events.map((event) {
return EventCard(
fieldController: widget.calendarBloc.fieldController,
databaseController:
widget.calendarBloc.databaseController,
event: event,
viewId: widget.viewId,
rowCache: widget.rowCache,
constraints: const BoxConstraints.expand(),
autoEdit: false,
isDraggable: false,

View File

@ -53,11 +53,13 @@ class FlowyMobileStateContainer extends StatelessWidget {
? '🛸'
: ''),
style: const TextStyle(fontSize: 40),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
title,
style: theme.textTheme.labelLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
@ -65,6 +67,7 @@ class FlowyMobileStateContainer extends StatelessWidget {
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.hintColor,
),
textAlign: TextAlign.center,
),
if (_stateType == _FlowyMobileStateContainerType.error) ...[
const SizedBox(height: 8),

View File

@ -60,7 +60,10 @@ class RowBackendService {
return DatabaseEventUpdateRowMeta(payload).send();
}
Future<Either<Unit, FlowyError>> deleteRow(RowId rowId) {
static Future<Either<Unit, FlowyError>> deleteRow(
String viewId,
RowId rowId,
) {
final payload = RowIdPB.create()
..viewId = viewId
..rowId = rowId;
@ -68,10 +71,11 @@ class RowBackendService {
return DatabaseEventDeleteRow(payload).send();
}
Future<Either<Unit, FlowyError>> duplicateRow({
required RowId rowId,
static Future<Either<Unit, FlowyError>> duplicateRow(
String viewId,
RowId rowId, [
String? groupId,
}) {
]) {
final payload = RowIdPB.create()
..viewId = viewId
..rowId = rowId;

View File

@ -12,13 +12,11 @@ class CalendarEventEditorBloc
extends Bloc<CalendarEventEditorEvent, CalendarEventEditorState> {
final RowController rowController;
final CalendarLayoutSettingPB layoutSettings;
final RowBackendService _rowService;
CalendarEventEditorBloc({
required this.rowController,
required this.layoutSettings,
}) : _rowService = RowBackendService(viewId: rowController.viewId),
super(CalendarEventEditorState.initial()) {
}) : super(CalendarEventEditorState.initial()) {
on<CalendarEventEditorEvent>((event, emit) async {
await event.when(
initial: () {
@ -43,7 +41,10 @@ class CalendarEventEditorBloc
emit(state.copyWith(cells: cells));
},
delete: () async {
final result = await _rowService.deleteRow(rowController.rowId);
final result = await RowBackendService.deleteRow(
rowController.viewId,
rowController.rowId,
);
result.fold((l) => null, (err) => Log.error(err));
},
);

View File

@ -365,10 +365,9 @@ class _EventList extends StatelessWidget {
final autoEdit =
editingEvent?.event?.eventId == events[index].eventId;
return EventCard(
fieldController: context.read<CalendarBloc>().fieldController,
databaseController:
context.read<CalendarBloc>().databaseController,
event: events[index],
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
autoEdit: autoEdit,
);

View File

@ -1,8 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_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/card_cell.dart';
@ -27,20 +27,16 @@ import 'calendar_event_editor.dart';
class EventCard extends StatefulWidget {
const EventCard({
super.key,
required this.fieldController,
required this.databaseController,
required this.event,
required this.viewId,
required this.rowCache,
required this.constraints,
required this.autoEdit,
this.isDraggable = true,
this.padding = EdgeInsets.zero,
});
final FieldController fieldController;
final DatabaseController databaseController;
final CalendarDayEvent event;
final String viewId;
final RowCache rowCache;
final BoxConstraints constraints;
final bool autoEdit;
final bool isDraggable;
@ -53,6 +49,11 @@ class EventCard extends StatefulWidget {
class _EventCardState extends State<EventCard> {
late final PopoverController _popoverController;
String get viewId => widget.databaseController.viewId;
RowCache get rowCache => widget.databaseController.rowCache;
FieldController get fieldController =>
widget.databaseController.fieldController;
@override
void initState() {
super.initState();
@ -69,7 +70,7 @@ class _EventCardState extends State<EventCard> {
@override
Widget build(BuildContext context) {
final rowInfo = widget.rowCache.getRow(widget.event.eventId);
final rowInfo = rowCache.getRow(widget.event.eventId);
if (rowInfo == null) {
return const SizedBox.shrink();
}
@ -78,7 +79,7 @@ class _EventCardState extends State<EventCard> {
FieldType.URL: URLCardCellStyle(10),
};
final cellBuilder = CardCellBuilder<CalendarDayEvent>(
widget.rowCache.cellCache,
rowCache.cellCache,
styles: styles,
);
final renderHook = _calendarEventCardRenderHook(context);
@ -88,24 +89,19 @@ class _EventCardState extends State<EventCard> {
// in this row are updated.
key: ValueKey(widget.event.eventId),
rowMeta: rowInfo.rowMeta,
viewId: widget.viewId,
rowCache: widget.rowCache,
viewId: viewId,
rowCache: rowCache,
cardData: widget.event,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) {
if (PlatformExtension.isMobile) {
final dataController = RowController(
rowMeta: rowInfo.rowMeta,
viewId: widget.viewId,
rowCache: widget.rowCache,
);
context.push(
MobileCardDetailScreen.routeName,
MobileRowDetailPage.routeName,
extra: {
MobileCardDetailScreen.argRowController: dataController,
MobileCardDetailScreen.argFieldController: widget.fieldController,
MobileRowDetailPage.argRowId: rowInfo.rowId,
MobileRowDetailPage.argDatabaseController:
widget.databaseController,
},
);
} else {
@ -175,10 +171,10 @@ class _EventCardState extends State<EventCard> {
return const SizedBox.shrink();
}
return CalendarEventEditor(
fieldController: widget.fieldController,
rowCache: widget.rowCache,
fieldController: fieldController,
rowCache: rowCache,
rowMeta: widget.event.event.rowMeta,
viewId: widget.viewId,
viewId: viewId,
layoutSettings: settings,
);
},
@ -305,9 +301,10 @@ class _EventCardState extends State<EventCard> {
}
final children = selectedOptions.map(
(option) {
return SelectOptionTag.fromOption(
context: context,
return SelectOptionTag(
option: option,
fontSize: 9,
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
);
},
).toList();

View File

@ -484,15 +484,10 @@ class UnscheduleEventsList extends StatelessWidget {
onPressed: () {
if (PlatformExtension.isMobile) {
context.push(
MobileCardDetailScreen.routeName,
MobileRowDetailPage.routeName,
extra: {
MobileCardDetailScreen.argRowController: RowController(
rowMeta: event.rowMeta,
viewId: databaseController.viewId,
rowCache: databaseController.rowCache,
),
MobileCardDetailScreen.argFieldController:
databaseController.fieldController,
MobileRowDetailPage.argRowId: event.rowMeta.id,
MobileRowDetailPage.argDatabaseController: databaseController,
},
);
context.pop();

View File

@ -30,10 +30,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
databaseController.createRow();
},
deleteRow: (rowInfo) async {
final rowService = RowBackendService(
viewId: rowInfo.viewId,
);
await rowService.deleteRow(rowInfo.rowId);
await RowBackendService.deleteRow(rowInfo.viewId, rowInfo.rowId);
},
moveRow: (int from, int to) {
final List<RowInfo> rows = [...state.rowInfos];

View File

@ -0,0 +1,83 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'mobile_row_detail_bloc.freezed.dart';
class MobileRowDetailBloc
extends Bloc<MobileRowDetailEvent, MobileRowDetailState> {
final DatabaseController databaseController;
MobileRowDetailBloc({
required this.databaseController,
}) : super(MobileRowDetailState.initial()) {
on<MobileRowDetailEvent>(
(event, emit) {
event.when(
initial: (rowId) {
_startListening();
emit(
state.copyWith(
isLoading: false,
currentRowId: rowId,
rowInfos: databaseController.rowCache.rowInfos,
),
);
},
didLoadRows: (rows) {
emit(state.copyWith(rowInfos: rows));
},
changeRowId: (rowId) {
emit(state.copyWith(currentRowId: rowId));
},
);
},
);
}
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
onNumOfRowsChanged: (rowInfos, _, reason) {
if (!isClosed) {
add(MobileRowDetailEvent.didLoadRows(rowInfos));
}
},
onRowsUpdated: (rows, reason) {
if (!isClosed) {
add(
MobileRowDetailEvent.didLoadRows(
databaseController.rowCache.rowInfos,
),
);
}
},
);
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
}
}
@freezed
class MobileRowDetailEvent with _$MobileRowDetailEvent {
const factory MobileRowDetailEvent.initial(String rowId) = _Initial;
const factory MobileRowDetailEvent.didLoadRows(List<RowInfo> rows) =
_DidLoadRows;
const factory MobileRowDetailEvent.changeRowId(String rowId) = _ChangeRowId;
}
@freezed
class MobileRowDetailState with _$MobileRowDetailState {
const factory MobileRowDetailState({
required bool isLoading,
required String? currentRowId,
required List<RowInfo> rowInfos,
}) = _MobileRowDetailState;
factory MobileRowDetailState.initial() {
return const MobileRowDetailState(
isLoading: true,
rowInfos: [],
currentRowId: null,
);
}
}

View File

@ -1,60 +0,0 @@
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dartz/dartz.dart';
import '../../../application/row/row_service.dart';
part 'row_action_sheet_bloc.freezed.dart';
class RowActionSheetBloc
extends Bloc<RowActionSheetEvent, RowActionSheetState> {
final RowBackendService _rowService;
RowActionSheetBloc({
required String viewId,
required RowId rowId,
String? groupId,
}) : _rowService = RowBackendService(viewId: viewId),
super(RowActionSheetState.initial(rowId)) {
on<RowActionSheetEvent>(
(event, emit) async {
await event.when(
deleteRow: () async {
final result = await _rowService.deleteRow(state.rowId);
logResult(result);
},
duplicateRow: () async {
final result = await _rowService.duplicateRow(
rowId: state.rowId,
groupId: groupId,
);
logResult(result);
},
);
},
);
}
void logResult(Either<Unit, FlowyError> result) {
result.fold((l) => null, (err) => Log.error(err));
}
}
@freezed
class RowActionSheetEvent with _$RowActionSheetEvent {
const factory RowActionSheetEvent.duplicateRow() = _DuplicateRow;
const factory RowActionSheetEvent.deleteRow() = _DeleteRow;
}
@freezed
class RowActionSheetState with _$RowActionSheetState {
const factory RowActionSheetState({
required RowId rowId,
}) = _RowActionSheetState;
factory RowActionSheetState.initial(RowId rowId) => RowActionSheetState(
rowId: rowId,
);
}

View File

@ -2,11 +2,10 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/mobile_database_settings_button.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:collection/collection.dart';
@ -28,7 +27,6 @@ import 'widgets/footer/grid_footer.dart';
import 'widgets/header/grid_header.dart';
import 'widgets/row/mobile_row.dart';
import 'widgets/shortcuts.dart';
import '../../widgets/setting/mobile_database_settings_button.dart';
class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier();
@ -304,35 +302,31 @@ class _GridRows extends StatelessWidget {
required bool isDraggable,
Animation<double>? animation,
}) {
final rowCache = context.read<GridBloc>().getRowCache(rowId);
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
final rowMeta = context
.read<GridBloc>()
.databaseController
.rowCache
.getRow(rowId)
?.rowMeta;
if (rowMeta == null) {
Log.warn('RowMeta is null for rowId: $rowId');
return const SizedBox.shrink();
}
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
final rowController = RowController(
viewId: viewId,
rowMeta: rowMeta,
rowCache: rowCache,
);
final databaseController = context.read<GridBloc>().databaseController;
final child = MobileGridRow(
key: ValueKey(rowMeta.id),
rowId: rowId,
viewId: viewId,
isDraggable: isDraggable,
dataController: rowController,
cellBuilder: GridCellBuilder(cellCache: rowController.cellCache),
openDetailPage: (context, cellBuilder) {
databaseController: databaseController,
openDetailPage: (context) {
context.push(
MobileCardDetailScreen.routeName,
MobileRowDetailPage.routeName,
extra: {
MobileCardDetailScreen.argRowController: rowController,
MobileCardDetailScreen.argFieldController: fieldController,
MobileRowDetailPage.argRowId: rowId,
MobileRowDetailPage.argDatabaseController: databaseController,
},
);
},

View File

@ -1,6 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@ -10,7 +9,6 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
@ -27,43 +25,26 @@ class RowActions extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => RowActionSheetBloc(
viewId: viewId,
rowId: rowId,
groupId: groupId,
),
child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
builder: (context, state) {
final cells = _RowAction.values
.where((value) => value.enable())
.map((action) => _ActionCell(action: action))
.toList();
final cells = _RowAction.values
.where((value) => value.enable())
.map((action) => _actionCell(context, action))
.toList();
return ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: cells.length,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
);
},
),
return ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: cells.length,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
);
}
}
class _ActionCell extends StatelessWidget {
final _RowAction action;
const _ActionCell({required this.action, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget _actionCell(BuildContext context, _RowAction action) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
@ -77,7 +58,7 @@ class _ActionCell extends StatelessWidget {
),
onTap: () {
if (action.enable()) {
action.performAction(context);
action.performAction(context, viewId, rowId);
}
},
leftIcon: FlowySvg(
@ -121,18 +102,13 @@ extension _RowActionExtension on _RowAction {
}
}
void performAction(BuildContext context) {
void performAction(BuildContext context, String viewId, String rowId) {
switch (this) {
case _RowAction.duplicate:
context
.read<RowActionSheetBloc>()
.add(const RowActionSheetEvent.duplicateRow());
RowBackendService.duplicateRow(viewId, rowId);
break;
case _RowAction.delete:
context
.read<RowActionSheetBloc>()
.add(const RowActionSheetEvent.deleteRow());
RowBackendService.deleteRow(viewId, rowId);
break;
}
}

View File

@ -1,5 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
@ -17,20 +19,15 @@ import "package:appflowy/generated/locale_keys.g.dart";
import 'package:easy_localization/easy_localization.dart';
class MobileGridRow extends StatefulWidget {
final RowId viewId;
final DatabaseController databaseController;
final RowId rowId;
final RowController dataController;
final GridCellBuilder cellBuilder;
final void Function(BuildContext, GridCellBuilder) openDetailPage;
final void Function(BuildContext context) openDetailPage;
final bool isDraggable;
const MobileGridRow({
super.key,
required this.viewId,
required this.rowId,
required this.dataController,
required this.cellBuilder,
required this.databaseController,
required this.openDetailPage,
this.isDraggable = false,
});
@ -41,15 +38,26 @@ class MobileGridRow extends StatefulWidget {
class _MobileGridRowState extends State<MobileGridRow> {
late final RowBloc _rowBloc;
late final RowController _rowController;
late final GridCellBuilder _cellBuilder;
String get viewId => widget.databaseController.viewId;
RowCache get rowCache => widget.databaseController.rowCache;
@override
void initState() {
super.initState();
_rowController = RowController(
rowMeta: rowCache.getRow(widget.rowId)!.rowMeta,
viewId: viewId,
rowCache: rowCache,
);
_rowBloc = RowBloc(
rowId: widget.rowId,
dataController: widget.dataController,
viewId: widget.viewId,
dataController: _rowController,
viewId: viewId,
)..add(const RowEvent.initial());
_cellBuilder = GridCellBuilder(cellCache: rowCache.cellCache);
}
@override
@ -65,11 +73,8 @@ class _MobileGridRowState extends State<MobileGridRow> {
SizedBox(width: GridSize.leadingHeaderPadding),
Expanded(
child: RowContent(
builder: widget.cellBuilder,
onExpand: () => widget.openDetailPage(
context,
widget.cellBuilder,
),
builder: _cellBuilder,
onExpand: () => widget.openDetailPage(context),
),
),
],
@ -82,6 +87,7 @@ class _MobileGridRowState extends State<MobileGridRow> {
@override
Future<void> dispose() async {
_rowBloc.close();
_rowController.dispose();
super.dispose();
}
}

View File

@ -63,9 +63,11 @@ class _SelectOptionCellState extends State<SelectOptionCardCell> {
final children = state.selectedOptions
.map(
(option) => SelectOptionTag.fromOption(
context: context,
(option) => SelectOptionTag(
option: option,
fontSize: 11,
padding:
const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
),
)
.toList();

View File

@ -1,3 +1,7 @@
import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/checkbox_cell.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/number_cell.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/text_cell.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/cells/url_cell.dart';
import 'package:appflowy/mobile/presentation/database/card/row/cells/cells.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/util/platform_extension.dart';
@ -120,72 +124,151 @@ class GridCellBuilder {
throw UnimplementedError;
}
}
// editable cell/(card's propery value) widget
GridCellWidget _getMobileCardCellWidget(
ValueKey key,
DatabaseCellContext cellContext,
CellControllerBuilder cellControllerBuilder,
GridCellStyle? style,
) {
switch (cellContext.fieldType) {
case FieldType.RichText:
style as GridTextCellStyle?;
return MobileTextCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
);
case FieldType.Number:
style as GridNumberCellStyle?;
return MobileNumberCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return MobileTimestampCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.Checkbox:
return MobileCheckboxCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
style as DateCellStyle?;
return GridDateCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
);
case FieldType.URL:
style as GridURLCellStyle?;
return MobileURLCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
key: key,
);
case FieldType.SingleSelect:
return GridSingleSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.MultiSelect:
return GridMultiSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.Checklist:
return GridChecklistCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
GridCellWidget _getMobileCardCellWidget(
ValueKey key,
DatabaseCellContext cellContext,
CellControllerBuilder cellControllerBuilder,
GridCellStyle? style,
) {
switch (cellContext.fieldType) {
case FieldType.RichText:
style as GridTextCellStyle?;
return MobileTextCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
);
case FieldType.Number:
style as GridNumberCellStyle?;
return MobileNumberCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return MobileTimestampCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.Checkbox:
return MobileCheckboxCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
style as DateCellStyle?;
return GridDateCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
);
case FieldType.URL:
style as GridURLCellStyle?;
return MobileURLCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
key: key,
);
case FieldType.SingleSelect:
return GridSingleSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.MultiSelect:
return GridMultiSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.Checklist:
return GridChecklistCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
}
throw UnimplementedError;
}
}
class MobileRowDetailPageCellBuilder {
final CellMemCache cellCache;
MobileRowDetailPageCellBuilder({
required this.cellCache,
});
GridCellWidget build(
DatabaseCellContext cellContext, {
GridCellStyle? style,
}) {
final cellControllerBuilder = CellControllerBuilder(
cellContext: cellContext,
cellCache: cellCache,
);
final key = cellContext.key();
switch (cellContext.fieldType) {
case FieldType.RichText:
style as GridTextCellStyle?;
return RowDetailTextCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
);
case FieldType.Number:
style as GridNumberCellStyle?;
return RowDetailNumberCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return GridTimestampCell(
cellControllerBuilder: cellControllerBuilder,
fieldType: cellContext.fieldType,
style: style,
key: key,
);
case FieldType.Checkbox:
return RowDetailCheckboxCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
style as DateCellStyle?;
return GridDateCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
);
case FieldType.URL:
style as GridURLCellStyle?;
return RowDetailURLCell(
cellControllerBuilder: cellControllerBuilder,
hintText: style?.placeholder,
key: key,
);
case FieldType.SingleSelect:
return GridSingleSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.MultiSelect:
return GridMultiSelectCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.Checklist:
return GridChecklistCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
}
throw UnimplementedError;
}
throw UnimplementedError;
}
class BlankCell extends StatelessWidget {

View File

@ -3,7 +3,6 @@ import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_pi
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.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';
@ -14,35 +13,32 @@ import 'date_cell_bloc.dart';
import 'date_editor.dart';
class DateCellStyle extends GridCellStyle {
String? placeholder;
String placeholder;
Alignment alignment;
EdgeInsets? cellPadding;
final bool useRoundedBorder;
DateCellStyle({
this.placeholder,
this.alignment = Alignment.center,
this.placeholder = "",
this.alignment = Alignment.centerLeft,
this.cellPadding,
this.useRoundedBorder = false,
});
}
abstract class GridCellDelegate {
void onFocus(bool isFocus);
GridCellDelegate get delegate;
}
class GridDateCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final DateCellStyle? cellStyle;
late final DateCellStyle cellStyle;
GridDateCell({
super.key,
GridCellStyle? style,
required this.cellControllerBuilder,
Key? key,
}) : super(key: key) {
}) {
if (style != null) {
cellStyle = (style as DateCellStyle);
} else {
cellStyle = null;
cellStyle = DateCellStyle();
}
}
@ -66,21 +62,19 @@ class _DateCellState extends GridCellState<GridDateCell> {
@override
Widget build(BuildContext context) {
final alignment = widget.cellStyle != null
? widget.cellStyle!.alignment
: Alignment.centerLeft;
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<DateCellBloc, DateCellState>(
builder: (context, state) {
final child = GridDateCellText(
dateStr: state.dateStr,
placeholder: widget.cellStyle?.placeholder ?? "",
alignment: alignment,
cellPadding:
widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
);
final text = state.dateStr.isEmpty
? widget.cellStyle.placeholder
: state.dateStr;
final color =
state.dateStr.isEmpty ? Theme.of(context).hintColor : null;
final padding =
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets;
final alignment = widget.cellStyle.alignment;
if (PlatformExtension.isDesktopOrWeb) {
return AppFlowyPopover(
controller: _popover,
@ -88,7 +82,11 @@ class _DateCellState extends GridCellState<GridDateCell> {
direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(260, 620)),
margin: EdgeInsets.zero,
child: child,
child: Container(
alignment: alignment,
padding: padding,
child: FlowyText.medium(text, color: color),
),
popupBuilder: (BuildContext popoverContent) {
return DateCellEditor(
cellController: widget.cellControllerBuilder.build()
@ -101,9 +99,59 @@ class _DateCellState extends GridCellState<GridDateCell> {
widget.cellContainerNotifier.isFocus = false;
},
);
} else if (widget.cellStyle.useRoundedBorder) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileDateCellEditScreen(
controller: widget.cellControllerBuilder.build()
as DateCellController,
showAsFullScreen: false,
);
},
),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
padding: padding,
child: Row(
children: [
Expanded(
child: FlowyText.regular(
text,
fontSize: 16,
color: color,
maxLines: null,
),
),
const HSpace(6),
const RotatedBox(
quarterTurns: 3,
child: Icon(Icons.chevron_left),
),
const HSpace(2),
],
),
),
);
} else {
return FlowyButton(
text: child,
text: Container(
alignment: alignment,
padding: padding,
child: FlowyText.medium(text, color: color),
),
onTap: () {
showMobileBottomSheet(
context,
@ -139,36 +187,3 @@ class _DateCellState extends GridCellState<GridDateCell> {
@override
String? onCopy() => _cellBloc.state.dateStr;
}
class GridDateCellText extends StatelessWidget {
final String dateStr;
final String placeholder;
final Alignment alignment;
final EdgeInsets cellPadding;
const GridDateCellText({
required this.dateStr,
required this.placeholder,
required this.alignment,
required this.cellPadding,
super.key,
});
@override
Widget build(BuildContext context) {
final isPlaceholder = dateStr.isEmpty;
final text = isPlaceholder ? placeholder : dateStr;
return Align(
alignment: alignment,
child: Padding(
padding: cellPadding,
child: FlowyText.medium(
text,
color: isPlaceholder
? Theme.of(context).hintColor
: AFThemeExtension.of(context).textColor,
maxLines: null,
),
),
);
}
}

View File

@ -99,8 +99,7 @@ class _GridCellEnterRegion extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<CellContainerNotifier, bool>(
selector: (context, cellNotifier) =>
!cellNotifier.isFocus && (cellNotifier.onEnter || isPrimary),
selector: (context, cellNotifier) => !cellNotifier.isFocus && isPrimary,
builder: (context, showAccessory, _) {
final List<Widget> children = [child];
@ -112,17 +111,10 @@ class _GridCellEnterRegion extends StatelessWidget {
);
}
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) =>
CellContainerNotifier.of(context, listen: false).onEnter = true,
onExit: (p) =>
CellContainerNotifier.of(context, listen: false).onEnter = false,
child: Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: children,
),
return Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: children,
);
},
);

View File

@ -62,41 +62,33 @@ extension SelectOptionColorExtension on SelectOptionColorPB {
}
class SelectOptionTag extends StatelessWidget {
final String name;
final Color color;
final SelectOptionPB? option;
final String? name;
final double? fontSize;
final Color? color;
final TextStyle? textStyle;
final EdgeInsets padding;
final void Function(String)? onRemove;
const SelectOptionTag({
required this.name,
required this.color,
this.onRemove,
super.key,
});
factory SelectOptionTag.fromOption({
required BuildContext context,
required SelectOptionPB option,
Function(String)? onRemove,
}) {
return SelectOptionTag(
name: option.name,
color: option.color.toColor(context),
onRemove: onRemove,
);
}
this.option,
this.name,
this.fontSize,
this.color,
this.textStyle,
this.onRemove,
required this.padding,
}) : assert(option != null || name != null && color != null);
@override
Widget build(BuildContext context) {
EdgeInsets padding =
const EdgeInsets.symmetric(vertical: 1, horizontal: 8.0);
if (onRemove != null) {
padding = padding.copyWith(right: 2.0);
}
final optionName = option?.name ?? name!;
final optionColor = option?.color.toColor(context) ?? color!;
return Container(
padding: padding,
padding: onRemove == null ? padding : padding.copyWith(right: 2.0),
decoration: BoxDecoration(
color: color,
color: optionColor,
borderRadius: Corners.s6Border,
),
child: Row(
@ -104,17 +96,17 @@ class SelectOptionTag extends StatelessWidget {
children: [
Flexible(
child: FlowyText.regular(
name,
fontSize: FontSizes.s11,
optionName,
fontSize: fontSize,
overflow: TextOverflow.ellipsis,
color: AFThemeExtension.of(context).textColor,
),
),
if (onRemove != null) ...[
const HSpace(2),
const HSpace(4),
FlowyIconButton(
width: 18.0,
onPressed: () => onRemove?.call(name),
width: 16.0,
onPressed: () => onRemove?.call(optionName),
hoverColor: Colors.transparent,
icon: const FlowySvg(FlowySvgs.close_s),
),
@ -154,9 +146,11 @@ class SelectOptionTagCell extends StatelessWidget {
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsets.all(5.0),
child: SelectOptionTag.fromOption(
context: context,
child: SelectOptionTag(
option: option,
fontSize: 11,
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
),
),
),

View File

@ -11,7 +11,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:easy_localization/easy_localization.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -404,9 +403,9 @@ class _SelectOption extends StatelessWidget {
// padding
const HSpace(12),
// option tag
_SelectOptionTag(
optionName: option.name,
color: option.color.toColor(context),
SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 8),
),
const Spacer(),
// more options
@ -447,9 +446,11 @@ class _CreateOptionCell extends StatelessWidget {
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: _SelectOptionTag(
optionName: optionName,
child: SelectOptionTag(
name: optionName,
color: Theme.of(context).colorScheme.surfaceVariant,
padding:
const EdgeInsets.symmetric(horizontal: 11, vertical: 8),
),
),
),
@ -460,36 +461,6 @@ class _CreateOptionCell extends StatelessWidget {
}
}
class _SelectOptionTag extends StatelessWidget {
const _SelectOptionTag({
required this.optionName,
required this.color,
});
final String optionName;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: 6.0,
horizontal: 12.0,
),
decoration: BoxDecoration(
color: color,
borderRadius: Corners.s12Border,
),
child: FlowyText.regular(
optionName,
fontSize: 16,
overflow: TextOverflow.ellipsis,
color: AFThemeExtension.of(context).textColor,
),
);
}
}
class _MoreOptions extends StatelessWidget {
const _MoreOptions({
required this.option,

View File

@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_c
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -17,26 +18,28 @@ import 'select_option_editor.dart';
class SelectOptionCellStyle extends GridCellStyle {
String placeholder;
EdgeInsets? cellPadding;
bool useRoundedBorder;
SelectOptionCellStyle({
required this.placeholder,
this.placeholder = "",
this.cellPadding,
this.useRoundedBorder = false,
});
}
class GridSingleSelectCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final SelectOptionCellStyle? cellStyle;
late final SelectOptionCellStyle cellStyle;
GridSingleSelectCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
Key? key,
}) : super(key: key) {
}) {
if (style != null) {
cellStyle = (style as SelectOptionCellStyle);
} else {
cellStyle = null;
cellStyle = SelectOptionCellStyle();
}
}
@ -45,17 +48,16 @@ class GridSingleSelectCell extends GridCellWidget {
}
class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
final PopoverController _popoverController = PopoverController();
late SelectOptionCellBloc _cellBloc;
late final PopoverController _popover;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
_cellBloc = SelectOptionCellBloc(cellController: cellController)
..add(const SelectOptionCellEvent.initial());
_popover = PopoverController();
super.initState();
}
@override
@ -69,7 +71,7 @@ class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
cellStyle: widget.cellStyle,
onCellEditing: (isFocus) =>
widget.cellContainerNotifier.isFocus = isFocus,
popoverController: _popover,
popoverController: _popoverController,
cellControllerBuilder: widget.cellControllerBuilder,
);
},
@ -84,23 +86,23 @@ class _SingleSelectCellState extends GridCellState<GridSingleSelectCell> {
}
@override
void requestBeginFocus() => _popover.show();
void requestBeginFocus() => _popoverController.show();
}
//----------------------------------------------------------------
class GridMultiSelectCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final SelectOptionCellStyle? cellStyle;
late final SelectOptionCellStyle cellStyle;
GridMultiSelectCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
Key? key,
}) : super(key: key) {
}) {
if (style != null) {
cellStyle = (style as SelectOptionCellStyle);
} else {
cellStyle = null;
cellStyle = SelectOptionCellStyle();
}
}
@ -109,17 +111,16 @@ class GridMultiSelectCell extends GridCellWidget {
}
class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
final PopoverController _popoverController = PopoverController();
late SelectOptionCellBloc _cellBloc;
late final PopoverController _popover;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
_cellBloc = SelectOptionCellBloc(cellController: cellController)
..add(const SelectOptionCellEvent.initial());
_popover = PopoverController();
super.initState();
}
@override
@ -133,7 +134,7 @@ class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
cellStyle: widget.cellStyle,
onCellEditing: (isFocus) =>
widget.cellContainerNotifier.isFocus = isFocus,
popoverController: _popover,
popoverController: _popoverController,
cellControllerBuilder: widget.cellControllerBuilder,
);
},
@ -148,24 +149,24 @@ class _MultiSelectCellState extends GridCellState<GridMultiSelectCell> {
}
@override
void requestBeginFocus() => _popover.show();
void requestBeginFocus() => _popoverController.show();
}
class SelectOptionWrap extends StatefulWidget {
final List<SelectOptionPB> selectOptions;
final SelectOptionCellStyle? cellStyle;
final SelectOptionCellStyle cellStyle;
final CellControllerBuilder cellControllerBuilder;
final PopoverController popoverController;
final void Function(bool) onCellEditing;
const SelectOptionWrap({
super.key,
required this.selectOptions,
required this.cellControllerBuilder,
required this.onCellEditing,
required this.popoverController,
this.cellStyle,
Key? key,
}) : super(key: key);
required this.cellStyle,
});
@override
State<StatefulWidget> createState() => _SelectOptionWrapState();
@ -180,13 +181,8 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
Widget child = Padding(
padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
child: _buildOptions(context),
);
if (PlatformExtension.isDesktopOrWeb) {
child = AppFlowyPopover(
return AppFlowyPopover(
controller: widget.popoverController,
constraints: constraints,
margin: EdgeInsets.zero,
@ -200,11 +196,57 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
);
},
onClose: () => widget.onCellEditing(false),
child: child,
child: Padding(
padding: widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
child: _buildOptions(context),
),
);
} else if (widget.cellStyle.useRoundedBorder) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileSelectOptionEditor(
cellController: cellController,
);
},
),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: widget.selectOptions.isEmpty ? 13 : 10,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Row(
children: [
Expanded(child: _buildMobileOptions(isInRowDetail: true)),
const HSpace(6),
const RotatedBox(
quarterTurns: 3,
child: Icon(Icons.chevron_left),
),
const HSpace(2),
],
),
),
);
} else {
child = FlowyButton(
text: child,
return FlowyButton(
text: Padding(
padding: widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
child: _buildMobileOptions(isInRowDetail: false),
),
onTap: () {
showMobileBottomSheet(
context,
@ -218,17 +260,15 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
},
);
}
return child;
}
Widget _buildOptions(BuildContext context) {
final Widget child;
if (widget.selectOptions.isEmpty && widget.cellStyle != null) {
if (widget.selectOptions.isEmpty) {
child = Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: FlowyText.medium(
widget.cellStyle!.placeholder,
widget.cellStyle.placeholder,
color: Theme.of(context).hintColor,
),
);
@ -237,9 +277,9 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
(option) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: SelectOptionTag.fromOption(
context: context,
child: SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 8),
),
);
},
@ -252,4 +292,40 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
}
return Align(alignment: Alignment.centerLeft, child: child);
}
Widget _buildMobileOptions({required bool isInRowDetail}) {
if (widget.selectOptions.isEmpty) {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(vertical: 1),
child: FlowyText.medium(
widget.cellStyle.placeholder,
color: Theme.of(context).hintColor,
),
);
} else {
final children = widget.selectOptions.mapIndexed(
(index, option) {
return Padding(
padding: EdgeInsets.only(left: index == 0 ? 0 : 4),
child: SelectOptionTag(
option: option,
fontSize: 14,
padding: isInRowDetail
? const EdgeInsets.symmetric(horizontal: 11, vertical: 5)
: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
),
);
},
).toList();
return Align(
alignment: Alignment.centerLeft,
child: Wrap(
runSpacing: 4,
children: children,
),
);
}
}
}

View File

@ -264,6 +264,11 @@ class _CreateOptionCell extends StatelessWidget {
alignment: Alignment.centerLeft,
child: SelectOptionTag(
name: name,
fontSize: 11,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 1,
),
color: Theme.of(context).colorScheme.surfaceVariant,
),
),

View File

@ -165,10 +165,10 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
final children = widget.selectedOptionMap.values
.map(
(option) => SelectOptionTag.fromOption(
context: context,
(option) => SelectOptionTag(
option: option,
onRemove: (option) => widget.onRemove(option),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
),
)
.toList();

View File

@ -3,6 +3,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.da
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -12,11 +13,13 @@ class TimestampCellStyle extends GridCellStyle {
String? placeholder;
Alignment alignment;
EdgeInsets? cellPadding;
final bool useRoundedBorder;
TimestampCellStyle({
this.placeholder,
this.alignment = Alignment.center,
this.cellPadding,
this.useRoundedBorder = false,
});
}
@ -28,11 +31,11 @@ class GridTimestampCell extends GridCellWidget {
late final TimestampCellStyle? cellStyle;
GridTimestampCell({
super.key,
GridCellStyle? style,
required this.fieldType,
required this.cellControllerBuilder,
Key? key,
}) : super(key: key) {
}) {
if (style != null) {
cellStyle = (style as TimestampCellStyle);
} else {
@ -49,11 +52,11 @@ class _TimestampCellState extends GridCellState<GridTimestampCell> {
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as TimestampCellController;
_cellBloc = TimestampCellBloc(cellController: cellController)
..add(const TimestampCellEvent.initial());
super.initState();
}
@override
@ -68,19 +71,46 @@ class _TimestampCellState extends GridCellState<GridTimestampCell> {
builder: (context, state) {
final isEmpty = state.dateStr.isEmpty;
final text = isEmpty ? placeholder : state.dateStr;
return Align(
alignment: alignment,
child: Padding(
padding: padding,
child: FlowyText.medium(
text,
color: isEmpty
? Theme.of(context).hintColor
: AFThemeExtension.of(context).textColor,
maxLines: null,
if (PlatformExtension.isDesktopOrWeb ||
widget.cellStyle == null ||
!widget.cellStyle!.useRoundedBorder) {
return Align(
alignment: alignment,
child: Padding(
padding: padding,
child: FlowyText.medium(
text,
color: isEmpty ? Theme.of(context).hintColor : null,
maxLines: null,
),
),
),
);
);
} else {
return Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Padding(
padding: padding,
child: FlowyText.medium(
text,
fontSize: 16,
color: isEmpty
? Theme.of(context).hintColor
: AFThemeExtension.of(context).textColor,
maxLines: null,
),
),
);
}
},
),
);

View File

@ -1,13 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RowActionList extends StatelessWidget {
final RowController rowController;
@ -18,33 +17,36 @@ class RowActionList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<RowActionSheetBloc>(
create: (context) => RowActionSheetBloc(
viewId: rowController.viewId,
rowId: rowController.rowId,
groupId: rowController.groupId,
),
child: IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
RowDetailPageDuplicateButton(
rowId: rowController.rowId,
groupId: rowController.groupId,
),
const VSpace(4.0),
RowDetailPageDeleteButton(rowId: rowController.rowId),
],
),
return IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
RowDetailPageDuplicateButton(
viewId: rowController.viewId,
rowId: rowController.rowId,
groupId: rowController.groupId,
),
const VSpace(4.0),
RowDetailPageDeleteButton(
viewId: rowController.viewId,
rowId: rowController.rowId,
),
],
),
);
}
}
class RowDetailPageDeleteButton extends StatelessWidget {
const RowDetailPageDeleteButton({
super.key,
required this.viewId,
required this.rowId,
});
final String viewId;
final String rowId;
const RowDetailPageDeleteButton({required this.rowId, super.key});
@override
Widget build(BuildContext context) {
@ -54,9 +56,7 @@ class RowDetailPageDeleteButton extends StatelessWidget {
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
leftIcon: const FlowySvg(FlowySvgs.trash_m),
onTap: () {
context
.read<RowActionSheetBloc>()
.add(const RowActionSheetEvent.deleteRow());
RowBackendService.deleteRow(viewId, rowId);
FlowyOverlay.pop(context);
},
),
@ -65,12 +65,14 @@ class RowDetailPageDeleteButton extends StatelessWidget {
}
class RowDetailPageDuplicateButton extends StatelessWidget {
final String viewId;
final String rowId;
final String? groupId;
const RowDetailPageDuplicateButton({
super.key,
required this.viewId,
required this.rowId,
this.groupId,
super.key,
});
@override
@ -81,9 +83,7 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
leftIcon: const FlowySvg(FlowySvgs.copy_s),
onTap: () {
context
.read<RowActionSheetBloc>()
.add(const RowActionSheetEvent.duplicateRow());
RowBackendService.duplicateRow(viewId, rowId, groupId);
FlowyOverlay.pop(context);
},
),

View File

@ -535,16 +535,18 @@ GoRoute _mobileCalendarScreenRoute() {
GoRoute _mobileCardDetailScreenRoute() {
return GoRoute(
path: MobileCardDetailScreen.routeName,
parentNavigatorKey: AppGlobals.rootNavKey,
path: MobileRowDetailPage.routeName,
pageBuilder: (context, state) {
final args = state.extra as Map<String, dynamic>;
final rowController = args[MobileCardDetailScreen.argRowController];
final fieldController = args[MobileCardDetailScreen.argFieldController];
final databaseController =
args[MobileRowDetailPage.argDatabaseController];
final rowId = args[MobileRowDetailPage.argRowId]!;
return MaterialPage(
child: MobileCardDetailScreen(
rowController: rowController,
fieldController: fieldController,
child: MobileRowDetailPage(
databaseController: databaseController,
rowId: rowId,
),
);
},

View File

@ -1135,4 +1135,4 @@
"date": "Date",
"addField": "Add field"
}
}
}