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() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
group('calendar database view', () {
|
group('calendar', () {
|
||||||
testWidgets('update calendar layout', (tester) async {
|
testWidgets('update calendar layout', (tester) async {
|
||||||
await tester.initializeAppFlowy();
|
await tester.initializeAppFlowy();
|
||||||
await tester.tapGoButton();
|
await tester.tapGoButton();
|
||||||
@ -116,6 +116,7 @@ void main() {
|
|||||||
tester.assertRowDetailPageOpened();
|
tester.assertRowDetailPageOpened();
|
||||||
|
|
||||||
// Duplicate the event
|
// Duplicate the event
|
||||||
|
await tester.tapRowDetailPageRowActionButton();
|
||||||
await tester.tapRowDetailPageDuplicateRowButton();
|
await tester.tapRowDetailPageDuplicateRowButton();
|
||||||
await tester.dismissRowDetailPage();
|
await tester.dismissRowDetailPage();
|
||||||
|
|
||||||
@ -125,6 +126,7 @@ void main() {
|
|||||||
|
|
||||||
// Delete an event
|
// Delete an event
|
||||||
await tester.openCalendarEvent(index: 1);
|
await tester.openCalendarEvent(index: 1);
|
||||||
|
await tester.tapRowDetailPageRowActionButton();
|
||||||
await tester.tapRowDetailPageDeleteRowButton();
|
await tester.tapRowDetailPageDeleteRowButton();
|
||||||
|
|
||||||
// Check that there is 1 event
|
// Check that there is 1 event
|
||||||
@ -155,6 +157,7 @@ void main() {
|
|||||||
|
|
||||||
// Delete the event
|
// Delete the event
|
||||||
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
|
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
|
||||||
|
await tester.tapRowDetailPageRowActionButton();
|
||||||
await tester.tapRowDetailPageDeleteRowButton();
|
await tester.tapRowDetailPageDeleteRowButton();
|
||||||
|
|
||||||
// Create a new event in today's calendar cell
|
// 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.initializeAppFlowy();
|
||||||
await tester.tapGoButton();
|
await tester.tapGoButton();
|
||||||
|
|
||||||
@ -149,7 +182,7 @@ void main() {
|
|||||||
await tester.assertDocumentExistInRowDetailPage();
|
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 {
|
(tester) async {
|
||||||
await tester.initializeAppFlowy();
|
await tester.initializeAppFlowy();
|
||||||
await tester.tapGoButton();
|
await tester.tapGoButton();
|
||||||
@ -239,6 +272,7 @@ void main() {
|
|||||||
// Hover first row and then open the row page
|
// Hover first row and then open the row page
|
||||||
await tester.openFirstRowDetailPage();
|
await tester.openFirstRowDetailPage();
|
||||||
|
|
||||||
|
await tester.tapRowDetailPageRowActionButton();
|
||||||
await tester.tapRowDetailPageDeleteRowButton();
|
await tester.tapRowDetailPageDeleteRowButton();
|
||||||
await tester.tapEscButton();
|
await tester.tapEscButton();
|
||||||
|
|
||||||
@ -255,6 +289,7 @@ void main() {
|
|||||||
// Hover first row and then open the row page
|
// Hover first row and then open the row page
|
||||||
await tester.openFirstRowDetailPage();
|
await tester.openFirstRowDetailPage();
|
||||||
|
|
||||||
|
await tester.tapRowDetailPageRowActionButton();
|
||||||
await tester.tapRowDetailPageDuplicateRowButton();
|
await tester.tapRowDetailPageDuplicateRowButton();
|
||||||
await tester.tapEscButton();
|
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_banner.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.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_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/database_setting.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.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';
|
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);
|
expect(banner, findsOneWidget);
|
||||||
|
|
||||||
await startGesture(
|
await startGesture(
|
||||||
getTopLeft(banner),
|
getCenter(banner),
|
||||||
kind: PointerDeviceKind.mouse,
|
kind: PointerDeviceKind.mouse,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -524,6 +525,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await tapButton(deleteButton);
|
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 {
|
Future<void> scrollGridByOffset(Offset offset) async {
|
||||||
await drag(find.byType(GridPage), offset);
|
await drag(find.byType(GridPage), offset);
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
@ -601,6 +627,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await tapButton(button);
|
await tapButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> tapRowDetailPageRowActionButton() async {
|
||||||
|
await tapButton(find.byType(RowActionButton));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> tapRowDetailPageCreatePropertyButton() async {
|
Future<void> tapRowDetailPageCreatePropertyButton() async {
|
||||||
await tapButton(find.byType(CreateRowFieldButton));
|
await tapButton(find.byType(CreateRowFieldButton));
|
||||||
}
|
}
|
||||||
@ -670,6 +700,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
expect(field, findsOneWidget);
|
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 {
|
Future<void> findFieldWithName(String name) async {
|
||||||
final field = find.byWidgetPredicate(
|
final field = find.byWidgetPredicate(
|
||||||
(widget) => widget is FieldCellButton && widget.field.name == name,
|
(widget) => widget is FieldCellButton && widget.field.name == name,
|
||||||
|
@ -395,13 +395,7 @@ class RowDataBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void insertDate(FieldInfo fieldInfo, DateTime date) {
|
void insertDate(FieldInfo fieldInfo, DateTime date) {
|
||||||
assert(
|
assert(FieldType.DateTime == fieldInfo.fieldType);
|
||||||
[
|
|
||||||
FieldType.DateTime,
|
|
||||||
FieldType.LastEditedTime,
|
|
||||||
FieldType.CreatedTime,
|
|
||||||
].contains(fieldInfo.fieldType),
|
|
||||||
);
|
|
||||||
final timestamp = date.millisecondsSinceEpoch ~/ 1000;
|
final timestamp = date.millisecondsSinceEpoch ~/ 1000;
|
||||||
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
|
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
|
||||||
}
|
}
|
||||||
|
@ -32,11 +32,7 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
|
|||||||
await _listenRowMeteChanged();
|
await _listenRowMeteChanged();
|
||||||
},
|
},
|
||||||
didReceiveRowMeta: (RowMetaPB rowMeta) {
|
didReceiveRowMeta: (RowMetaPB rowMeta) {
|
||||||
emit(
|
emit(state.copyWith(rowMeta: rowMeta));
|
||||||
state.copyWith(
|
|
||||||
rowMeta: rowMeta,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
setCover: (String coverURL) {
|
setCover: (String coverURL) {
|
||||||
_updateMeta(coverURL: 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/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/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.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';
|
part 'row_detail_bloc.freezed.dart';
|
||||||
|
|
||||||
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||||
final RowBackendService rowService;
|
|
||||||
final RowController rowController;
|
final RowController rowController;
|
||||||
|
|
||||||
RowDetailBloc({
|
RowDetailBloc({
|
||||||
required this.rowController,
|
required this.rowController,
|
||||||
}) : rowService = RowBackendService(viewId: rowController.viewId),
|
}) : super(RowDetailState.initial()) {
|
||||||
super(RowDetailState.initial()) {
|
|
||||||
on<RowDetailEvent>(
|
on<RowDetailEvent>(
|
||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
@ -58,14 +55,8 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
|||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
deleteRow: (rowId) async {
|
reorderField: (fieldId, fromIndex, toIndex) async {
|
||||||
await rowService.deleteRow(rowId);
|
await _reorderField(fieldId, fromIndex, toIndex, emit);
|
||||||
},
|
|
||||||
duplicateRow: (String rowId, String? groupId) async {
|
|
||||||
await rowService.duplicateRow(
|
|
||||||
rowId: rowId,
|
|
||||||
groupId: groupId,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -94,6 +85,25 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
|||||||
fieldId: fieldId,
|
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
|
@freezed
|
||||||
@ -102,9 +112,11 @@ class RowDetailEvent with _$RowDetailEvent {
|
|||||||
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
|
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
|
||||||
const factory RowDetailEvent.showField(String fieldId) = _ShowField;
|
const factory RowDetailEvent.showField(String fieldId) = _ShowField;
|
||||||
const factory RowDetailEvent.hideField(String fieldId) = _HideField;
|
const factory RowDetailEvent.hideField(String fieldId) = _HideField;
|
||||||
const factory RowDetailEvent.deleteRow(String rowId) = _DeleteRow;
|
const factory RowDetailEvent.reorderField(
|
||||||
const factory RowDetailEvent.duplicateRow(String rowId, String? groupId) =
|
String fieldId,
|
||||||
_DuplicateRow;
|
int fromIndex,
|
||||||
|
int toIndex,
|
||||||
|
) = _ReorderField;
|
||||||
const factory RowDetailEvent.didReceiveCellDatas(
|
const factory RowDetailEvent.didReceiveCellDatas(
|
||||||
List<DatabaseCellContext> gridCells,
|
List<DatabaseCellContext> gridCells,
|
||||||
) = _DidReceiveCellDatas;
|
) = _DidReceiveCellDatas;
|
||||||
|
@ -154,29 +154,33 @@ class FieldCellButton extends StatelessWidget {
|
|||||||
final FieldPB field;
|
final FieldPB field;
|
||||||
final int? maxLines;
|
final int? maxLines;
|
||||||
final BorderRadius? radius;
|
final BorderRadius? radius;
|
||||||
|
final EdgeInsets? margin;
|
||||||
const FieldCellButton({
|
const FieldCellButton({
|
||||||
required this.field,
|
required this.field,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.maxLines = 1,
|
this.maxLines = 1,
|
||||||
this.radius = BorderRadius.zero,
|
this.radius = BorderRadius.zero,
|
||||||
|
this.margin,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyButton(
|
return FlowyButton(
|
||||||
hoverColor: AFThemeExtension.of(context).greyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
leftIcon: FlowySvg(
|
leftIcon: FlowySvg(
|
||||||
field.fieldType.icon(),
|
field.fieldType.icon(),
|
||||||
|
color: Theme.of(context).iconTheme.color,
|
||||||
),
|
),
|
||||||
radius: radius,
|
radius: radius,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
field.name,
|
field.name,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
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))
|
.map((action) => _ActionCell(action: action))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
//
|
return ListView.separated(
|
||||||
final list = ListView.separated(
|
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
controller: ScrollController(),
|
controller: ScrollController(),
|
||||||
itemCount: cells.length,
|
itemCount: cells.length,
|
||||||
@ -53,7 +52,6 @@ class RowActions extends StatelessWidget {
|
|||||||
return cells[index];
|
return cells[index];
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return list;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -70,6 +68,7 @@ class _ActionCell extends StatelessWidget {
|
|||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
useIntrinsicWidth: true,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
action.title(),
|
action.title(),
|
||||||
color: action.enable()
|
color: action.enable()
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
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/theme_extension.dart';
|
||||||
|
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -91,50 +91,31 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
|
|||||||
|
|
||||||
class AccessoryHover extends StatefulWidget {
|
class AccessoryHover extends StatefulWidget {
|
||||||
final CellAccessory child;
|
final CellAccessory child;
|
||||||
final EdgeInsets contentPadding;
|
const AccessoryHover({required this.child, super.key});
|
||||||
const AccessoryHover({
|
|
||||||
required this.child,
|
|
||||||
this.contentPadding = EdgeInsets.zero,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AccessoryHover> createState() => _AccessoryHoverState();
|
State<AccessoryHover> createState() => _AccessoryHoverState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AccessoryHoverState extends State<AccessoryHover> {
|
class _AccessoryHoverState extends State<AccessoryHover> {
|
||||||
late AccessoryHoverState _hoverState;
|
bool _isHover = false;
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<Widget> children = [
|
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;
|
final accessoryBuilder = widget.child.accessoryBuilder;
|
||||||
if (accessoryBuilder != null) {
|
if (accessoryBuilder != null && _isHover) {
|
||||||
final accessories = accessoryBuilder(
|
final accessories = accessoryBuilder(
|
||||||
(GridCellAccessoryBuildContext(
|
(GridCellAccessoryBuildContext(
|
||||||
anchorContext: context,
|
anchorContext: context,
|
||||||
@ -149,36 +130,20 @@ class _AccessoryHoverState extends State<AccessoryHover> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChangeNotifierProvider.value(
|
return MouseRegion(
|
||||||
value: _hoverState,
|
|
||||||
child: MouseRegion(
|
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
opaque: false,
|
opaque: false,
|
||||||
onEnter: (p) => setState(() => _hoverState.onHover = true),
|
onEnter: (p) => setState(() => _isHover = true),
|
||||||
onExit: (p) => setState(() => _hoverState.onHover = false),
|
onExit: (p) => setState(() => _isHover = false),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.loose,
|
fit: StackFit.loose,
|
||||||
alignment: AlignmentDirectional.center,
|
alignment: AlignmentDirectional.center,
|
||||||
children: children,
|
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 {
|
class CellAccessoryContainer extends StatelessWidget {
|
||||||
final List<GridCellAccessoryBuilder> accessories;
|
final List<GridCellAccessoryBuilder> accessories;
|
||||||
const CellAccessoryContainer({required this.accessories, Key? key})
|
const CellAccessoryContainer({required this.accessories, Key? key})
|
||||||
|
@ -35,6 +35,7 @@ class GridCellBuilder {
|
|||||||
case FieldType.Checkbox:
|
case FieldType.Checkbox:
|
||||||
return GridCheckboxCell(
|
return GridCheckboxCell(
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
style: style,
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
case FieldType.DateTime:
|
case FieldType.DateTime:
|
||||||
@ -71,6 +72,7 @@ class GridCellBuilder {
|
|||||||
case FieldType.Number:
|
case FieldType.Number:
|
||||||
return GridNumberCell(
|
return GridNumberCell(
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
style: style,
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
case FieldType.RichText:
|
case FieldType.RichText:
|
||||||
|
@ -9,12 +9,29 @@ import 'checkbox_cell_bloc.dart';
|
|||||||
import '../../../../grid/presentation/layout/sizes.dart';
|
import '../../../../grid/presentation/layout/sizes.dart';
|
||||||
import '../../cell_builder.dart';
|
import '../../cell_builder.dart';
|
||||||
|
|
||||||
|
class GridCheckboxCellStyle extends GridCellStyle {
|
||||||
|
EdgeInsets? cellPadding;
|
||||||
|
|
||||||
|
GridCheckboxCellStyle({
|
||||||
|
this.cellPadding,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class GridCheckboxCell extends GridCellWidget {
|
class GridCheckboxCell extends GridCellWidget {
|
||||||
final CellControllerBuilder cellControllerBuilder;
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
late final GridCheckboxCellStyle cellStyle;
|
||||||
|
|
||||||
GridCheckboxCell({
|
GridCheckboxCell({
|
||||||
required this.cellControllerBuilder,
|
required this.cellControllerBuilder,
|
||||||
|
GridCellStyle? style,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key) {
|
||||||
|
if (style != null) {
|
||||||
|
cellStyle = (style as GridCheckboxCellStyle);
|
||||||
|
} else {
|
||||||
|
cellStyle = GridCheckboxCellStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
GridCellState<GridCheckboxCell> createState() => _CheckboxCellState();
|
GridCellState<GridCheckboxCell> createState() => _CheckboxCellState();
|
||||||
@ -46,7 +63,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
|
|||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: GridSize.cellContentInsets,
|
padding:
|
||||||
|
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
|
||||||
child: FlowyIconButton(
|
child: FlowyIconButton(
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
onPressed: () => context
|
onPressed: () => context
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.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: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 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../../grid/presentation/layout/sizes.dart';
|
import '../../../../grid/presentation/layout/sizes.dart';
|
||||||
@ -10,9 +11,15 @@ import 'date_cell_bloc.dart';
|
|||||||
import 'date_editor.dart';
|
import 'date_editor.dart';
|
||||||
|
|
||||||
class DateCellStyle extends GridCellStyle {
|
class DateCellStyle extends GridCellStyle {
|
||||||
|
String? placeholder;
|
||||||
Alignment alignment;
|
Alignment alignment;
|
||||||
|
EdgeInsets? cellPadding;
|
||||||
|
|
||||||
DateCellStyle({this.alignment = Alignment.center});
|
DateCellStyle({
|
||||||
|
this.placeholder,
|
||||||
|
this.alignment = Alignment.center,
|
||||||
|
this.cellPadding,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GridCellDelegate {
|
abstract class GridCellDelegate {
|
||||||
@ -71,7 +78,10 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
|||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: GridDateCellText(
|
child: GridDateCellText(
|
||||||
dateStr: state.dateStr,
|
dateStr: state.dateStr,
|
||||||
|
placeholder: widget.cellStyle?.placeholder ?? "",
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
|
cellPadding:
|
||||||
|
widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
|
||||||
),
|
),
|
||||||
popupBuilder: (BuildContext popoverContent) {
|
popupBuilder: (BuildContext popoverContent) {
|
||||||
return DateCellEditor(
|
return DateCellEditor(
|
||||||
@ -107,26 +117,33 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
|||||||
|
|
||||||
class GridDateCellText extends StatelessWidget {
|
class GridDateCellText extends StatelessWidget {
|
||||||
final String dateStr;
|
final String dateStr;
|
||||||
|
final String placeholder;
|
||||||
final Alignment alignment;
|
final Alignment alignment;
|
||||||
|
final EdgeInsets cellPadding;
|
||||||
const GridDateCellText({
|
const GridDateCellText({
|
||||||
required this.dateStr,
|
required this.dateStr,
|
||||||
|
required this.placeholder,
|
||||||
required this.alignment,
|
required this.alignment,
|
||||||
|
required this.cellPadding,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox.expand(
|
final isPlaceholder = dateStr.isEmpty;
|
||||||
child: Align(
|
final text = isPlaceholder ? placeholder : dateStr;
|
||||||
|
return Align(
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: GridSize.cellContentInsets,
|
padding: cellPadding,
|
||||||
child: FlowyText.medium(
|
child: FlowyText.medium(
|
||||||
dateStr,
|
text,
|
||||||
|
color: isPlaceholder
|
||||||
|
? Theme.of(context).hintColor
|
||||||
|
: AFThemeExtension.of(context).textColor,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,33 @@ import 'number_cell_bloc.dart';
|
|||||||
import '../../../../grid/presentation/layout/sizes.dart';
|
import '../../../../grid/presentation/layout/sizes.dart';
|
||||||
import '../../cell_builder.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 {
|
class GridNumberCell extends GridCellWidget {
|
||||||
final CellControllerBuilder cellControllerBuilder;
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
late final GridNumberCellStyle cellStyle;
|
||||||
|
|
||||||
GridNumberCell({
|
GridNumberCell({
|
||||||
required this.cellControllerBuilder,
|
required this.cellControllerBuilder,
|
||||||
Key? key,
|
required GridCellStyle? style,
|
||||||
}) : super(key: key);
|
super.key,
|
||||||
|
}) {
|
||||||
|
if (style != null) {
|
||||||
|
cellStyle = (style as GridNumberCellStyle);
|
||||||
|
} else {
|
||||||
|
cellStyle = GridNumberCellStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
GridEditableTextCell<GridNumberCell> createState() => _NumberCellState();
|
GridEditableTextCell<GridNumberCell> createState() => _NumberCellState();
|
||||||
@ -57,9 +77,10 @@ class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
|
|||||||
maxLines: null,
|
maxLines: null,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
|
hintText: widget.cellStyle.placeholder,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -93,7 +93,7 @@ class SelectOptionTag extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
EdgeInsets padding =
|
EdgeInsets padding =
|
||||||
const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0);
|
const EdgeInsets.symmetric(vertical: 1.5, horizontal: 8.0);
|
||||||
if (onRemove != null) {
|
if (onRemove != null) {
|
||||||
padding = padding.copyWith(right: 2.0);
|
padding = padding.copyWith(right: 2.0);
|
||||||
}
|
}
|
||||||
@ -110,6 +110,7 @@ class SelectOptionTag extends StatelessWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: FlowyText.medium(
|
child: FlowyText.medium(
|
||||||
name,
|
name,
|
||||||
|
fontSize: FontSizes.s11,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
|
@ -13,9 +13,11 @@ import 'select_option_editor.dart';
|
|||||||
|
|
||||||
class SelectOptionCellStyle extends GridCellStyle {
|
class SelectOptionCellStyle extends GridCellStyle {
|
||||||
String placeholder;
|
String placeholder;
|
||||||
|
EdgeInsets? cellPadding;
|
||||||
|
|
||||||
SelectOptionCellStyle({
|
SelectOptionCellStyle({
|
||||||
required this.placeholder,
|
required this.placeholder,
|
||||||
|
this.cellPadding,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,10 +172,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
|||||||
final Widget child = _buildOptions(context);
|
final Widget child = _buildOptions(context);
|
||||||
|
|
||||||
final constraints = BoxConstraints.loose(
|
final constraints = BoxConstraints.loose(
|
||||||
Size(
|
Size(SelectOptionCellEditor.editorPanelWidth, 300),
|
||||||
SelectOptionCellEditor.editorPanelWidth,
|
|
||||||
300,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
controller: widget.popoverController,
|
controller: widget.popoverController,
|
||||||
@ -191,7 +190,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
|||||||
},
|
},
|
||||||
onClose: () => widget.onCellEditing.value = false,
|
onClose: () => widget.onCellEditing.value = false,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: GridSize.cellContentInsets,
|
padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -200,9 +199,12 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
|||||||
Widget _buildOptions(BuildContext context) {
|
Widget _buildOptions(BuildContext context) {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
if (widget.selectOptions.isEmpty && widget.cellStyle != null) {
|
if (widget.selectOptions.isEmpty && widget.cellStyle != null) {
|
||||||
child = FlowyText.medium(
|
child = Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||||
|
child: FlowyText.medium(
|
||||||
widget.cellStyle!.placeholder,
|
widget.cellStyle!.placeholder,
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final children = widget.selectOptions.map(
|
final children = widget.selectOptions.map(
|
||||||
|
@ -14,6 +14,7 @@ class GridTextCellStyle extends GridCellStyle {
|
|||||||
double emojiFontSize;
|
double emojiFontSize;
|
||||||
double emojiHPadding;
|
double emojiHPadding;
|
||||||
bool showEmoji;
|
bool showEmoji;
|
||||||
|
EdgeInsets? cellPadding;
|
||||||
|
|
||||||
GridTextCellStyle({
|
GridTextCellStyle({
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
@ -22,6 +23,7 @@ class GridTextCellStyle extends GridCellStyle {
|
|||||||
this.showEmoji = true,
|
this.showEmoji = true,
|
||||||
this.emojiFontSize = 16,
|
this.emojiFontSize = 16,
|
||||||
this.emojiHPadding = 0,
|
this.emojiHPadding = 0,
|
||||||
|
this.cellPadding,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +74,8 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: widget.cellStyle.cellPadding ??
|
||||||
|
EdgeInsets.only(
|
||||||
left: GridSize.cellContentInsets.left,
|
left: GridSize.cellContentInsets.left,
|
||||||
right: GridSize.cellContentInsets.right,
|
right: GridSize.cellContentInsets.right,
|
||||||
),
|
),
|
||||||
|
@ -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/cell_builder.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
import 'package:appflowy_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: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 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class TimestampCellStyle extends GridCellStyle {
|
class TimestampCellStyle extends GridCellStyle {
|
||||||
|
String? placeholder;
|
||||||
Alignment alignment;
|
Alignment alignment;
|
||||||
|
EdgeInsets? cellPadding;
|
||||||
|
|
||||||
TimestampCellStyle({this.alignment = Alignment.center});
|
TimestampCellStyle({
|
||||||
|
this.placeholder,
|
||||||
|
this.alignment = Alignment.center,
|
||||||
|
this.cellPadding,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class GridTimestampCell extends GridCellWidget {
|
class GridTimestampCell extends GridCellWidget {
|
||||||
@ -51,16 +58,28 @@ class _TimestampCellState extends GridCellState<GridTimestampCell> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final alignment = widget.cellStyle != null
|
final alignment = widget.cellStyle?.alignment ?? Alignment.centerLeft;
|
||||||
? widget.cellStyle!.alignment
|
final placeholder = widget.cellStyle?.placeholder ?? "";
|
||||||
: Alignment.centerLeft;
|
final padding = widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets;
|
||||||
|
|
||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: _cellBloc,
|
value: _cellBloc,
|
||||||
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
|
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return GridTimestampCellText(
|
final isEmpty = state.dateStr.isEmpty;
|
||||||
dateStr: state.dateStr,
|
final text = isEmpty ? placeholder : state.dateStr;
|
||||||
|
return Align(
|
||||||
alignment: alignment,
|
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;
|
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/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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/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/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:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -20,36 +12,39 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
class RowActionList extends StatelessWidget {
|
class RowActionList extends StatelessWidget {
|
||||||
final RowController rowController;
|
final RowController rowController;
|
||||||
const RowActionList({
|
const RowActionList({
|
||||||
required String viewId,
|
|
||||||
required this.rowController,
|
required this.rowController,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return BlocProvider<RowActionSheetBloc>(
|
||||||
|
create: (context) => RowActionSheetBloc(
|
||||||
|
viewId: rowController.viewId,
|
||||||
|
rowId: rowController.rowId,
|
||||||
|
groupId: rowController.groupId,
|
||||||
|
),
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 10),
|
|
||||||
child: FlowyText(LocaleKeys.grid_row_action.tr()),
|
|
||||||
),
|
|
||||||
const VSpace(15),
|
|
||||||
RowDetailPageDeleteButton(rowId: rowController.rowId),
|
|
||||||
RowDetailPageDuplicateButton(
|
RowDetailPageDuplicateButton(
|
||||||
rowId: rowController.rowId,
|
rowId: rowController.rowId,
|
||||||
groupId: rowController.groupId,
|
groupId: rowController.groupId,
|
||||||
),
|
),
|
||||||
|
const VSpace(4.0),
|
||||||
|
RowDetailPageDeleteButton(rowId: rowController.rowId),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RowDetailPageDeleteButton extends StatelessWidget {
|
class RowDetailPageDeleteButton extends StatelessWidget {
|
||||||
final String rowId;
|
final String rowId;
|
||||||
const RowDetailPageDeleteButton({required this.rowId, Key? key})
|
const RowDetailPageDeleteButton({required this.rowId, super.key});
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -59,7 +54,9 @@ class RowDetailPageDeleteButton extends StatelessWidget {
|
|||||||
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.trash_m),
|
leftIcon: const FlowySvg(FlowySvgs.trash_m),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
|
context
|
||||||
|
.read<RowActionSheetBloc>()
|
||||||
|
.add(const RowActionSheetEvent.deleteRow());
|
||||||
FlowyOverlay.pop(context);
|
FlowyOverlay.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -73,8 +70,8 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
|
|||||||
const RowDetailPageDuplicateButton({
|
const RowDetailPageDuplicateButton({
|
||||||
required this.rowId,
|
required this.rowId,
|
||||||
this.groupId,
|
this.groupId,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -85,91 +82,11 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
|
|||||||
leftIcon: const FlowySvg(FlowySvgs.copy_s),
|
leftIcon: const FlowySvg(FlowySvgs.copy_s),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context
|
context
|
||||||
.read<RowDetailBloc>()
|
.read<RowActionSheetBloc>()
|
||||||
.add(RowDetailEvent.duplicateRow(rowId, groupId));
|
.add(const RowActionSheetEvent.duplicateRow());
|
||||||
FlowyOverlay.pop(context);
|
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/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_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/emoji_picker/emoji_picker.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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 {
|
class RowBanner extends StatefulWidget {
|
||||||
final String viewId;
|
final RowController rowController;
|
||||||
final RowMetaPB rowMeta;
|
final GridCellBuilder cellBuilder;
|
||||||
final RowBannerCellBuilder cellBuilder;
|
|
||||||
const RowBanner({
|
const RowBanner({
|
||||||
required this.viewId,
|
required this.rowController,
|
||||||
required this.rowMeta,
|
|
||||||
required this.cellBuilder,
|
required this.cellBuilder,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@ -34,12 +38,16 @@ class _RowBannerState extends State<RowBanner> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<RowBannerBloc>(
|
return BlocProvider<RowBannerBloc>(
|
||||||
create: (context) => RowBannerBloc(
|
create: (context) => RowBannerBloc(
|
||||||
viewId: widget.viewId,
|
viewId: widget.rowController.viewId,
|
||||||
rowMeta: widget.rowMeta,
|
rowMeta: widget.rowController.rowMeta,
|
||||||
)..add(const RowBannerEvent.initial()),
|
)..add(const RowBannerEvent.initial()),
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
onEnter: (event) => _isHovering.value = true,
|
onEnter: (event) => _isHovering.value = true,
|
||||||
onExit: (event) => _isHovering.value = false,
|
onExit: (event) => _isHovering.value = false,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(60, 34, 60, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -50,9 +58,19 @@ class _RowBannerState extends State<RowBanner> {
|
|||||||
popoverController: popoverController,
|
popoverController: popoverController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const HSpace(4),
|
||||||
_BannerTitle(
|
_BannerTitle(
|
||||||
cellBuilder: widget.cellBuilder,
|
cellBuilder: widget.cellBuilder,
|
||||||
popoverController: popoverController,
|
popoverController: popoverController,
|
||||||
|
rowController: widget.rowController,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 12,
|
||||||
|
right: 12,
|
||||||
|
child: RowActionButton(rowController: widget.rowController),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -74,7 +92,10 @@ class _BannerAction extends StatelessWidget {
|
|||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
valueListenable: isHovering,
|
valueListenable: isHovering,
|
||||||
builder: (BuildContext context, bool value, Widget? child) {
|
builder: (BuildContext context, bool value, Widget? child) {
|
||||||
if (value) {
|
if (!value) {
|
||||||
|
return const SizedBox(height: _kBannerActionHeight);
|
||||||
|
}
|
||||||
|
|
||||||
return BlocBuilder<RowBannerBloc, RowBannerState>(
|
return BlocBuilder<RowBannerBloc, RowBannerState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
@ -103,21 +124,22 @@ class _BannerAction extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return const SizedBox(height: _kBannerActionHeight);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BannerTitle extends StatefulWidget {
|
class _BannerTitle extends StatefulWidget {
|
||||||
final RowBannerCellBuilder cellBuilder;
|
final GridCellBuilder cellBuilder;
|
||||||
final PopoverController popoverController;
|
final PopoverController popoverController;
|
||||||
|
final RowController rowController;
|
||||||
|
|
||||||
const _BannerTitle({
|
const _BannerTitle({
|
||||||
required this.cellBuilder,
|
required this.cellBuilder,
|
||||||
required this.popoverController,
|
required this.popoverController,
|
||||||
});
|
required this.rowController,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_BannerTitle> createState() => _BannerTitleState();
|
State<_BannerTitle> createState() => _BannerTitleState();
|
||||||
@ -139,10 +161,24 @@ class _BannerTitleState extends State<_BannerTitle> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
children.add(const HSpace(4));
|
||||||
|
|
||||||
if (state.primaryField != null) {
|
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(
|
children.add(
|
||||||
Expanded(
|
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) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 26,
|
height: 26,
|
||||||
width: 160,
|
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
|
useIntrinsicWidth: true,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||||
),
|
),
|
||||||
leftIcon: const Icon(
|
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
|
||||||
Icons.emoji_emotions,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
onTap: widget.showEmojiPicker,
|
onTap: widget.showEmojiPicker,
|
||||||
|
margin: const EdgeInsets.all(4),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -239,16 +273,14 @@ class RemoveEmojiButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 26,
|
height: 26,
|
||||||
width: 160,
|
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
|
useIntrinsicWidth: true,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||||
),
|
),
|
||||||
leftIcon: const Icon(
|
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
|
||||||
Icons.emoji_emotions,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
onTap: onRemoved,
|
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/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_detail_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/row_document.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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'cell_builder.dart';
|
import 'cell_builder.dart';
|
||||||
import 'cells/text_cell/text_cell.dart';
|
|
||||||
import 'row_action.dart';
|
|
||||||
import 'row_banner.dart';
|
import 'row_banner.dart';
|
||||||
import 'row_property.dart';
|
import 'row_property.dart';
|
||||||
|
|
||||||
@ -47,17 +41,29 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyDialog(
|
return FlowyDialog(
|
||||||
child: BlocProvider(
|
child: BlocProvider(
|
||||||
create: (context) {
|
create: (context) => RowDetailBloc(rowController: widget.rowController)
|
||||||
return RowDetailBloc(rowController: widget.rowController)
|
..add(const RowDetailEvent.initial()),
|
||||||
..add(const RowDetailEvent.initial());
|
|
||||||
},
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
children: [
|
children: [
|
||||||
_rowBanner(),
|
RowBanner(
|
||||||
IntrinsicHeight(child: _responsiveRowInfo()),
|
rowController: widget.rowController,
|
||||||
const Divider(height: 1.0),
|
cellBuilder: widget.cellBuilder,
|
||||||
const VSpace(10),
|
),
|
||||||
|
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(
|
RowDocument(
|
||||||
viewId: widget.rowController.viewId,
|
viewId: widget.rowController.viewId,
|
||||||
rowId: widget.rowController.rowId,
|
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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.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_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/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_cell.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.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/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:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'accessory/cell_accessory.dart';
|
import 'accessory/cell_accessory.dart';
|
||||||
import 'cell_builder.dart';
|
import 'cell_builder.dart';
|
||||||
|
import 'cells/checkbox_cell/checkbox_cell.dart';
|
||||||
import 'cells/date_cell/date_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/select_option_cell/select_option_cell.dart';
|
||||||
import 'cells/text_cell/text_cell.dart';
|
import 'cells/text_cell/text_cell.dart';
|
||||||
import 'cells/timestamp_cell/timestamp_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
|
/// Display the row properties in a list. Only use this widget in the
|
||||||
/// [RowDetailPage].
|
/// [RowDetailPage].
|
||||||
///
|
|
||||||
class RowPropertyList extends StatelessWidget {
|
class RowPropertyList extends StatelessWidget {
|
||||||
final String viewId;
|
final String viewId;
|
||||||
final GridCellBuilder cellBuilder;
|
final GridCellBuilder cellBuilder;
|
||||||
@ -38,28 +43,75 @@ class RowPropertyList extends StatelessWidget {
|
|||||||
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
||||||
buildWhen: (previous, current) => previous.cells != current.cells,
|
buildWhen: (previous, current) => previous.cells != current.cells,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Column(
|
final children = state.cells
|
||||||
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)
|
.where((element) => !element.fieldInfo.field.isPrimary)
|
||||||
.map(
|
.mapIndexed(
|
||||||
(cell) => _PropertyCell(
|
(index, cell) => _PropertyCell(
|
||||||
|
key: ValueKey('row_detail_${cell.fieldId}'),
|
||||||
cellContext: cell,
|
cellContext: cell,
|
||||||
cellBuilder: cellBuilder,
|
cellBuilder: cellBuilder,
|
||||||
|
index: index,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList();
|
||||||
const VSpace(20),
|
return ReorderableListView(
|
||||||
|
shrinkWrap: true,
|
||||||
// Create a new property(field) button
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
CreateRowFieldButton(viewId: viewId),
|
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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,10 +119,12 @@ class RowPropertyList extends StatelessWidget {
|
|||||||
class _PropertyCell extends StatefulWidget {
|
class _PropertyCell extends StatefulWidget {
|
||||||
final DatabaseCellContext cellContext;
|
final DatabaseCellContext cellContext;
|
||||||
final GridCellBuilder cellBuilder;
|
final GridCellBuilder cellBuilder;
|
||||||
|
final int index;
|
||||||
const _PropertyCell({
|
const _PropertyCell({
|
||||||
required this.cellContext,
|
required this.cellContext,
|
||||||
required this.cellBuilder,
|
required this.cellBuilder,
|
||||||
Key? key,
|
Key? key,
|
||||||
|
required this.index,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -78,45 +132,65 @@ class _PropertyCell extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PropertyCellState extends State<_PropertyCell> {
|
class _PropertyCellState extends State<_PropertyCell> {
|
||||||
final PopoverController popover = PopoverController();
|
final PopoverController _popoverController = PopoverController();
|
||||||
|
bool _isFieldHover = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final style = _customCellStyle(widget.cellContext.fieldType);
|
final style = _customCellStyle(widget.cellContext.fieldType);
|
||||||
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
||||||
|
|
||||||
final gesture = GestureDetector(
|
final dragThumb = MouseRegion(
|
||||||
behavior: HitTestBehavior.opaque,
|
cursor: SystemMouseCursors.grab,
|
||||||
onTap: () => cell.requestFocus.notify(),
|
child: SizedBox(
|
||||||
child: AccessoryHover(
|
width: 16,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
|
height: 30,
|
||||||
child: cell,
|
child: _isFieldHover ? const FlowySvg(FlowySvgs.drag_element_s) : null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return IntrinsicHeight(
|
final gesture = GestureDetector(
|
||||||
child: ConstrainedBox(
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () => cell.requestFocus.notify(),
|
||||||
|
child: AccessoryHover(child: cell),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
constraints: const BoxConstraints(minHeight: 30),
|
constraints: const BoxConstraints(minHeight: 30),
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (event) => setState(() => _isFieldHover = true),
|
||||||
|
onExit: (event) => setState(() => _isFieldHover = false),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
ReorderableDragStartListener(
|
||||||
|
index: widget.index,
|
||||||
|
enabled: _isFieldHover,
|
||||||
|
child: dragThumb,
|
||||||
|
),
|
||||||
|
const HSpace(4),
|
||||||
AppFlowyPopover(
|
AppFlowyPopover(
|
||||||
controller: popover,
|
controller: _popoverController,
|
||||||
constraints: BoxConstraints.loose(const Size(240, 600)),
|
constraints: BoxConstraints.loose(const Size(240, 600)),
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
triggerActions: PopoverTriggerFlags.none,
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
popupBuilder: (popoverContext) => buildFieldEditor(),
|
popupBuilder: (popoverContext) => buildFieldEditor(),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 150,
|
width: 160,
|
||||||
height: 40,
|
height: 30,
|
||||||
child: FieldCellButton(
|
child: FieldCellButton(
|
||||||
field: widget.cellContext.fieldInfo.field,
|
field: widget.cellContext.fieldInfo.field,
|
||||||
onTap: () => popover.show(),
|
onTap: () => _popoverController.show(),
|
||||||
radius: BorderRadius.circular(6),
|
radius: BorderRadius.circular(6),
|
||||||
|
margin:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const HSpace(8),
|
||||||
Expanded(child: gesture),
|
Expanded(child: gesture),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -133,11 +207,11 @@ class _PropertyCellState extends State<_PropertyCell> {
|
|||||||
field: widget.cellContext.fieldInfo.field,
|
field: widget.cellContext.fieldInfo.field,
|
||||||
),
|
),
|
||||||
onHidden: (fieldId) {
|
onHidden: (fieldId) {
|
||||||
popover.close();
|
_popoverController.close();
|
||||||
context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
|
context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
|
||||||
},
|
},
|
||||||
onDeleted: (fieldId) {
|
onDeleted: (fieldId) {
|
||||||
popover.close();
|
_popoverController.close();
|
||||||
|
|
||||||
NavigatorAlertDialog(
|
NavigatorAlertDialog(
|
||||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||||
@ -155,26 +229,35 @@ class _PropertyCellState extends State<_PropertyCell> {
|
|||||||
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
case FieldType.Checkbox:
|
case FieldType.Checkbox:
|
||||||
return null;
|
return GridCheckboxCellStyle(
|
||||||
|
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||||
|
);
|
||||||
case FieldType.DateTime:
|
case FieldType.DateTime:
|
||||||
return DateCellStyle(
|
return DateCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
|
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
);
|
);
|
||||||
case FieldType.LastEditedTime:
|
case FieldType.LastEditedTime:
|
||||||
case FieldType.CreatedTime:
|
case FieldType.CreatedTime:
|
||||||
return TimestampCellStyle(
|
return TimestampCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
|
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
);
|
);
|
||||||
case FieldType.MultiSelect:
|
case FieldType.MultiSelect:
|
||||||
return SelectOptionCellStyle(
|
return SelectOptionCellStyle(
|
||||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||||
);
|
);
|
||||||
case FieldType.Checklist:
|
case FieldType.Checklist:
|
||||||
return SelectOptionCellStyle(
|
return SelectOptionCellStyle(
|
||||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
);
|
);
|
||||||
case FieldType.Number:
|
case FieldType.Number:
|
||||||
return null;
|
return GridNumberCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
);
|
||||||
case FieldType.RichText:
|
case FieldType.RichText:
|
||||||
return GridTextCellStyle(
|
return GridTextCellStyle(
|
||||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
@ -182,6 +265,7 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
|||||||
case FieldType.SingleSelect:
|
case FieldType.SingleSelect:
|
||||||
return SelectOptionCellStyle(
|
return SelectOptionCellStyle(
|
||||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||||
);
|
);
|
||||||
|
|
||||||
case FieldType.URL:
|
case FieldType.URL:
|
||||||
@ -195,3 +279,81 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
|||||||
}
|
}
|
||||||
throw UnimplementedError;
|
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…
x
Reference in New Issue
Block a user