Merge remote-tracking branch 'origin/main' into customize_font_size

This commit is contained in:
Lucas.Xu 2022-11-28 18:11:06 +08:00
commit 14ac2db06d
155 changed files with 4670 additions and 1710 deletions

View File

@ -78,10 +78,10 @@ jobs:
cargo install cargo-make cargo install cargo-make
cargo install duckscript_cli cargo install duckscript_cli
- name: Cargo make flowy_dev - name: Cargo make appflowy-deps-tools
working-directory: frontend working-directory: frontend
run: | run: |
cargo make flowy_dev cargo make appflowy-deps-tools
- name: Config Flutter - name: Config Flutter
run: | run: |

View File

@ -64,7 +64,7 @@ jobs:
- name: Cargo make flowy dev - name: Cargo make flowy dev
working-directory: frontend working-directory: frontend
run: | run: |
cargo make flowy_dev cargo make appflowy-deps-tools
- name: Flutter Deps - name: Flutter Deps
run: flutter packages pub get run: flutter packages pub get
@ -73,7 +73,7 @@ jobs:
- name: Build FlowySDK - name: Build FlowySDK
working-directory: frontend working-directory: frontend
run: | run: |
cargo make --profile development-linux-x86_64 flowy-sdk-dev cargo make --profile development-linux-x86_64 appflowy-sdk-dev
- name: Flutter Code Generation - name: Flutter Code Generation
working-directory: frontend/app_flowy working-directory: frontend/app_flowy

View File

@ -54,7 +54,7 @@ jobs:
working-directory: frontend working-directory: frontend
run: | run: |
cargo install cargo-make cargo install cargo-make
cargo make flowy_dev cargo make appflowy-deps-tools
- name: Flutter Deps - name: Flutter Deps
working-directory: frontend/app_flowy working-directory: frontend/app_flowy

View File

@ -63,7 +63,7 @@ jobs:
source $HOME/.cargo/env source $HOME/.cargo/env
cargo install --force cargo-make cargo install --force cargo-make
cargo install --force duckscript_cli cargo install --force duckscript_cli
cargo make flowy_dev cargo make appflowy-deps-tools
- name: Build Linux app - name: Build Linux app
working-directory: frontend working-directory: frontend
@ -161,7 +161,7 @@ jobs:
source $HOME/.cargo/env source $HOME/.cargo/env
cargo install --force cargo-make cargo install --force cargo-make
cargo install --force duckscript_cli cargo install --force duckscript_cli
cargo make flowy_dev cargo make appflowy-deps-tools
- name: Build macOS app for x86_64 - name: Build macOS app for x86_64
working-directory: frontend working-directory: frontend
@ -234,7 +234,7 @@ jobs:
vcpkg integrate install vcpkg integrate install
cargo install --force cargo-make cargo install --force cargo-make
cargo install --force duckscript_cli cargo install --force duckscript_cli
cargo make flowy_dev cargo make appflowy-deps-tools
- name: Build Windows app - name: Build Windows app
working-directory: frontend working-directory: frontend

View File

@ -39,12 +39,12 @@ jobs:
working-directory: frontend working-directory: frontend
run: | run: |
cargo install cargo-make cargo install cargo-make
cargo make flowy_dev cargo make appflowy-deps-tools
- name: Build FlowySDK - name: Build FlowySDK
working-directory: frontend working-directory: frontend
run: | run: |
cargo make --profile development-linux-x86_64 flowy-sdk-dev cargo make --profile development-linux-x86_64 appflowy-sdk-dev
- run: rustup component add rustfmt - run: rustup component add rustfmt
working-directory: frontend/rust-lib working-directory: frontend/rust-lib

View File

@ -1,11 +1,14 @@
# Release Notes # Release Notes
## Version 0.0.7 - 11/27/2022
### New features
- Support adding filters by the text property in Grid
## Version 0.0.6.2 - 10/30/2022 ## Version 0.0.6.2 - 10/30/2022
- Fix some bugs - Fix some bugs
## Version 0.0.6.1 - 10/26/2022 ## Version 0.0.6.1 - 10/26/2022
### New features ### New features
- Optimzie appflowy_editor dark mode style - Optimize appflowy_editor dark mode style
### Bug Fixes ### Bug Fixes
- Unable to copy the text with checkbox or link style - Unable to copy the text with checkbox or link style

View File

@ -48,7 +48,7 @@
{ {
"label": "AF: build_flowy_sdk_for_android", "label": "AF: build_flowy_sdk_for_android",
"type": "shell", "type": "shell",
"command": "cargo make --profile development-android flowy-sdk-dev-android", "command": "cargo make --profile development-android appflowy-sdk-dev-android",
"group": "build", "group": "build",
"options": { "options": {
"cwd": "${workspaceFolder}" "cwd": "${workspaceFolder}"

View File

@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi" LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.0.6.2" CURRENT_APP_VERSION = "0.0.7"
FEATURES = "flutter" FEATURES = "flutter"
PRODUCT_NAME = "AppFlowy" PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html

View File

@ -167,7 +167,35 @@
"filter": "Filter", "filter": "Filter",
"sortBy": "Sort by", "sortBy": "Sort by",
"Properties": "Properties", "Properties": "Properties",
"group": "Group" "group": "Group",
"addFilter": "Add Filter",
"deleteFilter": "Delete filter",
"filterBy": "Filter by...",
"typeAValue": "Type a value..."
},
"textFilter": {
"contains": "Contains",
"doesNotContain": "Does not contain",
"endsWith": "Ends with",
"startWith": "Starts with",
"is": "Is",
"isNot": "Is not",
"isEmpty": "Is empty",
"isNotEmpty": "Is not empty",
"choicechipPrefix": {
"isNot": "Not",
"startWith": "Starts with",
"endWith": "Ends with",
"isEmpty": "is empty",
"isNotEmpty": "is not empty"
}
},
"checkboxFilter": {
"isChecked": "Checked",
"isUnchecked": "Unchecked",
"choicechipPrefix": {
"is": "is"
}
}, },
"field": { "field": {
"hide": "Hide", "hide": "Hide",

View File

@ -136,9 +136,9 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
} }
void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) { void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
final fieldContext = fieldController.getField(group.fieldId); final fieldInfo = fieldController.getField(group.fieldId);
if (fieldContext == null) { if (fieldInfo == null) {
Log.warn("FieldContext should not be null"); Log.warn("fieldInfo should not be null");
return; return;
} }
@ -147,7 +147,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
// group.groupId, // group.groupId,
// GroupItem( // GroupItem(
// row: row, // row: row,
// fieldContext: fieldContext, // fieldInfo: fieldInfo,
// isDraggable: !isEdit, // isDraggable: !isEdit,
// ), // ),
// ); // );
@ -204,7 +204,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
items: _buildGroupItems(group), items: _buildGroupItems(group),
customData: GroupData( customData: GroupData(
group: group, group: group,
fieldContext: fieldController.getField(group.fieldId)!, fieldInfo: fieldController.getField(group.fieldId)!,
), ),
); );
}).toList(); }).toList();
@ -275,10 +275,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
List<AppFlowyGroupItem> _buildGroupItems(GroupPB group) { List<AppFlowyGroupItem> _buildGroupItems(GroupPB group) {
final items = group.rows.map((row) { final items = group.rows.map((row) {
final fieldContext = fieldController.getField(group.fieldId); final fieldInfo = fieldController.getField(group.fieldId);
return GroupItem( return GroupItem(
row: row, row: row,
fieldContext: fieldContext!, fieldInfo: fieldInfo!,
); );
}).toList(); }).toList();
@ -374,11 +374,11 @@ class GridFieldEquatable extends Equatable {
class GroupItem extends AppFlowyGroupItem { class GroupItem extends AppFlowyGroupItem {
final RowPB row; final RowPB row;
final GridFieldContext fieldContext; final FieldInfo fieldInfo;
GroupItem({ GroupItem({
required this.row, required this.row,
required this.fieldContext, required this.fieldInfo,
bool draggable = true, bool draggable = true,
}) { }) {
super.draggable = draggable; super.draggable = draggable;
@ -401,22 +401,22 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
@override @override
void insertRow(GroupPB group, RowPB row, int? index) { void insertRow(GroupPB group, RowPB row, int? index) {
final fieldContext = fieldController.getField(group.fieldId); final fieldInfo = fieldController.getField(group.fieldId);
if (fieldContext == null) { if (fieldInfo == null) {
Log.warn("FieldContext should not be null"); Log.warn("fieldInfo should not be null");
return; return;
} }
if (index != null) { if (index != null) {
final item = GroupItem( final item = GroupItem(
row: row, row: row,
fieldContext: fieldContext, fieldInfo: fieldInfo,
); );
controller.insertGroupItem(group.groupId, index, item); controller.insertGroupItem(group.groupId, index, item);
} else { } else {
final item = GroupItem( final item = GroupItem(
row: row, row: row,
fieldContext: fieldContext, fieldInfo: fieldInfo,
); );
controller.addGroupItem(group.groupId, item); controller.addGroupItem(group.groupId, item);
} }
@ -429,30 +429,30 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
@override @override
void updateRow(GroupPB group, RowPB row) { void updateRow(GroupPB group, RowPB row) {
final fieldContext = fieldController.getField(group.fieldId); final fieldInfo = fieldController.getField(group.fieldId);
if (fieldContext == null) { if (fieldInfo == null) {
Log.warn("FieldContext should not be null"); Log.warn("fieldInfo should not be null");
return; return;
} }
controller.updateGroupItem( controller.updateGroupItem(
group.groupId, group.groupId,
GroupItem( GroupItem(
row: row, row: row,
fieldContext: fieldContext, fieldInfo: fieldInfo,
), ),
); );
} }
@override @override
void addNewRow(GroupPB group, RowPB row, int? index) { void addNewRow(GroupPB group, RowPB row, int? index) {
final fieldContext = fieldController.getField(group.fieldId); final fieldInfo = fieldController.getField(group.fieldId);
if (fieldContext == null) { if (fieldInfo == null) {
Log.warn("FieldContext should not be null"); Log.warn("fieldInfo should not be null");
return; return;
} }
final item = GroupItem( final item = GroupItem(
row: row, row: row,
fieldContext: fieldContext, fieldInfo: fieldInfo,
draggable: false, draggable: false,
); );
@ -479,10 +479,10 @@ class BoardEditingRow {
class GroupData { class GroupData {
final GroupPB group; final GroupPB group;
final GridFieldContext fieldContext; final FieldInfo fieldInfo;
GroupData({ GroupData({
required this.group, required this.group,
required this.fieldContext, required this.fieldInfo,
}); });
CheckboxGroup? asCheckboxGroup() { CheckboxGroup? asCheckboxGroup() {
@ -490,7 +490,7 @@ class GroupData {
return CheckboxGroup(group); return CheckboxGroup(group);
} }
FieldType get fieldType => fieldContext.fieldType; FieldType get fieldType => fieldInfo.fieldType;
} }
class CheckboxGroup { class CheckboxGroup {

View File

@ -12,7 +12,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'board_listener.dart'; import 'board_listener.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>); typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
typedef OnGridChanged = void Function(GridPB); typedef OnGridChanged = void Function(GridPB);
typedef DidLoadGroups = void Function(List<GroupPB>); typedef DidLoadGroups = void Function(List<GroupPB>);
typedef OnUpdatedGroup = void Function(List<GroupPB>); typedef OnUpdatedGroup = void Function(List<GroupPB>);

View File

@ -58,14 +58,14 @@ class BoardDateCellState with _$BoardDateCellState {
const factory BoardDateCellState({ const factory BoardDateCellState({
required DateCellDataPB? data, required DateCellDataPB? data,
required String dateStr, required String dateStr,
required GridFieldContext fieldContext, required FieldInfo fieldInfo,
}) = _BoardDateCellState; }) = _BoardDateCellState;
factory BoardDateCellState.initial(GridDateCellController context) { factory BoardDateCellState.initial(GridDateCellController context) {
final cellData = context.getCellData(); final cellData = context.getCellData();
return BoardDateCellState( return BoardDateCellState(
fieldContext: context.fieldContext, fieldInfo: context.fieldInfo,
data: cellData, data: cellData,
dateStr: _dateStrFromCellData(cellData), dateStr: _dateStrFromCellData(cellData),
); );

View File

@ -63,10 +63,9 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
return RowInfo( return RowInfo(
gridId: _rowService.gridId, gridId: _rowService.gridId,
fields: UnmodifiableListView( fields: UnmodifiableListView(
state.cells.map((cell) => cell.identifier.fieldContext).toList(), state.cells.map((cell) => cell.identifier.fieldInfo).toList(),
), ),
rowPB: state.rowPB, rowPB: state.rowPB,
visible: true,
); );
} }
@ -133,10 +132,10 @@ class BoardCellEquatable extends Equatable {
@override @override
List<Object?> get props { List<Object?> get props {
return [ return [
identifier.fieldContext.id, identifier.fieldInfo.id,
identifier.fieldContext.fieldType, identifier.fieldInfo.fieldType,
identifier.fieldContext.visibility, identifier.fieldInfo.visibility,
identifier.fieldContext.width, identifier.fieldInfo.width,
]; ];
} }
} }

View File

@ -236,7 +236,7 @@ class _BoardContentState extends State<BoardContent> {
child: BoardCard( child: BoardCard(
gridId: gridId, gridId: gridId,
groupId: groupData.group.groupId, groupId: groupData.group.groupId,
fieldId: groupItem.fieldContext.id, fieldId: groupItem.fieldInfo.id,
isEditing: isEditing, isEditing: isEditing,
cellBuilder: cellBuilder, cellBuilder: cellBuilder,
dataController: cardController, dataController: cardController,
@ -285,9 +285,8 @@ class _BoardContentState extends State<BoardContent> {
) { ) {
final rowInfo = RowInfo( final rowInfo = RowInfo(
gridId: gridId, gridId: gridId,
fields: UnmodifiableListView(fieldController.fieldContexts), fields: UnmodifiableListView(fieldController.fieldInfos),
rowPB: rowPB, rowPB: rowPB,
visible: true,
); );
final dataController = GridRowDataController( final dataController = GridRowDataController(

View File

@ -1,10 +1,12 @@
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../generated/locale_keys.g.dart';
import 'board_setting.dart'; import 'board_setting.dart';
class BoardToolbarContext { class BoardToolbarContext {
@ -30,6 +32,7 @@ class BoardToolbar extends StatelessWidget {
height: 40, height: 40,
child: Row( child: Row(
children: [ children: [
const Spacer(),
_SettingButton( _SettingButton(
settingContext: BoardSettingContext.from(toolbarContext), settingContext: BoardSettingContext.from(toolbarContext),
), ),
@ -61,16 +64,18 @@ class _SettingButtonState extends State<_SettingButton> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppFlowyPopover( return AppFlowyPopover(
controller: popoverController, controller: popoverController,
direction: PopoverDirection.leftWithTopAligned,
triggerActions: PopoverTriggerFlags.none,
constraints: BoxConstraints.loose(const Size(260, 400)), constraints: BoxConstraints.loose(const Size(260, 400)),
child: FlowyIconButton( child: FlowyTextButton(
width: 22, LocaleKeys.settings_title.tr(),
icon: Padding( fontSize: 14,
padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0), fillColor: Colors.transparent,
child: svgWidget( hoverColor: AFThemeExtension.of(context).lightGreyHover,
"grid/setting/setting", padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),
color: Theme.of(context).colorScheme.onSurface, onPressed: () {
), popoverController.show();
), },
), ),
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
return BoardSettingListPopover( return BoardSettingListPopover(

View File

@ -130,14 +130,3 @@ class DocumentPluginDisplay extends PluginDisplay with NavigationItem {
@override @override
List<NavigationItem> get navigationItems => [this]; List<NavigationItem> get navigationItems => [this];
} }
extension QuestionBubbleExtension on ShareAction {
String get name {
switch (this) {
case ShareAction.markdown:
return LocaleKeys.shareAction_markdown.tr();
case ShareAction.copyLink:
return LocaleKeys.shareAction_copyLink.tr();
}
}
}

View File

@ -150,22 +150,22 @@ class IGridCellController<T, D> extends Equatable {
_fieldNotifier = fieldNotifier, _fieldNotifier = fieldNotifier,
_fieldService = FieldService( _fieldService = FieldService(
gridId: cellId.gridId, gridId: cellId.gridId,
fieldId: cellId.fieldContext.id, fieldId: cellId.fieldInfo.id,
), ),
_cacheKey = GridCellCacheKey( _cacheKey = GridCellCacheKey(
rowId: cellId.rowId, rowId: cellId.rowId,
fieldId: cellId.fieldContext.id, fieldId: cellId.fieldInfo.id,
); );
String get gridId => cellId.gridId; String get gridId => cellId.gridId;
String get rowId => cellId.rowId; String get rowId => cellId.rowId;
String get fieldId => cellId.fieldContext.id; String get fieldId => cellId.fieldInfo.id;
GridFieldContext get fieldContext => cellId.fieldContext; FieldInfo get fieldInfo => cellId.fieldInfo;
FieldType get fieldType => cellId.fieldContext.fieldType; FieldType get fieldType => cellId.fieldInfo.fieldType;
VoidCallback? startListening({ VoidCallback? startListening({
required void Function(T?) onCellChanged, required void Function(T?) onCellChanged,
@ -179,7 +179,7 @@ class IGridCellController<T, D> extends Equatable {
_cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey)); _cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey));
_cellListener = _cellListener =
CellListener(rowId: cellId.rowId, fieldId: cellId.fieldContext.id); CellListener(rowId: cellId.rowId, fieldId: cellId.fieldInfo.id);
/// 1.Listen on user edit event and load the new cell data if needed. /// 1.Listen on user edit event and load the new cell data if needed.
/// For example: /// For example:
@ -310,30 +310,33 @@ class IGridCellController<T, D> extends Equatable {
@override @override
List<Object> get props => List<Object> get props =>
[_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldContext.id]; [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldInfo.id];
} }
class GridCellFieldNotifierImpl extends IGridCellFieldNotifier { class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
final GridFieldController _cache; final GridFieldController _fieldController;
OnChangeset? _onChangesetFn; OnReceiveUpdateFields? _onChangesetFn;
GridCellFieldNotifierImpl(GridFieldController cache) : _cache = cache; GridCellFieldNotifierImpl(GridFieldController cache)
: _fieldController = cache;
@override @override
void onCellDispose() { void onCellDispose() {
if (_onChangesetFn != null) { if (_onChangesetFn != null) {
_cache.removeListener(onChangesetListener: _onChangesetFn!); _fieldController.removeListener(onChangesetListener: _onChangesetFn!);
_onChangesetFn = null; _onChangesetFn = null;
} }
} }
@override @override
void onCellFieldChanged(void Function(FieldPB p1) callback) { void onCellFieldChanged(void Function(FieldInfo) callback) {
_onChangesetFn = (GridFieldChangesetPB changeset) { _onChangesetFn = (List<FieldInfo> filedInfos) {
for (final updatedField in changeset.updatedFields) { for (final field in filedInfos) {
callback(updatedField); callback(field);
} }
}; };
_cache.addListener(onChangeset: _onChangesetFn); _fieldController.addListener(
onFieldsUpdated: _onChangesetFn,
);
} }
} }

View File

@ -1,10 +1,10 @@
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'cell_service.dart'; import 'cell_service.dart';
abstract class IGridCellFieldNotifier { abstract class IGridCellFieldNotifier {
void onCellFieldChanged(void Function(FieldPB) callback); void onCellFieldChanged(void Function(FieldInfo) callback);
void onCellDispose(); void onCellDispose();
} }

View File

@ -60,17 +60,17 @@ class GridCellIdentifier with _$GridCellIdentifier {
const factory GridCellIdentifier({ const factory GridCellIdentifier({
required String gridId, required String gridId,
required String rowId, required String rowId,
required GridFieldContext fieldContext, required FieldInfo fieldInfo,
}) = _GridCellIdentifier; }) = _GridCellIdentifier;
// ignore: unused_element // ignore: unused_element
const GridCellIdentifier._(); const GridCellIdentifier._();
String get fieldId => fieldContext.id; String get fieldId => fieldInfo.id;
FieldType get fieldType => fieldContext.fieldType; FieldType get fieldType => fieldInfo.fieldType;
ValueKey key() { ValueKey key() {
return ValueKey("$rowId$fieldId${fieldContext.fieldType}"); return ValueKey("$rowId$fieldId${fieldInfo.fieldType}");
} }
} }

View File

@ -176,7 +176,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
final result = await FieldService.updateFieldTypeOption( final result = await FieldService.updateFieldTypeOption(
gridId: cellController.gridId, gridId: cellController.gridId,
fieldId: cellController.fieldContext.id, fieldId: cellController.fieldInfo.id,
typeOptionData: newDateTypeOption.writeToBuffer(), typeOptionData: newDateTypeOption.writeToBuffer(),
); );

View File

@ -58,14 +58,14 @@ class DateCellState with _$DateCellState {
const factory DateCellState({ const factory DateCellState({
required DateCellDataPB? data, required DateCellDataPB? data,
required String dateStr, required String dateStr,
required GridFieldContext fieldContext, required FieldInfo fieldInfo,
}) = _DateCellState; }) = _DateCellState;
factory DateCellState.initial(GridDateCellController context) { factory DateCellState.initial(GridDateCellController context) {
final cellData = context.getCellData(); final cellData = context.getCellData();
return DateCellState( return DateCellState(
fieldContext: context.fieldContext, fieldInfo: context.fieldInfo,
data: cellData, data: cellData,
dateStr: _dateStrFromCellData(cellData), dateStr: _dateStrFromCellData(cellData),
); );

View File

@ -11,7 +11,7 @@ class SelectOptionService {
SelectOptionService({required this.cellId}); SelectOptionService({required this.cellId});
String get gridId => cellId.gridId; String get gridId => cellId.gridId;
String get fieldId => cellId.fieldContext.id; String get fieldId => cellId.fieldInfo.id;
String get rowId => cellId.rowId; String get rowId => cellId.rowId;
Future<Either<Unit, FlowyError>> create({required String name}) { Future<Either<Unit, FlowyError>> create({required String name}) {

View File

@ -1,22 +1,26 @@
import 'dart:collection'; import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart'; import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
import 'package:app_flowy/plugins/grid/application/filter/filter_listener.dart';
import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
import 'package:app_flowy/plugins/grid/application/grid_service.dart'; import 'package:app_flowy/plugins/grid/application/grid_service.dart';
import 'package:app_flowy/plugins/grid/application/setting/setting_listener.dart'; import 'package:app_flowy/plugins/grid/application/setting/setting_listener.dart';
import 'package:app_flowy/plugins/grid/application/setting/setting_service.dart'; import 'package:app_flowy/plugins/grid/application/setting/setting_service.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../row/row_cache.dart'; import '../row/row_cache.dart';
class _GridFieldNotifier extends ChangeNotifier { class _GridFieldNotifier extends ChangeNotifier {
List<GridFieldContext> _fieldContexts = []; List<FieldInfo> _fieldInfos = [];
set fieldContexts(List<GridFieldContext> fieldContexts) { set fieldInfos(List<FieldInfo> fieldInfos) {
_fieldContexts = fieldContexts; _fieldInfos = fieldInfos;
notifyListeners(); notifyListeners();
} }
@ -24,91 +28,225 @@ class _GridFieldNotifier extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
List<GridFieldContext> get fieldContexts => _fieldContexts; List<FieldInfo> get fieldInfos => _fieldInfos;
} }
typedef OnChangeset = void Function(GridFieldChangesetPB); class _GridFilterNotifier extends ChangeNotifier {
typedef OnReceiveFields = void Function(List<GridFieldContext>); List<FilterInfo> _filters = [];
set filters(List<FilterInfo> filters) {
_filters = filters;
notifyListeners();
}
void notify() {
notifyListeners();
}
List<FilterInfo> get filters => _filters;
}
typedef OnReceiveUpdateFields = void Function(List<FieldInfo>);
typedef OnReceiveFields = void Function(List<FieldInfo>);
typedef OnReceiveFilters = void Function(List<FilterInfo>);
class GridFieldController { class GridFieldController {
final String gridId; final String gridId;
// Listeners
final GridFieldsListener _fieldListener; final GridFieldsListener _fieldListener;
final SettingListener _settingListener; final SettingListener _settingListener;
final Map<OnReceiveFields, VoidCallback> _fieldCallbackMap = {}; final FiltersListener _filterListener;
final Map<OnChangeset, OnChangeset> _changesetCallbackMap = {};
// FFI services
final GridFFIService _gridFFIService; final GridFFIService _gridFFIService;
final SettingFFIService _settingFFIService; final SettingFFIService _settingFFIService;
final FilterFFIService _filterFFIService;
// Field callbacks
final Map<OnReceiveFields, VoidCallback> _fieldCallbacks = {};
_GridFieldNotifier? _fieldNotifier = _GridFieldNotifier(); _GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
final Map<String, GridGroupConfigurationPB> _configurationByFieldId = {};
List<GridFieldContext> get fieldContexts => // Field updated callbacks
[..._fieldNotifier?.fieldContexts ?? []]; final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>
_updatedFieldCallbacks = {};
// Group callbacks
final Map<String, GroupConfigurationPB> _groupConfigurationByFieldId = {};
// Filter callbacks
final Map<OnReceiveFilters, VoidCallback> _filterCallbacks = {};
_GridFilterNotifier? _filterNotifier = _GridFilterNotifier();
final Map<String, FilterPB> _filterPBByFieldId = {};
// Getters
List<FieldInfo> get fieldInfos => [..._fieldNotifier?.fieldInfos ?? []];
List<FilterInfo> get filterInfos => [..._filterNotifier?.filters ?? []];
FieldInfo? getField(String fieldId) {
final fields = _fieldNotifier?.fieldInfos
.where((element) => element.id == fieldId)
.toList() ??
[];
if (fields.isEmpty) {
return null;
}
assert(fields.length == 1);
return fields.first;
}
FilterInfo? getFilter(String filterId) {
final filters = _filterNotifier?.filters
.where((element) => element.filter.id == filterId)
.toList() ??
[];
if (filters.isEmpty) {
return null;
}
assert(filters.length == 1);
return filters.first;
}
GridFieldController({required this.gridId}) GridFieldController({required this.gridId})
: _fieldListener = GridFieldsListener(gridId: gridId), : _fieldListener = GridFieldsListener(gridId: gridId),
_settingListener = SettingListener(gridId: gridId),
_filterListener = FiltersListener(viewId: gridId),
_gridFFIService = GridFFIService(gridId: gridId), _gridFFIService = GridFFIService(gridId: gridId),
_settingFFIService = SettingFFIService(viewId: gridId), _filterFFIService = FilterFFIService(viewId: gridId),
_settingListener = SettingListener(gridId: gridId) { _settingFFIService = SettingFFIService(viewId: gridId) {
//Listen on field's changes
_listenOnFieldChanges();
//Listen on setting changes
_listenOnSettingChanges();
//Listen on the fitler changes
_listenOnFilterChanges();
_settingFFIService.getSetting().then((result) {
result.fold(
(setting) => _updateSettingConfiguration(setting),
(err) => Log.error(err),
);
});
}
void _listenOnFilterChanges() {
//Listen on the fitler changes
_filterListener.start(onFilterChanged: (result) {
result.fold(
(changeset) {
final List<FilterInfo> filters = filterInfos;
// Deletes the filters
final deleteFilterIds =
changeset.deleteFilters.map((e) => e.id).toList();
if (deleteFilterIds.isNotEmpty) {
filters.retainWhere(
(element) => !deleteFilterIds.contains(element.filter.id),
);
_filterPBByFieldId.removeWhere(
(key, value) => deleteFilterIds.contains(value.id));
}
// Inserts the new filter if it's not exist
for (final newFilter in changeset.insertFilters) {
final filterIndex = filters
.indexWhere((element) => element.filter.id == newFilter.id);
if (filterIndex == -1) {
final fieldInfo = _findFieldInfoForFilter(fieldInfos, newFilter);
if (fieldInfo != null) {
_filterPBByFieldId[fieldInfo.id] = newFilter;
filters.add(FilterInfo(gridId, newFilter, fieldInfo));
}
}
}
for (final updatedFilter in changeset.updateFilters) {
final filterIndex = filters.indexWhere(
(element) => element.filter.id == updatedFilter.filterId,
);
// Remove the old filter
if (filterIndex != -1) {
filters.removeAt(filterIndex);
_filterPBByFieldId.removeWhere(
(key, value) => value.id == updatedFilter.filterId);
}
// Insert the filter if there is a fitler and its field info is
// not null
if (updatedFilter.hasFilter()) {
final fieldInfo = _findFieldInfoForFilter(
fieldInfos,
updatedFilter.filter,
);
if (fieldInfo != null) {
// Insert the filter with the position: filterIndex, otherwise,
// append it to the end of the list.
final filterInfo =
FilterInfo(gridId, updatedFilter.filter, fieldInfo);
if (filterIndex != -1) {
filters.insert(filterIndex, filterInfo);
} else {
filters.add(filterInfo);
}
_filterPBByFieldId[fieldInfo.id] = updatedFilter.filter;
}
}
}
_updateFieldInfos();
_filterNotifier?.filters = filters;
},
(err) => Log.error(err),
);
});
}
void _listenOnSettingChanges() {
//Listen on setting changes
_settingListener.start(onSettingUpdated: (result) {
result.fold(
(setting) => _updateSettingConfiguration(setting),
(r) => Log.error(r),
);
});
}
void _listenOnFieldChanges() {
//Listen on field's changes //Listen on field's changes
_fieldListener.start(onFieldsChanged: (result) { _fieldListener.start(onFieldsChanged: (result) {
result.fold( result.fold(
(changeset) { (changeset) {
_deleteFields(changeset.deletedFields); _deleteFields(changeset.deletedFields);
_insertFields(changeset.insertedFields); _insertFields(changeset.insertedFields);
_updateFields(changeset.updatedFields);
for (final listener in _changesetCallbackMap.values) { final updateFields = _updateFields(changeset.updatedFields);
listener(changeset); for (final listener in _updatedFieldCallbacks.values) {
listener(updateFields);
} }
}, },
(err) => Log.error(err), (err) => Log.error(err),
); );
}); });
//Listen on setting changes
_settingListener.start(onSettingUpdated: (result) {
result.fold(
(setting) => _updateGroupConfiguration(setting),
(r) => Log.error(r),
);
});
_settingFFIService.getSetting().then((result) {
result.fold(
(setting) => _updateGroupConfiguration(setting),
(err) => Log.error(err),
);
});
} }
GridFieldContext? getField(String fieldId) { void _updateSettingConfiguration(GridSettingPB setting) {
final fields = _fieldNotifier?.fieldContexts _groupConfigurationByFieldId.clear();
.where(
(element) => element.id == fieldId,
)
.toList();
if (fields?.isEmpty ?? true) {
return null;
}
return fields!.first;
}
void _updateGroupConfiguration(GridSettingPB setting) {
_configurationByFieldId.clear();
for (final configuration in setting.groupConfigurations.items) { for (final configuration in setting.groupConfigurations.items) {
_configurationByFieldId[configuration.fieldId] = configuration; _groupConfigurationByFieldId[configuration.fieldId] = configuration;
} }
_updateFieldContexts();
for (final configuration in setting.filters.items) {
_filterPBByFieldId[configuration.fieldId] = configuration;
}
_updateFieldInfos();
} }
void _updateFieldContexts() { void _updateFieldInfos() {
if (_fieldNotifier != null) { if (_fieldNotifier != null) {
for (var field in _fieldNotifier!.fieldContexts) { for (var field in _fieldNotifier!.fieldInfos) {
if (_configurationByFieldId[field.id] != null) { field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
field._isGroupField = true; field._hasFilter = _filterPBByFieldId[field.id] != null;
} else {
field._isGroupField = false;
}
} }
_fieldNotifier?.notify(); _fieldNotifier?.notify();
} }
@ -116,20 +254,33 @@ class GridFieldController {
Future<void> dispose() async { Future<void> dispose() async {
await _fieldListener.stop(); await _fieldListener.stop();
await _filterListener.stop();
await _settingListener.stop();
for (final callback in _fieldCallbacks.values) {
_fieldNotifier?.removeListener(callback);
}
_fieldNotifier?.dispose(); _fieldNotifier?.dispose();
_fieldNotifier = null; _fieldNotifier = null;
for (final callback in _filterCallbacks.values) {
_filterNotifier?.removeListener(callback);
}
_filterNotifier?.dispose();
_filterNotifier = null;
} }
Future<Either<Unit, FlowyError>> loadFields( Future<Either<Unit, FlowyError>> loadFields({
{required List<FieldIdPB> fieldIds}) async { required List<FieldIdPB> fieldIds,
}) async {
final result = await _gridFFIService.getFields(fieldIds: fieldIds); final result = await _gridFFIService.getFields(fieldIds: fieldIds);
return Future( return Future(
() => result.fold( () => result.fold(
(newFields) { (newFields) {
_fieldNotifier?.fieldContexts = newFields.items _fieldNotifier?.fieldInfos =
.map((field) => GridFieldContext(field: field)) newFields.map((field) => FieldInfo(field: field)).toList();
.toList(); _loadFilters();
_updateFieldContexts(); _updateFieldInfos();
return left(unit); return left(unit);
}, },
(err) => right(err), (err) => right(err),
@ -137,20 +288,43 @@ class GridFieldController {
); );
} }
Future<Either<Unit, FlowyError>> _loadFilters() async {
return _filterFFIService.getAllFilters().then((result) {
return result.fold(
(filterPBs) {
final List<FilterInfo> filters = [];
for (final filterPB in filterPBs) {
final fieldInfo = _findFieldInfoForFilter(fieldInfos, filterPB);
if (fieldInfo != null) {
final filterInfo = FilterInfo(gridId, filterPB, fieldInfo);
filters.add(filterInfo);
}
}
_updateFieldInfos();
_filterNotifier?.filters = filters;
return left(unit);
},
(err) => right(err),
);
});
}
void addListener({ void addListener({
OnReceiveFields? onFields, OnReceiveFields? onFields,
OnChangeset? onChangeset, OnReceiveUpdateFields? onFieldsUpdated,
OnReceiveFilters? onFilters,
bool Function()? listenWhen, bool Function()? listenWhen,
}) { }) {
if (onChangeset != null) { if (onFieldsUpdated != null) {
callback(c) { callback(List<FieldInfo> updateFields) {
if (listenWhen != null && listenWhen() == false) { if (listenWhen != null && listenWhen() == false) {
return; return;
} }
onChangeset(c); onFieldsUpdated(updateFields);
} }
_changesetCallbackMap[onChangeset] = callback; _updatedFieldCallbacks[onFieldsUpdated] = callback;
} }
if (onFields != null) { if (onFields != null) {
@ -158,27 +332,42 @@ class GridFieldController {
if (listenWhen != null && listenWhen() == false) { if (listenWhen != null && listenWhen() == false) {
return; return;
} }
onFields(fieldContexts); onFields(fieldInfos);
} }
_fieldCallbackMap[onFields] = callback; _fieldCallbacks[onFields] = callback;
_fieldNotifier?.addListener(callback); _fieldNotifier?.addListener(callback);
} }
if (onFilters != null) {
callback() {
if (listenWhen != null && listenWhen() == false) {
return;
}
onFilters(filterInfos);
}
_filterCallbacks[onFilters] = callback;
_filterNotifier?.addListener(callback);
}
} }
void removeListener({ void removeListener({
OnReceiveFields? onFieldsListener, OnReceiveFields? onFieldsListener,
OnChangeset? onChangesetListener, OnReceiveFilters? onFiltersListener,
OnReceiveUpdateFields? onChangesetListener,
}) { }) {
if (onFieldsListener != null) { if (onFieldsListener != null) {
final callback = _fieldCallbackMap.remove(onFieldsListener); final callback = _fieldCallbacks.remove(onFieldsListener);
if (callback != null) { if (callback != null) {
_fieldNotifier?.removeListener(callback); _fieldNotifier?.removeListener(callback);
} }
} }
if (onFiltersListener != null) {
if (onChangesetListener != null) { final callback = _filterCallbacks.remove(onFiltersListener);
_changesetCallbackMap.remove(onChangesetListener); if (callback != null) {
_filterNotifier?.removeListener(callback);
}
} }
} }
@ -186,58 +375,65 @@ class GridFieldController {
if (deletedFields.isEmpty) { if (deletedFields.isEmpty) {
return; return;
} }
final List<GridFieldContext> newFields = fieldContexts; final List<FieldInfo> newFields = fieldInfos;
final Map<String, FieldIdPB> deletedFieldMap = { final Map<String, FieldIdPB> deletedFieldMap = {
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
}; };
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null)); newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
_fieldNotifier?.fieldContexts = newFields; _fieldNotifier?.fieldInfos = newFields;
} }
void _insertFields(List<IndexFieldPB> insertedFields) { void _insertFields(List<IndexFieldPB> insertedFields) {
if (insertedFields.isEmpty) { if (insertedFields.isEmpty) {
return; return;
} }
final List<GridFieldContext> newFields = fieldContexts; final List<FieldInfo> newFields = fieldInfos;
for (final indexField in insertedFields) { for (final indexField in insertedFields) {
final gridField = GridFieldContext(field: indexField.field_1); final gridField = FieldInfo(field: indexField.field_1);
if (newFields.length > indexField.index) { if (newFields.length > indexField.index) {
newFields.insert(indexField.index, gridField); newFields.insert(indexField.index, gridField);
} else { } else {
newFields.add(gridField); newFields.add(gridField);
} }
} }
_fieldNotifier?.fieldContexts = newFields; _fieldNotifier?.fieldInfos = newFields;
} }
void _updateFields(List<FieldPB> updatedFields) { List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {
if (updatedFields.isEmpty) { if (updatedFieldPBs.isEmpty) {
return; return [];
} }
final List<GridFieldContext> newFields = fieldContexts;
for (final updatedField in updatedFields) { final List<FieldInfo> newFields = fieldInfos;
final List<FieldInfo> updatedFields = [];
for (final updatedFieldPB in updatedFieldPBs) {
final index = final index =
newFields.indexWhere((field) => field.id == updatedField.id); newFields.indexWhere((field) => field.id == updatedFieldPB.id);
if (index != -1) { if (index != -1) {
newFields.removeAt(index); newFields.removeAt(index);
final gridField = GridFieldContext(field: updatedField); final fieldInfo = FieldInfo(field: updatedFieldPB);
newFields.insert(index, gridField); newFields.insert(index, fieldInfo);
updatedFields.add(fieldInfo);
} }
} }
_fieldNotifier?.fieldContexts = newFields;
if (updatedFields.isNotEmpty) {
_fieldNotifier?.fieldInfos = newFields;
}
return updatedFields;
} }
} }
class GridRowFieldNotifierImpl extends IGridRowFieldNotifier { class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
final GridFieldController _cache; final GridFieldController _cache;
OnChangeset? _onChangesetFn; OnReceiveUpdateFields? _onChangesetFn;
OnReceiveFields? _onFieldFn; OnReceiveFields? _onFieldFn;
GridRowFieldNotifierImpl(GridFieldController cache) : _cache = cache; GridRowFieldNotifierImpl(GridFieldController cache) : _cache = cache;
@override @override
UnmodifiableListView<GridFieldContext> get fields => UnmodifiableListView<FieldInfo> get fields =>
UnmodifiableListView(_cache.fieldContexts); UnmodifiableListView(_cache.fieldInfos);
@override @override
void onRowFieldsChanged(VoidCallback callback) { void onRowFieldsChanged(VoidCallback callback) {
@ -246,14 +442,14 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
} }
@override @override
void onRowFieldChanged(void Function(FieldPB) callback) { void onRowFieldChanged(void Function(FieldInfo) callback) {
_onChangesetFn = (GridFieldChangesetPB changeset) { _onChangesetFn = (List<FieldInfo> fieldInfos) {
for (final updatedField in changeset.updatedFields) { for (final updatedField in fieldInfos) {
callback(updatedField); callback(updatedField);
} }
}; };
_cache.addListener(onChangeset: _onChangesetFn); _cache.addListener(onFieldsUpdated: _onChangesetFn);
} }
@override @override
@ -270,10 +466,25 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
} }
} }
class GridFieldContext { FieldInfo? _findFieldInfoForFilter(
List<FieldInfo> fieldInfos, FilterPB filter) {
final fieldIndex = fieldInfos.indexWhere((element) {
return element.id == filter.fieldId &&
element.fieldType == filter.fieldType;
});
if (fieldIndex != -1) {
return fieldInfos[fieldIndex];
} else {
return null;
}
}
class FieldInfo {
final FieldPB _field; final FieldPB _field;
bool _isGroupField = false; bool _isGroupField = false;
bool _hasFilter = false;
String get id => _field.id; String get id => _field.id;
FieldType get fieldType => _field.fieldType; FieldType get fieldType => _field.fieldType;
@ -290,6 +501,8 @@ class GridFieldContext {
bool get isGroupField => _isGroupField; bool get isGroupField => _isGroupField;
bool get hasFilter => _hasFilter;
bool get canGroup { bool get canGroup {
switch (_field.fieldType) { switch (_field.fieldType) {
case FieldType.Checkbox: case FieldType.Checkbox:
@ -311,5 +524,19 @@ class GridFieldContext {
return false; return false;
} }
GridFieldContext({required FieldPB field}) : _field = field; bool get canCreateFilter {
if (hasFilter) return false;
switch (_field.fieldType) {
case FieldType.Checkbox:
// case FieldType.MultiSelect:
case FieldType.RichText:
// case FieldType.SingleSelect:
return true;
default:
return false;
}
}
FieldInfo({required FieldPB field}) : _field = field;
} }

View File

@ -34,7 +34,6 @@ class FieldService {
bool? frozen, bool? frozen,
bool? visibility, bool? visibility,
double? width, double? width,
List<int>? typeOptionData,
}) { }) {
var payload = FieldChangesetPB.create() var payload = FieldChangesetPB.create()
..gridId = gridId ..gridId = gridId
@ -60,10 +59,6 @@ class FieldService {
payload.width = width.toInt(); payload.width = width.toInt();
} }
if (typeOptionData != null) {
payload.typeOptionData = typeOptionData;
}
return GridEventUpdateField(payload).send(); return GridEventUpdateField(payload).send();
} }

View File

@ -4,7 +4,7 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo;
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'type_option_context.dart'; import 'type_option_context.dart';
@ -18,18 +18,18 @@ class TypeOptionDataController {
/// Returns a [TypeOptionDataController] used to modify the specified /// Returns a [TypeOptionDataController] used to modify the specified
/// [FieldPB]'s data /// [FieldPB]'s data
/// ///
/// Should call [loadTypeOptionData] if the passed-in [GridFieldContext] /// Should call [loadTypeOptionData] if the passed-in [FieldInfo]
/// is null /// is null
/// ///
TypeOptionDataController({ TypeOptionDataController({
required this.gridId, required this.gridId,
required this.loader, required this.loader,
GridFieldContext? fieldContext, FieldInfo? fieldInfo,
}) { }) {
if (fieldContext != null) { if (fieldInfo != null) {
_data = TypeOptionPB.create() _data = TypeOptionPB.create()
..gridId = gridId ..gridId = gridId
..field_2 = fieldContext.field; ..field_2 = fieldInfo.field;
} }
} }

View File

@ -0,0 +1,99 @@
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'filter_listener.dart';
import 'filter_service.dart';
part 'checkbox_filter_editor_bloc.freezed.dart';
class CheckboxFilterEditorBloc
extends Bloc<CheckboxFilterEditorEvent, CheckboxFilterEditorState> {
final FilterInfo filterInfo;
final FilterFFIService _ffiService;
final FilterListener _listener;
CheckboxFilterEditorBloc({required this.filterInfo})
: _ffiService = FilterFFIService(viewId: filterInfo.viewId),
_listener = FilterListener(
viewId: filterInfo.viewId,
filterId: filterInfo.filter.id,
),
super(CheckboxFilterEditorState.initial(filterInfo)) {
on<CheckboxFilterEditorEvent>(
(event, emit) async {
event.when(
initial: () async {
_startListening();
},
updateCondition: (CheckboxFilterCondition condition) {
_ffiService.insertCheckboxFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.field.id,
condition: condition,
);
},
delete: () {
_ffiService.deleteFilter(
fieldId: filterInfo.field.id,
filterId: filterInfo.filter.id,
fieldType: filterInfo.field.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
final filterInfo = state.filterInfo.copyWith(filter: filter);
final checkboxFilter = filterInfo.checkboxFilter()!;
emit(state.copyWith(
filterInfo: filterInfo,
filter: checkboxFilter,
));
},
);
},
);
}
void _startListening() {
_listener.start(
onDeleted: () {
if (!isClosed) add(const CheckboxFilterEditorEvent.delete());
},
onUpdated: (filter) {
if (!isClosed) add(CheckboxFilterEditorEvent.didReceiveFilter(filter));
},
);
}
@override
Future<void> close() async {
await _listener.stop();
return super.close();
}
}
@freezed
class CheckboxFilterEditorEvent with _$CheckboxFilterEditorEvent {
const factory CheckboxFilterEditorEvent.initial() = _Initial;
const factory CheckboxFilterEditorEvent.didReceiveFilter(FilterPB filter) =
_DidReceiveFilter;
const factory CheckboxFilterEditorEvent.updateCondition(
CheckboxFilterCondition condition) = _UpdateCondition;
const factory CheckboxFilterEditorEvent.delete() = _Delete;
}
@freezed
class CheckboxFilterEditorState with _$CheckboxFilterEditorState {
const factory CheckboxFilterEditorState({
required FilterInfo filterInfo,
required CheckboxFilterPB filter,
}) = _GridFilterState;
factory CheckboxFilterEditorState.initial(FilterInfo filterInfo) {
return CheckboxFilterEditorState(
filterInfo: filterInfo,
filter: filterInfo.checkboxFilter()!,
);
}
}

View File

@ -1,206 +0,0 @@
import 'package:app_flowy/plugins/grid/application/filter/filter_listener.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/number_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'filter_service.dart';
part 'filter_bloc.freezed.dart';
class GridFilterBloc extends Bloc<GridFilterEvent, GridFilterState> {
final String viewId;
final FilterFFIService _ffiService;
final FilterListener _listener;
GridFilterBloc({required this.viewId})
: _ffiService = FilterFFIService(viewId: viewId),
_listener = FilterListener(viewId: viewId),
super(GridFilterState.initial()) {
on<GridFilterEvent>(
(event, emit) async {
event.when(
initial: () async {
_startListening();
await _loadFilters();
},
deleteFilter: (
String fieldId,
String filterId,
FieldType fieldType,
) {
_ffiService.deleteFilter(
fieldId: fieldId,
filterId: filterId,
fieldType: fieldType,
);
},
didReceiveFilters: (filters) {
emit(state.copyWith(filters: filters));
},
createCheckboxFilter: (
String fieldId,
CheckboxFilterCondition condition,
) {
_ffiService.createCheckboxFilter(
fieldId: fieldId,
condition: condition,
);
},
createNumberFilter: (
String fieldId,
NumberFilterCondition condition,
String content,
) {
_ffiService.createNumberFilter(
fieldId: fieldId,
condition: condition,
content: content,
);
},
createTextFilter: (
String fieldId,
TextFilterCondition condition,
String content,
) {
_ffiService.createTextFilter(
fieldId: fieldId,
condition: condition,
);
},
createDateFilter: (
String fieldId,
DateFilterCondition condition,
int timestamp,
) {
_ffiService.createDateFilter(
fieldId: fieldId,
condition: condition,
timestamp: timestamp,
);
},
createDateFilterInRange: (
String fieldId,
DateFilterCondition condition,
int start,
int end,
) {
_ffiService.createDateFilter(
fieldId: fieldId,
condition: condition,
start: start,
end: end,
);
},
);
},
);
}
void _startListening() {
_listener.start(onFilterChanged: (result) {
result.fold(
(changeset) {
final List<FilterPB> filters = List.from(state.filters);
// Deletes the filters
final deleteFilterIds =
changeset.deleteFilters.map((e) => e.id).toList();
filters.retainWhere(
(element) => !deleteFilterIds.contains(element.id),
);
// Inserts the new filter if it's not exist
for (final newFilter in changeset.insertFilters) {
final index =
filters.indexWhere((element) => element.id == newFilter.id);
if (index == -1) {
filters.add(newFilter);
}
}
if (!isClosed) {
add(GridFilterEvent.didReceiveFilters(filters));
}
},
(err) => Log.error(err),
);
});
}
Future<void> _loadFilters() async {
final result = await _ffiService.getAllFilters();
result.fold(
(filters) {
if (!isClosed) {
add(GridFilterEvent.didReceiveFilters(filters));
}
},
(err) => Log.error(err),
);
}
@override
Future<void> close() async {
await _listener.stop();
return super.close();
}
}
@freezed
class GridFilterEvent with _$GridFilterEvent {
const factory GridFilterEvent.initial() = _Initial;
const factory GridFilterEvent.didReceiveFilters(List<FilterPB> filters) =
_DidReceiveFilters;
const factory GridFilterEvent.deleteFilter({
required String fieldId,
required String filterId,
required FieldType fieldType,
}) = _DeleteFilter;
const factory GridFilterEvent.createTextFilter({
required String fieldId,
required TextFilterCondition condition,
required String content,
}) = _CreateTextFilter;
const factory GridFilterEvent.createCheckboxFilter({
required String fieldId,
required CheckboxFilterCondition condition,
}) = _CreateCheckboxFilter;
const factory GridFilterEvent.createNumberFilter({
required String fieldId,
required NumberFilterCondition condition,
required String content,
}) = _CreateCheckboxFitler;
const factory GridFilterEvent.createDateFilter({
required String fieldId,
required DateFilterCondition condition,
required int start,
}) = _CreateDateFitler;
const factory GridFilterEvent.createDateFilterInRange({
required String fieldId,
required DateFilterCondition condition,
required int start,
required int end,
}) = _CreateDateFitlerInRange;
}
@freezed
class GridFilterState with _$GridFilterState {
const factory GridFilterState({
required List<FilterPB> filters,
}) = _GridFilterState;
factory GridFilterState.initial() => const GridFilterState(
filters: [],
);
}

View File

@ -0,0 +1,179 @@
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pbserver.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/number_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'filter_service.dart';
part 'filter_create_bloc.freezed.dart';
class GridCreateFilterBloc
extends Bloc<GridCreateFilterEvent, GridCreateFilterState> {
final String viewId;
final FilterFFIService _ffiService;
final GridFieldController fieldController;
void Function(List<FieldInfo>)? _onFieldFn;
GridCreateFilterBloc({required this.viewId, required this.fieldController})
: _ffiService = FilterFFIService(viewId: viewId),
super(GridCreateFilterState.initial(fieldController.fieldInfos)) {
on<GridCreateFilterEvent>(
(event, emit) async {
event.when(
initial: () async {
_startListening();
},
didReceiveFields: (List<FieldInfo> fields) {
emit(
state.copyWith(
allFields: fields,
creatableFields: _filterFields(fields, state.filterText),
),
);
},
didReceiveFilterText: (String text) {
emit(
state.copyWith(
filterText: text,
creatableFields: _filterFields(state.allFields, text),
),
);
},
createDefaultFilter: (FieldInfo field) {
emit(state.copyWith(didCreateFilter: true));
_createDefaultFilter(field);
},
);
},
);
}
List<FieldInfo> _filterFields(
List<FieldInfo> fields,
String filterText,
) {
final List<FieldInfo> allFields = List.from(fields);
final keyword = filterText.toLowerCase();
allFields.retainWhere((field) {
if (field.canCreateFilter) {
return false;
}
if (filterText.isNotEmpty) {
return field.name.toLowerCase().contains(keyword);
}
return true;
});
return allFields;
}
void _startListening() {
_onFieldFn = (fields) {
fields.retainWhere((field) => field.canCreateFilter);
add(GridCreateFilterEvent.didReceiveFields(fields));
};
fieldController.addListener(onFields: _onFieldFn);
}
Future<Either<Unit, FlowyError>> _createDefaultFilter(FieldInfo field) async {
final fieldId = field.id;
switch (field.fieldType) {
case FieldType.Checkbox:
return _ffiService.insertCheckboxFilter(
fieldId: fieldId,
condition: CheckboxFilterCondition.IsChecked,
);
case FieldType.DateTime:
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
return _ffiService.insertDateFilter(
fieldId: fieldId,
condition: DateFilterCondition.DateIs,
timestamp: timestamp,
);
case FieldType.MultiSelect:
return _ffiService.insertSingleSelectFilter(
fieldId: fieldId,
condition: SelectOptionCondition.OptionIs,
);
case FieldType.Number:
return _ffiService.insertNumberFilter(
fieldId: fieldId,
condition: NumberFilterCondition.Equal,
content: "",
);
case FieldType.RichText:
return _ffiService.insertTextFilter(
fieldId: fieldId,
condition: TextFilterCondition.Contains,
content: '',
);
case FieldType.SingleSelect:
return _ffiService.insertSingleSelectFilter(
fieldId: fieldId,
condition: SelectOptionCondition.OptionIs,
);
case FieldType.URL:
return _ffiService.insertURLFilter(
fieldId: fieldId,
condition: TextFilterCondition.Contains,
);
}
return left(unit);
}
@override
Future<void> close() async {
if (_onFieldFn != null) {
fieldController.removeListener(onFieldsListener: _onFieldFn);
_onFieldFn = null;
}
return super.close();
}
}
@freezed
class GridCreateFilterEvent with _$GridCreateFilterEvent {
const factory GridCreateFilterEvent.initial() = _Initial;
const factory GridCreateFilterEvent.didReceiveFields(List<FieldInfo> fields) =
_DidReceiveFields;
const factory GridCreateFilterEvent.createDefaultFilter(FieldInfo field) =
_CreateDefaultFilter;
const factory GridCreateFilterEvent.didReceiveFilterText(String text) =
_DidReceiveFilterText;
}
@freezed
class GridCreateFilterState with _$GridCreateFilterState {
const factory GridCreateFilterState({
required String filterText,
required List<FieldInfo> creatableFields,
required List<FieldInfo> allFields,
required bool didCreateFilter,
}) = _GridFilterState;
factory GridCreateFilterState.initial(List<FieldInfo> fields) {
return GridCreateFilterState(
filterText: "",
creatableFields: getCreatableFilter(fields),
allFields: fields,
didCreateFilter: false,
);
}
}
List<FieldInfo> getCreatableFilter(List<FieldInfo> fieldInfos) {
final List<FieldInfo> creatableFields = List.from(fieldInfos);
creatableFields.retainWhere((element) => element.canCreateFilter);
return creatableFields;
}

View File

@ -6,17 +6,18 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/filter_changeset.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/filter_changeset.pb.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
typedef UpdateFilterNotifiedValue typedef UpdateFilterNotifiedValue
= Either<FilterChangesetNotificationPB, FlowyError>; = Either<FilterChangesetNotificationPB, FlowyError>;
class FilterListener { class FiltersListener {
final String viewId; final String viewId;
PublishNotifier<UpdateFilterNotifiedValue>? _filterNotifier = PublishNotifier<UpdateFilterNotifiedValue>? _filterNotifier =
PublishNotifier(); PublishNotifier();
GridNotificationListener? _listener; GridNotificationListener? _listener;
FilterListener({required this.viewId}); FiltersListener({required this.viewId});
void start({ void start({
required void Function(UpdateFilterNotifiedValue) onFilterChanged, required void Function(UpdateFilterNotifiedValue) onFilterChanged,
@ -51,3 +52,76 @@ class FilterListener {
_filterNotifier = null; _filterNotifier = null;
} }
} }
class FilterListener {
final String viewId;
final String filterId;
PublishNotifier<FilterPB>? _onDeleteNotifier = PublishNotifier();
PublishNotifier<FilterPB>? _onUpdateNotifier = PublishNotifier();
GridNotificationListener? _listener;
FilterListener({required this.viewId, required this.filterId});
void start({
void Function()? onDeleted,
void Function(FilterPB)? onUpdated,
}) {
_onDeleteNotifier?.addPublishListener((_) {
onDeleted?.call();
});
_onUpdateNotifier?.addPublishListener((filter) {
onUpdated?.call(filter);
});
_listener = GridNotificationListener(
objectId: viewId,
handler: _handler,
);
}
void handleChangeset(FilterChangesetNotificationPB changeset) {
// check the delete filter
final deletedIndex = changeset.deleteFilters.indexWhere(
(element) => element.id == filterId,
);
if (deletedIndex != -1) {
_onDeleteNotifier?.value = changeset.deleteFilters[deletedIndex];
}
// check the updated filter
final updatedIndex = changeset.updateFilters.indexWhere(
(element) => element.filter.id == filterId,
);
if (updatedIndex != -1) {
_onUpdateNotifier?.value = changeset.updateFilters[updatedIndex].filter;
}
}
void _handler(
GridDartNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case GridDartNotification.DidUpdateFilter:
result.fold(
(payload) => handleChangeset(
FilterChangesetNotificationPB.fromBuffer(payload)),
(error) {},
);
break;
default:
break;
}
}
Future<void> stop() async {
await _listener?.stop();
_onDeleteNotifier?.dispose();
_onDeleteNotifier = null;
_onUpdateNotifier?.dispose();
_onUpdateNotifier = null;
}
}

View File

@ -0,0 +1,119 @@
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'filter_menu_bloc.freezed.dart';
class GridFilterMenuBloc
extends Bloc<GridFilterMenuEvent, GridFilterMenuState> {
final String viewId;
final GridFieldController fieldController;
void Function(List<FilterInfo>)? _onFilterFn;
void Function(List<FieldInfo>)? _onFieldFn;
GridFilterMenuBloc({required this.viewId, required this.fieldController})
: super(GridFilterMenuState.initial(
viewId,
fieldController.filterInfos,
fieldController.fieldInfos,
)) {
on<GridFilterMenuEvent>(
(event, emit) async {
event.when(
initial: () {
_startListening();
},
didReceiveFilters: (filters) {
emit(state.copyWith(filters: filters));
},
toggleMenu: () {
final isVisible = !state.isVisible;
emit(state.copyWith(isVisible: isVisible));
},
didReceiveFields: (List<FieldInfo> fields) {
emit(
state.copyWith(
fields: fields,
creatableFields: getCreatableFilter(fields),
),
);
},
);
},
);
}
void _startListening() {
_onFilterFn = (filters) {
add(GridFilterMenuEvent.didReceiveFilters(filters));
};
_onFieldFn = (fields) {
add(GridFilterMenuEvent.didReceiveFields(fields));
};
fieldController.addListener(
onFilters: (filters) {
_onFilterFn?.call(filters);
},
onFields: (fields) {
_onFieldFn?.call(fields);
},
);
}
@override
Future<void> close() {
if (_onFilterFn != null) {
fieldController.removeListener(onFiltersListener: _onFilterFn!);
_onFilterFn = null;
}
if (_onFieldFn != null) {
fieldController.removeListener(onFieldsListener: _onFieldFn!);
_onFieldFn = null;
}
return super.close();
}
}
@freezed
class GridFilterMenuEvent with _$GridFilterMenuEvent {
const factory GridFilterMenuEvent.initial() = _Initial;
const factory GridFilterMenuEvent.didReceiveFilters(
List<FilterInfo> filters) = _DidReceiveFilters;
const factory GridFilterMenuEvent.didReceiveFields(List<FieldInfo> fields) =
_DidReceiveFields;
const factory GridFilterMenuEvent.toggleMenu() = _SetMenuVisibility;
}
@freezed
class GridFilterMenuState with _$GridFilterMenuState {
const factory GridFilterMenuState({
required String viewId,
required List<FilterInfo> filters,
required List<FieldInfo> fields,
required List<FieldInfo> creatableFields,
required bool isVisible,
}) = _GridFilterMenuState;
factory GridFilterMenuState.initial(
String viewId,
List<FilterInfo> filterInfos,
List<FieldInfo> fields,
) =>
GridFilterMenuState(
viewId: viewId,
filters: filterInfos,
fields: fields,
creatableFields: getCreatableFilter(fields),
isVisible: false,
);
}
List<FieldInfo> getCreatableFilter(List<FieldInfo> fieldInfos) {
final List<FieldInfo> creatableFields = List.from(fieldInfos);
creatableFields.retainWhere((element) => element.canCreateFilter);
return creatableFields;
}

View File

@ -28,37 +28,42 @@ class FilterFFIService {
}); });
} }
Future<Either<Unit, FlowyError>> createTextFilter({ Future<Either<Unit, FlowyError>> insertTextFilter({
required String fieldId, required String fieldId,
String? filterId,
required TextFilterCondition condition, required TextFilterCondition condition,
String content = "", required String content,
}) { }) {
final filter = TextFilterPB() final filter = TextFilterPB()
..condition = condition ..condition = condition
..content = content; ..content = content;
return createFilter( return insertFilter(
fieldId: fieldId, fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.RichText, fieldType: FieldType.RichText,
data: filter.writeToBuffer(), data: filter.writeToBuffer(),
); );
} }
Future<Either<Unit, FlowyError>> createCheckboxFilter({ Future<Either<Unit, FlowyError>> insertCheckboxFilter({
required String fieldId, required String fieldId,
String? filterId,
required CheckboxFilterCondition condition, required CheckboxFilterCondition condition,
}) { }) {
final filter = CheckboxFilterPB()..condition = condition; final filter = CheckboxFilterPB()..condition = condition;
return createFilter( return insertFilter(
fieldId: fieldId, fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.Checkbox, fieldType: FieldType.Checkbox,
data: filter.writeToBuffer(), data: filter.writeToBuffer(),
); );
} }
Future<Either<Unit, FlowyError>> createNumberFilter({ Future<Either<Unit, FlowyError>> insertNumberFilter({
required String fieldId, required String fieldId,
String? filterId,
required NumberFilterCondition condition, required NumberFilterCondition condition,
String content = "", String content = "",
}) { }) {
@ -66,15 +71,17 @@ class FilterFFIService {
..condition = condition ..condition = condition
..content = content; ..content = content;
return createFilter( return insertFilter(
fieldId: fieldId, fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.Number, fieldType: FieldType.Number,
data: filter.writeToBuffer(), data: filter.writeToBuffer(),
); );
} }
Future<Either<Unit, FlowyError>> createDateFilter({ Future<Either<Unit, FlowyError>> insertDateFilter({
required String fieldId, required String fieldId,
String? filterId,
required DateFilterCondition condition, required DateFilterCondition condition,
int? start, int? start,
int? end, int? end,
@ -93,15 +100,17 @@ class FilterFFIService {
} }
} }
return createFilter( return insertFilter(
fieldId: fieldId, fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.DateTime, fieldType: FieldType.DateTime,
data: filter.writeToBuffer(), data: filter.writeToBuffer(),
); );
} }
Future<Either<Unit, FlowyError>> createURLFilter({ Future<Either<Unit, FlowyError>> insertURLFilter({
required String fieldId, required String fieldId,
String? filterId,
required TextFilterCondition condition, required TextFilterCondition condition,
String content = "", String content = "",
}) { }) {
@ -109,15 +118,17 @@ class FilterFFIService {
..condition = condition ..condition = condition
..content = content; ..content = content;
return createFilter( return insertFilter(
fieldId: fieldId, fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.URL, fieldType: FieldType.URL,
data: filter.writeToBuffer(), data: filter.writeToBuffer(),
); );
} }
Future<Either<Unit, FlowyError>> createSingleSelectFilter({ Future<Either<Unit, FlowyError>> insertSingleSelectFilter({
required String fieldId, required String fieldId,
String? filterId,
required SelectOptionCondition condition, required SelectOptionCondition condition,
List<String> optionIds = const [], List<String> optionIds = const [],
}) { }) {
@ -125,15 +136,17 @@ class FilterFFIService {
..condition = condition ..condition = condition
..optionIds.addAll(optionIds); ..optionIds.addAll(optionIds);
return createFilter( return insertFilter(
fieldId: fieldId, fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.SingleSelect, fieldType: FieldType.SingleSelect,
data: filter.writeToBuffer(), data: filter.writeToBuffer(),
); );
} }
Future<Either<Unit, FlowyError>> createMultiSelectFilter({ Future<Either<Unit, FlowyError>> insertMultiSelectFilter({
required String fieldId, required String fieldId,
String? filterId,
required SelectOptionCondition condition, required SelectOptionCondition condition,
List<String> optionIds = const [], List<String> optionIds = const [],
}) { }) {
@ -141,25 +154,31 @@ class FilterFFIService {
..condition = condition ..condition = condition
..optionIds.addAll(optionIds); ..optionIds.addAll(optionIds);
return createFilter( return insertFilter(
fieldId: fieldId, fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.MultiSelect, fieldType: FieldType.MultiSelect,
data: filter.writeToBuffer(), data: filter.writeToBuffer(),
); );
} }
Future<Either<Unit, FlowyError>> createFilter({ Future<Either<Unit, FlowyError>> insertFilter({
required String fieldId, required String fieldId,
String? filterId,
required FieldType fieldType, required FieldType fieldType,
required List<int> data, required List<int> data,
}) { }) {
TextFilterCondition.DoesNotContain.value; TextFilterCondition.DoesNotContain.value;
final insertFilterPayload = CreateFilterPayloadPB.create() var insertFilterPayload = AlterFilterPayloadPB.create()
..fieldId = fieldId ..fieldId = fieldId
..fieldType = fieldType ..fieldType = fieldType
..data = data; ..data = data;
if (filterId != null) {
insertFilterPayload.filterId = filterId;
}
final payload = GridSettingChangesetPB.create() final payload = GridSettingChangesetPB.create()
..gridId = viewId ..gridId = viewId
..insertFilter = insertFilterPayload; ..insertFilter = insertFilterPayload;
@ -189,6 +208,7 @@ class FilterFFIService {
final payload = GridSettingChangesetPB.create() final payload = GridSettingChangesetPB.create()
..gridId = viewId ..gridId = viewId
..deleteFilter = deleteFilterPayload; ..deleteFilter = deleteFilterPayload;
return GridEventUpdateGridSetting(payload).send().then((result) { return GridEventUpdateGridSetting(payload).send().then((result) {
return result.fold( return result.fold(
(l) => left(l), (l) => left(l),

View File

@ -0,0 +1,110 @@
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pbserver.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'filter_listener.dart';
import 'filter_service.dart';
part 'text_filter_editor_bloc.freezed.dart';
class TextFilterEditorBloc
extends Bloc<TextFilterEditorEvent, TextFilterEditorState> {
final FilterInfo filterInfo;
final FilterFFIService _ffiService;
final FilterListener _listener;
TextFilterEditorBloc({required this.filterInfo})
: _ffiService = FilterFFIService(viewId: filterInfo.viewId),
_listener = FilterListener(
viewId: filterInfo.viewId,
filterId: filterInfo.filter.id,
),
super(TextFilterEditorState.initial(filterInfo)) {
on<TextFilterEditorEvent>(
(event, emit) async {
event.when(
initial: () async {
_startListening();
},
updateCondition: (TextFilterCondition condition) {
_ffiService.insertTextFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.field.id,
condition: condition,
content: state.filter.content,
);
},
updateContent: (content) {
_ffiService.insertTextFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.field.id,
condition: state.filter.condition,
content: content,
);
},
delete: () {
_ffiService.deleteFilter(
fieldId: filterInfo.field.id,
filterId: filterInfo.filter.id,
fieldType: filterInfo.field.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
final filterInfo = state.filterInfo.copyWith(filter: filter);
final textFilter = filterInfo.textFilter()!;
emit(state.copyWith(
filterInfo: filterInfo,
filter: textFilter,
));
},
);
},
);
}
void _startListening() {
_listener.start(
onDeleted: () {
if (!isClosed) add(const TextFilterEditorEvent.delete());
},
onUpdated: (filter) {
if (!isClosed) add(TextFilterEditorEvent.didReceiveFilter(filter));
},
);
}
@override
Future<void> close() async {
await _listener.stop();
return super.close();
}
}
@freezed
class TextFilterEditorEvent with _$TextFilterEditorEvent {
const factory TextFilterEditorEvent.initial() = _Initial;
const factory TextFilterEditorEvent.didReceiveFilter(FilterPB filter) =
_DidReceiveFilter;
const factory TextFilterEditorEvent.updateCondition(
TextFilterCondition condition) = _UpdateCondition;
const factory TextFilterEditorEvent.updateContent(String content) =
_UpdateContent;
const factory TextFilterEditorEvent.delete() = _Delete;
}
@freezed
class TextFilterEditorState with _$TextFilterEditorState {
const factory TextFilterEditorState({
required FilterInfo filterInfo,
required TextFilterPB filter,
}) = _GridFilterState;
factory TextFilterEditorState.initial(FilterInfo filterInfo) {
return TextFilterEditorState(
filterInfo: filterInfo,
filter: filterInfo.textFilter()!,
);
}
}

View File

@ -16,12 +16,11 @@ import 'row/row_service.dart';
part 'grid_bloc.freezed.dart'; part 'grid_bloc.freezed.dart';
class GridBloc extends Bloc<GridEvent, GridState> { class GridBloc extends Bloc<GridEvent, GridState> {
final GridDataController dataController; final GridController gridController;
void Function()? _createRowOperation; void Function()? _createRowOperation;
GridBloc({required ViewPB view}) GridBloc({required ViewPB view, required this.gridController})
: dataController = GridDataController(view: view), : super(GridState.initial(view.id)) {
super(GridState.initial(view.id)) {
on<GridEvent>( on<GridEvent>(
(event, emit) async { (event, emit) async {
await event.when( await event.when(
@ -32,9 +31,9 @@ class GridBloc extends Bloc<GridEvent, GridState> {
createRow: () { createRow: () {
state.loadingState.when( state.loadingState.when(
loading: () { loading: () {
_createRowOperation = () => dataController.createRow(); _createRowOperation = () => gridController.createRow();
}, },
finish: (_) => dataController.createRow(), finish: (_) => gridController.createRow(),
); );
}, },
deleteRow: (rowInfo) async { deleteRow: (rowInfo) async {
@ -66,17 +65,17 @@ class GridBloc extends Bloc<GridEvent, GridState> {
@override @override
Future<void> close() async { Future<void> close() async {
await dataController.dispose(); await gridController.dispose();
return super.close(); return super.close();
} }
GridRowCache? getRowCache(String blockId, String rowId) { GridRowCache? getRowCache(String blockId, String rowId) {
final GridBlockCache? blockCache = dataController.blocks[blockId]; final GridBlockCache? blockCache = gridController.blocks[blockId];
return blockCache?.rowCache; return blockCache?.rowCache;
} }
void _startListening() { void _startListening() {
dataController.addListener( gridController.addListener(
onGridChanged: (grid) { onGridChanged: (grid) {
if (!isClosed) { if (!isClosed) {
add(GridEvent.didReceiveGridUpdate(grid)); add(GridEvent.didReceiveGridUpdate(grid));
@ -96,7 +95,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
} }
Future<void> _openGrid(Emitter<GridState> emit) async { Future<void> _openGrid(Emitter<GridState> emit) async {
final result = await dataController.openGrid(); final result = await gridController.openGrid();
result.fold( result.fold(
(grid) { (grid) {
if (_createRowOperation != null) { if (_createRowOperation != null) {
@ -124,7 +123,7 @@ class GridEvent with _$GridEvent {
RowsChangedReason listState, RowsChangedReason listState,
) = _DidReceiveRowUpdate; ) = _DidReceiveRowUpdate;
const factory GridEvent.didReceiveFieldUpdate( const factory GridEvent.didReceiveFieldUpdate(
UnmodifiableListView<GridFieldContext> fields, List<FieldInfo> fields,
) = _DidReceiveFieldUpdate; ) = _DidReceiveFieldUpdate;
const factory GridEvent.didReceiveGridUpdate( const factory GridEvent.didReceiveGridUpdate(
@ -163,9 +162,9 @@ class GridLoadingState with _$GridLoadingState {
} }
class GridFieldEquatable extends Equatable { class GridFieldEquatable extends Equatable {
final UnmodifiableListView<GridFieldContext> _fields; final List<FieldInfo> _fields;
const GridFieldEquatable( const GridFieldEquatable(
UnmodifiableListView<GridFieldContext> fields, List<FieldInfo> fields,
) : _fields = fields; ) : _fields = fields;
@override @override
@ -182,6 +181,5 @@ class GridFieldEquatable extends Equatable {
]; ];
} }
UnmodifiableListView<GridFieldContext> get value => UnmodifiableListView<FieldInfo> get value => UnmodifiableListView(_fields);
UnmodifiableListView(_fields);
} }

View File

@ -1,5 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
@ -12,7 +13,8 @@ import 'field/field_controller.dart';
import 'prelude.dart'; import 'prelude.dart';
import 'row/row_cache.dart'; import 'row/row_cache.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>); typedef OnFieldsChanged = void Function(List<FieldInfo>);
typedef OnFiltersChanged = void Function(List<FilterInfo>);
typedef OnGridChanged = void Function(GridPB); typedef OnGridChanged = void Function(GridPB);
typedef OnRowsChanged = void Function( typedef OnRowsChanged = void Function(
@ -21,20 +23,19 @@ typedef OnRowsChanged = void Function(
); );
typedef ListenOnRowChangedCondition = bool Function(); typedef ListenOnRowChangedCondition = bool Function();
class GridDataController { class GridController {
final String gridId; final String gridId;
final GridFFIService _gridFFIService; final GridFFIService _gridFFIService;
final GridFieldController fieldController; final GridFieldController fieldController;
OnRowsChanged? _onRowChanged;
OnGridChanged? _onGridChanged;
// Getters
// key: the block id // key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks; final LinkedHashMap<String, GridBlockCache> _blocks;
UnmodifiableMapView<String, GridBlockCache> get blocks => UnmodifiableMapView<String, GridBlockCache> get blocks =>
UnmodifiableMapView(_blocks); UnmodifiableMapView(_blocks);
OnRowsChanged? _onRowChanged;
OnFieldsChanged? _onFieldsChanged;
OnGridChanged? _onGridChanged;
List<RowInfo> get rowInfos { List<RowInfo> get rowInfos {
final List<RowInfo> rows = []; final List<RowInfo> rows = [];
for (var block in _blocks.values) { for (var block in _blocks.values) {
@ -43,7 +44,7 @@ class GridDataController {
return rows; return rows;
} }
GridDataController({required ViewPB view}) GridController({required ViewPB view})
: gridId = view.id, : gridId = view.id,
// ignore: prefer_collection_literals // ignore: prefer_collection_literals
_blocks = LinkedHashMap(), _blocks = LinkedHashMap(),
@ -51,32 +52,36 @@ class GridDataController {
fieldController = GridFieldController(gridId: view.id); fieldController = GridFieldController(gridId: view.id);
void addListener({ void addListener({
required OnGridChanged onGridChanged, OnGridChanged? onGridChanged,
required OnRowsChanged onRowsChanged, OnRowsChanged? onRowsChanged,
required OnFieldsChanged onFieldsChanged, OnFieldsChanged? onFieldsChanged,
OnFiltersChanged? onFiltersChanged,
}) { }) {
_onGridChanged = onGridChanged; _onGridChanged = onGridChanged;
_onRowChanged = onRowsChanged; _onRowChanged = onRowsChanged;
_onFieldsChanged = onFieldsChanged;
fieldController.addListener(onFields: (fields) { fieldController.addListener(
_onFieldsChanged?.call(UnmodifiableListView(fields)); onFields: onFieldsChanged,
}); onFilters: onFiltersChanged,
);
} }
// Loads the rows from each block // Loads the rows from each block
Future<Either<Unit, FlowyError>> openGrid() async { Future<Either<Unit, FlowyError>> openGrid() async {
final result = await _gridFFIService.openGrid(); return _gridFFIService.openGrid().then((result) {
return Future( return result.fold(
() => result.fold(
(grid) async { (grid) async {
_initialBlocks(grid.blocks); _initialBlocks(grid.blocks);
_onGridChanged?.call(grid); _onGridChanged?.call(grid);
return await fieldController.loadFields(fieldIds: grid.fields);
final result = await fieldController.loadFields(
fieldIds: grid.fields,
);
return result;
}, },
(err) => right(err), (err) => right(err),
), );
); });
} }
Future<void> createRow() async { Future<void> createRow() async {

View File

@ -15,7 +15,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
GridHeaderBloc({ GridHeaderBloc({
required this.gridId, required this.gridId,
required this.fieldController, required this.fieldController,
}) : super(GridHeaderState.initial(fieldController.fieldContexts)) { }) : super(GridHeaderState.initial(fieldController.fieldInfos)) {
on<GridHeaderEvent>( on<GridHeaderEvent>(
(event, emit) async { (event, emit) async {
await event.map( await event.map(
@ -41,7 +41,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
Future<void> _moveField( Future<void> _moveField(
_MoveField value, Emitter<GridHeaderState> emit) async { _MoveField value, Emitter<GridHeaderState> emit) async {
final fields = List<GridFieldContext>.from(state.fields); final fields = List<FieldInfo>.from(state.fields);
fields.insert(value.toIndex, fields.removeAt(value.fromIndex)); fields.insert(value.toIndex, fields.removeAt(value.fromIndex));
emit(state.copyWith(fields: fields)); emit(state.copyWith(fields: fields));
@ -69,18 +69,18 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
@freezed @freezed
class GridHeaderEvent with _$GridHeaderEvent { class GridHeaderEvent with _$GridHeaderEvent {
const factory GridHeaderEvent.initial() = _InitialHeader; const factory GridHeaderEvent.initial() = _InitialHeader;
const factory GridHeaderEvent.didReceiveFieldUpdate( const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldInfo> fields) =
List<GridFieldContext> fields) = _DidReceiveFieldUpdate; _DidReceiveFieldUpdate;
const factory GridHeaderEvent.moveField( const factory GridHeaderEvent.moveField(
FieldPB field, int fromIndex, int toIndex) = _MoveField; FieldPB field, int fromIndex, int toIndex) = _MoveField;
} }
@freezed @freezed
class GridHeaderState with _$GridHeaderState { class GridHeaderState with _$GridHeaderState {
const factory GridHeaderState({required List<GridFieldContext> fields}) = const factory GridHeaderState({required List<FieldInfo> fields}) =
_GridHeaderState; _GridHeaderState;
factory GridHeaderState.initial(List<GridFieldContext> fields) { factory GridHeaderState.initial(List<FieldInfo> fields) {
// final List<FieldPB> newFields = List.from(fields); // final List<FieldPB> newFields = List.from(fields);
// newFields.retainWhere((field) => field.visibility); // newFields.retainWhere((field) => field.visibility);
return GridHeaderState(fields: fields); return GridHeaderState(fields: fields);

View File

@ -42,12 +42,16 @@ class GridFFIService {
return GridEventCreateBoardCard(payload).send(); return GridEventCreateBoardCard(payload).send();
} }
Future<Either<RepeatedFieldPB, FlowyError>> getFields( Future<Either<List<FieldPB>, FlowyError>> getFields(
{required List<FieldIdPB> fieldIds}) { {List<FieldIdPB>? fieldIds}) {
final payload = GetFieldPayloadPB.create() var payload = GetFieldPayloadPB.create()..gridId = gridId;
..gridId = gridId
..fieldIds = RepeatedFieldIdPB(items: fieldIds); if (fieldIds != null) {
return GridEventGetFields(payload).send(); payload.fieldIds = RepeatedFieldIdPB(items: fieldIds);
}
return GridEventGetFields(payload).send().then((result) {
return result.fold((l) => left(l.items), (r) => right(r));
});
} }
Future<Either<Unit, FlowyError>> closeGrid() { Future<Either<Unit, FlowyError>> closeGrid() {
@ -55,7 +59,7 @@ class GridFFIService {
return FolderEventCloseView(request).send(); return FolderEventCloseView(request).send();
} }
Future<Either<RepeatedGridGroupPB, FlowyError>> loadGroups() { Future<Either<RepeatedGroupPB, FlowyError>> loadGroups() {
final payload = GridIdPB(value: gridId); final payload = GridIdPB(value: gridId);
return GridEventGetGroup(payload).send(); return GridEventGetGroup(payload).send();
} }

View File

@ -35,7 +35,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
}, },
didReceiveCells: (_DidReceiveCells value) async { didReceiveCells: (_DidReceiveCells value) async {
final cells = value.gridCellMap.values final cells = value.gridCellMap.values
.map((e) => GridCellEquatable(e.fieldContext)) .map((e) => GridCellEquatable(e.fieldInfo))
.toList(); .toList();
emit(state.copyWith( emit(state.copyWith(
gridCellMap: value.gridCellMap, gridCellMap: value.gridCellMap,
@ -88,16 +88,16 @@ class RowState with _$RowState {
gridCellMap: cellDataMap, gridCellMap: cellDataMap,
cells: UnmodifiableListView( cells: UnmodifiableListView(
cellDataMap.values cellDataMap.values
.map((e) => GridCellEquatable(e.fieldContext)) .map((e) => GridCellEquatable(e.fieldInfo))
.toList(), .toList(),
), ),
); );
} }
class GridCellEquatable extends Equatable { class GridCellEquatable extends Equatable {
final GridFieldContext _fieldContext; final FieldInfo _fieldContext;
const GridCellEquatable(GridFieldContext field) : _fieldContext = field; const GridCellEquatable(FieldInfo field) : _fieldContext = field;
@override @override
List<Object?> get props => [ List<Object?> get props => [

View File

@ -4,18 +4,19 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'row_list.dart';
part 'row_cache.freezed.dart'; part 'row_cache.freezed.dart';
typedef RowUpdateCallback = void Function(); typedef RowUpdateCallback = void Function();
abstract class IGridRowFieldNotifier { abstract class IGridRowFieldNotifier {
UnmodifiableListView<GridFieldContext> get fields; UnmodifiableListView<FieldInfo> get fields;
void onRowFieldsChanged(VoidCallback callback); void onRowFieldsChanged(VoidCallback callback);
void onRowFieldChanged(void Function(FieldPB) callback); void onRowFieldChanged(void Function(FieldInfo) callback);
void onRowDispose(); void onRowDispose();
} }
@ -30,18 +31,14 @@ class GridRowCache {
/// _rows containers the current block's rows /// _rows containers the current block's rows
/// Use List to reverse the order of the GridRow. /// Use List to reverse the order of the GridRow.
List<RowInfo> _rowInfos = []; final RowList _rowList = RowList();
/// Use Map for faster access the raw row data.
final HashMap<String, RowInfo> _rowInfoByRowId;
final GridCellCache _cellCache; final GridCellCache _cellCache;
final IGridRowFieldNotifier _fieldNotifier; final IGridRowFieldNotifier _fieldNotifier;
final _RowChangesetNotifier _rowChangeReasonNotifier; final _RowChangesetNotifier _rowChangeReasonNotifier;
UnmodifiableListView<RowInfo> get visibleRows { UnmodifiableListView<RowInfo> get visibleRows {
var visibleRows = [..._rowInfos]; var visibleRows = [..._rowList.rows];
visibleRows.retainWhere((element) => element.visible);
return UnmodifiableListView(visibleRows); return UnmodifiableListView(visibleRows);
} }
@ -52,7 +49,6 @@ class GridRowCache {
required this.block, required this.block,
required IGridRowFieldNotifier notifier, required IGridRowFieldNotifier notifier,
}) : _cellCache = GridCellCache(gridId: gridId), }) : _cellCache = GridCellCache(gridId: gridId),
_rowInfoByRowId = HashMap(),
_rowChangeReasonNotifier = _RowChangesetNotifier(), _rowChangeReasonNotifier = _RowChangesetNotifier(),
_fieldNotifier = notifier { _fieldNotifier = notifier {
// //
@ -63,8 +59,7 @@ class GridRowCache {
for (final row in block.rows) { for (final row in block.rows) {
final rowInfo = buildGridRow(row); final rowInfo = buildGridRow(row);
_rowInfos.add(rowInfo); _rowList.add(rowInfo);
_rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
} }
} }
@ -82,91 +77,53 @@ class GridRowCache {
_showRows(changeset.visibleRows); _showRows(changeset.visibleRows);
} }
void _deleteRows(List<String> deletedRows) { void _deleteRows(List<String> deletedRowIds) {
if (deletedRows.isEmpty) { for (final rowId in deletedRowIds) {
return; final deletedRow = _rowList.remove(rowId);
} if (deletedRow != null) {
_rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow));
final List<RowInfo> newRows = [];
final DeletedIndexs deletedIndex = [];
final Map<String, String> deletedRowByRowId = {
for (var rowId in deletedRows) rowId: rowId
};
_rowInfos.asMap().forEach((index, RowInfo rowInfo) {
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
newRows.add(rowInfo);
} else {
_rowInfoByRowId.remove(rowInfo.rowPB.id);
deletedIndex.add(DeletedIndex(index: index, row: rowInfo));
} }
}); }
_rowInfos = newRows;
_rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex));
} }
void _insertRows(List<InsertedRowPB> insertRows) { void _insertRows(List<InsertedRowPB> insertRows) {
if (insertRows.isEmpty) { for (final insertedRow in insertRows) {
return; final insertedIndex =
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
if (insertedIndex != null) {
_rowChangeReasonNotifier
.receive(RowsChangedReason.insert(insertedIndex));
}
} }
InsertedIndexs insertIndexs = [];
for (final InsertedRowPB insertRow in insertRows) {
final insertIndex = InsertedIndex(
index: insertRow.index,
rowId: insertRow.row.id,
);
insertIndexs.add(insertIndex);
final rowInfo = buildGridRow(insertRow.row);
_rowInfos.insert(insertRow.index, rowInfo);
_rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
}
_rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
} }
void _updateRows(List<RowPB> updatedRows) { void _updateRows(List<RowPB> updatedRows) {
if (updatedRows.isEmpty) { if (updatedRows.isEmpty) return;
return;
final updatedIndexs =
_rowList.updateRows(updatedRows, (rowPB) => buildGridRow(rowPB));
if (updatedIndexs.isNotEmpty) {
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
} }
final UpdatedIndexs updatedIndexs = UpdatedIndexs();
for (final RowPB updatedRow in updatedRows) {
final rowId = updatedRow.id;
final index = _rowInfos.indexWhere(
(rowInfo) => rowInfo.rowPB.id == rowId,
);
if (index != -1) {
final rowInfo = buildGridRow(updatedRow);
_rowInfoByRowId[rowId] = rowInfo;
_rowInfos.removeAt(index);
_rowInfos.insert(index, rowInfo);
updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
}
}
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
} }
void _hideRows(List<String> invisibleRows) { void _hideRows(List<String> invisibleRows) {
for (final rowId in invisibleRows) { for (final rowId in invisibleRows) {
_rowInfoByRowId[rowId]?.visible = false; final deletedRow = _rowList.remove(rowId);
} if (deletedRow != null) {
_rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow));
if (invisibleRows.isNotEmpty) { }
_rowChangeReasonNotifier
.receive(const RowsChangedReason.filterDidChange());
} }
} }
void _showRows(List<String> visibleRows) { void _showRows(List<InsertedRowPB> visibleRows) {
for (final rowId in visibleRows) { for (final insertedRow in visibleRows) {
_rowInfoByRowId[rowId]?.visible = true; final insertedIndex =
} _rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
if (visibleRows.isNotEmpty) { if (insertedIndex != null) {
_rowChangeReasonNotifier _rowChangeReasonNotifier
.receive(const RowsChangedReason.filterDidChange()); .receive(RowsChangedReason.insert(insertedIndex));
}
} }
} }
@ -188,7 +145,7 @@ class GridRowCache {
notifyUpdate() { notifyUpdate() {
if (onCellUpdated != null) { if (onCellUpdated != null) {
final rowInfo = _rowInfoByRowId[rowId]; final rowInfo = _rowList.get(rowId);
if (rowInfo != null) { if (rowInfo != null) {
final GridCellMap cellDataMap = final GridCellMap cellDataMap =
_makeGridCells(rowId, rowInfo.rowPB); _makeGridCells(rowId, rowInfo.rowPB);
@ -214,7 +171,7 @@ class GridRowCache {
} }
GridCellMap loadGridCells(String rowId) { GridCellMap loadGridCells(String rowId) {
final RowPB? data = _rowInfoByRowId[rowId]?.rowPB; final RowPB? data = _rowList.get(rowId)?.rowPB;
if (data == null) { if (data == null) {
_loadRow(rowId); _loadRow(rowId);
} }
@ -242,7 +199,7 @@ class GridRowCache {
cellDataMap[field.id] = GridCellIdentifier( cellDataMap[field.id] = GridCellIdentifier(
rowId: rowId, rowId: rowId,
gridId: gridId, gridId: gridId,
fieldContext: field, fieldInfo: field,
); );
} }
} }
@ -256,26 +213,20 @@ class GridRowCache {
final updatedRow = optionRow.row; final updatedRow = optionRow.row;
updatedRow.freeze(); updatedRow.freeze();
final index = final rowInfo = _rowList.get(updatedRow.id);
_rowInfos.indexWhere((rowInfo) => rowInfo.rowPB.id == updatedRow.id); final rowIndex = _rowList.indexOfRow(updatedRow.id);
if (index != -1) { if (rowInfo != null && rowIndex != null) {
// update the corresponding row in _rows if they are not the same final updatedRowInfo = rowInfo.copyWith(rowPB: updatedRow);
if (_rowInfos[index].rowPB != updatedRow) { _rowList.remove(updatedRow.id);
final rowInfo = _rowInfos.removeAt(index).copyWith(rowPB: updatedRow); _rowList.insert(rowIndex, updatedRowInfo);
_rowInfos.insert(index, rowInfo);
_rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
// Calculate the update index final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
final UpdatedIndexs updatedIndexs = UpdatedIndexs(); updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex( index: rowIndex,
index: index, rowId: updatedRowInfo.rowPB.id,
rowId: rowInfo.rowPB.id, );
);
// _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
_rowChangeReasonNotifier
.receive(RowsChangedReason.update(updatedIndexs));
}
} }
} }
@ -284,7 +235,6 @@ class GridRowCache {
gridId: gridId, gridId: gridId,
fields: _fieldNotifier.fields, fields: _fieldNotifier.fields,
rowPB: rowPB, rowPB: rowPB,
visible: true,
); );
} }
} }
@ -302,7 +252,6 @@ class _RowChangesetNotifier extends ChangeNotifier {
update: (_) => notifyListeners(), update: (_) => notifyListeners(),
fieldDidChange: (_) => notifyListeners(), fieldDidChange: (_) => notifyListeners(),
initial: (_) {}, initial: (_) {},
filterDidChange: (_FilterDidChange value) => notifyListeners(),
); );
} }
} }
@ -311,23 +260,23 @@ class _RowChangesetNotifier extends ChangeNotifier {
class RowInfo with _$RowInfo { class RowInfo with _$RowInfo {
factory RowInfo({ factory RowInfo({
required String gridId, required String gridId,
required UnmodifiableListView<GridFieldContext> fields, required UnmodifiableListView<FieldInfo> fields,
required RowPB rowPB, required RowPB rowPB,
required bool visible,
}) = _RowInfo; }) = _RowInfo;
} }
typedef InsertedIndexs = List<InsertedIndex>; typedef InsertedIndexs = List<InsertedIndex>;
typedef DeletedIndexs = List<DeletedIndex>; typedef DeletedIndexs = List<DeletedIndex>;
typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>; // key: id of the row
// value: UpdatedIndex
typedef UpdatedIndexMap = LinkedHashMap<String, UpdatedIndex>;
@freezed @freezed
class RowsChangedReason with _$RowsChangedReason { class RowsChangedReason with _$RowsChangedReason {
const factory RowsChangedReason.insert(InsertedIndexs items) = _Insert; const factory RowsChangedReason.insert(InsertedIndex item) = _Insert;
const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete; const factory RowsChangedReason.delete(DeletedIndex item) = _Delete;
const factory RowsChangedReason.update(UpdatedIndexs indexs) = _Update; const factory RowsChangedReason.update(UpdatedIndexMap indexs) = _Update;
const factory RowsChangedReason.fieldDidChange() = _FieldDidChange; const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
const factory RowsChangedReason.filterDidChange() = _FilterDidChange;
const factory RowsChangedReason.initial() = InitialListState; const factory RowsChangedReason.initial() = InitialListState;
} }
@ -342,10 +291,10 @@ class InsertedIndex {
class DeletedIndex { class DeletedIndex {
final int index; final int index;
final RowInfo row; final RowInfo rowInfo;
DeletedIndex({ DeletedIndex({
required this.index, required this.index,
required this.row, required this.rowInfo,
}); });
} }

View File

@ -0,0 +1,156 @@
import 'dart:collection';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'row_cache.dart';
class RowList {
/// _rows containers the current block's rows
/// Use List to reverse the order of the GridRow.
List<RowInfo> _rowInfos = [];
List<RowInfo> get rows => List.from(_rowInfos);
/// Use Map for faster access the raw row data.
final HashMap<String, RowInfo> _rowInfoByRowId = HashMap();
RowInfo? get(String rowId) {
return _rowInfoByRowId[rowId];
}
int? indexOfRow(String rowId) {
final rowInfo = _rowInfoByRowId[rowId];
if (rowInfo != null) {
return _rowInfos.indexOf(rowInfo);
}
return null;
}
void add(RowInfo rowInfo) {
final rowId = rowInfo.rowPB.id;
if (contains(rowId)) {
final index =
_rowInfos.indexWhere((element) => element.rowPB.id == rowId);
_rowInfos.removeAt(index);
_rowInfos.insert(index, rowInfo);
} else {
_rowInfos.add(rowInfo);
}
_rowInfoByRowId[rowId] = rowInfo;
}
InsertedIndex? insert(int index, RowInfo rowInfo) {
final rowId = rowInfo.rowPB.id;
var insertedIndex = index;
if (_rowInfos.length <= insertedIndex) {
insertedIndex = _rowInfos.length;
}
final oldRowInfo = get(rowId);
if (oldRowInfo != null) {
_rowInfos.insert(insertedIndex, rowInfo);
_rowInfos.remove(oldRowInfo);
_rowInfoByRowId[rowId] = rowInfo;
return null;
} else {
_rowInfos.insert(insertedIndex, rowInfo);
_rowInfoByRowId[rowId] = rowInfo;
return InsertedIndex(index: insertedIndex, rowId: rowId);
}
}
DeletedIndex? remove(String rowId) {
final rowInfo = _rowInfoByRowId[rowId];
if (rowInfo != null) {
final index = _rowInfos.indexOf(rowInfo);
if (index != -1) {
_rowInfoByRowId.remove(rowInfo.rowPB.id);
_rowInfos.remove(rowInfo);
}
return DeletedIndex(index: index, rowInfo: rowInfo);
} else {
return null;
}
}
InsertedIndexs insertRows(
List<InsertedRowPB> insertedRows,
RowInfo Function(RowPB) builder,
) {
InsertedIndexs insertIndexs = [];
for (final insertRow in insertedRows) {
final isContains = contains(insertRow.row.id);
var index = insertRow.index;
if (_rowInfos.length < index) {
index = _rowInfos.length;
}
insert(index, builder(insertRow.row));
if (!isContains) {
insertIndexs.add(InsertedIndex(
index: index,
rowId: insertRow.row.id,
));
}
}
return insertIndexs;
}
DeletedIndexs removeRows(List<String> rowIds) {
final List<RowInfo> newRows = [];
final DeletedIndexs deletedIndex = [];
final Map<String, String> deletedRowByRowId = {
for (var rowId in rowIds) rowId: rowId
};
_rowInfos.asMap().forEach((index, RowInfo rowInfo) {
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
newRows.add(rowInfo);
} else {
_rowInfoByRowId.remove(rowInfo.rowPB.id);
deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
}
});
_rowInfos = newRows;
return deletedIndex;
}
UpdatedIndexMap updateRows(
List<RowPB> updatedRows,
RowInfo Function(RowPB) builder,
) {
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
for (final RowPB updatedRow in updatedRows) {
final rowId = updatedRow.id;
final index = _rowInfos.indexWhere(
(rowInfo) => rowInfo.rowPB.id == rowId,
);
if (index != -1) {
final rowInfo = builder(updatedRow);
insert(index, rowInfo);
updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
}
}
return updatedIndexs;
}
List<DeletedIndex> markRowsAsInvisible(List<String> rowIds) {
final List<DeletedIndex> deletedRows = [];
for (final rowId in rowIds) {
final rowInfo = _rowInfoByRowId[rowId];
if (rowInfo != null) {
final index = _rowInfos.indexOf(rowInfo);
if (index != -1) {
deletedRows.add(DeletedIndex(index: index, rowInfo: rowInfo));
}
}
}
return deletedRows;
}
bool contains(String rowId) {
return _rowInfoByRowId[rowId] != null;
}
}

View File

@ -12,14 +12,14 @@ part 'group_bloc.freezed.dart';
class GridGroupBloc extends Bloc<GridGroupEvent, GridGroupState> { class GridGroupBloc extends Bloc<GridGroupEvent, GridGroupState> {
final GridFieldController _fieldController; final GridFieldController _fieldController;
final SettingFFIService _settingFFIService; final SettingFFIService _settingFFIService;
Function(List<GridFieldContext>)? _onFieldsFn; Function(List<FieldInfo>)? _onFieldsFn;
GridGroupBloc({ GridGroupBloc({
required String viewId, required String viewId,
required GridFieldController fieldController, required GridFieldController fieldController,
}) : _fieldController = fieldController, }) : _fieldController = fieldController,
_settingFFIService = SettingFFIService(viewId: viewId), _settingFFIService = SettingFFIService(viewId: viewId),
super(GridGroupState.initial(viewId, fieldController.fieldContexts)) { super(GridGroupState.initial(viewId, fieldController.fieldInfos)) {
on<GridGroupEvent>( on<GridGroupEvent>(
(event, emit) async { (event, emit) async {
event.when( event.when(
@ -67,19 +67,19 @@ class GridGroupEvent with _$GridGroupEvent {
String fieldId, String fieldId,
FieldType fieldType, FieldType fieldType,
) = _GroupByField; ) = _GroupByField;
const factory GridGroupEvent.didReceiveFieldUpdate( const factory GridGroupEvent.didReceiveFieldUpdate(List<FieldInfo> fields) =
List<GridFieldContext> fields) = _DidReceiveFieldUpdate; _DidReceiveFieldUpdate;
} }
@freezed @freezed
class GridGroupState with _$GridGroupState { class GridGroupState with _$GridGroupState {
const factory GridGroupState({ const factory GridGroupState({
required String gridId, required String gridId,
required List<GridFieldContext> fieldContexts, required List<FieldInfo> fieldContexts,
}) = _GridGroupState; }) = _GridGroupState;
factory GridGroupState.initial( factory GridGroupState.initial(
String gridId, List<GridFieldContext> fieldContexts) => String gridId, List<FieldInfo> fieldContexts) =>
GridGroupState( GridGroupState(
gridId: gridId, gridId: gridId,
fieldContexts: fieldContexts, fieldContexts: fieldContexts,

View File

@ -10,13 +10,12 @@ part 'property_bloc.freezed.dart';
class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> { class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
final GridFieldController _fieldController; final GridFieldController _fieldController;
Function(List<GridFieldContext>)? _onFieldsFn; Function(List<FieldInfo>)? _onFieldsFn;
GridPropertyBloc( GridPropertyBloc(
{required String gridId, required GridFieldController fieldController}) {required String gridId, required GridFieldController fieldController})
: _fieldController = fieldController, : _fieldController = fieldController,
super( super(GridPropertyState.initial(gridId, fieldController.fieldInfos)) {
GridPropertyState.initial(gridId, fieldController.fieldContexts)) {
on<GridPropertyEvent>( on<GridPropertyEvent>(
(event, emit) async { (event, emit) async {
await event.map( await event.map(
@ -69,7 +68,7 @@ class GridPropertyEvent with _$GridPropertyEvent {
const factory GridPropertyEvent.setFieldVisibility( const factory GridPropertyEvent.setFieldVisibility(
String fieldId, bool visibility) = _SetFieldVisibility; String fieldId, bool visibility) = _SetFieldVisibility;
const factory GridPropertyEvent.didReceiveFieldUpdate( const factory GridPropertyEvent.didReceiveFieldUpdate(
List<GridFieldContext> fields) = _DidReceiveFieldUpdate; List<FieldInfo> fields) = _DidReceiveFieldUpdate;
const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) = const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) =
_MoveField; _MoveField;
} }
@ -78,12 +77,12 @@ class GridPropertyEvent with _$GridPropertyEvent {
class GridPropertyState with _$GridPropertyState { class GridPropertyState with _$GridPropertyState {
const factory GridPropertyState({ const factory GridPropertyState({
required String gridId, required String gridId,
required List<GridFieldContext> fieldContexts, required List<FieldInfo> fieldContexts,
}) = _GridPropertyState; }) = _GridPropertyState;
factory GridPropertyState.initial( factory GridPropertyState.initial(
String gridId, String gridId,
List<GridFieldContext> fieldContexts, List<FieldInfo> fieldContexts,
) => ) =>
GridPropertyState( GridPropertyState(
gridId: gridId, gridId: gridId,

View File

@ -25,7 +25,8 @@ class GridSettingBloc extends Bloc<GridSettingEvent, GridSettingState> {
@freezed @freezed
class GridSettingEvent with _$GridSettingEvent { class GridSettingEvent with _$GridSettingEvent {
const factory GridSettingEvent.performAction(GridSettingAction action) = _PerformAction; const factory GridSettingEvent.performAction(GridSettingAction action) =
_PerformAction;
} }
@freezed @freezed
@ -40,7 +41,7 @@ class GridSettingState with _$GridSettingState {
} }
enum GridSettingAction { enum GridSettingAction {
filter, showFilters,
sortBy, sortBy,
properties, showProperties,
} }

View File

@ -1,7 +1,8 @@
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart';
import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
@ -15,6 +16,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import '../application/row/row_cache.dart'; import '../application/row/row_cache.dart';
import '../application/setting/setting_bloc.dart';
import 'controller/grid_scroll.dart'; import 'controller/grid_scroll.dart';
import 'layout/layout.dart'; import 'layout/layout.dart';
import 'layout/sizes.dart'; import 'layout/sizes.dart';
@ -24,17 +26,20 @@ import 'widgets/footer/grid_footer.dart';
import 'widgets/header/grid_header.dart'; import 'widgets/header/grid_header.dart';
import 'widgets/row/row_detail.dart'; import 'widgets/row/row_detail.dart';
import 'widgets/shortcuts.dart'; import 'widgets/shortcuts.dart';
import 'widgets/filter/menu.dart';
import 'widgets/toolbar/grid_toolbar.dart'; import 'widgets/toolbar/grid_toolbar.dart';
class GridPage extends StatefulWidget { class GridPage extends StatefulWidget {
final ViewPB view; final ViewPB view;
final GridController gridController;
final VoidCallback? onDeleted; final VoidCallback? onDeleted;
GridPage({ GridPage({
required this.view, required this.view,
this.onDeleted, this.onDeleted,
Key? key, Key? key,
}) : super(key: ValueKey(view.id)); }) : gridController = GridController(view: view),
super(key: key);
@override @override
State<GridPage> createState() => _GridPageState(); State<GridPage> createState() => _GridPageState();
@ -46,8 +51,19 @@ class _GridPageState extends State<GridPage> {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider<GridBloc>( BlocProvider<GridBloc>(
create: (context) => getIt<GridBloc>(param1: widget.view) create: (context) => GridBloc(
..add(const GridEvent.initial()), view: widget.view,
gridController: widget.gridController,
)..add(const GridEvent.initial()),
),
BlocProvider<GridFilterMenuBloc>(
create: (context) => GridFilterMenuBloc(
viewId: widget.view.id,
fieldController: widget.gridController.fieldController,
)..add(const GridFilterMenuEvent.initial()),
),
BlocProvider<GridSettingBloc>(
create: (context) => GridSettingBloc(gridId: widget.view.id),
), ),
], ],
child: BlocBuilder<GridBloc, GridState>( child: BlocBuilder<GridBloc, GridState>(
@ -122,7 +138,8 @@ class _FlowyGridState extends State<FlowyGrid> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const _GridToolbarAdaptor(), const GridToolbar(),
const GridFilterMenu(),
_gridHeader(context, state.gridId), _gridHeader(context, state.gridId),
Flexible(child: child), Flexible(child: child),
const RowCountBadge(), const RowCountBadge(),
@ -166,7 +183,7 @@ class _FlowyGridState extends State<FlowyGrid> {
Widget _gridHeader(BuildContext context, String gridId) { Widget _gridHeader(BuildContext context, String gridId) {
final fieldController = final fieldController =
context.read<GridBloc>().dataController.fieldController; context.read<GridBloc>().gridController.fieldController;
return GridHeaderSliverAdaptor( return GridHeaderSliverAdaptor(
gridId: gridId, gridId: gridId,
fieldController: fieldController, fieldController: fieldController,
@ -175,27 +192,6 @@ class _FlowyGridState extends State<FlowyGrid> {
} }
} }
class _GridToolbarAdaptor extends StatelessWidget {
const _GridToolbarAdaptor({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocSelector<GridBloc, GridState, GridToolbarContext>(
selector: (state) {
final fieldController =
context.read<GridBloc>().dataController.fieldController;
return GridToolbarContext(
gridId: state.gridId,
fieldController: fieldController,
);
},
builder: (context, toolbarContext) {
return GridToolbar(toolbarContext: toolbarContext);
},
);
}
}
class _GridRows extends StatefulWidget { class _GridRows extends StatefulWidget {
const _GridRows({Key? key}) : super(key: key); const _GridRows({Key? key}) : super(key: key);
@ -211,20 +207,16 @@ class _GridRowsState extends State<_GridRows> {
return BlocConsumer<GridBloc, GridState>( return BlocConsumer<GridBloc, GridState>(
listenWhen: (previous, current) => previous.reason != current.reason, listenWhen: (previous, current) => previous.reason != current.reason,
listener: (context, state) { listener: (context, state) {
state.reason.mapOrNull( state.reason.whenOrNull(
insert: (value) { insert: (item) {
for (final item in value.items) { _key.currentState?.insertItem(item.index);
_key.currentState?.insertItem(item.index);
}
}, },
delete: (value) { delete: (item) {
for (final item in value.items) { _key.currentState?.removeItem(
_key.currentState?.removeItem( item.index,
item.index, (context, animation) =>
(context, animation) => _renderRow(context, item.rowInfo, animation),
_renderRow(context, item.row, animation), );
);
}
}, },
); );
}, },
@ -235,9 +227,13 @@ class _GridRowsState extends State<_GridRows> {
initialItemCount: context.read<GridBloc>().state.rowInfos.length, initialItemCount: context.read<GridBloc>().state.rowInfos.length,
itemBuilder: itemBuilder:
(BuildContext context, int index, Animation<double> animation) { (BuildContext context, int index, Animation<double> animation) {
final RowInfo rowInfo = final rowInfos = context.read<GridBloc>().state.rowInfos;
context.read<GridBloc>().state.rowInfos[index]; if (index >= rowInfos.length) {
return _renderRow(context, rowInfo, animation); return const SizedBox();
} else {
final RowInfo rowInfo = rowInfos[index];
return _renderRow(context, rowInfo, animation);
}
}, },
); );
}, },
@ -258,7 +254,7 @@ class _GridRowsState extends State<_GridRows> {
if (rowCache == null) return const SizedBox(); if (rowCache == null) return const SizedBox();
final fieldController = final fieldController =
context.read<GridBloc>().dataController.fieldController; context.read<GridBloc>().gridController.fieldController;
final dataController = GridRowDataController( final dataController = GridRowDataController(
rowInfo: rowInfo, rowInfo: rowInfo,
fieldController: fieldController, fieldController: fieldController,

View File

@ -2,7 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'sizes.dart'; import 'sizes.dart';
class GridLayout { class GridLayout {
static double headerWidth(List<GridFieldContext> fields) { static double headerWidth(List<FieldInfo> fields) {
if (fields.isEmpty) return 0; if (fields.isEmpty) return 0;
final fieldsWidth = fields final fieldsWidth = fields

View File

@ -1,4 +1,3 @@
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -69,7 +68,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
controller: _popover, controller: _popover,
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(320, 520)), constraints: BoxConstraints.loose(const Size(260, 500)),
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: SizedBox.expand( child: SizedBox.expand(
child: GestureDetector( child: GestureDetector(
@ -81,7 +80,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
padding: GridSize.cellContentInsets, padding: GridSize.cellContentInsets,
child: FlowyText.medium( child: FlowyText.medium(
state.dateStr, state.dateStr,
fontSize: FontSizes.s14, overflow: TextOverflow.ellipsis,
), ),
), ),
), ),

View File

@ -9,7 +9,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/color_extension.dart'; import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/text_style.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
@ -29,7 +28,6 @@ import '../../header/type_option/date.dart';
final kToday = DateTime.now(); final kToday = DateTime.now();
final kFirstDay = DateTime(kToday.year, kToday.month - 3, kToday.day); final kFirstDay = DateTime(kToday.year, kToday.month - 3, kToday.day);
final kLastDay = DateTime(kToday.year, kToday.month + 3, kToday.day); final kLastDay = DateTime(kToday.year, kToday.month + 3, kToday.day);
const kMargin = EdgeInsets.symmetric(horizontal: 6, vertical: 10);
class DateCellEditor extends StatefulWidget { class DateCellEditor extends StatefulWidget {
final VoidCallback onDismissed; final VoidCallback onDismissed;
@ -116,25 +114,25 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
return BlocProvider.value( return BlocProvider.value(
value: bloc, value: bloc,
child: BlocBuilder<DateCalBloc, DateCalState>( child: BlocBuilder<DateCalBloc, DateCalState>(
buildWhen: (p, c) => false, buildWhen: (p, c) => p != c,
builder: (context, state) { builder: (context, state) {
List<Widget> children = [ List<Widget> children = [
_buildCalendar(context), _buildCalendar(context),
_TimeTextField( if (state.dateTypeOptionPB.includeTime)
bloc: context.read<DateCalBloc>(), _TimeTextField(
popoverMutex: popoverMutex, bloc: context.read<DateCalBloc>(),
), popoverMutex: popoverMutex,
Divider(height: 1, color: Theme.of(context).dividerColor), ),
Divider(height: 1.0, color: Theme.of(context).dividerColor),
const _IncludeTimeButton(), const _IncludeTimeButton(),
Divider(height: 1.0, color: Theme.of(context).dividerColor),
_DateTypeOptionButton(popoverMutex: popoverMutex) _DateTypeOptionButton(popoverMutex: popoverMutex)
]; ];
return ListView.separated( return ListView.separated(
shrinkWrap: true, shrinkWrap: true,
controller: ScrollController(), controller: ScrollController(),
separatorBuilder: (context, index) { separatorBuilder: (context, index) => VSpace(GridSize.cellVPadding),
return VSpace(GridSize.typeOptionSeparatorHeight);
},
itemCount: children.length, itemCount: children.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return children[index]; return children[index];
@ -155,17 +153,23 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
Widget _buildCalendar(BuildContext context) { Widget _buildCalendar(BuildContext context) {
return BlocBuilder<DateCalBloc, DateCalState>( return BlocBuilder<DateCalBloc, DateCalState>(
builder: (context, state) { builder: (context, state) {
final textStyle = Theme.of(context).textTheme.bodyMedium!;
final boxDecoration = BoxDecoration(
color: Theme.of(context).colorScheme.surface,
shape: BoxShape.rectangle,
borderRadius: Corners.s6Border,
);
return TableCalendar( return TableCalendar(
firstDay: kFirstDay, firstDay: kFirstDay,
lastDay: kLastDay, lastDay: kLastDay,
focusedDay: state.focusedDay, focusedDay: state.focusedDay,
rowHeight: 40, rowHeight: GridSize.typeOptionItemHeight,
calendarFormat: state.format, calendarFormat: state.format,
daysOfWeekHeight: 40, daysOfWeekHeight: GridSize.typeOptionItemHeight,
headerStyle: HeaderStyle( headerStyle: HeaderStyle(
formatButtonVisible: false, formatButtonVisible: false,
titleCentered: true, titleCentered: true,
titleTextStyle: TextStyles.body1.size(FontSizes.s14), titleTextStyle: textStyle,
leftChevronMargin: EdgeInsets.zero, leftChevronMargin: EdgeInsets.zero,
leftChevronPadding: EdgeInsets.zero, leftChevronPadding: EdgeInsets.zero,
leftChevronIcon: svgWidget("home/arrow_left"), leftChevronIcon: svgWidget("home/arrow_left"),
@ -177,57 +181,25 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
daysOfWeekStyle: DaysOfWeekStyle( daysOfWeekStyle: DaysOfWeekStyle(
dowTextFormatter: (date, locale) => dowTextFormatter: (date, locale) =>
DateFormat.E(locale).format(date).toUpperCase(), DateFormat.E(locale).format(date).toUpperCase(),
weekdayStyle: TextStyles.general( weekdayStyle: AFThemeExtension.of(context).caption,
fontSize: 13, weekendStyle: AFThemeExtension.of(context).caption,
fontWeight: FontWeight.w400,
color: Theme.of(context).hintColor,
),
weekendStyle: TextStyles.general(
fontSize: 13,
fontWeight: FontWeight.w400,
color: Theme.of(context).hintColor,
),
), ),
calendarStyle: CalendarStyle( calendarStyle: CalendarStyle(
cellMargin: const EdgeInsets.all(3), cellMargin: const EdgeInsets.all(3),
defaultDecoration: BoxDecoration( defaultDecoration: boxDecoration,
color: Theme.of(context).colorScheme.surface, selectedDecoration: boxDecoration.copyWith(
shape: BoxShape.rectangle, color: Theme.of(context).colorScheme.primary),
borderRadius: const BorderRadius.all(Radius.circular(6)), todayDecoration: boxDecoration.copyWith(
), color: AFThemeExtension.of(context).lightGreyHover),
selectedDecoration: BoxDecoration( weekendDecoration: boxDecoration,
color: Theme.of(context).colorScheme.primary, outsideDecoration: boxDecoration,
shape: BoxShape.rectangle, defaultTextStyle: textStyle,
borderRadius: const BorderRadius.all(Radius.circular(6)), weekendTextStyle: textStyle,
), selectedTextStyle:
todayDecoration: BoxDecoration( textStyle.textColor(Theme.of(context).colorScheme.surface),
color: AFThemeExtension.of(context).lightGreyHover, todayTextStyle: textStyle,
shape: BoxShape.rectangle, outsideTextStyle:
borderRadius: const BorderRadius.all(Radius.circular(6)), textStyle.textColor(Theme.of(context).disabledColor),
),
weekendDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
outsideDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
defaultTextStyle: TextStyles.body1.size(FontSizes.s14),
weekendTextStyle: TextStyles.body1.size(FontSizes.s14),
selectedTextStyle: TextStyles.general(
fontSize: FontSizes.s14,
color: Theme.of(context).colorScheme.surface,
),
todayTextStyle: TextStyles.general(
fontSize: FontSizes.s14,
),
outsideTextStyle: TextStyles.general(
fontSize: FontSizes.s14,
color: Theme.of(context).disabledColor,
),
), ),
selectedDayPredicate: (day) { selectedDayPredicate: (day) {
return state.calData.fold( return state.calData.fold(
@ -263,9 +235,9 @@ class _IncludeTimeButton extends StatelessWidget {
selector: (state) => state.dateTypeOptionPB.includeTime, selector: (state) => state.dateTypeOptionPB.includeTime,
builder: (context, includeTime) { builder: (context, includeTime) {
return SizedBox( return SizedBox(
height: 50, height: GridSize.typeOptionItemHeight,
child: Padding( child: Padding(
padding: kMargin, padding: GridSize.typeOptionContentInsets,
child: Row( child: Row(
children: [ children: [
svgWidget( svgWidget(
@ -273,10 +245,7 @@ class _IncludeTimeButton extends StatelessWidget {
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
const HSpace(4), const HSpace(4),
FlowyText.medium( FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
LocaleKeys.grid_field_includeTime.tr(),
fontSize: FontSizes.s14,
),
const Spacer(), const Spacer(),
Toggle( Toggle(
value: includeTime, value: includeTime,
@ -298,6 +267,7 @@ class _IncludeTimeButton extends StatelessWidget {
class _TimeTextField extends StatefulWidget { class _TimeTextField extends StatefulWidget {
final DateCalBloc bloc; final DateCalBloc bloc;
final PopoverMutex popoverMutex; final PopoverMutex popoverMutex;
const _TimeTextField({ const _TimeTextField({
required this.bloc, required this.bloc,
required this.popoverMutex, required this.popoverMutex,
@ -316,58 +286,51 @@ class _TimeTextFieldState extends State<_TimeTextField> {
void initState() { void initState() {
_focusNode = FocusNode(); _focusNode = FocusNode();
_controller = TextEditingController(text: widget.bloc.state.time); _controller = TextEditingController(text: widget.bloc.state.time);
if (widget.bloc.state.dateTypeOptionPB.includeTime) {
_focusNode.addListener(() {
if (mounted) {
widget.bloc.add(DateCalEvent.setTime(_controller.text));
}
if (_focusNode.hasFocus) { _focusNode.addListener(() {
widget.popoverMutex.close(); if (mounted) {
} widget.bloc.add(DateCalEvent.setTime(_controller.text));
}); }
});
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex.close();
}
});
widget.popoverMutex.listenOnPopoverChanged(() {
if (_focusNode.hasFocus) {
_focusNode.unfocus();
}
});
widget.popoverMutex.listenOnPopoverChanged(() {
if (_focusNode.hasFocus) {
_focusNode.unfocus();
}
});
}
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocConsumer<DateCalBloc, DateCalState>( _controller.text = widget.bloc.state.time ?? "";
listener: (context, state) { _controller.selection =
_controller.text = state.time ?? ""; TextSelection.collapsed(offset: _controller.text.length);
}, return Padding(
listenWhen: (p, c) => p.time != c.time, padding: GridSize.typeOptionContentInsets,
builder: (context, state) { child: RoundedInputField(
if (state.dateTypeOptionPB.includeTime) { height: GridSize.typeOptionItemHeight,
return Padding( focusNode: _focusNode,
padding: kMargin, autoFocus: true,
child: RoundedInputField( hintText: widget.bloc.state.timeHintText,
height: 40, controller: _controller,
focusNode: _focusNode, style: Theme.of(context).textTheme.bodyMedium!,
autoFocus: true, normalBorderColor: Theme.of(context).colorScheme.outline,
hintText: state.timeHintText, errorBorderColor: Theme.of(context).colorScheme.error,
controller: _controller, focusBorderColor: Theme.of(context).colorScheme.primary,
style: TextStyles.body1.size(FontSizes.s14), cursorColor: Theme.of(context).colorScheme.primary,
normalBorderColor: Theme.of(context).colorScheme.outline, errorText:
errorBorderColor: Theme.of(context).colorScheme.error, widget.bloc.state.timeFormatError.fold(() => "", (error) => error),
focusBorderColor: Theme.of(context).colorScheme.primary, onEditingComplete: (value) =>
cursorColor: Theme.of(context).colorScheme.primary, widget.bloc.add(DateCalEvent.setTime(value)),
errorText: state.timeFormatError.fold(() => "", (error) => error), ),
onEditingComplete: (value) {
widget.bloc.add(DateCalEvent.setTime(value));
},
),
);
} else {
return const SizedBox();
}
},
); );
} }
@ -397,12 +360,14 @@ class _DateTypeOptionButton extends StatelessWidget {
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
offset: const Offset(20, 0), offset: const Offset(20, 0),
constraints: BoxConstraints.loose(const Size(140, 100)), constraints: BoxConstraints.loose(const Size(140, 100)),
child: FlowyButton( child: SizedBox(
text: FlowyText.medium(title, fontSize: 14), height: GridSize.typeOptionItemHeight,
margin: kMargin, child: FlowyButton(
rightIcon: svgWidget( text: FlowyText.medium(title),
"grid/more", rightIcon: svgWidget(
color: Theme.of(context).colorScheme.onSurface, "grid/more",
color: Theme.of(context).colorScheme.onSurface,
),
), ),
), ),
popupBuilder: (BuildContext popContext) { popupBuilder: (BuildContext popContext) {
@ -476,9 +441,8 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
controller: ScrollController(), controller: ScrollController(),
separatorBuilder: (context, index) { separatorBuilder: (context, index) =>
return VSpace(GridSize.typeOptionSeparatorHeight); VSpace(GridSize.typeOptionSeparatorHeight),
},
itemCount: children.length, itemCount: children.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return children[index]; return children[index];

View File

@ -61,7 +61,6 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
SliverToBoxAdapter( SliverToBoxAdapter(
child: _TextField(popoverMutex: popoverMutex), child: _TextField(popoverMutex: popoverMutex),
), ),
const SliverToBoxAdapter(child: VSpace(6)),
const SliverToBoxAdapter(child: TypeOptionSeparator()), const SliverToBoxAdapter(child: TypeOptionSeparator()),
const SliverToBoxAdapter(child: VSpace(6)), const SliverToBoxAdapter(child: VSpace(6)),
const SliverToBoxAdapter(child: _Title()), const SliverToBoxAdapter(child: _Title()),
@ -145,7 +144,7 @@ class _TextField extends StatelessWidget {
value: (option) => option); value: (option) => option);
return SizedBox( return SizedBox(
height: 62, height: 52,
child: SelectOptionTextField( child: SelectOptionTextField(
options: state.options, options: state.options,
selectedOptionMap: optionMap, selectedOptionMap: optionMap,

View File

@ -49,6 +49,7 @@ class SelectOptionTextField extends StatefulWidget {
class _SelectOptionTextFieldState extends State<SelectOptionTextField> { class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
late FocusNode focusNode; late FocusNode focusNode;
late TextEditingController controller; late TextEditingController controller;
var textLength = 0;
@override @override
void initState() { void initState() {
@ -61,6 +62,14 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
super.initState(); super.initState();
} }
String? _suffixText() {
if (widget.maxLength != null) {
return '${textLength.toString()}/${widget.maxLength.toString()}';
} else {
return null;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextFieldTags( return TextFieldTags(
@ -83,6 +92,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
focusNode: focusNode, focusNode: focusNode,
onTap: widget.onClick, onTap: widget.onClick,
onChanged: (text) { onChanged: (text) {
textLength = text.length;
if (onChanged != null) { if (onChanged != null) {
onChanged(text); onChanged(text);
} }
@ -114,6 +124,8 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
isDense: true, isDense: true,
prefixIcon: _renderTags(context, sc), prefixIcon: _renderTags(context, sc),
hintText: LocaleKeys.grid_selectOption_searchOption.tr(), hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
suffixText: _suffixText(),
counterText: "",
prefixIconConstraints: prefixIconConstraints:
BoxConstraints(maxWidth: widget.distanceToText), BoxConstraints(maxWidth: widget.distanceToText),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(

View File

@ -0,0 +1,210 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/filter/checkbox_filter_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/condition_button.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/disclosure_button.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'choicechip.dart';
class CheckboxFilterChoicechip extends StatefulWidget {
final FilterInfo filterInfo;
const CheckboxFilterChoicechip({required this.filterInfo, Key? key})
: super(key: key);
@override
State<CheckboxFilterChoicechip> createState() =>
_CheckboxFilterChoicechipState();
}
class _CheckboxFilterChoicechipState extends State<CheckboxFilterChoicechip> {
late CheckboxFilterEditorBloc bloc;
@override
void initState() {
bloc = CheckboxFilterEditorBloc(filterInfo: widget.filterInfo)
..add(const CheckboxFilterEditorEvent.initial());
super.initState();
}
@override
void dispose() {
bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: bloc,
child: BlocBuilder<CheckboxFilterEditorBloc, CheckboxFilterEditorState>(
builder: (blocContext, state) {
return AppFlowyPopover(
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(200, 76)),
direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: (BuildContext context) {
return CheckboxFilterEditor(bloc: bloc);
},
child: ChoiceChipButton(
filterInfo: widget.filterInfo,
filterDesc: _makeFilterDesc(state),
),
);
},
),
);
}
String _makeFilterDesc(CheckboxFilterEditorState state) {
final prefix = LocaleKeys.grid_checkboxFilter_choicechipPrefix_is.tr();
return "$prefix ${state.filter.condition.filterName}";
}
}
class CheckboxFilterEditor extends StatefulWidget {
final CheckboxFilterEditorBloc bloc;
const CheckboxFilterEditor({required this.bloc, Key? key}) : super(key: key);
@override
State<CheckboxFilterEditor> createState() => _CheckboxFilterEditorState();
}
class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
final popoverMutex = PopoverMutex();
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: widget.bloc,
child: BlocBuilder<CheckboxFilterEditorBloc, CheckboxFilterEditorState>(
builder: (context, state) {
final List<Widget> children = [
_buildFilterPannel(context, state),
];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
child: IntrinsicHeight(child: Column(children: children)),
);
},
),
);
}
Widget _buildFilterPannel(
BuildContext context, CheckboxFilterEditorState state) {
return SizedBox(
height: 20,
child: Row(
children: [
FlowyText(state.filterInfo.field.name),
const HSpace(4),
CheckboxFilterConditionList(
filterInfo: state.filterInfo,
popoverMutex: popoverMutex,
onCondition: (condition) {
context
.read<CheckboxFilterEditorBloc>()
.add(CheckboxFilterEditorEvent.updateCondition(condition));
},
),
const Spacer(),
DisclosureButton(
popoverMutex: popoverMutex,
onAction: (action) {
switch (action) {
case FilterDisclosureAction.delete:
context
.read<CheckboxFilterEditorBloc>()
.add(const CheckboxFilterEditorEvent.delete());
break;
}
},
),
],
),
);
}
}
class CheckboxFilterConditionList extends StatelessWidget {
final FilterInfo filterInfo;
final PopoverMutex popoverMutex;
final Function(CheckboxFilterCondition) onCondition;
const CheckboxFilterConditionList({
required this.filterInfo,
required this.popoverMutex,
required this.onCondition,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final checkboxFilter = filterInfo.checkboxFilter()!;
return PopoverActionList<ConditionWrapper>(
asBarrier: true,
mutex: popoverMutex,
direction: PopoverDirection.bottomWithCenterAligned,
actions: CheckboxFilterCondition.values
.map(
(action) => ConditionWrapper(
action,
checkboxFilter.condition == action,
),
)
.toList(),
buildChild: (controller) {
return ConditionButton(
conditionName: checkboxFilter.condition.filterName,
onTap: () => controller.show(),
);
},
onSelected: (action, controller) async {
onCondition(action.inner);
controller.close();
},
);
}
}
class ConditionWrapper extends ActionCell {
final CheckboxFilterCondition inner;
final bool isSelected;
ConditionWrapper(this.inner, this.isSelected);
@override
Widget? rightIcon(Color iconColor) {
if (isSelected) {
return svgWidget("grid/checkmark");
} else {
return null;
}
}
@override
String get name => inner.filterName;
}
extension TextFilterConditionExtension on CheckboxFilterCondition {
String get filterName {
switch (this) {
case CheckboxFilterCondition.IsChecked:
return LocaleKeys.grid_checkboxFilter_isChecked.tr();
case CheckboxFilterCondition.IsUnChecked:
return LocaleKeys.grid_checkboxFilter_isUnchecked.tr();
default:
return "";
}
}
}

View File

@ -0,0 +1,76 @@
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;
class ChoiceChipButton extends StatelessWidget {
final FilterInfo filterInfo;
final VoidCallback? onTap;
final String filterDesc;
const ChoiceChipButton({
Key? key,
required this.filterInfo,
this.filterDesc = '',
this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final borderSide = BorderSide(
color: AFThemeExtension.of(context).toggleOffFill,
width: 1.0,
);
final decoration = BoxDecoration(
color: Colors.transparent,
border: Border.fromBorderSide(borderSide),
borderRadius: const BorderRadius.all(Radius.circular(14)),
);
return SizedBox(
height: 28,
child: FlowyButton(
decoration: decoration,
useIntrinsicWidth: true,
text: FlowyText(filterInfo.field.name, fontSize: 12),
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
radius: const BorderRadius.all(Radius.circular(14)),
leftIcon: svgWidget(
filterInfo.field.fieldType.iconName(),
color: Theme.of(context).colorScheme.onSurface,
),
rightIcon: _ChoicechipFilterDesc(filterDesc: filterDesc),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onTap: onTap,
),
);
}
}
class _ChoicechipFilterDesc extends StatelessWidget {
final String filterDesc;
const _ChoicechipFilterDesc({this.filterDesc = '', Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
final arrow = Transform.rotate(
angle: -math.pi / 2,
child: svgWidget("home/arrow_left"),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Row(
children: [
if (filterDesc.isNotEmpty) FlowyText(': $filterDesc'),
arrow,
],
),
);
}
}

View File

@ -0,0 +1,15 @@
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:flutter/material.dart';
import 'choicechip.dart';
class DateFilterChoicechip extends StatelessWidget {
final FilterInfo filterInfo;
const DateFilterChoicechip({required this.filterInfo, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return ChoiceChipButton(filterInfo: filterInfo);
}
}

View File

@ -0,0 +1,15 @@
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:flutter/material.dart';
import 'choicechip.dart';
class NumberFilterChoicechip extends StatelessWidget {
final FilterInfo filterInfo;
const NumberFilterChoicechip({required this.filterInfo, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return ChoiceChipButton(filterInfo: filterInfo);
}
}

View File

@ -0,0 +1,15 @@
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:flutter/material.dart';
import 'choicechip.dart';
class SelectOptionFilterChoicechip extends StatelessWidget {
final FilterInfo filterInfo;
const SelectOptionFilterChoicechip({required this.filterInfo, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return ChoiceChipButton(filterInfo: filterInfo);
}
}

View File

@ -0,0 +1,268 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/filter/text_filter_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/condition_button.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/disclosure_button.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/text_field.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'choicechip.dart';
class TextFilterChoicechip extends StatefulWidget {
final FilterInfo filterInfo;
const TextFilterChoicechip({required this.filterInfo, Key? key})
: super(key: key);
@override
State<TextFilterChoicechip> createState() => _TextFilterChoicechipState();
}
class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
late TextFilterEditorBloc bloc;
@override
void initState() {
bloc = TextFilterEditorBloc(filterInfo: widget.filterInfo)
..add(const TextFilterEditorEvent.initial());
super.initState();
}
@override
void dispose() {
bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: bloc,
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
builder: (blocContext, state) {
return AppFlowyPopover(
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(200, 76)),
direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: (BuildContext context) {
return TextFilterEditor(bloc: bloc);
},
child: ChoiceChipButton(
filterInfo: widget.filterInfo,
filterDesc: _makeFilterDesc(state),
),
);
},
),
);
}
String _makeFilterDesc(TextFilterEditorState state) {
String filterDesc = state.filter.condition.choicechipPrefix;
if (state.filter.condition == TextFilterCondition.TextIsEmpty ||
state.filter.condition == TextFilterCondition.TextIsNotEmpty) {
return filterDesc;
}
if (state.filter.content.isNotEmpty) {
filterDesc += " ${state.filter.content}";
}
return filterDesc;
}
}
class TextFilterEditor extends StatefulWidget {
final TextFilterEditorBloc bloc;
const TextFilterEditor({required this.bloc, Key? key}) : super(key: key);
@override
State<TextFilterEditor> createState() => _TextFilterEditorState();
}
class _TextFilterEditorState extends State<TextFilterEditor> {
final popoverMutex = PopoverMutex();
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: widget.bloc,
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
builder: (context, state) {
final List<Widget> children = [
_buildFilterPannel(context, state),
];
if (state.filter.condition != TextFilterCondition.TextIsEmpty &&
state.filter.condition != TextFilterCondition.TextIsNotEmpty) {
children.add(const VSpace(4));
children.add(_buildFilterTextField(context, state));
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
child: IntrinsicHeight(child: Column(children: children)),
);
},
),
);
}
Widget _buildFilterPannel(BuildContext context, TextFilterEditorState state) {
return SizedBox(
height: 20,
child: Row(
children: [
FlowyText(state.filterInfo.field.name),
const HSpace(4),
TextFilterConditionList(
filterInfo: state.filterInfo,
popoverMutex: popoverMutex,
onCondition: (condition) {
context
.read<TextFilterEditorBloc>()
.add(TextFilterEditorEvent.updateCondition(condition));
},
),
const Spacer(),
DisclosureButton(
popoverMutex: popoverMutex,
onAction: (action) {
switch (action) {
case FilterDisclosureAction.delete:
context
.read<TextFilterEditorBloc>()
.add(const TextFilterEditorEvent.delete());
break;
}
},
),
],
),
);
}
Widget _buildFilterTextField(
BuildContext context, TextFilterEditorState state) {
return FilterTextField(
text: state.filter.content,
hintText: LocaleKeys.grid_settings_typeAValue.tr(),
autoFucous: false,
onSubmitted: (text) {
context
.read<TextFilterEditorBloc>()
.add(TextFilterEditorEvent.updateContent(text));
},
);
}
}
class TextFilterConditionList extends StatelessWidget {
final FilterInfo filterInfo;
final PopoverMutex popoverMutex;
final Function(TextFilterCondition) onCondition;
const TextFilterConditionList({
required this.filterInfo,
required this.popoverMutex,
required this.onCondition,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final textFilter = filterInfo.textFilter()!;
return PopoverActionList<ConditionWrapper>(
asBarrier: true,
mutex: popoverMutex,
direction: PopoverDirection.bottomWithCenterAligned,
actions: TextFilterCondition.values
.map(
(action) => ConditionWrapper(
action,
textFilter.condition == action,
),
)
.toList(),
buildChild: (controller) {
return ConditionButton(
conditionName: textFilter.condition.filterName,
onTap: () => controller.show(),
);
},
onSelected: (action, controller) async {
onCondition(action.inner);
controller.close();
},
);
}
}
class ConditionWrapper extends ActionCell {
final TextFilterCondition inner;
final bool isSelected;
ConditionWrapper(this.inner, this.isSelected);
@override
Widget? rightIcon(Color iconColor) {
if (isSelected) {
return svgWidget("grid/checkmark");
} else {
return null;
}
}
@override
String get name => inner.filterName;
}
extension TextFilterConditionExtension on TextFilterCondition {
String get filterName {
switch (this) {
case TextFilterCondition.Contains:
return LocaleKeys.grid_textFilter_contains.tr();
case TextFilterCondition.DoesNotContain:
return LocaleKeys.grid_textFilter_doesNotContain.tr();
case TextFilterCondition.EndsWith:
return LocaleKeys.grid_textFilter_endsWith.tr();
case TextFilterCondition.Is:
return LocaleKeys.grid_textFilter_is.tr();
case TextFilterCondition.IsNot:
return LocaleKeys.grid_textFilter_isNot.tr();
case TextFilterCondition.StartsWith:
return LocaleKeys.grid_textFilter_startWith.tr();
case TextFilterCondition.TextIsEmpty:
return LocaleKeys.grid_textFilter_isEmpty.tr();
case TextFilterCondition.TextIsNotEmpty:
return LocaleKeys.grid_textFilter_isNotEmpty.tr();
default:
return "";
}
}
String get choicechipPrefix {
switch (this) {
case TextFilterCondition.DoesNotContain:
return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr();
case TextFilterCondition.EndsWith:
return LocaleKeys.grid_textFilter_choicechipPrefix_endWith.tr();
case TextFilterCondition.IsNot:
return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr();
case TextFilterCondition.StartsWith:
return LocaleKeys.grid_textFilter_choicechipPrefix_startWith.tr();
case TextFilterCondition.TextIsEmpty:
return LocaleKeys.grid_textFilter_choicechipPrefix_isEmpty.tr();
case TextFilterCondition.TextIsNotEmpty:
return LocaleKeys.grid_textFilter_choicechipPrefix_isNotEmpty.tr();
default:
return "";
}
}
}

View File

@ -0,0 +1,14 @@
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:flutter/material.dart';
import 'choicechip.dart';
class URLFilterChoicechip extends StatelessWidget {
final FilterInfo filterInfo;
const URLFilterChoicechip({required this.filterInfo, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return ChoiceChipButton(filterInfo: filterInfo);
}
}

View File

@ -0,0 +1,37 @@
import 'dart:math' as math;
import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
class ConditionButton extends StatelessWidget {
final String conditionName;
final VoidCallback onTap;
const ConditionButton({
required this.conditionName,
required this.onTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final arrow = Transform.rotate(
angle: -math.pi / 2,
child: svgWidget("home/arrow_left"),
);
return SizedBox(
height: 20,
child: FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(conditionName, fontSize: 10),
margin: const EdgeInsets.symmetric(horizontal: 4),
radius: const BorderRadius.all(Radius.circular(2)),
rightIcon: arrow,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onTap: onTap,
),
);
}
}

View File

@ -0,0 +1,168 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/filter/filter_create_bloc.dart';
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/text_field.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class GridCreateFilterList extends StatefulWidget {
final String viewId;
final GridFieldController fieldController;
final VoidCallback onClosed;
final VoidCallback? onCreateFilter;
const GridCreateFilterList({
required this.viewId,
required this.fieldController,
required this.onClosed,
this.onCreateFilter,
Key? key,
}) : super(key: key);
@override
State<StatefulWidget> createState() => _GridCreateFilterListState();
}
class _GridCreateFilterListState extends State<GridCreateFilterList> {
late GridCreateFilterBloc editBloc;
@override
void initState() {
editBloc = GridCreateFilterBloc(
viewId: widget.viewId,
fieldController: widget.fieldController,
)..add(const GridCreateFilterEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: editBloc,
child: BlocListener<GridCreateFilterBloc, GridCreateFilterState>(
listener: (context, state) {
if (state.didCreateFilter) {
widget.onClosed();
}
},
child: BlocBuilder<GridCreateFilterBloc, GridCreateFilterState>(
builder: (context, state) {
final cells = state.creatableFields.map((fieldInfo) {
return SizedBox(
height: GridSize.typeOptionItemHeight,
child: _FilterPropertyCell(
fieldInfo: fieldInfo,
onTap: (fieldInfo) => createFilter(fieldInfo),
),
);
}).toList();
List<Widget> slivers = [
SliverPersistentHeader(
pinned: true,
delegate: _FilterTextFieldDelegate(),
),
SliverToBoxAdapter(
child: ListView.separated(
controller: ScrollController(),
shrinkWrap: true,
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
separatorBuilder: (BuildContext context, int index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
),
),
];
return CustomScrollView(
shrinkWrap: true,
slivers: slivers,
controller: ScrollController(),
physics: StyledScrollPhysics(),
);
},
),
),
);
}
@override
Future<void> dispose() async {
editBloc.close();
super.dispose();
}
void createFilter(FieldInfo field) {
editBloc.add(GridCreateFilterEvent.createDefaultFilter(field));
widget.onCreateFilter?.call();
}
}
class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
_FilterTextFieldDelegate();
double fixHeight = 46;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Container(
color: Theme.of(context).colorScheme.background,
height: fixHeight,
child: FilterTextField(
hintText: LocaleKeys.grid_settings_filterBy.tr(),
onChanged: (text) {
context
.read<GridCreateFilterBloc>()
.add(GridCreateFilterEvent.didReceiveFilterText(text));
},
),
),
);
}
@override
double get maxExtent => fixHeight;
@override
double get minExtent => fixHeight;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
class _FilterPropertyCell extends StatelessWidget {
final FieldInfo fieldInfo;
final Function(FieldInfo) onTap;
const _FilterPropertyCell({
required this.fieldInfo,
required this.onTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyButton(
text: FlowyText.medium(fieldInfo.name, fontSize: 12),
onTap: () => onTap(fieldInfo),
leftIcon: svgWidget(
fieldInfo.fieldType.iconName(),
color: Theme.of(context).colorScheme.onSurface,
),
);
}
}

View File

@ -0,0 +1,73 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
class DisclosureButton extends StatefulWidget {
final PopoverMutex popoverMutex;
final Function(FilterDisclosureAction) onAction;
const DisclosureButton({
required this.popoverMutex,
required this.onAction,
Key? key,
}) : super(key: key);
@override
State<DisclosureButton> createState() => _DisclosureButtonState();
}
class _DisclosureButtonState extends State<DisclosureButton> {
@override
Widget build(BuildContext context) {
return PopoverActionList<FilterDisclosureActionWrapper>(
asBarrier: true,
mutex: widget.popoverMutex,
direction: PopoverDirection.rightWithTopAligned,
actions: FilterDisclosureAction.values
.map((action) => FilterDisclosureActionWrapper(action))
.toList(),
buildChild: (controller) {
return FlowyIconButton(
width: 20,
icon: svgWidget(
"editor/details",
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: () => controller.show(),
);
},
onSelected: (action, controller) async {
widget.onAction(action.inner);
controller.close();
},
);
}
}
enum FilterDisclosureAction {
delete,
}
class FilterDisclosureActionWrapper extends ActionCell {
final FilterDisclosureAction inner;
FilterDisclosureActionWrapper(this.inner);
@override
Widget? leftIcon(Color iconColor) => null;
@override
String get name => inner.name;
}
extension FilterDisclosureActionExtension on FilterDisclosureAction {
String get name {
switch (this) {
case FilterDisclosureAction.delete:
return LocaleKeys.grid_settings_deleteFilter.tr();
}
}
}

View File

@ -0,0 +1,43 @@
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
class FilterInfo {
final String viewId;
final FilterPB filter;
final FieldInfo field;
FilterInfo(this.viewId, this.filter, this.field);
FilterInfo copyWith({FilterPB? filter, FieldInfo? field}) {
return FilterInfo(
viewId,
filter ?? this.filter,
field ?? this.field,
);
}
DateFilterPB? dateFilter() {
if (filter.fieldType != FieldType.DateTime) {
return null;
}
return DateFilterPB.fromBuffer(filter.data);
}
TextFilterPB? textFilter() {
if (filter.fieldType != FieldType.RichText) {
return null;
}
return TextFilterPB.fromBuffer(filter.data);
}
CheckboxFilterPB? checkboxFilter() {
if (filter.fieldType != FieldType.Checkbox) {
return null;
}
return CheckboxFilterPB.fromBuffer(filter.data);
}
}

View File

@ -0,0 +1,135 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart';
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'create_filter_list.dart';
import 'menu_item.dart';
class GridFilterMenu extends StatelessWidget {
const GridFilterMenu({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
builder: (context, state) {
if (state.isVisible) {
return _wrapPadding(Column(
children: [
buildDivider(context),
const VSpace(6),
buildFilterItems(state.viewId, state),
],
));
} else {
return const SizedBox();
}
},
);
}
Widget _wrapPadding(Widget child) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
vertical: 6,
),
child: child,
);
}
Widget buildDivider(BuildContext context) {
return Divider(
height: 1.0,
color: AFThemeExtension.of(context).toggleOffFill,
);
}
Widget buildFilterItems(String viewId, GridFilterMenuState state) {
final List<Widget> children = state.filters
.map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
.toList();
return Row(
children: [
SingleChildScrollView(
controller: ScrollController(),
scrollDirection: Axis.horizontal,
child: Wrap(
spacing: 4,
children: children,
),
),
const HSpace(4),
if (state.creatableFields.isNotEmpty) AddFilterButton(viewId: viewId),
],
);
}
}
class AddFilterButton extends StatefulWidget {
final String viewId;
const AddFilterButton({required this.viewId, Key? key}) : super(key: key);
@override
State<AddFilterButton> createState() => _AddFilterButtonState();
}
class _AddFilterButtonState extends State<AddFilterButton> {
late PopoverController popoverController;
@override
void initState() {
popoverController = PopoverController();
super.initState();
}
@override
Widget build(BuildContext context) {
return wrapPopover(
context,
SizedBox(
height: 28,
child: FlowyButton(
text: FlowyText(
LocaleKeys.grid_settings_addFilter.tr(),
fontSize: 12,
),
useIntrinsicWidth: true,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
leftIcon: svgWidget(
"home/add",
color: Theme.of(context).colorScheme.onSurface,
),
onTap: () => popoverController.show(),
),
),
);
}
Widget wrapPopover(BuildContext buildContext, Widget child) {
return AppFlowyPopover(
controller: popoverController,
constraints: BoxConstraints.loose(const Size(200, 300)),
margin: const EdgeInsets.all(6),
triggerActions: PopoverTriggerFlags.none,
child: child,
popupBuilder: (BuildContext context) {
final bloc = buildContext.read<GridFilterMenuBloc>();
return GridCreateFilterList(
viewId: widget.viewId,
fieldController: bloc.fieldController,
onClosed: () => popoverController.close(),
);
},
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pbenum.dart';
import 'package:flutter/material.dart';
import 'choicechip/checkbox.dart';
import 'choicechip/date.dart';
import 'choicechip/number.dart';
import 'choicechip/select_option.dart';
import 'choicechip/text.dart';
import 'choicechip/url.dart';
import 'filter_info.dart';
class FilterMenuItem extends StatelessWidget {
final FilterInfo filterInfo;
const FilterMenuItem({required this.filterInfo, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return buildFilterChoicechip(filterInfo);
}
}
Widget buildFilterChoicechip(FilterInfo filterInfo) {
switch (filterInfo.field.fieldType) {
case FieldType.Checkbox:
return CheckboxFilterChoicechip(filterInfo: filterInfo);
case FieldType.DateTime:
return DateFilterChoicechip(filterInfo: filterInfo);
case FieldType.MultiSelect:
return SelectOptionFilterChoicechip(filterInfo: filterInfo);
case FieldType.Number:
return NumberFilterChoicechip(filterInfo: filterInfo);
case FieldType.RichText:
return TextFilterChoicechip(filterInfo: filterInfo);
case FieldType.SingleSelect:
return SelectOptionFilterChoicechip(filterInfo: filterInfo);
case FieldType.URL:
return URLFilterChoicechip(filterInfo: filterInfo);
default:
return const SizedBox();
}
}

View File

@ -0,0 +1,76 @@
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/text_style.dart';
import 'package:flutter/material.dart';
import 'package:textstyle_extensions/textstyle_extensions.dart';
class FilterTextField extends StatefulWidget {
final String hintText;
final String text;
final void Function(String)? onChanged;
final void Function(String)? onSubmitted;
final bool autoFucous;
const FilterTextField({
this.hintText = "",
this.text = "",
this.onChanged,
this.onSubmitted,
this.autoFucous = true,
Key? key,
}) : super(key: key);
@override
State<FilterTextField> createState() => FilterTextFieldState();
}
class FilterTextFieldState extends State<FilterTextField> {
late FocusNode focusNode;
late TextEditingController controller;
@override
void initState() {
focusNode = FocusNode();
controller = TextEditingController();
controller.text = widget.text;
if (widget.autoFucous) {
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
}
super.initState();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
focusNode: focusNode,
onChanged: (text) {
widget.onChanged?.call(text);
},
onSubmitted: (text) {
widget.onSubmitted?.call(text);
},
maxLines: 1,
style: TextStyles.body1.size(FontSizes.s12),
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(10),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 1.0,
),
borderRadius: Corners.s10Border,
),
isDense: true,
hintText: widget.hintText,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 1.0,
),
borderRadius: Corners.s8Border,
),
),
);
}
}

View File

@ -45,7 +45,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
builder: (context, state) { builder: (context, state) {
final button = AppFlowyPopover( final button = AppFlowyPopover(
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
constraints: BoxConstraints.loose(const Size(240, 840)), constraints: BoxConstraints.loose(const Size(240, 440)),
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
controller: popoverController, controller: popoverController,
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
@ -172,6 +172,7 @@ class FieldCellButton extends StatelessWidget {
field.fieldType.iconName(), field.fieldType.iconName(),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
radius: BorderRadius.zero,
text: FlowyText.medium( text: FlowyText.medium(
text, text,
maxLines: maxLines, maxLines: maxLines,

View File

@ -88,9 +88,9 @@ class _EditFieldButton extends StatelessWidget {
} }
class _FieldOperationList extends StatelessWidget { class _FieldOperationList extends StatelessWidget {
final GridFieldCellContext fieldContext; final GridFieldCellContext fieldInfo;
final VoidCallback onDismissed; final VoidCallback onDismissed;
const _FieldOperationList(this.fieldContext, this.onDismissed, {Key? key}) const _FieldOperationList(this.fieldInfo, this.onDismissed, {Key? key})
: super(key: key); : super(key: key);
@override @override
@ -113,14 +113,14 @@ class _FieldOperationList extends StatelessWidget {
bool enable = true; bool enable = true;
switch (action) { switch (action) {
case FieldAction.delete: case FieldAction.delete:
enable = !fieldContext.field.isPrimary; enable = !fieldInfo.field.isPrimary;
break; break;
default: default:
break; break;
} }
return FieldActionCell( return FieldActionCell(
fieldContext: fieldContext, fieldInfo: fieldInfo,
action: action, action: action,
onTap: onDismissed, onTap: onDismissed,
enable: enable, enable: enable,
@ -131,13 +131,13 @@ class _FieldOperationList extends StatelessWidget {
} }
class FieldActionCell extends StatelessWidget { class FieldActionCell extends StatelessWidget {
final GridFieldCellContext fieldContext; final GridFieldCellContext fieldInfo;
final VoidCallback onTap; final VoidCallback onTap;
final FieldAction action; final FieldAction action;
final bool enable; final bool enable;
const FieldActionCell({ const FieldActionCell({
required this.fieldContext, required this.fieldInfo,
required this.action, required this.action,
required this.onTap, required this.onTap,
required this.enable, required this.enable,
@ -153,7 +153,7 @@ class FieldActionCell extends StatelessWidget {
), ),
onTap: () { onTap: () {
if (enable) { if (enable) {
action.run(context, fieldContext); action.run(context, fieldInfo);
onTap(); onTap();
} }
}, },
@ -196,7 +196,7 @@ extension _FieldActionExtension on FieldAction {
} }
} }
void run(BuildContext context, GridFieldCellContext fieldContext) { void run(BuildContext context, GridFieldCellContext fieldInfo) {
switch (this) { switch (this) {
case FieldAction.hide: case FieldAction.hide:
context context
@ -207,8 +207,8 @@ extension _FieldActionExtension on FieldAction {
PopoverContainer.of(context).close(); PopoverContainer.of(context).close();
FieldService( FieldService(
gridId: fieldContext.gridId, gridId: fieldInfo.gridId,
fieldId: fieldContext.field.id, fieldId: fieldInfo.field.id,
).duplicateField(); ).duplicateField();
break; break;
@ -219,8 +219,8 @@ extension _FieldActionExtension on FieldAction {
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
confirm: () { confirm: () {
FieldService( FieldService(
gridId: fieldContext.gridId, gridId: fieldInfo.gridId,
fieldId: fieldContext.field.id, fieldId: fieldInfo.field.id,
).deleteField(); ).deleteField();
}, },
).show(context); ).show(context);

View File

@ -115,7 +115,7 @@ class _FieldTypeOptionCell extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return state.field.fold( return state.field.fold(
() => const SizedBox(), () => const SizedBox(),
(fieldContext) { (fieldInfo) {
final dataController = final dataController =
context.read<FieldEditorBloc>().dataController; context.read<FieldEditorBloc>().dataController;
return FieldTypeOptionEditor( return FieldTypeOptionEditor(

View File

@ -87,7 +87,7 @@ class _SwitchFieldButton extends StatelessWidget {
final widget = AppFlowyPopover( final widget = AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(460, 540)), constraints: BoxConstraints.loose(const Size(460, 540)),
asBarrier: true, asBarrier: true,
triggerActions: PopoverTriggerFlags.click | PopoverTriggerFlags.hover, triggerActions: PopoverTriggerFlags.click,
mutex: popoverMutex, mutex: popoverMutex,
offset: const Offset(20, 0), offset: const Offset(20, 0),
popupBuilder: (popOverContext) { popupBuilder: (popOverContext) {

View File

@ -11,7 +11,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo;
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'checkbox.dart'; import 'checkbox.dart';
@ -130,18 +130,17 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({ TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({
required String gridId, required String gridId,
required GridFieldContext fieldContext, required FieldInfo fieldInfo,
}) { }) {
final loader = final loader = FieldTypeOptionLoader(gridId: gridId, field: fieldInfo.field);
FieldTypeOptionLoader(gridId: gridId, field: fieldContext.field);
final dataController = TypeOptionDataController( final dataController = TypeOptionDataController(
gridId: gridId, gridId: gridId,
loader: loader, loader: loader,
fieldContext: fieldContext, fieldInfo: fieldInfo,
); );
return makeTypeOptionContextWithDataController( return makeTypeOptionContextWithDataController(
gridId: gridId, gridId: gridId,
fieldType: fieldContext.fieldType, fieldType: fieldInfo.fieldType,
dataController: dataController, dataController: dataController,
); );
} }

View File

@ -53,13 +53,22 @@ class DateTypeOptionWidget extends TypeOptionWidget {
listener: (context, state) => listener: (context, state) =>
typeOptionContext.typeOption = state.typeOption, typeOptionContext.typeOption = state.typeOption,
builder: (context, state) { builder: (context, state) {
return Column( final List<Widget> children = [
children: [ const TypeOptionSeparator(),
const TypeOptionSeparator(), _renderDateFormatButton(context, state.typeOption.dateFormat),
_renderDateFormatButton(context, state.typeOption.dateFormat), _renderTimeFormatButton(context, state.typeOption.timeFormat),
_renderTimeFormatButton(context, state.typeOption.timeFormat), const _IncludeTimeButton(),
const _IncludeTimeButton(), ];
],
return ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
itemCount: children.length,
itemBuilder: (BuildContext context, int index) {
return children[index];
},
); );
}, },
), ),

View File

@ -224,13 +224,13 @@ class RowContent extends StatelessWidget {
final GridCellWidget child = builder.build(cellId); final GridCellWidget child = builder.build(cellId);
return CellContainer( return CellContainer(
width: cellId.fieldContext.width.toDouble(), width: cellId.fieldInfo.width.toDouble(),
rowStateNotifier: rowStateNotifier:
Provider.of<RegionStateNotifier>(context, listen: false), Provider.of<RegionStateNotifier>(context, listen: false),
accessoryBuilder: (buildContext) { accessoryBuilder: (buildContext) {
final builder = child.accessoryBuilder; final builder = child.accessoryBuilder;
List<GridCellAccessoryBuilder> accessories = []; List<GridCellAccessoryBuilder> accessories = [];
if (cellId.fieldContext.isPrimary) { if (cellId.fieldInfo.isPrimary) {
accessories.add( accessories.add(
GridCellAccessoryBuilder( GridCellAccessoryBuilder(
builder: (key) => PrimaryCellAccessory( builder: (key) => PrimaryCellAccessory(

View File

@ -281,7 +281,7 @@ class _RowDetailCellState extends State<_RowDetailCell> {
width: 150, width: 150,
child: FieldCellButton( child: FieldCellButton(
maxLines: null, maxLines: null,
field: widget.cellId.fieldContext.field, field: widget.cellId.fieldInfo.field,
onTap: () => popover.show(), onTap: () => popover.show(),
), ),
), ),
@ -297,11 +297,11 @@ class _RowDetailCellState extends State<_RowDetailCell> {
Widget buildFieldEditor() { Widget buildFieldEditor() {
return FieldEditor( return FieldEditor(
gridId: widget.cellId.gridId, gridId: widget.cellId.gridId,
fieldName: widget.cellId.fieldContext.field.name, fieldName: widget.cellId.fieldInfo.field.name,
isGroupField: widget.cellId.fieldContext.isGroupField, isGroupField: widget.cellId.fieldInfo.isGroupField,
typeOptionLoader: FieldTypeOptionLoader( typeOptionLoader: FieldTypeOptionLoader(
gridId: widget.cellId.gridId, gridId: widget.cellId.gridId,
field: widget.cellId.fieldContext.field, field: widget.cellId.fieldInfo.field,
), ),
onDeleted: (fieldId) { onDeleted: (fieldId) {
popover.close(); popover.close();

View File

@ -0,0 +1,81 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../filter/create_filter_list.dart';
class FilterButton extends StatefulWidget {
const FilterButton({Key? key}) : super(key: key);
@override
State<FilterButton> createState() => _FilterButtonState();
}
class _FilterButtonState extends State<FilterButton> {
final _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
builder: (context, state) {
final textColor = state.filters.isEmpty
? null
: Theme.of(context).colorScheme.primary;
return wrapPopover(
context,
SizedBox(
height: 26,
child: FlowyTextButton(
LocaleKeys.grid_settings_filter.tr(),
fontSize: 14,
fontColor: textColor,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
onPressed: () {
final bloc = context.read<GridFilterMenuBloc>();
if (bloc.state.filters.isEmpty) {
_popoverController.show();
} else {
bloc.add(const GridFilterMenuEvent.toggleMenu());
}
},
),
),
);
},
);
}
Widget wrapPopover(BuildContext buildContext, Widget child) {
return AppFlowyPopover(
controller: _popoverController,
direction: PopoverDirection.leftWithTopAligned,
constraints: BoxConstraints.loose(const Size(200, 300)),
offset: const Offset(0, 10),
margin: const EdgeInsets.all(6),
triggerActions: PopoverTriggerFlags.none,
child: child,
popupBuilder: (BuildContext context) {
final bloc = buildContext.read<GridFilterMenuBloc>();
return GridCreateFilterList(
viewId: bloc.viewId,
fieldController: bloc.fieldController,
onClosed: () => _popoverController.close(),
onCreateFilter: () {
if (!bloc.state.isVisible) {
bloc.add(const GridFilterMenuEvent.toggleMenu());
}
},
);
},
);
}
}

View File

@ -30,14 +30,14 @@ class GridGroupList extends StatelessWidget {
)..add(const GridGroupEvent.initial()), )..add(const GridGroupEvent.initial()),
child: BlocBuilder<GridGroupBloc, GridGroupState>( child: BlocBuilder<GridGroupBloc, GridGroupState>(
builder: (context, state) { builder: (context, state) {
final cells = state.fieldContexts.map((fieldContext) { final cells = state.fieldContexts.map((fieldInfo) {
Widget cell = _GridGroupCell( Widget cell = _GridGroupCell(
fieldContext: fieldContext, fieldInfo: fieldInfo,
onSelected: () => onDismissed(), onSelected: () => onDismissed(),
key: ValueKey(fieldContext.id), key: ValueKey(fieldInfo.id),
); );
if (!fieldContext.canGroup) { if (!fieldInfo.canGroup) {
cell = IgnorePointer(child: Opacity(opacity: 0.3, child: cell)); cell = IgnorePointer(child: Opacity(opacity: 0.3, child: cell));
} }
return cell; return cell;
@ -61,9 +61,9 @@ class GridGroupList extends StatelessWidget {
class _GridGroupCell extends StatelessWidget { class _GridGroupCell extends StatelessWidget {
final VoidCallback onSelected; final VoidCallback onSelected;
final GridFieldContext fieldContext; final FieldInfo fieldInfo;
const _GridGroupCell({ const _GridGroupCell({
required this.fieldContext, required this.fieldInfo,
required this.onSelected, required this.onSelected,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -71,7 +71,7 @@ class _GridGroupCell extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget? rightIcon; Widget? rightIcon;
if (fieldContext.isGroupField) { if (fieldInfo.isGroupField) {
rightIcon = Padding( rightIcon = Padding(
padding: const EdgeInsets.all(2.0), padding: const EdgeInsets.all(2.0),
child: svgWidget("grid/checkmark"), child: svgWidget("grid/checkmark"),
@ -81,17 +81,17 @@ class _GridGroupCell extends StatelessWidget {
return SizedBox( return SizedBox(
height: GridSize.typeOptionItemHeight, height: GridSize.typeOptionItemHeight,
child: FlowyButton( child: FlowyButton(
text: FlowyText.medium(fieldContext.name), text: FlowyText.medium(fieldInfo.name),
leftIcon: svgWidget( leftIcon: svgWidget(
fieldContext.fieldType.iconName(), fieldInfo.fieldType.iconName(),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
rightIcon: rightIcon, rightIcon: rightIcon,
onTap: () { onTap: () {
context.read<GridGroupBloc>().add( context.read<GridGroupBloc>().add(
GridGroupEvent.setGroupByField( GridGroupEvent.setGroupByField(
fieldContext.id, fieldInfo.id,
fieldContext.fieldType, fieldInfo.fieldType,
), ),
); );
onSelected(); onSelected();

View File

@ -51,7 +51,7 @@ class _GridPropertyListState extends State<GridPropertyList> {
return _GridPropertyCell( return _GridPropertyCell(
popoverMutex: _popoverMutex, popoverMutex: _popoverMutex,
gridId: widget.gridId, gridId: widget.gridId,
fieldContext: field, fieldInfo: field,
key: ValueKey(field.id), key: ValueKey(field.id),
); );
}).toList(); }).toList();
@ -74,12 +74,12 @@ class _GridPropertyListState extends State<GridPropertyList> {
} }
class _GridPropertyCell extends StatelessWidget { class _GridPropertyCell extends StatelessWidget {
final GridFieldContext fieldContext; final FieldInfo fieldInfo;
final String gridId; final String gridId;
final PopoverMutex popoverMutex; final PopoverMutex popoverMutex;
const _GridPropertyCell({ const _GridPropertyCell({
required this.gridId, required this.gridId,
required this.fieldContext, required this.fieldInfo,
required this.popoverMutex, required this.popoverMutex,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -87,7 +87,7 @@ class _GridPropertyCell extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final checkmark = svgWidget( final checkmark = svgWidget(
fieldContext.visibility ? 'home/show' : 'home/hide', fieldInfo.visibility ? 'home/show' : 'home/hide',
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
); );
@ -104,7 +104,7 @@ class _GridPropertyCell extends StatelessWidget {
onPressed: () { onPressed: () {
context.read<GridPropertyBloc>().add( context.read<GridPropertyBloc>().add(
GridPropertyEvent.setFieldVisibility( GridPropertyEvent.setFieldVisibility(
fieldContext.id, !fieldContext.visibility)); fieldInfo.id, !fieldInfo.visibility));
}, },
icon: checkmark.padding(all: 6), icon: checkmark.padding(all: 6),
) )
@ -116,21 +116,22 @@ class _GridPropertyCell extends StatelessWidget {
return AppFlowyPopover( return AppFlowyPopover(
mutex: popoverMutex, mutex: popoverMutex,
offset: const Offset(20, 0), offset: const Offset(20, 0),
direction: PopoverDirection.leftWithTopAligned,
constraints: BoxConstraints.loose(const Size(240, 400)), constraints: BoxConstraints.loose(const Size(240, 400)),
child: FlowyButton( child: FlowyButton(
text: FlowyText.medium(fieldContext.name), text: FlowyText.medium(fieldInfo.name),
leftIcon: svgWidget( leftIcon: svgWidget(
fieldContext.fieldType.iconName(), fieldInfo.fieldType.iconName(),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
return FieldEditor( return FieldEditor(
gridId: gridId, gridId: gridId,
fieldName: fieldContext.name, fieldName: fieldInfo.name,
typeOptionLoader: FieldTypeOptionLoader( typeOptionLoader: FieldTypeOptionLoader(
gridId: gridId, gridId: gridId,
field: fieldContext.field, field: fieldInfo.field,
), ),
); );
}, },

View File

@ -6,7 +6,6 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import '../../../application/field/field_controller.dart'; import '../../../application/field/field_controller.dart';
@ -31,33 +30,11 @@ class GridSettingList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider(
create: (context) => GridSettingBloc(gridId: settingContext.gridId),
child: BlocListener<GridSettingBloc, GridSettingState>(
listenWhen: (previous, current) =>
previous.selectedAction != current.selectedAction,
listener: (context, state) {
state.selectedAction.foldLeft(null, (_, action) {
onAction(action, settingContext);
});
},
child: BlocBuilder<GridSettingBloc, GridSettingState>(
builder: (context, state) {
return _renderList();
},
),
),
);
}
String identifier() {
return toString();
}
Widget _renderList() {
final cells = final cells =
GridSettingAction.values.where((value) => value.enable()).map((action) { GridSettingAction.values.where((value) => value.enable()).map((action) {
return _SettingItem(action: action); return _SettingItem(
action: action,
onAction: (action) => onAction(action, settingContext));
}).toList(); }).toList();
return SizedBox( return SizedBox(
@ -80,33 +57,24 @@ class GridSettingList extends StatelessWidget {
class _SettingItem extends StatelessWidget { class _SettingItem extends StatelessWidget {
final GridSettingAction action; final GridSettingAction action;
final Function(GridSettingAction) onAction;
const _SettingItem({ const _SettingItem({
required this.action, required this.action,
required this.onAction,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isSelected = context
.read<GridSettingBloc>()
.state
.selectedAction
.foldLeft(false, (_, selectedAction) => selectedAction == action);
return SizedBox( return SizedBox(
height: GridSize.typeOptionItemHeight, height: GridSize.typeOptionItemHeight,
child: FlowyButton( child: FlowyButton(
isSelected: isSelected,
text: FlowyText.medium( text: FlowyText.medium(
action.title(), action.title(),
color: action.enable() ? null : Theme.of(context).disabledColor, color: action.enable() ? null : Theme.of(context).disabledColor,
), ),
onTap: () { onTap: () => onAction(action),
context
.read<GridSettingBloc>()
.add(GridSettingEvent.performAction(action));
},
leftIcon: svgWidget( leftIcon: svgWidget(
action.iconName(), action.iconName(),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
@ -119,29 +87,29 @@ class _SettingItem extends StatelessWidget {
extension _GridSettingExtension on GridSettingAction { extension _GridSettingExtension on GridSettingAction {
String iconName() { String iconName() {
switch (this) { switch (this) {
case GridSettingAction.filter: case GridSettingAction.showFilters:
return 'grid/setting/filter'; return 'grid/setting/filter';
case GridSettingAction.sortBy: case GridSettingAction.sortBy:
return 'grid/setting/sort'; return 'grid/setting/sort';
case GridSettingAction.properties: case GridSettingAction.showProperties:
return 'grid/setting/properties'; return 'grid/setting/properties';
} }
} }
String title() { String title() {
switch (this) { switch (this) {
case GridSettingAction.filter: case GridSettingAction.showFilters:
return LocaleKeys.grid_settings_filter.tr(); return LocaleKeys.grid_settings_filter.tr();
case GridSettingAction.sortBy: case GridSettingAction.sortBy:
return LocaleKeys.grid_settings_sortBy.tr(); return LocaleKeys.grid_settings_sortBy.tr();
case GridSettingAction.properties: case GridSettingAction.showProperties:
return LocaleKeys.grid_settings_Properties.tr(); return LocaleKeys.grid_settings_Properties.tr();
} }
} }
bool enable() { bool enable() {
switch (this) { switch (this) {
case GridSettingAction.properties: case GridSettingAction.showProperties:
return true; return true;
default: default:
return false; return false;

View File

@ -1,14 +1,9 @@
import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../application/field/field_controller.dart'; import '../../../application/field/field_controller.dart';
import '../../layout/sizes.dart'; import '../../layout/sizes.dart';
import 'grid_property.dart'; import 'filter_button.dart';
import 'grid_setting.dart'; import 'setting_button.dart';
class GridToolbarContext { class GridToolbarContext {
final String gridId; final String gridId;
@ -20,82 +15,21 @@ class GridToolbarContext {
} }
class GridToolbar extends StatelessWidget { class GridToolbar extends StatelessWidget {
final GridToolbarContext toolbarContext; const GridToolbar({Key? key}) : super(key: key);
const GridToolbar({required this.toolbarContext, Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final settingContext = GridSettingContext(
gridId: toolbarContext.gridId,
fieldController: toolbarContext.fieldController,
);
return SizedBox( return SizedBox(
height: 40, height: 40,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
SizedBox(width: GridSize.leadingHeaderPadding), SizedBox(width: GridSize.leadingHeaderPadding),
_SettingButton(settingContext: settingContext),
const Spacer(), const Spacer(),
const FilterButton(),
const SettingButton(),
], ],
), ),
); );
} }
} }
class _SettingButton extends StatelessWidget {
final GridSettingContext settingContext;
const _SettingButton({required this.settingContext, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(260, 400)),
offset: const Offset(0, 10),
margin: const EdgeInsets.all(6),
child: FlowyIconButton(
width: 22,
icon: svgWidget(
"grid/setting/setting",
color: Theme.of(context).colorScheme.onSurface,
).padding(horizontal: 3, vertical: 3),
),
popupBuilder: (BuildContext context) {
return _GridSettingListPopover(settingContext: settingContext);
},
);
}
}
class _GridSettingListPopover extends StatefulWidget {
final GridSettingContext settingContext;
const _GridSettingListPopover({Key? key, required this.settingContext})
: super(key: key);
@override
State<StatefulWidget> createState() => _GridSettingListPopoverState();
}
class _GridSettingListPopoverState extends State<_GridSettingListPopover> {
GridSettingAction? _action;
@override
Widget build(BuildContext context) {
if (_action == GridSettingAction.properties) {
return GridPropertyList(
gridId: widget.settingContext.gridId,
fieldController: widget.settingContext.fieldController,
);
}
return GridSettingList(
settingContext: widget.settingContext,
onAction: (action, settingContext) {
setState(() {
_action = action;
});
},
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'grid_property.dart';
import 'grid_setting.dart';
class SettingButton extends StatefulWidget {
const SettingButton({Key? key}) : super(key: key);
@override
State<SettingButton> createState() => _SettingButtonState();
}
class _SettingButtonState extends State<SettingButton> {
late PopoverController popoverController;
@override
void initState() {
popoverController = PopoverController();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocSelector<GridBloc, GridState, GridSettingContext>(
selector: (state) {
final fieldController =
context.read<GridBloc>().gridController.fieldController;
return GridSettingContext(
gridId: state.gridId,
fieldController: fieldController,
);
},
builder: (context, settingContext) {
return AppFlowyPopover(
controller: popoverController,
constraints: BoxConstraints.loose(const Size(260, 400)),
direction: PopoverDirection.leftWithTopAligned,
offset: const Offset(0, 10),
margin: const EdgeInsets.all(6),
triggerActions: PopoverTriggerFlags.none,
child: FlowyTextButton(
LocaleKeys.settings_title.tr(),
fontSize: 14,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),
onPressed: () {
popoverController.show();
},
),
popupBuilder: (BuildContext context) {
return _GridSettingListPopover(settingContext: settingContext);
},
);
},
);
}
}
class _GridSettingListPopover extends StatefulWidget {
final GridSettingContext settingContext;
const _GridSettingListPopover({Key? key, required this.settingContext})
: super(key: key);
@override
State<StatefulWidget> createState() => _GridSettingListPopoverState();
}
class _GridSettingListPopoverState extends State<_GridSettingListPopover> {
GridSettingAction? _action;
@override
Widget build(BuildContext context) {
if (_action == GridSettingAction.showProperties) {
return GridPropertyList(
gridId: widget.settingContext.gridId,
fieldController: widget.settingContext.fieldController,
);
}
return GridSettingList(
settingContext: widget.settingContext,
onAction: (action, settingContext) {
setState(() {
_action = action;
});
},
);
}
}

View File

@ -127,11 +127,6 @@ void _resolveDocDeps(GetIt getIt) {
} }
void _resolveGridDeps(GetIt getIt) { void _resolveGridDeps(GetIt getIt) {
// GridPB
getIt.registerFactoryParam<GridBloc, ViewPB, void>(
(view, _) => GridBloc(view: view),
);
getIt.registerFactoryParam<GridHeaderBloc, String, GridFieldController>( getIt.registerFactoryParam<GridHeaderBloc, String, GridFieldController>(
(gridId, fieldController) => GridHeaderBloc( (gridId, fieldController) => GridHeaderBloc(
gridId: gridId, gridId: gridId,

View File

@ -1,5 +1,4 @@
import 'package:app_flowy/user/application/user_listener.dart'; import 'package:app_flowy/user/application/user_listener.dart';
import 'package:app_flowy/workspace/application/edit_panel/edit_context.dart';
import 'package:flowy_infra/time/duration.dart'; import 'package:flowy_infra/time/duration.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
@ -38,40 +37,12 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
showLoading: (e) async { showLoading: (e) async {
emit(state.copyWith(isLoading: e.isLoading)); emit(state.copyWith(isLoading: e.isLoading));
}, },
setEditPanel: (e) async {
emit(state.copyWith(panelContext: some(e.editContext)));
},
dismissEditPanel: (value) async {
emit(state.copyWith(panelContext: none()));
},
forceCollapse: (e) async {
emit(state.copyWith(forceCollapse: e.forceCollapse));
},
didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) {
emit(state.copyWith(workspaceSetting: value.setting)); emit(state.copyWith(workspaceSetting: value.setting));
}, },
unauthorized: (_Unauthorized value) { unauthorized: (_Unauthorized value) {
emit(state.copyWith(unauthorized: true)); emit(state.copyWith(unauthorized: true));
}, },
collapseMenu: (_CollapseMenu e) {
emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed));
},
editPanelResizeStart: (_EditPanelResizeStart e) {
emit(state.copyWith(
resizeType: MenuResizeType.drag,
resizeStart: state.resizeOffset,
));
},
editPanelResized: (_EditPanelResized e) {
final newPosition =
(e.offset + state.resizeStart).clamp(-50, 200).toDouble();
if (state.resizeOffset != newPosition) {
emit(state.copyWith(resizeOffset: newPosition));
}
},
editPanelResizeEnd: (_EditPanelResizeEnd e) {
emit(state.copyWith(resizeType: MenuResizeType.slide));
},
); );
}, },
); );
@ -112,42 +83,22 @@ extension MenuResizeTypeExtension on MenuResizeType {
class HomeEvent with _$HomeEvent { class HomeEvent with _$HomeEvent {
const factory HomeEvent.initial() = _Initial; const factory HomeEvent.initial() = _Initial;
const factory HomeEvent.showLoading(bool isLoading) = _ShowLoading; const factory HomeEvent.showLoading(bool isLoading) = _ShowLoading;
const factory HomeEvent.forceCollapse(bool forceCollapse) = _ForceCollapse;
const factory HomeEvent.setEditPanel(EditPanelContext editContext) =
_ShowEditPanel;
const factory HomeEvent.dismissEditPanel() = _DismissEditPanel;
const factory HomeEvent.didReceiveWorkspaceSetting( const factory HomeEvent.didReceiveWorkspaceSetting(
WorkspaceSettingPB setting) = _DidReceiveWorkspaceSetting; WorkspaceSettingPB setting) = _DidReceiveWorkspaceSetting;
const factory HomeEvent.unauthorized(String msg) = _Unauthorized; const factory HomeEvent.unauthorized(String msg) = _Unauthorized;
const factory HomeEvent.collapseMenu() = _CollapseMenu;
const factory HomeEvent.editPanelResized(double offset) = _EditPanelResized;
const factory HomeEvent.editPanelResizeStart() = _EditPanelResizeStart;
const factory HomeEvent.editPanelResizeEnd() = _EditPanelResizeEnd;
} }
@freezed @freezed
class HomeState with _$HomeState { class HomeState with _$HomeState {
const factory HomeState({ const factory HomeState({
required bool isLoading, required bool isLoading,
required bool forceCollapse,
required Option<EditPanelContext> panelContext,
required WorkspaceSettingPB workspaceSetting, required WorkspaceSettingPB workspaceSetting,
required bool unauthorized, required bool unauthorized,
required bool isMenuCollapsed,
required double resizeOffset,
required double resizeStart,
required MenuResizeType resizeType,
}) = _HomeState; }) = _HomeState;
factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState( factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState(
isLoading: false, isLoading: false,
forceCollapse: false,
panelContext: none(),
workspaceSetting: workspaceSetting, workspaceSetting: workspaceSetting,
unauthorized: false, unauthorized: false,
isMenuCollapsed: false,
resizeOffset: 0,
resizeStart: 0,
resizeType: MenuResizeType.slide,
); );
} }

View File

@ -0,0 +1,124 @@
import 'package:app_flowy/user/application/user_listener.dart';
import 'package:app_flowy/workspace/application/edit_panel/edit_context.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart'
show WorkspaceSettingPB;
import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dartz/dartz.dart';
part 'home_setting_bloc.freezed.dart';
class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
final UserWorkspaceListener _listener;
HomeSettingBloc(
UserProfilePB user,
WorkspaceSettingPB workspaceSetting,
) : _listener = UserWorkspaceListener(userProfile: user),
super(HomeSettingState.initial(workspaceSetting)) {
on<HomeSettingEvent>(
(event, emit) async {
await event.map(
initial: (_Initial value) {},
setEditPanel: (e) async {
emit(state.copyWith(panelContext: some(e.editContext)));
},
dismissEditPanel: (value) async {
emit(state.copyWith(panelContext: none()));
},
forceCollapse: (e) async {
emit(state.copyWith(forceCollapse: e.forceCollapse));
},
didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) {
emit(state.copyWith(workspaceSetting: value.setting));
},
collapseMenu: (_CollapseMenu e) {
emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed));
},
editPanelResizeStart: (_EditPanelResizeStart e) {
emit(state.copyWith(
resizeType: MenuResizeType.drag,
resizeStart: state.resizeOffset,
));
},
editPanelResized: (_EditPanelResized e) {
final newPosition =
(e.offset + state.resizeStart).clamp(-50, 200).toDouble();
if (state.resizeOffset != newPosition) {
emit(state.copyWith(resizeOffset: newPosition));
}
},
editPanelResizeEnd: (_EditPanelResizeEnd e) {
emit(state.copyWith(resizeType: MenuResizeType.slide));
},
);
},
);
}
@override
Future<void> close() async {
await _listener.stop();
return super.close();
}
}
enum MenuResizeType {
slide,
drag,
}
extension MenuResizeTypeExtension on MenuResizeType {
Duration duration() {
switch (this) {
case MenuResizeType.drag:
return 30.milliseconds;
case MenuResizeType.slide:
return 350.milliseconds;
}
}
}
@freezed
class HomeSettingEvent with _$HomeSettingEvent {
const factory HomeSettingEvent.initial() = _Initial;
const factory HomeSettingEvent.forceCollapse(bool forceCollapse) =
_ForceCollapse;
const factory HomeSettingEvent.setEditPanel(EditPanelContext editContext) =
_ShowEditPanel;
const factory HomeSettingEvent.dismissEditPanel() = _DismissEditPanel;
const factory HomeSettingEvent.didReceiveWorkspaceSetting(
WorkspaceSettingPB setting) = _DidReceiveWorkspaceSetting;
const factory HomeSettingEvent.collapseMenu() = _CollapseMenu;
const factory HomeSettingEvent.editPanelResized(double offset) =
_EditPanelResized;
const factory HomeSettingEvent.editPanelResizeStart() = _EditPanelResizeStart;
const factory HomeSettingEvent.editPanelResizeEnd() = _EditPanelResizeEnd;
}
@freezed
class HomeSettingState with _$HomeSettingState {
const factory HomeSettingState({
required bool forceCollapse,
required Option<EditPanelContext> panelContext,
required WorkspaceSettingPB workspaceSetting,
required bool unauthorized,
required bool isMenuCollapsed,
required double resizeOffset,
required double resizeStart,
required MenuResizeType resizeType,
}) = _HomeSettingState;
factory HomeSettingState.initial(WorkspaceSettingPB workspaceSetting) =>
HomeSettingState(
forceCollapse: false,
panelContext: none(),
workspaceSetting: workspaceSetting,
unauthorized: false,
isMenuCollapsed: false,
resizeOffset: 0,
resizeStart: 0,
resizeType: MenuResizeType.slide,
);
}

View File

@ -1,6 +1,6 @@
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// ignore: import_of_legacy_library_into_null_safe // ignore: import_of_legacy_library_into_null_safe
@ -20,20 +20,19 @@ class HomeLayout {
late double menuSpacing; late double menuSpacing;
late Duration animDuration; late Duration animDuration;
HomeLayout(BuildContext context, BoxConstraints homeScreenConstraint, HomeLayout(BuildContext context, BoxConstraints homeScreenConstraint) {
bool forceCollapse) { final homeSetting = context.read<HomeSettingBloc>().state;
final homeBlocState = context.read<HomeBloc>().state;
showEditPanel = homeBlocState.panelContext.isSome(); showEditPanel = homeSetting.panelContext.isSome();
menuWidth = Sizes.sideBarMed; menuWidth = Sizes.sideBarMed;
if (context.widthPx >= PageBreaks.desktop) { if (context.widthPx >= PageBreaks.desktop) {
menuWidth = Sizes.sideBarLg; menuWidth = Sizes.sideBarLg;
} }
menuWidth += homeBlocState.resizeOffset; menuWidth += homeSetting.resizeOffset;
if (forceCollapse) { if (homeSetting.forceCollapse) {
showMenu = false; showMenu = false;
} else { } else {
showMenu = true; showMenu = true;
@ -43,7 +42,7 @@ class HomeLayout {
homePageLOffset = (showMenu && !menuIsDrawer) ? menuWidth : 0.0; homePageLOffset = (showMenu && !menuIsDrawer) ? menuWidth : 0.0;
menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0; menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0;
animDuration = homeBlocState.resizeType.duration(); animDuration = homeSetting.resizeType.duration();
editPanelWidth = HomeSizes.editPanelWidth; editPanelWidth = HomeSizes.editPanelWidth;
homePageROffset = showEditPanel ? editPanelWidth : 0; homePageROffset = showEditPanel ? editPanelWidth : 0;

View File

@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/blank/blank.dart';
import 'package:app_flowy/startup/plugin/plugin.dart'; import 'package:app_flowy/startup/plugin/plugin.dart';
import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:app_flowy/workspace/application/home/home_service.dart'; import 'package:app_flowy/workspace/application/home/home_service.dart';
import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/hotkeys.dart'; import 'package:app_flowy/workspace/presentation/home/hotkeys.dart';
import 'package:app_flowy/workspace/application/view/view_ext.dart'; import 'package:app_flowy/workspace/application/view/view_ext.dart';
@ -44,6 +45,12 @@ class _HomeScreenState extends State<HomeScreen> {
..add(const HomeEvent.initial()); ..add(const HomeEvent.initial());
}, },
), ),
BlocProvider<HomeSettingBloc>(
create: (context) {
return HomeSettingBloc(widget.user, widget.workspaceSetting)
..add(const HomeSettingEvent.initial());
},
),
], ],
child: HomeHotKeys( child: HomeHotKeys(
child: Scaffold( child: Scaffold(
@ -54,20 +61,20 @@ class _HomeScreenState extends State<HomeScreen> {
Log.error("Push to login screen when user token was invalid"); Log.error("Push to login screen when user token was invalid");
} }
}, },
child: BlocBuilder<HomeBloc, HomeState>( child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
buildWhen: (previous, current) => previous != current, buildWhen: (previous, current) => previous != current,
builder: (context, state) { builder: (context, state) {
final collapsedNotifier = final collapsedNotifier =
getIt<HomeStackManager>().collapsedNotifier; getIt<HomeStackManager>().collapsedNotifier;
collapsedNotifier.addPublishListener((isCollapsed) { collapsedNotifier.addPublishListener((isCollapsed) {
context context
.read<HomeBloc>() .read<HomeSettingBloc>()
.add(HomeEvent.forceCollapse(isCollapsed)); .add(HomeSettingEvent.forceCollapse(isCollapsed));
}); });
return FlowyContainer( return FlowyContainer(
Theme.of(context).colorScheme.surface, Theme.of(context).colorScheme.surface,
// Colors.white, // Colors.white,
child: _buildBody(context, state), child: _buildBody(context),
); );
}, },
), ),
@ -76,25 +83,22 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
Widget _buildBody(BuildContext context, HomeState state) { Widget _buildBody(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
final layout = HomeLayout(context, constraints, state.forceCollapse); final layout = HomeLayout(context, constraints);
final homeStack = HomeStack( final homeStack = HomeStack(
layout: layout, layout: layout,
delegate: HomeScreenStackAdaptor( delegate: HomeScreenStackAdaptor(
buildContext: context, buildContext: context,
homeState: state,
), ),
); );
final menu = _buildHomeMenu( final menu = _buildHomeMenu(
layout: layout, layout: layout,
context: context, context: context,
state: state,
); );
final homeMenuResizer = _buildHomeMenuResizer(context: context); final homeMenuResizer = _buildHomeMenuResizer(context: context);
final editPanel = _buildEditPanel( final editPanel = _buildEditPanel(
homeState: state,
layout: layout, layout: layout,
context: context, context: context,
); );
@ -111,11 +115,11 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
Widget _buildHomeMenu( Widget _buildHomeMenu({
{required HomeLayout layout, required HomeLayout layout,
required BuildContext context, required BuildContext context,
required HomeState state}) { }) {
final workspaceSetting = state.workspaceSetting; final workspaceSetting = widget.workspaceSetting;
final homeMenu = HomeMenu( final homeMenu = HomeMenu(
user: widget.user, user: widget.user,
workspaceSetting: workspaceSetting, workspaceSetting: workspaceSetting,
@ -144,12 +148,12 @@ class _HomeScreenState extends State<HomeScreen> {
return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu)); return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
} }
Widget _buildEditPanel( Widget _buildEditPanel({
{required HomeState homeState, required BuildContext context,
required BuildContext context, required HomeLayout layout,
required HomeLayout layout}) { }) {
final homeBloc = context.read<HomeBloc>(); final homeBloc = context.read<HomeSettingBloc>();
return BlocBuilder<HomeBloc, HomeState>( return BlocBuilder<HomeSettingBloc, HomeSettingState>(
buildWhen: (previous, current) => buildWhen: (previous, current) =>
previous.panelContext != current.panelContext, previous.panelContext != current.panelContext,
builder: (context, state) { builder: (context, state) {
@ -160,7 +164,7 @@ class _HomeScreenState extends State<HomeScreen> {
child: EditPanel( child: EditPanel(
panelContext: panelContext, panelContext: panelContext,
onEndEdit: () => onEndEdit: () =>
homeBloc.add(const HomeEvent.dismissEditPanel()), homeBloc.add(const HomeSettingEvent.dismissEditPanel()),
), ),
), ),
), ),
@ -177,17 +181,17 @@ class _HomeScreenState extends State<HomeScreen> {
child: GestureDetector( child: GestureDetector(
dragStartBehavior: DragStartBehavior.down, dragStartBehavior: DragStartBehavior.down,
onHorizontalDragStart: (details) => context onHorizontalDragStart: (details) => context
.read<HomeBloc>() .read<HomeSettingBloc>()
.add(const HomeEvent.editPanelResizeStart()), .add(const HomeSettingEvent.editPanelResizeStart()),
onHorizontalDragUpdate: (details) => context onHorizontalDragUpdate: (details) => context
.read<HomeBloc>() .read<HomeSettingBloc>()
.add(HomeEvent.editPanelResized(details.localPosition.dx)), .add(HomeSettingEvent.editPanelResized(details.localPosition.dx)),
onHorizontalDragEnd: (details) => context onHorizontalDragEnd: (details) => context
.read<HomeBloc>() .read<HomeSettingBloc>()
.add(const HomeEvent.editPanelResizeEnd()), .add(const HomeSettingEvent.editPanelResizeEnd()),
onHorizontalDragCancel: () => context onHorizontalDragCancel: () => context
.read<HomeBloc>() .read<HomeSettingBloc>()
.add(const HomeEvent.editPanelResizeEnd()), .add(const HomeSettingEvent.editPanelResizeEnd()),
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: SizedBox( child: SizedBox(
width: 10, width: 10,
@ -252,11 +256,9 @@ class _HomeScreenState extends State<HomeScreen> {
class HomeScreenStackAdaptor extends HomeStackDelegate { class HomeScreenStackAdaptor extends HomeStackDelegate {
final BuildContext buildContext; final BuildContext buildContext;
final HomeState homeState;
HomeScreenStackAdaptor({ HomeScreenStackAdaptor({
required this.buildContext, required this.buildContext,
required this.homeState,
}); });
@override @override

View File

@ -1,7 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
@ -22,7 +22,9 @@ class HomeHotKeys extends StatelessWidget {
hotKeyManager.register( hotKeyManager.register(
hotKey, hotKey,
keyDownHandler: (hotKey) { keyDownHandler: (hotKey) {
context.read<HomeBloc>().add(const HomeEvent.collapseMenu()); context
.read<HomeSettingBloc>()
.add(const HomeSettingEvent.collapseMenu());
getIt<HomeStackManager>().collapsedNotifier.value = getIt<HomeStackManager>().collapsedNotifier.value =
!getIt<HomeStackManager>().collapsedNotifier.currentValue!; !getIt<HomeStackManager>().collapsedNotifier.currentValue!;
}, },

View File

@ -54,7 +54,7 @@ class AddButtonActionWrapper extends ActionCell {
AddButtonActionWrapper({required this.pluginBuilder}); AddButtonActionWrapper({required this.pluginBuilder});
@override @override
Widget? icon(Color iconColor) => Widget? leftIcon(Color iconColor) =>
svgWidget(pluginBuilder.menuIcon, color: iconColor); svgWidget(pluginBuilder.menuIcon, color: iconColor);
@override @override

View File

@ -187,7 +187,7 @@ class DisclosureActionWrapper extends ActionCell {
DisclosureActionWrapper(this.inner); DisclosureActionWrapper(this.inner);
@override @override
Widget? icon(Color iconColor) => inner.icon(iconColor); Widget? leftIcon(Color iconColor) => inner.icon(iconColor);
@override @override
String get name => inner.name; String get name => inner.name;

View File

@ -211,7 +211,7 @@ class ViewDisclosureActionWrapper extends ActionCell {
ViewDisclosureActionWrapper(this.inner); ViewDisclosureActionWrapper(this.inner);
@override @override
Widget? icon(Color iconColor) => inner.icon(iconColor); Widget? leftIcon(Color iconColor) => inner.icon(iconColor);
@override @override
String get name => inner.name; String get name => inner.name;

View File

@ -4,6 +4,7 @@ export './app/menu_app.dart';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/trash/menu.dart'; import 'package:app_flowy/plugins/trash/menu.dart';
import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart'; import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -21,7 +22,6 @@ import 'package:expandable/expandable.dart';
import 'package:flowy_infra/time/duration.dart'; import 'package:flowy_infra/time/duration.dart';
import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/menu/menu_bloc.dart'; import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:app_flowy/core/frameless_window.dart'; import 'package:app_flowy/core/frameless_window.dart';
// import 'package:app_flowy/workspace/presentation/home/home_sizes.dart'; // import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
@ -68,7 +68,7 @@ class HomeMenu extends StatelessWidget {
getIt<HomeStackManager>().setPlugin(state.plugin); getIt<HomeStackManager>().setPlugin(state.plugin);
}, },
), ),
BlocListener<HomeBloc, HomeState>( BlocListener<HomeSettingBloc, HomeSettingState>(
listenWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed, listenWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
listener: (context, state) { listener: (context, state) {
_collapsedNotifier.value = state.isMenuCollapsed; _collapsedNotifier.value = state.isMenuCollapsed;
@ -231,8 +231,8 @@ class MenuTopBar extends StatelessWidget {
width: 28, width: 28,
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
onPressed: () => context onPressed: () => context
.read<HomeBloc>() .read<HomeSettingBloc>()
.add(const HomeEvent.collapseMenu()), .add(const HomeSettingEvent.collapseMenu()),
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
icon: svgWidget( icon: svgWidget(
"home/hide_menu", "home/hide_menu",

View File

@ -1,7 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
import 'package:flowy_infra/color_extension.dart'; import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
@ -36,26 +36,6 @@ class NavigationNotifier with ChangeNotifier {
} }
} }
// [[diagram: HomeStack navigation flow]]
//
// 2.notify listeners DefaultHomeStackContext
//
// HomeStackNotifie HomeStack HomeStackContext impl
//
// DocStackContext
//
// 3.notify change 1.set context
//
//
//
// NavigationNotifier ViewSectionItem
//
//
//
//
//
// FlowyNavigation 4.render navigation items
//
class FlowyNavigation extends StatelessWidget { class FlowyNavigation extends StatelessWidget {
const FlowyNavigation({Key? key}) : super(key: key); const FlowyNavigation({Key? key}) : super(key: key);
@ -109,7 +89,9 @@ class FlowyNavigation extends StatelessWidget {
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
onPressed: () { onPressed: () {
notifier.value = false; notifier.value = false;
ctx.read<HomeBloc>().add(const HomeEvent.collapseMenu()); ctx
.read<HomeSettingBloc>()
.add(const HomeSettingEvent.collapseMenu());
}, },
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
icon: svgWidget( icon: svgWidget(

View File

@ -94,7 +94,6 @@ class _BuildEmojiPickerViewState extends State<BuildEmojiPickerView> {
return Stack( return Stack(
children: [ children: [
Positioned( Positioned(
//TODO @gaganyadav80: Not sure about the calculated position.
top: widget.offset!.dy - top: widget.offset!.dy -
MediaQuery.of(context).size.height / 2.83 - MediaQuery.of(context).size.height / 2.83 -
30, 30,
@ -103,7 +102,6 @@ class _BuildEmojiPickerViewState extends State<BuildEmojiPickerView> {
child: Material( child: Material(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
child: SizedBox( child: SizedBox(
//TODO @gaganyadav80: FIXIT: Gets too large when fullscreen.
height: MediaQuery.of(context).size.height / 2.83 + 20, height: MediaQuery.of(context).size.height / 2.83 + 20,
width: MediaQuery.of(context).size.width / 3.92, width: MediaQuery.of(context).size.width / 3.92,
child: ClipRRect( child: ClipRRect(

View File

@ -167,7 +167,7 @@ class BubbleActionWrapper extends ActionCell {
BubbleActionWrapper(this.inner); BubbleActionWrapper(this.inner);
@override @override
Widget? icon(Color iconColor) => FlowyText.regular(inner.emoji); Widget? leftIcon(Color iconColor) => FlowyText.regular(inner.emoji);
@override @override
String get name => inner.name; String get name => inner.name;

View File

@ -8,21 +8,25 @@ import 'package:styled_widget/styled_widget.dart';
class PopoverActionList<T extends PopoverAction> extends StatefulWidget { class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
final List<T> actions; final List<T> actions;
final PopoverMutex? mutex;
final Function(T, PopoverController) onSelected; final Function(T, PopoverController) onSelected;
final BoxConstraints constraints; final BoxConstraints constraints;
final PopoverDirection direction; final PopoverDirection direction;
final Widget Function(PopoverController) buildChild; final Widget Function(PopoverController) buildChild;
final VoidCallback? onClosed; final VoidCallback? onClosed;
final bool asBarrier;
const PopoverActionList({ const PopoverActionList({
required this.actions, required this.actions,
required this.buildChild, required this.buildChild,
required this.onSelected, required this.onSelected,
this.mutex,
this.onClosed, this.onClosed,
this.direction = PopoverDirection.rightWithTopAligned, this.direction = PopoverDirection.rightWithTopAligned,
this.asBarrier = false,
this.constraints = const BoxConstraints( this.constraints = const BoxConstraints(
minWidth: 120, minWidth: 120,
maxWidth: 360, maxWidth: 460,
maxHeight: 300, maxHeight: 300,
), ),
Key? key, Key? key,
@ -47,9 +51,11 @@ class _PopoverActionListState<T extends PopoverAction>
final child = widget.buildChild(popoverController); final child = widget.buildChild(popoverController);
return AppFlowyPopover( return AppFlowyPopover(
asBarrier: widget.asBarrier,
controller: popoverController, controller: popoverController,
constraints: widget.constraints, constraints: widget.constraints,
direction: widget.direction, direction: widget.direction,
mutex: widget.mutex,
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
onClose: widget.onClosed, onClose: widget.onClosed,
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
@ -82,7 +88,8 @@ class _PopoverActionListState<T extends PopoverAction>
} }
abstract class ActionCell extends PopoverAction { abstract class ActionCell extends PopoverAction {
Widget? icon(Color iconColor); Widget? leftIcon(Color iconColor) => null;
Widget? rightIcon(Color iconColor) => null;
String get name; String get name;
} }
@ -113,7 +120,11 @@ class ActionCellWidget<T extends PopoverAction> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final actionCell = action as ActionCell; final actionCell = action as ActionCell;
final icon = actionCell.icon(Theme.of(context).colorScheme.onSurface); final leftIcon =
actionCell.leftIcon(Theme.of(context).colorScheme.onSurface);
final rightIcon =
actionCell.rightIcon(Theme.of(context).colorScheme.onSurface);
return FlowyHover( return FlowyHover(
child: GestureDetector( child: GestureDetector(
@ -123,13 +134,20 @@ class ActionCellWidget<T extends PopoverAction> extends StatelessWidget {
height: itemHeight, height: itemHeight,
child: Row( child: Row(
children: [ children: [
if (icon != null) ...[icon, HSpace(ActionListSizes.itemHPadding)], if (leftIcon != null) ...[
leftIcon,
HSpace(ActionListSizes.itemHPadding)
],
Expanded( Expanded(
child: FlowyText.medium( child: FlowyText.medium(
actionCell.name, actionCell.name,
overflow: TextOverflow.visible, overflow: TextOverflow.visible,
), ),
), ),
if (rightIcon != null) ...[
HSpace(ActionListSizes.itemHPadding),
rightIcon,
],
], ],
), ),
).padding( ).padding(

View File

@ -16,6 +16,8 @@ class FlowyButton extends StatelessWidget {
final Color? hoverColor; final Color? hoverColor;
final bool isSelected; final bool isSelected;
final BorderRadius radius; final BorderRadius radius;
final BoxDecoration? decoration;
final bool useIntrinsicWidth;
const FlowyButton({ const FlowyButton({
Key? key, Key? key,
@ -28,6 +30,8 @@ class FlowyButton extends StatelessWidget {
this.hoverColor, this.hoverColor,
this.isSelected = false, this.isSelected = false,
this.radius = const BorderRadius.all(Radius.circular(6)), this.radius = const BorderRadius.all(Radius.circular(6)),
this.decoration,
this.useIntrinsicWidth = false,
}) : super(key: key); }) : super(key: key);
@override @override
@ -59,16 +63,24 @@ class FlowyButton extends StatelessWidget {
children.add(Expanded(child: text)); children.add(Expanded(child: text));
if (rightIcon != null) { if (rightIcon != null) {
children.add( children.add(rightIcon!);
SizedBox.fromSize(size: const Size.square(16), child: rightIcon!));
} }
return Padding( Widget child = Row(
padding: margin, mainAxisAlignment: MainAxisAlignment.center,
child: Row( crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: children,
crossAxisAlignment: CrossAxisAlignment.center, );
children: children,
if (useIntrinsicWidth) {
child = IntrinsicWidth(child: child);
}
return Container(
decoration: decoration,
child: Padding(
padding: margin,
child: child,
), ),
); );
} }
@ -89,6 +101,7 @@ class FlowyTextButton extends StatelessWidget {
final BorderRadius? radius; final BorderRadius? radius;
final MainAxisAlignment mainAxisAlignment; final MainAxisAlignment mainAxisAlignment;
final String? tooltip; final String? tooltip;
final BoxConstraints constraints;
// final HoverDisplayConfig? hoverDisplay; // final HoverDisplayConfig? hoverDisplay;
const FlowyTextButton( const FlowyTextButton(
@ -106,6 +119,7 @@ class FlowyTextButton extends StatelessWidget {
this.radius, this.radius,
this.mainAxisAlignment = MainAxisAlignment.start, this.mainAxisAlignment = MainAxisAlignment.start,
this.tooltip, this.tooltip,
this.constraints = const BoxConstraints(minWidth: 58.0, minHeight: 30.0),
}) : super(key: key); }) : super(key: key);
@override @override
@ -146,6 +160,7 @@ class FlowyTextButton extends StatelessWidget {
splashColor: Colors.transparent, splashColor: Colors.transparent,
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
elevation: 0, elevation: 0,
constraints: constraints,
onPressed: onPressed, onPressed: onPressed,
child: child, child: child,
); );
@ -161,18 +176,3 @@ class FlowyTextButton extends StatelessWidget {
return child; return child;
} }
} }
// return TextButton(
// style: ButtonStyle(
// textStyle: MaterialStateProperty.all(TextStyle(fontSize: fontSize)),
// alignment: Alignment.centerLeft,
// foregroundColor: MaterialStateProperty.all(Colors.black),
// padding: MaterialStateProperty.all<EdgeInsets>(
// const EdgeInsets.symmetric(horizontal: 2)),
// ),
// onPressed: onPressed,
// child: Text(
// text,
// overflow: TextOverflow.ellipsis,
// softWrap: false,
// ),
// );

View File

@ -118,7 +118,7 @@ class _RoundedInputFieldState extends State<RoundedInputField> {
contentPadding: widget.contentPadding, contentPadding: widget.contentPadding,
hintText: widget.hintText, hintText: widget.hintText,
hintStyle: hintStyle:
Theme.of(context).textTheme.bodyMedium!.textColor(borderColor), Theme.of(context).textTheme.bodySmall!.textColor(borderColor),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: borderColor, color: borderColor,

View File

@ -1209,7 +1209,7 @@ packages:
name: textfield_tags name: textfield_tags
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0+1" version: "2.0.2"
textstyle_extensions: textstyle_extensions:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -70,7 +70,7 @@ dependencies:
connectivity_plus: ^2.3.6+1 connectivity_plus: ^2.3.6+1
connectivity_plus_platform_interface: ^1.2.2 connectivity_plus_platform_interface: ^1.2.2
easy_localization: ^3.0.0 easy_localization: ^3.0.0
textfield_tags: ^2.0.0 textfield_tags: ^2.0.2
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2

View File

@ -25,16 +25,16 @@ void main() {
boardBloc = BoardBloc(view: context.gridView) boardBloc = BoardBloc(view: context.gridView)
..add(const BoardEvent.initial()); ..add(const BoardEvent.initial());
final fieldContext = context.singleSelectFieldContext(); final fieldInfo = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader( final loader = FieldTypeOptionLoader(
gridId: context.gridView.id, gridId: context.gridView.id,
field: fieldContext.field, field: fieldInfo.field,
); );
editorBloc = FieldEditorBloc( editorBloc = FieldEditorBloc(
gridId: context.gridView.id, gridId: context.gridView.id,
fieldName: fieldContext.name, fieldName: fieldInfo.name,
isGroupField: fieldContext.isGroupField, isGroupField: fieldInfo.isGroupField,
loader: loader, loader: loader,
)..add(const FieldEditorEvent.initial()); )..add(const FieldEditorEvent.initial());

View File

@ -14,9 +14,9 @@ void main() {
setUpAll(() async { setUpAll(() async {
boardTest = await AppFlowyBoardTest.ensureInitialized(); boardTest = await AppFlowyBoardTest.ensureInitialized();
context = await boardTest.createTestBoard(); context = await boardTest.createTestBoard();
final fieldContext = context.singleSelectFieldContext(); final fieldInfo = context.singleSelectFieldContext();
editorBloc = context.createFieldEditor( editorBloc = context.createFieldEditor(
fieldContext: fieldContext, fieldInfo: fieldInfo,
)..add(const FieldEditorEvent.initial()); )..add(const FieldEditorEvent.initial());
await boardResponseFuture(); await boardResponseFuture();

View File

@ -78,26 +78,26 @@ class BoardTestContext {
return _boardDataController.blocks; return _boardDataController.blocks;
} }
List<GridFieldContext> get fieldContexts => fieldController.fieldContexts; List<FieldInfo> get fieldContexts => fieldController.fieldInfos;
GridFieldController get fieldController { GridFieldController get fieldController {
return _boardDataController.fieldController; return _boardDataController.fieldController;
} }
FieldEditorBloc createFieldEditor({ FieldEditorBloc createFieldEditor({
GridFieldContext? fieldContext, FieldInfo? fieldInfo,
}) { }) {
IFieldTypeOptionLoader loader; IFieldTypeOptionLoader loader;
if (fieldContext == null) { if (fieldInfo == null) {
loader = NewFieldTypeOptionLoader(gridId: gridView.id); loader = NewFieldTypeOptionLoader(gridId: gridView.id);
} else { } else {
loader = loader =
FieldTypeOptionLoader(gridId: gridView.id, field: fieldContext.field); FieldTypeOptionLoader(gridId: gridView.id, field: fieldInfo.field);
} }
final editorBloc = FieldEditorBloc( final editorBloc = FieldEditorBloc(
fieldName: fieldContext?.name ?? '', fieldName: fieldInfo?.name ?? '',
isGroupField: fieldContext?.isGroupField ?? false, isGroupField: fieldInfo?.isGroupField ?? false,
loader: loader, loader: loader,
gridId: gridView.id, gridId: gridView.id,
); );
@ -146,10 +146,10 @@ class BoardTestContext {
return Future(() => editorBloc); return Future(() => editorBloc);
} }
GridFieldContext singleSelectFieldContext() { FieldInfo singleSelectFieldContext() {
final fieldContext = fieldContexts final fieldInfo = fieldContexts
.firstWhere((element) => element.fieldType == FieldType.SingleSelect); .firstWhere((element) => element.fieldType == FieldType.SingleSelect);
return fieldContext; return fieldInfo;
} }
GridFieldCellContext singleSelectFieldCellContext() { GridFieldCellContext singleSelectFieldCellContext() {
@ -157,15 +157,15 @@ class BoardTestContext {
return GridFieldCellContext(gridId: gridView.id, field: field); return GridFieldCellContext(gridId: gridView.id, field: field);
} }
GridFieldContext textFieldContext() { FieldInfo textFieldContext() {
final fieldContext = fieldContexts final fieldInfo = fieldContexts
.firstWhere((element) => element.fieldType == FieldType.RichText); .firstWhere((element) => element.fieldType == FieldType.RichText);
return fieldContext; return fieldInfo;
} }
GridFieldContext checkboxFieldContext() { FieldInfo checkboxFieldContext() {
final fieldContext = fieldContexts final fieldInfo = fieldContexts
.firstWhere((element) => element.fieldType == FieldType.Checkbox); .firstWhere((element) => element.fieldType == FieldType.Checkbox);
return fieldContext; return fieldInfo;
} }
} }

View File

@ -6,7 +6,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'util.dart'; import '../util.dart';
void main() { void main() {
late AppFlowyGridCellTest cellTest; late AppFlowyGridCellTest cellTest;
@ -19,9 +19,8 @@ void main() {
setUp(() async { setUp(() async {
await cellTest.createTestGrid(); await cellTest.createTestGrid();
await cellTest.createTestRow(); await cellTest.createTestRow();
cellController = await cellTest.makeCellController( cellController =
FieldType.SingleSelect, await cellTest.makeCellController(FieldType.SingleSelect, 0);
);
}); });
blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>( blocTest<SelectOptionCellEditorBloc, SelectOptionEditorState>(

View File

@ -3,20 +3,20 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'util.dart'; import '../util.dart';
Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async { Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
final context = await gridTest.createTestGrid(); final context = await gridTest.createTestGrid();
final fieldContext = context.singleSelectFieldContext(); final fieldInfo = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader( final loader = FieldTypeOptionLoader(
gridId: context.gridView.id, gridId: context.gridView.id,
field: fieldContext.field, field: fieldInfo.field,
); );
return FieldEditorBloc( return FieldEditorBloc(
gridId: context.gridView.id, gridId: context.gridView.id,
fieldName: fieldContext.name, fieldName: fieldInfo.name,
isGroupField: fieldContext.isGroupField, isGroupField: fieldInfo.isGroupField,
loader: loader, loader: loader,
)..add(const FieldEditorEvent.initial()); )..add(const FieldEditorEvent.initial());
} }
@ -33,16 +33,16 @@ void main() {
setUp(() async { setUp(() async {
final context = await gridTest.createTestGrid(); final context = await gridTest.createTestGrid();
final fieldContext = context.singleSelectFieldContext(); final fieldInfo = context.singleSelectFieldContext();
final loader = FieldTypeOptionLoader( final loader = FieldTypeOptionLoader(
gridId: context.gridView.id, gridId: context.gridView.id,
field: fieldContext.field, field: fieldInfo.field,
); );
editorBloc = FieldEditorBloc( editorBloc = FieldEditorBloc(
gridId: context.gridView.id, gridId: context.gridView.id,
fieldName: fieldContext.name, fieldName: fieldInfo.name,
isGroupField: fieldContext.isGroupField, isGroupField: fieldInfo.isGroupField,
loader: loader, loader: loader,
)..add(const FieldEditorEvent.initial()); )..add(const FieldEditorEvent.initial());

View File

@ -0,0 +1,148 @@
import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
test('create a text filter)', () async {
final context = await gridTest.createTestGrid();
final service = FilterFFIService(viewId: context.gridView.id);
final textField = context.textFieldContext();
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterCondition.TextIsEmpty,
content: "");
await gridResponseFuture();
assert(context.fieldController.filterInfos.length == 1);
});
test('delete a text filter)', () async {
final context = await gridTest.createTestGrid();
final service = FilterFFIService(viewId: context.gridView.id);
final textField = context.textFieldContext();
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterCondition.TextIsEmpty,
content: "");
await gridResponseFuture();
final filterInfo = context.fieldController.filterInfos.first;
await service.deleteFilter(
fieldId: textField.id,
filterId: filterInfo.filter.id,
fieldType: textField.fieldType,
);
await gridResponseFuture();
assert(context.fieldController.filterInfos.isEmpty);
});
test('filter rows with condition: text is empty', () async {
final context = await gridTest.createTestGrid();
final service = FilterFFIService(viewId: context.gridView.id);
final gridController = GridController(view: context.gridView);
final gridBloc = GridBloc(
view: context.gridView,
gridController: gridController,
)..add(const GridEvent.initial());
await gridResponseFuture();
final textField = context.textFieldContext();
service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterCondition.TextIsEmpty,
content: "");
await gridResponseFuture();
assert(gridBloc.state.rowInfos.length == 3);
});
test('filter rows with condition: text is empty(After edit the row)',
() async {
final context = await gridTest.createTestGrid();
final service = FilterFFIService(viewId: context.gridView.id);
final gridController = GridController(view: context.gridView);
final gridBloc = GridBloc(
view: context.gridView,
gridController: gridController,
)..add(const GridEvent.initial());
await gridResponseFuture();
final textField = context.textFieldContext();
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterCondition.TextIsEmpty,
content: "");
await gridResponseFuture();
final controller = await context.makeTextCellController(0);
controller.saveCellData("edit text cell content");
await gridResponseFuture();
assert(gridBloc.state.rowInfos.length == 2);
controller.saveCellData("");
await gridResponseFuture();
assert(gridBloc.state.rowInfos.length == 3);
});
test('filter rows with condition: text is not empty', () async {
final context = await gridTest.createTestGrid();
final service = FilterFFIService(viewId: context.gridView.id);
final textField = context.textFieldContext();
await gridResponseFuture();
await service.insertTextFilter(
fieldId: textField.id,
condition: TextFilterCondition.TextIsNotEmpty,
content: "");
await gridResponseFuture();
assert(context.rowInfos.isEmpty);
});
test('filter rows with condition: checkbox uncheck', () async {
final context = await gridTest.createTestGrid();
final checkboxField = context.checkboxFieldContext();
final service = FilterFFIService(viewId: context.gridView.id);
final gridController = GridController(view: context.gridView);
final gridBloc = GridBloc(
view: context.gridView,
gridController: gridController,
)..add(const GridEvent.initial());
await gridResponseFuture();
await service.insertCheckboxFilter(
fieldId: checkboxField.id,
condition: CheckboxFilterCondition.IsUnChecked,
);
await gridResponseFuture();
assert(gridBloc.state.rowInfos.length == 3);
});
test('filter rows with condition: checkbox check', () async {
final context = await gridTest.createTestGrid();
final checkboxField = context.checkboxFieldContext();
final service = FilterFFIService(viewId: context.gridView.id);
final gridController = GridController(view: context.gridView);
final gridBloc = GridBloc(
view: context.gridView,
gridController: gridController,
)..add(const GridEvent.initial());
await gridResponseFuture();
await service.insertCheckboxFilter(
fieldId: checkboxField.id,
condition: CheckboxFilterCondition.IsChecked,
);
await gridResponseFuture();
assert(gridBloc.state.rowInfos.isEmpty);
});
}

Some files were not shown because too many files have changed in this diff Show More