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:
push:
branches: [main]
branches:
- "main"
paths:
- "frontend/app_flowy/**"
pull_request:
branches: [main]
branches:
- "main"
paths:
- "frontend/app_flowy/**"
env:
CARGO_TERM_COLOR: always

View File

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

View File

@ -5,13 +5,13 @@ on:
branches:
- "main"
paths:
- "frontend/app_flowy/packages/appflowy_editor"
- "frontend/app_flowy/packages/appflowy_editor/**"
pull_request:
branches:
- "main"
paths:
- "frontend/app_flowy/packages/appflowy_editor"
- "frontend/app_flowy/packages/appflowy_editor/**"
env:
CARGO_TERM_COLOR: always
@ -37,4 +37,12 @@ jobs:
working-directory: frontend/app_flowy/packages/appflowy_editor
run: |
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:
push:
branches: [ main ]
pull_request:
branches: [ main ]
branches:
- "main"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
pull_request:
branches:
- "main"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
env:
CARGO_TERM_COLOR: always

View File

@ -3,10 +3,17 @@ name: Unit test(Rust)
on:
push:
branches:
- 'main'
- "main"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
pull_request:
branches:
- 'main'
- "main"
paths:
- "frontend/rust-lib/**"
- "shared-lib/**"
env:
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": {
"filter": "Filter",
"sortBy": "Sort by",
"Properties": "Properties"
"Properties": "Properties",
"group": "Group"
},
"field": {
"hide": "Hide",

View File

@ -1,6 +1,6 @@
import 'dart:async';
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_service.dart';
import 'package:appflowy_board/appflowy_board.dart';
@ -25,7 +25,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
final MoveRowFFIService _rowService;
LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap();
GridFieldCache get fieldCache => _gridDataController.fieldCache;
GridFieldController get fieldController =>
_gridDataController.fieldController;
String get gridId => _gridDataController.gridId;
BoardBloc({required ViewPB view})
@ -110,9 +111,11 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
emit(state.copyWith(noneOrError: some(error)));
},
didReceiveGroups: (List<GroupPB> groups) {
emit(state.copyWith(
groupIds: groups.map((group) => group.groupId).toList(),
));
emit(
state.copyWith(
groupIds: groups.map((group) => group.groupId).toList(),
),
);
},
);
},
@ -154,6 +157,23 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
}
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) {
final delegate = GroupControllerDelegateImpl(
controller: boardController,
@ -184,38 +204,35 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
}
},
didLoadGroups: (groups) {
List<AFBoardColumnData> columns = groups.map((group) {
return AFBoardColumnData(
id: group.groupId,
name: group.desc,
items: _buildRows(group),
customData: group,
);
}).toList();
boardController.addColumns(columns);
if (isClosed) return;
initializeGroups(groups);
add(BoardEvent.didReceiveGroups(groups));
},
onDeletedGroup: (groupIds) {
if (isClosed) return;
//
},
onInsertedGroup: (insertedGroups) {
if (isClosed) return;
//
},
onUpdatedGroup: (updatedGroups) {
//
if (isClosed) return;
for (final group in updatedGroups) {
final columnController =
boardController.getColumnController(group.groupId);
if (columnController != null) {
columnController.updateColumnName(group.desc);
}
columnController?.updateColumnName(group.desc);
}
},
onError: (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 '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/row/row_cache.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';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>);
typedef OnGridChanged = void Function(GridPB);
typedef DidLoadGroups = void Function(List<GroupPB>);
typedef OnUpdatedGroup = void Function(List<GroupPB>);
typedef OnDeletedGroup = void Function(List<String>);
typedef OnInsertedGroup = void Function(List<InsertedGroupPB>);
typedef OnResetGroups = void Function(List<GroupPB>);
typedef OnRowsChanged = void Function(
List<RowInfo>,
@ -28,7 +29,7 @@ typedef OnError = void Function(FlowyError);
class BoardDataController {
final String gridId;
final GridFFIService _gridFFIService;
final GridFieldCache fieldCache;
final GridFieldController fieldController;
final BoardListener _listener;
// key: the block id
@ -55,7 +56,7 @@ class BoardDataController {
// ignore: prefer_collection_literals
_blocks = LinkedHashMap(),
_gridFFIService = GridFFIService(gridId: view.id),
fieldCache = GridFieldCache(gridId: view.id);
fieldController = GridFieldController(gridId: view.id);
void addListener({
required OnGridChanged onGridChanged,
@ -65,6 +66,7 @@ class BoardDataController {
required OnUpdatedGroup onUpdatedGroup,
required OnDeletedGroup onDeletedGroup,
required OnInsertedGroup onInsertedGroup,
required OnResetGroups onResetGroups,
required OnError? onError,
}) {
_onGridChanged = onGridChanged;
@ -73,28 +75,36 @@ class BoardDataController {
_onRowsChanged = onRowsChanged;
_onError = onError;
fieldCache.addListener(onFields: (fields) {
fieldController.addListener(onFields: (fields) {
_onFieldsChanged?.call(UnmodifiableListView(fields));
});
_listener.start(onBoardChanged: (result) {
result.fold(
(changeset) {
if (changeset.updateGroups.isNotEmpty) {
onUpdatedGroup.call(changeset.updateGroups);
}
_listener.start(
onBoardChanged: (result) {
result.fold(
(changeset) {
if (changeset.updateGroups.isNotEmpty) {
onUpdatedGroup.call(changeset.updateGroups);
}
if (changeset.insertedGroups.isNotEmpty) {
onInsertedGroup.call(changeset.insertedGroups);
}
if (changeset.insertedGroups.isNotEmpty) {
onInsertedGroup.call(changeset.insertedGroups);
}
if (changeset.deletedGroups.isNotEmpty) {
onDeletedGroup.call(changeset.deletedGroups);
}
},
(e) => _onError?.call(e),
);
});
if (changeset.deletedGroups.isNotEmpty) {
onDeletedGroup.call(changeset.deletedGroups);
}
},
(e) => _onError?.call(e),
);
},
onGroupByNewField: (result) {
result.fold(
(groups) => onResetGroups(groups),
(e) => _onError?.call(e),
);
},
);
}
Future<Either<Unit, FlowyError>> loadData() async {
@ -103,16 +113,15 @@ class BoardDataController {
() => result.fold(
(grid) async {
_onGridChanged?.call(grid);
return await _loadFields(grid).then((result) {
return result.fold(
(l) {
_loadGroups(grid.blocks);
return left(l);
},
(err) => right(err),
);
});
return await fieldController.loadFields(fieldIds: grid.fields).then(
(result) => result.fold(
(l) {
_loadGroups(grid.blocks);
return left(l);
},
(err) => right(err),
),
);
},
(err) => right(err),
),
@ -126,33 +135,19 @@ class BoardDataController {
Future<void> dispose() async {
await _gridFFIService.closeGrid();
await fieldCache.dispose();
await fieldController.dispose();
for (final blockCache in _blocks.values) {
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 {
for (final block in blocks) {
final cache = GridBlockCache(
gridId: gridId,
block: block,
fieldCache: fieldCache,
fieldController: fieldController,
);
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-grid/dart_notification.pb.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';
typedef UpdateBoardNotifiedValue = Either<GroupViewChangesetPB, FlowyError>;
typedef GroupUpdateValue = Either<GroupViewChangesetPB, FlowyError>;
typedef GroupByNewFieldValue = Either<List<GroupPB>, FlowyError>;
class BoardListener {
final String viewId;
PublishNotifier<UpdateBoardNotifiedValue>? _groupNotifier = PublishNotifier();
PublishNotifier<GroupUpdateValue>? _groupUpdateNotifier = PublishNotifier();
PublishNotifier<GroupByNewFieldValue>? _groupByNewFieldNotifier =
PublishNotifier();
GridNotificationListener? _listener;
BoardListener(this.viewId);
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(
objectId: viewId,
handler: _handler,
@ -32,9 +38,16 @@ class BoardListener {
switch (ty) {
case GridNotification.DidUpdateGroupView:
result.fold(
(payload) => _groupNotifier?.value =
(payload) => _groupUpdateNotifier?.value =
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;
default:
@ -44,7 +57,10 @@ class BoardListener {
Future<void> stop() async {
await _listener?.stop();
_groupNotifier?.dispose();
_groupNotifier = null;
_groupUpdateNotifier?.dispose();
_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/field/field_controller.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:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
@ -20,8 +20,6 @@ class BoardDateCellBloc extends Bloc<BoardDateCellEvent, BoardDateCellState> {
emit(state.copyWith(
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.didReceiveCellUpdate(DateCellDataPB? data) =
_DidReceiveCellUpdate;
const factory BoardDateCellEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
}
@freezed
@ -62,14 +58,14 @@ class BoardDateCellState with _$BoardDateCellState {
const factory BoardDateCellState({
required DateCellDataPB? data,
required String dateStr,
required FieldPB field,
required GridFieldContext fieldContext,
}) = _BoardDateCellState;
factory BoardDateCellState.initial(GridDateCellController context) {
final cellData = context.getCellData();
return BoardDateCellState(
field: context.field,
fieldContext: context.fieldContext,
data: cellData,
dateStr: _dateStrFromCellData(cellData),
);

View File

@ -59,7 +59,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
return RowInfo(
gridId: _rowService.gridId,
fields: UnmodifiableListView(
state.cells.map((cell) => cell.identifier.field).toList(),
state.cells.map((cell) => cell.identifier.fieldContext).toList(),
),
rowPB: state.rowPB,
);
@ -120,9 +120,9 @@ class BoardCellEquatable extends Equatable {
@override
List<Object?> get props => [
identifier.field.id,
identifier.field.fieldType,
identifier.field.visibility,
identifier.field.width,
identifier.fieldContext.id,
identifier.fieldContext.fieldType,
identifier.fieldContext.visibility,
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/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/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:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flutter/foundation.dart';
@ -10,15 +10,15 @@ typedef OnCardChanged = void Function(GridCellMap, RowsChangedReason);
class CardDataController extends BoardCellBuilderDelegate {
final RowPB rowPB;
final GridFieldCache _fieldCache;
final GridFieldController _fieldController;
final GridRowCache _rowCache;
final List<VoidCallback> _onCardChangedListeners = [];
CardDataController({
required this.rowPB,
required GridFieldCache fieldCache,
required GridFieldController fieldController,
required GridRowCache rowCache,
}) : _fieldCache = fieldCache,
}) : _fieldController = fieldController,
_rowCache = rowCache;
GridCellMap loadData() {
@ -41,7 +41,7 @@ class CardDataController extends BoardCellBuilderDelegate {
@override
GridCellFieldNotifier buildFieldNotifier() {
return GridCellFieldNotifier(
notifier: GridCellFieldNotifierImpl(_fieldCache));
notifier: GridCellFieldNotifierImpl(_fieldController));
}
@override

View File

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

View File

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

View File

@ -1,7 +1,8 @@
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/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/widgets/toolbar/grid_group.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_property.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
@ -18,16 +19,16 @@ import 'board_toolbar.dart';
class BoardSettingContext {
final String viewId;
final GridFieldCache fieldCache;
final GridFieldController fieldController;
BoardSettingContext({
required this.viewId,
required this.fieldCache,
required this.fieldController,
});
factory BoardSettingContext.from(BoardToolbarContext toolbarContext) =>
BoardSettingContext(
viewId: toolbarContext.viewId,
fieldCache: toolbarContext.fieldCache,
fieldController: toolbarContext.fieldController,
);
}
@ -125,6 +126,8 @@ extension _GridSettingExtension on BoardSettingAction {
switch (this) {
case BoardSettingAction.properties:
return 'grid/setting/properties';
case BoardSettingAction.groups:
return 'grid/setting/group';
}
}
@ -132,6 +135,8 @@ extension _GridSettingExtension on BoardSettingAction {
switch (this) {
case BoardSettingAction.properties:
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)),
child: GridPropertyList(
gridId: widget.settingContext.viewId,
fieldCache: widget.settingContext.fieldCache,
fieldController: widget.settingContext.fieldController,
),
);
}
@ -169,6 +174,8 @@ class _BoardSettingListPopoverState extends State<BoardSettingListPopover> {
settingContext: widget.settingContext,
onAction: (action, settingContext) {
switch (action) {
case BoardSettingAction.groups:
break;
case BoardSettingAction.properties:
setState(() {
_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:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
@ -10,11 +10,11 @@ import 'board_setting.dart';
class BoardToolbarContext {
final String viewId;
final GridFieldCache fieldCache;
final GridFieldController fieldController;
BoardToolbarContext({
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/protobuf/flowy-grid/block_entities.pb.dart';
import '../field/field_cache.dart';
import '../field/field_controller.dart';
import '../row/row_cache.dart';
import 'block_listener.dart';
@ -19,12 +19,12 @@ class GridBlockCache {
GridBlockCache({
required this.gridId,
required this.block,
required GridFieldCache fieldCache,
required GridFieldController fieldController,
}) {
_rowCache = GridRowCache(
gridId: gridId,
block: block,
notifier: GridRowFieldNotifierImpl(fieldCache),
notifier: GridRowFieldNotifierImpl(fieldController),
);
_listener = GridBlockListener(blockId: block.id);

View File

@ -148,10 +148,10 @@ class IGridCellController<T, D> extends Equatable {
_cellDataLoader = cellDataLoader,
_cellDataPersistence = cellDataPersistence,
_fieldNotifier = fieldNotifier,
_fieldService =
FieldService(gridId: cellId.gridId, fieldId: cellId.field.id),
_cacheKey =
GridCellCacheKey(rowId: cellId.rowId, fieldId: cellId.field.id);
_fieldService = FieldService(
gridId: cellId.gridId, fieldId: cellId.fieldContext.id),
_cacheKey = GridCellCacheKey(
rowId: cellId.rowId, fieldId: cellId.fieldContext.id);
IGridCellController<T, D> clone() {
return IGridCellController(
@ -166,11 +166,11 @@ class IGridCellController<T, D> extends Equatable {
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(
{required void Function(T?) onCellChanged,
@ -182,7 +182,8 @@ class IGridCellController<T, D> extends Equatable {
isListening = true;
_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.
/// For example:
@ -308,14 +309,14 @@ class IGridCellController<T, D> extends Equatable {
@override
List<Object> get props =>
[_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.field.id];
[_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldContext.id];
}
class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
final GridFieldCache _cache;
FieldChangesetCallback? _onChangesetFn;
final GridFieldController _cache;
OnChangeset? _onChangesetFn;
GridCellFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
GridCellFieldNotifierImpl(GridFieldController cache) : _cache = cache;
@override
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 'dart:convert' show utf8;
import '../../field/field_cache.dart';
import '../../field/field_controller.dart';
import '../../field/type_option/type_option_context.dart';
import 'cell_field_notifier.dart';
part 'cell_service.freezed.dart';
part 'cell_data_loader.dart';
part 'context_builder.dart';
part 'cell_controller.dart';
part 'cell_cache.dart';
part 'cell_data_persistence.dart';
@ -60,17 +60,17 @@ class GridCellIdentifier with _$GridCellIdentifier {
const factory GridCellIdentifier({
required String gridId,
required String rowId,
required FieldPB field,
required GridFieldContext fieldContext,
}) = _GridCellIdentifier;
// ignore: unused_element
const GridCellIdentifier._();
String get fieldId => field.id;
String get fieldId => fieldContext.id;
FieldType get fieldType => field.fieldType;
FieldType get fieldType => fieldContext.fieldType;
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(
gridId: cellController.gridId,
fieldId: cellController.field.id,
fieldId: cellController.fieldContext.id,
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/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
@ -20,8 +20,6 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
emit(state.copyWith(
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.didReceiveCellUpdate(DateCellDataPB? data) =
_DidReceiveCellUpdate;
const factory DateCellEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
}
@freezed
@ -62,14 +58,14 @@ class DateCellState with _$DateCellState {
const factory DateCellState({
required DateCellDataPB? data,
required String dateStr,
required FieldPB field,
required GridFieldContext fieldContext,
}) = _DateCellState;
factory DateCellState.initial(GridDateCellController context) {
final cellData = context.getCellData();
return DateCellState(
field: context.field,
fieldContext: context.fieldContext,
data: cellData,
dateStr: _dateStrFromCellData(cellData),
);

View File

@ -11,7 +11,7 @@ class SelectOptionService {
SelectOptionService({required this.cellId});
String get gridId => cellId.gridId;
String get fieldId => cellId.field.id;
String get fieldId => cellId.fieldContext.id;
String get rowId => cellId.rowId;
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:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'field_service.freezed.dart';
/// 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_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
@ -17,12 +18,12 @@ class TypeOptionDataController {
TypeOptionDataController({
required this.gridId,
required this.loader,
FieldPB? field,
GridFieldContext? fieldContext,
}) {
if (field != null) {
if (fieldContext != null) {
_data = FieldTypeOptionDataPB.create()
..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:freezed_annotation/freezed_annotation.dart';
import 'block/block_cache.dart';
import 'field/field_controller.dart';
import 'grid_data_controller.dart';
import 'row/row_cache.dart';
import 'dart:collection';
@ -101,7 +102,7 @@ class GridEvent with _$GridEvent {
RowsChangedReason listState,
) = _DidReceiveRowUpdate;
const factory GridEvent.didReceiveFieldUpdate(
UnmodifiableListView<FieldPB> fields,
UnmodifiableListView<GridFieldContext> fields,
) = _DidReceiveFieldUpdate;
const factory GridEvent.didReceiveGridUpdate(
@ -138,9 +139,9 @@ class GridLoadingState with _$GridLoadingState {
}
class GridFieldEquatable extends Equatable {
final UnmodifiableListView<FieldPB> _fields;
final UnmodifiableListView<GridFieldContext> _fields;
const GridFieldEquatable(
UnmodifiableListView<FieldPB> fields,
UnmodifiableListView<GridFieldContext> fields,
) : _fields = fields;
@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-folder/view.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 'dart:async';
import 'package:dartz/dartz.dart';
import 'block/block_cache.dart';
import 'field/field_cache.dart';
import 'field/field_controller.dart';
import 'prelude.dart';
import 'row/row_cache.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>);
typedef OnGridChanged = void Function(GridPB);
typedef OnRowsChanged = void Function(
@ -25,7 +24,7 @@ typedef ListenOnRowChangedCondition = bool Function();
class GridDataController {
final String gridId;
final GridFFIService _gridFFIService;
final GridFieldCache fieldCache;
final GridFieldController fieldController;
// key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks;
@ -49,7 +48,7 @@ class GridDataController {
// ignore: prefer_collection_literals
_blocks = LinkedHashMap(),
_gridFFIService = GridFFIService(gridId: view.id),
fieldCache = GridFieldCache(gridId: view.id);
fieldController = GridFieldController(gridId: view.id);
void addListener({
required OnGridChanged onGridChanged,
@ -60,7 +59,7 @@ class GridDataController {
_onRowChanged = onRowsChanged;
_onFieldsChanged = onFieldsChanged;
fieldCache.addListener(onFields: (fields) {
fieldController.addListener(onFields: (fields) {
_onFieldsChanged?.call(UnmodifiableListView(fields));
});
}
@ -72,7 +71,7 @@ class GridDataController {
(grid) async {
_initialBlocks(grid.blocks);
_onGridChanged?.call(grid);
return await _loadFields(grid);
return await fieldController.loadFields(fieldIds: grid.fields);
},
(err) => right(err),
),
@ -85,7 +84,7 @@ class GridDataController {
Future<void> dispose() async {
await _gridFFIService.closeGrid();
await fieldCache.dispose();
await fieldController.dispose();
for (final blockCache in _blocks.values) {
blockCache.dispose();
@ -102,7 +101,7 @@ class GridDataController {
final cache = GridBlockCache(
gridId: gridId,
block: block,
fieldCache: fieldCache,
fieldController: fieldController,
);
cache.addListener(
@ -114,18 +113,4 @@ class GridDataController {
_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:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'field/field_cache.dart';
import 'field/field_controller.dart';
part 'grid_header_bloc.freezed.dart';
class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
final GridFieldCache fieldCache;
final GridFieldController fieldController;
final String gridId;
GridHeaderBloc({
required this.gridId,
required this.fieldCache,
}) : super(GridHeaderState.initial(fieldCache.fields)) {
required this.fieldController,
}) : super(GridHeaderState.initial(fieldController.fieldContexts)) {
on<GridHeaderEvent>(
(event, emit) async {
await event.map(
@ -36,7 +35,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
Future<void> _moveField(
_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));
emit(state.copyWith(fields: fields));
@ -49,7 +48,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
}
Future<void> _startListening() async {
fieldCache.addListener(
fieldController.addListener(
onFields: (fields) => add(GridHeaderEvent.didReceiveFieldUpdate(fields)),
listenWhen: () => !isClosed,
);
@ -64,18 +63,18 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
@freezed
class GridHeaderEvent with _$GridHeaderEvent {
const factory GridHeaderEvent.initial() = _InitialHeader;
const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldPB> fields) =
_DidReceiveFieldUpdate;
const factory GridHeaderEvent.didReceiveFieldUpdate(
List<GridFieldContext> fields) = _DidReceiveFieldUpdate;
const factory GridHeaderEvent.moveField(
FieldPB field, int fromIndex, int toIndex) = _MoveField;
}
@freezed
class GridHeaderState with _$GridHeaderState {
const factory GridHeaderState({required List<FieldPB> fields}) =
const factory GridHeaderState({required List<GridFieldContext> fields}) =
_GridHeaderState;
factory GridHeaderState.initial(List<FieldPB> fields) {
factory GridHeaderState.initial(List<GridFieldContext> fields) {
// final List<FieldPB> newFields = List.from(fields);
// newFields.retainWhere((field) => field.visibility);
return GridHeaderState(fields: fields);

View File

@ -1,7 +1,7 @@
import 'dart:collection';
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: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';
@ -35,7 +35,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
},
didReceiveCells: (_DidReceiveCells value) async {
final cells = value.gridCellMap.values
.map((e) => GridCellEquatable(e.field))
.map((e) => GridCellEquatable(e.fieldContext))
.toList();
emit(state.copyWith(
gridCellMap: value.gridCellMap,
@ -87,21 +87,23 @@ class RowState with _$RowState {
rowInfo: rowInfo,
gridCellMap: cellDataMap,
cells: UnmodifiableListView(
cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
cellDataMap.values
.map((e) => GridCellEquatable(e.fieldContext))
.toList(),
),
);
}
class GridCellEquatable extends Equatable {
final FieldPB _field;
final GridFieldContext _fieldContext;
const GridCellEquatable(FieldPB field) : _field = field;
const GridCellEquatable(GridFieldContext field) : _fieldContext = field;
@override
List<Object?> get props => [
_field.id,
_field.fieldType,
_field.visibility,
_field.width,
_fieldContext.id,
_fieldContext.fieldType,
_fieldContext.visibility,
_fieldContext.width,
];
}

View File

@ -1,5 +1,6 @@
import 'dart:collection';
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/log.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();
abstract class IGridRowFieldNotifier {
UnmodifiableListView<FieldPB> get fields;
UnmodifiableListView<GridFieldContext> get fields;
void onRowFieldsChanged(VoidCallback callback);
void onRowFieldChanged(void Function(FieldPB) callback);
void onRowDispose();
@ -217,7 +218,7 @@ class GridRowCache {
cellDataMap[field.id] = GridCellIdentifier(
rowId: rowId,
gridId: gridId,
field: field,
fieldContext: field,
);
}
}
@ -284,7 +285,7 @@ class _RowChangesetNotifier extends ChangeNotifier {
class RowInfo with _$RowInfo {
const factory RowInfo({
required String gridId,
required UnmodifiableListView<FieldPB> fields,
required UnmodifiableListView<GridFieldContext> fields,
required RowPB rowPB,
}) = _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 '../../presentation/widgets/cell/cell_builder.dart';
import '../cell/cell_service/cell_service.dart';
import '../field/field_cache.dart';
import '../field/field_controller.dart';
import 'row_cache.dart';
typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason);
@ -10,14 +10,14 @@ typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason);
class GridRowDataController extends GridCellBuilderDelegate {
final RowInfo rowInfo;
final List<VoidCallback> _onRowChangedListeners = [];
final GridFieldCache _fieldCache;
final GridFieldController _fieldController;
final GridRowCache _rowCache;
GridRowDataController({
required this.rowInfo,
required GridFieldCache fieldCache,
required GridFieldController fieldController,
required GridRowCache rowCache,
}) : _fieldCache = fieldCache,
}) : _fieldController = fieldController,
_rowCache = rowCache;
GridCellMap loadData() {
@ -41,7 +41,7 @@ class GridRowDataController extends GridCellBuilderDelegate {
@override
GridCellFieldNotifier buildFieldNotifier() {
return GridCellFieldNotifier(
notifier: GridCellFieldNotifierImpl(_fieldCache));
notifier: GridCellFieldNotifierImpl(_fieldController));
}
@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:flowy_sdk/log.dart';
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_cache.dart';
import '../field/field_controller.dart';
part 'property_bloc.freezed.dart';
class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
final GridFieldCache _fieldCache;
Function(List<FieldPB>)? _onFieldsFn;
final GridFieldController _fieldController;
Function(List<GridFieldContext>)? _onFieldsFn;
GridPropertyBloc({required String gridId, required GridFieldCache fieldCache})
: _fieldCache = fieldCache,
super(GridPropertyState.initial(gridId, fieldCache.fields)) {
GridPropertyBloc(
{required String gridId, required GridFieldController fieldController})
: _fieldController = fieldController,
super(
GridPropertyState.initial(gridId, fieldController.fieldContexts)) {
on<GridPropertyEvent>(
(event, emit) async {
await event.map(
@ -33,7 +34,7 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
);
},
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
emit(state.copyWith(fields: value.fields));
emit(state.copyWith(fieldContexts: value.fields));
},
moveField: (_MoveField value) {
//
@ -46,7 +47,7 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
@override
Future<void> close() async {
if (_onFieldsFn != null) {
_fieldCache.removeListener(onFieldsListener: _onFieldsFn!);
_fieldController.removeListener(onFieldsListener: _onFieldsFn!);
_onFieldsFn = null;
}
return super.close();
@ -55,7 +56,7 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
void _startListening() {
_onFieldsFn =
(fields) => add(GridPropertyEvent.didReceiveFieldUpdate(fields));
_fieldCache.addListener(
_fieldController.addListener(
onFields: _onFieldsFn,
listenWhen: () => !isClosed,
);
@ -67,8 +68,8 @@ class GridPropertyEvent with _$GridPropertyEvent {
const factory GridPropertyEvent.initial() = _Initial;
const factory GridPropertyEvent.setFieldVisibility(
String fieldId, bool visibility) = _SetFieldVisibility;
const factory GridPropertyEvent.didReceiveFieldUpdate(List<FieldPB> fields) =
_DidReceiveFieldUpdate;
const factory GridPropertyEvent.didReceiveFieldUpdate(
List<GridFieldContext> fields) = _DidReceiveFieldUpdate;
const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) =
_MoveField;
}
@ -77,12 +78,15 @@ class GridPropertyEvent with _$GridPropertyEvent {
class GridPropertyState with _$GridPropertyState {
const factory GridPropertyState({
required String gridId,
required List<FieldPB> fields,
required List<GridFieldContext> fieldContexts,
}) = _GridPropertyState;
factory GridPropertyState.initial(String gridId, List<FieldPB> fields) =>
factory GridPropertyState.initial(
String gridId,
List<GridFieldContext> fieldContexts,
) =>
GridPropertyState(
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/startup/startup.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) {
final fieldCache = context.read<GridBloc>().dataController.fieldCache;
final fieldController =
context.read<GridBloc>().dataController.fieldController;
return GridHeaderSliverAdaptor(
gridId: gridId,
fieldCache: fieldCache,
fieldController: fieldController,
anchorScrollController: headerScrollController,
);
}
@ -174,10 +175,11 @@ class _GridToolbarAdaptor extends StatelessWidget {
Widget build(BuildContext context) {
return BlocSelector<GridBloc, GridState, GridToolbarContext>(
selector: (state) {
final fieldCache = context.read<GridBloc>().dataController.fieldCache;
final fieldController =
context.read<GridBloc>().dataController.fieldController;
return GridToolbarContext(
gridId: state.gridId,
fieldCache: fieldCache,
fieldController: fieldController,
);
},
builder: (context, toolbarContext) {
@ -248,10 +250,11 @@ class _GridRowsState extends State<_GridRows> {
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return const SizedBox();
final fieldCache = context.read<GridBloc>().dataController.fieldCache;
final fieldController =
context.read<GridBloc>().dataController.fieldController;
final dataController = GridRowDataController(
rowInfo: rowInfo,
fieldCache: fieldCache,
fieldController: fieldController,
rowCache: rowCache,
);
@ -265,7 +268,7 @@ class _GridRowsState extends State<_GridRows> {
_openRowDetailPage(
context,
rowInfo,
fieldCache,
fieldController,
rowCache,
cellBuilder,
);
@ -278,13 +281,13 @@ class _GridRowsState extends State<_GridRows> {
void _openRowDetailPage(
BuildContext context,
RowInfo rowInfo,
GridFieldCache fieldCache,
GridFieldController fieldController,
GridRowCache rowCache,
GridCellBuilder cellBuilder,
) {
final dataController = GridRowDataController(
rowInfo: rowInfo,
fieldCache: fieldCache,
fieldController: fieldController,
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';
class GridLayout {
static double headerWidth(List<FieldPB> fields) {
static double headerWidth(List<GridFieldContext> fields) {
if (fields.isEmpty) return 0;
final fieldsWidth = fields

View File

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

View File

@ -1,5 +1,6 @@
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_data_controller.dart';
import 'package:appflowy_popover/popover.dart';
@ -126,17 +127,18 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder(
TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({
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(
gridId: gridId,
loader: loader,
field: field,
fieldContext: fieldContext,
);
return makeTypeOptionContextWithDataController(
gridId: gridId,
fieldType: field.fieldType,
fieldType: fieldContext.fieldType,
dataController: dataController,
);
}

View File

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

View File

@ -225,16 +225,16 @@ class _RowDetailCellState extends State<_RowDetailCell> {
constraints: BoxConstraints.loose(const Size(240, 200)),
child: FieldEditor(
gridId: widget.cellId.gridId,
fieldName: widget.cellId.field.name,
fieldName: widget.cellId.fieldContext.field.name,
typeOptionLoader: FieldTypeOptionLoader(
gridId: widget.cellId.gridId,
field: widget.cellId.field,
field: widget.cellId.fieldContext.field,
),
),
);
},
child: FieldCellButton(
field: widget.cellId.field,
field: widget.cellId.fieldContext.field,
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/text.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_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart';
import '../../../application/field/field_cache.dart';
import '../../../application/field/field_controller.dart';
import '../../layout/sizes.dart';
class GridPropertyList extends StatefulWidget {
final String gridId;
final GridFieldCache fieldCache;
final GridFieldController fieldController;
const GridPropertyList({
required this.gridId,
required this.fieldCache,
required this.fieldController,
Key? key,
}) : super(key: key);
@ -45,15 +44,15 @@ class _GridPropertyListState extends State<GridPropertyList> {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<GridPropertyBloc>(
param1: widget.gridId, param2: widget.fieldCache)
param1: widget.gridId, param2: widget.fieldController)
..add(const GridPropertyEvent.initial()),
child: BlocBuilder<GridPropertyBloc, GridPropertyState>(
builder: (context, state) {
final cells = state.fields.map((field) {
final cells = state.fieldContexts.map((field) {
return _GridPropertyCell(
popoverMutex: _popoverMutex,
gridId: widget.gridId,
field: field,
fieldContext: field,
key: ValueKey(field.id),
);
}).toList();
@ -76,12 +75,12 @@ class _GridPropertyListState extends State<GridPropertyList> {
}
class _GridPropertyCell extends StatelessWidget {
final FieldPB field;
final GridFieldContext fieldContext;
final String gridId;
final PopoverMutex popoverMutex;
const _GridPropertyCell({
required this.gridId,
required this.field,
required this.fieldContext,
required this.popoverMutex,
Key? key,
}) : super(key: key);
@ -90,7 +89,7 @@ class _GridPropertyCell extends StatelessWidget {
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
final checkmark = field.visibility
final checkmark = fieldContext.visibility
? svgWidget('home/show', color: theme.iconColor)
: svgWidget('home/hide', color: theme.iconColor);
@ -108,7 +107,7 @@ class _GridPropertyCell extends StatelessWidget {
onPressed: () {
context.read<GridPropertyBloc>().add(
GridPropertyEvent.setFieldVisibility(
field.id, !field.visibility));
fieldContext.id, !fieldContext.visibility));
},
icon: checkmark.padding(all: 6),
)
@ -122,18 +121,19 @@ class _GridPropertyCell extends StatelessWidget {
triggerActions: PopoverTriggerActionFlags.click,
offset: const Offset(20, 0),
child: FlowyButton(
text: FlowyText.medium(field.name, fontSize: 12),
text: FlowyText.medium(fieldContext.name, fontSize: 12),
hoverColor: theme.hover,
leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor),
leftIcon: svgWidget(fieldContext.fieldType.iconName(),
color: theme.iconColor),
),
popupBuilder: (BuildContext context) {
return OverlayContainer(
constraints: BoxConstraints.loose(const Size(240, 200)),
child: FieldEditor(
gridId: gridId,
fieldName: field.name,
typeOptionLoader:
FieldTypeOptionLoader(gridId: gridId, field: field),
fieldName: fieldContext.name,
typeOptionLoader: FieldTypeOptionLoader(
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:app_flowy/generated/locale_keys.g.dart';
import '../../../application/field/field_cache.dart';
import '../../../application/field/field_controller.dart';
import '../../layout/sizes.dart';
class GridSettingContext {
final String gridId;
final GridFieldCache fieldCache;
final GridFieldController fieldController;
GridSettingContext({
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_bloc/flutter_bloc.dart';
import '../../../application/field/field_cache.dart';
import '../../../application/field/field_controller.dart';
import '../../layout/sizes.dart';
import 'grid_property.dart';
import 'grid_setting.dart';
class GridToolbarContext {
final String gridId;
final GridFieldCache fieldCache;
final GridFieldController fieldController;
GridToolbarContext({
required this.gridId,
required this.fieldCache,
required this.fieldController,
});
}
@ -30,7 +30,7 @@ class GridToolbar extends StatelessWidget {
Widget build(BuildContext context) {
final settingContext = GridSettingContext(
gridId: toolbarContext.gridId,
fieldCache: toolbarContext.fieldCache,
fieldController: toolbarContext.fieldController,
);
return SizedBox(
height: 40,
@ -89,7 +89,7 @@ class _GridSettingListPopoverState extends State<_GridSettingListPopover> {
constraints: BoxConstraints.loose(const Size(260, 400)),
child: GridPropertyList(
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:get_it/get_it.dart';
import '../plugins/grid/application/field/field_cache.dart';
import '../plugins/grid/application/field/field_controller.dart';
class DependencyResolver {
static Future<void> resolve(GetIt getIt) async {
@ -154,10 +154,10 @@ void _resolveGridDeps(GetIt getIt) {
(view, _) => GridBloc(view: view),
);
getIt.registerFactoryParam<GridHeaderBloc, String, GridFieldCache>(
(gridId, fieldCache) => GridHeaderBloc(
getIt.registerFactoryParam<GridHeaderBloc, String, GridFieldController>(
(gridId, fieldController) => GridHeaderBloc(
gridId: gridId,
fieldCache: fieldCache,
fieldController: fieldController,
),
);
@ -200,7 +200,7 @@ void _resolveGridDeps(GetIt getIt) {
),
);
getIt.registerFactoryParam<GridPropertyBloc, String, GridFieldCache>(
(gridId, cache) => GridPropertyBloc(gridId: gridId, fieldCache: cache),
getIt.registerFactoryParam<GridPropertyBloc, String, GridFieldController>(
(gridId, cache) => GridPropertyBloc(gridId: gridId, fieldController: cache),
);
}

View File

@ -89,6 +89,12 @@ class AFBoardDataController extends ChangeNotifier
if (columnIds.isNotEmpty && notify) notifyListeners();
}
void clear() {
_columnDatas.clear();
_columnControllers.clear();
notifyListeners();
}
AFBoardColumnDataController? getColumnController(String columnId) {
final columnController = _columnControllers[columnId];
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
* Support insert image.
* Support insert link.

View File

@ -20,27 +20,39 @@ and the Flutter guide for
<a href="https://twitter.com/appflowy"><b>Twitter</b></a>
</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">
<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>
## Key Features
* Allow you to 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
* [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
* Build rich, intuitive editors
* Design and modify an ever expanding list of customizable features including
* 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
flutter pub add appflowy_editor
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
final editorState = EditorState.empty(); // an empty state
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
final json = ...;
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
git clone https://github.com/AppFlowy-IO/AppFlowy.git
cd frontend/app_flowy/packages/appflowy_editor/example
flutter run
```
## Customizing Your Editor
## How to customize
### 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.
### Customizing Components
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
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.
Below are some examples of component customizations:
## More Examples
* Customize a component
* [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
* [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)
* 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
* [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)
* [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
* [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
* 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)
### Customizing Shortcut Events
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).
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
Please refer to the API documentation.
## 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
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.
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
@override
@ -27,7 +27,7 @@ At this point, nothing magic will happen after typing `_xxx_`.
![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
import 'package:appflowy_editor/appflowy_editor.dart';
@ -35,23 +35,25 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
// Since we only need to handler the input of `underscore`.
// All inputs except `underscore` will be ignored directly.
// Since we only need to handle the input of an 'underscore' character,
// all inputs except `underscore` will be ignored immediately.
if (event.logicalKey != LogicalKeyboardKey.underscore) {
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
// ...
FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
// ...
// Obtaining the selection and selected nodes of the current document through `selectionService`.
// And determine whether the selection is collapsed and whether the selected node is a text node.
// Obtain the selection and selected nodes of the current document through the 'selectionService'
// to determine whether the selection is collapsed and whether the selected node is a text node.
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
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
1. return, if not found.
2. if found, the text wrapped in between two underscores will be displayed in italic.
1. if one is _not_ found, return without doing anything.
2. if one is found, the text enclosed within the two underscores will be formatted to display in italics.
```dart
// ...
@ -73,14 +75,14 @@ FlowyKeyEventHandler underscoreToItalicHandler = (editorState, event) {
final textNode = textNodes.first;
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('_');
if (previousUnderscore == -1) {
return KeyEventResult.ignored;
}
// Delete the previous `underscore`,
// update the style of the text surrounded by two underscores to `italic`,
// Delete the previous 'underscore',
// update the style of the text surrounded by the two underscores to 'italic',
// and update the cursor position.
TransactionBuilder(editorState)
..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
@override
@ -120,14 +122,15 @@ Widget build(BuildContext context) {
![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
@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
{
@ -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
2. the other verifies the correctness of the [Node]().
2. the other verifies the correctness of the [Node](../lib/src/document/node.dart).
```dart
@ -179,9 +182,7 @@ class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
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]().**
> For the definition of the [Selectable](), please refer to this [link]().
Note that the `State` object that is returned by the `Widget` must implement [Selectable](../lib/src/render/selection/selectable.dart) using the `with` keyword.
```dart
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
class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
@ -256,6 +257,8 @@ class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
}
```
... and register `NetworkImageNodeWidgetBuilder` in the `AppFlowyEditor`.
```dart
final editorState = EditorState(
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
> 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
const text = 'Welcome to Appflowy 😁';
// Get the instance of editor.
// Get the instance of the editor.
final editor = tester.editor;
// Insert empty text node.
// Insert an empty text node.
editor.insertEmptyTextNode();
// Insert text node with string.
// Insert a text node with the text string we defined earlier.
editor.insertTextNode(text);
// Insert text node with heading style.
// Insert the same text, but with the heading style.
editor.insertTextNode(text, attributes: {
StyleKey.subtype: StyleKey.heading,
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(
'',
attributes: {
@ -30,40 +39,46 @@ 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
await editor.startTesting();
```
**Get the number of nodes in the document**
Get the number of nodes in the document.
```dart
final length = editor.documentLength;
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
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
await editor.updateSelection(
Selection.single(path: firstTextNode.path, startOffset: 0),
);
```
**Get the selection**
Get the current selection.
```dart
final selection = editor.documentSelection;
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
// Command + A.
// Meta + A.
await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true);
// Command + shift + S.
// Meta + shift + S.
await editor.pressLogicKey(
LogicalKeyboardKey.keyS,
isMetaPressed: true,
@ -71,25 +86,29 @@ await editor.pressLogicKey(
);
```
**Simulate a text input**
We will then simulate text input.
```dart
// Insert 'Hello World' at the beginning of the first node.
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
// Get plain text.
// Get the text of the first text node as plain text
final textAfterInserted = firstTextNode.toRawString();
print(textAfterInserted);
// Get attributes.
// Get the attributes of the text node
final attributes = firstTextNode.attributes;
print(attributes);
```
## Example
For example, we are going to test `select_all_handler.dart`
## A Complete Code Example
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
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]
/// 3. override the function `Widget build(NodeWidgetContext<Node> context)`
/// 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`
/// to verify the data structure in [Node].
@ -50,7 +50,8 @@ class ImageNodeWidget extends StatefulWidget {
State<ImageNodeWidget> createState() => _ImageNodeWidgetState();
}
class _ImageNodeWidgetState extends State<ImageNodeWidget> with Selectable {
class _ImageNodeWidgetState extends State<ImageNodeWidget>
with SelectableMixin {
bool isHovered = false;
Node get node => widget.node;
EditorState get editorState => widget.editorState;

View File

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

View File

@ -33,7 +33,7 @@ class LinkNodeWidget extends StatefulWidget {
}
class _YouTubeLinkNodeWidgetState extends State<LinkNodeWidget>
with Selectable {
with SelectableMixin {
Node get node => widget.node;
EditorState get editorState => widget.editorState;
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/selection_service.dart';
export 'src/service/scroll_service.dart';
export 'src/service/toolbar_service.dart';
export 'src/service/keyboard_service.dart';
export 'src/service/input_service.dart';

View File

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

View File

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

View File

@ -3,8 +3,6 @@ import 'dart:math';
import 'package:appflowy_editor/src/document/attributes.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import './attributes.dart';
// constant number: 2^53 - 1
const int _maxInt = 9007199254740991;
@ -463,7 +461,7 @@ class Delta extends Iterable<TextOperation> {
@override
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,15 +40,15 @@ class RichTextNodeWidget extends StatefulWidget {
// customize
class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
with Selectable, DefaultSelectable {
with SelectableMixin, DefaultSelectable {
@override
GlobalKey? get iconKey => null;
final _richTextKey = GlobalKey(debugLabel: 'rich_text');
@override
Selectable<StatefulWidget> get forward =>
_richTextKey.currentState as Selectable;
SelectableMixin<StatefulWidget> get forward =>
_richTextKey.currentState as SelectableMixin;
@override
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: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.
///
/// 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.
mixin Selectable<T extends StatefulWidget> on State<T> {
mixin SelectableMixin<T extends StatefulWidget> on State<T> {
/// Returns the [Selection] surrounded by start and end
/// in current widget.
///

View File

@ -510,7 +510,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
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));
return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height);
}

View File

@ -1,8 +1,13 @@
name: appflowy_editor
description: A highly customizable rich-text editor for Flutter
version: 0.0.3
version: 0.0.4
homepage: https://github.com/AppFlowy-IO/AppFlowy
platforms:
linux:
macos:
windows:
environment:
sdk: ">=2.17.0 <3.0.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/position.dart';
import 'package:appflowy_editor/src/document/selection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
@ -75,7 +74,7 @@ void main() {
final path2 = <int>[1];
expect(pathEquals(path1, path2), true);
expect(hashList(path1), hashList(path2));
expect(Object.hashAll(path1), Object.hashAll(path2));
});
test('test path utils 2', () {
@ -83,7 +82,7 @@ void main() {
final path2 = <int>[2];
expect(pathEquals(path1, path2), false);
expect(hashList(path1) != hashList(path2), true);
expect(Object.hashAll(path1) != Object.hashAll(path2), true);
});
test('test position comparator', () {

View File

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

View File

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

View File

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

View File

@ -91,6 +91,9 @@ pub struct GroupPB {
#[pb(index = 5)]
pub is_default: bool,
#[pb(index = 6)]
pub is_visible: bool,
}
impl std::convert::From<Group> for GroupPB {
@ -101,6 +104,7 @@ impl std::convert::From<Group> for GroupPB {
desc: group.name,
rows: group.rows,
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)]
pub struct CreateGridGroupPayloadPB {
pub struct InsertGroupPayloadPB {
#[pb(index = 1)]
pub field_id: String,
@ -134,22 +138,22 @@ pub struct CreateGridGroupPayloadPB {
pub field_type: FieldType,
}
impl TryInto<CreatGroupParams> for CreateGridGroupPayloadPB {
impl TryInto<InsertGroupParams> for InsertGroupPayloadPB {
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)
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0;
Ok(CreatGroupParams {
Ok(InsertGroupParams {
field_id,
field_type_rev: self.field_type.into(),
})
}
}
pub struct CreatGroupParams {
pub struct InsertGroupParams {
pub field_id: String,
pub field_type_rev: FieldTypeRevision,
}

View File

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

View File

@ -35,6 +35,32 @@ pub(crate) async fn get_grid_setting_handler(
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)]
pub(crate) async fn get_grid_blocks_handler(
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.
async fn get_type_option_data(field_rev: &FieldRevision, field_type: &FieldType) -> FlowyResult<Vec<u8>> {
let s = field_rev
.get_type_option_str(field_type)
.unwrap_or_else(|| default_type_option_builder_from_type(field_type).entry().json_str());
let s = field_rev.get_type_option_str(field_type).unwrap_or_else(|| {
default_type_option_builder_from_type(field_type)
.data_format()
.json_str()
});
let field_type: FieldType = field_rev.ty.into();
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)
}
@ -337,7 +365,7 @@ pub(crate) async fn update_select_option_handler(
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?;
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::GetGridBlocks, get_grid_blocks_handler)
.event(GridEvent::GetGridSetting, get_grid_setting_handler)
// .event(GridEvent::UpdateGridSetting, update_grid_setting_handler)
.event(GridEvent::UpdateGridSetting, update_grid_setting_handler)
// Field
.event(GridEvent::GetFields, get_fields_handler)
.event(GridEvent::UpdateField, update_field_handler)
@ -75,8 +75,8 @@ pub enum GridEvent {
/// [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.
#[event(input = "GridIdPB", input = "GridSettingChangesetPayloadPB")]
/// The event handler accepts [GridSettingChangesetPayloadPB] and return errors if failed to modify the grid's settings.
#[event(input = "GridSettingChangesetPayloadPB")]
UpdateGridSetting = 3,
/// [GetFields] event is used to get the grid's settings.
@ -225,4 +225,7 @@ pub enum GridEvent {
#[event(input = "MoveGroupRowPayloadPB")]
MoveGroupRow = 112,
#[event(input = "MoveGroupRowPayloadPB")]
GroupByField = 113,
}

View File

@ -30,7 +30,7 @@ macro_rules! impl_type_option {
($target: ident, $field_type:expr) => {
impl std::convert::From<&FieldRevision> for $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(),
Some(target) => target,
}
@ -39,7 +39,7 @@ macro_rules! impl_type_option {
impl std::convert::From<&std::sync::Arc<FieldRevision>> for $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(),
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 {
match serde_json::to_string(&self) {
Ok(s) => s,

View File

@ -101,25 +101,25 @@ pub fn try_decode_cell_data(
let field_type: FieldTypeRevision = t_field_type.into();
let data = match t_field_type {
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),
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),
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),
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),
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),
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),
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),
};
Some(data)

View File

@ -1,7 +1,7 @@
use crate::entities::{FieldPB, FieldType};
use crate::services::field::type_options::*;
use bytes::Bytes;
use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry};
use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataFormat};
use indexmap::IndexMap;
pub struct FieldBuilder {
@ -78,14 +78,14 @@ impl FieldBuilder {
pub fn build(self) -> FieldRevision {
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
}
}
pub trait TypeOptionBuilder {
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> {

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_operation;
pub(crate) mod type_options;
pub use field_builder::*;
pub use field_operation::*;
pub use type_options::*;

View File

@ -5,7 +5,7 @@ use crate::services::field::{BoxTypeOptionBuilder, CheckboxCellData, TypeOptionB
use bytes::Bytes;
use flowy_derive::ProtoBuf;
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 std::str::FromStr;
@ -26,7 +26,7 @@ impl TypeOptionBuilder for CheckboxTypeOptionBuilder {
FieldType::Checkbox
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0
}
}

View File

@ -9,7 +9,7 @@ use chrono::format::strftime::StrftimeItems;
use chrono::{NaiveDateTime, Timelike};
use flowy_derive::ProtoBuf;
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};
// Date
@ -189,7 +189,7 @@ impl TypeOptionBuilder for DateTypeOptionBuilder {
FieldType::DateTime
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0
}
}

View File

@ -6,7 +6,7 @@ use crate::services::field::{BoxTypeOptionBuilder, NumberCellData, TypeOptionBui
use bytes::Bytes;
use flowy_derive::ProtoBuf;
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;
@ -45,7 +45,7 @@ impl TypeOptionBuilder for NumberTypeOptionBuilder {
FieldType::Number
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0
}
}

View File

@ -9,7 +9,7 @@ use crate::services::field::{
use bytes::Bytes;
use flowy_derive::ProtoBuf;
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};
// Multiple select
@ -108,7 +108,7 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
FieldType::MultiSelect
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0
}
}

View File

@ -5,7 +5,7 @@ use bytes::Bytes;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::{internal_error, ErrorCode, FlowyResult};
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 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) {
let options = self.mut_options();
if let Some(index) = options

View File

@ -9,7 +9,7 @@ use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
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};
// Single select
@ -91,7 +91,7 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
FieldType::SingleSelect
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0
}
}

View File

@ -8,7 +8,7 @@ use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
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};
#[derive(Default)]
@ -21,7 +21,7 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder {
FieldType::RichText
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0
}
}

View File

@ -6,7 +6,7 @@ use bytes::Bytes;
use fancy_regex::Regex;
use flowy_derive::ProtoBuf;
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 serde::{Deserialize, Serialize};
@ -20,7 +20,7 @@ impl TypeOptionBuilder for URLTypeOptionBuilder {
FieldType::URL
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
fn data_format(&self) -> &dyn TypeOptionDataFormat {
&self.0
}
}

View File

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

View File

@ -179,6 +179,10 @@ impl GridRevisionEditor {
None => Err(ErrorCode::FieldDoesNotExist.into()),
Some(field_type) => {
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?;
Ok(())
}
@ -207,6 +211,11 @@ impl GridRevisionEditor {
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<()> {
// let block_ids = self
// .get_block_metas()
@ -221,7 +230,9 @@ impl GridRevisionEditor {
let type_option_json_builder = |field_type: &FieldTypeRevision| -> String {
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
@ -521,12 +532,20 @@ impl GridRevisionEditor {
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
}
pub async fn update_filter(&self, params: CreateFilterParams) -> FlowyResult<()> {
let _ = self.view_manager.update_filter(params).await?;
pub async fn create_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
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(())
}
@ -824,7 +843,7 @@ impl JsonDeserializer for TypeOptionJsonDeserializer {
fn deserialize(&self, type_option_data: Vec<u8>) -> CollaborateResult<String> {
// The type_option_data sent from Dart is serialized by protobuf.
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);
Ok(json)
}

View File

@ -1,12 +1,16 @@
use crate::dart_notification::{send_dart_notification, GridNotification};
use crate::entities::{
CreateFilterParams, CreateRowParams, DeleteFilterParams, GridFilterConfiguration, GridLayout, GridLayoutPB,
GridSettingPB, GroupChangesetPB, GroupPB, GroupViewChangesetPB, InsertedGroupPB, InsertedRowPB, MoveGroupParams,
RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RowPB,
CreateRowParams, DeleteFilterParams, DeleteGroupParams, GridFilterConfigurationPB, GridGroupConfigurationPB,
GridLayout, GridLayoutPB, GridSettingPB, GroupChangesetPB, GroupPB, GroupViewChangesetPB, InsertFilterParams,
InsertGroupParams, InsertedGroupPB, InsertedRowPB, MoveGroupParams, RepeatedGridFilterConfigurationPB,
RepeatedGridGroupConfigurationPB, RowPB,
};
use crate::services::grid_editor_task::GridServiceTaskScheduler;
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_grid_data_model::revision::{
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::entities::revision::Revision;
use lib_infra::future::{wrap_future, AFFuture, FutureResult};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::future::Future;
use std::sync::Arc;
use tokio::sync::RwLock;
@ -30,11 +32,9 @@ pub struct GridViewRevisionEditor {
rev_manager: Arc<RevisionManager>,
field_delegate: Arc<dyn GridViewFieldDelegate>,
row_delegate: Arc<dyn GridViewRowDelegate>,
group_service: Arc<RwLock<GroupService>>,
group_controller: Arc<RwLock<Box<dyn GroupController>>>,
scheduler: Arc<dyn GridServiceTaskScheduler>,
did_load_group: AtomicBool,
}
impl GridViewRevisionEditor {
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn new(
@ -52,16 +52,16 @@ impl GridViewRevisionEditor {
let view_revision_pad = rev_manager.load::<GridViewRevisionPadBuilder>(Some(cloud)).await?;
let pad = Arc::new(RwLock::new(view_revision_pad));
let rev_manager = Arc::new(rev_manager);
let configuration_reader = GroupConfigurationReaderImpl(pad.clone());
let configuration_writer = GroupConfigurationWriterImpl {
user_id: user_id.to_owned(),
rev_manager: rev_manager.clone(),
view_pad: pad.clone(),
};
let group_service = GroupService::new(view_id.clone(), configuration_reader, configuration_writer).await;
let group_controller = new_group_controller(
user_id.to_owned(),
view_id.clone(),
pad.clone(),
rev_manager.clone(),
field_delegate.clone(),
row_delegate.clone(),
)
.await?;
let user_id = user_id.to_owned();
let did_load_group = AtomicBool::new(false);
Ok(Self {
pad,
user_id,
@ -70,24 +70,21 @@ impl GridViewRevisionEditor {
scheduler,
field_delegate,
row_delegate,
group_service: Arc::new(RwLock::new(group_service)),
did_load_group,
group_controller: Arc::new(RwLock::new(group_controller)),
})
}
pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
match params.group_id.as_ref() {
None => {}
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;
}
if params.group_id.is_none() {
return;
}
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) {
@ -112,13 +109,11 @@ impl GridViewRevisionEditor {
pub(crate) async fn did_delete_row(&self, row_rev: &RowRevision) {
// Send the group notification if the current view has groups;
if let Some(changesets) = self
.group_service
.write()
.await
.did_delete_row(row_rev, |field_id| self.field_delegate.get_field_rev(&field_id))
.await
{
let changesets = self
.mut_group_controller(|group_controller, field_rev| group_controller.did_delete_row(row_rev, &field_rev))
.await;
if let Some(changesets) = changesets {
for changeset in changesets {
self.notify_did_update_group(changeset).await;
}
@ -126,13 +121,11 @@ impl GridViewRevisionEditor {
}
pub(crate) async fn did_update_row(&self, row_rev: &RowRevision) {
if let Some(changesets) = self
.group_service
.write()
.await
.did_update_row(row_rev, |field_id| self.field_delegate.get_field_rev(&field_id))
.await
{
let changesets = self
.mut_group_controller(|group_controller, field_rev| group_controller.did_update_row(row_rev, &field_rev))
.await;
if let Some(changesets) = changesets {
for changeset in changesets {
self.notify_did_update_group(changeset).await;
}
@ -146,54 +139,38 @@ impl GridViewRevisionEditor {
to_group_id: &str,
to_row_id: Option<String>,
) -> Vec<GroupChangesetPB> {
match self
.group_service
.write()
.await
.move_group_row(row_rev, row_changeset, to_group_id, to_row_id, |field_id| {
self.field_delegate.get_field_rev(&field_id)
let changesets = self
.mut_group_controller(|group_controller, field_rev| {
let move_row_context = MoveGroupRowContext {
row_rev,
row_changeset,
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
{
None => vec![],
Some(changesets) => changesets,
}
.await;
changesets.unwrap_or_default()
}
/// Only call once after grid view editor initialized
#[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn load_groups(&self) -> FlowyResult<Vec<GroupPB>> {
let groups = if !self.did_load_group.load(Ordering::SeqCst) {
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
};
let groups = self.group_controller.read().await.groups();
tracing::trace!("Number of groups: {}", groups.len());
Ok(groups.into_iter().map(GroupPB::from).collect())
}
pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
let _ = self
.group_service
.group_controller
.write()
.await
.move_group(&params.from_group_id, &params.to_group_id)
.await?;
match self.group_service.read().await.get_group(&params.from_group_id).await {
.move_group(&params.from_group_id, &params.to_group_id)?;
match self.group_controller.read().await.get_group(&params.from_group_id) {
None => {}
Some((index, group)) => {
let inserted_group = InsertedGroupPB {
@ -206,6 +183,7 @@ impl GridViewRevisionEditor {
inserted_groups: vec![inserted_group],
deleted_groups: vec![params.from_group_id.clone()],
update_groups: vec![],
new_groups: vec![],
};
self.notify_did_update_view(changeset).await;
@ -220,27 +198,52 @@ impl GridViewRevisionEditor {
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;
match self.pad.read().await.get_all_filters(&field_revs) {
None => vec![],
Some(filters) => filters
.into_values()
.flatten()
.map(|filter| GridFilterConfiguration::from(filter.as_ref()))
.map(|filter| GridFilterConfigurationPB::from(filter.as_ref()))
.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| {
let filter_rev = FilterConfigurationRevision {
id: gen_grid_filter_id(),
field_id: insert_filter.field_id.clone(),
condition: insert_filter.condition,
content: insert_filter.content,
field_id: params.field_id.clone(),
condition: params.condition,
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)
})
.await
@ -260,7 +263,7 @@ impl GridViewRevisionEditor {
#[tracing::instrument(level = "trace", skip_all, err)]
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 {
match self.group_service.write().await.did_update_field(&field_rev).await? {
match self.group_controller.write().await.did_update_field(&field_rev)? {
None => {}
Some(changeset) => {
self.notify_did_update_view(changeset).await;
@ -270,6 +273,44 @@ impl GridViewRevisionEditor {
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) {
send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup)
.payload(changeset)
@ -295,6 +336,78 @@ impl GridViewRevisionEditor {
}
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(
@ -335,13 +448,10 @@ impl RevisionObjectBuilder for GridViewRevisionPadBuilder {
struct GroupConfigurationReaderImpl(Arc<RwLock<GridViewRevisionPad>>);
impl GroupConfigurationReader for GroupConfigurationReaderImpl {
fn get_group_configuration(
&self,
field_rev: Arc<FieldRevision>,
) -> AFFuture<Option<Arc<GroupConfigurationRevision>>> {
fn get_configuration(&self) -> AFFuture<Option<Arc<GroupConfigurationRevision>>> {
let view_pad = self.0.clone();
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() {
None
} else {
@ -359,7 +469,7 @@ struct GroupConfigurationWriterImpl {
}
impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
fn save_group_configuration(
fn save_configuration(
&self,
field_id: &str,
field_type: FieldTypeRevision,
@ -385,31 +495,40 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
}
pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc<FieldRevision>]) -> GridSettingPB {
let current_layout_type: GridLayout = view_pad.layout.clone().into();
let filters_by_field_id = view_pad
let layout_type: GridLayout = view_pad.layout.clone().into();
let filter_configurations = view_pad
.get_all_filters(field_revs)
.map(|filters_by_field_id| {
filters_by_field_id
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect::<HashMap<String, RepeatedGridConfigurationFilterPB>>()
.map(|(_, v)| {
let repeated_filter: RepeatedGridFilterConfigurationPB = v.into();
repeated_filter.items
})
.flatten()
.collect::<Vec<GridFilterConfigurationPB>>()
})
.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| {
groups_by_field_id
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect::<HashMap<String, RepeatedGridGroupConfigurationPB>>()
.map(|(_, v)| {
let repeated_group: RepeatedGridGroupConfigurationPB = v.into();
repeated_group.items
})
.flatten()
.collect::<Vec<GridGroupConfigurationPB>>()
})
.unwrap_or_default();
GridSettingPB {
layouts: GridLayoutPB::all(),
current_layout_type,
filter_configuration_by_field_id: filters_by_field_id,
group_configuration_by_field_id: groups_by_field_id,
layout_type,
filter_configurations: filter_configurations.into(),
group_configurations: group_configurations.into(),
}
}

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