Merge branch 'main' into feat/flowy-overlay

This commit is contained in:
Vincent Chan 2022-09-05 12:06:28 +08:00
commit bd40768c6a
131 changed files with 2185 additions and 1380 deletions

View File

@ -7,9 +7,16 @@ name: Flutter lint
on: on:
push: push:
branches: [main] branches:
- "main"
paths:
- "frontend/app_flowy/**"
pull_request: pull_request:
branches: [main] branches:
- "main"
paths:
- "frontend/app_flowy/**"
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always

View File

@ -4,11 +4,14 @@ on:
push: push:
branches: branches:
- "main" - "main"
paths:
- "frontend/app_flowy/**"
pull_request: pull_request:
branches: branches:
- "main" - "main"
- "feat/flowy_editor" paths:
- "frontend/app_flowy/**"
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always

View File

@ -5,13 +5,13 @@ on:
branches: branches:
- "main" - "main"
paths: paths:
- "frontend/app_flowy/packages/appflowy_editor" - "frontend/app_flowy/packages/appflowy_editor/**"
pull_request: pull_request:
branches: branches:
- "main" - "main"
paths: paths:
- "frontend/app_flowy/packages/appflowy_editor" - "frontend/app_flowy/packages/appflowy_editor/**"
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@ -37,4 +37,12 @@ jobs:
working-directory: frontend/app_flowy/packages/appflowy_editor working-directory: frontend/app_flowy/packages/appflowy_editor
run: | run: |
flutter pub get flutter pub get
flutter test flutter test --coverage
- uses: codecov/codecov-action@v3
with:
name: appflowy_editor
env_vars: ${{ matrix.os }}
fail_ci_if_error: true
verbose: true

View File

@ -2,10 +2,18 @@ name: Rust lint
on: on:
push: push:
branches: [ main ] branches:
pull_request: - "main"
branches: [ main ] paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
pull_request:
branches:
- "main"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always

View File

@ -2,11 +2,18 @@ name: Unit test(Rust)
on: on:
push: push:
branches: branches:
- 'main' - "main"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
pull_request: pull_request:
branches: branches:
- 'main' - "main"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always

View File

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2H13C13.5523 2 14 2.44772 14 3V6" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 2H3C2.44772 2 2 2.44772 2 3V6" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 14H3C2.44772 14 2 13.5523 2 13V10" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 14H13C13.5523 14 14 13.5523 14 13V10" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="6" y="6" width="4" height="4" rx="1" stroke="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@ -160,7 +160,8 @@
"settings": { "settings": {
"filter": "Filter", "filter": "Filter",
"sortBy": "Sort by", "sortBy": "Sort by",
"Properties": "Properties" "Properties": "Properties",
"group": "Group"
}, },
"field": { "field": {
"hide": "Hide", "hide": "Hide",

View File

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:app_flowy/plugins/grid/application/row/row_service.dart'; import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_board/appflowy_board.dart';
@ -25,7 +25,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
final MoveRowFFIService _rowService; final MoveRowFFIService _rowService;
LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap(); LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap();
GridFieldCache get fieldCache => _gridDataController.fieldCache; GridFieldController get fieldController =>
_gridDataController.fieldController;
String get gridId => _gridDataController.gridId; String get gridId => _gridDataController.gridId;
BoardBloc({required ViewPB view}) BoardBloc({required ViewPB view})
@ -110,9 +111,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
emit(state.copyWith(noneOrError: some(error))); emit(state.copyWith(noneOrError: some(error)));
}, },
didReceiveGroups: (List<GroupPB> groups) { didReceiveGroups: (List<GroupPB> groups) {
emit(state.copyWith( emit(
groupIds: groups.map((group) => group.groupId).toList(), state.copyWith(
)); groupIds: groups.map((group) => group.groupId).toList(),
),
);
}, },
); );
}, },
@ -154,6 +157,23 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
} }
void initializeGroups(List<GroupPB> groups) { void initializeGroups(List<GroupPB> groups) {
for (var controller in groupControllers.values) {
controller.dispose();
}
groupControllers.clear();
boardController.clear();
//
List<AFBoardColumnData> columns = groups.map((group) {
return AFBoardColumnData(
id: group.groupId,
name: group.desc,
items: _buildRows(group),
customData: group,
);
}).toList();
boardController.addColumns(columns);
for (final group in groups) { for (final group in groups) {
final delegate = GroupControllerDelegateImpl( final delegate = GroupControllerDelegateImpl(
controller: boardController, controller: boardController,
@ -184,38 +204,35 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
} }
}, },
didLoadGroups: (groups) { didLoadGroups: (groups) {
List<AFBoardColumnData> columns = groups.map((group) { if (isClosed) return;
return AFBoardColumnData(
id: group.groupId,
name: group.desc,
items: _buildRows(group),
customData: group,
);
}).toList();
boardController.addColumns(columns);
initializeGroups(groups); initializeGroups(groups);
add(BoardEvent.didReceiveGroups(groups)); add(BoardEvent.didReceiveGroups(groups));
}, },
onDeletedGroup: (groupIds) { onDeletedGroup: (groupIds) {
if (isClosed) return;
// //
}, },
onInsertedGroup: (insertedGroups) { onInsertedGroup: (insertedGroups) {
if (isClosed) return;
// //
}, },
onUpdatedGroup: (updatedGroups) { onUpdatedGroup: (updatedGroups) {
// if (isClosed) return;
for (final group in updatedGroups) { for (final group in updatedGroups) {
final columnController = final columnController =
boardController.getColumnController(group.groupId); boardController.getColumnController(group.groupId);
if (columnController != null) { columnController?.updateColumnName(group.desc);
columnController.updateColumnName(group.desc);
}
} }
}, },
onError: (err) { onError: (err) {
Log.error(err); Log.error(err);
}, },
onResetGroups: (groups) {
if (isClosed) return;
initializeGroups(groups);
add(BoardEvent.didReceiveGroups(groups));
},
); );
} }

View File

@ -1,7 +1,7 @@
import 'dart:collection'; import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.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/row/row_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
@ -12,12 +12,13 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'board_listener.dart'; import 'board_listener.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>); typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>);
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>);
typedef OnDeletedGroup = void Function(List<String>); typedef OnDeletedGroup = void Function(List<String>);
typedef OnInsertedGroup = void Function(List<InsertedGroupPB>); typedef OnInsertedGroup = void Function(List<InsertedGroupPB>);
typedef OnResetGroups = void Function(List<GroupPB>);
typedef OnRowsChanged = void Function( typedef OnRowsChanged = void Function(
List<RowInfo>, List<RowInfo>,
@ -28,7 +29,7 @@ typedef OnError = void Function(FlowyError);
class BoardDataController { class BoardDataController {
final String gridId; final String gridId;
final GridFFIService _gridFFIService; final GridFFIService _gridFFIService;
final GridFieldCache fieldCache; final GridFieldController fieldController;
final BoardListener _listener; final BoardListener _listener;
// key: the block id // key: the block id
@ -55,7 +56,7 @@ class BoardDataController {
// ignore: prefer_collection_literals // ignore: prefer_collection_literals
_blocks = LinkedHashMap(), _blocks = LinkedHashMap(),
_gridFFIService = GridFFIService(gridId: view.id), _gridFFIService = GridFFIService(gridId: view.id),
fieldCache = GridFieldCache(gridId: view.id); fieldController = GridFieldController(gridId: view.id);
void addListener({ void addListener({
required OnGridChanged onGridChanged, required OnGridChanged onGridChanged,
@ -65,6 +66,7 @@ class BoardDataController {
required OnUpdatedGroup onUpdatedGroup, required OnUpdatedGroup onUpdatedGroup,
required OnDeletedGroup onDeletedGroup, required OnDeletedGroup onDeletedGroup,
required OnInsertedGroup onInsertedGroup, required OnInsertedGroup onInsertedGroup,
required OnResetGroups onResetGroups,
required OnError? onError, required OnError? onError,
}) { }) {
_onGridChanged = onGridChanged; _onGridChanged = onGridChanged;
@ -73,28 +75,36 @@ class BoardDataController {
_onRowsChanged = onRowsChanged; _onRowsChanged = onRowsChanged;
_onError = onError; _onError = onError;
fieldCache.addListener(onFields: (fields) { fieldController.addListener(onFields: (fields) {
_onFieldsChanged?.call(UnmodifiableListView(fields)); _onFieldsChanged?.call(UnmodifiableListView(fields));
}); });
_listener.start(onBoardChanged: (result) { _listener.start(
result.fold( onBoardChanged: (result) {
(changeset) { result.fold(
if (changeset.updateGroups.isNotEmpty) { (changeset) {
onUpdatedGroup.call(changeset.updateGroups); if (changeset.updateGroups.isNotEmpty) {
} onUpdatedGroup.call(changeset.updateGroups);
}
if (changeset.insertedGroups.isNotEmpty) { if (changeset.insertedGroups.isNotEmpty) {
onInsertedGroup.call(changeset.insertedGroups); onInsertedGroup.call(changeset.insertedGroups);
} }
if (changeset.deletedGroups.isNotEmpty) { if (changeset.deletedGroups.isNotEmpty) {
onDeletedGroup.call(changeset.deletedGroups); onDeletedGroup.call(changeset.deletedGroups);
} }
}, },
(e) => _onError?.call(e), (e) => _onError?.call(e),
); );
}); },
onGroupByNewField: (result) {
result.fold(
(groups) => onResetGroups(groups),
(e) => _onError?.call(e),
);
},
);
} }
Future<Either<Unit, FlowyError>> loadData() async { Future<Either<Unit, FlowyError>> loadData() async {
@ -103,16 +113,15 @@ class BoardDataController {
() => result.fold( () => result.fold(
(grid) async { (grid) async {
_onGridChanged?.call(grid); _onGridChanged?.call(grid);
return await fieldController.loadFields(fieldIds: grid.fields).then(
return await _loadFields(grid).then((result) { (result) => result.fold(
return result.fold( (l) {
(l) { _loadGroups(grid.blocks);
_loadGroups(grid.blocks); return left(l);
return left(l); },
}, (err) => right(err),
(err) => right(err), ),
); );
});
}, },
(err) => right(err), (err) => right(err),
), ),
@ -126,33 +135,19 @@ class BoardDataController {
Future<void> dispose() async { Future<void> dispose() async {
await _gridFFIService.closeGrid(); await _gridFFIService.closeGrid();
await fieldCache.dispose(); await fieldController.dispose();
for (final blockCache in _blocks.values) { for (final blockCache in _blocks.values) {
blockCache.dispose(); blockCache.dispose();
} }
} }
Future<Either<Unit, FlowyError>> _loadFields(GridPB grid) async {
final result = await _gridFFIService.getFields(fieldIds: grid.fields);
return Future(
() => result.fold(
(fields) {
fieldCache.fields = fields.items;
_onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields));
return left(unit);
},
(err) => right(err),
),
);
}
Future<void> _loadGroups(List<BlockPB> blocks) async { Future<void> _loadGroups(List<BlockPB> blocks) async {
for (final block in blocks) { for (final block in blocks) {
final cache = GridBlockCache( final cache = GridBlockCache(
gridId: gridId, gridId: gridId,
block: block, block: block,
fieldCache: fieldCache, fieldController: fieldController,
); );
cache.addListener(onRowsChanged: (reason) { cache.addListener(onRowsChanged: (reason) {

View File

@ -5,20 +5,26 @@ import 'package:flowy_infra/notifier.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/dart_notification.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
typedef UpdateBoardNotifiedValue = Either<GroupViewChangesetPB, FlowyError>; typedef GroupUpdateValue = Either<GroupViewChangesetPB, FlowyError>;
typedef GroupByNewFieldValue = Either<List<GroupPB>, FlowyError>;
class BoardListener { class BoardListener {
final String viewId; final String viewId;
PublishNotifier<UpdateBoardNotifiedValue>? _groupNotifier = PublishNotifier(); PublishNotifier<GroupUpdateValue>? _groupUpdateNotifier = PublishNotifier();
PublishNotifier<GroupByNewFieldValue>? _groupByNewFieldNotifier =
PublishNotifier();
GridNotificationListener? _listener; GridNotificationListener? _listener;
BoardListener(this.viewId); BoardListener(this.viewId);
void start({ void start({
required void Function(UpdateBoardNotifiedValue) onBoardChanged, required void Function(GroupUpdateValue) onBoardChanged,
required void Function(GroupByNewFieldValue) onGroupByNewField,
}) { }) {
_groupNotifier?.addPublishListener(onBoardChanged); _groupUpdateNotifier?.addPublishListener(onBoardChanged);
_groupByNewFieldNotifier?.addPublishListener(onGroupByNewField);
_listener = GridNotificationListener( _listener = GridNotificationListener(
objectId: viewId, objectId: viewId,
handler: _handler, handler: _handler,
@ -32,9 +38,16 @@ class BoardListener {
switch (ty) { switch (ty) {
case GridNotification.DidUpdateGroupView: case GridNotification.DidUpdateGroupView:
result.fold( result.fold(
(payload) => _groupNotifier?.value = (payload) => _groupUpdateNotifier?.value =
left(GroupViewChangesetPB.fromBuffer(payload)), left(GroupViewChangesetPB.fromBuffer(payload)),
(error) => _groupNotifier?.value = right(error), (error) => _groupUpdateNotifier?.value = right(error),
);
break;
case GridNotification.DidGroupByNewField:
result.fold(
(payload) => _groupByNewFieldNotifier?.value =
left(GroupViewChangesetPB.fromBuffer(payload).newGroups),
(error) => _groupByNewFieldNotifier?.value = right(error),
); );
break; break;
default: default:
@ -44,7 +57,10 @@ class BoardListener {
Future<void> stop() async { Future<void> stop() async {
await _listener?.stop(); await _listener?.stop();
_groupNotifier?.dispose(); _groupUpdateNotifier?.dispose();
_groupNotifier = null; _groupUpdateNotifier = null;
_groupByNewFieldNotifier?.dispose();
_groupByNewFieldNotifier = null;
} }
} }

View File

@ -1,6 +1,6 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
@ -20,8 +20,6 @@ class BoardDateCellBloc extends Bloc<BoardDateCellEvent, BoardDateCellState> {
emit(state.copyWith( emit(state.copyWith(
data: cellData, dateStr: _dateStrFromCellData(cellData))); data: cellData, dateStr: _dateStrFromCellData(cellData)));
}, },
didReceiveFieldUpdate: (FieldPB value) =>
emit(state.copyWith(field: value)),
); );
}, },
); );
@ -53,8 +51,6 @@ class BoardDateCellEvent with _$BoardDateCellEvent {
const factory BoardDateCellEvent.initial() = _InitialCell; const factory BoardDateCellEvent.initial() = _InitialCell;
const factory BoardDateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = const factory BoardDateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
_DidReceiveCellUpdate; _DidReceiveCellUpdate;
const factory BoardDateCellEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
} }
@freezed @freezed
@ -62,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 FieldPB field, required GridFieldContext fieldContext,
}) = _BoardDateCellState; }) = _BoardDateCellState;
factory BoardDateCellState.initial(GridDateCellController context) { factory BoardDateCellState.initial(GridDateCellController context) {
final cellData = context.getCellData(); final cellData = context.getCellData();
return BoardDateCellState( return BoardDateCellState(
field: context.field, fieldContext: context.fieldContext,
data: cellData, data: cellData,
dateStr: _dateStrFromCellData(cellData), dateStr: _dateStrFromCellData(cellData),
); );

View File

@ -59,7 +59,7 @@ 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.field).toList(), state.cells.map((cell) => cell.identifier.fieldContext).toList(),
), ),
rowPB: state.rowPB, rowPB: state.rowPB,
); );
@ -120,9 +120,9 @@ class BoardCellEquatable extends Equatable {
@override @override
List<Object?> get props => [ List<Object?> get props => [
identifier.field.id, identifier.fieldContext.id,
identifier.field.fieldType, identifier.fieldContext.fieldType,
identifier.field.visibility, identifier.fieldContext.visibility,
identifier.field.width, identifier.fieldContext.width,
]; ];
} }

View File

@ -1,7 +1,7 @@
import 'package:app_flowy/plugins/board/presentation/card/card_cell_builder.dart'; import 'package:app_flowy/plugins/board/presentation/card/card_cell_builder.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.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:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -10,15 +10,15 @@ typedef OnCardChanged = void Function(GridCellMap, RowsChangedReason);
class CardDataController extends BoardCellBuilderDelegate { class CardDataController extends BoardCellBuilderDelegate {
final RowPB rowPB; final RowPB rowPB;
final GridFieldCache _fieldCache; final GridFieldController _fieldController;
final GridRowCache _rowCache; final GridRowCache _rowCache;
final List<VoidCallback> _onCardChangedListeners = []; final List<VoidCallback> _onCardChangedListeners = [];
CardDataController({ CardDataController({
required this.rowPB, required this.rowPB,
required GridFieldCache fieldCache, required GridFieldController fieldController,
required GridRowCache rowCache, required GridRowCache rowCache,
}) : _fieldCache = fieldCache, }) : _fieldController = fieldController,
_rowCache = rowCache; _rowCache = rowCache;
GridCellMap loadData() { GridCellMap loadData() {
@ -41,7 +41,7 @@ class CardDataController extends BoardCellBuilderDelegate {
@override @override
GridCellFieldNotifier buildFieldNotifier() { GridCellFieldNotifier buildFieldNotifier() {
return GridCellFieldNotifier( return GridCellFieldNotifier(
notifier: GridCellFieldNotifierImpl(_fieldCache)); notifier: GridCellFieldNotifierImpl(_fieldController));
} }
@override @override

View File

@ -43,4 +43,5 @@ class BoardSettingState with _$BoardSettingState {
enum BoardSettingAction { enum BoardSettingAction {
properties, properties,
groups,
} }

View File

@ -5,7 +5,7 @@ import 'dart:collection';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_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/plugins/grid/presentation/widgets/cell/cell_builder.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
@ -83,8 +83,7 @@ class _BoardContentState extends State<BoardContent> {
return BlocListener<BoardBloc, BoardState>( return BlocListener<BoardBloc, BoardState>(
listener: (context, state) => _handleEditState(state, context), listener: (context, state) => _handleEditState(state, context),
child: BlocBuilder<BoardBloc, BoardState>( child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (previous, current) => buildWhen: (previous, current) => previous.groupIds != current.groupIds,
previous.groupIds.length != current.groupIds.length,
builder: (context, state) { builder: (context, state) {
final theme = context.read<AppTheme>(); final theme = context.read<AppTheme>();
return Container( return Container(
@ -96,6 +95,7 @@ class _BoardContentState extends State<BoardContent> {
const _ToolbarBlocAdaptor(), const _ToolbarBlocAdaptor(),
Expanded( Expanded(
child: AFBoard( child: AFBoard(
key: UniqueKey(),
scrollManager: scrollManager, scrollManager: scrollManager,
scrollController: scrollController, scrollController: scrollController,
dataController: context.read<BoardBloc>().boardController, dataController: context.read<BoardBloc>().boardController,
@ -223,10 +223,10 @@ class _BoardContentState extends State<BoardContent> {
/// Return placeholder widget if the rowCache is null. /// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox(key: ObjectKey(columnItem)); if (rowCache == null) return SizedBox(key: ObjectKey(columnItem));
final fieldCache = context.read<BoardBloc>().fieldCache; final fieldController = context.read<BoardBloc>().fieldController;
final gridId = context.read<BoardBloc>().gridId; final gridId = context.read<BoardBloc>().gridId;
final cardController = CardDataController( final cardController = CardDataController(
fieldCache: fieldCache, fieldController: fieldController,
rowCache: rowCache, rowCache: rowCache,
rowPB: rowPB, rowPB: rowPB,
); );
@ -253,7 +253,7 @@ class _BoardContentState extends State<BoardContent> {
dataController: cardController, dataController: cardController,
openCard: (context) => _openCard( openCard: (context) => _openCard(
gridId, gridId,
fieldCache, fieldController,
rowPB, rowPB,
rowCache, rowCache,
context, context,
@ -272,17 +272,17 @@ class _BoardContentState extends State<BoardContent> {
); );
} }
void _openCard(String gridId, GridFieldCache fieldCache, RowPB rowPB, void _openCard(String gridId, GridFieldController fieldController,
GridRowCache rowCache, BuildContext context) { RowPB rowPB, GridRowCache rowCache, BuildContext context) {
final rowInfo = RowInfo( final rowInfo = RowInfo(
gridId: gridId, gridId: gridId,
fields: UnmodifiableListView(fieldCache.fields), fields: UnmodifiableListView(fieldController.fieldContexts),
rowPB: rowPB, rowPB: rowPB,
); );
final dataController = GridRowDataController( final dataController = GridRowDataController(
rowInfo: rowInfo, rowInfo: rowInfo,
fieldCache: fieldCache, fieldController: fieldController,
rowCache: rowCache, rowCache: rowCache,
); );
@ -308,7 +308,7 @@ class _ToolbarBlocAdaptor extends StatelessWidget {
final bloc = context.read<BoardBloc>(); final bloc = context.read<BoardBloc>();
final toolbarContext = BoardToolbarContext( final toolbarContext = BoardToolbarContext(
viewId: bloc.gridId, viewId: bloc.gridId,
fieldCache: bloc.fieldCache, fieldController: bloc.fieldController,
); );
return BoardToolbar(toolbarContext: toolbarContext); return BoardToolbar(toolbarContext: toolbarContext);

View File

@ -1,7 +1,8 @@
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/board/application/toolbar/board_setting_bloc.dart'; import 'package:app_flowy/plugins/board/application/toolbar/board_setting_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart'; import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_group.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_property.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_property.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
@ -18,16 +19,16 @@ import 'board_toolbar.dart';
class BoardSettingContext { class BoardSettingContext {
final String viewId; final String viewId;
final GridFieldCache fieldCache; final GridFieldController fieldController;
BoardSettingContext({ BoardSettingContext({
required this.viewId, required this.viewId,
required this.fieldCache, required this.fieldController,
}); });
factory BoardSettingContext.from(BoardToolbarContext toolbarContext) => factory BoardSettingContext.from(BoardToolbarContext toolbarContext) =>
BoardSettingContext( BoardSettingContext(
viewId: toolbarContext.viewId, viewId: toolbarContext.viewId,
fieldCache: toolbarContext.fieldCache, fieldController: toolbarContext.fieldController,
); );
} }
@ -125,6 +126,8 @@ extension _GridSettingExtension on BoardSettingAction {
switch (this) { switch (this) {
case BoardSettingAction.properties: case BoardSettingAction.properties:
return 'grid/setting/properties'; return 'grid/setting/properties';
case BoardSettingAction.groups:
return 'grid/setting/group';
} }
} }
@ -132,6 +135,8 @@ extension _GridSettingExtension on BoardSettingAction {
switch (this) { switch (this) {
case BoardSettingAction.properties: case BoardSettingAction.properties:
return LocaleKeys.grid_settings_Properties.tr(); return LocaleKeys.grid_settings_Properties.tr();
case BoardSettingAction.groups:
return LocaleKeys.grid_settings_group.tr();
} }
} }
} }
@ -158,7 +163,7 @@ class _BoardSettingListPopoverState extends State<BoardSettingListPopover> {
constraints: BoxConstraints.loose(const Size(260, 400)), constraints: BoxConstraints.loose(const Size(260, 400)),
child: GridPropertyList( child: GridPropertyList(
gridId: widget.settingContext.viewId, gridId: widget.settingContext.viewId,
fieldCache: widget.settingContext.fieldCache, fieldController: widget.settingContext.fieldController,
), ),
); );
} }
@ -169,6 +174,8 @@ class _BoardSettingListPopoverState extends State<BoardSettingListPopover> {
settingContext: widget.settingContext, settingContext: widget.settingContext,
onAction: (action, settingContext) { onAction: (action, settingContext) {
switch (action) { switch (action) {
case BoardSettingAction.groups:
break;
case BoardSettingAction.properties: case BoardSettingAction.properties:
setState(() { setState(() {
_showGridPropertyList = true; _showGridPropertyList = true;

View File

@ -1,4 +1,4 @@
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:appflowy_popover/popover.dart'; import 'package:appflowy_popover/popover.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
@ -10,11 +10,11 @@ import 'board_setting.dart';
class BoardToolbarContext { class BoardToolbarContext {
final String viewId; final String viewId;
final GridFieldCache fieldCache; final GridFieldController fieldController;
BoardToolbarContext({ BoardToolbarContext({
required this.viewId, required this.viewId,
required this.fieldCache, required this.fieldController,
}); });
} }

View File

@ -2,7 +2,7 @@ import 'dart:async';
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 '../field/field_cache.dart'; import '../field/field_controller.dart';
import '../row/row_cache.dart'; import '../row/row_cache.dart';
import 'block_listener.dart'; import 'block_listener.dart';
@ -19,12 +19,12 @@ class GridBlockCache {
GridBlockCache({ GridBlockCache({
required this.gridId, required this.gridId,
required this.block, required this.block,
required GridFieldCache fieldCache, required GridFieldController fieldController,
}) { }) {
_rowCache = GridRowCache( _rowCache = GridRowCache(
gridId: gridId, gridId: gridId,
block: block, block: block,
notifier: GridRowFieldNotifierImpl(fieldCache), notifier: GridRowFieldNotifierImpl(fieldController),
); );
_listener = GridBlockListener(blockId: block.id); _listener = GridBlockListener(blockId: block.id);

View File

@ -148,10 +148,10 @@ class IGridCellController<T, D> extends Equatable {
_cellDataLoader = cellDataLoader, _cellDataLoader = cellDataLoader,
_cellDataPersistence = cellDataPersistence, _cellDataPersistence = cellDataPersistence,
_fieldNotifier = fieldNotifier, _fieldNotifier = fieldNotifier,
_fieldService = _fieldService = FieldService(
FieldService(gridId: cellId.gridId, fieldId: cellId.field.id), gridId: cellId.gridId, fieldId: cellId.fieldContext.id),
_cacheKey = _cacheKey = GridCellCacheKey(
GridCellCacheKey(rowId: cellId.rowId, fieldId: cellId.field.id); rowId: cellId.rowId, fieldId: cellId.fieldContext.id);
IGridCellController<T, D> clone() { IGridCellController<T, D> clone() {
return IGridCellController( return IGridCellController(
@ -166,11 +166,11 @@ class IGridCellController<T, D> extends Equatable {
String get rowId => cellId.rowId; String get rowId => cellId.rowId;
String get fieldId => cellId.field.id; String get fieldId => cellId.fieldContext.id;
FieldPB get field => cellId.field; GridFieldContext get fieldContext => cellId.fieldContext;
FieldType get fieldType => cellId.field.fieldType; FieldType get fieldType => cellId.fieldContext.fieldType;
VoidCallback? startListening( VoidCallback? startListening(
{required void Function(T?) onCellChanged, {required void Function(T?) onCellChanged,
@ -182,7 +182,8 @@ class IGridCellController<T, D> extends Equatable {
isListening = true; isListening = true;
_cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey)); _cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey));
_cellListener = CellListener(rowId: cellId.rowId, fieldId: cellId.field.id); _cellListener =
CellListener(rowId: cellId.rowId, fieldId: cellId.fieldContext.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:
@ -308,14 +309,14 @@ class IGridCellController<T, D> extends Equatable {
@override @override
List<Object> get props => List<Object> get props =>
[_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.field.id]; [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldContext.id];
} }
class GridCellFieldNotifierImpl extends IGridCellFieldNotifier { class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
final GridFieldCache _cache; final GridFieldController _cache;
FieldChangesetCallback? _onChangesetFn; OnChangeset? _onChangesetFn;
GridCellFieldNotifierImpl(GridFieldCache cache) : _cache = cache; GridCellFieldNotifierImpl(GridFieldController cache) : _cache = cache;
@override @override
void onCellDispose() { void onCellDispose() {

View File

@ -16,12 +16,12 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_listener.dart';
import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'dart:convert' show utf8; import 'dart:convert' show utf8;
import '../../field/field_cache.dart'; import '../../field/field_controller.dart';
import '../../field/type_option/type_option_context.dart'; import '../../field/type_option/type_option_context.dart';
import 'cell_field_notifier.dart'; import 'cell_field_notifier.dart';
part 'cell_service.freezed.dart'; part 'cell_service.freezed.dart';
part 'cell_data_loader.dart'; part 'cell_data_loader.dart';
part 'context_builder.dart'; part 'cell_controller.dart';
part 'cell_cache.dart'; part 'cell_cache.dart';
part 'cell_data_persistence.dart'; part 'cell_data_persistence.dart';
@ -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 FieldPB field, required GridFieldContext fieldContext,
}) = _GridCellIdentifier; }) = _GridCellIdentifier;
// ignore: unused_element // ignore: unused_element
const GridCellIdentifier._(); const GridCellIdentifier._();
String get fieldId => field.id; String get fieldId => fieldContext.id;
FieldType get fieldType => field.fieldType; FieldType get fieldType => fieldContext.fieldType;
ValueKey key() { ValueKey key() {
return ValueKey("$rowId$fieldId${field.fieldType}"); return ValueKey("$rowId$fieldId${fieldContext.fieldType}");
} }
} }

View File

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

View File

@ -1,5 +1,5 @@
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
@ -20,8 +20,6 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
emit(state.copyWith( emit(state.copyWith(
data: cellData, dateStr: _dateStrFromCellData(cellData))); data: cellData, dateStr: _dateStrFromCellData(cellData)));
}, },
didReceiveFieldUpdate: (FieldPB value) =>
emit(state.copyWith(field: value)),
); );
}, },
); );
@ -53,8 +51,6 @@ class DateCellEvent with _$DateCellEvent {
const factory DateCellEvent.initial() = _InitialCell; const factory DateCellEvent.initial() = _InitialCell;
const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
_DidReceiveCellUpdate; _DidReceiveCellUpdate;
const factory DateCellEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
} }
@freezed @freezed
@ -62,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 FieldPB field, required GridFieldContext fieldContext,
}) = _DateCellState; }) = _DateCellState;
factory DateCellState.initial(GridDateCellController context) { factory DateCellState.initial(GridDateCellController context) {
final cellData = context.getCellData(); final cellData = context.getCellData();
return DateCellState( return DateCellState(
field: context.field, fieldContext: context.fieldContext,
data: cellData, data: cellData,
dateStr: _dateStrFromCellData(cellData), dateStr: _dateStrFromCellData(cellData),
); );

View File

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

View File

@ -1,192 +0,0 @@
import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter/foundation.dart';
import '../row/row_cache.dart';
class FieldsNotifier extends ChangeNotifier {
List<FieldPB> _fields = [];
set fields(List<FieldPB> fields) {
_fields = fields;
notifyListeners();
}
List<FieldPB> get fields => _fields;
}
typedef FieldChangesetCallback = void Function(FieldChangesetPB);
typedef FieldsCallback = void Function(List<FieldPB>);
class GridFieldCache {
final String gridId;
final GridFieldsListener _fieldListener;
FieldsNotifier? _fieldNotifier = FieldsNotifier();
final Map<FieldsCallback, VoidCallback> _fieldsCallbackMap = {};
final Map<FieldChangesetCallback, FieldChangesetCallback>
_changesetCallbackMap = {};
GridFieldCache({required this.gridId})
: _fieldListener = GridFieldsListener(gridId: gridId) {
_fieldListener.start(onFieldsChanged: (result) {
result.fold(
(changeset) {
_deleteFields(changeset.deletedFields);
_insertFields(changeset.insertedFields);
_updateFields(changeset.updatedFields);
for (final listener in _changesetCallbackMap.values) {
listener(changeset);
}
},
(err) => Log.error(err),
);
});
}
Future<void> dispose() async {
await _fieldListener.stop();
_fieldNotifier?.dispose();
_fieldNotifier = null;
}
UnmodifiableListView<FieldPB> get unmodifiableFields =>
UnmodifiableListView(_fieldNotifier?.fields ?? []);
List<FieldPB> get fields => [..._fieldNotifier?.fields ?? []];
set fields(List<FieldPB> fields) {
_fieldNotifier?.fields = [...fields];
}
void addListener({
FieldsCallback? onFields,
FieldChangesetCallback? onChangeset,
bool Function()? listenWhen,
}) {
if (onChangeset != null) {
fn(c) {
if (listenWhen != null && listenWhen() == false) {
return;
}
onChangeset(c);
}
_changesetCallbackMap[onChangeset] = fn;
}
if (onFields != null) {
fn() {
if (listenWhen != null && listenWhen() == false) {
return;
}
onFields(fields);
}
_fieldsCallbackMap[onFields] = fn;
_fieldNotifier?.addListener(fn);
}
}
void removeListener({
FieldsCallback? onFieldsListener,
FieldChangesetCallback? onChangesetListener,
}) {
if (onFieldsListener != null) {
final fn = _fieldsCallbackMap.remove(onFieldsListener);
if (fn != null) {
_fieldNotifier?.removeListener(fn);
}
}
if (onChangesetListener != null) {
_changesetCallbackMap.remove(onChangesetListener);
}
}
void _deleteFields(List<FieldIdPB> deletedFields) {
if (deletedFields.isEmpty) {
return;
}
final List<FieldPB> newFields = fields;
final Map<String, FieldIdPB> deletedFieldMap = {
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
};
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
_fieldNotifier?.fields = newFields;
}
void _insertFields(List<IndexFieldPB> insertedFields) {
if (insertedFields.isEmpty) {
return;
}
final List<FieldPB> newFields = fields;
for (final indexField in insertedFields) {
if (newFields.length > indexField.index) {
newFields.insert(indexField.index, indexField.field_1);
} else {
newFields.add(indexField.field_1);
}
}
_fieldNotifier?.fields = newFields;
}
void _updateFields(List<FieldPB> updatedFields) {
if (updatedFields.isEmpty) {
return;
}
final List<FieldPB> newFields = fields;
for (final updatedField in updatedFields) {
final index =
newFields.indexWhere((field) => field.id == updatedField.id);
if (index != -1) {
newFields.removeAt(index);
newFields.insert(index, updatedField);
}
}
_fieldNotifier?.fields = newFields;
}
}
class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
final GridFieldCache _cache;
FieldChangesetCallback? _onChangesetFn;
FieldsCallback? _onFieldFn;
GridRowFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
@override
UnmodifiableListView<FieldPB> get fields => _cache.unmodifiableFields;
@override
void onRowFieldsChanged(VoidCallback callback) {
_onFieldFn = (_) => callback();
_cache.addListener(onFields: _onFieldFn);
}
@override
void onRowFieldChanged(void Function(FieldPB) callback) {
_onChangesetFn = (FieldChangesetPB changeset) {
for (final updatedField in changeset.updatedFields) {
callback(updatedField);
}
};
_cache.addListener(onChangeset: _onChangesetFn);
}
@override
void onRowDispose() {
if (_onFieldFn != null) {
_cache.removeListener(onFieldsListener: _onFieldFn!);
_onFieldFn = null;
}
if (_onChangesetFn != null) {
_cache.removeListener(onChangesetListener: _onChangesetFn!);
_onChangesetFn = null;
}
}
}

View File

@ -0,0 +1,281 @@
import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/field/grid_listener.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_service.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/log.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/setting_entities.pb.dart';
import 'package:flutter/foundation.dart';
import '../row/row_cache.dart';
class _GridFieldNotifier extends ChangeNotifier {
List<GridFieldContext> _fieldContexts = [];
set fieldContexts(List<GridFieldContext> fieldContexts) {
_fieldContexts = fieldContexts;
notifyListeners();
}
void notify() {
notifyListeners();
}
List<GridFieldContext> get fieldContexts => _fieldContexts;
}
typedef OnChangeset = void Function(FieldChangesetPB);
typedef OnReceiveFields = void Function(List<GridFieldContext>);
class GridFieldController {
final String gridId;
final GridFieldsListener _fieldListener;
final SettingListener _settingListener;
final Map<OnReceiveFields, VoidCallback> _fieldCallbackMap = {};
final Map<OnChangeset, OnChangeset> _changesetCallbackMap = {};
_GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
List<String> _groupFieldIds = [];
final GridFFIService _gridFFIService;
final SettingFFIService _settingFFIService;
List<GridFieldContext> get fieldContexts =>
[..._fieldNotifier?.fieldContexts ?? []];
GridFieldController({required this.gridId})
: _fieldListener = GridFieldsListener(gridId: gridId),
_gridFFIService = GridFFIService(gridId: gridId),
_settingFFIService = SettingFFIService(viewId: gridId),
_settingListener = SettingListener(gridId: gridId) {
//Listen on field's changes
_fieldListener.start(onFieldsChanged: (result) {
result.fold(
(changeset) {
_deleteFields(changeset.deletedFields);
_insertFields(changeset.insertedFields);
_updateFields(changeset.updatedFields);
for (final listener in _changesetCallbackMap.values) {
listener(changeset);
}
},
(err) => Log.error(err),
);
});
//Listen on setting changes
_settingListener.start(onSettingUpdated: (result) {
result.fold(
(setting) => _updateFieldsWhenSettingChanged(setting),
(r) => Log.error(r),
);
});
_settingFFIService.getSetting().then((result) {
result.fold(
(setting) => _updateFieldsWhenSettingChanged(setting),
(err) => Log.error(err),
);
});
}
void _updateFieldsWhenSettingChanged(GridSettingPB setting) {
_groupFieldIds = setting.groupConfigurations.items
.map((item) => item.groupFieldId)
.toList();
_updateFieldContexts();
}
void _updateFieldContexts() {
if (_fieldNotifier != null) {
for (var field in _fieldNotifier!.fieldContexts) {
if (_groupFieldIds.contains(field.id)) {
field._isGroupField = true;
} else {
field._isGroupField = false;
}
}
_fieldNotifier?.notify();
}
}
Future<void> dispose() async {
await _fieldListener.stop();
_fieldNotifier?.dispose();
_fieldNotifier = null;
}
Future<Either<Unit, FlowyError>> loadFields(
{required List<FieldIdPB> fieldIds}) async {
final result = await _gridFFIService.getFields(fieldIds: fieldIds);
return Future(
() => result.fold(
(newFields) {
_fieldNotifier?.fieldContexts = newFields.items
.map((field) => GridFieldContext(field: field))
.toList();
_updateFieldContexts();
return left(unit);
},
(err) => right(err),
),
);
}
void addListener({
OnReceiveFields? onFields,
OnChangeset? onChangeset,
bool Function()? listenWhen,
}) {
if (onChangeset != null) {
callback(c) {
if (listenWhen != null && listenWhen() == false) {
return;
}
onChangeset(c);
}
_changesetCallbackMap[onChangeset] = callback;
}
if (onFields != null) {
callback() {
if (listenWhen != null && listenWhen() == false) {
return;
}
onFields(fieldContexts);
}
_fieldCallbackMap[onFields] = callback;
_fieldNotifier?.addListener(callback);
}
}
void removeListener({
OnReceiveFields? onFieldsListener,
OnChangeset? onChangesetListener,
}) {
if (onFieldsListener != null) {
final callback = _fieldCallbackMap.remove(onFieldsListener);
if (callback != null) {
_fieldNotifier?.removeListener(callback);
}
}
if (onChangesetListener != null) {
_changesetCallbackMap.remove(onChangesetListener);
}
}
void _deleteFields(List<FieldIdPB> deletedFields) {
if (deletedFields.isEmpty) {
return;
}
final List<GridFieldContext> newFields = fieldContexts;
final Map<String, FieldIdPB> deletedFieldMap = {
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
};
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
_fieldNotifier?.fieldContexts = newFields;
}
void _insertFields(List<IndexFieldPB> insertedFields) {
if (insertedFields.isEmpty) {
return;
}
final List<GridFieldContext> newFields = fieldContexts;
for (final indexField in insertedFields) {
final gridField = GridFieldContext(field: indexField.field_1);
if (newFields.length > indexField.index) {
newFields.insert(indexField.index, gridField);
} else {
newFields.add(gridField);
}
}
_fieldNotifier?.fieldContexts = newFields;
}
void _updateFields(List<FieldPB> updatedFields) {
if (updatedFields.isEmpty) {
return;
}
final List<GridFieldContext> newFields = fieldContexts;
for (final updatedField in updatedFields) {
final index =
newFields.indexWhere((field) => field.id == updatedField.id);
if (index != -1) {
newFields.removeAt(index);
final gridField = GridFieldContext(field: updatedField);
newFields.insert(index, gridField);
}
}
_fieldNotifier?.fieldContexts = newFields;
}
}
class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
final GridFieldController _cache;
OnChangeset? _onChangesetFn;
OnReceiveFields? _onFieldFn;
GridRowFieldNotifierImpl(GridFieldController cache) : _cache = cache;
@override
UnmodifiableListView<GridFieldContext> get fields =>
UnmodifiableListView(_cache.fieldContexts);
@override
void onRowFieldsChanged(VoidCallback callback) {
_onFieldFn = (_) => callback();
_cache.addListener(onFields: _onFieldFn);
}
@override
void onRowFieldChanged(void Function(FieldPB) callback) {
_onChangesetFn = (FieldChangesetPB changeset) {
for (final updatedField in changeset.updatedFields) {
callback(updatedField);
}
};
_cache.addListener(onChangeset: _onChangesetFn);
}
@override
void onRowDispose() {
if (_onFieldFn != null) {
_cache.removeListener(onFieldsListener: _onFieldFn!);
_onFieldFn = null;
}
if (_onChangesetFn != null) {
_cache.removeListener(onChangesetListener: _onChangesetFn!);
_onChangesetFn = null;
}
}
}
class GridFieldContext {
final FieldPB _field;
bool _isGroupField = false;
String get id => _field.id;
FieldType get fieldType => _field.fieldType;
bool get visibility => _field.visibility;
double get width => _field.width.toDouble();
bool get isPrimary => _field.isPrimary;
String get name => _field.name;
FieldPB get field => _field;
bool get isGroupField => _isGroupField;
GridFieldContext({required FieldPB field}) : _field = field;
}

View File

@ -5,6 +5,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_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';
part 'field_service.freezed.dart'; part 'field_service.freezed.dart';
/// FieldService consists of lots of event functions. We define the events in the backend(Rust), /// FieldService consists of lots of event functions. We define the events in the backend(Rust),

View File

@ -1,3 +1,4 @@
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:flowy_infra/notifier.dart'; import 'package:flowy_infra/notifier.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';
@ -17,12 +18,12 @@ class TypeOptionDataController {
TypeOptionDataController({ TypeOptionDataController({
required this.gridId, required this.gridId,
required this.loader, required this.loader,
FieldPB? field, GridFieldContext? fieldContext,
}) { }) {
if (field != null) { if (fieldContext != null) {
_data = FieldTypeOptionDataPB.create() _data = FieldTypeOptionDataPB.create()
..gridId = gridId ..gridId = gridId
..field_2 = field; ..field_2 = fieldContext.field;
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'block/block_cache.dart'; import 'block/block_cache.dart';
import 'field/field_controller.dart';
import 'grid_data_controller.dart'; import 'grid_data_controller.dart';
import 'row/row_cache.dart'; import 'row/row_cache.dart';
import 'dart:collection'; import 'dart:collection';
@ -101,7 +102,7 @@ class GridEvent with _$GridEvent {
RowsChangedReason listState, RowsChangedReason listState,
) = _DidReceiveRowUpdate; ) = _DidReceiveRowUpdate;
const factory GridEvent.didReceiveFieldUpdate( const factory GridEvent.didReceiveFieldUpdate(
UnmodifiableListView<FieldPB> fields, UnmodifiableListView<GridFieldContext> fields,
) = _DidReceiveFieldUpdate; ) = _DidReceiveFieldUpdate;
const factory GridEvent.didReceiveGridUpdate( const factory GridEvent.didReceiveGridUpdate(
@ -138,9 +139,9 @@ class GridLoadingState with _$GridLoadingState {
} }
class GridFieldEquatable extends Equatable { class GridFieldEquatable extends Equatable {
final UnmodifiableListView<FieldPB> _fields; final UnmodifiableListView<GridFieldContext> _fields;
const GridFieldEquatable( const GridFieldEquatable(
UnmodifiableListView<FieldPB> fields, UnmodifiableListView<GridFieldContext> fields,
) : _fields = fields; ) : _fields = fields;
@override @override
@ -157,5 +158,6 @@ class GridFieldEquatable extends Equatable {
]; ];
} }
UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields); UnmodifiableListView<GridFieldContext> get value =>
UnmodifiableListView(_fields);
} }

View File

@ -4,16 +4,15 @@ 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';
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/grid_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
import 'dart:async'; import 'dart:async';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'block/block_cache.dart'; import 'block/block_cache.dart';
import 'field/field_cache.dart'; 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<FieldPB>); typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>);
typedef OnGridChanged = void Function(GridPB); typedef OnGridChanged = void Function(GridPB);
typedef OnRowsChanged = void Function( typedef OnRowsChanged = void Function(
@ -25,7 +24,7 @@ typedef ListenOnRowChangedCondition = bool Function();
class GridDataController { class GridDataController {
final String gridId; final String gridId;
final GridFFIService _gridFFIService; final GridFFIService _gridFFIService;
final GridFieldCache fieldCache; final GridFieldController fieldController;
// key: the block id // key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks; final LinkedHashMap<String, GridBlockCache> _blocks;
@ -49,7 +48,7 @@ class GridDataController {
// ignore: prefer_collection_literals // ignore: prefer_collection_literals
_blocks = LinkedHashMap(), _blocks = LinkedHashMap(),
_gridFFIService = GridFFIService(gridId: view.id), _gridFFIService = GridFFIService(gridId: view.id),
fieldCache = GridFieldCache(gridId: view.id); fieldController = GridFieldController(gridId: view.id);
void addListener({ void addListener({
required OnGridChanged onGridChanged, required OnGridChanged onGridChanged,
@ -60,7 +59,7 @@ class GridDataController {
_onRowChanged = onRowsChanged; _onRowChanged = onRowsChanged;
_onFieldsChanged = onFieldsChanged; _onFieldsChanged = onFieldsChanged;
fieldCache.addListener(onFields: (fields) { fieldController.addListener(onFields: (fields) {
_onFieldsChanged?.call(UnmodifiableListView(fields)); _onFieldsChanged?.call(UnmodifiableListView(fields));
}); });
} }
@ -72,7 +71,7 @@ class GridDataController {
(grid) async { (grid) async {
_initialBlocks(grid.blocks); _initialBlocks(grid.blocks);
_onGridChanged?.call(grid); _onGridChanged?.call(grid);
return await _loadFields(grid); return await fieldController.loadFields(fieldIds: grid.fields);
}, },
(err) => right(err), (err) => right(err),
), ),
@ -85,7 +84,7 @@ class GridDataController {
Future<void> dispose() async { Future<void> dispose() async {
await _gridFFIService.closeGrid(); await _gridFFIService.closeGrid();
await fieldCache.dispose(); await fieldController.dispose();
for (final blockCache in _blocks.values) { for (final blockCache in _blocks.values) {
blockCache.dispose(); blockCache.dispose();
@ -102,7 +101,7 @@ class GridDataController {
final cache = GridBlockCache( final cache = GridBlockCache(
gridId: gridId, gridId: gridId,
block: block, block: block,
fieldCache: fieldCache, fieldController: fieldController,
); );
cache.addListener( cache.addListener(
@ -114,18 +113,4 @@ class GridDataController {
_blocks[block.id] = cache; _blocks[block.id] = cache;
} }
} }
Future<Either<Unit, FlowyError>> _loadFields(GridPB grid) async {
final result = await _gridFFIService.getFields(fieldIds: grid.fields);
return Future(
() => result.fold(
(fields) {
fieldCache.fields = fields.items;
_onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields));
return left(unit);
},
(err) => right(err),
),
);
}
} }

View File

@ -4,19 +4,18 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
import 'field/field_controller.dart';
import 'field/field_cache.dart';
part 'grid_header_bloc.freezed.dart'; part 'grid_header_bloc.freezed.dart';
class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> { class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
final GridFieldCache fieldCache; final GridFieldController fieldController;
final String gridId; final String gridId;
GridHeaderBloc({ GridHeaderBloc({
required this.gridId, required this.gridId,
required this.fieldCache, required this.fieldController,
}) : super(GridHeaderState.initial(fieldCache.fields)) { }) : super(GridHeaderState.initial(fieldController.fieldContexts)) {
on<GridHeaderEvent>( on<GridHeaderEvent>(
(event, emit) async { (event, emit) async {
await event.map( await event.map(
@ -36,7 +35,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<FieldPB>.from(state.fields); final fields = List<GridFieldContext>.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));
@ -49,7 +48,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
} }
Future<void> _startListening() async { Future<void> _startListening() async {
fieldCache.addListener( fieldController.addListener(
onFields: (fields) => add(GridHeaderEvent.didReceiveFieldUpdate(fields)), onFields: (fields) => add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
listenWhen: () => !isClosed, listenWhen: () => !isClosed,
); );
@ -64,18 +63,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(List<FieldPB> fields) = const factory GridHeaderEvent.didReceiveFieldUpdate(
_DidReceiveFieldUpdate; List<GridFieldContext> fields) = _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<FieldPB> fields}) = const factory GridHeaderState({required List<GridFieldContext> fields}) =
_GridHeaderState; _GridHeaderState;
factory GridHeaderState.initial(List<FieldPB> fields) { factory GridHeaderState.initial(List<GridFieldContext> fields) {
// final List<FieldPB> newFields = List.from(fields); // final List<FieldPB> newFields = List.from(fields);
// newFields.retainWhere((field) => field.visibility); // newFields.retainWhere((field) => field.visibility);
return GridHeaderState(fields: fields); return GridHeaderState(fields: fields);

View File

@ -1,7 +1,7 @@
import 'dart:collection'; import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
@ -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.field)) .map((e) => GridCellEquatable(e.fieldContext))
.toList(); .toList();
emit(state.copyWith( emit(state.copyWith(
gridCellMap: value.gridCellMap, gridCellMap: value.gridCellMap,
@ -87,21 +87,23 @@ class RowState with _$RowState {
rowInfo: rowInfo, rowInfo: rowInfo,
gridCellMap: cellDataMap, gridCellMap: cellDataMap,
cells: UnmodifiableListView( cells: UnmodifiableListView(
cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(), cellDataMap.values
.map((e) => GridCellEquatable(e.fieldContext))
.toList(),
), ),
); );
} }
class GridCellEquatable extends Equatable { class GridCellEquatable extends Equatable {
final FieldPB _field; final GridFieldContext _fieldContext;
const GridCellEquatable(FieldPB field) : _field = field; const GridCellEquatable(GridFieldContext field) : _fieldContext = field;
@override @override
List<Object?> get props => [ List<Object?> get props => [
_field.id, _fieldContext.id,
_field.fieldType, _fieldContext.fieldType,
_field.visibility, _fieldContext.visibility,
_field.width, _fieldContext.width,
]; ];
} }

View File

@ -1,5 +1,6 @@
import 'dart:collection'; import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
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';
@ -12,7 +13,7 @@ part 'row_cache.freezed.dart';
typedef RowUpdateCallback = void Function(); typedef RowUpdateCallback = void Function();
abstract class IGridRowFieldNotifier { abstract class IGridRowFieldNotifier {
UnmodifiableListView<FieldPB> get fields; UnmodifiableListView<GridFieldContext> get fields;
void onRowFieldsChanged(VoidCallback callback); void onRowFieldsChanged(VoidCallback callback);
void onRowFieldChanged(void Function(FieldPB) callback); void onRowFieldChanged(void Function(FieldPB) callback);
void onRowDispose(); void onRowDispose();
@ -217,7 +218,7 @@ class GridRowCache {
cellDataMap[field.id] = GridCellIdentifier( cellDataMap[field.id] = GridCellIdentifier(
rowId: rowId, rowId: rowId,
gridId: gridId, gridId: gridId,
field: field, fieldContext: field,
); );
} }
} }
@ -284,7 +285,7 @@ class _RowChangesetNotifier extends ChangeNotifier {
class RowInfo with _$RowInfo { class RowInfo with _$RowInfo {
const factory RowInfo({ const factory RowInfo({
required String gridId, required String gridId,
required UnmodifiableListView<FieldPB> fields, required UnmodifiableListView<GridFieldContext> fields,
required RowPB rowPB, required RowPB rowPB,
}) = _RowInfo; }) = _RowInfo;
} }

View File

@ -2,7 +2,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../presentation/widgets/cell/cell_builder.dart'; import '../../presentation/widgets/cell/cell_builder.dart';
import '../cell/cell_service/cell_service.dart'; import '../cell/cell_service/cell_service.dart';
import '../field/field_cache.dart'; import '../field/field_controller.dart';
import 'row_cache.dart'; import 'row_cache.dart';
typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason); typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason);
@ -10,14 +10,14 @@ typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason);
class GridRowDataController extends GridCellBuilderDelegate { class GridRowDataController extends GridCellBuilderDelegate {
final RowInfo rowInfo; final RowInfo rowInfo;
final List<VoidCallback> _onRowChangedListeners = []; final List<VoidCallback> _onRowChangedListeners = [];
final GridFieldCache _fieldCache; final GridFieldController _fieldController;
final GridRowCache _rowCache; final GridRowCache _rowCache;
GridRowDataController({ GridRowDataController({
required this.rowInfo, required this.rowInfo,
required GridFieldCache fieldCache, required GridFieldController fieldController,
required GridRowCache rowCache, required GridRowCache rowCache,
}) : _fieldCache = fieldCache, }) : _fieldController = fieldController,
_rowCache = rowCache; _rowCache = rowCache;
GridCellMap loadData() { GridCellMap loadData() {
@ -41,7 +41,7 @@ class GridRowDataController extends GridCellBuilderDelegate {
@override @override
GridCellFieldNotifier buildFieldNotifier() { GridCellFieldNotifier buildFieldNotifier() {
return GridCellFieldNotifier( return GridCellFieldNotifier(
notifier: GridCellFieldNotifierImpl(_fieldCache)); notifier: GridCellFieldNotifierImpl(_fieldController));
} }
@override @override

View File

@ -0,0 +1,84 @@
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../field/field_controller.dart';
import 'setting_service.dart';
part 'group_bloc.freezed.dart';
class GridGroupBloc extends Bloc<GridGroupEvent, GridGroupState> {
final GridFieldController _fieldController;
final SettingFFIService _settingFFIService;
Function(List<GridFieldContext>)? _onFieldsFn;
GridGroupBloc(
{required String viewId, required GridFieldController fieldController})
: _fieldController = fieldController,
_settingFFIService = SettingFFIService(viewId: viewId),
super(GridGroupState.initial(viewId, fieldController.fieldContexts)) {
on<GridGroupEvent>(
(event, emit) async {
event.when(
initial: () {
_startListening();
},
didReceiveFieldUpdate: (fieldContexts) {
emit(state.copyWith(fieldContexts: fieldContexts));
},
setGroupByField: (String fieldId, FieldType fieldType) {
_settingFFIService.groupByField(
fieldId: fieldId,
fieldType: fieldType,
);
},
);
},
);
}
@override
Future<void> close() async {
if (_onFieldsFn != null) {
_fieldController.removeListener(onFieldsListener: _onFieldsFn!);
_onFieldsFn = null;
}
return super.close();
}
void _startListening() {
_onFieldsFn = (fieldContexts) =>
add(GridGroupEvent.didReceiveFieldUpdate(fieldContexts));
_fieldController.addListener(
onFields: _onFieldsFn,
listenWhen: () => !isClosed,
);
}
}
@freezed
class GridGroupEvent with _$GridGroupEvent {
const factory GridGroupEvent.initial() = _Initial;
const factory GridGroupEvent.setGroupByField(
String fieldId,
FieldType fieldType,
) = _GroupByField;
const factory GridGroupEvent.didReceiveFieldUpdate(
List<GridFieldContext> fields) = _DidReceiveFieldUpdate;
}
@freezed
class GridGroupState with _$GridGroupState {
const factory GridGroupState({
required String gridId,
required List<GridFieldContext> fieldContexts,
}) = _GridGroupState;
factory GridGroupState.initial(
String gridId, List<GridFieldContext> fieldContexts) =>
GridGroupState(
gridId: gridId,
fieldContexts: fieldContexts,
);
}

View File

@ -1,21 +1,22 @@
import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
import '../field/field_cache.dart'; import '../field/field_controller.dart';
part 'property_bloc.freezed.dart'; part 'property_bloc.freezed.dart';
class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> { class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
final GridFieldCache _fieldCache; final GridFieldController _fieldController;
Function(List<FieldPB>)? _onFieldsFn; Function(List<GridFieldContext>)? _onFieldsFn;
GridPropertyBloc({required String gridId, required GridFieldCache fieldCache}) GridPropertyBloc(
: _fieldCache = fieldCache, {required String gridId, required GridFieldController fieldController})
super(GridPropertyState.initial(gridId, fieldCache.fields)) { : _fieldController = fieldController,
super(
GridPropertyState.initial(gridId, fieldController.fieldContexts)) {
on<GridPropertyEvent>( on<GridPropertyEvent>(
(event, emit) async { (event, emit) async {
await event.map( await event.map(
@ -33,7 +34,7 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
); );
}, },
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) { didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
emit(state.copyWith(fields: value.fields)); emit(state.copyWith(fieldContexts: value.fields));
}, },
moveField: (_MoveField value) { moveField: (_MoveField value) {
// //
@ -46,7 +47,7 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
@override @override
Future<void> close() async { Future<void> close() async {
if (_onFieldsFn != null) { if (_onFieldsFn != null) {
_fieldCache.removeListener(onFieldsListener: _onFieldsFn!); _fieldController.removeListener(onFieldsListener: _onFieldsFn!);
_onFieldsFn = null; _onFieldsFn = null;
} }
return super.close(); return super.close();
@ -55,7 +56,7 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
void _startListening() { void _startListening() {
_onFieldsFn = _onFieldsFn =
(fields) => add(GridPropertyEvent.didReceiveFieldUpdate(fields)); (fields) => add(GridPropertyEvent.didReceiveFieldUpdate(fields));
_fieldCache.addListener( _fieldController.addListener(
onFields: _onFieldsFn, onFields: _onFieldsFn,
listenWhen: () => !isClosed, listenWhen: () => !isClosed,
); );
@ -67,8 +68,8 @@ class GridPropertyEvent with _$GridPropertyEvent {
const factory GridPropertyEvent.initial() = _Initial; const factory GridPropertyEvent.initial() = _Initial;
const factory GridPropertyEvent.setFieldVisibility( const factory GridPropertyEvent.setFieldVisibility(
String fieldId, bool visibility) = _SetFieldVisibility; String fieldId, bool visibility) = _SetFieldVisibility;
const factory GridPropertyEvent.didReceiveFieldUpdate(List<FieldPB> fields) = const factory GridPropertyEvent.didReceiveFieldUpdate(
_DidReceiveFieldUpdate; List<GridFieldContext> fields) = _DidReceiveFieldUpdate;
const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) = const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) =
_MoveField; _MoveField;
} }
@ -77,12 +78,15 @@ 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<FieldPB> fields, required List<GridFieldContext> fieldContexts,
}) = _GridPropertyState; }) = _GridPropertyState;
factory GridPropertyState.initial(String gridId, List<FieldPB> fields) => factory GridPropertyState.initial(
String gridId,
List<GridFieldContext> fieldContexts,
) =>
GridPropertyState( GridPropertyState(
gridId: gridId, gridId: gridId,
fields: fields, fieldContexts: fieldContexts,
); );
} }

View File

@ -0,0 +1,59 @@
import 'package:app_flowy/plugins/grid/application/setting/setting_service.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
import 'setting_listener.dart';
typedef OnError = void Function(FlowyError);
typedef OnSettingUpdated = void Function(GridSettingPB);
class SettingController {
final String viewId;
final SettingFFIService _ffiService;
final SettingListener _listener;
OnSettingUpdated? _onSettingUpdated;
OnError? _onError;
GridSettingPB? _setting;
GridSettingPB? get setting => _setting;
SettingController({
required this.viewId,
}) : _ffiService = SettingFFIService(viewId: viewId),
_listener = SettingListener(gridId: viewId) {
// Load setting
_ffiService.getSetting().then((result) {
result.fold(
(newSetting) => updateSetting(newSetting),
(err) => _onError?.call(err),
);
});
// Listen on the seting changes
_listener.start(onSettingUpdated: (result) {
result.fold(
(newSetting) => updateSetting(newSetting),
(err) => _onError?.call(err),
);
});
}
void startListeing({
required OnSettingUpdated onSettingUpdated,
required OnError onError,
}) {
assert(_onSettingUpdated == null, 'Should call once');
assert(_onError == null, 'Should call once');
_onSettingUpdated = onSettingUpdated;
_onError = onError;
}
void updateSetting(GridSettingPB newSetting) {
_setting = newSetting;
_onSettingUpdated?.call(newSetting);
}
void dispose() {
_onSettingUpdated = null;
_onError = null;
_listener.stop();
}
}

View File

@ -0,0 +1,47 @@
import 'dart:typed_data';
import 'package:app_flowy/core/grid_notification.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_infra/notifier.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pbserver.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
typedef UpdateSettingNotifiedValue = Either<GridSettingPB, FlowyError>;
class SettingListener {
final String gridId;
GridNotificationListener? _listener;
PublishNotifier<UpdateSettingNotifiedValue>? _updateSettingNotifier =
PublishNotifier();
SettingListener({required this.gridId});
void start({
required void Function(UpdateSettingNotifiedValue) onSettingUpdated,
}) {
_updateSettingNotifier?.addPublishListener(onSettingUpdated);
_listener = GridNotificationListener(objectId: gridId, handler: _handler);
}
void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
switch (ty) {
case GridNotification.DidUpdateGridSetting:
result.fold(
(payload) => _updateSettingNotifier?.value = left(
GridSettingPB.fromBuffer(payload),
),
(error) => _updateSettingNotifier?.value = right(error),
);
break;
default:
break;
}
}
Future<void> stop() async {
await _listener?.stop();
_updateSettingNotifier?.dispose();
_updateSettingNotifier = null;
}
}

View File

@ -0,0 +1,32 @@
import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/dispatch/dispatch.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/grid_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
class SettingFFIService {
final String viewId;
const SettingFFIService({required this.viewId});
Future<Either<GridSettingPB, FlowyError>> getSetting() {
final payload = GridIdPB.create()..value = viewId;
return GridEventGetGridSetting(payload).send();
}
Future<Either<Unit, FlowyError>> groupByField({
required String fieldId,
required FieldType fieldType,
}) {
final insertGroupPayload = InsertGroupPayloadPB.create()
..fieldId = fieldId
..fieldType = fieldType;
final payload = GridSettingChangesetPayloadPB.create()
..gridId = viewId
..insertGroup = insertGroupPayload;
return GridEventUpdateGridSetting(payload).send();
}
}

View File

@ -1,4 +1,4 @@
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_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/startup/startup.dart';
import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
@ -158,10 +158,11 @@ class _FlowyGridState extends State<FlowyGrid> {
} }
Widget _gridHeader(BuildContext context, String gridId) { Widget _gridHeader(BuildContext context, String gridId) {
final fieldCache = context.read<GridBloc>().dataController.fieldCache; final fieldController =
context.read<GridBloc>().dataController.fieldController;
return GridHeaderSliverAdaptor( return GridHeaderSliverAdaptor(
gridId: gridId, gridId: gridId,
fieldCache: fieldCache, fieldController: fieldController,
anchorScrollController: headerScrollController, anchorScrollController: headerScrollController,
); );
} }
@ -174,10 +175,11 @@ class _GridToolbarAdaptor extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocSelector<GridBloc, GridState, GridToolbarContext>( return BlocSelector<GridBloc, GridState, GridToolbarContext>(
selector: (state) { selector: (state) {
final fieldCache = context.read<GridBloc>().dataController.fieldCache; final fieldController =
context.read<GridBloc>().dataController.fieldController;
return GridToolbarContext( return GridToolbarContext(
gridId: state.gridId, gridId: state.gridId,
fieldCache: fieldCache, fieldController: fieldController,
); );
}, },
builder: (context, toolbarContext) { builder: (context, toolbarContext) {
@ -248,10 +250,11 @@ class _GridRowsState extends State<_GridRows> {
/// Return placeholder widget if the rowCache is null. /// Return placeholder widget if the rowCache is null.
if (rowCache == null) return const SizedBox(); if (rowCache == null) return const SizedBox();
final fieldCache = context.read<GridBloc>().dataController.fieldCache; final fieldController =
context.read<GridBloc>().dataController.fieldController;
final dataController = GridRowDataController( final dataController = GridRowDataController(
rowInfo: rowInfo, rowInfo: rowInfo,
fieldCache: fieldCache, fieldController: fieldController,
rowCache: rowCache, rowCache: rowCache,
); );
@ -265,7 +268,7 @@ class _GridRowsState extends State<_GridRows> {
_openRowDetailPage( _openRowDetailPage(
context, context,
rowInfo, rowInfo,
fieldCache, fieldController,
rowCache, rowCache,
cellBuilder, cellBuilder,
); );
@ -278,13 +281,13 @@ class _GridRowsState extends State<_GridRows> {
void _openRowDetailPage( void _openRowDetailPage(
BuildContext context, BuildContext context,
RowInfo rowInfo, RowInfo rowInfo,
GridFieldCache fieldCache, GridFieldController fieldController,
GridRowCache rowCache, GridRowCache rowCache,
GridCellBuilder cellBuilder, GridCellBuilder cellBuilder,
) { ) {
final dataController = GridRowDataController( final dataController = GridRowDataController(
rowInfo: rowInfo, rowInfo: rowInfo,
fieldCache: fieldCache, fieldController: fieldController,
rowCache: rowCache, rowCache: rowCache,
); );

View File

@ -1,8 +1,8 @@
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; 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<FieldPB> fields) { static double headerWidth(List<GridFieldContext> fields) {
if (fields.isEmpty) return 0; if (fields.isEmpty) return 0;
final fieldsWidth = fields final fieldsWidth = fields

View File

@ -1,5 +1,5 @@
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_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart';
@ -20,11 +20,11 @@ import 'field_cell.dart';
class GridHeaderSliverAdaptor extends StatefulWidget { class GridHeaderSliverAdaptor extends StatefulWidget {
final String gridId; final String gridId;
final GridFieldCache fieldCache; final GridFieldController fieldController;
final ScrollController anchorScrollController; final ScrollController anchorScrollController;
const GridHeaderSliverAdaptor({ const GridHeaderSliverAdaptor({
required this.gridId, required this.gridId,
required this.fieldCache, required this.fieldController,
required this.anchorScrollController, required this.anchorScrollController,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -40,7 +40,7 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
return BlocProvider( return BlocProvider(
create: (context) { create: (context) {
final bloc = getIt<GridHeaderBloc>( final bloc = getIt<GridHeaderBloc>(
param1: widget.gridId, param2: widget.fieldCache); param1: widget.gridId, param2: widget.fieldController);
bloc.add(const GridHeaderEvent.initial()); bloc.add(const GridHeaderEvent.initial());
return bloc; return bloc;
}, },
@ -101,7 +101,7 @@ class _GridHeaderState extends State<_GridHeader> {
final cells = state.fields final cells = state.fields
.where((field) => field.visibility) .where((field) => field.visibility)
.map((field) => .map((field) =>
GridFieldCellContext(gridId: widget.gridId, field: field)) GridFieldCellContext(gridId: widget.gridId, field: field.field))
.map((ctx) => GridFieldCell( .map((ctx) => GridFieldCell(
key: _getKeyById(ctx.field.id), key: _getKeyById(ctx.field.id),
cellContext: ctx, cellContext: ctx,

View File

@ -1,5 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart';
import 'package:appflowy_popover/popover.dart'; import 'package:appflowy_popover/popover.dart';
@ -126,17 +127,18 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder(
TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({ TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({
required String gridId, required String gridId,
required FieldPB field, required GridFieldContext fieldContext,
}) { }) {
final loader = FieldTypeOptionLoader(gridId: gridId, field: field); final loader =
FieldTypeOptionLoader(gridId: gridId, field: fieldContext.field);
final dataController = TypeOptionDataController( final dataController = TypeOptionDataController(
gridId: gridId, gridId: gridId,
loader: loader, loader: loader,
field: field, fieldContext: fieldContext,
); );
return makeTypeOptionContextWithDataController( return makeTypeOptionContextWithDataController(
gridId: gridId, gridId: gridId,
fieldType: field.fieldType, fieldType: fieldContext.fieldType,
dataController: dataController, dataController: dataController,
); );
} }

View File

@ -189,13 +189,13 @@ class RowContent extends StatelessWidget {
final GridCellWidget child = builder.build(cellId); final GridCellWidget child = builder.build(cellId);
return CellContainer( return CellContainer(
width: cellId.field.width.toDouble(), width: cellId.fieldContext.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.field.isPrimary) { if (cellId.fieldContext.isPrimary) {
accessories.add( accessories.add(
GridCellAccessoryBuilder( GridCellAccessoryBuilder(
builder: (key) => PrimaryCellAccessory( builder: (key) => PrimaryCellAccessory(

View File

@ -225,16 +225,16 @@ class _RowDetailCellState extends State<_RowDetailCell> {
constraints: BoxConstraints.loose(const Size(240, 200)), constraints: BoxConstraints.loose(const Size(240, 200)),
child: FieldEditor( child: FieldEditor(
gridId: widget.cellId.gridId, gridId: widget.cellId.gridId,
fieldName: widget.cellId.field.name, fieldName: widget.cellId.fieldContext.field.name,
typeOptionLoader: FieldTypeOptionLoader( typeOptionLoader: FieldTypeOptionLoader(
gridId: widget.cellId.gridId, gridId: widget.cellId.gridId,
field: widget.cellId.field, field: widget.cellId.fieldContext.field,
), ),
), ),
); );
}, },
child: FieldCellButton( child: FieldCellButton(
field: widget.cellId.field, field: widget.cellId.fieldContext.field,
onTap: () => popover.show(), onTap: () => popover.show(),
), ),
), ),

View File

@ -0,0 +1,110 @@
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.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:app_flowy/plugins/grid/application/setting/group_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class GridGroupList extends StatelessWidget {
final String viewId;
final GridFieldController fieldController;
const GridGroupList({
required this.viewId,
required this.fieldController,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => GridGroupBloc(
viewId: viewId,
fieldController: fieldController,
)..add(const GridGroupEvent.initial()),
child: BlocBuilder<GridGroupBloc, GridGroupState>(
builder: (context, state) {
final cells = state.fieldContexts.map((fieldContext) {
return _GridGroupCell(
fieldContext: fieldContext,
key: ValueKey(fieldContext.id),
);
}).toList();
return ListView.separated(
shrinkWrap: true,
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
separatorBuilder: (BuildContext context, int index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
);
},
),
);
}
void show(BuildContext context) {
FlowyOverlay.of(context).insertWithAnchor(
widget: OverlayContainer(
constraints: BoxConstraints.loose(const Size(260, 400)),
child: this,
),
identifier: identifier(),
anchorContext: context,
anchorDirection: AnchorDirection.bottomRight,
style: FlowyOverlayStyle(blur: false),
);
}
static String identifier() {
return (GridGroupList).toString();
}
}
class _GridGroupCell extends StatelessWidget {
final GridFieldContext fieldContext;
const _GridGroupCell({required this.fieldContext, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();
Widget? rightIcon;
if (fieldContext.isGroupField) {
rightIcon = Padding(
padding: const EdgeInsets.all(2.0),
child: svgWidget("grid/checkmark"),
);
}
return SizedBox(
height: GridSize.typeOptionItemHeight,
child: FlowyButton(
text: FlowyText.medium(fieldContext.name, fontSize: 12),
hoverColor: theme.hover,
leftIcon: svgWidget(fieldContext.fieldType.iconName(),
color: theme.iconColor),
rightIcon: rightIcon,
onTap: () {
context.read<GridGroupBloc>().add(
GridGroupEvent.setGroupByField(
fieldContext.id,
fieldContext.fieldType,
),
);
FlowyOverlay.of(context).remove(GridGroupList.identifier());
},
),
);
}
}

View File

@ -11,20 +11,19 @@ import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.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:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import '../../../application/field/field_cache.dart'; import '../../../application/field/field_controller.dart';
import '../../layout/sizes.dart'; import '../../layout/sizes.dart';
class GridPropertyList extends StatefulWidget { class GridPropertyList extends StatefulWidget {
final String gridId; final String gridId;
final GridFieldCache fieldCache; final GridFieldController fieldController;
const GridPropertyList({ const GridPropertyList({
required this.gridId, required this.gridId,
required this.fieldCache, required this.fieldController,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -45,15 +44,15 @@ class _GridPropertyListState extends State<GridPropertyList> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => getIt<GridPropertyBloc>( create: (context) => getIt<GridPropertyBloc>(
param1: widget.gridId, param2: widget.fieldCache) param1: widget.gridId, param2: widget.fieldController)
..add(const GridPropertyEvent.initial()), ..add(const GridPropertyEvent.initial()),
child: BlocBuilder<GridPropertyBloc, GridPropertyState>( child: BlocBuilder<GridPropertyBloc, GridPropertyState>(
builder: (context, state) { builder: (context, state) {
final cells = state.fields.map((field) { final cells = state.fieldContexts.map((field) {
return _GridPropertyCell( return _GridPropertyCell(
popoverMutex: _popoverMutex, popoverMutex: _popoverMutex,
gridId: widget.gridId, gridId: widget.gridId,
field: field, fieldContext: field,
key: ValueKey(field.id), key: ValueKey(field.id),
); );
}).toList(); }).toList();
@ -76,12 +75,12 @@ class _GridPropertyListState extends State<GridPropertyList> {
} }
class _GridPropertyCell extends StatelessWidget { class _GridPropertyCell extends StatelessWidget {
final FieldPB field; final GridFieldContext fieldContext;
final String gridId; final String gridId;
final PopoverMutex popoverMutex; final PopoverMutex popoverMutex;
const _GridPropertyCell({ const _GridPropertyCell({
required this.gridId, required this.gridId,
required this.field, required this.fieldContext,
required this.popoverMutex, required this.popoverMutex,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -90,7 +89,7 @@ class _GridPropertyCell extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
final checkmark = field.visibility final checkmark = fieldContext.visibility
? svgWidget('home/show', color: theme.iconColor) ? svgWidget('home/show', color: theme.iconColor)
: svgWidget('home/hide', color: theme.iconColor); : svgWidget('home/hide', color: theme.iconColor);
@ -108,7 +107,7 @@ class _GridPropertyCell extends StatelessWidget {
onPressed: () { onPressed: () {
context.read<GridPropertyBloc>().add( context.read<GridPropertyBloc>().add(
GridPropertyEvent.setFieldVisibility( GridPropertyEvent.setFieldVisibility(
field.id, !field.visibility)); fieldContext.id, !fieldContext.visibility));
}, },
icon: checkmark.padding(all: 6), icon: checkmark.padding(all: 6),
) )
@ -122,18 +121,19 @@ class _GridPropertyCell extends StatelessWidget {
triggerActions: PopoverTriggerActionFlags.click, triggerActions: PopoverTriggerActionFlags.click,
offset: const Offset(20, 0), offset: const Offset(20, 0),
child: FlowyButton( child: FlowyButton(
text: FlowyText.medium(field.name, fontSize: 12), text: FlowyText.medium(fieldContext.name, fontSize: 12),
hoverColor: theme.hover, hoverColor: theme.hover,
leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor), leftIcon: svgWidget(fieldContext.fieldType.iconName(),
color: theme.iconColor),
), ),
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
return OverlayContainer( return OverlayContainer(
constraints: BoxConstraints.loose(const Size(240, 200)), constraints: BoxConstraints.loose(const Size(240, 200)),
child: FieldEditor( child: FieldEditor(
gridId: gridId, gridId: gridId,
fieldName: field.name, fieldName: fieldContext.name,
typeOptionLoader: typeOptionLoader: FieldTypeOptionLoader(
FieldTypeOptionLoader(gridId: gridId, field: field), gridId: gridId, field: fieldContext.field),
), ),
); );
}, },

View File

@ -11,16 +11,16 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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_cache.dart'; import '../../../application/field/field_controller.dart';
import '../../layout/sizes.dart'; import '../../layout/sizes.dart';
class GridSettingContext { class GridSettingContext {
final String gridId; final String gridId;
final GridFieldCache fieldCache; final GridFieldController fieldController;
GridSettingContext({ GridSettingContext({
required this.gridId, required this.gridId,
required this.fieldCache, required this.fieldController,
}); });
} }

View File

@ -8,17 +8,17 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../application/field/field_cache.dart'; import '../../../application/field/field_controller.dart';
import '../../layout/sizes.dart'; import '../../layout/sizes.dart';
import 'grid_property.dart'; import 'grid_property.dart';
import 'grid_setting.dart'; import 'grid_setting.dart';
class GridToolbarContext { class GridToolbarContext {
final String gridId; final String gridId;
final GridFieldCache fieldCache; final GridFieldController fieldController;
GridToolbarContext({ GridToolbarContext({
required this.gridId, required this.gridId,
required this.fieldCache, required this.fieldController,
}); });
} }
@ -30,7 +30,7 @@ class GridToolbar extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final settingContext = GridSettingContext( final settingContext = GridSettingContext(
gridId: toolbarContext.gridId, gridId: toolbarContext.gridId,
fieldCache: toolbarContext.fieldCache, fieldController: toolbarContext.fieldController,
); );
return SizedBox( return SizedBox(
height: 40, height: 40,
@ -89,7 +89,7 @@ class _GridSettingListPopoverState extends State<_GridSettingListPopover> {
constraints: BoxConstraints.loose(const Size(260, 400)), constraints: BoxConstraints.loose(const Size(260, 400)),
child: GridPropertyList( child: GridPropertyList(
gridId: widget.settingContext.gridId, gridId: widget.settingContext.gridId,
fieldCache: widget.settingContext.fieldCache, fieldController: widget.settingContext.fieldController,
), ),
); );
} }

View File

@ -21,7 +21,7 @@ import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import '../plugins/grid/application/field/field_cache.dart'; import '../plugins/grid/application/field/field_controller.dart';
class DependencyResolver { class DependencyResolver {
static Future<void> resolve(GetIt getIt) async { static Future<void> resolve(GetIt getIt) async {
@ -154,10 +154,10 @@ void _resolveGridDeps(GetIt getIt) {
(view, _) => GridBloc(view: view), (view, _) => GridBloc(view: view),
); );
getIt.registerFactoryParam<GridHeaderBloc, String, GridFieldCache>( getIt.registerFactoryParam<GridHeaderBloc, String, GridFieldController>(
(gridId, fieldCache) => GridHeaderBloc( (gridId, fieldController) => GridHeaderBloc(
gridId: gridId, gridId: gridId,
fieldCache: fieldCache, fieldController: fieldController,
), ),
); );
@ -200,7 +200,7 @@ void _resolveGridDeps(GetIt getIt) {
), ),
); );
getIt.registerFactoryParam<GridPropertyBloc, String, GridFieldCache>( getIt.registerFactoryParam<GridPropertyBloc, String, GridFieldController>(
(gridId, cache) => GridPropertyBloc(gridId: gridId, fieldCache: cache), (gridId, cache) => GridPropertyBloc(gridId: gridId, fieldController: cache),
); );
} }

View File

@ -89,6 +89,12 @@ class AFBoardDataController extends ChangeNotifier
if (columnIds.isNotEmpty && notify) notifyListeners(); if (columnIds.isNotEmpty && notify) notifyListeners();
} }
void clear() {
_columnDatas.clear();
_columnControllers.clear();
notifyListeners();
}
AFBoardColumnDataController? getColumnController(String columnId) { AFBoardColumnDataController? getColumnController(String columnId) {
final columnController = _columnControllers[columnId]; final columnController = _columnControllers[columnId];
if (columnController == null) { if (columnController == null) {

View File

@ -1,3 +1,8 @@
## 0.0.4
* Support more shortcut events.
* Fix some bugs.
* Update the documentation.
## 0.0.3 ## 0.0.3
* Support insert image. * Support insert image.
* Support insert link. * Support insert link.

View File

@ -20,27 +20,39 @@ and the Flutter guide for
<a href="https://twitter.com/appflowy"><b>Twitter</b></a> <a href="https://twitter.com/appflowy"><b>Twitter</b></a>
</p> </p>
<p align="center">
<a href="https://codecov.io/gh/AppFlowy-IO/AppFlowy" >
<img src="https://codecov.io/gh/AppFlowy-IO/AppFlowy/branch/main/graph/badge.svg?token=YTFKUF70B6"/>
</a>
</p>
<div align="center"> <div align="center">
<img src="https://i.ibb.co/HNnc1jP/appflowy-editor-example.gif" width = "900"/> <img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/images/appflowy-editor-example.gif?raw=true" width = "700" style = "padding: 100"/>
</div> </div>
## Key Features ## Key Features
* Allow you to build rich, intuitive editors * Build rich, intuitive editors
* Design and modify it your way by customizing components, shortcut events, and many more coming soon including menu options and themes * Design and modify an ever expanding list of customizable features including
* [Test-covered](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/testing.md) and maintained by AppFlowy's core team along with a community of more than 1,000 builders * components (such as form input controls, numbered lists, and rich text widgets)
* shortcut events
* menu options (**coming soon!**)
* themes (**coming soon!**)
* [Test-coverage](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/testing.md) and on-going maintenance by AppFlowy's core team and community of more than 1,000 builders
## Getting Started
## Getting started Add the AppFlowy editor [Flutter package](https://docs.flutter.dev/development/packages-and-plugins/using-packages) to your environment.
```shell ```shell
flutter pub add appflowy_editor flutter pub add appflowy_editor
flutter pub get flutter pub get
``` ```
## How to use ## Creating Your First Editor
Start by creating a new empty AppFlowyEditor object.
Let's create a new AppFlowyEditor object
```dart ```dart
final editorState = EditorState.empty(); // an empty state final editorState = EditorState.empty(); // an empty state
final editor = AppFlowyEditor( final editor = AppFlowyEditor(
@ -50,7 +62,8 @@ final editor = AppFlowyEditor(
); );
``` ```
You can also create an editor from a JSON file You can also create an editor from a JSON object in order to configure your initial state.
```dart ```dart
final json = ...; final json = ...;
final editorState = EditorState(StateTree.fromJson(data)); final editorState = EditorState(StateTree.fromJson(data));
@ -61,37 +74,43 @@ final editor = AppFlowyEditor(
); );
``` ```
To get a sense for how you might use it, run this example: To get a sense for how the AppFlowy Editor works, run our example:
```shell ```shell
git clone https://github.com/AppFlowy-IO/AppFlowy.git git clone https://github.com/AppFlowy-IO/AppFlowy.git
cd frontend/app_flowy/packages/appflowy_editor/example cd frontend/app_flowy/packages/appflowy_editor/example
flutter run flutter run
``` ```
## Customizing Your Editor
## How to customize ### Customizing Components
### Customize a component
Please refer to [customizing a component](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-component) for more details.
Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing components](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-component).
### Customize a shortcut event Below are some examples of component customizations:
Please refer to [customizing a shortcut event](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-shortcut-event) for more details.
## More Examples * [Checkbox Text](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart) demonstrates how to extend new styles based on existing rich text components
* Customize a component * [Image](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) demonstrates how to extend a new node and render it
* [Checkbox Text](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart) shows you how to extend new styles based on existing rich text components * See further examples of [rich-text plugins](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text)
* [Image](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) teaches you how to extend a new node and render it
* And more examples on [rich-text plugins](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text) ### Customizing Shortcut Events
* Customize a shortcut event
* [BIUS](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart) shows you how to make text bold/italic/underline/strikethrough through shortcut keys Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing shortcut events](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-shortcut-event).
* [Paste HTML](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart) gives you an idea on how to handle pasted styles through shortcut keys
* Need more examples? Check out [Internal key event handlers](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers) Below are some examples of shortcut event customizations:
* [BIUS](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart) demonstrates how to make text bold/italic/underline/strikethrough through shortcut keys
* [Paste HTML](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart) gives you an idea on how to handle pasted styles through shortcut keys
* Need more examples? Check out [Internal key event handlers](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers)
## Glossary ## Glossary
Please refer to the API documentation. Please refer to the API documentation.
## Contributing ## Contributing
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details. Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.
Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
## License ## License
Distributed under the AGPLv3 License. See LICENSE for more information. Distributed under the AGPLv3 License. See [LICENSE](https://github.com/AppFlowy-IO/AppFlowy-Docs/blob/main/LICENSE) for more information.

View File

@ -1,12 +1,12 @@
# How to customize ... # Customizing Editor Features
## Customize a shortcut event ## Customizing a Shortcut Event
We will use a simple example to illustrate how to quickly add a shortcut event. We will use a simple example to illustrate how to quickly add a shortcut event.
For example, typing `_xxx_` will be converted into _xxx_. In this example, text that starts and ends with an underscore ( \_ ) character will be rendered in italics for emphasis. So typing `_xxx_` will automatically be converted into _xxx_.
Let's start with a blank document. Let's start with a blank document:
```dart ```dart
@override @override
@ -27,7 +27,7 @@ At this point, nothing magic will happen after typing `_xxx_`.
![Before](./images/customizing_a_shortcut_event_before.gif) ![Before](./images/customizing_a_shortcut_event_before.gif)
Next, we will create a function to handle an underscore input. To implement our shortcut event we will create a function to handle an underscore input.
```dart ```dart
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
@ -35,23 +35,25 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) { FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
// Since we only need to handler the input of `underscore`. // Since we only need to handle the input of an 'underscore' character,
// All inputs except `underscore` will be ignored directly. // all inputs except `underscore` will be ignored immediately.
if (event.logicalKey != LogicalKeyboardKey.underscore) { if (event.logicalKey != LogicalKeyboardKey.underscore) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
}; };
``` ```
Then, we need to determine if the currently selected node is `TextNode` and the selection is collapsed. Then, we need to determine if the currently selected node is a `TextNode` and if the selection is collapsed.
If so, we will continue.
```dart ```dart
// ... // ...
FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) { FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
// ... // ...
// Obtaining the selection and selected nodes of the current document through `selectionService`. // Obtain the selection and selected nodes of the current document through the 'selectionService'
// And determine whether the selection is collapsed and whether the selected node is a text node. // to determine whether the selection is collapsed and whether the selected node is a text node.
final selectionService = editorState.service.selectionService; final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value; final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>(); final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
@ -60,11 +62,11 @@ FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
} }
``` ```
Now, we start dealing with underscore. Now, we deal with handling the underscore.
Look for the position of the previous underscore and Look for the position of the previous underscore and
1. return, if not found. 1. if one is _not_ found, return without doing anything.
2. if found, the text wrapped in between two underscores will be displayed in italic. 2. if one is found, the text enclosed within the two underscores will be formatted to display in italics.
```dart ```dart
// ... // ...
@ -73,14 +75,14 @@ FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
final textNode = textNodes.first; final textNode = textNodes.first;
final text = textNode.toRawString(); final text = textNode.toRawString();
// Determine if `underscore` already exists in the text node // Determine if an 'underscore' already exists in the text node
final previousUnderscore = text.indexOf('_'); final previousUnderscore = text.indexOf('_');
if (previousUnderscore == -1) { if (previousUnderscore == -1) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
// Delete the previous `underscore`, // Delete the previous 'underscore',
// update the style of the text surrounded by two underscores to `italic`, // update the style of the text surrounded by the two underscores to 'italic',
// and update the cursor position. // and update the cursor position.
TransactionBuilder(editorState) TransactionBuilder(editorState)
..deleteText(textNode, previousUnderscore, 1) ..deleteText(textNode, previousUnderscore, 1)
@ -99,7 +101,7 @@ FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
}; };
``` ```
So far, the 'underscore handler' function is done and the only task left is to inject it into the AppFlowyEditor. Now our 'underscore handler' function is done and the only task left is to inject it into the AppFlowyEditor.
```dart ```dart
@override @override
@ -120,14 +122,15 @@ Widget build(BuildContext context) {
![After](./images/customizing_a_shortcut_event_after.gif) ![After](./images/customizing_a_shortcut_event_after.gif)
[Complete code example]() Check out the [complete code](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart) file of this example.
## Customize a component
We will use a simple example to showcase how to quickly add a custom component.
For example, we want to render an image from the network. ## Customizing a Component
We will use a simple example to show how to quickly add a custom component.
To start with, let's create an empty document by running commands as follows: In this example we will render an image from the network.
Let's start with a blank document:
```dart ```dart
@override @override
@ -144,9 +147,9 @@ Widget build(BuildContext context) {
} }
``` ```
Next, we choose a unique string for your custom node's type. We use `network_image` in this case. And we add `network_image_src` to the `attributes` to describe the link of the image. Next, we will choose a unique string for your custom node's type.
> For the definition of the [Node](), please refer to this [link](). We'll use `network_image` in this case. And we add `network_image_src` to the `attributes` to describe the link of the image.
```JSON ```JSON
{ {
@ -157,9 +160,9 @@ Next, we choose a unique string for your custom node's type. We use `network_ima
} }
``` ```
Then, we create a class that inherits [NodeWidgetBuilder](). As shown in the autoprompt, we need to implement two functions: Then, we create a class that inherits [NodeWidgetBuilder](../lib/src/service/render_plugin_service.dart). As shown in the autoprompt, we need to implement two functions:
1. one returns a widget 1. one returns a widget
2. the other verifies the correctness of the [Node](). 2. the other verifies the correctness of the [Node](../lib/src/document/node.dart).
```dart ```dart
@ -179,9 +182,7 @@ class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
Now, let's implement a simple image widget based on `Image`. Now, let's implement a simple image widget based on `Image`.
**It is important to note that the `State` of the returned `Widget` must be with [Selectable]().** Note that the `State` object that is returned by the `Widget` must implement [Selectable](../lib/src/render/selection/selectable.dart) using the `with` keyword.
> For the definition of the [Selectable](), please refer to this [link]().
```dart ```dart
class _NetworkImageNodeWidget extends StatefulWidget { class _NetworkImageNodeWidget extends StatefulWidget {
@ -236,7 +237,7 @@ class __NetworkImageNodeWidgetState extends State<_NetworkImageNodeWidget>
} }
``` ```
Finally, we return `_NetworkImageNodeWidget` in the `build` function of `NetworkImageNodeWidgetBuilder` and register `NetworkImageNodeWidgetBuilder` into `AppFlowyEditor`. Finally, we return `_NetworkImageNodeWidget` in the `build` function of `NetworkImageNodeWidgetBuilder`...
```dart ```dart
class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder { class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
@ -256,6 +257,8 @@ class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
} }
``` ```
... and register `NetworkImageNodeWidgetBuilder` in the `AppFlowyEditor`.
```dart ```dart
final editorState = EditorState( final editorState = EditorState(
document: StateTree.empty() document: StateTree.empty()
@ -281,6 +284,6 @@ return AppFlowyEditor(
); );
``` ```
![](./images/customizing_a_component.gif) ![Whew!](./images/customizing_a_component.gif)
[Here you can check out the complete code file of this example]() Check out the [complete code](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) file of this example.

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 KiB

View File

@ -1,24 +1,33 @@
# Testing # Testing
> The directory structure of test files is consistent with the code files, making it easy for us to map a file with the corresponding test and check if the test is updated The directory structure of test files mirrors that of the code files, making it easy for us to map a file with the corresponding test and check if the test is updated.
## Testing Functions For an overview of testing best practices in Flutter applications, please refer to Flutter's [introduction to widget testing](https://docs.flutter.dev/cookbook/testing/widget/introduction) as well as their [introduction to unit testing](https://docs.flutter.dev/cookbook/testing/unit/introduction).
There you will learn how to do such things as such as simulate a click as well as leverage the `test` and `expect` functions.
## Testing Basic Editor Functions
The example code below shows how to construct a document that will be used in our testing.
**Construct a document for testing**
```dart ```dart
const text = 'Welcome to Appflowy 😁'; const text = 'Welcome to Appflowy 😁';
// Get the instance of editor. // Get the instance of the editor.
final editor = tester.editor; final editor = tester.editor;
// Insert empty text node.
// Insert an empty text node.
editor.insertEmptyTextNode(); editor.insertEmptyTextNode();
// Insert text node with string.
// Insert a text node with the text string we defined earlier.
editor.insertTextNode(text); editor.insertTextNode(text);
// Insert text node with heading style.
// Insert the same text, but with the heading style.
editor.insertTextNode(text, attributes: { editor.insertTextNode(text, attributes: {
StyleKey.subtype: StyleKey.heading, StyleKey.subtype: StyleKey.heading,
StyleKey.heading: StyleKey.h1, StyleKey.heading: StyleKey.h1,
}); });
// Insert text node with bulleted list style and bold style.
// Insert our text with the bulleted list style and the bold style.
// If you want to modify the style of the inserted text, you need to use the Delta parameter.
editor.insertTextNode( editor.insertTextNode(
'', '',
attributes: { attributes: {
@ -30,66 +39,76 @@ editor.insertTextNode(
); );
``` ```
**The `startTesting` function must be called before testing**. The `startTesting` function of the editor must be called before you begin your test.
```dart ```dart
await editor.startTesting(); await editor.startTesting();
``` ```
**Get the number of nodes in the document** Get the number of nodes in the document.
```dart ```dart
final length = editor.documentLength; final length = editor.documentLength;
print(length); print(length);
``` ```
**Get the node of a defined path** Get the node of a defined path. In this case we are getting the first node of the document which is the text "Welcome to Appflowy 😁".
```dart ```dart
final firstTextNode = editor.nodeAtPath([0]) as TextNode; final firstTextNode = editor.nodeAtPath([0]) as TextNode;
``` ```
**Update selection** Update the [Selection](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart) so that our text "Welcome to Appflowy 😁" is selected. We will start our selection from the beginning of the string.
```dart ```dart
await editor.updateSelection( await editor.updateSelection(
Selection.single(path: firstTextNode.path, startOffset: 0), Selection.single(path: firstTextNode.path, startOffset: 0),
); );
``` ```
**Get the selection** Get the current selection.
```dart ```dart
final selection = editor.documentSelection; final selection = editor.documentSelection;
print(selection); print(selection);
``` ```
**Simulate shortcut event inputs** Next we will simulate the input of a shortcut key being pressed that will select all the text.
```dart ```dart
// Command + A. // Meta + A.
await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true); await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true);
// Command + shift + S. // Meta + shift + S.
await editor.pressLogicKey( await editor.pressLogicKey(
LogicalKeyboardKey.keyS, LogicalKeyboardKey.keyS,
isMetaPressed: true, isMetaPressed: true,
isShiftPressed: true, isShiftPressed: true,
); );
``` ```
**Simulate a text input** We will then simulate text input.
```dart ```dart
// Insert 'Hello World' at the beginning of the first node. // Insert 'Hello World' at the beginning of the first node.
editor.insertText(firstTextNode, 'Hello World', 0); editor.insertText(firstTextNode, 'Hello World', 0);
``` ```
**Get information about the text node** Once the text has been added, we can get information about the text node.
```dart ```dart
// Get plain text. // Get the text of the first text node as plain text
final textAfterInserted = firstTextNode.toRawString(); final textAfterInserted = firstTextNode.toRawString();
print(textAfterInserted); print(textAfterInserted);
// Get attributes. // Get the attributes of the text node
final attributes = firstTextNode.attributes; final attributes = firstTextNode.attributes;
print(attributes); print(attributes);
``` ```
## Example ## A Complete Code Example
For example, we are going to test `select_all_handler.dart`
In the example code below we are going to test `select_all_handler.dart` by inserting 100 lines of text that read "Welcome to Appflowy 😁" and then simulating the "selectAll" shortcut key being pressed.
Afterwards, we will `expect` that the current selection of the editor is equal to the selection of all the lines that were generated.
```dart ```dart
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
@ -124,5 +143,3 @@ void main() async {
}); });
} }
``` ```
For more information about testing, such as simulating a click, please refer to [An introduction to widget testing](https://docs.flutter.dev/cookbook/testing/widget/introduction)

View File

@ -11,7 +11,7 @@ import 'package:flutter/material.dart';
/// 2. create a class extends [NodeWidgetBuilder] /// 2. create a class extends [NodeWidgetBuilder]
/// 3. override the function `Widget build(NodeWidgetContext<Node> context)` /// 3. override the function `Widget build(NodeWidgetContext<Node> context)`
/// and return a widget to render. The returned widget should be /// and return a widget to render. The returned widget should be
/// a StatefulWidget and mixin with [Selectable]. /// a StatefulWidget and mixin with [SelectableMixin].
/// ///
/// 4. override the getter `nodeValidator` /// 4. override the getter `nodeValidator`
/// to verify the data structure in [Node]. /// to verify the data structure in [Node].
@ -50,7 +50,8 @@ class ImageNodeWidget extends StatefulWidget {
State<ImageNodeWidget> createState() => _ImageNodeWidgetState(); State<ImageNodeWidget> createState() => _ImageNodeWidgetState();
} }
class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable { class _ImageNodeWidgetState extends State<ImageNodeWidget>
with SelectableMixin {
bool isHovered = false; bool isHovered = false;
Node get node => widget.node; Node get node => widget.node;
EditorState get editorState => widget.editorState; EditorState get editorState => widget.editorState;

View File

@ -31,7 +31,7 @@ class _NetworkImageNodeWidget extends StatefulWidget {
} }
class __NetworkImageNodeWidgetState extends State<_NetworkImageNodeWidget> class __NetworkImageNodeWidgetState extends State<_NetworkImageNodeWidget>
with Selectable { with SelectableMixin {
RenderBox get _renderBox => context.findRenderObject() as RenderBox; RenderBox get _renderBox => context.findRenderObject() as RenderBox;
@override @override

View File

@ -33,7 +33,7 @@ class LinkNodeWidget extends StatefulWidget {
} }
class _YouTubeLinkNodeWidgetState extends State<LinkNodeWidget> class _YouTubeLinkNodeWidgetState extends State<LinkNodeWidget>
with Selectable { with SelectableMixin {
Node get node => widget.node; Node get node => widget.node;
EditorState get editorState => widget.editorState; EditorState get editorState => widget.editorState;
String get src => widget.node.attributes['youtube_link'] as String; String get src => widget.node.attributes['youtube_link'] as String;

View File

@ -20,5 +20,6 @@ export 'src/service/render_plugin_service.dart';
export 'src/service/service.dart'; export 'src/service/service.dart';
export 'src/service/selection_service.dart'; export 'src/service/selection_service.dart';
export 'src/service/scroll_service.dart'; export 'src/service/scroll_service.dart';
export 'src/service/toolbar_service.dart';
export 'src/service/keyboard_service.dart'; export 'src/service/keyboard_service.dart';
export 'src/service/input_service.dart'; export 'src/service/input_service.dart';

View File

@ -1,7 +1,6 @@
import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/node.dart';
import './state_tree.dart'; import './state_tree.dart';
import './node.dart';
/// [NodeIterator] is used to traverse the nodes in visual order. /// [NodeIterator] is used to traverse the nodes in visual order.
class NodeIterator implements Iterator<Node> { class NodeIterator implements Iterator<Node> {

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import './path.dart'; import './path.dart';
class Position { class Position {
@ -21,7 +19,7 @@ class Position {
@override @override
int get hashCode { int get hashCode {
final pathHash = hashList(path); final pathHash = Object.hashAll(path);
return Object.hash(pathHash, offset); return Object.hash(pathHash, offset);
} }

View File

@ -3,8 +3,6 @@ import 'dart:math';
import 'package:appflowy_editor/src/document/attributes.dart'; import 'package:appflowy_editor/src/document/attributes.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import './attributes.dart';
// constant number: 2^53 - 1 // constant number: 2^53 - 1
const int _maxInt = 9007199254740991; const int _maxInt = 9007199254740991;
@ -463,7 +461,7 @@ class Delta extends Iterable<TextOperation> {
@override @override
int get hashCode { int get hashCode {
return hashList(_operations); return Object.hashAll(_operations);
} }
/// Returned an inverted delta that has the opposite effect of against a base document delta. /// Returned an inverted delta that has the opposite effect of against a base document delta.

View File

@ -10,7 +10,8 @@ extension NodeExtensions on Node {
key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>(); key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
BuildContext? get context => key?.currentContext; BuildContext? get context => key?.currentContext;
Selectable? get selectable => key?.currentState?.unwrapOrNull<Selectable>(); SelectableMixin? get selectable =>
key?.currentState?.unwrapOrNull<SelectableMixin>();
bool inSelection(Selection selection) { bool inSelection(Selection selection) {
if (selection.start.path <= selection.end.path) { if (selection.start.path <= selection.end.path) {

View File

@ -32,7 +32,8 @@ class ImageNodeWidget extends StatefulWidget {
State<ImageNodeWidget> createState() => _ImageNodeWidgetState(); State<ImageNodeWidget> createState() => _ImageNodeWidgetState();
} }
class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable { class _ImageNodeWidgetState extends State<ImageNodeWidget>
with SelectableMixin {
final _imageKey = GlobalKey(); final _imageKey = GlobalKey();
double? _imageWidth; double? _imageWidth;

View File

@ -42,7 +42,7 @@ class BulletedListTextNodeWidget extends StatefulWidget {
// customize // customize
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget> class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
with Selectable, DefaultSelectable { with SelectableMixin, DefaultSelectable {
@override @override
final iconKey = GlobalKey(); final iconKey = GlobalKey();
@ -51,8 +51,8 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
final _iconRightPadding = 5.0; final _iconRightPadding = 5.0;
@override @override
Selectable<StatefulWidget> get forward => SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as Selectable; _richTextKey.currentState as SelectableMixin;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -40,7 +40,7 @@ class CheckboxNodeWidget extends StatefulWidget {
} }
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget> class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
with Selectable, DefaultSelectable { with SelectableMixin, DefaultSelectable {
@override @override
final iconKey = GlobalKey(); final iconKey = GlobalKey();
@ -49,8 +49,8 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
final _iconRightPadding = 5.0; final _iconRightPadding = 5.0;
@override @override
Selectable<StatefulWidget> get forward => SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as Selectable; _richTextKey.currentState as SelectableMixin;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -4,7 +4,7 @@ import 'package:appflowy_editor/src/render/selection/selectable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
mixin DefaultSelectable { mixin DefaultSelectable {
Selectable get forward; SelectableMixin get forward;
GlobalKey? get iconKey; GlobalKey? get iconKey;

View File

@ -42,7 +42,7 @@ class FlowyRichText extends StatefulWidget {
State<FlowyRichText> createState() => _FlowyRichTextState(); State<FlowyRichText> createState() => _FlowyRichTextState();
} }
class _FlowyRichTextState extends State<FlowyRichText> with Selectable { class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
var _textKey = GlobalKey(); var _textKey = GlobalKey();
final _placeholderTextKey = GlobalKey(); final _placeholderTextKey = GlobalKey();

View File

@ -39,7 +39,7 @@ class HeadingTextNodeWidget extends StatefulWidget {
// customize // customize
class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget> class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
with Selectable, DefaultSelectable { with SelectableMixin, DefaultSelectable {
@override @override
GlobalKey? get iconKey => null; GlobalKey? get iconKey => null;
@ -47,8 +47,8 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
final _topPadding = 5.0; final _topPadding = 5.0;
@override @override
Selectable<StatefulWidget> get forward => SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as Selectable; _richTextKey.currentState as SelectableMixin;
@override @override
Offset get baseOffset { Offset get baseOffset {

View File

@ -42,7 +42,7 @@ class NumberListTextNodeWidget extends StatefulWidget {
// customize // customize
class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget> class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
with Selectable, DefaultSelectable { with SelectableMixin, DefaultSelectable {
@override @override
final iconKey = GlobalKey(); final iconKey = GlobalKey();
@ -51,8 +51,8 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
final _iconRightPadding = 5.0; final _iconRightPadding = 5.0;
@override @override
Selectable<StatefulWidget> get forward => SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as Selectable; _richTextKey.currentState as SelectableMixin;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -41,7 +41,7 @@ class QuotedTextNodeWidget extends StatefulWidget {
// customize // customize
class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget> class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
with Selectable, DefaultSelectable { with SelectableMixin, DefaultSelectable {
@override @override
final iconKey = GlobalKey(); final iconKey = GlobalKey();
@ -50,8 +50,8 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
final _iconRightPadding = 5.0; final _iconRightPadding = 5.0;
@override @override
Selectable<StatefulWidget> get forward => SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as Selectable; _richTextKey.currentState as SelectableMixin;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -40,15 +40,15 @@ class RichTextNodeWidget extends StatefulWidget {
// customize // customize
class _RichTextNodeWidgetState extends State<RichTextNodeWidget> class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
with Selectable, DefaultSelectable { with SelectableMixin, DefaultSelectable {
@override @override
GlobalKey? get iconKey => null; GlobalKey? get iconKey => null;
final _richTextKey = GlobalKey(debugLabel: 'rich_text'); final _richTextKey = GlobalKey(debugLabel: 'rich_text');
@override @override
Selectable<StatefulWidget> get forward => SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as Selectable; _richTextKey.currentState as SelectableMixin;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -2,12 +2,12 @@ import 'package:appflowy_editor/src/document/position.dart';
import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/document/selection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// [Selectable] is used for the editor to calculate the position /// [SelectableMixin] is used for the editor to calculate the position
/// and size of the selection. /// and size of the selection.
/// ///
/// The widget returned by NodeWidgetBuilder must be with [Selectable], /// The widget returned by NodeWidgetBuilder must be with [SelectableMixin],
/// otherwise the [AppFlowySelectionService] will not work properly. /// otherwise the [AppFlowySelectionService] will not work properly.
mixin Selectable<T extends StatefulWidget> on State<T> { mixin SelectableMixin<T extends StatefulWidget> on State<T> {
/// Returns the [Selection] surrounded by start and end /// Returns the [Selection] surrounded by start and end
/// in current widget. /// in current widget.
/// ///

View File

@ -510,7 +510,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
editorState.service.scrollService?.enable(); editorState.service.scrollService?.enable();
} }
Rect _transformRectToGlobal(Selectable selectable, Rect r) { Rect _transformRectToGlobal(SelectableMixin selectable, Rect r) {
final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top)); final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top));
return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height); return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height);
} }

View File

@ -1,8 +1,13 @@
name: appflowy_editor name: appflowy_editor
description: A highly customizable rich-text editor for Flutter description: A highly customizable rich-text editor for Flutter
version: 0.0.3 version: 0.0.4
homepage: https://github.com/AppFlowy-IO/AppFlowy homepage: https://github.com/AppFlowy-IO/AppFlowy
platforms:
linux:
macos:
windows:
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
flutter: ">=1.17.0" flutter: ">=1.17.0"

View File

@ -1,7 +1,6 @@
import 'package:appflowy_editor/src/document/path.dart'; import 'package:appflowy_editor/src/document/path.dart';
import 'package:appflowy_editor/src/document/position.dart'; import 'package:appflowy_editor/src/document/position.dart';
import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/document/selection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
@ -75,7 +74,7 @@ void main() {
final path2 = <int>[1]; final path2 = <int>[1];
expect(pathEquals(path1, path2), true); expect(pathEquals(path1, path2), true);
expect(hashList(path1), hashList(path2)); expect(Object.hashAll(path1), Object.hashAll(path2));
}); });
test('test path utils 2', () { test('test path utils 2', () {
@ -83,7 +82,7 @@ void main() {
final path2 = <int>[2]; final path2 = <int>[2];
expect(pathEquals(path1, path2), false); expect(pathEquals(path1, path2), false);
expect(hashList(path1) != hashList(path2), true); expect(Object.hashAll(path1) != Object.hashAll(path2), true);
}); });
test('test position comparator', () { test('test position comparator', () {

View File

@ -35,7 +35,7 @@ packages:
path: "packages/appflowy_editor" path: "packages/appflowy_editor"
relative: true relative: true
source: path source: path
version: "0.0.3" version: "0.0.4"
appflowy_popover: appflowy_popover:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -13,6 +13,8 @@ pub enum GridNotification {
DidUpdateField = 50, DidUpdateField = 50,
DidUpdateGroupView = 60, DidUpdateGroupView = 60,
DidUpdateGroup = 61, DidUpdateGroup = 61,
DidGroupByNewField = 62,
DidUpdateGridSetting = 70,
} }
impl std::default::Default for GridNotification { impl std::default::Default for GridNotification {

View File

@ -10,33 +10,33 @@ use std::convert::TryInto;
use std::sync::Arc; use std::sync::Arc;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct GridFilterConfiguration { pub struct GridFilterConfigurationPB {
#[pb(index = 1)] #[pb(index = 1)]
pub id: String, pub id: String,
} }
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct RepeatedGridConfigurationFilterPB { pub struct RepeatedGridFilterConfigurationPB {
#[pb(index = 1)] #[pb(index = 1)]
pub items: Vec<GridFilterConfiguration>, pub items: Vec<GridFilterConfigurationPB>,
} }
impl std::convert::From<&FilterConfigurationRevision> for GridFilterConfiguration { impl std::convert::From<&FilterConfigurationRevision> for GridFilterConfigurationPB {
fn from(rev: &FilterConfigurationRevision) -> Self { fn from(rev: &FilterConfigurationRevision) -> Self {
Self { id: rev.id.clone() } Self { id: rev.id.clone() }
} }
} }
impl std::convert::From<Vec<Arc<FilterConfigurationRevision>>> for RepeatedGridConfigurationFilterPB { impl std::convert::From<Vec<Arc<FilterConfigurationRevision>>> for RepeatedGridFilterConfigurationPB {
fn from(revs: Vec<Arc<FilterConfigurationRevision>>) -> Self { fn from(revs: Vec<Arc<FilterConfigurationRevision>>) -> Self {
RepeatedGridConfigurationFilterPB { RepeatedGridFilterConfigurationPB {
items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(), items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(),
} }
} }
} }
impl std::convert::From<Vec<GridFilterConfiguration>> for RepeatedGridConfigurationFilterPB { impl std::convert::From<Vec<GridFilterConfigurationPB>> for RepeatedGridFilterConfigurationPB {
fn from(items: Vec<GridFilterConfiguration>) -> Self { fn from(items: Vec<GridFilterConfigurationPB>) -> Self {
Self { items } Self { items }
} }
} }
@ -78,7 +78,7 @@ pub struct DeleteFilterParams {
} }
#[derive(ProtoBuf, Debug, Default, Clone)] #[derive(ProtoBuf, Debug, Default, Clone)]
pub struct CreateGridFilterPayloadPB { pub struct InsertFilterPayloadPB {
#[pb(index = 1)] #[pb(index = 1)]
pub field_id: String, pub field_id: String,
@ -92,7 +92,7 @@ pub struct CreateGridFilterPayloadPB {
pub content: Option<String>, pub content: Option<String>,
} }
impl CreateGridFilterPayloadPB { impl InsertFilterPayloadPB {
#[allow(dead_code)] #[allow(dead_code)]
pub fn new<T: Into<i32>>(field_rev: &FieldRevision, condition: T, content: Option<String>) -> Self { pub fn new<T: Into<i32>>(field_rev: &FieldRevision, condition: T, content: Option<String>) -> Self {
Self { Self {
@ -104,10 +104,10 @@ impl CreateGridFilterPayloadPB {
} }
} }
impl TryInto<CreateFilterParams> for CreateGridFilterPayloadPB { impl TryInto<InsertFilterParams> for InsertFilterPayloadPB {
type Error = ErrorCode; type Error = ErrorCode;
fn try_into(self) -> Result<CreateFilterParams, Self::Error> { fn try_into(self) -> Result<InsertFilterParams, Self::Error> {
let field_id = NotEmptyStr::parse(self.field_id) let field_id = NotEmptyStr::parse(self.field_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)? .map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0; .0;
@ -130,7 +130,7 @@ impl TryInto<CreateFilterParams> for CreateGridFilterPayloadPB {
} }
} }
Ok(CreateFilterParams { Ok(InsertFilterParams {
field_id, field_id,
field_type_rev: self.field_type.into(), field_type_rev: self.field_type.into(),
condition, condition,
@ -139,7 +139,7 @@ impl TryInto<CreateFilterParams> for CreateGridFilterPayloadPB {
} }
} }
pub struct CreateFilterParams { pub struct InsertFilterParams {
pub field_id: String, pub field_id: String,
pub field_type_rev: FieldTypeRevision, pub field_type_rev: FieldTypeRevision,
pub condition: u8, pub condition: u8,

View File

@ -91,6 +91,9 @@ pub struct GroupPB {
#[pb(index = 5)] #[pb(index = 5)]
pub is_default: bool, pub is_default: bool,
#[pb(index = 6)]
pub is_visible: bool,
} }
impl std::convert::From<Group> for GroupPB { impl std::convert::From<Group> for GroupPB {
@ -101,6 +104,7 @@ impl std::convert::From<Group> for GroupPB {
desc: group.name, desc: group.name,
rows: group.rows, rows: group.rows,
is_default: group.is_default, is_default: group.is_default,
is_visible: group.is_visible,
} }
} }
} }
@ -126,7 +130,7 @@ impl std::convert::From<Vec<Arc<GroupConfigurationRevision>>> for RepeatedGridGr
} }
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct CreateGridGroupPayloadPB { pub struct InsertGroupPayloadPB {
#[pb(index = 1)] #[pb(index = 1)]
pub field_id: String, pub field_id: String,
@ -134,22 +138,22 @@ pub struct CreateGridGroupPayloadPB {
pub field_type: FieldType, pub field_type: FieldType,
} }
impl TryInto<CreatGroupParams> for CreateGridGroupPayloadPB { impl TryInto<InsertGroupParams> for InsertGroupPayloadPB {
type Error = ErrorCode; type Error = ErrorCode;
fn try_into(self) -> Result<CreatGroupParams, Self::Error> { fn try_into(self) -> Result<InsertGroupParams, Self::Error> {
let field_id = NotEmptyStr::parse(self.field_id) let field_id = NotEmptyStr::parse(self.field_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)? .map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0; .0;
Ok(CreatGroupParams { Ok(InsertGroupParams {
field_id, field_id,
field_type_rev: self.field_type.into(), field_type_rev: self.field_type.into(),
}) })
} }
} }
pub struct CreatGroupParams { pub struct InsertGroupParams {
pub field_id: String, pub field_id: String,
pub field_type_rev: FieldTypeRevision, pub field_type_rev: FieldTypeRevision,
} }

View File

@ -134,15 +134,21 @@ pub struct GroupViewChangesetPB {
pub inserted_groups: Vec<InsertedGroupPB>, pub inserted_groups: Vec<InsertedGroupPB>,
#[pb(index = 3)] #[pb(index = 3)]
pub deleted_groups: Vec<String>, pub new_groups: Vec<GroupPB>,
#[pb(index = 4)] #[pb(index = 4)]
pub deleted_groups: Vec<String>,
#[pb(index = 5)]
pub update_groups: Vec<GroupPB>, pub update_groups: Vec<GroupPB>,
} }
impl GroupViewChangesetPB { impl GroupViewChangesetPB {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.inserted_groups.is_empty() && self.deleted_groups.is_empty() && self.update_groups.is_empty() self.new_groups.is_empty()
&& self.inserted_groups.is_empty()
&& self.deleted_groups.is_empty()
&& self.update_groups.is_empty()
} }
} }

View File

@ -1,13 +1,12 @@
use crate::entities::{ use crate::entities::{
CreatGroupParams, CreateFilterParams, CreateGridFilterPayloadPB, CreateGridGroupPayloadPB, DeleteFilterParams, DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams, DeleteGroupPayloadPB, InsertFilterParams,
DeleteFilterPayloadPB, DeleteGroupParams, DeleteGroupPayloadPB, RepeatedGridConfigurationFilterPB, InsertFilterPayloadPB, InsertGroupParams, InsertGroupPayloadPB, RepeatedGridFilterConfigurationPB,
RepeatedGridGroupConfigurationPB, RepeatedGridGroupConfigurationPB,
}; };
use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode; use flowy_error::ErrorCode;
use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::parser::NotEmptyStr;
use flowy_grid_data_model::revision::LayoutRevision; use flowy_grid_data_model::revision::LayoutRevision;
use std::collections::HashMap;
use std::convert::TryInto; use std::convert::TryInto;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use strum_macros::EnumIter; use strum_macros::EnumIter;
@ -19,13 +18,13 @@ pub struct GridSettingPB {
pub layouts: Vec<GridLayoutPB>, pub layouts: Vec<GridLayoutPB>,
#[pb(index = 2)] #[pb(index = 2)]
pub current_layout_type: GridLayout, pub layout_type: GridLayout,
#[pb(index = 3)] #[pb(index = 3)]
pub filter_configuration_by_field_id: HashMap<String, RepeatedGridConfigurationFilterPB>, pub filter_configurations: RepeatedGridFilterConfigurationPB,
#[pb(index = 4)] #[pb(index = 4)]
pub group_configuration_by_field_id: HashMap<String, RepeatedGridGroupConfigurationPB>, pub group_configurations: RepeatedGridGroupConfigurationPB,
} }
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
@ -85,13 +84,13 @@ pub struct GridSettingChangesetPayloadPB {
pub layout_type: GridLayout, pub layout_type: GridLayout,
#[pb(index = 3, one_of)] #[pb(index = 3, one_of)]
pub insert_filter: Option<CreateGridFilterPayloadPB>, pub insert_filter: Option<InsertFilterPayloadPB>,
#[pb(index = 4, one_of)] #[pb(index = 4, one_of)]
pub delete_filter: Option<DeleteFilterPayloadPB>, pub delete_filter: Option<DeleteFilterPayloadPB>,
#[pb(index = 5, one_of)] #[pb(index = 5, one_of)]
pub insert_group: Option<CreateGridGroupPayloadPB>, pub insert_group: Option<InsertGroupPayloadPB>,
#[pb(index = 6, one_of)] #[pb(index = 6, one_of)]
pub delete_group: Option<DeleteGroupPayloadPB>, pub delete_group: Option<DeleteGroupPayloadPB>,
@ -102,7 +101,7 @@ impl TryInto<GridSettingChangesetParams> for GridSettingChangesetPayloadPB {
fn try_into(self) -> Result<GridSettingChangesetParams, Self::Error> { fn try_into(self) -> Result<GridSettingChangesetParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.grid_id) let view_id = NotEmptyStr::parse(self.grid_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)? .map_err(|_| ErrorCode::ViewIdInvalid)?
.0; .0;
let insert_filter = match self.insert_filter { let insert_filter = match self.insert_filter {
@ -139,9 +138,9 @@ impl TryInto<GridSettingChangesetParams> for GridSettingChangesetPayloadPB {
pub struct GridSettingChangesetParams { pub struct GridSettingChangesetParams {
pub grid_id: String, pub grid_id: String,
pub layout_type: LayoutRevision, pub layout_type: LayoutRevision,
pub insert_filter: Option<CreateFilterParams>, pub insert_filter: Option<InsertFilterParams>,
pub delete_filter: Option<DeleteFilterParams>, pub delete_filter: Option<DeleteFilterParams>,
pub insert_group: Option<CreatGroupParams>, pub insert_group: Option<InsertGroupParams>,
pub delete_group: Option<DeleteGroupParams>, pub delete_group: Option<DeleteGroupParams>,
} }

View File

@ -35,6 +35,32 @@ pub(crate) async fn get_grid_setting_handler(
data_result(grid_setting) data_result(grid_setting)
} }
#[tracing::instrument(level = "trace", skip(data, manager), err)]
pub(crate) async fn update_grid_setting_handler(
data: Data<GridSettingChangesetPayloadPB>,
manager: AppData<Arc<GridManager>>,
) -> Result<(), FlowyError> {
let params: GridSettingChangesetParams = data.into_inner().try_into()?;
let editor = manager.get_grid_editor(&params.grid_id)?;
if let Some(insert_params) = params.insert_group {
let _ = editor.create_group(insert_params).await?;
}
if let Some(delete_params) = params.delete_group {
let _ = editor.delete_group(delete_params).await?;
}
if let Some(create_filter) = params.insert_filter {
let _ = editor.create_filter(create_filter).await?;
}
if let Some(delete_filter) = params.delete_filter {
let _ = editor.delete_filter(delete_filter).await?;
}
Ok(())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)] #[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn get_grid_blocks_handler( pub(crate) async fn get_grid_blocks_handler(
data: Data<QueryBlocksPayloadPB>, data: Data<QueryBlocksPayloadPB>,
@ -203,12 +229,14 @@ pub(crate) async fn move_field_handler(
/// The FieldMeta contains multiple data, each of them belongs to a specific FieldType. /// The FieldMeta contains multiple data, each of them belongs to a specific FieldType.
async fn get_type_option_data(field_rev: &FieldRevision, field_type: &FieldType) -> FlowyResult<Vec<u8>> { async fn get_type_option_data(field_rev: &FieldRevision, field_type: &FieldType) -> FlowyResult<Vec<u8>> {
let s = field_rev let s = field_rev.get_type_option_str(field_type).unwrap_or_else(|| {
.get_type_option_str(field_type) default_type_option_builder_from_type(field_type)
.unwrap_or_else(|| default_type_option_builder_from_type(field_type).entry().json_str()); .data_format()
.json_str()
});
let field_type: FieldType = field_rev.ty.into(); let field_type: FieldType = field_rev.ty.into();
let builder = type_option_builder_from_json_str(&s, &field_type); let builder = type_option_builder_from_json_str(&s, &field_type);
let type_option_data = builder.entry().protobuf_bytes().to_vec(); let type_option_data = builder.data_format().protobuf_bytes().to_vec();
Ok(type_option_data) Ok(type_option_data)
} }
@ -337,7 +365,7 @@ pub(crate) async fn update_select_option_handler(
type_option.delete_option(option); type_option.delete_option(option);
} }
mut_field_rev.insert_type_option_entry(&*type_option); mut_field_rev.insert_type_option(&*type_option);
let _ = editor.replace_field(field_rev).await?; let _ = editor.replace_field(field_rev).await?;
if let Some(cell_content_changeset) = cell_content_changeset { if let Some(cell_content_changeset) = cell_content_changeset {

View File

@ -11,7 +11,7 @@ pub fn create(grid_manager: Arc<GridManager>) -> Module {
.event(GridEvent::GetGrid, get_grid_handler) .event(GridEvent::GetGrid, get_grid_handler)
.event(GridEvent::GetGridBlocks, get_grid_blocks_handler) .event(GridEvent::GetGridBlocks, get_grid_blocks_handler)
.event(GridEvent::GetGridSetting, get_grid_setting_handler) .event(GridEvent::GetGridSetting, get_grid_setting_handler)
// .event(GridEvent::UpdateGridSetting, update_grid_setting_handler) .event(GridEvent::UpdateGridSetting, update_grid_setting_handler)
// Field // Field
.event(GridEvent::GetFields, get_fields_handler) .event(GridEvent::GetFields, get_fields_handler)
.event(GridEvent::UpdateField, update_field_handler) .event(GridEvent::UpdateField, update_field_handler)
@ -75,8 +75,8 @@ pub enum GridEvent {
/// [UpdateGridSetting] event is used to update the grid's settings. /// [UpdateGridSetting] event is used to update the grid's settings.
/// ///
/// The event handler accepts [GridIdPB] and return errors if failed to modify the grid's settings. /// The event handler accepts [GridSettingChangesetPayloadPB] and return errors if failed to modify the grid's settings.
#[event(input = "GridIdPB", input = "GridSettingChangesetPayloadPB")] #[event(input = "GridSettingChangesetPayloadPB")]
UpdateGridSetting = 3, UpdateGridSetting = 3,
/// [GetFields] event is used to get the grid's settings. /// [GetFields] event is used to get the grid's settings.
@ -225,4 +225,7 @@ pub enum GridEvent {
#[event(input = "MoveGroupRowPayloadPB")] #[event(input = "MoveGroupRowPayloadPB")]
MoveGroupRow = 112, MoveGroupRow = 112,
#[event(input = "MoveGroupRowPayloadPB")]
GroupByField = 113,
} }

View File

@ -30,7 +30,7 @@ macro_rules! impl_type_option {
($target: ident, $field_type:expr) => { ($target: ident, $field_type:expr) => {
impl std::convert::From<&FieldRevision> for $target { impl std::convert::From<&FieldRevision> for $target {
fn from(field_rev: &FieldRevision) -> $target { fn from(field_rev: &FieldRevision) -> $target {
match field_rev.get_type_option_entry::<$target>($field_type.into()) { match field_rev.get_type_option::<$target>($field_type.into()) {
None => $target::default(), None => $target::default(),
Some(target) => target, Some(target) => target,
} }
@ -39,7 +39,7 @@ macro_rules! impl_type_option {
impl std::convert::From<&std::sync::Arc<FieldRevision>> for $target { impl std::convert::From<&std::sync::Arc<FieldRevision>> for $target {
fn from(field_rev: &std::sync::Arc<FieldRevision>) -> $target { fn from(field_rev: &std::sync::Arc<FieldRevision>) -> $target {
match field_rev.get_type_option_entry::<$target>($field_type.into()) { match field_rev.get_type_option::<$target>($field_type.into()) {
None => $target::default(), None => $target::default(),
Some(target) => target, Some(target) => target,
} }
@ -52,7 +52,7 @@ macro_rules! impl_type_option {
} }
} }
impl TypeOptionDataEntry for $target { impl TypeOptionDataFormat for $target {
fn json_str(&self) -> String { fn json_str(&self) -> String {
match serde_json::to_string(&self) { match serde_json::to_string(&self) {
Ok(s) => s, Ok(s) => s,

View File

@ -101,25 +101,25 @@ pub fn try_decode_cell_data(
let field_type: FieldTypeRevision = t_field_type.into(); let field_type: FieldTypeRevision = t_field_type.into();
let data = match t_field_type { let data = match t_field_type {
FieldType::RichText => field_rev FieldType::RichText => field_rev
.get_type_option_entry::<RichTextTypeOptionPB>(field_type)? .get_type_option::<RichTextTypeOptionPB>(field_type)?
.decode_cell_data(cell_data.into(), s_field_type, field_rev), .decode_cell_data(cell_data.into(), s_field_type, field_rev),
FieldType::Number => field_rev FieldType::Number => field_rev
.get_type_option_entry::<NumberTypeOptionPB>(field_type)? .get_type_option::<NumberTypeOptionPB>(field_type)?
.decode_cell_data(cell_data.into(), s_field_type, field_rev), .decode_cell_data(cell_data.into(), s_field_type, field_rev),
FieldType::DateTime => field_rev FieldType::DateTime => field_rev
.get_type_option_entry::<DateTypeOptionPB>(field_type)? .get_type_option::<DateTypeOptionPB>(field_type)?
.decode_cell_data(cell_data.into(), s_field_type, field_rev), .decode_cell_data(cell_data.into(), s_field_type, field_rev),
FieldType::SingleSelect => field_rev FieldType::SingleSelect => field_rev
.get_type_option_entry::<SingleSelectTypeOptionPB>(field_type)? .get_type_option::<SingleSelectTypeOptionPB>(field_type)?
.decode_cell_data(cell_data.into(), s_field_type, field_rev), .decode_cell_data(cell_data.into(), s_field_type, field_rev),
FieldType::MultiSelect => field_rev FieldType::MultiSelect => field_rev
.get_type_option_entry::<MultiSelectTypeOptionPB>(field_type)? .get_type_option::<MultiSelectTypeOptionPB>(field_type)?
.decode_cell_data(cell_data.into(), s_field_type, field_rev), .decode_cell_data(cell_data.into(), s_field_type, field_rev),
FieldType::Checkbox => field_rev FieldType::Checkbox => field_rev
.get_type_option_entry::<CheckboxTypeOptionPB>(field_type)? .get_type_option::<CheckboxTypeOptionPB>(field_type)?
.decode_cell_data(cell_data.into(), s_field_type, field_rev), .decode_cell_data(cell_data.into(), s_field_type, field_rev),
FieldType::URL => field_rev FieldType::URL => field_rev
.get_type_option_entry::<URLTypeOptionPB>(field_type)? .get_type_option::<URLTypeOptionPB>(field_type)?
.decode_cell_data(cell_data.into(), s_field_type, field_rev), .decode_cell_data(cell_data.into(), s_field_type, field_rev),
}; };
Some(data) Some(data)

View File

@ -1,7 +1,7 @@
use crate::entities::{FieldPB, FieldType}; use crate::entities::{FieldPB, FieldType};
use crate::services::field::type_options::*; use crate::services::field::type_options::*;
use bytes::Bytes; use bytes::Bytes;
use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry}; use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataFormat};
use indexmap::IndexMap; use indexmap::IndexMap;
pub struct FieldBuilder { pub struct FieldBuilder {
@ -78,14 +78,14 @@ impl FieldBuilder {
pub fn build(self) -> FieldRevision { pub fn build(self) -> FieldRevision {
let mut field_rev = self.field_rev; let mut field_rev = self.field_rev;
field_rev.insert_type_option_entry(self.type_option_builder.entry()); field_rev.insert_type_option(self.type_option_builder.data_format());
field_rev field_rev
} }
} }
pub trait TypeOptionBuilder { pub trait TypeOptionBuilder {
fn field_type(&self) -> FieldType; fn field_type(&self) -> FieldType;
fn entry(&self) -> &dyn TypeOptionDataEntry; fn data_format(&self) -> &dyn TypeOptionDataFormat;
} }
pub fn default_type_option_builder_from_type(field_type: &FieldType) -> Box<dyn TypeOptionBuilder> { pub fn default_type_option_builder_from_type(field_type: &FieldType) -> Box<dyn TypeOptionBuilder> {

View File

@ -0,0 +1,45 @@
use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB};
use crate::services::grid_editor::GridRevisionEditor;
use flowy_error::FlowyResult;
use flowy_grid_data_model::revision::{TypeOptionDataDeserializer, TypeOptionDataFormat};
use std::sync::Arc;
pub async fn edit_field_type_option<T>(
field_id: &str,
editor: Arc<GridRevisionEditor>,
action: impl FnOnce(&mut T),
) -> FlowyResult<()>
where
T: TypeOptionDataDeserializer + TypeOptionDataFormat,
{
let get_type_option = async {
let field_rev = editor.get_field_rev(field_id).await?;
field_rev.get_type_option::<T>(field_rev.ty)
};
if let Some(mut type_option) = get_type_option.await {
action(&mut type_option);
let bytes = type_option.protobuf_bytes().to_vec();
let _ = editor
.update_field_type_option(&editor.grid_id, field_id, bytes)
.await?;
}
Ok(())
}
pub async fn edit_single_select_type_option(
field_id: &str,
editor: Arc<GridRevisionEditor>,
action: impl FnOnce(&mut SingleSelectTypeOptionPB),
) -> FlowyResult<()> {
edit_field_type_option(field_id, editor, action).await
}
pub async fn edit_multi_select_type_option(
field_id: &str,
editor: Arc<GridRevisionEditor>,
action: impl FnOnce(&mut MultiSelectTypeOptionPB),
) -> FlowyResult<()> {
edit_field_type_option(field_id, editor, action).await
}

View File

@ -1,5 +1,7 @@
mod field_builder; mod field_builder;
mod field_operation;
pub(crate) mod type_options; pub(crate) mod type_options;
pub use field_builder::*; pub use field_builder::*;
pub use field_operation::*;
pub use type_options::*; pub use type_options::*;

View File

@ -5,7 +5,7 @@ use crate::services::field::{BoxTypeOptionBuilder, CheckboxCellData, TypeOptionB
use bytes::Bytes; use bytes::Bytes;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -26,7 +26,7 @@ impl TypeOptionBuilder for CheckboxTypeOptionBuilder {
FieldType::Checkbox FieldType::Checkbox
} }
fn entry(&self) -> &dyn TypeOptionDataEntry { fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0 &self.0
} }
} }

View File

@ -9,7 +9,7 @@ use chrono::format::strftime::StrftimeItems;
use chrono::{NaiveDateTime, Timelike}; use chrono::{NaiveDateTime, Timelike};
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// Date // Date
@ -189,7 +189,7 @@ impl TypeOptionBuilder for DateTypeOptionBuilder {
FieldType::DateTime FieldType::DateTime
} }
fn entry(&self) -> &dyn TypeOptionDataEntry { fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0 &self.0
} }
} }

View File

@ -6,7 +6,7 @@ use crate::services::field::{BoxTypeOptionBuilder, NumberCellData, TypeOptionBui
use bytes::Bytes; use bytes::Bytes;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
use rust_decimal::Decimal; use rust_decimal::Decimal;
@ -45,7 +45,7 @@ impl TypeOptionBuilder for NumberTypeOptionBuilder {
FieldType::Number FieldType::Number
} }
fn entry(&self) -> &dyn TypeOptionDataEntry { fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0 &self.0
} }
} }

View File

@ -9,7 +9,7 @@ use crate::services::field::{
use bytes::Bytes; use bytes::Bytes;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// Multiple select // Multiple select
@ -108,7 +108,7 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
FieldType::MultiSelect FieldType::MultiSelect
} }
fn entry(&self) -> &dyn TypeOptionDataEntry { fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0 &self.0
} }
} }

View File

@ -5,7 +5,7 @@ use bytes::Bytes;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::{internal_error, ErrorCode, FlowyResult}; use flowy_error::{internal_error, ErrorCode, FlowyResult};
use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::parser::NotEmptyStr;
use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry}; use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataFormat};
use nanoid::nanoid; use nanoid::nanoid;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -75,7 +75,7 @@ pub fn make_selected_select_options(
} }
} }
pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync { pub trait SelectOptionOperation: TypeOptionDataFormat + Send + Sync {
fn insert_option(&mut self, new_option: SelectOptionPB) { fn insert_option(&mut self, new_option: SelectOptionPB) {
let options = self.mut_options(); let options = self.mut_options();
if let Some(index) = options if let Some(index) = options

View File

@ -9,7 +9,7 @@ use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use bytes::Bytes; use bytes::Bytes;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// Single select // Single select
@ -91,7 +91,7 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
FieldType::SingleSelect FieldType::SingleSelect
} }
fn entry(&self) -> &dyn TypeOptionDataEntry { fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0 &self.0
} }
} }

View File

@ -8,7 +8,7 @@ use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use bytes::Bytes; use bytes::Bytes;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Default)] #[derive(Default)]
@ -21,7 +21,7 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder {
FieldType::RichText FieldType::RichText
} }
fn entry(&self) -> &dyn TypeOptionDataEntry { fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0 &self.0
} }
} }

View File

@ -6,7 +6,7 @@ use bytes::Bytes;
use fancy_regex::Regex; use fancy_regex::Regex;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataFormat};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -20,7 +20,7 @@ impl TypeOptionBuilder for URLTypeOptionBuilder {
FieldType::URL FieldType::URL
} }
fn entry(&self) -> &dyn TypeOptionDataEntry { fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0 &self.0
} }
} }

View File

@ -188,7 +188,7 @@ fn filter_cell(
FieldType::RichText => filter_cache.text_filter.get(&filter_id).and_then(|filter| { FieldType::RichText => filter_cache.text_filter.get(&filter_id).and_then(|filter| {
Some( Some(
field_rev field_rev
.get_type_option_entry::<RichTextTypeOptionPB>(field_type_rev)? .get_type_option::<RichTextTypeOptionPB>(field_type_rev)?
.apply_filter(any_cell_data, filter.value()) .apply_filter(any_cell_data, filter.value())
.ok(), .ok(),
) )
@ -196,7 +196,7 @@ fn filter_cell(
FieldType::Number => filter_cache.number_filter.get(&filter_id).and_then(|filter| { FieldType::Number => filter_cache.number_filter.get(&filter_id).and_then(|filter| {
Some( Some(
field_rev field_rev
.get_type_option_entry::<NumberTypeOptionPB>(field_type_rev)? .get_type_option::<NumberTypeOptionPB>(field_type_rev)?
.apply_filter(any_cell_data, filter.value()) .apply_filter(any_cell_data, filter.value())
.ok(), .ok(),
) )
@ -204,7 +204,7 @@ fn filter_cell(
FieldType::DateTime => filter_cache.date_filter.get(&filter_id).and_then(|filter| { FieldType::DateTime => filter_cache.date_filter.get(&filter_id).and_then(|filter| {
Some( Some(
field_rev field_rev
.get_type_option_entry::<DateTypeOptionPB>(field_type_rev)? .get_type_option::<DateTypeOptionPB>(field_type_rev)?
.apply_filter(any_cell_data, filter.value()) .apply_filter(any_cell_data, filter.value())
.ok(), .ok(),
) )
@ -212,7 +212,7 @@ fn filter_cell(
FieldType::SingleSelect => filter_cache.select_option_filter.get(&filter_id).and_then(|filter| { FieldType::SingleSelect => filter_cache.select_option_filter.get(&filter_id).and_then(|filter| {
Some( Some(
field_rev field_rev
.get_type_option_entry::<SingleSelectTypeOptionPB>(field_type_rev)? .get_type_option::<SingleSelectTypeOptionPB>(field_type_rev)?
.apply_filter(any_cell_data, filter.value()) .apply_filter(any_cell_data, filter.value())
.ok(), .ok(),
) )
@ -220,7 +220,7 @@ fn filter_cell(
FieldType::MultiSelect => filter_cache.select_option_filter.get(&filter_id).and_then(|filter| { FieldType::MultiSelect => filter_cache.select_option_filter.get(&filter_id).and_then(|filter| {
Some( Some(
field_rev field_rev
.get_type_option_entry::<MultiSelectTypeOptionPB>(field_type_rev)? .get_type_option::<MultiSelectTypeOptionPB>(field_type_rev)?
.apply_filter(any_cell_data, filter.value()) .apply_filter(any_cell_data, filter.value())
.ok(), .ok(),
) )
@ -228,7 +228,7 @@ fn filter_cell(
FieldType::Checkbox => filter_cache.checkbox_filter.get(&filter_id).and_then(|filter| { FieldType::Checkbox => filter_cache.checkbox_filter.get(&filter_id).and_then(|filter| {
Some( Some(
field_rev field_rev
.get_type_option_entry::<CheckboxTypeOptionPB>(field_type_rev)? .get_type_option::<CheckboxTypeOptionPB>(field_type_rev)?
.apply_filter(any_cell_data, filter.value()) .apply_filter(any_cell_data, filter.value())
.ok(), .ok(),
) )
@ -236,7 +236,7 @@ fn filter_cell(
FieldType::URL => filter_cache.url_filter.get(&filter_id).and_then(|filter| { FieldType::URL => filter_cache.url_filter.get(&filter_id).and_then(|filter| {
Some( Some(
field_rev field_rev
.get_type_option_entry::<URLTypeOptionPB>(field_type_rev)? .get_type_option::<URLTypeOptionPB>(field_type_rev)?
.apply_filter(any_cell_data, filter.value()) .apply_filter(any_cell_data, filter.value())
.ok(), .ok(),
) )

View File

@ -179,6 +179,10 @@ impl GridRevisionEditor {
None => Err(ErrorCode::FieldDoesNotExist.into()), None => Err(ErrorCode::FieldDoesNotExist.into()),
Some(field_type) => { Some(field_type) => {
let _ = self.update_field_rev(params, field_type).await?; let _ = self.update_field_rev(params, field_type).await?;
match self.view_manager.did_update_field(&field_id).await {
Ok(_) => {}
Err(e) => tracing::error!("View manager update field failed: {:?}", e),
}
let _ = self.notify_did_update_grid_field(&field_id).await?; let _ = self.notify_did_update_grid_field(&field_id).await?;
Ok(()) Ok(())
} }
@ -207,6 +211,11 @@ impl GridRevisionEditor {
Ok(()) Ok(())
} }
pub async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
let _ = self.view_manager.group_by_field(field_id).await?;
Ok(())
}
pub async fn switch_to_field_type(&self, field_id: &str, field_type: &FieldType) -> FlowyResult<()> { pub async fn switch_to_field_type(&self, field_id: &str, field_type: &FieldType) -> FlowyResult<()> {
// let block_ids = self // let block_ids = self
// .get_block_metas() // .get_block_metas()
@ -221,7 +230,9 @@ impl GridRevisionEditor {
let type_option_json_builder = |field_type: &FieldTypeRevision| -> String { let type_option_json_builder = |field_type: &FieldTypeRevision| -> String {
let field_type: FieldType = field_type.into(); let field_type: FieldType = field_type.into();
return default_type_option_builder_from_type(&field_type).entry().json_str(); return default_type_option_builder_from_type(&field_type)
.data_format()
.json_str();
}; };
let _ = self let _ = self
@ -521,12 +532,20 @@ impl GridRevisionEditor {
self.view_manager.get_setting().await self.view_manager.get_setting().await
} }
pub async fn get_grid_filter(&self) -> FlowyResult<Vec<GridFilterConfiguration>> { pub async fn get_grid_filter(&self) -> FlowyResult<Vec<GridFilterConfigurationPB>> {
self.view_manager.get_filters().await self.view_manager.get_filters().await
} }
pub async fn update_filter(&self, params: CreateFilterParams) -> FlowyResult<()> { pub async fn create_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
let _ = self.view_manager.update_filter(params).await?; self.view_manager.insert_or_update_group(params).await
}
pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
self.view_manager.delete_group(params).await
}
pub async fn create_filter(&self, params: InsertFilterParams) -> FlowyResult<()> {
let _ = self.view_manager.insert_or_update_filter(params).await?;
Ok(()) Ok(())
} }
@ -824,7 +843,7 @@ impl JsonDeserializer for TypeOptionJsonDeserializer {
fn deserialize(&self, type_option_data: Vec<u8>) -> CollaborateResult<String> { fn deserialize(&self, type_option_data: Vec<u8>) -> CollaborateResult<String> {
// The type_option_data sent from Dart is serialized by protobuf. // The type_option_data sent from Dart is serialized by protobuf.
let builder = type_option_builder_from_bytes(type_option_data, &self.0); let builder = type_option_builder_from_bytes(type_option_data, &self.0);
let json = builder.entry().json_str(); let json = builder.data_format().json_str();
tracing::trace!("Deserialize type option data to: {}", json); tracing::trace!("Deserialize type option data to: {}", json);
Ok(json) Ok(json)
} }

View File

@ -1,12 +1,16 @@
use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::dart_notification::{send_dart_notification, GridNotification};
use crate::entities::{ use crate::entities::{
CreateFilterParams, CreateRowParams, DeleteFilterParams, GridFilterConfiguration, GridLayout, GridLayoutPB, CreateRowParams, DeleteFilterParams, DeleteGroupParams, GridFilterConfigurationPB, GridGroupConfigurationPB,
GridSettingPB, GroupChangesetPB, GroupPB, GroupViewChangesetPB, InsertedGroupPB, InsertedRowPB, MoveGroupParams, GridLayout, GridLayoutPB, GridSettingPB, GroupChangesetPB, GroupPB, GroupViewChangesetPB, InsertFilterParams,
RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RowPB, InsertGroupParams, InsertedGroupPB, InsertedRowPB, MoveGroupParams, RepeatedGridFilterConfigurationPB,
RepeatedGridGroupConfigurationPB, RowPB,
}; };
use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::grid_editor_task::GridServiceTaskScheduler;
use crate::services::grid_view_manager::{GridViewFieldDelegate, GridViewRowDelegate}; use crate::services::grid_view_manager::{GridViewFieldDelegate, GridViewRowDelegate};
use crate::services::group::{GroupConfigurationReader, GroupConfigurationWriter, GroupService}; use crate::services::group::{
default_group_configuration, find_group_field, make_group_controller, GroupConfigurationReader,
GroupConfigurationWriter, GroupController, MoveGroupRowContext,
};
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{ use flowy_grid_data_model::revision::{
gen_grid_filter_id, FieldRevision, FieldTypeRevision, FilterConfigurationRevision, GroupConfigurationRevision, gen_grid_filter_id, FieldRevision, FieldTypeRevision, FilterConfigurationRevision, GroupConfigurationRevision,
@ -16,9 +20,7 @@ use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilde
use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad}; use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad};
use flowy_sync::entities::revision::Revision; use flowy_sync::entities::revision::Revision;
use lib_infra::future::{wrap_future, AFFuture, FutureResult}; use lib_infra::future::{wrap_future, AFFuture, FutureResult};
use std::collections::HashMap; use std::future::Future;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -30,11 +32,9 @@ pub struct GridViewRevisionEditor {
rev_manager: Arc<RevisionManager>, rev_manager: Arc<RevisionManager>,
field_delegate: Arc<dyn GridViewFieldDelegate>, field_delegate: Arc<dyn GridViewFieldDelegate>,
row_delegate: Arc<dyn GridViewRowDelegate>, row_delegate: Arc<dyn GridViewRowDelegate>,
group_service: Arc<RwLock<GroupService>>, group_controller: Arc<RwLock<Box<dyn GroupController>>>,
scheduler: Arc<dyn GridServiceTaskScheduler>, scheduler: Arc<dyn GridServiceTaskScheduler>,
did_load_group: AtomicBool,
} }
impl GridViewRevisionEditor { impl GridViewRevisionEditor {
#[tracing::instrument(level = "trace", skip_all, err)] #[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn new( pub(crate) async fn new(
@ -52,16 +52,16 @@ impl GridViewRevisionEditor {
let view_revision_pad = rev_manager.load::<GridViewRevisionPadBuilder>(Some(cloud)).await?; let view_revision_pad = rev_manager.load::<GridViewRevisionPadBuilder>(Some(cloud)).await?;
let pad = Arc::new(RwLock::new(view_revision_pad)); let pad = Arc::new(RwLock::new(view_revision_pad));
let rev_manager = Arc::new(rev_manager); let rev_manager = Arc::new(rev_manager);
let group_controller = new_group_controller(
let configuration_reader = GroupConfigurationReaderImpl(pad.clone()); user_id.to_owned(),
let configuration_writer = GroupConfigurationWriterImpl { view_id.clone(),
user_id: user_id.to_owned(), pad.clone(),
rev_manager: rev_manager.clone(), rev_manager.clone(),
view_pad: pad.clone(), field_delegate.clone(),
}; row_delegate.clone(),
let group_service = GroupService::new(view_id.clone(), configuration_reader, configuration_writer).await; )
.await?;
let user_id = user_id.to_owned(); let user_id = user_id.to_owned();
let did_load_group = AtomicBool::new(false);
Ok(Self { Ok(Self {
pad, pad,
user_id, user_id,
@ -70,24 +70,21 @@ impl GridViewRevisionEditor {
scheduler, scheduler,
field_delegate, field_delegate,
row_delegate, row_delegate,
group_service: Arc::new(RwLock::new(group_service)), group_controller: Arc::new(RwLock::new(group_controller)),
did_load_group,
}) })
} }
pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
match params.group_id.as_ref() { if params.group_id.is_none() {
None => {} return;
Some(group_id) => {
self.group_service
.write()
.await
.will_create_row(row_rev, group_id, |field_id| {
self.field_delegate.get_field_rev(&field_id)
})
.await;
}
} }
let group_id = params.group_id.as_ref().unwrap();
let _ = self
.mut_group_controller(|group_controller, field_rev| {
group_controller.will_create_row(row_rev, &field_rev, group_id);
Ok(())
})
.await;
} }
pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) { pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) {
@ -112,13 +109,11 @@ impl GridViewRevisionEditor {
pub(crate) async fn did_delete_row(&self, row_rev: &RowRevision) { pub(crate) async fn did_delete_row(&self, row_rev: &RowRevision) {
// Send the group notification if the current view has groups; // Send the group notification if the current view has groups;
if let Some(changesets) = self let changesets = self
.group_service .mut_group_controller(|group_controller, field_rev| group_controller.did_delete_row(row_rev, &field_rev))
.write() .await;
.await
.did_delete_row(row_rev, |field_id| self.field_delegate.get_field_rev(&field_id)) if let Some(changesets) = changesets {
.await
{
for changeset in changesets { for changeset in changesets {
self.notify_did_update_group(changeset).await; self.notify_did_update_group(changeset).await;
} }
@ -126,13 +121,11 @@ impl GridViewRevisionEditor {
} }
pub(crate) async fn did_update_row(&self, row_rev: &RowRevision) { pub(crate) async fn did_update_row(&self, row_rev: &RowRevision) {
if let Some(changesets) = self let changesets = self
.group_service .mut_group_controller(|group_controller, field_rev| group_controller.did_update_row(row_rev, &field_rev))
.write() .await;
.await
.did_update_row(row_rev, |field_id| self.field_delegate.get_field_rev(&field_id)) if let Some(changesets) = changesets {
.await
{
for changeset in changesets { for changeset in changesets {
self.notify_did_update_group(changeset).await; self.notify_did_update_group(changeset).await;
} }
@ -146,54 +139,38 @@ impl GridViewRevisionEditor {
to_group_id: &str, to_group_id: &str,
to_row_id: Option<String>, to_row_id: Option<String>,
) -> Vec<GroupChangesetPB> { ) -> Vec<GroupChangesetPB> {
match self let changesets = self
.group_service .mut_group_controller(|group_controller, field_rev| {
.write() let move_row_context = MoveGroupRowContext {
.await row_rev,
.move_group_row(row_rev, row_changeset, to_group_id, to_row_id, |field_id| { row_changeset,
self.field_delegate.get_field_rev(&field_id) field_rev: field_rev.as_ref(),
to_group_id,
to_row_id,
};
let changesets = group_controller.move_group_row(move_row_context)?;
Ok(changesets)
}) })
.await .await;
{
None => vec![], changesets.unwrap_or_default()
Some(changesets) => changesets,
}
} }
/// Only call once after grid view editor initialized /// Only call once after grid view editor initialized
#[tracing::instrument(level = "trace", skip(self))] #[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn load_groups(&self) -> FlowyResult<Vec<GroupPB>> { pub(crate) async fn load_groups(&self) -> FlowyResult<Vec<GroupPB>> {
let groups = if !self.did_load_group.load(Ordering::SeqCst) { let groups = self.group_controller.read().await.groups();
self.did_load_group.store(true, Ordering::SeqCst);
let field_revs = self.field_delegate.get_field_revs().await;
let row_revs = self.row_delegate.gv_row_revs().await;
match self
.group_service
.write()
.await
.load_groups(&field_revs, row_revs)
.await
{
None => vec![],
Some(groups) => groups,
}
} else {
self.group_service.read().await.groups().await
};
tracing::trace!("Number of groups: {}", groups.len()); tracing::trace!("Number of groups: {}", groups.len());
Ok(groups.into_iter().map(GroupPB::from).collect()) Ok(groups.into_iter().map(GroupPB::from).collect())
} }
pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
let _ = self let _ = self
.group_service .group_controller
.write() .write()
.await .await
.move_group(&params.from_group_id, &params.to_group_id) .move_group(&params.from_group_id, &params.to_group_id)?;
.await?; match self.group_controller.read().await.get_group(&params.from_group_id) {
match self.group_service.read().await.get_group(&params.from_group_id).await {
None => {} None => {}
Some((index, group)) => { Some((index, group)) => {
let inserted_group = InsertedGroupPB { let inserted_group = InsertedGroupPB {
@ -206,6 +183,7 @@ impl GridViewRevisionEditor {
inserted_groups: vec![inserted_group], inserted_groups: vec![inserted_group],
deleted_groups: vec![params.from_group_id.clone()], deleted_groups: vec![params.from_group_id.clone()],
update_groups: vec![], update_groups: vec![],
new_groups: vec![],
}; };
self.notify_did_update_view(changeset).await; self.notify_did_update_view(changeset).await;
@ -220,27 +198,52 @@ impl GridViewRevisionEditor {
grid_setting grid_setting
} }
pub(crate) async fn get_filters(&self) -> Vec<GridFilterConfiguration> { pub(crate) async fn get_filters(&self) -> Vec<GridFilterConfigurationPB> {
let field_revs = self.field_delegate.get_field_revs().await; let field_revs = self.field_delegate.get_field_revs().await;
match self.pad.read().await.get_all_filters(&field_revs) { match self.pad.read().await.get_all_filters(&field_revs) {
None => vec![], None => vec![],
Some(filters) => filters Some(filters) => filters
.into_values() .into_values()
.flatten() .flatten()
.map(|filter| GridFilterConfiguration::from(filter.as_ref())) .map(|filter| GridFilterConfigurationPB::from(filter.as_ref()))
.collect(), .collect(),
} }
} }
pub(crate) async fn insert_filter(&self, insert_filter: CreateFilterParams) -> FlowyResult<()> { pub(crate) async fn insert_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
if let Some(field_rev) = self.field_delegate.get_field_rev(&params.field_id).await {
let _ = self
.modify(|pad| {
let configuration = default_group_configuration(&field_rev);
let changeset = pad.insert_group(&params.field_id, &params.field_type_rev, configuration)?;
Ok(changeset)
})
.await?;
}
if self.group_controller.read().await.field_id() != params.field_id {
let _ = self.group_by_field(&params.field_id).await?;
self.notify_did_update_setting().await;
}
Ok(())
}
pub(crate) async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
self.modify(|pad| {
let changeset = pad.delete_filter(&params.field_id, &params.field_type_rev, &params.group_id)?;
Ok(changeset)
})
.await
}
pub(crate) async fn insert_filter(&self, params: InsertFilterParams) -> FlowyResult<()> {
self.modify(|pad| { self.modify(|pad| {
let filter_rev = FilterConfigurationRevision { let filter_rev = FilterConfigurationRevision {
id: gen_grid_filter_id(), id: gen_grid_filter_id(),
field_id: insert_filter.field_id.clone(), field_id: params.field_id.clone(),
condition: insert_filter.condition, condition: params.condition,
content: insert_filter.content, content: params.content,
}; };
let changeset = pad.insert_filter(&insert_filter.field_id, &insert_filter.field_type_rev, filter_rev)?; let changeset = pad.insert_filter(&params.field_id, &params.field_type_rev, filter_rev)?;
Ok(changeset) Ok(changeset)
}) })
.await .await
@ -260,7 +263,7 @@ impl GridViewRevisionEditor {
#[tracing::instrument(level = "trace", skip_all, err)] #[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> { pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> {
if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await { if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await {
match self.group_service.write().await.did_update_field(&field_rev).await? { match self.group_controller.write().await.did_update_field(&field_rev)? {
None => {} None => {}
Some(changeset) => { Some(changeset) => {
self.notify_did_update_view(changeset).await; self.notify_did_update_view(changeset).await;
@ -270,6 +273,44 @@ impl GridViewRevisionEditor {
Ok(()) Ok(())
} }
pub(crate) async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await {
let new_group_controller = new_group_controller_with_field_rev(
self.user_id.clone(),
self.view_id.clone(),
self.pad.clone(),
self.rev_manager.clone(),
field_rev,
self.row_delegate.clone(),
)
.await?;
let new_groups = new_group_controller.groups().into_iter().map(GroupPB::from).collect();
*self.group_controller.write().await = new_group_controller;
let changeset = GroupViewChangesetPB {
view_id: self.view_id.clone(),
new_groups,
..Default::default()
};
debug_assert!(!changeset.is_empty());
if !changeset.is_empty() {
send_dart_notification(&changeset.view_id, GridNotification::DidGroupByNewField)
.payload(changeset)
.send();
}
}
Ok(())
}
async fn notify_did_update_setting(&self) {
let setting = self.get_setting().await;
send_dart_notification(&self.view_id, GridNotification::DidUpdateGridSetting)
.payload(setting)
.send();
}
pub async fn notify_did_update_group(&self, changeset: GroupChangesetPB) { pub async fn notify_did_update_group(&self, changeset: GroupChangesetPB) {
send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup) send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup)
.payload(changeset) .payload(changeset)
@ -295,6 +336,78 @@ impl GridViewRevisionEditor {
} }
Ok(()) Ok(())
} }
async fn mut_group_controller<F, T>(&self, f: F) -> Option<T>
where
F: FnOnce(&mut Box<dyn GroupController>, Arc<FieldRevision>) -> FlowyResult<T>,
{
let group_field_id = self.group_controller.read().await.field_id().to_owned();
match self.field_delegate.get_field_rev(&group_field_id).await {
None => None,
Some(field_rev) => {
let mut write_guard = self.group_controller.write().await;
f(&mut write_guard, field_rev).ok()
}
}
}
#[allow(dead_code)]
async fn async_mut_group_controller<F, O, T>(&self, f: F) -> Option<T>
where
F: FnOnce(Arc<RwLock<Box<dyn GroupController>>>, Arc<FieldRevision>) -> O,
O: Future<Output = FlowyResult<T>> + Sync + 'static,
{
let group_field_id = self.group_controller.read().await.field_id().to_owned();
match self.field_delegate.get_field_rev(&group_field_id).await {
None => None,
Some(field_rev) => {
let _write_guard = self.group_controller.write().await;
f(self.group_controller.clone(), field_rev).await.ok()
}
}
}
}
async fn new_group_controller(
user_id: String,
view_id: String,
pad: Arc<RwLock<GridViewRevisionPad>>,
rev_manager: Arc<RevisionManager>,
field_delegate: Arc<dyn GridViewFieldDelegate>,
row_delegate: Arc<dyn GridViewRowDelegate>,
) -> FlowyResult<Box<dyn GroupController>> {
let configuration_reader = GroupConfigurationReaderImpl(pad.clone());
let field_revs = field_delegate.get_field_revs().await;
// Read the group field or find a new group field
let field_rev = configuration_reader
.get_configuration()
.await
.and_then(|configuration| {
field_revs
.iter()
.find(|field_rev| field_rev.id == configuration.field_id)
.cloned()
})
.unwrap_or_else(|| find_group_field(&field_revs).unwrap());
new_group_controller_with_field_rev(user_id, view_id, pad, rev_manager, field_rev, row_delegate).await
}
async fn new_group_controller_with_field_rev(
user_id: String,
view_id: String,
pad: Arc<RwLock<GridViewRevisionPad>>,
rev_manager: Arc<RevisionManager>,
field_rev: Arc<FieldRevision>,
row_delegate: Arc<dyn GridViewRowDelegate>,
) -> FlowyResult<Box<dyn GroupController>> {
let configuration_reader = GroupConfigurationReaderImpl(pad.clone());
let configuration_writer = GroupConfigurationWriterImpl {
user_id,
rev_manager,
view_pad: pad,
};
let row_revs = row_delegate.gv_row_revs().await;
make_group_controller(view_id, field_rev, row_revs, configuration_reader, configuration_writer).await
} }
async fn apply_change( async fn apply_change(
@ -335,13 +448,10 @@ impl RevisionObjectBuilder for GridViewRevisionPadBuilder {
struct GroupConfigurationReaderImpl(Arc<RwLock<GridViewRevisionPad>>); struct GroupConfigurationReaderImpl(Arc<RwLock<GridViewRevisionPad>>);
impl GroupConfigurationReader for GroupConfigurationReaderImpl { impl GroupConfigurationReader for GroupConfigurationReaderImpl {
fn get_group_configuration( fn get_configuration(&self) -> AFFuture<Option<Arc<GroupConfigurationRevision>>> {
&self,
field_rev: Arc<FieldRevision>,
) -> AFFuture<Option<Arc<GroupConfigurationRevision>>> {
let view_pad = self.0.clone(); let view_pad = self.0.clone();
wrap_future(async move { wrap_future(async move {
let mut groups = view_pad.read().await.groups.get_objects(&field_rev.id, &field_rev.ty)?; let mut groups = view_pad.read().await.get_all_groups();
if groups.is_empty() { if groups.is_empty() {
None None
} else { } else {
@ -359,7 +469,7 @@ struct GroupConfigurationWriterImpl {
} }
impl GroupConfigurationWriter for GroupConfigurationWriterImpl { impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
fn save_group_configuration( fn save_configuration(
&self, &self,
field_id: &str, field_id: &str,
field_type: FieldTypeRevision, field_type: FieldTypeRevision,
@ -385,31 +495,40 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
} }
pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc<FieldRevision>]) -> GridSettingPB { pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc<FieldRevision>]) -> GridSettingPB {
let current_layout_type: GridLayout = view_pad.layout.clone().into(); let layout_type: GridLayout = view_pad.layout.clone().into();
let filters_by_field_id = view_pad let filter_configurations = view_pad
.get_all_filters(field_revs) .get_all_filters(field_revs)
.map(|filters_by_field_id| { .map(|filters_by_field_id| {
filters_by_field_id filters_by_field_id
.into_iter() .into_iter()
.map(|(k, v)| (k, v.into())) .map(|(_, v)| {
.collect::<HashMap<String, RepeatedGridConfigurationFilterPB>>() let repeated_filter: RepeatedGridFilterConfigurationPB = v.into();
repeated_filter.items
})
.flatten()
.collect::<Vec<GridFilterConfigurationPB>>()
}) })
.unwrap_or_default(); .unwrap_or_default();
let groups_by_field_id = view_pad
.get_all_groups(field_revs) let group_configurations = view_pad
.get_groups_by_field_revs(field_revs)
.map(|groups_by_field_id| { .map(|groups_by_field_id| {
groups_by_field_id groups_by_field_id
.into_iter() .into_iter()
.map(|(k, v)| (k, v.into())) .map(|(_, v)| {
.collect::<HashMap<String, RepeatedGridGroupConfigurationPB>>() let repeated_group: RepeatedGridGroupConfigurationPB = v.into();
repeated_group.items
})
.flatten()
.collect::<Vec<GridGroupConfigurationPB>>()
}) })
.unwrap_or_default(); .unwrap_or_default();
GridSettingPB { GridSettingPB {
layouts: GridLayoutPB::all(), layouts: GridLayoutPB::all(),
current_layout_type, layout_type,
filter_configuration_by_field_id: filters_by_field_id, filter_configurations: filter_configurations.into(),
group_configuration_by_field_id: groups_by_field_id, group_configurations: group_configurations.into(),
} }
} }

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