Bläddra i källkod

feat: Create a "view" for all database references in a document (#2083)

* feat: add archive for compression

* feat: add service to manage zipped work spaces

* feat: export service in barrel file

* feat: ignore .ephemeral directory

* feat: add first compressed workspace file

* fix: directory path was wrong

* feat: add a somewhat useful test

* fix: move to same file (delete later)

* fix: use script path vs. working directory for CI

* fix: read from asset bundle instead of file system

* fix: workaround to run integration in multiple files on desktop (flutter/flutter#101031

* feat: remove .ephemeral from .gitignore, no longer created

* feat: document test changes

* fix: lucas suggestion

* feat: mark assets as excluded in pubspec.yaml

* feat: add class for build utilities

* feat: add script runner for release builds

* feat: add build script as task in flowy project

* fix: typo in pubspec.yaml

* chore: use constants for exclude tag

* feat: add appversion as argument to build tool

* feat: use dart script in release.yml

* chore: remove task

* fix: careless error

Co-authored-by: Mihir <[email protected]>

* feat: add translations for view of

* fix: typo in getAllDatabase

* feat: add view of database

* fix: remove unused import

* fix: use effective dart typing

* fix: insertPage marked as async, should return future

* fix: Remove multi-line string

* fix: ref can be null

* fix: unused imports caused analyzer to fail

* feat: also fix. Add empty document as option and change name to _name

* chore: move referenced database tests to empty document test file

* feat: add test utilities

* feat: add new integration test on an empty document

* feat: register test in runner

* fix: missing reference in insert_page_command

* fix: analyzer errors

---------

Co-authored-by: Mihir <[email protected]>
Alex Wallen 2 år sedan
förälder
incheckning
e2009c063b

+ 3 - 4
.github/workflows/integration_test.yml

@@ -105,11 +105,11 @@ jobs:
         working-directory: frontend/appflowy_flutter
         run: |
           if [ "$RUNNER_OS" == "Linux" ]; then
-            flutter test integration_test -d Linux --coverage
+            flutter test integration_test/runner.dart -d Linux --coverage
           elif [ "$RUNNER_OS" == "macOS" ]; then
-            flutter test integration_test -d macOS --coverage
+            flutter test integration_test/runner.dart -d macOS --coverage
           elif [ "$RUNNER_OS" == "Windows" ]; then
-            flutter test integration_test -d Windows --coverage
+            flutter test integration_test/runner.dart -d Windows --coverage
           fi
         shell: bash
 
@@ -120,4 +120,3 @@ jobs:
       #     env_vars: ${{ matrix.os }}
       #     fail_ci_if_error: true
       #     verbose: true
-

+ 23 - 7
.github/workflows/release.yml

@@ -3,7 +3,7 @@ name: release
 on:
   push:
     tags:
-      - '*'
+      - "*"
 
 env:
   FLUTTER_VERSION: "3.7.5"
@@ -136,7 +136,11 @@ jobs:
       fail-fast: false
       matrix:
         job:
-          - { target: x86_64-apple-darwin, os: macos-10.15, extra-build-args: "" }
+          - {
+              target: x86_64-apple-darwin,
+              os: macos-10.15,
+              extra-build-args: "",
+            }
     steps:
       - name: Checkout source code
         uses: actions/checkout@v3
@@ -172,7 +176,7 @@ jobs:
         working-directory: frontend
         run: |
           flutter config --enable-macos-desktop
-          cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-mac-x86_64 appflowy
+          dart ./scripts/flutter_release_build/build_flowy.dart . ${{ github.ref_name }}
 
       - name: Create macOS dmg
         run: |
@@ -225,9 +229,21 @@ jobs:
       fail-fast: false
       matrix:
         job:
-          - { arch: x86_64, target: x86_64-unknown-linux-gnu, os: ubuntu-20.04, extra-build-args: "", flutter_profile: production-linux-x86_64 }
+          - {
+              arch: x86_64,
+              target: x86_64-unknown-linux-gnu,
+              os: ubuntu-20.04,
+              extra-build-args: "",
+              flutter_profile: production-linux-x86_64,
+            }
           # - { arch: aarch64, target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, extra-build-args: "", flutter_profile: production-linux-aarch64 }
-          - { arch: x86_64, target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, extra-build-args: "", flutter_profile: production-linux-x86_64}
+          - {
+              arch: x86_64,
+              target: x86_64-unknown-linux-gnu,
+              os: ubuntu-18.04,
+              extra-build-args: "",
+              flutter_profile: production-linux-x86_64,
+            }
     steps:
       - name: Checkout source code
         uses: actions/checkout@v3
@@ -275,7 +291,7 @@ jobs:
         working-directory: frontend
         run: |
           flutter config --enable-linux-desktop
-          cargo make --env APP_VERSION=${{ github.ref_name }} --profile ${{ matrix.job.flutter_profile}} appflowy
+          dart ./scripts/flutter_release_build/build_flowy.dart . ${{ github.ref_name }}
 
       - name: Archive Assert
         working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
@@ -361,4 +377,4 @@ jobs:
       - name: Notify Discord
         run: |
           curl -H "Content-Type: application/json" -d '{"username": "release@appflowy", "content": "🎉 AppFlowy ${{ github.ref_name }} is available. https://github.com/AppFlowy-IO/AppFlowy/releases/tag/'${{ github.ref_name }}'"}' "https://discord.com/api/webhooks/${{ secrets.DISCORD }}"
-        shell: bash
+        shell: bash

+ 224 - 222
frontend/.vscode/tasks.json

@@ -1,223 +1,225 @@
 {
-	"version": "2.0.0",
-	// https://code.visualstudio.com/docs/editor/tasks
-	// https://gist.github.com/deadalusai/9e13e36d61ec7fb72148
-	// ${workspaceRoot}: the root folder of the team
-	// ${file}: the current opened file
-	// ${fileBasename}: the current opened file's basename
-	// ${fileDirname}: the current opened file's dirname
-	// ${fileExtname}: the current opened file's extension
-	// ${cwd}: the current working directory of the spawned process
-	"tasks": [
-		{
-			"label": "AF: Clean + Rebuild All",
-			"type": "shell",
-			"dependsOrder": "sequence",
-			"dependsOn": [
-				"AF: Dart Clean",
-				"AF: Flutter Clean",
-				"AF: Build Appflowy Core",
-				"AF: Flutter Pub Get",
-				"AF: Flutter Package Get",
-				"AF: Generate Language Files",
-				"AF: Generate Freezed Files"
-			],
-			"presentation": {
-				"reveal": "always",
-				"panel": "new"
-			}
-		},
-		{
-			"label": "AF: Clean + Rebuild All (Android)",
-			"type": "shell",
-			"dependsOrder": "sequence",
-			"dependsOn": [
-				"AF: Dart Clean",
-				"AF: Flutter Clean",
-				"AF: Build Appflowy Core_for_android",
-				"AF: Flutter Pub Get",
-				"AF: Flutter Package Get",
-				"AF: Generate Language Files",
-				"AF: Generate Freezed Files"
-			],
-			"presentation": {
-				"reveal": "always",
-				"panel": "new"
-			}
-		},
-		{
-			"label": "AF: Build Appflowy Core_for_android",
-			"type": "shell",
-			"command": "cargo make --profile development-android appflowy-core-dev-android",
-			"group": "build",
-			"options": {
-				"cwd": "${workspaceFolder}"
-			}
-		},
-		{
-			"label": "AF: Build Appflowy Core",
-			"type": "shell",
-			"windows": {
-				"command": "cargo make --profile development-windows-x86 appflowy-core-dev"
-			},
-			"linux": {
-				"command": "cargo make --profile \"development-linux-$(uname -m)\" appflowy-core-dev"
-			},
-			"osx": {
-				"command": "cargo make --profile \"development-mac-$(uname -m)\" appflowy-core-dev"
-			},
-			"group": "build",
-			"options": {
-				"cwd": "${workspaceFolder}"
-			}
-		},
-		{
-			"label": "AF: Code Gen",
-			"type": "shell",
-			"dependsOrder": "sequence",
-			"dependsOn": [
-				"AF: Flutter Clean",
-				"AF: Flutter Pub Get",
-				"AF: Flutter Package Get",
-				"AF: Generate Language Files",
-				"AF: Generate Freezed Files"
-			],
-			"group": {
-				"kind": "build",
-				"isDefault": true
-			},
-			"presentation": {
-				"reveal": "always",
-				"panel": "new"
-			}
-		},
-		{
-			"label": "AF: Flutter Clean",
-			"type": "shell",
-			"command": "flutter clean",
-			"options": {
-				"cwd": "${workspaceFolder}/appflowy_flutter"
-			}
-		},
-		{
-			"label": "AF: Flutter Pub Get",
-			"type": "shell",
-			"command": "flutter pub get",
-			"options": {
-				"cwd": "${workspaceFolder}/appflowy_flutter"
-			}
-		},
-		{
-			"label": "AF: Flutter Package Get",
-			"type": "shell",
-			"command": "flutter packages pub get",
-			"options": {
-				"cwd": "${workspaceFolder}/appflowy_flutter"
-			}
-		},
-		{
-			"label": "AF: Generate Freezed Files",
-			"type": "shell",
-			"command": "flutter pub run build_runner build --delete-conflicting-outputs",
-			"options": {
-				"cwd": "${workspaceFolder}/appflowy_flutter"
-			}
-		},
-		{
-			"label": "AF: Generate Language Files",
-			"type": "shell",
-			"command": "sh ./scripts/generate_language_files.sh",
-			"windows": {
-				"options": {
-					"shell": {
-						"executable": "cmd.exe",
-						"args": [
-							"/d",
-							"/c",
-							".\\scripts\\generate_language_files.cmd"
-						]
-					}
-				}
-			},
-			"group": "build",
-			"options": {
-				"cwd": "${workspaceFolder}"
-			}
-		},
-		{
-			"label": "AF: Flutter Clean",
-			"type": "shell",
-			"command": "cargo make flutter_clean",
-			"group": "build",
-			"options": {
-				"cwd": "${workspaceFolder}"
-			}
-		},
-		{
-			"label": "AF: flutter build aar",
-			"type": "flutter",
-			"command": "flutter",
-			"args": [
-				"build",
-				"aar"
-			],
-			"group": "build",
-			"problemMatcher": [],
-			"detail": "appflowy_flutter"
-		},
-		{
-			"label": "AF: Tauri UI Dev",
-			"type": "shell",
-			"isBackground": true,
-			"command": "yarn",
-			"args": ["dev"],
-			"options": {
-				"cwd": "${workspaceFolder}/appflowy_tauri"
-			}
-		},
-		{
-			"label": "AF: Tauri UI Build",
-			"type": "shell",
-			"command": "npm run build",
-			"options": {
-				"cwd": "${workspaceFolder}/appflowy_tauri"
-			}
-		},
-		{
-			"label": "AF: Tauri Dev",
-			"type": "shell",
-			"command": "npm run tauri:dev",
-			"options": {
-				"cwd": "${workspaceFolder}/appflowy_tauri"
-			}
-		},
-		{
-			"label": "AF: Tauri Clean",
-			"type": "shell",
-			"command": "cargo make tauri_clean",
-			"options": {
-				"cwd": "${workspaceFolder}"
-			}
-		},
-		{
-			"label": "AF: Tauri Clean + Dev",
-			"type": "shell",
-			"dependsOrder": "sequence",
-			"dependsOn": [
-				"AF: Tauri Clean",
-				"AF: Tauri UI Dev"
-			],
-			"options": {
-				"cwd": "${workspaceFolder}"
-			}
-		},
-		{
-			"label": "AF: Tauri ESLint",
-			"type": "shell",
-			"command": "npx eslint --fix src",
-			"options": {
-				"cwd": "${workspaceFolder}/appflowy_tauri"
-			}
-		},	
-	]
-}
+  "version": "2.0.0",
+  // https://code.visualstudio.com/docs/editor/tasks
+  // https://gist.github.com/deadalusai/9e13e36d61ec7fb72148
+  // ${workspaceRoot}: the root folder of the team
+  // ${file}: the current opened file
+  // ${fileBasename}: the current opened file's basename
+  // ${fileDirname}: the current opened file's dirname
+  // ${fileExtname}: the current opened file's extension
+  // ${cwd}: the current working directory of the spawned process
+  "tasks": [
+    {
+      "label": "AF: Clean + Rebuild All",
+      "type": "shell",
+      "dependsOrder": "sequence",
+      "dependsOn": [
+        "AF: Dart Clean",
+        "AF: Flutter Clean",
+        "AF: Build Appflowy Core",
+        "AF: Flutter Pub Get",
+        "AF: Flutter Package Get",
+        "AF: Generate Language Files",
+        "AF: Generate Freezed Files"
+      ],
+      "presentation": {
+        "reveal": "always",
+        "panel": "new"
+      }
+    },
+    {
+      "label": "AF: Clean + Rebuild All (Android)",
+      "type": "shell",
+      "dependsOrder": "sequence",
+      "dependsOn": [
+        "AF: Dart Clean",
+        "AF: Flutter Clean",
+        "AF: Build Appflowy Core_for_android",
+        "AF: Flutter Pub Get",
+        "AF: Flutter Package Get",
+        "AF: Generate Language Files",
+        "AF: Generate Freezed Files"
+      ],
+      "presentation": {
+        "reveal": "always",
+        "panel": "new"
+      }
+    },
+    {
+      "label": "AF: Build Appflowy Core_for_android",
+      "type": "shell",
+      "command": "cargo make --profile development-android appflowy-core-dev-android",
+      "group": "build",
+      "options": {
+        "cwd": "${workspaceFolder}"
+      }
+    },
+    {
+      "label": "AF: Build Appflowy Core",
+      "type": "shell",
+      "windows": {
+        "command": "cargo make --profile development-windows-x86 appflowy-core-dev"
+      },
+      "linux": {
+        "command": "cargo make --profile \"development-linux-$(uname -m)\" appflowy-core-dev"
+      },
+      "osx": {
+        "command": "cargo make --profile \"development-mac-$(uname -m)\" appflowy-core-dev"
+      },
+      "group": "build",
+      "options": {
+        "cwd": "${workspaceFolder}"
+      }
+    },
+    {
+      "label": "AF: Code Gen",
+      "type": "shell",
+      "dependsOrder": "sequence",
+      "dependsOn": [
+        "AF: Flutter Clean",
+        "AF: Flutter Pub Get",
+        "AF: Flutter Package Get",
+        "AF: Generate Language Files",
+        "AF: Generate Freezed Files"
+      ],
+      "group": {
+        "kind": "build",
+        "isDefault": true
+      },
+      "presentation": {
+        "reveal": "always",
+        "panel": "new"
+      }
+    },
+    {
+      "label": "AF: Flutter Clean",
+      "type": "shell",
+      "command": "flutter clean",
+      "options": {
+        "cwd": "${workspaceFolder}/appflowy_flutter"
+      }
+    },
+    {
+      "label": "AF: Flutter Pub Get",
+      "type": "shell",
+      "command": "flutter pub get",
+      "options": {
+        "cwd": "${workspaceFolder}/appflowy_flutter"
+      }
+    },
+    {
+      "label": "AF: Flutter Package Get",
+      "type": "shell",
+      "command": "flutter packages pub get",
+      "options": {
+        "cwd": "${workspaceFolder}/appflowy_flutter"
+      }
+    },
+    {
+      "label": "AF: Generate Freezed Files",
+      "type": "shell",
+      "command": "flutter pub run build_runner build --delete-conflicting-outputs",
+      "options": {
+        "cwd": "${workspaceFolder}/appflowy_flutter"
+      }
+    },
+    {
+      "label": "AF: Generate Language Files",
+      "type": "shell",
+      "command": "sh ./scripts/generate_language_files.sh",
+      "windows": {
+        "options": {
+          "shell": {
+            "executable": "cmd.exe",
+            "args": [
+              "/d",
+              "/c",
+              ".\\scripts\\generate_language_files.cmd"
+            ]
+          }
+        }
+      },
+      "group": "build",
+      "options": {
+        "cwd": "${workspaceFolder}"
+      }
+    },
+    {
+      "label": "AF: Flutter Clean",
+      "type": "shell",
+      "command": "cargo make flutter_clean",
+      "group": "build",
+      "options": {
+        "cwd": "${workspaceFolder}"
+      }
+    },
+    {
+      "label": "AF: flutter build aar",
+      "type": "flutter",
+      "command": "flutter",
+      "args": [
+        "build",
+        "aar"
+      ],
+      "group": "build",
+      "problemMatcher": [],
+      "detail": "appflowy_flutter"
+    },
+    {
+      "label": "AF: Tauri UI Dev",
+      "type": "shell",
+      "isBackground": true,
+      "command": "yarn",
+      "args": [
+        "dev"
+      ],
+      "options": {
+        "cwd": "${workspaceFolder}/appflowy_tauri"
+      }
+    },
+    {
+      "label": "AF: Tauri UI Build",
+      "type": "shell",
+      "command": "npm run build",
+      "options": {
+        "cwd": "${workspaceFolder}/appflowy_tauri"
+      }
+    },
+    {
+      "label": "AF: Tauri Dev",
+      "type": "shell",
+      "command": "npm run tauri:dev",
+      "options": {
+        "cwd": "${workspaceFolder}/appflowy_tauri"
+      }
+    },
+    {
+      "label": "AF: Tauri Clean",
+      "type": "shell",
+      "command": "cargo make tauri_clean",
+      "options": {
+        "cwd": "${workspaceFolder}"
+      }
+    },
+    {
+      "label": "AF: Tauri Clean + Dev",
+      "type": "shell",
+      "dependsOrder": "sequence",
+      "dependsOn": [
+        "AF: Tauri Clean",
+        "AF: Tauri UI Dev"
+      ],
+      "options": {
+        "cwd": "${workspaceFolder}"
+      }
+    },
+    {
+      "label": "AF: Tauri ESLint",
+      "type": "shell",
+      "command": "npx eslint --fix src",
+      "options": {
+        "cwd": "${workspaceFolder}/appflowy_tauri"
+      }
+    },
+  ]
+}

+ 2 - 2
frontend/appflowy_flutter/.gitignore

@@ -40,7 +40,7 @@ lib/generated_plugin_registrant.dart
 lib/generated/
 
 # Freezed generated files
-*.g.dart 
+*.g.dart
 *.freezed.dart
 
 # Symbolication related
@@ -67,4 +67,4 @@ windows/flutter/dart_ffi/
 **/**/*.so
 **/**/Brewfile.lock.json
 **/.sandbox
-**/.vscode/
+**/.vscode/

BIN
frontend/appflowy_flutter/assets/test/workspaces/board.zip


BIN
frontend/appflowy_flutter/assets/test/workspaces/empty_document.zip


+ 4 - 2
frontend/appflowy_flutter/assets/translations/en.json

@@ -326,7 +326,8 @@
     "checklist": {
       "panelTitle": "Add an item"
     },
-    "menuName": "Grid"
+    "menuName": "Grid",
+    "referencedGridPrefix": "View of"
   },
   "document": {
     "menuName": "Document",
@@ -390,7 +391,8 @@
     "column": {
       "create_new_card": "New"
     },
-    "menuName": "Board"
+    "menuName": "Board",
+    "referencedBoardPrefix": "View of"
   },
   "calendar": {
     "menuName": "Calendar",

+ 43 - 0
frontend/appflowy_flutter/integration_test/board_test.dart

@@ -0,0 +1,43 @@
+import 'package:appflowy_board/appflowy_board.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'util/util.dart';
+
+/// Integration tests for an empty board. The [TestWorkspaceService] will load
+/// a workspace from an empty board `assets/test/workspaces/board.zip` for all
+/// tests.
+///
+/// To create another integration test with a preconfigured workspace.
+/// Use the following steps.
+/// 1. Create a new workspace from the AppFlowy launch screen.
+/// 2. Modify the workspace until it is suitable as the starting point for
+///    the integration test you need to land.
+/// 3. Use a zip utility program to zip the workspace folder that you created.
+/// 4. Add the zip file under `assets/test/workspaces/`
+/// 5. Add a new enumeration to [TestWorkspace] in `integration_test/utils/data.dart`.
+///    For example, if you added a workspace called `empty_calendar.zip`,
+///    then [TestWorkspace] should have the following value:
+/// ```dart
+/// enum TestWorkspace {
+///   board('board'),
+///   empty_calendar('empty_calendar');
+///
+///   /* code */
+/// }
+/// ```
+/// 6. Double check that the .zip file that you added is included as an asset in
+///    the pubspec.yaml file under appflowy_flutter.
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  const service = TestWorkspaceService(TestWorkspace.board);
+
+  group('board', () {
+    setUpAll(() async => await service.setUpAll());
+    setUp(() async => await service.setUp());
+
+    testWidgets('integration test unzips the proper workspace and loads it correctly.', (tester) async {
+      await tester.initializeAppFlowy();
+      expect(find.byType(AppFlowyBoard), findsOneWidget);
+    });
+  });
+}

+ 120 - 0
frontend/appflowy_flutter/integration_test/empty_document_test.dart

@@ -0,0 +1,120 @@
+import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'util/keyboard.dart';
+import 'util/util.dart';
+
+/// Integration tests for an empty document. The [TestWorkspaceService] will load a workspace from an empty document `assets/test/workspaces/empty_document.zip` for all tests.
+///
+/// To create another integration test with a preconfigured workspace. Use the following steps:
+/// 1. Create a new workspace from the AppFlowy launch screen.
+/// 2. Modify the workspace until it is suitable as the starting point for the integration test you need to land.
+/// 3. Use a zip utility program to zip the workspace folder that you created.
+/// 4. Add the zip file under `assets/test/workspaces/`
+/// 5. Add a new enumeration to [TestWorkspace] in `integration_test/utils/data.dart`. For example, if you added a workspace called `empty_calendar.zip`, then [TestWorkspace] should have the following value:
+/// ```dart
+/// enum TestWorkspace {
+///   board('board'),
+///   empty_calendar('empty_calendar');
+///
+///   /* code */
+/// }
+/// ```
+/// 6. Double check that the .zip file that you added is included as an asset in the pubspec.yaml file under appflowy_flutter.
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  const service = TestWorkspaceService(TestWorkspace.emptyDocument);
+
+  group('Tests on a workspace with only an empty document', () {
+    setUpAll(() async => await service.setUpAll());
+    setUp(() async => await service.setUp());
+
+    testWidgets('/board shortcut creates a new board and view of the board', (tester) async {
+      await tester.initializeAppFlowy();
+
+      // Needs tab to obtain focus for the app flowy editor.
+      // by default the tap appears at the center of the widget.
+      final Finder editor = find.byType(AppFlowyEditor);
+      await tester.tap(editor);
+      await tester.pumpAndSettle();
+
+      // tester.sendText() cannot be used since the editor
+      // does not contain any EditableText widgets.
+      // to interact with the app during an integration test,
+      // simulate physical keyboard events.
+      await FlowyTestKeyboard.simulateKeyDownEvent([
+        LogicalKeyboardKey.slash,
+        LogicalKeyboardKey.keyB,
+        LogicalKeyboardKey.keyO,
+        LogicalKeyboardKey.keyA,
+        LogicalKeyboardKey.keyR,
+        LogicalKeyboardKey.keyD,
+        LogicalKeyboardKey.arrowDown,
+      ], tester: tester);
+
+      // Checks whether the options in the selection menu
+      // for /board exist.
+      expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
+
+      // Finalizes the slash command that creates the board.
+      await FlowyTestKeyboard.simulateKeyDownEvent([
+        LogicalKeyboardKey.enter,
+      ], tester: tester);
+
+      // Checks whether new board is referenced and properly on the page.
+      expect(find.byType(BuiltInPageWidget), findsOneWidget);
+
+      // Checks whether the new database was created
+      const newBoardLabel = "Untitled";
+      expect(find.text(newBoardLabel), findsOneWidget);
+
+      // Checks whether a view of the database was created
+      const viewOfBoardLabel = "View of Untitled";
+      expect(find.text(viewOfBoardLabel), findsNWidgets(2));
+    });
+
+    testWidgets('/grid shortcut creates a new grid and view of the grid', (tester) async {
+      await tester.initializeAppFlowy();
+
+      // Needs tab to obtain focus for the app flowy editor.
+      // by default the tap appears at the center of the widget.
+      final Finder editor = find.byType(AppFlowyEditor);
+      await tester.tap(editor);
+      await tester.pumpAndSettle();
+
+      // tester.sendText() cannot be used since the editor
+      // does not contain any EditableText widgets.
+      // to interact with the app during an integration test,
+      // simulate physical keyboard events.
+      await FlowyTestKeyboard.simulateKeyDownEvent([
+        LogicalKeyboardKey.slash,
+        LogicalKeyboardKey.keyG,
+        LogicalKeyboardKey.keyR,
+        LogicalKeyboardKey.keyI,
+        LogicalKeyboardKey.keyD,
+        LogicalKeyboardKey.arrowDown,
+      ], tester: tester);
+
+      // Checks whether the options in the selection menu
+      // for /grid exist.
+      expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
+
+      // Finalizes the slash command that creates the board.
+      await simulateKeyDownEvent(LogicalKeyboardKey.enter);
+      await tester.pumpAndSettle();
+
+      // Checks whether new board is referenced and properly on the page.
+      expect(find.byType(BuiltInPageWidget), findsOneWidget);
+
+      // Checks whether the new database was created
+      const newTableLabel = "Untitled";
+      expect(find.text(newTableLabel), findsOneWidget);
+
+      // Checks whether a view of the database was created
+      const viewOfTableLabel = "View of Untitled";
+      expect(find.text(viewOfTableLabel), findsNWidgets(2));
+    });
+  });
+}

+ 19 - 0
frontend/appflowy_flutter/integration_test/runner.dart

@@ -0,0 +1,19 @@
+import 'package:integration_test/integration_test.dart';
+
+import 'board_test.dart' as board_test;
+import 'switch_folder_test.dart' as switch_folder_test;
+import 'empty_document_test.dart' as empty_document_test;
+
+/// The main task runner for all integration tests in AppFlowy.
+///
+/// Having a single entrypoint for integration tests is necessary due to an
+/// [issue caused by switching files with integration testing](https://github.com/flutter/flutter/issues/101031).
+/// If flutter/flutter#101031 is resolved, this file can be removed completely.
+/// Once removed, the integration_test.yaml must be updated to exclude this as
+/// as the test target.
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+  switch_folder_test.main();
+  board_test.main();
+  empty_document_test.main();
+}

+ 0 - 135
frontend/appflowy_flutter/integration_test/switch_folder_test.dart

@@ -1,10 +1,5 @@
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
 import 'package:appflowy/user/presentation/folder/folder_widget.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/style_widget/text_field.dart';
-import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:integration_test/integration_test.dart';
 
@@ -162,135 +157,5 @@ void main() {
         await TestFolder.currentLocation(),
       );
     });
-
-    testWidgets('/board shortcut creates a new board', (tester) async {
-      const folderName = 'appflowy';
-      await TestFolder.cleanTestLocation(folderName);
-      await TestFolder.setTestLocation(folderName);
-
-      await tester.initializeAppFlowy();
-
-      // tap open button
-      await mockGetDirectoryPath(folderName);
-      await tester.tapOpenFolderButton();
-
-      await tester.wait(1000);
-      await tester.expectToSeeWelcomePage();
-
-      final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
-      // Necessary for being able to enterText when not in debug mode
-      binding.testTextInput.register();
-
-      // Needs tab to obtain focus for the app flowy editor.
-      // by default the tap appears at the center of the widget.
-      final Finder editor = find.byType(AppFlowyEditor);
-      await tester.tap(editor);
-      await tester.pumpAndSettle();
-
-      // tester.sendText() cannot be used since the editor
-      // does not contain any EditableText widgets.
-      // to interact with the app during an integration test,
-      // simulate physical keyboard events.
-      await simulateKeyDownEvent(LogicalKeyboardKey.enter);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.enter);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.slash);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.keyB);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.keyO);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.keyR);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.keyD);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
-      await tester.pumpAndSettle();
-
-      // Checks whether the options in the selection menu
-      // for /board exist.
-      expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
-
-      // Finalizes the slash command that creates the board.
-      await simulateKeyDownEvent(LogicalKeyboardKey.enter);
-      await tester.pumpAndSettle();
-
-      // Checks whether new board is referenced and properly on the page.
-      expect(find.byType(BuiltInPageWidget), findsOneWidget);
-
-      // Checks whether the new board is in the side bar.
-      final sidebarLabel = LocaleKeys.newPageText.tr();
-      expect(find.text(sidebarLabel), findsOneWidget);
-    });
-
-    testWidgets('/grid shortcut creates a new grid', (tester) async {
-      const folderName = 'appflowy';
-      await TestFolder.cleanTestLocation(folderName);
-      await TestFolder.setTestLocation(folderName);
-
-      await tester.initializeAppFlowy();
-
-      // tap open button
-      await mockGetDirectoryPath(folderName);
-      await tester.tapOpenFolderButton();
-
-      await tester.wait(1000);
-      await tester.expectToSeeWelcomePage();
-
-      final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
-      // Necessary for being able to enterText when not in debug mode
-      binding.testTextInput.register();
-
-      // Needs tab to obtain focus for the app flowy editor.
-      // by default the tap appears at the center of the widget.
-      final Finder editor = find.byType(AppFlowyEditor);
-      await tester.tap(editor);
-      await tester.pumpAndSettle();
-
-      // tester.sendText() cannot be used since the editor
-      // does not contain any EditableText widgets.
-      // to interact with the app during an integration test,
-      // simulate physical keyboard events.
-      await simulateKeyDownEvent(LogicalKeyboardKey.enter);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.enter);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.arrowLeft);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.slash);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.keyG);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.keyR);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.keyI);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.keyD);
-      await tester.pumpAndSettle();
-      await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
-      await tester.pumpAndSettle();
-
-      // Checks whether the options in the selection menu
-      // for /grid exist.
-      expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
-
-      // Finalizes the slash command that creates the board.
-      await simulateKeyDownEvent(LogicalKeyboardKey.enter);
-      await tester.pumpAndSettle();
-
-      // Checks whether new board is referenced and properly on the page.
-      expect(find.byType(BuiltInPageWidget), findsOneWidget);
-
-      // Checks whether the new board is in the side bar.
-      final sidebarLabel = LocaleKeys.newPageText.tr();
-      expect(find.text(sidebarLabel), findsOneWidget);
-    });
   });
 }

+ 66 - 0
frontend/appflowy_flutter/integration_test/util/data.dart

@@ -0,0 +1,66 @@
+import 'dart:io';
+
+import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
+import 'package:archive/archive_io.dart';
+import 'package:flutter/services.dart';
+import 'package:path/path.dart' as p;
+import 'package:path_provider/path_provider.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+enum TestWorkspace {
+  board("board"),
+  emptyDocument("empty_document");
+
+  const TestWorkspace(this._name);
+
+  final String _name;
+
+  Future<File> get zip async {
+    final Directory parent = await TestWorkspace._parent;
+    final File out = File(p.join(parent.path, '$_name.zip'));
+    if (await out.exists()) return out;
+    await out.create();
+    final ByteData data = await rootBundle.load(_asset);
+    await out.writeAsBytes(data.buffer.asUint8List());
+    return out;
+  }
+
+  Future<Directory> get root async {
+    final Directory parent = await TestWorkspace._parent;
+    return Directory(p.join(parent.path, _name));
+  }
+
+  static Future<Directory> get _parent async {
+    final Directory root = await getTemporaryDirectory();
+    if (await root.exists()) return root;
+    await root.create();
+    return root;
+  }
+
+  String get _asset => 'assets/test/workspaces/$_name.zip';
+}
+
+class TestWorkspaceService {
+  const TestWorkspaceService(this.workspace);
+
+  final TestWorkspace workspace;
+
+  /// Instructs the application to read workspace data from the workspace found under this [TestWorkspace]'s path.
+  Future<void> setUpAll() async {
+    SharedPreferences.setMockInitialValues(
+      {
+        kSettingsLocationDefaultLocation:
+            await workspace.root.then((value) => value.path),
+      },
+    );
+  }
+
+  /// Workspaces that are checked into source are compressed. [TestWorkspaceService.setUp()] decompresses the file into an ephemeral directory that will be ignored by source control.
+  Future<void> setUp() async {
+    final inputStream =
+        InputFileStream(await workspace.zip.then((value) => value.path));
+    final archive = ZipDecoder().decodeBuffer(inputStream);
+    extractArchiveToDisk(
+        archive, await TestWorkspace._parent.then((value) => value.path));
+  }
+}

+ 12 - 0
frontend/appflowy_flutter/integration_test/util/keyboard.dart

@@ -0,0 +1,12 @@
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart' as flutter_test;
+
+class FlowyTestKeyboard {
+  static Future<void> simulateKeyDownEvent(List<LogicalKeyboardKey> keys,
+      {required flutter_test.WidgetTester tester}) async {
+    for (final LogicalKeyboardKey key in keys) {
+      await flutter_test.simulateKeyDownEvent(key);
+      await tester.pumpAndSettle();
+    }
+  }
+}

+ 1 - 0
frontend/appflowy_flutter/integration_test/util/util.dart

@@ -1,3 +1,4 @@
 export 'base.dart';
 export 'launch.dart';
 export 'settings.dart';
+export 'data.dart';

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart

@@ -5,7 +5,7 @@ import 'package:dartz/dartz.dart';
 
 class DatabaseBackendService {
   static Future<Either<List<DatabaseDescriptionPB>, FlowyError>>
-      getAllDatabase() {
+      getAllDatabases() {
     return DatabaseEventGetDatabases().send().then((result) {
       return result.fold((l) => left(l.items), (r) => right(r));
     });

+ 48 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/base/insert_page_command.dart

@@ -1,20 +1,55 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/database_view_service.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
+import 'package:appflowy/workspace/application/app/app_service.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:easy_localization/easy_localization.dart';
 
 const String kAppID = 'app_id';
 const String kViewID = 'view_id';
 
 extension InsertPage on EditorState {
-  void insertPage(AppPB appPB, ViewPB viewPB) {
+  Future<void> insertPage(AppPB appPB, ViewPB viewPB) async {
     final selection = service.selectionService.currentSelection.value;
     final textNodes =
         service.selectionService.currentSelectedNodes.whereType<TextNode>();
     if (selection == null || textNodes.isEmpty) {
       return;
     }
+
+    // get the database that the view is associated with
+    final database =
+        await DatabaseViewBackendService(viewId: viewPB.id).openGrid().then(
+              (value) => value.getLeftOrNull(),
+            );
+
+    if (database == null) {
+      throw StateError(
+          'The database associated with ${viewPB.id} could not be found while attempting to create a referenced ${viewPB.layout.name}.');
+    }
+
+    final prefix = referencedBoardPrefix(viewPB.layout);
+
+    final ref = await AppBackendService().createView(
+      appId: appPB.id,
+      name: "$prefix ${viewPB.name}",
+      desc: appPB.desc,
+      layoutType: viewPB.layout,
+      ext: {
+        'database_id': database.id,
+      },
+    ).then(
+      (value) => value.getLeftOrNull(),
+    );
+
+    // TODO(a-wallen): Show error dialog here.
+    if (ref == null) {
+      return;
+    }
+
     final transaction = this.transaction;
     transaction.insertNode(
       selection.end.path,
@@ -22,13 +57,24 @@ extension InsertPage on EditorState {
         type: _convertPageType(viewPB),
         attributes: {
           kAppID: appPB.id,
-          kViewID: viewPB.id,
+          kViewID: ref.id,
         },
       ),
     );
     apply(transaction);
   }
 
+  String referencedBoardPrefix(ViewLayoutTypePB layout) {
+    switch (layout) {
+      case ViewLayoutTypePB.Grid:
+        return LocaleKeys.grid_referencedGridPrefix.tr();
+      case ViewLayoutTypePB.Board:
+        return LocaleKeys.board_referencedBoardPrefix.tr();
+      default:
+        throw UnimplementedError();
+    }
+  }
+
   String _convertPageType(ViewPB viewPB) {
     switch (viewPB.layout) {
       case ViewLayoutTypePB.Grid:

+ 5 - 26
frontend/appflowy_flutter/pubspec.yaml

@@ -95,6 +95,7 @@ dependencies:
   http: ^0.13.5
   json_annotation: ^4.7.0
   path: ^1.8.2
+  archive: ^3.3.0
 
 dev_dependencies:
   flutter_lints: ^2.0.1
@@ -163,30 +164,8 @@ flutter:
     - assets/images/common/
     - assets/images/grid/setting/
     - assets/translations/
-  #   - images/a_dot_ham.jpeg
 
-  # An image asset can refer to one or more resolution-specific "variants", see
-  # https://flutter.dev/assets-and-images/#resolution-aware.
-
-  # For details regarding adding assets from package dependencies, see
-  # https://flutter.dev/assets-and-images/#from-packages
-
-  # To add custom fonts to your application, add a fonts section here,
-  # in this "flutter" section. Each entry in this list should have a
-  # "family" key with the font family name, and a "fonts" key with a
-  # list giving the asset and other descriptors for the font. For
-  # example:
-  # fonts:
-  #   - family: Schyler
-  #     fonts:
-  #       - asset: fonts/Schyler-Regular.ttf
-  #       - asset: fonts/Schyler-Italic.ttf
-  #         style: italic
-  #   - family: Trajan Pro
-  #     fonts:
-  #       - asset: fonts/TrajanPro.ttf
-  #       - asset: fonts/TrajanPro_Bold.ttf
-  #         weight: 700
-  #
-  # For details regarding fonts from package dependencies,
-  # see https://flutter.dev/custom-fonts/#from-packages
+    # The following assets will be excluded in release.
+    # BEGIN: EXCLUDE_IN_RELEASE
+    - assets/test/workspaces/
+    # END: EXCLUDE_IN_RELEASE

+ 28 - 0
frontend/scripts/flutter_release_build/build_flowy.dart

@@ -0,0 +1,28 @@
+import 'dart:io';
+
+part 'tool.dart';
+
+const excludeTagBegin = 'BEGIN: EXCLUDE_IN_RELEASE';
+const excludeTagEnd = 'END: EXCLUDE_IN_RELEASE';
+
+Future<void> main(List<String> args) async {
+  const help = '''
+A build script that modifies build assets before building the release version of AppFlowy.
+
+args[0]: The directory that contains the AppFlowy git repository. Should be the parent to appflowy_flutter. (absolute path)
+args[1]: The appflowy version to be built (github ref_name).
+''';
+  const numArgs = 2;
+  assert(args.length == numArgs,
+      'Expected ${numArgs}, got ${args.length}. Read the following for instructions about how to use this script.\n\n$help');
+  if (args[0] == '-h' || args[0] == '--help') {
+    stdout.write(help);
+    stdout.flush();
+  }
+  final repositoryRoot = Directory(args[0]);
+  assert(await repositoryRoot.exists(),
+      '$repositoryRoot is an invalid directory. Please try again with a valid directory.\n\n$help');
+  final appVersion = args[1];
+  await _BuildTool(repositoryRoot: repositoryRoot.path, appVersion: appVersion)
+      .run();
+}

+ 115 - 0
frontend/scripts/flutter_release_build/tool.dart

@@ -0,0 +1,115 @@
+part of 'build_flowy.dart';
+
+enum _ScanMode {
+  ignore,
+  target,
+}
+
+enum _ModifyMode {
+  include,
+  exclude,
+}
+
+class _BuildTool {
+  const _BuildTool({
+    required this.repositoryRoot,
+    required this.appVersion,
+  });
+
+  final String repositoryRoot;
+  final String appVersion;
+
+  String get projectRoot =>
+      [repositoryRoot, 'appflowy_flutter'].join(Platform.pathSeparator);
+
+  File get pubspec =>
+      File([projectRoot, 'pubspec.yaml'].join(Platform.pathSeparator));
+
+  Future<String> get _architecture async =>
+      await Process.run('uname', ['-m']).then((value) => value.stdout.trim());
+
+  Future<String> get _commandForOS async {
+    // Check the operating system and CPU architecture
+    var os = Platform.operatingSystem;
+    var arch = Platform.isMacOS ? await _architecture : Platform.localHostname;
+
+    // Determine the appropriate command based on the OS and architecture
+    if (os == 'windows') {
+      return 'cargo make --env APP_VERSION=$appVersion --profile production-windows-x86 appflowy';
+    }
+
+    if (os == 'linux') {
+      return 'cargo make --env APP_VERSION=$appVersion --profile production-linux-x86_64 appflowy';
+    }
+
+    if (os == 'macos') {
+      if (arch == 'x86_64') {
+        return 'cargo make --env APP_VERSION=$appVersion --profile production-mac-x86_64 appflowy';
+      }
+      if (arch == 'arm64') {
+        return 'cargo make --env APP_VERSION=$appVersion --profile production-mac-arm64 appflowy';
+      }
+      throw 'Unsupported CPU architecture: $arch';
+    }
+
+    throw 'Unsupported operating system: $os';
+  }
+
+  /// Scans a file for lines between # BEGIN: EXCLUDE_IN_RELEASE and
+  /// END: EXCLUDE_IN_RELEASE. Will add a comment to remove those assets
+  /// from the build.
+  Future<void> _process_directives(
+    File file, {
+    required _ModifyMode mode,
+  }) async {
+    // Read the contents of the file into a list
+    var lines = await file.readAsLines();
+
+    // Find the lines between BEGIN: EXCLUDE_IN_RELEASE and END: EXCLUDE_IN_RELEASE
+    var scanMode = _ScanMode.ignore;
+    for (var i = 0; i < lines.length; i++) {
+      var line = lines[i];
+      if (line.contains(excludeTagBegin)) {
+        scanMode = _ScanMode.target;
+      } else if (line.contains(excludeTagEnd)) {
+        scanMode = _ScanMode.ignore;
+      } else if (scanMode == _ScanMode.target) {
+        lines[i] = _modify(line, mode: mode);
+      }
+    }
+
+    // Write the modified contents back to the file
+    await file.writeAsString(lines.join('\n'));
+  }
+
+  String _modify(String line, {required _ModifyMode mode}) {
+    switch (mode) {
+      case _ModifyMode.include:
+        return line.split('#').where((element) => element != '#').join();
+      case _ModifyMode.exclude:
+        return '#$line';
+    }
+  }
+
+  Future<void> _build() async {
+    final cwd = Directory.current;
+    Directory.current = repositoryRoot;
+
+    final cmd = await _commandForOS;
+    // Run the command using the Process.run() function
+    // final build = await Process.run('echo', ['hello'], runInShell: true);
+    final build =
+        await Process.start(cmd.split(' ')[0], cmd.split(' ').sublist(1));
+    await stdout.addStream(build.stdout);
+    await stderr.addStream(build.stderr);
+    Directory.current = cwd;
+  }
+
+  Future<void> run() async {
+    final pubspec = this.pubspec;
+
+    await _process_directives(pubspec, mode: _ModifyMode.exclude);
+    await _build();
+    await _process_directives(pubspec, mode: _ModifyMode.include);
+  }
+}