mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
ef6f9a3175
commit
1ca130d7de
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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})
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 |
13
frontend/resources/flowy_icons/16x/emoji.svg
Normal file
13
frontend/resources/flowy_icons/16x/emoji.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user