mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
af07b53484
commit
5f94ba129e
@ -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(
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -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";
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
},
|
||||
);
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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];
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
@ -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,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -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);
|
||||
},
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -1135,4 +1135,4 @@
|
||||
"date": "Date",
|
||||
"addField": "Add field"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user