Compare commits

...
32 Commits
Author SHA1 Message Date
Lucas.Xu 71d1189048 chore: bump version 0.1.6 (#2665) 2023-05-31 13:12:49 +08:00
Nathan.fooo 757009a97d chore: revert reorder rows in grid (#2533) (#2669)
This reverts commit f8f0599462.
2023-05-31 11:21:33 +08:00
Lucas.Xuandnathan e8665dc76c feat: support exporting document data to afdoc format (#2610)
* feat: support exporting document data to afdoc format

* feat: export database

* fix: resolve comment issues

* fix: add error tips when exporting files failed

---------

Co-authored-by: nathan <nathan@appflowy.io>
2023-05-30 17:55:38 +08:00
Daniyar Nurmukhamet f022f9aa06 feature: changed url field ux to be directly editable (#2523)
* feature: changed url field ux to be directly editable

* feature: added link svg icon
2023-05-30 11:04:10 +08:00
Alex Wallen 32650b7941 Revert "feat: support regenerating ai results #2012 (#2528)" (#2642)
This reverts commit 1f1a97b21b.
2023-05-28 11:56:38 -10:00
Alex Wallen 5a7339b092 [Fix] Integration test flakes. (#2635)
* feat: add tearDown and tearDownAll to reset workspaces

* fix: register tearDown and tearDownAll in empty_document_test

* feat: register tearDown and tearDownAll in board_test

* feat: register tearDown and tearDownAll in open_ai
2023-05-28 10:23:20 -10:00
Nathan.fooo 7ca028942c chore: implement export handler (#2625)
* chore: implement export handler

* chore: fix export
2023-05-27 21:21:34 +08:00
Alex Wallen 6fbd88fe76 Revert "Revert "feat: support regenerating ai results #2012 (#2528)" (#2622)" (#2636)
This reverts commit 2b5ecf5609.
2023-05-26 14:00:20 -10:00
Alex Wallen 2b5ecf5609 Revert "feat: support regenerating ai results #2012 (#2528)" (#2622)
This reverts commit 1f1a97b21b.
2023-05-26 13:59:21 -10:00
GitStartandgitstart 18672b197c fix: Update Indonesian language from Bahasa to Bahasa Indonesia (#2555)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
2023-05-19 20:00:25 +08:00
GitStartandgitstart 63a267ca6d fix: Text in Show menu tooltip does not get translated (#2563)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
2023-05-19 19:42:06 +08:00
1f1a97b21b feat: support regenerating ai results #2012 (#2528)
* feat: added emoji and network image support

* fix: code cleanup and improvements

* feat: rewrite AI response #2012

* fix: code refactor, rewrite in smart edit

* chore: delete the redundant file

---------

Co-authored-by: ahmeduzair890 <ahmeduzair12123@gmail.com>
Co-authored-by: rizwan3395 <rizwan.ios33gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-05-19 19:26:39 +08:00
Nils Jacobsen ca1ce8e19e chore: update inlang.config.js (#2542) 2023-05-19 19:26:04 +08:00
Mathias Mogensen f8f0599462 feat: reorder rows in grid (#2533)
* feat: reorder rows in grid

Allows reordering of rows in a Grid where no current filter and sorting is applied

Closes: #1123

* fix: resolve review comments
2023-05-16 21:20:05 +08:00
Daniyar NurmukhametandLucas.Xu a4845e668d feature: added name icon (#2529)
* feature: added name icon

* feature: added generating color in the defined HSL ranges

* refactor: removed trailing commas for readibility

* fix: fixed trimming name and separated color picker into util file

* fix: changed to using where instead of trim

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

* feat: simplify the color generator

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-05-15 19:11:43 +08:00
Daniyar Nurmukhamet dd5bc7808c feature: added making text longer (#2532)
* feature: added making text longer

* fix: corrected I10n
2023-05-15 17:01:37 +08:00
Richard Shiue a993a2a181 fix: remove icon button not working in popover (#2518) 2023-05-15 10:59:34 +08:00
Richard Shiue 99ad9dcaff fix: don't show whitespace when no options are selected (#2493) 2023-05-15 10:56:38 +08:00
Annie d44de62b90 chore: Update README.md
Add a screenshot of the calendar view
2023-05-12 17:12:00 +08:00
Lucas.Xu 33b5837f04 chore: update changelog.md (#2510) 2023-05-11 19:26:26 +08:00
Lucas.Xu 54f757d9b2 bump version 0.1.5 (#2506)
* chore: bump version 0.1.5

* fix: could not trigger slash menu after inserting an emoji

* Revert "feat: add hover effect on an event card (#2487)"

This reverts commit f0a4b4b77d.

* feat: add hover effect on an event card

* fix: #2469 duplicated cover
2023-05-11 19:04:36 +08:00
Nathan.fooo cf27409c8d fix: number sort (#2507) 2023-05-11 18:35:36 +08:00
Lucas.Xu bd5250ae2f chore: update editor v0.1.12 and format the readme (#2489) 2023-05-10 15:01:15 +08:00
Yijing Huang f0a4b4b77d feat: add hover effect on an event card (#2487)
* chore: delete unncessary openCard method in RowCardContainer

* chore: delete unnessary code and add comment
2023-05-10 10:19:52 +08:00
Richard Shiue 151ee89855 fix: show delete icon for document icon properly (#2475) 2023-05-09 22:35:33 +08:00
Richard Shiue 2b7282195b feat: improve sidebar item dragged appearance (#2471) 2023-05-09 22:33:42 +08:00
Richard Shiue 4a1714b537 feat: select which properties to show in calendar (#2482) 2023-05-09 22:26:25 +08:00
Richard Shiue 6731037a8c fix: wrong day of week (#2468) 2023-05-08 09:51:20 +08:00
Annie 06f056aa4b chore: Update README.md
Update product screenshots
2023-05-07 15:47:45 +08:00
Lucas.Xu a2e9d63f63 chore: bump version 0.1.4 (#2455) 2023-05-04 20:05:23 +08:00
Richard Shiue 5d0a349236 fix: default include time (#2444)
* fix: default include time

* chore: clarify logic and add comments
2023-05-04 12:41:48 +08:00
Richard ShiueandNathan.fooo db8d030a85 feat: show unscheduled events in calendar toolbar (#2411)
* refactor: use same show row detail function

* fix: adjust popover offset

* feat: show unscheduled events in toolbar

* chore: apply suggestions from Xazin

* refactor: refactor list item into separate widget

---------

Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2023-05-04 11:05:17 +08:00
61 changed files with 1207 additions and 658 deletions

No files matched your search

+1 -1
View File
@@ -138,7 +138,7 @@ jobs:
job:
- {
target: x86_64-apple-darwin,
os: macos-10.15,
os: macos-11,
extra-build-args: "",
}
steps:
+27
View File
@@ -1,5 +1,32 @@
# 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
- Fix: calendar dates don't match with weekdays.
- Fix: sort numbers in Grid.
## Version 0.1.4 - 04/05/2023
### New features
- Use AppFlowy’s calendar views to plan and manage tasks and deadlines.
- Writing can be improved with the help of OpenAI.
## Version 0.1.3 - 24/04/2023
### New features
- Launch the official Dark Mode.
- Customize the font color and highlight color by setting a hex color value and an opacity level.
### Bug Fixes
- Fix: the slash menu can be triggered by all other keyboards than English.
- Fix: convert the single asterisk to italic text and the double asterisks to bold text.
## Version 0.1.2 - 03/28/2023
### Bug Fixes
+5 -3
View File
@@ -23,9 +23,11 @@ You are in charge of your data and customizations.
<a href="https://twitter.com/appflowy"><b>Twitter</b></a>
</p>
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/200787830-96be260b-d0a0-4152-864e-6730b19095cd.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/174753177-98e4c899-2356-4137-bb42-374bba2b127b.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/190650183-a940f1e0-a2c5-4797-ab3a-56758f6f696c.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
<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
+1 -1
View File
@@ -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.3"
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",
@@ -418,7 +423,9 @@
"showWeekNumbers": "Show week numbers",
"showWeekends": "Show weekends",
"firstDayOfWeek": "Start week on",
"layoutDateField": "Layout calendar by"
"layoutDateField": "Layout calendar by",
"noDateTitle": "No Date",
"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));
}
}
@@ -45,12 +45,7 @@ class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
Future<Option<FlowyError>> save(DateCellData data) {
var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
// This is a bit of a hack. This converts the data.date which is in
// UTC to Local but actually changes the timestamp instead of just
// changing the isUtc flag
final dateTime = DateTime(data.date.year, data.date.month, data.date.day);
final date = (dateTime.millisecondsSinceEpoch ~/ 1000).toString();
final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
payload.date = date;
payload.isUtc = data.date.isUtc;
payload.includeTime = data.includeTime;
@@ -51,5 +51,6 @@ class CalendarSettingEvent with _$CalendarSettingEvent {
}
enum CalendarSettingAction {
properties,
layout,
}
@@ -1,24 +1,23 @@
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/number_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/url_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../grid/presentation/layout/sizes.dart';
import '../../widgets/row/cells/select_option_cell/extension.dart';
import '../application/calendar_bloc.dart';
import 'calendar_page.dart';
class CalendarDayCard extends StatelessWidget {
final String viewId;
@@ -102,7 +101,7 @@ class CalendarDayCard extends StatelessWidget {
);
}
GestureDetector _buildCard(BuildContext context, CalendarDayEvent event) {
Widget _buildCard(BuildContext context, CalendarDayEvent event) {
final styles = <FieldType, CardCellStyle>{
FieldType.Number: NumberCardCellStyle(10),
FieldType.URL: URLCardCellStyle(10),
@@ -162,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(
@@ -193,7 +195,12 @@ class CalendarDayCard extends StatelessWidget {
cardData: event.dateFieldId,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) => _showRowDetailPage(event, context),
openCard: (context) => showEventDetails(
context: context,
event: event,
viewId: viewId,
rowCache: _rowCache,
),
styleConfiguration: const RowCardStyleConfiguration(
showAccessory: false,
cellPadding: EdgeInsets.zero,
@@ -203,44 +210,24 @@ class CalendarDayCard extends StatelessWidget {
onEndEditing: () {},
);
return GestureDetector(
onTap: () => _showRowDetailPage(event, context),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).dividerColor,
width: 1.5,
),
),
borderRadius: Corners.s6Border,
),
child: card,
),
return FlowyHover(
style: HoverStyle(
hoverColor: Theme.of(context).colorScheme.tertiaryContainer,
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
),
);
}
void _showRowDetailPage(CalendarDayEvent event, BuildContext context) {
final dataController = RowController(
rowId: event.eventId,
viewId: viewId,
rowCache: _rowCache,
);
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: _rowCache.cellCache,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).dividerColor,
width: 1.5,
),
),
dataController: dataController,
);
},
borderRadius: Corners.s6Border,
),
child: card,
),
);
}
@@ -9,6 +9,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../application/row/row_cache.dart';
import '../../application/row/row_data_controller.dart';
import '../../widgets/row/cell_builder.dart';
import '../../widgets/row/row_detail.dart';
@@ -76,7 +77,12 @@ class _CalendarPageState extends State<CalendarPage> {
listenWhen: (p, c) => p.editEvent != c.editEvent,
listener: (context, state) {
if (state.editEvent != null) {
_showEditEventPage(state.editEvent!.event!, context);
showEventDetails(
context: context,
event: state.editEvent!.event!,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
}
},
),
@@ -165,8 +171,9 @@ class _CalendarPageState extends State<CalendarPage> {
}
Widget _headerWeekDayBuilder(day) {
// incoming day starts from Monday, the symbols start from Sunday
final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
final weekDayString = symbols.WEEKDAYS[day];
final weekDayString = symbols.WEEKDAYS[(day + 1) % 7];
return Center(
child: Padding(
padding: CalendarSize.daysOfWeekInsets,
@@ -210,27 +217,32 @@ class _CalendarPageState extends State<CalendarPage> {
}
WeekDays _weekdayFromInt(int dayOfWeek) {
// MonthView places the first day of week on the second column for some reason.
return WeekDays.values[(dayOfWeek + 1) % 7];
}
void _showEditEventPage(CalendarDayEvent event, BuildContext context) {
final dataController = RowController(
rowId: event.eventId,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: _calendarBloc.rowCache.cellCache,
),
dataController: dataController,
);
},
);
// dayOfWeek starts from Sunday, WeekDays starts from Monday
return WeekDays.values[(dayOfWeek - 1) % 7];
}
}
void showEventDetails({
required BuildContext context,
required CalendarDayEvent event,
required String viewId,
required RowCache rowCache,
}) {
final dataController = RowController(
rowId: event.eventId,
viewId: viewId,
rowCache: rowCache,
);
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: rowCache.cellCache,
),
dataController: dataController,
);
},
);
}
@@ -217,6 +217,7 @@ class LayoutDateField extends StatelessWidget {
direction: PopoverDirection.leftWithTopAligned,
constraints: BoxConstraints.loose(const Size(300, 400)),
mutex: popoverMutex,
offset: const Offset(-16, 0),
popupBuilder: (context) {
return BlocProvider(
create: (context) => getIt<DatabasePropertyBloc>(
@@ -237,9 +238,9 @@ class LayoutDateField extends StatelessWidget {
onUpdated(fieldInfo.id);
popoverMutex.close();
},
leftIcon: svgWidget('grid/field/date'),
leftIcon: const FlowySvg(name: 'grid/field/date'),
rightIcon: fieldInfo.id == fieldId
? svgWidget('grid/checkmark')
? const FlowySvg(name: 'grid/checkmark')
: null,
),
);
@@ -333,12 +334,13 @@ class FirstDayOfWeek extends StatelessWidget {
direction: PopoverDirection.leftWithTopAligned,
constraints: BoxConstraints.loose(const Size(300, 400)),
mutex: popoverMutex,
offset: const Offset(-16, 0),
popupBuilder: (context) {
final symbols =
DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
// starts from sunday
final items = symbols.WEEKDAYS.asMap().entries.map((entry) {
final index = (entry.key - 1) % 7;
final index = entry.key;
final string = entry.value;
return SizedBox(
height: GridSize.popoverItemHeight,
@@ -348,8 +350,9 @@ class FirstDayOfWeek extends StatelessWidget {
onUpdated(index);
popoverMutex.close();
},
rightIcon:
firstDayOfWeek == index ? svgWidget('grid/checkmark') : null,
rightIcon: firstDayOfWeek == index
? const FlowySvg(name: 'grid/checkmark')
: null,
),
);
}).toList();
@@ -2,8 +2,10 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_property.dart';
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
@@ -38,6 +40,11 @@ class CalendarSetting extends StatelessWidget {
final CalendarSettingAction? action =
state.selectedAction.foldLeft(null, (previous, action) => action);
switch (action) {
case CalendarSettingAction.properties:
return GridPropertyList(
viewId: settingContext.viewId,
fieldController: settingContext.fieldController,
);
case CalendarSettingAction.layout:
return CalendarLayoutSetting(
onUpdated: onUpdated,
@@ -78,9 +85,16 @@ class AllCalendarSettings extends StatelessWidget {
}
Widget _settingItem(BuildContext context, CalendarSettingAction action) {
Widget? icon;
if (action.iconName() != null) {
icon = FlowySvg(
name: action.iconName()!,
);
}
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
leftIcon: icon,
text: FlowyText.medium(action.title()),
onTap: () {
context
@@ -93,8 +107,19 @@ class AllCalendarSettings extends StatelessWidget {
}
extension _SettingExtension on CalendarSettingAction {
String? iconName() {
switch (this) {
case CalendarSettingAction.properties:
return 'grid/setting/properties';
case CalendarSettingAction.layout:
return null;
}
}
String title() {
switch (this) {
case CalendarSettingAction.properties:
return LocaleKeys.grid_settings_Properties.tr();
case CalendarSettingAction.layout:
return LocaleKeys.grid_settings_layout.tr();
}
@@ -1,6 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -19,7 +21,8 @@ class CalendarToolbar extends StatelessWidget {
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
children: const [
_UnscheduleEventsButton(),
_SettingButton(),
],
),
@@ -28,25 +31,22 @@ class CalendarToolbar extends StatelessWidget {
}
class _SettingButton extends StatefulWidget {
const _SettingButton({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _SettingButtonState();
}
class _SettingButtonState extends State<_SettingButton> {
late PopoverController popoverController;
@override
void initState() {
popoverController = PopoverController();
super.initState();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
controller: popoverController,
direction: PopoverDirection.bottomWithRightAligned,
triggerActions: PopoverTriggerFlags.none,
constraints: BoxConstraints.loose(const Size(300, 400)),
margin: EdgeInsets.zero,
child: FlowyTextButton(
@@ -54,7 +54,6 @@ class _SettingButtonState extends State<_SettingButton> {
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
onPressed: () => popoverController.show(),
),
popupBuilder: (BuildContext popoverContext) {
final bloc = context.watch<CalendarBloc>();
@@ -81,3 +80,100 @@ class _SettingButtonState extends State<_SettingButton> {
);
}
}
class _UnscheduleEventsButton extends StatefulWidget {
const _UnscheduleEventsButton({Key? key}) : super(key: key);
@override
State<_UnscheduleEventsButton> createState() =>
_UnscheduleEventsButtonState();
}
class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> {
late final PopoverController _controller;
@override
void initState() {
super.initState();
_controller = PopoverController();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {
final unscheduledEvents = state.allEvents
.where((e) => e.date == DateTime.fromMillisecondsSinceEpoch(0))
.toList();
final viewId = context.read<CalendarBloc>().viewId;
final rowCache = context.read<CalendarBloc>().rowCache;
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _controller,
offset: const Offset(0, 8),
child: FlowyTextButton(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${unscheduledEvents.length})",
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
),
popupBuilder: (context) {
if (unscheduledEvents.isEmpty) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: Center(
child: FlowyText.medium(
LocaleKeys.calendar_settings_emptyNoDate.tr(),
color: Theme.of(context).hintColor,
),
),
);
}
return ListView.separated(
itemBuilder: (context, index) => _UnscheduledEventItem(
event: unscheduledEvents[index],
onPressed: () {
showEventDetails(
context: context,
event: unscheduledEvents[index].event!,
viewId: viewId,
rowCache: rowCache,
);
_controller.close();
},
),
itemCount: unscheduledEvents.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
},
);
},
);
}
}
class _UnscheduledEventItem extends StatelessWidget {
final CalendarEventData<CalendarDayEvent> event;
final VoidCallback onPressed;
const _UnscheduledEventItem({
required this.event,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyTextButton(
event.title,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
onPressed: onPressed,
),
);
}
}
@@ -43,6 +43,7 @@ class RowCardContainer extends StatelessWidget {
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => openCard(context),
child: Padding(
padding: const EdgeInsets.all(8),
@@ -84,15 +84,23 @@ class DateCellCalendarBloc
bool? includeTime,
}) {
final DateCellData newDateData = state.dateCellData.fold(
() => DateCellData(
date: date ?? DateTime.now(),
time: time,
includeTime: includeTime ?? false,
),
() {
DateTime newDate = DateTime.now();
if (date != null) {
newDate = _utcToLocalAddTime(date);
}
return DateCellData(
date: newDate,
time: time,
includeTime: includeTime ?? false,
);
},
(dateData) {
var newDateData = dateData;
if (date != null && !isSameDay(newDateData.date, date)) {
newDateData = newDateData.copyWith(date: date);
newDateData = newDateData.copyWith(
date: _utcToLocalAddTime(date),
);
}
if (newDateData.time != time) {
@@ -151,6 +159,21 @@ class DateCellCalendarBloc
);
}
DateTime _utcToLocalAddTime(DateTime date) {
final now = DateTime.now();
// the incoming date is Utc. this trick converts it into Local
// and add the current time, though
// the time may be overwritten by explicitly provided time string
return DateTime(
date.year,
date.month,
date.day,
now.hour,
now.minute,
now.second,
);
}
String timeFormatPrompt(FlowyError error) {
String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}.";
switch (state.dateTypeOptionPB.timeFormat) {
@@ -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,
],
);
}
@@ -226,6 +226,9 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
if (temporaryNodeTypes.contains(node.type)) {
transaction.deleteNode(node);
}
if (kCoverType == node.type && !node.path.equals([0])) {
transaction.deleteNode(node);
}
}
if (transaction.operations.isNotEmpty) {
await editorState.apply(transaction, withUpdateCursor: false);
@@ -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,11 +1,10 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
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/image.dart';
import 'package:flutter/material.dart';
class EmojiPopover extends StatefulWidget {
@@ -33,57 +32,53 @@ class _EmojiPopoverState extends State<EmojiPopover> {
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(15),
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
widget.onEmojiChanged(emoji);
},
customWidget: (Config config, EmojiViewState state) {
return Stack(
alignment: Alignment.topRight,
children: [
Container(
padding: EdgeInsets.only(top: widget.showRemoveButton ? 25 : 0),
child: DefaultEmojiPickerView(config, state),
),
_buildDeleteButtonIfNeed(),
],
);
},
config: Config(
columns: 8,
emojiSizeMax: 28,
bgColor: Colors.transparent,
iconColor: Theme.of(context).iconTheme.color!,
iconColorSelected: Theme.of(context).colorScheme.onSurface,
selectedHoverColor: Theme.of(context).colorScheme.secondary,
progressIndicatorColor: Theme.of(context).iconTheme.color!,
buttonMode: ButtonMode.CUPERTINO,
initCategory: Category.RECENT,
),
),
);
}
Widget _buildDeleteButtonIfNeed() {
if (!widget.showRemoveButton) {
return const SizedBox();
}
return FlowyButton(
onTap: () => widget.removeIcon(),
useIntrinsicWidth: true,
text: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
child: Column(
children: [
const FlowySvg(name: 'editor/delete'),
const SizedBox(
width: 5,
),
FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
if (widget.showRemoveButton)
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Align(
alignment: Alignment.centerRight,
child: DeleteButton(onPressed: widget.removeIcon),
),
),
Expanded(
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
widget.onEmojiChanged(emoji);
},
config: Config(
columns: 8,
emojiSizeMax: 28,
bgColor: Colors.transparent,
iconColor: Theme.of(context).iconTheme.color!,
iconColorSelected: Theme.of(context).colorScheme.onSurface,
selectedHoverColor: Theme.of(context).colorScheme.secondary,
progressIndicatorColor: Theme.of(context).iconTheme.color!,
buttonMode: ButtonMode.CUPERTINO,
initCategory: Category.RECENT,
),
),
),
],
),
);
}
}
class DeleteButton extends StatelessWidget {
final VoidCallback onPressed;
const DeleteButton({required this.onPressed, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyButton(
onTap: onPressed,
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
),
leftIcon: const FlowySvg(name: 'editor/delete'),
);
}
}
@@ -29,29 +29,32 @@ void _showEmojiSelectionMenu(
menuService.dismiss();
_emojiSelectionMenu?.remove();
_emojiSelectionMenu = OverlayEntry(builder: (context) {
return Positioned(
top: alignment == Alignment.bottomLeft ? offset.dy : null,
bottom: alignment == Alignment.topLeft ? offset.dy : null,
left: offset.dx,
child: Material(
child: EmojiSelectionMenu(
editorState: editorState,
onSubmitted: (text) {
// insert emoji
editorState.insertEmoji(text);
},
onExit: () {
_dismissEmojiSelectionMenu();
//close emoji panel
},
_emojiSelectionMenu = OverlayEntry(
builder: (context) {
return Positioned(
top: alignment == Alignment.bottomLeft ? offset.dy : null,
bottom: alignment == Alignment.topLeft ? offset.dy : null,
left: offset.dx,
child: Material(
child: EmojiSelectionMenu(
editorState: editorState,
onSubmitted: (text) {
// insert emoji
editorState.insertEmoji(text);
},
onExit: () {
_dismissEmojiSelectionMenu();
//close emoji panel
},
),
),
),
);
},);
);
},
);
Overlay.of(context).insert(_emojiSelectionMenu!);
_editorState = editorState;
editorState.service.selectionService.currentSelection
.addListener(_dismissEmojiSelectionMenu);
}
@@ -62,6 +65,7 @@ void _dismissEmojiSelectionMenu() {
_editorState?.service.selectionService.currentSelection
.removeListener(_dismissEmojiSelectionMenu);
_editorState?.service.keyboardService?.enable();
_editorState = null;
}
@@ -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);
}
}
@@ -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 = [];
@@ -61,6 +61,10 @@ class ViewSection extends StatelessWidget {
.add(ViewSectionEvent.moveView(oldIndex, index));
},
ignorePrimaryScrollController: true,
buildDraggableFeedback: (context, constraints, child) => ConstrainedBox(
constraints: constraints,
child: Material(color: Colors.transparent, child: child),
),
children: children,
);
}
@@ -149,6 +149,8 @@ class HomeMenu extends StatelessWidget {
),
);
},
proxyDecorator: (child, index, animation) =>
Material(color: Colors.transparent, child: child),
);
},
),
@@ -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,
@@ -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(),
),
);
},
);
},
),
),
],
),
);
}
}
@@ -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(
@@ -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();
}
@@ -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,
);
}
}
@@ -588,6 +588,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.macos;
PRODUCT_NAME = AppFlowy;
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -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":
@@ -132,6 +132,21 @@ class FlowyHoverContainer extends StatelessWidget {
width: style.borderWidth,
);
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final iconTheme = theme.iconTheme;
// override text's theme with foregroundColorOnHover when it is hovered
final hoverTheme = theme.copyWith(
textTheme: textTheme.copyWith(
bodyMedium: textTheme.bodyMedium?.copyWith(
color: style.foregroundColorOnHover ?? theme.colorScheme.onSurface,
),
),
iconTheme: iconTheme.copyWith(
color: style.foregroundColorOnHover ?? theme.colorScheme.onSurface,
),
);
return Container(
margin: style.contentMargin,
decoration: BoxDecoration(
@@ -139,19 +154,8 @@ class FlowyHoverContainer extends StatelessWidget {
color: style.hoverColor ?? Theme.of(context).colorScheme.secondary,
borderRadius: style.borderRadius,
),
child:
//override text's theme with foregroundColorOnHover when it is hovered
Theme(
data: Theme.of(context).copyWith(
textTheme: Theme.of(context).textTheme.copyWith(
bodyMedium: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: style.foregroundColorOnHover ??
Theme.of(context).colorScheme.onSurface),
),
iconTheme: Theme.of(context).iconTheme.copyWith(
color: style.foregroundColorOnHover ??
Theme.of(context).colorScheme.onSurface),
),
child: Theme(
data: hoverTheme,
child: child,
),
);
+2 -2
View File
@@ -45,10 +45,10 @@ packages:
dependency: "direct main"
description:
name: appflowy_editor
sha256: a1dbca3d7d33f4669f1d44bfa1e06b6646f46726c219921b8a59d9ee22bf252d
sha256: "6f7d2b0b54ca1049cb396229549d228b5bbd7ea6d09f1f7325a20db2d7586a5f"
url: "https://pub.dev"
source: hosted
version: "0.1.9"
version: "0.1.12"
appflowy_popover:
dependency: "direct main"
description:
+2 -2
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.1.2
version: 0.1.6
environment:
sdk: ">=2.19.0 <3.0.0"
@@ -42,7 +42,7 @@ dependencies:
git:
url: https://github.com/AppFlowy-IO/appflowy-board.git
ref: a183c57
appflowy_editor: ^0.1.9
appflowy_editor: ^0.1.12
appflowy_popover:
path: packages/appflowy_popover
+1
View File
@@ -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(&params.value).await?;
let content = CSVExport
.export_database(&params.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)
}
}
@@ -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,
@@ -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,
@@ -216,7 +216,17 @@ impl TypeOptionCellDataCompare for NumberTypeOptionPB {
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
) -> Ordering {
cell_data.0.cmp(&other_cell_data.0)
let left = NumberCellData::from_format_str(&cell_data.0, self.sign_positive, &self.format);
let right =
NumberCellData::from_format_str(&other_cell_data.0, self.sign_positive, &self.format);
match (left, right) {
(Ok(left), Ok(right)) => {
return left.decimal().cmp(&right.decimal());
},
(Ok(_), Err(_)) => Ordering::Greater,
(Err(_), Ok(_)) => Ordering::Less,
(Err(_), Err(_)) => Ordering::Equal,
}
}
}
impl std::default::Default for NumberTypeOptionPB {
@@ -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);
}
}
@@ -161,7 +161,7 @@ pub fn make_test_grid() -> BuildDatabaseContext {
for field_type in FieldType::iter() {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("DA"),
FieldType::Number => row_builder.insert_number_cell("4"),
FieldType::Number => row_builder.insert_number_cell("14"),
FieldType::DateTime => row_builder.insert_date_cell("1668704685"),
FieldType::SingleSelect => {
row_builder.insert_single_select_cell(|mut options| options.remove(0))
@@ -9,4 +9,5 @@ mod layout_test;
mod snapshot_test;
mod sort_test;
mod export_test;
mod mock_data;
@@ -212,7 +212,7 @@ async fn sort_number_by_descending_test() {
let scripts = vec![
AssertCellContentOrder {
field_id: number_field.id.clone(),
orders: vec!["$1", "$2", "$3", "$4", "", "$5"],
orders: vec!["$1", "$2", "$3", "$14", "", "$5"],
},
InsertSort {
field_rev: number_field.clone(),
@@ -220,7 +220,7 @@ async fn sort_number_by_descending_test() {
},
AssertCellContentOrder {
field_id: number_field.id.clone(),
orders: vec!["$5", "$4", "$3", "$2", "$1", ""],
orders: vec!["$14", "$5", "$3", "$2", "$1", ""],
},
];
test.run_scripts(scripts).await;
@@ -2,175 +2,78 @@
"document": {
"type": "editor",
"children": [
{ "type": "cover" },
{
"type": "text",
"attributes": {
"subtype": "heading",
"heading": "h1"
},
"attributes": { "subtype": "heading", "heading": "h1" },
"delta": [{ "insert": "Welcome to AppFlowy!" }]
},
{
"type": "text",
"attributes": { "subtype": "heading", "heading": "h2" },
"delta": [{ "insert": "Here are the basics" }]
},
{
"type": "text",
"attributes": { "subtype": "checkbox", "checkbox": null },
"delta": [{ "insert": "Click anywhere and just start typing." }]
},
{
"type": "text",
"attributes": { "subtype": "checkbox", "checkbox": false },
"delta": [
{
"insert": "🌟 Welcome to AppFlowy!"
}
"insert": "Highlight ",
"attributes": { "backgroundColor": "0x4dffeb3b" }
},
{ "insert": "any text, and use the editing menu to " },
{ "insert": "style", "attributes": { "italic": true } },
{ "insert": " " },
{ "insert": "your", "attributes": { "bold": true } },
{ "insert": " " },
{ "insert": "writing", "attributes": { "underline": true } },
{ "insert": " " },
{ "insert": "however", "attributes": { "code": true } },
{ "insert": " you " },
{ "insert": "like.", "attributes": { "strikethrough": true } }
]
},
{
"type": "text",
"attributes": {
"subtype": "heading",
"heading": "h2"
},
"attributes": { "subtype": "checkbox", "checkbox": null },
"delta": [
{
"insert": "Here are the basics"
}
]
},
{
"type": "text",
"attributes": {
"subtype": "checkbox",
"checkbox": null
},
"delta": [
{
"insert": "Click anywhere and just start typing."
}
]
},
{
"type": "text",
"attributes": {
"subtype": "checkbox",
"checkbox": null
},
"delta": [
{
"insert": "Highlight",
"attributes": {
"backgroundColor": "0x6000BCF0"
}
},
{
"insert": " any text, and use the editing menu to "
},
{
"insert": "style",
"attributes": {
"italic": true
}
},
{
"insert": " "
},
{
"insert": "your",
"attributes": {
"bold": true
}
},
{
"insert": " "
},
{
"insert": "writing",
"attributes": {
"underline": true
}
},
{
"insert": " "
},
{
"insert": "however",
"attributes": {
"code": true
}
},
{
"insert": " you "
},
{
"insert": "like.",
"attributes": {
"strikethrough": true
}
}
]
},
{
"type": "text",
"attributes": {
"subtype": "checkbox",
"checkbox": null
},
"delta": [
{
"insert": "As soon as you type "
},
{ "insert": "As soon as you type " },
{
"insert": "/",
"attributes": {
"code": true
}
"attributes": { "code": true, "color": "0xff00b5ff" }
},
{ "insert": " a menu will pop up. Select " },
{
"insert": " a menu will pop up. Select different types of content blocks you can add."
}
"insert": "different types",
"attributes": { "backgroundColor": "0x4d9c27b0" }
},
{ "insert": " of content blocks you can add." }
]
},
{
"type": "text",
"attributes": {
"subtype": "checkbox",
"checkbox": null
},
"attributes": { "subtype": "checkbox", "checkbox": null },
"delta": [
{
"insert": "Type "
},
{
"insert": "/",
"attributes": {
"code": true
}
},
{
"insert": " followed by "
},
{
"insert": "/bullet",
"attributes": {
"code": true
}
},
{
"insert": " or "
},
{
"insert": "/c.",
"attributes": {
"code": true
}
}
{ "insert": "Type " },
{ "insert": "/", "attributes": { "code": true } },
{ "insert": " followed by " },
{ "insert": "/bullet", "attributes": { "code": true } },
{ "insert": " or " },
{ "insert": "/num", "attributes": { "code": true } },
{ "insert": " to create a list.", "attributes": { "code": false } }
]
},
{
"type": "text",
"attributes": {
"subtype": "checkbox",
"checkbox": true
},
"attributes": { "subtype": "checkbox", "checkbox": true },
"delta": [
{
"insert": "Click "
},
{
"insert": "+ New Page ",
"attributes": {
"code": true
}
},
{ "insert": "Click " },
{ "insert": "+ New Page ", "attributes": { "code": true } },
{
"insert": "button at the bottom of your sidebar to add a new page."
}
@@ -178,32 +81,24 @@
},
{
"type": "text",
"attributes": {
"subtype": "checkbox",
"checkbox": null
},
"attributes": { "subtype": "checkbox", "checkbox": null },
"delta": [
{
"insert": "Click "
},
{
"insert": "+",
"attributes": {
"code": true
}
},
{
"insert": " next to any page title in the sidebar to quickly add a new subpage."
}
{ "insert": "Click " },
{ "insert": "+", "attributes": { "code": true } },
{ "insert": " next to any page title in the sidebar to " },
{ "insert": "quickly", "attributes": { "color": "0xff8427e0" } },
{ "insert": " add a new subpage, " },
{ "insert": "Document", "attributes": { "code": true } },
{ "insert": ", ", "attributes": { "code": false } },
{ "insert": "Grid", "attributes": { "code": true } },
{ "insert": ", or ", "attributes": { "code": false } },
{ "insert": "Kanban Board", "attributes": { "code": true } },
{ "insert": ".", "attributes": { "code": false } }
]
},
{
"type": "text",
"attributes": {
"checkbox": null
},
"delta": []
},
{ "type": "text", "delta": [] },
{ "type": "divider" },
{ "type": "text", "attributes": { "checkbox": null }, "delta": [] },
{
"type": "text",
"attributes": {
@@ -211,11 +106,7 @@
"checkbox": null,
"heading": "h2"
},
"delta": [
{
"insert": "Markdown"
}
]
"delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }]
},
{
"type": "text",
@@ -225,98 +116,64 @@
"heading": null
},
"delta": [
{ "insert": "Keyboard shortcuts " },
{
"insert": "Heading "
}
]
},
{
"type": "text",
"attributes": {
"subtype": "number-list",
"number": 2
},
"delta": [
{
"insert": "bold text",
"insert": "guide",
"attributes": {
"bold": true,
"defaultFormatting": true
}
}
]
},
{
"type": "text",
"attributes": {
"subtype": "number-list",
"number": 3
},
"delta": [
{
"insert": "italicized text",
"attributes": {
"italic": true
}
}
]
},
{
"type": "text",
"attributes": {
"subtype": "number-list",
"number": 4,
"number-list": null
},
"delta": [
{
"insert": "Ordered List"
}
]
},
{
"type": "text",
"attributes": {
"number": 5,
"subtype": "number-list"
},
"delta": [
{
"insert": "code",
"attributes": {
"code": true
}
}
]
},
{
"type": "text",
"attributes": {
"number": 6,
"subtype": "number-list"
},
"delta": [
{
"insert": "Strikethrough",
"attributes": {
"strikethrough": true
"href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts"
}
},
{ "retain": 1, "attributes": { "strikethrough": true } }
]
},
{
"type": "text",
"attributes": {
"subtype": "number-list",
"number": 2,
"heading": null
},
"delta": [
{ "insert": "Markdown " },
{
"retain": 1,
"insert": "reference",
"attributes": {
"strikethrough": true
"href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown"
}
},
{ "retain": 1, "attributes": { "strikethrough": true } }
]
},
{
"type": "text",
"attributes": { "number": 3, "subtype": "number-list" },
"delta": [
{ "insert": "Type " },
{ "insert": "/code", "attributes": { "code": true } },
{
"insert": " to insert a code block",
"attributes": { "code": false }
}
]
},
{
"type": "text",
"attributes": {
"checkbox": null
"subtype": "code_block",
"number": 3,
"heading": null,
"number-list": null,
"theme": "vs",
"language": "rust"
},
"delta": []
"delta": [
{
"insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}"
},
{ "retain": 1, "attributes": { "strikethrough": true } }
]
},
{ "type": "text", "attributes": { "checkbox": null }, "delta": [] },
{
"type": "text",
"attributes": {
@@ -324,94 +181,72 @@
"checkbox": null,
"heading": "h2"
},
"delta": [
{
"insert": "Have a question?"
}
]
"delta": [{ "insert": "Have a question❓" }]
},
{
"type": "text",
"attributes": {
"subtype": "quote"
},
"attributes": { "subtype": "quote" },
"delta": [
{
"insert": "Click "
},
{
"insert": "?",
"attributes": {
"code": true
}
},
{
"insert": " at the bottom right for help and support."
}
{ "insert": "Click " },
{ "insert": "?", "attributes": { "code": true } },
{ "insert": " at the bottom right for help and support." }
]
},
{ "type": "text", "delta": [] },
{
"type": "callout",
"children": [
{ "type": "text", "delta": [] },
{
"type": "text",
"attributes": { "subtype": "heading", "heading": "h2" },
"delta": [{ "insert": "Like AppFlowy? Follow us:" }]
},
{
"type": "text",
"attributes": { "subtype": "bulleted-list" },
"delta": [
{
"insert": "GitHub",
"attributes": {
"href": "https://github.com/AppFlowy-IO/AppFlowy"
}
}
]
},
{
"type": "text",
"attributes": { "subtype": "bulleted-list" },
"delta": [
{
"insert": "Twitter",
"attributes": { "href": "https://twitter.com/appflowy" }
},
{ "insert": ": @appflowy" }
]
},
{
"type": "text",
"attributes": { "subtype": "bulleted-list" },
"delta": [
{
"insert": "Newsletter",
"attributes": { "href": "https://blog-appflowy.ghost.io/" }
}
]
}
],
"attributes": { "emoji": "🥰" }
},
{ "type": "text", "delta": [] },
{
"type": "text",
"attributes": { "subtype": null, "heading": null },
"delta": []
},
{
"type": "text",
"attributes": {
"subtype": "heading",
"heading": "h2"
},
"delta": [
{
"insert": "Like AppFlowy? Follow us:"
}
]
},
{
"type": "text",
"attributes": {
"subtype": "bulleted-list",
"quote": null
},
"delta": [
{
"insert": "GitHub",
"attributes": {
"href": "https://github.com/AppFlowy-IO/AppFlowy"
}
}
]
},
{
"type": "text",
"attributes": {
"subtype": "bulleted-list"
},
"delta": [
{
"insert": "Twitter: @appflowy"
}
]
},
{
"type": "text",
"attributes": {
"subtype": "bulleted-list"
},
"delta": [
{
"insert": "Newsletter",
"attributes": {
"href": "https://blog-appflowy.ghost.io/"
}
}
]
},
{
"type": "text",
"attributes": {
"subtype": null,
"heading": null
},
"attributes": { "subtype": null, "heading": null },
"delta": []
}
]
@@ -23,6 +23,11 @@ args[1]: The appflowy version to be built (github ref_name).
assert(await repositoryRoot.exists(),
'$repositoryRoot is an invalid directory. Please try again with a valid directory.\n\n$help');
final appVersion = args[1];
await _BuildTool(repositoryRoot: repositoryRoot.path, appVersion: appVersion)
.run();
String? arch;
if (args.length > 2) arch = args[2];
await _BuildTool(
repositoryRoot: repositoryRoot.path,
appVersion: appVersion,
arch: arch,
).run();
}
@@ -14,10 +14,12 @@ class _BuildTool {
const _BuildTool({
required this.repositoryRoot,
required this.appVersion,
this.arch,
});
final String repositoryRoot;
final String appVersion;
final String? arch;
String get projectRoot =>
[repositoryRoot, 'appflowy_flutter'].join(Platform.pathSeparator);
@@ -30,8 +32,9 @@ class _BuildTool {
Future<String> get _commandForOS async {
// Check the operating system and CPU architecture
var os = Platform.operatingSystem;
var arch = Platform.isMacOS ? await _architecture : Platform.localHostname;
final os = Platform.operatingSystem;
final arch = this.arch ??
(Platform.isMacOS ? await _architecture : Platform.localHostname);
// Determine the appropriate command based on the OS and architecture
if (os == 'windows') {
@@ -1,6 +1,7 @@
[Setup]
AppName=AppFlowy
AppVersion={#AppVersion}
AppPublisher=AppFlowy-IO
WizardStyle=modern
Compression=lzma2
SolidCompression=yes
@@ -9,8 +10,8 @@ DefaultGroupName=AppFlowy
SetupIconFile=flowy_logo.ico
UninstallDisplayIcon={app}\AppFlowy.exe
UninstallDisplayName=AppFlowy
AppPublisher=AppFlowy-IO
VersionInfoVersion={#AppVersion}
UsePreviousAppDir=no
[Files]
Source: "AppFlowy\AppFlowy.exe";DestDir: "{app}";DestName: "AppFlowy.exe"
@@ -18,4 +19,5 @@ Source: "AppFlowy\*";DestDir: "{app}"
Source: "AppFlowy\data\*";DestDir: "{app}\data\"; Flags: recursesubdirs
[Icons]
Name: "{group}\AppFlowy";Filename: "{app}\AppFlowy.exe"
Name: "{userdesktop}\AppFlowy"; Filename: "{app}\AppFlowy.exe"
Name: "{group}\AppFlowy"; Filename: "{app}\AppFlowy.exe"
+14 -26
View File
@@ -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()]
};
}