Compare commits
17
Commits
chat_rag_only
...
0.7.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e63f767926 | ||
|
|
8826e479eb | ||
|
|
8e4fe3d559 | ||
|
|
e2ee11e48a | ||
|
|
bdc0fa1f2a | ||
|
|
3836545682 | ||
|
|
02eb0e0b83 | ||
|
|
38c2937f64 | ||
|
|
06d5bc734b | ||
|
|
a521541cb7 | ||
|
|
7d61252e6a | ||
|
|
cb2b9ac537 | ||
|
|
af31f8cf6f | ||
|
|
87c1245a3f | ||
|
|
49d9417cac | ||
|
|
1f29235538 | ||
|
|
fd1e36b21a |
No files matched your search
+18
-2
@@ -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
|
||||
|
||||
+66
-6
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+9
-1
@@ -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 {
|
||||
|
||||
+10
-3
@@ -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 {
|
||||
|
||||
+1
@@ -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(
|
||||
|
||||
+9
-1
@@ -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 {
|
||||
|
||||
+13
-8
@@ -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,
|
||||
|
||||
+3
-1
@@ -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;
|
||||
|
||||
|
||||
+40
-24
@@ -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(
|
||||
|
||||
+6
@@ -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(),
|
||||
|
||||
+1
-1
@@ -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,
|
||||
),
|
||||
|
||||
+4
-8
@@ -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,
|
||||
|
||||
+14
-11
@@ -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),
|
||||
|
||||
+68
-8
@@ -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) {
|
||||
|
||||
+79
-23
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -100,7 +100,7 @@ class ChatAIMessageWidget extends StatelessWidget {
|
||||
onSelectedMetadata: onSelectedMetadata,
|
||||
),
|
||||
if (state.sources.isNotEmpty && !isLastMessage)
|
||||
const VSpace(16.0),
|
||||
const VSpace(8.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
+1
-1
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+8
-1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-8
@@ -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,
|
||||
|
||||
+3
-2
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+14
-10
@@ -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);
|
||||
|
||||
+21
-9
@@ -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;
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
+5
-1
@@ -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
|
||||
|
||||
+11
-1
@@ -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)
|
||||
|
||||
+18
-15
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+5
-4
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
+1
-1
@@ -456,7 +456,7 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
|
||||
return context.simpleTableDefaultHeaderColor;
|
||||
}
|
||||
|
||||
return Theme.of(context).colorScheme.surface;
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
bool _isInHeader() {
|
||||
|
||||
+5
@@ -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);
|
||||
}
|
||||
+12
-2
@@ -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: {},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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;
|
||||
}
|
||||
|
||||
+241
@@ -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');
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+14
-1
@@ -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
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+40
@@ -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;
|
||||
}
|
||||
+14
-15
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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(
|
||||
|
||||
+6
-1
@@ -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();
|
||||
|
||||
+6
-2
@@ -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;
|
||||
|
||||
+8
-3
@@ -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;
|
||||
|
||||
+59
-3
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+54
@@ -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() {
|
||||
|
||||
+7
@@ -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;
|
||||
|
||||
+53
-11
@@ -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,
|
||||
|
||||
+30
-11
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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(
|
||||
|
||||
+58
-52
@@ -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),
|
||||
|
||||
+1
@@ -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:
|
||||
|
||||
+6
-1
@@ -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,
|
||||
|
||||
+4
-6
@@ -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();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -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,
|
||||
|
||||
+5
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,4 +158,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
+62
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+64
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+62
@@ -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
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+2339
-106
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
@@ -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
Reference in new issue
Block a user