mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: sync the icon & title (#4150)
* feat: sync the icon & title * feat: diff the view data when refreshing * fix: unable to update folder * test: refactor bloc tests
This commit is contained in:
parent
b466b425a5
commit
9a1ea138fc
@ -202,7 +202,7 @@ extension Expectation on WidgetTester {
|
||||
return find.descendant(
|
||||
of: find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is ViewItem &&
|
||||
widget is InnerViewItem &&
|
||||
widget.view.name == parentName &&
|
||||
widget.view.layout == parentLayout,
|
||||
skipOffstage: false,
|
||||
|
@ -45,7 +45,7 @@ class MobilePersonalFolder extends StatelessWidget {
|
||||
...views.map(
|
||||
(view) => MobileViewItem(
|
||||
key: ValueKey(
|
||||
'${FolderCategoryType.personal.name} ${view.hashCode} ',
|
||||
'${FolderCategoryType.personal.name} ${view.id}',
|
||||
),
|
||||
isDraggable: true,
|
||||
categoryType: FolderCategoryType.personal,
|
||||
|
@ -76,14 +76,10 @@ class MobileViewItem extends StatelessWidget {
|
||||
p.lastCreatedView?.id != c.lastCreatedView!.id,
|
||||
listener: (context, state) => context.pushView(state.lastCreatedView!),
|
||||
builder: (context, state) {
|
||||
// don't remove this code. it's related to the backend service.
|
||||
view.childViews
|
||||
..clear()
|
||||
..addAll(state.childViews);
|
||||
return InnerMobileViewItem(
|
||||
view: state.view,
|
||||
parentView: parentView,
|
||||
childViews: state.childViews,
|
||||
childViews: state.view.childViews,
|
||||
categoryType: categoryType,
|
||||
level: level,
|
||||
leftPadding: leftPadding,
|
||||
@ -163,7 +159,7 @@ class InnerMobileViewItem extends StatelessWidget {
|
||||
if (childViews.isNotEmpty) {
|
||||
final children = childViews.map((childView) {
|
||||
return MobileViewItem(
|
||||
key: ValueKey('${categoryType.name} ${childView.hashCode}'),
|
||||
key: ValueKey('${categoryType.name} ${childView.id}'),
|
||||
parentView: view,
|
||||
categoryType: categoryType,
|
||||
isFirstChild: childView.id == childViews.first.id,
|
||||
|
@ -1,286 +0,0 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'app_bloc.freezed.dart';
|
||||
|
||||
class AppBloc extends Bloc<AppEvent, AppState> {
|
||||
final ViewBackendService appService;
|
||||
final ViewListener viewListener;
|
||||
|
||||
AppBloc({required ViewPB view})
|
||||
: appService = ViewBackendService(),
|
||||
viewListener = ViewListener(viewId: view.id),
|
||||
super(AppState.initial(view)) {
|
||||
on<AppEvent>((event, emit) async {
|
||||
await event.map(
|
||||
initial: (e) async {
|
||||
_startListening();
|
||||
await _loadViews(emit);
|
||||
},
|
||||
createView: (CreateView value) async {
|
||||
await _createView(value, emit);
|
||||
},
|
||||
loadViews: (_) async {
|
||||
await _loadViews(emit);
|
||||
},
|
||||
delete: (e) async {
|
||||
await _deleteApp(emit);
|
||||
},
|
||||
deleteView: (deletedView) async {
|
||||
await _deleteView(emit, deletedView.viewId);
|
||||
},
|
||||
rename: (e) async {
|
||||
await _renameView(e, emit);
|
||||
},
|
||||
appDidUpdate: (e) async {
|
||||
final latestCreatedView = state.latestCreatedView;
|
||||
final views = e.view.childViews;
|
||||
AppState newState = state.copyWith(
|
||||
views: views,
|
||||
view: e.view,
|
||||
);
|
||||
if (latestCreatedView != null) {
|
||||
final index = views
|
||||
.indexWhere((element) => element.id == latestCreatedView.id);
|
||||
if (index == -1) {
|
||||
newState = newState.copyWith(latestCreatedView: null);
|
||||
}
|
||||
emit(newState);
|
||||
}
|
||||
emit(newState);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
viewListener.start(
|
||||
onViewUpdated: (app) {
|
||||
if (!isClosed) {
|
||||
add(AppEvent.appDidUpdate(app));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _renameView(Rename e, Emitter<AppState> emit) async {
|
||||
final result = await ViewBackendService.updateView(
|
||||
viewId: state.view.id,
|
||||
name: e.newName,
|
||||
);
|
||||
result.fold(
|
||||
(l) => emit(state.copyWith(successOrFailure: left(unit))),
|
||||
(error) => emit(state.copyWith(successOrFailure: right(error))),
|
||||
);
|
||||
}
|
||||
|
||||
// Delete the current app
|
||||
Future<void> _deleteApp(Emitter<AppState> emit) async {
|
||||
final result = await ViewBackendService.delete(viewId: state.view.id);
|
||||
result.fold(
|
||||
(unit) => emit(state.copyWith(successOrFailure: left(unit))),
|
||||
(error) => emit(state.copyWith(successOrFailure: right(error))),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteView(Emitter<AppState> emit, String viewId) async {
|
||||
final result = await ViewBackendService.deleteView(viewId: viewId);
|
||||
result.fold(
|
||||
(unit) => emit(state.copyWith(successOrFailure: left(unit))),
|
||||
(error) => emit(state.copyWith(successOrFailure: right(error))),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createView(CreateView value, Emitter<AppState> emit) async {
|
||||
// create a child view for the current view
|
||||
final result = await ViewBackendService.createView(
|
||||
parentViewId: state.view.id,
|
||||
name: value.name,
|
||||
desc: value.desc ?? "",
|
||||
layoutType: value.layoutType,
|
||||
initialDataBytes: value.initialDataBytes,
|
||||
ext: value.ext ?? {},
|
||||
openAfterCreate: true,
|
||||
);
|
||||
result.fold(
|
||||
(view) => emit(
|
||||
state.copyWith(
|
||||
latestCreatedView: value.openAfterCreated ? view : null,
|
||||
successOrFailure: left(unit),
|
||||
),
|
||||
),
|
||||
(error) {
|
||||
Log.error(error);
|
||||
emit(state.copyWith(successOrFailure: right(error)));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await viewListener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _loadViews(Emitter<AppState> emit) async {
|
||||
final viewsOrFailed =
|
||||
await ViewBackendService.getChildViews(viewId: state.view.id);
|
||||
viewsOrFailed.fold(
|
||||
(views) => emit(state.copyWith(views: views)),
|
||||
(error) {
|
||||
Log.error(error);
|
||||
emit(state.copyWith(successOrFailure: right(error)));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AppEvent with _$AppEvent {
|
||||
const factory AppEvent.initial() = Initial;
|
||||
const factory AppEvent.createView(
|
||||
String name,
|
||||
ViewLayoutPB layoutType, {
|
||||
String? desc,
|
||||
|
||||
/// ~~The initial data should be the JSON of the document~~
|
||||
/// ~~For example: {"document":{"type":"editor","children":[]}}~~
|
||||
///
|
||||
/// - Document:
|
||||
/// the initial data should be the string that can be converted into [DocumentDataPB]
|
||||
///
|
||||
List<int>? initialDataBytes,
|
||||
Map<String, String>? ext,
|
||||
|
||||
/// open the view after created
|
||||
@Default(true) bool openAfterCreated,
|
||||
}) = CreateView;
|
||||
const factory AppEvent.loadViews() = LoadApp;
|
||||
const factory AppEvent.delete() = DeleteApp;
|
||||
const factory AppEvent.deleteView(String viewId) = DeleteView;
|
||||
const factory AppEvent.rename(String newName) = Rename;
|
||||
const factory AppEvent.appDidUpdate(ViewPB view) = AppDidUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AppState with _$AppState {
|
||||
const factory AppState({
|
||||
required ViewPB view,
|
||||
required List<ViewPB> views,
|
||||
ViewPB? latestCreatedView,
|
||||
required Either<Unit, FlowyError> successOrFailure,
|
||||
}) = _AppState;
|
||||
|
||||
factory AppState.initial(ViewPB view) => AppState(
|
||||
view: view,
|
||||
views: view.childViews,
|
||||
successOrFailure: left(unit),
|
||||
);
|
||||
}
|
||||
|
||||
class ViewDataContext extends ChangeNotifier {
|
||||
final String viewId;
|
||||
final ValueNotifier<List<ViewPB>> _viewsNotifier = ValueNotifier([]);
|
||||
final ValueNotifier<ViewPB?> _selectedViewNotifier = ValueNotifier(null);
|
||||
VoidCallback? _menuSharedStateListener;
|
||||
ExpandableController expandController =
|
||||
ExpandableController(initialExpanded: false);
|
||||
|
||||
ViewDataContext({required this.viewId}) {
|
||||
_setLatestView(getIt<MenuSharedState>().latestOpenView);
|
||||
_menuSharedStateListener =
|
||||
getIt<MenuSharedState>().addLatestViewListener((view) {
|
||||
_setLatestView(view);
|
||||
});
|
||||
}
|
||||
|
||||
VoidCallback onViewSelected(void Function(ViewPB?) callback) {
|
||||
void listener() {
|
||||
callback(_selectedViewNotifier.value);
|
||||
}
|
||||
|
||||
_selectedViewNotifier.addListener(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
void removeOnViewSelectedListener(VoidCallback listener) {
|
||||
_selectedViewNotifier.removeListener(listener);
|
||||
}
|
||||
|
||||
void _setLatestView(ViewPB? view) {
|
||||
view?.freeze();
|
||||
|
||||
if (_selectedViewNotifier.value != view) {
|
||||
_selectedViewNotifier.value = view;
|
||||
_expandIfNeed();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
ViewPB? get selectedView => _selectedViewNotifier.value;
|
||||
|
||||
set views(List<ViewPB> views) {
|
||||
if (_viewsNotifier.value != views) {
|
||||
_viewsNotifier.value = views;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
UnmodifiableListView<ViewPB> get views =>
|
||||
UnmodifiableListView(_viewsNotifier.value);
|
||||
|
||||
VoidCallback onViewsChanged(
|
||||
void Function(UnmodifiableListView<ViewPB>) callback,
|
||||
) {
|
||||
void listener() {
|
||||
callback(views);
|
||||
}
|
||||
|
||||
_viewsNotifier.addListener(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
void removeOnViewChangedListener(VoidCallback listener) {
|
||||
_viewsNotifier.removeListener(listener);
|
||||
}
|
||||
|
||||
void _expandIfNeed() {
|
||||
if (_selectedViewNotifier.value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_viewsNotifier.value
|
||||
.map((e) => e.id)
|
||||
.toList()
|
||||
.contains(_selectedViewNotifier.value?.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expandController.expanded == false) {
|
||||
// Workaround: Delay 150 milliseconds to make the smooth animation while expanding
|
||||
Future.delayed(const Duration(milliseconds: 150), () {
|
||||
expandController.expanded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_menuSharedStateListener != null) {
|
||||
getIt<MenuSharedState>()
|
||||
.removeLatestViewListener(_menuSharedStateListener!);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export 'app_bloc.dart';
|
@ -1,109 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/workspace/application/app/app_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
part 'menu_view_section_bloc.freezed.dart';
|
||||
|
||||
class ViewSectionBloc extends Bloc<ViewSectionEvent, ViewSectionState> {
|
||||
void Function()? _viewsListener;
|
||||
void Function()? _selectedViewlistener;
|
||||
final ViewDataContext _appViewData;
|
||||
|
||||
ViewSectionBloc({
|
||||
required ViewDataContext appViewData,
|
||||
}) : _appViewData = appViewData,
|
||||
super(ViewSectionState.initial(appViewData)) {
|
||||
on<ViewSectionEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
setSelectedView: (view) {
|
||||
emit(state.copyWith(selectedView: view));
|
||||
},
|
||||
didReceiveViewUpdated: (views) {
|
||||
emit(state.copyWith(views: views));
|
||||
},
|
||||
moveView: (fromIndex, toIndex) async {
|
||||
_moveView(fromIndex, toIndex, emit);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_viewsListener = _appViewData.onViewsChanged((views) {
|
||||
if (!isClosed) {
|
||||
add(ViewSectionEvent.didReceiveViewUpdated(views));
|
||||
}
|
||||
});
|
||||
_selectedViewlistener = _appViewData.onViewSelected((view) {
|
||||
if (!isClosed) {
|
||||
add(ViewSectionEvent.setSelectedView(view));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _moveView(
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
Emitter<ViewSectionState> emit,
|
||||
) async {
|
||||
if (fromIndex < state.views.length) {
|
||||
final viewId = state.views[fromIndex].id;
|
||||
final views = List<ViewPB>.from(state.views);
|
||||
views.insert(toIndex, views.removeAt(fromIndex));
|
||||
emit(state.copyWith(views: views));
|
||||
|
||||
final result = await ViewBackendService.moveView(
|
||||
viewId: viewId,
|
||||
fromIndex: fromIndex,
|
||||
toIndex: toIndex,
|
||||
);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_selectedViewlistener != null) {
|
||||
_appViewData.removeOnViewSelectedListener(_selectedViewlistener!);
|
||||
}
|
||||
|
||||
if (_viewsListener != null) {
|
||||
_appViewData.removeOnViewChangedListener(_viewsListener!);
|
||||
}
|
||||
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ViewSectionEvent with _$ViewSectionEvent {
|
||||
const factory ViewSectionEvent.initial() = _Initial;
|
||||
const factory ViewSectionEvent.setSelectedView(ViewPB? view) =
|
||||
_SetSelectedView;
|
||||
const factory ViewSectionEvent.moveView(int fromIndex, int toIndex) =
|
||||
_MoveView;
|
||||
const factory ViewSectionEvent.didReceiveViewUpdated(List<ViewPB> views) =
|
||||
_DidReceiveViewUpdated;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ViewSectionState with _$ViewSectionState {
|
||||
const factory ViewSectionState({
|
||||
required List<ViewPB> views,
|
||||
ViewPB? selectedView,
|
||||
}) = _ViewSectionState;
|
||||
|
||||
factory ViewSectionState.initial(ViewDataContext appViewData) =>
|
||||
ViewSectionState(
|
||||
views: appViewData.views,
|
||||
selectedView: appViewData.selectedView,
|
||||
);
|
||||
}
|
@ -7,12 +7,14 @@ import 'package:appflowy/workspace/application/favorite/favorite_listener.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
part 'view_bloc.freezed.dart';
|
||||
|
||||
@ -36,20 +38,17 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
add(ViewEvent.viewDidUpdate(left(result)));
|
||||
},
|
||||
onViewChildViewsUpdated: (result) async {
|
||||
final view = await ViewBackendService.getView(
|
||||
result.parentViewId,
|
||||
);
|
||||
view.fold(
|
||||
(view) => add(ViewEvent.viewDidUpdate(left(view))),
|
||||
(error) => add(ViewEvent.viewDidUpdate(right(error))),
|
||||
);
|
||||
final view = await _updateChildViews(result);
|
||||
if (!isClosed && view != null) {
|
||||
add(ViewEvent.viewUpdateChildView(view));
|
||||
}
|
||||
},
|
||||
);
|
||||
favoriteListener.start(
|
||||
favoritesUpdated: (result, isFavorite) {
|
||||
result.fold((error) {}, (result) {
|
||||
final current =
|
||||
result.items.firstWhereOrNull((v) => v.id == view.id);
|
||||
result.items.firstWhereOrNull((v) => v.id == state.view.id);
|
||||
if (current != null) {
|
||||
add(ViewEvent.viewDidUpdate(left(current)));
|
||||
}
|
||||
@ -57,28 +56,40 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
},
|
||||
);
|
||||
final isExpanded = await _getViewIsExpanded(view);
|
||||
emit(state.copyWith(isExpanded: isExpanded));
|
||||
await _loadViewsWhenExpanded(emit, isExpanded);
|
||||
},
|
||||
setIsEditing: (e) {
|
||||
emit(state.copyWith(isEditing: e.isEditing));
|
||||
},
|
||||
setIsExpanded: (e) async {
|
||||
if (e.isExpanded) {
|
||||
if (e.isExpanded && !state.isExpanded) {
|
||||
await _loadViewsWhenExpanded(emit, true);
|
||||
} else {
|
||||
emit(state.copyWith(isExpanded: e.isExpanded));
|
||||
}
|
||||
await _setViewIsExpanded(view, e.isExpanded);
|
||||
},
|
||||
viewDidUpdate: (e) {
|
||||
viewDidUpdate: (e) async {
|
||||
final result = await ViewBackendService.getView(
|
||||
view.id,
|
||||
);
|
||||
final view_ = result.fold((l) => l, (r) => null);
|
||||
e.result.fold(
|
||||
(view) => emit(
|
||||
state.copyWith(
|
||||
view: view,
|
||||
childViews: view.childViews,
|
||||
successOrFailure: left(unit),
|
||||
),
|
||||
),
|
||||
(view) async {
|
||||
Log.debug('viewDidUpdate: $view');
|
||||
// ignore child view changes because it only contains one level
|
||||
// children data.
|
||||
if (_isSameViewIgnoreChildren(view, state.view)) {
|
||||
// do nothing.
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: view_ ?? view,
|
||||
successOrFailure: left(unit),
|
||||
),
|
||||
);
|
||||
},
|
||||
(error) => emit(
|
||||
state.copyWith(successOrFailure: right(error)),
|
||||
),
|
||||
@ -91,7 +102,17 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
);
|
||||
emit(
|
||||
result.fold(
|
||||
(l) => state.copyWith(successOrFailure: left(unit)),
|
||||
(l) {
|
||||
final view = state.view;
|
||||
view.freeze();
|
||||
final newView = view.rebuild(
|
||||
(b) => b.name = e.newName,
|
||||
);
|
||||
return state.copyWith(
|
||||
successOrFailure: left(unit),
|
||||
view: newView,
|
||||
);
|
||||
},
|
||||
(error) => state.copyWith(successOrFailure: right(error)),
|
||||
),
|
||||
);
|
||||
@ -149,6 +170,13 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
viewUpdateChildView: (e) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: e.result,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -165,30 +193,39 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
bool isExpanded,
|
||||
) async {
|
||||
if (!isExpanded) {
|
||||
return;
|
||||
}
|
||||
if (state.childViews.isNotEmpty) {
|
||||
// notify the old child views
|
||||
emit(
|
||||
state.copyWith(
|
||||
childViews: state.childViews,
|
||||
isExpanded: true,
|
||||
view: view,
|
||||
isExpanded: false,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final viewsOrFailed =
|
||||
await ViewBackendService.getChildViews(viewId: state.view.id);
|
||||
|
||||
viewsOrFailed.fold(
|
||||
(childViews) => emit(
|
||||
state.copyWith(
|
||||
childViews: childViews,
|
||||
isExpanded: true,
|
||||
),
|
||||
),
|
||||
(childViews) {
|
||||
state.view.freeze();
|
||||
final viewWithChildViews = state.view.rebuild((b) {
|
||||
b.childViews.clear();
|
||||
b.childViews.addAll(childViews);
|
||||
});
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: viewWithChildViews,
|
||||
isExpanded: true,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
(error) => emit(
|
||||
state.copyWith(
|
||||
successOrFailure: right(error),
|
||||
isExpanded: true,
|
||||
isLoading: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -216,6 +253,70 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<ViewPB?> _updateChildViews(
|
||||
ChildViewUpdatePB update,
|
||||
) async {
|
||||
Log.debug(
|
||||
'received child views of ${this.view.name}(${this.view.id}) update, $update',
|
||||
);
|
||||
if (update.createChildViews.isNotEmpty) {
|
||||
// refresh the child views if the update isn't empty
|
||||
// because there's no info to get the inserted index.
|
||||
assert(update.parentViewId == this.view.id);
|
||||
final view = await ViewBackendService.getView(
|
||||
update.parentViewId,
|
||||
);
|
||||
return view.fold((l) => l, (r) => null);
|
||||
}
|
||||
|
||||
final view = state.view;
|
||||
view.freeze();
|
||||
final childViews = [...view.childViews];
|
||||
if (update.deleteChildViews.isNotEmpty) {
|
||||
childViews.removeWhere((v) => update.deleteChildViews.contains(v.id));
|
||||
return view.rebuild((p0) {
|
||||
p0.childViews.clear();
|
||||
p0.childViews.addAll(childViews);
|
||||
});
|
||||
}
|
||||
|
||||
if (update.updateChildViews.isNotEmpty) {
|
||||
final view = await ViewBackendService.getView(
|
||||
update.parentViewId,
|
||||
);
|
||||
final childViews = view.fold((l) => l.childViews, (r) => []);
|
||||
bool isSameOrder = true;
|
||||
if (childViews.length == update.updateChildViews.length) {
|
||||
for (var i = 0; i < childViews.length; i++) {
|
||||
if (childViews[i].id != update.updateChildViews[i].id) {
|
||||
isSameOrder = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isSameOrder = false;
|
||||
}
|
||||
if (!isSameOrder) {
|
||||
return view.fold((l) => l, (r) => null);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _isSameViewIgnoreChildren(ViewPB from, ViewPB to) {
|
||||
return _hash(from) == _hash(to);
|
||||
}
|
||||
|
||||
int _hash(ViewPB view) => Object.hash(
|
||||
view.id,
|
||||
view.name,
|
||||
view.createTime,
|
||||
view.icon,
|
||||
view.parentViewId,
|
||||
view.layout,
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -239,25 +340,27 @@ class ViewEvent with _$ViewEvent {
|
||||
}) = CreateView;
|
||||
const factory ViewEvent.viewDidUpdate(Either<ViewPB, FlowyError> result) =
|
||||
ViewDidUpdate;
|
||||
const factory ViewEvent.viewUpdateChildView(ViewPB result) =
|
||||
ViewUpdateChildView;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ViewState with _$ViewState {
|
||||
const factory ViewState({
|
||||
required ViewPB view,
|
||||
required List<ViewPB> childViews,
|
||||
required bool isEditing,
|
||||
required bool isExpanded,
|
||||
required Either<Unit, FlowyError> successOrFailure,
|
||||
@Default(true) bool isLoading,
|
||||
@Default(null) ViewPB? lastCreatedView,
|
||||
}) = _ViewState;
|
||||
|
||||
factory ViewState.init(ViewPB view) => ViewState(
|
||||
view: view,
|
||||
childViews: view.childViews,
|
||||
isExpanded: false,
|
||||
isEditing: false,
|
||||
successOrFailure: left(unit),
|
||||
lastCreatedView: null,
|
||||
isLoading: true,
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:flutter/material.dart';
|
||||
@ -66,9 +65,6 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
|
||||
if (!_shouldAccept(data.data, position)) {
|
||||
return;
|
||||
}
|
||||
Log.debug(
|
||||
'offset: $offset, position: $position, size: ${renderBox.size}',
|
||||
);
|
||||
_updatePosition(position);
|
||||
},
|
||||
onLeave: (_) => _updatePosition(
|
||||
|
@ -84,14 +84,10 @@ class ViewItem extends StatelessWidget {
|
||||
listener: (context, state) =>
|
||||
context.read<TabsBloc>().openPlugin(state.lastCreatedView!),
|
||||
builder: (context, state) {
|
||||
// don't remove this code. it's related to the backend service.
|
||||
view.childViews
|
||||
..clear()
|
||||
..addAll(state.childViews);
|
||||
return InnerViewItem(
|
||||
view: state.view,
|
||||
parentView: parentView,
|
||||
childViews: state.childViews,
|
||||
childViews: state.view.childViews,
|
||||
categoryType: categoryType,
|
||||
level: level,
|
||||
leftPadding: leftPadding,
|
||||
|
@ -164,12 +164,14 @@ class _ViewTitleState extends State<_ViewTitle> {
|
||||
_resetTextEditingController();
|
||||
viewListener.start(
|
||||
onViewUpdated: (view) {
|
||||
if (name != view.name || icon != view.icon.value) {
|
||||
widget.onUpdated();
|
||||
}
|
||||
setState(() {
|
||||
name = view.name;
|
||||
icon = view.icon.value;
|
||||
_resetTextEditingController();
|
||||
});
|
||||
widget.onUpdated();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:ffi/ffi.dart' as ffi;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:ffi/ffi.dart' as ffi;
|
||||
|
||||
import 'ffi.dart';
|
||||
|
||||
class Log {
|
||||
@ -13,15 +14,14 @@ class Log {
|
||||
Log() {
|
||||
_logger = Logger(
|
||||
printer: PrettyPrinter(
|
||||
methodCount: 2, // number of method calls to be displayed
|
||||
errorMethodCount:
|
||||
8, // number of method calls if stacktrace is provided
|
||||
lineLength: 120, // width of the output
|
||||
colors: true, // Colorful log messages
|
||||
printEmojis: true, // Print an emoji for each log message
|
||||
printTime: false // Should each log print contain a timestamp
|
||||
),
|
||||
level: kDebugMode ? Level.verbose : Level.info,
|
||||
methodCount: 2, // number of method calls to be displayed
|
||||
errorMethodCount: 8, // number of method calls if stacktrace is provided
|
||||
lineLength: 120, // width of the output
|
||||
colors: true, // Colorful log messages
|
||||
printEmojis: true, // Print an emoji for each log message
|
||||
printTime: false, // Should each log print contain a timestamp
|
||||
),
|
||||
level: kDebugMode ? Level.trace : Level.info,
|
||||
);
|
||||
}
|
||||
|
||||
@ -29,7 +29,11 @@ class Log {
|
||||
if (isReleaseVersion()) {
|
||||
log(0, toNativeUtf8(msg));
|
||||
} else {
|
||||
Log.shared._logger.i(msg, error, stackTrace);
|
||||
Log.shared._logger.i(
|
||||
msg,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +41,11 @@ class Log {
|
||||
if (isReleaseVersion()) {
|
||||
log(1, toNativeUtf8(msg));
|
||||
} else {
|
||||
Log.shared._logger.d(msg, error, stackTrace);
|
||||
Log.shared._logger.d(
|
||||
msg,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +53,11 @@ class Log {
|
||||
if (isReleaseVersion()) {
|
||||
log(3, toNativeUtf8(msg));
|
||||
} else {
|
||||
Log.shared._logger.w(msg, error, stackTrace);
|
||||
Log.shared._logger.w(
|
||||
msg,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,7 +65,11 @@ class Log {
|
||||
if (isReleaseVersion()) {
|
||||
log(2, toNativeUtf8(msg));
|
||||
} else {
|
||||
Log.shared._logger.v(msg, error, stackTrace);
|
||||
Log.shared._logger.t(
|
||||
msg,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,7 +77,11 @@ class Log {
|
||||
if (isReleaseVersion()) {
|
||||
log(4, toNativeUtf8(msg));
|
||||
} else {
|
||||
Log.shared._logger.e(msg, error, stackTrace);
|
||||
Log.shared._logger.e(
|
||||
msg,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ dependencies:
|
||||
protobuf: ^3.1.0
|
||||
dartz: ^0.10.1
|
||||
freezed_annotation:
|
||||
logger: ^1.0.0
|
||||
logger: ^2.0.0
|
||||
plugin_platform_interface: ^2.1.3
|
||||
json_annotation: ^4.7.0
|
||||
|
||||
|
@ -1067,13 +1067,13 @@ packages:
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
logger:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: logger
|
||||
sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f"
|
||||
sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "2.0.2+1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -163,6 +163,8 @@ dependency_overrides:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "82f3baf"
|
||||
|
||||
logger: ^2.0.0
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
@ -7,11 +8,10 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/board.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
|
||||
import '../../util.dart';
|
||||
import '../grid_test/util.dart';
|
||||
@ -27,7 +27,7 @@ class AppFlowyBoardTest {
|
||||
}
|
||||
|
||||
Future<BoardTestContext> createTestBoard() async {
|
||||
final app = await unitTest.createTestApp();
|
||||
final app = await unitTest.createWorkspace();
|
||||
final builder = BoardPluginBuilder();
|
||||
return ViewBackendService.createView(
|
||||
parentViewId: app.id,
|
||||
|
@ -5,7 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
|
||||
import '../util.dart';
|
||||
|
||||
Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async {
|
||||
final app = await gridTest.unitTest.createTestApp();
|
||||
final app = await gridTest.unitTest.createWorkspace();
|
||||
final context = await ViewBackendService.createView(
|
||||
parentViewId: app.id,
|
||||
name: "Filter Grid",
|
||||
|
@ -162,7 +162,7 @@ class AppFlowyGridTest {
|
||||
}
|
||||
|
||||
Future<GridTestContext> createTestGrid() async {
|
||||
final app = await unitTest.createTestApp();
|
||||
final app = await unitTest.createWorkspace();
|
||||
final context = await ViewBackendService.createView(
|
||||
parentViewId: app.id,
|
||||
name: "Test Grid",
|
||||
|
@ -1,166 +0,0 @@
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/app/app_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/menu_view_section_bloc.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../../util.dart';
|
||||
|
||||
void main() {
|
||||
late AppFlowyUnitTest testContext;
|
||||
setUpAll(() async {
|
||||
testContext = await AppFlowyUnitTest.ensureInitialized();
|
||||
});
|
||||
|
||||
test('rename app test', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
bloc.add(const AppEvent.rename('Hello world'));
|
||||
await blocResponseFuture();
|
||||
|
||||
expect(bloc.state.view.name, 'Hello world');
|
||||
});
|
||||
|
||||
test('delete app test', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
bloc.add(const AppEvent.delete());
|
||||
await blocResponseFuture();
|
||||
|
||||
final apps = await testContext.loadApps();
|
||||
expect(apps.where((element) => element.id == app.id).isEmpty, true);
|
||||
});
|
||||
|
||||
test('create documents in order', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
bloc.add(const AppEvent.createView("3", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(bloc.state.views[0].name == '1');
|
||||
assert(bloc.state.views[1].name == '2');
|
||||
assert(bloc.state.views[2].name == '3');
|
||||
});
|
||||
|
||||
test('reorder documents test', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
bloc.add(const AppEvent.createView("3", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
assert(bloc.state.views.length == 3);
|
||||
|
||||
final appViewData = ViewDataContext(viewId: app.id);
|
||||
appViewData.views = bloc.state.views;
|
||||
|
||||
final viewSectionBloc = ViewSectionBloc(
|
||||
appViewData: appViewData,
|
||||
)..add(const ViewSectionEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
viewSectionBloc.add(const ViewSectionEvent.moveView(0, 2));
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(bloc.state.views[0].name == '2');
|
||||
assert(bloc.state.views[1].name == '3');
|
||||
assert(bloc.state.views[2].name == '1');
|
||||
});
|
||||
|
||||
test('open latest view test', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
assert(
|
||||
bloc.state.latestCreatedView == null,
|
||||
"assert initial latest create view is null after initialize",
|
||||
);
|
||||
|
||||
bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
assert(
|
||||
bloc.state.latestCreatedView!.id == bloc.state.views.last.id,
|
||||
"create a view and assert the latest create view is this view",
|
||||
);
|
||||
|
||||
bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
assert(
|
||||
bloc.state.latestCreatedView!.id == bloc.state.views.last.id,
|
||||
"create a view and assert the latest create view is this view",
|
||||
);
|
||||
});
|
||||
|
||||
test('open latest documents test', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
bloc.add(const AppEvent.createView("document 1", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
final document1 = bloc.state.latestCreatedView;
|
||||
assert(document1!.name == "document 1");
|
||||
|
||||
bloc.add(const AppEvent.createView("document 2", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
final document2 = bloc.state.latestCreatedView;
|
||||
assert(document2!.name == "document 2");
|
||||
|
||||
// Open document 1
|
||||
// ignore: unused_local_variable
|
||||
final documentBloc = DocumentBloc(view: document1!)
|
||||
..add(const DocumentEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
final workspaceSetting = await FolderEventGetCurrentWorkspaceSetting()
|
||||
.send()
|
||||
.then((result) => result.fold((l) => l, (r) => throw Exception()));
|
||||
workspaceSetting.latestView.id == document1.id;
|
||||
});
|
||||
|
||||
test('open latest document test', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
bloc.add(const AppEvent.createView("document 1", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
final document = bloc.state.latestCreatedView;
|
||||
assert(document!.name == "document 1");
|
||||
|
||||
bloc.add(const AppEvent.createView("grid 2", ViewLayoutPB.Grid));
|
||||
await blocResponseFuture();
|
||||
final grid = bloc.state.latestCreatedView;
|
||||
assert(grid!.name == "grid 2");
|
||||
|
||||
var workspaceSetting = await FolderEventGetCurrentWorkspaceSetting()
|
||||
.send()
|
||||
.then((result) => result.fold((l) => l, (r) => throw Exception()));
|
||||
workspaceSetting.latestView.id == grid!.id;
|
||||
|
||||
// Open grid 1
|
||||
// ignore: unused_local_variable
|
||||
final documentBloc = DocumentBloc(view: document!)
|
||||
..add(const DocumentEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
workspaceSetting = await FolderEventGetCurrentWorkspaceSetting()
|
||||
.send()
|
||||
.then((result) => result.fold((l) => l, (r) => throw Exception()));
|
||||
workspaceSetting.latestView.id == document.id;
|
||||
});
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import 'package:appflowy/workspace/application/app/app_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../../util.dart';
|
||||
|
||||
void main() {
|
||||
late AppFlowyUnitTest testContext;
|
||||
setUpAll(() async {
|
||||
testContext = await AppFlowyUnitTest.ensureInitialized();
|
||||
});
|
||||
|
||||
test('create a document', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
bloc.add(const AppEvent.createView("Test document", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(bloc.state.views.length == 1);
|
||||
assert(bloc.state.views.last.name == "Test document");
|
||||
assert(bloc.state.views.last.layout == ViewLayoutPB.Document);
|
||||
});
|
||||
|
||||
test('create a grid', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
bloc.add(const AppEvent.createView("Test grid", ViewLayoutPB.Grid));
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(bloc.state.views.length == 1);
|
||||
assert(bloc.state.views.last.name == "Test grid");
|
||||
assert(bloc.state.views.last.layout == ViewLayoutPB.Grid);
|
||||
});
|
||||
|
||||
test('create a kanban', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
bloc.add(const AppEvent.createView("Test board", ViewLayoutPB.Board));
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(bloc.state.views.length == 1);
|
||||
assert(bloc.state.views.last.name == "Test board");
|
||||
assert(bloc.state.views.last.layout == ViewLayoutPB.Board);
|
||||
});
|
||||
|
||||
test('create a calendar', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final bloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
bloc.add(const AppEvent.createView("Test calendar", ViewLayoutPB.Calendar));
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(bloc.state.views.length == 1);
|
||||
assert(bloc.state.views.last.name == "Test calendar");
|
||||
assert(bloc.state.views.last.layout == ViewLayoutPB.Calendar);
|
||||
});
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/app/app_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/home/home_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -13,7 +13,7 @@ void main() {
|
||||
testContext = await AppFlowyUnitTest.ensureInitialized();
|
||||
});
|
||||
|
||||
test('initi home screen', () async {
|
||||
test('init home screen', () async {
|
||||
final workspaceSetting = await FolderEventGetCurrentWorkspaceSetting()
|
||||
.send()
|
||||
.then((result) => result.fold((l) => l, (r) => throw Exception()));
|
||||
@ -36,16 +36,16 @@ void main() {
|
||||
..add(const HomeEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
final app = await testContext.createTestApp();
|
||||
final appBloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
assert(appBloc.state.latestCreatedView == null);
|
||||
final app = await testContext.createWorkspace();
|
||||
final appBloc = ViewBloc(view: app)..add(const ViewEvent.initial());
|
||||
assert(appBloc.state.lastCreatedView == null);
|
||||
|
||||
appBloc
|
||||
.add(const AppEvent.createView("New document", ViewLayoutPB.Document));
|
||||
.add(const ViewEvent.createView("New document", ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(appBloc.state.latestCreatedView != null);
|
||||
final latestView = appBloc.state.latestCreatedView!;
|
||||
assert(appBloc.state.lastCreatedView != null);
|
||||
final latestView = appBloc.state.lastCreatedView!;
|
||||
final _ = DocumentBloc(view: latestView)
|
||||
..add(const DocumentEvent.initial());
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/plugins/trash/application/trash_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/app/app_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -7,42 +7,42 @@ import '../../util.dart';
|
||||
|
||||
class TrashTestContext {
|
||||
late ViewPB view;
|
||||
late AppBloc appBloc;
|
||||
late ViewBloc viewBloc;
|
||||
late List<ViewPB> allViews;
|
||||
final AppFlowyUnitTest unitTest;
|
||||
|
||||
TrashTestContext(this.unitTest);
|
||||
|
||||
Future<void> initialize() async {
|
||||
view = await unitTest.createTestApp();
|
||||
appBloc = AppBloc(view: view)..add(const AppEvent.initial());
|
||||
view = await unitTest.createWorkspace();
|
||||
viewBloc = ViewBloc(view: view)..add(const ViewEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
appBloc.add(
|
||||
const AppEvent.createView(
|
||||
viewBloc.add(
|
||||
const ViewEvent.createView(
|
||||
"Document 1",
|
||||
ViewLayoutPB.Document,
|
||||
),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
|
||||
appBloc.add(
|
||||
const AppEvent.createView(
|
||||
viewBloc.add(
|
||||
const ViewEvent.createView(
|
||||
"Document 2",
|
||||
ViewLayoutPB.Document,
|
||||
),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
|
||||
appBloc.add(
|
||||
const AppEvent.createView(
|
||||
viewBloc.add(
|
||||
const ViewEvent.createView(
|
||||
"Document 3",
|
||||
ViewLayoutPB.Document,
|
||||
),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
|
||||
allViews = [...appBloc.state.view.childViews];
|
||||
allViews = [...viewBloc.state.view.childViews];
|
||||
assert(allViews.length == 3, 'but receive ${allViews.length}');
|
||||
}
|
||||
}
|
||||
@ -67,22 +67,29 @@ void main() {
|
||||
await blocResponseFuture(millisecond: 200);
|
||||
|
||||
// delete a view
|
||||
final deletedView = context.appBloc.state.view.childViews[0];
|
||||
context.appBloc.add(AppEvent.deleteView(deletedView.id));
|
||||
final deletedView = context.viewBloc.state.view.childViews[0];
|
||||
final deleteViewBloc = ViewBloc(view: deletedView)
|
||||
..add(const ViewEvent.initial());
|
||||
await blocResponseFuture();
|
||||
assert(context.appBloc.state.view.childViews.length == 2);
|
||||
deleteViewBloc.add(const ViewEvent.delete());
|
||||
await blocResponseFuture();
|
||||
assert(context.viewBloc.state.view.childViews.length == 2);
|
||||
assert(trashBloc.state.objects.length == 1);
|
||||
assert(trashBloc.state.objects.first.id == deletedView.id);
|
||||
|
||||
// put back
|
||||
trashBloc.add(TrashEvent.putback(deletedView.id));
|
||||
await blocResponseFuture();
|
||||
assert(context.appBloc.state.view.childViews.length == 3);
|
||||
assert(context.viewBloc.state.view.childViews.length == 3);
|
||||
assert(trashBloc.state.objects.isEmpty);
|
||||
|
||||
// delete all views
|
||||
for (final view in context.allViews) {
|
||||
context.appBloc.add(AppEvent.deleteView(view.id));
|
||||
final deleteViewBloc = ViewBloc(view: view)
|
||||
..add(const ViewEvent.initial());
|
||||
await blocResponseFuture();
|
||||
deleteViewBloc.add(const ViewEvent.delete());
|
||||
await blocResponseFuture();
|
||||
await blocResponseFuture();
|
||||
}
|
||||
expect(trashBloc.state.objects[0].id, context.allViews[0].id);
|
||||
|
@ -1,100 +1,181 @@
|
||||
import 'package:appflowy/workspace/application/app/app_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../util.dart';
|
||||
|
||||
void main() {
|
||||
const name = 'Hello world';
|
||||
|
||||
late AppFlowyUnitTest testContext;
|
||||
|
||||
setUpAll(() async {
|
||||
testContext = await AppFlowyUnitTest.ensureInitialized();
|
||||
});
|
||||
|
||||
Future<ViewBloc> createTestViewBloc() async {
|
||||
final view = await testContext.createWorkspace();
|
||||
final viewBloc = ViewBloc(view: view)
|
||||
..add(
|
||||
const ViewEvent.initial(),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
return viewBloc;
|
||||
}
|
||||
|
||||
test('rename view test', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
|
||||
final appBloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
appBloc.add(
|
||||
const AppEvent.createView("Test document", ViewLayoutPB.Document),
|
||||
);
|
||||
|
||||
final viewBloc = await createTestViewBloc();
|
||||
viewBloc.add(const ViewEvent.rename(name));
|
||||
await blocResponseFuture();
|
||||
|
||||
final viewBloc = ViewBloc(view: appBloc.state.views.first)
|
||||
..add(const ViewEvent.initial());
|
||||
viewBloc.add(const ViewEvent.rename('Hello world'));
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(viewBloc.state.view.name == "Hello world");
|
||||
expect(viewBloc.state.view.name, name);
|
||||
});
|
||||
|
||||
test('duplicate view test', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final appBloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
appBloc.add(
|
||||
const AppEvent.createView("Test document", ViewLayoutPB.Document),
|
||||
final viewBloc = await createTestViewBloc();
|
||||
// create a nested view
|
||||
viewBloc.add(
|
||||
const ViewEvent.createView(name, ViewLayoutPB.Document),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
|
||||
final viewBloc = ViewBloc(view: appBloc.state.views.first)
|
||||
..add(const ViewEvent.initial());
|
||||
expect(viewBloc.state.view.childViews.length, 1);
|
||||
final childViewBloc = ViewBloc(view: viewBloc.state.view.childViews.first)
|
||||
..add(
|
||||
const ViewEvent.initial(),
|
||||
);
|
||||
childViewBloc.add(const ViewEvent.duplicate());
|
||||
await blocResponseFuture();
|
||||
|
||||
viewBloc.add(const ViewEvent.duplicate());
|
||||
await blocResponseFuture();
|
||||
|
||||
expect(appBloc.state.views.length, 2);
|
||||
expect(viewBloc.state.view.childViews.length, 2);
|
||||
});
|
||||
|
||||
test('delete view test', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
final appBloc = AppBloc(view: app)..add(const AppEvent.initial());
|
||||
await blocResponseFuture();
|
||||
|
||||
appBloc.add(
|
||||
const AppEvent.createView("Test document", ViewLayoutPB.Document),
|
||||
final viewBloc = await createTestViewBloc();
|
||||
viewBloc.add(
|
||||
const ViewEvent.createView(name, ViewLayoutPB.Document),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
expect(appBloc.state.views.length, 1);
|
||||
|
||||
final viewBloc = ViewBloc(view: appBloc.state.views.first)
|
||||
..add(const ViewEvent.initial());
|
||||
expect(viewBloc.state.view.childViews.length, 1);
|
||||
final childViewBloc = ViewBloc(view: viewBloc.state.view.childViews.first)
|
||||
..add(
|
||||
const ViewEvent.initial(),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
|
||||
viewBloc.add(const ViewEvent.delete());
|
||||
childViewBloc.add(const ViewEvent.delete());
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(appBloc.state.views.isEmpty);
|
||||
assert(viewBloc.state.view.childViews.isEmpty);
|
||||
});
|
||||
|
||||
test('create nested view test', () async {
|
||||
final app = await testContext.createTestApp();
|
||||
|
||||
final appBloc = AppBloc(view: app);
|
||||
appBloc
|
||||
..add(
|
||||
const AppEvent.initial(),
|
||||
)
|
||||
..add(
|
||||
const AppEvent.createView('Document 1', ViewLayoutPB.Document),
|
||||
);
|
||||
final viewBloc = await createTestViewBloc();
|
||||
viewBloc.add(
|
||||
const ViewEvent.createView('Document 1', ViewLayoutPB.Document),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
|
||||
// create a nested view
|
||||
const name = 'Document 1 - 1';
|
||||
final viewBloc = ViewBloc(view: appBloc.state.views.first);
|
||||
viewBloc
|
||||
final document1Bloc = ViewBloc(view: viewBloc.state.view.childViews.first)
|
||||
..add(
|
||||
const ViewEvent.initial(),
|
||||
)
|
||||
);
|
||||
await blocResponseFuture();
|
||||
const name = 'Document 1 - 1';
|
||||
document1Bloc.add(
|
||||
const ViewEvent.createView('Document 1 - 1', ViewLayoutPB.Document),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
expect(document1Bloc.state.view.childViews.length, 1);
|
||||
expect(document1Bloc.state.view.childViews.first.name, name);
|
||||
});
|
||||
|
||||
test('create documents in order', () async {
|
||||
final viewBloc = await createTestViewBloc();
|
||||
final names = ['1', '2', '3'];
|
||||
for (final name in names) {
|
||||
viewBloc.add(
|
||||
ViewEvent.createView(name, ViewLayoutPB.Document),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
}
|
||||
|
||||
expect(viewBloc.state.view.childViews.length, 3);
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
expect(viewBloc.state.view.childViews[i].name, names[i]);
|
||||
}
|
||||
});
|
||||
|
||||
test('open latest view test', () async {
|
||||
final viewBloc = await createTestViewBloc();
|
||||
expect(viewBloc.state.lastCreatedView, isNull);
|
||||
|
||||
viewBloc.add(const ViewEvent.createView('1', ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
expect(
|
||||
viewBloc.state.lastCreatedView!.id,
|
||||
viewBloc.state.view.childViews.last.id,
|
||||
);
|
||||
expect(
|
||||
viewBloc.state.lastCreatedView!.name,
|
||||
'1',
|
||||
);
|
||||
|
||||
viewBloc.add(const ViewEvent.createView('2', ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
expect(
|
||||
viewBloc.state.lastCreatedView!.name,
|
||||
'2',
|
||||
);
|
||||
});
|
||||
|
||||
test('open latest document test', () async {
|
||||
const name1 = 'document';
|
||||
final viewBloc = await createTestViewBloc();
|
||||
viewBloc.add(const ViewEvent.createView(name1, ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
final document = viewBloc.state.lastCreatedView!;
|
||||
assert(document.name == name1);
|
||||
|
||||
const gird = 'grid';
|
||||
viewBloc.add(const ViewEvent.createView(gird, ViewLayoutPB.Document));
|
||||
await blocResponseFuture();
|
||||
assert(viewBloc.state.lastCreatedView!.name == gird);
|
||||
|
||||
var workspaceSetting =
|
||||
await FolderEventGetCurrentWorkspaceSetting().send().then(
|
||||
(result) => result.fold(
|
||||
(l) => l,
|
||||
(r) => throw Exception(),
|
||||
),
|
||||
);
|
||||
workspaceSetting.latestView.id == viewBloc.state.lastCreatedView!.id;
|
||||
|
||||
// ignore: unused_local_variable
|
||||
final documentBloc = DocumentBloc(view: document)
|
||||
..add(
|
||||
const ViewEvent.createView(name, ViewLayoutPB.Document),
|
||||
const DocumentEvent.initial(),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
|
||||
assert(viewBloc.state.childViews.first.name == name);
|
||||
workspaceSetting =
|
||||
await FolderEventGetCurrentWorkspaceSetting().send().then(
|
||||
(result) => result.fold(
|
||||
(l) => l,
|
||||
(r) => throw Exception(),
|
||||
),
|
||||
);
|
||||
workspaceSetting.latestView.id == document.id;
|
||||
});
|
||||
|
||||
test('create views', () async {
|
||||
final viewBloc = await createTestViewBloc();
|
||||
const layouts = ViewLayoutPB.values;
|
||||
for (var i = 0; i < layouts.length; i++) {
|
||||
final layout = layouts[i];
|
||||
viewBloc.add(
|
||||
ViewEvent.createView('Test $layout', layout),
|
||||
);
|
||||
await blocResponseFuture();
|
||||
expect(viewBloc.state.view.childViews.length, i + 1);
|
||||
expect(viewBloc.state.view.childViews.last.name, 'Test $layout');
|
||||
expect(viewBloc.state.view.childViews.last.layout, layout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
import 'package:appflowy/main.dart';
|
||||
import 'package:appflowy/startup/launch_configuration.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:appflowy/main.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AppFlowyIntegrateTest {
|
||||
@ -84,7 +84,7 @@ class AppFlowyUnitTest {
|
||||
workspaceService = WorkspaceService(workspaceId: currentWorkspace.id);
|
||||
}
|
||||
|
||||
Future<ViewPB> createTestApp() async {
|
||||
Future<ViewPB> createWorkspace() async {
|
||||
final result = await workspaceService.createApp(name: "Test App");
|
||||
return result.fold(
|
||||
(app) => app,
|
||||
|
@ -2,7 +2,9 @@ use std::collections::HashSet;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use collab::core::collab_state::SyncState;
|
||||
use collab_folder::{Folder, TrashChange, TrashChangeReceiver, ViewChange, ViewChangeReceiver};
|
||||
use collab_folder::{
|
||||
Folder, TrashChange, TrashChangeReceiver, View, ViewChange, ViewChangeReceiver,
|
||||
};
|
||||
use tokio_stream::wrappers::WatchStream;
|
||||
use tokio_stream::StreamExt;
|
||||
use tracing::{event, Level};
|
||||
@ -43,11 +45,12 @@ pub(crate) fn subscribe_folder_view_changed(
|
||||
}
|
||||
},
|
||||
ViewChange::DidUpdate { view } => {
|
||||
notify_view_did_change(view.clone());
|
||||
notify_child_views_changed(
|
||||
view_pb_without_child_views(Arc::new(view.clone())),
|
||||
ChildViewChangeReason::DidUpdateView,
|
||||
);
|
||||
notify_parent_view_did_change(folder.clone(), vec![view.parent_view_id]);
|
||||
notify_parent_view_did_change(folder.clone(), vec![view.parent_view_id.clone()]);
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -181,6 +184,14 @@ pub(crate) fn notify_did_update_workspace(workspace_id: &str, folder: &Folder) {
|
||||
.send();
|
||||
}
|
||||
|
||||
fn notify_view_did_change(view: View) -> Option<()> {
|
||||
let view_pb = view_pb_without_child_views(Arc::new(view.clone()));
|
||||
send_notification(&view.id, FolderNotification::DidUpdateView)
|
||||
.payload(view_pb)
|
||||
.send();
|
||||
None
|
||||
}
|
||||
|
||||
pub enum ChildViewChangeReason {
|
||||
DidCreateView,
|
||||
DidDeleteView,
|
||||
|
Loading…
Reference in New Issue
Block a user