mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge remote-tracking branch 'origin/main' into customize_font_size
This commit is contained in:
commit
14ac2db06d
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -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: |
|
||||||
|
4
.github/workflows/dart_lint.yml
vendored
4
.github/workflows/dart_lint.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/dart_test.yml
vendored
2
.github/workflows/dart_test.yml
vendored
@ -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
|
||||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -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
|
||||||
|
4
.github/workflows/rust_lint.yml
vendored
4
.github/workflows/rust_lint.yml
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
2
frontend/.vscode/tasks.json
vendored
2
frontend/.vscode/tasks.json
vendored
@ -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}"
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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>);
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
|
@ -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}) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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: [],
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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),
|
||||||
|
@ -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()!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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 => [
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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];
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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(
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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(
|
||||||
|
@ -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();
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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!;
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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",
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
@ -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,
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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());
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>(
|
@ -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());
|
||||||
|
|
@ -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
Loading…
Reference in New Issue
Block a user