feat: revamp row detail page UI (#3328)

* feat: revamp row detail page UI

* chore: some minor details and fix tests

* fix: fix tests

* chore: remove unused field

* chore: code cleanup

* test: add reordering fields in row page tests

* chore: remove duplicate and delete row events

* chore: timestamp cell ui adjustment

* chore: remove unused code

* test: fix new integration tests
This commit is contained in:
Richard Shiue 2023-09-13 19:10:08 +08:00 committed by GitHub
parent ef6f9a3175
commit 1ca130d7de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 650 additions and 489 deletions

View File

@ -9,7 +9,7 @@ import 'util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('calendar database view', () {
group('calendar', () {
testWidgets('update calendar layout', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -116,6 +116,7 @@ void main() {
tester.assertRowDetailPageOpened();
// Duplicate the event
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDuplicateRowButton();
await tester.dismissRowDetailPage();
@ -125,6 +126,7 @@ void main() {
// Delete an event
await tester.openCalendarEvent(index: 1);
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
// Check that there is 1 event
@ -155,6 +157,7 @@ void main() {
// Delete the event
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
// Create a new event in today's calendar cell

View File

@ -135,7 +135,40 @@ void main() {
}
});
testWidgets('check document is exist in row detail page', (tester) async {
testWidgets('change order of fields and cells', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
// Assert that the first field in the row details page is the select
// option tyoe
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
// Reorder first field in list
final gesture = await tester.hoverOnFieldInRowDetail(index: 0);
await tester.pumpAndSettle();
await tester.reorderFieldInRowDetail(offset: 30);
// Orders changed, now the checkbox is first
tester.assertFirstFieldInRowDetailByType(FieldType.Checkbox);
await gesture.removePointer();
await tester.pumpAndSettle();
// Reorder second field in list
await tester.hoverOnFieldInRowDetail(index: 1);
await tester.pumpAndSettle();
await tester.reorderFieldInRowDetail(offset: -30);
// First field is now back to select option
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
});
testWidgets('check document exists in row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -149,7 +182,7 @@ void main() {
await tester.assertDocumentExistInRowDetailPage();
});
testWidgets('update the content of the document and re-open it',
testWidgets('update the contents of the document and re-open it',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -239,6 +272,7 @@ void main() {
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
await tester.tapEscButton();
@ -255,6 +289,7 @@ void main() {
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDuplicateRowButton();
await tester.tapEscButton();

View File

@ -48,6 +48,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
@ -485,7 +486,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(banner, findsOneWidget);
await startGesture(
getTopLeft(banner),
getCenter(banner),
kind: PointerDeviceKind.mouse,
);
@ -524,6 +525,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(deleteButton);
}
Future<TestGesture> hoverOnFieldInRowDetail({required int index}) async {
final fieldButtons = find.byType(FieldCellButton);
final button = find
.descendant(of: find.byType(RowDetailPage), matching: fieldButtons)
.at(index);
return startGesture(
getCenter(button),
kind: PointerDeviceKind.mouse,
);
}
Future<void> reorderFieldInRowDetail({required double offset}) async {
final thumb = find
.byWidgetPredicate(
(widget) => widget is ReorderableDragStartListener && widget.enabled,
)
.first;
await drag(
thumb,
Offset(0, offset),
kind: PointerDeviceKind.mouse,
);
await pumpAndSettle();
}
Future<void> scrollGridByOffset(Offset offset) async {
await drag(find.byType(GridPage), offset);
await pumpAndSettle();
@ -601,6 +627,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(button);
}
Future<void> tapRowDetailPageRowActionButton() async {
await tapButton(find.byType(RowActionButton));
}
Future<void> tapRowDetailPageCreatePropertyButton() async {
await tapButton(find.byType(CreateRowFieldButton));
}
@ -670,6 +700,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(field, findsOneWidget);
}
void assertFirstFieldInRowDetailByType(FieldType fieldType) {
final firstField = find
.descendant(
of: find.byType(RowDetailPage),
matching: find.byType(FieldCellButton),
)
.first;
final widget = this.widget<FieldCellButton>(firstField);
expect(widget.field.fieldType, fieldType);
}
Future<void> findFieldWithName(String name) async {
final field = find.byWidgetPredicate(
(widget) => widget is FieldCellButton && widget.field.name == name,

View File

@ -395,13 +395,7 @@ class RowDataBuilder {
}
void insertDate(FieldInfo fieldInfo, DateTime date) {
assert(
[
FieldType.DateTime,
FieldType.LastEditedTime,
FieldType.CreatedTime,
].contains(fieldInfo.fieldType),
);
assert(FieldType.DateTime == fieldInfo.fieldType);
final timestamp = date.millisecondsSinceEpoch ~/ 1000;
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
}

View File

@ -32,11 +32,7 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
await _listenRowMeteChanged();
},
didReceiveRowMeta: (RowMetaPB rowMeta) {
emit(
state.copyWith(
rowMeta: rowMeta,
),
);
emit(state.copyWith(rowMeta: rowMeta));
},
setCover: (String coverURL) {
_updateMeta(coverURL: coverURL);

View File

@ -1,23 +1,20 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../../../application/cell/cell_service.dart';
import '../../../application/field/field_service.dart';
import '../../../application/row/row_controller.dart';
part 'row_detail_bloc.freezed.dart';
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
final RowBackendService rowService;
final RowController rowController;
RowDetailBloc({
required this.rowController,
}) : rowService = RowBackendService(viewId: rowController.viewId),
super(RowDetailState.initial()) {
}) : super(RowDetailState.initial()) {
on<RowDetailEvent>(
(event, emit) async {
await event.when(
@ -58,14 +55,8 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
(err) => Log.error(err),
);
},
deleteRow: (rowId) async {
await rowService.deleteRow(rowId);
},
duplicateRow: (String rowId, String? groupId) async {
await rowService.duplicateRow(
rowId: rowId,
groupId: groupId,
);
reorderField: (fieldId, fromIndex, toIndex) async {
await _reorderField(fieldId, fromIndex, toIndex, emit);
},
);
},
@ -94,6 +85,25 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
fieldId: fieldId,
);
}
Future<void> _reorderField(
String fieldId,
int fromIndex,
int toIndex,
Emitter<RowDetailState> emit,
) async {
final cells = List<DatabaseCellContext>.from(state.cells);
cells.insert(toIndex, cells.removeAt(fromIndex));
emit(state.copyWith(cells: cells));
final fieldService =
FieldBackendService(viewId: rowController.viewId, fieldId: fieldId);
final result = await fieldService.moveField(
fromIndex,
toIndex,
);
result.fold((l) {}, (err) => Log.error(err));
}
}
@freezed
@ -102,9 +112,11 @@ class RowDetailEvent with _$RowDetailEvent {
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
const factory RowDetailEvent.showField(String fieldId) = _ShowField;
const factory RowDetailEvent.hideField(String fieldId) = _HideField;
const factory RowDetailEvent.deleteRow(String rowId) = _DeleteRow;
const factory RowDetailEvent.duplicateRow(String rowId, String? groupId) =
_DuplicateRow;
const factory RowDetailEvent.reorderField(
String fieldId,
int fromIndex,
int toIndex,
) = _ReorderField;
const factory RowDetailEvent.didReceiveCellDatas(
List<DatabaseCellContext> gridCells,
) = _DidReceiveCellDatas;

View File

@ -154,29 +154,33 @@ class FieldCellButton extends StatelessWidget {
final FieldPB field;
final int? maxLines;
final BorderRadius? radius;
final EdgeInsets? margin;
const FieldCellButton({
required this.field,
required this.onTap,
this.maxLines = 1,
this.radius = BorderRadius.zero,
this.margin,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyButton(
hoverColor: AFThemeExtension.of(context).greyHover,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onTap: onTap,
leftIcon: FlowySvg(
field.fieldType.icon(),
color: Theme.of(context).iconTheme.color,
),
radius: radius,
text: FlowyText.medium(
field.name,
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
color: AFThemeExtension.of(context).textColor,
),
margin: GridSize.cellContentInsets,
margin: margin ?? GridSize.cellContentInsets,
);
}
}

View File

@ -40,8 +40,7 @@ class RowActions extends StatelessWidget {
.map((action) => _ActionCell(action: action))
.toList();
//
final list = ListView.separated(
return ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: cells.length,
@ -53,7 +52,6 @@ class RowActions extends StatelessWidget {
return cells[index];
},
);
return list;
},
),
);
@ -70,6 +68,7 @@ class _ActionCell extends StatelessWidget {
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
useIntrinsicWidth: true,
text: FlowyText.medium(
action.title(),
color: action.enable()

View File

@ -1,9 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
@ -91,50 +91,31 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
class AccessoryHover extends StatefulWidget {
final CellAccessory child;
final EdgeInsets contentPadding;
const AccessoryHover({
required this.child,
this.contentPadding = EdgeInsets.zero,
Key? key,
}) : super(key: key);
const AccessoryHover({required this.child, super.key});
@override
State<AccessoryHover> createState() => _AccessoryHoverState();
}
class _AccessoryHoverState extends State<AccessoryHover> {
late AccessoryHoverState _hoverState;
VoidCallback? _listenerFn;
@override
void initState() {
_hoverState = AccessoryHoverState();
_listenerFn = () =>
_hoverState.onHover = widget.child.onAccessoryHover?.value ?? false;
widget.child.onAccessoryHover?.addListener(_listenerFn!);
super.initState();
}
@override
void dispose() {
_hoverState.dispose();
if (_listenerFn != null) {
widget.child.onAccessoryHover?.removeListener(_listenerFn!);
_listenerFn = null;
}
super.dispose();
}
bool _isHover = false;
@override
Widget build(BuildContext context) {
final List<Widget> children = [
Padding(padding: widget.contentPadding, child: widget.child),
DecoratedBox(
decoration: BoxDecoration(
color: _isHover
? AFThemeExtension.of(context).lightGreyHover
: Colors.transparent,
borderRadius: Corners.s6Border,
),
child: widget.child,
),
];
final accessoryBuilder = widget.child.accessoryBuilder;
if (accessoryBuilder != null) {
if (accessoryBuilder != null && _isHover) {
final accessories = accessoryBuilder(
(GridCellAccessoryBuildContext(
anchorContext: context,
@ -149,36 +130,20 @@ class _AccessoryHoverState extends State<AccessoryHover> {
);
}
return ChangeNotifierProvider.value(
value: _hoverState,
child: MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) => setState(() => _hoverState.onHover = true),
onExit: (p) => setState(() => _hoverState.onHover = false),
child: Stack(
fit: StackFit.loose,
alignment: AlignmentDirectional.center,
children: children,
),
return MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) => setState(() => _isHover = true),
onExit: (p) => setState(() => _isHover = false),
child: Stack(
fit: StackFit.loose,
alignment: AlignmentDirectional.center,
children: children,
),
);
}
}
class AccessoryHoverState extends ChangeNotifier {
bool _onHover = false;
set onHover(bool value) {
if (_onHover != value) {
_onHover = value;
notifyListeners();
}
}
bool get onHover => _onHover;
}
class CellAccessoryContainer extends StatelessWidget {
final List<GridCellAccessoryBuilder> accessories;
const CellAccessoryContainer({required this.accessories, Key? key})

View File

@ -35,6 +35,7 @@ class GridCellBuilder {
case FieldType.Checkbox:
return GridCheckboxCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.DateTime:
@ -71,6 +72,7 @@ class GridCellBuilder {
case FieldType.Number:
return GridNumberCell(
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.RichText:

View File

@ -9,12 +9,29 @@ import 'checkbox_cell_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
class GridCheckboxCellStyle extends GridCellStyle {
EdgeInsets? cellPadding;
GridCheckboxCellStyle({
this.cellPadding,
});
}
class GridCheckboxCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final GridCheckboxCellStyle cellStyle;
GridCheckboxCell({
required this.cellControllerBuilder,
GridCellStyle? style,
Key? key,
}) : super(key: key);
}) : super(key: key) {
if (style != null) {
cellStyle = (style as GridCheckboxCellStyle);
} else {
cellStyle = GridCheckboxCellStyle();
}
}
@override
GridCellState<GridCheckboxCell> createState() => _CheckboxCellState();
@ -46,7 +63,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: GridSize.cellContentInsets,
padding:
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
child: FlowyIconButton(
hoverColor: Colors.transparent,
onPressed: () => context

View File

@ -1,7 +1,8 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.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/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
@ -10,9 +11,15 @@ import 'date_cell_bloc.dart';
import 'date_editor.dart';
class DateCellStyle extends GridCellStyle {
String? placeholder;
Alignment alignment;
EdgeInsets? cellPadding;
DateCellStyle({this.alignment = Alignment.center});
DateCellStyle({
this.placeholder,
this.alignment = Alignment.center,
this.cellPadding,
});
}
abstract class GridCellDelegate {
@ -71,7 +78,10 @@ class _DateCellState extends GridCellState<GridDateCell> {
margin: EdgeInsets.zero,
child: GridDateCellText(
dateStr: state.dateStr,
placeholder: widget.cellStyle?.placeholder ?? "",
alignment: alignment,
cellPadding:
widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
),
popupBuilder: (BuildContext popoverContent) {
return DateCellEditor(
@ -107,24 +117,31 @@ class _DateCellState extends GridCellState<GridDateCell> {
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) {
return SizedBox.expand(
child: Align(
alignment: alignment,
child: Padding(
padding: GridSize.cellContentInsets,
child: FlowyText.medium(
dateStr,
maxLines: null,
),
final isPlaceholder = dateStr.isEmpty;
final text = isPlaceholder ? placeholder : dateStr;
return Align(
alignment: alignment,
child: Padding(
padding: cellPadding,
child: FlowyText.medium(
text,
color: isPlaceholder
? Theme.of(context).hintColor
: AFThemeExtension.of(context).textColor,
maxLines: null,
),
),
);

View File

@ -7,13 +7,33 @@ import 'number_cell_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
class GridNumberCellStyle extends GridCellStyle {
String? placeholder;
TextStyle? textStyle;
EdgeInsets? cellPadding;
GridNumberCellStyle({
this.placeholder,
this.textStyle,
this.cellPadding,
});
}
class GridNumberCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final GridNumberCellStyle cellStyle;
GridNumberCell({
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
required GridCellStyle? style,
super.key,
}) {
if (style != null) {
cellStyle = (style as GridNumberCellStyle);
} else {
cellStyle = GridNumberCellStyle();
}
}
@override
GridEditableTextCell<GridNumberCell> createState() => _NumberCellState();
@ -57,9 +77,10 @@ class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
hintText: widget.cellStyle.placeholder,
isDense: true,
),
),

View File

@ -93,7 +93,7 @@ class SelectOptionTag extends StatelessWidget {
@override
Widget build(BuildContext context) {
EdgeInsets padding =
const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0);
const EdgeInsets.symmetric(vertical: 1.5, horizontal: 8.0);
if (onRemove != null) {
padding = padding.copyWith(right: 2.0);
}
@ -110,6 +110,7 @@ class SelectOptionTag extends StatelessWidget {
Flexible(
child: FlowyText.medium(
name,
fontSize: FontSizes.s11,
overflow: TextOverflow.ellipsis,
color: AFThemeExtension.of(context).textColor,
),

View File

@ -13,9 +13,11 @@ import 'select_option_editor.dart';
class SelectOptionCellStyle extends GridCellStyle {
String placeholder;
EdgeInsets? cellPadding;
SelectOptionCellStyle({
required this.placeholder,
this.cellPadding,
});
}
@ -170,10 +172,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
final Widget child = _buildOptions(context);
final constraints = BoxConstraints.loose(
Size(
SelectOptionCellEditor.editorPanelWidth,
300,
),
Size(SelectOptionCellEditor.editorPanelWidth, 300),
);
return AppFlowyPopover(
controller: widget.popoverController,
@ -191,7 +190,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
},
onClose: () => widget.onCellEditing.value = false,
child: Padding(
padding: GridSize.cellContentInsets,
padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
child: child,
),
);
@ -200,9 +199,12 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
Widget _buildOptions(BuildContext context) {
final Widget child;
if (widget.selectOptions.isEmpty && widget.cellStyle != null) {
child = FlowyText.medium(
widget.cellStyle!.placeholder,
color: Theme.of(context).hintColor,
child = Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: FlowyText.medium(
widget.cellStyle!.placeholder,
color: Theme.of(context).hintColor,
),
);
} else {
final children = widget.selectOptions.map(

View File

@ -14,6 +14,7 @@ class GridTextCellStyle extends GridCellStyle {
double emojiFontSize;
double emojiHPadding;
bool showEmoji;
EdgeInsets? cellPadding;
GridTextCellStyle({
this.placeholder,
@ -22,6 +23,7 @@ class GridTextCellStyle extends GridCellStyle {
this.showEmoji = true,
this.emojiFontSize = 16,
this.emojiHPadding = 0,
this.cellPadding,
});
}
@ -72,10 +74,11 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
}
},
child: Padding(
padding: EdgeInsets.only(
left: GridSize.cellContentInsets.left,
right: GridSize.cellContentInsets.right,
),
padding: widget.cellStyle.cellPadding ??
EdgeInsets.only(
left: GridSize.cellContentInsets.left,
right: GridSize.cellContentInsets.right,
),
child: Row(
children: [
if (widget.cellStyle.showEmoji)

View File

@ -3,14 +3,21 @@ 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:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class TimestampCellStyle extends GridCellStyle {
String? placeholder;
Alignment alignment;
EdgeInsets? cellPadding;
TimestampCellStyle({this.alignment = Alignment.center});
TimestampCellStyle({
this.placeholder,
this.alignment = Alignment.center,
this.cellPadding,
});
}
class GridTimestampCell extends GridCellWidget {
@ -51,16 +58,28 @@ class _TimestampCellState extends GridCellState<GridTimestampCell> {
@override
Widget build(BuildContext context) {
final alignment = widget.cellStyle != null
? widget.cellStyle!.alignment
: Alignment.centerLeft;
final alignment = widget.cellStyle?.alignment ?? Alignment.centerLeft;
final placeholder = widget.cellStyle?.placeholder ?? "";
final padding = widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets;
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
builder: (context, state) {
return GridTimestampCellText(
dateStr: state.dateStr,
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,
),
),
);
},
),
@ -81,29 +100,3 @@ class _TimestampCellState extends GridCellState<GridTimestampCell> {
return;
}
}
class GridTimestampCellText extends StatelessWidget {
final String dateStr;
final Alignment alignment;
const GridTimestampCellText({
required this.dateStr,
required this.alignment,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Align(
alignment: alignment,
child: Padding(
padding: GridSize.cellContentInsets,
child: FlowyText.medium(
dateStr,
maxLines: null,
),
),
),
);
}
}

View File

@ -1,18 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.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';
@ -20,36 +12,39 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class RowActionList extends StatelessWidget {
final RowController rowController;
const RowActionList({
required String viewId,
required this.rowController,
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 10),
child: FlowyText(LocaleKeys.grid_row_action.tr()),
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),
],
),
const VSpace(15),
RowDetailPageDeleteButton(rowId: rowController.rowId),
RowDetailPageDuplicateButton(
rowId: rowController.rowId,
groupId: rowController.groupId,
),
],
),
);
}
}
class RowDetailPageDeleteButton extends StatelessWidget {
final String rowId;
const RowDetailPageDeleteButton({required this.rowId, Key? key})
: super(key: key);
const RowDetailPageDeleteButton({required this.rowId, super.key});
@override
Widget build(BuildContext context) {
@ -59,7 +54,9 @@ class RowDetailPageDeleteButton extends StatelessWidget {
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
leftIcon: const FlowySvg(FlowySvgs.trash_m),
onTap: () {
context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
context
.read<RowActionSheetBloc>()
.add(const RowActionSheetEvent.deleteRow());
FlowyOverlay.pop(context);
},
),
@ -73,8 +70,8 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
const RowDetailPageDuplicateButton({
required this.rowId,
this.groupId,
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -85,91 +82,11 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
leftIcon: const FlowySvg(FlowySvgs.copy_s),
onTap: () {
context
.read<RowDetailBloc>()
.add(RowDetailEvent.duplicateRow(rowId, groupId));
.read<RowActionSheetBloc>()
.add(const RowActionSheetEvent.duplicateRow());
FlowyOverlay.pop(context);
},
),
);
}
}
class CreateRowFieldButton extends StatefulWidget {
final String viewId;
const CreateRowFieldButton({
required this.viewId,
Key? key,
}) : super(key: key);
@override
State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
}
class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
late PopoverController popoverController;
late TypeOptionPB typeOption;
@override
void initState() {
popoverController = PopoverController();
super.initState();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(240, 200)),
controller: popoverController,
direction: PopoverDirection.topWithLeftAligned,
triggerActions: PopoverTriggerFlags.none,
margin: EdgeInsets.zero,
child: SizedBox(
height: 40,
child: FlowyButton(
text: FlowyText.medium(
LocaleKeys.grid_field_newProperty.tr(),
color: AFThemeExtension.of(context).textColor,
),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onTap: () async {
final result = await TypeOptionBackendService.createFieldTypeOption(
viewId: widget.viewId,
);
result.fold(
(l) {
typeOption = l;
popoverController.show();
},
(r) => Log.error("Failed to create field type option: $r"),
);
},
leftIcon: FlowySvg(
FlowySvgs.add_m,
color: AFThemeExtension.of(context).textColor,
),
),
),
popupBuilder: (BuildContext popOverContext) {
return FieldEditor(
viewId: widget.viewId,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: typeOption.field_2,
),
onDeleted: (fieldId) {
popoverController.close();
NavigatorAlertDialog(
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () {
context
.read<RowDetailBloc>()
.add(RowDetailEvent.deleteField(fieldId));
},
).show(context);
},
);
},
);
}
}

View File

@ -1,23 +1,27 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.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_controller.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.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';
typedef RowBannerCellBuilder = Widget Function(String fieldId);
import 'cell_builder.dart';
import 'cells/cells.dart';
class RowBanner extends StatefulWidget {
final String viewId;
final RowMetaPB rowMeta;
final RowBannerCellBuilder cellBuilder;
final RowController rowController;
final GridCellBuilder cellBuilder;
const RowBanner({
required this.viewId,
required this.rowMeta,
required this.rowController,
required this.cellBuilder,
super.key,
});
@ -34,25 +38,39 @@ class _RowBannerState extends State<RowBanner> {
Widget build(BuildContext context) {
return BlocProvider<RowBannerBloc>(
create: (context) => RowBannerBloc(
viewId: widget.viewId,
rowMeta: widget.rowMeta,
viewId: widget.rowController.viewId,
rowMeta: widget.rowController.rowMeta,
)..add(const RowBannerEvent.initial()),
child: MouseRegion(
onEnter: (event) => _isHovering.value = true,
onExit: (event) => _isHovering.value = false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Stack(
children: [
SizedBox(
height: 30,
child: _BannerAction(
isHovering: _isHovering,
popoverController: popoverController,
Padding(
padding: const EdgeInsets.fromLTRB(60, 34, 60, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 30,
child: _BannerAction(
isHovering: _isHovering,
popoverController: popoverController,
),
),
const HSpace(4),
_BannerTitle(
cellBuilder: widget.cellBuilder,
popoverController: popoverController,
rowController: widget.rowController,
),
],
),
),
_BannerTitle(
cellBuilder: widget.cellBuilder,
popoverController: popoverController,
Positioned(
top: 12,
right: 12,
child: RowActionButton(rowController: widget.rowController),
),
],
),
@ -74,50 +92,54 @@ class _BannerAction extends StatelessWidget {
return ValueListenableBuilder(
valueListenable: isHovering,
builder: (BuildContext context, bool value, Widget? child) {
if (value) {
return BlocBuilder<RowBannerBloc, RowBannerState>(
builder: (context, state) {
final children = <Widget>[];
final rowMeta = state.rowMeta;
if (rowMeta.icon.isEmpty) {
children.add(
EmojiPickerButton(
showEmojiPicker: () => popoverController.show(),
),
);
} else {
children.add(
RemoveEmojiButton(
onRemoved: () {
context
.read<RowBannerBloc>()
.add(const RowBannerEvent.setIcon(''));
},
),
);
}
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
},
);
} else {
if (!value) {
return const SizedBox(height: _kBannerActionHeight);
}
return BlocBuilder<RowBannerBloc, RowBannerState>(
builder: (context, state) {
final children = <Widget>[];
final rowMeta = state.rowMeta;
if (rowMeta.icon.isEmpty) {
children.add(
EmojiPickerButton(
showEmojiPicker: () => popoverController.show(),
),
);
} else {
children.add(
RemoveEmojiButton(
onRemoved: () {
context
.read<RowBannerBloc>()
.add(const RowBannerEvent.setIcon(''));
},
),
);
}
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
},
);
},
);
}
}
class _BannerTitle extends StatefulWidget {
final RowBannerCellBuilder cellBuilder;
final GridCellBuilder cellBuilder;
final PopoverController popoverController;
final RowController rowController;
const _BannerTitle({
required this.cellBuilder,
required this.popoverController,
});
required this.rowController,
Key? key,
}) : super(key: key);
@override
State<_BannerTitle> createState() => _BannerTitleState();
@ -139,10 +161,24 @@ class _BannerTitleState extends State<_BannerTitle> {
);
}
children.add(const HSpace(4));
if (state.primaryField != null) {
final style = GridTextCellStyle(
placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(),
textStyle: Theme.of(context).textTheme.titleLarge,
showEmoji: false,
autofocus: true,
cellPadding: EdgeInsets.zero,
);
final cellContext = DatabaseCellContext(
viewId: widget.rowController.viewId,
rowMeta: widget.rowController.rowMeta,
fieldInfo: FieldInfo.initial(state.primaryField!),
);
children.add(
Expanded(
child: widget.cellBuilder(state.primaryField!.id),
child: widget.cellBuilder.build(cellContext, style: style),
),
);
}
@ -211,16 +247,14 @@ class _EmojiPickerButtonState extends State<EmojiPickerButton> {
Widget build(BuildContext context) {
return SizedBox(
height: 26,
width: 160,
child: FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.medium(
LocaleKeys.document_plugins_cover_addIcon.tr(),
),
leftIcon: const Icon(
Icons.emoji_emotions,
size: 16,
),
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
onTap: widget.showEmojiPicker,
margin: const EdgeInsets.all(4),
),
);
}
@ -239,16 +273,14 @@ class RemoveEmojiButton extends StatelessWidget {
Widget build(BuildContext context) {
return SizedBox(
height: 26,
width: 160,
child: FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.medium(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
leftIcon: const Icon(
Icons.emoji_emotions,
size: 16,
),
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
onTap: onRemoved,
margin: const EdgeInsets.all(4),
),
);
}
@ -263,3 +295,22 @@ Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) {
),
);
}
class RowActionButton extends StatelessWidget {
final RowController rowController;
const RowActionButton({super.key, required this.rowController});
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (context) => RowActionList(rowController: rowController),
child: FlowyIconButton(
width: 20,
height: 20,
icon: const FlowySvg(FlowySvgs.details_horizontal_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
),
);
}
}

View File

@ -1,18 +1,12 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_document.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';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'cell_builder.dart';
import 'cells/text_cell/text_cell.dart';
import 'row_action.dart';
import 'row_banner.dart';
import 'row_property.dart';
@ -47,17 +41,29 @@ class _RowDetailPageState extends State<RowDetailPage> {
Widget build(BuildContext context) {
return FlowyDialog(
child: BlocProvider(
create: (context) {
return RowDetailBloc(rowController: widget.rowController)
..add(const RowDetailEvent.initial());
},
create: (context) => RowDetailBloc(rowController: widget.rowController)
..add(const RowDetailEvent.initial()),
child: ListView(
controller: scrollController,
children: [
_rowBanner(),
IntrinsicHeight(child: _responsiveRowInfo()),
const Divider(height: 1.0),
const VSpace(10),
RowBanner(
rowController: widget.rowController,
cellBuilder: widget.cellBuilder,
),
const VSpace(16),
Padding(
padding: const EdgeInsets.only(left: 40, right: 60),
child: RowPropertyList(
cellBuilder: widget.cellBuilder,
viewId: widget.rowController.viewId,
),
),
const VSpace(20),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 60),
child: Divider(height: 1.0),
),
const VSpace(20),
RowDocument(
viewId: widget.rowController.viewId,
rowId: widget.rowController.rowId,
@ -68,104 +74,4 @@ class _RowDetailPageState extends State<RowDetailPage> {
),
);
}
Widget _rowBanner() {
return BlocBuilder<RowDetailBloc, RowDetailState>(
builder: (context, state) {
final paddingOffset = getHorizontalPadding(context);
return Padding(
padding: EdgeInsets.only(
left: paddingOffset,
right: paddingOffset,
top: 20,
),
child: RowBanner(
rowMeta: widget.rowController.rowMeta,
viewId: widget.rowController.viewId,
cellBuilder: (fieldId) {
final fieldInfo = state.cells
.firstWhereOrNull(
(e) => e.fieldInfo.field.id == fieldId,
)
?.fieldInfo;
if (fieldInfo != null) {
final style = GridTextCellStyle(
placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(),
textStyle: Theme.of(context).textTheme.titleLarge,
showEmoji: false,
autofocus: true,
);
final cellContext = DatabaseCellContext(
viewId: widget.rowController.viewId,
rowMeta: widget.rowController.rowMeta,
fieldInfo: fieldInfo,
);
return widget.cellBuilder.build(cellContext, style: style);
} else {
return const SizedBox.shrink();
}
},
),
);
},
);
}
Widget _responsiveRowInfo() {
final rowDataColumn = RowPropertyList(
cellBuilder: widget.cellBuilder,
viewId: widget.rowController.viewId,
);
final rowOptionColumn = RowActionList(
viewId: widget.rowController.viewId,
rowController: widget.rowController,
);
final paddingOffset = getHorizontalPadding(context);
if (MediaQuery.of(context).size.width > 800) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 3,
child: Padding(
padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
child: rowDataColumn,
),
),
const VerticalDivider(width: 1.0),
Flexible(
child: Padding(
padding: EdgeInsets.fromLTRB(20, 0, paddingOffset, 0),
child: rowOptionColumn,
),
),
],
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
child: rowDataColumn,
),
const Divider(height: 1.0),
Padding(
padding: EdgeInsets.symmetric(horizontal: paddingOffset),
child: rowOptionColumn,
)
],
);
}
}
}
double getHorizontalPadding(BuildContext context) {
if (MediaQuery.of(context).size.width > 800) {
return 50;
} else {
return 20;
}
}

View File

@ -1,21 +1,27 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.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_cell.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.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';
import 'accessory/cell_accessory.dart';
import 'cell_builder.dart';
import 'cells/checkbox_cell/checkbox_cell.dart';
import 'cells/date_cell/date_cell.dart';
import 'cells/number_cell/number_cell.dart';
import 'cells/select_option_cell/select_option_cell.dart';
import 'cells/text_cell/text_cell.dart';
import 'cells/timestamp_cell/timestamp_cell.dart';
@ -23,7 +29,6 @@ import 'cells/url_cell/url_cell.dart';
/// Display the row properties in a list. Only use this widget in the
/// [RowDetailPage].
///
class RowPropertyList extends StatelessWidget {
final String viewId;
final GridCellBuilder cellBuilder;
@ -38,39 +43,88 @@ class RowPropertyList extends StatelessWidget {
return BlocBuilder<RowDetailBloc, RowDetailState>(
buildWhen: (previous, current) => previous.cells != current.cells,
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// The rest of the fields are displayed in the order of the field
// list
...state.cells
.where((element) => !element.fieldInfo.field.isPrimary)
.map(
(cell) => _PropertyCell(
cellContext: cell,
cellBuilder: cellBuilder,
),
)
.toList(),
const VSpace(20),
// Create a new property(field) button
CreateRowFieldButton(viewId: viewId),
],
final children = state.cells
.where((element) => !element.fieldInfo.field.isPrimary)
.mapIndexed(
(index, cell) => _PropertyCell(
key: ValueKey('row_detail_${cell.fieldId}'),
cellContext: cell,
cellBuilder: cellBuilder,
index: index,
),
)
.toList();
return ReorderableListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
onReorder: (oldIndex, newIndex) {
final reorderedField = children[oldIndex].cellContext.fieldId;
_reorderField(
context,
state.cells,
reorderedField,
oldIndex,
newIndex,
);
},
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.transparent,
child: Stack(
children: [
child,
const MouseRegion(cursor: SystemMouseCursors.grabbing),
],
),
),
footer: Padding(
padding: const EdgeInsets.only(left: 20),
child: CreateRowFieldButton(viewId: viewId),
),
children: children,
);
},
);
}
void _reorderField(
BuildContext context,
List<DatabaseCellContext> cells,
String reorderedFieldId,
int oldIndex,
int newIndex,
) {
// when reorderiing downwards, need to update index
if (oldIndex < newIndex) {
newIndex--;
}
// also update index when the index is after the index of the primary field
// in the original list of DatabaseCellContext's
final primaryFieldIndex =
cells.indexWhere((element) => element.fieldInfo.isPrimary);
if (oldIndex >= primaryFieldIndex) {
oldIndex++;
}
if (newIndex >= primaryFieldIndex) {
newIndex++;
}
context.read<RowDetailBloc>().add(
RowDetailEvent.reorderField(reorderedFieldId, oldIndex, newIndex),
);
}
}
class _PropertyCell extends StatefulWidget {
final DatabaseCellContext cellContext;
final GridCellBuilder cellBuilder;
final int index;
const _PropertyCell({
required this.cellContext,
required this.cellBuilder,
Key? key,
required this.index,
}) : super(key: key);
@override
@ -78,45 +132,65 @@ class _PropertyCell extends StatefulWidget {
}
class _PropertyCellState extends State<_PropertyCell> {
final PopoverController popover = PopoverController();
final PopoverController _popoverController = PopoverController();
bool _isFieldHover = false;
@override
Widget build(BuildContext context) {
final style = _customCellStyle(widget.cellContext.fieldType);
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
final gesture = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
child: cell,
final dragThumb = MouseRegion(
cursor: SystemMouseCursors.grab,
child: SizedBox(
width: 16,
height: 30,
child: _isFieldHover ? const FlowySvg(FlowySvgs.drag_element_s) : null,
),
);
return IntrinsicHeight(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 30),
final gesture = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(child: cell),
);
return Container(
margin: const EdgeInsets.only(bottom: 8),
constraints: const BoxConstraints(minHeight: 30),
child: MouseRegion(
onEnter: (event) => setState(() => _isFieldHover = true),
onExit: (event) => setState(() => _isFieldHover = false),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
ReorderableDragStartListener(
index: widget.index,
enabled: _isFieldHover,
child: dragThumb,
),
const HSpace(4),
AppFlowyPopover(
controller: popover,
controller: _popoverController,
constraints: BoxConstraints.loose(const Size(240, 600)),
margin: EdgeInsets.zero,
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (popoverContext) => buildFieldEditor(),
child: SizedBox(
width: 150,
height: 40,
width: 160,
height: 30,
child: FieldCellButton(
field: widget.cellContext.fieldInfo.field,
onTap: () => popover.show(),
onTap: () => _popoverController.show(),
radius: BorderRadius.circular(6),
margin:
const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
),
),
),
const HSpace(8),
Expanded(child: gesture),
],
),
@ -133,11 +207,11 @@ class _PropertyCellState extends State<_PropertyCell> {
field: widget.cellContext.fieldInfo.field,
),
onHidden: (fieldId) {
popover.close();
_popoverController.close();
context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
},
onDeleted: (fieldId) {
popover.close();
_popoverController.close();
NavigatorAlertDialog(
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
@ -155,26 +229,35 @@ class _PropertyCellState extends State<_PropertyCell> {
GridCellStyle? _customCellStyle(FieldType fieldType) {
switch (fieldType) {
case FieldType.Checkbox:
return null;
return GridCheckboxCellStyle(
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.DateTime:
return DateCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
);
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),
);
case FieldType.MultiSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.Checklist:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
case FieldType.Number:
return null;
return GridNumberCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
case FieldType.RichText:
return GridTextCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
@ -182,6 +265,7 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
case FieldType.SingleSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.URL:
@ -195,3 +279,81 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
}
throw UnimplementedError;
}
class CreateRowFieldButton extends StatefulWidget {
final String viewId;
const CreateRowFieldButton({required this.viewId, super.key});
@override
State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
}
class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
late PopoverController popoverController;
late TypeOptionPB typeOption;
@override
void initState() {
popoverController = PopoverController();
super.initState();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(240, 200)),
controller: popoverController,
direction: PopoverDirection.topWithLeftAligned,
triggerActions: PopoverTriggerFlags.none,
margin: EdgeInsets.zero,
child: SizedBox(
height: 30,
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
text: FlowyText.medium(
LocaleKeys.grid_field_newProperty.tr(),
color: Theme.of(context).hintColor,
),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onTap: () async {
final result = await TypeOptionBackendService.createFieldTypeOption(
viewId: widget.viewId,
);
result.fold(
(l) {
typeOption = l;
popoverController.show();
},
(r) => Log.error("Failed to create field type option: $r"),
);
},
leftIcon: FlowySvg(
FlowySvgs.add_m,
color: Theme.of(context).hintColor,
),
),
),
popupBuilder: (BuildContext popOverContext) {
return FieldEditor(
viewId: widget.viewId,
typeOptionLoader: FieldTypeOptionLoader(
viewId: widget.viewId,
field: typeOption.field_2,
),
onDeleted: (fieldId) {
popoverController.close();
NavigatorAlertDialog(
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () {
context
.read<RowDetailBloc>()
.add(RowDetailEvent.deleteField(fieldId));
},
).show(context);
},
);
},
);
}
}

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="4" cy="8" r="1" fill="#333333"/>
<circle cx="8" cy="8" r="1" fill="#333333"/>
<circle cx="12" cy="8" r="1" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 239 B

View File

@ -0,0 +1,13 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_160_12007)">
<path d="M8 12.998C10.7614 12.998 13 10.7595 13 7.99805C13 5.23662 10.7614 2.99805 8 2.99805C5.23857 2.99805 3 5.23662 3 7.99805C3 10.7595 5.23857 12.998 8 12.998Z" stroke="#333333" stroke-linejoin="round"/>
<path d="M9.75 9.74805C9.75 9.74805 9.25 10.748 8 10.748C6.75 10.748 6.25 9.74805 6.25 9.74805" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.25 6.99805H9.25" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.25 6.49805V7.49805" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_160_12007">
<rect width="12" height="12" fill="white" transform="translate(2 1.99805)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 846 B