feat: reorder sort precedence (#4592)

* feat: reorder sorts

* chore: add tests, fix tests, fix tauri build and fix clippy

* fix: add missing import
This commit is contained in:
Richard Shiue 2024-02-05 13:52:59 +08:00 committed by GitHub
parent fda70ff560
commit a515715543
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 589 additions and 134 deletions

View File

@ -86,7 +86,7 @@ void main() {
testWidgets('add checkbox sort', (tester) async { testWidgets('add checkbox sort', (tester) async {
await tester.openV020database(); await tester.openV020database();
// create a filter // create a sort
await tester.tapDatabaseSortButton(); await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done'); await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
@ -136,7 +136,7 @@ void main() {
testWidgets('add number sort', (tester) async { testWidgets('add number sort', (tester) async {
await tester.openV020database(); await tester.openV020database();
// create a filter // create a sort
await tester.tapDatabaseSortButton(); await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Number, 'number'); await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
@ -188,7 +188,7 @@ void main() {
testWidgets('add checkbox and number sort', (tester) async { testWidgets('add checkbox and number sort', (tester) async {
await tester.openV020database(); await tester.openV020database();
// create a filter // create a sort
await tester.tapDatabaseSortButton(); await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done'); await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
@ -264,5 +264,111 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
testWidgets('reorder sort', (tester) async {
await tester.openV020database();
// create a sort
await tester.tapDatabaseSortButton();
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
// open the sort menu and sort checkbox by descending
await tester.tapSortMenuInSettingBar();
await tester.tapSortButtonByName('Done');
await tester.tapSortByDescending();
// add another sort, this time by number descending
await tester.tapSortMenuInSettingBar();
await tester.tapCreateSortByFieldTypeInSortMenu(
FieldType.Number,
'number',
);
await tester.tapSortButtonByName('number');
await tester.tapSortByDescending();
// check checkbox cell order
for (final (index, content) in <bool>[
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
].indexed) {
await tester.assertCheckboxCell(
rowIndex: index,
isSelected: content,
);
}
// check number cell order
for (final (index, content) in <String>[
'1',
'0.2',
'0.1',
'-1',
'-2',
'12',
'11',
'10',
'2',
'',
].indexed) {
tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
// reorder sort
await tester.tapSortMenuInSettingBar();
await tester.reorderSort(
(FieldType.Number, 'number'),
(FieldType.Checkbox, 'Done'),
);
// check checkbox cell order
for (final (index, content) in <bool>[
false,
false,
false,
false,
true,
true,
true,
true,
true,
false,
].indexed) {
await tester.assertCheckboxCell(
rowIndex: index,
isSelected: content,
);
}
// check the number cell order
for (final (index, content) in <String>[
'12',
'11',
'10',
'2',
'1',
'0.2',
'0.1',
'-1',
'-2',
'',
].indexed) {
tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.Number,
content: content,
);
}
});
}); });
} }

View File

@ -1070,6 +1070,34 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(findSortItem); await tapButton(findSortItem);
} }
/// Must call [tapSortMenuInSettingBar] first.
Future<void> reorderSort(
(FieldType, String) from,
(FieldType, String) to,
) async {
final fromSortItem = find.byWidgetPredicate(
(widget) =>
widget is DatabaseSortItem &&
widget.sortInfo.fieldInfo.fieldType == from.$1 &&
widget.sortInfo.fieldInfo.name == from.$2,
);
final toSortItem = find.byWidgetPredicate(
(widget) =>
widget is DatabaseSortItem &&
widget.sortInfo.fieldInfo.fieldType == to.$1 &&
widget.sortInfo.fieldInfo.name == to.$2,
);
final dragElement = find.descendant(
of: fromSortItem,
matching: find.byType(ReorderableDragStartListener),
);
await drag(
dragElement,
getCenter(toSortItem) - getCenter(fromSortItem),
);
await pumpAndSettle(const Duration(milliseconds: 200));
}
/// Must call [tapSortButtonByName] first. /// Must call [tapSortButtonByName] first.
Future<void> tapSortByDescending() async { Future<void> tapSortByDescending() async {
await tapButton( await tapButton(

View File

@ -282,16 +282,19 @@ class FieldController {
) { ) {
for (final newSortPB in changeset.insertSorts) { for (final newSortPB in changeset.insertSorts) {
final sortIndex = newSortInfos final sortIndex = newSortInfos
.indexWhere((element) => element.sortId == newSortPB.id); .indexWhere((element) => element.sortId == newSortPB.sort.id);
if (sortIndex == -1) { if (sortIndex == -1) {
final fieldInfo = _findFieldInfo( final fieldInfo = _findFieldInfo(
fieldInfos: fieldInfos, fieldInfos: fieldInfos,
fieldId: newSortPB.fieldId, fieldId: newSortPB.sort.fieldId,
fieldType: newSortPB.fieldType, fieldType: newSortPB.sort.fieldType,
); );
if (fieldInfo != null) { if (fieldInfo != null) {
newSortInfos.add(SortInfo(sortPB: newSortPB, fieldInfo: fieldInfo)); newSortInfos.insert(
newSortPB.index,
SortInfo(sortPB: newSortPB.sort, fieldInfo: fieldInfo),
);
} }
} }
} }

View File

@ -75,6 +75,20 @@ class SortBackendService {
}); });
} }
Future<Either<Unit, FlowyError>> reorderSort({
required String fromSortId,
required String toSortId,
}) {
final payload = DatabaseSettingChangesetPB()
..viewId = viewId
..reorderSort = (ReorderSortPayloadPB()
..viewId = viewId
..fromSortId = fromSortId
..toSortId = toSortId);
return DatabaseEventUpdateDatabaseSetting(payload).send();
}
Future<Either<Unit, FlowyError>> deleteSort({ Future<Either<Unit, FlowyError>> deleteSort({
required String fieldId, required String fieldId,
required String sortId, required String sortId,

View File

@ -71,6 +71,23 @@ class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
); );
result.fold((l) => null, (err) => Log.error(err)); result.fold((l) => null, (err) => Log.error(err));
}, },
reorderSort: (fromIndex, toIndex) async {
if (fromIndex < toIndex) {
toIndex--;
}
final fromId = state.sortInfos[fromIndex].sortId;
final toId = state.sortInfos[toIndex].sortId;
final newSorts = [...state.sortInfos];
newSorts.insert(toIndex, newSorts.removeAt(fromIndex));
emit(state.copyWith(sortInfos: newSorts));
final result = await _sortBackendSvc.reorderSort(
fromSortId: fromId,
toSortId: toId,
);
result.fold((l) => null, (err) => Log.error(err));
},
); );
}, },
); );
@ -113,6 +130,8 @@ class SortEditorEvent with _$SortEditorEvent {
) = _SetCondition; ) = _SetCondition;
const factory SortEditorEvent.deleteSort(SortInfo sortInfo) = _DeleteSort; const factory SortEditorEvent.deleteSort(SortInfo sortInfo) = _DeleteSort;
const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts; const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts;
const factory SortEditorEvent.reorderSort(int oldIndex, int newIndex) =
_ReorderSort;
} }
@freezed @freezed

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart';
@ -48,33 +50,58 @@ class _SortEditorState extends State<SortEditor> {
)..add(const SortEditorEvent.initial()), )..add(const SortEditorEvent.initial()),
child: BlocBuilder<SortEditorBloc, SortEditorState>( child: BlocBuilder<SortEditorBloc, SortEditorState>(
builder: (context, state) { builder: (context, state) {
return Column( final sortInfos = state.sortInfos;
children: [
...state.sortInfos.map( return ReorderableListView.builder(
(info) => Padding( onReorder: (oldIndex, newIndex) => context
padding: const EdgeInsets.symmetric(vertical: 6), .read<SortEditorBloc>()
child: DatabaseSortItem( .add(SortEditorEvent.reorderSort(oldIndex, newIndex)),
sortInfo: info, itemCount: state.sortInfos.length,
popoverMutex: popoverMutex, itemBuilder: (context, index) => Padding(
), key: ValueKey(sortInfos[index].sortId),
), padding: const EdgeInsets.symmetric(vertical: 6),
child: DatabaseSortItem(
index: index,
sortInfo: sortInfos[index],
popoverMutex: popoverMutex,
), ),
Row( ),
proxyDecorator: (child, index, animation) => Material(
color: Colors.transparent,
child: Stack(
children: [ children: [
Flexible( BlocProvider.value(
child: DatabaseAddSortButton( value: context.read<SortEditorBloc>(),
viewId: widget.viewId, child: child,
fieldController: widget.fieldController,
popoverMutex: popoverMutex,
),
), ),
const HSpace(6), MouseRegion(
Flexible( cursor: Platform.isWindows
child: DatabaseDeleteSortButton(popoverMutex: popoverMutex), ? SystemMouseCursors.click
: SystemMouseCursors.grabbing,
child: const SizedBox.expand(),
), ),
], ],
), ),
], ),
shrinkWrap: true,
buildDefaultDragHandles: false,
footer: Row(
children: [
Flexible(
child: DatabaseAddSortButton(
viewId: widget.viewId,
fieldController: widget.fieldController,
popoverMutex: popoverMutex,
),
),
const HSpace(6),
Flexible(
child: DatabaseDeleteSortButton(
popoverMutex: popoverMutex,
),
),
],
),
); );
}, },
), ),
@ -85,10 +112,12 @@ class _SortEditorState extends State<SortEditor> {
class DatabaseSortItem extends StatelessWidget { class DatabaseSortItem extends StatelessWidget {
const DatabaseSortItem({ const DatabaseSortItem({
super.key, super.key,
required this.index,
required this.popoverMutex, required this.popoverMutex,
required this.sortInfo, required this.sortInfo,
}); });
final int index;
final PopoverMutex popoverMutex; final PopoverMutex popoverMutex;
final SortInfo sortInfo; final SortInfo sortInfo;
@ -107,6 +136,23 @@ class DatabaseSortItem extends StatelessWidget {
return Row( return Row(
children: [ children: [
ReorderableDragStartListener(
index: index,
child: MouseRegion(
cursor: Platform.isWindows
? SystemMouseCursors.click
: SystemMouseCursors.grab,
child: SizedBox(
width: 14,
height: 14,
child: FlowySvg(
FlowySvgs.drag_element_s,
color: Theme.of(context).iconTheme.color,
),
),
),
),
const HSpace(6),
SizedBox( SizedBox(
height: 26, height: 26,
child: SortChoiceButton( child: SortChoiceButton(
@ -122,8 +168,8 @@ class DatabaseSortItem extends StatelessWidget {
popoverMutex: popoverMutex, popoverMutex: popoverMutex,
), ),
), ),
const HSpace(6),
const Spacer(), const Spacer(),
const HSpace(6),
deleteButton, deleteButton,
], ],
); );

View File

@ -31,27 +31,25 @@ class SortMenu extends StatelessWidget {
)..add(const SortMenuEvent.initial()), )..add(const SortMenuEvent.initial()),
child: BlocBuilder<SortMenuBloc, SortMenuState>( child: BlocBuilder<SortMenuBloc, SortMenuState>(
builder: (context, state) { builder: (context, state) {
if (state.sortInfos.isNotEmpty) { if (state.sortInfos.isEmpty) {
return AppFlowyPopover( return const SizedBox.shrink();
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(320, 200)),
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 5),
popupBuilder: (BuildContext popoverContext) {
return SingleChildScrollView(
child: SortEditor(
viewId: state.viewId,
fieldController:
context.read<SortMenuBloc>().fieldController,
sortInfos: state.sortInfos,
),
);
},
child: SortChoiceChip(sortInfos: state.sortInfos),
);
} }
return const SizedBox.shrink(); return AppFlowyPopover(
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(320, 200)),
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 5),
margin: const EdgeInsets.fromLTRB(6.0, 0.0, 6.0, 6.0),
popupBuilder: (BuildContext popoverContext) {
return SortEditor(
viewId: state.viewId,
fieldController: context.read<SortMenuBloc>().fieldController,
sortInfos: state.sortInfos,
);
},
child: SortChoiceChip(sortInfos: state.sortInfos),
);
}, },
), ),
); );

View File

@ -169,6 +169,7 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
FlowySvg( FlowySvg(
widget.fieldInfo.fieldType.svgData, widget.fieldInfo.fieldType.svgData,
color: Theme.of(context).iconTheme.color, color: Theme.of(context).iconTheme.color,
size: const Size.square(16),
), ),
], ],
), ),

View File

@ -816,7 +816,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -838,7 +838,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-database" name = "collab-database"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -867,7 +867,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-document" name = "collab-document"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -886,7 +886,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-entity" name = "collab-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -901,7 +901,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-folder" name = "collab-folder"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -938,7 +938,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-plugins" name = "collab-plugins"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -977,7 +977,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-user" name = "collab-user"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",

View File

@ -34,18 +34,38 @@ lru = "0.12.0"
[dependencies] [dependencies]
serde_json.workspace = true serde_json.workspace = true
serde.workspace = true serde.workspace = true
tauri = { version = "1.5", features = ["clipboard-all", "fs-all", "shell-open"] } tauri = { version = "1.5", features = [
"clipboard-all",
"fs-all",
"shell-open",
] }
tauri-utils = "1.5.2" tauri-utils = "1.5.2"
bytes.workspace = true bytes.workspace = true
tracing.workspace = true tracing.workspace = true
lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = ["use_serde"] } lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = [
flowy-core = { path = "../../rust-lib/flowy-core", features = ["rev-sqlite", "ts"] } "use_serde",
] }
flowy-core = { path = "../../rust-lib/flowy-core", features = [
"rev-sqlite",
"ts",
] }
flowy-user = { path = "../../rust-lib/flowy-user", features = ["tauri_ts"] } flowy-user = { path = "../../rust-lib/flowy-user", features = ["tauri_ts"] }
flowy-config = { path = "../../rust-lib/flowy-config", features = ["tauri_ts"] } flowy-config = { path = "../../rust-lib/flowy-config", features = ["tauri_ts"] }
flowy-date = { path = "../../rust-lib/flowy-date", features = ["tauri_ts"] } flowy-date = { path = "../../rust-lib/flowy-date", features = ["tauri_ts"] }
flowy-error = { path = "../../rust-lib/flowy-error", features = ["impl_from_sqlite", "impl_from_dispatch_error", "impl_from_appflowy_cloud", "impl_from_reqwest", "impl_from_serde", "tauri_ts"] } flowy-error = { path = "../../rust-lib/flowy-error", features = [
flowy-document = { path = "../../rust-lib/flowy-document", features = ["tauri_ts"] } "impl_from_sqlite",
flowy-notification = { path = "../../rust-lib/flowy-notification", features = ["tauri_ts"] } "impl_from_dispatch_error",
"impl_from_appflowy_cloud",
"impl_from_reqwest",
"impl_from_serde",
"tauri_ts",
] }
flowy-document = { path = "../../rust-lib/flowy-document", features = [
"tauri_ts",
] }
flowy-notification = { path = "../../rust-lib/flowy-notification", features = [
"tauri_ts",
] }
uuid = "1.5.0" uuid = "1.5.0"
[features] [features]
@ -72,10 +92,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23
# To switch to the local path, run: # To switch to the local path, run:
# scripts/tool/update_collab_source.sh # scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }

View File

@ -12,7 +12,7 @@ const deleteSortsFromChange = (database: Database, changeset: SortChangesetNotif
const insertSortsFromChange = (database: Database, changeset: SortChangesetNotificationPB) => { const insertSortsFromChange = (database: Database, changeset: SortChangesetNotificationPB) => {
changeset.insert_sorts.forEach(sortPB => { changeset.insert_sorts.forEach(sortPB => {
database.sorts.push(pbToSort(sortPB)); database.sorts.push(pbToSort(sortPB.sort));
}); });
}; };

View File

@ -1,9 +1,5 @@
[workspace] [workspace]
members = [ members = ["af-wasm", "af-user", "af-persistence"]
"af-wasm",
"af-user",
"af-persistence",
]
resolver = "2" resolver = "2"
[workspace.dependencies] [workspace.dependencies]
@ -15,7 +11,7 @@ parking_lot = { version = "0.12.1" }
tracing = { version = "0.1.22" } tracing = { version = "0.1.22" }
serde = { version = "1.0.194", features = ["derive"] } serde = { version = "1.0.194", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
collab-integrate = { path = "../../rust-lib/collab-integrate"} collab-integrate = { path = "../../rust-lib/collab-integrate" }
flowy-notification = { path = "../../rust-lib/flowy-notification" } flowy-notification = { path = "../../rust-lib/flowy-notification" }
flowy-user-pub = { path = "../../rust-lib/flowy-user-pub" } flowy-user-pub = { path = "../../rust-lib/flowy-user-pub" }
flowy-server = { path = "../../rust-lib/flowy-server" } flowy-server = { path = "../../rust-lib/flowy-server" }
@ -42,7 +38,6 @@ wasm-bindgen-futures = "0.4.40"
serde-wasm-bindgen = "0.4" serde-wasm-bindgen = "0.4"
[profile.dev] [profile.dev]
opt-level = 0 opt-level = 0
lto = false lto = false
@ -70,10 +65,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23
# To switch to the local path, run: # To switch to the local path, run:
# scripts/tool/update_collab_source.sh # scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }

View File

@ -744,7 +744,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -766,7 +766,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-database" name = "collab-database"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -795,7 +795,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-document" name = "collab-document"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -814,7 +814,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-entity" name = "collab-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -829,7 +829,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-folder" name = "collab-folder"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -866,7 +866,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-plugins" name = "collab-plugins"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -905,7 +905,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-user" name = "collab-user"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",

View File

@ -115,10 +115,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23
# To switch to the local path, run: # To switch to the local path, run:
# scripts/tool/update_collab_source.sh # scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" }

View File

@ -84,7 +84,7 @@ pub struct DeleteFilterPayloadPB {
pub field_type: FieldType, pub field_type: FieldType,
#[pb(index = 3)] #[pb(index = 3)]
#[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] #[validate(custom = "crate::entities::utils::validate_filter_id")]
pub filter_id: String, pub filter_id: String,
#[pb(index = 4)] #[pb(index = 4)]
@ -103,7 +103,7 @@ pub struct UpdateFilterPayloadPB {
/// Create a new filter if the filter_id is None /// Create a new filter if the filter_id is None
#[pb(index = 3, one_of)] #[pb(index = 3, one_of)]
#[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] #[validate(custom = "crate::entities::utils::validate_filter_id")]
pub filter_id: Option<String>, pub filter_id: Option<String>,
#[pb(index = 4)] #[pb(index = 4)]

View File

@ -35,3 +35,25 @@ pub use share_entities::*;
pub use sort_entities::*; pub use sort_entities::*;
pub use type_option_entities::*; pub use type_option_entities::*;
pub use view_entities::*; pub use view_entities::*;
mod utils {
use fancy_regex::Regex;
use lib_infra::impl_regex_validator;
use validator::ValidationError;
impl_regex_validator!(
validate_filter_id,
Regex::new(r"^[A-Za-z0-9_-]{6}$").unwrap(),
"invalid filter_id"
);
impl_regex_validator!(
validate_sort_id,
Regex::new(r"^s:[A-Za-z0-9_-]{6}$").unwrap(),
"invalid sort_id"
);
impl_regex_validator!(
validate_group_id,
Regex::new(r"^g:[A-Za-z0-9_-]{6}$").unwrap(),
"invalid group_id"
);
}

View File

@ -15,7 +15,7 @@ use crate::entities::{
}; };
use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting};
use super::BoardLayoutSettingPB; use super::{BoardLayoutSettingPB, ReorderSortPayloadPB};
/// [DatabaseViewSettingPB] defines the setting options for the grid. Such as the filter, group, and sort. /// [DatabaseViewSettingPB] defines the setting options for the grid. Such as the filter, group, and sort.
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
@ -39,9 +39,8 @@ pub struct DatabaseViewSettingPB {
pub field_settings: RepeatedFieldSettingsPB, pub field_settings: RepeatedFieldSettingsPB,
} }
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum, EnumIter)] #[derive(Debug, Default, Clone, PartialEq, Eq, ProtoBuf_Enum, EnumIter)]
#[repr(u8)] #[repr(u8)]
#[derive(Default)]
pub enum DatabaseLayoutPB { pub enum DatabaseLayoutPB {
#[default] #[default]
Grid = 0, Grid = 0,
@ -96,6 +95,10 @@ pub struct DatabaseSettingChangesetPB {
#[pb(index = 7, one_of)] #[pb(index = 7, one_of)]
#[validate] #[validate]
pub reorder_sort: Option<ReorderSortPayloadPB>,
#[pb(index = 8, one_of)]
#[validate]
pub delete_sort: Option<DeleteSortPayloadPB>, pub delete_sort: Option<DeleteSortPayloadPB>,
} }

View File

@ -41,6 +41,15 @@ impl std::convert::From<Sort> for SortPB {
} }
} }
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct SortWithIndexPB {
#[pb(index = 1)]
pub index: u32,
#[pb(index = 2)]
pub sort: SortPB,
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct RepeatedSortPB { pub struct RepeatedSortPB {
#[pb(index = 1)] #[pb(index = 1)]
@ -105,12 +114,28 @@ pub struct UpdateSortPayloadPB {
/// Create a new sort if the sort_id is None /// Create a new sort if the sort_id is None
#[pb(index = 4, one_of)] #[pb(index = 4, one_of)]
#[validate(custom = "super::utils::validate_sort_id")]
pub sort_id: Option<String>, pub sort_id: Option<String>,
#[pb(index = 5)] #[pb(index = 5)]
pub condition: SortConditionPB, pub condition: SortConditionPB,
} }
#[derive(Debug, Default, Clone, Validate, ProtoBuf)]
pub struct ReorderSortPayloadPB {
#[pb(index = 1)]
#[validate(custom = "lib_infra::validator_fn::required_not_empty_str")]
pub view_id: String,
#[pb(index = 2)]
#[validate(custom = "super::utils::validate_sort_id")]
pub from_sort_id: String,
#[pb(index = 3)]
#[validate(custom = "super::utils::validate_sort_id")]
pub to_sort_id: String,
}
#[derive(ProtoBuf, Debug, Default, Clone, Validate)] #[derive(ProtoBuf, Debug, Default, Clone, Validate)]
pub struct DeleteSortPayloadPB { pub struct DeleteSortPayloadPB {
#[pb(index = 1)] #[pb(index = 1)]
@ -118,7 +143,7 @@ pub struct DeleteSortPayloadPB {
pub view_id: String, pub view_id: String,
#[pb(index = 2)] #[pb(index = 2)]
#[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] #[validate(custom = "super::utils::validate_sort_id")]
pub sort_id: String, pub sort_id: String,
} }
@ -128,7 +153,7 @@ pub struct SortChangesetNotificationPB {
pub view_id: String, pub view_id: String,
#[pb(index = 2)] #[pb(index = 2)]
pub insert_sorts: Vec<SortPB>, pub insert_sorts: Vec<SortWithIndexPB>,
#[pb(index = 3)] #[pb(index = 3)]
pub delete_sorts: Vec<SortPB>, pub delete_sorts: Vec<SortPB>,

View File

@ -88,28 +88,32 @@ pub(crate) async fn update_database_setting_handler(
) -> Result<(), FlowyError> { ) -> Result<(), FlowyError> {
let manager = upgrade_manager(manager)?; let manager = upgrade_manager(manager)?;
let params = data.try_into_inner()?; let params = data.try_into_inner()?;
let editor = manager.get_database_with_view_id(&params.view_id).await?; let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
if let Some(update_filter) = params.update_filter { if let Some(update_filter) = params.update_filter {
editor database_editor
.create_or_update_filter(update_filter.try_into()?) .create_or_update_filter(update_filter.try_into()?)
.await?; .await?;
} }
if let Some(delete_filter) = params.delete_filter { if let Some(delete_filter) = params.delete_filter {
editor.delete_filter(delete_filter).await?; database_editor.delete_filter(delete_filter).await?;
} }
if let Some(update_sort) = params.update_sort { if let Some(update_sort) = params.update_sort {
let _ = editor.create_or_update_sort(update_sort).await?; let _ = database_editor.create_or_update_sort(update_sort).await?;
}
if let Some(reorder_sort) = params.reorder_sort {
database_editor.reorder_sort(reorder_sort).await?;
} }
if let Some(delete_sort) = params.delete_sort { if let Some(delete_sort) = params.delete_sort {
editor.delete_sort(delete_sort).await?; database_editor.delete_sort(delete_sort).await?;
} }
if let Some(layout_type) = params.layout_type { if let Some(layout_type) = params.layout_type {
editor database_editor
.update_view_layout(&params.view_id, layout_type.into()) .update_view_layout(&params.view_id, layout_type.into())
.await?; .await?;
} }

View File

@ -228,10 +228,16 @@ impl DatabaseEditor {
pub async fn create_or_update_sort(&self, params: UpdateSortPayloadPB) -> FlowyResult<Sort> { pub async fn create_or_update_sort(&self, params: UpdateSortPayloadPB) -> FlowyResult<Sort> {
let view_editor = self.database_views.get_view_editor(&params.view_id).await?; let view_editor = self.database_views.get_view_editor(&params.view_id).await?;
let sort = view_editor.insert_or_update_sort(params).await?; let sort = view_editor.v_create_or_update_sort(params).await?;
Ok(sort) Ok(sort)
} }
pub async fn reorder_sort(&self, params: ReorderSortPayloadPB) -> FlowyResult<()> {
let view_editor = self.database_views.get_view_editor(&params.view_id).await?;
view_editor.v_reorder_sort(params).await?;
Ok(())
}
pub async fn delete_sort(&self, params: DeleteSortPayloadPB) -> FlowyResult<()> { pub async fn delete_sort(&self, params: DeleteSortPayloadPB) -> FlowyResult<()> {
let view_editor = self.database_views.get_view_editor(&params.view_id).await?; let view_editor = self.database_views.get_view_editor(&params.view_id).await?;
view_editor.v_delete_sort(params).await?; view_editor.v_delete_sort(params).await?;
@ -1459,6 +1465,13 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
self.database.lock().insert_sort(view_id, sort); self.database.lock().insert_sort(view_id, sort);
} }
fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str) {
self
.database
.lock()
.move_sort(view_id, from_sort_id, to_sort_id);
}
fn remove_sort(&self, view_id: &str, sort_id: &str) { fn remove_sort(&self, view_id: &str, sort_id: &str) {
self.database.lock().remove_sort(view_id, sort_id); self.database.lock().remove_sort(view_id, sort_id);
} }

View File

@ -17,8 +17,8 @@ use lib_dispatch::prelude::af_spawn;
use crate::entities::{ use crate::entities::{
CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterPayloadPB, CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterPayloadPB,
DeleteSortPayloadPB, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB, DeleteSortPayloadPB, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB,
LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, RowMetaPB, LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, ReorderSortPayloadPB,
RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB, RowMetaPB, RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB,
UpdateFilterParams, UpdateSortPayloadPB, UpdateFilterParams, UpdateSortPayloadPB,
}; };
use crate::notification::{send_notification, DatabaseNotification}; use crate::notification::{send_notification, DatabaseNotification};
@ -499,7 +499,7 @@ impl DatabaseViewEditor {
} }
#[tracing::instrument(level = "trace", skip(self), err)] #[tracing::instrument(level = "trace", skip(self), err)]
pub async fn insert_or_update_sort(&self, params: UpdateSortPayloadPB) -> FlowyResult<Sort> { pub async fn v_create_or_update_sort(&self, params: UpdateSortPayloadPB) -> FlowyResult<Sort> {
let is_exist = params.sort_id.is_some(); let is_exist = params.sort_id.is_some();
let sort_id = match params.sort_id { let sort_id = match params.sort_id {
None => gen_database_sort_id(), None => gen_database_sort_id(),
@ -513,9 +513,11 @@ impl DatabaseViewEditor {
condition: params.condition.into(), condition: params.condition.into(),
}; };
let mut sort_controller = self.sort_controller.write().await;
self.delegate.insert_sort(&self.view_id, sort.clone()); self.delegate.insert_sort(&self.view_id, sort.clone());
let changeset = if is_exist {
let mut sort_controller = self.sort_controller.write().await;
let notification = if is_exist {
sort_controller sort_controller
.apply_changeset(SortChangeset::from_update(sort.clone())) .apply_changeset(SortChangeset::from_update(sort.clone()))
.await .await
@ -525,10 +527,29 @@ impl DatabaseViewEditor {
.await .await
}; };
drop(sort_controller); drop(sort_controller);
notify_did_update_sort(changeset).await; notify_did_update_sort(notification).await;
Ok(sort) Ok(sort)
} }
pub async fn v_reorder_sort(&self, params: ReorderSortPayloadPB) -> FlowyResult<()> {
self
.delegate
.move_sort(&self.view_id, &params.from_sort_id, &params.to_sort_id);
let notification = self
.sort_controller
.write()
.await
.apply_changeset(SortChangeset::from_reorder(
params.from_sort_id,
params.to_sort_id,
))
.await;
notify_did_update_sort(notification).await;
Ok(())
}
pub async fn v_delete_sort(&self, params: DeleteSortPayloadPB) -> FlowyResult<()> { pub async fn v_delete_sort(&self, params: DeleteSortPayloadPB) -> FlowyResult<()> {
let notification = self let notification = self
.sort_controller .sort_controller

View File

@ -75,6 +75,8 @@ pub trait DatabaseViewOperation: Send + Sync + 'static {
fn insert_sort(&self, view_id: &str, sort: Sort); fn insert_sort(&self, view_id: &str, sort: Sort);
fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str);
fn remove_sort(&self, view_id: &str, sort_id: &str); fn remove_sort(&self, view_id: &str, sort_id: &str);
fn get_all_sorts(&self, view_id: &str) -> Vec<Sort>; fn get_all_sorts(&self, view_id: &str) -> Vec<Sort>;

View File

@ -13,8 +13,8 @@ use flowy_error::FlowyResult;
use lib_infra::future::Fut; use lib_infra::future::Fut;
use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher};
use crate::entities::FieldType;
use crate::entities::SortChangesetNotificationPB; use crate::entities::SortChangesetNotificationPB;
use crate::entities::{FieldType, SortWithIndexPB};
use crate::services::cell::CellCache; use crate::services::cell::CellCache;
use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier};
use crate::services::field::{default_order, TypeOptionCellExt}; use crate::services::field::{default_order, TypeOptionCellExt};
@ -184,7 +184,10 @@ impl SortController {
if let Some(insert_sort) = changeset.insert_sort { if let Some(insert_sort) = changeset.insert_sort {
if let Some(sort) = self.delegate.get_sort(&self.view_id, &insert_sort.id).await { if let Some(sort) = self.delegate.get_sort(&self.view_id, &insert_sort.id).await {
notification.insert_sorts.push(sort.as_ref().into()); notification.insert_sorts.push(SortWithIndexPB {
index: self.sorts.len() as u32,
sort: sort.as_ref().into(),
});
self.sorts.push(sort); self.sorts.push(sort);
} }
} }
@ -209,6 +212,23 @@ impl SortController {
} }
} }
if let Some((from_id, to_id)) = changeset.reorder_sort {
let moved_sort = self.delegate.get_sort(&self.view_id, &from_id).await;
let from_index = self.sorts.iter().position(|sort| sort.id == from_id);
let to_index = self.sorts.iter().position(|sort| sort.id == to_id);
if let (Some(sort), Some(from_index), Some(to_index)) = (moved_sort, from_index, to_index) {
self.sorts.remove(from_index);
self.sorts.insert(to_index, sort.clone());
notification.delete_sorts.push(sort.as_ref().into());
notification.insert_sorts.push(SortWithIndexPB {
index: to_index as u32,
sort: sort.as_ref().into(),
});
}
}
if !notification.is_empty() { if !notification.is_empty() {
self self
.gen_task(SortEvent::SortDidChanged, QualityOfService::UserInteractive) .gen_task(SortEvent::SortDidChanged, QualityOfService::UserInteractive)

View File

@ -126,6 +126,7 @@ pub struct SortChangeset {
pub(crate) insert_sort: Option<Sort>, pub(crate) insert_sort: Option<Sort>,
pub(crate) update_sort: Option<Sort>, pub(crate) update_sort: Option<Sort>,
pub(crate) delete_sort: Option<String>, pub(crate) delete_sort: Option<String>,
pub(crate) reorder_sort: Option<(String, String)>,
} }
impl SortChangeset { impl SortChangeset {
@ -134,6 +135,7 @@ impl SortChangeset {
insert_sort: Some(sort), insert_sort: Some(sort),
update_sort: None, update_sort: None,
delete_sort: None, delete_sort: None,
reorder_sort: None,
} }
} }
@ -142,6 +144,7 @@ impl SortChangeset {
insert_sort: None, insert_sort: None,
update_sort: Some(sort), update_sort: Some(sort),
delete_sort: None, delete_sort: None,
reorder_sort: None,
} }
} }
@ -150,6 +153,16 @@ impl SortChangeset {
insert_sort: None, insert_sort: None,
update_sort: None, update_sort: None,
delete_sort: Some(sort_id), delete_sort: Some(sort_id),
reorder_sort: None,
}
}
pub fn from_reorder(from_sort_id: String, to_sort_id: String) -> Self {
Self {
insert_sort: None,
update_sort: None,
delete_sort: None,
reorder_sort: Some((from_sort_id, to_sort_id)),
} }
} }
} }

View File

@ -47,3 +47,55 @@ async fn sort_checkbox_and_then_text_by_descending_test() {
]; ];
test.run_scripts(scripts).await; test.run_scripts(scripts).await;
} }
#[tokio::test]
async fn reorder_sort_test() {
let mut test = DatabaseSortTest::new().await;
let checkbox_field = test.get_first_field(FieldType::Checkbox);
let text_field = test.get_first_field(FieldType::RichText);
// Use the same sort set up as above
let scripts = vec![
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "No", "No", "No", "Yes", ""],
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"],
},
InsertSort {
field: checkbox_field.clone(),
condition: SortCondition::Descending,
},
InsertSort {
field: text_field.clone(),
condition: SortCondition::Ascending,
},
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "Yes", "No", "No", "", "No"],
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "AE", "", "AE", "C", "CB", "DA"],
},
];
test.run_scripts(scripts).await;
let sorts = test.editor.get_all_sorts(&test.view_id).await.items;
let scripts = vec![
ReorderSort {
from_sort_id: sorts[1].id.clone(),
to_sort_id: sorts[0].id.clone(),
},
AssertCellContentOrder {
field_id: checkbox_field.id.clone(),
orders: vec!["Yes", "Yes", "No", "No", "", "No", "Yes"],
},
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "AE", "AE", "C", "CB", "DA", ""],
},
];
test.run_scripts(scripts).await;
}

View File

@ -7,10 +7,12 @@ use collab_database::rows::RowId;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use tokio::sync::broadcast::Receiver; use tokio::sync::broadcast::Receiver;
use flowy_database2::entities::{DeleteSortPayloadPB, FieldType, UpdateSortPayloadPB}; use flowy_database2::entities::{
DeleteSortPayloadPB, FieldType, ReorderSortPayloadPB, UpdateSortPayloadPB,
};
use flowy_database2::services::cell::stringify_cell_data; use flowy_database2::services::cell::stringify_cell_data;
use flowy_database2::services::database_view::DatabaseViewChanged; use flowy_database2::services::database_view::DatabaseViewChanged;
use flowy_database2::services::sort::{Sort, SortCondition}; use flowy_database2::services::sort::SortCondition;
use crate::database::database_editor::DatabaseEditorTest; use crate::database::database_editor::DatabaseEditorTest;
@ -19,6 +21,10 @@ pub enum SortScript {
field: Field, field: Field,
condition: SortCondition, condition: SortCondition,
}, },
ReorderSort {
from_sort_id: String,
to_sort_id: String,
},
DeleteSort { DeleteSort {
sort_id: String, sort_id: String,
}, },
@ -41,7 +47,6 @@ pub enum SortScript {
pub struct DatabaseSortTest { pub struct DatabaseSortTest {
inner: DatabaseEditorTest, inner: DatabaseEditorTest,
pub current_sort_rev: Option<Sort>,
recv: Option<Receiver<DatabaseViewChanged>>, recv: Option<Receiver<DatabaseViewChanged>>,
} }
@ -50,7 +55,6 @@ impl DatabaseSortTest {
let editor_test = DatabaseEditorTest::new_grid().await; let editor_test = DatabaseEditorTest::new_grid().await;
Self { Self {
inner: editor_test, inner: editor_test,
current_sort_rev: None,
recv: None, recv: None,
} }
} }
@ -77,8 +81,25 @@ impl DatabaseSortTest {
field_type: FieldType::from(field.field_type), field_type: FieldType::from(field.field_type),
condition: condition.into(), condition: condition.into(),
}; };
let sort_rev = self.editor.create_or_update_sort(params).await.unwrap(); let _ = self.editor.create_or_update_sort(params).await.unwrap();
self.current_sort_rev = Some(sort_rev); },
SortScript::ReorderSort {
from_sort_id,
to_sort_id,
} => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id)
.await
.unwrap(),
);
let params = ReorderSortPayloadPB {
view_id: self.view_id.clone(),
from_sort_id,
to_sort_id,
};
self.editor.reorder_sort(params).await.unwrap();
}, },
SortScript::DeleteSort { sort_id } => { SortScript::DeleteSort { sort_id } => {
self.recv = Some( self.recv = Some(
@ -93,7 +114,6 @@ impl DatabaseSortTest {
sort_id, sort_id,
}; };
self.editor.delete_sort(params).await.unwrap(); self.editor.delete_sort(params).await.unwrap();
self.current_sort_rev = None;
}, },
SortScript::AssertCellContentOrder { field_id, orders } => { SortScript::AssertCellContentOrder { field_id, orders } => {
let mut cells = vec![]; let mut cells = vec![];

View File

@ -85,16 +85,21 @@ async fn sort_change_notification_by_update_text_test() {
async fn sort_text_by_ascending_and_delete_sort_test() { async fn sort_text_by_ascending_and_delete_sort_test() {
let mut test = DatabaseSortTest::new().await; let mut test = DatabaseSortTest::new().await;
let text_field = test.get_first_field(FieldType::RichText).clone(); let text_field = test.get_first_field(FieldType::RichText).clone();
let scripts = vec![InsertSort {
field: text_field.clone(),
condition: SortCondition::Ascending,
}];
test.run_scripts(scripts).await;
let sort = test.current_sort_rev.as_ref().unwrap();
let scripts = vec![ let scripts = vec![
DeleteSort { InsertSort {
sort_id: sort.id.clone(), field: text_field.clone(),
condition: SortCondition::Ascending,
}, },
AssertCellContentOrder {
field_id: text_field.id.clone(),
orders: vec!["A", "AE", "AE", "C", "CB", "DA", ""],
},
];
test.run_scripts(scripts).await;
let sort = test.editor.get_all_sorts(&test.view_id).await.items[0].clone();
let scripts = vec![
DeleteSort { sort_id: sort.id },
AssertCellContentOrder { AssertCellContentOrder {
field_id: text_field.id.clone(), field_id: text_field.id.clone(),
orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"], orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"],

View File

@ -15,3 +15,28 @@ pub fn required_valid_path(s: &str) -> Result<(), ValidationError> {
(_, _) => Err(ValidationError::new("invalid_path")), (_, _) => Err(ValidationError::new("invalid_path")),
} }
} }
#[macro_export]
/// Macro to implement a custom validator function for a regex expression.
/// This is intended to replace `validator` crate's own regex validator, which
/// isn't compatible with `fancy_regex`.
///
/// # Arguments:
///
/// - name of the validator function
/// - the `fancy_regex::Regex` object
/// - error message of the `ValidationError`
///
macro_rules! impl_regex_validator {
($validator: ident, $regex: expr, $error: expr) => {
pub(crate) fn $validator(arg: &str) -> Result<(), ValidationError> {
let check = $regex.is_match(arg).unwrap();
if check {
Ok(())
} else {
Err(ValidationError::new($error))
}
}
};
}