Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71d1189048 | ||
|
|
757009a97d | ||
|
|
e8665dc76c | ||
|
|
f022f9aa06 | ||
|
|
32650b7941 | ||
|
|
5a7339b092 | ||
|
|
7ca028942c | ||
|
|
6fbd88fe76 | ||
|
|
2b5ecf5609 | ||
|
|
18672b197c | ||
|
|
63a267ca6d | ||
|
|
1f1a97b21b | ||
|
|
ca1ce8e19e | ||
|
|
f8f0599462 | ||
|
|
a4845e668d | ||
|
|
dd5bc7808c | ||
|
|
a993a2a181 | ||
|
|
99ad9dcaff | ||
|
|
d44de62b90 |
No files matched your search
@@ -1,5 +1,10 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.1.6 - 30/05/2023
|
||||
|
||||
### New features
|
||||
- Support exporting your document and database data.
|
||||
|
||||
## Version 0.1.5 - 11/05/2023
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -26,6 +26,7 @@ You are in charge of your data and customizations.
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664610-fc209a97-815e-4716-af07-d94a859d1907.png" alt="AppFlowy Docs & Notes & Wikis" width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664628-5def2450-914a-4b2d-b907-92b7476b9863.png" alt="AppFlowy Databases for Tasks and Projects" width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664642-22e26c1b-5eae-4635-9aa6-b12ecf1c3c46.png" alt="AppFlowy Kanban Board for To-Dos" width="1000px" /></p>
|
||||
<p align="center"><img src="https://github.com/AppFlowy-IO/AppFlowy/assets/12026239/6be93d2b-a5c5-48a9-b7cf-c599d5f5140c" alt="AppFlowy Calendars for Plan and Manage Content" width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664657-dc5291f3-67b0-4a43-a818-640e92735deb.png" alt="AppFlowy OpenAI GPT Writers" width="1000px" /></p>
|
||||
|
||||
## User Installation
|
||||
|
||||
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.1.5"
|
||||
CURRENT_APP_VERSION = "0.1.6"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 8.91521C7.21574 9.22582 7.49099 9.48283 7.80707 9.66881C8.12315 9.85479 8.47268 9.96538 8.83194 9.99309C9.1912 10.0208 9.5518 9.96497 9.88926 9.8294C10.2267 9.69383 10.5332 9.48169 10.7878 9.20736L12.2949 7.58431C12.7525 7.07413 13.0056 6.39083 12.9999 5.68156C12.9942 4.9723 12.73 4.29384 12.2643 3.7923C11.7986 3.29075 11.1686 3.00627 10.51 3.0001C9.85142 2.99394 9.21693 3.26659 8.7432 3.75935L7.87913 4.68448" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 7.08479C8.78426 6.77418 8.50901 6.51717 8.19293 6.33119C7.87685 6.14521 7.52732 6.03462 7.16806 6.00691C6.8088 5.9792 6.4482 6.03503 6.11074 6.1706C5.77327 6.30617 5.46683 6.51831 5.21218 6.79264L3.7051 8.41569C3.24755 8.92587 2.99437 9.60918 3.00009 10.3184C3.00582 11.0277 3.26998 11.7062 3.73569 12.2077C4.2014 12.7092 4.8314 12.9937 5.48999 12.9999C6.14858 13.0061 6.78307 12.7334 7.2568 12.2407L8.11584 11.3155" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -45,7 +45,8 @@
|
||||
"medium": "medium",
|
||||
"large": "large",
|
||||
"fontSize": "Font Size",
|
||||
"import": "Import"
|
||||
"import": "Import",
|
||||
"moreOptions": "More options"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Rename",
|
||||
@@ -183,6 +184,7 @@
|
||||
},
|
||||
"files": {
|
||||
"defaultLocation": "Where your data is stored now",
|
||||
"exportData": "Export your data",
|
||||
"doubleTapToCopy": "Double tap to copy the path",
|
||||
"restoreLocation": "Restore to AppFlowy default path",
|
||||
"customizeLocation": "Open another folder",
|
||||
@@ -201,7 +203,9 @@
|
||||
"create": "Create",
|
||||
"folderPath": "Path to store your folder",
|
||||
"locationCannotBeEmpty": "Path cannot be empty",
|
||||
"pathCopiedSnackbar": "File storage path copied to clipboard!"
|
||||
"pathCopiedSnackbar": "File storage path copied to clipboard!",
|
||||
"exportFileSuccess": "Export file successfully!",
|
||||
"exportFileFail": "Export file failed!"
|
||||
},
|
||||
"user": {
|
||||
"name": "Name",
|
||||
@@ -363,7 +367,8 @@
|
||||
"smartEditFixSpelling": "Fix spelling",
|
||||
"warning": "⚠️ AI responses can be inaccurate or misleading.",
|
||||
"smartEditSummarize": "Summarize",
|
||||
"smartEditImproveWriting": "Improve Writing",
|
||||
"smartEditImproveWriting": "Improve writing",
|
||||
"smartEditMakeLonger": "Make longer",
|
||||
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
|
||||
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
|
||||
"smartEditDisabled": "Connect OpenAI in Settings",
|
||||
@@ -423,4 +428,4 @@
|
||||
"emptyNoDate": "No unscheduled events"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ void main() {
|
||||
group('board', () {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
tearDown(() async => await service.tearDown());
|
||||
tearDownAll(() async => await service.tearDownAll());
|
||||
|
||||
testWidgets(
|
||||
'integration test unzips the proper workspace and loads it correctly.',
|
||||
|
||||
@@ -30,6 +30,8 @@ void main() {
|
||||
group('Tests on a workspace with only an empty document', () {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
tearDown(() async => await service.tearDown());
|
||||
tearDownAll(() async => await service.tearDownAll());
|
||||
|
||||
testWidgets('/board shortcut creates a new board and view of the board',
|
||||
(tester) async {
|
||||
@@ -69,6 +71,7 @@ void main() {
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Checks whether new board is referenced and properly on the page.
|
||||
expect(find.byType(BuiltInPageWidget), findsOneWidget);
|
||||
@@ -113,7 +116,12 @@ void main() {
|
||||
expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
|
||||
|
||||
// Finalizes the slash command that creates the board.
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.enter);
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
LogicalKeyboardKey.enter,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Checks whether new board is referenced and properly on the page.
|
||||
|
||||
@@ -16,6 +16,8 @@ void main() {
|
||||
group('integration tests for open-ai smart menu', () {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
tearDown(() async => await service.tearDown());
|
||||
tearDownAll(() async => await service.tearDownAll());
|
||||
|
||||
testWidgets('testing selection on open-ai smart menu replace', (tester) async {
|
||||
final appFlowyEditor = await setUpOpenAITesting(tester);
|
||||
|
||||
@@ -39,6 +39,13 @@ enum TestWorkspace {
|
||||
return root;
|
||||
}
|
||||
|
||||
Future<void> clean() async {
|
||||
final Directory workspaceRoot = await root;
|
||||
if (await workspaceRoot.exists()) {
|
||||
workspaceRoot.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
String get _asset => 'assets/test/workspaces/$_name.zip';
|
||||
}
|
||||
|
||||
@@ -67,4 +74,12 @@ class TestWorkspaceService {
|
||||
await TestWorkspace._parent.then((value) => value.path),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tearDown() async {
|
||||
await workspace.clean();
|
||||
}
|
||||
|
||||
Future<void> tearDownAll() async {
|
||||
await SharedPreferences.getInstance().then((value) => value.remove(kSettingsLocationDefaultLocation));
|
||||
}
|
||||
}
|
||||
+3
@@ -161,6 +161,9 @@ class CalendarDayCard extends StatelessWidget {
|
||||
});
|
||||
|
||||
renderHook.addSelectOptionHook((selectedOptions, cardData, _) {
|
||||
if (selectedOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final children = selectedOptions.map(
|
||||
(option) {
|
||||
return SelectOptionTag.fromOption(
|
||||
|
||||
+86
-47
@@ -19,6 +19,8 @@ import 'url_cell_bloc.dart';
|
||||
|
||||
class GridURLCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
TextStyle? textStyle;
|
||||
bool? autofocus;
|
||||
|
||||
List<GridURLCellAccessoryType> accessoryTypes;
|
||||
|
||||
@@ -29,8 +31,8 @@ class GridURLCellStyle extends GridCellStyle {
|
||||
}
|
||||
|
||||
enum GridURLCellAccessoryType {
|
||||
edit,
|
||||
copyURL,
|
||||
visitURL,
|
||||
}
|
||||
|
||||
class GridURLCell extends GridCellWidget {
|
||||
@@ -56,15 +58,14 @@ class GridURLCell extends GridCellWidget {
|
||||
GridCellAccessoryBuildContext buildContext,
|
||||
) {
|
||||
switch (ty) {
|
||||
case GridURLCellAccessoryType.edit:
|
||||
case GridURLCellAccessoryType.visitURL:
|
||||
final cellContext = cellControllerBuilder.build() as URLCellController;
|
||||
return GridCellAccessoryBuilder(
|
||||
builder: (Key key) => _EditURLAccessory(
|
||||
builder: (Key key) => _VisitURLAccessory(
|
||||
key: key,
|
||||
anchorContext: buildContext.anchorContext,
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
);
|
||||
|
||||
case GridURLCellAccessoryType.copyURL:
|
||||
final cellContext = cellControllerBuilder.build() as URLCellController;
|
||||
return GridCellAccessoryBuilder(
|
||||
@@ -89,11 +90,11 @@ class GridURLCell extends GridCellWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.edit
|
||||
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.visitURL
|
||||
if (accessories.isEmpty) {
|
||||
accessories.add(
|
||||
accessoryFromType(
|
||||
GridURLCellAccessoryType.edit,
|
||||
GridURLCellAccessoryType.visitURL,
|
||||
buildContext,
|
||||
),
|
||||
);
|
||||
@@ -106,6 +107,8 @@ class GridURLCell extends GridCellWidget {
|
||||
class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
final _popoverController = PopoverController();
|
||||
late URLCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
late FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -113,6 +116,8 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
widget.cellControllerBuilder.build() as URLCellController;
|
||||
_cellBloc = URLCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const URLCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
_focusNode = FocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -122,60 +127,61 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<URLCellBloc, URLCellState>(
|
||||
builder: (context, state) {
|
||||
final richText = Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: FlowyText.medium(
|
||||
state.content,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
final urlEditor = Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: GridSize.cellContentInsets.left,
|
||||
right: GridSize.cellContentInsets.right,
|
||||
),
|
||||
);
|
||||
|
||||
return AppFlowyPopover(
|
||||
margin: EdgeInsets.zero,
|
||||
controller: _popoverController,
|
||||
constraints: BoxConstraints.loose(const Size(300, 160)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
offset: const Offset(0, 8),
|
||||
child: SizedBox.expand(
|
||||
child: GestureDetector(
|
||||
child: Align(alignment: Alignment.centerLeft, child: richText),
|
||||
onTap: () async {
|
||||
final url = context.read<URLCellBloc>().state.url;
|
||||
await _openUrlOrEdit(url);
|
||||
},
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
maxLines: 1,
|
||||
style: (widget.cellStyle?.textStyle ??
|
||||
Theme.of(context).textTheme.bodyMedium)
|
||||
?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
autofocus: false,
|
||||
onEditingComplete: focusChanged,
|
||||
onSubmitted: (value) => focusChanged(isUrlSubmitted: true),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: GridSize.cellContentInsets.top,
|
||||
bottom: GridSize.cellContentInsets.bottom,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
hintText: widget.cellStyle?.placeholder,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return URLEditorPopover(
|
||||
cellController:
|
||||
widget.cellControllerBuilder.build() as URLCellController,
|
||||
onExit: () => _popoverController.close(),
|
||||
);
|
||||
},
|
||||
onClose: () {
|
||||
widget.onCellEditing.value = false;
|
||||
},
|
||||
);
|
||||
return urlEditor;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void focusChanged({
|
||||
bool isUrlSubmitted = false,
|
||||
}) {
|
||||
if (mounted) {
|
||||
if (_cellBloc.isClosed == false &&
|
||||
_controller.text != _cellBloc.state.content) {
|
||||
_cellBloc.add(URLCellEvent.updateURL(_controller.text));
|
||||
}
|
||||
if (isUrlSubmitted) {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openUrlOrEdit(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (url.isNotEmpty && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
widget.onCellEditing.value = true;
|
||||
@@ -269,3 +275,36 @@ class _CopyURLAccessoryState extends State<_CopyURLAccessory>
|
||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
||||
}
|
||||
}
|
||||
|
||||
class _VisitURLAccessory extends StatefulWidget {
|
||||
final URLCellController cellContext;
|
||||
const _VisitURLAccessory({required this.cellContext, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _VisitURLAccessoryState();
|
||||
}
|
||||
|
||||
class _VisitURLAccessoryState extends State<_VisitURLAccessory>
|
||||
with GridCellAccessoryState {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return svgWidget(
|
||||
"editor/link",
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() {
|
||||
var content =
|
||||
widget.cellContext.getCellData(loadIfNotExist: false)?.content ?? "";
|
||||
if (!content.contains('http://')) {
|
||||
content = 'http://$content';
|
||||
}
|
||||
final uri = Uri.parse(content);
|
||||
if (content.isNotEmpty) {
|
||||
canLaunchUrl(uri).then((value) => launchUrl(uri));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,8 +361,8 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
return GridURLCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
accessoryTypes: [
|
||||
GridURLCellAccessoryType.edit,
|
||||
GridURLCellAccessoryType.copyURL,
|
||||
GridURLCellAccessoryType.visitURL,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/font_size_switcher.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -14,6 +16,7 @@ class DocumentMoreButton extends StatelessWidget {
|
||||
return PopupMenuButton<int>(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
offset: const Offset(0, 30),
|
||||
tooltip: LocaleKeys.moreAction_moreOptions.tr(),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ class DeleteButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
onTap: () => onPressed,
|
||||
onTap: onPressed,
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||
|
||||
+10
-1
@@ -6,7 +6,8 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
enum SmartEditAction {
|
||||
summarize,
|
||||
fixSpelling,
|
||||
improveWriting;
|
||||
improveWriting,
|
||||
makeItLonger;
|
||||
|
||||
String get toInstruction {
|
||||
switch (this) {
|
||||
@@ -16,6 +17,8 @@ enum SmartEditAction {
|
||||
return 'Correct this to standard English:';
|
||||
case SmartEditAction.improveWriting:
|
||||
return 'Rewrite this in your own words:';
|
||||
case SmartEditAction.makeItLonger:
|
||||
return 'Make this text longer:';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +30,8 @@ enum SmartEditAction {
|
||||
return 'Correct this to standard English:\n\n$input';
|
||||
case SmartEditAction.improveWriting:
|
||||
return 'Rewrite this:\n\n$input';
|
||||
case SmartEditAction.makeItLonger:
|
||||
return 'Make this text longer:\n\n$input';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +43,8 @@ enum SmartEditAction {
|
||||
return SmartEditAction.fixSpelling;
|
||||
case 2:
|
||||
return SmartEditAction.improveWriting;
|
||||
case 3:
|
||||
return SmartEditAction.makeItLonger;
|
||||
}
|
||||
return SmartEditAction.fixSpelling;
|
||||
}
|
||||
@@ -50,6 +57,8 @@ enum SmartEditAction {
|
||||
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
|
||||
case SmartEditAction.improveWriting:
|
||||
return LocaleKeys.document_plugins_smartEditImproveWriting.tr();
|
||||
case SmartEditAction.makeItLonger:
|
||||
return LocaleKeys.document_plugins_smartEditMakeLonger.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,4 +179,4 @@ void _resolveGridDeps(GetIt getIt) {
|
||||
(viewId, cache) =>
|
||||
DatabasePropertyBloc(viewId: viewId, fieldController: cache),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'dart:ui';
|
||||
|
||||
class ColorGenerator {
|
||||
Color generateColorFromString(String string) {
|
||||
final hash = string.hashCode;
|
||||
int r = (hash & 0xFF0000) >> 16;
|
||||
int g = (hash & 0x00FF00) >> 8;
|
||||
int b = hash & 0x0000FF;
|
||||
return Color.fromRGBO(r, g, b, 0.5);
|
||||
}
|
||||
}
|
||||
+15
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingsFileExportState {
|
||||
@@ -8,6 +9,20 @@ class SettingsFileExportState {
|
||||
initialize();
|
||||
}
|
||||
|
||||
List<ViewPB> get selectedViews {
|
||||
final selectedViews = <ViewPB>[];
|
||||
for (var i = 0; i < apps.length; i++) {
|
||||
if (selectedApps[i]) {
|
||||
for (var j = 0; j < apps[i].belongings.items.length; j++) {
|
||||
if (selectedItems[i][j]) {
|
||||
selectedViews.add(apps[i].belongings.items[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedViews;
|
||||
}
|
||||
|
||||
List<AppPB> apps;
|
||||
List<bool> expanded = [];
|
||||
List<bool> selectedApps = [];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/color_generator/color_generator.dart';
|
||||
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
@@ -45,8 +46,31 @@ class MenuUser extends StatelessWidget {
|
||||
String iconUrl = context.read<MenuUserBloc>().state.userProfile.iconUrl;
|
||||
if (iconUrl.isEmpty) {
|
||||
iconUrl = defaultUserAvatar;
|
||||
final String name = context.read<MenuUserBloc>().state.userProfile.name;
|
||||
final Color color = ColorGenerator().generateColorFromString(name);
|
||||
const initialsCount = 2;
|
||||
// Taking the first letters of the name components and limiting to 2 elements
|
||||
final nameInitials = name
|
||||
.split(' ')
|
||||
.where((element) => element.isNotEmpty)
|
||||
.take(initialsCount)
|
||||
.map((element) => element[0].toUpperCase())
|
||||
.join('');
|
||||
return Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: FlowyText.semibold(
|
||||
nameInitials,
|
||||
color: Colors.white,
|
||||
fontSize: nameInitials.length == initialsCount ? 12 : 14,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
|
||||
class SettingsExportFileWidget extends StatefulWidget {
|
||||
const SettingsExportFileWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SettingsExportFileWidget> createState() =>
|
||||
SettingsExportFileWidgetState();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class SettingsExportFileWidgetState extends State<SettingsExportFileWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.settings_files_exportData.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: LocaleKeys.settings_files_open.tr(),
|
||||
child: FlowyIconButton(
|
||||
height: 40,
|
||||
width: 40,
|
||||
icon: const Icon(Icons.folder_open_outlined),
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
onPressed: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const FlowyDialog(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 20,
|
||||
),
|
||||
child: FileExporterWidget(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -12,8 +12,8 @@ import '../../../../startup/launch_configuration.dart';
|
||||
import '../../../../startup/startup.dart';
|
||||
import '../../../../startup/tasks/prelude.dart';
|
||||
|
||||
class SettingsFileLocationCustomzier extends StatefulWidget {
|
||||
const SettingsFileLocationCustomzier({
|
||||
class SettingsFileLocationCustomizer extends StatefulWidget {
|
||||
const SettingsFileLocationCustomizer({
|
||||
super.key,
|
||||
required this.cubit,
|
||||
});
|
||||
@@ -21,13 +21,13 @@ class SettingsFileLocationCustomzier extends StatefulWidget {
|
||||
final SettingsLocationCubit cubit;
|
||||
|
||||
@override
|
||||
State<SettingsFileLocationCustomzier> createState() =>
|
||||
SettingsFileLocationCustomzierState();
|
||||
State<SettingsFileLocationCustomizer> createState() =>
|
||||
SettingsFileLocationCustomizerState();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class SettingsFileLocationCustomzierState
|
||||
extends State<SettingsFileLocationCustomzier> {
|
||||
class SettingsFileLocationCustomizerState
|
||||
extends State<SettingsFileLocationCustomizer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<SettingsLocationCubit>.value(
|
||||
|
||||
+126
-38
@@ -1,15 +1,22 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:dartz/dartz.dart' as dartz;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:tuple/tuple.dart';
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
|
||||
class FileExporterWidget extends StatefulWidget {
|
||||
@@ -22,24 +29,44 @@ class FileExporterWidget extends StatefulWidget {
|
||||
class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
// Map<String, List<String>> _selectedPages = {};
|
||||
|
||||
SettingsFileExporterCubit? cubit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_files_selectFiles.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const VSpace(8),
|
||||
Expanded(child: _buildFileSelector(context)),
|
||||
const VSpace(8),
|
||||
_buildButtons(context)
|
||||
],
|
||||
return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
|
||||
future: FolderEventReadCurrentWorkspace().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
|
||||
if (workspaces != null) {
|
||||
final apps = workspaces.workspace.apps.items;
|
||||
cubit ??= SettingsFileExporterCubit(apps: apps);
|
||||
return BlocProvider<SettingsFileExporterCubit>.value(
|
||||
value: cubit!,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_files_selectFiles.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const VSpace(8),
|
||||
const Expanded(child: _ExpandedList()),
|
||||
const VSpace(8),
|
||||
_buildButtons()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const CircularProgressIndicator();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildButtons(BuildContext context) {
|
||||
Widget _buildButtons() {
|
||||
return Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
@@ -55,8 +82,28 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
onPressed: () async {
|
||||
await getIt<FilePickerService>()
|
||||
.getDirectoryPath()
|
||||
.then((exportPath) {
|
||||
Navigator.of(context).pop();
|
||||
.then((exportPath) async {
|
||||
if (exportPath != null && cubit != null) {
|
||||
final views = cubit!.state.selectedViews;
|
||||
final result =
|
||||
await _AppFlowyFileExporter.exportToPath(exportPath, views);
|
||||
if (result.item1) {
|
||||
// success
|
||||
_showToast(LocaleKeys.settings_files_exportFileSuccess.tr());
|
||||
} else {
|
||||
_showToast(
|
||||
LocaleKeys.settings_files_exportFileFail.tr() +
|
||||
result.item2.join('\n'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_showToast(LocaleKeys.settings_files_exportFileFail.tr());
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).popUntil(
|
||||
(router) => router.settings.name == '/',
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -64,24 +111,14 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>
|
||||
_buildFileSelector(BuildContext context) {
|
||||
return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
|
||||
future: FolderEventReadCurrentWorkspace().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
|
||||
if (workspaces != null) {
|
||||
final apps = workspaces.workspace.apps.items;
|
||||
return BlocProvider<SettingsFileExporterCubit>(
|
||||
create: (_) => SettingsFileExporterCubit(apps: apps),
|
||||
child: const _ExpandedList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const CircularProgressIndicator();
|
||||
},
|
||||
void _showToast(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: FlowyText(
|
||||
message,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -131,9 +168,9 @@ class _ExpandedListState extends State<_ExpandedList> {
|
||||
final apps = state.apps;
|
||||
final expanded = state.expanded;
|
||||
final selectedItems = state.selectedItems;
|
||||
final isExpaned = expanded[index] == true;
|
||||
final isExpanded = expanded[index] == true;
|
||||
List<Widget> expandedChildren = [];
|
||||
if (isExpaned) {
|
||||
if (isExpanded) {
|
||||
for (var i = 0; i < selectedItems[index].length; i++) {
|
||||
final name = apps[index].belongings.items[i].name;
|
||||
final checkbox = CheckboxListTile(
|
||||
@@ -160,7 +197,7 @@ class _ExpandedListState extends State<_ExpandedList> {
|
||||
child: ListTile(
|
||||
title: FlowyText.medium(apps[index].name),
|
||||
trailing: Icon(
|
||||
isExpaned
|
||||
isExpanded
|
||||
? Icons.arrow_drop_down_rounded
|
||||
: Icons.arrow_drop_up_rounded,
|
||||
),
|
||||
@@ -182,3 +219,54 @@ extension AppFlowy on dartz.Either {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _AppFlowyFileExporter {
|
||||
static Future<Tuple2<bool, List<String>>> exportToPath(
|
||||
String path,
|
||||
List<ViewPB> views,
|
||||
) async {
|
||||
final failedFileNames = <String>[];
|
||||
final Map<String, int> names = {};
|
||||
final documentService = DocumentService();
|
||||
for (final view in views) {
|
||||
String? content;
|
||||
String? fileExtension;
|
||||
switch (view.layout) {
|
||||
case ViewLayoutTypePB.Document:
|
||||
final document = await documentService.openDocument(view: view);
|
||||
document.fold(
|
||||
(l) => content = l.content,
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
fileExtension = 'afdoc';
|
||||
break;
|
||||
default:
|
||||
final result = await exportDatabase(view.id);
|
||||
result.fold(
|
||||
(pb) => content = pb.data,
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
fileExtension = 'afdb';
|
||||
break;
|
||||
}
|
||||
if (content != null) {
|
||||
final count = names.putIfAbsent(view.name, () => 0);
|
||||
final name = count == 0 ? view.name : '${view.name}($count)';
|
||||
final file = File(p.join(path, '$name.$fileExtension'));
|
||||
await file.writeAsString(content!);
|
||||
names[view.name] = count + 1;
|
||||
} else {
|
||||
failedFileNames.add(view.name);
|
||||
}
|
||||
}
|
||||
|
||||
return Tuple2(failedFileNames.isEmpty, failedFileNames);
|
||||
}
|
||||
}
|
||||
|
||||
Future<dartz.Either<ExportCSVPB, FlowyError>> exportDatabase(
|
||||
String viewId,
|
||||
) async {
|
||||
final payload = DatabaseViewIdPB.create()..value = viewId;
|
||||
return DatabaseEventExportCSV(payload).send();
|
||||
}
|
||||
+10
-11
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_export_file_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -14,22 +15,20 @@ class SettingsFileSystemView extends StatefulWidget {
|
||||
|
||||
class _SettingsFileSystemViewState extends State<SettingsFileSystemView> {
|
||||
final _locationCubit = SettingsLocationCubit()..fetchLocation();
|
||||
late final _items = [
|
||||
SettingsFileLocationCustomizer(
|
||||
cubit: _locationCubit,
|
||||
),
|
||||
const SettingsExportFileWidget()
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return SettingsFileLocationCustomzier(
|
||||
cubit: _locationCubit,
|
||||
);
|
||||
} else if (index == 1) {
|
||||
// return _buildExportDatabaseButton();
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) => _items[index],
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemCount: 2, // make the divider taking effect.
|
||||
itemCount: _items.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ String languageFromLocale(Locale locale) {
|
||||
case "hu":
|
||||
return "Magyar";
|
||||
case "id":
|
||||
return "Bahasa";
|
||||
return "Bahasa Indonesia";
|
||||
case "it":
|
||||
return "Italiano";
|
||||
case "ja":
|
||||
|
||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.1.5
|
||||
version: 0.1.6
|
||||
|
||||
environment:
|
||||
sdk: ">=2.19.0 <3.0.0"
|
||||
|
||||
Generated
+1
@@ -1141,6 +1141,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crossbeam-utils",
|
||||
"csv",
|
||||
"dashmap",
|
||||
"database-model",
|
||||
"diesel",
|
||||
|
||||
@@ -47,6 +47,7 @@ atomic_refcell = "0.1.9"
|
||||
crossbeam-utils = "0.8.15"
|
||||
async-stream = "0.3.4"
|
||||
parking_lot = "0.12.1"
|
||||
csv = "1.1.6"
|
||||
|
||||
[dev-dependencies]
|
||||
flowy-test = { path = "../flowy-test" }
|
||||
|
||||
@@ -203,3 +203,9 @@ pub struct DatabaseLayoutIdPB {
|
||||
#[pb(index = 2)]
|
||||
pub layout: LayoutTypePB,
|
||||
}
|
||||
|
||||
#[derive(Clone, ProtoBuf, Default, Debug)]
|
||||
pub struct ExportCSVPB {
|
||||
#[pb(index = 1)]
|
||||
pub data: String,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::entities::*;
|
||||
use crate::manager::DatabaseManager;
|
||||
use crate::services::cell::{FromCellString, ToCellChangesetString, TypeCellData};
|
||||
use crate::services::export::CSVExport;
|
||||
use crate::services::field::{
|
||||
default_type_option_builder_from_type, select_type_option_from_field_rev,
|
||||
type_option_builder_from_json_str, DateCellChangeset, DateChangesetPB, SelectOptionCellChangeset,
|
||||
@@ -644,3 +645,16 @@ pub(crate) async fn get_calendar_event_handler(
|
||||
Some(event) => data_result_ok(event),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(data, manager), err)]
|
||||
pub(crate) async fn export_csv_handler(
|
||||
data: AFPluginData<DatabaseViewIdPB>,
|
||||
manager: AFPluginState<Arc<DatabaseManager>>,
|
||||
) -> DataResult<ExportCSVPB, FlowyError> {
|
||||
let params = data.into_inner();
|
||||
let database_editor = manager.get_database_editor(¶ms.value).await?;
|
||||
let content = CSVExport
|
||||
.export_database(¶ms.value, &database_editor)
|
||||
.await?;
|
||||
data_result_ok(ExportCSVPB { data: content })
|
||||
}
|
||||
@@ -55,7 +55,8 @@ pub fn init(database_manager: Arc<DatabaseManager>) -> AFPlugin {
|
||||
.event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler)
|
||||
// Layout setting
|
||||
.event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler)
|
||||
.event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler);
|
||||
.event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler)
|
||||
.event(DatabaseEvent::ExportCSV, export_csv_handler);
|
||||
|
||||
plugin
|
||||
}
|
||||
@@ -256,4 +257,7 @@ pub enum DatabaseEvent {
|
||||
|
||||
#[event(input = "MoveCalendarEventPB")]
|
||||
MoveCalendarEvent = 119,
|
||||
|
||||
#[event(input = "DatabaseViewIdPB", output = "ExportCSVPB")]
|
||||
ExportCSV = 120,
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use crate::entities::FieldType;
|
||||
|
||||
use crate::services::cell::TypeCellData;
|
||||
use crate::services::database::DatabaseEditor;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateCellData, DateTypeOptionPB,
|
||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
|
||||
URLCellData,
|
||||
};
|
||||
use database_model::{FieldRevision, TypeOptionDataDeserializer};
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ExportField {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub field_type: i64,
|
||||
pub visibility: bool,
|
||||
pub width: i64,
|
||||
pub type_options: HashMap<String, Value>,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct ExportCell {
|
||||
data: String,
|
||||
field_type: FieldType,
|
||||
}
|
||||
|
||||
impl From<&Arc<FieldRevision>> for ExportField {
|
||||
fn from(field_rev: &Arc<FieldRevision>) -> Self {
|
||||
let field_type = FieldType::from(field_rev.ty);
|
||||
let mut type_options: HashMap<String, Value> = HashMap::new();
|
||||
|
||||
field_rev
|
||||
.type_options
|
||||
.iter()
|
||||
.filter(|(k, _)| k == &&field_rev.ty.to_string())
|
||||
.for_each(|(k, s)| {
|
||||
let value = match field_type {
|
||||
FieldType::RichText => {
|
||||
let pb = RichTextTypeOptionPB::from_json_str(s);
|
||||
serde_json::to_value(pb).unwrap()
|
||||
},
|
||||
FieldType::Number => {
|
||||
let pb = NumberTypeOptionPB::from_json_str(s);
|
||||
let mut map = Map::new();
|
||||
map.insert("format".to_string(), json!(pb.format as u8));
|
||||
map.insert("scale".to_string(), json!(pb.scale));
|
||||
map.insert("symbol".to_string(), json!(pb.symbol));
|
||||
map.insert("name".to_string(), json!(pb.name));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::DateTime => {
|
||||
let pb = DateTypeOptionPB::from_json_str(s);
|
||||
let mut map = Map::new();
|
||||
map.insert("date_format".to_string(), json!(pb.date_format as u8));
|
||||
map.insert("time_format".to_string(), json!(pb.time_format as u8));
|
||||
map.insert("field_type".to_string(), json!(FieldType::DateTime as u8));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::SingleSelect => {
|
||||
let pb = SingleSelectTypeOptionPB::from_json_str(s);
|
||||
let value = serde_json::to_string(&pb).unwrap();
|
||||
let mut map = Map::new();
|
||||
map.insert("content".to_string(), Value::String(value));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::MultiSelect => {
|
||||
let pb = MultiSelectTypeOptionPB::from_json_str(s);
|
||||
let value = serde_json::to_string(&pb).unwrap();
|
||||
let mut map = Map::new();
|
||||
map.insert("content".to_string(), Value::String(value));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::Checkbox => {
|
||||
let pb = CheckboxTypeOptionPB::from_json_str(s);
|
||||
serde_json::to_value(pb).unwrap()
|
||||
},
|
||||
FieldType::URL => {
|
||||
let pb = RichTextTypeOptionPB::from_json_str(s);
|
||||
serde_json::to_value(pb).unwrap()
|
||||
},
|
||||
FieldType::Checklist => {
|
||||
let pb = ChecklistTypeOptionPB::from_json_str(s);
|
||||
let value = serde_json::to_string(&pb).unwrap();
|
||||
let mut map = Map::new();
|
||||
map.insert("content".to_string(), Value::String(value));
|
||||
Value::Object(map)
|
||||
},
|
||||
};
|
||||
type_options.insert(k.clone(), value);
|
||||
});
|
||||
Self {
|
||||
id: field_rev.id.clone(),
|
||||
name: field_rev.name.clone(),
|
||||
field_type: field_rev.ty as i64,
|
||||
visibility: true,
|
||||
width: 100,
|
||||
type_options,
|
||||
is_primary: field_rev.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CSVExport;
|
||||
impl CSVExport {
|
||||
pub async fn export_database(
|
||||
&self,
|
||||
view_id: &str,
|
||||
database_editor: &Arc<DatabaseEditor>,
|
||||
) -> FlowyResult<String> {
|
||||
let mut wtr = csv::Writer::from_writer(vec![]);
|
||||
let row_revs = database_editor.get_all_row_revs(view_id).await?;
|
||||
let field_revs = database_editor.get_field_revs(None).await?;
|
||||
|
||||
// Write fields
|
||||
let field_records = field_revs
|
||||
.iter()
|
||||
.map(|field| ExportField::from(field))
|
||||
.map(|field| serde_json::to_string(&field).unwrap())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
wtr
|
||||
.write_record(&field_records)
|
||||
.map_err(|e| FlowyError::internal().context(e))?;
|
||||
|
||||
// Write rows
|
||||
let mut field_by_field_id = IndexMap::new();
|
||||
field_revs.into_iter().for_each(|field| {
|
||||
field_by_field_id.insert(field.id.clone(), field);
|
||||
});
|
||||
for row_rev in row_revs {
|
||||
let cells = field_by_field_id
|
||||
.iter()
|
||||
.map(|(field_id, field)| {
|
||||
let field_type = FieldType::from(field.ty);
|
||||
let data = row_rev
|
||||
.cells
|
||||
.get(field_id)
|
||||
.map(|cell| TypeCellData::try_from(cell))
|
||||
.map(|data| {
|
||||
data
|
||||
.map(|data| match field_type {
|
||||
FieldType::DateTime => {
|
||||
match serde_json::from_str::<DateCellData>(&data.cell_str) {
|
||||
Ok(cell_data) => cell_data.timestamp.unwrap_or_default().to_string(),
|
||||
Err(_) => "".to_string(),
|
||||
}
|
||||
},
|
||||
FieldType::URL => match serde_json::from_str::<URLCellData>(&data.cell_str) {
|
||||
Ok(cell_data) => cell_data.content,
|
||||
Err(_) => "".to_string(),
|
||||
},
|
||||
_ => data.cell_str,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let cell = ExportCell { data, field_type };
|
||||
serde_json::to_string(&cell).unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = wtr.write_record(&cells) {
|
||||
tracing::warn!("CSV failed to write record: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let data = wtr
|
||||
.into_inner()
|
||||
.map_err(|e| FlowyError::internal().context(e))?;
|
||||
let csv = String::from_utf8(data).map_err(|e| FlowyError::internal().context(e))?;
|
||||
Ok(csv)
|
||||
}
|
||||
}
|
||||
+2
@@ -168,6 +168,7 @@ impl ToString for DateCellData {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum DateFormat {
|
||||
Local = 0,
|
||||
US = 1,
|
||||
@@ -216,6 +217,7 @@ impl DateFormat {
|
||||
#[derive(
|
||||
Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize, ProtoBuf_Enum,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum TimeFormat {
|
||||
TwelveHour = 0,
|
||||
TwentyFourHour = 1,
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ lazy_static! {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum NumberFormat {
|
||||
Num = 0,
|
||||
USD = 1,
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder {
|
||||
pub struct RichTextTypeOptionPB {
|
||||
#[pb(index = 1)]
|
||||
#[serde(default)]
|
||||
data: String,
|
||||
pub data: String,
|
||||
}
|
||||
impl_type_option!(RichTextTypeOptionPB, FieldType::RichText);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ mod util;
|
||||
pub mod cell;
|
||||
pub mod database;
|
||||
pub mod database_view;
|
||||
pub mod export;
|
||||
pub mod field;
|
||||
pub mod filter;
|
||||
pub mod group;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use crate::database::database_editor::DatabaseEditorTest;
|
||||
use flowy_database::services::export::CSVExport;
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_test() {
|
||||
let test = DatabaseEditorTest::new_grid().await;
|
||||
|
||||
let s = CSVExport
|
||||
.export_database(&test.view_id, &test.editor)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut reader = csv::Reader::from_reader(s.as_bytes());
|
||||
for header in reader.headers() {
|
||||
println!("{:?}", header);
|
||||
}
|
||||
|
||||
let export_csv_records = reader.records();
|
||||
for record in export_csv_records {
|
||||
let record = record.unwrap();
|
||||
println!("{:?}", record);
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@ mod layout_test;
|
||||
mod snapshot_test;
|
||||
mod sort_test;
|
||||
|
||||
mod export_test;
|
||||
mod mock_data;
|
||||
+14
-26
@@ -1,29 +1,17 @@
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @type { import("@inlang/core/config").DefineConfig }
|
||||
*/
|
||||
export async function defineConfig(env) {
|
||||
const plugin = await env.$import(
|
||||
"https://cdn.jsdelivr.net/gh/samuelstroschein/inlang-plugin-json@1.1.1/dist/index.js"
|
||||
);
|
||||
const { default: pluginJson } = await env.$import(
|
||||
'https://cdn.jsdelivr.net/gh/samuelstroschein/inlang-plugin-json@2/dist/index.js'
|
||||
);
|
||||
|
||||
const { default: standardLintRules } = await env.$import(
|
||||
"https://cdn.jsdelivr.net/gh/inlang/standard-lint-rules@1.1.1/dist/index.js"
|
||||
);
|
||||
const { default: standardLintRules } = await env.$import(
|
||||
'https://cdn.jsdelivr.net/gh/inlang/standard-lint-rules@2/dist/index.js'
|
||||
);
|
||||
|
||||
const pluginConfig = {
|
||||
pathPattern: "./frontend/appflowy_flutter/assets/translations/{language}.json",
|
||||
};
|
||||
|
||||
return {
|
||||
referenceLanguage: "en",
|
||||
languages: await plugin.getLanguages({ ...env, pluginConfig }),
|
||||
readResources: (args) => plugin.readResources({ ...args, ...env, pluginConfig }),
|
||||
writeResources: (args) => plugin.writeResources({ ...args, ...env, pluginConfig }),
|
||||
lint: {
|
||||
rules: [standardLintRules()],
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
referenceLanguage: 'en',
|
||||
plugins: [pluginJson({
|
||||
pathPattern: './frontend/appflowy_flutter/assets/translations/{language}.json',
|
||||
variableReferencePattern: ["@:"]
|
||||
}), standardLintRules()]
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user