chore: merge wih main

This commit is contained in:
Zack Fu Zi Xiang 2024-07-10 21:31:10 +08:00
parent ea0dbe9a9a
commit 647934bf41
No known key found for this signature in database
393 changed files with 8268 additions and 7867 deletions

View File

@ -1,72 +0,0 @@
name: Deploy Web (Test)
on:
push:
branches:
- build/test
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
env:
SSH_PRIVATE_KEY: ${{ secrets.WEB_TEST_SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.WEB_TEST_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.WEB_TEST_REMOTE_USER }}
SSL_CERTIFICATE: ${{ secrets.WEB_TEST_SSL_CERTIFICATE }}
SSL_CERTIFICATE_KEY: ${{ secrets.WEB_TEST_SSL_CERTIFICATE_KEY }}
ENV_FILE: test.env
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
pnpm install
- name: copy env file
working-directory: frontend/appflowy_web_app
run: |
cp ${{ env.ENV_FILE }} .env
- name: test and lint
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint
- name: build
working-directory: frontend/appflowy_web_app
run: |
pnpm run build
- name: generate SSL certificate
run: |
echo "${{ env.SSL_CERTIFICATE }}" > nginx-signed.crt
echo "${{ env.SSL_CERTIFICATE_KEY }}" > nginx-signed.key
- name: Deploy to EC2
uses: easingthemes/ssh-deploy@main
with:
SSH_PRIVATE_KEY: ${{ env.SSH_PRIVATE_KEY }}
ARGS: "-rlgoDzvc -i"
SOURCE: "frontend/appflowy_web_app/dist frontend/appflowy_web_app/server.cjs frontend/appflowy_web_app/start.sh frontend/appflowy_web_app/Dockerfile frontend/appflowy_web_app/nginx.conf frontend/appflowy_web_app/.env nginx-signed.crt nginx-signed.key"
REMOTE_HOST: ${{ env.REMOTE_HOST }}
REMOTE_USER: ${{ env.REMOTE_USER }}
EXCLUDE: "frontend/appflowy_web_app/dist/, frontend/appflowy_web_app/node_modules/"
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

View File

@ -135,7 +135,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
job: job:
- { target: x86_64-apple-darwin, os: macos-11, extra-build-args: "" } - { target: x86_64-apple-darwin, os: macos-latest, extra-build-args: "" }
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -233,7 +233,7 @@ jobs:
job: job:
- { - {
targets: "aarch64-apple-darwin,x86_64-apple-darwin", targets: "aarch64-apple-darwin,x86_64-apple-darwin",
os: macos-11, os: macos-latest,
extra-build-args: "", extra-build-args: "",
} }
steps: steps:

View File

@ -1,4 +1,8 @@
# Release Notes # Release Notes
## Version 0.6.3 - 08/07/2024
### New Features
- Publish a Document to the Web
## Version 0.6.2 - 01/07/2024 ## Version 0.6.2 - 01/07/2024
### New Features ### New Features
- Added support for duplicating spaces. - Added support for duplicating spaces.

View File

@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi" LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.6.2" APPFLOWY_VERSION = "0.6.3"
FLUTTER_DESKTOP_FEATURES = "dart" FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy" PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0" MACOSX_DEPLOYMENT_TARGET = "11.0"

View File

@ -8,9 +8,10 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Empty', () { group('Empty', () {
testWidgets('toggle theme mode', (tester) async { testWidgets('empty test', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton(); await tester.tapAnonymousSignInButton();
await tester.wait(1000);
}); });
}); });
} }

View File

@ -82,11 +82,11 @@ void main() {
HeadingBlockKeys.type, HeadingBlockKeys.type,
); );
expect( expect(
importedPageEditorState.getNodeAtPath([2])!.type, importedPageEditorState.getNodeAtPath([1])!.type,
HeadingBlockKeys.type, HeadingBlockKeys.type,
); );
expect( expect(
importedPageEditorState.getNodeAtPath([4])!.type, importedPageEditorState.getNodeAtPath([2])!.type,
TableBlockKeys.type, TableBlockKeys.type,
); );
}); });

View File

@ -51,13 +51,14 @@ void main() {
}, },
); );
final shareButton = find.byType(ShareActionList); final shareButton = find.byType(DocumentShareButton);
final shareButtonState = final shareButtonState =
tester.state(shareButton) as ShareActionListState; tester.widget(shareButton) as DocumentShareButton;
final path = await mockSaveFilePath( final path = await mockSaveFilePath(
p.join( p.join(
context.applicationDataDirectory, context.applicationDataDirectory,
'${shareButtonState.name}.md', '${shareButtonState.view.name}.md',
), ),
); );

View File

@ -29,4 +29,11 @@ abstract class Env {
defaultValue: '', defaultValue: '',
) )
static const String afCloudUrl = _Env.afCloudUrl; static const String afCloudUrl = _Env.afCloudUrl;
@EnviedField(
obfuscate: false,
varName: 'INTERNAL_BUILD',
defaultValue: '',
)
static const String internalBuild = _Env.internalBuild;
} }

View File

@ -119,7 +119,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
final uniqueMessages = {...allMessages, ...messages}.toList() final uniqueMessages = {...allMessages, ...messages}.toList()
..sort((a, b) => b.id.compareTo(a.id)); ..sort((a, b) => b.id.compareTo(a.id));
uniqueMessages.insertAll(0, onetimeMessages); uniqueMessages.insertAll(0, onetimeMessages);
emit( emit(
state.copyWith( state.copyWith(
messages: uniqueMessages, messages: uniqueMessages,
@ -380,7 +379,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
} }
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) { Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
final streamMessageId = nanoid(); final streamMessageId = (questionMessageId + 1).toString();
lastStreamMessageId = streamMessageId; lastStreamMessageId = streamMessageId;
return TextMessage( return TextMessage(

View File

@ -1,10 +1,16 @@
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
@ -176,9 +182,20 @@ class CopyButton extends StatelessWidget {
size: const Size.square(14), size: const Size.square(14),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
onPressed: () { onPressed: () async {
Clipboard.setData(ClipboardData(text: textMessage.text)); final document = markdownToDocument(textMessage.text);
showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); await getIt<ClipboardService>().setData(
ClipboardServiceData(
plainText: textMessage.text,
inAppJson: jsonEncode(document.toJson()),
),
);
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.grid_url_copiedNotification.tr(),
);
}
}, },
), ),
); );

View File

@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'chat_input.dart'; import 'chat_input.dart';

View File

@ -0,0 +1,377 @@
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'selectable_highlight.dart';
enum AIMarkdownType {
appflowyEditor,
markdownWidget,
}
// Wrap the appflowy_editor or markdown_widget as a chat text message widget
class AIMarkdownText extends StatelessWidget {
const AIMarkdownText({
super.key,
required this.markdown,
this.type = AIMarkdownType.appflowyEditor,
});
final String markdown;
final AIMarkdownType type;
@override
Widget build(BuildContext context) {
switch (type) {
case AIMarkdownType.appflowyEditor:
return _AppFlowyEditorMarkdown(markdown: markdown);
case AIMarkdownType.markdownWidget:
return _ThirdPartyMarkdown(markdown: markdown);
}
}
}
class _AppFlowyEditorMarkdown extends StatefulWidget {
const _AppFlowyEditorMarkdown({
required this.markdown,
});
// the text should be the markdown format
final String markdown;
@override
State<_AppFlowyEditorMarkdown> createState() =>
_AppFlowyEditorMarkdownState();
}
class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
late EditorState editorState;
late final styleCustomizer = EditorStyleCustomizer(
context: context,
padding: EdgeInsets.zero,
);
late final editorStyle = styleCustomizer.style().copyWith(
// hide the cursor
cursorColor: Colors.transparent,
cursorWidth: 0,
);
late EditorScrollController scrollController;
@override
void initState() {
super.initState();
editorState = _parseMarkdown(widget.markdown);
scrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: true,
);
}
@override
void didUpdateWidget(covariant _AppFlowyEditorMarkdown oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.markdown != widget.markdown) {
editorState.dispose();
editorState = _parseMarkdown(widget.markdown);
scrollController.dispose();
scrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: true,
);
}
}
@override
void dispose() {
scrollController.dispose();
editorState.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final blockBuilders = getEditorBuilderMap(
context: context,
editorState: editorState,
styleCustomizer: styleCustomizer,
// the editor is not editable in the chat
editable: false,
);
return IntrinsicHeight(
child: AppFlowyEditor(
shrinkWrap: true,
// the editor is not editable in the chat
editable: false,
editorStyle: editorStyle,
editorScrollController: scrollController,
blockComponentBuilders: blockBuilders,
commandShortcutEvents: [customCopyCommand],
editorState: editorState,
),
);
}
EditorState _parseMarkdown(String markdown) {
final document = markdownToDocument(
markdown,
markdownParsers: [
const MarkdownCodeBlockParser(),
],
);
final editorState = EditorState(document: document);
return editorState;
}
}
class _ThirdPartyMarkdown extends StatelessWidget {
const _ThirdPartyMarkdown({
required this.markdown,
});
final String markdown;
@override
Widget build(BuildContext context) {
return MarkdownWidget(
data: markdown,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
config: configFromContext(context),
);
}
MarkdownConfig configFromContext(BuildContext context) {
return MarkdownConfig(
configs: [
HrConfig(color: AFThemeExtension.of(context).textColor),
_ChatH1Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
_ChatH2Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
_ChatH3Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
H4Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H5Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H6Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
PreConfig(
builder: (code, language) {
return ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 800,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
child: SelectableHighlightView(
code,
language: language,
theme: getHighlightTheme(context),
padding: const EdgeInsets.all(14),
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
),
);
},
),
PConfig(
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
CodeConfig(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
BlockquoteConfig(
sideColor: AFThemeExtension.of(context).lightGreyHover,
textColor: AFThemeExtension.of(context).textColor,
),
],
);
}
Map<String, TextStyle> getHighlightTheme(BuildContext context) {
return {
'root': TextStyle(
color: const Color(0xffabb2bf),
backgroundColor:
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
),
'comment': const TextStyle(
color: Color(0xff5c6370),
fontStyle: FontStyle.italic,
),
'quote': const TextStyle(
color: Color(0xff5c6370),
fontStyle: FontStyle.italic,
),
'doctag': const TextStyle(color: Color(0xffc678dd)),
'keyword': const TextStyle(color: Color(0xffc678dd)),
'formula': const TextStyle(color: Color(0xffc678dd)),
'section': const TextStyle(color: Color(0xffe06c75)),
'name': const TextStyle(color: Color(0xffe06c75)),
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
'deletion': const TextStyle(color: Color(0xffe06c75)),
'subst': const TextStyle(color: Color(0xffe06c75)),
'literal': const TextStyle(color: Color(0xff56b6c2)),
'string': const TextStyle(color: Color(0xff98c379)),
'regexp': const TextStyle(color: Color(0xff98c379)),
'addition': const TextStyle(color: Color(0xff98c379)),
'attribute': const TextStyle(color: Color(0xff98c379)),
'meta-string': const TextStyle(color: Color(0xff98c379)),
'built_in': const TextStyle(color: Color(0xffe6c07b)),
'attr': const TextStyle(color: Color(0xffd19a66)),
'variable': const TextStyle(color: Color(0xffd19a66)),
'template-variable': const TextStyle(color: Color(0xffd19a66)),
'type': const TextStyle(color: Color(0xffd19a66)),
'selector-class': const TextStyle(color: Color(0xffd19a66)),
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
'number': const TextStyle(color: Color(0xffd19a66)),
'symbol': const TextStyle(color: Color(0xff61aeee)),
'bullet': const TextStyle(color: Color(0xff61aeee)),
'link': const TextStyle(color: Color(0xff61aeee)),
'meta': const TextStyle(color: Color(0xff61aeee)),
'selector-id': const TextStyle(color: Color(0xff61aeee)),
'title': const TextStyle(color: Color(0xff61aeee)),
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
'strong': const TextStyle(fontWeight: FontWeight.bold),
};
}
}
class _ChatH1Config extends HeadingConfig {
const _ChatH1Config({
this.style = const TextStyle(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h1.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
///config class for h2
class _ChatH2Config extends HeadingConfig {
const _ChatH2Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h2.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class _ChatH3Config extends HeadingConfig {
const _ChatH3Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h3.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}

View File

@ -2,19 +2,15 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'selectable_highlight.dart';
class ChatAITextMessageWidget extends StatelessWidget { class ChatAITextMessageWidget extends StatelessWidget {
const ChatAITextMessageWidget({ const ChatAITextMessageWidget({
@ -59,248 +55,12 @@ class ChatAITextMessageWidget extends StatelessWidget {
if (state.text.isEmpty) { if (state.text.isEmpty) {
return const ChatAILoading(); return const ChatAILoading();
} else { } else {
return _textWidgetBuilder(user, context, state.text); return AIMarkdownText(markdown: state.text);
} }
}, },
), ),
); );
} }
Widget _textWidgetBuilder(
User user,
BuildContext context,
String text,
) {
return MarkdownWidget(
data: text,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
config: configFromContext(context),
);
}
MarkdownConfig configFromContext(BuildContext context) {
return MarkdownConfig(
configs: [
HrConfig(color: AFThemeExtension.of(context).textColor),
ChatH1Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
ChatH2Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
ChatH3Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
H4Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H5Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H6Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
PreConfig(
builder: (code, language) {
return ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 800,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
child: SelectableHighlightView(
code,
language: language,
theme: getHightlineTheme(context),
padding: const EdgeInsets.all(14),
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
),
);
},
),
PConfig(
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
CodeConfig(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
BlockquoteConfig(
sideColor: AFThemeExtension.of(context).lightGreyHover,
textColor: AFThemeExtension.of(context).textColor,
),
],
);
}
}
Map<String, TextStyle> getHightlineTheme(BuildContext context) {
return {
'root': TextStyle(
color: const Color(0xffabb2bf),
backgroundColor:
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
),
'comment':
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
'quote':
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
'doctag': const TextStyle(color: Color(0xffc678dd)),
'keyword': const TextStyle(color: Color(0xffc678dd)),
'formula': const TextStyle(color: Color(0xffc678dd)),
'section': const TextStyle(color: Color(0xffe06c75)),
'name': const TextStyle(color: Color(0xffe06c75)),
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
'deletion': const TextStyle(color: Color(0xffe06c75)),
'subst': const TextStyle(color: Color(0xffe06c75)),
'literal': const TextStyle(color: Color(0xff56b6c2)),
'string': const TextStyle(color: Color(0xff98c379)),
'regexp': const TextStyle(color: Color(0xff98c379)),
'addition': const TextStyle(color: Color(0xff98c379)),
'attribute': const TextStyle(color: Color(0xff98c379)),
'meta-string': const TextStyle(color: Color(0xff98c379)),
'built_in': const TextStyle(color: Color(0xffe6c07b)),
'attr': const TextStyle(color: Color(0xffd19a66)),
'variable': const TextStyle(color: Color(0xffd19a66)),
'template-variable': const TextStyle(color: Color(0xffd19a66)),
'type': const TextStyle(color: Color(0xffd19a66)),
'selector-class': const TextStyle(color: Color(0xffd19a66)),
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
'number': const TextStyle(color: Color(0xffd19a66)),
'symbol': const TextStyle(color: Color(0xff61aeee)),
'bullet': const TextStyle(color: Color(0xff61aeee)),
'link': const TextStyle(color: Color(0xff61aeee)),
'meta': const TextStyle(color: Color(0xff61aeee)),
'selector-id': const TextStyle(color: Color(0xff61aeee)),
'title': const TextStyle(color: Color(0xff61aeee)),
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
'strong': const TextStyle(fontWeight: FontWeight.bold),
};
}
class ChatH1Config extends HeadingConfig {
const ChatH1Config({
this.style = const TextStyle(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h1.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
///config class for h2
class ChatH2Config extends HeadingConfig {
const ChatH2Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h2.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class ChatH3Config extends HeadingConfig {
const ChatH3Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h3.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
} }
class StreamingError extends StatelessWidget { class StreamingError extends StatelessWidget {

View File

@ -52,7 +52,6 @@ class TextMessageText extends StatelessWidget {
text, text,
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
lineHeight: 1.5,
maxLines: null, maxLines: null,
selectable: true, selectable: true,
color: AFThemeExtension.of(context).textColor, color: AFThemeExtension.of(context).textColor,

View File

@ -18,6 +18,7 @@ import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/util/debounce.dart'; import 'package:appflowy/util/debounce.dart';
import 'package:appflowy/util/throttle.dart'; import 'package:appflowy/util/throttle.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -37,6 +38,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_bloc.freezed.dart'; part 'document_bloc.freezed.dart';
bool enableDocumentInternalLog = false;
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
DocumentBloc({ DocumentBloc({
required this.documentId, required this.documentId,
@ -212,6 +215,10 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
} }
Future<EditorState?> _initAppFlowyEditorState(DocumentDataPB data) async { Future<EditorState?> _initAppFlowyEditorState(DocumentDataPB data) async {
if (enableDocumentInternalLog) {
Log.info('document data: ${data.toProto3Json()}');
}
final document = data.toDocument(); final document = data.toDocument();
if (document == null) { if (document == null) {
assert(false, 'document is null'); assert(false, 'document is null');

View File

@ -180,6 +180,8 @@ extension NodeToBlock on Node {
String? parentId, String? parentId,
String? childrenId, String? childrenId,
Attributes? attributes, Attributes? attributes,
String? externalId,
String? externalType,
}) { }) {
assert(id.isNotEmpty); assert(id.isNotEmpty);
final block = BlockPB.create() final block = BlockPB.create()
@ -192,6 +194,12 @@ extension NodeToBlock on Node {
if (parentId != null && parentId.isNotEmpty) { if (parentId != null && parentId.isNotEmpty) {
block.parentId = parentId; block.parentId = parentId;
} }
if (externalId != null && externalId.isNotEmpty) {
block.externalId = externalId;
}
if (externalType != null && externalType.isNotEmpty) {
block.externalType = externalType;
}
return block; return block;
} }

View File

@ -1,41 +1,181 @@
import 'dart:io'; import 'dart:io';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/export/document_exporter.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.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-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_share_bloc.freezed.dart'; part 'document_share_bloc.freezed.dart';
const _url = 'https://appflowy.com';
class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> { class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
DocumentShareBloc({ DocumentShareBloc({
required this.view, required this.view,
}) : super(const DocumentShareState.initial()) { }) : super(DocumentShareState.initial()) {
on<DocumentShareEvent>((event, emit) async { on<DocumentShareEvent>((event, emit) async {
await event.when( await event.when(
initial: () async {
viewListener = ViewListener(viewId: view.id)
..start(
onViewUpdated: (value) {
add(DocumentShareEvent.updateViewName(value.name));
},
onViewMoveToTrash: (p0) {
add(const DocumentShareEvent.setPublishStatus(false));
},
);
add(const DocumentShareEvent.updatePublishStatus());
},
share: (type, path) async { share: (type, path) async {
if (DocumentShareType.unimplemented.contains(type)) { if (DocumentShareType.unimplemented.contains(type)) {
Log.error('DocumentShareType $type is not implemented'); Log.error('DocumentShareType $type is not implemented');
return; return;
} }
emit(const DocumentShareState.loading()); emit(state.copyWith(isLoading: true));
final exporter = DocumentExporter(view); final result = await _export(type, path);
final FlowyResult<ExportDataPB, FlowyError> result =
await exporter.export(type.exportType).then((value) { emit(
return value.fold( state.copyWith(
isLoading: false,
exportResult: result,
),
);
},
publish: (nameSpace, publishName) async {
// set space name
try {
final result =
await ViewBackendService.getPublishNameSpace().getOrThrow();
await ViewBackendService.publish(
view,
name: publishName,
).getOrThrow();
emit(
state.copyWith(
isPublished: true,
publishResult: FlowySuccess(null),
unpublishResult: null,
url: '$_url/${result.namespace}/$publishName',
),
);
} catch (e) {
Log.error('publish error: $e');
emit(
state.copyWith(
isPublished: false,
publishResult: FlowyResult.failure(
FlowyError(msg: 'publish error: $e'),
),
unpublishResult: null,
url: '',
),
);
}
},
unPublish: () async {
emit(
state.copyWith(
publishResult: null,
unpublishResult: null,
),
);
final result = await ViewBackendService.unpublish(view);
final isPublished = !result.isSuccess;
result.onFailure((f) {
Log.error('unpublish error: $f');
});
emit(
state.copyWith(
isPublished: isPublished,
publishResult: null,
unpublishResult: result,
url: result.fold((_) => '', (_) => state.url),
),
);
},
updateViewName: (viewName) async {
emit(state.copyWith(viewName: viewName));
},
setPublishStatus: (isPublished) {
emit(
state.copyWith(
isPublished: isPublished,
url: isPublished ? state.url : '',
),
);
},
updatePublishStatus: () async {
final publishInfo = await ViewBackendService.getPublishInfo(view);
final enablePublish =
await UserBackendService.getCurrentUserProfile().fold(
(v) => v.authenticator == AuthenticatorPB.AppFlowyCloud,
(p) => false,
);
publishInfo.fold((s) {
emit(
state.copyWith(
isPublished: true,
url: '$_url/${s.namespace}/${s.publishName}',
viewName: view.name,
enablePublish: enablePublish,
),
);
}, (f) {
emit(
state.copyWith(
isPublished: false,
url: '',
viewName: view.name,
enablePublish: enablePublish,
),
);
});
},
);
});
}
final ViewPB view;
late final ViewListener viewListener;
late final exporter = DocumentExporter(view);
@override
Future<void> close() async {
await viewListener.stop();
return super.close();
}
Future<FlowyResult<ExportDataPB, FlowyError>> _export(
DocumentShareType type,
String? path,
) async {
final result = await exporter.export(type.exportType);
return result.fold(
(s) { (s) {
if (path != null) { if (path != null) {
switch (type) { switch (type) {
case DocumentShareType.markdown: case DocumentShareType.markdown:
return FlowyResult.success(_saveMarkdownToPath(s, path)); return FlowySuccess(_saveMarkdownToPath(s, path));
case DocumentShareType.html: case DocumentShareType.html:
return FlowyResult.success(_saveHTMLToPath(s, path)); return FlowySuccess(_saveHTMLToPath(s, path));
default: default:
break; break;
} }
@ -44,16 +184,8 @@ class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
}, },
(f) => FlowyResult.failure(f), (f) => FlowyResult.failure(f),
); );
});
emit(DocumentShareState.finish(result));
},
);
});
} }
final ViewPB view;
ExportDataPB _saveMarkdownToPath(String markdown, String path) { ExportDataPB _saveMarkdownToPath(String markdown, String path) {
File(path).writeAsStringSync(markdown); File(path).writeAsStringSync(markdown);
return ExportDataPB() return ExportDataPB()
@ -93,15 +225,41 @@ enum DocumentShareType {
@freezed @freezed
class DocumentShareEvent with _$DocumentShareEvent { class DocumentShareEvent with _$DocumentShareEvent {
const factory DocumentShareEvent.share(DocumentShareType type, String? path) = const factory DocumentShareEvent.initial() = _Initial;
Share; const factory DocumentShareEvent.share(
DocumentShareType type,
String? path,
) = _Share;
const factory DocumentShareEvent.publish(
String nameSpace,
String pageId,
) = _Publish;
const factory DocumentShareEvent.unPublish() = _UnPublish;
const factory DocumentShareEvent.updateViewName(String name) =
_UpdateViewName;
const factory DocumentShareEvent.updatePublishStatus() = _UpdatePublishStatus;
const factory DocumentShareEvent.setPublishStatus(bool isPublished) =
_SetPublishStatus;
} }
@freezed @freezed
class DocumentShareState with _$DocumentShareState { class DocumentShareState with _$DocumentShareState {
const factory DocumentShareState.initial() = _Initial; const factory DocumentShareState({
const factory DocumentShareState.loading() = _Loading; required bool isPublished,
const factory DocumentShareState.finish( required bool isLoading,
FlowyResult<ExportDataPB, FlowyError> successOrFail, required String url,
) = _Finish; required String viewName,
required bool enablePublish,
FlowyResult<ExportDataPB, FlowyError>? exportResult,
FlowyResult<void, FlowyError>? publishResult,
FlowyResult<void, FlowyError>? unpublishResult,
}) = _DocumentShareState;
factory DocumentShareState.initial() => const DocumentShareState(
isLoading: false,
isPublished: false,
enablePublish: true,
url: '',
viewName: '',
);
} }

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/document_service.dart'; import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -21,6 +22,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:nanoid/nanoid.dart'; import 'package:nanoid/nanoid.dart';
const _kExternalTextType = 'text';
/// Uses to adjust the data structure between the editor and the backend. /// Uses to adjust the data structure between the editor and the backend.
/// ///
/// The editor uses a tree structure to represent the document, while the backend uses a flat structure. /// The editor uses a tree structure to represent the document, while the backend uses a flat structure.
@ -34,11 +37,9 @@ class TransactionAdapter {
final DocumentService documentService; final DocumentService documentService;
final String documentId; final String documentId;
final bool _enableDebug = false;
Future<void> apply(Transaction transaction, EditorState editorState) async { Future<void> apply(Transaction transaction, EditorState editorState) async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
if (_enableDebug) { if (enableDocumentInternalLog) {
Log.debug('transaction => ${transaction.toJson()}'); Log.debug('transaction => ${transaction.toJson()}');
} }
final actions = transaction.operations final actions = transaction.operations
@ -60,8 +61,10 @@ class TransactionAdapter {
textId: payload.textId, textId: payload.textId,
delta: payload.delta, delta: payload.delta,
); );
if (_enableDebug) { if (enableDocumentInternalLog) {
Log.debug('create external text: ${payload.delta}'); Log.debug(
'[editor_transaction_adapter] create external text: ${payload.delta}',
);
} }
} else if (type == TextDeltaType.update) { } else if (type == TextDeltaType.update) {
await documentService.updateExternalText( await documentService.updateExternalText(
@ -69,8 +72,10 @@ class TransactionAdapter {
textId: payload.textId, textId: payload.textId,
delta: payload.delta, delta: payload.delta,
); );
if (_enableDebug) { if (enableDocumentInternalLog) {
Log.debug('update external text: ${payload.delta}'); Log.debug(
'[editor_transaction_adapter] update external text: ${payload.delta}',
);
} }
} }
} }
@ -82,9 +87,9 @@ class TransactionAdapter {
); );
final elapsed = stopwatch.elapsedMilliseconds; final elapsed = stopwatch.elapsedMilliseconds;
stopwatch.stop(); stopwatch.stop();
if (_enableDebug) { if (enableDocumentInternalLog) {
Log.debug( Log.debug(
'apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms', '[editor_transaction_adapter] apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms',
); );
} }
} }
@ -136,8 +141,9 @@ extension on InsertOperation {
// create the external text if the node contains the delta in its data. // create the external text if the node contains the delta in its data.
final delta = node.delta; final delta = node.delta;
TextDeltaPayloadPB? textDeltaPayloadPB; TextDeltaPayloadPB? textDeltaPayloadPB;
String? textId;
if (delta != null) { if (delta != null) {
final textId = nanoid(6); textId = nanoid(6);
textDeltaPayloadPB = TextDeltaPayloadPB( textDeltaPayloadPB = TextDeltaPayloadPB(
documentId: documentId, documentId: documentId,
@ -148,13 +154,17 @@ extension on InsertOperation {
// sync the text id to the node // sync the text id to the node
node.externalValues = ExternalValues( node.externalValues = ExternalValues(
externalId: textId, externalId: textId,
externalType: 'text', externalType: _kExternalTextType,
); );
} }
// remove the delta from the data when the incremental update is stable. // remove the delta from the data when the incremental update is stable.
final payload = BlockActionPayloadPB() final payload = BlockActionPayloadPB()
..block = node.toBlock(childrenId: nanoid(6)) ..block = node.toBlock(
childrenId: nanoid(6),
externalId: textId,
externalType: textId != null ? _kExternalTextType : null,
)
..parentId = parentId ..parentId = parentId
..prevId = prevId; ..prevId = prevId;
@ -248,7 +258,7 @@ extension on UpdateOperation {
node.externalValues = ExternalValues( node.externalValues = ExternalValues(
externalId: textId, externalId: textId,
externalType: 'text', externalType: _kExternalTextType,
); );
actions.add( actions.add(

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart';
@ -9,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -284,6 +286,14 @@ class _MobileToolbarState extends State<_MobileToolbar>
if (canUpdateCachedKeyboardHeight) { if (canUpdateCachedKeyboardHeight) {
cachedKeyboardHeight.value = height; cachedKeyboardHeight.value = height;
if (defaultTargetPlatform == TargetPlatform.android) {
// cache the keyboard height with the view padding in Android
if (cachedKeyboardHeight.value != 0) {
cachedKeyboardHeight.value +=
MediaQuery.of(context).viewPadding.bottom;
}
}
} }
if (height == 0) { if (height == 0) {
@ -385,13 +395,19 @@ class _MobileToolbarState extends State<_MobileToolbar>
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: showMenuNotifier, valueListenable: showMenuNotifier,
builder: (_, showingMenu, __) { builder: (_, showingMenu, __) {
var paddingHeight = height; var keyboardHeight = height;
if (Platform.isAndroid) { if (defaultTargetPlatform == TargetPlatform.android) {
paddingHeight = if (!showingMenu) {
height + MediaQuery.of(context).viewPadding.bottom; // take the max value of the keyboard height and the view padding
// to make sure the toolbar is above the keyboard
keyboardHeight = max(
keyboardHeight,
MediaQuery.of(context).viewInsets.bottom,
);
}
} }
return SizedBox( return SizedBox(
height: paddingHeight, height: keyboardHeight,
child: (showingMenu && selectedMenuIndex != null) child: (showingMenu && selectedMenuIndex != null)
? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call( ? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call(
context, context,

View File

@ -0,0 +1,52 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:markdown/markdown.dart' as md;
class MarkdownCodeBlockParser extends CustomMarkdownParser {
const MarkdownCodeBlockParser();
@override
List<Node> transform(
md.Node element,
List<CustomMarkdownParser> parsers, {
MarkdownListType listType = MarkdownListType.unknown,
int? startNumber,
}) {
if (element is! md.Element) {
return [];
}
if (element.tag != 'pre') {
return [];
}
final ec = element.children;
if (ec == null || ec.isEmpty) {
return [];
}
final code = ec.first;
if (code is! md.Element || code.tag != 'code') {
return [];
}
String? language;
if (code.attributes.containsKey('class')) {
final classes = code.attributes['class']!.split(' ');
final languageClass = classes.firstWhere(
(c) => c.startsWith('language-'),
orElse: () => '',
);
language = languageClass.substring('language-'.length);
}
final deltaDecoder = DeltaMarkdownDecoder();
return [
codeBlockNode(
language: language,
delta: deltaDecoder.convertNodes(code.children),
),
];
}
}

View File

@ -1,3 +1,4 @@
export 'callout_node_parser.dart'; export 'callout_node_parser.dart';
export 'markdown_code_parser.dart';
export 'math_equation_node_parser.dart'; export 'math_equation_node_parser.dart';
export 'toggle_list_node_parser.dart'; export 'toggle_list_node_parser.dart';

View File

@ -0,0 +1,130 @@
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/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/export/document_exporter.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ExportTab extends StatelessWidget {
const ExportTab({
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
const VSpace(10),
_ExportButton(
title: LocaleKeys.shareAction_html.tr(),
svg: FlowySvgs.export_html_s,
onTap: () => _exportHTML(context),
),
const VSpace(10),
_ExportButton(
title: LocaleKeys.shareAction_markdown.tr(),
svg: FlowySvgs.export_markdown_s,
onTap: () => _exportMarkdown(context),
),
const VSpace(10),
_ExportButton(
title: LocaleKeys.shareAction_clipboard.tr(),
svg: FlowySvgs.duplicate_s,
onTap: () => _exportToClipboard(context),
),
],
);
}
Future<void> _exportHTML(BuildContext context) async {
final viewName = context.read<DocumentShareBloc>().state.viewName;
final exportPath = await getIt<FilePickerService>().saveFile(
dialogTitle: '',
fileName: '${viewName.toFileName()}.html',
);
if (context.mounted && exportPath != null) {
context.read<DocumentShareBloc>().add(
DocumentShareEvent.share(
DocumentShareType.html,
exportPath,
),
);
}
}
Future<void> _exportMarkdown(BuildContext context) async {
final viewName = context.read<DocumentShareBloc>().state.viewName;
final exportPath = await getIt<FilePickerService>().saveFile(
dialogTitle: '',
fileName: '${viewName.toFileName()}.md',
);
if (context.mounted && exportPath != null) {
context.read<DocumentShareBloc>().add(
DocumentShareEvent.share(
DocumentShareType.markdown,
exportPath,
),
);
}
}
Future<void> _exportToClipboard(BuildContext context) async {
final documentExporter =
DocumentExporter(context.read<DocumentShareBloc>().view);
final result = await documentExporter.export(DocumentExportType.markdown);
result.fold(
(markdown) {
getIt<ClipboardService>().setData(
ClipboardServiceData(plainText: markdown),
);
showToastNotification(
context,
message: LocaleKeys.grid_url_copiedNotification.tr(),
);
},
(error) => showToastNotification(context, message: error.msg),
);
}
}
class _ExportButton extends StatelessWidget {
const _ExportButton({
required this.title,
required this.svg,
required this.onTap,
});
final String title;
final FlowySvgData svg;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final color = Theme.of(context).isLightMode
? const Color(0x1E14171B)
: Colors.white.withOpacity(0.1);
final radius = BorderRadius.circular(10.0);
return FlowyButton(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
iconPadding: 12,
decoration: BoxDecoration(
border: Border.all(
color: color,
),
borderRadius: radius,
),
radius: radius,
text: FlowyText(title),
leftIcon: FlowySvg(svg),
onTap: onTap,
);
}
}

View File

@ -0,0 +1,11 @@
import 'package:appflowy/util/theme_extension.dart';
import 'package:flutter/material.dart';
class ShareMenuColors {
static Color borderColor(BuildContext context) {
final borderColor = Theme.of(context).isLightMode
? const Color(0x1E14171B)
: Colors.white.withOpacity(0.1);
return borderColor;
}
}

View File

@ -0,0 +1,19 @@
String replaceInvalidChars(String input) {
final RegExp invalidCharsRegex = RegExp('[^a-zA-Z0-9-]');
return input.replaceAll(invalidCharsRegex, '-');
}
Future<String> generateNameSpace() async {
return '';
}
// The backend limits the publish name to a maximum of 120 characters.
// If the combined length of the ID and the name exceeds 120 characters,
// we will truncate the name to ensure the final result is within the limit.
// The name should only contain alphanumeric characters and hyphens.
Future<String> generatePublishName(String id, String name) async {
if (name.length >= 120 - id.length) {
name = name.substring(0, 120 - id.length);
}
return replaceInvalidChars('$name-$id');
}

View File

@ -0,0 +1,284 @@
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/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/share/pubish_color_extension.dart';
import 'package:appflowy/plugins/document/presentation/share/publish_name_generator.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.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/style_widget/hover.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) {
_showToast(context, state);
},
builder: (context, state) {
return state.isPublished
? _PublishedWidget(
url: state.url,
onVisitSite: () {},
onUnPublish: () {
context
.read<DocumentShareBloc>()
.add(const DocumentShareEvent.unPublish());
},
)
: _UnPublishWidget(
onPublish: () async {
final id = context.read<DocumentShareBloc>().view.id;
final publishName = await generatePublishName(
id,
state.viewName,
);
if (context.mounted) {
context.read<DocumentShareBloc>().add(
DocumentShareEvent.publish('', publishName),
);
}
},
);
},
);
}
void _showToast(BuildContext context, DocumentShareState state) {
if (state.publishResult != null) {
state.publishResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.publish_publishSuccessfully.tr(),
),
(error) => showToastNotification(
context,
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
),
);
} else if (state.unpublishResult != null) {
state.unpublishResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
),
(error) => showToastNotification(
context,
message: LocaleKeys.publish_unpublishFailed.tr(),
description: error.msg,
),
);
}
}
}
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) {
getIt<ClipboardService>().setData(
ClipboardServiceData(plainText: url),
);
showToastNotification(
context,
message: LocaleKeys.grid_url_copy.tr(),
);
},
onSubmitted: (url) {},
),
const VSpace(16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildUnpublishButton(),
const Spacer(),
_buildVisitSiteButton(),
],
),
],
);
}
Widget _buildUnpublishButton() {
return SizedBox(
height: 36,
width: 184,
child: FlowyButton(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: ShareMenuColors.borderColor(context)),
),
radius: BorderRadius.circular(10),
text: FlowyText.regular(
LocaleKeys.shareAction_unPublish.tr(),
textAlign: TextAlign.center,
),
onTap: widget.onUnPublish,
),
);
}
Widget _buildVisitSiteButton() {
return RoundedTextButton(
onPressed: () {
safeLaunchUrl(controller.text);
},
title: LocaleKeys.shareAction_visitSite.tr(),
width: 184,
height: 36,
borderRadius: const BorderRadius.all(Radius.circular(10)),
fillColor: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
);
}
}
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(
readOnly: true,
autoFocus: false,
controller: controller,
enableBorderColor: ShareMenuColors.borderColor(context),
suffixIcon: _buildCopyLinkIcon(context),
),
);
}
Widget _buildCopyLinkIcon(BuildContext context) {
return FlowyHover(
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,
),
),
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_share_bloc.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/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/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/export/document_exporter.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:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.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:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -28,23 +30,44 @@ class DocumentShareButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => getIt<DocumentShareBloc>(param1: view), create: (context) => getIt<DocumentShareBloc>(param1: view)
..add(const DocumentShareEvent.initial()),
child: BlocListener<DocumentShareBloc, DocumentShareState>( child: BlocListener<DocumentShareBloc, DocumentShareState>(
listener: (context, state) { listener: (context, state) {
state.mapOrNull( if (state.isLoading == false && state.exportResult != null) {
finish: (state) { state.exportResult!.fold(
state.successOrFail.fold(
(data) => _handleExportData(context, data), (data) => _handleExportData(context, data),
_handleExportError, _handleExportError,
); );
}, }
);
}, },
child: BlocBuilder<DocumentShareBloc, DocumentShareState>( child: BlocBuilder<DocumentShareBloc, DocumentShareState>(
builder: (context, state) => SizedBox( builder: (context, state) {
final tabs = [
if (state.enablePublish) ShareMenuTab.publish,
ShareMenuTab.exportAs,
];
final shareBloc = context.read<DocumentShareBloc>();
return SizedBox(
height: 32.0, height: 32.0,
child: IntrinsicWidth(child: ShareActionList(view: view)), child: IntrinsicWidth(
child: AppFlowyPopover(
direction: PopoverDirection.bottomWithRightAligned,
constraints: const BoxConstraints(
maxWidth: 422,
), ),
offset: const Offset(0, 8),
popupBuilder: (context) => BlocProvider.value(
value: shareBloc,
child: ShareMenu(
tabs: tabs,
),
),
child: const InnerDocumentShareButton(),
),
),
);
},
), ),
), ),
); );
@ -75,6 +98,24 @@ class DocumentShareButton extends StatelessWidget {
} }
} }
class InnerDocumentShareButton extends StatelessWidget {
const InnerDocumentShareButton({super.key});
@override
Widget build(BuildContext context) {
return RoundedTextButton(
title: LocaleKeys.shareAction_buttonText.tr(),
padding: const EdgeInsets.symmetric(horizontal: 14.0),
fontSize: 14.0,
fontWeight: FontWeight.w500,
borderRadius: const BorderRadius.all(
Radius.circular(10.0),
),
textColor: Theme.of(context).colorScheme.onPrimary,
);
}
}
class ShareActionList extends StatefulWidget { class ShareActionList extends StatefulWidget {
const ShareActionList({ const ShareActionList({
super.key, super.key,

View File

@ -0,0 +1,186 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
import 'package:appflowy/plugins/document/presentation/share/export_tab.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'publish_tab.dart';
enum ShareMenuTab {
share,
publish,
exportAs;
String get i18n {
switch (this) {
case ShareMenuTab.share:
return LocaleKeys.shareAction_shareTab.tr();
case ShareMenuTab.publish:
return LocaleKeys.shareAction_publishTab.tr();
case ShareMenuTab.exportAs:
return LocaleKeys.shareAction_exportAsTab.tr();
}
}
}
class ShareMenu extends StatefulWidget {
const ShareMenu({
super.key,
required this.tabs,
});
final List<ShareMenuTab> tabs;
@override
State<ShareMenu> createState() => _ShareMenuState();
}
class _ShareMenuState extends State<ShareMenu>
with SingleTickerProviderStateMixin {
late ShareMenuTab selectedTab = widget.tabs.first;
late final tabController = TabController(
length: widget.tabs.length,
vsync: this,
initialIndex: widget.tabs.indexOf(selectedTab),
);
@override
Widget build(BuildContext context) {
if (widget.tabs.isEmpty) {
return const SizedBox.shrink();
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const VSpace(10),
Container(
alignment: Alignment.centerLeft,
height: 30,
child: _buildTabBar(context),
),
Divider(
color: Theme.of(context).dividerColor,
height: 1,
thickness: 1,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14.0),
child: _buildTab(context),
),
const VSpace(20),
],
);
}
@override
void dispose() {
tabController.dispose();
super.dispose();
}
Widget _buildTabBar(BuildContext context) {
final children = [
for (final tab in widget.tabs)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _Segment(
tab: tab,
isSelected: selectedTab == tab,
),
),
];
return TabBar(
indicatorSize: TabBarIndicatorSize.label,
indicator: RoundUnderlineTabIndicator(
width: 68.0,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 3,
),
insets: const EdgeInsets.only(bottom: -2),
),
isScrollable: true,
controller: tabController,
tabs: children,
onTap: (index) {
setState(() {
selectedTab = widget.tabs[index];
});
},
);
}
Widget _buildTab(BuildContext context) {
switch (selectedTab) {
case ShareMenuTab.publish:
return const PublishTab();
case ShareMenuTab.exportAs:
return const ExportTab();
default:
return const Center(
child: FlowyText('🏡 under construction'),
);
}
}
}
class _Segment extends StatefulWidget {
const _Segment({
required this.tab,
required this.isSelected,
});
final bool isSelected;
final ShareMenuTab tab;
@override
State<_Segment> createState() => _SegmentState();
}
class _SegmentState extends State<_Segment> {
bool isHovered = false;
@override
Widget build(BuildContext context) {
Color? textColor = Theme.of(context).hintColor;
if (isHovered) {
textColor = const Color(0xFF00BCF0);
} else if (widget.isSelected) {
textColor = null;
}
Widget child = MouseRegion(
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
child: FlowyText(
widget.tab.i18n,
textAlign: TextAlign.center,
color: textColor,
),
);
if (widget.tab == ShareMenuTab.publish) {
final isPublished = context.watch<DocumentShareBloc>().state.isPublished;
// show checkmark icon if published
if (isPublished) {
child = Row(
children: [
const FlowySvg(
FlowySvgs.published_checkmark_s,
blendMode: null,
),
const HSpace(6),
child,
],
);
}
}
return child;
}
}

View File

@ -5,6 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
show WorkspaceSettingPB; show WorkspaceSettingPB;
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'home_bloc.freezed.dart'; part 'home_bloc.freezed.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> { class HomeBloc extends Bloc<HomeEvent, HomeState> {

View File

@ -54,9 +54,11 @@ class SidebarSectionsBloc
_initial(userProfile, workspaceId); _initial(userProfile, workspaceId);
final sectionViews = await _getSectionViews(); final sectionViews = await _getSectionViews();
if (sectionViews != null) { if (sectionViews != null) {
final containsSpace = _containsSpace(sectionViews);
emit( emit(
state.copyWith( state.copyWith(
section: sectionViews, section: sectionViews,
containsSpace: containsSpace,
), ),
); );
} }
@ -65,9 +67,11 @@ class SidebarSectionsBloc
_reset(userProfile, workspaceId); _reset(userProfile, workspaceId);
final sectionViews = await _getSectionViews(); final sectionViews = await _getSectionViews();
if (sectionViews != null) { if (sectionViews != null) {
final containsSpace = _containsSpace(sectionViews);
emit( emit(
state.copyWith( state.copyWith(
section: sectionViews, section: sectionViews,
containsSpace: containsSpace,
), ),
); );
} }
@ -160,9 +164,11 @@ class SidebarSectionsBloc
_initial(userProfile, workspaceId); _initial(userProfile, workspaceId);
final sectionViews = await _getSectionViews(); final sectionViews = await _getSectionViews();
if (sectionViews != null) { if (sectionViews != null) {
final containsSpace = _containsSpace(sectionViews);
emit( emit(
state.copyWith( state.copyWith(
section: sectionViews, section: sectionViews,
containsSpace: containsSpace,
), ),
); );
// try to open the fist view in public section or private section // try to open the fist view in public section or private section
@ -229,6 +235,11 @@ class SidebarSectionsBloc
} }
} }
bool _containsSpace(SidebarSection section) {
return section.publicViews.any((view) => view.isSpace) ||
section.privateViews.any((view) => view.isSpace);
}
void _initial(UserProfilePB userProfile, String workspaceId) { void _initial(UserProfilePB userProfile, String workspaceId) {
_workspaceService = WorkspaceService(workspaceId: workspaceId); _workspaceService = WorkspaceService(workspaceId: workspaceId);
@ -292,6 +303,7 @@ class SidebarSectionsState with _$SidebarSectionsState {
required SidebarSection section, required SidebarSection section,
@Default(null) ViewPB? lastCreatedRootView, @Default(null) ViewPB? lastCreatedRootView,
FlowyResult<void, FlowyError>? createRootViewResult, FlowyResult<void, FlowyError>? createRootViewResult,
@Default(true) bool containsSpace,
}) = _SidebarSectionsState; }) = _SidebarSectionsState;
factory SidebarSectionsState.initial() => const SidebarSectionsState( factory SidebarSectionsState.initial() => const SidebarSectionsState(

View File

@ -4,12 +4,14 @@ import 'dart:convert';
import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/list_extension.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy/workspace/application/workspace/prelude.dart';
import 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart'; import 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -20,6 +22,7 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart'; import 'package:flowy_infra/uuid.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
@ -68,6 +71,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
_initial(userProfile, workspaceId); _initial(userProfile, workspaceId);
final (spaces, publicViews, privateViews) = await _getSpaces(); final (spaces, publicViews, privateViews) = await _getSpaces();
final shouldShowUpgradeDialog = await this.shouldShowUpgradeDialog( final shouldShowUpgradeDialog = await this.shouldShowUpgradeDialog(
spaces: spaces, spaces: spaces,
publicViews: publicViews, publicViews: publicViews,
@ -82,14 +86,13 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
currentSpace: currentSpace, currentSpace: currentSpace,
isExpanded: isExpanded, isExpanded: isExpanded,
shouldShowUpgradeDialog: shouldShowUpgradeDialog, shouldShowUpgradeDialog: shouldShowUpgradeDialog,
isInitialized: true,
), ),
); );
if (shouldShowUpgradeDialog) { if (shouldShowUpgradeDialog && !integrationMode().isTest) {
if (!integrationMode().isTest) {
add(const SpaceEvent.migrate()); add(const SpaceEvent.migrate());
} }
}
if (openFirstPage) { if (openFirstPage) {
if (currentSpace != null) { if (currentSpace != null) {
@ -192,13 +195,31 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
open: (space) async { open: (space) async {
await _openSpace(space); await _openSpace(space);
final isExpanded = await _getSpaceExpandStatus(space); final isExpanded = await _getSpaceExpandStatus(space);
emit(state.copyWith(currentSpace: space, isExpanded: isExpanded)); final views = await ViewBackendService.getChildViews(
viewId: space.id,
);
final currentSpace = views.fold(
(views) {
space.freeze();
return space.rebuild((b) {
b.childViews.clear();
b.childViews.addAll(views);
});
},
(_) => space,
);
emit(
state.copyWith(
currentSpace: currentSpace,
isExpanded: isExpanded,
),
);
// don't open the page automatically on mobile // don't open the page automatically on mobile
if (PlatformExtension.isDesktop) { if (PlatformExtension.isDesktop) {
// open the first page by default // open the first page by default
if (space.childViews.isNotEmpty) { if (currentSpace.childViews.isNotEmpty) {
final firstPage = space.childViews.first; final firstPage = currentSpace.childViews.first;
emit( emit(
state.copyWith( state.copyWith(
lastCreatedPage: firstPage, lastCreatedPage: firstPage,
@ -330,8 +351,9 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
if (sectionViews == null || sectionViews.views.isEmpty) { if (sectionViews == null || sectionViews.views.isEmpty) {
return (<ViewPB>[], <ViewPB>[], <ViewPB>[]); return (<ViewPB>[], <ViewPB>[], <ViewPB>[]);
} }
final publicViews = sectionViews.publicViews;
final privateViews = sectionViews.privateViews; final publicViews = sectionViews.publicViews.unique((e) => e.id);
final privateViews = sectionViews.privateViews.unique((e) => e.id);
final publicSpaces = publicViews.where((e) => e.isSpace); final publicSpaces = publicViews.where((e) => e.isSpace);
final privateSpaces = privateViews.where((e) => e.isSpace); final privateSpaces = privateViews.where((e) => e.isSpace);
@ -690,6 +712,7 @@ class SpaceState with _$SpaceState {
FlowyResult<void, FlowyError>? createPageResult, FlowyResult<void, FlowyError>? createPageResult,
@Default(false) bool shouldShowUpgradeDialog, @Default(false) bool shouldShowUpgradeDialog,
@Default(false) bool isDuplicatingSpace, @Default(false) bool isDuplicatingSpace,
@Default(false) bool isInitialized,
}) = _SpaceState; }) = _SpaceState;
factory SpaceState.initial() => const SpaceState(); factory SpaceState.initial() => const SpaceState();

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv.dart';
@ -7,6 +8,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_listener.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@ -138,11 +140,18 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
); );
}, },
delete: (e) async { delete: (e) async {
// unpublish the page and all its child pages if they are published
await _unpublishPage(view);
final result = await ViewBackendService.delete(viewId: view.id); final result = await ViewBackendService.delete(viewId: view.id);
emit( emit(
result.fold( result.fold(
(l) => (l) {
state.copyWith(successOrFailure: FlowyResult.success(null)), return state.copyWith(
successOrFailure: FlowyResult.success(null),
);
},
(error) => state.copyWith( (error) => state.copyWith(
successOrFailure: FlowyResult.failure(error), successOrFailure: FlowyResult.failure(error),
), ),
@ -180,8 +189,11 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
); );
emit( emit(
result.fold( result.fold(
(l) => (l) {
state.copyWith(successOrFailure: FlowyResult.success(null)), return state.copyWith(
successOrFailure: FlowyResult.success(null),
);
},
(error) => state.copyWith( (error) => state.copyWith(
successOrFailure: FlowyResult.failure(error), successOrFailure: FlowyResult.failure(error),
), ),
@ -236,6 +248,13 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
} }
add(const ViewEvent.setIsExpanded(false)); add(const ViewEvent.setIsExpanded(false));
}, },
unpublish: (value) async {
if (value.sync) {
await _unpublishPage(view);
} else {
unawaited(_unpublishPage(view));
}
},
); );
}, },
); );
@ -383,6 +402,20 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
return null; return null;
} }
// unpublish the page and all its child pages
Future<void> _unpublishPage(ViewPB views) async {
final (_, publishedPages) = await ViewBackendService.containPublishedPage(
view,
);
await Future.wait(
publishedPages.map((view) async {
Log.info('unpublishing page: ${view.id}, ${view.name}');
await ViewBackendService.unpublish(view);
}),
);
}
bool _isSameViewIgnoreChildren(ViewPB from, ViewPB to) { bool _isSameViewIgnoreChildren(ViewPB from, ViewPB to) {
return _hash(from) == _hash(to); return _hash(from) == _hash(to);
} }
@ -430,6 +463,8 @@ class ViewEvent with _$ViewEvent {
) = UpdateViewVisibility; ) = UpdateViewVisibility;
const factory ViewEvent.updateIcon(String? icon) = UpdateIcon; const factory ViewEvent.updateIcon(String? icon) = UpdateIcon;
const factory ViewEvent.collapseAllPages() = CollapseAllPages; const factory ViewEvent.collapseAllPages() = CollapseAllPages;
// this event will unpublish the page and all its child pages if they are published
const factory ViewEvent.unpublish({required bool sync}) = Unpublish;
} }
@freezed @freezed

View File

@ -291,4 +291,78 @@ class ViewBackendService {
); );
return FolderEventUpdateViewVisibilityStatus(payload).send(); 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();
}
static Future<List<ViewPB>> getAllChildViews(ViewPB view) async {
final views = <ViewPB>[];
final childViews =
await ViewBackendService.getChildViews(viewId: view.id).fold(
(s) => s,
(f) => [],
);
for (final child in childViews) {
// filter the view itself
if (child.id == view.id) {
continue;
}
views.add(child);
views.addAll(await getAllChildViews(child));
}
return views;
}
static Future<(bool, List<ViewPB>)> containPublishedPage(ViewPB view) async {
final childViews = await ViewBackendService.getAllChildViews(view);
final views = [view, ...childViews];
final List<ViewPB> publishedPages = [];
for (final view in views) {
final publishInfo = await ViewBackendService.getPublishInfo(view);
if (publishInfo.isSuccess) {
publishedPages.add(view);
}
}
return (publishedPages.isNotEmpty, publishedPages);
}
} }

View File

@ -15,6 +15,14 @@ class ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> {
await event.when( await event.when(
initial: () async { initial: () async {
add(const ViewTitleBarEvent.reload()); add(const ViewTitleBarEvent.reload());
viewListener = ViewListener(
viewId: view.id,
)..start(
onViewUpdated: (p0) {
add(const ViewTitleBarEvent.reload());
},
);
}, },
reload: () async { reload: () async {
final List<ViewPB> ancestors = final List<ViewPB> ancestors =
@ -30,6 +38,13 @@ class ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> {
} }
final ViewPB view; final ViewPB view;
late final ViewListener viewListener;
@override
Future<void> close() {
viewListener.stop();
return super.close();
}
} }
@freezed @freezed

View File

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/share/import_service.dart'; import 'package:appflowy/workspace/application/settings/share/import_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_type.dart';
@ -220,7 +221,12 @@ class _ImportPanelState extends State<ImportPanel> {
Uint8List? _documentDataFrom(ImportType importType, String data) { Uint8List? _documentDataFrom(ImportType importType, String data) {
switch (importType) { switch (importType) {
case ImportType.markdownOrText: case ImportType.markdownOrText:
final document = markdownToDocument(data); final document = markdownToDocument(
data,
markdownParsers: [
const MarkdownCodeBlockParser(),
],
);
return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer(); return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
case ImportType.historyDocument: case ImportType.historyDocument:
final document = EditorMigration.migrateDocument(data); final document = EditorMigration.migrateDocument(data);

View File

@ -11,6 +11,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
typedef MovePageMenuOnSelected = void Function(ViewPB space, ViewPB view);
class MovePageMenu extends StatefulWidget { class MovePageMenu extends StatefulWidget {
const MovePageMenu({ const MovePageMenu({
super.key, super.key,
@ -23,7 +25,7 @@ class MovePageMenu extends StatefulWidget {
final ViewPB sourceView; final ViewPB sourceView;
final UserProfilePB userProfile; final UserProfilePB userProfile;
final String workspaceId; final String workspaceId;
final void Function(ViewPB view) onSelected; final MovePageMenuOnSelected onSelected;
@override @override
State<MovePageMenu> createState() => _MovePageMenuState(); State<MovePageMenu> createState() => _MovePageMenuState();
@ -88,7 +90,7 @@ class _MovePageMenuState extends State<MovePageMenu> {
); );
} }
return Expanded( return Expanded(
child: _buildGroupedViews(state.queryResults!), child: _buildGroupedViews(space, state.queryResults!),
); );
}, },
), ),
@ -99,7 +101,7 @@ class _MovePageMenuState extends State<MovePageMenu> {
); );
} }
Widget _buildGroupedViews(List<ViewPB> views) { Widget _buildGroupedViews(ViewPB space, List<ViewPB> views) {
final groupedViews = views final groupedViews = views
.where( .where(
(view) => (view) =>
@ -108,7 +110,7 @@ class _MovePageMenuState extends State<MovePageMenu> {
.toList(); .toList();
return _MovePageGroupedViews( return _MovePageGroupedViews(
views: groupedViews, views: groupedViews,
onSelected: widget.onSelected, onSelected: (view) => widget.onSelected(space, view),
); );
} }
@ -124,7 +126,7 @@ class _MovePageMenuState extends State<MovePageMenu> {
child: CurrentSpace( child: CurrentSpace(
onTapBlankArea: () { onTapBlankArea: () {
// move the page to current space // move the page to current space
widget.onSelected(space); widget.onSelected(space, space);
}, },
space: space, space: space,
), ),
@ -145,7 +147,7 @@ class _MovePageMenuState extends State<MovePageMenu> {
disableSelectedStatus: true, disableSelectedStatus: true,
// hide the ... and + buttons // hide the ... and + buttons
rightIconsBuilder: (context, view) => [], rightIconsBuilder: (context, view) => [],
onSelected: (_, view) => widget.onSelected(view), onSelected: (_, view) => widget.onSelected(space, view),
), ),
), ),
), ),

View File

@ -1,7 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/plugins/blank/blank.dart';
@ -32,11 +30,12 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/side
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB; show UserProfilePB;
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
Loading? _duplicateSpaceLoading; Loading? _duplicateSpaceLoading;
@ -360,10 +359,24 @@ class _SidebarState extends State<_Sidebar> {
Widget _renderFolderOrSpace(EdgeInsets menuHorizontalInset) { Widget _renderFolderOrSpace(EdgeInsets menuHorizontalInset) {
final spaceState = context.read<SpaceBloc>().state; final spaceState = context.read<SpaceBloc>().state;
final workspaceState = context.read<UserWorkspaceBloc>().state; final workspaceState = context.read<UserWorkspaceBloc>().state;
if (!spaceState.isInitialized) {
return const SizedBox.shrink();
}
// there's no space or the workspace is not collaborative, // there's no space or the workspace is not collaborative,
// show the folder section (Workspace, Private, Personal) // show the folder section (Workspace, Private, Personal)
// otherwise, show the space // otherwise, show the space
return spaceState.spaces.isEmpty || !workspaceState.isCollabWorkspaceOn final sidebarSectionBloc = context.watch<SidebarSectionsBloc>();
final containsSpace = sidebarSectionBloc.state.containsSpace;
if (containsSpace && spaceState.spaces.isEmpty) {
context.read<SpaceBloc>().add(const SpaceEvent.didReceiveSpaceUpdate());
}
return !containsSpace ||
spaceState.spaces.isEmpty ||
!workspaceState.isCollabWorkspaceOn
? Expanded( ? Expanded(
child: Padding( child: Padding(
padding: menuHorizontalInset - const EdgeInsets.only(right: 6), padding: menuHorizontalInset - const EdgeInsets.only(right: 6),

View File

@ -19,6 +19,7 @@ import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class SpacePermissionSwitch extends StatefulWidget { class SpacePermissionSwitch extends StatefulWidget {
@ -222,14 +223,37 @@ class SpaceCancelOrConfirmButton extends StatelessWidget {
} }
} }
class DeleteSpacePopup extends StatelessWidget { class ConfirmDeletionPopup extends StatefulWidget {
const DeleteSpacePopup({super.key}); const ConfirmDeletionPopup({
super.key,
required this.title,
required this.description,
required this.onConfirm,
});
final String title;
final String description;
final VoidCallback onConfirm;
@override
State<ConfirmDeletionPopup> createState() => _ConfirmDeletionPopupState();
}
class _ConfirmDeletionPopupState extends State<ConfirmDeletionPopup> {
final focusNode = FocusNode();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final space = context.read<SpaceBloc>().state.currentSpace; return KeyboardListener(
final name = space != null ? space.name : ''; focusNode: focusNode,
return Padding( autofocus: true,
onKeyEvent: (event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
Navigator.of(context).pop();
}
},
child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 20.0, vertical: 20.0,
horizontal: 20.0, horizontal: 20.0,
@ -240,11 +264,14 @@ class DeleteSpacePopup extends StatelessWidget {
children: [ children: [
Row( Row(
children: [ children: [
FlowyText( Flexible(
LocaleKeys.space_deleteConfirmation.tr() + name, child: FlowyText(
widget.title,
fontSize: 14.0, fontSize: 14.0,
overflow: TextOverflow.ellipsis,
), ),
const Spacer(), ),
const HSpace(6.0),
FlowyButton( FlowyButton(
useIntrinsicWidth: true, useIntrinsicWidth: true,
text: const FlowySvg(FlowySvgs.upgrade_close_s), text: const FlowySvg(FlowySvgs.upgrade_close_s),
@ -254,7 +281,7 @@ class DeleteSpacePopup extends StatelessWidget {
), ),
const VSpace(8.0), const VSpace(8.0),
FlowyText.regular( FlowyText.regular(
LocaleKeys.space_deleteConfirmationDescription.tr(), widget.description,
fontSize: 12.0, fontSize: 12.0,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
maxLines: 3, maxLines: 3,
@ -264,7 +291,7 @@ class DeleteSpacePopup extends StatelessWidget {
SpaceCancelOrConfirmButton( SpaceCancelOrConfirmButton(
onCancel: () => Navigator.of(context).pop(), onCancel: () => Navigator.of(context).pop(),
onConfirm: () { onConfirm: () {
context.read<SpaceBloc>().add(const SpaceEvent.delete(null)); widget.onConfirm();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
confirmButtonName: LocaleKeys.space_delete.tr(), confirmButtonName: LocaleKeys.space_delete.tr(),
@ -272,6 +299,7 @@ class DeleteSpacePopup extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }

View File

@ -227,18 +227,14 @@ class _SidebarSpaceHeaderState extends State<SidebarSpaceHeader> {
void _showDeleteSpaceDialog(BuildContext context) { void _showDeleteSpaceDialog(BuildContext context) {
final spaceBloc = context.read<SpaceBloc>(); final spaceBloc = context.read<SpaceBloc>();
showDialog( final space = spaceBloc.state.currentSpace;
final name = space != null ? space.name : '';
showConfirmDeletionDialog(
context: context, context: context,
builder: (_) { name: name,
return Dialog( description: LocaleKeys.space_deleteConfirmationDescription.tr(),
shape: RoundedRectangleBorder( onConfirm: () {
borderRadius: BorderRadius.circular(12.0), context.read<SpaceBloc>().add(const SpaceEvent.delete(null));
),
child: BlocProvider.value(
value: spaceBloc,
child: const SizedBox(width: 440, child: DeleteSpacePopup()),
),
);
}, },
); );
} }

View File

@ -105,14 +105,16 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
case WorkspaceMoreAction.divider: case WorkspaceMoreAction.divider:
break; break;
case WorkspaceMoreAction.delete: case WorkspaceMoreAction.delete:
await NavigatorAlertDialog( await showConfirmDeletionDialog(
title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), context: context,
confirm: () { name: workspace.name,
description: LocaleKeys.workspace_deleteWorkspaceHintText.tr(),
onConfirm: () {
workspaceBloc.add( workspaceBloc.add(
UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId), UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId),
); );
}, },
).show(context); );
case WorkspaceMoreAction.rename: case WorkspaceMoreAction.rename:
await NavigatorTextFieldDialog( await NavigatorTextFieldDialog(
title: LocaleKeys.workspace_renameWorkspace.tr(), title: LocaleKeys.workspace_renameWorkspace.tr(),

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart';
@ -5,6 +7,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
@ -17,9 +20,10 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.d
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -334,6 +338,7 @@ class _InnerViewItemState extends State<InnerViewItem> {
onMove: widget.isPlaceholder onMove: widget.isPlaceholder
? (from, to) => _moveViewCrossSection( ? (from, to) => _moveViewCrossSection(
context, context,
null,
widget.view, widget.view,
widget.parentView, widget.parentView,
widget.spaceType, widget.spaceType,
@ -690,7 +695,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
spaceType: widget.spaceType, spaceType: widget.spaceType,
onEditing: (value) => onEditing: (value) =>
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)), context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),
onAction: (action, data) { onAction: (action, data) async {
switch (action) { switch (action) {
case ViewMoreActionType.favorite: case ViewMoreActionType.favorite:
case ViewMoreActionType.unFavorite: case ViewMoreActionType.unFavorite:
@ -699,6 +704,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
.add(FavoriteEvent.toggle(widget.view)); .add(FavoriteEvent.toggle(widget.view));
break; break;
case ViewMoreActionType.rename: case ViewMoreActionType.rename:
unawaited(
NavigatorTextFieldDialog( NavigatorTextFieldDialog(
title: LocaleKeys.disclosureAction_rename.tr(), title: LocaleKeys.disclosureAction_rename.tr(),
autoSelectAllText: true, autoSelectAllText: true,
@ -707,10 +713,27 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
onConfirm: (newValue, _) { onConfirm: (newValue, _) {
context.read<ViewBloc>().add(ViewEvent.rename(newValue)); context.read<ViewBloc>().add(ViewEvent.rename(newValue));
}, },
).show(context); ).show(context),
);
break; break;
case ViewMoreActionType.delete: case ViewMoreActionType.delete:
// get if current page contains published child views
final (containPublishedPage, _) =
await ViewBackendService.containPublishedPage(
widget.view,
);
if (containPublishedPage && context.mounted) {
await showConfirmDeletionDialog(
context: context,
name: widget.view.name,
description: LocaleKeys.publish_containsPublishedPage.tr(),
onConfirm: () {
context.read<ViewBloc>().add(const ViewEvent.delete()); context.read<ViewBloc>().add(const ViewEvent.delete());
},
);
} else if (context.mounted) {
context.read<ViewBloc>().add(const ViewEvent.delete());
}
break; break;
case ViewMoreActionType.duplicate: case ViewMoreActionType.duplicate:
context.read<ViewBloc>().add(const ViewEvent.duplicate()); context.read<ViewBloc>().add(const ViewEvent.duplicate());
@ -726,22 +749,22 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
return; return;
} }
final result = data; final result = data;
ViewBackendService.updateViewIcon( await ViewBackendService.updateViewIcon(
viewId: widget.view.id, viewId: widget.view.id,
viewIcon: result.emoji, viewIcon: result.emoji,
iconType: result.type.toProto(), iconType: result.type.toProto(),
); );
break; break;
case ViewMoreActionType.moveTo: case ViewMoreActionType.moveTo:
final target = data; final value = data;
if (target is! ViewPB) { if (value is! (ViewPB, ViewPB)) {
return; return;
} }
debugPrint( final space = value.$1;
'Move view ${widget.view.id}, ${widget.view.name} to ${target.id}, ${target.name}', final target = value.$2;
);
_moveViewCrossSection( _moveViewCrossSection(
context, context,
space,
widget.view, widget.view,
widget.parentView, widget.parentView,
widget.spaceType, widget.spaceType,
@ -802,6 +825,7 @@ bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) {
void _moveViewCrossSection( void _moveViewCrossSection(
BuildContext context, BuildContext context,
ViewPB? toSpace,
ViewPB view, ViewPB view,
ViewPB? parentView, ViewPB? parentView,
FolderSpaceType spaceType, FolderSpaceType spaceType,
@ -822,6 +846,17 @@ void _moveViewCrossSection(
final toSection = spaceType == FolderSpaceType.public final toSection = spaceType == FolderSpaceType.public
? ViewSectionPB.Public ? ViewSectionPB.Public
: ViewSectionPB.Private; : ViewSectionPB.Private;
final currentSpace = context.read<SpaceBloc>().state.currentSpace;
if (currentSpace != null &&
toSpace != null &&
currentSpace.id != toSpace.id) {
Log.info(
'Move view(${from.name}) to another space(${toSpace.name}), unpublish the view',
);
context.read<ViewBloc>().add(const ViewEvent.unpublish(sync: false));
}
context.read<ViewBloc>().add( context.read<ViewBloc>().add(
ViewEvent.move( ViewEvent.move(
from, from,

View File

@ -192,8 +192,8 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
sourceView: sourceView, sourceView: sourceView,
userProfile: userProfile, userProfile: userProfile,
workspaceId: workspaceId, workspaceId: workspaceId,
onSelected: (view) { onSelected: (space, view) {
onTap(controller, view); onTap(controller, (space, view));
}, },
); );
}, },

View File

@ -53,7 +53,7 @@ class SettingsAIView extends StatelessWidget {
AIModelSelection(), AIModelSelection(),
_AISearchToggle(value: false), _AISearchToggle(value: false),
// Disable local AI configuration for now. It's not ready for production. // Disable local AI configuration for now. It's not ready for production.
// LocalAIConfiguration(), LocalAIConfiguration(),
], ],
); );
}, },

View File

@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/color_to_hex_string.dart'; import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flutter/material.dart';
class DocumentColorSettingButton extends StatefulWidget { class DocumentColorSettingButton extends StatefulWidget {
const DocumentColorSettingButton({ const DocumentColorSettingButton({
@ -90,24 +92,11 @@ class DocumentColorSettingDialogState
late TextEditingController hexController; late TextEditingController hexController;
late TextEditingController opacityController; late TextEditingController opacityController;
void updateSelectedColor() {
if (widget.formKey.currentState!.validate()) {
setState(() {
final colorValue = int.tryParse(
hexController.text.combineHexWithOpacity(opacityController.text),
);
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
selectedColorOnDialog = Color(colorValue!);
widget.onChanged(selectedColorOnDialog!);
});
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
selectedColorOnDialog = widget.currentColor; selectedColorOnDialog = widget.currentColor;
currentColorHexString = widget.currentColor.toHexString(); currentColorHexString = ColorExtension(widget.currentColor).toHexString();
hexController = TextEditingController( hexController = TextEditingController(
text: currentColorHexString.extractHex(), text: currentColorHexString.extractHex(),
); );
@ -145,17 +134,25 @@ class DocumentColorSettingDialogState
controller: hexController, controller: hexController,
labelText: LocaleKeys.editor_hexValue.tr(), labelText: LocaleKeys.editor_hexValue.tr(),
hintText: '6fc9e7', hintText: '6fc9e7',
onChanged: (_) => updateSelectedColor(), onChanged: (_) => _updateSelectedColor(),
onFieldSubmitted: (_) => updateSelectedColor(), onFieldSubmitted: (_) => _updateSelectedColor(),
validator: (v) => validateHexValue(v, opacityController.text), validator: (v) => validateHexValue(v, opacityController.text),
suffixIcon: GestureDetector(
onTap: () => _showColorPickerDialog(
context: context,
currentColor: widget.currentColor,
updateColor: _updateColor,
),
child: const Icon(Icons.color_lens_rounded),
),
), ),
const VSpace(8), const VSpace(8),
_ColorSettingTextField( _ColorSettingTextField(
controller: opacityController, controller: opacityController,
labelText: LocaleKeys.editor_opacity.tr(), labelText: LocaleKeys.editor_opacity.tr(),
hintText: '50', hintText: '50',
onChanged: (_) => updateSelectedColor(), onChanged: (_) => _updateSelectedColor(),
onFieldSubmitted: (_) => updateSelectedColor(), onFieldSubmitted: (_) => _updateSelectedColor(),
validator: (value) => validateOpacityValue(value), validator: (value) => validateOpacityValue(value),
), ),
], ],
@ -164,6 +161,28 @@ class DocumentColorSettingDialogState
], ],
); );
} }
void _updateSelectedColor() {
if (widget.formKey.currentState!.validate()) {
setState(() {
final colorValue = int.tryParse(
hexController.text.combineHexWithOpacity(opacityController.text),
);
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
selectedColorOnDialog = Color(colorValue!);
widget.onChanged(selectedColorOnDialog!);
});
}
}
void _updateColor(Color color) {
setState(() {
hexController.text = ColorExtension(color).toHexString().extractHex();
opacityController.text =
ColorExtension(color).toHexString().extractOpacity();
});
_updateSelectedColor();
}
} }
class _ColorSettingTextField extends StatelessWidget { class _ColorSettingTextField extends StatelessWidget {
@ -172,6 +191,7 @@ class _ColorSettingTextField extends StatelessWidget {
required this.labelText, required this.labelText,
required this.hintText, required this.hintText,
required this.onFieldSubmitted, required this.onFieldSubmitted,
this.suffixIcon,
this.onChanged, this.onChanged,
this.validator, this.validator,
}); });
@ -180,6 +200,7 @@ class _ColorSettingTextField extends StatelessWidget {
final String labelText; final String labelText;
final String hintText; final String hintText;
final void Function(String) onFieldSubmitted; final void Function(String) onFieldSubmitted;
final Widget? suffixIcon;
final void Function(String)? onChanged; final void Function(String)? onChanged;
final String? Function(String?)? validator; final String? Function(String?)? validator;
@ -191,6 +212,7 @@ class _ColorSettingTextField extends StatelessWidget {
decoration: InputDecoration( decoration: InputDecoration(
labelText: labelText, labelText: labelText,
hintText: hintText, hintText: hintText,
suffixIcon: suffixIcon,
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide(color: style.colorScheme.outline), borderSide: BorderSide(color: style.colorScheme.outline),
), ),
@ -241,3 +263,145 @@ String? validateOpacityValue(String? value) {
} }
return null; return null;
} }
const _kColorCircleWidth = 46.0;
const _kColorCircleHeight = 46.0;
const _kColorCircleRadius = 23.0;
const _kColorOpacityThumbRadius = 23.0;
const _kDialogButtonPaddingHorizontal = 24.0;
const _kDialogButtonPaddingVertical = 12.0;
const _kColorsColumnSpacing = 3.0;
class _ColorPicker extends StatelessWidget {
const _ColorPicker({
required this.selectedColor,
required this.onColorChanged,
});
final Color selectedColor;
final void Function(Color) onColorChanged;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
child: ColorPicker(
width: _kColorCircleWidth,
height: _kColorCircleHeight,
borderRadius: _kColorCircleRadius,
enableOpacity: true,
opacityThumbRadius: _kColorOpacityThumbRadius,
columnSpacing: _kColorsColumnSpacing,
enableTooltips: false,
pickersEnabled: const {
ColorPickerType.both: false,
ColorPickerType.primary: true,
ColorPickerType.accent: true,
ColorPickerType.wheel: true,
},
subheading: Text(
LocaleKeys.settings_appearance_documentSettings_colorShade.tr(),
style: theme.textTheme.labelLarge,
),
opacitySubheading: Text(
LocaleKeys.settings_appearance_documentSettings_opacity.tr(),
style: theme.textTheme.labelLarge,
),
onColorChanged: onColorChanged,
),
);
}
}
class _ColorPickerActions extends StatelessWidget {
const _ColorPickerActions({
required this.onReset,
required this.onUpdate,
});
final VoidCallback onReset;
final VoidCallback onUpdate;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 24,
child: FlowyTextButton(
LocaleKeys.button_cancel.tr(),
padding: const EdgeInsets.symmetric(
horizontal: _kDialogButtonPaddingHorizontal,
vertical: _kDialogButtonPaddingVertical,
),
fontColor: AFThemeExtension.of(context).textColor,
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
radius: Corners.s12Border,
onPressed: onReset,
),
),
const HSpace(8),
SizedBox(
height: 48,
child: FlowyTextButton(
LocaleKeys.button_done.tr(),
padding: const EdgeInsets.symmetric(
horizontal: _kDialogButtonPaddingHorizontal,
vertical: _kDialogButtonPaddingVertical,
),
radius: Corners.s12Border,
fontHoverColor: Colors.white,
fillColor: Theme.of(context).colorScheme.primary,
hoverColor: const Color(0xFF005483),
onPressed: onUpdate,
),
),
],
);
}
}
void _showColorPickerDialog({
required BuildContext context,
String? title,
required Color currentColor,
required void Function(Color) updateColor,
}) {
final style = Theme.of(context);
Color selectedColor = currentColor;
showDialog(
context: context,
barrierColor: const Color.fromARGB(128, 0, 0, 0),
builder: (context) {
return AlertDialog(
icon: const Icon(Icons.palette),
title: Text(
title ??
LocaleKeys.settings_appearance_documentSettings_pickColor.tr(),
style: style.textTheme.titleLarge,
),
content: _ColorPicker(
selectedColor: selectedColor,
onColorChanged: (color) => selectedColor = color,
),
actionsPadding: const EdgeInsets.all(8),
actions: [
_ColorPickerActions(
onReset: () {
updateColor(currentColor);
Navigator.of(context).pop();
},
onUpdate: () {
updateColor(selectedColor);
Navigator.of(context).pop();
},
),
],
);
},
);
}

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
@ -10,6 +9,8 @@ import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:toastification/toastification.dart';
export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
@ -283,3 +284,58 @@ class OkCancelButton extends StatelessWidget {
); );
} }
} }
void showToastNotification(
BuildContext context, {
required String message,
String? description,
}) {
toastification.show(
context: context,
type: ToastificationType.success,
style: ToastificationStyle.flat,
title: FlowyText(message),
description: description != null
? FlowyText.regular(
description,
fontSize: 12,
lineHeight: 1.2,
maxLines: 3,
)
: null,
alignment: Alignment.bottomCenter,
autoCloseDuration: const Duration(milliseconds: 3000),
showProgressBar: false,
backgroundColor: Theme.of(context).colorScheme.surface,
borderSide: BorderSide(
color: Colors.grey.withOpacity(0.4),
),
);
}
Future<void> showConfirmDeletionDialog({
required BuildContext context,
required String name,
required String description,
required VoidCallback onConfirm,
}) {
return showDialog(
context: context,
builder: (_) {
final title = LocaleKeys.space_deleteConfirmation.tr() + name;
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: SizedBox(
width: 440,
child: ConfirmDeletionPopup(
title: title,
description: description,
onConfirm: onConfirm,
),
),
);
},
);
}

View File

@ -1,9 +1,12 @@
import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/startup/tasks/rust_sdk.dart'; import 'package:appflowy/startup/tasks/rust_sdk.dart';
import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
@ -203,10 +206,25 @@ class FlowyVersionDescription extends CustomActionCell {
thickness: 1.0, thickness: 1.0,
), ),
const VSpace(6), const VSpace(6),
FlowyText( GestureDetector(
"$appName $version", behavior: HitTestBehavior.opaque,
onDoubleTap: () {
if (Env.internalBuild != '1') {
return;
}
enableDocumentInternalLog = !enableDocumentInternalLog;
showToastNotification(
context,
message: enableDocumentInternalLog
? 'Enabled Internal Log'
: 'Disabled Internal Log',
);
},
child: FlowyText(
'$appName $version',
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
), ),
),
], ],
).padding( ).padding(
horizontal: ActionListSizes.itemHPadding, horizontal: ActionListSizes.itemHPadding,

View File

@ -1,14 +1,15 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
enum ViewActionType { enum ViewActionType {
@ -46,8 +47,8 @@ class ViewAction extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlowyButton( return FlowyButton(
onTap: () { onTap: () async {
context.read<ViewBloc>().add(type.actionEvent); await _onAction(context);
mutex?.close(); mutex?.close();
}, },
text: FlowyText.regular( text: FlowyText.regular(
@ -63,4 +64,25 @@ class ViewAction extends StatelessWidget {
hoverColor: AFThemeExtension.of(context).lightGreyHover, hoverColor: AFThemeExtension.of(context).lightGreyHover,
); );
} }
Future<void> _onAction(BuildContext context) async {
if (type == ViewActionType.delete) {
final (containPublishedPage, _) =
await ViewBackendService.containPublishedPage(view);
if (containPublishedPage && context.mounted) {
await showConfirmDeletionDialog(
context: context,
name: view.name,
description: LocaleKeys.publish_containsPublishedPage.tr(),
onConfirm: () {
context.read<ViewBloc>().add(const ViewEvent.delete());
},
);
} else if (context.mounted) {
context.read<ViewBloc>().add(const ViewEvent.delete());
}
} else {
context.read<ViewBloc>().add(type.actionEvent);
}
}
} }

View File

@ -37,7 +37,9 @@ class ViewTitleBar extends StatelessWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: SizedBox( child: SizedBox(
height: 24, height: 24,
child: Row(children: _buildViewTitles(context, ancestors)), child: Row(
children: _buildViewTitles(context, ancestors),
),
), ),
); );
}, },
@ -76,6 +78,7 @@ class ViewTitleBar extends StatelessWidget {
} }
final child = FlowyTooltip( final child = FlowyTooltip(
key: ValueKey(view.id),
message: view.name, message: view.name,
child: _ViewTitle( child: _ViewTitle(
view: view, view: view,

View File

@ -1,10 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:flowy_infra/size.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flowy_infra/size.dart';
class FlowyTextField extends StatefulWidget { class FlowyTextField extends StatefulWidget {
final String? hintText; final String? hintText;
final String? text; final String? text;
@ -37,6 +36,8 @@ class FlowyTextField extends StatefulWidget {
final List<TextInputFormatter>? inputFormatters; final List<TextInputFormatter>? inputFormatters;
final bool obscureText; final bool obscureText;
final bool isDense; final bool isDense;
final bool readOnly;
final Color? enableBorderColor;
const FlowyTextField({ const FlowyTextField({
super.key, super.key,
@ -71,6 +72,8 @@ class FlowyTextField extends StatefulWidget {
this.inputFormatters, this.inputFormatters,
this.obscureText = false, this.obscureText = false,
this.isDense = true, this.isDense = true,
this.readOnly = false,
this.enableBorderColor,
}); });
@override @override
@ -144,6 +147,7 @@ class FlowyTextFieldState extends State<FlowyTextField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextField( return TextField(
readOnly: widget.readOnly,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
onChanged: (text) { onChanged: (text) {
@ -178,7 +182,8 @@ class FlowyTextFieldState extends State<FlowyTextField> {
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: Corners.s8Border, borderRadius: Corners.s8Border,
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline, color: widget.enableBorderColor ??
Theme.of(context).colorScheme.outline,
), ),
), ),
isDense: false, isDense: false,
@ -199,7 +204,10 @@ class FlowyTextFieldState extends State<FlowyTextField> {
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: Corners.s8Border, borderRadius: Corners.s8Border,
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, color: widget.readOnly
? widget.enableBorderColor ??
Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary,
), ),
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(

View File

@ -13,6 +13,7 @@ class RoundedTextButton extends StatelessWidget {
final Color? hoverColor; final Color? hoverColor;
final Color? textColor; final Color? textColor;
final double? fontSize; final double? fontSize;
final FontWeight? fontWeight;
final EdgeInsets padding; final EdgeInsets padding;
const RoundedTextButton({ const RoundedTextButton({
@ -27,6 +28,7 @@ class RoundedTextButton extends StatelessWidget {
this.hoverColor, this.hoverColor,
this.textColor, this.textColor,
this.fontSize, this.fontSize,
this.fontWeight,
this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
}); });
@ -42,6 +44,7 @@ class RoundedTextButton extends StatelessWidget {
child: SizedBox.expand( child: SizedBox.expand(
child: FlowyTextButton( child: FlowyTextButton(
title ?? '', title ?? '',
fontWeight: fontWeight,
onPressed: onPressed, onPressed: onPressed,
fontSize: fontSize, fontSize: fontSize,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@ -53,11 +53,11 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: e8ee051 ref: d2d9873
resolved-ref: e8ee051719eded6621ccdc2722f696411c020209 resolved-ref: d2d987312d3a667336c7e12c36da7dbbb62d66db
url: "https://github.com/AppFlowy-IO/appflowy-editor.git" url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git source: git
version: "3.0.0" version: "3.1.0"
appflowy_editor_plugins: appflowy_editor_plugins:
dependency: "direct main" dependency: "direct main"
description: description:
@ -121,6 +121,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003
url: "https://pub.dev"
source: hosted
version: "2.2.8"
bidi:
dependency: transitive
description:
name: bidi
sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63"
url: "https://pub.dev"
source: hosted
version: "2.0.10"
bitsdojo_window: bitsdojo_window:
dependency: "direct main" dependency: "direct main"
description: description:
@ -394,6 +410,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" 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: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -578,6 +602,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
flex_color_picker:
dependency: "direct main"
description:
name: flex_color_picker
sha256: "809af4ec82ede3b140ed0219b97d548de99e47aa4b99b14a10f705a2dbbcba5e"
url: "https://pub.dev"
source: hosted
version: "3.5.1"
flex_seed_scheme:
dependency: transitive
description:
name: flex_seed_scheme
sha256: "6c595e545b0678e1fe17e8eec3d1fbca7237482da194fadc20ad8607dc7a7f3d"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flowy_infra: flowy_infra:
dependency: "direct main" dependency: "direct main"
description: description:
@ -949,6 +989,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
iconsax_flutter:
dependency: transitive
description:
name: iconsax_flutter
sha256: "95b65699da8ea98f87c5d232f06b0debaaf1ec1332b697e4d90969ec9a93037d"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
image:
dependency: transitive
description:
name: image
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
image_gallery_saver: image_gallery_saver:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1203,7 +1259,7 @@ packages:
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
markdown: markdown:
dependency: transitive dependency: "direct main"
description: description:
name: markdown name: markdown
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
@ -1410,6 +1466,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.2.1"
pausable_timer:
dependency: transitive
description:
name: pausable_timer
sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074"
url: "https://pub.dev"
source: hosted
version: "3.1.0+3"
pdf:
dependency: transitive
description:
name: pdf
sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0"
url: "https://pub.dev"
source: hosted
version: "3.11.0"
pdf_widget_wrapper:
dependency: transitive
description:
name: pdf_widget_wrapper
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
url: "https://pub.dev"
source: hosted
version: "1.0.4"
percent_indicator: percent_indicator:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1530,6 +1610,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
printing:
dependency: transitive
description:
name: printing
sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3
url: "https://pub.dev"
source: hosted
version: "5.13.1"
process: process:
dependency: transitive dependency: transitive
description: description:
@ -1570,6 +1658,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" version: "1.2.3"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
realtime_client: realtime_client:
dependency: transitive dependency: transitive
description: description:
@ -2049,6 +2145,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
toastification:
dependency: "direct main"
description:
name: toastification
sha256: "5e751acc2fb5b8d008138dac255d62290fde4e5a24824f29809ac098c3dfe395"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
tuple: tuple:
dependency: transitive dependency: transitive
description: description:

View File

@ -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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.2 version: 0.6.3
environment: environment:
flutter: ">=3.22.0" flutter: ">=3.22.0"
@ -66,6 +66,7 @@ dependencies:
styled_widget: ^0.4.1 styled_widget: ^0.4.1
expandable: ^5.0.1 expandable: ^5.0.1
flutter_colorpicker: ^1.0.3 flutter_colorpicker: ^1.0.3
flex_color_picker: ^3.5.1
highlight: ^0.7.0 highlight: ^0.7.0
package_info_plus: ^6.0.0 package_info_plus: ^6.0.0
url_launcher: ^6.1.11 url_launcher: ^6.1.11
@ -141,6 +142,7 @@ dependencies:
shimmer: ^3.0.0 shimmer: ^3.0.0
isolates: ^3.0.3+8 isolates: ^3.0.3+8
markdown_widget: ^2.3.2+6 markdown_widget: ^2.3.2+6
markdown:
# Window Manager for MacOS and Linux # Window Manager for MacOS and Linux
window_manager: ^0.3.9 window_manager: ^0.3.9
@ -149,6 +151,8 @@ dependencies:
bitsdojo_window: ^0.1.6 bitsdojo_window: ^0.1.6
flutter_highlight: ^0.7.0 flutter_highlight: ^0.7.0
custom_sliding_segmented_control: ^1.8.3
toastification: ^2.0.0
dev_dependencies: dev_dependencies:
flutter_lints: ^3.0.1 flutter_lints: ^3.0.1
@ -185,7 +189,7 @@ dependency_overrides:
appflowy_editor: appflowy_editor:
git: git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "e8ee051" ref: "d2d9873"
appflowy_editor_plugins: appflowy_editor_plugins:
git: git:

View File

@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -192,7 +192,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -203,6 +203,40 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "appflowy-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8"
dependencies = [
"anyhow",
"appflowy-plugin",
"bytes",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "appflowy-plugin"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8"
dependencies = [
"anyhow",
"cfg-if",
"crossbeam-utils",
"log",
"once_cell",
"parking_lot 0.12.1",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
]
[[package]] [[package]]
name = "appflowy_tauri" name = "appflowy_tauri"
version = "0.0.0" version = "0.0.0"
@ -772,7 +806,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
@ -821,7 +855,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api-entity" name = "client-api-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"collab-entity", "collab-entity",
"collab-rt-entity", "collab-rt-entity",
@ -833,7 +867,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -907,7 +941,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -931,7 +965,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-database" name = "collab-database"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -961,7 +995,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-document" name = "collab-document"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -981,7 +1015,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-entity" name = "collab-entity"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -996,7 +1030,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-folder" name = "collab-folder"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -1034,7 +1068,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-plugins" name = "collab-plugins"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1073,7 +1107,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -1098,7 +1132,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1115,7 +1149,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-user" name = "collab-user"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -1341,7 +1375,7 @@ dependencies = [
"cssparser-macros", "cssparser-macros",
"dtoa-short", "dtoa-short",
"itoa 1.0.6", "itoa 1.0.6",
"phf 0.8.0", "phf 0.11.2",
"smallvec", "smallvec",
] ]
@ -1452,7 +1486,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -1847,6 +1881,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"allo-isolate", "allo-isolate",
"anyhow", "anyhow",
"appflowy-local-ai",
"appflowy-plugin",
"bytes", "bytes",
"dashmap", "dashmap",
"flowy-chat-pub", "flowy-chat-pub",
@ -1854,7 +1890,6 @@ dependencies = [
"flowy-derive", "flowy-derive",
"flowy-error", "flowy-error",
"flowy-notification", "flowy-notification",
"flowy-sidecar",
"flowy-sqlite", "flowy-sqlite",
"futures", "futures",
"lib-dispatch", "lib-dispatch",
@ -2136,7 +2171,6 @@ dependencies = [
"fancy-regex 0.11.0", "fancy-regex 0.11.0",
"flowy-codegen", "flowy-codegen",
"flowy-derive", "flowy-derive",
"flowy-sidecar",
"flowy-sqlite", "flowy-sqlite",
"lib-dispatch", "lib-dispatch",
"protobuf", "protobuf",
@ -2172,12 +2206,14 @@ dependencies = [
"flowy-notification", "flowy-notification",
"flowy-search-pub", "flowy-search-pub",
"flowy-sqlite", "flowy-sqlite",
"futures",
"lazy_static", "lazy_static",
"lib-dispatch", "lib-dispatch",
"lib-infra", "lib-infra",
"nanoid", "nanoid",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"protobuf", "protobuf",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"strum_macros 0.21.1", "strum_macros 0.21.1",
@ -2198,6 +2234,7 @@ dependencies = [
"collab-entity", "collab-entity",
"collab-folder", "collab-folder",
"lib-infra", "lib-infra",
"serde",
"uuid", "uuid",
] ]
@ -2323,24 +2360,6 @@ dependencies = [
"serde_repr", "serde_repr",
] ]
[[package]]
name = "flowy-sidecar"
version = "0.1.0"
dependencies = [
"anyhow",
"crossbeam-utils",
"lib-infra",
"log",
"once_cell",
"parking_lot 0.12.1",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
]
[[package]] [[package]]
name = "flowy-sqlite" name = "flowy-sqlite"
version = "0.1.0" version = "0.1.0"
@ -2921,7 +2940,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -2938,7 +2957,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -3370,7 +3389,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -3865,7 +3884,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [ dependencies = [
"regex-automata", "regex-automata 0.1.10",
] ]
[[package]] [[package]]
@ -3892,9 +3911,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.5.0" version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "memmap2" name = "memmap2"
@ -4878,7 +4897,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [ dependencies = [
"bytes", "bytes",
"heck 0.4.1", "heck 0.4.1",
"itertools 0.10.5", "itertools 0.11.0",
"log", "log",
"multimap", "multimap",
"once_cell", "once_cell",
@ -4899,7 +4918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.10.5", "itertools 0.11.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.47", "syn 2.0.47",
@ -5225,13 +5244,14 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.8.4" version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [ dependencies = [
"aho-corasick 1.0.2", "aho-corasick 1.0.2",
"memchr", "memchr",
"regex-syntax 0.7.2", "regex-automata 0.4.7",
"regex-syntax 0.8.4",
] ]
[[package]] [[package]]
@ -5243,6 +5263,17 @@ dependencies = [
"regex-syntax 0.6.29", "regex-syntax 0.6.29",
] ]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick 1.0.2",
"memchr",
"regex-syntax 0.8.4",
]
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.29" version = "0.6.29"
@ -5251,9 +5282,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.7.2" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]] [[package]]
name = "rend" name = "rend"
@ -5863,7 +5894,7 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a2f92bb#a2f92bb13e7c3d33783359d02235ba4a5058a32f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -8122,9 +8153,9 @@ dependencies = [
[[package]] [[package]]
name = "yrs" name = "yrs"
version = "0.18.8" version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" checksum = "71df0198938b69f1eec0d5f19f591c6e4f2f770b0bf16f858428f6d91b8bb280"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"atomic_refcell", "atomic_refcell",

View File

@ -29,7 +29,7 @@ tokio = "1.34.0"
tokio-stream = "0.1.14" tokio-stream = "0.1.14"
async-trait = "0.1.74" async-trait = "0.1.74"
chrono = { version = "0.4.31", default-features = false, features = ["clock"] } chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
yrs = "0.18.8" yrs = "0.19.1"
# Please use the following script to update collab. # Please use the following script to update collab.
# Working directory: frontend # Working directory: frontend
# #
@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a2f92bb" }
[dependencies] [dependencies]
serde_json.workspace = true serde_json.workspace = true
@ -106,10 +106,26 @@ default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
[patch.crates-io] [patch.crates-io]
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } # Please use the following script to update collab.
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } # Working directory: frontend
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } #
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } # To update the commit ID, run:
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } # scripts/tool/update_collab_rev.sh new_rev_id
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } #
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } # To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
# Working directory: frontend
# To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" }

View File

@ -1,34 +0,0 @@
FROM oven/bun:latest
WORKDIR /app
RUN apt-get update && \
apt-get install -y nginx
RUN bun install cheerio pino axios pino-pretty
COPY . .
RUN addgroup --system nginx && \
adduser --system --no-create-home --disabled-login --ingroup nginx nginx
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
COPY dist/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/nginx.conf
COPY nginx-signed.crt /etc/ssl/certs/nginx-signed.crt
COPY nginx-signed.key /etc/ssl/private/nginx-signed.key
RUN chown -R nginx:nginx /etc/ssl/certs/nginx-signed.crt /etc/ssl/private/nginx-signed.key
COPY start.sh /app/start.sh
RUN chmod +x /app/start.sh
EXPOSE 80 443
CMD ["/app/start.sh"]

View File

@ -1,284 +1,163 @@
<div align="center"> <div align="center">
<div align="center">
<h1><code>AppFlowy Web Project</code></h1> <h1>AppFlowy Web</h1>
</div>
<div>Welcome to the AppFlowy Web Project, a robust and versatile platform designed to bring the innovative features of <img src="https://img.shields.io/badge/React-v18.2.0-blue"/>
AppFlowy to the web. This project uniquely supports running as a desktop application via Tauri, and offers web <img src="https://img.shields.io/badge/TypeScript-v4.9.5-blue"/>
interfaces powered by WebAssembly (WASM). Dive into an exceptional development experience with high performance and <img src="https://img.shields.io/badge/Nginx-v1.21.6-brightgreen"/>
extensive capabilities.</div> <img src="https://img.shields.io/badge/Bun-latest-black"/>
<img src="https://img.shields.io/badge/Docker-v20.10.12-blue"/>
</div> </div>
## 🐑 Features ## 🌟 Introduction
- **Cross-Platform Compatibility**: Seamlessly run on desktop environments with Tauri, and on any web browser through Welcome to the AppFlowy Web project! This project aims to bring the powerful features of AppFlowy to the web. Whether
WASM. you're a developer looking to contribute or a user eager to try out the latest features, this guide will help you get
- **High Performance**: Leverage the speed and efficiency of WebAssembly for your web interfaces. started.
- **Tauri Integration**: Build lightweight, secure, and efficient desktop applications.
- **Flexible Development**: Utilize a wide range of AppFlowy's functionalities in your web or desktop projects.
## 🚀 Getting Started AppFlowy Web is built with the following technologies:
### 🛠️ Prerequisites - **React**: A JavaScript library for building user interfaces.
- **TypeScript**: A typed superset of JavaScript that compiles to plain JavaScript.
- **Bun**: A fast all-in-one JavaScript runtime.
- **Nginx**: A high-performance web server.
- **Docker**: A platform to develop, ship, and run applications in containers.
Before you begin, ensure you have the following installed: ### Resource Sharing
- Node.js (v14 or later) To maintain consistency across different platforms, the Web project shares i18n translation files and Icons with the
- Rust (latest stable version) Flutter project. This ensures a unified user experience and reduces duplication of effort in maintaining these
- Tauri prerequisites for your operating system resources.
- PNPM (8.5.0)
### 🏗️ Installation - **i18n Translation Files**: The translation files are shared to provide a consistent localization experience across
both Web and Flutter applications. The path to the translation files is `frontend/resources/translations/`.
#### Clone the Repository > The translation files are stored in JSON format and contain translations for different languages. The files are
named according to the language code (e.g., `en.json` for English, `es.json` for Spanish, etc.).
- **Icons**: The icon set used in the Web project is the same as the one used in the Flutter project, ensuring visual
consistency. The icons are stored in the `frontend/resources/flowy_icons/` directory.
Let's dive in and get the project up and running! 🚀
## 🛠 Getting Started
### Prerequisites
Before you begin, make sure you have the following installed on your system:
- [Node.js](https://nodejs.org/) (v18.6.0) 🌳
- [pnpm](https://pnpm.io/) (package manager) 📦
- [Jest](https://jestjs.io/) (testing framework) 🃏
- [Cypress](https://www.cypress.io/) (end-to-end testing) 🧪
### Clone the Repository
First, clone the repository to your local machine:
```bash ```bash
git clone https://github.com/AppFlowy-IO/AppFlowy git clone https://github.com/AppFlowy-IO/AppFlowy.git
cd frontend/appflowy_web_app
``` ```
#### 🌐 Install the frontend dependencies: ### Install Dependencies
Install the required dependencies using pnpm:
```bash ```bash
cd frontend/appflowy_web_app ## ensure you have pnpm installed, if not run the following command
# npm install -g pnpm@8.5.0
pnpm install pnpm install
``` ```
#### 🖥️ Desktop Application (Tauri) (Optional) ### Configure Environment Variables
> **Note**: if you want to run the web app in the browser, skip this step Create a `.env` file in the root of the project and add the following environment variables:
- Follow the instructions [here](https://tauri.app/v1/guides/getting-started/prerequisites/) to install Tauri
##### Windows and Linux Prerequisites
###### Windows only
- Install the Duckscript CLI and vcpkg
```bash ```bash
cargo install --force duckscript_cli AF_BASE_URL=http://localhost:8080
vcpkg integrate install AF_GOTRUE_URL=http://localhost:9999
AF_WS_URL=ws://localhost:8080/ws/v1
``` ```
###### Linux only ### Start the Development Server
- Install the required dependencies To start the development server, run the following command:
```bash
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
```
- **Get error**: failed to run custom build command for librocksdb-sys v6.11.4
```bash
sudo apt install clang
```
##### Install Tauri Dependencies
- Install cargo-make
```bash
cargo install --force cargo-make
```
- Install AppFlowy dev tools
```bash
# install development tools
# make sure you are in the root directory of the project
cd frontend
cargo make appflowy-tauri-deps-tools
```
- Build the service/dependency
```bash
# make sure you are in the root directory of the project
cd frontend/appflowy_web_app
mkdir dist
cd src-tauri
cargo build
```
### 🚀 Running the Application
#### 🌐 Web Application
- Run the web application
```bash ```bash
pnpm run dev pnpm run dev
``` ```
- Open your browser and navigate to `http://localhost:3000`, You can now interact with the AppFlowy web application
#### 🖥️ Desktop Application (Tauri) ### 🚀 Building for Production(Optional)
**Ensure close web application before running the desktop application** if you want to run the production build, use the following commands
- Run the desktop application
```bash ```bash
pnpm run tauri:dev pnpm run build
pnpm run start
``` ```
- The AppFlowy desktop application will open, and you can interact with it
### 🛠️ Development This will start the application in development mode. Open http://localhost:3000 to view it in the browser.
#### How to add or modify i18n keys ## 🧪 Running Tests
- Modify the i18n files in `frontend/resources/translations/en.json` to add or modify i18n keys ### Unit Tests
- Run the following command to update the i18n keys in the application
We use **Jest** for running unit tests. To run the tests, use the following command:
```bash ```bash
pnpm run sync:i18n pnpm run test:unit
``` ```
#### How to modify the theme This will execute all the unit tests in the project and provide a summary of the results. ✅
Don't modify the theme file in `frontend/appflowy_web_app/src/styles/variables` directly) ### Components Tests
- Modify the theme file in `frontend/appflowy_web_app/style-dictionary/tokens/base.json( or dark.json or light.json)` to We use **Cypress** for end-to-end testing. To run the Cypress tests, use the following command:
add or modify theme keys
- Run the following command to update the theme in the application
```bash ```bash
pnpm run css:variables pnpm run cypress:open
``` ```
#### How to add or modify the environment variables This will open the Cypress Test Runner where you can run your end-to-end tests. 🧪
- Modify the environment file in `frontend/appflowy_web_app/.env` to add or modify environment variables Alternatively, to run Cypress tests in the headless mode, use:
#### How to create symlink for the @appflowyinc/client-api-wasm in local development
- Run the following command to create a symlink for the @appflowyinc/client-api-wasm
```bash
# ensure you are in the frontend/appflowy_web_app directory
pnpm run link:client-api $source_path $target_path
# Example
# pnpm run link:client-api ../../../AppFlowy-Cloud/libs/client-api-wasm/pkg ./node_modules/@appflowyinc/client-api-wasm
```
### 📝 About the Project
#### 📁 Directory Structure
- `frontend/appflowy_web_app`: Contains the web application source code
- `frontend/appflowy_web_app/src`: Contains the app entry point and the source code
- `frontend/appflowy_web_app/src/components`: Contains the react components
- `frontend/appflowy_web_app/src/styles`: Contains the styles for the application
- `frontend/appflowy_web_app/src/utils`: Contains the utility functions
- `frontend/appflowy_web_app/src/i18n`: Contains the i18n files
- `frontend/appflowy_web_app/src/assets`: Contains the assets for the application
- `frontend/appflowy_web_app/src/store`: Contains the redux store
- `frontend/appflowy_web_app/src/@types`: Contains the typescript types
- `frontend/appflowy_web_app/src/applications/services`: Contains the services for the application. In vite.config.ts,
we have defined the alias for the services directory for different environments(Tauri/Web)
```typescript
resolve: {
alias: [
// ...
{
find: '$client-services',
replacement: !!process.env.TAURI_PLATFORM
? `${__dirname}/src/application/services/tauri-services`
: `${__dirname}/src/application/services/js-services`,
},
]
}
```
### 📦 Deployment
Use the AppFlowy CI/CD pipeline to deploy the application to the test and production environments.
- Push the changes to the main branch
- Deploy Test Environment
- Automatically, the test environment will be deployed if merged to the main branch or build/test branch
- Deploy Production Environment
- Navigate to the Actions tab
- Click on the workflow and select the Run workflow
- Enter the options
- Click on the Run workflow button
#### 📦 Deployment (Self-Hosted EC2)
##### Pre-requisites
Please ensure you have learned about:
- [Deploy Web application on AWS Cloud using EC2 Instance](https://www.youtube.com/watch?v=gWVIIU1ev0Y)
- [How to Install and Use Rsync Command](https://operavps.com/docs/install-rsync-command-in-linux/)
- [How to Use ssh-keygen to Generate a New SSH Key?](https://www.ssh.com/academy/ssh/keygen)
- [Linux post-installation steps for Docker Engine](https://docs.docker.com/engine/install/linux-postinstall/)
- [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html)
And then follow the steps below:
1. Ensure you have the following installed on your server:
- Docker: [Install Docker](https://docs.docker.com/engine/install/)
- Rsync: [Install Rsync](https://operavps.com/docs/install-rsync-command-in-linux/)
2. Create a new user for deploy, and generate an SSH key for the user
```bash
sudo adduser appflowy(or any name)
sudo su - appflowy
mkdir ~/.ssh
chmod 700 ~/.ssh
ssh-keygen -t rsa
chmod 600 ~/.ssh/authorized_keys
# add the user to the docker group, to run docker commands without sudo
sudo usermod -aG docker ${USER}
```
- visit the `~/.ssh/id_rsa` and `~/.ssh/id_rsa.pub` to get the private and public key respectively
- add the public key to the `~/.ssh/authorized_keys` file
- ensure the private key is kept safe
- exit and login back to the server with the new
user: `ssh -i your-existing-key.pem ec2-user@your-instance-public-dns`
3. Clone the AppFlowy repository
4. Set the following secrets in your
repository, have to
know [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions)
> Note: Test Environment: prefix the secret with `WEB_TEST_` and Production Environment: prefix the secret with `WEB_`
> for example, `WEB_TEST_SSH_PRIVATE_KEY` and `WEB_SSH_PRIVATE_KEY`
- `SSH_PRIVATE_KEY`: The private key generated in step 2: cat ~/.ssh/id_rsa
- `REMOTE_HOST`: The host of the server: `your-instance-public-dns` or `your-instance-ip`
- `REMOTE_USER`: The user created in step 2: `appflowy`
- `SSL_CERTIFICATE`: The SSL certificate for the
server - [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html)
- `SSL_CERTIFICATE_KEY`: The SSL certificate key for the
server - [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html)
5. Run the deployment workflow to deploy the application(production or test environment)
> Note: the test server will **automatically** deploy if merged to the main branch or build/test branch
### 🧪 Testing
> We use Cypress for end-to-end testing and component testing - [Cypress](https://www.cypress.io/)
#### 🧪 End-to-End Testing
> to be continued
#### 🧪 Component Testing
Run the following command to run the component tests
```bash ```bash
pnpm run test:components pnpm run test:components
``` ```
Both commands will provide detailed test results and generate a code coverage report.
## 🔄 Development Workflow
### Linting
To maintain code quality, we use **ESLint**. To run the linter and fix any linting errors, use the following command:
```bash
pnpm run lint
```
## 🚀 Production Deployment
Our production deployment process is automated using GitHub Actions. The process involves:
1. **Setting up an AWS EC2 instance**: We use an EC2 instance to host the application.
2. **Installing Docker and Docker Compose**: Docker is installed on the AWS instance.
3. **Configuring SSH Access**: SSH access is set up with a user and password.
4. **Preparing Project Configuration**: We configure `Dockerfile`, `nginx.conf`, and `server.cjs` in the web project.
5. **Using GitHub Actions**: We use the easingthemes/ssh-deploy@main action to deploy the project to the remote server.
The deployment steps include building the Docker image and running the Docker container with the necessary port
mappings:
```bash
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
```
The Web server runs on Bun. For more details about Bun, please refer to the [Bun documentation](https://bun.sh/).

View File

@ -1,3 +0,0 @@
AF_WS_URL=wss://beta.appflowy.cloud/ws/v1
AF_BASE_URL=https://beta.appflowy.cloud
AF_GOTRUE_URL=https://beta.appflowy.cloud/gotrue

View File

@ -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"
}

View File

@ -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"
]
}
]

File diff suppressed because one or more lines are too long

View File

@ -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"
}
}

View File

@ -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."
}

View File

@ -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."}

View File

@ -1,6 +0,0 @@
{
"code": 0,
"data": {
"is_new": false
}
}

View File

@ -25,96 +25,9 @@
// -- This will overwrite an existing command -- // -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) // 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', () => { Cypress.Commands.add('mockAPI', () => {
cy.fixture('sign_in_success').then((json) => { // Mock the API
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');
}); });
// Example use: export {};
// 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);
});
});

View File

@ -7,6 +7,9 @@
<title>Components App</title> <title>Components App</title>
</head> </head>
<body id="body"> <body id="body">
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.26.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.26.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js"></script>
<div data-cy-root></div> <div data-cy-root></div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,32 @@
FROM oven/bun:latest
WORKDIR /app
RUN apt-get update && \
apt-get install -y nginx supervisor
RUN bun install cheerio pino pino-pretty
COPY . .
COPY supervisord.conf /app/supervisord.conf
RUN addgroup --system nginx && \
adduser --system --no-create-home --disabled-login --ingroup nginx nginx
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
COPY dist /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/nginx.conf
COPY start.sh /app/start.sh
RUN chmod +x /app/start.sh
RUN chmod +x /app/supervisord.conf
EXPOSE 80
CMD ["supervisord", "-c", "/app/supervisord.conf"]

View File

@ -0,0 +1,28 @@
if [ -z "$1" ]; then
echo "No port number provided"
exit 1
fi
PORT=$1
echo "Starting deployment on port $PORT"
rm -rf deploy
tar -xzf build-output.tar.gz
rm -rf build-output.tar.gz
mv dist deploy/dist
mv .env deploy/.env
cd deploy
docker system prune -f
docker build -t appflowy-web-app-"$PORT" .
docker rm -f appflowy-web-app-"$PORT" || true
docker run -d --env-file .env -p "$PORT":80 --restart always --name appflowy-web-app-"$PORT" appflowy-web-app-"$PORT"

View File

@ -40,29 +40,6 @@ http {
server { server {
listen 80; listen 80;
server_name localhost; server_name localhost;
#server_name appflowy.com *.appflowy.com;
location / {
return 301 https://$host$request_uri;
}
}
# Additional server block for HTTPS
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name localhost;
#server_name appflowy.com *.appflowy.com;
ssl_certificate /etc/ssl/certs/nginx-signed.crt;
ssl_certificate_key /etc/ssl/private/nginx-signed.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / { location / {
proxy_pass http://localhost:3000; proxy_pass http://localhost:3000;
@ -89,6 +66,18 @@ http {
access_log off; access_log off;
} }
location /og-image.png {
root /usr/share/nginx/html;
expires 30d;
access_log off;
}
location /covers/ {
root /usr/share/nginx/html;
expires 30d;
access_log off;
}
error_page 404 /404.html; error_page 404 /404.html;
location = /404.html { location = /404.html {
root /usr/share/nginx/html; root /usr/share/nginx/html;
@ -98,5 +87,6 @@ http {
location = /50x.html { location = /50x.html {
root /usr/share/nginx/html; root /usr/share/nginx/html;
} }
} }
} }

View File

@ -0,0 +1,193 @@
import path from 'path';
import * as fs from 'fs';
import pino from 'pino';
import { type CheerioAPI, load } from 'cheerio';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import { fetch } from 'bun';
const distDir = path.join(__dirname, 'dist');
const indexPath = path.join(distDir, 'index.html');
const baseURL = process.env.AF_BASE_URL as string;
const defaultSite = 'https://appflowy.io';
const setOrUpdateMetaTag = ($: CheerioAPI, selector: string, attribute: string, content: string) => {
if ($(selector).length === 0) {
$('head').append(`<meta ${attribute}="${selector.match(/\[(.*?)\]/)?.[1]}" content="${content}">`);
} else {
$(selector).attr('content', content);
}
};
const logger = pino({
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
destination: `${__dirname}/pino-logger.log`,
},
},
level: 'info',
});
const logRequestTimer = (req: Request) => {
const start = Date.now();
const pathname = new URL(req.url).pathname;
logger.info(`Incoming request: ${pathname}`);
return () => {
const duration = Date.now() - start;
logger.info(`Request for ${pathname} took ${duration}ms`);
};
};
const fetchMetaData = async (url: string) => {
logger.info(`Fetching meta data from ${url}`);
try {
const response = await fetch(url, {
verbose: true,
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
} catch (error) {
logger.error(`Error fetching meta data ${error}`);
return null;
}
};
const createServer = async (req: Request) => {
const timer = logRequestTimer(req);
const reqUrl = new URL(req.url);
const hostname = req.headers.get('host');
logger.info(`Request URL: ${hostname}${reqUrl.pathname}`);
const [namespace, publishName] = reqUrl.pathname.slice(1).split('/');
logger.info(`Namespace: ${namespace}, Publish Name: ${publishName}`);
if (req.method === 'GET') {
if (namespace === '' || !publishName) {
timer();
return new Response(null, {
status: 302,
headers: {
Location: defaultSite,
},
});
}
let metaData;
try {
metaData = await fetchMetaData(`${baseURL}/api/workspace/published/${namespace}/${publishName}`);
} catch (error) {
logger.error(`Error fetching meta data: ${error}`);
}
const htmlData = fs.readFileSync(indexPath, 'utf8');
const $ = load(htmlData);
const description = 'Write, share, and publish docs quickly on AppFlowy.\nGet started for free.';
let title = 'AppFlowy';
const url = `https://${hostname}${reqUrl.pathname}`;
let image = '/og-image.png';
let favicon = '/appflowy.svg';
try {
if (metaData && metaData.view) {
const view = metaData.view;
const emoji = view.icon.value;
const titleList = [];
if (emoji) {
const emojiCode = emoji.codePointAt(0).toString(16); // Convert emoji to hex code
const baseUrl = 'https://raw.githubusercontent.com/googlefonts/noto-emoji/main/svg/emoji_u';
favicon = `${baseUrl}${emojiCode}.svg`;
}
if (view.name) {
titleList.push(view.name);
titleList.push('|');
}
titleList.push('AppFlowy');
title = titleList.join(' ');
try {
const cover = view.extra ? JSON.parse(view.extra)?.cover : null;
if (cover) {
if (['unsplash', 'custom'].includes(cover.type)) {
image = cover.value;
} else if (cover.type === 'built_in') {
image = `/covers/m_cover_image_${cover.value}.png`;
}
}
} catch (_) {
// Do nothing
}
}
} catch (error) {
logger.error(`Error injecting meta data: ${error}`);
}
$('title').text(title);
$('link[rel="icon"]').attr('href', favicon);
setOrUpdateMetaTag($, 'meta[name="description"]', 'name', description);
setOrUpdateMetaTag($, 'meta[property="og:title"]', 'property', title);
setOrUpdateMetaTag($, 'meta[property="og:description"]', 'property', description);
setOrUpdateMetaTag($, 'meta[property="og:image"]', 'property', image);
setOrUpdateMetaTag($, 'meta[property="og:url"]', 'property', url);
setOrUpdateMetaTag($, 'meta[property="og:site_name"]', 'property', 'AppFlowy');
setOrUpdateMetaTag($, 'meta[property="og:type"]', 'property', 'website');
setOrUpdateMetaTag($, 'meta[name="twitter:card"]', 'name', 'summary_large_image');
setOrUpdateMetaTag($, 'meta[name="twitter:title"]', 'name', title);
setOrUpdateMetaTag($, 'meta[name="twitter:description"]', 'name', description);
setOrUpdateMetaTag($, 'meta[name="twitter:image"]', 'name', image);
setOrUpdateMetaTag($, 'meta[name="twitter:site"]', 'name', '@appflowy');
timer();
return new Response($.html(), {
headers: { 'Content-Type': 'text/html' },
});
} else {
timer();
logger.error({ message: 'Method not allowed', method: req.method });
return new Response('Method not allowed', { status: 405 });
}
};
declare const Bun: {
serve: (options: { port: number; fetch: typeof createServer; error: (err: Error) => Response }) => void;
};
const start = () => {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
Bun.serve({
port: 3000,
fetch: createServer,
error: (err) => {
logger.error(`Internal Server Error: ${err}`);
return new Response('Internal Server Error', { status: 500 });
},
});
logger.info('Server is running on port 3000');
logger.info(`Base URL: ${baseURL}`);
} catch (err) {
logger.error(err);
process.exit(1);
}
};
start();
export {};

View File

@ -1,10 +1,12 @@
#!/bin/bash #!/usr/bin/env bash
# Start the frontend server
bun run server.cjs &
# Start the nginx server # Start the nginx server
service nginx start service nginx start
# Start the frontend server
bun run server.ts
tail -f /dev/null tail -f /dev/null

View File

@ -0,0 +1,9 @@
[supervisord]
nodaemon=true
[program:bun]
command=sh /app/start.sh
autostart=true
autorestart=true
stderr_logfile=/var/log/bun.err.log
stdout_logfile=/var/log/bun.out.log

View File

@ -13,7 +13,7 @@
content="AppFlowy is an AI collaborative workspace where you achieve more without losing control of your data" content="AppFlowy is an AI collaborative workspace where you achieve more without losing control of your data"
/> />
<meta property="og:image" <meta property="og:image"
content="https://d3uafhn8yrvdfn.cloudfront.net/website/production/_next/static/media/og-image.e347bfb5.png" content="/_next/static/media/og-image.e347bfb5.png"
/> />
<meta property="og:url" content="https://appflowy.com" /> <meta property="og:url" content="https://appflowy.com" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
@ -22,15 +22,17 @@
content="AppFlowy is an AI collaborative workspace where you achieve more without losing control of your data" content="AppFlowy is an AI collaborative workspace where you achieve more without losing control of your data"
/> />
<meta name="twitter:image" <meta name="twitter:image"
content="https://d3uafhn8yrvdfn.cloudfront.net/website/production/_next/static/media/og-image.e347bfb5.png" content="/_next/static/media/og-image.e347bfb5.png"
/> />
<meta name="twitter:site" content="@appflowy" /> <meta name="twitter:site" content="@appflowy" />
<meta name="twitter:creator" content="@appflowy" /> <meta name="twitter:creator" content="@appflowy" />
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap" rel="stylesheet">
</head> </head>
<body id="body"> <body id="body">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const userAgent = window.navigator.userAgent.toLowerCase(); const userAgent = window.navigator.userAgent.toLowerCase();
@ -62,5 +64,8 @@
} }
}); });
</script> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.26.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.26.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js"></script>
</body> </body>
</html> </html>

View File

@ -24,7 +24,7 @@
"coverage": "pnpm run test:unit && pnpm run test:components" "coverage": "pnpm run test:unit && pnpm run test:components"
}, },
"dependencies": { "dependencies": {
"@appflowyinc/client-api-wasm": "0.0.3", "@appflowyinc/client-api-wasm": "0.1.1",
"@atlaskit/primitives": "^5.5.3", "@atlaskit/primitives": "^5.5.3",
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
@ -43,6 +43,8 @@
"colorthief": "^2.4.0", "colorthief": "^2.4.0",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"decimal.js": "^10.4.3", "decimal.js": "^10.4.3",
"dexie": "^4.0.7",
"dexie-react-hooks": "^1.1.7",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.5.2",
"emoji-regex": "^10.2.1", "emoji-regex": "^10.2.1",
"events": "^3.3.0", "events": "^3.3.0",
@ -56,6 +58,7 @@
"katex": "^0.16.7", "katex": "^0.16.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"notistack": "^3.0.1",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"protoc-gen-ts": "0.8.7", "protoc-gen-ts": "0.8.7",
@ -136,6 +139,7 @@
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"babel-jest": "^29.6.2", "babel-jest": "^29.6.2",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"cheerio": "1.0.0-rc.12",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^13.7.2", "cypress": "^13.7.2",
"eslint": "^8.57.0", "eslint": "^8.57.0",
@ -145,6 +149,8 @@
"istanbul-lib-coverage": "^3.2.2", "istanbul-lib-coverage": "^3.2.2",
"jest-environment-jsdom": "^29.6.2", "jest-environment-jsdom": "^29.6.2",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"pino": "^9.2.0",
"pino-pretty": "^11.2.1",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"prettier": "2.8.4", "prettier": "2.8.4",
"prettier-plugin-tailwindcss": "^0.2.2", "prettier-plugin-tailwindcss": "^0.2.2",

View File

@ -1,9 +1,13 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies: dependencies:
'@appflowyinc/client-api-wasm': '@appflowyinc/client-api-wasm':
specifier: 0.0.3 specifier: 0.1.1
version: 0.0.3 version: 0.1.1
'@atlaskit/primitives': '@atlaskit/primitives':
specifier: ^5.5.3 specifier: ^5.5.3
version: 5.7.0(@types/react@18.2.66)(react@18.2.0) version: 5.7.0(@types/react@18.2.66)(react@18.2.0)
@ -58,6 +62,12 @@ dependencies:
decimal.js: decimal.js:
specifier: ^10.4.3 specifier: ^10.4.3
version: 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: emoji-mart:
specifier: ^5.5.2 specifier: ^5.5.2
version: 5.6.0 version: 5.6.0
@ -97,6 +107,9 @@ dependencies:
nanoid: nanoid:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.2 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: numeral:
specifier: ^2.0.6 specifier: ^2.0.6
version: 2.0.6 version: 2.0.6
@ -333,6 +346,9 @@ devDependencies:
chalk: chalk:
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2 version: 4.1.2
cheerio:
specifier: 1.0.0-rc.12
version: 1.0.0-rc.12
cross-env: cross-env:
specifier: ^7.0.3 specifier: ^7.0.3
version: 7.0.3 version: 7.0.3
@ -360,6 +376,12 @@ devDependencies:
nyc: nyc:
specifier: ^15.1.0 specifier: ^15.1.0
version: 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: postcss:
specifier: ^8.4.21 specifier: ^8.4.21
version: 8.4.21 version: 8.4.21
@ -429,8 +451,8 @@ packages:
'@jridgewell/gen-mapping': 0.3.5 '@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
/@appflowyinc/client-api-wasm@0.0.3: /@appflowyinc/client-api-wasm@0.1.1:
resolution: {integrity: sha512-ARjLhiDZ8MiZ9egWDbAX9VAdXXS30av+InCPLrS/iqCMYrhuuU9rxS9jQeNEB7jucFrj158gBRusimFN7P/lyw==} resolution: {integrity: sha512-7+/TCmzMi9KrxX3HFLJv9R6ON2AO5xQavV547ii7RZM8+5bZJakuf6+pnyCzOquQX07q3ZYwJCa3MIgDvficaA==}
dev: false dev: false
/@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0): /@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 deprecated: Use your platform's native atob() and btoa() methods instead
dev: true 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: /acorn-globals@7.0.1:
resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==}
dependencies: dependencies:
@ -4886,6 +4915,11 @@ packages:
engines: {node: '>= 4.0.0'} engines: {node: '>= 4.0.0'}
dev: true 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): /autoprefixer@10.4.13(postcss@8.4.21):
resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@ -5180,6 +5214,13 @@ packages:
ieee754: 1.2.1 ieee754: 1.2.1
dev: true 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: /cachedir@2.4.0:
resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -5289,6 +5330,30 @@ packages:
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
dev: true 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: /chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
@ -5739,6 +5804,10 @@ packages:
dependencies: dependencies:
'@babel/runtime': 7.24.1 '@babel/runtime': 7.24.1
/dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
dev: true
/dayjs@1.11.10: /dayjs@1.11.10:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dev: true dev: true
@ -5860,6 +5929,22 @@ packages:
minimist: 1.2.8 minimist: 1.2.8
dev: true 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: /didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dev: true dev: true
@ -6372,6 +6457,11 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true 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: /eventemitter2@6.4.7:
resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
dev: true dev: true
@ -6455,6 +6545,10 @@ packages:
resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
engines: {'0': node >=0.6.0} engines: {'0': node >=0.6.0}
/fast-copy@3.0.2:
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
dev: true
/fast-deep-equal@3.1.3: /fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@ -6492,6 +6586,15 @@ packages:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
dev: true 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: /fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
dependencies: dependencies:
@ -6961,6 +7064,10 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: true dev: true
/help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
dev: true
/hoist-non-react-statics@3.3.2: /hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
dependencies: dependencies:
@ -6983,6 +7090,15 @@ packages:
void-elements: 3.1.0 void-elements: 3.1.0
dev: false 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: /http-proxy-agent@5.0.0:
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -7881,6 +7997,11 @@ packages:
- supports-color - supports-color
- ts-node - ts-node
/joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
dev: true
/jpeg-js@0.4.4: /jpeg-js@0.4.4:
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
dev: false dev: false
@ -8440,6 +8561,21 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true 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: /npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -8576,6 +8712,11 @@ packages:
resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==}
dev: false 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: /once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies: dependencies:
@ -8721,6 +8862,13 @@ packages:
json-parse-even-better-errors: 2.3.1 json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4 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: /parse5@7.1.2:
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
dependencies: dependencies:
@ -8810,6 +8958,54 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true 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: /pirates@4.0.6:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -9027,6 +9223,10 @@ packages:
fromentries: 1.3.2 fromentries: 1.3.2
dev: true dev: true
/process-warning@3.0.0:
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
dev: true
/process@0.11.10: /process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'} engines: {node: '>= 0.6.0'}
@ -9115,6 +9315,10 @@ packages:
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
dev: true dev: true
/quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
dev: true
/quick-lru@5.1.1: /quick-lru@5.1.1:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -9635,12 +9839,28 @@ packages:
util-deprecate: 1.0.2 util-deprecate: 1.0.2
dev: true 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: /readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
dependencies: dependencies:
picomatch: 2.3.1 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): /redux-thunk@3.1.0(redux@5.0.1):
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
peerDependencies: peerDependencies:
@ -9924,6 +10144,11 @@ packages:
is-regex: 1.1.4 is-regex: 1.1.4
dev: true 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: /safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@ -9973,6 +10198,10 @@ packages:
compute-scroll-into-view: 3.1.0 compute-scroll-into-view: 3.1.0
dev: false dev: false
/secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
dev: true
/semver@5.7.2: /semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true hasBin: true
@ -10151,6 +10380,12 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: true 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: /source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -10194,6 +10429,11 @@ packages:
which: 2.0.2 which: 2.0.2
dev: true dev: true
/split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
dev: true
/sprintf-js@1.0.3: /sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
@ -10306,6 +10546,12 @@ packages:
safe-buffer: 5.1.2 safe-buffer: 5.1.2
dev: true 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: /strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -10499,6 +10745,12 @@ packages:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
dev: true 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: /throttleit@1.0.1:
resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==}
dev: true dev: true
@ -11414,7 +11666,3 @@ packages:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
dev: true dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

View File

@ -1,38 +1,12 @@
<svg width='100%' height='100%' viewBox='0 0 41 40' fill='none' xmlns='http://www.w3.org/2000/svg'> <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path d="M455.885 297.861C455.85 297.861 455.82 297.886 455.814 297.92C444.323 359.916 394.213 414.254 336.155 444.713C328.858 448.526 326.952 450.629 318.768 451.286H435.513C438.163 451.384 440.805 450.945 443.282 449.998C445.759 449.051 448.019 447.613 449.927 445.772C451.836 443.931 453.353 441.724 454.389 439.283C455.425 436.843 455.958 434.218 455.956 431.566V297.933C455.956 297.893 455.924 297.861 455.885 297.861V297.861Z" fill="#F7931E"/>
d='M39.9564 24.0195C38.8098 30.1683 33.7828 35.5321 28.0061 38.5411C27.3005 38.9336 26.4627 39.1516 25.6689 39.1952H37.9279C39.1185 39.1952 39.9564 38.323 39.9564 37.2328V24.0195Z' <path d="M327.511 447.668C325.125 447.832 322.731 447.832 320.345 447.668H327.511Z" fill="#FFCE00"/>
fill='#F7931E' <path d="M210.239 178.025C208.465 179.504 206.69 180.852 204.882 182.134C175.071 203.103 84.0943 271.073 63.5522 241.821C43.3716 213.095 64.6368 131.617 117.027 92.3728C118.013 91.584 119 90.8609 120.018 90.1707C176.846 50.204 219.377 55.7257 240.05 85.0105C259.245 112.488 237.651 155.61 210.239 178.025Z" fill="#8427E0"/>
/> <path d="M432.62 241.066C404.486 260.787 360.082 237.78 338.061 209.481C337.173 208.33 336.319 207.18 335.497 205.997C314.528 176.088 246.624 85.1106 275.81 64.5356C304.996 43.9606 389.137 66.409 427.46 121.002C428.315 122.218 429.136 123.401 429.925 124.584C467.197 179.67 461.248 220.82 432.62 241.066Z" fill="#00B5FF"/>
<path <path d="M394.954 420.458C393.968 421.225 392.982 421.948 391.996 422.627C335.136 462.594 292.605 457.04 272.03 427.788C252.737 400.311 274.298 357.287 301.611 334.773C303.386 333.294 305.193 331.913 307.001 330.632C336.812 309.695 427.789 241.89 448.331 270.977C468.643 299.67 447.378 381.215 394.954 420.458Z" fill="#FFBD00"/>
d='M15.4381 12.1576C15.2617 12.2884 15.0853 12.4192 14.9089 12.55C11.9103 14.6432 2.82634 21.3589 0.753788 18.4371C-1.27467 15.6026 0.886079 7.57868 6.08952 3.69755C6.17771 3.61033 6.31 3.56672 6.3982 3.4795C12.0867 -0.48885 16.32 0.078058 18.3926 2.95621C20.3328 5.65992 18.1721 9.93353 15.4381 12.1576Z' <path d="M236.205 448.131C206.92 468.673 122.878 446.192 84.5216 391.632L82.2537 388.345C44.7849 333.095 50.701 291.978 79.3943 271.83C107.496 252.11 151.9 275.117 173.921 303.383C174.808 304.534 175.663 305.684 176.517 306.867C197.487 336.711 265.457 427.688 236.205 448.131Z" fill="#E3006D"/>
fill='#8427E0' <path d="M240.905 137.634C230.873 159.981 212.651 177.627 189.993 186.935C146.049 205.439 81.5306 228.939 70.4214 203.369C59.5423 178.225 79.6572 127.018 116.995 92.3754L117.783 91.718C175.761 50.0421 219.114 55.3667 240.05 85.0131C250.403 99.902 248.859 119.458 240.905 137.634Z" fill="#9327FF"/>
/> <path d="M432.62 241.066C417.37 251.78 397.354 249.907 378.718 241.46C356.357 231.053 338.75 212.589 329.417 189.759C311.241 145.75 289.056 83.3019 314.101 72.4885C340.394 61.1164 395.382 83.7621 429.925 124.583C467.197 179.669 461.248 220.819 432.62 241.066Z" fill="#00C8FF"/>
<path <path d="M394.954 420.458L394.297 420.984C336.286 462.726 292.868 457.434 272.03 427.787C261.578 412.899 263.123 393.441 271.077 375.2C281.097 352.845 299.324 335.195 321.989 325.899C365.899 307.361 430.451 283.894 441.56 309.465C452.669 335.036 432.325 385.684 394.954 420.458Z" fill="#FFCE00"/>
d='M33.8715 36.098C33.7833 36.1852 33.6951 36.2288 33.5628 36.316C27.8743 40.2844 23.641 39.7175 21.5684 36.8393C19.6282 34.1356 21.7889 29.862 24.5229 27.638C24.6993 27.5072 24.8757 27.3763 25.0521 27.2455C28.0507 25.1959 37.1347 18.4366 39.1631 21.3584C41.2357 24.1929 39.119 32.2169 33.8715 36.098Z' <path d="M197.914 440.311C171.62 451.65 116.83 429.136 82.2537 388.446C44.7849 333.098 50.701 291.981 79.3943 271.833C94.6119 261.118 114.628 262.959 133.264 271.406C155.591 281.872 173.141 300.384 182.401 323.237C200.609 366.918 222.959 429.432 197.914 440.311Z" fill="#FB006D"/>
fill='#FFBD00'
/>
<path
d='M17.9954 38.8459C15.085 40.8955 6.70658 38.6715 2.87014 33.264C2.78195 33.1768 2.69376 33.046 2.64966 32.9588C-1.09858 27.5078 -0.481224 23.4086 2.38508 21.4462C5.20728 19.4838 9.61698 21.7515 11.8218 24.586C11.91 24.7168 11.9982 24.804 12.0864 24.9349C14.159 27.8566 20.9499 36.8399 17.9954 38.8459Z'
fill='#E3006D'
/>
<path
d='M15.4385 12.1576C11.3816 13.9455 2.73857 17.6086 1.45976 14.6432C0.357338 12.1576 2.3858 7.09899 6.08994 3.69755C6.17814 3.61033 6.31043 3.56672 6.39862 3.4795C12.0871 -0.48885 16.3204 0.078058 18.393 2.95621C20.3333 5.65992 18.1725 9.93353 15.4385 12.1576Z'
fill='#9327FF'
/>
<path
d='M37.6624 18.3955C34.8402 20.3579 30.4305 18.0903 28.2257 15.2557C28.1375 15.1249 28.0493 15.0377 27.9611 14.9069C25.8444 11.9415 19.0535 2.95819 21.9639 0.952211C24.8743 -1.09738 33.2968 1.12664 37.1333 6.53407C37.2215 6.6649 37.3096 6.75211 37.3978 6.88294C41.102 12.334 40.5287 16.3895 37.6624 18.3955Z'
fill='#00B5FF'
/>
<path
d='M37.6628 18.3934C34.8406 20.3557 30.4309 18.0881 28.2261 15.2536C26.4181 11.1108 22.9344 2.95603 25.8448 1.73499C28.4906 0.601179 33.9587 2.86881 37.4423 6.88077C41.1024 12.3318 40.5291 16.3874 37.6628 18.3934Z'
fill='#00C8FF'
/>
<path
d='M33.8715 36.0986C33.7833 36.1858 33.6951 36.2294 33.5628 36.3166C27.8743 40.285 23.641 39.7181 21.5684 36.8399C19.6282 34.1362 21.7889 29.8626 24.5229 27.6386C28.5799 25.8506 37.2229 22.1875 38.5017 25.1529C39.6482 27.6386 37.6197 32.6971 33.8715 36.0986Z'
fill='#FFCE00'
/>
<path
d='M14.2031 38.061C11.5572 39.1948 6.08922 36.9708 2.64966 32.9588C-1.09858 27.5078 -0.481224 23.4086 2.38508 21.4462C5.20728 19.4838 9.61698 21.7515 11.8218 24.586C13.6298 28.6852 17.1135 36.8399 14.2031 38.061Z'
fill='#FB006D'
/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

View File

@ -0,0 +1,35 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const jestCoverageFile = path.join(__dirname, '../coverage/jest/coverage-final.json');
const cypressCoverageFile = path.join(__dirname, '../coverage/cypress/coverage-final.json');
const nycOutputDir = path.join(__dirname, '../coverage/.nyc_output');
// Ensure .nyc_output directory exists
if (fs.existsSync(nycOutputDir)) {
fs.rmSync(nycOutputDir, { recursive: true });
}
fs.mkdirSync(nycOutputDir, { recursive: true });
if (fs.existsSync(path.join(__dirname, '../coverage/merged'))) {
fs.rmSync(path.join(__dirname, '../coverage/merged'), { recursive: true });
}
// Copy Jest coverage file
fs.copyFileSync(jestCoverageFile, path.join(nycOutputDir, 'jest-coverage.json'));
// Copy Cypress E2E coverage file
fs.copyFileSync(cypressCoverageFile, path.join(nycOutputDir, 'cypress-coverage.json'));
// Merge coverage files
execSync('nyc merge ./coverage/.nyc_output ./coverage/merged/coverage-final.json', { stdio: 'inherit' });
// Move the merged result to the .nyc_output directory
fs.rmSync(nycOutputDir, { recursive: true });
fs.mkdirSync(nycOutputDir, { recursive: true });
fs.copyFileSync(path.join(__dirname, '../coverage/merged/coverage-final.json'), path.join(nycOutputDir, 'out.json'));
// Generate final merged report
execSync('nyc report --reporter=html --reporter=text-summary --report-dir=coverage/merged --temp-dir=coverage/.nyc_output', { stdio: 'inherit' });
console.log(`Merged coverage report written to coverage/merged`);

View File

@ -1,114 +0,0 @@
const path = require('path');
const fs = require('fs');
const pino = require('pino');
const cheerio = require('cheerio');
const axios = require('axios');
const distDir = path.join(__dirname, 'dist');
const indexPath = path.join(distDir, 'index.html');
const setOrUpdateMetaTag = ($, selector, attribute, content) => {
if ($(selector).length === 0) {
$('head').append(`<meta ${attribute}="${selector.match(/\[(.*?)\]/)[1]}" content="${content}">`);
} else {
$(selector).attr('content', content);
}
};
// Create a new logger instance
const logger = pino({
transport: {
target: 'pino-pretty',
level: 'info',
options: {
colorize: true,
translateTime: 'SYS:standard',
destination: `${__dirname}/pino-logger.log`,
},
},
});
const logRequestTimer = (req) => {
const start = Date.now();
const pathname = new URL(req.url).pathname;
logger.info(`Incoming request: ${pathname}`);
return () => {
const duration = Date.now() - start;
logger.info(`Request for ${pathname} took ${duration}ms`);
};
};
const fetchMetaData = async (url) => {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
logger.error('Error fetching meta data', error);
return null;
}
};
const createServer = async (req) => {
const timer = logRequestTimer(req);
if (req.method === 'GET') {
const pageId = req.url.split('/').pop();
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;
// }
$('title').text(title);
setOrUpdateMetaTag($, 'meta[name="description"]', 'name', description);
setOrUpdateMetaTag($, 'meta[property="og:title"]', 'property', title);
setOrUpdateMetaTag($, 'meta[property="og:description"]', 'property', description);
setOrUpdateMetaTag($, 'meta[property="og:image"]', 'property', image);
setOrUpdateMetaTag($, 'meta[property="og:url"]', 'property', url);
setOrUpdateMetaTag($, 'meta[property="og:type"]', 'property', 'article');
setOrUpdateMetaTag($, 'meta[name="twitter:card"]', 'name', 'summary_large_image');
setOrUpdateMetaTag($, 'meta[name="twitter:title"]', 'name', title);
setOrUpdateMetaTag($, 'meta[name="twitter:description"]', 'name', description);
setOrUpdateMetaTag($, 'meta[name="twitter:image"]', 'name', image);
timer();
return new Response($.html(), {
headers: { 'Content-Type': 'text/html' },
});
} else {
timer();
logger.error({ message: 'Method not allowed', method: req.method });
return new Response('Method not allowed', { status: 405 });
}
};
const start = () => {
try {
Bun.serve({
port: 3000,
fetch: createServer,
error: (err) => {
logger.error(`Internal Server Error: ${err}`);
return new Response('Internal Server Error', { status: 500 });
},
});
logger.info(`Server is running on port 3000`);
} catch (err) {
logger.error(err);
process.exit(1);
}
};
start();

View File

@ -194,6 +194,40 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "appflowy-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8"
dependencies = [
"anyhow",
"appflowy-plugin",
"bytes",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "appflowy-plugin"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8"
dependencies = [
"anyhow",
"cfg-if",
"crossbeam-utils",
"log",
"once_cell",
"parking_lot 0.12.1",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
]
[[package]] [[package]]
name = "appflowy_tauri" name = "appflowy_tauri"
version = "0.0.0" version = "0.0.0"
@ -1331,7 +1365,7 @@ dependencies = [
"cssparser-macros", "cssparser-macros",
"dtoa-short", "dtoa-short",
"itoa 1.0.10", "itoa 1.0.10",
"phf 0.8.0", "phf 0.11.2",
"smallvec", "smallvec",
] ]
@ -1887,6 +1921,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"allo-isolate", "allo-isolate",
"anyhow", "anyhow",
"appflowy-local-ai",
"appflowy-plugin",
"bytes", "bytes",
"dashmap", "dashmap",
"flowy-chat-pub", "flowy-chat-pub",
@ -1894,7 +1930,6 @@ dependencies = [
"flowy-derive", "flowy-derive",
"flowy-error", "flowy-error",
"flowy-notification", "flowy-notification",
"flowy-sidecar",
"flowy-sqlite", "flowy-sqlite",
"futures", "futures",
"lib-dispatch", "lib-dispatch",
@ -2176,7 +2211,6 @@ dependencies = [
"fancy-regex 0.11.0", "fancy-regex 0.11.0",
"flowy-codegen", "flowy-codegen",
"flowy-derive", "flowy-derive",
"flowy-sidecar",
"flowy-sqlite", "flowy-sqlite",
"lib-dispatch", "lib-dispatch",
"protobuf", "protobuf",
@ -2363,24 +2397,6 @@ dependencies = [
"serde_repr", "serde_repr",
] ]
[[package]]
name = "flowy-sidecar"
version = "0.1.0"
dependencies = [
"anyhow",
"crossbeam-utils",
"lib-infra",
"log",
"once_cell",
"parking_lot 0.12.1",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
]
[[package]] [[package]]
name = "flowy-sqlite" name = "flowy-sqlite"
version = "0.1.0" version = "0.1.0"
@ -4962,7 +4978,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [ dependencies = [
"bytes", "bytes",
"heck 0.4.1", "heck 0.4.1",
"itertools 0.10.5", "itertools 0.11.0",
"log", "log",
"multimap", "multimap",
"once_cell", "once_cell",
@ -4983,7 +4999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.10.5", "itertools 0.11.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.55",

View File

@ -29,7 +29,7 @@ tokio = "1.34.0"
tokio-stream = "0.1.14" tokio-stream = "0.1.14"
async-trait = "0.1.74" async-trait = "0.1.74"
chrono = { version = "0.4.31", default-features = false, features = ["clock"] } chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
yrs = "0.18.8" yrs = "0.19.1"
# Please use the following script to update collab. # Please use the following script to update collab.
# Working directory: frontend # Working directory: frontend
# #
@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a2f92bb" }
[dependencies] [dependencies]
serde_json.workspace = true serde_json.workspace = true
@ -107,10 +107,27 @@ default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
[patch.crates-io] [patch.crates-io]
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } # Please use the following script to update collab.
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } # Working directory: frontend
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } #
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } # To update the commit ID, run:
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } # scripts/tool/update_collab_rev.sh new_rev_id
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } #
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" } # To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
# Working directory: frontend
# To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" }

View File

@ -32,6 +32,7 @@ export enum BlockType {
OutlineBlock = 'outline', OutlineBlock = 'outline',
TableBlock = 'table', TableBlock = 'table',
TableCell = 'table/cell', TableCell = 'table/cell',
LinkPreview = 'link_preview',
} }
export enum InlineBlockType { export enum InlineBlockType {
@ -46,7 +47,7 @@ export enum AlignType {
} }
export interface BlockData { export interface BlockData {
bg_color?: string; bgColor?: string;
font_color?: string; font_color?: string;
align?: AlignType; align?: AlignType;
} }
@ -79,6 +80,10 @@ export interface MathEquationBlockData extends BlockData {
formula?: string; formula?: string;
} }
export interface LinkPreviewBlockData extends BlockData {
url?: string;
}
export enum ImageType { export enum ImageType {
Local = 0, Local = 0,
Internal = 1, Internal = 1,
@ -111,6 +116,8 @@ export interface TableCellBlockData extends BlockData {
height: number; height: number;
rowPosition: number; rowPosition: number;
width: number; width: number;
rowBackgroundColor: string;
colBackgroundColor: string;
} }
export interface DatabaseNodeData extends BlockData { export interface DatabaseNodeData extends BlockData {
@ -636,3 +643,27 @@ export enum LineHeightLayout {
normal = 'normal', normal = 'normal',
large = 'large', large = 'large',
} }
export interface ViewMetaIcon {
ty: number;
value: string;
}
export interface PublishViewInfo {
view_id: string;
name: string;
icon: ViewMetaIcon | null;
extra: string | null;
layout: number;
created_at: string;
created_by: string;
last_edited_time: string;
last_edited_by: string;
child_views: PublishViewInfo[] | null;
}
export interface PublishViewMetaData {
view: PublishViewInfo;
child_views: PublishViewInfo[];
ancestor_views: PublishViewInfo[];
}

View File

@ -18,7 +18,6 @@ import {
useSortsSelector, useSortsSelector,
} from '../selector'; } from '../selector';
import { useDatabaseViewId } from '../context'; import { useDatabaseViewId } from '../context';
import { IdProvider } from '@/components/_shared/context-provider/IdProvider';
import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
import { withTestingDatabase } from '@/application/database-yjs/__tests__/withTestingData'; import { withTestingDatabase } from '@/application/database-yjs/__tests__/withTestingData';
import { expect } from '@jest/globals'; import { expect } from '@jest/globals';
@ -31,11 +30,9 @@ const wrapperCreator =
(viewId: string, doc: YDoc, rowDocMap: Y.Map<YDoc>) => (viewId: string, doc: YDoc, rowDocMap: Y.Map<YDoc>) =>
({ children }: { children: React.ReactNode }) => { ({ children }: { children: React.ReactNode }) => {
return ( return (
<IdProvider objectId={viewId}>
<DatabaseContextProvider viewId={viewId} databaseDoc={doc} rowDocMap={rowDocMap} readOnly={true}> <DatabaseContextProvider viewId={viewId} databaseDoc={doc} rowDocMap={rowDocMap} readOnly={true}>
{children} {children}
</DatabaseContextProvider> </DatabaseContextProvider>
</IdProvider>
); );
}; };

View File

@ -1,4 +1,5 @@
import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
import * as Y from 'yjs'; import * as Y from 'yjs';
@ -9,6 +10,10 @@ export interface DatabaseContextState {
rowDocMap: Y.Map<YDoc>; rowDocMap: Y.Map<YDoc>;
isDatabaseRowPage?: boolean; isDatabaseRowPage?: boolean;
navigateToRow?: (rowId: string) => void; 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); export const DatabaseContext = createContext<DatabaseContextState | null>(null);

View File

@ -1,12 +1,4 @@
import { import { FieldId, SortId, YDatabaseField, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
FieldId,
SortId,
YDatabaseField,
YDoc,
YjsDatabaseKey,
YjsEditorKey,
YjsFolderKey,
} from '@/application/collab.type';
import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
import { import {
useDatabase, useDatabase,
@ -19,7 +11,6 @@ import {
import { filterBy, parseFilter } from '@/application/database-yjs/filter'; import { filterBy, parseFilter } from '@/application/database-yjs/filter';
import { groupByField } from '@/application/database-yjs/group'; import { groupByField } from '@/application/database-yjs/group';
import { sortBy } from '@/application/database-yjs/sort'; import { sortBy } from '@/application/database-yjs/sort';
import { useViewsIdSelector } from '@/application/folder-yjs';
import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse'; import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse';
import { DateTimeCell } from '@/application/database-yjs/cell.type'; import { DateTimeCell } from '@/application/database-yjs/cell.type';
import * as dayjs from 'dayjs'; import * as dayjs from 'dayjs';
@ -42,9 +33,8 @@ export interface Row {
const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty];
export function useDatabaseViewsSelector(iidIndex: string) { export function useDatabaseViewsSelector(_iidIndex: string) {
const database = useDatabase(); const database = useDatabase();
const { viewsId: visibleViewsId, views: folderViews } = useViewsIdSelector();
const views = database?.get(YjsDatabaseKey.views); const views = database?.get(YjsDatabaseKey.views);
const [viewIds, setViewIds] = useState<string[]>([]); const [viewIds, setViewIds] = useState<string[]>([]);
@ -65,22 +55,7 @@ export function useDatabaseViewsSelector(iidIndex: string) {
return Number(viewB.created_at) - Number(viewA.created_at); return Number(viewB.created_at) - Number(viewA.created_at);
}); });
const viewsId = []; setViewIds(viewsSorted.map(([key]) => key));
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);
}; };
observerEvent(); observerEvent();
@ -89,7 +64,7 @@ export function useDatabaseViewsSelector(iidIndex: string) {
return () => { return () => {
views.unobserve(observerEvent); views.unobserve(observerEvent);
}; };
}, [visibleViewsId, views, folderViews, iidIndex]); }, [views]);
return { return {
childViews, childViews,

View File

@ -2,6 +2,17 @@ import { YDoc } from '@/application/collab.type';
import { databasePrefix } from '@/application/constants'; import { databasePrefix } from '@/application/constants';
import { IndexeddbPersistence } from 'y-indexeddb'; import { IndexeddbPersistence } from 'y-indexeddb';
import * as Y from 'yjs'; 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>(); const openedSet = new Set<string>();
@ -32,10 +43,16 @@ export async function openCollabDB(docName: string): Promise<YDoc> {
return doc as YDoc; return doc as YDoc;
} }
export function getCollabDBName(id: string, type: string, uuid?: string) { export async function closeCollabDB(docName: string) {
if (!uuid) { const name = `${databasePrefix}_${docName}`;
return `${type}_${id}`;
if (openedSet.has(name)) {
openedSet.delete(name);
} }
return `${uuid}_${type}_${id}`; const doc = new Y.Doc();
const provider = new IndexeddbPersistence(name, doc);
await provider.destroy();
} }

View File

@ -0,0 +1,17 @@
import { Table } from 'dexie';
import { PublishViewInfo } from '@/application/collab.type';
export type ViewMeta = {
publish_name: string;
child_views: PublishViewInfo[];
ancestor_views: PublishViewInfo[];
} & PublishViewInfo;
export type ViewMetasTable = {
view_metas: Table<ViewMeta>;
};
export const viewMetasSchema = {
view_metas: 'publish_name',
};

View File

@ -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;
};

View File

@ -1,9 +0,0 @@
export enum CoverType {
NormalColor = 'color',
GradientColor = 'gradient',
BuildInImage = 'built_in',
CustomImage = 'custom',
LocalImage = 'local',
UpsplashImage = 'unsplash',
None = 'none',
}

View File

@ -1,2 +0,0 @@
export * from './selector';
export * from './context';

View File

@ -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,
};
}

View File

@ -0,0 +1,163 @@
import { YDoc } from '@/application/collab.type';
import { db } from '@/application/db';
import { ViewMeta } from '@/application/db/tables/view_metas';
import { AFConfigContext } from '@/components/app/AppConfig';
import { useLiveQuery } from 'dexie-react-hooks';
import { createContext, useCallback, useContext, useEffect, useRef } 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);
}, [namespace, publishName]);
const prevViewMeta = useRef(viewMeta);
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) {
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 { namespace, publishName } = info;
const res = await service?.getPublishViewMeta(namespace, publishName);
if (!res) {
throw new Error('View meta has not been published yet');
}
return res;
} catch (e) {
return Promise.reject(e);
}
},
[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]
);
useEffect(() => {
if (!viewMeta && prevViewMeta.current) {
window.location.reload();
return;
}
prevViewMeta.current = viewMeta;
}, [viewMeta]);
return (
<PublishContext.Provider
value={{
loadView,
viewMeta,
getViewRowsMap,
loadViewMeta,
toView,
namespace,
publishName,
}}
>
{children}
</PublishContext.Provider>
);
};
export function usePublishContext() {
return useContext(PublishContext);
}

View File

@ -0,0 +1 @@
export * from './context';

Some files were not shown because too many files have changed in this diff Show More