Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2991be276 | ||
|
|
3645dd1b8b | ||
|
|
4237a73810 | ||
|
|
80a2ebc216 | ||
|
|
02a9f0dfbb | ||
|
|
4a8536d403 | ||
|
|
9c3f083684 | ||
|
|
628eace341 | ||
|
|
77a53ef67a | ||
|
|
5dec6ea545 | ||
|
|
dbcf8ba86f | ||
|
|
63fe1a6ef3 | ||
|
|
a2c52508fa | ||
|
|
0709a2d2ee | ||
|
|
e73b310076 |
No files matched your search
@@ -69,4 +69,4 @@ jobs:
|
||||
SCRIPT_AFTER: |
|
||||
docker build -t appflowy-web-app .
|
||||
docker rm -f appflowy-web-app || true
|
||||
docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app
|
||||
docker run -d -p 80:80 -p 443:443 --env-file .env --name appflowy-web-app appflowy-web-app
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.6.0"
|
||||
APPFLOWY_VERSION = "0.6.1"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/space/mobile_space_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
@@ -22,6 +23,18 @@ class MobileSpace extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MobileSpaceState extends State<MobileSpace> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createNewPageNotifier.addListener(_createNewPage);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
createNewPageNotifier.removeListener(_createNewPage);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SpaceBloc, SpaceState>(
|
||||
@@ -81,6 +94,14 @@ class _MobileSpaceState extends State<MobileSpace> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _createNewPage() {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Pages extends StatelessWidget {
|
||||
|
||||
+5
-44
@@ -1,18 +1,15 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
final PropertyValueNotifier<ViewLayoutPB?> createNewPageNotifier =
|
||||
PropertyValueNotifier(null);
|
||||
|
||||
const _homeLabel = 'home';
|
||||
const _addLabel = 'add';
|
||||
const _notificationLabel = 'notification';
|
||||
@@ -93,7 +90,7 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
||||
void _onTap(BuildContext context, int bottomBarIndex) {
|
||||
if (_items[bottomBarIndex].label == _addLabel) {
|
||||
// show an add dialog
|
||||
_showCreatePageBottomSheet(context);
|
||||
createNewPageNotifier.value = ViewLayoutPB.Document;
|
||||
return;
|
||||
}
|
||||
// When navigating to a new branch, it's recommended to use the goBranch
|
||||
@@ -108,40 +105,4 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
||||
initialLocation: bottomBarIndex == navigationShell.currentIndex,
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreatePageBottomSheet(BuildContext context) {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
title: LocaleKeys.sideBar_addAPage.tr(),
|
||||
showDragHandle: true,
|
||||
showCloseButton: true,
|
||||
useRootNavigator: true,
|
||||
builder: (sheetContext) {
|
||||
return AddNewPageWidgetBottomSheet(
|
||||
view: ViewPB(),
|
||||
onAction: (layout) async {
|
||||
Navigator.of(sheetContext).pop();
|
||||
final currentWorkspaceId =
|
||||
await FolderEventReadCurrentWorkspace().send();
|
||||
await currentWorkspaceId.fold((s) async {
|
||||
final workspaceService = WorkspaceService(workspaceId: s.id);
|
||||
final result = await workspaceService.createView(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
viewSection: ViewSectionPB.Private,
|
||||
layout: layout,
|
||||
);
|
||||
result.fold((s) {
|
||||
context.pushView(s);
|
||||
}, (e) {
|
||||
Log.error('Failed to create new page: $e');
|
||||
});
|
||||
}, (e) {
|
||||
Log.error('Failed to read current workspace: $e');
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+117
-29
@@ -1,52 +1,102 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'document_share_bloc.freezed.dart';
|
||||
|
||||
const _url = 'https://test.appflowy.io';
|
||||
|
||||
class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
|
||||
DocumentShareBloc({
|
||||
required this.view,
|
||||
}) : super(const DocumentShareState.initial()) {
|
||||
}) : super(DocumentShareState.initial()) {
|
||||
on<DocumentShareEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final publishInfo = await ViewBackendService.getPublishInfo(view);
|
||||
publishInfo.fold((s) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: true,
|
||||
url: '$_url/${s.namespace}/${s.publishName}',
|
||||
),
|
||||
);
|
||||
}, (f) {
|
||||
emit(state.copyWith(isPublished: false, url: ''));
|
||||
});
|
||||
},
|
||||
share: (type, path) async {
|
||||
if (DocumentShareType.unimplemented.contains(type)) {
|
||||
Log.error('DocumentShareType $type is not implemented');
|
||||
return;
|
||||
}
|
||||
|
||||
emit(const DocumentShareState.loading());
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
final exporter = DocumentExporter(view);
|
||||
final FlowyResult<ExportDataPB, FlowyError> result =
|
||||
await exporter.export(type.exportType).then((value) {
|
||||
return value.fold(
|
||||
(s) {
|
||||
if (path != null) {
|
||||
switch (type) {
|
||||
case DocumentShareType.markdown:
|
||||
return FlowyResult.success(_saveMarkdownToPath(s, path));
|
||||
case DocumentShareType.html:
|
||||
return FlowyResult.success(_saveHTMLToPath(s, path));
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return FlowyResult.failure(FlowyError());
|
||||
},
|
||||
(f) => FlowyResult.failure(f),
|
||||
final result = await _export(type, path);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
exportResult: result,
|
||||
),
|
||||
);
|
||||
},
|
||||
publish: (url) async {
|
||||
// todo: optimize the logic
|
||||
const prefix = 'appflowy';
|
||||
final name = '${view.name}-${uuid()}'.substring(0, 19);
|
||||
|
||||
// set space name
|
||||
|
||||
try {
|
||||
final nameSpace =
|
||||
await ViewBackendService.getPublishNameSpace().getOrThrow();
|
||||
if (nameSpace.namespace != prefix) {
|
||||
await ViewBackendService.setPublishNameSpace(prefix).getOrThrow();
|
||||
}
|
||||
|
||||
final result = await ViewBackendService.publish(view, name: name);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: true,
|
||||
publishResult: result,
|
||||
url: '$_url/${nameSpace.namespace}/$name',
|
||||
),
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
Log.error('publish error: $e');
|
||||
|
||||
emit(DocumentShareState.finish(result));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: false,
|
||||
publishResult: FlowyResult.failure(
|
||||
FlowyError(msg: 'publish error: $e'),
|
||||
),
|
||||
url: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
unPublish: () async {
|
||||
await ViewBackendService.unpublish(view);
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: false,
|
||||
publishResult: null,
|
||||
url: '',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -54,6 +104,31 @@ class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
late final exporter = DocumentExporter(view);
|
||||
|
||||
Future<FlowyResult<ExportDataPB, FlowyError>> _export(
|
||||
DocumentShareType type,
|
||||
String? path,
|
||||
) async {
|
||||
final result = await exporter.export(type.exportType);
|
||||
return result.fold(
|
||||
(s) {
|
||||
if (path != null) {
|
||||
switch (type) {
|
||||
case DocumentShareType.markdown:
|
||||
return FlowySuccess(_saveMarkdownToPath(s, path));
|
||||
case DocumentShareType.html:
|
||||
return FlowySuccess(_saveHTMLToPath(s, path));
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return FlowyResult.failure(FlowyError());
|
||||
},
|
||||
(f) => FlowyResult.failure(f),
|
||||
);
|
||||
}
|
||||
|
||||
ExportDataPB _saveMarkdownToPath(String markdown, String path) {
|
||||
File(path).writeAsStringSync(markdown);
|
||||
return ExportDataPB()
|
||||
@@ -93,15 +168,28 @@ enum DocumentShareType {
|
||||
|
||||
@freezed
|
||||
class DocumentShareEvent with _$DocumentShareEvent {
|
||||
const factory DocumentShareEvent.share(DocumentShareType type, String? path) =
|
||||
Share;
|
||||
const factory DocumentShareEvent.initial() = _Initial;
|
||||
const factory DocumentShareEvent.share(
|
||||
DocumentShareType type,
|
||||
String? path,
|
||||
) = _Share;
|
||||
const factory DocumentShareEvent.publish(String url) = _Publish;
|
||||
const factory DocumentShareEvent.unPublish() = _UnPublish;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentShareState with _$DocumentShareState {
|
||||
const factory DocumentShareState.initial() = _Initial;
|
||||
const factory DocumentShareState.loading() = _Loading;
|
||||
const factory DocumentShareState.finish(
|
||||
FlowyResult<ExportDataPB, FlowyError> successOrFail,
|
||||
) = _Finish;
|
||||
const factory DocumentShareState({
|
||||
required bool isLoading,
|
||||
FlowyResult<ExportDataPB, FlowyError>? exportResult,
|
||||
required bool isPublished,
|
||||
FlowyResult<void, FlowyError>? publishResult,
|
||||
required String url,
|
||||
}) = _DocumentShareState;
|
||||
|
||||
factory DocumentShareState.initial() => const DocumentShareState(
|
||||
isLoading: false,
|
||||
isPublished: false,
|
||||
url: '',
|
||||
);
|
||||
}
|
||||
+21
-33
@@ -73,8 +73,7 @@ class _AppFlowyMobileToolbarState extends State<AppFlowyMobileToolbar> {
|
||||
ValueListenableBuilder(
|
||||
valueListenable: isKeyboardShow,
|
||||
builder: (context, isKeyboardShow, __) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 110),
|
||||
return SizedBox(
|
||||
// only adding padding when the keyboard is triggered by editor
|
||||
height: isKeyboardShow && widget.editorState.selection != null
|
||||
? widget.toolbarHeight
|
||||
@@ -383,37 +382,26 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: cachedKeyboardHeight,
|
||||
builder: (_, height, ___) {
|
||||
var paddingHeight = height;
|
||||
if (Platform.isAndroid) {
|
||||
// use the viewInsets to get the keyboard height on Android
|
||||
paddingHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
// if the padding height is 0 and the keyboard height is not 0,
|
||||
// use the keyboard height
|
||||
if (paddingHeight == 0 && height != 0) {
|
||||
paddingHeight = height + MediaQuery.of(context).viewPadding.bottom;
|
||||
}
|
||||
}
|
||||
debugPrint('Keyboard height: $paddingHeight');
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(microseconds: 110),
|
||||
height: paddingHeight,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: showMenuNotifier,
|
||||
builder: (_, showingMenu, __) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(microseconds: 110),
|
||||
height: height,
|
||||
child: (showingMenu && selectedMenuIndex != null)
|
||||
? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call(
|
||||
context,
|
||||
widget.editorState,
|
||||
this,
|
||||
) ??
|
||||
const SizedBox.shrink()
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: showMenuNotifier,
|
||||
builder: (_, showingMenu, __) {
|
||||
var paddingHeight = height;
|
||||
if (Platform.isAndroid) {
|
||||
paddingHeight =
|
||||
height + MediaQuery.of(context).viewPadding.bottom;
|
||||
}
|
||||
return SizedBox(
|
||||
height: paddingHeight,
|
||||
child: (showingMenu && selectedMenuIndex != null)
|
||||
? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call(
|
||||
context,
|
||||
widget.editorState,
|
||||
this,
|
||||
) ??
|
||||
const SizedBox.shrink()
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,246 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PublishTab extends StatelessWidget {
|
||||
const PublishTab({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<DocumentShareBloc, DocumentShareState>(
|
||||
listener: (context, state) {
|
||||
if (state.publishResult != null) {
|
||||
state.publishResult!.fold(
|
||||
(value) => showSnackBarMessage(context, 'Published successfully'),
|
||||
(error) =>
|
||||
showSnackBarMessage(context, 'Failed to publish: $error'),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return state.isPublished
|
||||
? _PublishedWidget(
|
||||
url: state.url,
|
||||
onVisitSite: () {},
|
||||
onUnPublish: () {
|
||||
context
|
||||
.read<DocumentShareBloc>()
|
||||
.add(const DocumentShareEvent.unPublish());
|
||||
},
|
||||
)
|
||||
: _UnPublishWidget(
|
||||
onPublish: () => context
|
||||
.read<DocumentShareBloc>()
|
||||
.add(const DocumentShareEvent.publish('')),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishedWidget extends StatefulWidget {
|
||||
const _PublishedWidget({
|
||||
required this.url,
|
||||
required this.onVisitSite,
|
||||
required this.onUnPublish,
|
||||
});
|
||||
|
||||
final String url;
|
||||
final VoidCallback onVisitSite;
|
||||
final VoidCallback onUnPublish;
|
||||
|
||||
@override
|
||||
State<_PublishedWidget> createState() => _PublishedWidgetState();
|
||||
}
|
||||
|
||||
class _PublishedWidgetState extends State<_PublishedWidget> {
|
||||
final controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.text = widget.url;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(16),
|
||||
const _PublishTabHeader(),
|
||||
const VSpace(16),
|
||||
_PublishUrl(
|
||||
controller: controller,
|
||||
onCopy: (url) {},
|
||||
onSubmitted: (url) {},
|
||||
),
|
||||
const VSpace(16),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildButton(
|
||||
context,
|
||||
name: LocaleKeys.shareAction_unPublish.tr(),
|
||||
borderColor: const Color(0x1E14171B),
|
||||
onTap: widget.onUnPublish,
|
||||
),
|
||||
const Spacer(),
|
||||
_buildButton(
|
||||
context,
|
||||
name: LocaleKeys.shareAction_visitSite.tr(),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
textColor: Colors.white,
|
||||
onTap: () {
|
||||
safeLaunchUrl(controller.text);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButton(
|
||||
BuildContext context, {
|
||||
required String name,
|
||||
Color? backgroundColor,
|
||||
Color borderColor = Colors.transparent,
|
||||
Color? textColor,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
width: 189,
|
||||
child: FlowyButton(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
text: FlowyText.regular(
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
color: textColor,
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UnPublishWidget extends StatelessWidget {
|
||||
const _UnPublishWidget({
|
||||
required this.onPublish,
|
||||
});
|
||||
|
||||
final VoidCallback onPublish;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(16),
|
||||
const _PublishTabHeader(),
|
||||
const VSpace(16),
|
||||
RoundedTextButton(
|
||||
height: 36,
|
||||
title: LocaleKeys.shareAction_publish.tr(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 9.0),
|
||||
fontSize: 14.0,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: onPublish,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishTabHeader extends StatelessWidget {
|
||||
const _PublishTabHeader();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const FlowySvg(FlowySvgs.share_publish_s),
|
||||
const HSpace(6),
|
||||
FlowyText(LocaleKeys.shareAction_publishToTheWeb.tr()),
|
||||
],
|
||||
),
|
||||
const VSpace(4),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.shareAction_publishToTheWebHint.tr(),
|
||||
fontSize: 12,
|
||||
maxLines: 3,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishUrl extends StatelessWidget {
|
||||
const _PublishUrl({
|
||||
required this.controller,
|
||||
required this.onCopy,
|
||||
required this.onSubmitted,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final void Function(String url) onCopy;
|
||||
final void Function(String url) onSubmitted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: FlowyTextField(
|
||||
autoFocus: false,
|
||||
controller: controller,
|
||||
suffixIcon: _buildCopyLinkIcon(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCopyLinkIcon() {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => onCopy(controller.text),
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(left: BorderSide(color: Color(0x141F2329))),
|
||||
),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.m_toolbar_link_m,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+36
-13
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_menu.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||
@@ -13,6 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -28,23 +30,44 @@ class DocumentShareButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<DocumentShareBloc>(param1: view),
|
||||
create: (context) => getIt<DocumentShareBloc>(param1: view)
|
||||
..add(const DocumentShareEvent.initial()),
|
||||
child: BlocListener<DocumentShareBloc, DocumentShareState>(
|
||||
listener: (context, state) {
|
||||
state.mapOrNull(
|
||||
finish: (state) {
|
||||
state.successOrFail.fold(
|
||||
(data) => _handleExportData(context, data),
|
||||
_handleExportError,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (state.isLoading == false && state.exportResult != null) {
|
||||
state.exportResult!.fold(
|
||||
(data) => _handleExportData(context, data),
|
||||
_handleExportError,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<DocumentShareBloc, DocumentShareState>(
|
||||
builder: (context, state) => SizedBox(
|
||||
height: 32.0,
|
||||
child: IntrinsicWidth(child: ShareActionList(view: view)),
|
||||
),
|
||||
builder: (context, state) {
|
||||
final shareBloc = context.read<DocumentShareBloc>();
|
||||
return SizedBox(
|
||||
height: 32.0,
|
||||
child: IntrinsicWidth(
|
||||
child: AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 422,
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
offset: const Offset(0, 8),
|
||||
popupBuilder: (context) => BlocProvider.value(
|
||||
value: shareBloc,
|
||||
child: const ShareMenu(),
|
||||
),
|
||||
child: RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
fontSize: 14.0,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'publish_tab.dart';
|
||||
|
||||
enum ShareMenuTab {
|
||||
share,
|
||||
publish,
|
||||
exportAs;
|
||||
|
||||
static List<ShareMenuTab> supportedTabs = [
|
||||
ShareMenuTab.share,
|
||||
ShareMenuTab.publish,
|
||||
ShareMenuTab.exportAs,
|
||||
];
|
||||
|
||||
String get i18n {
|
||||
switch (this) {
|
||||
case ShareMenuTab.share:
|
||||
return 'Share';
|
||||
case ShareMenuTab.publish:
|
||||
return LocaleKeys.shareAction_publish;
|
||||
case ShareMenuTab.exportAs:
|
||||
return 'Export as';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShareMenu extends StatefulWidget {
|
||||
const ShareMenu({super.key});
|
||||
|
||||
@override
|
||||
State<ShareMenu> createState() => _ShareMenuState();
|
||||
}
|
||||
|
||||
class _ShareMenuState extends State<ShareMenu> {
|
||||
ShareMenuTab selectedTab = ShareMenuTab.publish;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = {
|
||||
for (final tab in ShareMenuTab.supportedTabs)
|
||||
tab: _Segment(
|
||||
title: tab.i18n.tr(),
|
||||
isSelected: selectedTab == tab,
|
||||
),
|
||||
};
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 34,
|
||||
child: CustomSlidingSegmentedControl<ShareMenuTab>(
|
||||
initialValue: selectedTab,
|
||||
curve: Curves.linear,
|
||||
padding: 0,
|
||||
fixedWidth: 128,
|
||||
innerPadding: const EdgeInsets.all(3.0),
|
||||
children: children,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEEF0F3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
thumbDecoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x141F2225),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
onValueChanged: (v) {
|
||||
setState(() {
|
||||
selectedTab = v;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
_buildTab(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab(BuildContext context) {
|
||||
switch (selectedTab) {
|
||||
case ShareMenuTab.publish:
|
||||
return const PublishTab();
|
||||
|
||||
default:
|
||||
return const Center(
|
||||
child: FlowyText('coming soon'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Segment extends StatelessWidget {
|
||||
const _Segment({
|
||||
required this.title,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textColor = isSelected ? null : Theme.of(context).hintColor;
|
||||
return FlowyText(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
color: textColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
@@ -257,6 +258,10 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
);
|
||||
},
|
||||
reset: (userProfile, workspaceId) async {
|
||||
if (workspaceId == _workspaceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
_reset(userProfile, workspaceId);
|
||||
|
||||
add(
|
||||
@@ -321,6 +326,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
required String icon,
|
||||
required String iconColor,
|
||||
required SpacePermission permission,
|
||||
String? viewId,
|
||||
}) async {
|
||||
final section = switch (permission) {
|
||||
SpacePermission.publicToAll => ViewSectionPB.Public,
|
||||
@@ -331,6 +337,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
name: name,
|
||||
viewSection: section,
|
||||
setAsCurrent: false,
|
||||
viewId: viewId,
|
||||
);
|
||||
return await result.fold((space) async {
|
||||
Log.info('Space created: $space');
|
||||
@@ -476,56 +483,82 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
if (members.items.length == 1 || isOwner) {
|
||||
// create a new public space and a new private space
|
||||
// move all the views in the workspace to the new public/private space
|
||||
final publicViews =
|
||||
await _workspaceService.getPublicViews().getOrThrow();
|
||||
// only migrate the public space if there are any public views
|
||||
if (publicViews.isNotEmpty) {
|
||||
final publicSpace = await _createSpace(
|
||||
name: 'Shared',
|
||||
icon: builtInSpaceIcons.first,
|
||||
iconColor: builtInSpaceColors.first,
|
||||
permission: SpacePermission.publicToAll,
|
||||
);
|
||||
|
||||
if (publicSpace != null) {
|
||||
for (final view in publicViews.reversed) {
|
||||
if (view.isSpace) {
|
||||
continue;
|
||||
}
|
||||
await ViewBackendService.moveViewV2(
|
||||
viewId: view.id,
|
||||
newParentId: publicSpace.id,
|
||||
prevViewId: view.parentViewId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// create a new private space
|
||||
final privateViews =
|
||||
await _workspaceService.getPrivateViews().getOrThrow();
|
||||
// only migrate the private space if there are any private views
|
||||
if (privateViews.isNotEmpty) {
|
||||
final privateSpace = await _createSpace(
|
||||
name: 'Private',
|
||||
icon: builtInSpaceIcons.last,
|
||||
iconColor: builtInSpaceColors.last,
|
||||
permission: SpacePermission.private,
|
||||
var publicViews = await _workspaceService.getPublicViews().getOrThrow();
|
||||
final containsPublicSpace = publicViews.any(
|
||||
(e) => e.isSpace && e.spacePermission == SpacePermission.publicToAll,
|
||||
);
|
||||
if (privateSpace != null) {
|
||||
for (final view in privateViews.reversed) {
|
||||
publicViews = publicViews.where((e) => !e.isSpace).toList();
|
||||
// if there is already a public space, don't migrate the public space
|
||||
// only migrate the public space if there are any public views
|
||||
if (publicViews.isEmpty || containsPublicSpace) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final viewId = fixedUuid(user.id.toInt(), UuidType.publicSpace);
|
||||
final publicSpace = await _createSpace(
|
||||
name: 'Shared',
|
||||
icon: builtInSpaceIcons.first,
|
||||
iconColor: builtInSpaceColors.first,
|
||||
permission: SpacePermission.publicToAll,
|
||||
viewId: viewId,
|
||||
);
|
||||
|
||||
Log.info('migrating: created a new public space: ${publicSpace?.id}');
|
||||
|
||||
if (publicSpace != null) {
|
||||
for (final view in publicViews.reversed) {
|
||||
if (view.isSpace) {
|
||||
continue;
|
||||
}
|
||||
await ViewBackendService.moveViewV2(
|
||||
viewId: view.id,
|
||||
newParentId: privateSpace.id,
|
||||
prevViewId: view.parentViewId,
|
||||
newParentId: publicSpace.id,
|
||||
prevViewId: null,
|
||||
);
|
||||
Log.info(
|
||||
'migrating: migrate ${view.name}(${view.id}) to public space(${publicSpace.id})',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create a new private space
|
||||
final viewId = fixedUuid(user.id.toInt(), UuidType.privateSpace);
|
||||
var privateViews = await _workspaceService.getPrivateViews().getOrThrow();
|
||||
// if there is already a private space, don't migrate the private space
|
||||
final containsPrivateSpace = privateViews.any(
|
||||
(e) => e.isSpace && e.spacePermission == SpacePermission.private,
|
||||
);
|
||||
privateViews = privateViews.where((e) => !e.isSpace).toList();
|
||||
if (privateViews.isEmpty || containsPrivateSpace) {
|
||||
return true;
|
||||
}
|
||||
// only migrate the private space if there are any private views
|
||||
final privateSpace = await _createSpace(
|
||||
name: 'Private',
|
||||
icon: builtInSpaceIcons.last,
|
||||
iconColor: builtInSpaceColors.last,
|
||||
permission: SpacePermission.private,
|
||||
viewId: viewId,
|
||||
);
|
||||
Log.info('migrating: created a new private space: ${privateSpace?.id}');
|
||||
|
||||
if (privateSpace != null) {
|
||||
for (final view in privateViews.reversed) {
|
||||
if (view.isSpace) {
|
||||
continue;
|
||||
}
|
||||
await ViewBackendService.moveViewV2(
|
||||
viewId: view.id,
|
||||
newParentId: privateSpace.id,
|
||||
prevViewId: null,
|
||||
);
|
||||
Log.info(
|
||||
'migrating: migrate ${view.name}(${view.id}) to private space(${privateSpace.id})',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.error('migrate space error: $e');
|
||||
@@ -538,14 +571,16 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
required List<ViewPB> publicViews,
|
||||
required List<ViewPB> privateViews,
|
||||
}) async {
|
||||
final publicSpaces =
|
||||
spaces.where((e) => e.spacePermission == SpacePermission.publicToAll);
|
||||
final publicSpaces = spaces.where(
|
||||
(e) => e.spacePermission == SpacePermission.publicToAll,
|
||||
);
|
||||
if (publicSpaces.isEmpty && publicViews.isNotEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final privateSpaces =
|
||||
spaces.where((e) => e.spacePermission == SpacePermission.private);
|
||||
final privateSpaces = spaces.where(
|
||||
(e) => e.spacePermission == SpacePermission.private,
|
||||
);
|
||||
if (privateSpaces.isEmpty && privateViews.isNotEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ class ViewBackendService {
|
||||
/// If the index is null, the view will be added to the end of the list.
|
||||
int? index,
|
||||
ViewSectionPB? section,
|
||||
final String? viewId,
|
||||
}) {
|
||||
final payload = CreateViewPayloadPB.create()
|
||||
..parentViewId = parentViewId
|
||||
@@ -63,6 +64,10 @@ class ViewBackendService {
|
||||
payload.section = section;
|
||||
}
|
||||
|
||||
if (viewId != null) {
|
||||
payload.viewId = viewId;
|
||||
}
|
||||
|
||||
return FolderEventCreateView(payload).send();
|
||||
}
|
||||
|
||||
@@ -266,4 +271,42 @@ class ViewBackendService {
|
||||
);
|
||||
return FolderEventUpdateViewVisibilityStatus(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<PublishInfoResponsePB, FlowyError>> getPublishInfo(
|
||||
ViewPB view,
|
||||
) async {
|
||||
final payload = ViewIdPB()..value = view.id;
|
||||
return FolderEventGetPublishInfo(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> publish(
|
||||
ViewPB view, {
|
||||
String? name,
|
||||
}) async {
|
||||
final payload = PublishViewParamsPB()..viewId = view.id;
|
||||
|
||||
if (name != null) {
|
||||
payload.publishName = name;
|
||||
}
|
||||
return FolderEventPublishView(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> unpublish(
|
||||
ViewPB view,
|
||||
) async {
|
||||
final payload = UnpublishViewsPayloadPB(viewIds: [view.id]);
|
||||
return FolderEventUnpublishViews(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> setPublishNameSpace(
|
||||
String name,
|
||||
) async {
|
||||
final payload = SetPublishNamespacePayloadPB()..newNamespace = name;
|
||||
return FolderEventSetPublishNamespace(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<PublishNamespacePB, FlowyError>>
|
||||
getPublishNameSpace() async {
|
||||
return FolderEventGetPublishNamespace().send();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class WorkspaceService {
|
||||
int? index,
|
||||
ViewLayoutPB? layout,
|
||||
bool? setAsCurrent,
|
||||
String? viewId,
|
||||
}) {
|
||||
final payload = CreateViewPayloadPB.create()
|
||||
..parentViewId = workspaceId
|
||||
@@ -37,6 +38,10 @@ class WorkspaceService {
|
||||
payload.setAsCurrent = setAsCurrent;
|
||||
}
|
||||
|
||||
if (viewId != null) {
|
||||
payload.viewId = viewId;
|
||||
}
|
||||
|
||||
return FolderEventCreateView(payload).send();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import 'package:uuid/data.dart';
|
||||
import 'package:uuid/rng.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
const _uuid = Uuid();
|
||||
|
||||
String uuid() {
|
||||
return const Uuid().v4();
|
||||
return _uuid.v4();
|
||||
}
|
||||
|
||||
String fixedUuid(int seed, UuidType type) {
|
||||
return _uuid.v4(config: V4Options(null, MathRNG(seed: seed + type.index)));
|
||||
}
|
||||
|
||||
enum UuidType {
|
||||
// 0.6.0
|
||||
publicSpace,
|
||||
privateSpace,
|
||||
}
|
||||
@@ -13,6 +13,7 @@ class RoundedTextButton extends StatelessWidget {
|
||||
final Color? hoverColor;
|
||||
final Color? textColor;
|
||||
final double? fontSize;
|
||||
final FontWeight? fontWeight;
|
||||
final EdgeInsets padding;
|
||||
|
||||
const RoundedTextButton({
|
||||
@@ -27,6 +28,7 @@ class RoundedTextButton extends StatelessWidget {
|
||||
this.hoverColor,
|
||||
this.textColor,
|
||||
this.fontSize,
|
||||
this.fontWeight,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
});
|
||||
|
||||
@@ -42,6 +44,7 @@ class RoundedTextButton extends StatelessWidget {
|
||||
child: SizedBox.expand(
|
||||
child: FlowyTextButton(
|
||||
title ?? '',
|
||||
fontWeight: fontWeight,
|
||||
onPressed: onPressed,
|
||||
fontSize: fontSize,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
@@ -394,6 +394,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
custom_sliding_segmented_control:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: custom_sliding_segmented_control
|
||||
sha256: "53c3e931c3ae1f696085d1ec70ac8e934da836595a9b7d9b88fdd0fcbf2a5574"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2174,10 +2182,10 @@ packages:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
|
||||
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.3"
|
||||
version: "4.4.0"
|
||||
value_layout_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.6.0
|
||||
version: 0.6.1
|
||||
|
||||
environment:
|
||||
flutter: ">=3.22.0"
|
||||
@@ -149,6 +149,7 @@ dependencies:
|
||||
bitsdojo_window: ^0.1.6
|
||||
|
||||
flutter_highlight: ^0.7.0
|
||||
custom_sliding_segmented_control: ^1.8.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^3.0.1
|
||||
@@ -199,7 +200,7 @@ dependency_overrides:
|
||||
ref: e44458d
|
||||
path: sheet
|
||||
|
||||
uuid: ^4.1.0
|
||||
uuid: ^4.4.0
|
||||
|
||||
flutter_cache_manager:
|
||||
git:
|
||||
|
||||
@@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
|
||||
# Run the script:
|
||||
# scripts/tool/update_client_api_rev.sh new_rev_id
|
||||
# ⚠️⚠️⚠️️
|
||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" }
|
||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fa7565d9de2a5e65e3886c8230588d58140637c5" }
|
||||
|
||||
[dependencies]
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -54,7 +54,7 @@ yrs = "0.18.8"
|
||||
# Run the script:
|
||||
# scripts/tool/update_client_api_rev.sh new_rev_id
|
||||
# ⚠️⚠️⚠️️
|
||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" }
|
||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fa7565d9de2a5e65e3886c8230588d58140637c5" }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
|
||||
@@ -5,7 +5,7 @@ WORKDIR /app
|
||||
RUN apt-get update && \
|
||||
apt-get install -y nginx
|
||||
|
||||
RUN bun install cheerio pino axios pino-pretty
|
||||
RUN bun install cheerio pino pino-pretty
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "9eebea03-3ed5-4298-86b2-a7f77856d48b",
|
||||
"name": "workspace",
|
||||
"icon": "",
|
||||
"owner": {
|
||||
"id": 0,
|
||||
"name": "system"
|
||||
},
|
||||
"type": 0,
|
||||
"workspaceDatabaseId": "375874be-7a4f-4b7c-8b89-1dc9a39838f4"
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
[
|
||||
{
|
||||
"database_id": "037a985f-f369-4c4a-8011-620012850a68",
|
||||
"created_at": "1713429700",
|
||||
"views": [
|
||||
"48c52cf7-bf98-43fa-96ad-b31aade9b071"
|
||||
]
|
||||
},
|
||||
{
|
||||
"database_id": "daea6aee-9365-4703-a8e2-a2fa6a07b214",
|
||||
"created_at": "1714449533",
|
||||
"views": [
|
||||
"b6347acb-3174-4f0e-98e9-dcce07e5dbf7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"database_id": "4c658817-20db-4f56-b7f9-0637a22dfeb6",
|
||||
"created_at": "0",
|
||||
"views": [
|
||||
"7d2148fc-cace-4452-9c5c-96e52e6bf8b5",
|
||||
"e410747b-5f2f-45a0-b2f7-890ad3001355",
|
||||
"2143e95d-5dcb-4e0f-bb2c-50944e6e019f",
|
||||
"a5566e49-f156-4168-9b2d-17926c5da329",
|
||||
"135615fa-66f7-4451-9b54-d7e99445fca4",
|
||||
"b4e77203-5c8b-48df-bbc5-2e1143eb0e61",
|
||||
"a6af311f-cbc8-42c2-b801-7115619c3776"
|
||||
]
|
||||
},
|
||||
{
|
||||
"database_id": "4c658817-20db-4f56-b7f9-0637a22dfeb6",
|
||||
"created_at": "0",
|
||||
"views": [
|
||||
"7d2148fc-cace-4452-9c5c-96e52e6bf8b5",
|
||||
"e97877f5-c365-4025-9e6a-e590c4b19dbb",
|
||||
"f0c59921-04ee-4971-995c-79b7fd8c00e2",
|
||||
"7eb697cd-6a55-40bb-96ac-0d4a3bc924b2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"database_id": "ee63da2b-aa2a-4d0b-aab0-59008635363a",
|
||||
"created_at": "0",
|
||||
"views": [
|
||||
"2c1ee95a-1b09-4a1f-8d5e-501bc4861a9d",
|
||||
"91ea7c08-f6b3-4b81-aa1e-d3664686186f"
|
||||
]
|
||||
},
|
||||
{
|
||||
"database_id": "e788f014-d0d3-4dfe-81ef-aa1ebb4d6366",
|
||||
"created_at": "0",
|
||||
"views": [
|
||||
"1b0e322d-4909-4c63-914a-d034fc363097",
|
||||
"350f425b-b671-4e2d-8182-5998a6e62924"
|
||||
]
|
||||
},
|
||||
{
|
||||
"database_id": "ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d",
|
||||
"created_at": "0",
|
||||
"views": [
|
||||
"0ce13415-6cce-4497-94c6-475ad96c249e",
|
||||
"e4c89421-12b2-4d02-863d-20949eec9271"
|
||||
]
|
||||
},
|
||||
{
|
||||
"database_id": "ce267d12-3b61-4ebb-bb03-d65272f5f817",
|
||||
"created_at": "0",
|
||||
"views": [
|
||||
"ee3ae8ce-959a-4df3-8734-40b535ff88e3",
|
||||
"66a6f3bc-c78f-4f74-a09e-08d4717bf1fd",
|
||||
"2bf50c03-f41f-4363-b5b1-101216a6c5cc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"database_id": "87bc006e-c1eb-47fd-9ac6-e39b17956369",
|
||||
"created_at": "0",
|
||||
"views": [
|
||||
"7f233be4-1b4d-46b2-bcfc-f341b8d75267",
|
||||
"a734a068-e73d-4b4b-853c-4daffea389c0"
|
||||
]
|
||||
}
|
||||
]
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"data":{"state_vector":[6,248,208,217,159,7,16,226,250,246,177,3,17,211,142,141,147,5,8,196,148,203,38,26,230,150,209,139,14,193,6,167,238,246,72,86],"doc_state":[6,177,5,230,150,209,139,14,0,0,2,0,1,0,5,39,0,196,148,203,38,4,6,122,45,72,76,78,68,2,4,0,230,150,209,139,14,8,4,49,49,49,49,39,0,196,148,203,38,1,6,54,70,57,105,75,107,1,40,0,230,150,209,139,14,13,2,105,100,1,119,6,54,70,57,105,75,107,40,0,230,150,209,139,14,13,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,230,150,209,139,14,13,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,13,8,99,104,105,108,100,114,101,110,1,119,6,97,118,105,69,115,45,33,0,230,150,209,139,14,13,4,100,97,116,97,1,40,0,230,150,209,139,14,13,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,13,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,97,118,105,69,115,45,0,72,196,148,203,38,24,1,119,6,54,70,57,105,75,107,39,0,196,148,203,38,4,6,69,115,57,50,84,74,2,33,0,196,148,203,38,1,6,98,71,84,69,74,116,1,0,7,33,0,196,148,203,38,3,6,87,56,66,102,66,110,1,129,196,148,203,38,24,1,168,230,150,209,139,14,18,1,119,39,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,49,49,49,34,125,93,44,34,108,101,118,101,108,34,58,50,125,4,0,230,150,209,139,14,23,1,35,0,1,132,230,150,209,139,14,35,1,35,0,1,132,230,150,209,139,14,37,1,35,0,1,132,230,150,209,139,14,39,1,35,0,1,39,0,196,148,203,38,4,6,56,76,76,113,48,56,2,39,0,196,148,203,38,1,6,56,70,105,117,112,98,1,40,0,230,150,209,139,14,44,2,105,100,1,119,6,56,70,105,117,112,98,40,0,230,150,209,139,14,44,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,230,150,209,139,14,44,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,44,8,99,104,105,108,100,114,101,110,1,119,6,49,66,67,68,107,53,33,0,230,150,209,139,14,44,4,100,97,116,97,1,40,0,230,150,209,139,14,44,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,44,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,49,66,67,68,107,53,0,200,196,148,203,38,24,230,150,209,139,14,33,1,119,6,56,70,105,117,112,98,4,0,230,150,209,139,14,43,1,50,161,230,150,209,139,14,49,1,132,230,150,209,139,14,54,1,50,161,230,150,209,139,14,55,1,132,230,150,209,139,14,56,1,50,161,230,150,209,139,14,57,1,132,230,150,209,139,14,58,1,50,161,230,150,209,139,14,59,1,39,0,196,148,203,38,4,6,103,76,57,104,70,45,2,39,0,196,148,203,38,1,6,72,99,71,82,109,56,1,40,0,230,150,209,139,14,63,2,105,100,1,119,6,72,99,71,82,109,56,40,0,230,150,209,139,14,63,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,63,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,63,8,99,104,105,108,100,114,101,110,1,119,6,50,114,117,49,72,109,33,0,230,150,209,139,14,63,4,100,97,116,97,1,40,0,230,150,209,139,14,63,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,63,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,50,114,117,49,72,109,0,136,230,150,209,139,14,33,1,119,6,72,99,71,82,109,56,168,230,150,209,139,14,61,1,119,39,123,34,108,101,118,101,108,34,58,52,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,50,50,50,50,34,125,93,125,1,0,230,150,209,139,14,62,1,161,230,150,209,139,14,68,1,129,230,150,209,139,14,74,1,161,230,150,209,139,14,75,1,129,230,150,209,139,14,76,2,161,230,150,209,139,14,77,1,129,230,150,209,139,14,79,1,161,230,150,209,139,14,80,1,129,230,150,209,139,14,81,1,161,230,150,209,139,14,82,1,68,230,150,209,139,14,74,6,228,189,160,229,165,189,168,230,150,209,139,14,84,1,119,31,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,228,189,160,229,165,189,34,125,93,125,39,0,196,148,203,38,4,6,113,111,68,56,109,111,2,33,0,196,148,203,38,1,6,113,118,66,98,82,83,1,0,7,33,0,196,148,203,38,3,6,118,76,97,51,57,90,1,129,230,150,209,139,14,72,1,1,0,230,150,209,139,14,88,1,0,1,129,230,150,209,139,14,99,1,0,1,129,230,150,209,139,14,101,1,0,4,132,230,150,209,139,14,103,1,91,0,1,132,230,150,209,139,14,108,1,93,0,1,39,0,196,148,203,38,4,6,69,114,99,104,55,72,2,39,0,196,148,203,38,1,6,51,111,95,70,117,67,1,40,0,230,150,209,139,14,113,2,105,100,1,119,6,51,111,95,70,117,67,40,0,230,150,209,139,14,113,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,230,150,209,139,14,113,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,113,8,99,104,105,108,100,114,101,110,1,119,6,68,56,109,75,57,97,33,0,230,150,209,139,14,113,4,100,97,116,97,1,40,0,230,150,209,139,14,113,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,113,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,68,56,109,75,57,97,0,200,230,150,209,139,14,72,230,150,209,139,14,98,1,119,6,51,111,95,70,117,67,4,0,230,150,209,139,14,112,1,49,161,230,150,209,139,14,118,1,132,230,150,209,139,14,123,1,50,161,230,150,209,139,14,Line truncated
|
||||
@@ -1 +0,0 @@
|
||||
{"data":{"state_vector":[160,1,128,252,161,128,4,33,130,180,254,251,6,18,135,232,133,203,9,15,135,240,136,178,10,45,137,226,192,199,6,20,138,202,240,189,2,134,1,139,240,196,145,14,138,2,139,152,215,249,10,34,141,216,158,150,1,3,142,130,192,134,11,3,143,184,153,180,6,6,140,228,230,243,1,2,145,190,137,224,10,2,145,144,146,185,5,12,140,152,206,145,6,165,1,135,166,246,235,6,3,147,206,229,235,1,3,137,164,190,210,1,39,152,158,185,230,10,7,153,130,203,161,6,97,152,252,186,192,1,2,154,244,246,165,8,198,1,156,148,170,169,10,16,157,240,144,231,2,3,158,156,181,152,10,49,158,182,250,251,9,15,160,192,253,131,5,3,161,178,132,150,11,101,159,156,204,250,6,27,163,236,177,169,4,12,164,188,201,172,1,3,158,184,218,165,3,38,162,238,198,212,7,3,170,140,240,234,9,31,171,204,155,217,8,3,171,142,166,254,1,6,173,252,148,184,13,64,176,154,159,227,1,20,178,162,190,217,10,3,179,252,154,44,3,180,230,210,212,13,7,182,172,247,194,5,3,183,226,184,158,8,8,187,220,199,239,8,70,195,242,227,194,8,3,196,154,250,183,6,62,197,254,154,201,10,3,200,142,208,241,4,157,1,202,160,246,212,1,6,203,184,221,173,11,37,206,220,129,131,4,21,207,228,238,162,8,50,209,250,203,254,15,3,210,228,153,221,12,3,211,202,217,232,12,7,211,166,203,229,4,195,1,214,168,149,214,3,15,219,220,239,171,8,10,225,248,138,176,2,49,226,212,179,248,2,15,229,154,128,197,12,24,234,182,182,157,9,8,235,178,165,206,5,72,234,156,130,211,12,12,241,130,161,205,7,12,244,226,228,149,2,30,245,220,194,52,39,247,200,243,247,14,100,248,136,168,181,1,3,248,210,237,129,13,2,250,198,166,187,7,2,248,196,187,185,10,31,252,218,241,167,14,3,255,140,248,220,6,3,128,211,179,216,2,10,133,159,138,205,12,131,1,135,167,156,250,14,16,135,193,208,135,7,16,141,245,194,142,11,7,141,205,220,149,4,3,143,131,148,152,6,15,141,171,170,217,4,2,145,159,164,217,14,3,149,129,169,191,12,16,149,249,242,175,4,31,149,189,189,215,8,3,149,161,132,184,14,221,2,154,243,157,196,14,12,154,193,208,134,10,9,155,165,205,152,11,3,154,235,215,240,4,13,155,159,180,195,15,6,157,207,243,216,6,2,161,239,241,154,13,106,162,159,252,196,11,3,164,155,139,169,7,35,165,139,157,171,15,103,164,203,250,235,13,7,167,131,133,162,9,11,166,201,221,141,13,72,169,197,188,221,3,3,170,255,211,105,38,166,203,155,46,6,173,187,245,170,14,46,174,151,139,93,179,2,175,225,172,150,8,11,175,147,217,214,1,33,177,161,136,243,11,10,178,203,205,182,4,1,175,205,156,228,6,10,180,205,189,133,13,20,181,175,219,209,12,10,182,143,233,195,4,111,177,219,160,167,7,4,184,201,188,172,10,3,184,231,170,67,6,186,197,166,179,15,7,187,173,214,176,15,3,188,171,136,250,8,21,188,237,223,145,6,26,190,139,191,155,1,2,191,157,147,233,9,32,193,249,142,142,4,17,198,189,216,175,6,23,200,205,214,172,10,30,201,129,238,197,4,84,201,191,159,147,14,17,200,203,236,184,2,35,200,159,185,206,9,8,205,149,231,236,11,68,213,161,242,209,13,35,214,139,213,136,8,66,213,255,156,145,1,2,219,227,140,137,6,34,221,147,167,147,15,177,2,222,205,223,235,7,31,223,209,193,147,11,81,227,209,197,253,2,14,229,153,197,202,7,241,6,231,139,244,188,8,13,232,207,157,148,2,2,233,165,139,246,14,43,233,247,183,159,1,4,235,225,184,133,10,3,234,153,236,158,4,73,234,187,164,181,1,24,231,189,134,196,8,77,239,199,189,146,3,24,240,149,229,225,6,15,241,155,213,233,1,52,240,253,240,229,1,79,243,239,182,181,13,30,236,229,225,232,8,116,246,185,174,192,6,90,248,153,216,10,3,251,189,220,155,14,3,252,163,130,200,6,30,253,205,145,137,11,10,252,171,209,175,15,4,255,255,147,249,10,3],"doc_state":[160,1,3,209,250,203,254,15,0,161,226,212,179,248,2,9,1,161,226,212,179,248,2,10,1,136,174,151,139,93,249,1,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,78,2,155,159,180,195,15,0,161,198,189,216,175,6,22,1,161,253,205,145,137,11,9,5,7,186,197,166,179,15,0,161,161,178,132,150,11,98,1,161,161,178,132,150,11,99,1,129,161,178,132,150,11,100,1,161,161,178,132,150,11,97,1,161,161,178,132,150,11,94,1,161,161,178,132,150,11,95,1,129,186,197,166,179,15,2,1,3,187,173,214,176,15,0,168,244,226,228,149,2,27,1,122,4,56,115,160,190,64,16,0,168,244,226,228,149,2,28,1,122,0,0,0,0,102,32,153,171,136,248,153,216,10,2,1,118,2,2,105,100,119,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,32,153,171,1,252,171,209,175,15,0,161,128,211,179,216,2,9,4,103,165,139,157,171,15,0,8,0,201,129,238,197,4,52,1,118,1,2,105,100,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,168,201,129,238,197,4,51,1,122,0,0,0,0,102,74,244,14,161,170,140,240,234,9,22,1,39,0,203,184,221,173,11,1,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,1,40,0,165,139,157,171,15,3,2,105,100,1,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,40,0,Line truncated
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTI4Mjk2MjAsImlhdCI6MTcxMjgyNjAyMCwic3ViIjoiY2JmZjA2MGEtMTk2ZC00MTVhLWFhODAtNzU5YzAxODg2NDY2IiwiZW1haWwiOiJsdUBhcHBmbG93eS5pbyIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZ29vZ2xlIiwicHJvdmlkZXJzIjpbImdvb2dsZSJdfSwidXNlcl9tZXRhZGF0YSI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUNnOG9jTEhabVZBczRTb0ZlVFFuWG5CU2JiNTBBVXF0YktHNWx5MGllVHZCSklYZ1o3UmdRPXM5Ni1jIiwiY3VzdG9tX2NsYWltcyI6eyJoZCI6ImFwcGZsb3d5LmlvIn0sImVtYWlsIjoibHVAYXBwZmxvd3kuaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZnVsbF9uYW1lIjoiTHUgSGUiLCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJuYW1lIjoiTHUgSGUiLCJwaG9uZV92ZXJpZmllZCI6ZmFsc2UsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NMSFptVkFzNFNvRmVUUW5YbkJTYmI1MEFVcXRiS0c1bHkwaWVUdkJKSVhnWjdSZ1E9czk2LWMiLCJwcm92aWRlcl9pZCI6IjEwMTE2OTI1MDgyOTU1NDAyODM4MSIsInN1YiI6IjEwMTE2OTI1MDgyOTU1NDAyODM4MSJ9LCJyb2xlIjoiIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoicGFzc3dvcmQiLCJ0aW1lc3RhbXAiOjE3MTI4MjYwMjB9XSwic2Vzc2lvbl9pZCI6ImJmMzE5OTRlLTQwMTgtNDhjMS05Yzc0LWVmYzkyMGNjOWQ0NSJ9.QeTrRhsnBjBL1GUS3TIWOgU1SPM6RcaWwxZdMVfcFBU",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600,
|
||||
"expires_at": 4869016461,
|
||||
"refresh_token": "71vp1jJnSAVluZKaXkhG1A",
|
||||
"user": {
|
||||
"id": "cbff060a-196d-415a-aa80-759c01886466",
|
||||
"aud": "",
|
||||
"role": "",
|
||||
"email": "lu@appflowy.io",
|
||||
"email_confirmed_at": "2024-03-13T10:49:53.165361Z",
|
||||
"phone": "",
|
||||
"confirmed_at": "2024-03-13T10:49:53.165361Z",
|
||||
"last_sign_in_at": "2024-04-11T09:00:20.547468985Z",
|
||||
"app_metadata": {
|
||||
"provider": "google",
|
||||
"providers": [
|
||||
"google"
|
||||
]
|
||||
},
|
||||
"user_metadata": {
|
||||
"avatar_url": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c",
|
||||
"custom_claims": {
|
||||
"hd": "appflowy.io"
|
||||
},
|
||||
"email": "lu@appflowy.io",
|
||||
"email_verified": true,
|
||||
"full_name": "Lu He",
|
||||
"iss": "https://accounts.google.com",
|
||||
"name": "Lu He",
|
||||
"phone_verified": false,
|
||||
"picture": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c",
|
||||
"provider_id": "101169250829554028381",
|
||||
"sub": "101169250829554028381"
|
||||
},
|
||||
"identities": [
|
||||
{
|
||||
"identity_id": "e4cf8b69-7f80-42e9-aed2-e25132ad0178",
|
||||
"id": "101169250829554028381",
|
||||
"user_id": "cbff060a-196d-415a-aa80-759c01886466",
|
||||
"identity_data": {
|
||||
"avatar_url": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c",
|
||||
"custom_claims": {
|
||||
"hd": "appflowy.io"
|
||||
},
|
||||
"email": "lu@appflowy.io",
|
||||
"email_verified": true,
|
||||
"full_name": "Lu He",
|
||||
"iss": "https://accounts.google.com",
|
||||
"name": "Lu He",
|
||||
"phone_verified": false,
|
||||
"picture": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c",
|
||||
"provider_id": "101169250829554028381",
|
||||
"sub": "101169250829554028381"
|
||||
},
|
||||
"provider": "google",
|
||||
"last_sign_in_at": "2024-03-13T07:22:43.110504Z",
|
||||
"created_at": "2024-03-13T07:22:43.110543Z",
|
||||
"updated_at": "2024-04-04T06:15:14.03093Z"
|
||||
}
|
||||
],
|
||||
"created_at": "2024-03-13T07:22:43.102586Z",
|
||||
"updated_at": "2024-04-11T09:00:20.551485Z"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"uid": 304120109071339520,
|
||||
"uuid": "cbff060a-196d-415a-aa80-759c01886466",
|
||||
"email": "lu@appflowy.io",
|
||||
"password": "",
|
||||
"name": "Kilu",
|
||||
"metadata": {
|
||||
"icon_url": "🇽🇰"
|
||||
},
|
||||
"encryption_sign": null,
|
||||
"latest_workspace_id": "fcb503f9-9287-4de4-8de0-ea191e680968",
|
||||
"updated_at": 1710421586
|
||||
},
|
||||
"code": 0,
|
||||
"message": "Operation completed successfully."
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"data":{"user_profile":{"uid":304120109071339520,"uuid":"cbff060a-196d-415a-aa80-759c01886466","email":"lu@appflowy.io","password":"","name":"Kilu","metadata":{"icon_url":"🇽🇰"},"encryption_sign":null,"latest_workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","updated_at":1715847453},"visiting_workspace":{"workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","database_storage_id":"375874be-7a4f-4b7c-8b89-1dc9a39838f4","owner_uid":304120109071339520,"owner_name":"Kilu","workspace_type":0,"workspace_name":"Kilu Works","created_at":"2024-03-13T07:23:10.275174Z","icon":"😆"},"workspaces":[{"workspace_id":"81570fa8-8be9-4b2d-9f1c-1ef4f34079a8","database_storage_id":"6c1f1a2c-e8d5-4bc2-917f-495bce862abb","owner_uid":311828434584080384,"owner_name":"Zack Zi Xiang Fu","workspace_type":0,"workspace_name":"My Workspace","created_at":"2024-04-03T13:53:18.295918Z","icon":""},{"workspace_id":"fcb503f9-9287-4de4-8de0-ea191e680968","database_storage_id":"ae1b82a5-2b93-45c7-901a-f9357c544534","owner_uid":276169796100296704,"owner_name":"Annie Anqi Wang","workspace_type":0,"workspace_name":"AppFlowy Test","created_at":"2023-12-27T04:18:36.372013Z","icon":""},{"workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","database_storage_id":"375874be-7a4f-4b7c-8b89-1dc9a39838f4","owner_uid":304120109071339520,"owner_name":"Kilu","workspace_type":0,"workspace_name":"Kilu Works","created_at":"2024-03-13T07:23:10.275174Z","icon":"😆"}]},"code":0,"message":"Operation completed successfully."}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"is_new": false
|
||||
}
|
||||
}
|
||||
@@ -25,96 +25,9 @@
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
import { YDoc } from '@/application/collab.type';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import { JSDatabaseService } from '@/application/services/js-services/database.service';
|
||||
import { JSDocumentService } from '@/application/services/js-services/document.service';
|
||||
import { applyYDoc } from '@/application/ydoc/apply';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
Cypress.Commands.add('mockAPI', () => {
|
||||
cy.fixture('sign_in_success').then((json) => {
|
||||
cy.intercept('GET', `/api/user/verify/${json.access_token}`, {
|
||||
fixture: 'verify_token',
|
||||
}).as('verifyToken');
|
||||
cy.intercept('POST', '/gotrue/token?grant_type=password', json).as('loginSuccess');
|
||||
cy.intercept('POST', '/gotrue/token?grant_type=refresh_token', json).as('refreshToken');
|
||||
});
|
||||
cy.intercept('GET', '/api/user/profile', { fixture: 'user' }).as('getUserProfile');
|
||||
cy.intercept('GET', '/api/user/workspace', { fixture: 'user_workspace' }).as('getUserWorkspace');
|
||||
// Mock the API
|
||||
});
|
||||
|
||||
// Example use:
|
||||
// beforeEach(() => {
|
||||
// cy.mockAPI();
|
||||
// });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
Cypress.Commands.add('mockCurrentWorkspace', () => {
|
||||
cy.fixture('current_workspace').then((workspace) => {
|
||||
cy.stub(JSDatabaseService.prototype, 'currentWorkspace').resolves(workspace);
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
Cypress.Commands.add('mockGetWorkspaceDatabases', () => {
|
||||
cy.fixture('database/databases').then((databases) => {
|
||||
cy.stub(JSDatabaseService.prototype, 'getWorkspaceDatabases').resolves(databases);
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
Cypress.Commands.add('mockDatabase', () => {
|
||||
cy.mockCurrentWorkspace();
|
||||
cy.mockGetWorkspaceDatabases();
|
||||
|
||||
const ids = [
|
||||
'4c658817-20db-4f56-b7f9-0637a22dfeb6',
|
||||
'ce267d12-3b61-4ebb-bb03-d65272f5f817',
|
||||
'ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d',
|
||||
];
|
||||
|
||||
const mockOpenDatabase = cy.stub(JSDatabaseService.prototype, 'openDatabase');
|
||||
|
||||
ids.forEach((id) => {
|
||||
cy.fixture(`database/${id}`).then((database) => {
|
||||
cy.fixture(`database/rows/${id}`).then((rows) => {
|
||||
const doc = new Y.Doc();
|
||||
const rootRowsDoc = new Y.Doc();
|
||||
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||
const databaseState = new Uint8Array(database.data.doc_state);
|
||||
|
||||
applyYDoc(doc, databaseState);
|
||||
|
||||
Object.keys(rows).forEach((key) => {
|
||||
const data = rows[key];
|
||||
const rowDoc = new Y.Doc();
|
||||
|
||||
applyYDoc(rowDoc, new Uint8Array(data));
|
||||
rowsFolder.set(key, rowDoc);
|
||||
});
|
||||
mockOpenDatabase.withArgs(id).resolves({
|
||||
databaseDoc: doc,
|
||||
rows: rowsFolder,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
Cypress.Commands.add('mockDocument', (id: string) => {
|
||||
cy.fixture(`document/${id}`).then((subDocument) => {
|
||||
const doc = new Y.Doc();
|
||||
const state = new Uint8Array(subDocument.data.doc_state);
|
||||
|
||||
applyYDoc(doc, state);
|
||||
|
||||
cy.stub(JSDocumentService.prototype, 'openDocument').withArgs(id).resolves(doc);
|
||||
});
|
||||
});
|
||||
export {};
|
||||
@@ -24,7 +24,7 @@
|
||||
"coverage": "pnpm run test:unit && pnpm run test:components"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appflowyinc/client-api-wasm": "0.0.3",
|
||||
"@appflowyinc/client-api-wasm": "0.0.4",
|
||||
"@atlaskit/primitives": "^5.5.3",
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -43,6 +43,8 @@
|
||||
"colorthief": "^2.4.0",
|
||||
"dayjs": "^1.11.9",
|
||||
"decimal.js": "^10.4.3",
|
||||
"dexie": "^4.0.7",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"events": "^3.3.0",
|
||||
@@ -56,6 +58,7 @@
|
||||
"katex": "^0.16.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^4.0.0",
|
||||
"notistack": "^3.0.1",
|
||||
"numeral": "^2.0.6",
|
||||
"prismjs": "^1.29.0",
|
||||
"protoc-gen-ts": "0.8.7",
|
||||
@@ -136,6 +139,7 @@
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-jest": "^29.6.2",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.7.2",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -145,6 +149,8 @@
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"jest-environment-jsdom": "^29.6.2",
|
||||
"nyc": "^15.1.0",
|
||||
"pino": "^9.2.0",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "2.8.4",
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
|
||||
Generated
+256
-8
@@ -1,9 +1,13 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@appflowyinc/client-api-wasm':
|
||||
specifier: 0.0.3
|
||||
version: 0.0.3
|
||||
specifier: 0.0.4
|
||||
version: 0.0.4
|
||||
'@atlaskit/primitives':
|
||||
specifier: ^5.5.3
|
||||
version: 5.7.0(@types/react@18.2.66)(react@18.2.0)
|
||||
@@ -58,6 +62,12 @@ dependencies:
|
||||
decimal.js:
|
||||
specifier: ^10.4.3
|
||||
version: 10.4.3
|
||||
dexie:
|
||||
specifier: ^4.0.7
|
||||
version: 4.0.7
|
||||
dexie-react-hooks:
|
||||
specifier: ^1.1.7
|
||||
version: 1.1.7(@types/react@18.2.66)(dexie@4.0.7)(react@18.2.0)
|
||||
emoji-mart:
|
||||
specifier: ^5.5.2
|
||||
version: 5.6.0
|
||||
@@ -97,6 +107,9 @@ dependencies:
|
||||
nanoid:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.2
|
||||
notistack:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0)
|
||||
numeral:
|
||||
specifier: ^2.0.6
|
||||
version: 2.0.6
|
||||
@@ -333,6 +346,9 @@ devDependencies:
|
||||
chalk:
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2
|
||||
cheerio:
|
||||
specifier: 1.0.0-rc.12
|
||||
version: 1.0.0-rc.12
|
||||
cross-env:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
@@ -360,6 +376,12 @@ devDependencies:
|
||||
nyc:
|
||||
specifier: ^15.1.0
|
||||
version: 15.1.0
|
||||
pino:
|
||||
specifier: ^9.2.0
|
||||
version: 9.2.0
|
||||
pino-pretty:
|
||||
specifier: ^11.2.1
|
||||
version: 11.2.1
|
||||
postcss:
|
||||
specifier: ^8.4.21
|
||||
version: 8.4.21
|
||||
@@ -429,8 +451,8 @@ packages:
|
||||
'@jridgewell/gen-mapping': 0.3.5
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
/@appflowyinc/client-api-wasm@0.0.3:
|
||||
resolution: {integrity: sha512-ARjLhiDZ8MiZ9egWDbAX9VAdXXS30av+InCPLrS/iqCMYrhuuU9rxS9jQeNEB7jucFrj158gBRusimFN7P/lyw==}
|
||||
/@appflowyinc/client-api-wasm@0.0.4:
|
||||
resolution: {integrity: sha512-R8nm811vmh4oJU7LDFV7TJP89Y+WZKYrwpR1xrfL9m+PtyA/c21HvNJrvT+NssHfuz+IKj4L1cH2uXQcggnybA==}
|
||||
dev: false
|
||||
|
||||
/@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0):
|
||||
@@ -4580,6 +4602,13 @@ packages:
|
||||
deprecated: Use your platform's native atob() and btoa() methods instead
|
||||
dev: true
|
||||
|
||||
/abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
dev: true
|
||||
|
||||
/acorn-globals@7.0.1:
|
||||
resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==}
|
||||
dependencies:
|
||||
@@ -4886,6 +4915,11 @@ packages:
|
||||
engines: {node: '>= 4.0.0'}
|
||||
dev: true
|
||||
|
||||
/atomic-sleep@1.0.0:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
dev: true
|
||||
|
||||
/autoprefixer@10.4.13(postcss@8.4.21):
|
||||
resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -5180,6 +5214,13 @@ packages:
|
||||
ieee754: 1.2.1
|
||||
dev: true
|
||||
|
||||
/buffer@6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
dev: true
|
||||
|
||||
/cachedir@2.4.0:
|
||||
resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -5289,6 +5330,30 @@ packages:
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dev: true
|
||||
|
||||
/cheerio-select@2.1.0:
|
||||
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
css-select: 5.1.0
|
||||
css-what: 6.1.0
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.1.0
|
||||
dev: true
|
||||
|
||||
/cheerio@1.0.0-rc.12:
|
||||
resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
cheerio-select: 2.1.0
|
||||
dom-serializer: 2.0.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.1.0
|
||||
htmlparser2: 8.0.2
|
||||
parse5: 7.1.2
|
||||
parse5-htmlparser2-tree-adapter: 7.0.0
|
||||
dev: true
|
||||
|
||||
/chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
@@ -5739,6 +5804,10 @@ packages:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.1
|
||||
|
||||
/dateformat@4.6.3:
|
||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||
dev: true
|
||||
|
||||
/dayjs@1.11.10:
|
||||
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
|
||||
dev: true
|
||||
@@ -5860,6 +5929,22 @@ packages:
|
||||
minimist: 1.2.8
|
||||
dev: true
|
||||
|
||||
/dexie-react-hooks@1.1.7(@types/react@18.2.66)(dexie@4.0.7)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==}
|
||||
peerDependencies:
|
||||
'@types/react': '>=16'
|
||||
dexie: ^3.2 || ^4.0.1-alpha
|
||||
react: '>=16'
|
||||
dependencies:
|
||||
'@types/react': 18.2.66
|
||||
dexie: 4.0.7
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/dexie@4.0.7:
|
||||
resolution: {integrity: sha512-M+Lo6rk4pekIfrc2T0o2tvVJwL6EAAM/B78DNfb8aaxFVoI1f8/rz5KTxuAnApkwqTSuxx7T5t0RKH7qprapGg==}
|
||||
dev: false
|
||||
|
||||
/didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
dev: true
|
||||
@@ -6372,6 +6457,11 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/eventemitter2@6.4.7:
|
||||
resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
|
||||
dev: true
|
||||
@@ -6455,6 +6545,10 @@ packages:
|
||||
resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
|
||||
engines: {'0': node >=0.6.0}
|
||||
|
||||
/fast-copy@3.0.2:
|
||||
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
|
||||
dev: true
|
||||
|
||||
/fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -6492,6 +6586,15 @@ packages:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
dev: true
|
||||
|
||||
/fast-redact@3.5.0:
|
||||
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/fast-safe-stringify@2.1.1:
|
||||
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
|
||||
dev: true
|
||||
|
||||
/fastq@1.17.1:
|
||||
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
||||
dependencies:
|
||||
@@ -6961,6 +7064,10 @@ packages:
|
||||
tslib: 2.6.2
|
||||
dev: true
|
||||
|
||||
/help-me@5.0.0:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||
dev: true
|
||||
|
||||
/hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
dependencies:
|
||||
@@ -6983,6 +7090,15 @@ packages:
|
||||
void-elements: 3.1.0
|
||||
dev: false
|
||||
|
||||
/htmlparser2@8.0.2:
|
||||
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.1.0
|
||||
entities: 4.5.0
|
||||
dev: true
|
||||
|
||||
/http-proxy-agent@5.0.0:
|
||||
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -7881,6 +7997,11 @@ packages:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
/joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/jpeg-js@0.4.4:
|
||||
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
|
||||
dev: false
|
||||
@@ -8440,6 +8561,21 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/notistack@3.0.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==}
|
||||
engines: {node: '>=12.0.0', npm: '>=6.0.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
clsx: 1.2.1
|
||||
goober: 2.1.14(csstype@3.1.3)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- csstype
|
||||
dev: false
|
||||
|
||||
/npm-run-path@4.0.1:
|
||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -8576,6 +8712,11 @@ packages:
|
||||
resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==}
|
||||
dev: false
|
||||
|
||||
/on-exit-leak-free@2.1.2:
|
||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dev: true
|
||||
|
||||
/once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
dependencies:
|
||||
@@ -8721,6 +8862,13 @@ packages:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
/parse5-htmlparser2-tree-adapter@7.0.0:
|
||||
resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
parse5: 7.1.2
|
||||
dev: true
|
||||
|
||||
/parse5@7.1.2:
|
||||
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
||||
dependencies:
|
||||
@@ -8810,6 +8958,54 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/pino-abstract-transport@1.2.0:
|
||||
resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==}
|
||||
dependencies:
|
||||
readable-stream: 4.5.2
|
||||
split2: 4.2.0
|
||||
dev: true
|
||||
|
||||
/pino-pretty@11.2.1:
|
||||
resolution: {integrity: sha512-O05NuD9tkRasFRWVaF/uHLOvoRDFD7tb5VMertr78rbsYFjYp48Vg3477EshVAF5eZaEw+OpDl/tu+B0R5o+7g==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
colorette: 2.0.20
|
||||
dateformat: 4.6.3
|
||||
fast-copy: 3.0.2
|
||||
fast-safe-stringify: 2.1.1
|
||||
help-me: 5.0.0
|
||||
joycon: 3.1.1
|
||||
minimist: 1.2.8
|
||||
on-exit-leak-free: 2.1.2
|
||||
pino-abstract-transport: 1.2.0
|
||||
pump: 3.0.0
|
||||
readable-stream: 4.5.2
|
||||
secure-json-parse: 2.7.0
|
||||
sonic-boom: 4.0.1
|
||||
strip-json-comments: 3.1.1
|
||||
dev: true
|
||||
|
||||
/pino-std-serializers@7.0.0:
|
||||
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
|
||||
dev: true
|
||||
|
||||
/pino@9.2.0:
|
||||
resolution: {integrity: sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
atomic-sleep: 1.0.0
|
||||
fast-redact: 3.5.0
|
||||
on-exit-leak-free: 2.1.2
|
||||
pino-abstract-transport: 1.2.0
|
||||
pino-std-serializers: 7.0.0
|
||||
process-warning: 3.0.0
|
||||
quick-format-unescaped: 4.0.4
|
||||
real-require: 0.2.0
|
||||
safe-stable-stringify: 2.4.3
|
||||
sonic-boom: 4.0.1
|
||||
thread-stream: 3.1.0
|
||||
dev: true
|
||||
|
||||
/pirates@4.0.6:
|
||||
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -9027,6 +9223,10 @@ packages:
|
||||
fromentries: 1.3.2
|
||||
dev: true
|
||||
|
||||
/process-warning@3.0.0:
|
||||
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
|
||||
dev: true
|
||||
|
||||
/process@0.11.10:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
@@ -9115,6 +9315,10 @@ packages:
|
||||
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
|
||||
dev: true
|
||||
|
||||
/quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
dev: true
|
||||
|
||||
/quick-lru@5.1.1:
|
||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -9635,12 +9839,28 @@ packages:
|
||||
util-deprecate: 1.0.2
|
||||
dev: true
|
||||
|
||||
/readable-stream@4.5.2:
|
||||
resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
abort-controller: 3.0.0
|
||||
buffer: 6.0.3
|
||||
events: 3.3.0
|
||||
process: 0.11.10
|
||||
string_decoder: 1.3.0
|
||||
dev: true
|
||||
|
||||
/readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
/real-require@0.2.0:
|
||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
dev: true
|
||||
|
||||
/redux-thunk@3.1.0(redux@5.0.1):
|
||||
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
|
||||
peerDependencies:
|
||||
@@ -9924,6 +10144,11 @@ packages:
|
||||
is-regex: 1.1.4
|
||||
dev: true
|
||||
|
||||
/safe-stable-stringify@2.4.3:
|
||||
resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
@@ -9973,6 +10198,10 @@ packages:
|
||||
compute-scroll-into-view: 3.1.0
|
||||
dev: false
|
||||
|
||||
/secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||
dev: true
|
||||
|
||||
/semver@5.7.2:
|
||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||
hasBin: true
|
||||
@@ -10151,6 +10380,12 @@ packages:
|
||||
tslib: 2.6.2
|
||||
dev: true
|
||||
|
||||
/sonic-boom@4.0.1:
|
||||
resolution: {integrity: sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==}
|
||||
dependencies:
|
||||
atomic-sleep: 1.0.0
|
||||
dev: true
|
||||
|
||||
/source-map-js@1.2.0:
|
||||
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -10194,6 +10429,11 @@ packages:
|
||||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/split2@4.2.0:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
dev: true
|
||||
|
||||
/sprintf-js@1.0.3:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
|
||||
@@ -10306,6 +10546,12 @@ packages:
|
||||
safe-buffer: 5.1.2
|
||||
dev: true
|
||||
|
||||
/string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -10499,6 +10745,12 @@ packages:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
dev: true
|
||||
|
||||
/thread-stream@3.1.0:
|
||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
dev: true
|
||||
|
||||
/throttleit@1.0.1:
|
||||
resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==}
|
||||
dev: true
|
||||
@@ -11414,7 +11666,3 @@ packages:
|
||||
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
|
||||
engines: {node: '>=12.20'}
|
||||
dev: true
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
@@ -2,8 +2,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const pino = require('pino');
|
||||
const cheerio = require('cheerio');
|
||||
const axios = require('axios');
|
||||
|
||||
const { fetch } = require('bun');
|
||||
const distDir = path.join(__dirname, 'dist');
|
||||
const indexPath = path.join(distDir, 'index.html');
|
||||
|
||||
@@ -38,38 +37,74 @@ const logRequestTimer = (req) => {
|
||||
};
|
||||
|
||||
const fetchMetaData = async (url) => {
|
||||
logger.info(`Fetching meta data from ${url}`);
|
||||
try {
|
||||
const response = await axios.get(url);
|
||||
return response.data;
|
||||
const response = await fetch(url, {
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.error('Error fetching meta data', error);
|
||||
logger.error(`Error fetching meta data ${error}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const BASE_URL = process.env.AF_BASE_URL || 'https://test.appflowy.cloud';
|
||||
const createServer = async (req) => {
|
||||
const timer = logRequestTimer(req);
|
||||
const reqUrl = new URL(req.url);
|
||||
logger.info(`Request URL: ${reqUrl.pathname}`);
|
||||
const [
|
||||
namespace,
|
||||
publishName,
|
||||
] = reqUrl.pathname.slice(1).split('/');
|
||||
|
||||
logger.info(`Namespace: ${namespace}, Publish Name: ${publishName}`);
|
||||
if (namespace === '' || !publishName) {
|
||||
timer();
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Location': 'https://appflowy.io',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const pageId = req.url.split('/').pop();
|
||||
let metaData;
|
||||
try {
|
||||
metaData = await fetchMetaData(`${BASE_URL}/api/workspace/published/${namespace}/${publishName}`);
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching meta data: ${error}`);
|
||||
}
|
||||
|
||||
let htmlData = fs.readFileSync(indexPath, 'utf8');
|
||||
const $ = cheerio.load(htmlData);
|
||||
if (!pageId) {
|
||||
timer();
|
||||
return new Response($.html(), {
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
});
|
||||
}
|
||||
|
||||
const description = 'Write, share, comment, react, and publish docs quickly and securely on AppFlowy.';
|
||||
let title = 'AppFlowy';
|
||||
const url = 'https://appflowy.com';
|
||||
let image = 'https://d3uafhn8yrvdfn.cloudfront.net/website/production/_next/static/media/og-image.e347bfb5.png';
|
||||
// Inject meta data into the HTML to support SEO and social sharing
|
||||
// if (metaData) {
|
||||
// title = metaData.title;
|
||||
// image = metaData.image;
|
||||
// }
|
||||
if (metaData) {
|
||||
title = metaData.view.name;
|
||||
|
||||
try {
|
||||
const cover = metaData.view.extra ? JSON.parse(metaData.view.extra)?.cover : null;
|
||||
if (cover && ['unsplash', 'custom'].includes(cover.type)) {
|
||||
image = cover.value;
|
||||
}
|
||||
} catch (_) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
$('title').text(title);
|
||||
setOrUpdateMetaTag($, 'meta[name="description"]', 'name', description);
|
||||
|
||||
@@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
|
||||
# Run the script:
|
||||
# scripts/tool/update_client_api_rev.sh new_rev_id
|
||||
# ⚠️⚠️⚠️️
|
||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6262816043efeede8823d7a7ea252083adf407e9" }
|
||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "fa7565d9de2a5e65e3886c8230588d58140637c5" }
|
||||
|
||||
[dependencies]
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
useSortsSelector,
|
||||
} from '../selector';
|
||||
import { useDatabaseViewId } from '../context';
|
||||
import { IdProvider } from '@/components/_shared/context-provider/IdProvider';
|
||||
import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
|
||||
import { withTestingDatabase } from '@/application/database-yjs/__tests__/withTestingData';
|
||||
import { expect } from '@jest/globals';
|
||||
@@ -31,11 +30,9 @@ const wrapperCreator =
|
||||
(viewId: string, doc: YDoc, rowDocMap: Y.Map<YDoc>) =>
|
||||
({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<IdProvider objectId={viewId}>
|
||||
<DatabaseContextProvider viewId={viewId} databaseDoc={doc} rowDocMap={rowDocMap} readOnly={true}>
|
||||
{children}
|
||||
</DatabaseContextProvider>
|
||||
</IdProvider>
|
||||
<DatabaseContextProvider viewId={viewId} databaseDoc={doc} rowDocMap={rowDocMap} readOnly={true}>
|
||||
{children}
|
||||
</DatabaseContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||
import { ViewMeta } from '@/application/db/tables/view_metas';
|
||||
import { createContext, useContext } from 'react';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
@@ -9,6 +10,10 @@ export interface DatabaseContextState {
|
||||
rowDocMap: Y.Map<YDoc>;
|
||||
isDatabaseRowPage?: boolean;
|
||||
navigateToRow?: (rowId: string) => void;
|
||||
loadView?: (viewId: string) => Promise<YDoc>;
|
||||
getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>;
|
||||
loadViewMeta?: (viewId: string) => Promise<ViewMeta>;
|
||||
navigateToView?: (viewId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const DatabaseContext = createContext<DatabaseContextState | null>(null);
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
FieldId,
|
||||
SortId,
|
||||
YDatabaseField,
|
||||
YDoc,
|
||||
YjsDatabaseKey,
|
||||
YjsEditorKey,
|
||||
YjsFolderKey,
|
||||
} from '@/application/collab.type';
|
||||
import { FieldId, SortId, YDatabaseField, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||
import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
|
||||
import {
|
||||
useDatabase,
|
||||
@@ -19,7 +11,6 @@ import {
|
||||
import { filterBy, parseFilter } from '@/application/database-yjs/filter';
|
||||
import { groupByField } from '@/application/database-yjs/group';
|
||||
import { sortBy } from '@/application/database-yjs/sort';
|
||||
import { useViewsIdSelector } from '@/application/folder-yjs';
|
||||
import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse';
|
||||
import { DateTimeCell } from '@/application/database-yjs/cell.type';
|
||||
import * as dayjs from 'dayjs';
|
||||
@@ -42,9 +33,8 @@ export interface Row {
|
||||
|
||||
const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty];
|
||||
|
||||
export function useDatabaseViewsSelector(iidIndex: string) {
|
||||
export function useDatabaseViewsSelector(_iidIndex: string) {
|
||||
const database = useDatabase();
|
||||
const { viewsId: visibleViewsId, views: folderViews } = useViewsIdSelector();
|
||||
|
||||
const views = database?.get(YjsDatabaseKey.views);
|
||||
const [viewIds, setViewIds] = useState<string[]>([]);
|
||||
@@ -65,22 +55,7 @@ export function useDatabaseViewsSelector(iidIndex: string) {
|
||||
return Number(viewB.created_at) - Number(viewA.created_at);
|
||||
});
|
||||
|
||||
const viewsId = [];
|
||||
|
||||
for (const viewItem of viewsSorted) {
|
||||
const [key] = viewItem;
|
||||
const view = folderViews?.get(key);
|
||||
|
||||
if (
|
||||
visibleViewsId.includes(key) &&
|
||||
view &&
|
||||
(view.get(YjsFolderKey.bid) === iidIndex || view.get(YjsFolderKey.id) === iidIndex)
|
||||
) {
|
||||
viewsId.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
setViewIds(viewsId);
|
||||
setViewIds(viewsSorted.map(([key]) => key));
|
||||
};
|
||||
|
||||
observerEvent();
|
||||
@@ -89,7 +64,7 @@ export function useDatabaseViewsSelector(iidIndex: string) {
|
||||
return () => {
|
||||
views.unobserve(observerEvent);
|
||||
};
|
||||
}, [visibleViewsId, views, folderViews, iidIndex]);
|
||||
}, [views]);
|
||||
|
||||
return {
|
||||
childViews,
|
||||
|
||||
+11
-8
@@ -2,6 +2,17 @@ import { YDoc } from '@/application/collab.type';
|
||||
import { databasePrefix } from '@/application/constants';
|
||||
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||
import * as Y from 'yjs';
|
||||
import BaseDexie from 'dexie';
|
||||
import { viewMetasSchema, ViewMetasTable } from '@/application/db/tables/view_metas';
|
||||
|
||||
type DexieTables = ViewMetasTable;
|
||||
|
||||
export type Dexie<T = DexieTables> = BaseDexie & T;
|
||||
|
||||
export const db = new BaseDexie(`${databasePrefix}_cache`) as Dexie;
|
||||
const schema = Object.assign({}, viewMetasSchema);
|
||||
|
||||
db.version(1).stores(schema);
|
||||
|
||||
const openedSet = new Set<string>();
|
||||
|
||||
@@ -31,11 +42,3 @@ export async function openCollabDB(docName: string): Promise<YDoc> {
|
||||
|
||||
return doc as YDoc;
|
||||
}
|
||||
|
||||
export function getCollabDBName(id: string, type: string, uuid?: string) {
|
||||
if (!uuid) {
|
||||
return `${type}_${id}`;
|
||||
}
|
||||
|
||||
return `${uuid}_${type}_${id}`;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Table } from 'dexie';
|
||||
|
||||
export interface MetaData {
|
||||
view_id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
layout: number;
|
||||
extra: string | null;
|
||||
created_by: string | null;
|
||||
last_edited_by: string | null;
|
||||
last_edited_time: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export type ViewMeta = {
|
||||
publish_name: string;
|
||||
|
||||
child_views: MetaData[];
|
||||
ancestor_views: MetaData[];
|
||||
} & MetaData;
|
||||
|
||||
export type ViewMetasTable = {
|
||||
view_metas: Table<ViewMeta>;
|
||||
};
|
||||
|
||||
export const viewMetasSchema = {
|
||||
view_metas: 'publish_name',
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { ViewLayout, YFolder, YjsFolderKey } from '@/application/collab.type';
|
||||
import { createContext, useContext } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export interface Crumb {
|
||||
viewId: string;
|
||||
rowId?: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const FolderContext = createContext<{
|
||||
folder: YFolder | null;
|
||||
onNavigateToView?: (viewId: string) => void;
|
||||
crumbs?: Crumb[];
|
||||
setCrumbs?: React.Dispatch<React.SetStateAction<Crumb[]>>;
|
||||
} | null>(null);
|
||||
|
||||
export const useFolderContext = () => {
|
||||
return useContext(FolderContext)?.folder;
|
||||
};
|
||||
|
||||
export const useViewLayout = () => {
|
||||
const folder = useFolderContext();
|
||||
const { objectId } = useParams();
|
||||
const views = folder?.get(YjsFolderKey.views);
|
||||
const view = objectId ? views?.get(objectId) : null;
|
||||
|
||||
return Number(view?.get(YjsFolderKey.layout)) as ViewLayout;
|
||||
};
|
||||
|
||||
export const useNavigateToView = () => {
|
||||
return useContext(FolderContext)?.onNavigateToView;
|
||||
};
|
||||
|
||||
export const useCrumbs = () => {
|
||||
return useContext(FolderContext)?.crumbs;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
export enum CoverType {
|
||||
NormalColor = 'color',
|
||||
GradientColor = 'gradient',
|
||||
BuildInImage = 'built_in',
|
||||
CustomImage = 'custom',
|
||||
LocalImage = 'local',
|
||||
UpsplashImage = 'unsplash',
|
||||
None = 'none',
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './selector';
|
||||
export * from './context';
|
||||
@@ -1,70 +0,0 @@
|
||||
import { YjsFolderKey, YView } from '@/application/collab.type';
|
||||
import { useFolderContext } from '@/application/folder-yjs/context';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useViewsIdSelector() {
|
||||
const folder = useFolderContext();
|
||||
const [viewsId, setViewsId] = useState<string[]>([]);
|
||||
const views = folder?.get(YjsFolderKey.views);
|
||||
const trash = folder?.get(YjsFolderKey.section)?.get(YjsFolderKey.trash);
|
||||
const meta = folder?.get(YjsFolderKey.meta);
|
||||
|
||||
useEffect(() => {
|
||||
if (!views) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trashUid = trash ? Array.from(trash.keys())[0] : null;
|
||||
const userTrash = trashUid ? trash?.get(trashUid) : null;
|
||||
|
||||
const collectIds = () => {
|
||||
const trashIds = userTrash?.toJSON()?.map((item) => item.id) || [];
|
||||
|
||||
return Array.from(views.keys()).filter((id) => {
|
||||
return !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace);
|
||||
});
|
||||
};
|
||||
|
||||
setViewsId(collectIds());
|
||||
const observerEvent = () => setViewsId(collectIds());
|
||||
|
||||
views.observe(observerEvent);
|
||||
userTrash?.observe(observerEvent);
|
||||
|
||||
return () => {
|
||||
views.unobserve(observerEvent);
|
||||
userTrash?.unobserve(observerEvent);
|
||||
};
|
||||
}, [views, trash, meta]);
|
||||
|
||||
return {
|
||||
viewsId,
|
||||
views,
|
||||
};
|
||||
}
|
||||
|
||||
export function useViewSelector(viewId: string) {
|
||||
const folder = useFolderContext();
|
||||
const [clock, setClock] = useState<number>(0);
|
||||
const [view, setView] = useState<YView | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!folder) return;
|
||||
|
||||
const view = folder.get(YjsFolderKey.views)?.get(viewId);
|
||||
|
||||
setView(view || null);
|
||||
const observerEvent = () => setClock((prev) => prev + 1);
|
||||
|
||||
view?.observe(observerEvent);
|
||||
|
||||
return () => {
|
||||
view?.unobserve(observerEvent);
|
||||
};
|
||||
}, [folder, viewId]);
|
||||
|
||||
return {
|
||||
clock,
|
||||
view,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { YDoc } from '@/application/collab.type';
|
||||
import { db } from '@/application/db';
|
||||
import { ViewMeta } from '@/application/db/tables/view_metas';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
import { useLiveQuery } from 'dexie-react-hooks';
|
||||
import { createContext, useCallback, useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export interface PublishContextType {
|
||||
namespace: string;
|
||||
publishName: string;
|
||||
viewMeta?: ViewMeta;
|
||||
toView: (viewId: string) => Promise<void>;
|
||||
loadViewMeta: (viewId: string) => Promise<ViewMeta>;
|
||||
getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>;
|
||||
|
||||
loadView: (viewId: string) => Promise<YDoc>;
|
||||
}
|
||||
|
||||
export const PublishContext = createContext<PublishContextType | null>(null);
|
||||
|
||||
export const PublishProvider = ({
|
||||
children,
|
||||
namespace,
|
||||
publishName,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
namespace: string;
|
||||
publishName: string;
|
||||
}) => {
|
||||
const viewMeta = useLiveQuery(async () => {
|
||||
const name = `${namespace}_${publishName}`;
|
||||
|
||||
return db.view_metas.get(name);
|
||||
});
|
||||
const service = useContext(AFConfigContext)?.service;
|
||||
const navigate = useNavigate();
|
||||
const toView = useCallback(
|
||||
async (viewId: string) => {
|
||||
try {
|
||||
const res = await service?.getPublishInfo(viewId);
|
||||
|
||||
if (!res) {
|
||||
throw new Error('Not found');
|
||||
}
|
||||
|
||||
const { namespace, publishName } = res;
|
||||
|
||||
navigate(`/${namespace}/${publishName}`);
|
||||
} catch (e) {
|
||||
notify.error('The view has not been published yet.');
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
[navigate, service]
|
||||
);
|
||||
|
||||
const loadViewMeta = useCallback(
|
||||
async (viewId: string) => {
|
||||
try {
|
||||
const info = await service?.getPublishInfo(viewId);
|
||||
|
||||
if (!info) {
|
||||
throw new Error('View has not been published yet');
|
||||
}
|
||||
|
||||
const res = await service?.getPublishViewMeta(namespace, publishName);
|
||||
|
||||
if (!res) {
|
||||
throw new Error('View has not been published yet');
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
[namespace, publishName, service]
|
||||
);
|
||||
|
||||
const getViewRowsMap = useCallback(
|
||||
async (viewId: string, rowIds: string[]) => {
|
||||
try {
|
||||
const info = await service?.getPublishInfo(viewId);
|
||||
|
||||
if (!info) {
|
||||
throw new Error('View has not been published yet');
|
||||
}
|
||||
|
||||
const { namespace, publishName } = info;
|
||||
const res = await service?.getPublishDatabaseViewRows(namespace, publishName, rowIds);
|
||||
|
||||
if (!res) {
|
||||
throw new Error('View has not been published yet');
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
[service]
|
||||
);
|
||||
|
||||
const loadView = useCallback(
|
||||
async (viewId: string) => {
|
||||
try {
|
||||
const res = await service?.getPublishInfo(viewId);
|
||||
|
||||
if (!res) {
|
||||
throw new Error('View has not been published yet');
|
||||
}
|
||||
|
||||
const { namespace, publishName } = res;
|
||||
|
||||
const data = service?.getPublishView(namespace, publishName);
|
||||
|
||||
if (!data) {
|
||||
throw new Error('View has not been published yet');
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
[service]
|
||||
);
|
||||
|
||||
return (
|
||||
<PublishContext.Provider
|
||||
value={{
|
||||
loadView,
|
||||
viewMeta,
|
||||
getViewRowsMap,
|
||||
loadViewMeta,
|
||||
toView,
|
||||
namespace,
|
||||
publishName,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PublishContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function usePublishContext() {
|
||||
return useContext(PublishContext);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './context';
|
||||
-310
@@ -1,310 +0,0 @@
|
||||
import { CollabType } from '@/application/collab.type';
|
||||
import * as Y from 'yjs';
|
||||
import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor';
|
||||
import { expect } from '@jest/globals';
|
||||
import { getCollab, batchCollab, collabTypeToDBType } from '../cache';
|
||||
import { applyYDoc } from '@/application/ydoc/apply';
|
||||
import { getCollabDBName, openCollabDB } from '../cache/db';
|
||||
import { StrategyType } from '../cache/types';
|
||||
|
||||
jest.mock('@/application/ydoc/apply', () => ({
|
||||
applyYDoc: jest.fn(),
|
||||
}));
|
||||
jest.mock('../cache/db', () => ({
|
||||
openCollabDB: jest.fn(),
|
||||
getCollabDBName: jest.fn(),
|
||||
}));
|
||||
|
||||
const emptyDoc = new Y.Doc();
|
||||
const normalDoc = withTestingYDoc('1');
|
||||
const mockFetcher = jest.fn();
|
||||
const mockBatchFetcher = jest.fn();
|
||||
|
||||
describe('Cache functions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getCollab', () => {
|
||||
describe('with CACHE_ONLY strategy', () => {
|
||||
it('should throw error when no cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(emptyDoc);
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
|
||||
await expect(
|
||||
getCollab(
|
||||
mockFetcher,
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
StrategyType.CACHE_ONLY
|
||||
)
|
||||
).rejects.toThrow('No cache found');
|
||||
});
|
||||
it('should fetch collab with CACHE_ONLY strategy and existing cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
|
||||
const result = await getCollab(
|
||||
mockFetcher,
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
StrategyType.CACHE_ONLY
|
||||
);
|
||||
|
||||
expect(result).toBe(normalDoc);
|
||||
expect(mockFetcher).not.toHaveBeenCalled();
|
||||
expect(applyYDoc).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with CACHE_FIRST strategy', () => {
|
||||
it('should fetch collab with CACHE_FIRST strategy and existing cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
|
||||
mockFetcher.mockResolvedValue({ state: new Uint8Array() });
|
||||
|
||||
const result = await getCollab(
|
||||
mockFetcher,
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
StrategyType.CACHE_FIRST
|
||||
);
|
||||
|
||||
expect(result).toBe(normalDoc);
|
||||
expect(mockFetcher).not.toHaveBeenCalled();
|
||||
expect(applyYDoc).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch collab with CACHE_FIRST strategy and no cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(emptyDoc);
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
|
||||
mockFetcher.mockResolvedValue({ state: new Uint8Array() });
|
||||
|
||||
const result = await getCollab(
|
||||
mockFetcher,
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
StrategyType.CACHE_FIRST
|
||||
);
|
||||
|
||||
expect(result).toBe(emptyDoc);
|
||||
expect(mockFetcher).toHaveBeenCalled();
|
||||
expect(applyYDoc).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with CACHE_AND_NETWORK strategy', () => {
|
||||
it('should fetch collab with CACHE_AND_NETWORK strategy and existing cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
|
||||
mockFetcher.mockResolvedValue({ state: new Uint8Array() });
|
||||
|
||||
const result = await getCollab(
|
||||
mockFetcher,
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
StrategyType.CACHE_AND_NETWORK
|
||||
);
|
||||
|
||||
expect(result).toBe(normalDoc);
|
||||
expect(mockFetcher).toHaveBeenCalled();
|
||||
expect(applyYDoc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch collab with CACHE_AND_NETWORK strategy and no cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(emptyDoc);
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
|
||||
mockFetcher.mockResolvedValue({ state: new Uint8Array() });
|
||||
|
||||
const result = await getCollab(
|
||||
mockFetcher,
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
StrategyType.CACHE_AND_NETWORK
|
||||
);
|
||||
|
||||
expect(result).toBe(emptyDoc);
|
||||
expect(mockFetcher).toHaveBeenCalled();
|
||||
expect(applyYDoc).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with default strategy', () => {
|
||||
it('should fetch collab with default strategy', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
|
||||
mockFetcher.mockResolvedValue({ state: new Uint8Array() });
|
||||
|
||||
const result = await getCollab(
|
||||
mockFetcher,
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
StrategyType.NETWORK_ONLY
|
||||
);
|
||||
|
||||
expect(result).toBe(normalDoc);
|
||||
expect(mockFetcher).toHaveBeenCalled();
|
||||
expect(applyYDoc).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchCollab', () => {
|
||||
describe('with CACHE_ONLY strategy', () => {
|
||||
it('should batch fetch collabs with CACHE_ONLY strategy and no cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(emptyDoc);
|
||||
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
|
||||
await expect(
|
||||
batchCollab(
|
||||
mockBatchFetcher,
|
||||
[
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
],
|
||||
StrategyType.CACHE_ONLY
|
||||
)
|
||||
).rejects.toThrow('No cache found');
|
||||
});
|
||||
|
||||
it('should batch fetch collabs with CACHE_ONLY strategy and existing cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
|
||||
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
|
||||
await batchCollab(
|
||||
mockBatchFetcher,
|
||||
[
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
],
|
||||
StrategyType.CACHE_ONLY
|
||||
);
|
||||
|
||||
expect(mockBatchFetcher).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with CACHE_FIRST strategy', () => {
|
||||
it('should batch fetch collabs with CACHE_FIRST strategy and existing cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
|
||||
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
|
||||
await batchCollab(
|
||||
mockBatchFetcher,
|
||||
[
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
],
|
||||
StrategyType.CACHE_FIRST
|
||||
);
|
||||
|
||||
expect(mockBatchFetcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should batch fetch collabs with CACHE_FIRST strategy and no cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(emptyDoc);
|
||||
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
mockBatchFetcher.mockResolvedValue({ id1: [1, 2, 3] });
|
||||
|
||||
await batchCollab(
|
||||
mockBatchFetcher,
|
||||
[
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
],
|
||||
StrategyType.CACHE_FIRST
|
||||
);
|
||||
|
||||
expect(mockBatchFetcher).toHaveBeenCalled();
|
||||
expect(applyYDoc).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with CACHE_AND_NETWORK strategy', () => {
|
||||
it('should batch fetch collabs with CACHE_AND_NETWORK strategy', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
|
||||
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
mockBatchFetcher.mockResolvedValue({ id1: [1, 2, 3] });
|
||||
|
||||
await batchCollab(
|
||||
mockBatchFetcher,
|
||||
[
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
],
|
||||
StrategyType.CACHE_AND_NETWORK
|
||||
);
|
||||
|
||||
expect(mockBatchFetcher).toHaveBeenCalled();
|
||||
expect(applyYDoc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should batch fetch collabs with CACHE_AND_NETWORK strategy and no cache', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(emptyDoc);
|
||||
|
||||
(getCollabDBName as jest.Mock).mockReturnValue('testDB');
|
||||
mockBatchFetcher.mockResolvedValue({ id1: [1, 2, 3] });
|
||||
|
||||
await batchCollab(
|
||||
mockBatchFetcher,
|
||||
[
|
||||
{
|
||||
collabId: 'id1',
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
],
|
||||
StrategyType.CACHE_AND_NETWORK
|
||||
);
|
||||
|
||||
expect(mockBatchFetcher).toHaveBeenCalled();
|
||||
expect(applyYDoc).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('collabTypeToDBType', () => {
|
||||
it('should return correct DB type', () => {
|
||||
expect(collabTypeToDBType(CollabType.Document)).toBe('document');
|
||||
expect(collabTypeToDBType(CollabType.Folder)).toBe('folder');
|
||||
expect(collabTypeToDBType(CollabType.Database)).toBe('database');
|
||||
expect(collabTypeToDBType(CollabType.WorkspaceDatabase)).toBe('databases');
|
||||
expect(collabTypeToDBType(CollabType.DatabaseRow)).toBe('database_row');
|
||||
expect(collabTypeToDBType(CollabType.UserAwareness)).toBe('user_awareness');
|
||||
expect(collabTypeToDBType(CollabType.Empty)).toBe('');
|
||||
});
|
||||
});
|
||||
+83
-24
@@ -1,13 +1,13 @@
|
||||
import { expect } from '@jest/globals';
|
||||
import { fetchCollab, batchFetchCollab } from '../fetch';
|
||||
import { CollabType } from '@/application/collab.type';
|
||||
import { fetchPublishView, fetchPublishViewMeta, fetchViewInfo } from '../fetch';
|
||||
import { APIService } from '@/application/services/js-services/wasm';
|
||||
|
||||
jest.mock('@/application/services/js-services/wasm', () => {
|
||||
return {
|
||||
APIService: {
|
||||
getCollab: jest.fn(),
|
||||
batchGetCollab: jest.fn(),
|
||||
getPublishView: jest.fn(),
|
||||
getPublishViewMeta: jest.fn(),
|
||||
getPublishInfoWithViewId: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -17,41 +17,100 @@ describe('Collab fetch functions with deduplication', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('fetchCollab', () => {
|
||||
it('should fetch collab without duplicating requests', async () => {
|
||||
const workspaceId = 'workspace1';
|
||||
const id = 'id1';
|
||||
const type = CollabType.Document;
|
||||
describe('fetchPublishView', () => {
|
||||
it('should fetch publish view without duplicating requests', async () => {
|
||||
const namespace = 'namespace1';
|
||||
const publishName = 'publish1';
|
||||
const mockResponse = { data: 'mockData' };
|
||||
|
||||
(APIService.getCollab as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(APIService.getPublishView as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result1 = fetchCollab(workspaceId, id, type);
|
||||
const result2 = fetchCollab(workspaceId, id, type);
|
||||
const result1 = fetchPublishView(namespace, publishName);
|
||||
const result2 = fetchPublishView(namespace, publishName);
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
await expect(result1).resolves.toEqual(mockResponse);
|
||||
expect(APIService.getCollab).toHaveBeenCalledTimes(1);
|
||||
expect(APIService.getPublishView).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fetch publish view with different params', async () => {
|
||||
const namespace = 'namespace1';
|
||||
const publishName = 'publish1';
|
||||
const mockResponse = { data: 'mockData' };
|
||||
|
||||
(APIService.getPublishView as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result1 = fetchPublishView(namespace, publishName);
|
||||
const result2 = fetchPublishView(namespace, 'publish2');
|
||||
|
||||
expect(result1).not.toBe(result2);
|
||||
await expect(result1).resolves.toEqual(mockResponse);
|
||||
await expect(result2).resolves.toEqual(mockResponse);
|
||||
expect(APIService.getPublishView).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchFetchCollab', () => {
|
||||
it('should batch fetch collabs without duplicating requests', async () => {
|
||||
const workspaceId = 'workspace1';
|
||||
const params = [
|
||||
{ collabId: 'id1', collabType: CollabType.Document },
|
||||
{ collabId: 'id2', collabType: CollabType.Folder },
|
||||
];
|
||||
describe('fetchViewInfo', () => {
|
||||
it('should fetch view info without duplicating requests', async () => {
|
||||
const viewId = 'view1';
|
||||
const mockResponse = { data: 'mockData' };
|
||||
|
||||
(APIService.batchGetCollab as jest.Mock).mockResolvedValue(mockResponse);
|
||||
(APIService.getPublishInfoWithViewId as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result1 = batchFetchCollab(workspaceId, params);
|
||||
const result2 = batchFetchCollab(workspaceId, params);
|
||||
const result1 = fetchViewInfo(viewId);
|
||||
const result2 = fetchViewInfo(viewId);
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
await expect(result1).resolves.toEqual(mockResponse);
|
||||
expect(APIService.batchGetCollab).toHaveBeenCalledTimes(1);
|
||||
expect(APIService.getPublishInfoWithViewId).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fetch view info with different params', async () => {
|
||||
const viewId = 'view1';
|
||||
const mockResponse = { data: 'mockData' };
|
||||
|
||||
(APIService.getPublishInfoWithViewId as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result1 = fetchViewInfo(viewId);
|
||||
const result2 = fetchViewInfo('view2');
|
||||
|
||||
expect(result1).not.toBe(result2);
|
||||
await expect(result1).resolves.toEqual(mockResponse);
|
||||
await expect(result2).resolves.toEqual(mockResponse);
|
||||
expect(APIService.getPublishInfoWithViewId).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchPublishViewMeta', () => {
|
||||
it('should fetch publish view meta without duplicating requests', async () => {
|
||||
const namespace = 'namespace1';
|
||||
const publishName = 'publish1';
|
||||
const mockResponse = { data: 'mockData' };
|
||||
|
||||
(APIService.getPublishViewMeta as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result1 = fetchPublishViewMeta(namespace, publishName);
|
||||
const result2 = fetchPublishViewMeta(namespace, publishName);
|
||||
|
||||
expect(result1).toBe(result2);
|
||||
await expect(result1).resolves.toEqual(mockResponse);
|
||||
expect(APIService.getPublishViewMeta).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should fetch publish view meta with different params', async () => {
|
||||
const namespace = 'namespace1';
|
||||
const publishName = 'publish1';
|
||||
const mockResponse = { data: 'mockData' };
|
||||
|
||||
(APIService.getPublishViewMeta as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result1 = fetchPublishViewMeta(namespace, publishName);
|
||||
const result2 = fetchPublishViewMeta(namespace, 'publish2');
|
||||
|
||||
expect(result1).not.toBe(result2);
|
||||
await expect(result1).resolves.toEqual(mockResponse);
|
||||
await expect(result2).resolves.toEqual(mockResponse);
|
||||
expect(APIService.getPublishViewMeta).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor';
|
||||
import { AFClientService } from '../index';
|
||||
import { fetchViewInfo } from '@/application/services/js-services/fetch';
|
||||
import { expect, jest } from '@jest/globals';
|
||||
import { getBatchCollabs, getPublishView, getPublishViewMeta } from '@/application/services/js-services/cache';
|
||||
|
||||
jest.mock('@/application/services/js-services/wasm/client_api', () => {
|
||||
return {
|
||||
initAPIService: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('nanoid', () => {
|
||||
return {
|
||||
nanoid: jest.fn().mockReturnValue('12345678'),
|
||||
};
|
||||
});
|
||||
jest.mock('@/application/services/js-services/fetch', () => {
|
||||
return {
|
||||
fetchPublishView: jest.fn(),
|
||||
fetchPublishViewMeta: jest.fn(),
|
||||
fetchViewInfo: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@/application/services/js-services/cache', () => {
|
||||
return {
|
||||
getPublishView: jest.fn(),
|
||||
getPublishViewMeta: jest.fn(),
|
||||
getBatchCollabs: jest.fn(),
|
||||
};
|
||||
});
|
||||
describe('AFClientService', () => {
|
||||
let service: AFClientService;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = new AFClientService({
|
||||
cloudConfig: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
gotrueURL: 'http://localhost:3000',
|
||||
wsURL: 'ws://localhost:3000',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should get view meta', async () => {
|
||||
const namespace = 'namespace';
|
||||
const publishName = 'publishName';
|
||||
const mockResponse = {
|
||||
view_id: 'view_id',
|
||||
publish_name: publishName,
|
||||
metadata: {
|
||||
view: {
|
||||
name: 'viewName',
|
||||
view_id: 'view_id',
|
||||
},
|
||||
child_views: [],
|
||||
ancestor_views: [],
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
(getPublishViewMeta as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPublishViewMeta(namespace, publishName);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should get view', async () => {
|
||||
const namespace = 'namespace';
|
||||
const publishName = 'publishName';
|
||||
const mockResponse = {
|
||||
data: [1, 2, 3],
|
||||
meta: {
|
||||
metadata: {
|
||||
view: {
|
||||
name: 'viewName',
|
||||
view_id: 'view_id',
|
||||
},
|
||||
child_views: [],
|
||||
ancestor_views: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
(getPublishView as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPublishView(namespace, publishName);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should get view info', async () => {
|
||||
const viewId = 'viewId';
|
||||
const mockResponse = {
|
||||
namespace: 'namespace',
|
||||
publish_name: 'publishName',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
(fetchViewInfo as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPublishInfo(viewId);
|
||||
|
||||
expect(result).toEqual({
|
||||
namespace: 'namespace',
|
||||
publishName: 'publishName',
|
||||
});
|
||||
});
|
||||
|
||||
it('getPublishDatabaseViewRows', async () => {
|
||||
const namespace = 'namespace';
|
||||
const publishName = 'publishName';
|
||||
const rowIds = ['1', '2', '3'];
|
||||
const mockResponse = [withTestingYDoc('1'), withTestingYDoc('2'), withTestingYDoc('3')];
|
||||
|
||||
// @ts-ignore
|
||||
(getBatchCollabs as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPublishDatabaseViewRows(namespace, publishName, rowIds);
|
||||
|
||||
expect(result).toEqual({
|
||||
rows: expect.any(Object),
|
||||
destroy: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import { AuthService } from '@/application/services/services.type';
|
||||
import { ProviderType, SignUpWithEmailPasswordParams } from '@/application/user.type';
|
||||
import { APIService } from 'src/application/services/js-services/wasm';
|
||||
import { signInSuccess } from '@/application/services/js-services/session/auth';
|
||||
import { invalidToken } from 'src/application/services/js-services/session';
|
||||
import { afterSignInDecorator } from '@/application/services/js-services/decorator';
|
||||
|
||||
export class JSAuthService implements AuthService {
|
||||
constructor() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
getOAuthURL = async (_provider: ProviderType): Promise<string> => {
|
||||
return Promise.reject('Not implemented');
|
||||
};
|
||||
|
||||
@afterSignInDecorator(signInSuccess)
|
||||
async signInWithOAuth(_: { uri: string }): Promise<void> {
|
||||
return Promise.reject('Not implemented');
|
||||
}
|
||||
|
||||
signupWithEmailPassword = async (_params: SignUpWithEmailPasswordParams): Promise<void> => {
|
||||
return Promise.reject('Not implemented');
|
||||
};
|
||||
|
||||
@afterSignInDecorator(signInSuccess)
|
||||
async signinWithEmailPassword(email: string, password: string): Promise<void> {
|
||||
try {
|
||||
return APIService.signIn(email, password);
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
signOut = async (): Promise<void> => {
|
||||
invalidToken();
|
||||
return APIService.logout();
|
||||
};
|
||||
}
|
||||
Vendored
+145
@@ -0,0 +1,145 @@
|
||||
import { CollabType } from '@/application/collab.type';
|
||||
import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor';
|
||||
import { expect } from '@jest/globals';
|
||||
import {
|
||||
collabTypeToDBType,
|
||||
getPublishView,
|
||||
getPublishViewMeta,
|
||||
getBatchCollabs,
|
||||
} from '@/application/services/js-services/cache';
|
||||
import { openCollabDB, db } from '@/application/db';
|
||||
import { StrategyType } from '@/application/services/js-services/cache/types';
|
||||
|
||||
jest.mock('@/application/ydoc/apply', () => ({
|
||||
applyYDoc: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/application/db', () => ({
|
||||
openCollabDB: jest.fn(),
|
||||
db: {
|
||||
view_metas: {
|
||||
get: jest.fn(),
|
||||
put: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const normalDoc = withTestingYDoc('1');
|
||||
const mockFetcher = jest.fn();
|
||||
|
||||
async function runTestWithStrategy(strategy: StrategyType) {
|
||||
return getPublishView(
|
||||
mockFetcher,
|
||||
{
|
||||
namespace: 'appflowy',
|
||||
publishName: 'test',
|
||||
},
|
||||
strategy
|
||||
);
|
||||
}
|
||||
|
||||
async function runGetPublishViewMetaWithStrategy(strategy: StrategyType) {
|
||||
return getPublishViewMeta(
|
||||
mockFetcher,
|
||||
{
|
||||
namespace: 'appflowy',
|
||||
publishName: 'test',
|
||||
},
|
||||
strategy
|
||||
);
|
||||
}
|
||||
|
||||
describe('Cache functions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockFetcher.mockClear();
|
||||
(openCollabDB as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
describe('getPublishView', () => {
|
||||
it('should call fetcher when no cache found', async () => {
|
||||
mockFetcher.mockResolvedValue({ data: [1, 2, 3], meta: { metadata: { view: { id: '1' } } } });
|
||||
(db.view_metas.get as jest.Mock).mockResolvedValue(undefined);
|
||||
await runTestWithStrategy(StrategyType.CACHE_FIRST);
|
||||
expect(mockFetcher).toBeCalledTimes(1);
|
||||
|
||||
await runTestWithStrategy(StrategyType.CACHE_AND_NETWORK);
|
||||
expect(mockFetcher).toBeCalledTimes(2);
|
||||
await expect(runTestWithStrategy(StrategyType.CACHE_ONLY)).rejects.toThrow('No cache found');
|
||||
});
|
||||
it('should call fetcher when cache is invalid or strategy is CACHE_AND_NETWORK', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
|
||||
(db.view_metas.get as jest.Mock).mockResolvedValue({ view_id: '1' });
|
||||
mockFetcher.mockResolvedValue({ data: [1, 2, 3], meta: { metadata: { view: { id: '1' } } } });
|
||||
await runTestWithStrategy(StrategyType.CACHE_ONLY);
|
||||
expect(openCollabDB).toBeCalledTimes(1);
|
||||
|
||||
await runTestWithStrategy(StrategyType.CACHE_FIRST);
|
||||
expect(openCollabDB).toBeCalledTimes(2);
|
||||
expect(mockFetcher).toBeCalledTimes(0);
|
||||
|
||||
await runTestWithStrategy(StrategyType.CACHE_AND_NETWORK);
|
||||
expect(openCollabDB).toBeCalledTimes(3);
|
||||
expect(mockFetcher).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPublishViewMeta', () => {
|
||||
it('should call fetcher when no cache found', async () => {
|
||||
mockFetcher.mockResolvedValue({ metadata: { view: { id: '1' }, child_views: [], ancestor_views: [] } });
|
||||
(db.view_metas.get as jest.Mock).mockResolvedValue(undefined);
|
||||
await runGetPublishViewMetaWithStrategy(StrategyType.CACHE_FIRST);
|
||||
expect(mockFetcher).toBeCalledTimes(1);
|
||||
|
||||
await runGetPublishViewMetaWithStrategy(StrategyType.CACHE_AND_NETWORK);
|
||||
expect(mockFetcher).toBeCalledTimes(2);
|
||||
|
||||
await expect(runGetPublishViewMetaWithStrategy(StrategyType.CACHE_ONLY)).rejects.toThrow('No cache found');
|
||||
});
|
||||
|
||||
it('should call fetcher when cache is invalid or strategy is CACHE_AND_NETWORK', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
|
||||
(db.view_metas.get as jest.Mock).mockResolvedValue({ view_id: '1' });
|
||||
|
||||
mockFetcher.mockResolvedValue({ metadata: { view: { id: '1' }, child_views: [], ancestor_views: [] } });
|
||||
const meta = await runGetPublishViewMetaWithStrategy(StrategyType.CACHE_ONLY);
|
||||
expect(openCollabDB).toBeCalledTimes(0);
|
||||
expect(meta).toBeDefined();
|
||||
|
||||
await runGetPublishViewMetaWithStrategy(StrategyType.CACHE_FIRST);
|
||||
expect(openCollabDB).toBeCalledTimes(0);
|
||||
expect(mockFetcher).toBeCalledTimes(0);
|
||||
|
||||
await runGetPublishViewMetaWithStrategy(StrategyType.CACHE_AND_NETWORK);
|
||||
expect(openCollabDB).toBeCalledTimes(0);
|
||||
expect(mockFetcher).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBatchCollabs', () => {
|
||||
it('should return empty array when no cache found', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(undefined);
|
||||
const collabs = await getBatchCollabs(['1', '2', '3']);
|
||||
expect(collabs).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return collabs when cache found', async () => {
|
||||
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
|
||||
(db.view_metas.get as jest.Mock).mockResolvedValue({ view_id: '1' });
|
||||
const collabs = await getBatchCollabs(['1', '2', '3']);
|
||||
expect(collabs).toEqual([normalDoc, normalDoc, normalDoc]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('collabTypeToDBType', () => {
|
||||
it('should return correct DB type', () => {
|
||||
expect(collabTypeToDBType(CollabType.Document)).toBe('document');
|
||||
expect(collabTypeToDBType(CollabType.Folder)).toBe('folder');
|
||||
expect(collabTypeToDBType(CollabType.Database)).toBe('database');
|
||||
expect(collabTypeToDBType(CollabType.WorkspaceDatabase)).toBe('databases');
|
||||
expect(collabTypeToDBType(CollabType.DatabaseRow)).toBe('database_row');
|
||||
expect(collabTypeToDBType(CollabType.UserAwareness)).toBe('user_awareness');
|
||||
expect(collabTypeToDBType(CollabType.Empty)).toBe('');
|
||||
});
|
||||
});
|
||||
+155
-90
@@ -1,7 +1,8 @@
|
||||
import { MetaData } from '@/application/db/tables/view_metas';
|
||||
import { CollabType, YDoc, YjsEditorKey, YSharedRoot } from '@/application/collab.type';
|
||||
import { applyYDoc } from '@/application/ydoc/apply';
|
||||
import { getCollabDBName, openCollabDB } from './db';
|
||||
import { Fetcher, StrategyType } from './types';
|
||||
import { db, openCollabDB } from '@/application/db';
|
||||
import { Fetcher, StrategyType } from '@/application/services/js-services/cache/types';
|
||||
|
||||
export function collabTypeToDBType(type: CollabType) {
|
||||
switch (type) {
|
||||
@@ -32,30 +33,42 @@ const collabSharedRootKeyMap = {
|
||||
[CollabType.Empty]: YjsEditorKey.empty,
|
||||
};
|
||||
|
||||
export function hasCache(doc: YDoc, type: CollabType) {
|
||||
export function hasCollabCache(doc: YDoc) {
|
||||
const data = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||
|
||||
return data.has(collabSharedRootKeyMap[type] as string);
|
||||
return Object.values(collabSharedRootKeyMap).some((key) => {
|
||||
return data.has(key);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCollab(
|
||||
fetcher: Fetcher<{
|
||||
state: Uint8Array;
|
||||
}>,
|
||||
export async function hasViewMetaCache(name: string) {
|
||||
const data = await db.view_metas.get(name);
|
||||
|
||||
return !!data;
|
||||
}
|
||||
|
||||
export async function getPublishViewMeta<
|
||||
T extends {
|
||||
metadata: {
|
||||
view: MetaData;
|
||||
child_views: MetaData[];
|
||||
ancestor_views: MetaData[];
|
||||
};
|
||||
}
|
||||
>(
|
||||
fetcher: Fetcher<T>,
|
||||
{
|
||||
collabId,
|
||||
collabType,
|
||||
uuid,
|
||||
namespace,
|
||||
publishName,
|
||||
}: {
|
||||
uuid?: string;
|
||||
collabId: string;
|
||||
collabType: CollabType;
|
||||
namespace: string;
|
||||
publishName: string;
|
||||
},
|
||||
strategy: StrategyType = StrategyType.CACHE_AND_NETWORK
|
||||
) {
|
||||
const name = getCollabDBName(collabId, collabTypeToDBType(collabType), uuid);
|
||||
const collab = await openCollabDB(name);
|
||||
const exist = hasCache(collab, collabType);
|
||||
const name = `${namespace}_${publishName}`;
|
||||
const exist = await hasViewMetaCache(name);
|
||||
const meta = await db.view_metas.get(name);
|
||||
|
||||
switch (strategy) {
|
||||
case StrategyType.CACHE_ONLY: {
|
||||
@@ -63,103 +76,155 @@ export async function getCollab(
|
||||
throw new Error('No cache found');
|
||||
}
|
||||
|
||||
return collab;
|
||||
return meta;
|
||||
}
|
||||
|
||||
case StrategyType.CACHE_FIRST: {
|
||||
if (!exist) {
|
||||
await revalidateCollab(fetcher, collab);
|
||||
return revalidatePublishViewMeta(name, fetcher);
|
||||
}
|
||||
|
||||
return collab;
|
||||
return meta;
|
||||
}
|
||||
|
||||
case StrategyType.CACHE_AND_NETWORK: {
|
||||
if (!exist) {
|
||||
await revalidateCollab(fetcher, collab);
|
||||
return revalidatePublishViewMeta(name, fetcher);
|
||||
} else {
|
||||
void revalidateCollab(fetcher, collab);
|
||||
void revalidatePublishViewMeta(name, fetcher);
|
||||
}
|
||||
|
||||
return collab;
|
||||
return meta;
|
||||
}
|
||||
|
||||
default: {
|
||||
await revalidateCollab(fetcher, collab);
|
||||
|
||||
return collab;
|
||||
return revalidatePublishViewMeta(name, fetcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function revalidateCollab(
|
||||
fetcher: Fetcher<{
|
||||
state: Uint8Array;
|
||||
}>,
|
||||
collab: YDoc
|
||||
export async function getPublishView<
|
||||
T extends {
|
||||
data: number[];
|
||||
meta: {
|
||||
metadata: {
|
||||
view: MetaData;
|
||||
child_views: MetaData[];
|
||||
ancestor_views: MetaData[];
|
||||
};
|
||||
};
|
||||
}
|
||||
>(
|
||||
fetcher: Fetcher<T>,
|
||||
{
|
||||
namespace,
|
||||
publishName,
|
||||
}: {
|
||||
namespace: string;
|
||||
publishName: string;
|
||||
},
|
||||
strategy: StrategyType = StrategyType.CACHE_AND_NETWORK
|
||||
) {
|
||||
const { state } = await fetcher();
|
||||
const name = `${namespace}_${publishName}`;
|
||||
const doc = await openCollabDB(name);
|
||||
const exist = (await hasViewMetaCache(name)) && hasCollabCache(doc);
|
||||
|
||||
switch (strategy) {
|
||||
case StrategyType.CACHE_ONLY: {
|
||||
if (!exist) {
|
||||
throw new Error('No cache found');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case StrategyType.CACHE_FIRST: {
|
||||
if (!exist) {
|
||||
await revalidatePublishView(name, fetcher, doc);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case StrategyType.CACHE_AND_NETWORK: {
|
||||
if (!exist) {
|
||||
await revalidatePublishView(name, fetcher, doc);
|
||||
} else {
|
||||
void revalidatePublishView(name, fetcher, doc);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
await revalidatePublishView(name, fetcher, doc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
export async function revalidatePublishViewMeta<
|
||||
T extends {
|
||||
metadata: {
|
||||
view: MetaData;
|
||||
child_views: MetaData[];
|
||||
ancestor_views: MetaData[];
|
||||
};
|
||||
}
|
||||
>(name: string, fetcher: Fetcher<T>) {
|
||||
const { metadata } = await fetcher();
|
||||
|
||||
await db.view_metas.put(
|
||||
{
|
||||
publish_name: name,
|
||||
...metadata.view,
|
||||
child_views: metadata.child_views,
|
||||
ancestor_views: metadata.ancestor_views,
|
||||
},
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
export async function revalidatePublishView<
|
||||
T extends {
|
||||
data: number[];
|
||||
rows?: Record<string, number[]>;
|
||||
meta: {
|
||||
metadata: {
|
||||
view: MetaData;
|
||||
child_views: MetaData[];
|
||||
ancestor_views: MetaData[];
|
||||
};
|
||||
};
|
||||
}
|
||||
>(name: string, fetcher: Fetcher<T>, collab: YDoc) {
|
||||
const { data, meta, rows } = await fetcher();
|
||||
|
||||
await db.view_metas.put(
|
||||
{
|
||||
publish_name: name,
|
||||
...meta.metadata.view,
|
||||
child_views: meta.metadata.child_views,
|
||||
ancestor_views: meta.metadata.ancestor_views,
|
||||
},
|
||||
name
|
||||
);
|
||||
|
||||
for (const [key, value] of Object.entries(rows ?? {})) {
|
||||
const row = await openCollabDB(`${name}_${key}`);
|
||||
|
||||
applyYDoc(row, new Uint8Array(value));
|
||||
}
|
||||
|
||||
const state = new Uint8Array(data);
|
||||
|
||||
applyYDoc(collab, state);
|
||||
}
|
||||
|
||||
export async function batchCollab(
|
||||
batchFetcher: Fetcher<Record<string, number[]>>,
|
||||
collabs: {
|
||||
collabId: string;
|
||||
collabType: CollabType;
|
||||
uuid?: string;
|
||||
}[],
|
||||
strategy: StrategyType = StrategyType.CACHE_AND_NETWORK,
|
||||
itemCallback?: (id: string, doc: YDoc) => void
|
||||
) {
|
||||
const collabMap = new Map<string, YDoc>();
|
||||
export async function getBatchCollabs(names: string[]) {
|
||||
const collabs = await Promise.all(names.map((name) => openCollabDB(name)));
|
||||
|
||||
for (const { collabId, collabType, uuid } of collabs) {
|
||||
const name = getCollabDBName(collabId, collabTypeToDBType(collabType), uuid);
|
||||
const collab = await openCollabDB(name);
|
||||
const exist = hasCache(collab, collabType);
|
||||
|
||||
collabMap.set(collabId, collab);
|
||||
if (exist) {
|
||||
itemCallback?.(collabId, collab);
|
||||
}
|
||||
}
|
||||
|
||||
const notCacheIds = collabs.filter(({ collabId, collabType }) => {
|
||||
const id = collabMap.get(collabId);
|
||||
|
||||
if (!id) return false;
|
||||
|
||||
return !hasCache(id, collabType);
|
||||
});
|
||||
|
||||
if (strategy === StrategyType.CACHE_ONLY) {
|
||||
if (notCacheIds.length > 0) {
|
||||
throw new Error('No cache found');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (strategy === StrategyType.CACHE_FIRST && notCacheIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const states = await batchFetcher();
|
||||
|
||||
for (const [collabId, data] of Object.entries(states)) {
|
||||
const info = collabs.find((item) => item.collabId === collabId);
|
||||
const collab = collabMap.get(collabId);
|
||||
|
||||
if (!info || !collab) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const state = new Uint8Array(data);
|
||||
|
||||
applyYDoc(collab, state);
|
||||
|
||||
itemCallback?.(collabId, collab);
|
||||
}
|
||||
return collabs;
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import { CollabType, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||
import { batchCollab, getCollab } from '@/application/services/js-services/cache';
|
||||
import { StrategyType } from '@/application/services/js-services/cache/types';
|
||||
import { batchFetchCollab, fetchCollab } from '@/application/services/js-services/fetch';
|
||||
import { getCurrentWorkspace } from 'src/application/services/js-services/session';
|
||||
import { DatabaseService } from '@/application/services/services.type';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export class JSDatabaseService implements DatabaseService {
|
||||
private loadedDatabaseId: Set<string> = new Set();
|
||||
|
||||
private loadedWorkspaceId: Set<string> = new Set();
|
||||
|
||||
private cacheDatabaseRowDocMap: Map<string, Y.Doc> = new Map();
|
||||
|
||||
constructor() {
|
||||
//
|
||||
}
|
||||
|
||||
currentWorkspace() {
|
||||
return getCurrentWorkspace();
|
||||
}
|
||||
|
||||
async getWorkspaceDatabases(): Promise<{ views: string[]; database_id: string }[]> {
|
||||
const workspace = await this.currentWorkspace();
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error('Workspace database not found');
|
||||
}
|
||||
|
||||
const isLoaded = this.loadedWorkspaceId.has(workspace.id);
|
||||
|
||||
const workspaceDatabase = await getCollab(
|
||||
() => {
|
||||
return fetchCollab(workspace.id, workspace.workspaceDatabaseId, CollabType.WorkspaceDatabase);
|
||||
},
|
||||
{
|
||||
collabId: workspace.workspaceDatabaseId,
|
||||
collabType: CollabType.WorkspaceDatabase,
|
||||
},
|
||||
isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK
|
||||
);
|
||||
|
||||
if (!isLoaded) {
|
||||
this.loadedWorkspaceId.add(workspace.id);
|
||||
}
|
||||
|
||||
return workspaceDatabase.getMap(YjsEditorKey.data_section).get(YjsEditorKey.workspace_database).toJSON() as {
|
||||
views: string[];
|
||||
database_id: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
async openDatabase(databaseId: string): Promise<{
|
||||
databaseDoc: YDoc;
|
||||
rows: Y.Map<YDoc>;
|
||||
}> {
|
||||
const workspace = await this.currentWorkspace();
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error('Workspace database not found');
|
||||
}
|
||||
|
||||
const workspaceId = workspace.id;
|
||||
const isLoaded = this.loadedDatabaseId.has(databaseId);
|
||||
|
||||
const rootRowsDoc =
|
||||
this.cacheDatabaseRowDocMap.get(databaseId) ??
|
||||
new Y.Doc({
|
||||
guid: databaseId,
|
||||
});
|
||||
|
||||
if (!this.cacheDatabaseRowDocMap.has(databaseId)) {
|
||||
this.cacheDatabaseRowDocMap.set(databaseId, rootRowsDoc);
|
||||
}
|
||||
|
||||
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||
|
||||
const databaseDoc = await getCollab(
|
||||
() => {
|
||||
return fetchCollab(workspaceId, databaseId, CollabType.Database);
|
||||
},
|
||||
{
|
||||
collabId: databaseId,
|
||||
collabType: CollabType.Database,
|
||||
},
|
||||
isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK
|
||||
);
|
||||
|
||||
if (!isLoaded) this.loadedDatabaseId.add(databaseId);
|
||||
|
||||
const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase;
|
||||
const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString();
|
||||
const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders);
|
||||
const rowOrdersIds = rowOrders.toJSON() as {
|
||||
id: string;
|
||||
}[];
|
||||
|
||||
if (!rowOrdersIds) {
|
||||
throw new Error('Database rows not found');
|
||||
}
|
||||
|
||||
const rowsParams = rowOrdersIds.map((item) => ({
|
||||
collabId: item.id,
|
||||
collabType: CollabType.DatabaseRow,
|
||||
}));
|
||||
|
||||
void batchCollab(
|
||||
() => {
|
||||
return batchFetchCollab(workspaceId, rowsParams);
|
||||
},
|
||||
rowsParams,
|
||||
isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK,
|
||||
(id: string, doc: YDoc) => {
|
||||
if (!rowsFolder.has(id)) {
|
||||
rowsFolder.set(id, doc);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Update rows if there are new rows added after the database has been loaded
|
||||
rowOrders?.observe((event) => {
|
||||
if (event.changes.added.size > 0) {
|
||||
const rowIds = rowOrders.toJSON() as {
|
||||
id: string;
|
||||
}[];
|
||||
|
||||
const params = rowIds.map((item) => ({
|
||||
collabId: item.id,
|
||||
collabType: CollabType.DatabaseRow,
|
||||
}));
|
||||
|
||||
void batchCollab(
|
||||
() => {
|
||||
return batchFetchCollab(workspaceId, params);
|
||||
},
|
||||
params,
|
||||
StrategyType.CACHE_AND_NETWORK,
|
||||
(id: string, doc: YDoc) => {
|
||||
if (!rowsFolder.has(id)) {
|
||||
rowsFolder.set(id, doc);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
databaseDoc,
|
||||
rows: rowsFolder,
|
||||
};
|
||||
}
|
||||
|
||||
async closeDatabase(databaseId: string) {
|
||||
this.cacheDatabaseRowDocMap.delete(databaseId);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* @description:
|
||||
* * This is a decorator that can be used to read data from storage and fetch data from the server.
|
||||
* * If the data is already in storage, it will return the data from storage and fetch the data from the server in the background.
|
||||
*
|
||||
* @param getStorage A function that returns the data from storage. eg. `() => Promise<T | undefined>`
|
||||
*
|
||||
* @param setStorage A function that saves the data to storage. eg. `(data: T) => Promise<void>`
|
||||
*
|
||||
* @param fetchFunction A function that fetches the data from the server. eg. `(params: P) => Promise<T | undefined>`
|
||||
*
|
||||
* @returns: A function that returns the data from storage and fetches the data from the server in the background.
|
||||
*/
|
||||
export function asyncDataDecorator<P, T>(
|
||||
getStorage: () => Promise<T | undefined>,
|
||||
setStorage: (data: T) => Promise<void>,
|
||||
fetchFunction: (params: P) => Promise<T | undefined>
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
async function fetchData(params: P) {
|
||||
const data = await fetchFunction(params);
|
||||
|
||||
if (!data) return;
|
||||
await setStorage(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = async function (params: P) {
|
||||
const data = await getStorage();
|
||||
|
||||
await originalMethod.apply(this, [params]);
|
||||
if (data) {
|
||||
void fetchData(params);
|
||||
return data;
|
||||
} else {
|
||||
return fetchData(params);
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
export function afterSignInDecorator(successCallback: () => Promise<void>) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
await originalMethod.apply(this, args);
|
||||
await successCallback();
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type';
|
||||
import { getCollab } from '@/application/services/js-services/cache';
|
||||
import { StrategyType } from '@/application/services/js-services/cache/types';
|
||||
import { fetchCollab } from '@/application/services/js-services/fetch';
|
||||
import { getCurrentWorkspace } from 'src/application/services/js-services/session';
|
||||
import { DocumentService } from '@/application/services/services.type';
|
||||
|
||||
export class JSDocumentService implements DocumentService {
|
||||
private loaded: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
//
|
||||
}
|
||||
|
||||
async openDocument(docId: string): Promise<YDoc> {
|
||||
const workspace = await getCurrentWorkspace();
|
||||
|
||||
if (!workspace) {
|
||||
throw new Error('Workspace database not found');
|
||||
}
|
||||
|
||||
const isLoaded = this.loaded.has(docId);
|
||||
|
||||
const doc = await getCollab(
|
||||
() => {
|
||||
return fetchCollab(workspace.id, docId, CollabType.Document);
|
||||
},
|
||||
{
|
||||
collabId: docId,
|
||||
collabType: CollabType.Document,
|
||||
},
|
||||
isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK
|
||||
);
|
||||
|
||||
if (!isLoaded) this.loaded.add(docId);
|
||||
const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => {
|
||||
if (origin === CollabOrigin.LocalSync) {
|
||||
// Send the update to the server
|
||||
console.log('update', update);
|
||||
}
|
||||
};
|
||||
|
||||
doc.on('update', handleUpdate);
|
||||
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CollabType } from '@/application/collab.type';
|
||||
import { APIService } from '@/application/services/js-services/wasm';
|
||||
|
||||
const pendingRequests = new Map();
|
||||
@@ -31,36 +30,20 @@ function fetchWithDeduplication<Req, Res>(url: string, params: Req, fetchFunctio
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch collab
|
||||
* @param workspaceId
|
||||
* @param id
|
||||
* @param type [CollabType]
|
||||
*/
|
||||
export function fetchCollab(workspaceId: string, id: string, type: CollabType) {
|
||||
const fetchFunction = () => APIService.getCollab(workspaceId, id, type);
|
||||
export function fetchPublishView(namespace: string, publishName: string) {
|
||||
const fetchFunction = () => APIService.getPublishView(namespace, publishName);
|
||||
|
||||
return fetchWithDeduplication(`fetchCollab_${workspaceId}`, { id, type }, fetchFunction);
|
||||
return fetchWithDeduplication(`fetchPublishView_${namespace}`, { publishName }, fetchFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch fetch collab
|
||||
* Usage:
|
||||
* // load database rows
|
||||
* const rows = await batchFetchCollab(workspaceId, databaseRows.map((row) => ({ collabId: row.id, collabType: CollabType.DatabaseRow })));
|
||||
*
|
||||
* @param workspaceId
|
||||
* @param params [{ collabId: string; collabType: CollabType }]
|
||||
*/
|
||||
export function batchFetchCollab(workspaceId: string, params: { collabId: string; collabType: CollabType }[]) {
|
||||
const fetchFunction = () =>
|
||||
APIService.batchGetCollab(
|
||||
workspaceId,
|
||||
params.map(({ collabId, collabType }) => ({
|
||||
object_id: collabId,
|
||||
collab_type: collabType,
|
||||
}))
|
||||
);
|
||||
export function fetchViewInfo(viewId: string) {
|
||||
const fetchFunction = () => APIService.getPublishInfoWithViewId(viewId);
|
||||
|
||||
return fetchWithDeduplication(`batchFetchCollab_${workspaceId}`, params, fetchFunction);
|
||||
return fetchWithDeduplication(`fetchViewInfo`, { viewId }, fetchFunction);
|
||||
}
|
||||
|
||||
export function fetchPublishViewMeta(namespace: string, publishName: string) {
|
||||
const fetchFunction = () => APIService.getPublishViewMeta(namespace, publishName);
|
||||
|
||||
return fetchWithDeduplication(`fetchPublishViewMeta_${namespace}`, { publishName }, fetchFunction);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type';
|
||||
import { getCollab } from '@/application/services/js-services/cache';
|
||||
import { StrategyType } from '@/application/services/js-services/cache/types';
|
||||
import { fetchCollab } from '@/application/services/js-services/fetch';
|
||||
import { FolderService } from '@/application/services/services.type';
|
||||
|
||||
export class JSFolderService implements FolderService {
|
||||
private loaded: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
//
|
||||
}
|
||||
|
||||
async openWorkspace(workspaceId: string): Promise<YDoc> {
|
||||
const isLoaded = this.loaded.has(workspaceId);
|
||||
const doc = await getCollab(
|
||||
() => {
|
||||
return fetchCollab(workspaceId, workspaceId, CollabType.Folder);
|
||||
},
|
||||
{
|
||||
collabId: workspaceId,
|
||||
collabType: CollabType.Folder,
|
||||
},
|
||||
isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK
|
||||
);
|
||||
|
||||
if (!isLoaded) this.loaded.add(workspaceId);
|
||||
const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => {
|
||||
if (origin === CollabOrigin.LocalSync) {
|
||||
// Send the update to the server
|
||||
console.log('update', update);
|
||||
}
|
||||
};
|
||||
|
||||
doc.on('update', handleUpdate);
|
||||
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,28 @@
|
||||
import { JSDatabaseService } from '@/application/services/js-services/database.service';
|
||||
import {
|
||||
AFService,
|
||||
AFServiceConfig,
|
||||
AuthService,
|
||||
DatabaseService,
|
||||
DocumentService,
|
||||
FolderService,
|
||||
UserService,
|
||||
} from '@/application/services/services.type';
|
||||
import { JSUserService } from '@/application/services/js-services/user.service';
|
||||
import { JSAuthService } from '@/application/services/js-services/auth.service';
|
||||
import { JSFolderService } from '@/application/services/js-services/folder.service';
|
||||
import { JSDocumentService } from '@/application/services/js-services/document.service';
|
||||
import { YDoc } from '@/application/collab.type';
|
||||
import { getBatchCollabs, getPublishView, getPublishViewMeta } from '@/application/services/js-services/cache';
|
||||
import { StrategyType } from '@/application/services/js-services/cache/types';
|
||||
import { fetchPublishView, fetchPublishViewMeta, fetchViewInfo } from '@/application/services/js-services/fetch';
|
||||
import { AFService, AFServiceConfig } from '@/application/services/services.type';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { initAPIService } from '@/application/services/js-services/wasm/client_api';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export class AFClientService implements AFService {
|
||||
authService: AuthService;
|
||||
|
||||
userService: UserService;
|
||||
|
||||
documentService: DocumentService;
|
||||
|
||||
folderService: FolderService;
|
||||
|
||||
databaseService: DatabaseService;
|
||||
|
||||
private deviceId: string = nanoid(8);
|
||||
|
||||
private clientId: string = 'web';
|
||||
|
||||
getDeviceID = (): string => {
|
||||
return this.deviceId;
|
||||
};
|
||||
private publishViewLoaded: Set<string> = new Set();
|
||||
|
||||
getClientID = (): string => {
|
||||
return this.clientId;
|
||||
};
|
||||
private publishViewInfo: Map<
|
||||
string,
|
||||
{
|
||||
namespace: string;
|
||||
publishName: string;
|
||||
}
|
||||
> = new Map();
|
||||
|
||||
private cacheDatabaseRowDocMap: Map<string, Y.Doc> = new Map();
|
||||
|
||||
constructor(config: AFServiceConfig) {
|
||||
initAPIService({
|
||||
@@ -44,11 +30,104 @@ export class AFClientService implements AFService {
|
||||
deviceId: this.deviceId,
|
||||
clientId: this.clientId,
|
||||
});
|
||||
}
|
||||
|
||||
this.authService = new JSAuthService();
|
||||
this.userService = new JSUserService();
|
||||
this.documentService = new JSDocumentService();
|
||||
this.folderService = new JSFolderService();
|
||||
this.databaseService = new JSDatabaseService();
|
||||
async getPublishViewMeta(namespace: string, publishName: string) {
|
||||
const viewMeta = await getPublishViewMeta(
|
||||
() => {
|
||||
return fetchPublishViewMeta(namespace, publishName);
|
||||
},
|
||||
{
|
||||
namespace,
|
||||
publishName,
|
||||
},
|
||||
StrategyType.CACHE_AND_NETWORK
|
||||
);
|
||||
|
||||
if (!viewMeta) {
|
||||
return Promise.reject(new Error('View has not been published yet'));
|
||||
}
|
||||
|
||||
return viewMeta;
|
||||
}
|
||||
|
||||
async getPublishView(namespace: string, publishName: string) {
|
||||
const name = `${namespace}_${publishName}`;
|
||||
const isLoaded = this.publishViewLoaded.has(name);
|
||||
const doc = await getPublishView(
|
||||
() => {
|
||||
return fetchPublishView(namespace, publishName);
|
||||
},
|
||||
{
|
||||
namespace,
|
||||
publishName,
|
||||
},
|
||||
isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK
|
||||
);
|
||||
|
||||
if (!isLoaded) {
|
||||
this.publishViewLoaded.add(name);
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
async getPublishDatabaseViewRows(namespace: string, publishName: string, rowIds: string[]) {
|
||||
const name = `${namespace}_${publishName}`;
|
||||
|
||||
if (!this.publishViewLoaded.has(name)) {
|
||||
await this.getPublishView(namespace, publishName);
|
||||
}
|
||||
|
||||
const rootRowsDoc =
|
||||
this.cacheDatabaseRowDocMap.get(name) ??
|
||||
new Y.Doc({
|
||||
guid: name,
|
||||
});
|
||||
|
||||
if (!this.cacheDatabaseRowDocMap.has(name)) {
|
||||
this.cacheDatabaseRowDocMap.set(name, rootRowsDoc);
|
||||
}
|
||||
|
||||
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||
const docs = await getBatchCollabs(rowIds);
|
||||
|
||||
docs.forEach((doc, index) => {
|
||||
rowsFolder.set(rowIds[index], doc);
|
||||
});
|
||||
|
||||
return {
|
||||
rows: rowsFolder,
|
||||
destroy: () => {
|
||||
this.cacheDatabaseRowDocMap.delete(name);
|
||||
rootRowsDoc.destroy();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getPublishInfo(viewId: string) {
|
||||
if (this.publishViewInfo.has(viewId)) {
|
||||
return this.publishViewInfo.get(viewId) as {
|
||||
namespace: string;
|
||||
publishName: string;
|
||||
};
|
||||
}
|
||||
|
||||
const info = await fetchViewInfo(viewId);
|
||||
|
||||
const namespace = info.namespace;
|
||||
|
||||
if (!namespace) {
|
||||
return Promise.reject(new Error('View not found'));
|
||||
}
|
||||
|
||||
const data = {
|
||||
namespace,
|
||||
publishName: info.publish_name,
|
||||
};
|
||||
|
||||
this.publishViewInfo.set(viewId, data);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export async function signInSuccess() {
|
||||
// Do nothing
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './token';
|
||||
export * from './user';
|
||||
export * from './auth';
|
||||
@@ -1,37 +0,0 @@
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
|
||||
const tokenKey = 'token';
|
||||
|
||||
export function readTokenStr() {
|
||||
return sessionStorage.getItem(tokenKey);
|
||||
}
|
||||
|
||||
export function getAuthInfo() {
|
||||
const token = readTokenStr() || '';
|
||||
|
||||
try {
|
||||
const info = JSON.parse(token);
|
||||
|
||||
return {
|
||||
uuid: info.user.id,
|
||||
access_token: info.access_token,
|
||||
email: info.user.email,
|
||||
};
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeToken(token: string) {
|
||||
if (!token) {
|
||||
invalidToken();
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem(tokenKey, token);
|
||||
}
|
||||
|
||||
export function invalidToken() {
|
||||
sessionStorage.removeItem(tokenKey);
|
||||
notify.error('Invalid token, please login again');
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { UserProfile, UserWorkspace, Workspace } from '@/application/user.type';
|
||||
|
||||
const userKey = 'user';
|
||||
const workspaceKey = 'workspace';
|
||||
|
||||
export async function getSignInUser(): Promise<UserProfile | undefined> {
|
||||
const userStr = localStorage.getItem(userKey);
|
||||
|
||||
try {
|
||||
return userStr ? JSON.parse(userStr) : undefined;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setSignInUser(profile: UserProfile) {
|
||||
const userStr = JSON.stringify(profile);
|
||||
|
||||
localStorage.setItem(userKey, userStr);
|
||||
}
|
||||
|
||||
export async function getUserWorkspace(): Promise<UserWorkspace | undefined> {
|
||||
const str = localStorage.getItem(workspaceKey);
|
||||
|
||||
try {
|
||||
return str ? JSON.parse(str) : undefined;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setUserWorkspace(workspace: UserWorkspace) {
|
||||
const str = JSON.stringify(workspace);
|
||||
|
||||
localStorage.setItem(workspaceKey, str);
|
||||
}
|
||||
|
||||
export async function getCurrentWorkspace(): Promise<Workspace | undefined> {
|
||||
const userProfile = await getSignInUser();
|
||||
const userWorkspace = await getUserWorkspace();
|
||||
|
||||
return userWorkspace?.workspaces.find((workspace) => workspace.id === userProfile?.workspaceId);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { UserService } from '@/application/services/services.type';
|
||||
import { UserProfile, UserWorkspace } from '@/application/user.type';
|
||||
import { APIService } from 'src/application/services/js-services/wasm';
|
||||
import {
|
||||
getAuthInfo,
|
||||
getSignInUser,
|
||||
getUserWorkspace,
|
||||
invalidToken,
|
||||
setSignInUser,
|
||||
setUserWorkspace,
|
||||
} from 'src/application/services/js-services/session';
|
||||
import { asyncDataDecorator } from '@/application/services/js-services/decorator';
|
||||
|
||||
async function getUser() {
|
||||
try {
|
||||
const user = await APIService.getUser();
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
invalidToken();
|
||||
}
|
||||
}
|
||||
|
||||
export class JSUserService implements UserService {
|
||||
@asyncDataDecorator<void, UserProfile>(getSignInUser, setSignInUser, getUser)
|
||||
async getUserProfile(): Promise<UserProfile> {
|
||||
if (!getAuthInfo()) {
|
||||
return Promise.reject('Not authenticated');
|
||||
}
|
||||
|
||||
await this.getUserWorkspace();
|
||||
|
||||
return null!;
|
||||
}
|
||||
|
||||
async checkUser(): Promise<boolean> {
|
||||
return (await getSignInUser()) !== undefined;
|
||||
}
|
||||
|
||||
@asyncDataDecorator<void, UserWorkspace>(getUserWorkspace, setUserWorkspace, APIService.getUserWorkspace)
|
||||
async getUserWorkspace(): Promise<UserWorkspace> {
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import { CollabType } from '@/application/collab.type';
|
||||
import { ClientAPI } from '@appflowyinc/client-api-wasm';
|
||||
import { UserProfile, UserWorkspace } from '@/application/user.type';
|
||||
import { AFCloudConfig } from '@/application/services/services.type';
|
||||
import { invalidToken, readTokenStr, writeToken } from 'src/application/services/js-services/session';
|
||||
|
||||
let client: ClientAPI;
|
||||
|
||||
@@ -12,8 +9,14 @@ export function initAPIService(
|
||||
clientId: string;
|
||||
}
|
||||
) {
|
||||
window.refresh_token = writeToken;
|
||||
window.invalid_token = invalidToken;
|
||||
window.refresh_token = () => {
|
||||
//
|
||||
};
|
||||
|
||||
window.invalid_token = () => {
|
||||
// invalidToken();
|
||||
};
|
||||
|
||||
client = ClientAPI.new({
|
||||
base_url: config.baseURL,
|
||||
ws_addr: config.wsURL,
|
||||
@@ -26,96 +29,17 @@ export function initAPIService(
|
||||
},
|
||||
});
|
||||
|
||||
const token = readTokenStr();
|
||||
|
||||
if (token) {
|
||||
client.restore_token(token);
|
||||
}
|
||||
|
||||
client.subscribe();
|
||||
}
|
||||
|
||||
export function signIn(email: string, password: string) {
|
||||
return client.login(email, password);
|
||||
export async function getPublishView(publishNamespace: string, publishName: string) {
|
||||
return client.get_publish_view(publishNamespace, publishName);
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return client.logout();
|
||||
export async function getPublishInfoWithViewId(viewId: string) {
|
||||
return client.get_publish_info(viewId);
|
||||
}
|
||||
|
||||
export async function getUser(): Promise<UserProfile> {
|
||||
try {
|
||||
const user = await client.get_user();
|
||||
|
||||
if (!user) {
|
||||
throw new Error('No user found');
|
||||
}
|
||||
|
||||
return {
|
||||
uid: parseInt(user.uid),
|
||||
uuid: user.uuid || undefined,
|
||||
email: user.email || undefined,
|
||||
name: user.name || undefined,
|
||||
workspaceId: user.latest_workspace_id,
|
||||
iconUrl: user.icon_url || undefined,
|
||||
};
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCollab(workspaceId: string, object_id: string, collabType: CollabType) {
|
||||
const res = await client.get_collab({
|
||||
workspace_id: workspaceId,
|
||||
object_id: object_id,
|
||||
collab_type: Number(collabType) as 0 | 1 | 2 | 3 | 4 | 5,
|
||||
});
|
||||
|
||||
const state = new Uint8Array(res.doc_state);
|
||||
|
||||
return {
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
export async function batchGetCollab(
|
||||
workspaceId: string,
|
||||
params: {
|
||||
object_id: string;
|
||||
collab_type: CollabType;
|
||||
}[]
|
||||
) {
|
||||
const res = (await client.batch_get_collab(
|
||||
workspaceId,
|
||||
params.map((param) => ({
|
||||
object_id: param.object_id,
|
||||
collab_type: Number(param.collab_type) as 0 | 1 | 2 | 3 | 4 | 5,
|
||||
}))
|
||||
)) as unknown as Map<string, { doc_state: number[] }>;
|
||||
|
||||
const result: Record<string, number[]> = {};
|
||||
|
||||
res.forEach((value, key) => {
|
||||
result[key] = value.doc_state;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getUserWorkspace(): Promise<UserWorkspace> {
|
||||
const res = await client.get_user_workspace();
|
||||
|
||||
return {
|
||||
visitingWorkspaceId: res.visiting_workspace_id,
|
||||
workspaces: res.workspaces.map((workspace) => ({
|
||||
id: workspace.workspace_id,
|
||||
name: workspace.workspace_name,
|
||||
icon: workspace.icon,
|
||||
owner: {
|
||||
id: Number(workspace.owner_uid),
|
||||
name: workspace.owner_name,
|
||||
},
|
||||
type: workspace.workspace_type,
|
||||
workspaceDatabaseId: workspace.database_storage_id,
|
||||
})),
|
||||
};
|
||||
export async function getPublishViewMeta(publishNamespace: string, publishName: string) {
|
||||
return client.get_publish_view_meta(publishNamespace, publishName);
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
import { YDoc } from '@/application/collab.type';
|
||||
import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/user.type';
|
||||
import { ViewMeta } from '@/application/db/tables/view_metas';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export interface AFService {
|
||||
getDeviceID: () => string;
|
||||
getClientID: () => string;
|
||||
authService: AuthService;
|
||||
userService: UserService;
|
||||
documentService: DocumentService;
|
||||
folderService: FolderService;
|
||||
databaseService: DatabaseService;
|
||||
}
|
||||
export type AFService = PublishService;
|
||||
|
||||
export interface AFServiceConfig {
|
||||
cloudConfig: AFCloudConfig;
|
||||
@@ -22,35 +14,16 @@ export interface AFCloudConfig {
|
||||
wsURL: string;
|
||||
}
|
||||
|
||||
export interface AuthService {
|
||||
getOAuthURL: (provider: ProviderType) => Promise<string>;
|
||||
signInWithOAuth: (params: { uri: string }) => Promise<void>;
|
||||
signupWithEmailPassword: (params: SignUpWithEmailPasswordParams) => Promise<void>;
|
||||
signinWithEmailPassword: (email: string, password: string) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface DocumentService {
|
||||
openDocument: (docId: string) => Promise<YDoc>;
|
||||
}
|
||||
|
||||
export interface DatabaseService {
|
||||
getWorkspaceDatabases: () => Promise<{ views: string[]; database_id: string }[]>;
|
||||
openDatabase: (
|
||||
databaseId: string,
|
||||
rowIds?: string[]
|
||||
export interface PublishService {
|
||||
getPublishViewMeta: (namespace: string, publishName: string) => Promise<ViewMeta>;
|
||||
getPublishView: (namespace: string, publishName: string) => Promise<YDoc>;
|
||||
getPublishInfo: (viewId: string) => Promise<{ namespace: string; publishName: string }>;
|
||||
getPublishDatabaseViewRows: (
|
||||
namespace: string,
|
||||
publishName: string,
|
||||
rowIds: string[]
|
||||
) => Promise<{
|
||||
databaseDoc: YDoc;
|
||||
rows: Y.Map<YDoc>;
|
||||
destroy: () => void;
|
||||
}>;
|
||||
closeDatabase: (databaseId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface UserService {
|
||||
getUserProfile: () => Promise<UserProfile | null>;
|
||||
checkUser: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface FolderService {
|
||||
openWorkspace: (workspaceId: string) => Promise<YDoc>;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { AFCloudConfig, AuthService } from '@/application/services/services.type';
|
||||
import {
|
||||
AuthenticatorPB,
|
||||
OauthProviderPB,
|
||||
OauthSignInPB,
|
||||
SignInPayloadPB,
|
||||
SignUpPayloadPB,
|
||||
UserEventGetOauthURLWithProvider,
|
||||
UserEventOauthSignIn,
|
||||
UserEventSignInWithEmailPassword,
|
||||
UserEventSignOut,
|
||||
UserEventSignUp,
|
||||
UserProfilePB,
|
||||
} from './backend/events/flowy-user';
|
||||
import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/user.type';
|
||||
|
||||
export class TauriAuthService implements AuthService {
|
||||
|
||||
constructor (private cloudConfig: AFCloudConfig, private clientConfig: {
|
||||
deviceId: string;
|
||||
clientId: string;
|
||||
|
||||
}) {}
|
||||
|
||||
getDeviceID = (): string => {
|
||||
return this.clientConfig.deviceId;
|
||||
};
|
||||
getOAuthURL = async (provider: ProviderType): Promise<string> => {
|
||||
const providerDataRes = await UserEventGetOauthURLWithProvider(
|
||||
OauthProviderPB.fromObject({
|
||||
provider: provider as number,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!providerDataRes.ok) {
|
||||
throw new Error(providerDataRes.val.msg);
|
||||
}
|
||||
|
||||
const providerData = providerDataRes.val;
|
||||
|
||||
return providerData.oauth_url;
|
||||
};
|
||||
|
||||
signInWithOAuth = async ({ uri }: { uri: string }): Promise<void> => {
|
||||
const payload = OauthSignInPB.fromObject({
|
||||
authenticator: AuthenticatorPB.AppFlowyCloud,
|
||||
map: {
|
||||
sign_in_url: uri,
|
||||
device_id: this.getDeviceID(),
|
||||
},
|
||||
});
|
||||
|
||||
const res = await UserEventOauthSignIn(payload);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.val.msg);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
signinWithEmailPassword = async (email: string, password: string): Promise<void> => {
|
||||
const payload = SignInPayloadPB.fromObject({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
const res = await UserEventSignInWithEmailPassword(payload);
|
||||
|
||||
if (!res.ok) {
|
||||
return Promise.reject(res.val.msg);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
signupWithEmailPassword = async (params: SignUpWithEmailPasswordParams): Promise<void> => {
|
||||
const payload = SignUpPayloadPB.fromObject({
|
||||
name: params.name,
|
||||
email: params.email,
|
||||
password: params.password,
|
||||
device_id: this.getDeviceID(),
|
||||
});
|
||||
|
||||
const res = await UserEventSignUp(payload);
|
||||
|
||||
if (!res.ok) {
|
||||
return Promise.reject(res.val.msg);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
signOut = async () => {
|
||||
const res = await UserEventSignOut();
|
||||
|
||||
if (!res.ok) {
|
||||
return Promise.reject(res.val.msg);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
export function parseUserProfileFrom (userPB: UserProfilePB): UserProfile {
|
||||
const user = userPB.toObject();
|
||||
|
||||
return {
|
||||
uid: user.id as number,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
iconUrl: user.icon_url,
|
||||
workspaceId: user.workspace_id,
|
||||
};
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { YDoc } from '@/application/collab.type';
|
||||
import { DatabaseService } from '@/application/services/services.type';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export class TauriDatabaseService implements DatabaseService {
|
||||
constructor() {
|
||||
//
|
||||
}
|
||||
|
||||
async getWorkspaceDatabases(): Promise<{ views: string[]; database_id: string }[]> {
|
||||
return Promise.reject('Not implemented');
|
||||
}
|
||||
|
||||
async closeDatabase(_databaseId: string) {
|
||||
return Promise.reject('Not implemented');
|
||||
}
|
||||
|
||||
async openDatabase(_viewId: string): Promise<{
|
||||
databaseDoc: YDoc;
|
||||
rows: Y.Map<YDoc>;
|
||||
}> {
|
||||
return Promise.reject('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { DocumentService } from '@/application/services/services.type';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export class TauriDocumentService implements DocumentService {
|
||||
async openDocument(_id: string): Promise<Y.Doc> {
|
||||
return Promise.reject('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { YDoc } from '@/application/collab.type';
|
||||
import { FolderService } from '@/application/services/services.type';
|
||||
|
||||
export class TauriFolderService implements FolderService {
|
||||
constructor() {
|
||||
//
|
||||
}
|
||||
|
||||
async openWorkspace(_workspaceId: string): Promise<YDoc> {
|
||||
return Promise.reject('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,24 @@
|
||||
import {
|
||||
AFService,
|
||||
AFServiceConfig,
|
||||
AuthService,
|
||||
DatabaseService,
|
||||
DocumentService,
|
||||
FolderService,
|
||||
UserService,
|
||||
} from '@/application/services/services.type';
|
||||
import { TauriAuthService } from '@/application/services/tauri-services/auth.service';
|
||||
import { TauriDatabaseService } from '@/application/services/tauri-services/database.service';
|
||||
import { TauriFolderService } from '@/application/services/tauri-services/folder.service';
|
||||
import { TauriUserService } from '@/application/services/tauri-services/user.service';
|
||||
import { TauriDocumentService } from '@/application/services/tauri-services/document.service';
|
||||
import { AFService } from '@/application/services/services.type';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export class AFClientService implements AFService {
|
||||
authService: AuthService;
|
||||
|
||||
userService: UserService;
|
||||
|
||||
documentService: DocumentService;
|
||||
|
||||
folderService: FolderService;
|
||||
|
||||
databaseService: DatabaseService;
|
||||
|
||||
private deviceId: string = nanoid(8);
|
||||
|
||||
private clientId: string = 'web';
|
||||
private clientId: string = 'tauri';
|
||||
|
||||
getDeviceID = (): string => {
|
||||
return this.deviceId;
|
||||
};
|
||||
async getPublishView(_namespace: string, _publishName: string) {
|
||||
return Promise.reject('Method not implemented');
|
||||
}
|
||||
|
||||
getClientID = (): string => {
|
||||
return this.clientId;
|
||||
};
|
||||
async getPublishInfo(_viewId: string) {
|
||||
return Promise.reject('Method not implemented');
|
||||
}
|
||||
|
||||
constructor(config: AFServiceConfig) {
|
||||
this.authService = new TauriAuthService(config.cloudConfig, {
|
||||
deviceId: this.deviceId,
|
||||
clientId: this.clientId,
|
||||
});
|
||||
this.userService = new TauriUserService();
|
||||
this.documentService = new TauriDocumentService();
|
||||
this.folderService = new TauriFolderService();
|
||||
this.databaseService = new TauriDatabaseService();
|
||||
async getPublishViewMeta(_namespace: string, _publishName: string) {
|
||||
return Promise.reject('Method not implemented');
|
||||
}
|
||||
|
||||
async getPublishDatabaseViewRows(_namespace: string, _publishName: string, _rowIds: string[]) {
|
||||
return Promise.reject('Method not implemented');
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { UserService } from '@/application/services/services.type';
|
||||
import { UserProfile } from '@/application/user.type';
|
||||
import { UserEventGetUserProfile } from './backend/events/flowy-user';
|
||||
import { parseUserProfileFrom } from '@/application/services/tauri-services/auth.service';
|
||||
|
||||
export class TauriUserService implements UserService {
|
||||
async getUserProfile(): Promise<UserProfile | null> {
|
||||
const res = await UserEventGetUserProfile();
|
||||
|
||||
if (res.ok) {
|
||||
return parseUserProfileFrom(res.val);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async checkUser(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
export enum Authenticator {
|
||||
Local = 0,
|
||||
Supabase = 1,
|
||||
AppFlowyCloud = 2,
|
||||
}
|
||||
|
||||
export enum EncryptionType {
|
||||
NoEncryption = 0,
|
||||
Symmetric = 1,
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
uid: number;
|
||||
uuid?: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
iconUrl?: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export interface UserWorkspace {
|
||||
visitingWorkspaceId: string;
|
||||
workspaces: Workspace[];
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
owner: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
type: number;
|
||||
workspaceDatabaseId: string;
|
||||
}
|
||||
|
||||
export interface SignUpWithEmailPasswordParams {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export enum ProviderType {
|
||||
Apple = 0,
|
||||
Azure = 1,
|
||||
Bitbucket = 2,
|
||||
Discord = 3,
|
||||
Facebook = 4,
|
||||
Figma = 5,
|
||||
Github = 6,
|
||||
Gitlab = 7,
|
||||
Google = 8,
|
||||
Keycloak = 9,
|
||||
Kakao = 10,
|
||||
Linkedin = 11,
|
||||
Notion = 12,
|
||||
Spotify = 13,
|
||||
Slack = 14,
|
||||
Workos = 15,
|
||||
Twitch = 16,
|
||||
Twitter = 17,
|
||||
Email = 18,
|
||||
Phone = 19,
|
||||
Zoom = 20,
|
||||
}
|
||||
|
||||
export interface UserSetting {
|
||||
workspaceId: string;
|
||||
latestView?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
hasLatestView: boolean;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icons/ Arrow / right" opacity="0.5">
|
||||
<path id="Vector 15" d="M4.5 9.375L7.875 6L4.5 2.625" stroke="#171717" stroke-width="0.9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 291 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,23 +0,0 @@
|
||||
import { YFolder } from '@/application/collab.type';
|
||||
import { Crumb, FolderContext } from '@/application/folder-yjs';
|
||||
|
||||
export const FolderProvider: React.FC<{
|
||||
folder: YFolder | null;
|
||||
children?: React.ReactNode;
|
||||
onNavigateToView?: (viewId: string) => void;
|
||||
crumbs?: Crumb[];
|
||||
setCrumbs?: React.Dispatch<React.SetStateAction<Crumb[]>>;
|
||||
}> = ({ folder, children, onNavigateToView, crumbs, setCrumbs }) => {
|
||||
return (
|
||||
<FolderContext.Provider
|
||||
value={{
|
||||
folder,
|
||||
onNavigateToView,
|
||||
crumbs,
|
||||
setCrumbs,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FolderContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useContext, createContext } from 'react';
|
||||
|
||||
export const IdContext = createContext<IdProviderProps | null>(null);
|
||||
|
||||
interface IdProviderProps {
|
||||
objectId: string;
|
||||
}
|
||||
|
||||
export const IdProvider = ({ children, ...props }: IdProviderProps & { children: React.ReactNode }) => {
|
||||
return <IdContext.Provider value={props}>{children}</IdContext.Provider>;
|
||||
};
|
||||
|
||||
const defaultIdValue = {} as IdProviderProps;
|
||||
|
||||
export function useId() {
|
||||
return useContext(IdContext) || defaultIdValue;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { getCurrentWorkspace } from 'src/application/services/js-services/session';
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export function RecordNotFound({ open, title }: { open: boolean; title?: string }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Dialog open={open}>
|
||||
<DialogTitle>Oops.. something went wrong</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id='alert-dialog-description'>
|
||||
{title ? title : 'The record you are looking for does not exist.'}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions className={'flex w-full items-center justify-center'}>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const workspace = await getCurrentWorkspace();
|
||||
|
||||
if (!workspace) return;
|
||||
navigate(`/view/${workspace.id}`);
|
||||
}}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecordNotFound;
|
||||
@@ -1 +0,0 @@
|
||||
export * from './RecordNotFound';
|
||||
@@ -1,27 +1,14 @@
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const commonOptions = {
|
||||
style: {
|
||||
background: 'var(--bg-base)',
|
||||
color: 'var(--text-title)',
|
||||
shadows: 'var(--shadow)',
|
||||
},
|
||||
};
|
||||
|
||||
export const notify = {
|
||||
success: (message: string) => {
|
||||
toast.success(message, commonOptions);
|
||||
window.toast.success(message);
|
||||
},
|
||||
error: (message: string) => {
|
||||
toast.error(message, commonOptions);
|
||||
},
|
||||
loading: (message: string) => {
|
||||
toast.loading(message, commonOptions);
|
||||
window.toast.error(message);
|
||||
},
|
||||
info: (message: string) => {
|
||||
toast(message, commonOptions);
|
||||
window.toast.info(message);
|
||||
},
|
||||
clear: () => {
|
||||
toast.dismiss();
|
||||
window.toast.clear();
|
||||
},
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { YView } from '@/application/collab.type';
|
||||
import { usePageInfo } from '@/components/_shared/page/usePageInfo';
|
||||
import React from 'react';
|
||||
|
||||
export function Page({
|
||||
id,
|
||||
onClick,
|
||||
...props
|
||||
}: {
|
||||
id: string;
|
||||
onClick?: (view: YView) => void;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const { view, icon, name } = usePageInfo(id);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
onClick && view && onClick(view);
|
||||
}}
|
||||
className={'flex items-center justify-center gap-2 overflow-hidden'}
|
||||
{...props}
|
||||
>
|
||||
<div>{icon}</div>
|
||||
<div className={'flex-1 truncate'}>{name}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -1 +0,0 @@
|
||||
export * from './Page';
|
||||
@@ -1,92 +0,0 @@
|
||||
import { FontLayout, LineHeightLayout, ViewLayout, YjsFolderKey, YView } from '@/application/collab.type';
|
||||
import { useViewSelector } from '@/application/folder-yjs';
|
||||
import { CoverType } from '@/application/folder-yjs/folder.type';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg';
|
||||
import { ReactComponent as GridSvg } from '$icons/16x/grid.svg';
|
||||
import { ReactComponent as BoardSvg } from '$icons/16x/board.svg';
|
||||
import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface PageCover {
|
||||
type: CoverType;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PageExtra {
|
||||
cover: PageCover | null;
|
||||
fontLayout: FontLayout;
|
||||
lineHeightLayout: LineHeightLayout;
|
||||
font?: string;
|
||||
}
|
||||
|
||||
function parseExtra(extra: string): PageExtra {
|
||||
let extraObj;
|
||||
|
||||
try {
|
||||
extraObj = JSON.parse(extra);
|
||||
} catch (e) {
|
||||
extraObj = {};
|
||||
}
|
||||
|
||||
return {
|
||||
cover: extraObj.cover
|
||||
? {
|
||||
type: extraObj.cover.type,
|
||||
value: extraObj.cover.value,
|
||||
}
|
||||
: null,
|
||||
fontLayout: extraObj.font_layout || FontLayout.normal,
|
||||
lineHeightLayout: extraObj.line_height_layout || LineHeightLayout.normal,
|
||||
font: extraObj.font,
|
||||
};
|
||||
}
|
||||
|
||||
export function usePageInfo(id: string) {
|
||||
const { view } = useViewSelector(id);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const layout = view?.get(YjsFolderKey.layout);
|
||||
const icon = view?.get(YjsFolderKey.icon);
|
||||
const extra = view?.get(YjsFolderKey.extra);
|
||||
const name = view?.get(YjsFolderKey.name) || '';
|
||||
const iconObj = useMemo(() => {
|
||||
try {
|
||||
return JSON.parse(icon || '');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}, [icon]);
|
||||
|
||||
const extraObj = useMemo(() => {
|
||||
return parseExtra(extra || '');
|
||||
}, [extra]);
|
||||
|
||||
const defaultIcon = useMemo(() => {
|
||||
switch (parseInt(layout ?? '0')) {
|
||||
case ViewLayout.Document:
|
||||
return <DocumentSvg />;
|
||||
case ViewLayout.Grid:
|
||||
return <GridSvg />;
|
||||
case ViewLayout.Board:
|
||||
return <BoardSvg />;
|
||||
case ViewLayout.Calendar:
|
||||
return <CalendarSvg />;
|
||||
default:
|
||||
return <DocumentSvg />;
|
||||
}
|
||||
}, [layout]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!view);
|
||||
}, [view]);
|
||||
return {
|
||||
icon: iconObj?.value || defaultIcon,
|
||||
name: name || t('menuAppHeader.defaultNewPageName'),
|
||||
view: view as YView,
|
||||
loading,
|
||||
extra: extraObj,
|
||||
};
|
||||
}
|
||||
@@ -1,18 +1,12 @@
|
||||
import FolderPage from '@/pages/FolderPage';
|
||||
import PublishPage from '@/pages/PublishPage';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import ProtectedRoutes from '@/components/auth/ProtectedRoutes';
|
||||
import LoginPage from '@/pages/LoginPage';
|
||||
import ProductPage from '@/pages/ProductPage';
|
||||
import withAppWrapper from '@/components/app/withAppWrapper';
|
||||
import '@/styles/app.scss';
|
||||
|
||||
const AppMain = withAppWrapper(() => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path={'/'} element={<ProtectedRoutes />}>
|
||||
<Route path={'/view/:workspaceId'} element={<FolderPage />} />
|
||||
<Route path={'/view/:workspaceId/:objectId'} element={<ProductPage />} />
|
||||
</Route>
|
||||
<Route path={'/login'} element={<LoginPage />} />
|
||||
<Route path={'/:namespace/:publishName'} element={<PublishPage />} />
|
||||
</Routes>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
import { useAppLanguage } from '@/components/app/useAppLanguage';
|
||||
import React, { createContext, useEffect, useMemo, useState } from 'react';
|
||||
import { AFService } from '@/application/services/services.type';
|
||||
import React, { createContext, useEffect, useState } from 'react';
|
||||
import { AFService, AFServiceConfig } from '@/application/services/services.type';
|
||||
import { getService } from '@/application/services';
|
||||
import { useAppSelector } from '@/stores/store';
|
||||
|
||||
const defaultConfig: AFServiceConfig = {
|
||||
cloudConfig: {
|
||||
baseURL: import.meta.env.AF_BASE_URL
|
||||
? import.meta.env.AF_BASE_URL
|
||||
: import.meta.env.DEV
|
||||
? 'https://test.appflowy.cloud'
|
||||
: 'https://beta.appflowy.cloud',
|
||||
gotrueURL: import.meta.env.AF_GOTRUE_URL
|
||||
? import.meta.env.AF_GOTRUE_URL
|
||||
: import.meta.env.DEV
|
||||
? 'https://test.appflowy.cloud/gotrue'
|
||||
: 'https://beta.appflowy.cloud/gotrue',
|
||||
wsURL: import.meta.env.AF_WS_URL
|
||||
? import.meta.env.AF_WS_URL
|
||||
: import.meta.env.DEV
|
||||
? 'wss://test.appflowy.cloud/ws/v1'
|
||||
: 'wss://beta.appflowy.cloud/ws/v1',
|
||||
},
|
||||
};
|
||||
|
||||
export const AFConfigContext = createContext<
|
||||
| {
|
||||
@@ -12,7 +31,7 @@ export const AFConfigContext = createContext<
|
||||
>(undefined);
|
||||
|
||||
function AppConfig({ children }: { children: React.ReactNode }) {
|
||||
const appConfig = useAppSelector((state) => state.app.appConfig);
|
||||
const [appConfig] = useState<AFServiceConfig>(defaultConfig);
|
||||
const [service, setService] = useState<AFService>();
|
||||
|
||||
useAppLanguage();
|
||||
@@ -24,14 +43,15 @@ function AppConfig({ children }: { children: React.ReactNode }) {
|
||||
})();
|
||||
}, [appConfig]);
|
||||
|
||||
const config = useMemo(
|
||||
() => ({
|
||||
service,
|
||||
}),
|
||||
[service]
|
||||
return (
|
||||
<AFConfigContext.Provider
|
||||
value={{
|
||||
service,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AFConfigContext.Provider>
|
||||
);
|
||||
|
||||
return <AFConfigContext.Provider value={config}>{children}</AFConfigContext.Provider>;
|
||||
}
|
||||
|
||||
export default AppConfig;
|
||||
@@ -1,27 +1,52 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from 'src/stores/store';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { ErrorHandlerPage } from 'src/components/error/ErrorHandlerPage';
|
||||
import AppTheme from '@/components/app/AppTheme';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import AppConfig from '@/components/app/AppConfig';
|
||||
import { Suspense } from 'react';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
import { SnackbarProvider, useSnackbar } from 'notistack';
|
||||
|
||||
export default function withAppWrapper (Component: React.FC): React.FC {
|
||||
return function AppWrapper (): JSX.Element {
|
||||
export default function withAppWrapper(Component: React.FC): React.FC {
|
||||
return function AppWrapper(): JSX.Element {
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
window.toast = {
|
||||
success: (message: string) => {
|
||||
enqueueSnackbar(message, { variant: 'success' });
|
||||
},
|
||||
error: (message: string) => {
|
||||
enqueueSnackbar(message, { variant: 'error' });
|
||||
},
|
||||
warning: (message: string) => {
|
||||
enqueueSnackbar(message, { variant: 'warning' });
|
||||
},
|
||||
info: (message: string) => {
|
||||
enqueueSnackbar(message, { variant: 'info' });
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
closeSnackbar();
|
||||
},
|
||||
};
|
||||
}, [closeSnackbar, enqueueSnackbar]);
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<AppTheme>
|
||||
<ErrorBoundary FallbackComponent={ErrorHandlerPage}>
|
||||
<AppTheme>
|
||||
<ErrorBoundary FallbackComponent={ErrorHandlerPage}>
|
||||
<SnackbarProvider
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
preventDuplicate
|
||||
>
|
||||
<AppConfig>
|
||||
<Suspense>
|
||||
<Component />
|
||||
<Toaster />
|
||||
</Suspense>
|
||||
</AppConfig>
|
||||
</ErrorBoundary>
|
||||
</AppTheme>
|
||||
</Provider>
|
||||
</SnackbarProvider>
|
||||
</ErrorBoundary>
|
||||
</AppTheme>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import GoogleIcon from '@/assets/settings/google.png';
|
||||
import GithubIcon from '@/assets/settings/github.png';
|
||||
import DiscordIcon from '@/assets/settings/discord.png';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from './auth.hooks';
|
||||
import { ProviderType } from '@/application/user.type';
|
||||
import { useState } from 'react';
|
||||
import EmailOutlined from '@mui/icons-material/EmailOutlined';
|
||||
import SignInWithEmail from './SignInWithEmail';
|
||||
|
||||
export const LoginButtonGroup = () => {
|
||||
const { t } = useTranslation();
|
||||
const [openSignInWithEmail, setOpenSignInWithEmail] = useState(false);
|
||||
const { signInWithProvider } = useAuth();
|
||||
|
||||
return (
|
||||
<div className={'flex w-full flex-col items-center gap-4'}>
|
||||
<Button
|
||||
data-cy={'signInWithEmail'}
|
||||
onClick={() => {
|
||||
setOpenSignInWithEmail(true);
|
||||
}}
|
||||
className={'w-full rounded-lg border-text-title py-3 text-sm'}
|
||||
color={'inherit'}
|
||||
variant={'outlined'}
|
||||
>
|
||||
<EmailOutlined className={'mr-2 h-6 w-6'} />
|
||||
{t('signIn.signInWithEmail')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
onClick={() => {
|
||||
void signInWithProvider(ProviderType.Google);
|
||||
}}
|
||||
className={'w-full rounded-lg border-text-title py-3 text-sm'}
|
||||
color={'inherit'}
|
||||
variant={'outlined'}
|
||||
>
|
||||
<img src={GoogleIcon} alt={'Google'} className={'mr-2 h-6 w-6'} />
|
||||
{t('button.signInGoogle')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
onClick={() => {
|
||||
void signInWithProvider(ProviderType.Github);
|
||||
}}
|
||||
className={'w-full rounded-lg border-text-title py-3 text-sm'}
|
||||
color={'inherit'}
|
||||
variant={'outlined'}
|
||||
>
|
||||
<img src={GithubIcon} alt={'Github'} className={'mr-2 h-6 w-6'} />
|
||||
{t('button.signInGithub')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
onClick={() => {
|
||||
void signInWithProvider(ProviderType.Discord);
|
||||
}}
|
||||
className={'w-full rounded-lg border-text-title py-3 text-sm'}
|
||||
color={'inherit'}
|
||||
variant={'outlined'}
|
||||
>
|
||||
<img src={DiscordIcon} alt={'Discord'} className={'mr-2 h-6 w-6'} />
|
||||
{t('button.signInDiscord')}
|
||||
</Button>
|
||||
<SignInWithEmail open={openSignInWithEmail} onClose={() => setOpenSignInWithEmail(false)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, { lazy, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useAuth } from '@/components/auth/auth.hooks';
|
||||
import { currentUserActions, LoginState } from '@/stores/currentUser/slice';
|
||||
import { useAppDispatch } from '@/stores/store';
|
||||
import { getPlatform } from '@/utils/platform';
|
||||
import SplashScreen from '@/components/auth/SplashScreen';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Portal from '@mui/material/Portal';
|
||||
import { ReactComponent as Logo } from '@/assets/logo.svg';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const TauriAuth = lazy(() => import('@/components/tauri/TauriAuth'));
|
||||
|
||||
function ProtectedRoutes() {
|
||||
const { currentUser, checkUser, isReady } = useAuth();
|
||||
|
||||
const isLoading = currentUser?.loginState === LoginState.LOADING;
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const checkUserStatus = useCallback(async () => {
|
||||
if (!isReady) return;
|
||||
setChecked(false);
|
||||
try {
|
||||
if (!currentUser.isAuthenticated) {
|
||||
await checkUser();
|
||||
}
|
||||
} finally {
|
||||
setChecked(true);
|
||||
}
|
||||
}, [checkUser, isReady, currentUser.isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
void checkUserStatus();
|
||||
}, [checkUserStatus]);
|
||||
|
||||
const platform = useMemo(() => getPlatform(), []);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (checked && !currentUser.isAuthenticated && window.location.pathname !== '/login') {
|
||||
navigate(`/login?redirect=${encodeURIComponent(window.location.pathname)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (currentUser.user?.workspaceId && (window.location.pathname === '/' || window.location.pathname === '')) {
|
||||
navigate(`/view/${currentUser.user.workspaceId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'relative h-screen w-screen bg-bg-body'}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{checked ? (
|
||||
<SplashScreen />
|
||||
) : (
|
||||
<div className={'flex h-screen w-screen items-center justify-center'}>
|
||||
<Logo className={'h-20 w-20'} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && <StartLoading />}
|
||||
{platform.isTauri && <TauriAuth />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProtectedRoutes;
|
||||
|
||||
const StartLoading = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const preventDefault = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
dispatch(currentUserActions.resetLoginState());
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', preventDefault, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', preventDefault, true);
|
||||
};
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<Portal>
|
||||
<div className={'bg-bg-mask fixed inset-0 z-[1400] flex h-full w-full items-center justify-center bg-opacity-50'}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Button, CircularProgress, Dialog, DialogActions, DialogContent, TextField } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '@/components/auth/auth.hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function SignInWithEmail({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const { signInWithEmailPassword } = useAuth();
|
||||
|
||||
const handleSignIn = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await signInWithEmailPassword(email, password);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{
|
||||
zIndex: 1500,
|
||||
}}
|
||||
data-cy={'signInWithEmailDialog'}
|
||||
PaperProps={{
|
||||
className: 'w-[400px]',
|
||||
}}
|
||||
keepMounted={false}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void handleSignIn();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className={'mt-2 flex w-full flex-col gap-3'}>
|
||||
<TextField
|
||||
label={'Email'}
|
||||
size={'small'}
|
||||
data-cy={'email'}
|
||||
required={true}
|
||||
placeholder={'name@gmail.com'}
|
||||
type={'email'}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
size={'small'}
|
||||
data-cy={'password'}
|
||||
required={true}
|
||||
label={'Password'}
|
||||
placeholder={'Password'}
|
||||
type={'password'}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions className={'mb-4 flex w-full gap-2 px-6'}>
|
||||
<Button variant={'outlined'} className={'flex-1'} color={'inherit'} onClick={onClose}>
|
||||
{t('button.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
data-cy={'submit'}
|
||||
disabled={loading}
|
||||
className={'justify-content flex h-[33px] flex-1 items-center gap-2'}
|
||||
variant={'contained'}
|
||||
onClick={handleSignIn}
|
||||
>
|
||||
{loading && <CircularProgress size={20} />}
|
||||
{t('button.signIn')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignInWithEmail;
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import Layout from '@/components/layout/Layout';
|
||||
|
||||
function SplashScreen () {
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Outlet/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default SplashScreen;
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import Welcome from './Welcome';
|
||||
import withAppWrapper from '@/components/app/withAppWrapper';
|
||||
|
||||
describe('<Welcome />', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockAPI();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const AppWrapper = withAppWrapper(Welcome);
|
||||
|
||||
cy.mount(<AppWrapper />);
|
||||
});
|
||||
|
||||
it('should handle login success', () => {
|
||||
const AppWrapper = withAppWrapper(Welcome);
|
||||
|
||||
cy.mount(<AppWrapper />);
|
||||
|
||||
cy.get('[data-cy=signInWithEmail]').click();
|
||||
|
||||
cy.wait(100);
|
||||
|
||||
cy.get('[data-cy=signInWithEmailDialog]').as('dialog').should('be.visible');
|
||||
cy.get('[data-cy=email]').type('fakeEmail123');
|
||||
cy.get('[data-cy=password]').type('fakePassword123');
|
||||
cy.get('[data-cy=submit]').click();
|
||||
cy.wait('@loginSuccess');
|
||||
cy.wait('@verifyToken');
|
||||
cy.wait('@getUserProfile');
|
||||
cy.wait('@getUserWorkspace');
|
||||
cy.get('@dialog').should('not.exist');
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import { ReactComponent as AppflowyLogo } from '@/assets/logo.svg';
|
||||
import { Stack } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoginButtonGroup } from './LoginButtonGroup';
|
||||
import { getPlatform } from '@/utils/platform';
|
||||
import { lazy } from 'react';
|
||||
|
||||
const SignInAsAnonymous = lazy(() => import('@/components/tauri/SignInAsAnonymous'));
|
||||
|
||||
export const Welcome = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={(e) => e.preventDefault()} method='POST'>
|
||||
<Stack className='relative flex h-screen w-screen flex-col items-center justify-center gap-12 bg-bg-body text-center text-text-title'>
|
||||
<div className='flex justify-center' id='appflowy'>
|
||||
<AppflowyLogo className={'h-16 w-16'} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className='text-2xl font-semibold leading-9'>
|
||||
{t('welcomeTo')} {t('appName')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id='Get-Started' className='flex w-[340px] flex-col gap-4 ' aria-label='Get-Started'>
|
||||
{getPlatform().isTauri && <SignInAsAnonymous />}
|
||||
<div className={'w-w-full flex items-center justify-center gap-2 text-sm'}>
|
||||
<LoginButtonGroup />
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Welcome;
|
||||
@@ -1,192 +0,0 @@
|
||||
import { useAppDispatch, useAppSelector } from '@/stores/store';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import { ProviderType, UserProfile } from '@/application/user.type';
|
||||
import { currentUserActions } from '@/stores/currentUser/slice';
|
||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
import { notify } from '@/components/_shared/notify';
|
||||
|
||||
export const useAuth = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const AFConfig = useContext(AFConfigContext);
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
const isReady = !!AFConfig?.service;
|
||||
|
||||
const handleSuccess = useCallback(() => {
|
||||
notify.clear();
|
||||
dispatch(currentUserActions.loginSuccess());
|
||||
}, [dispatch]);
|
||||
|
||||
const setUser = useCallback(
|
||||
async (userProfile: UserProfile) => {
|
||||
handleSuccess();
|
||||
dispatch(currentUserActions.updateUser(userProfile));
|
||||
},
|
||||
[dispatch, handleSuccess]
|
||||
);
|
||||
|
||||
const handleStart = useCallback(() => {
|
||||
notify.clear();
|
||||
notify.loading('Loading...');
|
||||
dispatch(currentUserActions.loginStart());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleError = useCallback(
|
||||
({ message }: { message: string }) => {
|
||||
notify.clear();
|
||||
notify.error(message);
|
||||
dispatch(currentUserActions.loginError());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// Check if the user is authenticated
|
||||
const checkUser = useCallback(async () => {
|
||||
try {
|
||||
const userHasSignIn = await AFConfig?.service?.userService.checkUser();
|
||||
|
||||
if (!userHasSignIn) {
|
||||
throw new Error('Failed to check user');
|
||||
}
|
||||
|
||||
const userProfile = await AFConfig?.service?.userService.getUserProfile();
|
||||
|
||||
if (!userProfile) {
|
||||
throw new Error('Failed to check user');
|
||||
}
|
||||
|
||||
console.log('userProfile', userProfile);
|
||||
await setUser(userProfile);
|
||||
|
||||
return userProfile;
|
||||
} catch (e) {
|
||||
return Promise.reject('Failed to check user');
|
||||
}
|
||||
}, [AFConfig?.service?.userService, setUser]);
|
||||
|
||||
const register = useCallback(
|
||||
async (email: string, password: string, name: string): Promise<UserProfile | null> => {
|
||||
handleStart();
|
||||
try {
|
||||
const userProfile = await AFConfig?.service?.authService.signupWithEmailPassword({
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
});
|
||||
|
||||
if (!userProfile) {
|
||||
throw new Error('Failed to register');
|
||||
}
|
||||
|
||||
await setUser(userProfile);
|
||||
|
||||
return userProfile;
|
||||
} catch (e) {
|
||||
handleError({
|
||||
message: 'Failed to register',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[handleStart, AFConfig?.service?.authService, setUser, handleError]
|
||||
);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await AFConfig?.service?.authService.signOut();
|
||||
dispatch(currentUserActions.logout());
|
||||
} catch (e) {
|
||||
handleError({
|
||||
message: 'Failed to logout',
|
||||
});
|
||||
}
|
||||
}, [AFConfig?.service?.authService, dispatch, handleError]);
|
||||
|
||||
const signInAsAnonymous = useCallback(async () => {
|
||||
const fakeEmail = nanoid(8) + '@appflowy.io';
|
||||
const fakePassword = 'AppFlowy123@';
|
||||
const fakeName = 'Me';
|
||||
|
||||
await register(fakeEmail, fakePassword, fakeName);
|
||||
}, [register]);
|
||||
|
||||
const signInWithProvider = useCallback(
|
||||
async (provider: ProviderType) => {
|
||||
handleStart();
|
||||
try {
|
||||
const url = await AFConfig?.service?.authService.getOAuthURL(provider);
|
||||
|
||||
if (!url) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
await open(url);
|
||||
} catch {
|
||||
handleError({
|
||||
message: 'Failed to sign in',
|
||||
});
|
||||
}
|
||||
},
|
||||
[AFConfig?.service?.authService, handleError, handleStart]
|
||||
);
|
||||
|
||||
const signInWithOAuth = useCallback(
|
||||
async (uri: string) => {
|
||||
handleStart();
|
||||
try {
|
||||
await AFConfig?.service?.authService.signInWithOAuth({ uri });
|
||||
const userProfile = await AFConfig?.service?.userService.getUserProfile();
|
||||
|
||||
if (!userProfile) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
await setUser(userProfile);
|
||||
|
||||
return userProfile;
|
||||
} catch (e) {
|
||||
handleError({
|
||||
message: 'Failed to sign in',
|
||||
});
|
||||
}
|
||||
},
|
||||
[AFConfig?.service?.authService, AFConfig?.service?.userService, handleError, handleStart, setUser]
|
||||
);
|
||||
|
||||
const signInWithEmailPassword = useCallback(
|
||||
async (email: string, password: string) => {
|
||||
handleStart();
|
||||
try {
|
||||
await AFConfig?.service?.authService.signinWithEmailPassword(email, password);
|
||||
|
||||
const userProfile = await AFConfig?.service?.userService.getUserProfile();
|
||||
|
||||
if (!userProfile) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
await setUser(userProfile);
|
||||
|
||||
return userProfile;
|
||||
} catch (e) {
|
||||
handleError({
|
||||
message: 'Failed to sign in',
|
||||
});
|
||||
}
|
||||
},
|
||||
[AFConfig?.service?.authService, AFConfig?.service?.userService, handleError, handleStart, setUser]
|
||||
);
|
||||
|
||||
return {
|
||||
isReady,
|
||||
currentUser,
|
||||
checkUser,
|
||||
register,
|
||||
logout,
|
||||
signInWithProvider,
|
||||
signInAsAnonymous,
|
||||
signInWithOAuth,
|
||||
signInWithEmailPassword,
|
||||
};
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import { YDoc, YjsEditorKey } from '@/application/collab.type';
|
||||
import { DatabaseContextState } from '@/application/database-yjs';
|
||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
import { Log } from '@/utils/log';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
export function useGetDatabaseId(iidIndex: string) {
|
||||
const [databaseId, setDatabaseId] = useState<string>();
|
||||
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
||||
|
||||
const loadDatabaseId = useCallback(async () => {
|
||||
if (!databaseService) return;
|
||||
const databases = await databaseService.getWorkspaceDatabases();
|
||||
|
||||
console.log('databses', databases);
|
||||
const id = databases.find((item) => item.views.includes(iidIndex))?.database_id;
|
||||
|
||||
if (!id) return;
|
||||
setDatabaseId(id);
|
||||
}, [iidIndex, databaseService]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadDatabaseId();
|
||||
}, [loadDatabaseId]);
|
||||
return databaseId;
|
||||
}
|
||||
|
||||
export function useGetDatabaseDispatch() {
|
||||
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
||||
const onOpenDatabase = useCallback(
|
||||
async ({ databaseId, rowIds }: { databaseId: string; rowIds?: string[] }) => {
|
||||
if (!databaseService) return Promise.reject();
|
||||
return databaseService.openDatabase(databaseId, rowIds);
|
||||
},
|
||||
[databaseService]
|
||||
);
|
||||
|
||||
const onCloseDatabase = useCallback(
|
||||
(databaseId: string) => {
|
||||
if (!databaseService) return;
|
||||
void databaseService.closeDatabase(databaseId);
|
||||
},
|
||||
[databaseService]
|
||||
);
|
||||
|
||||
return {
|
||||
onOpenDatabase,
|
||||
onCloseDatabase,
|
||||
};
|
||||
}
|
||||
|
||||
export function useLoadDatabase({ databaseId, rowIds }: { databaseId?: string; rowIds?: string[] }) {
|
||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
||||
const [rows, setRows] = useState<DatabaseContextState['rowDocMap'] | null>(null); // Map<rowId, YDoc
|
||||
const [notFound, setNotFound] = useState<boolean>(false);
|
||||
const { onOpenDatabase, onCloseDatabase } = useGetDatabaseDispatch();
|
||||
|
||||
const handleOpenDatabase = useCallback(
|
||||
async (databaseId: string, rowIds?: string[]) => {
|
||||
try {
|
||||
setDoc(null);
|
||||
const { databaseDoc, rows } = await onOpenDatabase({
|
||||
databaseId,
|
||||
rowIds,
|
||||
});
|
||||
|
||||
console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON());
|
||||
console.log('rows', rows);
|
||||
|
||||
setDoc(databaseDoc);
|
||||
setRows(rows);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
setNotFound(true);
|
||||
}
|
||||
},
|
||||
[onOpenDatabase]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!databaseId) return;
|
||||
void handleOpenDatabase(databaseId, rowIds);
|
||||
return () => {
|
||||
onCloseDatabase(databaseId);
|
||||
};
|
||||
}, [handleOpenDatabase, databaseId, rowIds, onCloseDatabase]);
|
||||
|
||||
return { doc, rows, notFound };
|
||||
}
|
||||
@@ -1,23 +1,103 @@
|
||||
import { YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||
import { ViewMeta } from '@/application/db/tables/view_metas';
|
||||
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
|
||||
import DatabaseRow from '@/components/database/DatabaseRow';
|
||||
import DatabaseViews from '@/components/database/DatabaseViews';
|
||||
import { ViewMetaPreview, ViewMetaProps } from '@/components/view-meta/ViewMetaPreview';
|
||||
import React, { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import * as Y from 'yjs';
|
||||
import { DatabaseContextProvider } from './DatabaseContext';
|
||||
|
||||
import React, { memo } from 'react';
|
||||
export interface Database2Props extends ViewMetaProps {
|
||||
doc: YDoc;
|
||||
getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>;
|
||||
loadView?: (viewId: string) => Promise<YDoc>;
|
||||
navigateToView?: (viewId: string) => Promise<void>;
|
||||
loadViewMeta?: (viewId: string) => Promise<ViewMeta>;
|
||||
}
|
||||
|
||||
export const Database = memo(
|
||||
({
|
||||
viewId,
|
||||
onNavigateToView,
|
||||
iidIndex,
|
||||
}: {
|
||||
iidIndex: string;
|
||||
viewId: string;
|
||||
onNavigateToView: (viewId: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
|
||||
<DatabaseViews iidIndex={iidIndex} onChangeView={onNavigateToView} viewId={viewId} />
|
||||
</div>
|
||||
);
|
||||
function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView, ...viewMeta }: Database2Props) {
|
||||
const [search, setSearch] = useSearchParams();
|
||||
|
||||
const viewId = search.get('v') || viewMeta.viewId;
|
||||
|
||||
const rowIds = useMemo(() => {
|
||||
if (!viewId) return [];
|
||||
const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase;
|
||||
const rows = database.get(YjsDatabaseKey.views).get(viewId).get(YjsDatabaseKey.row_orders);
|
||||
|
||||
return rows.toArray().map((row) => row.get(YjsDatabaseKey.id));
|
||||
}, [doc, viewId]);
|
||||
|
||||
const iidIndex = useMemo(() => {
|
||||
const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase;
|
||||
|
||||
return database.get(YjsDatabaseKey.metas).get(YjsDatabaseKey.iid);
|
||||
}, [doc]);
|
||||
|
||||
const [rowDocMap, setRowDocMap] = useState<Y.Map<YDoc> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!getViewRowsMap || !rowIds.length || !iidIndex) return;
|
||||
|
||||
void (async () => {
|
||||
const { rows, destroy } = await getViewRowsMap(iidIndex, rowIds);
|
||||
|
||||
setRowDocMap(rows);
|
||||
return destroy;
|
||||
})();
|
||||
}, [getViewRowsMap, rowIds, iidIndex]);
|
||||
|
||||
const rowId = search.get('r');
|
||||
|
||||
const handleChangeView = useCallback(
|
||||
(viewId: string) => {
|
||||
setSearch({ v: viewId });
|
||||
},
|
||||
[setSearch]
|
||||
);
|
||||
|
||||
const handleNavigateToRow = useCallback(
|
||||
(rowId: string) => {
|
||||
setSearch({ r: rowId });
|
||||
},
|
||||
[setSearch]
|
||||
);
|
||||
|
||||
if (!rowDocMap || !viewId) {
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'flex w-full justify-center'}>
|
||||
<Suspense fallback={<ComponentLoading />}>
|
||||
<DatabaseContextProvider
|
||||
isDatabaseRowPage={!!rowId}
|
||||
navigateToRow={handleNavigateToRow}
|
||||
viewId={viewId}
|
||||
databaseDoc={doc}
|
||||
rowDocMap={rowDocMap}
|
||||
readOnly={true}
|
||||
loadView={loadView}
|
||||
navigateToView={navigateToView}
|
||||
loadViewMeta={loadViewMeta}
|
||||
>
|
||||
{rowId ? (
|
||||
<DatabaseRow rowId={rowId} />
|
||||
) : (
|
||||
<div className={'relative flex h-full w-full flex-col'}>
|
||||
{viewMeta && <ViewMetaPreview {...viewMeta} />}
|
||||
|
||||
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
|
||||
<DatabaseViews iidIndex={iidIndex} onChangeView={handleChangeView} viewId={viewId} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DatabaseContextProvider>
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Database;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { usePageInfo } from '@/components/_shared/page/usePageInfo';
|
||||
import React from 'react';
|
||||
|
||||
function DatabaseTitle({ viewId }: { viewId: string }) {
|
||||
const { name, icon } = usePageInfo(viewId);
|
||||
|
||||
return (
|
||||
<div className={'flex w-full flex-col py-4'}>
|
||||
<div className={'flex w-full items-center px-16 max-md:px-4'}>
|
||||
<div className={'flex items-center gap-2 text-3xl'}>
|
||||
<div>{icon}</div>
|
||||
<div className={'font-bold'}>{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DatabaseTitle;
|
||||
@@ -1,11 +1,10 @@
|
||||
import { renderDatabase } from '@/components/database/__tests__/withTestingDatabase';
|
||||
import '@/components/layout/layout.scss';
|
||||
import '@/styles/app.scss';
|
||||
|
||||
describe('<Database />', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport(1280, 720);
|
||||
Object.defineProperty(window.navigator, 'language', { value: 'en-US' });
|
||||
cy.mockDatabase();
|
||||
});
|
||||
|
||||
it('renders with a database', () => {
|
||||
@@ -18,7 +17,7 @@ describe('<Database />', () => {
|
||||
onNavigateToView,
|
||||
},
|
||||
() => {
|
||||
cy.get('[data-testid^=view-tab-]').should('have.length', 4);
|
||||
cy.get('[data-testid^=view-tab-]').should('have.length', 10);
|
||||
cy.get('.database-grid').should('exist');
|
||||
|
||||
cy.get('[data-testid=view-tab-e410747b-5f2f-45a0-b2f7-890ad3001355]').click();
|
||||
@@ -27,11 +26,13 @@ describe('<Database />', () => {
|
||||
|
||||
cy.wait(800);
|
||||
cy.get('[data-testid=view-tab-7d2148fc-cace-4452-9c5c-96e52e6bf8b5]').click();
|
||||
cy.wait(800);
|
||||
cy.get('.database-grid').should('exist');
|
||||
cy.wrap(onNavigateToView).should('have.been.calledWith', '7d2148fc-cace-4452-9c5c-96e52e6bf8b5');
|
||||
|
||||
cy.wait(800);
|
||||
cy.get('[data-testid=view-tab-2143e95d-5dcb-4e0f-bb2c-50944e6e019f]').click();
|
||||
cy.wait(800);
|
||||
cy.get('.database-calendar').should('exist');
|
||||
cy.wrap(onNavigateToView).should('have.been.calledWith', '2143e95d-5dcb-4e0f-bb2c-50944e6e019f');
|
||||
}
|
||||
|
||||
@@ -1,46 +1,39 @@
|
||||
import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type';
|
||||
import { YDoc } from '@/application/collab.type';
|
||||
import { applyYDoc } from '@/application/ydoc/apply';
|
||||
import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider';
|
||||
import { IdProvider } from '@/components/_shared/context-provider/IdProvider';
|
||||
import withAppWrapper from '@/components/app/withAppWrapper';
|
||||
import { DatabaseRow } from 'src/components/database/DatabaseRow';
|
||||
import { DatabaseContextProvider } from 'src/components/database/DatabaseContext';
|
||||
import * as Y from 'yjs';
|
||||
import '@/components/layout/layout.scss';
|
||||
import '@/styles/app.scss';
|
||||
|
||||
describe('<DatabaseRow />', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport(1280, 720);
|
||||
Object.defineProperty(window.navigator, 'language', { value: 'en-US' });
|
||||
Object.defineProperty(window.navigator, 'languages', { value: ['en-US'] });
|
||||
cy.mockDatabase();
|
||||
cy.mockDocument('f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0');
|
||||
});
|
||||
|
||||
it('renders with a row', () => {
|
||||
cy.wait(1000);
|
||||
cy.fixture('folder').then((folderJson) => {
|
||||
Object.defineProperty(window.navigator, 'language', { value: 'en-US' });
|
||||
cy.fixture('database/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((database) => {
|
||||
const doc = new Y.Doc();
|
||||
const state = new Uint8Array(folderJson.data.doc_state);
|
||||
const databaseState = new Uint8Array(database.data.doc_state);
|
||||
|
||||
applyYDoc(doc, state);
|
||||
const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder;
|
||||
applyYDoc(doc, databaseState);
|
||||
|
||||
cy.fixture('database/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((database) => {
|
||||
const doc = new Y.Doc();
|
||||
const databaseState = new Uint8Array(database.data.doc_state);
|
||||
cy.fixture('database/rows/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((rows) => {
|
||||
const rootRowsDoc = new Y.Doc();
|
||||
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||
const data = rows['2f944220-9f45-40d9-96b5-e8c0888daf7c'];
|
||||
const rowDoc = new Y.Doc();
|
||||
|
||||
applyYDoc(doc, databaseState);
|
||||
applyYDoc(rowDoc, new Uint8Array(data));
|
||||
rowsFolder.set('2f944220-9f45-40d9-96b5-e8c0888daf7c', rowDoc);
|
||||
|
||||
cy.fixture('database/rows/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((rows) => {
|
||||
const rootRowsDoc = new Y.Doc();
|
||||
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||
const data = rows['2f944220-9f45-40d9-96b5-e8c0888daf7c'];
|
||||
const rowDoc = new Y.Doc();
|
||||
|
||||
applyYDoc(rowDoc, new Uint8Array(data));
|
||||
rowsFolder.set('2f944220-9f45-40d9-96b5-e8c0888daf7c', rowDoc);
|
||||
cy.fixture('simple_doc').then((docJson) => {
|
||||
const subDoc = new Y.Doc();
|
||||
const state = new Uint8Array(docJson.data.doc_state);
|
||||
|
||||
applyYDoc(subDoc, state);
|
||||
const AppWrapper = withAppWrapper(() => {
|
||||
return (
|
||||
<div className={'flex h-screen w-screen flex-col overflow-y-auto py-4'}>
|
||||
@@ -48,8 +41,8 @@ describe('<DatabaseRow />', () => {
|
||||
rowId={'2f944220-9f45-40d9-96b5-e8c0888daf7c'}
|
||||
databaseDoc={doc}
|
||||
rows={rowsFolder}
|
||||
folder={folder}
|
||||
viewId={'7d2148fc-cace-4452-9c5c-96e52e6bf8b5'}
|
||||
loadView={() => Promise.resolve(subDoc)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -70,28 +63,25 @@ function TestDatabaseRow({
|
||||
rowId,
|
||||
databaseDoc,
|
||||
rows,
|
||||
folder,
|
||||
viewId,
|
||||
loadView,
|
||||
}: {
|
||||
rowId: string;
|
||||
databaseDoc: YDoc;
|
||||
rows: Y.Map<YDoc>;
|
||||
folder: YFolder;
|
||||
viewId: string;
|
||||
loadView?: (viewId: string) => Promise<YDoc>;
|
||||
}) {
|
||||
return (
|
||||
<FolderProvider folder={folder}>
|
||||
<IdProvider objectId={viewId}>
|
||||
<DatabaseContextProvider
|
||||
viewId={viewId}
|
||||
readOnly={true}
|
||||
isDatabaseRowPage
|
||||
databaseDoc={databaseDoc}
|
||||
rowDocMap={rows}
|
||||
>
|
||||
<DatabaseRow rowId={rowId} />
|
||||
</DatabaseContextProvider>
|
||||
</IdProvider>
|
||||
</FolderProvider>
|
||||
<DatabaseContextProvider
|
||||
viewId={viewId}
|
||||
readOnly={true}
|
||||
isDatabaseRowPage
|
||||
databaseDoc={databaseDoc}
|
||||
rowDocMap={rows}
|
||||
loadView={loadView}
|
||||
>
|
||||
<DatabaseRow rowId={rowId} />
|
||||
</DatabaseContextProvider>
|
||||
);
|
||||
}
|
||||
+1
-2
@@ -1,11 +1,10 @@
|
||||
import { renderDatabase } from '@/components/database/__tests__/withTestingDatabase';
|
||||
import '@/components/layout/layout.scss';
|
||||
import '@/styles/app.scss';
|
||||
|
||||
describe('<Database /> with filters and sorts', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport(1280, 720);
|
||||
Object.defineProperty(window.navigator, 'language', { value: 'en-US' });
|
||||
cy.mockDatabase();
|
||||
});
|
||||
|
||||
it('render a database with filters and sorts', () => {
|
||||
|
||||
+39
-57
@@ -1,12 +1,10 @@
|
||||
import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type';
|
||||
import { YDoc } from '@/application/collab.type';
|
||||
import { applyYDoc } from '@/application/ydoc/apply';
|
||||
import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider';
|
||||
import { IdProvider } from '@/components/_shared/context-provider/IdProvider';
|
||||
import withAppWrapper from '@/components/app/withAppWrapper';
|
||||
import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
|
||||
import DatabaseViews from '@/components/database/DatabaseViews';
|
||||
import { useState } from 'react';
|
||||
import * as Y from 'yjs';
|
||||
import { Database } from 'src/components/database/Database';
|
||||
|
||||
export function renderDatabase(
|
||||
{
|
||||
@@ -20,49 +18,39 @@ export function renderDatabase(
|
||||
},
|
||||
onAfterRender?: () => void
|
||||
) {
|
||||
cy.fixture('folder').then((folderJson) => {
|
||||
const doc = new Y.Doc();
|
||||
const state = new Uint8Array(folderJson.data.doc_state);
|
||||
cy.fixture(`database/${databaseId}`).then((database) => {
|
||||
cy.fixture(`database/rows/${databaseId}`).then((rows) => {
|
||||
const doc = new Y.Doc();
|
||||
const rootRowsDoc = new Y.Doc();
|
||||
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||
const databaseState = new Uint8Array(database.data.doc_state);
|
||||
|
||||
applyYDoc(doc, state);
|
||||
applyYDoc(doc, databaseState);
|
||||
|
||||
const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder;
|
||||
Object.keys(rows).forEach((key) => {
|
||||
const data = rows[key];
|
||||
const rowDoc = new Y.Doc();
|
||||
|
||||
cy.fixture(`database/${databaseId}`).then((database) => {
|
||||
cy.fixture(`database/rows/${databaseId}`).then((rows) => {
|
||||
const doc = new Y.Doc();
|
||||
const rootRowsDoc = new Y.Doc();
|
||||
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||
const databaseState = new Uint8Array(database.data.doc_state);
|
||||
|
||||
applyYDoc(doc, databaseState);
|
||||
|
||||
Object.keys(rows).forEach((key) => {
|
||||
const data = rows[key];
|
||||
const rowDoc = new Y.Doc();
|
||||
|
||||
applyYDoc(rowDoc, new Uint8Array(data));
|
||||
rowsFolder.set(key, rowDoc);
|
||||
});
|
||||
|
||||
const AppWrapper = withAppWrapper(() => {
|
||||
return (
|
||||
<div className={'flex h-screen w-screen flex-col py-4'}>
|
||||
<TestDatabase
|
||||
databaseDoc={doc}
|
||||
rows={rowsFolder}
|
||||
folder={folder}
|
||||
iidIndex={viewId}
|
||||
initialViewId={viewId}
|
||||
onNavigateToView={onNavigateToView}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
cy.mount(<AppWrapper />);
|
||||
onAfterRender?.();
|
||||
applyYDoc(rowDoc, new Uint8Array(data));
|
||||
rowsFolder.set(key, rowDoc);
|
||||
});
|
||||
|
||||
const AppWrapper = withAppWrapper(() => {
|
||||
return (
|
||||
<div className={'flex h-screen w-screen flex-col py-4'}>
|
||||
<TestDatabase
|
||||
databaseDoc={doc}
|
||||
rows={rowsFolder}
|
||||
iidIndex={viewId}
|
||||
initialViewId={viewId}
|
||||
onNavigateToView={onNavigateToView}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
cy.mount(<AppWrapper />);
|
||||
onAfterRender?.();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -70,14 +58,12 @@ export function renderDatabase(
|
||||
export function TestDatabase({
|
||||
databaseDoc,
|
||||
rows,
|
||||
folder,
|
||||
iidIndex,
|
||||
initialViewId,
|
||||
onNavigateToView,
|
||||
}: {
|
||||
databaseDoc: YDoc;
|
||||
rows: Y.Map<YDoc>;
|
||||
folder: YFolder;
|
||||
iidIndex: string;
|
||||
initialViewId: string;
|
||||
onNavigateToView: (viewId: string) => void;
|
||||
@@ -90,17 +76,13 @@ export function TestDatabase({
|
||||
};
|
||||
|
||||
return (
|
||||
<FolderProvider folder={folder}>
|
||||
<IdProvider objectId={iidIndex}>
|
||||
<DatabaseContextProvider
|
||||
viewId={activeViewId || iidIndex}
|
||||
databaseDoc={databaseDoc}
|
||||
rowDocMap={rows}
|
||||
readOnly={true}
|
||||
>
|
||||
<Database iidIndex={iidIndex} viewId={activeViewId} onNavigateToView={handleNavigateToView} />
|
||||
</DatabaseContextProvider>
|
||||
</IdProvider>
|
||||
</FolderProvider>
|
||||
<DatabaseContextProvider
|
||||
viewId={activeViewId || iidIndex}
|
||||
databaseDoc={databaseDoc}
|
||||
rowDocMap={rows}
|
||||
readOnly={true}
|
||||
>
|
||||
<DatabaseViews iidIndex={iidIndex} viewId={activeViewId} onChangeView={handleNavigateToView} />
|
||||
</DatabaseContextProvider>
|
||||
);
|
||||
}
|
||||
+12
-40
@@ -1,54 +1,28 @@
|
||||
import { YDatabaseField, YDatabaseFields, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||
import {
|
||||
DatabaseContextState,
|
||||
parseRelationTypeOption,
|
||||
useDatabase,
|
||||
useFieldSelector,
|
||||
useNavigateToRow,
|
||||
} from '@/application/database-yjs';
|
||||
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { DatabaseContext, DatabaseContextState, useDatabase, useNavigateToRow } from '@/application/database-yjs';
|
||||
import { RelationCell, RelationCellData } from '@/application/database-yjs/cell.type';
|
||||
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
|
||||
import { useGetDatabaseDispatch } from '@/components/database/Database.hooks';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
|
||||
const { field } = useFieldSelector(fieldId);
|
||||
const currentDatabaseId = useDatabase()?.get(YjsDatabaseKey.id);
|
||||
const { onOpenDatabase, onCloseDatabase } = useGetDatabaseDispatch();
|
||||
function RelationItems({ style, cell }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
|
||||
const database = useDatabase();
|
||||
const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString();
|
||||
const rowIds = useMemo(() => {
|
||||
return (cell.data?.toJSON() as RelationCellData) ?? [];
|
||||
}, [cell.data]);
|
||||
const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined;
|
||||
const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState<string | undefined>(undefined);
|
||||
const getViewRowsMap = useContext(DatabaseContext)?.getViewRowsMap;
|
||||
|
||||
const [rows, setRows] = useState<DatabaseContextState['rowDocMap'] | null>();
|
||||
|
||||
const navigateToRow = useNavigateToRow();
|
||||
|
||||
useEffect(() => {
|
||||
if (!databaseId || !rowIds.length) return;
|
||||
void onOpenDatabase({ databaseId, rowIds }).then(({ databaseDoc: doc, rows }) => {
|
||||
const fields = doc
|
||||
.getMap(YjsEditorKey.data_section)
|
||||
.get(YjsEditorKey.database)
|
||||
.get(YjsDatabaseKey.fields) as YDatabaseFields;
|
||||
|
||||
fields.forEach((field, fieldId) => {
|
||||
if ((field as YDatabaseField).get(YjsDatabaseKey.is_primary)) {
|
||||
setDatabasePrimaryFieldId(fieldId);
|
||||
}
|
||||
});
|
||||
if (!viewId || !rowIds.length) return;
|
||||
|
||||
void getViewRowsMap?.(viewId, rowIds).then(({ rows }) => {
|
||||
setRows(rows);
|
||||
});
|
||||
}, [onOpenDatabase, databaseId, rowIds, onCloseDatabase]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (currentDatabaseId !== databaseId && databaseId) {
|
||||
onCloseDatabase(databaseId);
|
||||
}
|
||||
};
|
||||
}, [databaseId, currentDatabaseId, onCloseDatabase]);
|
||||
}, [getViewRowsMap, rowIds, viewId]);
|
||||
|
||||
return (
|
||||
<div style={style} className={'relation-cell flex w-full items-center gap-2'}>
|
||||
@@ -64,9 +38,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
|
||||
}}
|
||||
className={'w-full cursor-pointer underline'}
|
||||
>
|
||||
{rowDoc && databasePrimaryFieldId && (
|
||||
<RelationPrimaryValue rowDoc={rowDoc} fieldId={databasePrimaryFieldId} />
|
||||
)}
|
||||
{rowDoc && <RelationPrimaryValue rowDoc={rowDoc} />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user