mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'main' into feat/flowy-overlay
This commit is contained in:
commit
bd40768c6a
11
.github/workflows/dart_lint.yml
vendored
11
.github/workflows/dart_lint.yml
vendored
@ -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
|
||||
|
5
.github/workflows/dart_test.yml
vendored
5
.github/workflows/dart_test.yml
vendored
@ -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
|
||||
|
14
.github/workflows/flowy_editor_test.yml
vendored
14
.github/workflows/flowy_editor_test.yml
vendored
@ -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
|
||||
|
||||
|
14
.github/workflows/rust_lint.yml
vendored
14
.github/workflows/rust_lint.yml
vendored
@ -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
|
||||
|
11
.github/workflows/rust_test.yml
vendored
11
.github/workflows/rust_test.yml
vendored
@ -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
|
||||
|
7
frontend/app_flowy/assets/images/grid/setting/group.svg
Normal file
7
frontend/app_flowy/assets/images/grid/setting/group.svg
Normal 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 |
@ -160,7 +160,8 @@
|
||||
"settings": {
|
||||
"filter": "Filter",
|
||||
"sortBy": "Sort by",
|
||||
"Properties": "Properties"
|
||||
"Properties": "Properties",
|
||||
"group": "Group"
|
||||
},
|
||||
"field": {
|
||||
"hide": "Hide",
|
||||
|
@ -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));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -43,4 +43,5 @@ class BoardSettingState with _$BoardSettingState {
|
||||
|
||||
enum BoardSettingAction {
|
||||
properties,
|
||||
groups,
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
);
|
||||
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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}) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
|
@ -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());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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_`.
|
||||
|
||||

|
||||
|
||||
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) {
|
||||
|
||||

|
||||
|
||||
[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(
|
||||
);
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
[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 |
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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> {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
///
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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', () {
|
||||
|
@ -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:
|
||||
|
@ -13,6 +13,8 @@ pub enum GridNotification {
|
||||
DidUpdateField = 50,
|
||||
DidUpdateGroupView = 60,
|
||||
DidUpdateGroup = 61,
|
||||
DidGroupByNewField = 62,
|
||||
DidUpdateGridSetting = 70,
|
||||
}
|
||||
|
||||
impl std::default::Default for GridNotification {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>,
|
||||
}
|
||||
|
||||
|
@ -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(¶ms.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 {
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
}
|
@ -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::*;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(¶ms.from_group_id, ¶ms.to_group_id)
|
||||
.await?;
|
||||
|
||||
match self.group_service.read().await.get_group(¶ms.from_group_id).await {
|
||||
.move_group(¶ms.from_group_id, ¶ms.to_group_id)?;
|
||||
match self.group_controller.read().await.get_group(¶ms.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(¶ms.field_id).await {
|
||||
let _ = self
|
||||
.modify(|pad| {
|
||||
let configuration = default_group_configuration(&field_rev);
|
||||
let changeset = pad.insert_group(¶ms.field_id, ¶ms.field_type_rev, configuration)?;
|
||||
Ok(changeset)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
if self.group_controller.read().await.field_id() != params.field_id {
|
||||
let _ = self.group_by_field(¶ms.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(¶ms.field_id, ¶ms.field_type_rev, ¶ms.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(¶ms.field_id, ¶ms.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
Loading…
x
Reference in New Issue
Block a user