From e73870e6e26e1362e2a32b5ba8199f45f97b0d73 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 8 Mar 2023 08:58:28 +0800 Subject: [PATCH 01/15] fix: auto generator bugs (#1934) --- .../plugins/openai/service/openai_client.dart | 81 +++++++++++++++++++ .../openai/service/text_completion.dart | 2 +- .../plugins/openai/util/editor_extension.dart | 20 ++++- .../widgets/auto_completion_node_widget.dart | 56 +++++++------ .../lib/src/service/keyboard_service.dart | 12 ++- 5 files changed, 139 insertions(+), 32 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart index 37a5279662..9c6f3339fc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/openai_client.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; import 'text_completion.dart'; import 'package:dartz/dartz.dart'; @@ -41,6 +43,17 @@ abstract class OpenAIRepository { double temperature = .3, }); + Future getStreamedCompletions({ + required String prompt, + required Future Function() onStart, + required Future Function(TextCompletionResponse response) onProcess, + required VoidCallback onEnd, + required void Function(OpenAIError error) onError, + String? suffix, + int maxTokens = 500, + double temperature = 0.3, + }); + /// Get edits from GPT-3 /// /// [input] is the input text @@ -103,6 +116,74 @@ class HttpOpenAIRepository implements OpenAIRepository { } } + @override + Future getStreamedCompletions({ + required String prompt, + required Future Function() onStart, + required Future Function(TextCompletionResponse response) onProcess, + required VoidCallback onEnd, + required void Function(OpenAIError error) onError, + String? suffix, + int maxTokens = 500, + double temperature = 0.3, + }) async { + final parameters = { + 'model': 'text-davinci-003', + 'prompt': prompt, + 'suffix': suffix, + 'max_tokens': maxTokens, + 'temperature': temperature, + 'stream': true, + }; + + final request = http.Request('POST', OpenAIRequestType.textCompletion.uri); + request.headers.addAll(headers); + request.body = jsonEncode(parameters); + + final response = await client.send(request); + + // NEED TO REFACTOR. + // WHY OPENAI USE TWO LINES TO INDICATE THE START OF THE STREAMING RESPONSE? + // AND WHY OPENAI USE [DONE] TO INDICATE THE END OF THE STREAMING RESPONSE? + int syntax = 0; + var previousSyntax = ''; + if (response.statusCode == 200) { + await for (final chunk in response.stream + .transform(const Utf8Decoder()) + .transform(const LineSplitter())) { + syntax += 1; + if (syntax == 3) { + await onStart(); + continue; + } else if (syntax < 3) { + continue; + } + final data = chunk.trim().split('data: '); + if (data.length > 1 && data[1] != '[DONE]') { + final response = TextCompletionResponse.fromJson( + json.decode(data[1]), + ); + if (response.choices.isNotEmpty) { + final text = response.choices.first.text; + if (text == previousSyntax && text == '\n') { + continue; + } + await onProcess(response); + previousSyntax = response.choices.first.text; + Log.editor.info(response.choices.first.text); + } + } else { + onEnd(); + } + } + } else { + final body = await response.stream.bytesToString(); + onError( + OpenAIError.fromJson(json.decode(body)['error']), + ); + } + } + @override Future> getEdits({ required String input, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/text_completion.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/text_completion.dart index b2c2a55cc6..067049adbf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/text_completion.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/service/text_completion.dart @@ -8,7 +8,7 @@ class TextCompletionChoice with _$TextCompletionChoice { required String text, required int index, // ignore: invalid_annotation_target - @JsonKey(name: 'finish_reason') required String finishReason, + @JsonKey(name: 'finish_reason') String? finishReason, }) = _TextCompletionChoice; factory TextCompletionChoice.fromJson(Map json) => diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/editor_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/editor_extension.dart index fd27c65cef..09238cabac 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/editor_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/util/editor_extension.dart @@ -11,6 +11,10 @@ extension TextRobot on EditorState { TextRobotInputType inputType = TextRobotInputType.word, Duration delay = const Duration(milliseconds: 10), }) async { + if (text == '\n') { + await insertNewLineAtCurrentSelection(); + return; + } final lines = text.split('\n'); for (final line in lines) { if (line.isEmpty) { @@ -28,13 +32,21 @@ extension TextRobot on EditorState { } break; case TextRobotInputType.word: - final words = line.split(' ').map((e) => '$e '); - for (final word in words) { + final words = line.split(' '); + if (words.length == 1 || + (words.length == 2 && + (words.first.isEmpty || words.last.isEmpty))) { await insertTextAtCurrentSelection( - word, + line, ); - await Future.delayed(delay, () {}); + } else { + for (final word in words.map((e) => '$e ')) { + await insertTextAtCurrentSelection( + word, + ); + } } + await Future.delayed(delay, () {}); break; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart index acbad03ea4..616192524b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart @@ -61,19 +61,14 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> { void initState() { super.initState(); - focusNode.addListener(() { - if (focusNode.hasFocus) { - widget.editorState.service.selectionService.clearSelection(); - } else { - widget.editorState.service.keyboardService?.enable(); - } - }); + textFieldFocusNode.addListener(_onFocusChanged); textFieldFocusNode.requestFocus(); } @override void dispose() { controller.dispose(); + textFieldFocusNode.removeListener(_onFocusChanged); super.dispose(); } @@ -242,30 +237,33 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> { loading.start(); await _updateEditingText(); final result = await UserBackendService.getCurrentUserProfile(); + result.fold((userProfile) async { final openAIRepository = HttpOpenAIRepository( client: http.Client(), apiKey: userProfile.openaiKey, ); - final completions = await openAIRepository.getCompletions( + await openAIRepository.getStreamedCompletions( prompt: controller.text, + onStart: () async { + loading.stop(); + await _makeSurePreviousNodeIsEmptyTextNode(); + }, + onProcess: (response) async { + if (response.choices.isNotEmpty) { + final text = response.choices.first.text; + await widget.editorState.autoInsertText( + text, + inputType: TextRobotInputType.word, + ); + } + }, + onEnd: () {}, + onError: (error) async { + loading.stop(); + await _showError(error.message); + }, ); - completions.fold((error) async { - loading.stop(); - await _showError(error.message); - }, (textCompletion) async { - loading.stop(); - await _makeSurePreviousNodeIsEmptyTextNode(); - // Open AI result uses two '\n' as the begin syntax. - var texts = textCompletion.choices.first.text.split('\n'); - if (texts.length > 2) { - texts.removeRange(0, 2); - await widget.editorState.autoInsertText( - texts.join('\n'), - ); - } - focusNode.requestFocus(); - }); }, (error) async { loading.stop(); await _showError( @@ -345,4 +343,14 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> { ), ); } + + void _onFocusChanged() { + if (textFieldFocusNode.hasFocus) { + widget.editorState.service.keyboardService?.disable( + disposition: UnfocusDisposition.previouslyFocusedChild, + ); + } else { + widget.editorState.service.keyboardService?.enable(); + } + } } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart index fad31c711e..a25b7a6f1e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart @@ -35,7 +35,10 @@ abstract class AppFlowyKeyboardService { /// you can disable the keyboard service of flowy_editor. /// But you need to call the `enable` function to restore after exiting /// your custom component, otherwise the keyboard service will fails. - void disable({bool showCursor = false}); + void disable({ + bool showCursor = false, + UnfocusDisposition disposition = UnfocusDisposition.scope, + }); } /// Process keyboard events @@ -102,10 +105,13 @@ class _AppFlowyKeyboardState extends State } @override - void disable({bool showCursor = false}) { + void disable({ + bool showCursor = false, + UnfocusDisposition disposition = UnfocusDisposition.scope, + }) { isFocus = false; this.showCursor = showCursor; - _focusNode.unfocus(); + _focusNode.unfocus(disposition: disposition); } @override From 90da54d12f97b04810ecc14c467a60217dcaf1a9 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 8 Mar 2023 10:59:52 +0800 Subject: [PATCH 02/15] feat: integrate database controller (tauri) * feat: using controllers in react hooks WIP (#1915) * chore: add edit / create field test * chore: add delete field test * chore: change log class arguments * chore: delete/create row * chore: set tracing log to debug level * fix: filter notification with id * chore: add get single select type option data * fix: high cpu usage * chore: format code * chore: update tokio version * chore: config tokio runtime subscriber * chore: add profiling feature * chore: setup auto login * chore: fix tauri build * chore: (unstable) using controllers * fix: initially authenticated and serializable fix * fix: ci warning * ci: compile error * fix: new folder trash overflow * fix: min width for nav panel * fix: nav panel and main panel animation on hide menu * fix: highlight active page * fix: post merge fixes * fix: post merge fix * fix: remove warnings * fix: change IDatabaseField fix eslint errors * chore: create cell component for each field type * chore: move cell hook into custom cell component * chore: refactor row hook * chore: add tauri clean --------- Co-authored-by: nathan Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com> * ci: fix wanrings --------- Co-authored-by: Askarbek Zadauly --- frontend/appflowy_tauri/package.json | 2 + .../components/_shared/Database.hooks.ts | 13 +- .../_shared/database-hooks/loadField.ts | 108 ++++++++ .../_shared/database-hooks/useCell.ts | 32 +++ .../_shared/database-hooks/useDatabase.ts | 68 +++++ .../_shared/database-hooks/useRow.ts | 43 +++ .../components/auth/ProtectedRoutes.tsx | 1 + .../components/board/Board.hooks.ts | 24 +- .../appflowy_app/components/board/Board.tsx | 17 +- .../components/board/BoardBlock.tsx | 29 +- .../{BoardBlockItem.tsx => BoardCard.tsx} | 58 ++-- .../components/board/BoardCell.tsx | 43 +++ .../components/board/BoardDateCell.tsx | 18 ++ .../components/board/BoardOptionsCell.tsx | 25 ++ .../components/board/BoardTextCell.tsx | 18 ++ .../layout/HeaderPanel/Breadcrumbs.tsx | 10 +- .../layout/HeaderPanel/HeaderPanel.tsx | 4 +- .../components/layout/MainPanel.tsx | 37 ++- .../NavigationPanel/NavigationPanel.hooks.ts | 18 +- .../NavigationPanel/NavigationPanel.tsx | 24 +- .../NavigationPanel/NavigationResizer.tsx | 8 +- .../layout/NavigationPanel/PageItem.hooks.ts | 4 +- .../layout/NavigationPanel/PageItem.tsx | 5 +- .../appflowy_app/components/layout/Screen.tsx | 33 +-- .../components/tests/DatabaseTestHelper.ts | 2 +- .../effects/database/cell/cell_controller.ts | 2 +- .../effects/folder/notifications/observer.ts | 3 +- .../stores/reducers/activePageId/slice.ts | 12 + .../stores/reducers/board/slice.ts | 2 +- .../stores/reducers/database/slice.ts | 261 ++++-------------- .../src/appflowy_app/stores/store.ts | 2 + .../src/appflowy_app/utils/log.ts | 21 +- .../src/appflowy_app/views/BoardPage.tsx | 10 +- frontend/rust-lib/flowy-core/src/lib.rs | 1 - 34 files changed, 616 insertions(+), 342 deletions(-) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useDatabase.ts create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useRow.ts rename frontend/appflowy_tauri/src/appflowy_app/components/board/{BoardBlockItem.tsx => BoardCard.tsx} (67%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/board/BoardDateCell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/board/BoardOptionsCell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/board/BoardTextCell.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/stores/reducers/activePageId/slice.ts diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index 18fb005b5e..e7511d6e1d 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -9,7 +9,9 @@ "preview": "vite preview", "format": "prettier --write .", "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .", + "test:errors": "eslint --quiet --ext .js,.ts,.tsx .", "test:prettier": "yarn prettier --list-different src", + "tauri:clean": "cargo make --cwd .. tauri_clean", "tauri:dev": "tauri dev", "test": "jest" }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts index d5a4345c92..50de845116 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Database.hooks.ts @@ -9,7 +9,7 @@ export const useDatabase = () => { const database = useAppSelector((state) => state.database); const newField = () => { - dispatch( + /* dispatch( databaseActions.addField({ field: { fieldId: nanoid(8), @@ -18,22 +18,25 @@ export const useDatabase = () => { title: 'new field', }, }) - ); + );*/ + console.log('depreciated'); }; const renameField = (fieldId: string, newTitle: string) => { - const field = database.fields[fieldId]; + /* const field = database.fields[fieldId]; field.title = newTitle; dispatch( databaseActions.updateField({ field, }) - ); + );*/ + console.log('depreciated'); }; const newRow = () => { - dispatch(databaseActions.addRow()); + // dispatch(databaseActions.addRow()); + console.log('depreciated'); }; return { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts new file mode 100644 index 0000000000..459794f455 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts @@ -0,0 +1,108 @@ +import { TypeOptionController } from '../../../stores/effects/database/field/type_option/type_option_controller'; +import { Some } from 'ts-results'; +import { IDatabaseField, ISelectOption } from '../../../stores/reducers/database/slice'; +import { + ChecklistTypeOptionPB, + DateFormat, + FieldType, + MultiSelectTypeOptionPB, + NumberFormat, + SingleSelectTypeOptionPB, + TimeFormat, +} from '../../../../services/backend'; +import { + makeChecklistTypeOptionContext, + makeDateTypeOptionContext, + makeMultiSelectTypeOptionContext, + makeNumberTypeOptionContext, + makeSingleSelectTypeOptionContext, +} from '../../../stores/effects/database/field/type_option/type_option_context'; +import { boardActions } from '../../../stores/reducers/board/slice'; +import { FieldInfo } from '../../../stores/effects/database/field/field_controller'; +import { AppDispatch } from '../../../stores/store'; + +export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?: AppDispatch): Promise { + const field = fieldInfo.field; + const typeOptionController = new TypeOptionController(viewId, Some(fieldInfo)); + + // temporary hack to set grouping field + let groupingFieldSelected = false; + + switch (field.field_type) { + case FieldType.SingleSelect: + case FieldType.MultiSelect: + case FieldType.Checklist: { + let selectOptions: ISelectOption[] = []; + let typeOption: SingleSelectTypeOptionPB | MultiSelectTypeOptionPB | ChecklistTypeOptionPB | undefined; + + if (field.field_type === FieldType.SingleSelect) { + typeOption = (await makeSingleSelectTypeOptionContext(typeOptionController).getTypeOption()).unwrap(); + if (!groupingFieldSelected) { + if (dispatch) { + dispatch(boardActions.setGroupingFieldId({ fieldId: field.id })); + } + groupingFieldSelected = true; + } + } + if (field.field_type === FieldType.MultiSelect) { + typeOption = (await makeMultiSelectTypeOptionContext(typeOptionController).getTypeOption()).unwrap(); + } + if (field.field_type === FieldType.Checklist) { + typeOption = (await makeChecklistTypeOptionContext(typeOptionController).getTypeOption()).unwrap(); + } + + if (typeOption) { + selectOptions = typeOption.options.map((option) => { + return { + selectOptionId: option.id, + title: option.name, + color: option.color, + }; + }); + } + + return { + fieldId: field.id, + title: field.name, + fieldType: field.field_type, + fieldOptions: { + selectOptions, + }, + }; + } + + case FieldType.Number: { + const typeOption = (await makeNumberTypeOptionContext(typeOptionController).getTypeOption()).unwrap(); + return { + fieldId: field.id, + title: field.name, + fieldType: field.field_type, + fieldOptions: { + numberFormat: typeOption.format, + }, + }; + } + + case FieldType.DateTime: { + const typeOption = (await makeDateTypeOptionContext(typeOptionController).getTypeOption()).unwrap(); + return { + fieldId: field.id, + title: field.name, + fieldType: field.field_type, + fieldOptions: { + dateFormat: typeOption.date_format, + timeFormat: typeOption.time_format, + includeTime: typeOption.include_time, + }, + }; + } + + default: { + return { + fieldId: field.id, + title: field.name, + fieldType: field.field_type, + }; + } + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts new file mode 100644 index 0000000000..57082f6f70 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts @@ -0,0 +1,32 @@ +import { CellIdentifier } from '../../../stores/effects/database/cell/cell_bd_svc'; +import { CellCache } from '../../../stores/effects/database/cell/cell_cache'; +import { FieldController } from '../../../stores/effects/database/field/field_controller'; +import { CellControllerBuilder } from '../../../stores/effects/database/cell/controller_builder'; +import { DateCellDataPB, SelectOptionCellDataPB, URLCellDataPB } from '../../../../services/backend'; +import { useEffect, useState } from 'react'; + +export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fieldController: FieldController) => { + const [data, setData] = useState(); + + useEffect(() => { + const builder = new CellControllerBuilder(cellIdentifier, cellCache, fieldController); + const cellController = builder.build(); + cellController.subscribeChanged({ + onCellChanged: (value) => { + setData(value.unwrap()); + }, + }); + + // ignore the return value, because we are using the subscription + void cellController.getCellData(); + + // dispose the cell controller when the component is unmounted + return () => { + void cellController.dispose(); + }; + }, []); + + return { + data, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useDatabase.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useDatabase.ts new file mode 100644 index 0000000000..b3fc047ed5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useDatabase.ts @@ -0,0 +1,68 @@ +import { useEffect, useState } from 'react'; +import { DatabaseController } from '../../../stores/effects/database/database_controller'; +import { + databaseActions, + DatabaseFieldMap, + IDatabaseColumn, + IDatabaseRow, +} from '../../../stores/reducers/database/slice'; +import { useAppDispatch, useAppSelector } from '../../../stores/store'; +import loadField from './loadField'; +import { FieldInfo } from '../../../stores/effects/database/field/field_controller'; +import { RowInfo } from '../../../stores/effects/database/row/row_cache'; + +export const useDatabase = (viewId: string) => { + const dispatch = useAppDispatch(); + const databaseStore = useAppSelector((state) => state.database); + const boardStore = useAppSelector((state) => state.board); + const [controller, setController] = useState(); + const [rows, setRows] = useState([]); + + useEffect(() => { + if (!viewId.length) return; + const c = new DatabaseController(viewId); + setController(c); + + // on unmount dispose the controller + return () => void c.dispose(); + }, [viewId]); + + const loadFields = async (fieldInfos: readonly FieldInfo[]) => { + const fields: DatabaseFieldMap = {}; + const columns: IDatabaseColumn[] = []; + + for (const fieldInfo of fieldInfos) { + const fieldPB = fieldInfo.field; + columns.push({ + fieldId: fieldPB.id, + sort: 'none', + visible: fieldPB.visibility, + }); + + const field = await loadField(viewId, fieldInfo, dispatch); + fields[field.fieldId] = field; + } + + dispatch(databaseActions.updateFields({ fields })); + dispatch(databaseActions.updateColumns({ columns })); + console.log(fields, columns); + }; + + useEffect(() => { + if (!controller) return; + + void (async () => { + controller.subscribe({ + onRowsChanged: (rowInfos) => { + setRows(rowInfos); + }, + onFieldsChanged: (fieldInfos) => { + void loadFields(fieldInfos); + }, + }); + await controller.open(); + })(); + }, [controller]); + + return { loadFields, controller, rows }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useRow.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useRow.ts new file mode 100644 index 0000000000..659596612d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useRow.ts @@ -0,0 +1,43 @@ +import { DatabaseController } from '../../../stores/effects/database/database_controller'; +import { RowController } from '../../../stores/effects/database/row/row_controller'; +import { RowInfo } from '../../../stores/effects/database/row/row_cache'; +import { CellIdentifier } from '../../../stores/effects/database/cell/cell_bd_svc'; +import { useEffect, useState } from 'react'; + +export const useRow = (viewId: string, databaseController: DatabaseController, rowInfo: RowInfo) => { + const [cells, setCells] = useState<{ fieldId: string; cellIdentifier: CellIdentifier }[]>([]); + const [rowController, setRowController] = useState(); + + useEffect(() => { + const rowCache = databaseController.databaseViewCache.getRowCache(); + const fieldController = databaseController.fieldController; + const c = new RowController(rowInfo, fieldController, rowCache); + setRowController(c); + + return () => { + // dispose row controller in future + }; + }, []); + + useEffect(() => { + if (!rowController) return; + + void (async () => { + const cellsPB = await rowController.loadCells(); + const loadingCells: { fieldId: string; cellIdentifier: CellIdentifier }[] = []; + + for (const [fieldId, cellIdentifier] of cellsPB.entries()) { + loadingCells.push({ + fieldId, + cellIdentifier, + }); + } + + setCells(loadingCells); + })(); + }, [rowController]); + + return { + cells: cells, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx index 5b8bde9576..eb8cdac5a2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'; import { GetStarted } from './GetStarted/GetStarted'; import { AppflowyLogo } from '../_shared/svg/AppflowyLogo'; + export const ProtectedRoutes = () => { const { currentUser, checkUser } = useAuth(); const [isLoading, setIsLoading] = useState(true); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.hooks.ts index 38f18c3f60..f02a16d34f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.hooks.ts @@ -1,34 +1,24 @@ import { useEffect, useState } from 'react'; import { useAppDispatch, useAppSelector } from '../../stores/store'; import { boardActions } from '../../stores/reducers/board/slice'; -import { ICellData, IDatabase, IDatabaseRow, ISelectOption } from '../../stores/reducers/database/slice'; +import { ISelectOption, ISelectOptionType } from '../../stores/reducers/database/slice'; export const useBoard = () => { const dispatch = useAppDispatch(); const groupingFieldId = useAppSelector((state) => state.board); const database = useAppSelector((state) => state.database); const [title, setTitle] = useState(''); - const [boardColumns, setBoardColumns] = - useState<(ISelectOption & { rows: (IDatabaseRow & { isGhost: boolean })[] })[]>(); + const [boardColumns, setBoardColumns] = useState([]); const [movingRowId, setMovingRowId] = useState(undefined); const [ghostLocation, setGhostLocation] = useState<{ column: number; row: number }>({ column: 0, row: 0 }); useEffect(() => { setTitle(database.title); - setBoardColumns( - database.fields[groupingFieldId].fieldOptions.selectOptions?.map((groupFieldItem) => { - const rows = database.rows - .filter((row) => row.cells[groupingFieldId].optionIds?.some((so) => so === groupFieldItem.selectOptionId)) - .map((row) => ({ - ...row, - isGhost: false, - })); - return { - ...groupFieldItem, - rows: rows, - }; - }) || [] - ); + if (database.fields[groupingFieldId]) { + setBoardColumns( + (database.fields[groupingFieldId].fieldOptions as ISelectOptionType | undefined)?.selectOptions || [] + ); + } }, [database, groupingFieldId]); const changeGroupingField = (fieldId: string) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx index e4cee56016..3869138342 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx @@ -1,13 +1,13 @@ import { SettingsSvg } from '../_shared/svg/SettingsSvg'; import { SearchInput } from '../_shared/SearchInput'; -import { useDatabase } from '../_shared/Database.hooks'; import { BoardBlock } from './BoardBlock'; import { NewBoardBlock } from './NewBoardBlock'; -import { IDatabaseRow } from '../../stores/reducers/database/slice'; import { useBoard } from './Board.hooks'; +import { useDatabase } from '../_shared/database-hooks/useDatabase'; + +export const Board = ({ viewId }: { viewId: string }) => { + const { controller, rows } = useDatabase(viewId); -export const Board = () => { - const { database, newField, renameField, newRow } = useDatabase(); const { title, boardColumns, @@ -36,16 +36,15 @@ export const Board = () => {
- {database && + {controller && boardColumns?.map((column, index) => ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx index 9d5e493d70..29bc1bc2a1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx @@ -1,24 +1,23 @@ import { Details2Svg } from '../_shared/svg/Details2Svg'; import AddSvg from '../_shared/svg/AddSvg'; -import { DatabaseFieldMap, ICellData, IDatabaseColumn, IDatabaseRow } from '../../stores/reducers/database/slice'; -import { BoardBlockItem } from './BoardBlockItem'; +import { BoardCard } from './BoardCard'; +import { RowInfo } from '../../stores/effects/database/row/row_cache'; +import { DatabaseController } from '../../stores/effects/database/database_controller'; export const BoardBlock = ({ + viewId, + controller, title, groupingFieldId, - count, - fields, - columns, rows, startMove, endMove, }: { + viewId: string; + controller: DatabaseController; title: string; groupingFieldId: string; - count: number; - fields: DatabaseFieldMap; - columns: IDatabaseColumn[]; - rows: IDatabaseRow[]; + rows: readonly RowInfo[]; startMove: (id: string) => void; endMove: () => void; }) => { @@ -27,7 +26,7 @@ export const BoardBlock = ({
{title} - ({count}) + ()
{rows.map((row, index) => ( - startMove(row.rowId)} + startMove={() => startMove(row.row.id)} endMove={() => endMove()} - > + > ))}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlockItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCard.tsx similarity index 67% rename from frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlockItem.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCard.tsx index 7741bbd05f..a6afd395fb 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlockItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCard.tsx @@ -3,22 +3,34 @@ import { Details2Svg } from '../_shared/svg/Details2Svg'; import { FieldType } from '../../../services/backend'; import { getBgColor } from '../_shared/getColor'; import { MouseEventHandler, useEffect, useRef, useState } from 'react'; +import { RowInfo } from '../../stores/effects/database/row/row_cache'; +import { useRow } from '../_shared/database-hooks/useRow'; +import { DatabaseController } from '../../stores/effects/database/database_controller'; +import { useAppSelector } from '../../stores/store'; +import { BoardCell } from './BoardCell'; -export const BoardBlockItem = ({ +export const BoardCard = ({ + viewId, + controller, groupingFieldId, - fields, - columns, + // fields, + // columns, row, startMove, endMove, }: { + viewId: string; + controller: DatabaseController; groupingFieldId: string; - fields: DatabaseFieldMap; - columns: IDatabaseColumn[]; - row: IDatabaseRow; + // fields: DatabaseFieldMap; + // columns: IDatabaseColumn[]; + row: RowInfo; startMove: () => void; endMove: () => void; }) => { + const { cells } = useRow(viewId, controller, row); + + const databaseStore = useAppSelector((state) => state.database); const [isMoving, setIsMoving] = useState(false); const [isDown, setIsDown] = useState(false); const [ghostWidth, setGhostWidth] = useState(0); @@ -26,6 +38,7 @@ export const BoardBlockItem = ({ const [ghostLeft, setGhostLeft] = useState(0); const [ghostTop, setGhostTop] = useState(0); const el = useRef(null); + useEffect(() => { if (el.current?.getBoundingClientRect && isMoving) { const { left, top, width, height } = el.current.getBoundingClientRect(); @@ -74,31 +87,14 @@ export const BoardBlockItem = ({
- {columns - .filter((column) => column.fieldId !== groupingFieldId) - .map((column, index) => { - switch (fields[column.fieldId].fieldType) { - case FieldType.MultiSelect: - return ( -
- {row.cells[column.fieldId].optionIds?.map((option, indexOption) => { - const selectOptions = fields[column.fieldId].fieldOptions.selectOptions; - const selectedOption = selectOptions?.find((so) => so.selectOptionId === option); - return ( -
- {selectedOption?.title} -
- ); - })} -
- ); - default: - return
{row.cells[column.fieldId].data}
; - } - })} + {cells.map((cell, index) => ( + + ))}
{isMoving && ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCell.tsx new file mode 100644 index 0000000000..496fc30af7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCell.tsx @@ -0,0 +1,43 @@ +import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc'; +import { CellCache } from '../../stores/effects/database/cell/cell_cache'; +import { FieldController } from '../../stores/effects/database/field/field_controller'; +import { FieldType } from '../../../services/backend'; +import { BoardOptionsCell } from './BoardOptionsCell'; +import { BoardDateCell } from './BoardDateCell'; +import { BoardTextCell } from './BoardTextCell'; + +export const BoardCell = ({ + cellIdentifier, + cellCache, + fieldController, +}: { + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; +}) => { + return ( + <> + {cellIdentifier.fieldType === FieldType.SingleSelect || + cellIdentifier.fieldType === FieldType.MultiSelect || + cellIdentifier.fieldType === FieldType.Checklist ? ( + + ) : cellIdentifier.fieldType === FieldType.DateTime ? ( + + ) : ( + + )} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardDateCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardDateCell.tsx new file mode 100644 index 0000000000..5047a8470a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardDateCell.tsx @@ -0,0 +1,18 @@ +import { DateCellDataPB } from '../../../services/backend'; +import { useCell } from '../_shared/database-hooks/useCell'; +import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc'; +import { CellCache } from '../../stores/effects/database/cell/cell_cache'; +import { FieldController } from '../../stores/effects/database/field/field_controller'; + +export const BoardDateCell = ({ + cellIdentifier, + cellCache, + fieldController, +}: { + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; +}) => { + const { data } = useCell(cellIdentifier, cellCache, fieldController); + return
{(data as DateCellDataPB | undefined)?.date || ''}
; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardOptionsCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardOptionsCell.tsx new file mode 100644 index 0000000000..6c9b3d046d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardOptionsCell.tsx @@ -0,0 +1,25 @@ +import { SelectOptionCellDataPB } from '../../../services/backend'; +import { useCell } from '../_shared/database-hooks/useCell'; +import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc'; +import { CellCache } from '../../stores/effects/database/cell/cell_cache'; +import { FieldController } from '../../stores/effects/database/field/field_controller'; + +export const BoardOptionsCell = ({ + cellIdentifier, + cellCache, + fieldController, +}: { + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; +}) => { + const { data } = useCell(cellIdentifier, cellCache, fieldController); + + return ( + <> + {(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => ( +
{option?.name || ''}
+ )) || ''} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardTextCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardTextCell.tsx new file mode 100644 index 0000000000..def0f1cd3a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/board/BoardTextCell.tsx @@ -0,0 +1,18 @@ +import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc'; +import { CellCache } from '../../stores/effects/database/cell/cell_cache'; +import { FieldController } from '../../stores/effects/database/field/field_controller'; +import { useCell } from '../_shared/database-hooks/useCell'; + +export const BoardTextCell = ({ + cellIdentifier, + cellCache, + fieldController, +}: { + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; +}) => { + const { data } = useCell(cellIdentifier, cellCache, fieldController); + + return
{(data as string | undefined) || ''}
; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/Breadcrumbs.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/Breadcrumbs.tsx index 2a0ef61101..e4258f30b3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/Breadcrumbs.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/Breadcrumbs.tsx @@ -1,7 +1,15 @@ -export const Breadcrumbs = () => { +import { ShowMenuSvg } from '../../_shared/svg/ShowMenuSvg'; + +export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boolean; onShowMenuClick: () => void }) => { return (
+ {menuHidden && ( + + )} + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/HeaderPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/HeaderPanel.tsx index 37a095afdb..f870d6fa2d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/HeaderPanel.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/HeaderPanel.tsx @@ -1,10 +1,10 @@ import { Breadcrumbs } from './Breadcrumbs'; import { PageOptions } from './PageOptions'; -export const HeaderPanel = () => { +export const HeaderPanel = ({ menuHidden, onShowMenuClick }: { menuHidden: boolean; onShowMenuClick: () => void }) => { return (
- +
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/MainPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/MainPanel.tsx index 0fd02f9113..b181693fd1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/MainPanel.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/MainPanel.tsx @@ -1,11 +1,40 @@ -import { ReactNode } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { HeaderPanel } from './HeaderPanel/HeaderPanel'; import { FooterPanel } from './FooterPanel'; -export const MainPanel = ({ children }: { children: ReactNode }) => { +const ANIMATION_DURATION = 300; + +export const MainPanel = ({ + left, + menuHidden, + onShowMenuClick, + children, +}: { + left: number; + menuHidden: boolean; + onShowMenuClick: () => void; + children: ReactNode; +}) => { + const [animation, setAnimation] = useState(false); + useEffect(() => { + if (!menuHidden) { + setTimeout(() => { + setAnimation(false); + }, ANIMATION_DURATION); + } else { + setAnimation(true); + } + }, [menuHidden]); + return ( -
- +
+
{children}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts index 07de52e2b1..a370f18f4d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.hooks.ts @@ -1,19 +1,22 @@ -import { useAppSelector } from '../../../stores/store'; +import { useAppDispatch, useAppSelector } from '../../../stores/store'; import { useNavigate } from 'react-router-dom'; import { IPage } from '../../../stores/reducers/pages/slice'; import { ViewLayoutTypePB } from '../../../../services/backend'; import { MouseEventHandler, useState } from 'react'; +import { activePageIdActions } from '../../../stores/reducers/activePageId/slice'; // number of pixels from left side of screen to show hidden navigation panel const FLOATING_PANEL_SHOW_WIDTH = 10; const FLOATING_PANEL_HIDE_EXTRA_WIDTH = 10; export const useNavigationPanelHooks = function () { + const dispatch = useAppDispatch(); const folders = useAppSelector((state) => state.folders); const pages = useAppSelector((state) => state.pages); const width = useAppSelector((state) => state.navigationWidth); const [navigationPanelFixed, setNavigationPanelFixed] = useState(true); const [slideInFloatingPanel, setSlideInFloatingPanel] = useState(true); + const [menuHidden, setMenuHidden] = useState(false); const navigate = useNavigate(); @@ -28,6 +31,14 @@ export const useNavigationPanelHooks = function () { const [floatingPanelWidth, setFloatingPanelWidth] = useState(0); + const onHideMenuClick = () => { + setMenuHidden(true); + }; + + const onShowMenuClick = () => { + setMenuHidden(false); + }; + const onPageClick = (page: IPage) => { let pageTypeRoute = (() => { switch (page.pageType) { @@ -43,6 +54,8 @@ export const useNavigationPanelHooks = function () { } })(); + dispatch(activePageIdActions.setActivePageId(page.id)); + navigate(`/page/${pageTypeRoute}/${page.id}`); }; @@ -66,5 +79,8 @@ export const useNavigationPanelHooks = function () { onScreenMouseMove, slideInFloatingPanel, setFloatingPanelWidth, + menuHidden, + onHideMenuClick, + onShowMenuClick, }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx index 44f79be47e..ca260a1574 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/NavigationPanel.tsx @@ -9,14 +9,19 @@ import { IPage } from '../../../stores/reducers/pages/slice'; import { useNavigate } from 'react-router-dom'; import React from 'react'; +const MINIMUM_WIDTH = 200; +const ANIMATION_DURATION = 300; + export const NavigationPanel = ({ - onCollapseNavigationClick, + onHideMenuClick, + menuHidden, width, folders, pages, onPageClick, }: { - onCollapseNavigationClick: () => void; + onHideMenuClick: () => void; + menuHidden: boolean; width: number; folders: IFolder[]; pages: IPage[]; @@ -24,9 +29,16 @@ export const NavigationPanel = ({ }) => { return ( <> -
+
- +
@@ -46,7 +58,7 @@ export const NavigationPanel = ({
- + ); }; @@ -58,7 +70,7 @@ type AppsContext = { }; const WorkspaceApps: React.FC = ({ folders, pages, onPageClick }) => ( -
+
{folders.map((folder, index) => ( { +export const NavigationResizer = ({ minWidth }: { minWidth: number }) => { const width = useAppSelector((state) => state.navigationWidth); const appDispatch = useAppDispatch(); const { onMouseDown, movementX } = useResizer(); useEffect(() => { - appDispatch(navigationWidthActions.changeWidth(width + movementX)); + if (width + movementX < minWidth) { + appDispatch(navigationWidthActions.changeWidth(minWidth)); + } else { + appDispatch(navigationWidthActions.changeWidth(width + movementX)); + } }, [movementX]); return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts index 77e53086be..9e608071e1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.hooks.ts @@ -1,5 +1,5 @@ import { IPage, pagesActions } from '../../../stores/reducers/pages/slice'; -import { useAppDispatch } from '../../../stores/store'; +import { useAppDispatch, useAppSelector } from '../../../stores/store'; import { useState } from 'react'; import { nanoid } from 'nanoid'; import { ViewBackendService } from '../../../stores/effects/folder/view/view_bd_svc'; @@ -9,6 +9,7 @@ export const usePageEvents = (page: IPage) => { const appDispatch = useAppDispatch(); const [showPageOptions, setShowPageOptions] = useState(false); const [showRenamePopup, setShowRenamePopup] = useState(false); + const activePageId = useAppSelector((state) => state.activePageId); const viewBackendService: ViewBackendService = new ViewBackendService(page.id); const error = useError(); @@ -69,5 +70,6 @@ export const usePageEvents = (page: IPage) => { duplicatePage, closePopup, closeRenamePopup, + activePageId, }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx index 7b1186f95d..0d0affe33f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/NavigationPanel/PageItem.tsx @@ -20,13 +20,16 @@ export const PageItem = ({ page, onPageClick }: { page: IPage; onPageClick: () = duplicatePage, closePopup, closeRenamePopup, + activePageId, } = usePageEvents(page); return (
onPageClick()} - className={'flex cursor-pointer items-center justify-between rounded-lg py-2 pl-8 pr-4 hover:bg-surface-2 '} + className={`flex cursor-pointer items-center justify-between rounded-lg py-2 pl-8 pr-4 hover:bg-surface-2 ${ + activePageId === page.id ? 'bg-surface-2' : '' + }`} >
+ } + placement='top-start' + > + toggleFormat(editor, format)} + > + {renderComponent} + + + ); +}; + +export default FormatButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx new file mode 100644 index 0000000000..0a18c3f5e9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/components.tsx @@ -0,0 +1,5 @@ +import ReactDOM from 'react-dom'; +export const Portal = ({ blockId, children }: { blockId: string; children: JSX.Element }) => { + const root = document.querySelectorAll(`[data-block-id=${blockId}]`)[0]; + return typeof document === 'object' && root ? ReactDOM.createPortal(children, root) : null; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx new file mode 100644 index 0000000000..5ad524c3b7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/HoveringToolbar/index.tsx @@ -0,0 +1,50 @@ +import { useEffect, useRef } from 'react'; +import { useFocused, useSlate } from 'slate-react'; +import FormatButton from './FormatButton'; +import { Portal } from './components'; +import { calcToolbarPosition } from '$app/utils/editor/toolbar'; + +const HoveringToolbar = ({ blockId }: { blockId: string }) => { + const editor = useSlate(); + const inFocus = useFocused(); + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + const blockDom = document.querySelectorAll(`[data-block-id=${blockId}]`)[0]; + const blockRect = blockDom?.getBoundingClientRect(); + + const position = calcToolbarPosition(editor, el, blockRect); + + if (!position) { + el.style.opacity = '0'; + } else { + el.style.opacity = '1'; + el.style.top = position.top; + el.style.left = position.left; + } + }); + + if (!inFocus) return null; + + return ( + +
{ + // prevent toolbar from taking focus away from editor + e.preventDefault(); + }} + > + {['bold', 'italic', 'underlined', 'strikethrough', 'code'].map((format) => ( + + ))} +
+
+ ); +}; + +export default HoveringToolbar; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx new file mode 100644 index 0000000000..730e7ee742 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockComponent.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Block, BlockType } from '$app/interfaces'; +import PageBlock from '../PageBlock'; +import TextBlock from '../TextBlock'; +import HeadingBlock from '../HeadingBlock'; +import ListBlock from '../ListBlock'; +import CodeBlock from '../CodeBlock'; + +export default function BlockComponent({ block }: { block: Block }) { + const renderComponent = () => { + switch (block.type) { + case BlockType.PageBlock: + return ; + case BlockType.TextBlock: + return ; + case BlockType.HeadingBlock: + return ; + case BlockType.ListBlock: + return ; + case BlockType.CodeBlock: + return ; + + default: + return null; + } + }; + + return ( +
+ {renderComponent()} +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.ts new file mode 100644 index 0000000000..8c9114f99d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/BlockList.hooks.ts @@ -0,0 +1,28 @@ +import { useContext, useEffect, useState } from "react"; +import { BlockContext } from "$app/utils/block_context"; +import { buildTree } from "$app/utils/tree"; +import { Block } from "$app/interfaces"; + +export function useBlockList() { + const blockContext = useContext(BlockContext); + + const [blockList, setBlockList] = useState([]); + + const [title, setTitle] = useState(''); + + useEffect(() => { + if (!blockContext) return; + const { blocksMap, id } = blockContext; + if (!id || !blocksMap) return; + const root = buildTree(id, blocksMap); + if (!root) return; + console.log(root); + setTitle(root.data.title); + setBlockList(root.children || []); + }, [blockContext]); + + return { + title, + blockList + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx new file mode 100644 index 0000000000..8364b6c75a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/BlockList/index.tsx @@ -0,0 +1,17 @@ +import { useBlockList } from './BlockList.hooks'; +import BlockComponent from './BlockComponent'; + +export default function BlockList() { + const { blockList, title } = useBlockList(); + + return ( +
+
{title}
+
+ {blockList?.map((block) => ( + + ))} +
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx new file mode 100644 index 0000000000..57e74cf783 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/CodeBlock/index.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Block } from '$app/interfaces'; + +export default function CodeBlock({ block }: { block: Block }) { + return
{block.data.text}
; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx new file mode 100644 index 0000000000..a7a39c57af --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/HeadingBlock/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Block } from '$app/interfaces'; +import TextBlock from '../TextBlock'; + +const fontSize: Record = { + 1: 'mt-8 text-3xl', + 2: 'mt-6 text-2xl', + 3: 'mt-4 text-xl', +}; +export default function HeadingBlock({ block }: { block: Block }) { + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx new file mode 100644 index 0000000000..703944bb23 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/ListBlock/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Block } from '$app/interfaces'; +import BlockComponent from '../BlockList/BlockComponent'; +import TextBlock from '../TextBlock'; + +export default function ListBlock({ block }: { block: Block }) { + const renderChildren = () => { + return block.children?.map((item) => ( +
  • + +
  • + )); + }; + + return ( +
    +
  • +
    + + {renderChildren()} +
    +
  • + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx new file mode 100644 index 0000000000..349f6f7d9e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/PageBlock/index.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Block } from '$app/interfaces'; + +export default function PageBlock({ block }: { block: Block }) { + return
    {block.data.title}
    ; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx new file mode 100644 index 0000000000..b1c73f1116 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/Leaf.tsx @@ -0,0 +1,28 @@ +import { RenderLeafProps } from 'slate-react'; + +const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => { + let newChildren = children; + if ('bold' in leaf && leaf.bold) { + newChildren = {children}; + } + + if ('code' in leaf && leaf.code) { + newChildren = {newChildren}; + } + + if ('italic' in leaf && leaf.italic) { + newChildren = {newChildren}; + } + + if ('underlined' in leaf && leaf.underlined) { + newChildren = {newChildren}; + } + + return ( + + {newChildren} + + ); +}; + +export default Leaf; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx new file mode 100644 index 0000000000..723573de27 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/block/TextBlock/index.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; +import { Block } from '$app/interfaces'; +import BlockComponent from '../BlockList/BlockComponent'; + +import { createEditor } from 'slate'; +import { Slate, Editable, withReact } from 'slate-react'; +import Leaf from './Leaf'; +import HoveringToolbar from '$app/components/HoveringToolbar'; +import { triggerHotkey } from '$app/utils/editor/hotkey'; + +export default function TextBlock({ block }: { block: Block }) { + const [editor] = useState(() => withReact(createEditor())); + + return ( +
    + console.log('===', e, editor.operations)} + value={[ + { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + type: 'paragraph', + children: [{ text: block.data.text }], + }, + ]} + > + + { + switch (event.key) { + case 'Enter': { + event.stopPropagation(); + event.preventDefault(); + return; + } + } + + triggerHotkey(event, editor); + }} + renderLeaf={(props) => } + placeholder='Enter some text...' + /> + +
    + {block.children?.map((item: Block) => ( + + ))} +
    +
    + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts new file mode 100644 index 0000000000..07cd1d8be9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/constants/toolbar.ts @@ -0,0 +1,25 @@ + +export const iconSize = { width: 18, height: 18 }; + +export const command: Record = { + bold: { + title: 'Bold', + key: '⌘ + B', + }, + underlined: { + title: 'Underlined', + key: '⌘ + U', + }, + italic: { + title: 'Italic', + key: '⌘ + I', + }, + code: { + title: 'Mark as code', + key: '⌘ + E', + }, + strikethrough: { + title: 'Strike through', + key: '⌘ + Shift + S or ⌘ + Shift + X', + }, +}; \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts new file mode 100644 index 0000000000..7aedced472 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/interfaces/index.ts @@ -0,0 +1,51 @@ + +// eslint-disable-next-line no-shadow +export enum BlockType { + PageBlock = 0, + HeadingBlock = 1, + ListBlock = 2, + TextBlock = 3, + CodeBlock = 4, + EmbedBlock = 5, + QuoteBlock = 6, + DividerBlock = 7, + MediaBlock = 8, + TableBlock = 9, +} + + + +export type BlockData = T extends BlockType.TextBlock ? TextBlockData : +T extends BlockType.PageBlock ? PageBlockData : +T extends BlockType.HeadingBlock ? HeadingBlockData: +T extends BlockType.ListBlock ? ListBlockData : any; + +export interface Block { + id: string; + type: BlockType; + data: BlockData; + parent: string | null; + prev: string | null; + next: string | null; + firstChild: string | null; + lastChild: string | null; + children?: Block[]; +} + + +interface TextBlockData { + text: string; + attr: string; +} + +interface PageBlockData { + title: string; +} + +interface ListBlockData { + type: 'ul' | 'ol'; +} + +interface HeadingBlockData { + level: number; +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts new file mode 100644 index 0000000000..7d5248902f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/block_context.ts @@ -0,0 +1,8 @@ + +import { createContext } from 'react'; +import { Block, BlockType } from '../interfaces'; + +export const BlockContext = createContext<{ + id?: string; + blocksMap?: Record; +} | null>(null); diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts new file mode 100644 index 0000000000..fd36928b76 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/format.ts @@ -0,0 +1,25 @@ +import { + Editor, + Transforms, + Text, + Node +} from 'slate'; + +export function toggleFormat(editor: Editor, format: string) { + const isActive = isFormatActive(editor, format) + Transforms.setNodes( + editor, + { [format]: isActive ? null : true }, + { match: Text.isText, split: true } + ) +} + +export const isFormatActive = (editor: Editor, format: string) => { + const [match] = Editor.nodes(editor, { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + match: (n: Node) => n[format] === true, + mode: 'all', + }) + return !!match +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts new file mode 100644 index 0000000000..fad418086d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/hotkey.ts @@ -0,0 +1,22 @@ +import isHotkey from 'is-hotkey'; +import { toggleFormat } from './format'; +import { Editor } from 'slate'; + +const HOTKEYS: Record = { + 'mod+b': 'bold', + 'mod+i': 'italic', + 'mod+u': 'underline', + 'mod+e': 'code', + 'mod+shift+X': 'strikethrough', + 'mod+shift+S': 'strikethrough', +}; + +export function triggerHotkey(event: React.KeyboardEvent, editor: Editor) { + for (const hotkey in HOTKEYS) { + if (isHotkey(hotkey, event)) { + event.preventDefault() + const format = HOTKEYS[hotkey] + toggleFormat(editor, format) + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts new file mode 100644 index 0000000000..80131a4d69 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/editor/toolbar.ts @@ -0,0 +1,28 @@ +import { Editor, Range } from 'slate'; +export function calcToolbarPosition(editor: Editor, el: HTMLDivElement, blockRect: DOMRect) { + const { selection } = editor; + + if (!selection || Range.isCollapsed(selection) || Editor.string(editor, selection) === '') { + return; + } + + const domSelection = window.getSelection(); + let domRange; + if (domSelection?.rangeCount === 0) { + domRange = document.createRange(); + domRange.setStart(el, domSelection?.anchorOffset); + domRange.setEnd(el, domSelection?.anchorOffset); + } else { + domRange = domSelection?.getRangeAt(0); + } + + const rect = domRange?.getBoundingClientRect() || { top: 0, left: 0, width: 0, height: 0 }; + + const top = `${-el.offsetHeight - 5}px`; + const left = `${rect.left - blockRect.left - el.offsetWidth / 2 + rect.width / 2}px`; + return { + top, + left, + } + +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/tree.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/tree.ts new file mode 100644 index 0000000000..6a3902f929 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/tree.ts @@ -0,0 +1,36 @@ +import { Block } from "../interfaces"; + +export function buildTree(id: string, blocksMap: Record) { + const head = blocksMap[id]; + let node: Block | null = head; + while(node) { + + if (node.parent) { + const parent = blocksMap[node.parent]; + !parent.children && (parent.children = []); + parent.children.push(node); + } + + if (node.firstChild) { + node = blocksMap[node.firstChild]; + } else if (node.next) { + node = blocksMap[node.next]; + } else { + while(node && node?.parent) { + const parent: Block | null = blocksMap[node.parent]; + if (parent?.next) { + node = blocksMap[parent.next]; + break; + } else { + node = parent; + } + } + if (node.id === head.id) { + node = null; + break; + } + } + + } + return head; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts index b2819b0b21..0a1abca58a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.hooks.ts @@ -1,10 +1,15 @@ +import { useEffect, useState } from 'react'; import { DocumentEventGetDocument, DocumentVersionPB, OpenDocumentPayloadPB, } from '../../services/backend/events/flowy-document'; +import { Block, BlockType } from '../interfaces'; +import { useParams } from 'react-router-dom'; export const useDocument = () => { + const params = useParams(); + const [blocksMap, setBlocksMap] = useState>(); const loadDocument = async (id: string): Promise => { const getDocumentResult = await DocumentEventGetDocument( OpenDocumentPayloadPB.fromObject({ @@ -20,5 +25,149 @@ export const useDocument = () => { throw new Error('get document error'); } }; - return { loadDocument }; + + const loadBlockData = async (blockId: string): Promise> => { + return { + [blockId]: { + id: blockId, + type: BlockType.PageBlock, + data: { title: 'Document Title' }, + parent: null, + next: null, + prev: null, + firstChild: "A", + lastChild: "E" + }, + "A": { + id: "A", + type: BlockType.HeadingBlock, + data: { level: 1, text: 'A Heading-1' }, + parent: blockId, + prev: null, + next: "B", + firstChild: null, + lastChild: null, + }, + "B": { + id: "B", + type: BlockType.TextBlock, + data: { text: 'Hello', attr: '' }, + parent: blockId, + prev: "A", + next: "C", + firstChild: null, + lastChild: null, + }, + "C": { + id: "C", + type: BlockType.TextBlock, + data: { text: 'block c' }, + prev: null, + parent: blockId, + next: "D", + firstChild: "F", + lastChild: null, + }, + "D": { + id: "D", + type: BlockType.ListBlock, + data: { type: 'number_list', text: 'D List' }, + prev: "C", + parent: blockId, + next: null, + firstChild: "G", + lastChild: "H", + }, + "E": { + id: "E", + type: BlockType.TextBlock, + data: { text: 'World', attr: '' }, + prev: "D", + parent: blockId, + next: null, + firstChild: null, + lastChild: null, + }, + "F": { + id: "F", + type: BlockType.TextBlock, + data: { text: 'Heading', attr: '' }, + prev: null, + parent: "C", + next: null, + firstChild: null, + lastChild: null, + }, + "G": { + id: "G", + type: BlockType.TextBlock, + data: { text: 'Item 1', attr: '' }, + prev: null, + parent: "D", + next: "H", + firstChild: null, + lastChild: null, + }, + "H": { + id: "H", + type: BlockType.TextBlock, + data: { text: 'Item 2', attr: '' }, + prev: "G", + parent: "D", + next: "I", + firstChild: null, + lastChild: null, + }, + "I": { + id: "I", + type: BlockType.HeadingBlock, + data: { level: 2, text: 'B Heading-1' }, + parent: blockId, + prev: "H", + next: 'L', + firstChild: null, + lastChild: null, + }, + "L": { + id: "L", + type: BlockType.TextBlock, + data: { text: '456' }, + parent: blockId, + prev: "I", + next: 'J', + firstChild: null, + lastChild: null, + }, + "J": { + id: "J", + type: BlockType.HeadingBlock, + data: { level: 3, text: 'C Heading-1' }, + parent: blockId, + prev: "L", + next: "K", + firstChild: null, + lastChild: null, + }, + "K": { + id: "K", + type: BlockType.TextBlock, + data: { text: '123' }, + parent: blockId, + prev: "J", + next: null, + firstChild: null, + lastChild: null, + }, + } + } + + useEffect(() => { + void (async () => { + if (!params?.id) return; + const data = await loadBlockData(params.id); + console.log(data); + setBlocksMap(data); + })(); + }, [params]); + return { blocksMap, blockId: params.id }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx index 2b6cf4e9f6..70be86656d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx @@ -1,17 +1,28 @@ -import { useParams } from 'react-router-dom'; -import { useEffect } from 'react'; import { useDocument } from './DocumentPage.hooks'; +import BlockList from '../components/block/BlockList'; +import { BlockContext } from '../utils/block_context'; +import { createTheme, ThemeProvider } from '@mui/material'; +const theme = createTheme({ + typography: { + fontFamily: ['Poppins'].join(','), + }, +}); export const DocumentPage = () => { - const params = useParams(); - const { loadDocument } = useDocument(); - useEffect(() => { - void (async () => { - if (!params?.id) return; - const content: any = await loadDocument(params.id); - console.log(content); - })(); - }, [params]); + const { blocksMap, blockId } = useDocument(); - return
    Document Page ID: {params.id}
    ; + return ( + +
    + + + +
    +
    + ); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/vite-env.d.ts b/frontend/appflowy_tauri/src/appflowy_app/vite-env.d.ts index 11f02fe2a0..ed77210660 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/vite-env.d.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/vite-env.d.ts @@ -1 +1,2 @@ /// + diff --git a/frontend/appflowy_tauri/src/styles/template.css b/frontend/appflowy_tauri/src/styles/template.css index 165bd3712f..1c9c6191e7 100644 --- a/frontend/appflowy_tauri/src/styles/template.css +++ b/frontend/appflowy_tauri/src/styles/template.css @@ -16,6 +16,10 @@ body { font-family: Poppins; } +::selection { + @apply bg-[#E0F8FF] +} + .btn { @apply rounded-xl border border-gray-500 px-4 py-3; } diff --git a/frontend/appflowy_tauri/tsconfig.json b/frontend/appflowy_tauri/tsconfig.json index a21d07380e..7e82aa0265 100644 --- a/frontend/appflowy_tauri/tsconfig.json +++ b/frontend/appflowy_tauri/tsconfig.json @@ -14,8 +14,15 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "types": ["node", "jest"], + "baseUrl": "./", + "paths": { + "@/*": ["src/*"], + "$app/*": ["src/appflowy_app/*"] + }, }, "include": ["src", "vite.config.ts", "../app_flowy/assets/translations"], + "exclude": ["node_modules"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/frontend/appflowy_tauri/vite.config.ts b/frontend/appflowy_tauri/vite.config.ts index b155feb0be..871e54f6d2 100644 --- a/frontend/appflowy_tauri/vite.config.ts +++ b/frontend/appflowy_tauri/vite.config.ts @@ -24,4 +24,10 @@ export default defineConfig({ // produce sourcemaps for debug builds sourcemap: !!process.env.TAURI_DEBUG, }, + resolve: { + alias: [ + { find: '@/', replacement: `${__dirname}/src/` }, + { find: '$app/', replacement: `${__dirname}/src/appflowy_app/` } + ], + }, }); From 5e8f6a53a0151848a5193fafee37b5cac325cf40 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 9 Mar 2023 13:15:22 +0700 Subject: [PATCH 07/15] relicense appflowy editor (#1938) * revert:"fix: remove keyword when click selection menu item" This reverts commit 5782dec45cf2dfb4628fe6f153c52109c9ce5a5b. * revert(appflowy_editor):revert "feat: double asterisks/underscores to bold text" This reverts commit c0964fad5d3b64c4d7b797ecfbfc6a9cfc2f9174. * revert(appflowy_editor):revert "fix: workaround infinity formatting" This reverts commit 6a902a2b218afcf0adc0e9cbf0a52864ecb93118. The Appflowy folder under the frontend had been removed before reverting. * chore(appflow_editor):update test variable after reverting * chore(appflowy_editor): comment out the test for reverting * chore(appflowy_editor): update variable type after reverting * chore(appflowy_editor): remove unused import after reverting * feat(appflowy_editor): double asterisk to bold text * test(appflowy_editor): test double asterisk to bold text * fix(appflowy_editor): delete slash after a selection menu item is selected * test(appflowy_editor): test selection menu widget after clicking * feat(appflowy_editor): double asterisk to bold text and remove slash after clicking selection menu item (#1935) * feat(appflowy_editor): double asterisk to bold text * test(appflowy_editor): test double asterisk to bold text * fix(appflowy_editor): delete slash after a selection menu item is selected * test(appflowy_editor): test selection menu widget after clicking * feat(appflowy_editor): double underscore to bold text * test(appflowy_editor): test double underscore to bold text * chore(appflowy_editor): put checkbox testing back * chore: format code --------- Co-authored-by: Yijing Huang --- .../plugins/board/board_menu_item.dart | 2 +- .../plugins/grid/grid_menu_item.dart | 2 +- .../plugins/horizontal_rule_node_widget.dart | 2 +- .../lib/plugin/AI/auto_completion.dart | 2 +- .../lib/plugin/AI/continue_to_write.dart | 2 +- .../core/legacy/built_in_attribute_keys.dart | 1 - .../selection_menu_item_widget.dart | 2 +- .../selection_menu_service.dart | 18 +- .../selection_menu/selection_menu_widget.dart | 17 +- .../markdown_syntax_to_styled_text.dart | 243 +++++++-------- .../built_in_shortcut_events.dart | 22 +- .../test/infra/test_raw_key_event.dart | 8 +- .../render/rich_text/checkbox_text_test.dart | 2 +- .../selection_menu_widget_test.dart | 91 +++--- ...wn_syntax_to_styled_text_handler_test.dart | 277 ------------------ .../markdown_syntax_to_styled_text_test.dart | 188 ++++++++++++ .../slash_handler_test.dart | 2 +- .../code_block/code_block_shortcut_event.dart | 2 +- .../src/divider/divider_shortcut_event.dart | 2 +- .../lib/src/emoji_picker/emoji_menu_item.dart | 2 +- .../math_equation_node_widget.dart | 2 +- 21 files changed, 396 insertions(+), 493 deletions(-) delete mode 100644 frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart index a368951f30..cccb671e7c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/board/board_menu_item.dart @@ -7,7 +7,7 @@ import 'package:flowy_infra/image.dart'; import 'package:flutter/material.dart'; SelectionMenuItem boardMenuItem = SelectionMenuItem( - name: () => LocaleKeys.document_plugins_referencedBoard.tr(), + name: LocaleKeys.document_plugins_referencedBoard.tr(), icon: (editorState, onSelected) { return svgWidget( 'editor/board', diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart index e69b406c7c..737c17f4f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/grid/grid_menu_item.dart @@ -7,7 +7,7 @@ import 'package:flowy_infra/image.dart'; import 'package:flutter/material.dart'; SelectionMenuItem gridMenuItem = SelectionMenuItem( - name: () => LocaleKeys.document_plugins_referencedGrid.tr(), + name: LocaleKeys.document_plugins_referencedGrid.tr(), icon: (editorState, onSelected) { return svgWidget( 'editor/grid', diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/horizontal_rule_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/horizontal_rule_node_widget.dart index c3d4cbeb35..7053e6f2ac 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/horizontal_rule_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/horizontal_rule_node_widget.dart @@ -37,7 +37,7 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) { }; SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem( - name: () => 'Horizontal rule', + name: 'Horizontal rule', icon: (editorState, onSelected) => Icon( Icons.horizontal_rule, color: onSelected diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/AI/auto_completion.dart b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/AI/auto_completion.dart index c2e9447b6b..e90f8bd3d2 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/AI/auto_completion.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/AI/auto_completion.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; SelectionMenuItem autoCompletionMenuItem = SelectionMenuItem( - name: () => 'Auto generate content', + name: 'Auto generate content', icon: (editorState, onSelected) => Icon( Icons.rocket, size: 18.0, diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/AI/continue_to_write.dart b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/AI/continue_to_write.dart index e3e407d481..4e0b2ec100 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/AI/continue_to_write.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/example/lib/plugin/AI/continue_to_write.dart @@ -4,7 +4,7 @@ import 'package:example/plugin/AI/text_robot.dart'; import 'package:flutter/material.dart'; SelectionMenuItem continueToWriteMenuItem = SelectionMenuItem( - name: () => 'Continue To Write', + name: 'Continue To Write', icon: (editorState, onSelected) => Icon( Icons.print, size: 18.0, diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/legacy/built_in_attribute_keys.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/legacy/built_in_attribute_keys.dart index 3f334bdd0a..6421e7efd0 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/legacy/built_in_attribute_keys.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/legacy/built_in_attribute_keys.dart @@ -37,7 +37,6 @@ class BuiltInAttributeKey { static String checkbox = 'checkbox'; static String code = 'code'; static String number = 'number'; - static String defaultFormating = 'defaultFormating'; static List partialStyleKeys = [ BuiltInAttributeKey.bold, diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart index 912d9447ff..1d3a73f93e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart @@ -47,7 +47,7 @@ class _SelectionMenuItemWidgetState extends State { : MaterialStateProperty.all(Colors.transparent), ), label: Text( - widget.item.name(), + widget.item.name, textAlign: TextAlign.left, style: TextStyle( color: (widget.isSelected || _onHover) diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart index 498438e6be..7e0ce3eeea 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart @@ -156,7 +156,7 @@ List get defaultSelectionMenuItems => _defaultSelectionMenuItems; final List _defaultSelectionMenuItems = [ SelectionMenuItem( - name: () => AppFlowyEditorLocalizations.current.text, + name: AppFlowyEditorLocalizations.current.text, icon: (editorState, onSelected) => _selectionMenuIcon('text', editorState, onSelected), keywords: ['text'], @@ -165,7 +165,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: () => AppFlowyEditorLocalizations.current.heading1, + name: AppFlowyEditorLocalizations.current.heading1, icon: (editorState, onSelected) => _selectionMenuIcon('h1', editorState, onSelected), keywords: ['heading 1, h1'], @@ -174,7 +174,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: () => AppFlowyEditorLocalizations.current.heading2, + name: AppFlowyEditorLocalizations.current.heading2, icon: (editorState, onSelected) => _selectionMenuIcon('h2', editorState, onSelected), keywords: ['heading 2, h2'], @@ -183,7 +183,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: () => AppFlowyEditorLocalizations.current.heading3, + name: AppFlowyEditorLocalizations.current.heading3, icon: (editorState, onSelected) => _selectionMenuIcon('h3', editorState, onSelected), keywords: ['heading 3, h3'], @@ -192,14 +192,14 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: () => AppFlowyEditorLocalizations.current.image, + name: AppFlowyEditorLocalizations.current.image, icon: (editorState, onSelected) => _selectionMenuIcon('image', editorState, onSelected), keywords: ['image'], handler: showImageUploadMenu, ), SelectionMenuItem( - name: () => AppFlowyEditorLocalizations.current.bulletedList, + name: AppFlowyEditorLocalizations.current.bulletedList, icon: (editorState, onSelected) => _selectionMenuIcon('bulleted_list', editorState, onSelected), keywords: ['bulleted list', 'list', 'unordered list'], @@ -208,7 +208,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: () => AppFlowyEditorLocalizations.current.numberedList, + name: AppFlowyEditorLocalizations.current.numberedList, icon: (editorState, onSelected) => _selectionMenuIcon('number', editorState, onSelected), keywords: ['numbered list', 'list', 'ordered list'], @@ -217,7 +217,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: () => AppFlowyEditorLocalizations.current.checkbox, + name: AppFlowyEditorLocalizations.current.checkbox, icon: (editorState, onSelected) => _selectionMenuIcon('checkbox', editorState, onSelected), keywords: ['todo list', 'list', 'checkbox list'], @@ -226,7 +226,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: () => AppFlowyEditorLocalizations.current.quote, + name: AppFlowyEditorLocalizations.current.quote, icon: (editorState, onSelected) => _selectionMenuIcon('quote', editorState, onSelected), keywords: ['quote', 'refer'], diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index 0d96853d74..3a88cfcd4a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart @@ -20,14 +20,14 @@ class SelectionMenuItem { required SelectionMenuItemHandler handler, }) { this.handler = (editorState, menuService, context) { - _deleteToSlash(editorState); + _deleteSlash(editorState); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { handler(editorState, menuService, context); }); }; } - final String Function() name; + final String name; final Widget Function(EditorState editorState, bool onSelected) icon; /// Customizes keywords for item. @@ -36,20 +36,23 @@ class SelectionMenuItem { final List keywords; late final SelectionMenuItemHandler handler; - void _deleteToSlash(EditorState editorState) { + void _deleteSlash(EditorState editorState) { final selectionService = editorState.service.selectionService; final selection = selectionService.currentSelection.value; final nodes = selectionService.currentSelectedNodes; if (selection != null && nodes.length == 1) { final node = nodes.first as TextNode; final end = selection.start.offset; - final start = node.toPlainText().substring(0, end).lastIndexOf('/'); + final lastSlashIndex = + node.toPlainText().substring(0, end).lastIndexOf('/'); + // delete all the texts after '/' along with '/' final transaction = editorState.transaction ..deleteText( node, - start, - selection.start.offset - start, + lastSlashIndex, + end - lastSlashIndex, ); + editorState.apply(transaction); } } @@ -81,7 +84,7 @@ class SelectionMenuItem { updateSelection, }) { return SelectionMenuItem( - name: () => name, + name: name, icon: (editorState, onSelected) => Icon( iconData, color: onSelected diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart index 94caff83b1..b38d838fed 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart @@ -265,135 +265,6 @@ ShortcutEventHandler markdownLinkOrImageHandler = (editorState, event) { return KeyEventResult.handled; }; -// convert **abc** to bold abc. -ShortcutEventHandler doubleAsterisksToBold = (editorState, event) { - final selectionService = editorState.service.selectionService; - final selection = selectionService.currentSelection.value; - final textNodes = selectionService.currentSelectedNodes.whereType(); - if (selection == null || !selection.isSingle || textNodes.length != 1) { - return KeyEventResult.ignored; - } - - final textNode = textNodes.first; - final text = textNode.toPlainText().substring(0, selection.end.offset); - - // make sure the last two characters are **. - if (text.length < 2 || text[selection.end.offset - 1] != '*') { - return KeyEventResult.ignored; - } - - // find all the index of `*`. - final asteriskIndexes = []; - for (var i = 0; i < text.length; i++) { - if (text[i] == '*') { - asteriskIndexes.add(i); - } - } - - if (asteriskIndexes.length < 3) { - return KeyEventResult.ignored; - } - - // make sure the second to last and third to last asterisks are connected. - final thirdToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 3]; - final secondToLastAsteriskIndex = asteriskIndexes[asteriskIndexes.length - 2]; - final lastAsterisIndex = asteriskIndexes[asteriskIndexes.length - 1]; - if (secondToLastAsteriskIndex != thirdToLastAsteriskIndex + 1 || - lastAsterisIndex == secondToLastAsteriskIndex + 1) { - return KeyEventResult.ignored; - } - - // delete the last three asterisks. - // update the style of the text surround by `** **` to bold. - // and update the cursor position. - final transaction = editorState.transaction - ..deleteText(textNode, lastAsterisIndex, 1) - ..deleteText(textNode, thirdToLastAsteriskIndex, 2) - ..formatText( - textNode, - thirdToLastAsteriskIndex, - selection.end.offset - thirdToLastAsteriskIndex - 3, - { - BuiltInAttributeKey.bold: true, - BuiltInAttributeKey.defaultFormating: true, - }, - ) - ..afterSelection = Selection.collapsed( - Position( - path: textNode.path, - offset: selection.end.offset - 3, - ), - ); - editorState.apply(transaction); - - return KeyEventResult.handled; -}; - -// convert __abc__ to bold abc. -ShortcutEventHandler doubleUnderscoresToBold = (editorState, event) { - final selectionService = editorState.service.selectionService; - final selection = selectionService.currentSelection.value; - final textNodes = selectionService.currentSelectedNodes.whereType(); - if (selection == null || !selection.isSingle || textNodes.length != 1) { - return KeyEventResult.ignored; - } - - final textNode = textNodes.first; - final text = textNode.toPlainText().substring(0, selection.end.offset); - - // make sure the last two characters are __. - if (text.length < 2 || text[selection.end.offset - 1] != '_') { - return KeyEventResult.ignored; - } - - // find all the index of `_`. - final underscoreIndexes = []; - for (var i = 0; i < text.length; i++) { - if (text[i] == '_') { - underscoreIndexes.add(i); - } - } - - if (underscoreIndexes.length < 3) { - return KeyEventResult.ignored; - } - - // make sure the second to last and third to last underscores are connected. - final thirdToLastUnderscoreIndex = - underscoreIndexes[underscoreIndexes.length - 3]; - final secondToLastUnderscoreIndex = - underscoreIndexes[underscoreIndexes.length - 2]; - final lastAsterisIndex = underscoreIndexes[underscoreIndexes.length - 1]; - if (secondToLastUnderscoreIndex != thirdToLastUnderscoreIndex + 1 || - lastAsterisIndex == secondToLastUnderscoreIndex + 1) { - return KeyEventResult.ignored; - } - - // delete the last three underscores. - // update the style of the text surround by `__ __` to bold. - // and update the cursor position. - final transaction = editorState.transaction - ..deleteText(textNode, lastAsterisIndex, 1) - ..deleteText(textNode, thirdToLastUnderscoreIndex, 2) - ..formatText( - textNode, - thirdToLastUnderscoreIndex, - selection.end.offset - thirdToLastUnderscoreIndex - 3, - { - BuiltInAttributeKey.bold: true, - BuiltInAttributeKey.defaultFormating: true, - }, - ) - ..afterSelection = Selection.collapsed( - Position( - path: textNode.path, - offset: selection.end.offset - 3, - ), - ); - editorState.apply(transaction); - return KeyEventResult.handled; -}; - ShortcutEventHandler underscoreToItalicHandler = (editorState, event) { // Obtain the selection and selected nodes of the current document through the 'selectionService' // to determine whether the selection is collapsed and whether the selected node is a text node. @@ -438,3 +309,117 @@ ShortcutEventHandler underscoreToItalicHandler = (editorState, event) { return KeyEventResult.handled; }; + +ShortcutEventHandler doubleAsteriskToBoldHanlder = (editorState, event) { + final selectionService = editorState.service.selectionService; + final selection = selectionService.currentSelection.value; + final textNodes = selectionService.currentSelectedNodes.whereType(); + + if (selection == null || !selection.isSingle || textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final textNode = textNodes.first; + final text = textNode.toPlainText(); + +// make sure the last two characters are '**' + if (text.length < 2 || text[selection.end.offset - 1] != '*') { + return KeyEventResult.ignored; + } + +// find all the index of '*' + final asteriskIndexList = []; + for (var i = 0; i < text.length; i++) { + if (text[i] == '*') { + asteriskIndexList.add(i); + } + } + + if (asteriskIndexList.length < 3) return KeyEventResult.ignored; + +// make sure the second to last and third to last asterisk are connected + final thirdToLastAsteriskIndex = + asteriskIndexList[asteriskIndexList.length - 3]; + final secondToLastAsteriskIndex = + asteriskIndexList[asteriskIndexList.length - 2]; + final lastAsteriskIndex = asteriskIndexList[asteriskIndexList.length - 1]; + if (secondToLastAsteriskIndex != thirdToLastAsteriskIndex + 1 || + lastAsteriskIndex == secondToLastAsteriskIndex + 1) { + return KeyEventResult.ignored; + } + +//delete the last three asterisks +//update the style of the text surround by '** **' to bold +//update the cursor position + final transaction = editorState.transaction + ..deleteText(textNode, lastAsteriskIndex, 1) + ..deleteText(textNode, thirdToLastAsteriskIndex, 2) + ..formatText(textNode, thirdToLastAsteriskIndex, + selection.end.offset - thirdToLastAsteriskIndex - 2, { + BuiltInAttributeKey.bold: true, + }) + ..afterSelection = Selection.collapsed( + Position(path: textNode.path, offset: selection.end.offset - 3)); + + editorState.apply(transaction); + + return KeyEventResult.handled; +}; + +//Implement in the same way as doubleAsteriskToBoldHanlder +ShortcutEventHandler doubleUnderscoreToBoldHanlder = (editorState, event) { + final selectionService = editorState.service.selectionService; + final selection = selectionService.currentSelection.value; + final textNodes = selectionService.currentSelectedNodes.whereType(); + + if (selection == null || !selection.isSingle || textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final textNode = textNodes.first; + final text = textNode.toPlainText(); + +// make sure the last two characters are '__' + if (text.length < 2 || text[selection.end.offset - 1] != '_') { + return KeyEventResult.ignored; + } + +// find all the index of '_' + final underscoreIndexList = []; + for (var i = 0; i < text.length; i++) { + if (text[i] == '_') { + underscoreIndexList.add(i); + } + } + + if (underscoreIndexList.length < 3) return KeyEventResult.ignored; + +// make sure the second to last and third to last underscore are connected + final thirdToLastUnderscoreIndex = + underscoreIndexList[underscoreIndexList.length - 3]; + final secondToLastUnderscoreIndex = + underscoreIndexList[underscoreIndexList.length - 2]; + final lastUnderscoreIndex = + underscoreIndexList[underscoreIndexList.length - 1]; + if (secondToLastUnderscoreIndex != thirdToLastUnderscoreIndex + 1 || + lastUnderscoreIndex == secondToLastUnderscoreIndex + 1) { + return KeyEventResult.ignored; + } + +//delete the last three underscores +//update the style of the text surround by '__ __' to bold +//update the cursor position + final transaction = editorState.transaction + ..deleteText(textNode, lastUnderscoreIndex, 1) + ..deleteText(textNode, thirdToLastUnderscoreIndex, 2) + ..formatText(textNode, thirdToLastUnderscoreIndex, + selection.end.offset - thirdToLastUnderscoreIndex - 2, { + BuiltInAttributeKey.bold: true, + }) + ..afterSelection = Selection.collapsed( + Position(path: textNode.path, offset: selection.end.offset - 3)); + + editorState.apply(transaction); + + return KeyEventResult.handled; +}; diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index 6c1c7bc259..d6338b6fe3 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -1,5 +1,3 @@ -// List<> - import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; @@ -284,16 +282,6 @@ List builtInShortcutEvents = [ command: 'tab', handler: tabHandler, ), - ShortcutEvent( - key: 'Double stars to bold', - command: 'shift+asterisk', - handler: doubleAsterisksToBold, - ), - ShortcutEvent( - key: 'Double underscores to bold', - command: 'shift+underscore', - handler: doubleUnderscoresToBold, - ), ShortcutEvent( key: 'Backquote to code', command: 'backquote', @@ -319,6 +307,16 @@ List builtInShortcutEvents = [ command: 'shift+underscore', handler: underscoreToItalicHandler, ), + ShortcutEvent( + key: 'Double asterisk to bold', + command: 'shift+digit 8', + handler: doubleAsteriskToBoldHanlder, + ), + ShortcutEvent( + key: 'Double underscore to bold', + command: 'shift+underscore', + handler: doubleUnderscoreToBoldHanlder, + ), // https://github.com/flutter/flutter/issues/104944 // Workaround: Using space editing on the web platform often results in errors, // so adding a shortcut event to handle the space input instead of using the diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_raw_key_event.dart index d00e458e5c..0a3d6cd74f 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/infra/test_raw_key_event.dart @@ -142,15 +142,15 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.keyZ) { return PhysicalKeyboardKey.keyZ; } - if (this == LogicalKeyboardKey.asterisk) { + if (this == LogicalKeyboardKey.tilde) { + return PhysicalKeyboardKey.backquote; + } + if (this == LogicalKeyboardKey.digit8) { return PhysicalKeyboardKey.digit8; } if (this == LogicalKeyboardKey.underscore) { return PhysicalKeyboardKey.minus; } - if (this == LogicalKeyboardKey.tilde) { - return PhysicalKeyboardKey.backquote; - } throw UnimplementedError(); } } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart index e396dbbf03..eceb429894 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart @@ -70,7 +70,7 @@ void main() async { }); // https://github.com/AppFlowy-IO/AppFlowy/issues/1763 - // [Bug] Mouse unable to click a certain area #1763 + // // [Bug] Mouse unable to click a certain area #1763 testWidgets('insert a new checkbox after an exsiting checkbox', (tester) async { // Before diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart index 99a4674efd..6b392cdebb 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart @@ -10,40 +10,44 @@ void main() async { }); group('selection_menu_widget.dart', () { - for (var i = 0; i < defaultSelectionMenuItems.length; i += 1) { - testWidgets('Selects number.$i item in selection menu with enter', - (tester) async { - final editor = await _prepare(tester); - for (var j = 0; j < i; j++) { - await editor.pressLogicKey(LogicalKeyboardKey.arrowDown); - } + // const i = defaultSelectionMenuItems.length; + // + // Because the `defaultSelectionMenuItems` uses localization, + // and the MaterialApp has not been initialized at the time of getting the value, + // it will crash. + // + // Use const value temporarily instead. + const i = 7; + testWidgets('Selects number.$i item in selection menu with keyboard', + (tester) async { + final editor = await _prepare(tester); + for (var j = 0; j < i; j++) { + await editor.pressLogicKey(LogicalKeyboardKey.arrowDown); + } - await editor.pressLogicKey(LogicalKeyboardKey.enter); - expect( - find.byType(SelectionMenuWidget, skipOffstage: false), - findsNothing, - ); - if (defaultSelectionMenuItems[i].name() != 'Image') { - await _testDefaultSelectionMenuItems(i, editor); - } - }); + await editor.pressLogicKey(LogicalKeyboardKey.enter); + expect( + find.byType(SelectionMenuWidget, skipOffstage: false), + findsNothing, + ); + if (defaultSelectionMenuItems[i].name != 'Image') { + await _testDefaultSelectionMenuItems(i, editor); + } + }); - testWidgets('Selects number.$i item in selection menu with click', - (tester) async { - final editor = await _prepare(tester); - - await tester.tap(find.byType(SelectionMenuItemWidget).at(i)); - await tester.pumpAndSettle(); - - expect( - find.byType(SelectionMenuWidget, skipOffstage: false), - findsNothing, - ); - if (defaultSelectionMenuItems[i].name() != 'Image') { - await _testDefaultSelectionMenuItems(i, editor); - } - }); - } + testWidgets('Selects number.$i item in selection menu with clicking', + (tester) async { + final editor = await _prepare(tester); + await tester.tap(find.byType(SelectionMenuItemWidget).at(i)); + await tester.pumpAndSettle(); + expect( + find.byType(SelectionMenuWidget, skipOffstage: false), + findsNothing, + ); + if (defaultSelectionMenuItems[i].name != 'Image') { + await _testDefaultSelectionMenuItems(i, editor); + } + }); testWidgets('Search item in selection menu util no results', (tester) async { @@ -136,7 +140,7 @@ Future _prepare(WidgetTester tester) async { ); for (final item in defaultSelectionMenuItems) { - expect(find.text(item.name()), findsOneWidget); + expect(find.text(item.name), findsOneWidget); } return Future.value(editor); @@ -146,28 +150,31 @@ Future _testDefaultSelectionMenuItems( int index, EditorWidgetTester editor) async { expect(editor.documentLength, 4); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); + expect((editor.nodeAtPath([0]) as TextNode).toPlainText(), + 'Welcome to Appflowy 😁'); expect((editor.nodeAtPath([1]) as TextNode).toPlainText(), 'Welcome to Appflowy 😁'); final node = editor.nodeAtPath([2]); final item = defaultSelectionMenuItems[index]; - final itemName = item.name(); - if (itemName == 'Text') { + if (item.name == 'Text') { expect(node?.subtype == null, true); - } else if (itemName == 'Heading 1') { + expect(node?.toString(), null); + } else if (item.name == 'Heading 1') { expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.attributes.heading, BuiltInAttributeKey.h1); - } else if (itemName == 'Heading 2') { + expect(node?.toString(), null); + } else if (item.name == 'Heading 2') { expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.attributes.heading, BuiltInAttributeKey.h2); - } else if (itemName == 'Heading 3') { + expect(node?.toString(), null); + } else if (item.name == 'Heading 3') { expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.attributes.heading, BuiltInAttributeKey.h3); - } else if (itemName == 'Bulleted list') { + expect(node?.toString(), null); + } else if (item.name == 'Bulleted list') { expect(node?.subtype, BuiltInAttributeKey.bulletedList); - } else if (itemName == 'Checkbox') { + } else if (item.name == 'Checkbox') { expect(node?.subtype, BuiltInAttributeKey.checkbox); expect(node?.attributes.check, false); - } else if (itemName == 'Quote') { - expect(node?.subtype, BuiltInAttributeKey.quote); } } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart deleted file mode 100644 index f39d58e6a3..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_handler_test.dart +++ /dev/null @@ -1,277 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import '../../infra/test_editor.dart'; - -void main() async { - setUpAll(() { - TestWidgetsFlutterBinding.ensureInitialized(); - }); - - group('markdown_syntax_to_styled_text_handler.dart', () { - group('convert double asterisks to bold', () { - Future insertAsterisk( - EditorWidgetTester editor, { - int repeat = 1, - }) async { - for (var i = 0; i < repeat; i++) { - await editor.pressLogicKey( - LogicalKeyboardKey.asterisk, - isShiftPressed: true, - ); - } - } - - testWidgets('**AppFlowy** to bold AppFlowy', (tester) async { - const text = '**AppFlowy*'; - final editor = tester.editor..insertTextNode(''); - await editor.startTesting(); - await editor.updateSelection( - Selection.single(path: [0], startOffset: 0), - ); - final textNode = editor.nodeAtPath([0]) as TextNode; - for (var i = 0; i < text.length; i++) { - await editor.insertText(textNode, text[i], i); - } - await insertAsterisk(editor); - final allBold = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: textNode.toPlainText().length, - ), - ); - expect(allBold, true); - expect(textNode.toPlainText(), 'AppFlowy'); - }); - - testWidgets('App**Flowy** to bold AppFlowy', (tester) async { - const text = 'App**Flowy*'; - final editor = tester.editor..insertTextNode(''); - await editor.startTesting(); - await editor.updateSelection( - Selection.single(path: [0], startOffset: 0), - ); - final textNode = editor.nodeAtPath([0]) as TextNode; - for (var i = 0; i < text.length; i++) { - await editor.insertText(textNode, text[i], i); - } - await insertAsterisk(editor); - final allBold = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: 3, - endOffset: textNode.toPlainText().length, - ), - ); - expect(allBold, true); - expect(textNode.toPlainText(), 'AppFlowy'); - }); - - testWidgets('***AppFlowy** to bold *AppFlowy', (tester) async { - const text = '***AppFlowy*'; - final editor = tester.editor..insertTextNode(''); - await editor.startTesting(); - await editor.updateSelection( - Selection.single(path: [0], startOffset: 0), - ); - final textNode = editor.nodeAtPath([0]) as TextNode; - for (var i = 0; i < text.length; i++) { - await editor.insertText(textNode, text[i], i); - } - await insertAsterisk(editor); - final allBold = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: 1, - endOffset: textNode.toPlainText().length, - ), - ); - expect(allBold, true); - expect(textNode.toPlainText(), '*AppFlowy'); - }); - - testWidgets('**AppFlowy** application to bold AppFlowy only', - (tester) async { - const boldText = '**AppFlowy*'; - final editor = tester.editor..insertTextNode(''); - await editor.startTesting(); - await editor.updateSelection( - Selection.single(path: [0], startOffset: 0), - ); - final textNode = editor.nodeAtPath([0]) as TextNode; - - for (var i = 0; i < boldText.length; i++) { - await editor.insertText(textNode, boldText[i], i); - } - await insertAsterisk(editor); - final boldTextLength = boldText.replaceAll('*', '').length; - final appFlowyBold = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: boldTextLength, - ), - ); - expect(appFlowyBold, true); - expect(textNode.toPlainText(), 'AppFlowy'); - }); - - testWidgets('**** nothing changes', (tester) async { - const text = '***'; - final editor = tester.editor..insertTextNode(''); - await editor.startTesting(); - await editor.updateSelection( - Selection.single(path: [0], startOffset: 0), - ); - final textNode = editor.nodeAtPath([0]) as TextNode; - for (var i = 0; i < text.length; i++) { - await editor.insertText(textNode, text[i], i); - } - await insertAsterisk(editor); - final allBold = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: textNode.toPlainText().length, - ), - ); - expect(allBold, false); - expect(textNode.toPlainText(), text); - }); - }); - - group('convert double underscores to bold', () { - Future insertUnderscore( - EditorWidgetTester editor, { - int repeat = 1, - }) async { - for (var i = 0; i < repeat; i++) { - await editor.pressLogicKey( - LogicalKeyboardKey.underscore, - isShiftPressed: true, - ); - } - } - - testWidgets('__AppFlowy__ to bold AppFlowy', (tester) async { - const text = '__AppFlowy_'; - final editor = tester.editor..insertTextNode(''); - await editor.startTesting(); - await editor.updateSelection( - Selection.single(path: [0], startOffset: 0), - ); - final textNode = editor.nodeAtPath([0]) as TextNode; - for (var i = 0; i < text.length; i++) { - await editor.insertText(textNode, text[i], i); - } - await insertUnderscore(editor); - final allBold = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: textNode.toPlainText().length, - ), - ); - expect(allBold, true); - expect(textNode.toPlainText(), 'AppFlowy'); - }); - - testWidgets('App__Flowy__ to bold AppFlowy', (tester) async { - const text = 'App__Flowy_'; - final editor = tester.editor..insertTextNode(''); - await editor.startTesting(); - await editor.updateSelection( - Selection.single(path: [0], startOffset: 0), - ); - final textNode = editor.nodeAtPath([0]) as TextNode; - for (var i = 0; i < text.length; i++) { - await editor.insertText(textNode, text[i], i); - } - await insertUnderscore(editor); - final allBold = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: 3, - endOffset: textNode.toPlainText().length, - ), - ); - expect(allBold, true); - expect(textNode.toPlainText(), 'AppFlowy'); - }); - - testWidgets('___AppFlowy__ to bold _AppFlowy', (tester) async { - const text = '___AppFlowy_'; - final editor = tester.editor..insertTextNode(''); - await editor.startTesting(); - await editor.updateSelection( - Selection.single(path: [0], startOffset: 0), - ); - final textNode = editor.nodeAtPath([0]) as TextNode; - for (var i = 0; i < text.length; i++) { - await editor.insertText(textNode, text[i], i); - } - await insertUnderscore(editor); - final allBold = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: 1, - endOffset: textNode.toPlainText().length, - ), - ); - expect(allBold, true); - expect(textNode.toPlainText(), '_AppFlowy'); - }); - - testWidgets('__AppFlowy__ application to bold AppFlowy only', - (tester) async { - const boldText = '__AppFlowy_'; - final editor = tester.editor..insertTextNode(''); - await editor.startTesting(); - await editor.updateSelection( - Selection.single(path: [0], startOffset: 0), - ); - final textNode = editor.nodeAtPath([0]) as TextNode; - - for (var i = 0; i < boldText.length; i++) { - await editor.insertText(textNode, boldText[i], i); - } - await insertUnderscore(editor); - final boldTextLength = boldText.replaceAll('_', '').length; - final appFlowyBold = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: boldTextLength, - ), - ); - expect(appFlowyBold, true); - expect(textNode.toPlainText(), 'AppFlowy'); - }); - - testWidgets('____ nothing changes', (tester) async { - const text = '___'; - final editor = tester.editor..insertTextNode(''); - await editor.startTesting(); - await editor.updateSelection( - Selection.single(path: [0], startOffset: 0), - ); - final textNode = editor.nodeAtPath([0]) as TextNode; - for (var i = 0; i < text.length; i++) { - await editor.insertText(textNode, text[i], i); - } - await insertUnderscore(editor); - final allBold = textNode.allSatisfyBoldInSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: textNode.toPlainText().length, - ), - ); - expect(allBold, false); - expect(textNode.toPlainText(), text); - }); - }); - }); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart index 662c7982b4..219fd07568 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart @@ -257,4 +257,192 @@ void main() async { }); }); }); + + group('convert double asterisk to bold', () { + Future insertAsterisk( + EditorWidgetTester editor, { + int repeat = 1, + }) async { + for (var i = 0; i < repeat; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.digit8, + isShiftPressed: true, + ); + } + } + + testWidgets('**AppFlowy** to bold AppFlowy', ((widgetTester) async { + const text = '**AppFlowy*'; + final editor = widgetTester.editor..insertTextNode(''); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + + await insertAsterisk(editor); + + final allBold = textNode.allSatisfyBoldInSelection(Selection.single( + path: [0], startOffset: 0, endOffset: textNode.toPlainText().length)); + + expect(allBold, true); + expect(textNode.toPlainText(), 'AppFlowy'); + })); + + testWidgets('App**Flowy** to bold AppFlowy', ((widgetTester) async { + const text = 'App**Flowy*'; + final editor = widgetTester.editor..insertTextNode(''); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + + await insertAsterisk(editor); + + final allBold = textNode.allSatisfyBoldInSelection(Selection.single( + path: [0], startOffset: 3, endOffset: textNode.toPlainText().length)); + + expect(allBold, true); + expect(textNode.toPlainText(), 'AppFlowy'); + })); + + testWidgets('***AppFlowy** to bold *AppFlowy', ((widgetTester) async { + const text = '***AppFlowy*'; + final editor = widgetTester.editor..insertTextNode(''); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + + await insertAsterisk(editor); + + final allBold = textNode.allSatisfyBoldInSelection(Selection.single( + path: [0], startOffset: 1, endOffset: textNode.toPlainText().length)); + + expect(allBold, true); + expect(textNode.toPlainText(), '*AppFlowy'); + })); + + testWidgets('**** nothing changes', ((widgetTester) async { + const text = '***'; + final editor = widgetTester.editor..insertTextNode(''); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + + await insertAsterisk(editor); + + final allBold = textNode.allSatisfyBoldInSelection(Selection.single( + path: [0], startOffset: 0, endOffset: textNode.toPlainText().length)); + + expect(allBold, false); + expect(textNode.toPlainText(), text); + })); + }); + + group('convert double underscore to bold', () { + Future insertUnderscore( + EditorWidgetTester editor, { + int repeat = 1, + }) async { + for (var i = 0; i < repeat; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.underscore, + isShiftPressed: true, + ); + } + } + + testWidgets('__AppFlowy__ to bold AppFlowy', ((widgetTester) async { + const text = '__AppFlowy_'; + final editor = widgetTester.editor..insertTextNode(''); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + + await insertUnderscore(editor); + + final allBold = textNode.allSatisfyBoldInSelection(Selection.single( + path: [0], startOffset: 0, endOffset: textNode.toPlainText().length)); + + expect(allBold, true); + expect(textNode.toPlainText(), 'AppFlowy'); + })); + + testWidgets('App__Flowy__ to bold AppFlowy', ((widgetTester) async { + const text = 'App__Flowy_'; + final editor = widgetTester.editor..insertTextNode(''); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + + await insertUnderscore(editor); + + final allBold = textNode.allSatisfyBoldInSelection(Selection.single( + path: [0], startOffset: 3, endOffset: textNode.toPlainText().length)); + + expect(allBold, true); + expect(textNode.toPlainText(), 'AppFlowy'); + })); + + testWidgets('__*AppFlowy__ to bold *AppFlowy', ((widgetTester) async { + const text = '__*AppFlowy_'; + final editor = widgetTester.editor..insertTextNode(''); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + + await insertUnderscore(editor); + + final allBold = textNode.allSatisfyBoldInSelection(Selection.single( + path: [0], startOffset: 1, endOffset: textNode.toPlainText().length)); + + expect(allBold, true); + expect(textNode.toPlainText(), '*AppFlowy'); + })); + + testWidgets('____ nothing changes', ((widgetTester) async { + const text = '___'; + final editor = widgetTester.editor..insertTextNode(''); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + + await insertUnderscore(editor); + + final allBold = textNode.allSatisfyBoldInSelection(Selection.single( + path: [0], startOffset: 0, endOffset: textNode.toPlainText().length)); + + expect(allBold, false); + expect(textNode.toPlainText(), text); + })); + }); } diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart index c00036ba11..a6e08d5fa8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart @@ -29,7 +29,7 @@ void main() async { ); for (final item in defaultSelectionMenuItems) { - expect(find.text(item.name()), findsOneWidget); + expect(find.text(item.name), findsOneWidget); } await editor.updateSelection(Selection.single(path: [1], startOffset: 0)); diff --git a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/code_block/code_block_shortcut_event.dart b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/code_block/code_block_shortcut_event.dart index d23d22a78d..d883c1a632 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/code_block/code_block_shortcut_event.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/code_block/code_block_shortcut_event.dart @@ -79,7 +79,7 @@ ShortcutEventHandler _pasteHandler = (editorState, event) { }; SelectionMenuItem codeBlockMenuItem = SelectionMenuItem( - name: () => 'Code Block', + name: 'Code Block', icon: (editorState, onSelected) => Icon( Icons.abc, color: onSelected diff --git a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/divider/divider_shortcut_event.dart b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/divider/divider_shortcut_event.dart index 2d4ad39bf1..96baec98b5 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/divider/divider_shortcut_event.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/divider/divider_shortcut_event.dart @@ -34,7 +34,7 @@ ShortcutEventHandler _insertDividerHandler = (editorState, event) { }; SelectionMenuItem dividerMenuItem = SelectionMenuItem( - name: () => 'Divider', + name: 'Divider', icon: (editorState, onSelected) => Icon( Icons.horizontal_rule, color: onSelected diff --git a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart index 810d497541..34720d16f4 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/emoji_picker/emoji_menu_item.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'emoji_picker.dart'; SelectionMenuItem emojiMenuItem = SelectionMenuItem( - name: () => 'Emoji', + name: 'Emoji', icon: (editorState, onSelected) => Icon( Icons.emoji_emotions_outlined, color: onSelected diff --git a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/math_ equation/math_equation_node_widget.dart b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/math_ equation/math_equation_node_widget.dart index 5485baa6c2..667ae2374a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/math_ equation/math_equation_node_widget.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor_plugins/lib/src/math_ equation/math_equation_node_widget.dart @@ -7,7 +7,7 @@ const String kMathEquationType = 'math_equation'; const String kMathEquationAttr = 'math_equation'; SelectionMenuItem mathEquationMenuItem = SelectionMenuItem( - name: () => 'Math Equation', + name: 'Math Equation', icon: (editorState, onSelected) => Icon( Icons.text_fields_rounded, color: onSelected From 77ff2e987a9e54d6b2099297ce663d75e2fa40e5 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 9 Mar 2023 14:25:33 +0800 Subject: [PATCH 08/15] chore: include time per cell (#1901) * style: autoformat * chore: add include_time to cell data * chore: remove include_time from date field type options * chore: fix tests * chore: custom deserializer for date cell data * chore: add more tests * chore: simplify date calculation logic * chore: move include time to per-cell setting in UI * test: add another text str test * chore: adapt changes from upstream --- .../cell/cell_controller_builder.dart | 2 +- .../cell/cell_data_persistence.dart | 14 +- .../row/cells/date_cell/date_cal_bloc.dart | 85 +++++----- .../row/cells/date_cell/date_editor.dart | 11 +- .../flowy-database/src/event_handler.rs | 1 + .../src/services/cell/cell_operation.rs | 7 +- .../src/services/database_view/editor.rs | 5 +- .../date_type_option/date_tests.rs | 136 +++++++++++++--- .../date_type_option/date_type_option.rs | 145 ++++++++---------- .../date_type_option_entities.rs | 97 ++++++++++-- .../text_type_option/text_tests.rs | 15 ++ .../tests/database/block_test/util.rs | 1 + .../tests/database/field_test/util.rs | 1 + 13 files changed, 345 insertions(+), 175 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart index d80142e3f8..42c29dc3e4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart @@ -13,7 +13,7 @@ typedef SelectOptionCellController = CellController; typedef ChecklistCellController = CellController; -typedef DateCellController = CellController; +typedef DateCellController = CellController; typedef URLCellController = CellController; class CellControllerBuilder { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart index f2b06c51c7..f0cb6531fa 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart @@ -27,24 +27,28 @@ class TextCellDataPersistence implements CellDataPersistence { } @freezed -class CalendarData with _$CalendarData { - const factory CalendarData({required DateTime date, String? time}) = - _CalendarData; +class DateCellData with _$DateCellData { + const factory DateCellData({ + required DateTime date, + String? time, + required bool includeTime, + }) = _DateCellData; } -class DateCellDataPersistence implements CellDataPersistence { +class DateCellDataPersistence implements CellDataPersistence { final CellIdentifier cellId; DateCellDataPersistence({ required this.cellId, }); @override - Future> save(CalendarData data) { + Future> save(DateCellData data) { var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId); final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString(); payload.date = date; payload.isUtc = data.date.isUtc; + payload.includeTime = data.includeTime; if (data.time != null) { payload.time = data.time!; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart index 486ce5b397..30c61c0636 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart @@ -15,7 +15,7 @@ import 'package:table_calendar/table_calendar.dart'; import 'dart:async'; import 'package:dartz/dartz.dart'; import 'package:protobuf/protobuf.dart'; -import 'package:fixnum/fixnum.dart' as $fixnum; + part 'date_cal_bloc.freezed.dart'; class DateCellCalendarBloc @@ -42,13 +42,13 @@ class DateCellCalendarBloc emit(state.copyWith(focusedDay: focusedDay)); }, didReceiveCellUpdate: (DateCellDataPB? cellData) { - final calData = calDataFromCellData(cellData); - final time = calData.foldRight( + final dateCellData = calDataFromCellData(cellData); + final time = dateCellData.foldRight( "", (dateData, previous) => dateData.time ?? ''); - emit(state.copyWith(calData: calData, time: time)); + emit(state.copyWith(dateCellData: dateCellData, time: time)); }, setIncludeTime: (includeTime) async { - await _updateTypeOption(emit, includeTime: includeTime); + await _updateDateData(emit, includeTime: includeTime); }, setDateFormat: (dateFormat) async { await _updateTypeOption(emit, dateFormat: dateFormat); @@ -57,14 +57,14 @@ class DateCellCalendarBloc await _updateTypeOption(emit, timeFormat: timeFormat); }, setTime: (time) async { - if (state.calData.isSome()) { + if (state.dateCellData.isSome()) { await _updateDateData(emit, time: time); } }, didUpdateCalData: - (Option data, Option timeFormatError) { + (Option data, Option timeFormatError) { emit(state.copyWith( - calData: data, timeFormatError: timeFormatError)); + dateCellData: data, timeFormatError: timeFormatError)); }, ); }, @@ -72,9 +72,13 @@ class DateCellCalendarBloc } Future _updateDateData(Emitter emit, - {DateTime? date, String? time}) { - final CalendarData newDateData = state.calData.fold( - () => CalendarData(date: date ?? DateTime.now(), time: time), + {DateTime? date, String? time, bool? includeTime}) { + final DateCellData newDateData = state.dateCellData.fold( + () => DateCellData( + date: date ?? DateTime.now(), + time: time, + includeTime: includeTime ?? false, + ), (dateData) { var newDateData = dateData; if (date != null && !isSameDay(newDateData.date, date)) { @@ -84,6 +88,11 @@ class DateCellCalendarBloc if (newDateData.time != time) { newDateData = newDateData.copyWith(time: time); } + + if (includeTime != null && newDateData.includeTime != includeTime) { + newDateData = newDateData.copyWith(includeTime: includeTime); + } + return newDateData; }, ); @@ -92,15 +101,16 @@ class DateCellCalendarBloc } Future _saveDateData( - Emitter emit, CalendarData newCalData) async { - if (state.calData == Some(newCalData)) { + Emitter emit, DateCellData newCalData) async { + if (state.dateCellData == Some(newCalData)) { return; } updateCalData( - Option calData, Option timeFormatError) { + Option dateCellData, Option timeFormatError) { if (!isClosed) { - add(DateCellCalendarEvent.didUpdateCalData(calData, timeFormatError)); + add(DateCellCalendarEvent.didUpdateCalData( + dateCellData, timeFormatError)); } } @@ -110,7 +120,7 @@ class DateCellCalendarBloc (err) { switch (ErrorCode.valueOf(err.code)!) { case ErrorCode.InvalidDateTimeFormat: - updateCalData(state.calData, Some(timeFormatPrompt(err))); + updateCalData(state.dateCellData, Some(timeFormatPrompt(err))); break; default: Log.error(err); @@ -159,7 +169,6 @@ class DateCellCalendarBloc Emitter emit, { DateFormat? dateFormat, TimeFormat? timeFormat, - bool? includeTime, }) async { state.dateTypeOptionPB.freeze(); final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) { @@ -170,10 +179,6 @@ class DateCellCalendarBloc if (timeFormat != null) { typeOption.timeFormat = timeFormat; } - - if (includeTime != null) { - typeOption.includeTime = includeTime; - } }); final result = await FieldBackendService.updateFieldTypeOption( @@ -208,7 +213,7 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent { const factory DateCellCalendarEvent.didReceiveCellUpdate( DateCellDataPB? data) = _DidReceiveCellUpdate; const factory DateCellCalendarEvent.didUpdateCalData( - Option data, Option timeFormatError) = + Option data, Option timeFormatError) = _DidUpdateCalData; } @@ -219,7 +224,7 @@ class DateCellCalendarState with _$DateCellCalendarState { required CalendarFormat format, required DateTime focusedDay, required Option timeFormatError, - required Option calData, + required Option dateCellData, required String? time, required String timeHintText, }) = _DateCellCalendarState; @@ -228,15 +233,15 @@ class DateCellCalendarState with _$DateCellCalendarState { DateTypeOptionPB dateTypeOptionPB, DateCellDataPB? cellData, ) { - Option calData = calDataFromCellData(cellData); + Option dateCellData = calDataFromCellData(cellData); final time = - calData.foldRight("", (dateData, previous) => dateData.time ?? ''); + dateCellData.foldRight("", (dateData, previous) => dateData.time ?? ''); return DateCellCalendarState( dateTypeOptionPB: dateTypeOptionPB, format: CalendarFormat.month, focusedDay: DateTime.now(), time: time, - calData: calData, + dateCellData: dateCellData, timeFormatError: none(), timeHintText: _timeHintText(dateTypeOptionPB), ); @@ -249,30 +254,30 @@ String _timeHintText(DateTypeOptionPB typeOption) { return LocaleKeys.document_date_timeHintTextInTwelveHour.tr(); case TimeFormat.TwentyFourHour: return LocaleKeys.document_date_timeHintTextInTwentyFourHour.tr(); + default: + return ""; } - return ""; } -Option calDataFromCellData(DateCellDataPB? cellData) { +Option calDataFromCellData(DateCellDataPB? cellData) { String? time = timeFromCellData(cellData); - Option calData = none(); + Option dateData = none(); if (cellData != null) { final timestamp = cellData.timestamp * 1000; final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt()); - calData = Some(CalendarData(date: date, time: time)); + dateData = Some(DateCellData( + date: date, + time: time, + includeTime: cellData.includeTime, + )); } - return calData; -} - -$fixnum.Int64 timestampFromDateTime(DateTime dateTime) { - final timestamp = (dateTime.millisecondsSinceEpoch ~/ 1000); - return $fixnum.Int64(timestamp); + return dateData; } String? timeFromCellData(DateCellDataPB? cellData) { - String? time; - if (cellData?.hasTime() ?? false) { - time = cellData?.time; + if (cellData == null || !cellData.hasTime()) { + return null; } - return time; + + return cellData.time; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart index 33375f7778..5a49415d4c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart @@ -111,12 +111,14 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> { child: BlocBuilder( buildWhen: (p, c) => p != c, builder: (context, state) { + bool includeTime = state.dateCellData + .fold(() => false, (dateData) => dateData.includeTime); List children = [ Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: _buildCalendar(context), ), - if (state.dateTypeOptionPB.includeTime) ...[ + if (includeTime) ...[ const VSpace(12.0), _TimeTextField( bloc: context.read(), @@ -206,7 +208,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> { textStyle.textColor(Theme.of(context).disabledColor), ), selectedDayPredicate: (day) { - return state.calData.fold( + return state.dateCellData.fold( () => false, (dateData) => isSameDay(dateData.date, day), ); @@ -238,7 +240,10 @@ class _IncludeTimeButton extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( - selector: (state) => state.dateTypeOptionPB.includeTime, + selector: (state) => state.dateCellData.fold( + () => false, + (dateData) => dateData.includeTime, + ), builder: (context, includeTime) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), diff --git a/frontend/rust-lib/flowy-database/src/event_handler.rs b/frontend/rust-lib/flowy-database/src/event_handler.rs index 30c52a4ac0..3e73790bb9 100644 --- a/frontend/rust-lib/flowy-database/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database/src/event_handler.rs @@ -517,6 +517,7 @@ pub(crate) async fn update_date_cell_handler( let cell_changeset = DateCellChangeset { date: data.date, time: data.time, + include_time: data.include_time, is_utc: data.is_utc, }; diff --git a/frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs index 7c66f69572..b519a2943e 100644 --- a/frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs @@ -39,7 +39,7 @@ pub trait CellDataChangeset: TypeOption { /// The changeset is able to parse into the concrete data struct if `TypeOption::CellChangeset` /// implements the `FromCellChangesetString` trait. /// For example,the SelectOptionCellChangeset,DateCellChangeset. etc. - /// + /// fn apply_changeset( &self, changeset: ::CellChangeset, @@ -142,7 +142,7 @@ where /// Decode the opaque cell data from one field type to another using the corresponding `TypeOption` /// -/// The cell data might become an empty string depends on the to_field_type's `TypeOption` +/// The cell data might become an empty string depends on the to_field_type's `TypeOption` /// support transform the from_field_type's cell data or not. /// /// # Arguments @@ -252,6 +252,7 @@ pub fn insert_date_cell(timestamp: i64, field_rev: &FieldRevision) -> CellRevisi let cell_data = serde_json::to_string(&DateCellChangeset { date: Some(timestamp.to_string()), time: None, + include_time: Some(false), is_utc: true, }) .unwrap(); @@ -279,7 +280,7 @@ pub fn delete_select_option_cell( CellRevision::new(data) } -/// Deserialize the String into cell specific data type. +/// Deserialize the String into cell specific data type. pub trait FromCellString { fn from_cell_str(s: &str) -> FlowyResult where diff --git a/frontend/rust-lib/flowy-database/src/services/database_view/editor.rs b/frontend/rust-lib/flowy-database/src/services/database_view/editor.rs index 4b4eb58a6a..70188e5031 100644 --- a/frontend/rust-lib/flowy-database/src/services/database_view/editor.rs +++ b/frontend/rust-lib/flowy-database/src/services/database_view/editor.rs @@ -862,7 +862,8 @@ impl DatabaseViewEditor { let timestamp = date_cell .into_date_field_cell_data() .unwrap_or_default() - .into(); + .timestamp + .unwrap_or_default(); Some(CalendarEventPB { row_id: row_id.to_string(), @@ -896,7 +897,7 @@ impl DatabaseViewEditor { // timestamp let timestamp = date_cell .into_date_field_cell_data() - .map(|date_cell_data| date_cell_data.0.unwrap_or_default()) + .map(|date_cell_data| date_cell_data.timestamp.unwrap_or_default()) .unwrap_or_default(); (row_id, timestamp) diff --git a/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_tests.rs index 78c4d7e08e..afaa230d50 100644 --- a/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_tests.rs +++ b/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_tests.rs @@ -3,8 +3,9 @@ mod tests { use crate::entities::FieldType; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; - use crate::services::field::*; - // use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOptionPB, TimeFormat}; + use crate::services::field::{ + DateCellChangeset, DateFormat, DateTypeOptionPB, FieldBuilder, TimeFormat, TypeOptionCellData, + }; use chrono::format::strftime::StrftimeItems; use chrono::{FixedOffset, NaiveDateTime}; use database_model::FieldRevision; @@ -18,16 +19,44 @@ mod tests { type_option.date_format = date_format; match date_format { DateFormat::Friendly => { - assert_date(&type_option, 1647251762, None, "Mar 14,2022", &field_rev); + assert_date( + &type_option, + 1647251762, + None, + "Mar 14,2022", + false, + &field_rev, + ); }, DateFormat::US => { - assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev); + assert_date( + &type_option, + 1647251762, + None, + "2022/03/14", + false, + &field_rev, + ); }, DateFormat::ISO => { - assert_date(&type_option, 1647251762, None, "2022-03-14", &field_rev); + assert_date( + &type_option, + 1647251762, + None, + "2022-03-14", + false, + &field_rev, + ); }, DateFormat::Local => { - assert_date(&type_option, 1647251762, None, "03/14/2022", &field_rev); + assert_date( + &type_option, + 1647251762, + None, + "03/14/2022", + false, + &field_rev, + ); }, } } @@ -41,25 +70,56 @@ mod tests { for time_format in TimeFormat::iter() { type_option.time_format = time_format; - type_option.include_time = true; match time_format { TimeFormat::TwentyFourHour => { - assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev); + assert_date( + &type_option, + 1653609600, + None, + "May 27,2022 00:00", + true, + &field_rev, + ); + assert_date( + &type_option, + 1653609600, + Some("9:00".to_owned()), + "May 27,2022 09:00", + true, + &field_rev, + ); assert_date( &type_option, 1653609600, Some("23:00".to_owned()), "May 27,2022 23:00", + true, &field_rev, ); }, TimeFormat::TwelveHour => { - assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev); + assert_date( + &type_option, + 1653609600, + None, + "May 27,2022 12:00 AM", + true, + &field_rev, + ); + assert_date( + &type_option, + 1653609600, + Some("9:00 AM".to_owned()), + "May 27,2022 09:00 AM", + true, + &field_rev, + ); assert_date( &type_option, 1653609600, Some("11:23 pm".to_owned()), "May 27,2022 11:23 PM", + true, &field_rev, ); }, @@ -72,14 +132,13 @@ mod tests { let type_option = DateTypeOptionPB::default(); let field_type = FieldType::DateTime; let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_date(&type_option, "abc", None, "", &field_rev); + assert_date(&type_option, "abc", None, "", false, &field_rev); } #[test] #[should_panic] fn date_type_option_invalid_include_time_str_test() { - let mut type_option = DateTypeOptionPB::new(); - type_option.include_time = true; + let type_option = DateTypeOptionPB::new(); let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); assert_date( @@ -87,31 +146,46 @@ mod tests { 1653609600, Some("1:".to_owned()), "May 27,2022 01:00", + true, &field_rev, ); } #[test] fn date_type_option_empty_include_time_str_test() { - let mut type_option = DateTypeOptionPB::new(); - type_option.include_time = true; + let type_option = DateTypeOptionPB::new(); let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); assert_date( &type_option, 1653609600, Some("".to_owned()), - "May 27,2022", + "May 27,2022 00:00", + true, &field_rev, ); } - /// The default time format is TwentyFourHour, so the include_time_str in twelve_hours_format will cause parser error. + #[test] + fn date_type_midnight_include_time_str_test() { + let type_option = DateTypeOptionPB::new(); + let field_type = FieldType::DateTime; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_date( + &type_option, + 1653609600, + Some("00:00".to_owned()), + "May 27,2022 00:00", + true, + &field_rev, + ); + } + + /// The default time format is TwentyFourHour, so the include_time_str in twelve_hours_format will cause parser error. #[test] #[should_panic] fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() { - let mut type_option = DateTypeOptionPB::new(); - type_option.include_time = true; + let type_option = DateTypeOptionPB::new(); let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); assert_date( @@ -119,6 +193,25 @@ mod tests { 1653609600, Some("1:00 am".to_owned()), "May 27,2022 01:00 AM", + true, + &field_rev, + ); + } + + // Attempting to parse include_time_str as TwelveHour when TwentyFourHour format is given should cause parser error. + #[test] + #[should_panic] + fn date_type_option_twenty_four_hours_include_time_str_in_twelve_hours_format() { + let mut type_option = DateTypeOptionPB::new(); + type_option.time_format = TimeFormat::TwelveHour; + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); + + assert_date( + &type_option, + 1653609600, + Some("20:00".to_owned()), + "May 27,2022 08:00 PM", + true, &field_rev, ); } @@ -154,17 +247,19 @@ mod tests { timestamp: T, include_time_str: Option, expected_str: &str, + include_time: bool, field_rev: &FieldRevision, ) { let changeset = DateCellChangeset { date: Some(timestamp.to_string()), time: include_time_str, is_utc: false, + include_time: Some(include_time), }; let (cell_str, _) = type_option.apply_changeset(changeset, None).unwrap(); assert_eq!( - decode_cell_data(cell_str, type_option, field_rev), + decode_cell_data(cell_str, type_option, include_time, field_rev), expected_str.to_owned(), ); } @@ -172,13 +267,14 @@ mod tests { fn decode_cell_data( cell_str: String, type_option: &DateTypeOptionPB, + include_time: bool, field_rev: &FieldRevision, ) -> String { let decoded_data = type_option .decode_cell_str(cell_str, &FieldType::DateTime, field_rev) .unwrap(); let decoded_data = type_option.convert_to_protobuf(decoded_data); - if type_option.include_time { + if include_time { format!("{} {}", decoded_data.date, decoded_data.time) .trim_end() .to_owned() diff --git a/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option.rs index d3d351c9cd..431ee508cd 100644 --- a/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option.rs +++ b/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option.rs @@ -8,7 +8,7 @@ use crate::services::field::{ }; use bytes::Bytes; use chrono::format::strftime::StrftimeItems; -use chrono::{NaiveDateTime, Timelike}; +use chrono::NaiveDateTime; use database_model::{FieldRevision, TypeOptionDataDeserializer, TypeOptionDataSerializer}; use flowy_derive::ProtoBuf; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; @@ -58,97 +58,59 @@ impl DateTypeOptionPB { Self::default() } - fn today_desc_from_timestamp>(&self, timestamp: T) -> DateCellDataPB { - let timestamp = timestamp.into(); - let native = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0); - if native.is_none() { + fn today_desc_from_timestamp(&self, cell_data: DateCellData) -> DateCellDataPB { + let timestamp = cell_data.timestamp.unwrap_or_default(); + let include_time = cell_data.include_time; + + let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0); + if naive.is_none() { return DateCellDataPB::default(); } - let native = native.unwrap(); - if native.timestamp() == 0 { + let naive = naive.unwrap(); + if timestamp == 0 { return DateCellDataPB::default(); } - - let time = native.time(); - let has_time = time.hour() != 0 || time.second() != 0; - - let utc = self.utc_date_time_from_native(native); let fmt = self.date_format.format_str(); - let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt))); + let date = format!("{}", naive.format_with_items(StrftimeItems::new(fmt))); - let mut time = "".to_string(); - if has_time && self.include_time { - let fmt = format!( - "{}{}", - self.date_format.format_str(), - self.time_format.format_str() - ); - time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, ""); - } + let time = if include_time { + let fmt = self.time_format.format_str(); + format!("{}", naive.format_with_items(StrftimeItems::new(&fmt))) + } else { + "".to_string() + }; - let timestamp = native.timestamp(); DateCellDataPB { date, time, + include_time, timestamp, } } - fn date_fmt(&self, time: &Option) -> String { - if self.include_time { - match time.as_ref() { - None => self.date_format.format_str().to_string(), - Some(time_str) => { - if time_str.is_empty() { - self.date_format.format_str().to_string() - } else { - format!( - "{} {}", - self.date_format.format_str(), - self.time_format.format_str() - ) - } - }, - } - } else { - self.date_format.format_str().to_string() - } - } - fn timestamp_from_utc_with_time( &self, - utc: &chrono::DateTime, - time: &Option, + naive_date: &NaiveDateTime, + time_str: &Option, ) -> FlowyResult { - if let Some(time_str) = time.as_ref() { + if let Some(time_str) = time_str.as_ref() { if !time_str.is_empty() { - let date_str = format!( - "{}{}", - utc.format_with_items(StrftimeItems::new(self.date_format.format_str())), - &time_str - ); + let naive_time = + chrono::NaiveTime::parse_from_str(&time_str, self.time_format.format_str()); - return match NaiveDateTime::parse_from_str(&date_str, &self.date_fmt(time)) { - Ok(native) => { - let utc = self.utc_date_time_from_native(native); - Ok(utc.timestamp()) + match naive_time { + Ok(naive_time) => { + return Ok(naive_date.date().and_time(naive_time).timestamp()); }, Err(_e) => { - let msg = format!("Parse {} failed", date_str); - Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg)) + let msg = format!("Parse {} failed", time_str); + return Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg)); }, }; } } - Ok(utc.timestamp()) - } - - fn utc_date_time_from_native( - &self, - naive: chrono::NaiveDateTime, - ) -> chrono::DateTime { - chrono::DateTime::::from_utc(naive, chrono::Utc) + Ok(naive_date.timestamp()) } } @@ -181,25 +143,40 @@ impl CellDataChangeset for DateTypeOptionPB { fn apply_changeset( &self, changeset: ::CellChangeset, - _type_cell_data: Option, + type_cell_data: Option, ) -> FlowyResult<(String, ::CellData)> { - let cell_data = match changeset.date_timestamp() { - None => 0, - Some(date_timestamp) => match (self.include_time, changeset.time) { - (true, Some(time)) => { - let time = Some(time.trim().to_uppercase()); - let native = NaiveDateTime::from_timestamp_opt(date_timestamp, 0); - if let Some(native) = native { - let utc = self.utc_date_time_from_native(native); - self.timestamp_from_utc_with_time(&utc, &time)? - } else { - date_timestamp - } - }, - _ => date_timestamp, + let (timestamp, include_time) = match type_cell_data { + None => (None, false), + Some(type_cell_data) => { + let cell_data = DateCellData::from_cell_str(&type_cell_data.cell_str).unwrap_or_default(); + (cell_data.timestamp, cell_data.include_time) }, }; - let date_cell_data = DateCellData(Some(cell_data)); + + let include_time = match changeset.include_time { + None => include_time, + Some(include_time) => include_time, + }; + let timestamp = match changeset.date_timestamp() { + None => timestamp, + Some(date_timestamp) => match (include_time, changeset.time) { + (true, Some(time)) => { + let time = Some(time.trim().to_uppercase()); + let naive = NaiveDateTime::from_timestamp_opt(date_timestamp, 0); + if let Some(naive) = naive { + Some(self.timestamp_from_utc_with_time(&naive, &time)?) + } else { + Some(date_timestamp) + } + }, + _ => Some(date_timestamp), + }, + }; + + let date_cell_data = DateCellData { + timestamp, + include_time, + }; Ok((date_cell_data.to_string(), date_cell_data)) } } @@ -215,7 +192,7 @@ impl TypeOptionCellDataFilter for DateTypeOptionPB { return true; } - filter.is_visible(cell_data.0) + filter.is_visible(cell_data.timestamp) } } @@ -225,7 +202,7 @@ impl TypeOptionCellDataCompare for DateTypeOptionPB { cell_data: &::CellData, other_cell_data: &::CellData, ) -> Ordering { - match (cell_data.0, other_cell_data.0) { + match (cell_data.timestamp, other_cell_data.timestamp) { (Some(left), Some(right)) => left.cmp(&right), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, diff --git a/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option_entities.rs index fd11207bef..f1b888dcaf 100644 --- a/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -1,3 +1,5 @@ +use std::fmt; + use crate::entities::CellIdPB; use crate::services::cell::{ CellProtobufBlobParser, DecodedCellData, FromCellChangesetString, FromCellString, @@ -6,6 +8,7 @@ use crate::services::cell::{ use bytes::Bytes; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::{internal_error, FlowyResult}; +use serde::de::Visitor; use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; @@ -19,6 +22,9 @@ pub struct DateCellDataPB { #[pb(index = 3)] pub timestamp: i64, + + #[pb(index = 4)] + pub include_time: bool, } #[derive(Clone, Debug, Default, ProtoBuf)] @@ -32,7 +38,10 @@ pub struct DateChangesetPB { #[pb(index = 3, one_of)] pub time: Option, - #[pb(index = 4)] + #[pb(index = 4, one_of)] + pub include_time: Option, + + #[pb(index = 5)] pub is_utc: bool, } @@ -40,6 +49,7 @@ pub struct DateChangesetPB { pub struct DateCellChangeset { pub date: Option, pub time: Option, + pub include_time: Option, pub is_utc: bool, } @@ -71,18 +81,74 @@ impl ToCellChangesetString for DateCellChangeset { } } -#[derive(Default, Clone, Debug)] -pub struct DateCellData(pub Option); - -impl std::convert::From for i64 { - fn from(timestamp: DateCellData) -> Self { - timestamp.0.unwrap_or(0) - } +#[derive(Default, Clone, Debug, Serialize)] +pub struct DateCellData { + pub timestamp: Option, + pub include_time: bool, } -impl std::convert::From for Option { - fn from(timestamp: DateCellData) -> Self { - timestamp.0 +impl<'de> serde::Deserialize<'de> for DateCellData { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + struct DateCellVisitor(); + + impl<'de> Visitor<'de> for DateCellVisitor { + type Value = DateCellData; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "DateCellData with type: str containing either an integer timestamp or the JSON representation", + ) + } + + fn visit_i64(self, value: i64) -> Result + where + E: serde::de::Error, + { + Ok(DateCellData { + timestamp: Some(value), + include_time: false, + }) + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + self.visit_i64(value as i64) + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut timestamp: Option = None; + let mut include_time: Option = None; + + while let Some(key) = map.next_key()? { + match key { + "timestamp" => { + timestamp = map.next_value()?; + }, + "include_time" => { + include_time = map.next_value()?; + }, + _ => {}, + } + } + + let include_time = include_time.unwrap_or(false); + + Ok(DateCellData { + timestamp, + include_time, + }) + } + } + + deserializer.deserialize_any(DateCellVisitor()) } } @@ -91,17 +157,14 @@ impl FromCellString for DateCellData { where Self: Sized, { - let num = s.parse::().ok(); - Ok(DateCellData(num)) + let result: DateCellData = serde_json::from_str(s).unwrap(); + Ok(result) } } impl ToString for DateCellData { fn to_string(&self) -> String { - match self.0 { - None => "".to_string(), - Some(val) => val.to_string(), - } + serde_json::to_string(self).unwrap() } } diff --git a/frontend/rust-lib/flowy-database/src/services/field/type_options/text_type_option/text_tests.rs b/frontend/rust-lib/flowy-database/src/services/field/type_options/text_type_option/text_tests.rs index 0e63708c6c..4099338acc 100644 --- a/frontend/rust-lib/flowy-database/src/services/field/type_options/text_type_option/text_tests.rs +++ b/frontend/rust-lib/flowy-database/src/services/field/type_options/text_type_option/text_tests.rs @@ -22,6 +22,21 @@ mod tests { ), "Mar 14,2022" ); + + let data = DateCellData { + timestamp: Some(1647251762), + include_time: true, + }; + + assert_eq!( + stringify_cell_data( + data.to_string(), + &FieldType::RichText, + &field_type, + &field_rev + ), + "Mar 14,2022" + ); } // Test parser the cell data which field's type is FieldType::SingleSelect to cell data diff --git a/frontend/rust-lib/flowy-database/tests/database/block_test/util.rs b/frontend/rust-lib/flowy-database/tests/database/block_test/util.rs index 7f55ac277e..f2d1fc9a89 100644 --- a/frontend/rust-lib/flowy-database/tests/database/block_test/util.rs +++ b/frontend/rust-lib/flowy-database/tests/database/block_test/util.rs @@ -43,6 +43,7 @@ impl DatabaseRowTestBuilder { date: Some(data.to_string()), time: None, is_utc: true, + include_time: Some(false), }) .unwrap(); let date_field = self.field_rev_with_type(&FieldType::DateTime); diff --git a/frontend/rust-lib/flowy-database/tests/database/field_test/util.rs b/frontend/rust-lib/flowy-database/tests/database/field_test/util.rs index 6fa0a93036..d13145a14e 100644 --- a/frontend/rust-lib/flowy-database/tests/database/field_test/util.rs +++ b/frontend/rust-lib/flowy-database/tests/database/field_test/util.rs @@ -64,6 +64,7 @@ pub fn make_date_cell_string(s: &str) -> String { date: Some(s.to_string()), time: None, is_utc: true, + include_time: Some(false), }) .unwrap() } From b89c69f294e7f326f1a1f271a650a828def7f287 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 9 Mar 2023 13:32:49 +0700 Subject: [PATCH 09/15] fix: #1778 (#1946) * fix: [FR] The text formatting toolbar should appear after the selection #1778 * chore: format code --- .../lib/src/service/selection_service.dart | 52 ++++++++++++++++--- .../lib/src/service/toolbar_service.dart | 12 ++++- .../rich_text/toolbar_rich_text_test.dart | 12 +++++ .../format_style_handler_test.dart | 1 + .../test/service/toolbar_service_test.dart | 8 +++ 5 files changed, 75 insertions(+), 10 deletions(-) diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart index 6e63de8747..b522aa9cef 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:appflowy_editor/src/flutter/overlay.dart'; import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart'; @@ -121,6 +123,9 @@ class _AppFlowySelectionState extends State EditorState get editorState => widget.editorState; + // Toolbar + Timer? _toolbarTimer; + @override void initState() { super.initState(); @@ -144,6 +149,7 @@ class _AppFlowySelectionState extends State clearSelection(); WidgetsBinding.instance.removeObserver(this); currentSelection.removeListener(_onSelectionChange); + _clearToolbar(); super.dispose(); } @@ -236,7 +242,7 @@ class _AppFlowySelectionState extends State // clear cursor areas // hide toolbar - editorState.service.toolbarService?.hide(); + // editorState.service.toolbarService?.hide(); // clear context menu _clearContextMenu(); @@ -482,13 +488,8 @@ class _AppFlowySelectionState extends State Overlay.of(context)?.insertAll(_selectionAreas); - if (toolbarOffset != null && layerLink != null) { - editorState.service.toolbarService?.showInOffset( - toolbarOffset, - alignment!, - layerLink, - ); - } + // show toolbar + _showToolbarWithDelay(toolbarOffset, layerLink, alignment!); } void _updateCursorAreas(Position position) { @@ -502,6 +503,7 @@ class _AppFlowySelectionState extends State currentSelectedNodes = [node]; _showCursor(node, position); + _clearToolbar(); } void _showCursor(Node node, Position position) { @@ -628,6 +630,40 @@ class _AppFlowySelectionState extends State _scrollUpOrDownIfNeeded(); } + void _showToolbarWithDelay( + Offset? toolbarOffset, + LayerLink? layerLink, + Alignment alignment, { + Duration delay = const Duration(milliseconds: 400), + }) { + if (toolbarOffset == null && layerLink == null) { + _clearToolbar(); + return; + } + if (_toolbarTimer?.isActive ?? false) { + _toolbarTimer?.cancel(); + } + _toolbarTimer = Timer( + delay, + () { + if (toolbarOffset != null && layerLink != null) { + editorState.service.toolbarService?.showInOffset( + toolbarOffset, + alignment, + layerLink, + ); + } + }, + ); + } + + void _clearToolbar() { + editorState.service.toolbarService?.hide(); + if (_toolbarTimer?.isActive ?? false) { + _toolbarTimer?.cancel(); + } + } + void _showDebugLayerIfNeeded({Offset? offset}) { // remove false to show debug overlay. // if (kDebugMode && false) { diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 762d550803..9fd8ca3648 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -7,7 +7,11 @@ import 'package:appflowy_editor/src/extensions/object_extensions.dart'; abstract class AppFlowyToolbarService { /// Show the toolbar widget beside the offset. - void showInOffset(Offset offset, Alignment alignment, LayerLink layerLink); + void showInOffset( + Offset offset, + Alignment alignment, + LayerLink layerLink, + ); /// Hide the toolbar widget. void hide(); @@ -45,7 +49,11 @@ class _FlowyToolbarState extends State } @override - void showInOffset(Offset offset, Alignment alignment, LayerLink layerLink) { + void showInOffset( + Offset offset, + Alignment alignment, + LayerLink layerLink, + ) { hide(); final items = _filterItems(toolbarItems); if (items.isEmpty) { diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart index b9e774b351..54f9ed0455 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart @@ -25,6 +25,7 @@ void main() async { await editor.updateSelection(h1); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final h1Button = find.byWidgetPredicate((widget) { @@ -52,6 +53,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(h2); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final h2Button = find.byWidgetPredicate((widget) { @@ -77,6 +79,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(h3); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final h3Button = find.byWidgetPredicate((widget) { @@ -104,6 +107,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(underline); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final underlineButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -132,6 +136,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(bold); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final boldButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -159,6 +164,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(italic); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final italicButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -187,6 +193,7 @@ void main() async { await editor.updateSelection(strikeThrough); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final strikeThroughButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -214,6 +221,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(code); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final codeButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -250,6 +258,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(quote); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final quoteButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -276,6 +285,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(bulletList); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final bulletListButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -306,6 +316,7 @@ void main() async { end: Position(path: [0], offset: singleLineText.length)); await editor.updateSelection(selection); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final highlightButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { @@ -343,6 +354,7 @@ void main() async { ); await editor.updateSelection(selection); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); final colorButton = find.byWidgetPredicate((widget) { if (widget is ToolbarItemWidget) { diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart index 0cb4c71600..222a7efe1b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart @@ -245,6 +245,7 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { await editor.updateSelection(selection); // show toolbar + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); // trigger the link menu diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart index 6c2ab09157..86cd29705d 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -24,6 +24,7 @@ void main() async { ); await editor.updateSelection(selection); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); // no link item @@ -72,6 +73,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [0], startOffset: 0, endOffset: text.length), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); void testHighlight(bool expectedValue) { @@ -138,6 +140,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [0], startOffset: 0, endOffset: text.length), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); var itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.h1'); expect(itemWidget.isHighlight, true); @@ -145,6 +148,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [1], startOffset: 0, endOffset: text.length), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.quote'); expect(itemWidget.isHighlight, true); @@ -152,6 +156,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [2], startOffset: 0, endOffset: text.length), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.bulleted_list'); expect(itemWidget.isHighlight, true); @@ -183,6 +188,7 @@ void main() async { await editor.updateSelection( Selection.single(path: [2], startOffset: text.length, endOffset: 0), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); expect( _itemWidgetForId(tester, 'appflowy.toolbar.h1').isHighlight, @@ -199,6 +205,7 @@ void main() async { end: Position(path: [1], offset: 0), ), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); expect( _itemWidgetForId(tester, 'appflowy.toolbar.bold').isHighlight, @@ -211,6 +218,7 @@ void main() async { end: Position(path: [0], offset: 0), ), ); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(find.byType(ToolbarWidget), findsOneWidget); expect( _itemWidgetForId(tester, 'appflowy.toolbar.bold').isHighlight, From 0dac41b1149fa1176f8daa7a6541738eca74d42b Mon Sep 17 00:00:00 2001 From: GouravShDev <74348508+GouravShDev@users.noreply.github.com> Date: Thu, 9 Mar 2023 12:03:12 +0530 Subject: [PATCH 10/15] chore: add node tests (#1943) --- .../test/core/document/node_test.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/node_test.dart b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/node_test.dart index 4e407fd321..853df05671 100644 --- a/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/node_test.dart +++ b/frontend/appflowy_flutter/packages/appflowy_editor/test/core/document/node_test.dart @@ -228,5 +228,23 @@ void main() async { final textNode = TextNode.empty()..delta = (Delta()..insert('AppFlowy')); expect(textNode.toPlainText(), 'AppFlowy'); }); + test('test node id', () { + final nodeA = Node( + type: 'example', + children: LinkedList(), + attributes: {}, + ); + final nodeAId = nodeA.id; + expect(nodeAId, 'example'); + final nodeB = Node( + type: 'example', + children: LinkedList(), + attributes: { + 'subtype': 'exampleSubtype', + }, + ); + final nodeBId = nodeB.id; + expect(nodeBId, 'example/exampleSubtype'); + }); }); } From 2368f5dc4adf6caf4741023ca32b7fcc3b542112 Mon Sep 17 00:00:00 2001 From: Yijing Huang Date: Fri, 10 Mar 2023 00:12:59 +0800 Subject: [PATCH 11/15] fix(appflowy_flutter): fix double click title issue #1324 double click the title to select all the text on it --- .../presentation/widgets/left_bar_item.dart | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart index 29788a8a32..e697262492 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart @@ -55,18 +55,26 @@ class _ViewLeftBarItemState extends State { return IntrinsicWidth( key: ValueKey(_controller.text), - child: TextField( - controller: _controller, - focusNode: _focusNode, - scrollPadding: EdgeInsets.zero, - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(vertical: 4.0), - border: InputBorder.none, - isDense: true, + child: GestureDetector( + onDoubleTap: () { + _controller.selection = TextSelection( + baseOffset: 0, + extentOffset: _controller.text.length, + ); + }, + child: TextField( + controller: _controller, + focusNode: _focusNode, + scrollPadding: EdgeInsets.zero, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 4.0), + border: InputBorder.none, + isDense: true, + ), + style: Theme.of(context).textTheme.bodyMedium, + // cursorColor: widget.cursorColor, + // obscureText: widget.enableObscure, ), - style: Theme.of(context).textTheme.bodyMedium, - // cursorColor: widget.cursorColor, - // obscureText: widget.enableObscure, ), ); } From 5b4043b805e51486c92cea5983c2316affc69466 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Fri, 10 Mar 2023 09:40:19 +0800 Subject: [PATCH 12/15] fix: SelectOptionCellBloc registeration error (#1948) --- .../row/cells/select_option_cell/select_option_cell.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart index 15683a9835..70f864ad80 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; @@ -111,7 +110,7 @@ class _MultiSelectCellState extends GridCellState { void initState() { final cellController = widget.cellControllerBuilder.build() as SelectOptionCellController; - _cellBloc = getIt(param1: cellController) + _cellBloc = SelectOptionCellBloc(cellController: cellController) ..add(const SelectOptionCellEvent.initial()); _popover = PopoverController(); super.initState(); From 688d55e00fea42a9ccfd10e43cc00487f7747e76 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 10 Mar 2023 17:33:25 +0800 Subject: [PATCH 13/15] fix: #1942 utc timestamp being parsed as local time (#1953) --- .../calendar/application/calendar_bloc.dart | 1 + .../widgets/row/cells/date_cell/date_cal_bloc.dart | 5 ++++- .../lib/plugins/trash/src/trash_cell.dart | 10 ++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart index 49c0f5e174..bf0e1ab8f6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart @@ -173,6 +173,7 @@ class CalendarBloc extends Bloc { final date = DateTime.fromMillisecondsSinceEpoch( eventPB.timestamp.toInt() * 1000, + isUtc: true, ); return CalendarEventData( title: eventPB.title, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart index 30c61c0636..058782ebcb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart @@ -264,7 +264,10 @@ Option calDataFromCellData(DateCellDataPB? cellData) { Option dateData = none(); if (cellData != null) { final timestamp = cellData.timestamp * 1000; - final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt()); + final date = DateTime.fromMillisecondsSinceEpoch( + timestamp.toInt(), + isUtc: true, + ); dateData = Some(DateCellData( date: date, time: time, diff --git a/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart b/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart index d492b25481..058f6f842a 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart @@ -61,10 +61,12 @@ class TrashCell extends StatelessWidget { } String dateFormatter($fixnum.Int64 inputTimestamps) { - var outputFormat = DateFormat('MM/dd/yyyy hh:mm a'); - var date = - DateTime.fromMillisecondsSinceEpoch(inputTimestamps.toInt() * 1000); - var outputDate = outputFormat.format(date); + final outputFormat = DateFormat('MM/dd/yyyy hh:mm a'); + final date = DateTime.fromMillisecondsSinceEpoch( + inputTimestamps.toInt() * 1000, + isUtc: true, + ); + final outputDate = outputFormat.format(date); return outputDate; } } From 668e1196d194a0453b40190dcf4e6c1d6dbb851d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 11 Mar 2023 12:07:31 +0700 Subject: [PATCH 14/15] chore: config tauri web driver test (#1947) * chore: setup tauri test * chore: update test * chore: update test * chore: update test * chore: update test * refactor: test folder location * chore: remove deps * ci: fix build --------- Co-authored-by: appflowy Co-authored-by: nathan --- frontend/appflowy_tauri/jest.config.cjs | 8 -- frontend/appflowy_tauri/package.json | 8 +- .../components/auth/GetStarted/GetStarted.tsx | 6 +- .../layout/HeaderPanel/PageOptions.tsx | 2 +- .../appflowy_tauri/src/tests/user.test.ts | 73 +++++++++--------- .../appflowy_tauri/test/specs/example.e2e.ts | 14 ---- frontend/appflowy_tauri/test/tsconfig.json | 13 ---- .../webdriver/selenium/package.json | 13 ++++ .../webdriver/selenium/test/test.cjs | 76 +++++++++++++++++++ frontend/scripts/makefile/protobuf.toml | 2 +- 10 files changed, 133 insertions(+), 82 deletions(-) delete mode 100644 frontend/appflowy_tauri/jest.config.cjs delete mode 100644 frontend/appflowy_tauri/test/specs/example.e2e.ts delete mode 100644 frontend/appflowy_tauri/test/tsconfig.json create mode 100644 frontend/appflowy_tauri/webdriver/selenium/package.json create mode 100644 frontend/appflowy_tauri/webdriver/selenium/test/test.cjs diff --git a/frontend/appflowy_tauri/jest.config.cjs b/frontend/appflowy_tauri/jest.config.cjs deleted file mode 100644 index b5ea02d82e..0000000000 --- a/frontend/appflowy_tauri/jest.config.cjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - globals: { - window: {}, - }, -}; diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index 3248c6d5e3..18ec8c8de9 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -12,8 +12,7 @@ "test:errors": "eslint --quiet --ext .js,.ts,.tsx .", "test:prettier": "yarn prettier --list-different src", "tauri:clean": "cargo make --cwd .. tauri_clean", - "tauri:dev": "tauri dev", - "test": "jest" + "tauri:dev": "tauri dev" }, "dependencies": { "@emotion/react": "^11.10.6", @@ -26,7 +25,7 @@ "i18next": "^22.4.10", "i18next-browser-languagedetector": "^7.0.1", "is-hotkey": "^0.2.0", - "jest": "^29.4.3", + "jest": "^29.5.0", "nanoid": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -46,8 +45,6 @@ "@tauri-apps/cli": "^1.2.2", "@types/google-protobuf": "^3.15.6", "@types/is-hotkey": "^0.1.7", - "@types/jest": "^29.4.0", - "@types/mocha": "^10.0.1", "@types/node": "^18.7.10", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", @@ -62,7 +59,6 @@ "prettier": "2.8.4", "prettier-plugin-tailwindcss": "^0.2.2", "tailwindcss": "^3.2.7", - "ts-jest": "^29.0.5", "typescript": "^4.6.4", "vite": "^4.0.0" } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/GetStarted/GetStarted.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/GetStarted/GetStarted.tsx index 04cd2865f1..21c97d3199 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/GetStarted/GetStarted.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/GetStarted/GetStarted.tsx @@ -9,7 +9,7 @@ export const GetStarted = () => { <>
    e.preventDefault()} method='POST'>
    -
    +
    @@ -19,8 +19,8 @@ export const GetStarted = () => {
    -
    -
    diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.tsx index 8f79998027..2690af57f0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/HeaderPanel/PageOptions.tsx @@ -13,7 +13,7 @@ export const PageOptions = () => { Share -
    diff --git a/frontend/appflowy_tauri/src/tests/user.test.ts b/frontend/appflowy_tauri/src/tests/user.test.ts index 41be820e97..44a2fa066a 100644 --- a/frontend/appflowy_tauri/src/tests/user.test.ts +++ b/frontend/appflowy_tauri/src/tests/user.test.ts @@ -1,42 +1,43 @@ -import { AuthBackendService, UserBackendService } from '../appflowy_app/stores/effects/user/user_bd_svc'; -import { randomFillSync } from 'crypto'; -import { nanoid } from '@reduxjs/toolkit'; +export {} +// import { AuthBackendService, UserBackendService } from '../appflowy_app/stores/effects/user/user_bd_svc'; +// import { randomFillSync } from 'crypto'; +// import { nanoid } from '@reduxjs/toolkit'; -beforeAll(() => { - //@ts-ignore - window.crypto = { - // @ts-ignore - getRandomValues: function (buffer) { - // @ts-ignore - return randomFillSync(buffer); - }, - }; -}); +// beforeAll(() => { +// //@ts-ignore +// window.crypto = { +// // @ts-ignore +// getRandomValues: function (buffer) { +// // @ts-ignore +// return randomFillSync(buffer); +// }, +// }; +// }); -describe('User backend service', () => { - it('sign up', async () => { - const service = new AuthBackendService(); - const result = await service.autoSignUp(); - expect(result.ok).toBeTruthy; - }); +// describe('User backend service', () => { +// it('sign up', async () => { +// const service = new AuthBackendService(); +// const result = await service.autoSignUp(); +// expect(result.ok).toBeTruthy; +// }); - it('sign in', async () => { - const authService = new AuthBackendService(); - const email = nanoid(4) + '@appflowy.io'; - const password = nanoid(10); - const signUpResult = await authService.signUp({ name: 'nathan', email: email, password: password }); - expect(signUpResult.ok).toBeTruthy; +// it('sign in', async () => { +// const authService = new AuthBackendService(); +// const email = nanoid(4) + '@appflowy.io'; +// const password = nanoid(10); +// const signUpResult = await authService.signUp({ name: 'nathan', email: email, password: password }); +// expect(signUpResult.ok).toBeTruthy; - const signInResult = await authService.signIn({ email: email, password: password }); - expect(signInResult.ok).toBeTruthy; - }); +// const signInResult = await authService.signIn({ email: email, password: password }); +// expect(signInResult.ok).toBeTruthy; +// }); - it('get user profile', async () => { - const service = new AuthBackendService(); - const result = await service.autoSignUp(); - const userProfile = result.unwrap(); +// it('get user profile', async () => { +// const service = new AuthBackendService(); +// const result = await service.autoSignUp(); +// const userProfile = result.unwrap(); - const userService = new UserBackendService(userProfile.id); - expect((await userService.getUserProfile()).unwrap()).toBe(userProfile); - }); -}); +// const userService = new UserBackendService(userProfile.id); +// expect((await userService.getUserProfile()).unwrap()).toBe(userProfile); +// }); +// }); diff --git a/frontend/appflowy_tauri/test/specs/example.e2e.ts b/frontend/appflowy_tauri/test/specs/example.e2e.ts deleted file mode 100644 index be4a485a82..0000000000 --- a/frontend/appflowy_tauri/test/specs/example.e2e.ts +++ /dev/null @@ -1,14 +0,0 @@ -describe('My Login application', () => { - it('should login with valid credentials', async () => { - await browser.url(`https://the-internet.herokuapp.com/login`) - - await $('#username').setValue('tomsmith') - await $('#password').setValue('SuperSecretPassword!') - await $('button[type="submit"]').click() - - await expect($('#flash')).toBeExisting() - await expect($('#flash')).toHaveTextContaining( - 'You logged into a secure area!') - }) -}) - diff --git a/frontend/appflowy_tauri/test/tsconfig.json b/frontend/appflowy_tauri/test/tsconfig.json deleted file mode 100644 index ddef7c61c3..0000000000 --- a/frontend/appflowy_tauri/test/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "moduleResolution": "node", - "module": "ESNext", - "types": [ - "node", - "@wdio/globals/types", - "expect-webdriverio", - "@wdio/mocha-framework" - ], - "target": "es2022" - } -} \ No newline at end of file diff --git a/frontend/appflowy_tauri/webdriver/selenium/package.json b/frontend/appflowy_tauri/webdriver/selenium/package.json new file mode 100644 index 0000000000..78bbd20aad --- /dev/null +++ b/frontend/appflowy_tauri/webdriver/selenium/package.json @@ -0,0 +1,13 @@ +{ + "name": "selenium", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "mocha" + }, + "dependencies": { + "chai": "^4.3.4", + "mocha": "^9.0.3", + "selenium-webdriver": "^4.0.0-beta.4" + } +} diff --git a/frontend/appflowy_tauri/webdriver/selenium/test/test.cjs b/frontend/appflowy_tauri/webdriver/selenium/test/test.cjs new file mode 100644 index 0000000000..7a57bdbbaf --- /dev/null +++ b/frontend/appflowy_tauri/webdriver/selenium/test/test.cjs @@ -0,0 +1,76 @@ +const os = require("os"); +const path = require("path"); +const { expect } = require("chai"); +const { spawn, spawnSync } = require("child_process"); +const { Builder, By, Capabilities, until } = require("selenium-webdriver"); +const { elementIsVisible, elementLocated } = require("selenium-webdriver/lib/until.js"); + +// create the path to the expected application binary +const application = path.resolve( + __dirname, + "..", + "..", + "..", + "src-tauri", + "target", + "release", + "appflowy_tauri" +); + +// keep track of the webdriver instance we create +let driver; + +// keep track of the tauri-driver process we start +let tauriDriver; + +before(async function() { + // set timeout to 2 minutes to allow the program to build if it needs to + this.timeout(120000); + + // ensure the program has been built + spawnSync("cargo", ["build", "--release"]); + + // start tauri-driver + tauriDriver = spawn( + path.resolve(os.homedir(), ".cargo", "bin", "tauri-driver"), + [], + { stdio: [null, process.stdout, process.stderr] } + ); + + const capabilities = new Capabilities(); + capabilities.set("tauri:options", { application }); + capabilities.setBrowserName("wry"); + + // start the webdriver client + driver = await new Builder() + .withCapabilities(capabilities) + .usingServer("http://localhost:4444/") + .build(); +}); + +after(async function() { + // stop the webdriver session + await driver.quit(); + + // kill the tauri-driver process + tauriDriver.kill(); +}); + +describe("AppFlowy Unit Test", () => { + it("should find get started button", async () => { + // should sign out if already sign in + const getStartedButton = await driver.wait(until.elementLocated(By.xpath("//*[@id=\"root\"]/form/div/div[3]"))); + getStartedButton.click(); + }); + + it("should get sign out button", async (done) => { + // const optionButton = await driver.wait(until.elementLocated(By.css('*[test-id=option-button]'))); + // const optionButton = await driver.wait(until.elementLocated(By.id('option-button'))); + // const optionButton = await driver.wait(until.elementLocated(By.css('[aria-label=option]'))); + + // Currently, only the find className is work + const optionButton = await driver.wait(until.elementLocated(By.className("relative h-8 w-8"))); + optionButton.click(); + await new Promise((resolve) => setTimeout(resolve, 4000)); + }); +}); diff --git a/frontend/scripts/makefile/protobuf.toml b/frontend/scripts/makefile/protobuf.toml index fff084fe22..463a1bd5f2 100644 --- a/frontend/scripts/makefile/protobuf.toml +++ b/frontend/scripts/makefile/protobuf.toml @@ -13,7 +13,7 @@ script_runner = "@shell" [tasks.install_tauri_protobuf.linux] script = """ -npm install -g protoc-gen-ts +sudo npm install -g protoc-gen-ts """ script_runner = "@shell" From 972ef2149c99fd291e28602c45c36613b0fd0214 Mon Sep 17 00:00:00 2001 From: Yijing Huang Date: Sat, 11 Mar 2023 21:15:34 +0800 Subject: [PATCH 15/15] fix(appflowy_flutter): fix cover image overflow #1916 (#1952) * fix(appflowy_flutter): fix cover image overflow #1916 * fix(appflowy_flutter): use OverflowBox to fix #1916 * chore: fix misspelling * fix: prevent the image being overstretched --------- Co-authored-by: Lucas.Xu --- .../plugins/cover/cover_node_widget.dart | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart index 5e95b0380c..452b3356ab 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/cover_node_widget.dart @@ -194,7 +194,7 @@ class _CoverImageState extends State<_CoverImage> { Widget build(BuildContext context) { return Stack( children: [ - _buildCoverImage(context), + _buildCoverImage(context, widget.editorState), _buildCoverOverlayButtons(context), ], ); @@ -251,7 +251,7 @@ class _CoverImageState extends State<_CoverImage> { ); } - Widget _buildCoverImage(BuildContext context) { + Widget _buildCoverImage(BuildContext context, EditorState editorState) { final screenSize = MediaQuery.of(context).size; const height = 200.0; final Widget coverImage; @@ -281,12 +281,17 @@ class _CoverImageState extends State<_CoverImage> { coverImage = const SizedBox(); // just an empty sizebox break; } - return UnconstrainedBox( - child: Container( - padding: const EdgeInsets.only(bottom: 10), - height: height, - width: screenSize.width, - child: coverImage, +//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an erorr + return SizedBox( + height: height, + child: OverflowBox( + maxWidth: screenSize.width, + child: Container( + padding: const EdgeInsets.only(bottom: 10), + height: double.infinity, + width: double.infinity, + child: coverImage, + ), ), ); }