Compare commits

...
17 Commits
Author SHA1 Message Date
Lucas e63f767926 feat: auto-dismiss collapsed handle on Android if no interaction occurs (#7088)
* feat: support auto-dismiss collapsed handle on Android

* fix: hit test area of collasepd handle is too big

* chore: upgrade appflowy_editor

* fix: simple table issues on mobile

* feat: highlight cell after insertion

* test: text color and cell background color test

* fix: sign_in_page_settings_test
2024-12-30 17:57:00 +08:00
Richard ShiueandLucas 8826e479eb fix(flutter_desktop): workspace menu ui issues (#7091)
* fix(flutter_desktop): remove log out and workspace option popovers conflict

* test: add integration test

* fix(flutter_desktop): workspace list scrollbar overlaps with list

* chore(flutter_desktop): fix padding around import from notion button

* chore(flutter_desktop): adjust popover conflict rules for workspace

* test: add integration tests

* chore(flutter_desktop): make the popoovers as barriers

* fix: regression from making the workspace item menu as barrier

* chore: update frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart

Co-authored-by: Lucas <lucas.xu@appflowy.io>

---------

Co-authored-by: Lucas <lucas.xu@appflowy.io>
2024-12-30 17:55:54 +08:00
Lucas 8e4fe3d559 chore: update changelog (#7095) 2024-12-30 17:54:30 +08:00
Richard Shiue e2ee11e48a feat: add toast messages for ai chat interactions (#7086) 2024-12-30 17:20:23 +08:00
Morn bdc0fa1f2a fix: toolbar menu not showing beacase keyboard height is not updated in time (#7060) 2024-12-30 17:20:15 +08:00
Morn 3836545682 fix: issues related to the emoji icon picker (#7063)
* fix: remove the scrolling conflict of the icon picker on macOS

* fix: the icon is not supported in sites tab

* feat: keep the icon panel open after click ramdom

* feat: the type of selector opened depends on the already set icon or emoji

* feat: the skin tone of the random emoji follows the selected skin ton

* fix: unit testing error
2024-12-30 17:20:08 +08:00
Richard Shiue 02eb0e0b83 chore(flutter_desktop): adjust toast style (#7083) 2024-12-30 17:19:56 +08:00
Lucas 38c2937f64 fix: docker ci (#7087) 2024-12-30 13:35:37 +08:00
Richard Shiue 06d5bc734b fix: try to fix message regeneration being appended instead of replaced (#7084) 2024-12-30 12:38:56 +08:00
Nathan.fooo a521541cb7 chore: only sync when doucment was changed (#7081)
* chore: only sync when doucment was changed

* chore: fmt
2024-12-30 10:26:06 +08:00
Kilu.He 7d61252e6a feat: project management & document editor features on web (#7051)
* fix: support page operations

* fix: support select language of code block

* fix: support hover controls of document

* fix: support slash panel of document

* feat: support image block and file block of document

* feat: support outline block of document

* feat: support add document on document

* feat: support create sub-page on document

* feat: support mention panel

* feat: support more actions

* feat: support create space

* fix: add some shortcuts to document

* fix: add some shortcuts to document

* fix: multiple select blocks

* fix: reupdate trash after restore and delete

* fix: lint check

* fix: callout block

* fix: view icon

* fix: colors

* fix: colors

* feat: support add grid/board/calendar

* feat: support database new layout

* fix: slash panel

* fix: bugs

* fix: grid table

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: bugs

* fix: some bugs

* fix: some bugs

* fix: invite members

* fix: styles

* fix: styles

* fix: styles

* fix: inline code

* fix: image block

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: some bugs

* fix: add applink

* fix: add applink

* fix: add applink

* fix: add share link

* fix: inline link

* fix: add log

* fix: simple table bugs

* fix: simple table bugs

* fix: some bugs

* fix: build size

* fix: build size

* fix: remove private from package.json

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: support quick note

* fix: some bugs

* fix: support paste md

* fix: modified toolbar

* fix: modified toolbard actions

* fix: adjust editor toolbar

* fix: support billing
2024-12-30 10:12:54 +08:00
Lucas cb2b9ac537 fix: simple table issues on mobile (#7068)
* feat: use fixed height for table menu

* feat: do not close menu after enabling header column/row

* fix: remove cell background color

* fix: the text color doesn't apply to the heading block

* fix: replace quick action icon color

* fix: add done button in text color menu

* fix: don't access clipboard when opening table action menu

* fix: table navigation issues and reminder issue

* fix: cursor flash when create a row and column at the same time

* fix: unable to insert page sometimes

* fix: use default name if the title is empty

* chore: replace align icons
2024-12-30 09:23:03 +08:00
Alexandre Cirilo af31f8cf6f chore(i18n): update fr-FR translations 🐦 (#7076) 2024-12-30 09:16:52 +08:00
Richard Shiue 87c1245a3f fix: launch review issues 0.7.9 (#7073)
* fix: disable select sources when on local AI

* chore: tooltips for right sidebar actions

* chore: update selection after inserting into existing page

* fix: mobile select sources button background color

* fix: ai response metadata

* chore: don't update selected sources section until reopen

* chore: show views from all spaces in select sources menu

* chore: revert podfile changes

* chore: decrease spacing between metadata and hover actions

* chore: improve placeholder for ai message metadata
2024-12-29 21:48:37 +08:00
Nathan.fooo 49d9417cac refactor: crate dir (#7080) 2024-12-29 14:47:28 +08:00
jumbi77 1f29235538 chore(i18n): update de-DE translations 🐦 (#7071) 2024-12-28 15:39:34 +08:00
Richard Shiue fd1e36b21a fix: numbered list misalignment (#7066)
* fix: numbered list misalignment

* chore: fixed width numbers
2024-12-27 16:08:27 +08:00
507 changed files with 25258 additions and 5661 deletions

No files matched your search

+18 -2
View File
@@ -1,8 +1,24 @@
# Release Notes
## Version 0.7.9 - 25/12/2024
## Version 0.7.9 - 30/12/2024
### New Features
- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser.
- Create beautiful documents with 22 content types and markdown support
- Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos
- Invite members to your workspace for seamless collaboration
- Create multiple public/private spaces to better organize your content
- Simple Table is now available on Mobile, designed specifically for mobile devices.
- Create and manage Simple Table blocks on Mobile with easy-to-use action menus.
- Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile
- Use '/' to insert a content block into a table cell on Desktop
- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources
- Add messages to an editable document while chatting with AI side by side
- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons
- Drag a page from the sidebar into a document to easily mention the page without typing its title
- Paste as plain text, a new option in the right-click paste menu
### Bug Fixes
- Fixed misalignment in numbered lists
- Resolved several bugs in the emoji menu
- Fixed a bug with checklist items
## Version 0.7.8 - 18/12/2024
### New Features
@@ -5,6 +5,7 @@ import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -102,8 +103,7 @@ void main() {
expect(memberCount, findsNWidgets(2));
});
testWidgets('only display one menu item in the workspace menu',
(tester) async {
testWidgets('workspace menu popover behavior test', (tester) async {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
@@ -128,6 +128,8 @@ void main() {
final workspaceItem = find.byWidgetPredicate(
(w) => w is WorkspaceMenuItem && w.workspace.name == name,
);
// the workspace menu shouldn't conflict with logout
await tester.hoverOnWidget(
workspaceItem,
onHover: () async {
@@ -136,15 +138,73 @@ void main() {
);
expect(moreButton, findsOneWidget);
await tester.tapButton(moreButton);
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
final logoutButton = find.byType(WorkspaceMoreButton);
await tester.tapButton(logoutButton);
expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget);
expect(moreButton, findsNothing);
await tester.tapButton(moreButton);
expect(find.text(LocaleKeys.button_logout.tr()), findsNothing);
expect(moreButton, findsOneWidget);
},
);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// clicking on the more action button for the same workspace shouldn't do
// anything
await tester.openCollaborativeWorkspaceMenu();
await tester.hoverOnWidget(
workspaceItem,
onHover: () async {
final moreButton = find.byWidgetPredicate(
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
);
expect(moreButton, findsOneWidget);
await tester.tapButton(moreButton);
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
// click it again
await tester.tapButton(moreButton);
// nothing should happen
expect(
find.text(LocaleKeys.button_rename.tr()),
findsOneWidget,
);
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
},
);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
// clicking on the more button of another workspace should close the menu
// for this one
await tester.openCollaborativeWorkspaceMenu();
final moreButton = find.byWidgetPredicate(
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
);
await tester.hoverOnWidget(
workspaceItem,
onHover: () async {
expect(moreButton, findsOneWidget);
await tester.tapButton(moreButton);
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
},
);
final otherWorspaceItem = find.byWidgetPredicate(
(w) => w is WorkspaceMenuItem && w.workspace.name != name,
);
final otherMoreButton = find.byWidgetPredicate(
(w) => w is WorkspaceMoreActionList && w.workspace.name != name,
);
await tester.hoverOnWidget(
otherWorspaceItem,
onHover: () async {
expect(otherMoreButton, findsOneWidget);
await tester.tapButton(otherMoreButton);
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
expect(moreButton, findsNothing);
},
);
});
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@@ -9,7 +10,14 @@ import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('calendar', () {
testWidgets('update calendar layout', (tester) async {
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -14,7 +14,14 @@ import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('grid edit field test:', () {
testWidgets('rename existing field', (tester) async {
@@ -67,6 +67,7 @@ void main() {
// open settings page to check the result
await tester.tapButton(settingsButton);
await tester.pumpAndSettle(const Duration(milliseconds: 250));
// check the server type
expect(
@@ -1,6 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -12,7 +13,14 @@ import '../../shared/emoji.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
RecentIcons.enable = false;
});
tearDownAll(() {
RecentIcons.enable = true;
});
group('Sidebar view item tests', () {
testWidgets('Access view item context menu by right click', (tester) async {
@@ -57,6 +57,7 @@ Future<T?> showMobileBottomSheet<T>(
double maxChildSize = 0.8,
double initialChildSize = 0.51,
double bottomSheetPadding = 0,
bool enablePadding = true,
}) async {
assert(
showHeader ||
@@ -172,14 +173,18 @@ Future<T?> showMobileBottomSheet<T>(
}
// ----- content area -----
// add content padding and extra bottom padding
children.add(
Padding(
padding:
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
child: child,
),
);
if (enablePadding) {
// add content padding and extra bottom padding
children.add(
Padding(
padding:
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
child: child,
),
);
} else {
children.add(child);
}
// ----- content area -----
if (children.length == 1) {
@@ -37,8 +37,13 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
// Only user enable chat with file and the plugin is already running
final supportChatWithFile = chatState.fileEnabled &&
chatState.pluginState.state == RunningStatePB.Running;
final aiType = chatState.pluginState.state == RunningStatePB.Running
? AIType.localAI
: AIType.appflowyAI;
emit(
state.copyWith(
aiType: aiType,
supportChatWithFile: supportChatWithFile,
chatState: chatState,
),
@@ -272,15 +272,19 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
// 3 mean message response from AI
if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) {
temporaryMessageIDMap[pb.messageId.toString()] =
answerStreamMessageId;
temporaryMessageIDMap.putIfAbsent(
pb.messageId.toString(),
() => answerStreamMessageId,
);
answerStreamMessageId = '';
}
// 1 mean message response from User
if (pb.authorType == 1 && questionStreamMessageId.isNotEmpty) {
temporaryMessageIDMap[pb.messageId.toString()] =
questionStreamMessageId;
temporaryMessageIDMap.putIfAbsent(
pb.messageId.toString(),
() => questionStreamMessageId,
);
questionStreamMessageId = '';
}
@@ -422,8 +426,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
(question) {
if (!isClosed) {
final streamAnswer = _createAnswerStreamMessage(
answerStream!,
question.messageId,
stream: answerStream!,
questionMessageId: question.messageId,
fakeQuestionMessageId: questionStreamMessage.id,
);
lastSentMessage = question;
@@ -479,8 +484,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
(success) {
if (!isClosed) {
final streamAnswer = _createAnswerStreamMessage(
answerStream!,
answerMessageId - 1,
stream: answerStream!,
questionMessageId: answerMessageId - 1,
).copyWith(id: answerMessageIdString);
add(ChatEvent.receiveMessage(streamAnswer));
@@ -491,11 +496,14 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
);
}
Message _createAnswerStreamMessage(
AnswerStream stream,
Int64 questionMessageId,
) {
answerStreamMessageId = (questionMessageId + 1).toString();
Message _createAnswerStreamMessage({
required AnswerStream stream,
required Int64 questionMessageId,
String? fakeQuestionMessageId,
}) {
answerStreamMessageId = fakeQuestionMessageId == null
? (questionMessageId + 1).toString()
: "${fakeQuestionMessageId}_ans";
return TextMessage(
id: answerStreamMessageId,
@@ -84,8 +84,10 @@ class ChatEditDocumentService {
final nodes = [
if (rootIsEmpty || !isLastLineEmpty) paragraphNode(),
...messageDocument.root.children,
paragraphNode(),
];
final insertPath = rootIsEmpty || listEquals(lastNodeOrNull.path, const [0])
final insertPath = rootIsEmpty ||
listEquals(lastNodeOrNull.path, const [0]) && isLastLineEmpty
? const [0]
: lastNodeOrNull.path.next;
@@ -4,6 +4,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -103,7 +104,7 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
ChatSettingsCubit() : super(ChatSettingsState.initial());
List<String> selectedSourceIds = [];
ChatSource? source;
List<ChatSource> sources = [];
List<ChatSource> selectedSources = [];
String filter = '';
@@ -111,25 +112,32 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
selectedSourceIds = [...newSelectedSourceIds];
}
void refreshSources(ViewPB view) async {
void refreshSources(List<ViewPB> spaceViews) async {
filter = "";
final newSource = await _recursiveBuild(view, null);
_restrictSelectionIfNecessary(newSource.children);
final newSources = await Future.wait(
spaceViews.map((view) => _recursiveBuild(view, null)),
);
for (final source in newSources) {
_restrictSelectionIfNecessary(source.children);
}
final selected = _buildSelectedSources(newSource).toList();
newSource.toggleIsExpanded();
final selected = newSources
.map((source) => _buildSelectedSources(source))
.flattened
.toList();
emit(
state.copyWith(
selectedSources: selected,
visibleSources: [newSource],
visibleSources: newSources,
),
);
source?.dispose();
source = newSource.copy();
sources
..forEach((e) => e.dispose())
..clear()
..addAll(newSources.map((e) => e.copy()));
selectedSources
..forEach((e) => e.dispose())
@@ -204,12 +212,13 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
for (final source in state.visibleSources) {
source.dispose();
}
if (source == null) {
if (sources.isEmpty) {
emit(ChatSettingsState.initial());
} else {
final selected =
selectedSources.map(_buildSearchResults).nonNulls.toList();
final visible = [_buildSearchResults(source!)].nonNulls.toList();
final visible =
sources.map(_buildSearchResults).nonNulls.nonNulls.toList();
emit(
state.copyWith(
selectedSources: selected,
@@ -301,25 +310,21 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
}
void updateSelectedStatus() {
if (source == null) {
if (sources.isEmpty) {
return;
}
_recursiveUpdateSelectedStatus(source!);
_restrictSelectionIfNecessary(source!.children);
for (final source in sources) {
_recursiveUpdateSelectedStatus(source);
}
_restrictSelectionIfNecessary(sources);
for (final visibleSource in state.visibleSources) {
visibleSource.dispose();
}
final visible = [_buildSearchResults(source!)].nonNulls.toList();
final visible = sources.map(_buildSearchResults).nonNulls.toList();
selectedSources
..forEach((e) => e.dispose())
..clear()
..addAll(_buildSelectedSources(source!));
emit(
state.copyWith(
visibleSources: visible,
selectedSources:
selectedSources.map(_buildSearchResults).nonNulls.toList(),
),
);
}
@@ -360,13 +365,24 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
?.toggleIsExpanded();
}
} else {
source?.findChildBySourceId(chatSource.view.id)?.toggleIsExpanded();
for (final source in sources) {
final child = source.findChildBySourceId(chatSource.view.id);
if (child != null) {
child.toggleIsExpanded();
break;
}
}
}
}
@override
Future<void> close() {
source?.dispose();
for (final child in sources) {
child.dispose();
}
for (final child in selectedSources) {
child.dispose();
}
for (final child in state.selectedSources) {
child.dispose();
}
@@ -49,7 +49,7 @@ class ChatEditorStyleCustomizer extends EditorStyleCustomizer {
DefaultAppearanceSettings.getDefaultSelectionColor(context),
defaultTextDirection: appearance.defaultTextDirection,
textStyleConfiguration: TextStyleConfiguration(
lineHeight: 1.4,
lineHeight: 20 / 14,
applyHeightToFirstAscent: true,
applyHeightToLastDescent: true,
text: baseTextStyle(fontFamily).copyWith(
@@ -506,6 +506,12 @@ class _PromptBottomActions extends StatelessWidget {
margin: DesktopAIPromptSizes.actionBarPadding,
child: BlocBuilder<AIPromptInputBloc, AIPromptInputState>(
builder: (context, state) {
if (state.chatState == null) {
return Align(
alignment: AlignmentDirectional.centerEnd,
child: _sendButton(),
);
}
return Row(
children: [
// predefinedFormatButton(),
@@ -312,7 +312,7 @@ class _LeadingActions extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).cardColor,
color: Theme.of(context).colorScheme.surface,
child: PromptInputMobileSelectSourcesButton(
onUpdateSelectedSources: onUpdateSelectedSources,
),
@@ -69,9 +69,9 @@ class _PromptInputMobileSelectSourcesButtonState
value: cubit,
),
],
child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(
selector: (state) => state.currentSpace,
builder: (context, spaceView) {
child: BlocSelector<SpaceBloc, SpaceState, List<ViewPB>>(
selector: (state) => state.spaces,
builder: (context, spaces) {
return BlocListener<ChatBloc, ChatState>(
listener: (context, state) {
cubit
@@ -97,11 +97,7 @@ class _PromptInputMobileSelectSourcesButtonState
],
),
onTap: () async {
if (spaceView != null) {
context
.read<ChatSettingsCubit>()
.refreshSources(spaceView);
}
context.read<ChatSettingsCubit>().refreshSources(spaces);
await showMobileBottomSheet<void>(
context,
backgroundColor: Theme.of(context).colorScheme.surface,
@@ -74,9 +74,9 @@ class _PromptInputDesktopSelectSourcesButtonState
value: cubit,
),
],
child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(
selector: (state) => state.currentSpace,
builder: (context, spaceView) {
child: BlocSelector<SpaceBloc, SpaceState, List<ViewPB>>(
selector: (state) => state.spaces,
builder: (context, spaces) {
return BlocListener<ChatBloc, ChatState>(
listener: (context, state) {
cubit
@@ -90,15 +90,11 @@ class _PromptInputDesktopSelectSourcesButtonState
margin: EdgeInsets.zero,
controller: popoverController,
onOpen: () {
if (spaceView != null) {
context.read<ChatSettingsCubit>().refreshSources(spaceView);
}
context.read<ChatSettingsCubit>().refreshSources(spaces);
},
onClose: () {
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
if (spaceView != null) {
context.read<ChatSettingsCubit>().refreshSources(spaceView);
}
context.read<ChatSettingsCubit>().refreshSources(spaces);
},
popupBuilder: (_) {
return BlocProvider.value(
@@ -395,12 +391,19 @@ class ChatSourceTreeItemInner extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlowyHover(
cursor: isSelectedSection ? SystemMouseCursors.basic : null,
style: HoverStyle(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
hoverColor: isSelectedSection
? Colors.transparent
: AFThemeExtension.of(context).lightGreyHover,
),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => onSelected?.call(chatSource),
onTap: () {
if (!isSelectedSection) {
onSelected?.call(chatSource);
}
},
child: Row(
children: [
const HSpace(4.0),
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -19,6 +20,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_w
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -243,7 +245,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
constraints: const BoxConstraints.tightFor(width: 300, height: 400),
onClose: () {
if (spaceView != null) {
context.read<ChatSettingsCubit>().refreshSources(spaceView);
context.read<ChatSettingsCubit>().refreshSources([spaceView]);
}
widget.onOverrideVisibility?.call(false);
},
@@ -272,12 +274,12 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
onPressed: () async {
final documentId = getOpenedDocumentId();
if (documentId != null) {
await onAddToExistingPage(documentId);
DocumentBloc.findOpen(documentId)?.forceReloadDocumentState();
await onAddToExistingPage(context, documentId);
await forceReloadAndUpdateSelection(documentId);
} else {
widget.onOverrideVisibility?.call(true);
if (spaceView != null) {
context.read<ChatSettingsCubit>().refreshSources(spaceView);
context.read<ChatSettingsCubit>().refreshSources([spaceView]);
}
popoverController.show();
}
@@ -296,9 +298,8 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
},
onAddToExistingPage: (documentId) async {
popoverController.close();
await onAddToExistingPage(documentId);
final view =
await ViewBackendService.getView(documentId).toNullable();
final view = await onAddToExistingPage(context, documentId);
if (context.mounted) {
openPageFromMessage(context, view);
}
@@ -307,12 +308,20 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
);
}
Future<void> onAddToExistingPage(String documentId) async {
Future<ViewPB?> onAddToExistingPage(
BuildContext context,
String documentId,
) async {
await ChatEditDocumentService.addMessageToPage(
documentId,
widget.textMessage,
);
await Future.delayed(const Duration(milliseconds: 500));
final view = await ViewBackendService.getView(documentId).toNullable();
if (context.mounted) {
showSaveMessageSuccessToast(context, view);
}
return view;
}
void addMessageToNewPage(BuildContext context) async {
@@ -325,12 +334,63 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
chatView.parentViewId,
[widget.textMessage],
);
if (context.mounted) {
showSaveMessageSuccessToast(context, newView);
openPageFromMessage(context, newView);
}
}
}
void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {
if (view == null) {
return;
}
showToastNotification(
context,
richMessage: TextSpan(
children: [
TextSpan(
text: LocaleKeys.chat_addToNewPageSuccessToast.tr(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: const Color(0xFFFFFFFF),
),
),
const TextSpan(
text: ' ',
),
TextSpan(
text: view.nameOrDefault,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: const Color(0xFFFFFFFF),
fontWeight: FontWeight.w700,
),
),
],
),
);
}
Future<void> forceReloadAndUpdateSelection(String documentId) async {
final bloc = DocumentBloc.findOpen(documentId);
if (bloc == null) {
return;
}
await bloc.forceReloadDocumentState();
await Future.delayed(const Duration(milliseconds: 500));
final editorState = bloc.state.editorState;
final lastNodePath = editorState?.getLastSelectable()?.$1.path;
if (editorState == null || lastNodePath == null) {
return;
}
unawaited(
editorState.updateSelectionWithReason(
Selection.collapsed(Position(path: lastNodePath)),
),
);
}
String? getOpenedDocumentId() {
final pageManager = getIt<TabsBloc>().state.currentPageManager;
if (!pageManager.showSecondaryPluginNotifier.value) {
@@ -1,11 +1,17 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
import 'package:time/time.dart';
class AIMessageMetadata extends StatefulWidget {
@@ -68,30 +74,44 @@ class _AIMessageMetadataState extends State<AIMessageMetadata> {
runSpacing: 4.0,
children: widget.sources.map(
(m) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 24,
maxWidth: 240,
),
child: FlowyButton(
margin: const EdgeInsets.all(4.0),
useIntrinsicWidth: true,
radius: BorderRadius.circular(8.0),
text: FlowyText(
m.name,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
leftIcon: FlowySvg(
FlowySvgs.icon_document_s,
size: const Size.square(16),
color: Theme.of(context).hintColor,
),
onTap: () {
widget.onSelectedMetadata?.call(m);
if (isURL(m.id)) {
return _MetadataButton(
name: m.id,
onTap: () => widget.onSelectedMetadata?.call(m),
);
} else if (isUUID(m.id)) {
return FutureBuilder<ViewPB?>(
future: ViewBackendService.getView(m.id)
.then((f) => f.toNullable()),
builder: (context, snapshot) {
final data = snapshot.data;
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done ||
data == null) {
return _MetadataButton(
name: m.name,
onTap: () => widget.onSelectedMetadata?.call(m),
);
}
return BlocProvider(
create: (_) => ViewBloc(view: data),
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
return _MetadataButton(
name: state.view.nameOrDefault,
onTap: () => widget.onSelectedMetadata?.call(m),
);
},
),
);
},
),
);
);
} else {
return _MetadataButton(
name: m.name,
onTap: () => widget.onSelectedMetadata?.call(m),
);
}
},
).toList(),
),
@@ -101,3 +121,39 @@ class _AIMessageMetadataState extends State<AIMessageMetadata> {
);
}
}
class _MetadataButton extends StatelessWidget {
const _MetadataButton({
this.name = "",
this.onTap,
});
final String name;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 24,
maxWidth: 240,
),
child: FlowyButton(
margin: const EdgeInsets.all(4.0),
useIntrinsicWidth: true,
radius: BorderRadius.circular(8.0),
text: FlowyText(
name,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
leftIcon: FlowySvg(
FlowySvgs.icon_document_s,
size: const Size.square(16),
color: Theme.of(context).hintColor,
),
onTap: onTap,
),
);
}
}
@@ -100,7 +100,7 @@ class ChatAIMessageWidget extends StatelessWidget {
onSelectedMetadata: onSelectedMetadata,
),
if (state.sources.isNotEmpty && !isLastMessage)
const VSpace(16.0),
const VSpace(8.0),
],
),
);
@@ -58,7 +58,7 @@ class _ChatErrorMessageWidgetState extends State<ChatErrorMessageWidget> {
mainAxisSize: MainAxisSize.min,
children: [
const FlowySvg(
FlowySvgs.warning_filled_s,
FlowySvgs.toast_error_filled_s,
blendMode: null,
),
const HSpace(8.0),
@@ -1,8 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/widgets.dart';
import 'package:universal_platform/universal_platform.dart';
@@ -10,6 +13,11 @@ import 'package:universal_platform/universal_platform.dart';
/// on mobile
void openPageFromMessage(BuildContext context, ViewPB? view) {
if (view == null) {
showToastNotification(
context,
message: LocaleKeys.chat_openPagePreviewFailedToast.tr(),
type: ToastificationType.error,
);
return;
}
if (UniversalPlatform.isDesktop) {
@@ -13,6 +13,18 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
EmojiData? kCachedEmojiData;
const _kRecentEmojiCategoryId = 'Recent';
class EmojiPickerResult {
EmojiPickerResult({
required this.emojiId,
required this.emoji,
this.isRandom = false,
});
final String emojiId;
final String emoji;
final bool isRandom;
}
class FlowyEmojiPicker extends StatefulWidget {
const FlowyEmojiPicker({
super.key,
@@ -21,7 +33,7 @@ class FlowyEmojiPicker extends StatefulWidget {
this.ensureFocus = false,
});
final EmojiSelectedCallback onEmojiSelected;
final ValueChanged<EmojiPickerResult> onEmojiSelected;
final int emojiPerLine;
final bool ensureFocus;
@@ -70,7 +82,9 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,
),
onEmojiSelected: (id, emoji) {
widget.onEmojiSelected.call(id, emoji);
widget.onEmojiSelected.call(
EmojiPickerResult(emojiId: id, emoji: emoji),
);
RecentIcons.putEmoji(id);
},
padding: const EdgeInsets.symmetric(horizontal: 16.0),
@@ -106,7 +120,12 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
onSkinToneChanged: (value) {
skinTone.value = value;
},
onRandomEmojiSelected: widget.onEmojiSelected,
onRandomEmojiSelected: (id, emoji) {
widget.onEmojiSelected.call(
EmojiPickerResult(emojiId: id, emoji: emoji, isRandom: true),
);
RecentIcons.putEmoji(id);
},
),
);
},
@@ -11,14 +11,17 @@ class MobileEmojiPickerScreen extends StatelessWidget {
const MobileEmojiPickerScreen({
super.key,
this.title,
this.selectedType,
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
});
final PickerTabType? selectedType;
final String? title;
final List<PickerTabType> tabs;
static const routeName = '/emoji_picker';
static const pageTitle = 'title';
static const iconSelectedType = 'iconSelectedType';
static const selectTabs = 'tabs';
@override
@@ -30,8 +33,9 @@ class MobileEmojiPickerScreen extends StatelessWidget {
body: SafeArea(
child: FlowyIconEmojiPicker(
tabs: tabs,
initialType: selectedType,
onSelectedEmoji: (r) {
context.pop<EmojiIconData>(r);
context.pop<EmojiIconData>(r.data);
},
),
),
@@ -244,9 +244,9 @@ class _RenameRowPopoverState extends State<RenameRowPopover> {
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 18),
defaultIcon: const FlowySvg(FlowySvgs.document_s),
onSubmitted: (emoji, _) {
widget.onUpdateIcon(emoji);
PopoverContainer.of(context).close();
onSubmitted: (r, _) {
widget.onUpdateIcon(r.data);
if (!r.keepOpen) PopoverContainer.of(context).close();
},
),
const HSpace(6),
@@ -422,8 +422,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
);
}
void forceReloadDocumentState() {
_documentCollabAdapter.syncV3();
Future<void> forceReloadDocumentState() {
return _documentCollabAdapter.syncV3();
}
// this is only used for debug mode
@@ -529,6 +529,11 @@ HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder(
) {
return HeadingBlockComponentBuilder(
configuration: configuration.copyWith(
textStyle: (node) => _buildTextStyleInTableCell(
context,
node: node,
configuration: configuration,
),
padding: (node) {
if (customHeadingPadding != null) {
return customHeadingPadding;
@@ -567,7 +572,9 @@ HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder(
configuration: configuration,
),
),
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
textStyleBuilder: (level) {
return styleCustomizer.headingStyleBuilder(level);
},
);
}
@@ -505,6 +505,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
Position(path: lastNode.path),
);
}
transaction.customSelectionType = SelectionType.inline;
transaction.reason = SelectionUpdateReason.uiEvent;
await editorState.apply(transaction);
}
@@ -27,11 +27,11 @@ enum OptionAlignType {
FlowySvgData get svg {
switch (this) {
case OptionAlignType.left:
return FlowySvgs.align_left_s;
return FlowySvgs.table_align_left_s;
case OptionAlignType.center:
return FlowySvgs.align_center_s;
return FlowySvgs.table_align_center_s;
case OptionAlignType.right:
return FlowySvgs.align_right_s;
return FlowySvgs.table_align_right_s;
}
}
@@ -26,8 +26,10 @@ class EmojiPickerButton extends StatelessWidget {
final EmojiIconData emoji;
final double emojiSize;
final Size emojiPickerSize;
final void Function(EmojiIconData emoji, PopoverController? controller)
onSubmitted;
final void Function(
SelectedEmojiIconResult result,
PopoverController? controller,
) onSubmitted;
final PopoverController popoverController = PopoverController();
final Widget? defaultIcon;
final Offset? offset;
@@ -85,8 +87,10 @@ class _DesktopEmojiPickerButton extends StatelessWidget {
final EmojiIconData emoji;
final double emojiSize;
final Size emojiPickerSize;
final void Function(EmojiIconData emoji, PopoverController? controller)
onSubmitted;
final void Function(
SelectedEmojiIconResult result,
PopoverController? controller,
) onSubmitted;
final PopoverController popoverController = PopoverController();
final Widget? defaultIcon;
final Offset? offset;
@@ -113,6 +117,7 @@ class _DesktopEmojiPickerButton extends StatelessWidget {
height: emojiPickerSize.height,
padding: const EdgeInsets.all(4.0),
child: FlowyIconEmojiPicker(
initialType: emoji.type.toPickerTabType(),
onSelectedEmoji: (r) {
onSubmitted(r, popoverController);
},
@@ -156,8 +161,10 @@ class _MobileEmojiPickerButton extends StatelessWidget {
final EmojiIconData emoji;
final double emojiSize;
final void Function(EmojiIconData emoji, PopoverController? controller)
onSubmitted;
final void Function(
SelectedEmojiIconResult result,
PopoverController? controller,
) onSubmitted;
final String? title;
final bool enable;
final EdgeInsets? margin;
@@ -177,11 +184,14 @@ class _MobileEmojiPickerButton extends StatelessWidget {
final result = await context.push<EmojiIconData>(
Uri(
path: MobileEmojiPickerScreen.routeName,
queryParameters: {MobileEmojiPickerScreen.pageTitle: title},
queryParameters: {
MobileEmojiPickerScreen.pageTitle: title,
MobileEmojiPickerScreen.iconSelectedType: emoji.type.name,
},
).toString(),
);
if (result != null) {
onSubmitted(result, null);
onSubmitted(result.toSelectedResult(), null);
}
}
: null,
@@ -227,11 +227,12 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
value: pageStyleIconBloc,
child: Expanded(
child: FlowyIconEmojiPicker(
initialType: icon.type.toPickerTabType(),
onSelectedEmoji: (r) {
pageStyleIconBloc.add(
PageStyleIconEvent.updateIcon(r, true),
PageStyleIconEvent.updateIcon(r.data, true),
);
Navigator.pop(context);
if (!r.keepOpen) Navigator.pop(context);
},
),
),
@@ -468,9 +468,9 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
popupBuilder: (BuildContext popoverContext) {
isPopoverOpen = true;
return FlowyIconEmojiPicker(
onSelectedEmoji: (result) {
widget.onIconOrCoverChanged(icon: result);
_popoverController.close();
onSelectedEmoji: (r) {
widget.onIconOrCoverChanged(icon: r.data);
if (!r.keepOpen) _popoverController.close();
},
);
},
@@ -838,9 +838,7 @@ class _DocumentIconState extends State<DocumentIcon> {
@override
Widget build(BuildContext context) {
Widget child = EmojiIconWidget(
emoji: widget.icon,
);
Widget child = EmojiIconWidget(emoji: widget.icon);
if (UniversalPlatform.isDesktopOrWeb) {
child = AppFlowyPopover(
@@ -852,9 +850,10 @@ class _DocumentIconState extends State<DocumentIcon> {
child: child,
popupBuilder: (BuildContext popoverContext) {
return FlowyIconEmojiPicker(
onSelectedEmoji: (result) {
widget.onChangeIcon(result);
_popoverController.close();
initialType: widget.icon.type.toPickerTabType(),
onSelectedEmoji: (r) {
widget.onChangeIcon(r.data);
if (!r.keepOpen) _popoverController.close();
},
);
},
@@ -864,7 +863,12 @@ class _DocumentIconState extends State<DocumentIcon> {
child: child,
onTap: () async {
final result = await context.push<EmojiIconData>(
MobileEmojiPickerScreen.routeName,
Uri(
path: MobileEmojiPickerScreen.routeName,
queryParameters: {
MobileEmojiPickerScreen.iconSelectedType: widget.icon.type.name,
},
).toString(),
);
if (result != null) {
widget.onChangeIcon(result);
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
@@ -67,24 +68,35 @@ class RawEmojiIconWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final defaultEmoji = EmojiText(
emoji: '❓',
fontSize: emojiSize,
textAlign: TextAlign.center,
final defaultEmoji = SizedBox(
width: emojiSize,
child: EmojiText(
emoji: '❓',
fontSize: emojiSize,
textAlign: TextAlign.center,
),
);
try {
switch (emoji.type) {
case FlowyIconType.emoji:
return EmojiText(
emoji: emoji.emoji,
fontSize: emojiSize,
textAlign: TextAlign.center,
return SizedBox(
width: emojiSize,
child: EmojiText(
emoji: emoji.emoji,
fontSize: emojiSize,
textAlign: TextAlign.center,
),
);
case FlowyIconType.icon:
final iconData = IconsData.fromJson(jsonDecode(emoji.emoji));
/// Under the same width conditions, icons on macOS seem to appear
/// larger than emojis, so 0.9 is used here to slightly reduce the
/// size of the icons
final iconSize = Platform.isMacOS ? emojiSize * 0.9 : emojiSize;
return IconWidget(
data: iconData,
size: emojiSize,
size: iconSize,
);
default:
return defaultEmoji;
@@ -173,8 +173,8 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Row(
mainAxisSize: MainAxisSize.min,
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
'@$formattedDate',
@@ -10,6 +10,7 @@ import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/shared/clipboard_state.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
@@ -510,7 +511,10 @@ class _MentionPageBlockContent extends StatelessWidget {
);
if (blockContent == null || blockContent.isEmpty) {
return shouldDisplayViewName ? view.name : '';
return shouldDisplayViewName
? view.name
.orDefault(LocaleKeys.menuAppHeader_defaultNewPageName.tr())
: '';
}
return shouldDisplayViewName
@@ -20,6 +20,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
abstract class AppFlowyMobileToolbarWidgetService {
void closeItemMenu();
void closeKeyboard();
PropertyValueNotifier<bool> get showMenuNotifier;
@@ -179,7 +180,13 @@ class _MobileToolbarState extends State<_MobileToolbar>
// but in this case, we don't want to update the cached keyboard height.
// This is because we want to keep the same height when the menu is shown.
bool canUpdateCachedKeyboardHeight = true;
ValueNotifier<double> cachedKeyboardHeight = ValueNotifier(0.0);
/// when the [_MobileToolbar] disposed before the keyboard height can be updated in time,
/// there will be an issue with the height being 0
/// this is used to globally record the height.
static double _globalCachedKeyboardHeight = 0.0;
ValueNotifier<double> cachedKeyboardHeight =
ValueNotifier(_globalCachedKeyboardHeight);
// used to check if click the same item again
int? selectedMenuIndex;
@@ -408,6 +415,9 @@ class _MobileToolbarState extends State<_MobileToolbar>
);
}
}
if (keyboardHeight > 0) {
_globalCachedKeyboardHeight = keyboardHeight;
}
return SizedBox(
height: keyboardHeight,
child: (showingMenu && selectedMenuIndex != null)
@@ -19,26 +19,29 @@ class NumberedListIcon extends StatelessWidget {
Widget build(BuildContext context) {
final textStyleConfiguration =
context.read<EditorState>().editorStyle.textStyleConfiguration;
final fontSize = textStyleConfiguration.text.fontSize ?? 16.0;
final height =
textStyleConfiguration.text.height ?? textStyleConfiguration.lineHeight;
final combinedTextStyle = textStyle?.combine(textStyleConfiguration.text) ??
textStyleConfiguration.text;
final size = fontSize * height;
return Container(
constraints: BoxConstraints(
minWidth: size,
minHeight: size,
),
margin: const EdgeInsets.only(top: 0.5, right: 8.0),
alignment: Alignment.center,
child: Center(
child: Text(
node.levelString,
style: combinedTextStyle,
strutStyle: StrutStyle.fromTextStyle(combinedTextStyle),
textDirection: textDirection,
final adjustedTextStyle = combinedTextStyle.copyWith(
height: height,
fontFeatures: [const FontFeature.tabularFigures()],
);
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(
node.levelString,
style: adjustedTextStyle,
strutStyle: StrutStyle.fromTextStyle(combinedTextStyle),
textHeightBehavior: TextHeightBehavior(
applyHeightToFirstAscent:
textStyleConfiguration.applyHeightToFirstAscent,
applyHeightToLastDescent:
textStyleConfiguration.applyHeightToLastDescent,
leadingDistribution: textStyleConfiguration.leadingDistribution,
),
textDirection: textDirection,
),
);
}
@@ -35,7 +35,7 @@ class _PageStyleIconState extends State<PageStyleIcon> {
builder: (context, state) {
final icon = state.icon ?? EmojiIconData.none();
return GestureDetector(
onTap: () => _showIconSelector(context),
onTap: () => _showIconSelector(context, icon),
behavior: HitTestBehavior.opaque,
child: Container(
height: 52,
@@ -66,7 +66,7 @@ class _PageStyleIconState extends State<PageStyleIcon> {
);
}
void _showIconSelector(BuildContext context) {
void _showIconSelector(BuildContext context, EmojiIconData icon) {
Navigator.pop(context);
final pageStyleIconBloc = PageStyleIconBloc(view: widget.view)
..add(const PageStyleIconEvent.initial());
@@ -85,11 +85,12 @@ class _PageStyleIconState extends State<PageStyleIcon> {
value: pageStyleIconBloc,
child: Expanded(
child: FlowyIconEmojiPicker(
initialType: icon.type.toPickerTabType(),
onSelectedEmoji: (r) {
pageStyleIconBloc.add(
PageStyleIconEvent.updateIcon(r, true),
PageStyleIconEvent.updateIcon(r.data, true),
);
Navigator.pop(ctx);
if (!r.keepOpen) Navigator.pop(ctx);
},
),
),
@@ -456,7 +456,7 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
return context.simpleTableDefaultHeaderColor;
}
return Theme.of(context).colorScheme.surface;
return Colors.transparent;
}
bool _isInHeader() {
@@ -269,6 +269,8 @@ class SimpleTableConstants {
static const actionSheetContentSectionHeight = 44.0;
static const actionSheetNormalActionSectionHeight = 48.0;
static const actionSheetButtonRadius = 12.0;
static const actionSheetBottomSheetHeight = 320.0;
}
enum SimpleTableBorderRenderType {
@@ -308,4 +310,7 @@ extension SimpleTableColors on BuildContext {
Color get simpleTableInsertActionBackgroundColor => Theme.of(this).isLightMode
? const Color(0xFFF2F2F7)
: const Color(0xFF2D3036);
Color? get simpleTableQuickActionBackgroundColor =>
Theme.of(this).isLightMode ? null : const Color(0xFFBBC3CD);
}
@@ -438,6 +438,8 @@ class SimpleTableActionMenu extends StatelessWidget {
return;
}
final beforeSelection = editorState.selection;
// increase the keep editor focus notifier to prevent the editor from losing focus
keepEditorFocusNotifier.increase();
@@ -469,6 +471,8 @@ class SimpleTableActionMenu extends StatelessWidget {
context,
showDragHandle: true,
showDivider: false,
useSafeArea: false,
enablePadding: false,
builder: (context) => Provider.value(
value: simpleTableContext,
child: SimpleTableBottomSheet(
@@ -482,7 +486,13 @@ class SimpleTableActionMenu extends StatelessWidget {
keepEditorFocusNotifier.decrease();
// remove the extra info
editorState.selectionType = null;
editorState.selectionExtraInfo = null;
if (beforeSelection != null) {
await editorState.updateSelectionWithReason(
beforeSelection,
customSelectionType: SelectionType.inline,
reason: SelectionUpdateReason.uiEvent,
extraInfo: {},
);
}
}
}
@@ -137,8 +137,8 @@ extension TableContentOperation on EditorState {
final cell = columnIndex >= row.children.length
? row.children.last
: row.children[columnIndex];
final startNode = cell.getFirstChildIndex();
final endNode = cell.getLastChildIndex();
final startNode = cell.getFirstFocusableChild();
final endNode = cell.getLastFocusableChild();
if (startNode == null || endNode == null) {
continue;
}
@@ -200,8 +200,8 @@ extension TableContentOperation on EditorState {
final row = tableNode.children[rowIndex];
for (var i = 0; i < row.children.length; i++) {
final cell = row.children[i];
final startNode = cell.getFirstChildIndex();
final endNode = cell.getLastChildIndex();
final startNode = cell.getFirstFocusableChild();
final endNode = cell.getLastFocusableChild();
if (startNode == null || endNode == null) {
continue;
}
@@ -255,8 +255,8 @@ extension TableContentOperation on EditorState {
final row = tableNode.children[i];
for (var j = 0; j < row.children.length; j++) {
final cell = row.children[j];
final startNode = cell.getFirstChildIndex();
final endNode = cell.getLastChildIndex();
final startNode = cell.getFirstFocusableChild();
final endNode = cell.getLastFocusableChild();
if (startNode == null || endNode == null) {
continue;
}
@@ -104,6 +104,18 @@ extension TableMapOperation on Node {
comparator: (iKey, index) => iKey >= index,
);
final rowBoldAttributes = _remapSource(
this.rowBoldAttributes,
index,
comparator: (iKey, index) => iKey >= index,
);
final rowTextColors = _remapSource(
this.rowTextColors,
index,
comparator: (iKey, index) => iKey >= index,
);
return attributes
.mergeValues(
SimpleTableBlockKeys.rowColors,
@@ -112,6 +124,14 @@ extension TableMapOperation on Node {
.mergeValues(
SimpleTableBlockKeys.rowAligns,
rowAligns,
)
.mergeValues(
SimpleTableBlockKeys.rowBoldAttributes,
rowBoldAttributes,
)
.mergeValues(
SimpleTableBlockKeys.rowTextColors,
rowTextColors,
);
} catch (e) {
Log.warn('Failed to map row insertion attributes: $e');
@@ -167,6 +187,18 @@ extension TableMapOperation on Node {
comparator: (iKey, index) => iKey >= index,
);
final columnBoldAttributes = _remapSource(
this.columnBoldAttributes,
index,
comparator: (iKey, index) => iKey >= index,
);
final columnTextColors = _remapSource(
this.columnTextColors,
index,
comparator: (iKey, index) => iKey >= index,
);
final bool distributeColumnWidthsEvenly =
attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly] ??
false;
@@ -189,6 +221,14 @@ extension TableMapOperation on Node {
.mergeValues(
SimpleTableBlockKeys.columnWidths,
columnWidths,
)
.mergeValues(
SimpleTableBlockKeys.columnBoldAttributes,
columnBoldAttributes,
)
.mergeValues(
SimpleTableBlockKeys.columnTextColors,
columnTextColors,
);
} catch (e) {
Log.warn('Failed to map row insertion attributes: $e');
@@ -238,6 +278,18 @@ extension TableMapOperation on Node {
index,
);
final (rowBoldAttributes, duplicatedRowBoldAttribute) =
_findDuplicatedEntryAndRemap(
this.rowBoldAttributes,
index,
);
final (rowTextColors, duplicatedRowTextColor) =
_findDuplicatedEntryAndRemap(
this.rowTextColors,
index,
);
return attributes
.mergeValues(
SimpleTableBlockKeys.rowColors,
@@ -248,6 +300,16 @@ extension TableMapOperation on Node {
SimpleTableBlockKeys.rowAligns,
rowAligns,
duplicatedEntry: duplicatedRowAlign,
)
.mergeValues(
SimpleTableBlockKeys.rowBoldAttributes,
rowBoldAttributes,
duplicatedEntry: duplicatedRowBoldAttribute,
)
.mergeValues(
SimpleTableBlockKeys.rowTextColors,
rowTextColors,
duplicatedEntry: duplicatedRowTextColor,
);
} catch (e) {
Log.warn('Failed to map row insertion attributes: $e');
@@ -304,6 +366,18 @@ extension TableMapOperation on Node {
index,
);
final (columnBoldAttributes, duplicatedColumnBoldAttribute) =
_findDuplicatedEntryAndRemap(
this.columnBoldAttributes,
index,
);
final (columnTextColors, duplicatedColumnTextColor) =
_findDuplicatedEntryAndRemap(
this.columnTextColors,
index,
);
return attributes
.mergeValues(
SimpleTableBlockKeys.columnColors,
@@ -319,6 +393,16 @@ extension TableMapOperation on Node {
SimpleTableBlockKeys.columnWidths,
columnWidths,
duplicatedEntry: duplicatedColumnWidth,
)
.mergeValues(
SimpleTableBlockKeys.columnBoldAttributes,
columnBoldAttributes,
duplicatedEntry: duplicatedColumnBoldAttribute,
)
.mergeValues(
SimpleTableBlockKeys.columnTextColors,
columnTextColors,
duplicatedEntry: duplicatedColumnTextColor,
);
} catch (e) {
Log.warn('Failed to map column duplication attributes: $e');
@@ -364,6 +448,7 @@ extension TableMapOperation on Node {
comparator: (iKey, index) => iKey > index,
filterIndex: index,
);
final columnAligns = _remapSource(
this.columnAligns,
index,
@@ -371,6 +456,7 @@ extension TableMapOperation on Node {
comparator: (iKey, index) => iKey > index,
filterIndex: index,
);
final columnWidths = _remapSource(
this.columnWidths,
index,
@@ -379,6 +465,22 @@ extension TableMapOperation on Node {
filterIndex: index,
);
final columnBoldAttributes = _remapSource(
this.columnBoldAttributes,
index,
increment: false,
comparator: (iKey, index) => iKey > index,
filterIndex: index,
);
final columnTextColors = _remapSource(
this.columnTextColors,
index,
increment: false,
comparator: (iKey, index) => iKey > index,
filterIndex: index,
);
return attributes
.mergeValues(
SimpleTableBlockKeys.columnColors,
@@ -391,6 +493,14 @@ extension TableMapOperation on Node {
.mergeValues(
SimpleTableBlockKeys.columnWidths,
columnWidths,
)
.mergeValues(
SimpleTableBlockKeys.columnBoldAttributes,
columnBoldAttributes,
)
.mergeValues(
SimpleTableBlockKeys.columnTextColors,
columnTextColors,
);
} catch (e) {
Log.warn('Failed to map column deletion attributes: $e');
@@ -443,6 +553,22 @@ extension TableMapOperation on Node {
filterIndex: index,
);
final rowBoldAttributes = _remapSource(
this.rowBoldAttributes,
index,
increment: false,
comparator: (iKey, index) => iKey > index,
filterIndex: index,
);
final rowTextColors = _remapSource(
this.rowTextColors,
index,
increment: false,
comparator: (iKey, index) => iKey > index,
filterIndex: index,
);
return attributes
.mergeValues(
SimpleTableBlockKeys.rowColors,
@@ -451,6 +577,14 @@ extension TableMapOperation on Node {
.mergeValues(
SimpleTableBlockKeys.rowAligns,
rowAligns,
)
.mergeValues(
SimpleTableBlockKeys.rowBoldAttributes,
rowBoldAttributes,
)
.mergeValues(
SimpleTableBlockKeys.rowTextColors,
rowTextColors,
);
} catch (e) {
Log.warn('Failed to map row deletion attributes: $e');
@@ -531,6 +665,10 @@ extension TableMapOperation on Node {
final duplicatedColumnColor = this.columnColors[fromIndex.toString()];
final duplicatedColumnAlign = this.columnAligns[fromIndex.toString()];
final duplicatedColumnWidth = this.columnWidths[fromIndex.toString()];
final duplicatedColumnBoldAttribute =
this.columnBoldAttributes[fromIndex.toString()];
final duplicatedColumnTextColor =
this.columnTextColors[fromIndex.toString()];
/// Case 1: fromIndex > toIndex
/// Before:
@@ -619,6 +757,34 @@ extension TableMapOperation on Node {
filterIndex: fromIndex,
);
final columnBoldAttributes = _remapSource(
this.columnBoldAttributes,
fromIndex,
increment: fromIndex > toIndex,
comparator: (iKey, index) {
if (fromIndex > toIndex) {
return iKey < fromIndex && iKey >= toIndex;
} else {
return iKey > fromIndex && iKey <= toIndex;
}
},
filterIndex: fromIndex,
);
final columnTextColors = _remapSource(
this.columnTextColors,
fromIndex,
increment: fromIndex > toIndex,
comparator: (iKey, index) {
if (fromIndex > toIndex) {
return iKey < fromIndex && iKey >= toIndex;
} else {
return iKey > fromIndex && iKey <= toIndex;
}
},
filterIndex: fromIndex,
);
return attributes
.mergeValues(
SimpleTableBlockKeys.columnColors,
@@ -652,6 +818,28 @@ extension TableMapOperation on Node {
)
: null,
removeNullValue: true,
)
.mergeValues(
SimpleTableBlockKeys.columnBoldAttributes,
columnBoldAttributes,
duplicatedEntry: duplicatedColumnBoldAttribute != null
? MapEntry(
toIndex.toString(),
duplicatedColumnBoldAttribute,
)
: null,
removeNullValue: true,
)
.mergeValues(
SimpleTableBlockKeys.columnTextColors,
columnTextColors,
duplicatedEntry: duplicatedColumnTextColor != null
? MapEntry(
toIndex.toString(),
duplicatedColumnTextColor,
)
: null,
removeNullValue: true,
);
} catch (e) {
Log.warn('Failed to map column deletion attributes: $e');
@@ -667,6 +855,9 @@ extension TableMapOperation on Node {
try {
final duplicatedRowColor = this.rowColors[fromIndex.toString()];
final duplicatedRowAlign = this.rowAligns[fromIndex.toString()];
final duplicatedRowBoldAttribute =
this.rowBoldAttributes[fromIndex.toString()];
final duplicatedRowTextColor = this.rowTextColors[fromIndex.toString()];
/// Example:
/// Case 1: fromIndex > toIndex
@@ -742,6 +933,34 @@ extension TableMapOperation on Node {
filterIndex: fromIndex,
);
final rowBoldAttributes = _remapSource(
this.rowBoldAttributes,
fromIndex,
increment: fromIndex > toIndex,
comparator: (iKey, index) {
if (fromIndex > toIndex) {
return iKey < fromIndex && iKey >= toIndex;
} else {
return iKey > fromIndex && iKey <= toIndex;
}
},
filterIndex: fromIndex,
);
final rowTextColors = _remapSource(
this.rowTextColors,
fromIndex,
increment: fromIndex > toIndex,
comparator: (iKey, index) {
if (fromIndex > toIndex) {
return iKey < fromIndex && iKey >= toIndex;
} else {
return iKey > fromIndex && iKey <= toIndex;
}
},
filterIndex: fromIndex,
);
return attributes
.mergeValues(
SimpleTableBlockKeys.rowColors,
@@ -764,6 +983,28 @@ extension TableMapOperation on Node {
)
: null,
removeNullValue: true,
)
.mergeValues(
SimpleTableBlockKeys.rowBoldAttributes,
rowBoldAttributes,
duplicatedEntry: duplicatedRowBoldAttribute != null
? MapEntry(
toIndex.toString(),
duplicatedRowBoldAttribute,
)
: null,
removeNullValue: true,
)
.mergeValues(
SimpleTableBlockKeys.rowTextColors,
rowTextColors,
duplicatedEntry: duplicatedRowTextColor != null
? MapEntry(
toIndex.toString(),
duplicatedRowTextColor,
)
: null,
removeNullValue: true,
);
} catch (e) {
Log.warn('Failed to map row reordering attributes: $e');
@@ -805,21 +805,21 @@ extension TableNodeExtension on Node {
/// Get first focusable child in the table cell.
///
/// If the current node is not a table cell node, it will return null.
Node? getFirstChildIndex() {
Node? getFirstFocusableChild() {
if (children.isEmpty) {
return this;
}
return children.first.getFirstChildIndex();
return children.first.getFirstFocusableChild();
}
/// Get last focusable child in the table cell.
///
/// If the current node is not a table cell node, it will return null.
Node? getLastChildIndex() {
Node? getLastFocusableChild() {
if (children.isEmpty) {
return this;
}
return children.last.getLastChildIndex();
return children.last.getLastFocusableChild();
}
/// Get table align of column
@@ -1,5 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@@ -40,6 +42,7 @@ KeyEventResult _arrowDownInTableCellHandler(EditorState editorState) {
if (rowIndex == parentTableNode.rowLength - 1) {
// focus on the next block
final nextNode = tableCellNode.next;
final nextBlock = tableCellNode.parentTableNode?.next;
if (nextNode != null) {
final nextFocusableSibling = parentTableNode.getNextFocusableSibling();
if (nextFocusableSibling != null) {
@@ -51,6 +54,16 @@ KeyEventResult _arrowDownInTableCellHandler(EditorState editorState) {
),
);
}
} else if (nextBlock != null) {
if (nextBlock.type != SimpleTableBlockKeys.type) {
newSelection = Selection.collapsed(
Position(
path: nextBlock.path,
),
);
} else {
return tableNavigationArrowDownCommand.handler(editorState);
}
}
} else {
// focus on next cell in the same column
@@ -4,10 +4,12 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_tab
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart';
final simpleTableCommands = [
tableNavigationArrowDownCommand,
arrowUpInTableCell,
arrowDownInTableCell,
arrowLeftInTableCell,
@@ -0,0 +1,40 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
final CommandShortcutEvent tableNavigationArrowDownCommand =
CommandShortcutEvent(
key: 'table navigation',
getDescription: () => 'table navigation',
command: 'arrow down',
handler: _tableNavigationArrowDownHandler,
);
KeyEventResult _tableNavigationArrowDownHandler(EditorState editorState) {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return KeyEventResult.ignored;
}
final nextNode = editorState.getNodeAtPath(selection.start.path.next);
if (nextNode == null) {
return KeyEventResult.ignored;
}
if (nextNode.type == SimpleTableBlockKeys.type) {
final firstCell = nextNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
if (firstCell != null) {
final firstFocusableChild = firstCell.getFirstFocusableChild();
if (firstFocusableChild != null) {
editorState.updateSelectionWithReason(
Selection.collapsed(
Position(path: firstFocusableChild.path),
),
);
return KeyEventResult.handled;
}
}
}
return KeyEventResult.ignored;
}
@@ -61,19 +61,12 @@ class SimpleTableCellQuickActions extends ISimpleTableBottomSheetActions {
SimpleTableMoreAction.copy,
),
),
FutureBuilder(
future: getIt<ClipboardService>().getData(),
builder: (context, snapshot) {
final hasContent = snapshot.data?.tableJson != null;
return SimpleTableQuickAction(
type: SimpleTableMoreAction.paste,
isEnabled: hasContent,
onTap: () => _onActionTap(
context,
SimpleTableMoreAction.paste,
),
);
},
SimpleTableQuickAction(
type: SimpleTableMoreAction.paste,
onTap: () => _onActionTap(
context,
SimpleTableMoreAction.paste,
),
),
SimpleTableQuickAction(
type: SimpleTableMoreAction.delete,
@@ -208,8 +201,10 @@ class SimpleTableQuickAction extends StatelessWidget {
onTapUp: isEnabled ? onTap : null,
child: FlowySvg(
type.leftIconSvg,
blendMode: null,
blendMode:
type == SimpleTableMoreAction.delete ? null : BlendMode.srcIn,
size: const Size.square(24),
color: context.simpleTableQuickActionBackgroundColor,
),
),
);
@@ -538,7 +533,11 @@ class SimpleTableCellActionButtons extends ISimpleTableBottomSheetActions {
}
// close the action menu
Navigator.of(context).pop();
// keep the action menu open if the action is enable header row or enable header column
if (action != SimpleTableMoreAction.enableHeaderRow &&
action != SimpleTableMoreAction.enableHeaderColumn) {
Navigator.of(context).pop();
}
}
}
@@ -199,8 +199,10 @@ class _SimpleTableMobileReorderButtonState
// show the bottom sheet
await showMobileBottomSheet(
context,
useSafeArea: false,
showDragHandle: true,
showDivider: false,
enablePadding: false,
builder: (context) => Provider.value(
value: simpleTableContext,
child: SimpleTableCellBottomSheet(
@@ -42,7 +42,12 @@ class SimpleTableAddColumnAndRowHoverButton extends StatelessWidget {
SimpleTableConstants.addColumnAndRowButtonBottomPadding,
right: SimpleTableConstants.addColumnButtonPadding,
child: SimpleTableAddColumnAndRowButton(
onTap: () => editorState.addColumnAndRowInTable(node),
onTap: () {
// cancel the selection to avoid flashing the selection
editorState.selection = null;
editorState.addColumnAndRowInTable(node);
},
),
)
: const SizedBox.shrink();
@@ -81,8 +81,12 @@ class _SimpleTableAddColumnHoverButtonState
child: Opacity(
opacity: shouldShow ? 1.0 : 0.0,
child: SimpleTableAddColumnButton(
onTap: () =>
widget.editorState.addColumnInTable(widget.tableNode),
onTap: () {
// cancel the selection to avoid flashing the selection
widget.editorState.selection = null;
widget.editorState.addColumnInTable(widget.tableNode);
},
onHorizontalDragStart: (details) {
context.read<SimpleTableContext>().isDraggingColumn = true;
startDraggingOffset = details.globalPosition;
@@ -87,9 +87,14 @@ class _SimpleTableAddRowHoverButtonState
SimpleTableConstants.cellBorderWidth,
right: SimpleTableConstants.addRowButtonRightPadding,
child: SimpleTableAddRowButton(
onTap: () => widget.editorState.addRowInTable(
widget.tableNode,
),
onTap: () {
// cancel the selection to avoid flashing the selection
widget.editorState.selection = null;
widget.editorState.addRowInTable(
widget.tableNode,
);
},
onVerticalDragStart: (details) {
context.read<SimpleTableContext>().isDraggingRow = true;
startDraggingOffset = details.globalPosition;
@@ -28,11 +28,13 @@ class SimpleTableCellBottomSheet extends StatefulWidget {
required this.type,
required this.cellNode,
required this.editorState,
this.scrollController,
});
final SimpleTableMoreActionType type;
final Node cellNode;
final EditorState editorState;
final ScrollController? scrollController;
@override
State<SimpleTableCellBottomSheet> createState() =>
@@ -81,7 +83,9 @@ class _SimpleTableCellBottomSheetState
_buildHeader(),
// content
..._buildContent(),
...menuState == _SimpleTableBottomSheetMenuState.cellActionMenu
? _buildScrollableContent()
: _buildNonScrollableContent(),
],
);
}
@@ -102,18 +106,51 @@ class _SimpleTableCellBottomSheetState
return BottomSheetHeader(
showBackButton: false,
showCloseButton: true,
showDoneButton: false,
showDoneButton: true,
showRemoveButton: false,
title: widget.type.name.capitalize(),
onClose: () => setState(() {
menuState = _SimpleTableBottomSheetMenuState.cellActionMenu;
}),
onDone: (_) => Navigator.pop(context),
);
default:
throw UnimplementedError('Unsupported menu state: $menuState');
}
}
List<Widget> _buildScrollableContent() {
return [
SizedBox(
height: SimpleTableConstants.actionSheetBottomSheetHeight,
child: Scrollbar(
controller: widget.scrollController,
child: SingleChildScrollView(
controller: widget.scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
..._buildContent(),
// safe area padding
VSpace(context.bottomSheetPadding() * 2),
],
),
),
),
),
];
}
List<Widget> _buildNonScrollableContent() {
return [
..._buildContent(),
// safe area padding
VSpace(context.bottomSheetPadding()),
];
}
List<Widget> _buildContent() {
switch (menuState) {
case _SimpleTableBottomSheetMenuState.cellActionMenu:
@@ -296,10 +333,12 @@ class SimpleTableBottomSheet extends StatefulWidget {
super.key,
required this.tableNode,
required this.editorState,
this.scrollController,
});
final Node tableNode;
final EditorState editorState;
final ScrollController? scrollController;
@override
State<SimpleTableBottomSheet> createState() => _SimpleTableBottomSheetState();
@@ -321,7 +360,24 @@ class _SimpleTableBottomSheetState extends State<SimpleTableBottomSheet> {
_buildHeader(),
// content
..._buildContent(),
SizedBox(
height: SimpleTableConstants.actionSheetBottomSheetHeight,
child: Scrollbar(
controller: widget.scrollController,
child: SingleChildScrollView(
controller: widget.scrollController,
child: Column(
children: [
// content
..._buildContent(),
// safe area padding
VSpace(context.bottomSheetPadding() * 2),
],
),
),
),
),
],
);
}
@@ -455,6 +455,21 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
final columnIndex = node.columnIndex;
final editorState = context.read<EditorState>();
editorState.insertColumnInTable(table, columnIndex);
final cell = table.getTableCellNode(
rowIndex: 0,
columnIndex: columnIndex,
);
if (cell == null) {
return;
}
// update selection
editorState.selection = Selection.collapsed(
Position(
path: cell.path.child(0),
),
);
}
void _insertColumnRight() {
@@ -466,6 +481,21 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
final columnIndex = node.columnIndex;
final editorState = context.read<EditorState>();
editorState.insertColumnInTable(table, columnIndex + 1);
final cell = table.getTableCellNode(
rowIndex: 0,
columnIndex: columnIndex + 1,
);
if (cell == null) {
return;
}
// update selection
editorState.selection = Selection.collapsed(
Position(
path: cell.path.child(0),
),
);
}
void _insertRowAbove() {
@@ -477,6 +507,18 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
final rowIndex = node.rowIndex;
final editorState = context.read<EditorState>();
editorState.insertRowInTable(table, rowIndex);
final cell = table.getTableCellNode(rowIndex: rowIndex, columnIndex: 0);
if (cell == null) {
return;
}
// update selection
editorState.selection = Selection.collapsed(
Position(
path: cell.path.child(0),
),
);
}
void _insertRowBelow() {
@@ -488,6 +530,18 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
final rowIndex = node.rowIndex;
final editorState = context.read<EditorState>();
editorState.insertRowInTable(table, rowIndex + 1);
final cell = table.getTableCellNode(rowIndex: rowIndex + 1, columnIndex: 0);
if (cell == null) {
return;
}
// update selection
editorState.selection = Selection.collapsed(
Position(
path: cell.path.child(0),
),
);
}
void _deleteRow() {
@@ -108,11 +108,18 @@ Future<bool> _showSlashMenu(
singleColumn: singleColumn,
style: style,
);
// disable the keyboard service
editorState.service.keyboardService?.disable();
if (!kIsWeb && Platform.environment.containsKey('FLUTTER_TEST')) {
await _selectionMenuService?.show();
} else {
await _selectionMenuService?.show();
}
// enable the keyboard service
editorState.service.keyboardService?.enable();
}
if (shouldInsertSlash) {
@@ -166,9 +166,10 @@ class EditorStyleCustomizer {
applyHeightToLastDescent: true,
),
textSpanDecorator: customizeAttributeDecorator,
mobileDragHandleBallSize: const Size.square(12.0),
magnifierSize: const Size(144, 96),
textScaleFactor: textScaleFactor,
mobileDragHandleLeftExtend: 12.0,
mobileDragHandleWidthExtend: 24.0,
);
}
@@ -34,6 +34,7 @@ class FlowyEmojiSearchBar extends StatefulWidget {
class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
final TextEditingController controller = TextEditingController();
EmojiSkinTone skinTone = lastSelectedEmojiSkinTone ?? EmojiSkinTone.none;
@override
void dispose() {
@@ -58,12 +59,18 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
),
const HSpace(8.0),
_RandomEmojiButton(
skinTone: skinTone,
emojiData: widget.emojiData,
onRandomEmojiSelected: widget.onRandomEmojiSelected,
),
const HSpace(8.0),
FlowyEmojiSkinToneSelector(
onEmojiSkinToneChanged: widget.onSkinToneChanged,
onEmojiSkinToneChanged: (v) {
setState(() {
skinTone = v;
});
widget.onSkinToneChanged.call(v);
},
),
],
),
@@ -73,10 +80,12 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
class _RandomEmojiButton extends StatelessWidget {
const _RandomEmojiButton({
required this.skinTone,
required this.emojiData,
required this.onRandomEmojiSelected,
});
final EmojiSkinTone skinTone;
final EmojiData emojiData;
final EmojiSelectedCallback onRandomEmojiSelected;
@@ -100,9 +109,14 @@ class _RandomEmojiButton extends StatelessWidget {
),
onTap: () {
final random = emojiData.random;
final emojiId = random.$1;
final emoji = emojiData.getEmojiById(
emojiId,
skinTone: skinTone,
);
onRandomEmojiSelected(
random.$1,
random.$2,
emojiId,
emoji,
);
},
),
@@ -131,6 +145,9 @@ class _SearchTextFieldState extends State<_SearchTextField> {
@override
void initState() {
super.initState();
/// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]
/// this is to ensure that focus can be regained within a short period of time
if (widget.ensureFocus) {
Future.delayed(const Duration(milliseconds: 200), () {
if (!mounted || focusNode.hasFocus) return;
@@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/icon.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart' hide Icon;
import 'package:flutter/services.dart';
import 'package:universal_platform/universal_platform.dart';
extension ToProto on FlowyIconType {
@@ -46,6 +47,10 @@ enum FlowyIconType {
custom;
}
extension FlowyIconTypeToPickerTabType on FlowyIconType {
PickerTabType? toPickerTabType() => name.toPickerTabType();
}
class EmojiIconData {
factory EmojiIconData.none() => const EmojiIconData(FlowyIconType.icon, '');
@@ -78,17 +83,35 @@ class EmojiIconData {
bool get isNotEmpty => emoji.isNotEmpty;
}
class SelectedEmojiIconResult {
SelectedEmojiIconResult(this.data, this.keepOpen);
final EmojiIconData data;
final bool keepOpen;
FlowyIconType get type => data.type;
String get emoji => data.emoji;
}
extension EmojiIconDataToSelectedResultExtension on EmojiIconData {
SelectedEmojiIconResult toSelectedResult({bool keepOpen = false}) =>
SelectedEmojiIconResult(this, keepOpen);
}
class FlowyIconEmojiPicker extends StatefulWidget {
const FlowyIconEmojiPicker({
super.key,
this.onSelectedEmoji,
this.initialType,
this.enableBackgroundColorSelection = true,
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
});
final ValueChanged<EmojiIconData>? onSelectedEmoji;
final ValueChanged<SelectedEmojiIconResult>? onSelectedEmoji;
final bool enableBackgroundColorSelection;
final List<PickerTabType> tabs;
final PickerTabType? initialType;
@override
State<FlowyIconEmojiPicker> createState() => _FlowyIconEmojiPickerState();
@@ -96,12 +119,23 @@ class FlowyIconEmojiPicker extends StatefulWidget {
class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
with SingleTickerProviderStateMixin {
late final controller = TabController(
length: widget.tabs.length,
vsync: this,
);
late TabController controller;
int currentIndex = 0;
@override
void initState() {
super.initState();
final initialType = widget.initialType;
if (initialType != null) {
currentIndex = widget.tabs.indexOf(initialType);
}
controller = TabController(
initialIndex: currentIndex,
length: widget.tabs.length,
vsync: this,
);
}
@override
void dispose() {
controller.dispose();
@@ -127,7 +161,8 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
),
_RemoveIconButton(
onTap: () {
widget.onSelectedEmoji?.call(EmojiIconData.none());
widget.onSelectedEmoji
?.call(EmojiIconData.none().toSelectedResult());
},
),
],
@@ -155,9 +190,12 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
return FlowyEmojiPicker(
ensureFocus: true,
emojiPerLine: _getEmojiPerLine(context),
onEmojiSelected: (_, emoji) => widget.onSelectedEmoji?.call(
EmojiIconData.emoji(emoji),
),
onEmojiSelected: (r) {
widget.onSelectedEmoji?.call(
EmojiIconData.emoji(r.emoji).toSelectedResult(keepOpen: r.isRandom),
);
SystemChannels.textInput.invokeMethod('TextInput.hide');
},
);
}
@@ -171,9 +209,13 @@ class _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>
Widget _buildIconPicker() {
return FlowyIconPicker(
ensureFocus: true,
enableBackgroundColorSelection: widget.enableBackgroundColorSelection,
onSelectedIcon: (result) {
widget.onSelectedEmoji?.call(result.toEmojiIconData());
onSelectedIcon: (r) {
widget.onSelectedEmoji?.call(
r.data.toEmojiIconData().toSelectedResult(keepOpen: r.isRandom),
);
SystemChannels.textInput.invokeMethod('TextInput.hide');
},
);
}
@@ -75,17 +75,31 @@ Future<List<IconGroup>> loadIconGroups() async {
}
}
class IconPickerResult {
IconPickerResult(this.data, this.isRandom);
final IconsData data;
final bool isRandom;
}
extension IconsDataToIconPickerResultExtension on IconsData {
IconPickerResult toResult({bool isRandom = false}) =>
IconPickerResult(this, isRandom);
}
class FlowyIconPicker extends StatefulWidget {
const FlowyIconPicker({
super.key,
required this.onSelectedIcon,
required this.enableBackgroundColorSelection,
this.iconPerLine = 9,
this.ensureFocus = false,
});
final bool enableBackgroundColorSelection;
final ValueChanged<IconsData> onSelectedIcon;
final ValueChanged<IconPickerResult> onSelectedIcon;
final int iconPerLine;
final bool ensureFocus;
@override
State<FlowyIconPicker> createState() => _FlowyIconPickerState();
@@ -142,6 +156,7 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: IconSearchBar(
ensureFocus: widget.ensureFocus,
onRandomTap: () {
final value = kIconGroups?.randomIcon();
if (value == null) {
@@ -154,8 +169,9 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
value.$2.content,
value.$2.name,
color,
),
).toResult(isRandom: true),
);
RecentIcons.putIcon(value.$2);
},
onKeywordChanged: (keyword) => {
debounce.call(() {
@@ -193,14 +209,14 @@ class _FlowyIconPickerState extends State<FlowyIconPicker> {
iconGroups: filteredIconGroups,
enableBackgroundColorSelection:
widget.enableBackgroundColorSelection,
onSelectedIcon: widget.onSelectedIcon,
onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),
iconPerLine: widget.iconPerLine,
);
}
return IconPicker(
iconGroups: iconGroups,
enableBackgroundColorSelection: widget.enableBackgroundColorSelection,
onSelectedIcon: widget.onSelectedIcon,
onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),
iconPerLine: widget.iconPerLine,
);
},
@@ -278,6 +294,7 @@ class _IconPickerState extends State<IconPicker> {
crossAxisCount: widget.iconPerLine,
),
itemCount: iconGroup.icons.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
final icon = iconGroup.icons[index];
@@ -16,9 +16,11 @@ class IconSearchBar extends StatefulWidget {
super.key,
required this.onRandomTap,
required this.onKeywordChanged,
this.ensureFocus = false,
});
final VoidCallback onRandomTap;
final bool ensureFocus;
final IconKeywordChangedCallback onKeywordChanged;
@override
@@ -46,6 +48,7 @@ class _IconSearchBarState extends State<IconSearchBar> {
Expanded(
child: _SearchTextField(
onKeywordChanged: widget.onKeywordChanged,
ensureFocus: widget.ensureFocus,
),
),
const HSpace(8.0),
@@ -93,9 +96,11 @@ class _RandomIconButton extends StatelessWidget {
class _SearchTextField extends StatefulWidget {
const _SearchTextField({
required this.onKeywordChanged,
this.ensureFocus = false,
});
final IconKeywordChangedCallback onKeywordChanged;
final bool ensureFocus;
@override
State<_SearchTextField> createState() => _SearchTextFieldState();
@@ -105,6 +110,20 @@ class _SearchTextFieldState extends State<_SearchTextField> {
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
/// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]
/// this is to ensure that focus can be regained within a short period of time
if (widget.ensureFocus) {
Future.delayed(const Duration(milliseconds: 200), () {
if (!mounted || focusNode.hasFocus) return;
focusNode.requestFocus();
});
}
}
@override
void dispose() {
controller.dispose();
@@ -15,6 +15,16 @@ enum PickerTabType {
}
}
extension StringToPickerTabType on String {
PickerTabType? toPickerTabType() {
try {
return PickerTabType.values.byName(this);
} on ArgumentError {
return null;
}
}
}
class PickerTab extends StatelessWidget {
const PickerTab({
super.key,
@@ -285,14 +285,21 @@ GoRoute _mobileEmojiPickerPageRoute() {
state.uri.queryParameters[MobileEmojiPickerScreen.pageTitle];
final selectTabs =
state.uri.queryParameters[MobileEmojiPickerScreen.selectTabs] ?? '';
final selectedType = state
.uri.queryParameters[MobileEmojiPickerScreen.iconSelectedType]
?.toPickerTabType();
final tabs = selectTabs
.split('-')
.map((e) => PickerTabType.values.byName(e))
.toList();
return MaterialExtendedPage(
child: tabs.isEmpty
? MobileEmojiPickerScreen(title: title)
: MobileEmojiPickerScreen(title: title, tabs: tabs),
? MobileEmojiPickerScreen(title: title, selectedType: selectedType)
: MobileEmojiPickerScreen(
title: title,
selectedType: selectedType,
tabs: tabs,
),
);
},
);
@@ -737,6 +737,7 @@ class HomeSecondaryTopBar extends StatelessWidget {
children: [
FlowyIconButton(
width: 24,
tooltipText: LocaleKeys.sideBar_closeSidebar.tr(),
radius: const BorderRadius.all(Radius.circular(8.0)),
icon: const FlowySvg(
FlowySvgs.show_menu_s,
@@ -749,6 +750,7 @@ class HomeSecondaryTopBar extends StatelessWidget {
const HSpace(8.0),
FlowyIconButton(
width: 24,
tooltipText: LocaleKeys.sideBar_expandSidebar.tr(),
radius: const BorderRadius.all(Radius.circular(8.0)),
icon: const FlowySvg(
FlowySvgs.full_view_s,
@@ -18,15 +18,23 @@ enum WorkspaceMoreAction {
divider,
}
class WorkspaceMoreActionList extends StatelessWidget {
class WorkspaceMoreActionList extends StatefulWidget {
const WorkspaceMoreActionList({
super.key,
required this.workspace,
required this.isShowingMoreActions,
required this.popoverMutex,
});
final UserWorkspacePB workspace;
final ValueNotifier<bool> isShowingMoreActions;
final PopoverMutex popoverMutex;
@override
State<WorkspaceMoreActionList> createState() =>
_WorkspaceMoreActionListState();
}
class _WorkspaceMoreActionListState extends State<WorkspaceMoreActionList> {
bool isPopoverOpen = false;
@override
Widget build(BuildContext context) {
@@ -45,16 +53,22 @@ class WorkspaceMoreActionList extends StatelessWidget {
return PopoverActionList<_WorkspaceMoreActionWrapper>(
direction: PopoverDirection.bottomWithLeftAligned,
actions: actions
.map((e) => _WorkspaceMoreActionWrapper(e, workspace))
.map(
(action) => _WorkspaceMoreActionWrapper(
action,
widget.workspace,
() => PopoverContainer.of(context).closeAll(),
),
)
.toList(),
mutex: widget.popoverMutex,
constraints: const BoxConstraints(minWidth: 220),
animationDuration: Durations.short3,
slideDistance: 2,
beginScaleFactor: 1.0,
beginOpacity: 0.8,
onClosed: () {
isShowingMoreActions.value = false;
},
onClosed: () => isPopoverOpen = false,
asBarrier: true,
buildChild: (controller) {
return SizedBox.square(
dimension: 24.0,
@@ -64,11 +78,10 @@ class WorkspaceMoreActionList extends StatelessWidget {
FlowySvgs.workspace_three_dots_s,
),
onTap: () {
if (!isShowingMoreActions.value) {
if (!isPopoverOpen) {
controller.show();
isPopoverOpen = true;
}
isShowingMoreActions.value = true;
},
),
);
@@ -79,10 +92,15 @@ class WorkspaceMoreActionList extends StatelessWidget {
}
class _WorkspaceMoreActionWrapper extends CustomActionCell {
_WorkspaceMoreActionWrapper(this.inner, this.workspace);
_WorkspaceMoreActionWrapper(
this.inner,
this.workspace,
this.closeWorkspaceMenu,
);
final WorkspaceMoreAction inner;
final UserWorkspacePB workspace;
final VoidCallback closeWorkspaceMenu;
@override
Widget buildWithContext(
@@ -117,6 +135,7 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
margin: const EdgeInsets.all(6),
onTap: () async {
PopoverContainer.of(context).closeAll();
closeWorkspaceMenu();
final workspaceBloc = context.read<UserWorkspaceBloc>();
switch (inner) {
@@ -96,9 +96,9 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
margin: const EdgeInsets.all(0),
popupBuilder: (_) => FlowyIconEmojiPicker(
tabs: const [PickerTabType.emoji],
onSelectedEmoji: (result) {
widget.onSelected(result);
controller.close();
onSelectedEmoji: (r) {
widget.onSelected(r.data);
if (!r.keepOpen) controller.close();
},
),
child: MouseRegion(
@@ -43,13 +43,7 @@ class WorkspacesMenu extends StatefulWidget {
}
class _WorkspacesMenuState extends State<WorkspacesMenu> {
final ValueNotifier<bool> isShowingMoreActions = ValueNotifier(false);
@override
void dispose() {
isShowingMoreActions.dispose();
super.dispose();
}
final popoverMutex = PopoverMutex();
@override
Widget build(BuildContext context) {
@@ -59,7 +53,7 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
children: [
// user email
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
padding: const EdgeInsets.only(left: 10.0, top: 6.0, right: 10.0),
child: Row(
children: [
Expanded(
@@ -71,18 +65,21 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
),
),
const HSpace(4.0),
const _WorkspaceMoreButton(),
WorkspaceMoreButton(
popoverMutex: popoverMutex,
),
const HSpace(8.0),
],
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0),
child: Divider(height: 1.0),
),
// workspace list
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -93,7 +90,7 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
userProfile: widget.userProfile,
isSelected: workspace.workspaceId ==
widget.currentWorkspace.workspaceId,
isShowingMoreActions: isShowingMoreActions,
popoverMutex: popoverMutex,
),
const VSpace(6.0),
],
@@ -102,13 +99,19 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
),
),
// add new workspace
const _CreateWorkspaceButton(),
const VSpace(6.0),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 6.0),
child: _CreateWorkspaceButton(),
),
if (UniversalPlatform.isDesktop) ...[
const _ImportNotionButton(),
const VSpace(6.0),
const Padding(
padding: EdgeInsets.only(left: 6.0, top: 6.0, right: 6.0),
child: _ImportNotionButton(),
),
],
const VSpace(6.0),
],
);
}
@@ -132,13 +135,13 @@ class WorkspaceMenuItem extends StatefulWidget {
required this.workspace,
required this.userProfile,
required this.isSelected,
required this.isShowingMoreActions,
required this.popoverMutex,
});
final UserProfilePB userProfile;
final UserWorkspacePB workspace;
final bool isSelected;
final ValueNotifier<bool> isShowingMoreActions;
final PopoverMutex popoverMutex;
@override
State<WorkspaceMenuItem> createState() => _WorkspaceMenuItemState();
@@ -230,7 +233,7 @@ class _WorkspaceMenuItemState extends State<WorkspaceMenuItem> {
},
child: WorkspaceMoreActionList(
workspace: widget.workspace,
isShowingMoreActions: widget.isShowingMoreActions,
popoverMutex: widget.popoverMutex,
),
),
const HSpace(8.0),
@@ -394,40 +397,35 @@ class _ImportNotionButton extends StatelessWidget {
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: Stack(
alignment: Alignment.centerRight,
children: [
FlowyButton(
key: importNotionButtonKey,
onTap: () {
_showImportNotinoDialog(context);
child: FlowyButton(
key: importNotionButtonKey,
onTap: () {
_showImportNotinoDialog(context);
},
margin: const EdgeInsets.symmetric(horizontal: 4.0),
text: Row(
children: [
_buildLeftIcon(context),
const HSpace(8.0),
FlowyText.regular(
LocaleKeys.workspace_importFromNotion.tr(),
),
],
),
rightIcon: FlowyTooltip(
message: LocaleKeys.workspace_learnMore.tr(),
preferBelow: true,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.information_s,
),
onPressed: () {
afLaunchUrlString(
'https://docs.appflowy.io/docs/guides/import-from-notion',
);
},
margin: const EdgeInsets.symmetric(horizontal: 4.0),
text: Row(
children: [
_buildLeftIcon(context),
const HSpace(8.0),
FlowyText.regular(
LocaleKeys.workspace_importFromNotion.tr(),
),
],
),
),
FlowyTooltip(
message: LocaleKeys.workspace_learnMore.tr(),
preferBelow: true,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.information_s,
),
onPressed: () {
afLaunchUrlString(
'https://docs.appflowy.io/docs/guides/import-from-notion',
);
},
),
),
],
),
),
);
}
@@ -478,14 +476,22 @@ class _ImportNotionButton extends StatelessWidget {
}
}
class _WorkspaceMoreButton extends StatelessWidget {
const _WorkspaceMoreButton();
@visibleForTesting
class WorkspaceMoreButton extends StatelessWidget {
const WorkspaceMoreButton({
super.key,
required this.popoverMutex,
});
final PopoverMutex popoverMutex;
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 6),
mutex: popoverMutex,
asBarrier: true,
popupBuilder: (_) => FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0),
leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s),
@@ -207,6 +207,7 @@ class _SidebarSwitchWorkspaceButtonState
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 5),
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
margin: EdgeInsets.zero,
animationDuration: Durations.short3,
beginScaleFactor: 1.0,
beginOpacity: 0.8,
@@ -641,12 +641,13 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
popupBuilder: (context) {
isIconPickerOpened = true;
return FlowyIconEmojiPicker(
onSelectedEmoji: (result) {
initialType: iconData.type.toPickerTabType(),
onSelectedEmoji: (r) {
ViewBackendService.updateViewIcon(
viewId: widget.view.id,
viewIcon: result,
viewIcon: r.data,
);
controller.close();
if (!r.keepOpen) controller.close();
},
);
},
@@ -770,13 +771,12 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
context.read<ViewBloc>().add(const ViewEvent.collapseAllPages());
break;
case ViewMoreActionType.changeIcon:
if (data is! EmojiIconData) {
if (data is! SelectedEmojiIconResult) {
return;
}
final result = data;
await ViewBackendService.updateViewIcon(
viewId: widget.view.id,
viewIcon: result,
viewIcon: data.data,
);
break;
case ViewMoreActionType.moveTo:
@@ -58,7 +58,11 @@ class ViewMoreActionPopover extends StatelessWidget {
(e) => ViewMoreActionTypeWrapper(e, view, (controller, data) {
onEditing(false);
onAction(e, data);
controller.close();
bool enableClose = true;
if (data is SelectedEmojiIconResult) {
if (data.keepOpen) enableClose = false;
}
if (enableClose) controller.close();
}),
)
.toList();
@@ -172,6 +176,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
margin: const EdgeInsets.all(0),
clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (_) => FlowyIconEmojiPicker(
initialType: sourceView.icon.toEmojiIconData().type.toPickerTabType(),
onSelectedEmoji: (result) => onTap(controller, result),
),
child: child,
@@ -1,4 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@@ -51,13 +53,9 @@ class PublishInfoViewItem extends StatelessWidget {
}
Widget _buildIcon() {
final icon = publishInfoView.view.icon.value;
final icon = publishInfoView.view.icon.toEmojiIconData();
return icon.isNotEmpty
? FlowyText.emoji(
icon,
fontSize: 16.0,
figmaLineHeight: 18.0,
)
? RawEmojiIconWidget(emoji: icon, emojiSize: 16.0)
: publishInfoView.view.defaultIcon();
}
}
@@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
SelectionMenuItem emojiMenuItem = SelectionMenuItem(
getName: LocaleKeys.document_plugins_emoji.tr,
@@ -109,7 +108,7 @@ class _EmojiSelectionMenuState extends State<EmojiSelectionMenu> {
@override
Widget build(BuildContext context) {
return FlowyEmojiPicker(
onEmojiSelected: (_, emoji) => widget.onSubmitted(emoji),
onEmojiSelected: (r) => widget.onSubmitted(r.emoji),
);
}
}
@@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
@@ -364,71 +365,59 @@ class OkCancelButton extends StatelessWidget {
void showToastNotification(
BuildContext context, {
required String message,
String? message,
TextSpan? richMessage,
String? description,
ToastificationType type = ToastificationType.success,
ToastificationCallbacks? callbacks,
double bottomPadding = 100,
}) {
if (UniversalPlatform.isMobile) {
toastification.showCustom(
alignment: Alignment.bottomCenter,
autoCloseDuration: const Duration(milliseconds: 3000),
callbacks: callbacks ?? const ToastificationCallbacks(),
builder: (_, __) => _MToast(
message: message,
type: type,
bottomPadding: bottomPadding,
description: description,
),
);
return;
}
toastification.show(
context: context,
type: type,
style: ToastificationStyle.flat,
closeButtonShowType: CloseButtonShowType.onHover,
assert(
(message == null) != (richMessage == null),
"Exactly one of message or richMessage must be non-null.",
);
toastification.showCustom(
alignment: Alignment.bottomCenter,
autoCloseDuration: const Duration(milliseconds: 3000),
showProgressBar: false,
backgroundColor: Theme.of(context).colorScheme.surface,
borderSide: BorderSide(
color: Colors.grey.withOpacity(0.4),
),
title: FlowyText(
message,
maxLines: 3,
),
description: description != null
? FlowyText.regular(
description,
fontSize: 12,
lineHeight: 1.2,
maxLines: 3,
)
: null,
callbacks: callbacks ?? const ToastificationCallbacks(),
builder: (_, item) {
return UniversalPlatform.isMobile
? _MobileToast(
message: message,
type: type,
bottomPadding: bottomPadding,
description: description,
)
: _DesktopToast(
message: message,
richMessage: richMessage,
type: type,
onDismiss: () => toastification.dismiss(item),
);
},
);
}
class _MToast extends StatelessWidget {
const _MToast({
required this.message,
class _MobileToast extends StatelessWidget {
const _MobileToast({
this.message,
this.type = ToastificationType.success,
this.bottomPadding = 100,
this.description,
});
final String message;
final String? message;
final ToastificationType type;
final double bottomPadding;
final String? description;
@override
Widget build(BuildContext context) {
if (message == null) {
return const SizedBox.shrink();
}
final hintText = FlowyText.regular(
message,
message!,
fontSize: 16.0,
figmaLineHeight: 18.0,
color: Colors.white,
@@ -498,6 +487,90 @@ class _MToast extends StatelessWidget {
}
}
class _DesktopToast extends StatelessWidget {
const _DesktopToast({
this.message,
this.richMessage,
required this.type,
this.onDismiss,
});
final String? message;
final TextSpan? richMessage;
final ToastificationType type;
final void Function()? onDismiss;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 360.0),
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
margin: const EdgeInsets.only(bottom: 32.0),
decoration: BoxDecoration(
color: Theme.of(context).isLightMode
? const Color(0xFF333333)
: const Color(0xFF363D49),
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// icon
FlowySvg(
switch (type) {
ToastificationType.warning => FlowySvgs.toast_warning_filled_s,
ToastificationType.success => FlowySvgs.toast_checked_filled_s,
ToastificationType.error => FlowySvgs.toast_error_filled_s,
_ => throw UnimplementedError(),
},
size: const Size.square(20.0),
blendMode: null,
),
const HSpace(8.0),
// text
Flexible(
child: message != null
? FlowyText(
message!,
maxLines: 2,
figmaLineHeight: 20.0,
overflow: TextOverflow.ellipsis,
color: const Color(0xFFFFFFFF),
)
: RichText(
text: richMessage!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(16.0),
// close
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onDismiss,
child: const SizedBox.square(
dimension: 24.0,
child: Center(
child: FlowySvg(
FlowySvgs.toast_close_s,
size: Size.square(16.0),
color: Color(0xFFBDBDBD),
),
),
),
),
),
],
),
),
);
}
}
Future<void> showConfirmDeletionDialog({
required BuildContext context,
required String name,
@@ -91,13 +91,15 @@ class _RenameViewPopoverState extends State<RenameViewPopover> {
}
Future<void> _updateViewIcon(
EmojiIconData emoji,
SelectedEmojiIconResult r,
PopoverController? _,
) async {
await ViewBackendService.updateViewIcon(
viewId: widget.viewId,
viewIcon: emoji,
viewIcon: r.data,
);
widget.popoverController.close();
if (!r.keepOpen) {
widget.popoverController.close();
}
}
}
+1 -1
View File
@@ -158,4 +158,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823
COCOAPODS: 1.15.2
COCOAPODS: 1.16.2
+4 -4
View File
@@ -61,8 +61,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: c68e5f6
resolved-ref: c68e5f6c585205083e27e875b822656425b2853f
ref: cfb8b1b
resolved-ref: cfb8b1b6eb06f73a4fb297b6fd1d54b0ccec2922
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "4.0.0"
@@ -757,8 +757,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "8a9fa49"
resolved-ref: "8a9fa491cb3b86baf78b0a33c2c37a29d1cae028"
ref: "355aa56"
resolved-ref: "355aa56e9c74a91e00370a882739e0bb98c30bd8"
url: "https://github.com/LucasXu0/emoji_mart.git"
source: git
version: "1.0.2"
+2 -2
View File
@@ -73,7 +73,7 @@ dependencies:
flutter_emoji_mart:
git:
url: https://github.com/LucasXu0/emoji_mart.git
ref: "8a9fa49"
ref: "355aa56"
flutter_math_fork: ^0.7.3
flutter_slidable: ^3.0.0
@@ -174,7 +174,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "c68e5f6"
ref: "cfb8b1b"
appflowy_editor_plugins:
git:
@@ -170,5 +170,67 @@ void main() {
'0': TableAlign.center.key,
});
});
test('delete a column with text color & bold style (1)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 3,
columnCount: 4,
);
// delete the column 1
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
});
await editorState.deleteColumnInTable(tableNode, 0);
expect(tableNode.columnTextColors, {
'0': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'0': true,
});
expect(tableNode.rowLength, 3);
expect(tableNode.columnLength, 3);
});
test('delete a column with text color & bold style (2)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 3,
columnCount: 4,
);
// delete the column 1
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
});
await editorState.deleteColumnInTable(tableNode, 1);
expect(tableNode.columnTextColors, {});
expect(tableNode.columnBoldAttributes, {});
expect(tableNode.rowLength, 3);
expect(tableNode.columnLength, 3);
});
});
}
@@ -161,5 +161,69 @@ void main() {
'1': TableAlign.center.key,
});
});
test('duplicate a column with text color & bold style (1)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 3,
columnCount: 4,
);
// duplicate the column 1
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
});
await editorState.duplicateColumnInTable(tableNode, 1);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
'2': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
'2': true,
});
});
test('duplicate a column with text color & bold style (2)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 3,
columnCount: 4,
);
// duplicate the column 1
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
});
await editorState.duplicateColumnInTable(tableNode, 0);
expect(tableNode.columnTextColors, {
'2': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'2': true,
});
});
});
}
@@ -190,5 +190,67 @@ void main() {
'0': TableAlign.center.key,
});
});
test('insert a column with text color & bold style (1)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 2,
columnCount: 3,
);
// insert the column at the first position
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'0': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'0': true,
});
await editorState.insertColumnInTable(tableNode, 0);
expect(tableNode.columnTextColors, {
'1': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'1': true,
});
});
test('insert a column with text color & bold style (2)', () async {
final (editorState, tableNode) = createEditorStateAndTable(
rowCount: 2,
columnCount: 3,
);
// insert the column at the first position
final tableCellNode =
tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);
await editorState.updateColumnTextColor(
tableCellNode: tableCellNode!,
color: '0xFF0000FF',
);
await editorState.toggleColumnBoldAttribute(
tableCellNode: tableCellNode,
isBold: true,
);
expect(tableNode.columnTextColors, {
'0': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'0': true,
});
await editorState.insertColumnInTable(tableNode, 1);
expect(tableNode.columnTextColors, {
'0': '0xFF0000FF',
});
expect(tableNode.columnBoldAttributes, {
'0': true,
});
});
});
}
+12 -8
View File
@@ -1031,7 +1031,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"arc-swap",
@@ -1056,7 +1056,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"async-trait",
@@ -1096,7 +1096,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"arc-swap",
@@ -1117,7 +1117,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"bytes",
@@ -1137,7 +1137,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"arc-swap",
@@ -1159,7 +1159,7 @@ dependencies = [
[[package]]
name = "collab-importer"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"async-recursion",
@@ -1209,6 +1209,9 @@ dependencies = [
"collab-folder",
"collab-plugins",
"collab-user",
"diesel",
"flowy-error",
"flowy-sqlite",
"futures",
"lib-infra",
"serde",
@@ -1220,7 +1223,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"async-stream",
@@ -1300,7 +1303,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"collab",
@@ -2144,6 +2147,7 @@ dependencies = [
"arc-swap",
"base64 0.21.5",
"bytes",
"collab-integrate",
"dashmap 6.0.1",
"flowy-ai-pub",
"flowy-codegen",
+8 -8
View File
@@ -120,14 +120,14 @@ custom-protocol = ["tauri/custom-protocol"]
# 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 = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
# Working directory: frontend
# To update the commit ID, run:
+2 -1
View File
@@ -15,13 +15,14 @@ export default defineConfig({
framework: 'react',
bundler: 'vite',
},
setupNodeEvents (on, config) {
setupNodeEvents(on, config) {
registerCodeCoverageTasks(on, config);
addMatchImageSnapshotPlugin(on, config);
return config;
},
supportFile: 'cypress/support/component.ts',
},
chromeWebSecurity: false,
retries: {
// Configure retry attempts for `cypress run`
// Default is 0
@@ -17,6 +17,7 @@ import 'cypress-real-events';
// Import commands.js using ES2015 syntax:
import '@cypress/code-coverage/support';
import 'cypress-real-events/support';
import './commands';
import './document';
@@ -114,7 +114,7 @@ export class DocumentTest {
blockText.applyDelta(child.text);
this.textMap.set(blockId, blockText);
const blockChildren = new Y.Array<BlockId>();
this.childrenMap.set(blockId, blockChildren);
@@ -137,7 +137,7 @@ export class DocumentTest {
children.push({
type: child.get(YjsEditorKey.block_type),
data: JSON.parse(child.get(YjsEditorKey.block_data)),
text: this.textMap.get(childId).toDelta(),
text: this.textMap.get(childId)?.toDelta() || [],
children: this.toJSONChildren(childId),
});
}
@@ -87,6 +87,16 @@ http {
access_log off;
}
location /.well-known/apple-app-site-association {
default_type application/json;
add_header Cache-Control "public, max-age=3600";
}
location /.well-known/assetlinks.json {
default_type application/json;
add_header Cache-Control "public, max-age=3600";
}
error_page 404 /404.html;
location = /404.html {
root /usr/share/nginx/html;
+5
View File
@@ -24,11 +24,13 @@
"coverage": "pnpm run test:unit && pnpm run test:components"
},
"dependencies": {
"@appflowyinc/editor": "^0.0.39",
"@atlaskit/primitives": "^5.5.3",
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@floating-ui/react": "^0.26.27",
"@jest/globals": "^29.7.0",
"@mui/icons-material": "^5.11.11",
"@mui/material": "6.0.0-alpha.2",
@@ -44,6 +46,7 @@
"decimal.js": "^10.4.3",
"dexie": "^4.0.7",
"dexie-react-hooks": "^1.1.7",
"dompurify": "^3.1.7",
"emoji-mart": "^5.5.2",
"emoji-regex": "^10.2.1",
"escape-string-regexp": "^5.0.0",
@@ -62,6 +65,7 @@
"katex": "^0.16.7",
"lightgallery": "^2.7.2",
"lodash-es": "^4.17.21",
"mermaid": "^11.4.1",
"nanoid": "^4.0.0",
"notistack": "^3.0.1",
"numeral": "^2.0.6",
@@ -122,6 +126,7 @@
"@tauri-apps/cli": "^1.5.11",
"@testing-library/react": "^16.0.0",
"@types/cypress-image-snapshot": "^3.1.9",
"@types/dompurify": "^3.0.5",
"@types/google-protobuf": "^3.15.12",
"@types/is-hotkey": "^0.1.7",
"@types/jest": "^29.5.3",
+2339 -106
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,25 @@
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": [
"VHB67HRSZG.com.appflowy.appflowy.flutter"
],
"paths": [
"*"
],
"components": [
{
"/": "/*"
}
]
}
]
},
"webcredentials": {
"apps": [
"VHB67HRSZG.com.appflowy.appflowy.flutter"
]
}
}
@@ -0,0 +1,14 @@
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "io.appflowy.appflowy",
"sha256_cert_fingerprints": [
"19:13:85:33:DB:B3:A2:FD:65:2F:61:D7:F2:35:95:79:FE:6E:CC:B5:AC:94:AA:02:9E:BE:E7:0E:02:6B:45:FF"
]
}
}
]
+12 -8
View File
@@ -1029,7 +1029,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"arc-swap",
@@ -1054,7 +1054,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"async-trait",
@@ -1094,7 +1094,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"arc-swap",
@@ -1115,7 +1115,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"bytes",
@@ -1135,7 +1135,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"arc-swap",
@@ -1157,7 +1157,7 @@ dependencies = [
[[package]]
name = "collab-importer"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"async-recursion",
@@ -1207,6 +1207,9 @@ dependencies = [
"collab-folder",
"collab-plugins",
"collab-user",
"diesel",
"flowy-error",
"flowy-sqlite",
"futures",
"lib-infra",
"serde",
@@ -1218,7 +1221,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"async-stream",
@@ -1298,7 +1301,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=dd8a1d13e97322338f027888a71340575cd9ad8f#dd8a1d13e97322338f027888a71340575cd9ad8f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=67b31e4bcc64d81332c37efecb0a8618681f1db9#67b31e4bcc64d81332c37efecb0a8618681f1db9"
dependencies = [
"anyhow",
"collab",
@@ -2189,6 +2192,7 @@ dependencies = [
"arc-swap",
"base64 0.21.7",
"bytes",
"collab-integrate",
"dashmap 6.0.1",
"flowy-ai-pub",
"flowy-codegen",
@@ -118,14 +118,14 @@ custom-protocol = ["tauri/custom-protocol"]
# 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 = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "dd8a1d13e97322338f027888a71340575cd9ad8f" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "67b31e4bcc64d81332c37efecb0a8618681f1db9" }
# Working directory: frontend
@@ -29,7 +29,12 @@ const wrapperCreator =
({ children }: { children: React.ReactNode }) => {
return (
<DatabaseContextProvider
iidIndex={viewId} viewId={viewId} databaseDoc={doc} rowDocMap={rowDocMap} readOnly={true}
showActions={true}
iidIndex={viewId}
viewId={viewId}
databaseDoc={doc}
rowDocMap={rowDocMap}
readOnly={true}
>
{children}
</DatabaseContextProvider>
@@ -17,37 +17,47 @@ export interface DatabaseContextState {
viewId: string;
rowDocMap: Record<RowId, YDoc> | null;
isDatabaseRowPage?: boolean;
scrollLeft?: number;
isDocumentBlock?: boolean;
navigateToRow?: (rowId: string) => void;
loadView?: LoadView;
createRowDoc?: CreateRowDoc;
loadViewMeta?: LoadViewMeta;
navigateToView?: (viewId: string, blockId?: string) => Promise<void>;
onRendered?: (height: number) => void;
showActions?: boolean;
}
export const DatabaseContext = createContext<DatabaseContextState | null>(null);
export const useDatabaseContext = () => {
const context = useContext(DatabaseContext);
if (!context) {
throw new Error('DatabaseContext is not provided');
}
return context;
};
export const useDatabase = () => {
const database = useContext(DatabaseContext)
?.databaseDoc?.getMap(YjsEditorKey.data_section)
const database = useDatabaseContext()
.databaseDoc?.getMap(YjsEditorKey.data_section)
.get(YjsEditorKey.database) as YDatabase;
return database;
};
export function useDatabaseViewId () {
return useContext(DatabaseContext)?.viewId;
}
export const useNavigateToRow = () => {
return useContext(DatabaseContext)?.navigateToRow;
return useDatabaseContext().navigateToRow;
};
export const useRowDocMap = () => {
return useContext(DatabaseContext)?.rowDocMap;
return useDatabaseContext().rowDocMap;
};
export const useIsDatabaseRowPage = () => {
return useContext(DatabaseContext)?.isDatabaseRowPage;
return useDatabaseContext().isDatabaseRowPage;
};
export const useRow = (rowId: string) => {
@@ -60,21 +70,21 @@ export const useRowData = (rowId: string) => {
return useRow(rowId)?.get(YjsEditorKey.database_row) as YDatabaseRow;
};
export const useViewId = () => {
const context = useContext(DatabaseContext);
export const useDatabaseViewId = () => {
const context = useDatabaseContext();
return context?.viewId;
};
export const useReadOnly = () => {
const context = useContext(DatabaseContext);
const context = useDatabaseContext();
return context?.readOnly;
return context?.readOnly || true;
};
export const useDatabaseView = () => {
const database = useDatabase();
const viewId = useViewId();
const viewId = useDatabaseViewId();
return viewId ? database?.get(YjsDatabaseKey.views)?.get(viewId) : undefined;
};
@@ -84,3 +94,9 @@ export function useDatabaseFields () {
return database.get(YjsDatabaseKey.fields);
}
export const useDatabaseSelectedView = (viewId: string) => {
const database = useDatabase();
return database.get(YjsDatabaseKey.views).get(viewId);
};
@@ -12,7 +12,7 @@ import {
useDatabaseFields,
useDatabaseView,
useRowDocMap,
useViewId,
useDatabaseViewId,
} from '@/application/database-yjs/context';
import { filterBy, parseFilter } from '@/application/database-yjs/filter';
import { groupByField } from '@/application/database-yjs/group';
@@ -89,7 +89,7 @@ export function useDatabaseViewsSelector (_iidIndex: string, visibleViewIds?: st
}
export function useFieldsSelector (visibilitys: FieldVisibility[] = defaultVisible) {
const viewId = useViewId();
const viewId = useDatabaseViewId();
const database = useDatabase();
const [columns, setColumns] = useState<Column[]>([]);
@@ -166,7 +166,7 @@ export function useFieldSelector (fieldId: string) {
export function useFiltersSelector () {
const database = useDatabase();
const viewId = useViewId();
const viewId = useDatabaseViewId();
const [filters, setFilters] = useState<string[]>([]);
useEffect(() => {
@@ -196,7 +196,7 @@ export function useFiltersSelector () {
export function useFilterSelector (filterId: string) {
const database = useDatabase();
const viewId = useViewId();
const viewId = useDatabaseViewId();
const fields = database?.get(YjsDatabaseKey.fields);
const [filterValue, setFilterValue] = useState<Filter | null>(null);
@@ -229,7 +229,7 @@ export function useFilterSelector (filterId: string) {
export function useSortsSelector () {
const database = useDatabase();
const viewId = useViewId();
const viewId = useDatabaseViewId();
const [sorts, setSorts] = useState<string[]>([]);
useEffect(() => {
@@ -265,7 +265,7 @@ export interface Sort {
export function useSortSelector (sortId: SortId) {
const database = useDatabase();
const viewId = useViewId();
const viewId = useDatabaseViewId();
const [sortValue, setSortValue] = useState<Sort | null>(null);
const views = database?.get(YjsDatabaseKey.views);
@@ -298,7 +298,7 @@ export function useSortSelector (sortId: SortId) {
export function useGroupsSelector () {
const database = useDatabase();
const viewId = useViewId();
const viewId = useDatabaseViewId();
const [groups, setGroups] = useState<string[]>([]);
useEffect(() => {
@@ -334,7 +334,7 @@ export interface GroupColumn {
export function useGroup (groupId: string) {
const database = useDatabase();
const viewId = useViewId() as string;
const viewId = useDatabaseViewId();
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
const group = view
?.get(YjsDatabaseKey.groups)
@@ -21,7 +21,7 @@ const openedSet = new Set<string>();
/**
* Open the collaboration database, and return a function to close it
*/
export async function openCollabDB (docName: string): Promise<YDoc> {
export async function openCollabDB(docName: string): Promise<YDoc> {
const name = `${databasePrefix}_${docName}`;
const doc = new Y.Doc({
guid: docName,
@@ -47,7 +47,7 @@ export async function openCollabDB (docName: string): Promise<YDoc> {
return doc as YDoc;
}
export async function closeCollabDB (docName: string) {
export async function closeCollabDB(docName: string) {
const name = `${databasePrefix}_${docName}`;
if (openedSet.has(name)) {
@@ -61,9 +61,9 @@ export async function closeCollabDB (docName: string) {
await provider.destroy();
}
export async function clearData () {
export async function clearData() {
const databases = await indexedDB.databases();
const deleteDatabase = async (dbInfo: IDBDatabaseInfo): Promise<boolean> => {
const dbName = dbInfo.name;
@@ -1,3 +1,5 @@
import { db } from '@/application/db';
import { ViewMeta } from '@/application/db/tables/view_metas';
import {
AppendBreadcrumb,
CreateRowDoc,
@@ -5,12 +7,11 @@ import {
LoadViewMeta,
View,
ViewInfo,
ViewLayout,
} from '@/application/types';
import { db } from '@/application/db';
import { ViewMeta } from '@/application/db/tables/view_metas';
import { notify } from '@/components/_shared/notify';
import { findAncestors, findView } from '@/components/_shared/outline/utils';
import { useService } from '@/components/main/app.hooks';
import { notify } from '@/components/_shared/notify';
import { useLiveQuery } from 'dexie-react-hooks';
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -175,59 +176,6 @@ export const PublishProvider = ({
}, [service, publishName]);
const navigate = useNavigate();
const toView = useCallback(
async (viewId: string, blockId?: string) => {
try {
const res = await service?.getPublishInfo(viewId);
if (!res) {
throw new Error('View has not been published yet');
}
const { namespace: viewNamespace, publishName } = res;
prevViewMeta.current = undefined;
const searchParams = new URLSearchParams('');
if (blockId) {
searchParams.set('blockId', blockId);
}
if (isTemplate) {
searchParams.set('template', 'true');
}
let url = `/${viewNamespace}/${publishName}`;
if (searchParams.toString()) {
url += `?${searchParams.toString()}`;
}
navigate(url, {
replace: true,
});
return;
} catch (e) {
return Promise.reject(e);
}
},
[navigate, service, isTemplate],
);
const loadOutline = useCallback(async () => {
if (!service || !namespace) return;
try {
const res = await service?.getPublishOutline(namespace);
if (!res) {
throw new Error('Publish outline not found');
}
setOutline(res);
} catch (e) {
notify.error('Publish outline not found');
}
}, [namespace, service]);
const loadViewMeta = useCallback(
async (viewId: string, callback?: (meta: View) => void) => {
@@ -284,6 +232,73 @@ export const PublishProvider = ({
[service],
);
const toView = useCallback(
async (viewId: string, blockId?: string) => {
try {
const view = await loadViewMeta(viewId);
const res = await service?.getPublishInfo(viewId);
if (!res) {
throw new Error('View has not been published yet');
}
const { namespace: viewNamespace, publishName } = res;
prevViewMeta.current = undefined;
const searchParams = new URLSearchParams('');
if (blockId) {
switch (view.layout) {
case ViewLayout.Document:
searchParams.set('blockId', blockId);
break;
case ViewLayout.Grid:
case ViewLayout.Board:
case ViewLayout.Calendar:
searchParams.set('r', blockId);
break;
default:
break;
}
}
if (isTemplate) {
searchParams.set('template', 'true');
}
let url = `/${viewNamespace}/${publishName}`;
if (searchParams.toString()) {
url += `?${searchParams.toString()}`;
}
navigate(url, {
replace: true,
});
return;
} catch (e) {
return Promise.reject(e);
}
},
[loadViewMeta, service, isTemplate, navigate],
);
const loadOutline = useCallback(async () => {
if (!service || !namespace) return;
try {
const res = await service?.getPublishOutline(namespace);
if (!res) {
throw new Error('Publish outline not found');
}
setOutline(res);
} catch (e) {
notify.error('Publish outline not found');
}
}, [namespace, service]);
const createRowDoc = useCallback(
async (rowKey: string) => {
try {
@@ -3,7 +3,7 @@ import axios, { AxiosInstance } from 'axios';
let axiosInstance: AxiosInstance | null = null;
export function initGrantService (baseURL: string) {
export function initGrantService(baseURL: string) {
if (axiosInstance) {
return;
}
@@ -21,7 +21,7 @@ export function initGrantService (baseURL: string) {
});
}
export async function refreshToken (refresh_token: string) {
export async function refreshToken(refresh_token: string) {
const response = await axiosInstance?.post<{
access_token: string;
expires_at: number;
@@ -34,12 +34,14 @@ export async function refreshToken (refresh_token: string) {
if (newToken) {
refreshSessionToken(JSON.stringify(newToken));
} else {
return Promise.reject('Failed to refresh token');
}
return newToken;
}
export async function signInWithMagicLink (email: string, authUrl: string) {
export async function signInWithMagicLink(email: string, authUrl: string) {
const res = await axiosInstance?.post(
'/magiclink',
{
@@ -58,13 +60,13 @@ export async function signInWithMagicLink (email: string, authUrl: string) {
return res?.data;
}
export async function settings () {
export async function settings() {
const res = await axiosInstance?.get('/settings');
return res?.data;
}
export function signInGoogle (authUrl: string) {
export function signInGoogle(authUrl: string) {
const provider = 'google';
const redirectTo = encodeURIComponent(authUrl);
const accessType = 'offline';
@@ -75,7 +77,7 @@ export function signInGoogle (authUrl: string) {
window.open(url, '_current');
}
export function signInApple (authUrl: string) {
export function signInApple(authUrl: string) {
const provider = 'apple';
const redirectTo = encodeURIComponent(authUrl);
const baseURL = axiosInstance?.defaults.baseURL;
@@ -84,7 +86,7 @@ export function signInApple (authUrl: string) {
window.open(url, '_current');
}
export function signInGithub (authUrl: string) {
export function signInGithub(authUrl: string) {
const provider = 'github';
const redirectTo = encodeURIComponent(authUrl);
const baseURL = axiosInstance?.defaults.baseURL;
@@ -93,7 +95,7 @@ export function signInGithub (authUrl: string) {
window.open(url, '_current');
}
export function signInDiscord (authUrl: string) {
export function signInDiscord(authUrl: string) {
const provider = 'discord';
const redirectTo = encodeURIComponent(authUrl);
const baseURL = axiosInstance?.defaults.baseURL;
@@ -14,7 +14,14 @@ import {
Subscriptions,
SubscriptionPlan,
SubscriptionInterval,
RequestAccessInfoStatus, ViewInfo,
RequestAccessInfoStatus,
ViewInfo,
UpdatePagePayload,
CreatePagePayload,
CreateSpacePayload,
UpdateSpacePayload,
Role,
WorkspaceMember, QuickNote, QuickNoteEditorData,
} from '@/application/types';
import { GlobalComment, Reaction } from '@/application/comment.type';
import { initGrantService, refreshToken } from '@/application/services/js-services/http/gotrue';
@@ -30,12 +37,15 @@ import {
} from '@/application/template.type';
import axios, { AxiosInstance } from 'axios';
import dayjs from 'dayjs';
import { omit } from 'lodash-es';
import { nanoid } from 'nanoid';
import { notify } from '@/components/_shared/notify';
export * from './gotrue';
let axiosInstance: AxiosInstance | null = null;
export function initAPIService (config: AFCloudConfig) {
export function initAPIService(config: AFCloudConfig) {
if (axiosInstance) {
return;
}
@@ -63,9 +73,14 @@ export function initAPIService (config: AFCloudConfig) {
const refresh_token = token.refresh_token;
if (isExpired) {
const newToken = await refreshToken(refresh_token);
try {
const newToken = await refreshToken(refresh_token);
access_token = newToken?.access_token || '';
access_token = newToken?.access_token || '';
} catch (e) {
invalidToken();
return config;
}
}
if (access_token) {
@@ -105,7 +120,7 @@ export function initAPIService (config: AFCloudConfig) {
});
}
export async function signInWithUrl (url: string) {
export async function signInWithUrl(url: string) {
const hash = new URL(url).hash;
if (!hash) {
@@ -142,7 +157,7 @@ export async function signInWithUrl (url: string) {
}
}
export async function verifyToken (accessToken: string) {
export async function verifyToken(accessToken: string) {
const url = `/api/user/verify/${accessToken}`;
const response = await axiosInstance?.get<{
code: number;
@@ -161,7 +176,7 @@ export async function verifyToken (accessToken: string) {
return Promise.reject(data);
}
export async function getCurrentUser (): Promise<User> {
export async function getCurrentUser(): Promise<User> {
const url = '/api/user/profile';
const response = await axiosInstance?.get<{
code: number;
@@ -209,7 +224,7 @@ interface AFWorkspace {
database_storage_id: string,
}
function afWorkspace2Workspace (workspace: AFWorkspace): Workspace {
function afWorkspace2Workspace(workspace: AFWorkspace): Workspace {
return {
id: workspace.workspace_id,
owner: {
@@ -224,7 +239,7 @@ function afWorkspace2Workspace (workspace: AFWorkspace): Workspace {
};
}
export async function openWorkspace (workspaceId: string) {
export async function openWorkspace(workspaceId: string) {
const url = `/api/workspace/${workspaceId}/open`;
const response = await axiosInstance?.put<{
code: number;
@@ -238,7 +253,7 @@ export async function openWorkspace (workspaceId: string) {
return Promise.reject(response?.data);
}
export async function getUserWorkspaceInfo (): Promise<{
export async function getUserWorkspaceInfo(): Promise<{
user_id: string;
selected_workspace: Workspace;
workspaces: Workspace[];
@@ -272,7 +287,7 @@ export async function getUserWorkspaceInfo (): Promise<{
return Promise.reject(data);
}
export async function getPublishViewMeta (namespace: string, publishName: string) {
export async function getPublishViewMeta(namespace: string, publishName: string) {
const url = `/api/workspace/v1/published/${namespace}/${publishName}`;
const response = await axiosInstance?.get<{
code: number;
@@ -291,7 +306,7 @@ export async function getPublishViewMeta (namespace: string, publishName: string
return response?.data.data;
}
export async function getPublishViewBlob (namespace: string, publishName: string) {
export async function getPublishViewBlob(namespace: string, publishName: string) {
const url = `/api/workspace/published/${namespace}/${publishName}/blob`;
const response = await axiosInstance?.get(url, {
responseType: 'blob',
@@ -300,16 +315,28 @@ export async function getPublishViewBlob (namespace: string, publishName: string
return blobToBytes(response?.data);
}
export async function updateCollab (workspaceId: string, objectId: string, collabType: Types, docState: Uint8Array, context: {
export async function updateCollab(workspaceId: string, objectId: string, collabType: Types, docState: Uint8Array, context: {
version_vector: number;
}) {
const url = `/api/workspace/v1/${workspaceId}/collab/${objectId}/web-update`;
let deviceId = localStorage.getItem('x-device-id');
if (!deviceId) {
deviceId = nanoid(8);
localStorage.setItem('x-device-id', deviceId);
}
const response = await axiosInstance?.post<{
code: number;
message: string;
}>(url, {
doc_state: Array.from(docState),
collab_type: collabType,
}, {
headers: {
'client-version': 'web',
'device-id': deviceId,
},
});
if (response?.data.code !== 0) {
@@ -319,7 +346,7 @@ export async function updateCollab (workspaceId: string, objectId: string, colla
return context;
}
export async function getCollab (workspaceId: string, objectId: string, collabType: Types) {
export async function getCollab(workspaceId: string, objectId: string, collabType: Types) {
const url = `/api/workspace/v1/${workspaceId}/collab/${objectId}`;
const response = await axiosInstance?.get<{
code: number;
@@ -345,7 +372,7 @@ export async function getCollab (workspaceId: string, objectId: string, collabTy
};
}
export async function getPageCollab (workspaceId: string, viewId: string) {
export async function getPageCollab(workspaceId: string, viewId: string) {
const url = `/api/workspace/${workspaceId}/page-view/${viewId}`;
const response = await axiosInstance?.get<{
code: number;
@@ -379,7 +406,7 @@ export async function getPageCollab (workspaceId: string, viewId: string) {
};
}
export async function getPublishView (publishNamespace: string, publishName: string) {
export async function getPublishView(publishNamespace: string, publishName: string) {
const meta = await getPublishViewMeta(publishNamespace, publishName);
const blob = await getPublishViewBlob(publishNamespace, publishName);
@@ -416,7 +443,7 @@ export async function getPublishView (publishNamespace: string, publishName: str
}
}
export async function getPublishInfoWithViewId (viewId: string) {
export async function getPublishInfoWithViewId(viewId: string) {
const url = `/api/workspace/published-info/${viewId}`;
const response = await axiosInstance?.get<{
code: number;
@@ -436,7 +463,7 @@ export async function getPublishInfoWithViewId (viewId: string) {
return Promise.reject(data);
}
export async function getAppFavorites (workspaceId: string) {
export async function getAppFavorites(workspaceId: string) {
const url = `/api/workspace/${workspaceId}/favorite`;
const response = await axiosInstance?.get<{
code: number;
@@ -455,7 +482,7 @@ export async function getAppFavorites (workspaceId: string) {
return Promise.reject(data);
}
export async function getAppTrash (workspaceId: string) {
export async function getAppTrash(workspaceId: string) {
const url = `/api/workspace/${workspaceId}/trash`;
const response = await axiosInstance?.get<{
code: number;
@@ -474,7 +501,7 @@ export async function getAppTrash (workspaceId: string) {
return Promise.reject(data);
}
export async function getAppRecent (workspaceId: string) {
export async function getAppRecent(workspaceId: string) {
const url = `/api/workspace/${workspaceId}/recent`;
const response = await axiosInstance?.get<{
code: number;
@@ -493,7 +520,7 @@ export async function getAppRecent (workspaceId: string) {
return Promise.reject(data);
}
export async function getAppOutline (workspaceId: string) {
export async function getAppOutline(workspaceId: string) {
const url = `/api/workspace/${workspaceId}/folder?depth=10`;
const response = await axiosInstance?.get<{
@@ -511,7 +538,7 @@ export async function getAppOutline (workspaceId: string) {
return Promise.reject(data);
}
export async function getView (workspaceId: string, viewId: string, depth: number = 1) {
export async function getView(workspaceId: string, viewId: string, depth: number = 1) {
const url = `/api/workspace/${workspaceId}/folder?depth=${depth}&root_view_id=${viewId}`;
const response = await axiosInstance?.get<{
code: number;
@@ -528,7 +555,7 @@ export async function getView (workspaceId: string, viewId: string, depth: numbe
return Promise.reject(data);
}
export async function getPublishOutline (publishNamespace: string) {
export async function getPublishOutline(publishNamespace: string) {
const url = `/api/workspace/published-outline/${publishNamespace}`;
const response = await axiosInstance?.get<{
code: number;
@@ -545,7 +572,7 @@ export async function getPublishOutline (publishNamespace: string) {
return Promise.reject(data);
}
export async function getPublishViewComments (viewId: string): Promise<GlobalComment[]> {
export async function getPublishViewComments(viewId: string): Promise<GlobalComment[]> {
const url = `/api/workspace/published-info/${viewId}/comment`;
const response = await axiosInstance?.get<{
code: number;
@@ -594,7 +621,7 @@ export async function getPublishViewComments (viewId: string): Promise<GlobalCom
return Promise.reject(data);
}
export async function getReactions (viewId: string, commentId?: string): Promise<Record<string, Reaction[]>> {
export async function getReactions(viewId: string, commentId?: string): Promise<Record<string, Reaction[]>> {
let url = `/api/workspace/published-info/${viewId}/reaction`;
if (commentId) {
@@ -645,7 +672,7 @@ export async function getReactions (viewId: string, commentId?: string): Promise
return Promise.reject(data);
}
export async function createGlobalCommentOnPublishView (viewId: string, content: string, replyCommentId?: string) {
export async function createGlobalCommentOnPublishView(viewId: string, content: string, replyCommentId?: string) {
const url = `/api/workspace/published-info/${viewId}/comment`;
const response = await axiosInstance?.post<{ code: number; message: string }>(url, {
content,
@@ -659,7 +686,7 @@ export async function createGlobalCommentOnPublishView (viewId: string, content:
return Promise.reject(response?.data.message);
}
export async function deleteGlobalCommentOnPublishView (viewId: string, commentId: string) {
export async function deleteGlobalCommentOnPublishView(viewId: string, commentId: string) {
const url = `/api/workspace/published-info/${viewId}/comment`;
const response = await axiosInstance?.delete<{ code: number; message: string }>(url, {
data: {
@@ -674,7 +701,7 @@ export async function deleteGlobalCommentOnPublishView (viewId: string, commentI
return Promise.reject(response?.data.message);
}
export async function addReaction (viewId: string, commentId: string, reactionType: string) {
export async function addReaction(viewId: string, commentId: string, reactionType: string) {
const url = `/api/workspace/published-info/${viewId}/reaction`;
const response = await axiosInstance?.post<{ code: number; message: string }>(url, {
comment_id: commentId,
@@ -688,7 +715,7 @@ export async function addReaction (viewId: string, commentId: string, reactionTy
return Promise.reject(response?.data.message);
}
export async function removeReaction (viewId: string, commentId: string, reactionType: string) {
export async function removeReaction(viewId: string, commentId: string, reactionType: string) {
const url = `/api/workspace/published-info/${viewId}/reaction`;
const response = await axiosInstance?.delete<{ code: number; message: string }>(url, {
data: {
@@ -704,7 +731,7 @@ export async function removeReaction (viewId: string, commentId: string, reactio
return Promise.reject(response?.data.message);
}
export async function getWorkspaces (): Promise<Workspace[]> {
export async function getWorkspaces(): Promise<Workspace[]> {
const query = new URLSearchParams({
include_member_count: 'true',
});
@@ -742,7 +769,7 @@ export interface WorkspaceFolder {
children: WorkspaceFolder[];
}
function iterateFolder (folder: WorkspaceFolder): FolderView {
function iterateFolder(folder: WorkspaceFolder): FolderView {
return {
id: folder.view_id,
name: folder.name,
@@ -756,7 +783,7 @@ function iterateFolder (folder: WorkspaceFolder): FolderView {
};
}
export async function getWorkspaceFolder (workspaceId: string): Promise<FolderView> {
export async function getWorkspaceFolder(workspaceId: string): Promise<FolderView> {
const url = `/api/workspace/${workspaceId}/folder`;
const response = await axiosInstance?.get<{
code: number;
@@ -779,7 +806,7 @@ export interface DuplicatePublishViewPayload {
dest_view_id: string;
}
export async function duplicatePublishView (workspaceId: string, payload: DuplicatePublishViewPayload) {
export async function duplicatePublishView(workspaceId: string, payload: DuplicatePublishViewPayload) {
const url = `/api/workspace/${workspaceId}/published-duplicate`;
const res = await axiosInstance?.post<{
@@ -794,7 +821,7 @@ export async function duplicatePublishView (workspaceId: string, payload: Duplic
return Promise.reject(res?.data.message);
}
export async function createTemplate (template: UploadTemplatePayload) {
export async function createTemplate(template: UploadTemplatePayload) {
const url = '/api/template-center/template';
const response = await axiosInstance?.post<{
code: number;
@@ -808,7 +835,7 @@ export async function createTemplate (template: UploadTemplatePayload) {
return Promise.reject(response?.data.message);
}
export async function updateTemplate (viewId: string, template: UploadTemplatePayload) {
export async function updateTemplate(viewId: string, template: UploadTemplatePayload) {
const url = `/api/template-center/template/${viewId}`;
const response = await axiosInstance?.put<{
code: number;
@@ -822,7 +849,7 @@ export async function updateTemplate (viewId: string, template: UploadTemplatePa
return Promise.reject(response?.data.message);
}
export async function getTemplates ({
export async function getTemplates({
categoryId,
nameContains,
}: {
@@ -853,7 +880,7 @@ export async function getTemplates ({
return Promise.reject(data);
}
export async function getTemplateById (viewId: string) {
export async function getTemplateById(viewId: string) {
const url = `/api/template-center/template/${viewId}`;
const response = await axiosInstance?.get<{
code: number;
@@ -870,7 +897,7 @@ export async function getTemplateById (viewId: string) {
return Promise.reject(data);
}
export async function deleteTemplate (viewId: string) {
export async function deleteTemplate(viewId: string) {
const url = `/api/template-center/template/${viewId}`;
const response = await axiosInstance?.delete<{
code: number;
@@ -884,7 +911,7 @@ export async function deleteTemplate (viewId: string) {
return Promise.reject(response?.data.message);
}
export async function getTemplateCategories () {
export async function getTemplateCategories() {
const url = '/api/template-center/category';
const response = await axiosInstance?.get<{
code: number;
@@ -904,7 +931,7 @@ export async function getTemplateCategories () {
return Promise.reject(data);
}
export async function addTemplateCategory (category: TemplateCategoryFormValues) {
export async function addTemplateCategory(category: TemplateCategoryFormValues) {
const url = '/api/template-center/category';
const response = await axiosInstance?.post<{
code: number;
@@ -918,7 +945,7 @@ export async function addTemplateCategory (category: TemplateCategoryFormValues)
return Promise.reject(response?.data.message);
}
export async function updateTemplateCategory (id: string, category: TemplateCategoryFormValues) {
export async function updateTemplateCategory(id: string, category: TemplateCategoryFormValues) {
const url = `/api/template-center/category/${id}`;
const response = await axiosInstance?.put<{
code: number;
@@ -932,7 +959,7 @@ export async function updateTemplateCategory (id: string, category: TemplateCate
return Promise.reject(response?.data.message);
}
export async function deleteTemplateCategory (categoryId: string) {
export async function deleteTemplateCategory(categoryId: string) {
const url = `/api/template-center/category/${categoryId}`;
const response = await axiosInstance?.delete<{
code: number;
@@ -946,7 +973,7 @@ export async function deleteTemplateCategory (categoryId: string) {
return Promise.reject(response?.data.message);
}
export async function getTemplateCreators () {
export async function getTemplateCreators() {
const url = '/api/template-center/creator';
const response = await axiosInstance?.get<{
code: number;
@@ -965,7 +992,7 @@ export async function getTemplateCreators () {
return Promise.reject(data);
}
export async function createTemplateCreator (creator: TemplateCreatorFormValues) {
export async function createTemplateCreator(creator: TemplateCreatorFormValues) {
const url = '/api/template-center/creator';
const response = await axiosInstance?.post<{
code: number;
@@ -979,7 +1006,7 @@ export async function createTemplateCreator (creator: TemplateCreatorFormValues)
return Promise.reject(response?.data.message);
}
export async function updateTemplateCreator (creatorId: string, creator: TemplateCreatorFormValues) {
export async function updateTemplateCreator(creatorId: string, creator: TemplateCreatorFormValues) {
const url = `/api/template-center/creator/${creatorId}`;
const response = await axiosInstance?.put<{
code: number;
@@ -993,7 +1020,7 @@ export async function updateTemplateCreator (creatorId: string, creator: Templat
return Promise.reject(response?.data.message);
}
export async function deleteTemplateCreator (creatorId: string) {
export async function deleteTemplateCreator(creatorId: string) {
const url = `/api/template-center/creator/${creatorId}`;
const response = await axiosInstance?.delete<{
code: number;
@@ -1007,7 +1034,7 @@ export async function deleteTemplateCreator (creatorId: string) {
return Promise.reject(response?.data.message);
}
export async function uploadFileToCDN (file: File) {
export async function uploadTemplateAvatar(file: File) {
const url = '/api/template-center/avatar';
const formData = new FormData();
@@ -1037,7 +1064,7 @@ export async function uploadFileToCDN (file: File) {
return Promise.reject(data);
}
export async function getInvitation (invitationId: string) {
export async function getInvitation(invitationId: string) {
const url = `/api/workspace/invite/${invitationId}`;
const response = await axiosInstance?.get<{
code: number;
@@ -1054,7 +1081,7 @@ export async function getInvitation (invitationId: string) {
return Promise.reject(data);
}
export async function acceptInvitation (invitationId: string) {
export async function acceptInvitation(invitationId: string) {
const url = `/api/workspace/accept-invite/${invitationId}`;
const response = await axiosInstance?.post<{
code: number;
@@ -1068,7 +1095,7 @@ export async function acceptInvitation (invitationId: string) {
return Promise.reject(response?.data.message);
}
export async function getRequestAccessInfo (requestId: string): Promise<GetRequestAccessInfoResponse> {
export async function getRequestAccessInfo(requestId: string): Promise<GetRequestAccessInfoResponse> {
const url = `/api/access-request/${requestId}`;
const response = await axiosInstance?.get<{
code: number;
@@ -1098,7 +1125,7 @@ export async function getRequestAccessInfo (requestId: string): Promise<GetReque
return Promise.reject(data);
}
export async function approveRequestAccess (requestId: string) {
export async function approveRequestAccess(requestId: string) {
const url = `/api/access-request/${requestId}/approve`;
const response = await axiosInstance?.post<{
code: number;
@@ -1114,7 +1141,7 @@ export async function approveRequestAccess (requestId: string) {
return Promise.reject(response?.data);
}
export async function sendRequestAccess (workspaceId: string, viewId: string) {
export async function sendRequestAccess(workspaceId: string, viewId: string) {
const url = `/api/access-request`;
const response = await axiosInstance?.post<{
code: number;
@@ -1131,7 +1158,7 @@ export async function sendRequestAccess (workspaceId: string, viewId: string) {
return Promise.reject(response?.data);
}
export async function getSubscriptionLink (workspaceId: string, plan: SubscriptionPlan, interval: SubscriptionInterval) {
export async function getSubscriptionLink(workspaceId: string, plan: SubscriptionPlan, interval: SubscriptionInterval) {
const url = `/billing/api/v1/subscription-link`;
const response = await axiosInstance?.get<{
code: number;
@@ -1155,7 +1182,7 @@ export async function getSubscriptionLink (workspaceId: string, plan: Subscripti
return Promise.reject(data);
}
export async function getSubscriptions () {
export async function getSubscriptions() {
const url = `/billing/api/v1/subscriptions`;
const response = await axiosInstance?.get<{
code: number;
@@ -1171,7 +1198,19 @@ export async function getSubscriptions () {
}
export async function getActiveSubscription (workspaceId: string) {
export async function getWorkspaceSubscriptions(workspaceId: string) {
try {
const plans = await getActiveSubscription(workspaceId);
const subscriptions = await getSubscriptions();
return subscriptions?.filter((subscription) => plans?.includes(subscription.plan));
} catch (e) {
return Promise.reject(e);
}
}
export async function getActiveSubscription(workspaceId: string) {
const url = `/billing/api/v1/active-subscription/${workspaceId}`;
const response = await axiosInstance?.get<{
@@ -1187,7 +1226,7 @@ export async function getActiveSubscription (workspaceId: string) {
return Promise.reject(response?.data);
}
export async function createImportTask (file: File) {
export async function createImportTask(file: File) {
const url = `/api/import/create`;
const fileName = file.name.split('.').slice(0, -1).join('.') || crypto.randomUUID();
@@ -1213,7 +1252,7 @@ export async function createImportTask (file: File) {
return Promise.reject(res?.data);
}
export async function uploadImportFile (presignedUrl: string, file: File, onProgress: (progress: number) => void) {
export async function uploadImportFile(presignedUrl: string, file: File, onProgress: (progress: number) => void) {
const response = await axios.put(presignedUrl, file, {
onUploadProgress: (progressEvent) => {
const { progress = 0 } = progressEvent;
@@ -1236,3 +1275,368 @@ export async function uploadImportFile (presignedUrl: string, file: File, onProg
});
}
export async function addAppPage(workspaceId: string, parentViewId: string, {
layout,
name,
}: CreatePagePayload) {
const url = `/api/workspace/${workspaceId}/page-view`;
const response = await axiosInstance?.post<{
code: number;
data: {
view_id: string;
};
message: string;
}>(url, {
parent_view_id: parentViewId,
layout,
name,
});
if (response?.data.code === 0) {
return response?.data.data.view_id;
}
return Promise.reject(response?.data);
}
export async function updatePage(workspaceId: string, viewId: string, data: UpdatePagePayload) {
const url = `/api/workspace/${workspaceId}/page-view/${viewId}`;
const res = await axiosInstance?.patch<{
code: number;
message: string;
}>(url, data);
if (res?.data.code === 0) {
return;
}
return Promise.reject(res?.data);
}
export async function deleteTrash(workspaceId: string, viewId?: string) {
if (viewId) {
const url = `/api/workspace/${workspaceId}/trash/${viewId}`;
const response = await axiosInstance?.delete<{
code: number;
message: string;
}>(url);
if (response?.data.code === 0) {
return;
}
return Promise.reject(response?.data);
} else {
const url = `/api/workspace/${workspaceId}/delete-all-pages-from-trash`;
const response = await axiosInstance?.post<{
code: number;
message: string;
}>(url);
if (response?.data.code === 0) {
return;
}
return Promise.reject(response?.data);
}
}
export async function moveToTrash(workspaceId: string, viewId: string) {
const url = `/api/workspace/${workspaceId}/page-view/${viewId}/move-to-trash`;
const response = await axiosInstance?.post<{
code: number;
message: string;
}>(url);
if (response?.data.code === 0) {
return;
}
return Promise.reject(response?.data);
}
export async function movePageTo(workspaceId: string, viewId: string, parentViewId: string, prevViewId?: string) {
const url = `/api/workspace/${workspaceId}/page-view/${viewId}/move`;
const response = await axiosInstance?.post<{
code: number;
message: string;
}>(url, {
new_parent_view_id: parentViewId,
prev_view_id: prevViewId,
});
if (response?.data.code === 0) {
return;
}
return Promise.reject(response?.data);
}
export async function restorePage(workspaceId: string, viewId?: string) {
const url = viewId ? `/api/workspace/${workspaceId}/page-view/${viewId}/restore-from-trash` : `/api/workspace/${workspaceId}/restore-all-pages-from-trash`;
const response = await axiosInstance?.post<{
code: number;
message: string;
}>(url);
if (response?.data.code === 0) {
return;
}
return Promise.reject(response?.data);
}
export async function createSpace(workspaceId: string, payload: CreateSpacePayload) {
const url = `/api/workspace/${workspaceId}/space`;
const response = await axiosInstance?.post<{
code: number;
data: {
view_id: string;
};
message: string;
}>(url, payload);
if (response?.data.code === 0) {
return response?.data.data.view_id;
}
return Promise.reject(response?.data);
}
export async function updateSpace(workspaceId: string, payload: UpdateSpacePayload) {
const url = `/api/workspace/${workspaceId}/space/${payload.view_id}`;
const data = omit(payload, ['view_id']);
const response = await axiosInstance?.patch<{
code: number;
message: string;
}>(url, data);
if (response?.data.code === 0) {
return;
}
return Promise.reject(response?.data);
}
export async function uploadFile(workspaceId: string, viewId: string, file: File, onProgress?: (progress: number) => void) {
const url = `/api/file_storage/${workspaceId}/v1/blob/${viewId}`;
// Check file size, if over 7MB, check subscription plan
if (file.size > 7 * 1024 * 1024) {
const plan = await getActiveSubscription(workspaceId);
if (plan?.length === 0 || plan?.[0] === SubscriptionPlan.Free) {
notify.error('Your file is over 7 MB limit of the Free plan. Upgrade for unlimited uploads.');
return Promise.reject({
code: 413,
message: 'File size is too large. Please upgrade your plan for unlimited uploads.',
});
}
}
try {
const response = await axiosInstance?.put<{
code: number;
message: string;
data: {
file_id: string;
}
}>(url, file, {
onUploadProgress: (progressEvent) => {
const { progress = 0 } = progressEvent;
onProgress?.(progress);
},
headers: {
'Content-Type': file.type || 'application/octet-stream',
},
});
if (response?.data.code === 0) {
const baseURL = axiosInstance?.defaults.baseURL;
const url = `${baseURL}/api/file_storage/${workspaceId}/v1/blob/${viewId}/${response?.data.data.file_id}`;
return url;
}
return Promise.reject(response?.data);
// eslint-disable-next-line
} catch (e: any) {
if (e.response.status === 413) {
return Promise.reject({
code: 413,
message: 'File size is too large. Please upgrade your plan for unlimited uploads.',
});
}
}
return Promise.reject({
code: -1,
message: 'Upload file failed.',
});
}
export async function inviteMembers(workspaceId: string, emails: string[]) {
const url = `/api/workspace/${workspaceId}/invite`;
const payload = emails.map(e => ({
email: e,
role: Role.Member,
}));
const res = await axiosInstance?.post<{
code: number;
message: string;
}>(url, payload);
if (res?.data.code === 0) {
return;
}
return Promise.reject(res?.data);
}
export async function getMembers(workspaceId: string) {
const url = `/api/workspace/${workspaceId}/member`;
const res = await axiosInstance?.get<{
code: number;
data: WorkspaceMember[];
message: string;
}>(url);
if (res?.data.code === 0) {
return res?.data.data;
}
return Promise.reject(res?.data);
}
export async function leaveWorkspace(workspaceId: string) {
const url = `/api/workspace/${workspaceId}/leave`;
const res = await axiosInstance?.post<{
code: number;
message: string;
}>(url);
if (res?.data.code === 0) {
return;
}
return Promise.reject(res?.data);
}
export async function deleteWorkspace(workspaceId: string) {
const url = `/api/workspace/${workspaceId}`;
const res = await axiosInstance?.delete<{
code: number;
message: string;
}>(url);
if (res?.data.code === 0) {
return;
}
return Promise.reject(res?.data);
}
export async function getQuickNoteList(workspaceId: string, params: {
offset?: number;
limit?: number;
searchTerm?: string;
}) {
const url = `/api/workspace/${workspaceId}/quick-note`;
const res = await axiosInstance?.get<{
code: number;
data: {
quick_notes: QuickNote[];
has_more: boolean;
};
message: string;
}>(url, {
params: {
offset: params.offset,
limit: params.limit,
search_term: params.searchTerm || undefined,
},
});
if (res?.data.code === 0) {
return {
data: res?.data.data.quick_notes,
has_more: res?.data.data.has_more,
};
}
return Promise.reject(res?.data);
}
export async function createQuickNote(workspaceId: string, payload: QuickNoteEditorData[]): Promise<QuickNote> {
const url = `/api/workspace/${workspaceId}/quick-note`;
const res = await axiosInstance?.post<{
code: number;
data: QuickNote;
message: string;
}>(url, {
data: payload,
});
if (res?.data.code === 0) {
return res?.data.data;
}
return Promise.reject(res?.data);
}
export async function updateQuickNote(workspaceId: string, noteId: string, payload: QuickNoteEditorData[]) {
const url = `/api/workspace/${workspaceId}/quick-note/${noteId}`;
const res = await axiosInstance?.put<{
code: number;
message: string;
}>(url, {
data: payload,
});
if (res?.data.code === 0) {
return;
}
return Promise.reject(res?.data);
}
export async function deleteQuickNote(workspaceId: string, noteId: string) {
const url = `/api/workspace/${workspaceId}/quick-note/${noteId}`;
const res = await axiosInstance?.delete<{
code: number;
message: string;
}>(url);
if (res?.data.code === 0) {
return;
}
return Promise.reject(res?.data);
}
export async function cancelSubscription(workspaceId: string, plan: SubscriptionPlan, reason?: string) {
const url = `/billing/api/v1/cancel-subscription`;
const res = await axiosInstance?.post<{
code: number;
message: string;
}>(url, {
workspace_id: workspaceId,
plan,
sync: true,
reason,
});
if (res?.data.code === 0) {
return;
}
return Promise.reject(res?.data);
}
@@ -29,10 +29,11 @@ import {
UploadTemplatePayload,
} from '@/application/template.type';
import {
CreatePagePayload, CreateSpacePayload,
DatabaseRelations,
DuplicatePublishView,
DuplicatePublishView, QuickNoteEditorData,
SubscriptionInterval, SubscriptionPlan,
Types,
Types, UpdatePagePayload, UpdateSpacePayload, WorkspaceMember,
YjsEditorKey,
} from '@/application/types';
import { applyYDoc } from '@/application/ydoc/apply';
@@ -56,15 +57,15 @@ export class AFClientService implements AFService {
}
> = new Map();
constructor (config: AFServiceConfig) {
constructor(config: AFServiceConfig) {
APIService.initAPIService(config.cloudConfig);
}
getClientId () {
getClientId() {
return this.clientId;
}
async getPublishViewMeta (namespace: string, publishName: string) {
async getPublishViewMeta(namespace: string, publishName: string) {
const name = `${namespace}_${publishName}`;
const isLoaded = this.publishViewLoaded.has(name);
@@ -86,7 +87,7 @@ export class AFClientService implements AFService {
return viewMeta;
}
async getPublishView (namespace: string, publishName: string) {
async getPublishView(namespace: string, publishName: string) {
const name = `${namespace}_${publishName}`;
const isLoaded = this.publishViewLoaded.has(name);
@@ -121,7 +122,7 @@ export class AFClientService implements AFService {
return doc;
}
async getPublishRowDocument (viewId: string) {
async getPublishRowDocument(viewId: string) {
const doc = await openCollabDB(viewId);
if (hasCollabCache(doc)) {
@@ -132,15 +133,15 @@ export class AFClientService implements AFService {
}
async createRowDoc (rowKey: string) {
async createRowDoc(rowKey: string) {
return createRowDoc(rowKey);
}
deleteRowDoc (rowKey: string) {
deleteRowDoc(rowKey: string) {
return deleteRowDoc(rowKey);
}
async getAppDatabaseViewRelations (workspaceId: string, databaseStorageId: string) {
async getAppDatabaseViewRelations(workspaceId: string, databaseStorageId: string) {
const res = await APIService.getCollab(workspaceId, databaseStorageId, Types.WorkspaceDatabase);
const doc = new Y.Doc();
@@ -159,7 +160,7 @@ export class AFClientService implements AFService {
return result;
}
async getPublishInfo (viewId: string) {
async getPublishInfo(viewId: string) {
if (this.publishViewInfo.has(viewId)) {
return this.publishViewInfo.get(viewId) as {
namespace: string;
@@ -185,31 +186,31 @@ export class AFClientService implements AFService {
return data;
}
async getPublishOutline (namespace: string) {
async getPublishOutline(namespace: string) {
return APIService.getPublishOutline(namespace);
}
async getAppOutline (workspaceId: string) {
async getAppOutline(workspaceId: string) {
return APIService.getAppOutline(workspaceId);
}
async getAppView (workspaceId: string, viewId: string) {
async getAppView(workspaceId: string, viewId: string) {
return APIService.getView(workspaceId, viewId);
}
async getAppFavorites (workspaceId: string) {
async getAppFavorites(workspaceId: string) {
return APIService.getAppFavorites(workspaceId);
}
async getAppRecent (workspaceId: string) {
async getAppRecent(workspaceId: string) {
return APIService.getAppRecent(workspaceId);
}
async getAppTrash (workspaceId: string) {
async getAppTrash(workspaceId: string) {
return APIService.getAppTrash(workspaceId);
}
async loginAuth (url: string) {
async loginAuth(url: string) {
try {
await APIService.signInWithUrl(url);
emit(EventType.SESSION_VALID);
@@ -222,43 +223,43 @@ export class AFClientService implements AFService {
}
@withSignIn()
async signInMagicLink ({ email }: { email: string; redirectTo: string }) {
async signInMagicLink({ email }: { email: string; redirectTo: string }) {
return await APIService.signInWithMagicLink(email, AUTH_CALLBACK_URL);
}
@withSignIn()
async signInGoogle (_: { redirectTo: string }) {
async signInGoogle(_: { redirectTo: string }) {
return APIService.signInGoogle(AUTH_CALLBACK_URL);
}
@withSignIn()
async signInApple (_: { redirectTo: string }) {
async signInApple(_: { redirectTo: string }) {
return APIService.signInApple(AUTH_CALLBACK_URL);
}
@withSignIn()
async signInGithub (_: { redirectTo: string }) {
async signInGithub(_: { redirectTo: string }) {
return APIService.signInGithub(AUTH_CALLBACK_URL);
}
@withSignIn()
async signInDiscord (_: { redirectTo: string }) {
async signInDiscord(_: { redirectTo: string }) {
return APIService.signInDiscord(AUTH_CALLBACK_URL);
}
async getWorkspaces () {
async getWorkspaces() {
const data = APIService.getWorkspaces();
return data;
}
async getWorkspaceFolder (workspaceId: string) {
async getWorkspaceFolder(workspaceId: string) {
const data = await APIService.getWorkspaceFolder(workspaceId);
return data;
}
async getCurrentUser () {
async getCurrentUser() {
const token = getTokenParsed();
const userId = token?.user?.id;
@@ -275,11 +276,11 @@ export class AFClientService implements AFService {
return user;
}
async openWorkspace (workspaceId: string) {
async openWorkspace(workspaceId: string) {
return APIService.openWorkspace(workspaceId);
}
async getUserWorkspaceInfo () {
async getUserWorkspaceInfo() {
const workspaceInfo = await APIService.getUserWorkspaceInfo();
if (!workspaceInfo) {
@@ -293,7 +294,7 @@ export class AFClientService implements AFService {
};
}
async duplicatePublishView (params: DuplicatePublishView) {
async duplicatePublishView(params: DuplicatePublishView) {
return APIService.duplicatePublishView(params.workspaceId, {
dest_view_id: params.spaceViewId,
published_view_id: params.viewId,
@@ -301,90 +302,90 @@ export class AFClientService implements AFService {
});
}
createCommentOnPublishView (viewId: string, content: string, replyCommentId: string | undefined): Promise<void> {
createCommentOnPublishView(viewId: string, content: string, replyCommentId: string | undefined): Promise<void> {
return APIService.createGlobalCommentOnPublishView(viewId, content, replyCommentId);
}
deleteCommentOnPublishView (viewId: string, commentId: string): Promise<void> {
deleteCommentOnPublishView(viewId: string, commentId: string): Promise<void> {
return APIService.deleteGlobalCommentOnPublishView(viewId, commentId);
}
getPublishViewGlobalComments (viewId: string): Promise<GlobalComment[]> {
getPublishViewGlobalComments(viewId: string): Promise<GlobalComment[]> {
return APIService.getPublishViewComments(viewId);
}
getPublishViewReactions (viewId: string, commentId?: string): Promise<Record<string, Reaction[]>> {
getPublishViewReactions(viewId: string, commentId?: string): Promise<Record<string, Reaction[]>> {
return APIService.getReactions(viewId, commentId);
}
addPublishViewReaction (viewId: string, commentId: string, reactionType: string): Promise<void> {
addPublishViewReaction(viewId: string, commentId: string, reactionType: string): Promise<void> {
return APIService.addReaction(viewId, commentId, reactionType);
}
removePublishViewReaction (viewId: string, commentId: string, reactionType: string): Promise<void> {
removePublishViewReaction(viewId: string, commentId: string, reactionType: string): Promise<void> {
return APIService.removeReaction(viewId, commentId, reactionType);
}
async getTemplateCategories () {
async getTemplateCategories() {
return APIService.getTemplateCategories();
}
async getTemplateCreators () {
async getTemplateCreators() {
return APIService.getTemplateCreators();
}
async createTemplate (template: UploadTemplatePayload) {
async createTemplate(template: UploadTemplatePayload) {
return APIService.createTemplate(template);
}
async updateTemplate (id: string, template: UploadTemplatePayload) {
async updateTemplate(id: string, template: UploadTemplatePayload) {
return APIService.updateTemplate(id, template);
}
async getTemplateById (id: string) {
async getTemplateById(id: string) {
return APIService.getTemplateById(id);
}
async getTemplates (params: {
async getTemplates(params: {
categoryId?: string;
nameContains?: string;
}) {
return APIService.getTemplates(params);
}
async deleteTemplate (id: string) {
async deleteTemplate(id: string) {
return APIService.deleteTemplate(id);
}
async addTemplateCategory (category: TemplateCategoryFormValues) {
async addTemplateCategory(category: TemplateCategoryFormValues) {
return APIService.addTemplateCategory(category);
}
async updateTemplateCategory (categoryId: string, category: TemplateCategoryFormValues) {
async updateTemplateCategory(categoryId: string, category: TemplateCategoryFormValues) {
return APIService.updateTemplateCategory(categoryId, category);
}
async deleteTemplateCategory (categoryId: string) {
async deleteTemplateCategory(categoryId: string) {
return APIService.deleteTemplateCategory(categoryId);
}
async updateTemplateCreator (creatorId: string, creator: TemplateCreatorFormValues) {
async updateTemplateCreator(creatorId: string, creator: TemplateCreatorFormValues) {
return APIService.updateTemplateCreator(creatorId, creator);
}
async createTemplateCreator (creator: TemplateCreatorFormValues) {
async createTemplateCreator(creator: TemplateCreatorFormValues) {
return APIService.createTemplateCreator(creator);
}
async deleteTemplateCreator (creatorId: string) {
async deleteTemplateCreator(creatorId: string) {
return APIService.deleteTemplateCreator(creatorId);
}
async uploadFileToCDN (file: File) {
return APIService.uploadFileToCDN(file);
async uploadTemplateAvatar(file: File) {
return APIService.uploadTemplateAvatar(file);
}
async getPageDoc (workspaceId: string, viewId: string, errorCallback?: (error: {
async getPageDoc(workspaceId: string, viewId: string, errorCallback?: (error: {
code: number;
}) => void) {
@@ -427,39 +428,47 @@ export class AFClientService implements AFService {
return doc;
}
async getInvitation (invitationId: string) {
async getInvitation(invitationId: string) {
return APIService.getInvitation(invitationId);
}
async acceptInvitation (invitationId: string) {
async acceptInvitation(invitationId: string) {
return APIService.acceptInvitation(invitationId);
}
approveRequestAccess (requestId: string): Promise<void> {
approveRequestAccess(requestId: string): Promise<void> {
return APIService.approveRequestAccess(requestId);
}
getRequestAccessInfo (requestId: string) {
getRequestAccessInfo(requestId: string) {
return APIService.getRequestAccessInfo(requestId);
}
sendRequestAccess (workspaceId: string, viewId: string): Promise<void> {
sendRequestAccess(workspaceId: string, viewId: string): Promise<void> {
return APIService.sendRequestAccess(workspaceId, viewId);
}
getSubscriptionLink (workspaceId: string, plan: SubscriptionPlan, interval: SubscriptionInterval) {
getSubscriptionLink(workspaceId: string, plan: SubscriptionPlan, interval: SubscriptionInterval) {
return APIService.getSubscriptionLink(workspaceId, plan, interval);
}
getSubscriptions () {
cancelSubscription(workspaceId: string, plan: SubscriptionPlan, reason?: string) {
return APIService.cancelSubscription(workspaceId, plan, reason);
}
getSubscriptions() {
return APIService.getSubscriptions();
}
getActiveSubscription (workspaceId: string) {
getActiveSubscription(workspaceId: string) {
return APIService.getActiveSubscription(workspaceId);
}
registerDocUpdate (doc: Y.Doc, context: {
getWorkspaceSubscriptions(workspaceId: string) {
return APIService.getWorkspaceSubscriptions(workspaceId);
}
registerDocUpdate(doc: Y.Doc, context: {
workspaceId: string, objectId: string, collabType: Types
}) {
const token = getTokenParsed();
@@ -474,9 +483,81 @@ export class AFClientService implements AFService {
sync.initialize();
}
async importFile (file: File, onProgress: (progress: number) => void) {
async importFile(file: File, onProgress: (progress: number) => void) {
const task = await APIService.createImportTask(file);
await APIService.uploadImportFile(task.presignedUrl, file, onProgress);
}
async createSpace(workspaceId: string, payload: CreateSpacePayload) {
return APIService.createSpace(workspaceId, payload);
}
async updateSpace(workspaceId: string, payload: UpdateSpacePayload) {
return APIService.updateSpace(workspaceId, payload);
}
async addAppPage(workspaceId: string, parentViewId: string, payload: CreatePagePayload) {
return APIService.addAppPage(workspaceId, parentViewId, payload);
}
async updateAppPage(workspaceId: string, viewId: string, data: UpdatePagePayload) {
return APIService.updatePage(workspaceId, viewId, data);
}
async deleteTrash(workspaceId: string, viewId?: string) {
return APIService.deleteTrash(workspaceId, viewId);
}
async moveToTrash(workspaceId: string, viewId: string) {
return APIService.moveToTrash(workspaceId, viewId);
}
async restoreFromTrash(workspaceId: string, viewId?: string) {
return APIService.restorePage(workspaceId, viewId);
}
async movePage(workspaceId: string, viewId: string, parentId: string, prevViewId?: string) {
return APIService.movePageTo(workspaceId, viewId, parentId, prevViewId);
}
async uploadFile(workspaceId: string, viewId: string, file: File, onProgress?: (progress: number) => void) {
return APIService.uploadFile(workspaceId, viewId, file, onProgress);
}
deleteWorkspace(workspaceId: string): Promise<void> {
return APIService.deleteWorkspace(workspaceId);
}
leaveWorkspace(workspaceId: string): Promise<void> {
return APIService.leaveWorkspace(workspaceId);
}
inviteMembers(workspaceId: string, emails: string[]): Promise<void> {
return APIService.inviteMembers(workspaceId, emails);
}
getWorkspaceMembers(workspaceId: string): Promise<WorkspaceMember[]> {
return APIService.getMembers(workspaceId);
}
getQuickNoteList(workspaceId: string, params: {
offset?: number;
limit?: number;
searchTerm?: string;
}) {
return APIService.getQuickNoteList(workspaceId, params);
}
createQuickNote(workspaceId: string, data: QuickNoteEditorData[]) {
return APIService.createQuickNote(workspaceId, data);
}
updateQuickNote(workspaceId: string, id: string, data: QuickNoteEditorData[]) {
return APIService.updateQuickNote(workspaceId, id, data);
}
deleteQuickNote(workspaceId: string, id: string) {
return APIService.deleteQuickNote(workspaceId, id);
}
}
@@ -6,7 +6,20 @@ import {
UserWorkspaceInfo,
View,
Workspace,
YDoc, DatabaseRelations, GetRequestAccessInfoResponse, Subscriptions, SubscriptionPlan, SubscriptionInterval, Types,
YDoc,
DatabaseRelations,
GetRequestAccessInfoResponse,
Subscriptions,
SubscriptionPlan,
SubscriptionInterval,
Types,
UpdatePagePayload,
CreatePagePayload,
CreateSpacePayload,
UpdateSpacePayload,
WorkspaceMember,
QuickNoteEditorData,
QuickNote, Subscription,
} from '@/application/types';
import { GlobalComment, Reaction } from '@/application/comment.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
@@ -18,7 +31,7 @@ import {
UploadTemplatePayload,
} from '@/application/template.type';
export type AFService = PublishService & AppService & TemplateService & {
export type AFService = PublishService & AppService & WorkspaceService & TemplateService & QuickNoteService & {
getClientId: () => string;
};
@@ -32,8 +45,15 @@ export interface AFCloudConfig {
wsURL: string;
}
export interface AppService {
export interface WorkspaceService {
openWorkspace: (workspaceId: string) => Promise<void>;
leaveWorkspace: (workspaceId: string) => Promise<void>;
deleteWorkspace: (workspaceId: string) => Promise<void>;
getWorkspaceMembers: (workspaceId: string) => Promise<WorkspaceMember[]>;
inviteMembers: (workspaceId: string, emails: string[]) => Promise<void>;
}
export interface AppService {
getPageDoc: (workspaceId: string, viewId: string, errorCallback?: (error: {
code: number;
}) => void) => Promise<YDoc>;
@@ -55,7 +75,7 @@ export interface AppService {
getWorkspaceFolder: (workspaceId: string) => Promise<FolderView>;
getCurrentUser: () => Promise<User>;
getUserWorkspaceInfo: () => Promise<UserWorkspaceInfo>;
uploadFileToCDN: (file: File) => Promise<string>;
uploadTemplateAvatar: (file: File) => Promise<string>;
getInvitation: (invitationId: string) => Promise<Invitation>;
acceptInvitation: (invitationId: string) => Promise<void>;
getRequestAccessInfo: (requestId: string) => Promise<GetRequestAccessInfoResponse>;
@@ -63,11 +83,36 @@ export interface AppService {
sendRequestAccess: (workspaceId: string, viewId: string) => Promise<void>;
getSubscriptionLink: (workspaceId: string, plan: SubscriptionPlan, interval: SubscriptionInterval) => Promise<string>;
getSubscriptions: () => Promise<Subscriptions>;
cancelSubscription: (workspaceId: string, plan: SubscriptionPlan, reason?: string) => Promise<void>;
getActiveSubscription: (workspaceId: string) => Promise<SubscriptionPlan[]>;
getWorkspaceSubscriptions: (workspaceId: string) => Promise<Subscription[]>;
registerDocUpdate: (doc: YDoc, context: {
workspaceId: string, objectId: string, collabType: Types
}) => void;
importFile: (file: File, onProgress: (progress: number) => void) => Promise<void>;
createSpace: (workspaceId: string, payload: CreateSpacePayload) => Promise<string>;
updateSpace: (workspaceId: string, payload: UpdateSpacePayload) => Promise<void>;
addAppPage: (workspaceId: string, parentViewId: string, payload: CreatePagePayload) => Promise<string>;
updateAppPage: (workspaceId: string, viewId: string, data: UpdatePagePayload) => Promise<void>;
deleteTrash: (workspaceId: string, viewId?: string) => Promise<void>;
moveToTrash: (workspaceId: string, viewId: string) => Promise<void>;
restoreFromTrash: (workspaceId: string, viewId?: string) => Promise<void>;
movePage: (workspaceId: string, viewId: string, parentId: string, prevViewId?: string) => Promise<void>;
uploadFile: (workspaceId: string, viewId: string, file: File, onProgress?: (progress: number) => void) => Promise<string>;
}
export interface QuickNoteService {
getQuickNoteList: (workspaceId: string, params: {
offset?: number;
limit?: number;
searchTerm?: string;
}) => Promise<{
data: QuickNote[];
has_more: boolean;
}>;
createQuickNote: (workspaceId: string, data: QuickNoteEditorData[]) => Promise<QuickNote>;
updateQuickNote: (workspaceId: string, id: string, data: QuickNoteEditorData[]) => Promise<void>;
deleteQuickNote: (workspaceId: string, id: string) => Promise<void>;
}
export interface TemplateService {
@@ -96,7 +141,7 @@ export interface PublishService {
getPublishRowDocument: (viewId: string) => Promise<YDoc>;
getPublishInfo: (viewId: string) => Promise<{ namespace: string; publishName: string }>;
getPublishOutline (namespace: string): Promise<View[]>;
getPublishOutline(namespace: string): Promise<View[]>;
getPublishViewGlobalComments: (viewId: string) => Promise<GlobalComment[]>;
createCommentOnPublishView: (viewId: string, content: string, replyCommentId?: string) => Promise<void>;
@@ -170,7 +170,7 @@ export class AFClientService implements AFService {
return Promise.reject('Method not implemented');
}
uploadFileToCDN (_file: File): Promise<string> {
uploadTemplateAvatar (_file: File): Promise<string> {
return Promise.resolve('');
}
@@ -258,11 +258,47 @@ export class AFClientService implements AFService {
return Promise.reject('Method not implemented');
}
registerDocUpdate (): void {
registerDocUpdate () {
throw new Error('Method not implemented.');
}
importFile (_file: File, _onProgress: (progress: number) => void) {
return Promise.reject('Method not implemented');
}
addAppPage (): Promise<string> {
return Promise.reject('Method not implemented');
}
deleteTrash (): Promise<void> {
return Promise.reject('Method not implemented');
}
movePage (): Promise<void> {
return Promise.reject('Method not implemented');
}
moveToTrash (): Promise<void> {
return Promise.reject('Method not implemented');
}
restoreFromTrash (): Promise<void> {
return Promise.reject('Method not implemented');
}
updateAppPage (): Promise<void> {
return Promise.reject('Method not implemented');
}
createSpace (): Promise<string> {
return Promise.reject('');
}
updateSpace (): Promise<void> {
return Promise.reject(undefined);
}
uploadFile(): Promise<string> {
return Promise.reject('');
}
}
Loaded 100 of 507 files, more files were not shown because too many files have changed in this diff. Show more