Merge branch 'main' into feat/flowy-overlay

This commit is contained in:
Nathan.fooo 2022-09-06 09:46:10 +08:00 committed by GitHub
commit f54f90b647
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 762 additions and 199 deletions

View File

@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
@ -12,7 +14,6 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:collection';
import 'board_data_controller.dart';
import 'group_controller.dart';
@ -164,12 +165,17 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
boardController.clear();
//
List<AFBoardColumnData> columns = groups.map((group) {
List<AFBoardColumnData> columns = groups
.where((group) => fieldController.getField(group.fieldId) != null)
.map((group) {
return AFBoardColumnData(
id: group.groupId,
name: group.desc,
items: _buildRows(group),
customData: group,
customData: BoardCustomData(
group: group,
fieldContext: fieldController.getField(group.fieldId)!,
),
);
}).toList();
boardController.addColumns(columns);
@ -177,6 +183,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
for (final group in groups) {
final delegate = GroupControllerDelegateImpl(
controller: boardController,
fieldController: fieldController,
onNewColumnItem: (groupId, row, index) {
add(BoardEvent.didCreateRow(groupId, row, index));
},
@ -238,10 +245,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
List<AFColumnItem> _buildRows(GroupPB group) {
final items = group.rows.map((row) {
return BoardColumnItem(
row: row,
fieldId: group.fieldId,
);
final fieldContext = fieldController.getField(group.fieldId);
return BoardColumnItem(row: row, fieldContext: fieldContext!);
}).toList();
return <AFColumnItem>[...items];
@ -332,15 +337,11 @@ class GridFieldEquatable extends Equatable {
class BoardColumnItem extends AFColumnItem {
final RowPB row;
final String fieldId;
final bool requestFocus;
final GridFieldContext fieldContext;
BoardColumnItem({
required this.row,
required this.fieldId,
this.requestFocus = false,
required this.fieldContext,
});
@override
@ -348,24 +349,29 @@ class BoardColumnItem extends AFColumnItem {
}
class GroupControllerDelegateImpl extends GroupControllerDelegate {
final GridFieldController fieldController;
final AFBoardDataController controller;
final void Function(String, RowPB, int?) onNewColumnItem;
GroupControllerDelegateImpl({
required this.controller,
required this.fieldController,
required this.onNewColumnItem,
});
@override
void insertRow(GroupPB group, RowPB row, int? index) {
final fieldContext = fieldController.getField(group.fieldId);
if (fieldContext == null) {
Log.warn("FieldContext should not be null");
return;
}
if (index != null) {
final item = BoardColumnItem(row: row, fieldId: group.fieldId);
final item = BoardColumnItem(row: row, fieldContext: fieldContext);
controller.insertColumnItem(group.groupId, index, item);
} else {
final item = BoardColumnItem(
row: row,
fieldId: group.fieldId,
);
final item = BoardColumnItem(row: row, fieldContext: fieldContext);
controller.addColumnItem(group.groupId, item);
}
}
@ -377,22 +383,25 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
@override
void updateRow(GroupPB group, RowPB row) {
final fieldContext = fieldController.getField(group.fieldId);
if (fieldContext == null) {
Log.warn("FieldContext should not be null");
return;
}
controller.updateColumnItem(
group.groupId,
BoardColumnItem(
row: row,
fieldId: group.fieldId,
),
BoardColumnItem(row: row, fieldContext: fieldContext),
);
}
@override
void addNewRow(GroupPB group, RowPB row, int? index) {
final item = BoardColumnItem(
row: row,
fieldId: group.fieldId,
requestFocus: true,
);
final fieldContext = fieldController.getField(group.fieldId);
if (fieldContext == null) {
Log.warn("FieldContext should not be null");
return;
}
final item = BoardColumnItem(row: row, fieldContext: fieldContext);
if (index != null) {
controller.insertColumnItem(group.groupId, index, item);
@ -414,3 +423,29 @@ class BoardEditingRow {
required this.index,
});
}
class BoardCustomData {
final GroupPB group;
final GridFieldContext fieldContext;
BoardCustomData({
required this.group,
required this.fieldContext,
});
CheckboxGroup? asCheckboxGroup() {
if (fieldType != FieldType.Checkbox) return null;
return CheckboxGroup(group);
}
FieldType get fieldType => fieldContext.fieldType;
}
class CheckboxGroup {
final GroupPB group;
CheckboxGroup(this.group);
// Hardcode value: "Yes" that equal to the value defined in Rust
// pub const CHECK: &str = "Yes";
bool get isCheck => group.groupId == "Yes";
}

View File

@ -18,7 +18,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
import 'package:flowy_infra_ui/widget/error_page.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/group.pbserver.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 '../../grid/application/row/row_cache.dart';
@ -37,8 +37,7 @@ class BoardPage extends StatelessWidget {
create: (context) =>
BoardBloc(view: view)..add(const BoardEvent.initial()),
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (previous, current) =>
previous.loadingState != current.loadingState,
buildWhen: (p, c) => p.loadingState != c.loadingState,
builder: (context, state) {
return state.loadingState.map(
loading: (_) =>
@ -85,36 +84,15 @@ class _BoardContentState extends State<BoardContent> {
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
builder: (context, state) {
final theme = context.read<AppTheme>();
final column = Column(
children: [const _ToolbarBlocAdaptor(), _buildBoard(context)],
);
return Container(
color: theme.surface,
color: context.read<AppTheme>().surface,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
const _ToolbarBlocAdaptor(),
Expanded(
child: AFBoard(
key: UniqueKey(),
scrollManager: scrollManager,
scrollController: scrollController,
dataController: context.read<BoardBloc>().boardController,
headerBuilder: _buildHeader,
footBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
columnConstraints:
const BoxConstraints.tightFor(width: 300),
config: AFBoardConfig(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
),
),
),
],
),
child: column,
),
);
},
@ -122,6 +100,27 @@ class _BoardContentState extends State<BoardContent> {
);
}
Expanded _buildBoard(BuildContext context) {
return Expanded(
child: AFBoard(
scrollManager: scrollManager,
scrollController: scrollController,
dataController: context.read<BoardBloc>().boardController,
headerBuilder: _buildHeader,
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
columnConstraints: const BoxConstraints.tightFor(width: 300),
config: AFBoardConfig(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
),
),
);
}
void _handleEditState(BoardState state, BuildContext context) {
state.editingRow.fold(
() => null,
@ -153,6 +152,7 @@ class _BoardContentState extends State<BoardContent> {
BuildContext context,
AFBoardColumnData columnData,
) {
final boardCustomData = columnData.customData as BoardCustomData;
return AppFlowyColumnHeader(
title: Flexible(
fit: FlexFit.tight,
@ -163,6 +163,7 @@ class _BoardContentState extends State<BoardContent> {
color: context.read<AppTheme>().textColor,
),
),
icon: _buildHeaderIcon(boardCustomData),
addIcon: SizedBox(
height: 20,
width: 20,
@ -182,7 +183,9 @@ class _BoardContentState extends State<BoardContent> {
}
Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) {
final group = columnData.customData as GroupPB;
final boardCustomData = columnData.customData as BoardCustomData;
final group = boardCustomData.group;
if (group.isDefault) {
return const SizedBox();
} else {
@ -247,7 +250,7 @@ class _BoardContentState extends State<BoardContent> {
child: BoardCard(
gridId: gridId,
groupId: column.id,
fieldId: boardColumnItem.fieldId,
fieldId: boardColumnItem.fieldContext.id,
isEditing: isEditing,
cellBuilder: cellBuilder,
dataController: cardController,
@ -325,3 +328,38 @@ extension HexColor on Color {
return Color(int.parse(buffer.toString(), radix: 16));
}
}
Widget? _buildHeaderIcon(BoardCustomData customData) {
Widget? widget;
switch (customData.fieldType) {
case FieldType.Checkbox:
final group = customData.asCheckboxGroup()!;
if (group.isCheck) {
widget = svgWidget('editor/editor_check');
} else {
widget = svgWidget('editor/editor_uncheck');
}
break;
case FieldType.DateTime:
break;
case FieldType.MultiSelect:
break;
case FieldType.Number:
break;
case FieldType.RichText:
break;
case FieldType.SingleSelect:
break;
case FieldType.URL:
break;
}
if (widget != null) {
widget = SizedBox(
width: 20,
height: 20,
child: widget,
);
}
return widget;
}

View File

@ -14,33 +14,37 @@ class EditableCellNotifier {
}
class EditableRowNotifier {
Map<EditableCellId, EditableCellNotifier> cells = {};
final Map<EditableCellId, EditableCellNotifier> _cells = {};
void insertCell(
GridCellIdentifier cellIdentifier,
EditableCellNotifier notifier,
) {
cells[EditableCellId.from(cellIdentifier)] = notifier;
_cells[EditableCellId.from(cellIdentifier)] = notifier;
}
void becomeFirstResponder() {
for (final notifier in cells.values) {
for (final notifier in _cells.values) {
notifier.becomeFirstResponder.notify();
}
}
void resignFirstResponder() {
for (final notifier in cells.values) {
for (final notifier in _cells.values) {
notifier.resignFirstResponder.notify();
}
}
void clear() {
_cells.clear();
}
void dispose() {
for (final notifier in cells.values) {
for (final notifier in _cells.values) {
notifier.resignFirstResponder.notify();
}
cells.clear();
_cells.clear();
}
}

View File

@ -4,7 +4,6 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.da
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'board_cell.dart';
import 'define.dart';

View File

@ -89,20 +89,20 @@ class _BoardCardState extends State<BoardCard> {
List<GridCellIdentifier> cells,
) {
final List<Widget> children = [];
rowNotifier.clear();
cells.asMap().forEach(
(int index, GridCellIdentifier cellId) {
final cellNotifier = EditableCellNotifier();
Widget child = widget.cellBuilder.buildCell(
widget.groupId,
cellId,
widget.isEditing,
index == 0 ? widget.isEditing : false,
cellNotifier,
);
if (index == 0) {
rowNotifier.insertCell(cellId, cellNotifier);
}
child = Padding(
key: cellId.key(),
padding: const EdgeInsets.only(left: 4, right: 4),

View File

@ -7,6 +7,7 @@ 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/group.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
import 'package:flutter/foundation.dart';
import '../row/row_cache.dart';
@ -35,12 +36,12 @@ class GridFieldController {
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;
_GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
final Map<String, GridGroupConfigurationPB> _configurationByFieldId = {};
List<GridFieldContext> get fieldContexts =>
[..._fieldNotifier?.fieldContexts ?? []];
@ -67,31 +68,43 @@ class GridFieldController {
//Listen on setting changes
_settingListener.start(onSettingUpdated: (result) {
result.fold(
(setting) => _updateFieldsWhenSettingChanged(setting),
(setting) => _updateGroupConfiguration(setting),
(r) => Log.error(r),
);
});
_settingFFIService.getSetting().then((result) {
result.fold(
(setting) => _updateFieldsWhenSettingChanged(setting),
(setting) => _updateGroupConfiguration(setting),
(err) => Log.error(err),
);
});
}
void _updateFieldsWhenSettingChanged(GridSettingPB setting) {
_groupFieldIds = setting.groupConfigurations.items
.map((item) => item.groupFieldId)
GridFieldContext? getField(String fieldId) {
final fields = _fieldNotifier?.fieldContexts
.where(
(element) => element.id == fieldId,
)
.toList();
if (fields?.isEmpty ?? true) {
return null;
}
return fields!.first;
}
void _updateGroupConfiguration(GridSettingPB setting) {
_configurationByFieldId.clear();
for (final configuration in setting.groupConfigurations.items) {
_configurationByFieldId[configuration.fieldId] = configuration;
}
_updateFieldContexts();
}
void _updateFieldContexts() {
if (_fieldNotifier != null) {
for (var field in _fieldNotifier!.fieldContexts) {
if (_groupFieldIds.contains(field.id)) {
if (_configurationByFieldId[field.id] != null) {
field._isGroupField = true;
} else {
field._isGroupField = false;
@ -277,5 +290,26 @@ class GridFieldContext {
bool get isGroupField => _isGroupField;
bool get canGroup {
switch (_field.fieldType) {
case FieldType.Checkbox:
return true;
case FieldType.DateTime:
return false;
case FieldType.MultiSelect:
return true;
case FieldType.Number:
return false;
case FieldType.RichText:
return false;
case FieldType.SingleSelect:
return true;
case FieldType.URL:
return false;
}
return false;
}
GridFieldContext({required FieldPB field}) : _field = field;
}

View File

@ -31,10 +31,15 @@ class GridGroupList extends StatelessWidget {
child: BlocBuilder<GridGroupBloc, GridGroupState>(
builder: (context, state) {
final cells = state.fieldContexts.map((fieldContext) {
return _GridGroupCell(
Widget cell = _GridGroupCell(
fieldContext: fieldContext,
key: ValueKey(fieldContext.id),
);
if (!fieldContext.canGroup) {
cell = IgnorePointer(child: Opacity(opacity: 0.3, child: cell));
}
return cell;
}).toList();
return ListView.separated(

View File

@ -1,87 +1,71 @@
# appflowy_board
The **appflowy_board** is a package that is used in [AppFlowy](https://github.com/AppFlowy-IO/AppFlowy). For the moment, this package is iterated very fast.
<h1 align="center"><b>AppFlowy Board</b></h1>
<p align="center">A customizable and draggable Kanban Board widget for Flutter</p>
**appflowy_board** will be a standard git repository when it becomes stable.
## Getting Started
<p align="center">
<a href="https://discord.gg/ZCCYN4Anzq"><b>Discord</b></a>
<a href="https://twitter.com/appflowy"><b>Twitter</b></a>
</p>
<p>
<img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_2.gif?raw=true" width="680" title="AppFlowyBoard">
<p align="center">
<img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_1.gif?raw=true" width="680" title="AppFlowyBoard">
</p>
## Intro
appflowy_board is a customizable and draggable Kanban Board widget for Flutter.
You can use it to create a Kanban Board tool like those in Trello.
Check out [AppFlowy](https://github.com/AppFlowy-IO/AppFlowy) to see how appflowy_board is used to build a BoardView database.
<p align="center">
<img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_2.gif?raw=true" width="680" title="AppFlowyBoard">
</p>
## Getting Started
Add the AppFlowy Board [Flutter package](https://docs.flutter.dev/development/packages-and-plugins/using-packages) to your environment.
With Flutter:
```dart
@override
void initState() {
final column1 = BoardColumnData(id: "To Do", items: [
TextItem("Card 1"),
TextItem("Card 2"),
TextItem("Card 3"),
TextItem("Card 4"),
]);
final column2 = BoardColumnData(id: "In Progress", items: [
TextItem("Card 5"),
TextItem("Card 6"),
]);
flutter pub add appflowy_board
```
final column3 = BoardColumnData(id: "Done", items: []);
This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):
```dart
dependencies:
appflowy_board: ^0.0.6
```
Import the package in your Dart file:
```dart
import 'package:appflowy_board/appflowy_board.dart';
```
## Usage Example
To quickly grasp how it can be used, look at the /example/lib folder.
First, run main.dart to play with the demo.
Second, let's delve into multi_board_list_example.dart to understand a few key components:
* A Board widget is created via instantiating an AFBoard() object.
* In the AFBoard() object, you can find:
* AFBoardDataController, which is defined in board_data.dart, is feeded with prepopulated mock data. It also contains callback functions to materialize future user data.
* Three builders: AppFlowyColumnHeader, AppFlowyColumnFooter, AppFlowyColumnItemCard. See below image for what they are used for.
<p>
<img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_builders.jpg?raw=true" width="100%" title="AppFlowyBoard">
</p>
## Glossary
Please refer to the API documentation.
## Contributing
Contributions are what make the open source community such an amazing place to 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](https://github.com/AppFlowy-IO/AppFlowy-Docs/blob/main/LICENSE) for more information.
boardDataController.addColumn(column1);
boardDataController.addColumn(column2);
boardDataController.addColumn(column3);
super.initState();
}
@override
Widget build(BuildContext context) {
final config = BoardConfig(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
);
return Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Board(
dataController: boardDataController,
footBuilder: (context, columnData) {
return AppFlowyColumnFooter(
icon: const Icon(Icons.add, size: 20),
title: const Text('New'),
height: 50,
margin: config.columnItemPadding,
);
},
headerBuilder: (context, columnData) {
return AppFlowyColumnHeader(
icon: const Icon(Icons.lightbulb_circle),
title: Text(columnData.id),
addIcon: const Icon(Icons.add, size: 20),
moreIcon: const Icon(Icons.more_horiz, size: 20),
height: 50,
margin: config.columnItemPadding,
);
},
cardBuilder: (context, item) {
final textItem = item as TextItem;
return AppFlowyColumnItemCard(
key: ObjectKey(item),
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(textItem.s),
),
),
);
},
columnConstraints: const BoxConstraints.tightFor(width: 240),
config: BoardConfig(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
),
),
),
);
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -66,7 +66,7 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: AFBoard(
dataController: boardDataController,
footBuilder: (context, columnData) {
footerBuilder: (context, columnData) {
return AppFlowyColumnFooter(
icon: const Icon(Icons.add, size: 20),
title: const Text('New'),

View File

@ -56,7 +56,7 @@ class AFBoard extends StatelessWidget {
final AFBoardColumnHeaderBuilder? headerBuilder;
///
final AFBoardColumnFooterBuilder? footBuilder;
final AFBoardColumnFooterBuilder? footerBuilder;
///
final AFBoardDataController dataController;
@ -78,7 +78,7 @@ class AFBoard extends StatelessWidget {
required this.dataController,
required this.cardBuilder,
this.background,
this.footBuilder,
this.footerBuilder,
this.headerBuilder,
this.scrollController,
this.scrollManager,
@ -112,7 +112,7 @@ class AFBoard extends StatelessWidget {
delegate: phantomController,
columnConstraints: columnConstraints,
cardBuilder: cardBuilder,
footBuilder: footBuilder,
footBuilder: footerBuilder,
headerBuilder: headerBuilder,
phantomController: phantomController,
onReorder: dataController.moveColumn,

View File

@ -31,7 +31,7 @@ typedef AFBoardColumnCardBuilder = Widget Function(
typedef AFBoardColumnHeaderBuilder = Widget? Function(
BuildContext context,
AFBoardColumnData headerData,
AFBoardColumnData columnData,
);
typedef AFBoardColumnFooterBuilder = Widget Function(

View File

@ -44,14 +44,14 @@ pub struct GridGroupConfigurationPB {
pub id: String,
#[pb(index = 2)]
pub group_field_id: String,
pub field_id: String,
}
impl std::convert::From<&GroupConfigurationRevision> for GridGroupConfigurationPB {
fn from(rev: &GroupConfigurationRevision) -> Self {
GridGroupConfigurationPB {
id: rev.id.clone(),
group_field_id: rev.field_id.clone(),
field_id: rev.field_id.clone(),
}
}
}

View File

@ -107,7 +107,7 @@ impl GridBlockManager {
let editor = self.get_editor_from_row_id(&changeset.row_id).await?;
let _ = editor.update_row(changeset.clone()).await?;
match editor.get_row_rev(&changeset.row_id).await? {
None => tracing::error!("Internal error: can't find the row with id: {}", changeset.row_id),
None => tracing::error!("Update row failed, can't find the row with id: {}", changeset.row_id),
Some(row_rev) => {
let row_pb = make_row_from_row_rev(row_rev.clone());
let block_order_changeset = GridBlockChangesetPB::update(&editor.block_id, vec![row_pb]);

View File

@ -182,7 +182,6 @@ pub fn delete_select_option_cell(option_id: String, field_rev: &FieldRevision) -
CellRevision::new(data)
}
/// If the cell data is not String type, it should impl this trait.
/// Deserialize the String into cell specific data type.
pub trait FromCellString {
fn from_cell_str(s: &str) -> FlowyResult<Self>

View File

@ -96,6 +96,8 @@ impl GridViewRevisionEditor {
None => Some(0),
Some(_) => None,
};
self.group_controller.write().await.did_create_row(row_pb, group_id);
let inserted_row = InsertedRowPB {
row: row_pb.clone(),
index,

View File

@ -1,13 +1,16 @@
use crate::entities::GroupChangesetPB;
use crate::services::group::controller::MoveGroupRowContext;
use flowy_grid_data_model::revision::RowRevision;
use flowy_grid_data_model::revision::{CellRevision, RowRevision};
pub trait GroupAction: Send + Sync {
type CellDataType;
fn default_cell_rev(&self) -> Option<CellRevision> {
None
}
fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool;
fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB>;
fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB>;
fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec<GroupChangesetPB>;
}

View File

@ -7,7 +7,6 @@ use flowy_error::FlowyResult;
use flowy_grid_data_model::revision::{
FieldRevision, GroupConfigurationContentSerde, GroupRevision, RowChangeset, RowRevision, TypeOptionDataDeserializer,
};
use std::marker::PhantomData;
use std::sync::Arc;
@ -16,6 +15,7 @@ use std::sync::Arc;
// a new row.
pub trait GroupController: GroupControllerSharedOperation + Send + Sync {
fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str);
fn did_create_row(&mut self, row_pb: &RowPB, group_id: &str);
}
pub trait GroupGenerator {
@ -193,9 +193,14 @@ where
#[tracing::instrument(level = "trace", skip_all, fields(row_count=%row_revs.len(), group_result))]
fn fill_groups(&mut self, row_revs: &[Arc<RowRevision>], field_rev: &FieldRevision) -> FlowyResult<()> {
for row_rev in row_revs {
if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
let cell_rev = match row_rev.cells.get(&self.field_id) {
None => self.default_cell_rev(),
Some(cell_rev) => Some(cell_rev.clone()),
};
if let Some(cell_rev) = cell_rev {
let mut grouped_rows: Vec<GroupedRow> = vec![];
let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev);
let cell_bytes = decode_any_cell_data(cell_rev.data, field_rev);
let cell_data = cell_bytes.parser::<P>()?;
for group in self.group_ctx.concrete_groups() {
if self.can_group(&group.filter_content, &cell_data) {

View File

@ -1,4 +1,4 @@
use crate::entities::GroupChangesetPB;
use crate::entities::{GroupChangesetPB, InsertedRowPB, RowPB};
use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK};
use crate::services::group::action::GroupAction;
use crate::services::group::configuration::GroupContext;
@ -6,8 +6,11 @@ use crate::services::group::controller::{
GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext,
};
use crate::services::group::GeneratedGroup;
use flowy_grid_data_model::revision::{CheckboxGroupConfigurationRevision, FieldRevision, GroupRevision, RowRevision};
use crate::services::cell::insert_checkbox_cell;
use crate::services::group::{move_group_row, GeneratedGroup};
use flowy_grid_data_model::revision::{
CellRevision, CheckboxGroupConfigurationRevision, FieldRevision, GroupRevision, RowRevision,
};
pub type CheckboxGroupController = GenericGroupController<
CheckboxGroupConfigurationRevision,
@ -20,30 +23,83 @@ pub type CheckboxGroupContext = GroupContext<CheckboxGroupConfigurationRevision>
impl GroupAction for CheckboxGroupController {
type CellDataType = CheckboxCellData;
fn can_group(&self, _content: &str, _cell_data: &Self::CellDataType) -> bool {
false
fn default_cell_rev(&self) -> Option<CellRevision> {
Some(CellRevision::new(UNCHECK.to_string()))
}
fn add_row_if_match(&mut self, _row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
todo!()
fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool {
if cell_data.is_check() {
content == CHECK
} else {
content == UNCHECK
}
}
fn remove_row_if_match(
&mut self,
_row_rev: &RowRevision,
_cell_data: &Self::CellDataType,
) -> Vec<GroupChangesetPB> {
todo!()
fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
let mut changesets = vec![];
self.group_ctx.iter_mut_groups(|group| {
let mut changeset = GroupChangesetPB::new(group.id.clone());
let is_contained = group.contains_row(&row_rev.id);
if group.id == CHECK && cell_data.is_check() {
if !is_contained {
let row_pb = RowPB::from(row_rev);
changeset.inserted_rows.push(InsertedRowPB::new(row_pb.clone()));
group.add_row(row_pb);
}
} else if is_contained {
changeset.deleted_rows.push(row_rev.id.clone());
group.remove_row(&row_rev.id);
}
if !changeset.is_empty() {
changesets.push(changeset);
}
});
changesets
}
fn move_row(&mut self, _cell_data: &Self::CellDataType, _context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
todo!()
fn remove_row_if_match(&mut self, row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
let mut changesets = vec![];
self.group_ctx.iter_mut_groups(|group| {
let mut changeset = GroupChangesetPB::new(group.id.clone());
if group.contains_row(&row_rev.id) {
changeset.deleted_rows.push(row_rev.id.clone());
group.remove_row(&row_rev.id);
}
if !changeset.is_empty() {
changesets.push(changeset);
}
});
changesets
}
fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
let mut group_changeset = vec![];
self.group_ctx.iter_mut_groups(|group| {
if let Some(changeset) = move_group_row(group, &mut context) {
group_changeset.push(changeset);
}
});
group_changeset
}
}
impl GroupController for CheckboxGroupController {
fn will_create_row(&mut self, _row_rev: &mut RowRevision, _field_rev: &FieldRevision, _group_id: &str) {
todo!()
fn will_create_row(&mut self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) {
match self.group_ctx.get_group(group_id) {
None => tracing::warn!("Can not find the group: {}", group_id),
Some((_, group)) => {
let is_check = group.id == CHECK;
let cell_rev = insert_checkbox_cell(is_check, field_rev);
row_rev.cells.insert(field_rev.id.clone(), cell_rev);
}
}
}
fn did_create_row(&mut self, row_pb: &RowPB, group_id: &str) {
if let Some(group) = self.group_ctx.get_mut_group(group_id) {
group.add_row(row_pb.clone())
}
}
}
@ -58,13 +114,13 @@ impl GroupGenerator for CheckboxGroupGenerator {
_type_option: &Option<Self::TypeOptionType>,
) -> Vec<GeneratedGroup> {
let check_group = GeneratedGroup {
group_rev: GroupRevision::new("true".to_string(), CHECK.to_string()),
filter_content: "".to_string(),
group_rev: GroupRevision::new(CHECK.to_string(), "".to_string()),
filter_content: CHECK.to_string(),
};
let uncheck_group = GeneratedGroup {
group_rev: GroupRevision::new("false".to_string(), UNCHECK.to_string()),
filter_content: "".to_string(),
group_rev: GroupRevision::new(UNCHECK.to_string(), "".to_string()),
filter_content: UNCHECK.to_string(),
};
vec![check_group, uncheck_group]
}

View File

@ -77,4 +77,6 @@ impl GroupControllerSharedOperation for DefaultGroupController {
impl GroupController for DefaultGroupController {
fn will_create_row(&mut self, _row_rev: &mut RowRevision, _field_rev: &FieldRevision, _group_id: &str) {}
fn did_create_row(&mut self, _row_rev: &RowPB, _group_id: &str) {}
}

View File

@ -1,4 +1,4 @@
use crate::entities::GroupChangesetPB;
use crate::entities::{GroupChangesetPB, RowPB};
use crate::services::cell::insert_select_option_cell;
use crate::services::field::{MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser};
use crate::services::group::action::GroupAction;
@ -46,10 +46,10 @@ impl GroupAction for MultiSelectGroupController {
changesets
}
fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
let mut group_changeset = vec![];
self.group_ctx.iter_mut_groups(|group| {
if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) {
if let Some(changeset) = move_group_row(group, &mut context) {
group_changeset.push(changeset);
}
});
@ -67,6 +67,12 @@ impl GroupController for MultiSelectGroupController {
}
}
}
fn did_create_row(&mut self, row_pb: &RowPB, group_id: &str) {
if let Some(group) = self.group_ctx.get_mut_group(group_id) {
group.add_row(row_pb.clone())
}
}
}
pub struct MultiSelectGroupGenerator();

View File

@ -46,10 +46,10 @@ impl GroupAction for SingleSelectGroupController {
changesets
}
fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
let mut group_changeset = vec![];
self.group_ctx.iter_mut_groups(|group| {
if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) {
if let Some(changeset) = move_group_row(group, &mut context) {
group_changeset.push(changeset);
}
});
@ -65,10 +65,14 @@ impl GroupController for SingleSelectGroupController {
Some(group) => {
let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
row_rev.cells.insert(field_rev.id.clone(), cell_rev);
group.add_row(RowPB::from(row_rev));
}
}
}
fn did_create_row(&mut self, row_pb: &RowPB, group_id: &str) {
if let Some(group) = self.group_ctx.get_mut_group(group_id) {
group.add_row(row_pb.clone())
}
}
}
pub struct SingleSelectGroupGenerator();

View File

@ -62,11 +62,7 @@ pub fn remove_select_option_row(
}
}
pub fn move_select_option_row(
group: &mut Group,
_cell_data: &SelectOptionCellDataPB,
context: &mut MoveGroupRowContext,
) -> Option<GroupChangesetPB> {
pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> Option<GroupChangesetPB> {
let mut changeset = GroupChangesetPB::new(group.id.clone());
let MoveGroupRowContext {
row_rev,

View File

@ -0,0 +1,387 @@
use crate::core::{Delta, DeltaIterator};
use crate::rich_text::{is_block, RichTextAttributeKey, RichTextAttributeValue, RichTextAttributes};
use std::collections::HashMap;
const LINEFEEDASCIICODE: i32 = 0x0A;
#[cfg(test)]
mod tests {
use crate::codec::markdown::markdown_encoder::markdown_encoder;
use crate::rich_text::RichTextDelta;
#[test]
fn markdown_encoder_header_1_test() {
let json = r#"[{"insert":"header 1"},{"insert":"\n","attributes":{"header":1}}]"#;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "# header 1\n");
}
#[test]
fn markdown_encoder_header_2_test() {
let json = r#"[{"insert":"header 2"},{"insert":"\n","attributes":{"header":2}}]"#;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "## header 2\n");
}
#[test]
fn markdown_encoder_header_3_test() {
let json = r#"[{"insert":"header 3"},{"insert":"\n","attributes":{"header":3}}]"#;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "### header 3\n");
}
#[test]
fn markdown_encoder_bold_italics_underlined_test() {
let json = r#"[{"insert":"bold","attributes":{"bold":true}},{"insert":" "},{"insert":"italics","attributes":{"italic":true}},{"insert":" "},{"insert":"underlined","attributes":{"underline":true}},{"insert":" "},{"insert":"\n","attributes":{"header":3}}]"#;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "### **bold** _italics_ <u>underlined</u> \n");
}
#[test]
fn markdown_encoder_strikethrough_highlight_test() {
let json = r##"[{"insert":"strikethrough","attributes":{"strike":true}},{"insert":" "},{"insert":"highlighted","attributes":{"background":"#ffefe3"}},{"insert":"\n"}]"##;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "~~strikethrough~~ <mark>highlighted</mark>\n");
}
#[test]
fn markdown_encoder_numbered_list_test() {
let json = r#"[{"insert":"numbered list\nitem 1"},{"insert":"\n","attributes":{"list":"ordered"}},{"insert":"item 2"},{"insert":"\n","attributes":{"list":"ordered"}},{"insert":"item3"},{"insert":"\n","attributes":{"list":"ordered"}}]"#;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "numbered list\n\n1. item 1\n1. item 2\n1. item3\n");
}
#[test]
fn markdown_encoder_bullet_list_test() {
let json = r#"[{"insert":"bullet list\nitem1"},{"insert":"\n","attributes":{"list":"bullet"}}]"#;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "bullet list\n\n* item1\n");
}
#[test]
fn markdown_encoder_check_list_test() {
let json = r#"[{"insert":"check list\nchecked"},{"insert":"\n","attributes":{"list":"checked"}},{"insert":"unchecked"},{"insert":"\n","attributes":{"list":"unchecked"}}]"#;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "check list\n\n- [x] checked\n\n- [ ] unchecked\n");
}
#[test]
fn markdown_encoder_code_test() {
let json = r#"[{"insert":"code this "},{"insert":"print(\"hello world\")","attributes":{"code":true}},{"insert":"\n"}]"#;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "code this `print(\"hello world\")`\n");
}
#[test]
fn markdown_encoder_quote_block_test() {
let json = r#"[{"insert":"this is a quote block"},{"insert":"\n","attributes":{"blockquote":true}}]"#;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "> this is a quote block\n");
}
#[test]
fn markdown_encoder_link_test() {
let json = r#"[{"insert":"appflowy","attributes":{"link":"https://www.appflowy.io/"}},{"insert":"\n"}]"#;
let delta = RichTextDelta::from_json(json).unwrap();
let md = markdown_encoder(&delta);
assert_eq!(md, "[appflowy](https://www.appflowy.io/)\n");
}
}
struct Attribute {
key: RichTextAttributeKey,
value: RichTextAttributeValue,
}
pub fn markdown_encoder(delta: &Delta<RichTextAttributes>) -> String {
let mut markdown_buffer = String::new();
let mut line_buffer = String::new();
let mut current_inline_style = RichTextAttributes::default();
let mut current_block_lines: Vec<String> = Vec::new();
let mut iterator = DeltaIterator::new(delta);
let mut current_block_style: Option<Attribute> = None;
while iterator.has_next() {
let operation = iterator.next().unwrap();
let operation_data = operation.get_data();
if !operation_data.contains("\n") {
handle_inline(
&mut current_inline_style,
&mut line_buffer,
String::from(operation_data),
operation.get_attributes(),
)
} else {
handle_line(
&mut line_buffer,
&mut markdown_buffer,
String::from(operation_data),
operation.get_attributes(),
&mut current_block_style,
&mut current_block_lines,
&mut current_inline_style,
)
}
}
handle_block(&mut current_block_style, &mut current_block_lines, &mut markdown_buffer);
markdown_buffer
}
fn handle_inline(
current_inline_style: &mut RichTextAttributes,
buffer: &mut String,
mut text: String,
attributes: RichTextAttributes,
) {
let mut marked_for_removal: HashMap<RichTextAttributeKey, RichTextAttributeValue> = HashMap::new();
for key in current_inline_style
.clone()
.keys()
.collect::<Vec<&RichTextAttributeKey>>()
.into_iter()
.rev()
{
if is_block(key) {
continue;
}
if attributes.contains_key(key) {
continue;
}
let padding = trim_right(buffer);
write_attribute(buffer, key, current_inline_style.get(key).unwrap(), true);
if !padding.is_empty() {
buffer.push_str(&padding)
}
marked_for_removal.insert(key.clone(), current_inline_style.get(key).unwrap().clone());
}
for (marked_for_removal_key, marked_for_removal_value) in &marked_for_removal {
current_inline_style.retain(|inline_style_key, inline_style_value| {
inline_style_key != marked_for_removal_key && inline_style_value != marked_for_removal_value
})
}
for (key, value) in attributes.iter() {
if is_block(key) {
continue;
}
if current_inline_style.contains_key(key) {
continue;
}
let original_text = text.clone();
text = text.trim_start().to_string();
let padding = " ".repeat(original_text.len() - text.len());
if !padding.is_empty() {
buffer.push_str(&padding)
}
write_attribute(buffer, key, value, false)
}
buffer.push_str(&text);
*current_inline_style = attributes;
}
fn trim_right(buffer: &mut String) -> String {
let text = buffer.clone();
if !text.ends_with(" ") {
return String::from("");
}
let result = text.trim_end();
buffer.clear();
buffer.push_str(result);
" ".repeat(text.len() - result.len())
}
fn write_attribute(buffer: &mut String, key: &RichTextAttributeKey, value: &RichTextAttributeValue, close: bool) {
match key {
RichTextAttributeKey::Bold => buffer.push_str("**"),
RichTextAttributeKey::Italic => buffer.push_str("_"),
RichTextAttributeKey::Underline => {
if close {
buffer.push_str("</u>")
} else {
buffer.push_str("<u>")
}
}
RichTextAttributeKey::StrikeThrough => {
if close {
buffer.push_str("~~")
} else {
buffer.push_str("~~")
}
}
RichTextAttributeKey::Link => {
if close {
buffer.push_str(format!("]({})", value.0.as_ref().unwrap()).as_str())
} else {
buffer.push_str("[")
}
}
RichTextAttributeKey::Background => {
if close {
buffer.push_str("</mark>")
} else {
buffer.push_str("<mark>")
}
}
RichTextAttributeKey::CodeBlock => {
if close {
buffer.push_str("\n```")
} else {
buffer.push_str("```\n")
}
}
RichTextAttributeKey::InlineCode => {
if close {
buffer.push_str("`")
} else {
buffer.push_str("`")
}
}
_ => {}
}
}
fn handle_line(
buffer: &mut String,
markdown_buffer: &mut String,
data: String,
attributes: RichTextAttributes,
current_block_style: &mut Option<Attribute>,
current_block_lines: &mut Vec<String>,
current_inline_style: &mut RichTextAttributes,
) {
let mut span = String::new();
for c in data.chars() {
if (c as i32) == LINEFEEDASCIICODE {
if !span.is_empty() {
handle_inline(current_inline_style, buffer, span.clone(), attributes.clone());
}
handle_inline(
current_inline_style,
buffer,
String::from(""),
RichTextAttributes::default(),
);
let line_block_key = attributes.keys().find(|key| {
if is_block(*key) {
return true;
} else {
return false;
}
});
match (line_block_key, &current_block_style) {
(Some(line_block_key), Some(current_block_style))
if *line_block_key == current_block_style.key
&& *attributes.get(line_block_key).unwrap() == current_block_style.value =>
{
current_block_lines.push(buffer.clone());
}
(None, None) => {
current_block_lines.push(buffer.clone());
}
_ => {
handle_block(current_block_style, current_block_lines, markdown_buffer);
current_block_lines.clear();
current_block_lines.push(buffer.clone());
match line_block_key {
None => *current_block_style = None,
Some(line_block_key) => {
*current_block_style = Some(Attribute {
key: line_block_key.clone(),
value: attributes.get(line_block_key).unwrap().clone(),
})
}
}
}
}
buffer.clear();
span.clear();
} else {
span.push(c);
}
}
if !span.is_empty() {
handle_inline(current_inline_style, buffer, span.clone(), attributes)
}
}
fn handle_block(
block_style: &mut Option<Attribute>,
current_block_lines: &mut Vec<String>,
markdown_buffer: &mut String,
) {
if current_block_lines.is_empty() {
return;
}
if !markdown_buffer.is_empty() {
markdown_buffer.push('\n')
}
match block_style {
None => {
markdown_buffer.push_str(&current_block_lines.join("\n"));
markdown_buffer.push('\n');
}
Some(block_style) if block_style.key == RichTextAttributeKey::CodeBlock => {
write_attribute(markdown_buffer, &block_style.key, &block_style.value, false);
markdown_buffer.push_str(&current_block_lines.join("\n"));
write_attribute(markdown_buffer, &block_style.key, &block_style.value, true);
markdown_buffer.push('\n');
}
Some(block_style) => {
for line in current_block_lines {
write_block_tag(markdown_buffer, &block_style, false);
markdown_buffer.push_str(line);
markdown_buffer.push('\n');
}
}
}
}
fn write_block_tag(buffer: &mut String, block: &Attribute, close: bool) {
if close {
return;
}
if block.key == RichTextAttributeKey::BlockQuote {
buffer.push_str("> ");
} else if block.key == RichTextAttributeKey::List {
if block.value.0.as_ref().unwrap().eq("bullet") {
buffer.push_str("* ");
} else if block.value.0.as_ref().unwrap().eq("checked") {
buffer.push_str("- [x] ");
} else if block.value.0.as_ref().unwrap().eq("unchecked") {
buffer.push_str("- [ ] ");
} else if block.value.0.as_ref().unwrap().eq("ordered") {
buffer.push_str("1. ");
} else {
buffer.push_str("* ");
}
} else if block.key == RichTextAttributeKey::Header {
if block.value.0.as_ref().unwrap().eq("1") {
buffer.push_str("# ");
} else if block.value.0.as_ref().unwrap().eq("2") {
buffer.push_str("## ");
} else if block.value.0.as_ref().unwrap().eq("3") {
buffer.push_str("### ");
} else if block.key == RichTextAttributeKey::List {
}
}
}

View File

@ -1 +1 @@
pub mod markdown_encoder;

View File

@ -361,6 +361,10 @@ pub fn is_block_except_header(k: &RichTextAttributeKey) -> bool {
BLOCK_KEYS.contains(k)
}
pub fn is_block(k: &RichTextAttributeKey) -> bool {
BLOCK_KEYS.contains(k)
}
lazy_static! {
static ref BLOCK_KEYS: HashSet<RichTextAttributeKey> = HashSet::from_iter(vec![
RichTextAttributeKey::Header,