mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: show loading indicator when loading data from remote (#3120)
* chore: show circle indicator if fetch the data from remote * chore: fix the lb warnings * chore: create sdk-build for macOS
This commit is contained in:
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@ -20,14 +20,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# - name: Create .env file
|
|
||||||
# working-directory: frontend/appflowy_flutter
|
|
||||||
# run: |
|
|
||||||
# touch .env
|
|
||||||
# echo SUPABASE_URL=${{ secrets.HOST_URL }} >> .env
|
|
||||||
# echo SUPABASE_ANON_KEY=${{ secrets.HOST_ANON_KEY }} >> .env
|
|
||||||
# echo SUPABASE_JWT_SECRET=${{ secrets.HOST_JWT_SECRET }} >> .env
|
|
||||||
|
|
||||||
- name: Build release notes
|
- name: Build release notes
|
||||||
run: |
|
run: |
|
||||||
touch ${{ env.RELEASE_NOTES_PATH }}
|
touch ${{ env.RELEASE_NOTES_PATH }}
|
||||||
|
@ -13,6 +13,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'database_view_service.dart';
|
import 'database_view_service.dart';
|
||||||
import 'defines.dart';
|
import 'defines.dart';
|
||||||
import 'layout/layout_service.dart';
|
import 'layout/layout_service.dart';
|
||||||
@ -92,6 +93,8 @@ class DatabaseController {
|
|||||||
final DatabaseGroupListener _groupListener;
|
final DatabaseGroupListener _groupListener;
|
||||||
final DatabaseLayoutSettingListener _layoutListener;
|
final DatabaseLayoutSettingListener _layoutListener;
|
||||||
|
|
||||||
|
final ValueNotifier<bool> _isLoading = ValueNotifier(true);
|
||||||
|
|
||||||
DatabaseController({required ViewPB view})
|
DatabaseController({required ViewPB view})
|
||||||
: viewId = view.id,
|
: viewId = view.id,
|
||||||
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
|
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
|
||||||
@ -109,6 +112,12 @@ class DatabaseController {
|
|||||||
_listenOnLayoutChanged();
|
_listenOnLayoutChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setIsLoading(bool isLoading) {
|
||||||
|
_isLoading.value = isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueNotifier<bool> get isLoading => _isLoading;
|
||||||
|
|
||||||
void addListener({
|
void addListener({
|
||||||
DatabaseCallbacks? onDatabaseChanged,
|
DatabaseCallbacks? onDatabaseChanged,
|
||||||
DatabaseLayoutSettingCallbacks? onLayoutChanged,
|
DatabaseLayoutSettingCallbacks? onLayoutChanged,
|
||||||
|
@ -3,12 +3,16 @@ import 'dart:collection';
|
|||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
import '../grid/presentation/widgets/filter/filter_info.dart';
|
import '../grid/presentation/widgets/filter/filter_info.dart';
|
||||||
import 'field/field_controller.dart';
|
import 'field/field_controller.dart';
|
||||||
import 'row/row_cache.dart';
|
import 'row/row_cache.dart';
|
||||||
import 'row/row_service.dart';
|
import 'row/row_service.dart';
|
||||||
|
|
||||||
|
part 'defines.freezed.dart';
|
||||||
|
|
||||||
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
|
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
|
||||||
typedef OnFiltersChanged = void Function(List<FilterInfo>);
|
typedef OnFiltersChanged = void Function(List<FilterInfo>);
|
||||||
typedef OnSortsChanged = void Function(List<SortInfo>);
|
typedef OnSortsChanged = void Function(List<SortInfo>);
|
||||||
@ -27,3 +31,11 @@ typedef OnNumOfRowsChanged = void Function(
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef OnError = void Function(FlowyError);
|
typedef OnError = void Function(FlowyError);
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class LoadingState with _$LoadingState {
|
||||||
|
const factory LoadingState.loading() = _Loading;
|
||||||
|
const factory LoadingState.finish(
|
||||||
|
Either<Unit, FlowyError> successOrFail,
|
||||||
|
) = _Finish;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:appflowy/plugins/database_view/application/defines.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||||
import 'package:appflowy_board/appflowy_board.dart';
|
import 'package:appflowy_board/appflowy_board.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
@ -254,11 +255,14 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
Future<void> _openGrid(Emitter<BoardState> emit) async {
|
Future<void> _openGrid(Emitter<BoardState> emit) async {
|
||||||
final result = await databaseController.open();
|
final result = await databaseController.open();
|
||||||
result.fold(
|
result.fold(
|
||||||
(grid) => emit(
|
(grid) {
|
||||||
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
|
databaseController.setIsLoading(false);
|
||||||
),
|
emit(
|
||||||
|
state.copyWith(loadingState: LoadingState.finish(left(unit))),
|
||||||
|
);
|
||||||
|
},
|
||||||
(err) => emit(
|
(err) => emit(
|
||||||
state.copyWith(loadingState: GridLoadingState.finish(right(err))),
|
state.copyWith(loadingState: LoadingState.finish(right(err))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -323,7 +327,7 @@ class BoardState with _$BoardState {
|
|||||||
required Option<DatabasePB> grid,
|
required Option<DatabasePB> grid,
|
||||||
required List<String> groupIds,
|
required List<String> groupIds,
|
||||||
required Option<BoardEditingRow> editingRow,
|
required Option<BoardEditingRow> editingRow,
|
||||||
required GridLoadingState loadingState,
|
required LoadingState loadingState,
|
||||||
required Option<FlowyError> noneOrError,
|
required Option<FlowyError> noneOrError,
|
||||||
}) = _BoardState;
|
}) = _BoardState;
|
||||||
|
|
||||||
@ -333,18 +337,10 @@ class BoardState with _$BoardState {
|
|||||||
groupIds: [],
|
groupIds: [],
|
||||||
editingRow: none(),
|
editingRow: none(),
|
||||||
noneOrError: none(),
|
noneOrError: none(),
|
||||||
loadingState: const _Loading(),
|
loadingState: const LoadingState.loading(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
|
||||||
class GridLoadingState with _$GridLoadingState {
|
|
||||||
const factory GridLoadingState.loading() = _Loading;
|
|
||||||
const factory GridLoadingState.finish(
|
|
||||||
Either<Unit, FlowyError> successOrFail,
|
|
||||||
) = _Finish;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GridFieldEquatable extends Equatable {
|
class GridFieldEquatable extends Equatable {
|
||||||
final UnmodifiableListView<FieldPB> _fields;
|
final UnmodifiableListView<FieldPB> _fields;
|
||||||
const GridFieldEquatable(
|
const GridFieldEquatable(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/defines.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
@ -133,11 +134,14 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
Future<void> _openDatabase(Emitter<CalendarState> emit) async {
|
Future<void> _openDatabase(Emitter<CalendarState> emit) async {
|
||||||
final result = await databaseController.open();
|
final result = await databaseController.open();
|
||||||
result.fold(
|
result.fold(
|
||||||
(database) => emit(
|
(database) {
|
||||||
state.copyWith(loadingState: DatabaseLoadingState.finish(left(unit))),
|
databaseController.setIsLoading(false);
|
||||||
),
|
emit(
|
||||||
|
state.copyWith(loadingState: LoadingState.finish(left(unit))),
|
||||||
|
);
|
||||||
|
},
|
||||||
(err) => emit(
|
(err) => emit(
|
||||||
state.copyWith(loadingState: DatabaseLoadingState.finish(right(err))),
|
state.copyWith(loadingState: LoadingState.finish(right(err))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -425,7 +429,7 @@ class CalendarState with _$CalendarState {
|
|||||||
CalendarEventData<CalendarDayEvent>? updateEvent,
|
CalendarEventData<CalendarDayEvent>? updateEvent,
|
||||||
required List<String> deleteEventIds,
|
required List<String> deleteEventIds,
|
||||||
required Option<CalendarLayoutSettingPB> settings,
|
required Option<CalendarLayoutSettingPB> settings,
|
||||||
required DatabaseLoadingState loadingState,
|
required LoadingState loadingState,
|
||||||
required Option<FlowyError> noneOrError,
|
required Option<FlowyError> noneOrError,
|
||||||
}) = _CalendarState;
|
}) = _CalendarState;
|
||||||
|
|
||||||
@ -436,18 +440,10 @@ class CalendarState with _$CalendarState {
|
|||||||
deleteEventIds: [],
|
deleteEventIds: [],
|
||||||
settings: none(),
|
settings: none(),
|
||||||
noneOrError: none(),
|
noneOrError: none(),
|
||||||
loadingState: const _Loading(),
|
loadingState: const LoadingState.loading(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DatabaseLoadingState with _$DatabaseLoadingState {
|
|
||||||
const factory DatabaseLoadingState.loading() = _Loading;
|
|
||||||
const factory DatabaseLoadingState.finish(
|
|
||||||
Either<Unit, FlowyError> successOrFail,
|
|
||||||
) = _Finish;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CalendarEditingRow {
|
class CalendarEditingRow {
|
||||||
RowPB row;
|
RowPB row;
|
||||||
int? index;
|
int? index;
|
||||||
|
@ -159,10 +159,22 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
],
|
],
|
||||||
child: BlocBuilder<CalendarBloc, CalendarState>(
|
child: BlocBuilder<CalendarBloc, CalendarState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return _buildCalendar(
|
return ValueListenableBuilder<bool>(
|
||||||
context,
|
valueListenable: widget.databaseController.isLoading,
|
||||||
_eventController,
|
builder: (_, value, ___) {
|
||||||
state.settings.foldLeft(0, (previous, a) => a.firstDayOfWeek),
|
if (value) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _buildCalendar(
|
||||||
|
context,
|
||||||
|
_eventController,
|
||||||
|
state.settings
|
||||||
|
.foldLeft(0, (previous, a) => a.firstDayOfWeek),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/defines.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -134,12 +134,13 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
|||||||
final result = await databaseController.open();
|
final result = await databaseController.open();
|
||||||
result.fold(
|
result.fold(
|
||||||
(grid) {
|
(grid) {
|
||||||
|
databaseController.setIsLoading(false);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
|
state.copyWith(loadingState: LoadingState.finish(left(unit))),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(err) => emit(
|
(err) => emit(
|
||||||
state.copyWith(loadingState: GridLoadingState.finish(right(err))),
|
state.copyWith(loadingState: LoadingState.finish(right(err))),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -177,7 +178,7 @@ class GridState with _$GridState {
|
|||||||
required GridFieldEquatable fields,
|
required GridFieldEquatable fields,
|
||||||
required List<RowInfo> rowInfos,
|
required List<RowInfo> rowInfos,
|
||||||
required int rowCount,
|
required int rowCount,
|
||||||
required GridLoadingState loadingState,
|
required LoadingState loadingState,
|
||||||
required bool reorderable,
|
required bool reorderable,
|
||||||
required ChangedReason reason,
|
required ChangedReason reason,
|
||||||
required List<SortInfo> sorts,
|
required List<SortInfo> sorts,
|
||||||
@ -191,21 +192,13 @@ class GridState with _$GridState {
|
|||||||
grid: none(),
|
grid: none(),
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
reorderable: true,
|
reorderable: true,
|
||||||
loadingState: const _Loading(),
|
loadingState: const LoadingState.loading(),
|
||||||
reason: const InitialListState(),
|
reason: const InitialListState(),
|
||||||
filters: [],
|
filters: [],
|
||||||
sorts: [],
|
sorts: [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
|
||||||
class GridLoadingState with _$GridLoadingState {
|
|
||||||
const factory GridLoadingState.loading() = _Loading;
|
|
||||||
const factory GridLoadingState.finish(
|
|
||||||
Either<Unit, FlowyError> successOrFail,
|
|
||||||
) = _Finish;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GridFieldEquatable extends Equatable {
|
class GridFieldEquatable extends Equatable {
|
||||||
final List<FieldInfo> _fields;
|
final List<FieldInfo> _fields;
|
||||||
const GridFieldEquatable(
|
const GridFieldEquatable(
|
||||||
|
@ -47,20 +47,29 @@ class GridSettingBar extends StatelessWidget {
|
|||||||
listener: (context, state) => toggleExtension.toggle(),
|
listener: (context, state) => toggleExtension.toggle(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: SizedBox(
|
child: ValueListenableBuilder<bool>(
|
||||||
height: 40,
|
valueListenable: controller.isLoading,
|
||||||
child: Row(
|
builder: (context, value, child) {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
if (value) {
|
||||||
children: [
|
return const SizedBox.shrink();
|
||||||
SizedBox(width: GridSize.leadingHeaderPadding),
|
} else {
|
||||||
const Spacer(),
|
return SizedBox(
|
||||||
const FilterButton(),
|
height: 40,
|
||||||
const SortButton(),
|
child: Row(
|
||||||
SettingButton(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
databaseController: controller,
|
children: [
|
||||||
),
|
SizedBox(width: GridSize.leadingHeaderPadding),
|
||||||
],
|
const Spacer(),
|
||||||
),
|
const FilterButton(),
|
||||||
|
const SortButton(),
|
||||||
|
SettingButton(
|
||||||
|
databaseController: controller,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -93,30 +93,46 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
|
|||||||
],
|
],
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
||||||
children: [
|
builder: (context, state) {
|
||||||
BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
return ValueListenableBuilder<bool>(
|
||||||
builder: (context, state) {
|
valueListenable: state
|
||||||
return const Flexible(
|
.tabBarControllerByViewId[state.parentView.id]!
|
||||||
child: Padding(
|
.controller
|
||||||
padding: EdgeInsets.only(left: 50),
|
.isLoading,
|
||||||
child: DatabaseTabBar(),
|
builder: (_, value, ___) {
|
||||||
),
|
if (value) {
|
||||||
);
|
return const SizedBox.shrink();
|
||||||
|
} else {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return const Flexible(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 50),
|
||||||
|
child: DatabaseTabBar(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 300,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 50),
|
||||||
|
child: pageSettingBarFromState(state),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
},
|
||||||
builder: (context, state) {
|
|
||||||
return SizedBox(
|
|
||||||
width: 300,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 50),
|
|
||||||
child: pageSettingBarFromState(state),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
BlocBuilder<GridTabBarBloc, GridTabBarState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
@ -65,7 +65,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
|||||||
child: BlocBuilder<DocumentBloc, DocumentState>(
|
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return state.loadingState.when(
|
return state.loadingState.when(
|
||||||
loading: () => const SizedBox.shrink(),
|
loading: () =>
|
||||||
|
const Center(child: CircularProgressIndicator.adaptive()),
|
||||||
finish: (result) => result.fold(
|
finish: (result) => result.fold(
|
||||||
(error) {
|
(error) {
|
||||||
Log.error(error);
|
Log.error(error);
|
||||||
|
@ -18,7 +18,7 @@ class UserBackendService {
|
|||||||
static Future<Either<FlowyError, UserProfilePB>>
|
static Future<Either<FlowyError, UserProfilePB>>
|
||||||
getCurrentUserProfile() async {
|
getCurrentUserProfile() async {
|
||||||
final result = await UserEventGetUserProfile().send().then((value) {
|
final result = await UserEventGetUserProfile().send().then((value) {
|
||||||
value.fold((l) => null, (r) => Log.error(r));
|
value.fold((l) => null, (r) => Log.info(r));
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
return result.swap();
|
return result.swap();
|
||||||
|
@ -10,10 +10,10 @@ use collab_document::document_data::default_document_data;
|
|||||||
use collab_document::YrsDocAction;
|
use collab_document::YrsDocAction;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
use crate::document::MutexDocument;
|
|
||||||
use flowy_document_deps::cloud::DocumentCloudService;
|
use flowy_document_deps::cloud::DocumentCloudService;
|
||||||
use flowy_error::{internal_error, FlowyError, FlowyResult};
|
use flowy_error::{internal_error, FlowyError, FlowyResult};
|
||||||
|
|
||||||
|
use crate::document::MutexDocument;
|
||||||
use crate::entities::DocumentSnapshotPB;
|
use crate::entities::DocumentSnapshotPB;
|
||||||
|
|
||||||
pub trait DocumentUser: Send + Sync {
|
pub trait DocumentUser: Send + Sync {
|
||||||
@ -77,13 +77,13 @@ impl DocumentManager {
|
|||||||
let mut updates = vec![];
|
let mut updates = vec![];
|
||||||
if !self.is_doc_exist(doc_id)? {
|
if !self.is_doc_exist(doc_id)? {
|
||||||
// Try to get the document from the cloud service
|
// Try to get the document from the cloud service
|
||||||
if let Ok(document_updates) = self.cloud_service.get_document_updates(doc_id).await {
|
match self.cloud_service.get_document_updates(doc_id).await {
|
||||||
updates = document_updates;
|
Ok(document_updates) => updates = document_updates,
|
||||||
} else {
|
Err(e) => {
|
||||||
return Err(
|
tracing::error!("Get document data failed: {:?}", e);
|
||||||
FlowyError::record_not_found().context(format!("document: {} is not exist", doc_id)),
|
return Err(FlowyError::internal().context("Can't not read the document data"));
|
||||||
);
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("open_document: {:?}", doc_id);
|
tracing::debug!("open_document: {:?}", doc_id);
|
||||||
|
@ -1,108 +0,0 @@
|
|||||||
# AppFlowy Cloud Architecture
|
|
||||||
|
|
||||||
AppFlowy supports multiple cloud solutions. Users can choose their preferred cloud provider, such as Supabase, Firebase,
|
|
||||||
AWS, or our own AppFlowyCloud (Self-hosted server).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Design
|
|
||||||
|
|
||||||
AppFlowy use the traits [AppFlowyServer] to abstract the cloud provider. Each cloud provider implements the [AppFlowyServer]
|
|
||||||
trait. As the image below shows. Users can choose their preferred cloud provider or simply use the default option, which is the LocalServer. When using the
|
|
||||||
LocalServer, data is stored on the local file system. Users can migrate to a cloud provider if needed. For instance, one
|
|
||||||
could migrate from LocalServer to AppFlowyCloud. This migration would create a new user in the cloud and transfer all the
|
|
||||||
data from the local database to the cloud.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## AppFlowy Cloud Implementation (WIP)
|
|
||||||
|
|
||||||
### Restful API
|
|
||||||
|
|
||||||
### Table schema
|
|
||||||
|
|
||||||
## Supabase Implementation
|
|
||||||
|
|
||||||
### Table schema
|
|
||||||

|
|
||||||
|
|
||||||
1. `af_roles` table: This table contains a list of roles that are used in your application, such as 'Owner', 'Member', and 'Guest'.
|
|
||||||
|
|
||||||
2. `af_permissions` table: This table stores permissions that are used in your application. Each permission has a name, a description, and an access level.
|
|
||||||
|
|
||||||
3. `af_role_permissions` table: This is a many-to-many relation table between roles and permissions. It represents which permissions a role has.
|
|
||||||
|
|
||||||
4. `af_user` table: This stores the details of users like uuid, email, uid, name, created_at. Here, uid is an auto-incrementing integer that uniquely identifies a user.
|
|
||||||
|
|
||||||
5. `af_workspace` table: This table contains all the workspaces. Each workspace has an owner which is associated with the uid of a user in the `af_user` table.
|
|
||||||
|
|
||||||
6. `af_workspace_member` table: This table maintains a list of all the members associated with a workspace and their roles.
|
|
||||||
|
|
||||||
7. `af_collab` and `af_collab_member` tables: These tables store the collaborations and their members respectively. Each collaboration has an owner and a workspace associated with it.
|
|
||||||
|
|
||||||
8. `af_collab_update`, `af_collab_update_document`, `af_collab_update_database`, `af_collab_update_w_database`, `af_collab_update_folder`, `af_database_row_update` tables: These tables are used for handling updates to collaborations.
|
|
||||||
|
|
||||||
9. `af_collab_statistics`, `af_collab_snapshot`, `af_collab_state`: These tables and view are used for maintaining statistics and snapshots of collaborations.
|
|
||||||
|
|
||||||
10. `af_user_profile_view` view: This view is used to get the latest workspace_id for each user.
|
|
||||||
|
|
||||||

|
|
||||||
Here's a detailed description for each of these triggers:
|
|
||||||
|
|
||||||
1. `create_af_workspace_trigger`:
|
|
||||||
|
|
||||||
This trigger is designed to automate the process of workspace creation in the `af_workspace` table after a new user is inserted into the `af_user` table. When a new user is added, this trigger fires and inserts a new record into the `af_workspace` table, setting the `owner_uid` to the UID of the new user.
|
|
||||||
|
|
||||||
2. `manage_af_workspace_member_role_trigger`:
|
|
||||||
|
|
||||||
This trigger helps to manage the roles of workspace members. After an insert operation on the `af_workspace` table, this trigger automatically fires and creates a new record in the `af_workspace_member` table. The new record identifies the user as a member of the workspace with the role 'Owner'. This ensures that every new workspace has an owner.
|
|
||||||
|
|
||||||
3. `insert_into_af_collab_trigger`:
|
|
||||||
|
|
||||||
The purpose of this trigger is to ensure consistency between the `af_collab_update` and `af_collab` tables. When an insert operation is about to be performed on the `af_collab_update` table, this trigger fires before the insert operation. It checks if a corresponding collaboration exists in the `af_collab` table using the oid and uid. If a corresponding collaboration does not exist, the trigger creates one, using the oid, uid, and current timestamp. This way, every collab update operation corresponds to a valid collaboration.
|
|
||||||
|
|
||||||
4. `insert_into_af_collab_member_trigger`:
|
|
||||||
|
|
||||||
This trigger helps to manage the membership of users in collaborations. After a new collaboration is inserted into the `af_collab` table, this trigger fires. It checks if a corresponding collaboration member exists in the `af_collab_member` table. If a corresponding member does not exist, the trigger creates one, using the collaboration id and user id. This ensures that every collaboration has at least one member.
|
|
||||||
|
|
||||||
5. `af_collab_snapshot_update_edit_count_trigger`:
|
|
||||||
|
|
||||||
This trigger is designed to keep track of the number of edits on each collaboration snapshot in the `af_collab_snapshot` table. When an update operation is performed on the `af_collab_snapshot` table, this trigger fires. It increments the `edit_count` of the corresponding record in the `af_collab_snapshot` table by one. This ensures that the application can keep track of how many times each collaboration snapshot has been edited.
|
|
||||||
|
|
||||||
|
|
||||||
### Supabase configuration
|
|
||||||
|
|
||||||
#### Test
|
|
||||||
In order to run the test, you need to set up the .env.test file.
|
|
||||||
```dotenv
|
|
||||||
# Supabase configuration
|
|
||||||
SUPABASE_URL="your-supabase-url"
|
|
||||||
SUPABASE_ANON_KEY="your-supabase-anonymous-key"
|
|
||||||
SUPABASE_KEY="your-supabase-key"
|
|
||||||
SUPABASE_JWT_SECRET="your-supabase-jwt-secret"
|
|
||||||
|
|
||||||
# Supabase Database configuration
|
|
||||||
SUPABASE_DB="your-supabase-db-url"
|
|
||||||
SUPABASE_DB_USER="your-db-username"
|
|
||||||
SUPABASE_DB_PORT="your-db-port"
|
|
||||||
SUPABASE_DB_PASSWORD="your-db-password"
|
|
||||||
```
|
|
||||||
|
|
||||||
1. `SUPABASE_URL`: This is the URL of your Supabase server instance. Your application will use this URL to interact with the Supabase service.
|
|
||||||
|
|
||||||
2. `SUPABASE_ANON_KEY`: This is the anonymous API key from Supabase, used for operations that don't require user authentication. Operations performed with this key are done as the anonymous role in the database.
|
|
||||||
|
|
||||||
3. `SUPABASE_KEY`: This is the API key with higher privileges from Supabase. It is generally used for server-side operations that require more permissions than an anonymous user.
|
|
||||||
|
|
||||||
4. `SUPABASE_JWT_SECRET`: This is the secret used to verify JWT tokens generated by Supabase. JWT or JSON Web Token is a standard method for securely transferring data between parties as a JSON object.
|
|
||||||
|
|
||||||
5. `SUPABASE_DB`: This is the URL for the database your Supabase server instance is using.
|
|
||||||
|
|
||||||
6. `SUPABASE_DB_USER`: This is the username used to authenticate with the Supabase database, in this case, it's 'postgres', which is a common default for PostgreSQL.
|
|
||||||
|
|
||||||
7. `SUPABASE_DB_PORT`: This is the port number where your Supabase database service is accessible. The default PostgreSQL port is 5432, and you are using this default port.
|
|
||||||
|
|
||||||
8. `SUPABASE_DB_PASSWORD`: This is the password used to authenticate the `SUPABASE_DB_USER` with the Supabase database.
|
|
||||||
|
|
||||||
For example, if you want to run the supabase tests located in flowy-test crate. You need to put the `.env.test` file under
|
|
||||||
the flowy-test folder.
|
|
Binary file not shown.
Before Width: | Height: | Size: 61 KiB |
@ -1,78 +0,0 @@
|
|||||||
@startuml
|
|
||||||
title "Application"
|
|
||||||
left to right direction
|
|
||||||
package "AppFlowy Application" {
|
|
||||||
[User]
|
|
||||||
}
|
|
||||||
|
|
||||||
cloud "Supabase Server" {
|
|
||||||
[RESTful Component]
|
|
||||||
[Realtime Component]
|
|
||||||
[Postgres DB]
|
|
||||||
}
|
|
||||||
|
|
||||||
database "LocalServer" {
|
|
||||||
[Local Server Component]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
cloud "AppFlowy Cloud Server" {
|
|
||||||
[RESTful Component] as [AppFlowy RESTful Component]
|
|
||||||
[Realtime Component] as [AppFlowy Realtime Component]
|
|
||||||
[Postgres DB] as [AppFlowy Postgres DB]
|
|
||||||
}
|
|
||||||
|
|
||||||
User --> [AppFlowy Application]
|
|
||||||
[AppFlowy Application] --> [Local Server Component] : Connect
|
|
||||||
|
|
||||||
[AppFlowy Application] --> [RESTful Component] : RESTful API Communication
|
|
||||||
[AppFlowy Application] <..> [Realtime Component] : WebSocket Communication
|
|
||||||
|
|
||||||
[AppFlowy Application] --> [AppFlowy RESTful Component] : RESTful API Communication
|
|
||||||
[AppFlowy Application] <..> [AppFlowy Realtime Component] : WebSocket Communication
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
|
|
||||||
|
|
||||||
@startuml
|
|
||||||
left to right direction
|
|
||||||
|
|
||||||
interface AppFlowyServer {
|
|
||||||
+ enable_sync(_enable: bool)
|
|
||||||
+ user_service(): Arc<dyn UserService>
|
|
||||||
+ folder_service(): Arc<dyn FolderCloudService>
|
|
||||||
+ database_service(): Arc<dyn DatabaseCloudService>
|
|
||||||
+ document_service(): Arc<dyn DocumentCloudService>
|
|
||||||
+ collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
|
|
||||||
}
|
|
||||||
|
|
||||||
class SupabaseServer {
|
|
||||||
+ enable_sync(_enable: bool)
|
|
||||||
+ user_service(): Arc<dyn UserService>
|
|
||||||
+ folder_service(): Arc<dyn FolderCloudService>
|
|
||||||
+ database_service(): Arc<dyn DatabaseCloudService>
|
|
||||||
+ document_service(): Arc<dyn DocumentCloudService>
|
|
||||||
+ collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
|
|
||||||
}
|
|
||||||
|
|
||||||
class SelfHostServer {
|
|
||||||
+ user_service(): Arc<dyn UserService>
|
|
||||||
+ folder_service(): Arc<dyn FolderCloudService>
|
|
||||||
+ database_service(): Arc<dyn DatabaseCloudService>
|
|
||||||
+ document_service(): Arc<dyn DocumentCloudService>
|
|
||||||
+ collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
|
|
||||||
}
|
|
||||||
|
|
||||||
class LocalServer {
|
|
||||||
+ user_service(): Arc<dyn UserService>
|
|
||||||
+ folder_service(): Arc<dyn FolderCloudService>
|
|
||||||
+ database_service(): Arc<dyn DatabaseCloudService>
|
|
||||||
+ document_service(): Arc<dyn DocumentCloudService>
|
|
||||||
+ collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
|
|
||||||
}
|
|
||||||
|
|
||||||
SupabaseServer -u-|> AppFlowyServer
|
|
||||||
SelfHostServer -u-|> AppFlowyServer
|
|
||||||
LocalServer -u-|> AppFlowyServer
|
|
||||||
|
|
||||||
@enduml
|
|
Binary file not shown.
Before Width: | Height: | Size: 59 KiB |
Binary file not shown.
Before Width: | Height: | Size: 75 KiB |
@ -1,203 +0,0 @@
|
|||||||
@startuml
|
|
||||||
left to right direction
|
|
||||||
|
|
||||||
entity "af_roles" as roles {
|
|
||||||
id : SERIAL (PK)
|
|
||||||
name : TEXT
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_permissions" as permissions {
|
|
||||||
id : SERIAL (PK)
|
|
||||||
name : VARCHAR(255)
|
|
||||||
access_level : INTEGER
|
|
||||||
description : TEXT
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_role_permissions" as role_permissions {
|
|
||||||
role_id : INT (FK af_roles.id)
|
|
||||||
permission_id : INT (FK af_permissions.id)
|
|
||||||
--
|
|
||||||
(role_id, permission_id) : PK
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_user" as user {
|
|
||||||
uuid : UUID (PK)
|
|
||||||
email : TEXT
|
|
||||||
uid : BIGSERIAL
|
|
||||||
name : TEXT
|
|
||||||
created_at : TIMESTAMP WITH TIME ZONE
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_workspace" as workspace {
|
|
||||||
workspace_id : UUID (PK)
|
|
||||||
database_storage_id : UUID
|
|
||||||
owner_uid : BIGINT (FK af_user.uid)
|
|
||||||
created_at : TIMESTAMP WITH TIME ZONE
|
|
||||||
workspace_type : INTEGER
|
|
||||||
workspace_name : TEXT
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_workspace_member" as workspace_member {
|
|
||||||
uid : BIGINT
|
|
||||||
role_id : INT (FK af_roles.id)
|
|
||||||
workspace_id : UUID (FK af_workspace.workspace_id)
|
|
||||||
created_at : TIMESTAMP WITH TIME ZONE
|
|
||||||
updated_at : TIMESTAMP WITH TIME ZONE
|
|
||||||
--
|
|
||||||
(uid, workspace_id) : PK
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_collab" as collab {
|
|
||||||
oid : TEXT (PK)
|
|
||||||
owner_uid : BIGINT
|
|
||||||
workspace_id : UUID (FK af_workspace.workspace_id)
|
|
||||||
access_level : INTEGER
|
|
||||||
created_at : TIMESTAMP WITH TIME ZONE
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_collab_update" as collab_update {
|
|
||||||
oid : TEXT (FK af_collab.oid)
|
|
||||||
key : BIGSERIAL
|
|
||||||
value : BYTEA
|
|
||||||
value_size : INTEGER
|
|
||||||
partition_key : INTEGER
|
|
||||||
uid : BIGINT
|
|
||||||
md5 : TEXT
|
|
||||||
created_at : TIMESTAMP WITH TIME ZONE
|
|
||||||
workspace_id : UUID (FK af_workspace.workspace_id)
|
|
||||||
--
|
|
||||||
(oid, key, partition_key) : PK
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
entity "af_collab_update_document" as af_collab_update_document {
|
|
||||||
Inherits af_collab_update (partition_key = 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_collab_update_database" as af_collab_update_database {
|
|
||||||
Inherits af_collab_update (partition_key = 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_collab_update_w_database" as af_collab_update_w_database {
|
|
||||||
Inherits af_collab_update (partition_key = 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_collab_update_folder" as af_collab_update_folder {
|
|
||||||
Inherits af_collab_update (partition_key = 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
af_collab_update_document -u-|> collab_update
|
|
||||||
af_collab_update_database -u-|> collab_update
|
|
||||||
af_collab_update_w_database -u-|> collab_update
|
|
||||||
af_collab_update_folder -u-|> collab_update
|
|
||||||
|
|
||||||
entity "af_database_row_update" as database_row_update {
|
|
||||||
oid : TEXT
|
|
||||||
key : BIGSERIAL
|
|
||||||
value : BYTEA
|
|
||||||
value_size : INTEGER
|
|
||||||
partition_key : INTEGER
|
|
||||||
uid : BIGINT
|
|
||||||
md5 : TEXT
|
|
||||||
workspace_id : UUID (FK af_workspace.workspace_id)
|
|
||||||
--
|
|
||||||
(oid, key) : PK
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_collab_member" as collab_member {
|
|
||||||
uid : BIGINT (FK af_user.uid)
|
|
||||||
oid : TEXT (FK af_collab.oid)
|
|
||||||
role_id : INTEGER (FK af_roles.id)
|
|
||||||
--
|
|
||||||
(uid, oid) : PK
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_collab_statistics" as collab_statistics {
|
|
||||||
oid : TEXT (PK)
|
|
||||||
edit_count : BIGINT
|
|
||||||
}
|
|
||||||
|
|
||||||
entity "af_collab_snapshot" as collab_snapshot {
|
|
||||||
sid : BIGSERIAL (PK)
|
|
||||||
oid : TEXT (FK af_collab.oid)
|
|
||||||
name : TEXT
|
|
||||||
blob : BYTEA
|
|
||||||
blob_size : INTEGER
|
|
||||||
edit_count : BIGINT
|
|
||||||
created_at : TIMESTAMP WITH TIME ZONE
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
roles <-- role_permissions : FK
|
|
||||||
permissions <-u- role_permissions : FK
|
|
||||||
user <-- collab : FK
|
|
||||||
user <-- workspace : FK
|
|
||||||
user <-- collab_member : FK
|
|
||||||
roles <-- workspace_member : FK
|
|
||||||
workspace <-- workspace_member : FK
|
|
||||||
workspace <-- collab : FK
|
|
||||||
workspace <-- database_row_update : FK
|
|
||||||
collab <-- collab_update : FK
|
|
||||||
collab <-- collab_snapshot: FK
|
|
||||||
collab <-u- collab_member : FK
|
|
||||||
collab <-- collab_statistics : PK
|
|
||||||
roles <-- collab_member : FK
|
|
||||||
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
|
|
||||||
@startuml
|
|
||||||
title Triggers in Database Schema
|
|
||||||
|
|
||||||
participant "af_user" as A
|
|
||||||
participant "af_workspace" as B
|
|
||||||
participant "af_workspace_member" as C
|
|
||||||
participant "af_collab" as D
|
|
||||||
participant "af_collab_update" as E
|
|
||||||
participant "af_collab_member" as F
|
|
||||||
participant "af_collab_statistics" as G
|
|
||||||
participant "af_collab_snapshot" as H
|
|
||||||
|
|
||||||
A -> B: create_af_workspace_trigger
|
|
||||||
note right
|
|
||||||
This trigger fires after an insert on af_user. It automatically creates a workspace
|
|
||||||
with the uid of the new user as the owner_uid.
|
|
||||||
end note
|
|
||||||
|
|
||||||
B -> C: manage_af_workspace_member_role_trigger
|
|
||||||
note right
|
|
||||||
This trigger fires after an insert on af_workspace. It automatically
|
|
||||||
creates a workspace member in the af_workspace_member table with the
|
|
||||||
role 'Owner'.
|
|
||||||
end note
|
|
||||||
|
|
||||||
E -> D: insert_into_af_collab_trigger
|
|
||||||
note right
|
|
||||||
This trigger fires before an insert on af_collab_update.
|
|
||||||
It checks if a corresponding collab exists in the af_collab table.
|
|
||||||
If not, it creates one with the oid, uid, and current timestamp.
|
|
||||||
end note
|
|
||||||
|
|
||||||
D -> F: insert_into_af_collab_member_trigger
|
|
||||||
note right
|
|
||||||
This trigger fires after an insert on af_collab.
|
|
||||||
It automatically adds the collab's owner to the af_collab_member
|
|
||||||
table with the role 'Owner'.
|
|
||||||
end note
|
|
||||||
|
|
||||||
E -> G: af_collab_update_edit_count_trigger
|
|
||||||
note right
|
|
||||||
This trigger fires after an insert on af_collab_update.
|
|
||||||
It increments the edit_count of the corresponding collab in
|
|
||||||
the af_collab_statistics table.
|
|
||||||
end note
|
|
||||||
|
|
||||||
H -> G: af_collab_snapshot_update_edit_count_trigger
|
|
||||||
note right
|
|
||||||
This trigger fires after an insert on af_collab_snapshot.
|
|
||||||
It sets the edit_count of the new snapshot to the current
|
|
||||||
edit_count of the collab in the af_collab_statistics table.
|
|
||||||
end note
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 192 KiB |
@ -21,16 +21,16 @@ use crate::supabase::api::util::{ExtendedResponse, InsertParamsBuilder};
|
|||||||
use crate::supabase::api::{PostgresWrapper, SupabaseServerService};
|
use crate::supabase::api::{PostgresWrapper, SupabaseServerService};
|
||||||
use crate::supabase::define::*;
|
use crate::supabase::define::*;
|
||||||
|
|
||||||
pub struct RESTfulSupabaseCollabStorageImpl<T>(T);
|
pub struct SupabaseCollabStorageImpl<T>(T);
|
||||||
|
|
||||||
impl<T> RESTfulSupabaseCollabStorageImpl<T> {
|
impl<T> SupabaseCollabStorageImpl<T> {
|
||||||
pub fn new(server: T) -> Self {
|
pub fn new(server: T) -> Self {
|
||||||
Self(server)
|
Self(server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T> RemoteCollabStorage for RESTfulSupabaseCollabStorageImpl<T>
|
impl<T> RemoteCollabStorage for SupabaseCollabStorageImpl<T>
|
||||||
where
|
where
|
||||||
T: SupabaseServerService,
|
T: SupabaseServerService,
|
||||||
{
|
{
|
||||||
|
@ -12,17 +12,17 @@ use crate::supabase::api::request::{
|
|||||||
};
|
};
|
||||||
use crate::supabase::api::SupabaseServerService;
|
use crate::supabase::api::SupabaseServerService;
|
||||||
|
|
||||||
pub struct RESTfulSupabaseDatabaseServiceImpl<T> {
|
pub struct SupabaseDatabaseServiceImpl<T> {
|
||||||
server: T,
|
server: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> RESTfulSupabaseDatabaseServiceImpl<T> {
|
impl<T> SupabaseDatabaseServiceImpl<T> {
|
||||||
pub fn new(server: T) -> Self {
|
pub fn new(server: T) -> Self {
|
||||||
Self { server }
|
Self { server }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> DatabaseCloudService for RESTfulSupabaseDatabaseServiceImpl<T>
|
impl<T> DatabaseCloudService for SupabaseDatabaseServiceImpl<T>
|
||||||
where
|
where
|
||||||
T: SupabaseServerService,
|
T: SupabaseServerService,
|
||||||
{
|
{
|
||||||
|
@ -11,14 +11,14 @@ use lib_infra::future::FutureResult;
|
|||||||
use crate::supabase::api::request::{get_latest_snapshot_from_server, FetchObjectUpdateAction};
|
use crate::supabase::api::request::{get_latest_snapshot_from_server, FetchObjectUpdateAction};
|
||||||
use crate::supabase::api::SupabaseServerService;
|
use crate::supabase::api::SupabaseServerService;
|
||||||
|
|
||||||
pub struct RESTfulSupabaseDocumentServiceImpl<T>(T);
|
pub struct SupabaseDocumentServiceImpl<T>(T);
|
||||||
impl<T> RESTfulSupabaseDocumentServiceImpl<T> {
|
impl<T> SupabaseDocumentServiceImpl<T> {
|
||||||
pub fn new(server: T) -> Self {
|
pub fn new(server: T) -> Self {
|
||||||
Self(server)
|
Self(server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> DocumentCloudService for RESTfulSupabaseDocumentServiceImpl<T>
|
impl<T> DocumentCloudService for SupabaseDocumentServiceImpl<T>
|
||||||
where
|
where
|
||||||
T: SupabaseServerService,
|
T: SupabaseServerService,
|
||||||
{
|
{
|
||||||
@ -31,7 +31,7 @@ where
|
|||||||
async move {
|
async move {
|
||||||
let postgrest = try_get_postgrest?;
|
let postgrest = try_get_postgrest?;
|
||||||
let action = FetchObjectUpdateAction::new(document_id, CollabType::Document, postgrest);
|
let action = FetchObjectUpdateAction::new(document_id, CollabType::Document, postgrest);
|
||||||
action.run_with_fix_interval(5, 5).await
|
action.run_with_fix_interval(5, 10).await
|
||||||
}
|
}
|
||||||
.await,
|
.await,
|
||||||
)
|
)
|
||||||
|
@ -19,15 +19,15 @@ use crate::supabase::api::util::{ExtendedResponse, InsertParamsBuilder};
|
|||||||
use crate::supabase::api::SupabaseServerService;
|
use crate::supabase::api::SupabaseServerService;
|
||||||
use crate::supabase::define::*;
|
use crate::supabase::define::*;
|
||||||
|
|
||||||
pub struct RESTfulSupabaseFolderServiceImpl<T>(T);
|
pub struct SupabaseFolderServiceImpl<T>(T);
|
||||||
|
|
||||||
impl<T> RESTfulSupabaseFolderServiceImpl<T> {
|
impl<T> SupabaseFolderServiceImpl<T> {
|
||||||
pub fn new(server: T) -> Self {
|
pub fn new(server: T) -> Self {
|
||||||
Self(server)
|
Self(server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> FolderCloudService for RESTfulSupabaseFolderServiceImpl<T>
|
impl<T> FolderCloudService for SupabaseFolderServiceImpl<T>
|
||||||
where
|
where
|
||||||
T: SupabaseServerService,
|
T: SupabaseServerService,
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use collab_plugins::cloud_storage::{CollabObject, CollabType, RemoteCollabSnapshot};
|
use collab_plugins::cloud_storage::{CollabObject, CollabType, RemoteCollabSnapshot};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio_retry::strategy::FixedInterval;
|
use tokio_retry::strategy::FixedInterval;
|
||||||
use tokio_retry::{Action, Retry};
|
use tokio_retry::{Action, Condition, RetryIf};
|
||||||
|
|
||||||
use flowy_database_deps::cloud::{CollabObjectUpdate, CollabObjectUpdateByOid};
|
use flowy_database_deps::cloud::{CollabObjectUpdate, CollabObjectUpdateByOid};
|
||||||
use lib_infra::util::md5;
|
use lib_infra::util::md5;
|
||||||
@ -34,18 +34,20 @@ impl FetchObjectUpdateAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(self) -> Retry<Take<FixedInterval>, FetchObjectUpdateAction> {
|
pub fn run(self) -> RetryIf<Take<FixedInterval>, FetchObjectUpdateAction, RetryCondition> {
|
||||||
|
let postgrest = self.postgrest.clone();
|
||||||
let retry_strategy = FixedInterval::new(Duration::from_secs(5)).take(3);
|
let retry_strategy = FixedInterval::new(Duration::from_secs(5)).take(3);
|
||||||
Retry::spawn(retry_strategy, self)
|
RetryIf::spawn(retry_strategy, self, RetryCondition(postgrest))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_with_fix_interval(
|
pub fn run_with_fix_interval(
|
||||||
self,
|
self,
|
||||||
secs: u64,
|
secs: u64,
|
||||||
times: usize,
|
times: usize,
|
||||||
) -> Retry<Take<FixedInterval>, FetchObjectUpdateAction> {
|
) -> RetryIf<Take<FixedInterval>, FetchObjectUpdateAction, RetryCondition> {
|
||||||
|
let postgrest = self.postgrest.clone();
|
||||||
let retry_strategy = FixedInterval::new(Duration::from_secs(secs)).take(times);
|
let retry_strategy = FixedInterval::new(Duration::from_secs(secs)).take(times);
|
||||||
Retry::spawn(retry_strategy, self)
|
RetryIf::spawn(retry_strategy, self, RetryCondition(postgrest))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,9 +91,10 @@ impl BatchFetchObjectUpdateAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(self) -> Retry<Take<FixedInterval>, BatchFetchObjectUpdateAction> {
|
pub fn run(self) -> RetryIf<Take<FixedInterval>, BatchFetchObjectUpdateAction, RetryCondition> {
|
||||||
|
let postgrest = self.postgrest.clone();
|
||||||
let retry_strategy = FixedInterval::new(Duration::from_secs(5)).take(3);
|
let retry_strategy = FixedInterval::new(Duration::from_secs(5)).take(3);
|
||||||
Retry::spawn(retry_strategy, self)
|
RetryIf::spawn(retry_strategy, self, RetryCondition(postgrest))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,3 +305,10 @@ fn decode_hex_string(s: &str) -> Option<Vec<u8>> {
|
|||||||
let s = s.strip_prefix("\\x")?;
|
let s = s.strip_prefix("\\x")?;
|
||||||
hex::decode(s).ok()
|
hex::decode(s).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct RetryCondition(Weak<PostgresWrapper>);
|
||||||
|
impl Condition<anyhow::Error> for RetryCondition {
|
||||||
|
fn should_retry(&mut self, _error: &anyhow::Error) -> bool {
|
||||||
|
self.0.upgrade().is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -10,9 +10,9 @@ use flowy_server_config::supabase_config::SupabaseConfiguration;
|
|||||||
use flowy_user_deps::cloud::UserService;
|
use flowy_user_deps::cloud::UserService;
|
||||||
|
|
||||||
use crate::supabase::api::{
|
use crate::supabase::api::{
|
||||||
RESTfulPostgresServer, RESTfulSupabaseCollabStorageImpl, RESTfulSupabaseDatabaseServiceImpl,
|
RESTfulPostgresServer, RESTfulSupabaseUserAuthServiceImpl, SupabaseCollabStorageImpl,
|
||||||
RESTfulSupabaseDocumentServiceImpl, RESTfulSupabaseFolderServiceImpl,
|
SupabaseDatabaseServiceImpl, SupabaseDocumentServiceImpl, SupabaseFolderServiceImpl,
|
||||||
RESTfulSupabaseUserAuthServiceImpl, SupabaseServerServiceImpl,
|
SupabaseServerServiceImpl,
|
||||||
};
|
};
|
||||||
use crate::AppFlowyServer;
|
use crate::AppFlowyServer;
|
||||||
|
|
||||||
@ -96,25 +96,25 @@ impl AppFlowyServer for SupabaseServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn folder_service(&self) -> Arc<dyn FolderCloudService> {
|
fn folder_service(&self) -> Arc<dyn FolderCloudService> {
|
||||||
Arc::new(RESTfulSupabaseFolderServiceImpl::new(
|
Arc::new(SupabaseFolderServiceImpl::new(SupabaseServerServiceImpl(
|
||||||
SupabaseServerServiceImpl(self.restful_postgres.clone()),
|
self.restful_postgres.clone(),
|
||||||
))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn database_service(&self) -> Arc<dyn DatabaseCloudService> {
|
fn database_service(&self) -> Arc<dyn DatabaseCloudService> {
|
||||||
Arc::new(RESTfulSupabaseDatabaseServiceImpl::new(
|
Arc::new(SupabaseDatabaseServiceImpl::new(SupabaseServerServiceImpl(
|
||||||
SupabaseServerServiceImpl(self.restful_postgres.clone()),
|
self.restful_postgres.clone(),
|
||||||
))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn document_service(&self) -> Arc<dyn DocumentCloudService> {
|
fn document_service(&self) -> Arc<dyn DocumentCloudService> {
|
||||||
Arc::new(RESTfulSupabaseDocumentServiceImpl::new(
|
Arc::new(SupabaseDocumentServiceImpl::new(SupabaseServerServiceImpl(
|
||||||
SupabaseServerServiceImpl(self.restful_postgres.clone()),
|
self.restful_postgres.clone(),
|
||||||
))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collab_storage(&self) -> Option<Arc<dyn RemoteCollabStorage>> {
|
fn collab_storage(&self) -> Option<Arc<dyn RemoteCollabStorage>> {
|
||||||
Some(Arc::new(RESTfulSupabaseCollabStorageImpl::new(
|
Some(Arc::new(SupabaseCollabStorageImpl::new(
|
||||||
SupabaseServerServiceImpl(self.restful_postgres.clone()),
|
SupabaseServerServiceImpl(self.restful_postgres.clone()),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,8 @@ use uuid::Uuid;
|
|||||||
use flowy_database_deps::cloud::DatabaseCloudService;
|
use flowy_database_deps::cloud::DatabaseCloudService;
|
||||||
use flowy_folder_deps::cloud::FolderCloudService;
|
use flowy_folder_deps::cloud::FolderCloudService;
|
||||||
use flowy_server::supabase::api::{
|
use flowy_server::supabase::api::{
|
||||||
RESTfulPostgresServer, RESTfulSupabaseCollabStorageImpl, RESTfulSupabaseDatabaseServiceImpl,
|
RESTfulPostgresServer, RESTfulSupabaseUserAuthServiceImpl, SupabaseCollabStorageImpl,
|
||||||
RESTfulSupabaseFolderServiceImpl, RESTfulSupabaseUserAuthServiceImpl, SupabaseServerServiceImpl,
|
SupabaseDatabaseServiceImpl, SupabaseFolderServiceImpl, SupabaseServerServiceImpl,
|
||||||
};
|
};
|
||||||
use flowy_server::supabase::define::{USER_EMAIL, USER_UUID};
|
use flowy_server::supabase::define::{USER_EMAIL, USER_UUID};
|
||||||
use flowy_server_config::supabase_config::SupabaseConfiguration;
|
use flowy_server_config::supabase_config::SupabaseConfiguration;
|
||||||
@ -25,7 +25,7 @@ pub fn get_supabase_config() -> Option<SupabaseConfiguration> {
|
|||||||
pub fn collab_service() -> Arc<dyn RemoteCollabStorage> {
|
pub fn collab_service() -> Arc<dyn RemoteCollabStorage> {
|
||||||
let config = SupabaseConfiguration::from_env().unwrap();
|
let config = SupabaseConfiguration::from_env().unwrap();
|
||||||
let server = Arc::new(RESTfulPostgresServer::new(config));
|
let server = Arc::new(RESTfulPostgresServer::new(config));
|
||||||
Arc::new(RESTfulSupabaseCollabStorageImpl::new(
|
Arc::new(SupabaseCollabStorageImpl::new(
|
||||||
SupabaseServerServiceImpl::new(server),
|
SupabaseServerServiceImpl::new(server),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -33,7 +33,7 @@ pub fn collab_service() -> Arc<dyn RemoteCollabStorage> {
|
|||||||
pub fn database_service() -> Arc<dyn DatabaseCloudService> {
|
pub fn database_service() -> Arc<dyn DatabaseCloudService> {
|
||||||
let config = SupabaseConfiguration::from_env().unwrap();
|
let config = SupabaseConfiguration::from_env().unwrap();
|
||||||
let server = Arc::new(RESTfulPostgresServer::new(config));
|
let server = Arc::new(RESTfulPostgresServer::new(config));
|
||||||
Arc::new(RESTfulSupabaseDatabaseServiceImpl::new(
|
Arc::new(SupabaseDatabaseServiceImpl::new(
|
||||||
SupabaseServerServiceImpl::new(server),
|
SupabaseServerServiceImpl::new(server),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ pub fn user_auth_service() -> Arc<dyn UserService> {
|
|||||||
pub fn folder_service() -> Arc<dyn FolderCloudService> {
|
pub fn folder_service() -> Arc<dyn FolderCloudService> {
|
||||||
let config = SupabaseConfiguration::from_env().unwrap();
|
let config = SupabaseConfiguration::from_env().unwrap();
|
||||||
let server = Arc::new(RESTfulPostgresServer::new(config));
|
let server = Arc::new(RESTfulPostgresServer::new(config));
|
||||||
Arc::new(RESTfulSupabaseFolderServiceImpl::new(
|
Arc::new(SupabaseFolderServiceImpl::new(
|
||||||
SupabaseServerServiceImpl::new(server),
|
SupabaseServerServiceImpl::new(server),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -584,10 +584,7 @@ impl UserSession {
|
|||||||
match KV::get_object::<Session>(&self.session_config.session_cache_key) {
|
match KV::get_object::<Session>(&self.session_config.session_cache_key) {
|
||||||
None => Err(FlowyError::new(
|
None => Err(FlowyError::new(
|
||||||
ErrorCode::RecordNotFound,
|
ErrorCode::RecordNotFound,
|
||||||
format!(
|
"User is not logged in",
|
||||||
"Can't find the value of {}, User is not logged in",
|
|
||||||
self.session_config.session_cache_key
|
|
||||||
),
|
|
||||||
)),
|
)),
|
||||||
Some(session) => Ok(session),
|
Some(session) => Ok(session),
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,19 @@ script = [
|
|||||||
]
|
]
|
||||||
script_runner = "@shell"
|
script_runner = "@shell"
|
||||||
|
|
||||||
|
[tasks.sdk-build.mac]
|
||||||
|
private = true
|
||||||
|
script = [
|
||||||
|
"""
|
||||||
|
cd rust-lib/
|
||||||
|
rustup show
|
||||||
|
echo cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}"
|
||||||
|
RUSTFLAGS="-C target-cpu=native -C link-arg=-mmacosx-version-min=11.0" cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}"
|
||||||
|
cd ../
|
||||||
|
""",
|
||||||
|
]
|
||||||
|
script_runner = "@shell"
|
||||||
|
|
||||||
[tasks.sdk-build-android]
|
[tasks.sdk-build-android]
|
||||||
private = true
|
private = true
|
||||||
script = [
|
script = [
|
||||||
|
Reference in New Issue
Block a user