Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
966547faa0 | ||
|
|
3647af44c9 | ||
|
|
25b4a647b0 | ||
|
|
c6e3c1fd7c | ||
|
|
4fd35d0de4 | ||
|
|
7406c5e6a5 | ||
|
|
aa70885b21 | ||
|
|
8699c9c125 | ||
|
|
bc758e0b06 | ||
|
|
e38980ccc3 | ||
|
|
8dd180046f | ||
|
|
0cd2726752 | ||
|
|
11daa15b89 | ||
|
|
8436bc6144 | ||
|
|
59851f46b7 | ||
|
|
62f1d6eb82 | ||
|
|
ec80f72c8f | ||
|
|
b2c80981cc | ||
|
|
8fa425a76c | ||
|
|
ebe112581d | ||
|
|
bc8f35d7db | ||
|
|
058eeec932 | ||
|
|
8e10cba8e5 | ||
|
|
7a24f6b6f0 | ||
|
|
4f40945458 | ||
|
|
e59870e421 | ||
|
|
cf521fe20b | ||
|
|
f0d47ee50c | ||
|
|
2a5d393c9f | ||
|
|
98e0b832ec | ||
|
|
77e7cbe9ec | ||
|
|
acd53eb640 | ||
|
|
ffdf5d24a0 | ||
|
|
b247a49417 | ||
|
|
dace02d34d | ||
|
|
41d4351176 | ||
|
|
d4bc575c03 | ||
|
|
a59561aee3 | ||
|
|
92523321f1 | ||
|
|
537c9d0865 | ||
|
|
b4051c7e9f | ||
|
|
9e61133886 | ||
|
|
36f47f3636 | ||
|
|
40dcd13394 | ||
|
|
eb20c7c117 | ||
|
|
e9852a3934 | ||
|
|
78d8ec9a3e | ||
|
|
4220309cf8 | ||
|
|
812af0979e | ||
|
|
cca5005541 | ||
|
|
9306d4a4ee | ||
|
|
3ab1ff5e73 | ||
|
|
05fa306782 | ||
|
|
d054439df4 | ||
|
|
15aaacc4b7 | ||
|
|
ef9de52cbc | ||
|
|
8d9d0e9358 | ||
|
|
7f44b181bd | ||
|
|
01c3fec5aa | ||
|
|
94a3a59001 | ||
|
|
4a433a3176 | ||
|
|
f7749bdccc | ||
|
|
c864e836ee | ||
|
|
0738b5f87d | ||
|
|
6198c3907f | ||
|
|
891d794900 | ||
|
|
890f52a00a | ||
|
|
d007f98af6 | ||
|
|
15e9d65798 | ||
|
|
0626f0deb7 | ||
|
|
007c649c31 | ||
|
|
34ea508f5b | ||
|
|
f70aef68be | ||
|
|
61a31c90ee | ||
|
|
c7e3ed805a | ||
|
|
d36a846425 | ||
|
|
a02ec98e9c | ||
|
|
32c06cf1bf | ||
|
|
cf1096b516 | ||
|
|
938953d7b3 | ||
|
|
949dab9afe | ||
|
|
0996f5fd24 | ||
|
|
6e887f1f12 | ||
|
|
ab9338eb89 | ||
|
|
fd4088aed0 | ||
|
|
375ff5ed6d | ||
|
|
847290d9a4 | ||
|
|
56687581f2 | ||
|
|
3be39f5e7b | ||
|
|
1997231adf | ||
|
|
89face4f02 | ||
|
|
37ddce3a29 | ||
|
|
593df96b10 | ||
|
|
1b966171c4 | ||
|
|
047f1a0b39 | ||
|
|
f2f3506b29 | ||
|
|
0ccced4b54 | ||
|
|
f762a564b5 | ||
|
|
57b78ee3c6 | ||
|
|
5dfc470873 | ||
|
|
d5f8ce5435 | ||
|
|
3c65a96b04 | ||
|
|
6ba7fc0317 | ||
|
|
37a5054ebd | ||
|
|
048434024b | ||
|
|
2c757e9b6c | ||
|
|
9c59e1487e | ||
|
|
4b9b723521 | ||
|
|
a389836897 | ||
|
|
eec7723ce0 | ||
|
|
2f938a331f | ||
|
|
847d7cb35a | ||
|
|
094f8fc307 | ||
|
|
e3f11ea9c0 | ||
|
|
124d435f09 | ||
|
|
b700f95c7f | ||
|
|
daf9b48c73 | ||
|
|
0ea5b3c483 | ||
|
|
f3148640eb | ||
|
|
1c84ee1d53 | ||
|
|
cecd4f48ab | ||
|
|
1e001640aa | ||
|
|
3426e5b53e | ||
|
|
936b49a63f | ||
|
|
f6f80b48c9 | ||
|
|
759cb27a43 | ||
|
|
26a2bffcd1 | ||
|
|
fe55452c79 | ||
|
|
16adacac38 | ||
|
|
d86531ac92 | ||
|
|
9231455f0d | ||
|
|
0c6a1d4ae7 | ||
|
|
524efc2620 | ||
|
|
50a4f03931 | ||
|
|
a4681a4042 | ||
|
|
1ca130d7de | ||
|
|
ef6f9a3175 | ||
|
|
aa25aa4474 | ||
|
|
c7af04b317 | ||
|
|
9565173baf | ||
|
|
5f4e3ecc76 | ||
|
|
043c68a333 | ||
|
|
f639d51c11 | ||
|
|
71071b60b2 | ||
|
|
608255fa0f | ||
|
|
f8b0a26acc | ||
|
|
133dab934b | ||
|
|
a81670c447 | ||
|
|
35f84e42cc | ||
|
|
2f6df6a17d | ||
|
|
8bcc6384f2 | ||
|
|
a0fc4b86b0 | ||
|
|
790580eea4 | ||
|
|
3db9024a2d | ||
|
|
a821740469 | ||
|
|
af0ae2e375 | ||
|
|
7d6fa0b159 | ||
|
|
b73a34ed94 | ||
|
|
f3aaff77b9 | ||
|
|
9b7ff375b2 | ||
|
|
18498c0479 | ||
|
|
c652c32575 | ||
|
|
df8642d446 | ||
|
|
abb6eff23d | ||
|
|
3b4f8e53a2 | ||
|
|
4847b8b114 | ||
|
|
e8f124b97c | ||
|
|
f73d59fb57 | ||
|
|
897d7980f6 | ||
|
|
b88aed887a | ||
|
|
4e863bc87f | ||
|
|
507554a596 | ||
|
|
0806436c19 | ||
|
|
1205f0ebf7 | ||
|
|
26e56194ea | ||
|
|
4e67282f2b | ||
|
|
41ec2d992e | ||
|
|
f0e4f3db61 | ||
|
|
6634a0ecb3 | ||
|
|
255f30590f | ||
|
|
1ba7224088 |
No files matched your search
@@ -44,7 +44,7 @@ test "" = "$(grep '^Signed-off-by: ' "$1" |
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
printError "Please fix your commit message to match AppFlowy coding standards"
|
||||
printError "https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/code-submission-guidelines#commit-message-guidelines"
|
||||
printError "https://docs.appflowy.io/docs/documentation/software-contributions/conventions/git-conventions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -21,7 +21,7 @@ Before you mark this PR ready for review, run through this checklist!
|
||||
|
||||
#### PR Checklist
|
||||
|
||||
- [ ] My code adheres to the [AppFlowy Style Guide](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides)
|
||||
- [ ] My code adheres to [AppFlowy's Conventions](https://docs.appflowy.io/docs/documentation/software-contributions/conventions)
|
||||
- [ ] I've listed at least one issue that this PR fixes in the description above.
|
||||
- [ ] I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes.
|
||||
- [ ] All existing tests are passing.
|
||||
@@ -49,6 +49,15 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
# the following step is required to avoid running out of space
|
||||
- name: Maximize build space
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -88,7 +97,7 @@ jobs:
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install keybinder-3.0 libnotify-dev
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
vcpkg integrate install
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
|
||||
@@ -45,6 +45,15 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
# the following step is required to avoid running out of space
|
||||
- name: Maximize build space
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/share/boost
|
||||
df -h
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -85,7 +94,7 @@ jobs:
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install keybinder-3.0 libnotify-dev
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
vcpkg integrate install
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
|
||||
@@ -35,13 +35,15 @@ jobs:
|
||||
release_name: v${{ github.ref }}
|
||||
body_path: ${{ env.RELEASE_NOTES_PATH }}
|
||||
|
||||
# the package name should be with the format: AppFlowy-<version>-<os>-<arch>
|
||||
|
||||
build-for-windows:
|
||||
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
||||
needs: create-release
|
||||
env:
|
||||
WINDOWS_APP_RELEASE_PATH: frontend\appflowy_flutter\product\${{ github.ref_name }}\windows
|
||||
WINDOWS_ZIP_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64.zip
|
||||
WINDOWS_INSTALLER_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64
|
||||
WINDOWS_ZIP_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64.zip
|
||||
WINDOWS_INSTALLER_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -133,8 +135,8 @@ jobs:
|
||||
needs: create-release
|
||||
env:
|
||||
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release
|
||||
MACOS_X86_ZIP_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64.zip
|
||||
MACOS_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64
|
||||
MACOS_X86_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64.zip
|
||||
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -235,8 +237,8 @@ jobs:
|
||||
needs: create-release
|
||||
env:
|
||||
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release
|
||||
MACOS_AARCH64_ZIP_NAME: AppFlowy_${{ github.ref_name }}_macos-universal.zip
|
||||
MACOS_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-universal
|
||||
MACOS_AARCH64_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-universal.zip
|
||||
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-universal
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -339,9 +341,11 @@ jobs:
|
||||
needs: create-release
|
||||
env:
|
||||
LINUX_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/linux/Release
|
||||
LINUX_ZIP_NAME: AppFlowy_${{ matrix.job.target }}_${{ matrix.job.os }}.tar.gz
|
||||
LINUX_PACKAGE_NAME: AppFlowy_${{ github.ref_name }}_${{ matrix.job.os }}.deb
|
||||
# PKG_CONFIG_SYSROOT_DIR: /
|
||||
LINUX_ZIP_NAME: AppFlowy-${{ matrix.job.target }}-x86_64.tar.gz
|
||||
LINUX_PACKAGE_DEB_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.deb
|
||||
LINUX_PACKAGE_RPM_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.rpm
|
||||
LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -384,7 +388,8 @@ jobs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install keybinder-3.0 libnotify-dev
|
||||
sudo apt-get -y install alien
|
||||
source $HOME/.cargo/env
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
@@ -409,8 +414,17 @@ jobs:
|
||||
- name: Build Linux package (.deb)
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sh scripts/linux_distribution/deb/build_deb.sh appflowy_flutter/product/${{ github.ref_name }}/linux/Release ${{ github.ref_name }} ${{ env.LINUX_PACKAGE_NAME }}
|
||||
|
||||
sh scripts/linux_distribution/deb/build_deb.sh appflowy_flutter/product/${{ github.ref_name }}/linux/Release ${{ github.ref_name }} ${{ env.LINUX_PACKAGE_DEB_NAME }}
|
||||
|
||||
- name: Build Linux package (.rpm)
|
||||
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
|
||||
run: |
|
||||
ls -a
|
||||
sudo alien -r ${{ env.LINUX_PACKAGE_DEB_NAME }}
|
||||
echo '------------'
|
||||
cp -r ${{ env.LINUX_PACKAGE_TMP_RPM_NAME }} ${{ env.LINUX_PACKAGE_RPM_NAME }}
|
||||
ls -a
|
||||
|
||||
- name: Upload Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -423,15 +437,25 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Debian package
|
||||
id: upload-release-asset-install-package
|
||||
id: upload-release-asset-install-package-deb
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_NAME }}
|
||||
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_DEB_NAME }}
|
||||
asset_name: ${{ env.LINUX_PACKAGE_DEB_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
asset_name: ${{ env.LINUX_PACKAGE_NAME }}
|
||||
- name: Upload RPM package
|
||||
id: upload-release-asset-install-package-rpm
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_RPM_NAME }}
|
||||
asset_name: ${{ env.LINUX_PACKAGE_RPM_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
build-for-docker:
|
||||
|
||||
@@ -60,8 +60,8 @@ jobs:
|
||||
echo SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }} >> .env.ci
|
||||
echo SUPABASE_JWT_SECRET=${{ secrets.SUPABASE_JWT_SECRET }} >> .env.ci
|
||||
|
||||
- name: Create .env file in flowy-test
|
||||
working-directory: frontend/rust-lib/flowy-test
|
||||
- name: Create .env file in event-integration
|
||||
working-directory: frontend/rust-lib/event-integration
|
||||
run: |
|
||||
touch .env.ci
|
||||
echo SUPABASE_URL=${{ secrets.SUPABASE_URL }} >> .env.ci
|
||||
|
||||
@@ -32,6 +32,26 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Cache Rust Dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: rust-dependencies-${{ runner.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
frontend/appflowy_tauri/src-tauri
|
||||
|
||||
- name: Cache Node.js dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ runner.os }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_tauri/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -40,13 +60,6 @@ jobs:
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: 'ubuntu-latest-tauri'
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
frontend/appflowy_tauri/src-tauri
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
working-directory: frontend
|
||||
@@ -75,15 +88,23 @@ jobs:
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
npm install -g pnpm@${{ env.PNPM_VERSION }}
|
||||
|
||||
|
||||
- name: build
|
||||
- name: Build
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
cargo make --cwd .. tauri_build
|
||||
pnpm test
|
||||
pnpm test:errors
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
run: |
|
||||
diff_files=$(git status --porcelain)
|
||||
if [ -n "$diff_files" ]; then
|
||||
echo "There are uncommitted changes in the working tree. Please commit them before pushing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -41,3 +41,5 @@ pubspec.lock
|
||||
# ignore the deb filegit
|
||||
frontend/package
|
||||
frontend/*.deb
|
||||
|
||||
**/Cargo.toml.bak
|
||||
@@ -1,5 +1,92 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.3.6 - 10/16/2023
|
||||
|
||||
### New Features
|
||||
- Support setting Markdown styles through keyboard shortcuts.
|
||||
- Added Ukrainian language.
|
||||
- Support auto-hiding sidebar feature, ensuring a streamlined view even when resizing to a smaller window.
|
||||
- Support toggling the notifitcation on/off.
|
||||
- Added Lemonade theme.
|
||||
|
||||
|
||||
### Bug fixes
|
||||
- Improve Vietnamese translations.
|
||||
- Improve reminder feature.
|
||||
- Fix some UI issues.
|
||||
|
||||
## Version 0.3.5 - 10/09/2023
|
||||
|
||||
### New Features
|
||||
- Added support for browsing and inserting images from Unsplash.
|
||||
- Revamp and unify the emoji picker throughout AppFlowy.
|
||||
|
||||
### Bug fixes
|
||||
- Improve layout of the settings page.
|
||||
- Improve design of the restore page banner.
|
||||
- Improve UX of the reminders.
|
||||
- Other UI fixes.
|
||||
|
||||
## Version 0.3.4 - 10/02/2023
|
||||
|
||||
### New Features
|
||||
- Added support for creating a reminder.
|
||||
- Added support for finding and replacing in the document page.
|
||||
- Added support for showing the hidden fields in row detail page.
|
||||
- Adjust the toolbar style in RTL mode.
|
||||
|
||||
### Bug fixes
|
||||
- Improve snackbar UI design.
|
||||
- Improve dandelion theme.
|
||||
- Improve id-ID and pl-PL language translations.
|
||||
|
||||
## Version 0.3.3 - 09/24/2023
|
||||
|
||||
### New Features
|
||||
- Added an end date field to the time cell in the database.
|
||||
- Added Support for customizing the font family from GoogleFonts in the editor.
|
||||
- Set the uploaded image to cover by default.
|
||||
- Added Support for resetting the user icon on settings page
|
||||
- Add Urdu language translations.
|
||||
|
||||
### Bug fixes
|
||||
- Default colors for the blocks except for the callout were not transparent.
|
||||
- Option/Alt + click to add a block above didn't work on the first line.
|
||||
- Unable to paste HTML content containing `<mark>` tag.
|
||||
- Unable to select the text from anywhere in the line.
|
||||
- The selection in the editor didn't clear when editing the inline database.
|
||||
- Added a bottom border to new property column in the database.
|
||||
- Set minimum width of 50px for grid fields.
|
||||
|
||||
## Version 0.3.2 - 09/18/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Improve the performance of the editor, now it is much faster when editing a large document.
|
||||
- Support for reordering the rows of the database on Windows.
|
||||
- Revamp the row detail page of the database.
|
||||
- Revamp the checklist cell editor of the database.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Some UI issues
|
||||
|
||||
## Version 0.3.1 - 09/04/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Improve CJK (Chinese, Japanese, Korean) input method support.
|
||||
- Share a database in CSV format.
|
||||
- Support for aligning the block component with the toolbar.
|
||||
- Support for editing name when creating a new page.
|
||||
- Support for inserting a table in the document page.
|
||||
- Database views allow for independent field visibility toggling.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Paste multiple lines in code block.
|
||||
- Some UI issues
|
||||
|
||||
## Version 0.3.0 - 08/22/2023
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -47,7 +47,7 @@ You are in charge of your data and customizations.
|
||||
|
||||
## Getting Started with development
|
||||
|
||||
Please view the [documentation](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy) for OS specific development instructions
|
||||
Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific development instructions
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -73,7 +73,7 @@ Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitt
|
||||
|
||||
[](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)
|
||||
|
||||
To add translations, you can manually edit the JSON translation files in `/frontend/appflowy_flutter/assets/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
|
||||
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
|
||||
|
||||
|
||||
## Join the community to build AppFlowy together
|
||||
|
||||
Vendored
+8
@@ -285,6 +285,14 @@
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate AppFlowyEnv",
|
||||
"type": "shell",
|
||||
"command": "dart run build_runner build --delete-conflicting-outputs",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter/packages/appflowy_backend"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -20,11 +20,12 @@ run_task = { name = ["restore-crate-type"] }
|
||||
|
||||
[env]
|
||||
RUST_LOG = "info"
|
||||
CARGO_PROFILE = "dev"
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.3.0"
|
||||
CURRENT_APP_VERSION = "0.3.6"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
@@ -75,6 +76,7 @@ BUILD_ACTIVE_ARCHS_ONLY = true
|
||||
CRATE_TYPE = "staticlib"
|
||||
|
||||
[env.production-mac-arm64]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "aarch64-apple-darwin"
|
||||
@@ -86,6 +88,7 @@ BUILD_ACTIVE_ARCHS_ONLY = false
|
||||
CRATE_TYPE = "staticlib"
|
||||
|
||||
[env.production-mac-x86_64]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "x86_64-apple-darwin"
|
||||
@@ -97,6 +100,7 @@ BUILD_ACTIVE_ARCHS_ONLY = false
|
||||
CRATE_TYPE = "staticlib"
|
||||
|
||||
[env.production-mac-universal]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "macos"
|
||||
FLUTTER_OUTPUT_DIR = "Release"
|
||||
@@ -114,6 +118,7 @@ CRATE_TYPE = "cdylib"
|
||||
LIB_EXT = "dll"
|
||||
|
||||
[env.production-windows-x86]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "windows"
|
||||
RUST_COMPILE_TARGET = "x86_64-pc-windows-msvc"
|
||||
@@ -133,6 +138,7 @@ LIB_EXT = "so"
|
||||
LINUX_ARCH = "x64"
|
||||
|
||||
[env.production-linux-x86_64]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "linux"
|
||||
RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
|
||||
@@ -153,6 +159,7 @@ LINUX_ARCH = "arm64"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite,openssl_vendored"
|
||||
|
||||
[env.production-linux-aarch64]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "linux"
|
||||
RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu"
|
||||
@@ -202,6 +209,7 @@ FLUTTER_OUTPUT_DIR = "Debug"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite,openssl_vendored"
|
||||
|
||||
[env.production-android]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "android"
|
||||
CRATE_TYPE = "cdylib"
|
||||
|
||||
@@ -69,8 +69,8 @@ windows/flutter/dart_ffi/
|
||||
**/.sandbox
|
||||
**/.vscode/
|
||||
|
||||
*.env
|
||||
*.env.*
|
||||
.env
|
||||
.env.*
|
||||
|
||||
coverage/
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@ This Repository contains the codebase for the frontend of the application, curre
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
> We later expect to extend support to Android and iOS devices using Flutter.
|
||||
> We are actively working on support for Android & iOS!
|
||||
|
||||
_Additionally, we are working on a Web version built with Tauri!_
|
||||
|
||||
### Am I Eligible to Contribute?
|
||||
|
||||
Yes! You are eligible to contribute, check out the ways in which you can [contribute to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy). Some of the ways in which you can contribute are:
|
||||
Yes! You are eligible to contribute, check out the ways in which you can [contribute to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy). Some of the ways in which you can contribute are:
|
||||
|
||||
- Non-Coding Contributions
|
||||
- Documentation
|
||||
@@ -26,27 +28,23 @@ Yes! You are eligible to contribute, check out the ways in which you can [contri
|
||||
- Improve Translations
|
||||
- Coding Contributions
|
||||
|
||||
To contribute to `AppFlowy_Flutter` codebase specifically (coding contribution) we suggest you to have basic knowledge of Flutter. In case you are new to Flutter, we may suggest you to learn the basics and then try to contribute, get started with Flutter [here](https://flutter.dev/docs/get-started/codelab).
|
||||
To contribute to `AppFlowy_Flutter` codebase specifically (coding contribution) we suggest you to have basic knowledge of Flutter. In case you are new to Flutter, we suggest you learn the basics, and then contribute afterwards. To get started with Flutter read [here](https://flutter.dev/docs/get-started/codelab).
|
||||
|
||||
### What OS Should I Use for Development?
|
||||
### What OS should I use for development?
|
||||
|
||||
We support all OS for Development i.e Linux, macOS and Windows. However, most of us promote macOS and Linux over Windows. We have detailed [docs](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/environment-setup) on How to Setup `AppFlowy_Flutter` in your local system in each OS.
|
||||
We support all OS for Development i.e. Linux, MacOS and Windows. However, most of us promote macOS and Linux over Windows. We have detailed [docs](https://docs.appflowy.io/docs/documentation/appflowy/from-source/environment-setup) on how to setup `AppFlowy_Flutter` on your local system respectively per operating system.
|
||||
|
||||
### Getting Started ❇
|
||||
|
||||
We have a detailed documentation, on how to [get started](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) with the project, and make your first contribution. However, we do have some specific picks for you.
|
||||
We have detailed documentation on how to [get started](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) with the project, and make your first contribution. However, we do have some specific picks for you:
|
||||
|
||||
- [Code Architecture](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/frontend/codemap)
|
||||
- [Making Your First PR](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/submitting-your-first-pull-request)
|
||||
- [The Style Guide](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides)
|
||||
- [How to run/debug the application](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/launcher-and-tasks)
|
||||
- [Styleguide & Conventions](https://docs.appflowy.io/docs/documentation/software-contributions/conventions/naming-conventions)
|
||||
- [Making Your First PR](https://docs.appflowy.io/docs/documentation/software-contributions/submitting-code/submitting-your-first-pull-request)
|
||||
- [All AppFlowy Documentation](https://docs.appflowy.io/docs/documentation/appflowy) - Contribution guide, build and run, debugging, testing, localization, etc.
|
||||
|
||||
### Need Help?
|
||||
|
||||
- New to GitHub? Follow [these](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/setting-up-your-repositories) steps to get started
|
||||
- Stuck Somewhere? Join the [Discord](https://discord.gg/9Q2xaN37tV) Group and we are there to help you!
|
||||
|
||||
<!--
|
||||
## release check
|
||||
1. [entitlements](https://flutter.dev/desktop#setting-up-entitlements)
|
||||
2. [symbols stripped](https://flutter.dev/docs/development/platform-integration/c-interop) -->
|
||||
- New to GitHub? Follow [these](https://docs.appflowy.io/docs/documentation/software-contributions/submitting-code/setting-up-your-repositories) steps to get started
|
||||
- Stuck Somewhere? Join our [Discord](https://discord.gg/9Q2xaN37tV), we're there to help you!
|
||||
- Find out more about the [community initiatives](https://docs.appflowy.io/docs/appflowy/community).
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,23 @@
|
||||
# Initial Setup
|
||||
# 1. Copy the dev.env file to .env:
|
||||
# cp dev.env .env
|
||||
# 2. Alternatively, you can generate the .env file using the "Generate Env File" task in VSCode.
|
||||
|
||||
# Configuring Cloud Type
|
||||
# This configuration file is used to specify the cloud type and the necessary configurations for each cloud type. The available options are:
|
||||
# Local: 0
|
||||
# Supabase: 1
|
||||
# AppFlowy Cloud: 2
|
||||
|
||||
CLOUD_TYPE=0
|
||||
|
||||
# Supabase Configuration
|
||||
# If you're using Supabase (CLOUD_TYPE=1), you need to provide the following configurations:
|
||||
SUPABASE_URL=replace-with-your-supabase-url
|
||||
SUPABASE_ANON_KEY=replace-with-your-supabase-key
|
||||
|
||||
# AppFlowy Cloud Configuration
|
||||
# If you're using AppFlowy Cloud (CLOUD_TYPE=2), you need to provide the following configurations:
|
||||
APPFLOWY_CLOUD_BASE_URL=replace-with-your-appflowy-cloud-url
|
||||
APPFLOWY_CLOUD_WS_BASE_URL=replace-with-your-appflowy-cloud-ws-url
|
||||
APPFLOWY_CLOUD_GOTRUE_URL=replace-with-your-appflowy-cloud-gotrue-url
|
||||
@@ -80,7 +80,8 @@ void main() {
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
|
||||
expect(find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
|
||||
expect(
|
||||
find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'util/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('calendar database view', () {
|
||||
group('calendar', () {
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -79,33 +79,33 @@ void main() {
|
||||
// Tap on create new event button
|
||||
await tester.tapAddCalendarEventButton();
|
||||
|
||||
// Make sure that the row details page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
// Make sure that the event editor popup is shown
|
||||
tester.assertEventEditorOpen();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
|
||||
// Dismiss the event editor popup
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Double click on today's calendar cell to create a new event
|
||||
await tester.doubleClickCalendarCell(DateTime.now());
|
||||
|
||||
// Make sure that the row details page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
// Make sure that the event editor popup is shown
|
||||
tester.assertEventEditorOpen();
|
||||
|
||||
// Make sure that the event is inserted in the cell
|
||||
tester.assertNumberOfEventsInCalendar(2);
|
||||
|
||||
// Dismiss the event editor popup
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
tester.assertRowDetailPageOpened();
|
||||
tester.assertEventEditorOpen();
|
||||
|
||||
// Change the title of the event
|
||||
await tester.editTitleInRowDetailPage('hello world');
|
||||
await tester.dismissRowDetailPage();
|
||||
await tester.editEventTitle('hello world');
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Make sure that the event is edited
|
||||
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
|
||||
@@ -113,9 +113,14 @@ void main() {
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
tester.assertEventEditorOpen();
|
||||
|
||||
// Click on the open icon
|
||||
await tester.openEventToRowDetailPage();
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Duplicate the event
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDuplicateRowButton();
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
@@ -125,11 +130,23 @@ void main() {
|
||||
|
||||
// Delete an event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
await tester.deleteEventFromEventEditor();
|
||||
|
||||
// Check that there is 1 event
|
||||
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
|
||||
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
|
||||
|
||||
// Delete event from row detail page
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
await tester.openEventToRowDetailPage();
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
|
||||
// Check that there is 0 event
|
||||
tester.assertNumberOfEventsInCalendar(0, title: 'hello world');
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now());
|
||||
});
|
||||
|
||||
testWidgets('rescheduling events', (tester) async {
|
||||
@@ -143,7 +160,7 @@ void main() {
|
||||
final today = DateTime.now();
|
||||
final firstOfThisMonth = DateTime(today.year, today.month, 1);
|
||||
await tester.doubleClickCalendarCell(firstOfThisMonth);
|
||||
await tester.dismissRowDetailPage();
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Drag and drop the event onto the next week, same day
|
||||
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
|
||||
@@ -155,12 +172,12 @@ void main() {
|
||||
|
||||
// Delete the event
|
||||
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
await tester.deleteEventFromEventEditor();
|
||||
|
||||
// Create a new event in today's calendar cell
|
||||
await tester.scrollToToday();
|
||||
await tester.doubleClickCalendarCell(today);
|
||||
await tester.dismissRowDetailPage();
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Make sure that the event is today
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, today);
|
||||
@@ -179,12 +196,43 @@ void main() {
|
||||
await tester.selectDay(content: newDate.day);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
// Dismiss the event editor
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Make sure that the event is edited
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, newDate);
|
||||
|
||||
// Click on the unscheduled events button
|
||||
await tester.openUnscheduledEventsPopup();
|
||||
|
||||
// Assert that nothing shows up
|
||||
tester.findUnscheduledPopup(findsNothing, 0);
|
||||
|
||||
// Click on the event in the calendar
|
||||
await tester.openCalendarEvent(index: 0, date: newDate);
|
||||
|
||||
// Open the date editor of the event
|
||||
await tester.tapDateCellInRowDetailPage();
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Clear the date of the event
|
||||
await tester.clearDate();
|
||||
|
||||
// Dismiss the event editor
|
||||
await tester.dismissEventEditor();
|
||||
tester.assertNumberOfEventsInCalendar(0);
|
||||
|
||||
// Click on the unscheduled events button
|
||||
await tester.openUnscheduledEventsPopup();
|
||||
|
||||
// Assert that a popup appears and 1 unscheduled event
|
||||
tester.findUnscheduledPopup(findsOneWidget, 1);
|
||||
|
||||
// Click on the unscheduled event
|
||||
await tester.clickUnscheduledEvent();
|
||||
|
||||
tester.assertRowDetailPageOpened();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -185,7 +185,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit time cell', (tester) async {
|
||||
testWidgets('edit date time cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -218,7 +218,6 @@ void main() {
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('MMM dd, y').format(today),
|
||||
);
|
||||
|
||||
@@ -233,7 +232,6 @@ void main() {
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('MMM dd, y HH:mm').format(now),
|
||||
);
|
||||
|
||||
@@ -247,7 +245,6 @@ void main() {
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('dd/MM/y HH:mm').format(now),
|
||||
);
|
||||
|
||||
@@ -261,7 +258,6 @@ void main() {
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('dd/MM/y hh:mm a').format(now),
|
||||
);
|
||||
|
||||
@@ -273,7 +269,6 @@ void main() {
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: '',
|
||||
);
|
||||
|
||||
@@ -442,4 +437,106 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('edit checklist cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
const fieldType = FieldType.Checklist;
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
|
||||
// assert that there is no progress bar in the grid
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
|
||||
// tap on the first checklist cell
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// assert that the checklist editor is shown
|
||||
tester.assertChecklistEditorVisible(visible: true);
|
||||
|
||||
// create a new task with enter
|
||||
await tester.createNewChecklistTask(name: "task 0", enter: true);
|
||||
|
||||
// assert that the task is displayed
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 0",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// update the task's name
|
||||
await tester.renameChecklistTask(index: 0, name: "task 1");
|
||||
|
||||
// assert that the task's name is updated
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 1",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// dismiss new task editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// dismiss checklist cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progress bar is shown in grid at 0%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
|
||||
|
||||
// start editing the first checklist cell again
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// create another task with the create button
|
||||
await tester.createNewChecklistTask(name: "task 2", button: true);
|
||||
|
||||
// assert that the task was inserted
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// mark it as complete
|
||||
await tester.checkChecklistTask(index: 1);
|
||||
|
||||
// assert that the task was checked in the editor
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: true,
|
||||
);
|
||||
|
||||
// dismiss checklist editor
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progressbar is shown in grid at 50%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
|
||||
|
||||
// re-open the cell editor
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// hover over first task and delete it
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
|
||||
// dismiss cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progressbar is shown in grid at 100%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
|
||||
|
||||
// re-open the cell edior
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// delete the remaining task
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
|
||||
// dismiss the cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// check that the progress bar is not viisble
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database field settings', () {
|
||||
testWidgets('field visibility', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
|
||||
|
||||
// create a field
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// hide the field
|
||||
await tester.tapGridFieldWithName('New field 1');
|
||||
await tester.tapHidePropertyButton();
|
||||
await tester.noFieldWithName('New field 1');
|
||||
|
||||
// go back to inline database view, expect field to be shown
|
||||
await tester.tapTabBarLinkedViewByViewName('Untitled');
|
||||
await tester.findFieldWithName('New field 1');
|
||||
|
||||
// go back to linked database view, expect field to be hidden
|
||||
await tester.tapTabBarLinkedViewByViewName('grid');
|
||||
await tester.noFieldWithName('New field 1');
|
||||
|
||||
// use the settings button to show the field
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapViewPropertiesButton();
|
||||
await tester.tapViewTogglePropertyVisibilityButtonByName('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
await tester.findFieldWithName('New field 1');
|
||||
|
||||
// open first row in popup then hide the field
|
||||
await tester.openFirstRowDetailPage();
|
||||
await tester.tapGridFieldWithNameInRowDetailPage('New field 1');
|
||||
await tester.tapHidePropertyButtonInFieldEditor();
|
||||
await tester.dismissRowDetailPage();
|
||||
await tester.noFieldWithName('New field 1');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -106,26 +106,6 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('hide field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// create a field
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// Delete the field
|
||||
await tester.tapGridFieldWithName('New field 1');
|
||||
await tester.tapHidePropertyButton();
|
||||
|
||||
await tester.noFieldWithName('New field 1');
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create checklist field ', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -135,7 +135,88 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('check document is exist in row detail page', (tester) async {
|
||||
testWidgets('change order of fields and cells', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// Assert that the first field in the row details page is the select
|
||||
// option type
|
||||
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
|
||||
|
||||
// Reorder first field in list
|
||||
final gesture = await tester.hoverOnFieldInRowDetail(index: 0);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.reorderFieldInRowDetail(offset: 30);
|
||||
|
||||
// Orders changed, now the checkbox is first
|
||||
tester.assertFirstFieldInRowDetailByType(FieldType.Checkbox);
|
||||
await gesture.removePointer();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Reorder second field in list
|
||||
await tester.hoverOnFieldInRowDetail(index: 1);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.reorderFieldInRowDetail(offset: -30);
|
||||
|
||||
// First field is now back to select option
|
||||
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
|
||||
});
|
||||
|
||||
testWidgets('hide and show hidden fields', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// Assert that the show hidden fields button isn't visible
|
||||
tester.assertToggleShowHiddenFieldsVisibility(false);
|
||||
|
||||
// Hide the first field in the field list
|
||||
await tester.tapGridFieldWithNameInRowDetailPage("Type");
|
||||
await tester.tapHidePropertyButtonInFieldEditor();
|
||||
|
||||
// Assert that the field is now hidden
|
||||
tester.noFieldWithName("Type");
|
||||
|
||||
// Assert that the show hidden fields button appears
|
||||
tester.assertToggleShowHiddenFieldsVisibility(true);
|
||||
|
||||
// Click on the show hidden fields button
|
||||
await tester.toggleShowHiddenFields();
|
||||
|
||||
// Assert that the hidden field is shown again and that the show
|
||||
// hidden fields button is still present
|
||||
tester.findFieldWithName("Type");
|
||||
tester.assertToggleShowHiddenFieldsVisibility(true);
|
||||
|
||||
// Click hide hidden fields
|
||||
await tester.toggleShowHiddenFields();
|
||||
|
||||
// Assert that the hidden field has vanished
|
||||
tester.noFieldWithName("Type");
|
||||
|
||||
// Click show hidden fields
|
||||
await tester.toggleShowHiddenFields();
|
||||
|
||||
// delete the hidden field
|
||||
await tester.tapGridFieldWithNameInRowDetailPage("Type");
|
||||
await tester.tapDeletePropertyInFieldEditor();
|
||||
|
||||
// Assert that the that the show hidden fields button is gone
|
||||
tester.assertToggleShowHiddenFieldsVisibility(false);
|
||||
});
|
||||
|
||||
testWidgets('check document exists in row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -149,7 +230,7 @@ void main() {
|
||||
await tester.assertDocumentExistInRowDetailPage();
|
||||
});
|
||||
|
||||
testWidgets('update the content of the document and re-open it',
|
||||
testWidgets('update the contents of the document and re-open it',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -164,7 +245,7 @@ void main() {
|
||||
await tester.wait(500);
|
||||
|
||||
// Focus on the editor
|
||||
final textBlock = find.byType(TextBlockComponentWidget);
|
||||
final textBlock = find.byType(ParagraphBlockComponentWidget);
|
||||
await tester.tapAt(tester.getCenter(textBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -239,6 +320,7 @@ void main() {
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
await tester.tapEscButton();
|
||||
|
||||
@@ -255,6 +337,7 @@ void main() {
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDuplicateRowButton();
|
||||
await tester.tapEscButton();
|
||||
|
||||
|
||||
@@ -122,17 +122,17 @@ void main() {
|
||||
}
|
||||
|
||||
// check the checklist cell
|
||||
final List<double> checklistCells = [
|
||||
0.6,
|
||||
0.3,
|
||||
final List<double?> checklistCells = [
|
||||
0.67,
|
||||
0.33,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
];
|
||||
for (final (index, percent) in checklistCells.indexed) {
|
||||
await tester.assertChecklistCellInGrid(
|
||||
@@ -157,7 +157,6 @@ void main() {
|
||||
for (final (index, content) in dateCells.indexed) {
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.DateTime,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document alignment', () {
|
||||
testWidgets('edit alignment in toolbar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
final selection = Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: 1,
|
||||
);
|
||||
// click the first line of the readme
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.updateSelection(selection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the align center
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
|
||||
|
||||
// expect to see the align center
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final first = editorState.getNodeAtPath([0])!;
|
||||
expect(first.attributes[blockComponentAlign], 'center');
|
||||
|
||||
// click the align right
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
|
||||
expect(first.attributes[blockComponentAlign], 'right');
|
||||
|
||||
// click the align left
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
|
||||
expect(first.attributes[blockComponentAlign], 'left');
|
||||
});
|
||||
});
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('paste in codeblock', () {
|
||||
testWidgets('paste multiple lines in codeblock', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName();
|
||||
|
||||
// mock the clipboard
|
||||
const lines = 3;
|
||||
final text = List.generate(lines, (index) => 'line $index').join('\n');
|
||||
AppFlowyClipboard.mockSetData(
|
||||
AppFlowyClipboardData(
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
|
||||
await insertCodeBlockInDocument(tester);
|
||||
|
||||
// paste the text
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
expect(
|
||||
editorState.getNodeAtPath([0])!.delta!.toPlainText(),
|
||||
text,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Inserts an codeBlock in the document
|
||||
Future<void> insertCodeBlockInDocument(WidgetTester tester) async {
|
||||
// open the actions menu and insert the codeBlock
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_selectionMenu_codeBlock.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
@@ -226,6 +226,56 @@ void main() {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
|
||||
testWidgets(
|
||||
'paste the image from clipboard when html and image are both available',
|
||||
(tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
image: ('png', bytes),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(
|
||||
node.attributes[ImageBlockKeys.url],
|
||||
'https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png',
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
extension on WidgetTester {
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ void main() {
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
);
|
||||
// and with one paragraph block
|
||||
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
|
||||
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('delete the readme page and restore it', (tester) async {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// +, ... button beside the block component.
|
||||
group('document with option action button', () {
|
||||
testWidgets(
|
||||
'click + to add a block after current selection, and click + and option key to add a block before current selection',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
var editorState = tester.editor.getCurrentEditorState();
|
||||
expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isNotEmpty);
|
||||
|
||||
// add a new block after the current selection
|
||||
await tester.editor.hoverAndClickOptionAddButton([0], false);
|
||||
// await tester.pumpAndSettle();
|
||||
expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isEmpty);
|
||||
|
||||
// cancel the selection menu
|
||||
await tester.tapAt(Offset.zero);
|
||||
|
||||
await tester.editor.hoverAndClickOptionAddButton([0], true);
|
||||
await tester.pumpAndSettle();
|
||||
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);
|
||||
// cancel the selection menu
|
||||
await tester.tapAt(Offset.zero);
|
||||
await tester.tapAt(Offset.zero);
|
||||
|
||||
await tester.createNewPageWithName(name: 'test');
|
||||
await tester.openPage(gettingStarted);
|
||||
|
||||
// check the status again
|
||||
editorState = tester.editor.getCurrentEditorState();
|
||||
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);
|
||||
expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_alignment_test.dart' as document_alignment_test;
|
||||
import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
|
||||
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
|
||||
import 'document_create_and_delete_test.dart'
|
||||
as document_create_and_delete_test;
|
||||
import 'document_option_action_test.dart' as document_option_action_test;
|
||||
import 'document_text_direction_test.dart' as document_text_direction_test;
|
||||
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
|
||||
import 'document_with_database_test.dart' as document_with_database_test;
|
||||
import 'document_with_image_block_test.dart' as document_with_image_block_test;
|
||||
import 'document_with_inline_math_equation_test.dart'
|
||||
as document_with_inline_math_equation_test;
|
||||
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
|
||||
import 'document_with_outline_block_test.dart' as document_with_outline_block;
|
||||
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
|
||||
import 'edit_document_test.dart' as document_edit_test;
|
||||
import 'document_with_outline_block_test.dart' as document_with_outline_block;
|
||||
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
|
||||
|
||||
void startTesting() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -25,4 +30,9 @@ void startTesting() {
|
||||
document_with_outline_block.main();
|
||||
document_with_toggle_list_test.main();
|
||||
document_copy_and_paste_test.main();
|
||||
document_codeblock_paste_test.main();
|
||||
document_alignment_test.main();
|
||||
document_text_direction_test.main();
|
||||
document_option_action_test.main();
|
||||
document_with_image_block_test.main();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('text direction', () {
|
||||
testWidgets(
|
||||
'''no text direction items will be displayed in the default/LTR mode,and three text direction items will be displayed in the RTL mode.''',
|
||||
(tester) async {
|
||||
// combine the two tests into one to avoid the time-consuming process of initializing the app
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
final selection = Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: 1,
|
||||
);
|
||||
// click the first line of the readme
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.updateSelection(selection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// because this icons are defined in the appflowy_editor package, we can't fetch the icons by SVG data. [textDirectionItems]
|
||||
final textDirectionIconNames = [
|
||||
'toolbar/text_direction_auto',
|
||||
'toolbar/text_direction_left',
|
||||
'toolbar/text_direction_right',
|
||||
];
|
||||
// no text direction items in default/LTR mode
|
||||
var button = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is SVGIconItemWidget &&
|
||||
textDirectionIconNames.contains(widget.iconName),
|
||||
);
|
||||
expect(button, findsNothing);
|
||||
|
||||
// switch to the RTL mode
|
||||
await tester.switchLayoutDirectionMode(LayoutDirection.rtlLayout);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.updateSelection(selection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
button = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is SVGIconItemWidget &&
|
||||
textDirectionIconNames.contains(widget.iconName),
|
||||
);
|
||||
expect(button, findsNWidgets(3));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
@@ -31,6 +32,18 @@ void main() {
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3533
|
||||
// test: the selection of editor should be clear when editing the grid
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(
|
||||
Position(path: [1]),
|
||||
),
|
||||
);
|
||||
final gridTextCell = find.byType(GridTextCell).first;
|
||||
await tester.tapButton(gridTextCell);
|
||||
|
||||
expect(tester.editor.getCurrentEditorState().selection, isNull);
|
||||
});
|
||||
|
||||
testWidgets('insert a referenced board', (tester) async {
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:run_with_network_images/run_with_network_images.dart';
|
||||
|
||||
import '../util/mock/mock_file_picker.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('image block in document', () {
|
||||
testWidgets('insert an image from local file', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final file = File(imagePath)
|
||||
..writeAsBytesSync(image.buffer.asUint8List());
|
||||
|
||||
mockPickFilePaths(
|
||||
paths: [imagePath],
|
||||
);
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ResizableImage), findsOneWidget);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
|
||||
// remove the temp file
|
||||
file.deleteSync();
|
||||
});
|
||||
|
||||
testWidgets('insert an image from network', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
);
|
||||
const url =
|
||||
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
url,
|
||||
);
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.text(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ResizableImage), findsOneWidget);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], url);
|
||||
});
|
||||
|
||||
testWidgets('insert an image from unsplash', (tester) async {
|
||||
await runWithNetworkImages(() async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
'Unsplash',
|
||||
);
|
||||
expect(find.byType(UnsplashImageWidget), findsOneWidget);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||
import 'package:appflowy/user/application/user_settings_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:calendar_view/calendar_view.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';
|
||||
|
||||
import '../util/base.dart';
|
||||
import '../util/common_operations.dart';
|
||||
import '../util/editor_test_operations.dart';
|
||||
import '../util/keyboard.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Reminder in Document', () {
|
||||
testWidgets('Add reminder for tomorrow, and include time', (tester) async {
|
||||
const time = "23:59";
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
final dateTimeSettings =
|
||||
await UserSettingsBackendService().getDateTimeSettings();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.getCurrentEditorState().insertNewLine();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Trigger iline action menu and type 'remind tomorrow'
|
||||
final tomorrow = await _insertReminderTomorrow(tester);
|
||||
|
||||
Node node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!;
|
||||
Map<String, dynamic> mentionAttr =
|
||||
node.delta!.first.attributes![MentionBlockKeys.mention];
|
||||
|
||||
expect(node.type, 'paragraph');
|
||||
expect(mentionAttr['type'], MentionType.reminder.name);
|
||||
expect(mentionAttr['date'], tomorrow.toIso8601String());
|
||||
|
||||
await tester.tap(
|
||||
find.text(dateTimeSettings.dateFormat.formatDate(tomorrow, false)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byType(Toggle));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byType(FlowyTextField), time);
|
||||
|
||||
// Leave text field to submit
|
||||
await tester.tap(find.text(LocaleKeys.grid_field_includeTime.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!;
|
||||
mentionAttr = node.delta!.first.attributes![MentionBlockKeys.mention];
|
||||
|
||||
final tomorrowWithTime =
|
||||
_dateWithTime(dateTimeSettings.timeFormat, tomorrow, time);
|
||||
|
||||
expect(node.type, 'paragraph');
|
||||
expect(mentionAttr['type'], MentionType.reminder.name);
|
||||
expect(mentionAttr['date'], tomorrowWithTime.toIso8601String());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<DateTime> _insertReminderTomorrow(WidgetTester tester) async {
|
||||
await tester.editor.showAtMenu();
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
LogicalKeyboardKey.keyR,
|
||||
LogicalKeyboardKey.keyE,
|
||||
LogicalKeyboardKey.keyM,
|
||||
LogicalKeyboardKey.keyI,
|
||||
LogicalKeyboardKey.keyN,
|
||||
LogicalKeyboardKey.keyD,
|
||||
LogicalKeyboardKey.space,
|
||||
LogicalKeyboardKey.keyT,
|
||||
LogicalKeyboardKey.keyO,
|
||||
LogicalKeyboardKey.keyM,
|
||||
LogicalKeyboardKey.keyO,
|
||||
LogicalKeyboardKey.keyR,
|
||||
LogicalKeyboardKey.keyR,
|
||||
LogicalKeyboardKey.keyO,
|
||||
LogicalKeyboardKey.keyW,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[LogicalKeyboardKey.enter],
|
||||
tester: tester,
|
||||
);
|
||||
|
||||
return DateTime.now().add(const Duration(days: 1)).withoutTime;
|
||||
}
|
||||
|
||||
DateTime _dateWithTime(UserTimeFormatPB format, DateTime date, String time) {
|
||||
final t = format == UserTimeFormatPB.TwelveHour
|
||||
? DateFormat.jm().parse(time)
|
||||
: DateFormat.Hm().parse(time);
|
||||
|
||||
return DateTime.parse(
|
||||
'${date.year}${_padZeroLeft(date.month)}${_padZeroLeft(date.day)} ${_padZeroLeft(t.hour)}:${_padZeroLeft(t.minute)}',
|
||||
);
|
||||
}
|
||||
|
||||
String _padZeroLeft(int a) => a.toString().padLeft(2, '0');
|
||||
@@ -4,6 +4,7 @@ import 'package:integration_test/integration_test.dart';
|
||||
import 'database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database_cell_test.dart' as database_cell_test;
|
||||
import 'database_field_test.dart' as database_field_test;
|
||||
import 'database_field_settings_test.dart' as database_field_settings_test;
|
||||
import 'database_filter_test.dart' as database_filter_test;
|
||||
import 'database_row_page_test.dart' as database_row_page_test;
|
||||
import 'database_row_test.dart' as database_row_test;
|
||||
@@ -21,6 +22,8 @@ import 'tabs_test.dart' as tabs_test;
|
||||
import 'hotkeys_test.dart' as hotkeys_test;
|
||||
import 'appearance_settings_test.dart' as appearance_test_runner;
|
||||
import 'auth/auth_test.dart' as auth_test_runner;
|
||||
import 'settings/user_icon_test.dart' as user_icon_test;
|
||||
import 'settings/user_language_test.dart' as user_language_test;
|
||||
|
||||
/// The main task runner for all integration tests in AppFlowy.
|
||||
///
|
||||
@@ -47,6 +50,7 @@ void main() {
|
||||
// Database integration tests
|
||||
database_cell_test.main();
|
||||
database_field_test.main();
|
||||
database_field_settings_test.main();
|
||||
database_share_test.main();
|
||||
database_row_page_test.main();
|
||||
database_row_test.main();
|
||||
@@ -65,7 +69,11 @@ void main() {
|
||||
// Appearance integration test
|
||||
appearance_test_runner.main();
|
||||
|
||||
if (isSupabaseEnabled) {
|
||||
// User settings
|
||||
user_icon_test.main();
|
||||
user_language_test.main();
|
||||
|
||||
if (isCloudEnabled) {
|
||||
auth_test_runner.main();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Settings: user icon tests', () {
|
||||
testWidgets('select icon, select default option', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
tester.expectToSeeHomePage();
|
||||
await tester.openSettings();
|
||||
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
|
||||
final userAvatarFinder = find.descendant(
|
||||
of: find.byType(SettingsUserView),
|
||||
matching: find.byType(UserAvatar),
|
||||
);
|
||||
|
||||
// Open icon picker dialog
|
||||
await tester.tap(userAvatarFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select first option that isn't default
|
||||
await tester.tap(find.byType(IconOption).first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
|
||||
expect(userAvatar.iconUrl, isNotEmpty);
|
||||
|
||||
// Open icon picker dialog again
|
||||
await tester.tap(userAvatarFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the default option
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(IconGallery),
|
||||
matching: find.byType(UserAvatar),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
|
||||
expect(userAvatar.iconUrl, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../util/util.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Settings: user language tests', () {
|
||||
testWidgets('select language, language changed', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
tester.expectToSeeHomePage();
|
||||
await tester.openSettings();
|
||||
|
||||
await tester.openSettingsPage(SettingsPage.language);
|
||||
|
||||
final userLanguageFinder = find.descendant(
|
||||
of: find.byType(SettingsLanguageView),
|
||||
matching: find.byType(LanguageSelector),
|
||||
);
|
||||
|
||||
// Grab current locale
|
||||
LanguageSelector userLanguage =
|
||||
tester.widget<LanguageSelector>(userLanguageFinder);
|
||||
Locale currentLocale = userLanguage.currentLocale;
|
||||
|
||||
// Open language selector
|
||||
await tester.tap(userLanguageFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select first option that isn't default
|
||||
await tester.tap(find.byType(LanguageItem).at(1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Make sure the new locale is not the same as previous one
|
||||
userLanguage = tester.widget<LanguageSelector>(userLanguageFinder);
|
||||
expect(
|
||||
userLanguage.currentLocale,
|
||||
isNot(equals(currentLocale)),
|
||||
reason: "new language shouldn't equal the previous selected language",
|
||||
);
|
||||
|
||||
// Update the current locale to a new one
|
||||
currentLocale = userLanguage.currentLocale;
|
||||
|
||||
// Tried the same flow for the second time
|
||||
// Open language selector
|
||||
await tester.tap(userLanguageFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select second option that isn't default
|
||||
await tester.tap(find.byType(LanguageItem).at(2));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Make sure the new locale is not the same as previous one
|
||||
userLanguage = tester.widget<LanguageSelector>(userLanguageFinder);
|
||||
expect(
|
||||
userLanguage.currentLocale,
|
||||
isNot(equals(currentLocale)),
|
||||
reason: "new language shouldn't equal the previous selected language",
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
@@ -78,7 +79,7 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
const expectedMarkdown = r'''
|
||||
const expectedMarkdown = '''
|
||||
# Welcome to AppFlowy!
|
||||
## Here are the basics
|
||||
- [ ] Click anywhere and just start typing.
|
||||
@@ -105,6 +106,15 @@ fn main() {
|
||||
## Have a question❓
|
||||
> Click `?` at the bottom right for help and support.
|
||||
|
||||
> 🥰
|
||||
>
|
||||
> Like AppFlowy? Follow us:
|
||||
> [GitHub](https://github.com/AppFlowy-IO/AppFlowy)
|
||||
> [Twitter](https://twitter.com/appflowy): @appflowy
|
||||
> [Newsletter](https://blog-appflowy.ghost.io/)
|
||||
>
|
||||
|
||||
|
||||
|
||||
|
||||
''';
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
@@ -7,7 +8,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -22,17 +23,14 @@ void main() {
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new page
|
||||
const name = 'Hello AppFlowy';
|
||||
await tester.tapNewPageButton();
|
||||
await tester.enterText(find.byType(TextFormField), name);
|
||||
await tester.tapOKButton();
|
||||
|
||||
// expect to see a new document
|
||||
tester.expectToSeePageName(
|
||||
name,
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
);
|
||||
// and with one paragraph block
|
||||
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
|
||||
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('create a new document, grid, board and calendar',
|
||||
@@ -57,7 +55,7 @@ void main() {
|
||||
switch (layout) {
|
||||
case ViewLayoutPB.Document:
|
||||
// and with one paragraph block
|
||||
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
|
||||
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
|
||||
break;
|
||||
case ViewLayoutPB.Grid:
|
||||
expect(find.byType(GridPage), findsOneWidget);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
@@ -11,6 +14,10 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('customize the folder path', () {
|
||||
if (Platform.isWindows) {
|
||||
return;
|
||||
}
|
||||
|
||||
testWidgets('switch to B from A, then switch to A again', (tester) async {
|
||||
const userA = 'UserA';
|
||||
const userB = 'UserB';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/user/presentation/sign_in_screen.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/sync_setting_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -7,15 +7,15 @@ import 'base.dart';
|
||||
|
||||
extension AppFlowyAuthTest on WidgetTester {
|
||||
Future<void> tapGoogleLoginInButton() async {
|
||||
await tapButton(find.byType(GoogleSignUpButton));
|
||||
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
|
||||
}
|
||||
|
||||
Future<void> tapSignInAsGuest() async {
|
||||
await tapButton(find.byType(SignInAsGuestButton));
|
||||
await tapButton(find.byType(SignInAnonymousButton));
|
||||
}
|
||||
|
||||
void expectToSeeGoogleLoginButton() {
|
||||
expect(find.byType(GoogleSignUpButton), findsOneWidget);
|
||||
expect(find.byKey(const Key('signInWithGoogleButton')), findsOneWidget);
|
||||
}
|
||||
|
||||
void assertSwitchValue(Finder finder, bool value) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/presentation.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -9,8 +11,8 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class FlowyTestContext {
|
||||
FlowyTestContext({
|
||||
@@ -39,8 +41,8 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
IntegrationMode.integrationTest,
|
||||
);
|
||||
|
||||
await wait(3000);
|
||||
await pumpAndSettle(const Duration(seconds: 2));
|
||||
await waitUntilSignInPageShow();
|
||||
|
||||
return FlowyTestContext(
|
||||
applicationDataDirectory: directory,
|
||||
);
|
||||
@@ -78,6 +80,27 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<void> waitUntilSignInPageShow() async {
|
||||
final finder = find.byType(GoButton);
|
||||
await pumpUntilFound(finder);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> pumpUntilFound(
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
bool timerDone = false;
|
||||
final timer = Timer(timeout, () => timerDone = true);
|
||||
while (timerDone != true) {
|
||||
await pump();
|
||||
if (any(finder)) {
|
||||
timerDone = true;
|
||||
}
|
||||
}
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
Future<void> tapButton(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
@@ -279,6 +282,14 @@ extension CommonOperations on WidgetTester {
|
||||
// create a new page
|
||||
await tapAddViewButton(name: parentName ?? gettingStarted);
|
||||
await tapButtonWithName(layout.menuName);
|
||||
final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(
|
||||
KVKeys.showRenameDialogWhenCreatingNewFile,
|
||||
(value) => bool.parse(value),
|
||||
);
|
||||
final showRenameDialog = settingsOrFailure.fold((l) => false, (r) => r);
|
||||
if (showRenameDialog) {
|
||||
await tapOKButton();
|
||||
}
|
||||
await pumpAndSettle();
|
||||
|
||||
// hover on it and change it's name
|
||||
@@ -409,6 +420,14 @@ extension CommonOperations on WidgetTester {
|
||||
await gesture.up();
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
// tap the button with [FlowySvgData]
|
||||
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg.path == svg.path,
|
||||
);
|
||||
await tapButton(button);
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
@@ -24,6 +26,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
|
||||
@@ -35,8 +38,10 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
@@ -46,9 +51,11 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
@@ -240,36 +247,43 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
/// null percent means no progress bar should be found
|
||||
Future<void> assertChecklistCellInGrid({
|
||||
required int rowIndex,
|
||||
required double percent,
|
||||
required double? percent,
|
||||
}) async {
|
||||
final findCell = cellFinder(rowIndex, FieldType.Checklist);
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is ChecklistProgressBar) {
|
||||
return widget.percent == percent;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
|
||||
if (percent == null) {
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byType(ChecklistProgressBar),
|
||||
);
|
||||
expect(finder, findsNothing);
|
||||
} else {
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is ChecklistProgressBar) {
|
||||
return widget.percent == percent;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> assertDateCellInGrid({
|
||||
required int rowIndex,
|
||||
required FieldType fieldType,
|
||||
required String content,
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow, skipOffstage: false);
|
||||
final findCell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is GridDateCell && widget.fieldType == fieldType,
|
||||
),
|
||||
matching: find.byType(GridDateCell),
|
||||
skipOffstage: false,
|
||||
);
|
||||
|
||||
@@ -318,7 +332,17 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> toggleIncludeTime() async {
|
||||
final findDateEditor = find.byType(DateCellEditor);
|
||||
final findDateEditor = find.byType(IncludeTimeButton);
|
||||
final findToggle = find.byType(Toggle);
|
||||
final finder = find.descendant(
|
||||
of: findDateEditor,
|
||||
matching: findToggle,
|
||||
);
|
||||
await tapButton(finder);
|
||||
}
|
||||
|
||||
Future<void> toggleDateRange() async {
|
||||
final findDateEditor = find.byType(EndTimeButton);
|
||||
final findToggle = find.byType(Toggle);
|
||||
final finder = find.descendant(
|
||||
of: findDateEditor,
|
||||
@@ -450,6 +474,106 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(cell, matcher);
|
||||
}
|
||||
|
||||
Future<void> tapChecklistCellInGrid({required int rowIndex}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
final findCell = finderForFieldType(FieldType.Checklist);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
);
|
||||
|
||||
await tapButton(cell);
|
||||
}
|
||||
|
||||
void assertChecklistEditorVisible({required bool visible}) {
|
||||
final editor = find.byType(GridChecklistCellEditor);
|
||||
if (visible) {
|
||||
expect(editor, findsOneWidget);
|
||||
} else {
|
||||
expect(editor, findsNothing);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createNewChecklistTask({
|
||||
required String name,
|
||||
enter = false,
|
||||
button = false,
|
||||
}) async {
|
||||
assert(!(enter && button));
|
||||
final textField = find.descendant(
|
||||
of: find.byType(NewTaskItem),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
|
||||
await enterText(textField, name);
|
||||
await pumpAndSettle();
|
||||
if (enter) {
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle();
|
||||
} else {
|
||||
await tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(NewTaskItem),
|
||||
matching: find.byType(FlowyTextButton),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void assertChecklistTaskInEditor({
|
||||
required int index,
|
||||
required String name,
|
||||
required bool isChecked,
|
||||
}) {
|
||||
final task = find.byType(ChecklistItem).at(index);
|
||||
|
||||
final widget = this.widget<ChecklistItem>(task);
|
||||
assert(
|
||||
widget.option.data.name == name && widget.option.isSelected == isChecked,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> renameChecklistTask({
|
||||
required int index,
|
||||
required String name,
|
||||
}) async {
|
||||
final textField = find
|
||||
.descendant(
|
||||
of: find.byType(ChecklistItem),
|
||||
matching: find.byType(TextField),
|
||||
)
|
||||
.at(index);
|
||||
|
||||
await enterText(textField, name);
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> checkChecklistTask({required int index}) async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(ChecklistItem).at(index),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s,
|
||||
),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> deleteChecklistTask({required int index}) async {
|
||||
final task = find.byType(ChecklistItem).at(index);
|
||||
|
||||
await startGesture(getCenter(task), kind: PointerDeviceKind.mouse);
|
||||
await pumpAndSettle();
|
||||
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> openFirstRowDetailPage() async {
|
||||
await hoverOnFirstRowOfGrid();
|
||||
|
||||
@@ -486,7 +610,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(banner, findsOneWidget);
|
||||
|
||||
await startGesture(
|
||||
getTopLeft(banner),
|
||||
getCenter(banner),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
|
||||
@@ -503,6 +627,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(findDateCell);
|
||||
}
|
||||
|
||||
Future<void> tapGridFieldWithNameInRowDetailPage(String name) async {
|
||||
final fields = find.byWidgetPredicate(
|
||||
(widget) => widget is FieldCellButton && widget.field.name == name,
|
||||
);
|
||||
final field = find.descendant(
|
||||
of: find.byType(RowDetailPage),
|
||||
matching: fields,
|
||||
);
|
||||
await tapButton(field);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> duplicateRowInRowDetailPage() async {
|
||||
final duplicateButton = find.byType(RowDetailPageDuplicateButton);
|
||||
await tapButton(duplicateButton);
|
||||
@@ -513,6 +649,56 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(deleteButton);
|
||||
}
|
||||
|
||||
Future<TestGesture> hoverOnFieldInRowDetail({required int index}) async {
|
||||
final fieldButtons = find.byType(FieldCellButton);
|
||||
final button = find
|
||||
.descendant(of: find.byType(RowDetailPage), matching: fieldButtons)
|
||||
.at(index);
|
||||
return startGesture(
|
||||
getCenter(button),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> reorderFieldInRowDetail({required double offset}) async {
|
||||
final thumb = find
|
||||
.byWidgetPredicate(
|
||||
(widget) => widget is ReorderableDragStartListener && widget.enabled,
|
||||
)
|
||||
.first;
|
||||
await drag(
|
||||
thumb,
|
||||
Offset(0, offset),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
void assertToggleShowHiddenFieldsVisibility(bool shown) {
|
||||
final button = find.byType(ToggleHiddenFieldsVisibilityButton);
|
||||
if (shown) {
|
||||
expect(button, findsOneWidget);
|
||||
} else {
|
||||
expect(button, findsNothing);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleShowHiddenFields() async {
|
||||
final button = find.byType(ToggleHiddenFieldsVisibilityButton);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapDeletePropertyInFieldEditor() async {
|
||||
final deleteButton = find.byType(DeleteFieldButton);
|
||||
await tapButton(deleteButton);
|
||||
|
||||
final confirmButton = find.descendant(
|
||||
of: find.byType(NavigatorAlertDialog),
|
||||
matching: find.byType(PrimaryTextButton),
|
||||
);
|
||||
await tapButton(confirmButton);
|
||||
}
|
||||
|
||||
Future<void> scrollGridByOffset(Offset offset) async {
|
||||
await drag(find.byType(GridPage), offset);
|
||||
await pumpAndSettle();
|
||||
@@ -585,6 +771,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(field);
|
||||
}
|
||||
|
||||
Future<void> tapHidePropertyButtonInFieldEditor() async {
|
||||
final button = find.byType(FieldVisibilityToggleButton);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapRowDetailPageRowActionButton() async {
|
||||
await tapButton(find.byType(RowActionButton));
|
||||
}
|
||||
|
||||
Future<void> tapRowDetailPageCreatePropertyButton() async {
|
||||
await tapButton(find.byType(CreateRowFieldButton));
|
||||
}
|
||||
@@ -654,6 +849,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(field, findsOneWidget);
|
||||
}
|
||||
|
||||
void assertFirstFieldInRowDetailByType(FieldType fieldType) {
|
||||
final firstField = find
|
||||
.descendant(
|
||||
of: find.byType(RowDetailPage),
|
||||
matching: find.byType(FieldCellButton),
|
||||
)
|
||||
.first;
|
||||
|
||||
final widget = this.widget<FieldCellButton>(firstField);
|
||||
expect(widget.field.fieldType, fieldType);
|
||||
}
|
||||
|
||||
Future<void> findFieldWithName(String name) async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) => widget is FieldCellButton && widget.field.name == name,
|
||||
@@ -718,9 +925,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> assertRowCountInGridPage(int num) async {
|
||||
final text = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == rowCountString(num),
|
||||
);
|
||||
final text = find.text('${rowCountString()} $num', findRichText: true);
|
||||
expect(text, findsOneWidget);
|
||||
}
|
||||
|
||||
@@ -925,6 +1130,23 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Should call [tapDatabaseSettingButton] first.
|
||||
Future<void> tapViewPropertiesButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingItem);
|
||||
final findLayoutButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
widget.text == DatabaseSettingAction.showProperties.title(),
|
||||
);
|
||||
|
||||
final button = find.descendant(
|
||||
of: findSettingItem,
|
||||
matching: findLayoutButton,
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Should call [tapDatabaseSettingButton] first.
|
||||
Future<void> tapDatabaseLayoutButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingItem);
|
||||
@@ -1094,12 +1316,72 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(cards.at(index));
|
||||
}
|
||||
|
||||
void assertEventEditorOpen() {
|
||||
expect(find.byType(CalendarEventEditor), findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> dismissEventEditor() async {
|
||||
await simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
}
|
||||
|
||||
Future<void> editEventTitle(String title) async {
|
||||
final textField = find.descendant(
|
||||
of: find.byType(CalendarEventEditor),
|
||||
matching: find.byType(FlowyTextField),
|
||||
);
|
||||
|
||||
await enterText(textField, title);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> openEventToRowDetailPage() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(CalendarEventEditor),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.full_view_s,
|
||||
),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> deleteEventFromEventEditor() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(CalendarEventEditor),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
|
||||
),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
|
||||
final findEventCard = find.byType(EventCard);
|
||||
await drag(findEventCard.first, const Offset(0, 300));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> openUnscheduledEventsPopup() async {
|
||||
final button = find.byType(UnscheduledEventsButton);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
void findUnscheduledPopup(Matcher matcher, int numUnscheduledEvents) {
|
||||
expect(find.byType(UnscheduleEventsList), matcher);
|
||||
if (matcher != findsNothing) {
|
||||
expect(
|
||||
find.byType(UnscheduledEventCell),
|
||||
findsNWidgets(numUnscheduledEvents),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clickUnscheduledEvent() async {
|
||||
final unscheduledEvent = find.byType(UnscheduledEventCell);
|
||||
await tapButton(unscheduledEvent);
|
||||
}
|
||||
|
||||
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
|
||||
final findAddButton = find.byType(AddDatabaseViewButton);
|
||||
await tapButton(findAddButton);
|
||||
@@ -1111,6 +1393,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(findCreateButton);
|
||||
}
|
||||
|
||||
Future<void> tapTabBarLinkedViewByViewName(String name) async {
|
||||
final viewButton = findTabBarLinkViewByViewName(name);
|
||||
await tapButton(viewButton);
|
||||
}
|
||||
|
||||
Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) => widget is TabBarItemButton && widget.view.layout == layout,
|
||||
@@ -1212,6 +1499,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
Future<void> tapAddSelectOptionButton() async {
|
||||
await tapButtonWithName(LocaleKeys.grid_field_addSelectOption.tr());
|
||||
}
|
||||
|
||||
Future<void> tapViewTogglePropertyVisibilityButtonByName(
|
||||
String fieldName,
|
||||
) async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is GridPropertyCell && widget.fieldInfo.name == fieldName,
|
||||
);
|
||||
final toggleVisibilityButton =
|
||||
find.descendant(of: field, matching: find.byType(FlowyIconButton));
|
||||
await tapButton(toggleVisibilityButton);
|
||||
}
|
||||
}
|
||||
|
||||
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
|
||||
@@ -1235,7 +1534,7 @@ Finder finderForFieldType(FieldType fieldType) {
|
||||
return find.byType(GridDateCell, skipOffstage: false);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return find.byType(GridDateCell, skipOffstage: false);
|
||||
return find.byType(GridTimestampCell, skipOffstage: false);
|
||||
case FieldType.SingleSelect:
|
||||
return find.byType(GridSingleSelectCell, skipOffstage: false);
|
||||
case FieldType.MultiSelect:
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_popover.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -159,7 +161,7 @@ class EditorOperations {
|
||||
/// Must call [showAtMenu] first.
|
||||
Future<void> tapAtMenuItemWithName(String name) async {
|
||||
final atMenuItem = find.descendant(
|
||||
of: find.byType(SelectionMenuWidget),
|
||||
of: find.byType(InlineActionsHandler),
|
||||
matching: find.text(name, findRichText: true),
|
||||
);
|
||||
await tester.tapButton(atMenuItem);
|
||||
@@ -174,4 +176,34 @@ class EditorOperations {
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
/// hover and click on the + button beside the block component.
|
||||
Future<void> hoverAndClickOptionAddButton(
|
||||
Path path,
|
||||
bool withModifiedKey, // alt on windows or linux, option on macos
|
||||
) async {
|
||||
final optionAddButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is BlockComponentActionWrapper &&
|
||||
widget.node.path.equals(path),
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
optionAddButton,
|
||||
onHover: () async {
|
||||
if (withModifiedKey) {
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
|
||||
}
|
||||
await tester.tapButton(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is BlockAddButton &&
|
||||
widget.blockComponentContext.node.path.equals(path),
|
||||
),
|
||||
);
|
||||
if (withModifiedKey) {
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,11 @@ class MyMockClient extends Mock implements http.Client {
|
||||
final requestType = request.method;
|
||||
final requestUri = request.url;
|
||||
|
||||
if (requestType == 'POST' && requestUri == OpenAIRequestType.textCompletion.uri) {
|
||||
final responseHeaders = <String, String>{'content-type': 'text/event-stream'};
|
||||
if (requestType == 'POST' &&
|
||||
requestUri == OpenAIRequestType.textCompletion.uri) {
|
||||
final responseHeaders = <String, String>{
|
||||
'content-type': 'text/event-stream'
|
||||
};
|
||||
final responseBody = Stream.fromIterable([
|
||||
utf8.encode(
|
||||
'{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}',
|
||||
@@ -51,7 +54,9 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
|
||||
|
||||
var previousSyntax = '';
|
||||
if (response.statusCode == 200) {
|
||||
await for (final chunk in response.stream.transform(const Utf8Decoder()).transform(const LineSplitter())) {
|
||||
await for (final chunk in response.stream
|
||||
.transform(const Utf8Decoder())
|
||||
.transform(const LineSplitter())) {
|
||||
await onStart();
|
||||
final data = chunk.trim().split('data: ');
|
||||
if (data[0] != '[DONE]') {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
@@ -72,4 +74,35 @@ extension AppFlowySettings on WidgetTester {
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
// go to settings page and switch the layout direction
|
||||
Future<void> switchLayoutDirectionMode(
|
||||
LayoutDirection layoutDirection,
|
||||
) async {
|
||||
await openSettings();
|
||||
await openSettingsPage(SettingsPage.appearance);
|
||||
|
||||
final button = find.byKey(const ValueKey('layout_direction_option_button'));
|
||||
expect(button, findsOneWidget);
|
||||
await tapButton(button);
|
||||
|
||||
switch (layoutDirection) {
|
||||
case LayoutDirection.ltrLayout:
|
||||
final ltrButton = find.text(
|
||||
LocaleKeys.settings_appearance_layoutDirection_ltr.tr(),
|
||||
);
|
||||
await tapButton(ltrButton);
|
||||
break;
|
||||
case LayoutDirection.rtlLayout:
|
||||
final rtlButton = find.text(
|
||||
LocaleKeys.settings_appearance_layoutDirection_rtl.tr(),
|
||||
);
|
||||
await tapButton(rtlButton);
|
||||
break;
|
||||
}
|
||||
|
||||
// tap anywhere to close the settings page
|
||||
await tapAt(Offset.zero);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
@@ -38,4 +38,20 @@ post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
|
||||
installer.aggregate_targets.each do |target|
|
||||
target.xcconfigs.each do |variant, xcconfig|
|
||||
xcconfig_path = target.client_root + target.xcconfig_relative_path(variant)
|
||||
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
|
||||
end
|
||||
end
|
||||
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference
|
||||
xcconfig_path = config.base_configuration_reference.real_path
|
||||
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -50,6 +50,8 @@ PODS:
|
||||
- Toast
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- irondash_engine_context (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
@@ -66,6 +68,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- sign_in_with_apple (0.0.1):
|
||||
- Flutter
|
||||
- super_native_extensions (0.0.1):
|
||||
- Flutter
|
||||
- SwiftyGif (5.4.3)
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
@@ -83,11 +87,13 @@ DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
|
||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
|
||||
@@ -119,6 +125,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
irondash_engine_context:
|
||||
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
@@ -129,6 +137,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sign_in_with_apple:
|
||||
:path: ".symlinks/plugins/sign_in_with_apple/ios"
|
||||
super_native_extensions:
|
||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
webview_flutter_wkwebview:
|
||||
@@ -146,6 +156,7 @@ SPEC CHECKSUMS:
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
|
||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
@@ -153,6 +164,7 @@ SPEC CHECKSUMS:
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
|
||||
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
|
||||
super_native_extensions: 49e897b6039bb784226e7354e502a3c68e1659b5
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
|
||||
@@ -16,6 +16,17 @@
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>appflowy-flutter</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
@@ -7,6 +7,10 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
abstract class KeyValueStorage {
|
||||
Future<void> set(String key, String value);
|
||||
Future<Either<FlowyError, String>> get(String key);
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
);
|
||||
Future<void> remove(String key);
|
||||
Future<void> clear();
|
||||
}
|
||||
@@ -26,6 +30,18 @@ class DartKeyValue implements KeyValueStorage {
|
||||
return Left(FlowyError());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
) async {
|
||||
final value = await get(key);
|
||||
return value.fold(
|
||||
(l) => left(l),
|
||||
(r) => right(formatter(r)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String key) async {
|
||||
await _initSharedPreferencesIfNeeded();
|
||||
@@ -71,6 +87,18 @@ class RustKeyValue implements KeyValueStorage {
|
||||
return response.swap().map((r) => r.value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
) async {
|
||||
final value = await get(key);
|
||||
return value.fold(
|
||||
(l) => left(l),
|
||||
(r) => right(formatter(r)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String key) async {
|
||||
await ConfigEventRemoveKeyValue(
|
||||
|
||||
@@ -29,6 +29,8 @@ class KVKeys {
|
||||
'kDocumentAppearanceFontSize';
|
||||
static const String kDocumentAppearanceFontFamily =
|
||||
'kDocumentAppearanceFontFamily';
|
||||
static const String kDocumentAppearanceDefaultTextDirection =
|
||||
'kDocumentAppearanceDefaultTextDirection';
|
||||
|
||||
/// The key for saving the expanded views
|
||||
///
|
||||
@@ -41,4 +43,10 @@ class KVKeys {
|
||||
/// The value is a json string with the following format:
|
||||
/// {'SidebarFolderCategoryType.value': true}
|
||||
static const String expandedFolders = 'expandedFolders';
|
||||
|
||||
/// The key for saving if showing the rename dialog when creating a new file
|
||||
///
|
||||
/// The value is a boolean string.
|
||||
static const String showRenameDialogWhenCreatingNewFile =
|
||||
'showRenameDialogWhenCreatingNewFile';
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-date/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
class DateService {
|
||||
static Future<Either<FlowyError, DateTime>> queryDate(String search) async {
|
||||
final query = DateQueryPB.create()..query = search;
|
||||
final result = (await DateEventQueryDate(query).send()).swap();
|
||||
return result.fold((l) => left(l), (r) {
|
||||
final date = DateTime.tryParse(r.date);
|
||||
if (date != null) {
|
||||
return right(date);
|
||||
}
|
||||
|
||||
return left(FlowyError(msg: 'Could not parse Date (NLP) from String'));
|
||||
});
|
||||
}
|
||||
}
|
||||
+80
-9
@@ -1,5 +1,6 @@
|
||||
// lib/env/env.dart
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:envied/envied.dart';
|
||||
|
||||
part 'env.g.dart';
|
||||
@@ -17,6 +18,36 @@ part 'env.g.dart';
|
||||
///
|
||||
@Envied(path: '.env')
|
||||
abstract class Env {
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'CLOUD_TYPE',
|
||||
defaultValue: '0',
|
||||
)
|
||||
static final int cloudType = _Env.cloudType;
|
||||
|
||||
/// AppFlowy Cloud Configuration
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_BASE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudBaseUrl = _Env.afCloudBaseUrl;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_WS_BASE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudWSBaseUrl = _Env.afCloudWSBaseUrl;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_GOTRUE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudGoTrueUrl = _Env.afCloudGoTrueUrl;
|
||||
|
||||
// Supabase Configuration:
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_URL',
|
||||
@@ -29,22 +60,62 @@ abstract class Env {
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseAnonKey = _Env.supabaseAnonKey;
|
||||
}
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_JWT_SECRET',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseJwtSecret = _Env.supabaseJwtSecret;
|
||||
bool get isCloudEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return currentCloudType().isEnabled;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isSupabaseEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return Env.supabaseUrl.isNotEmpty &&
|
||||
Env.supabaseAnonKey.isNotEmpty &&
|
||||
Env.supabaseJwtSecret.isNotEmpty;
|
||||
return currentCloudType() == CloudType.supabase;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isAppFlowyCloudEnabled {
|
||||
// Only enable appflowy cloud in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return currentCloudType() == CloudType.appflowyCloud;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enum CloudType {
|
||||
unknown,
|
||||
supabase,
|
||||
appflowyCloud;
|
||||
|
||||
bool get isEnabled => this != CloudType.unknown;
|
||||
}
|
||||
|
||||
CloudType currentCloudType() {
|
||||
final value = Env.cloudType;
|
||||
if (value == 1) {
|
||||
if (Env.supabaseUrl.isEmpty || Env.supabaseAnonKey.isEmpty) {
|
||||
Log.error("Supabase is not configured");
|
||||
return CloudType.unknown;
|
||||
} else {
|
||||
return CloudType.supabase;
|
||||
}
|
||||
}
|
||||
|
||||
if (value == 2) {
|
||||
if (Env.afCloudBaseUrl.isEmpty || Env.afCloudWSBaseUrl.isEmpty) {
|
||||
Log.error("AppFlowy cloud is not configured");
|
||||
return CloudType.unknown;
|
||||
} else {
|
||||
return CloudType.appflowyCloud;
|
||||
}
|
||||
}
|
||||
|
||||
return CloudType.unknown;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// ThemeData in mobile
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ThemeData getMobileThemeData() {
|
||||
const mobileColorTheme = ColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primary: Color(0xFF2DA2F6), //primary 100
|
||||
onPrimary: Colors.white,
|
||||
// TODO(yijing): add color later
|
||||
secondary: Colors.white,
|
||||
onSecondary: Colors.white,
|
||||
error: Color(0xffFB006D),
|
||||
onError: Color(0xffFB006D),
|
||||
background: Colors.white,
|
||||
onBackground: Color(0xff2F3030), // title text
|
||||
outline: Color(0xffBDC0C5), //caption
|
||||
//Snack bar
|
||||
surface: Colors.white,
|
||||
onSurface: Color(0xff2F3030), // title text
|
||||
);
|
||||
return ThemeData(
|
||||
// color
|
||||
primaryColor: mobileColorTheme.primary, //primary 100
|
||||
primaryColorLight: const Color(0xFF57B5F8), //primary 80
|
||||
dividerColor: mobileColorTheme.outline, //caption
|
||||
scaffoldBackgroundColor: mobileColorTheme.background,
|
||||
appBarTheme: AppBarTheme(
|
||||
foregroundColor: mobileColorTheme.onBackground,
|
||||
backgroundColor: mobileColorTheme.background,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Poppins',
|
||||
color: mobileColorTheme.onBackground,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.05,
|
||||
),
|
||||
),
|
||||
// button
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
elevation: MaterialStateProperty.all(0),
|
||||
shadowColor: MaterialStateProperty.all(null),
|
||||
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) {
|
||||
return const Color(0xFF57B5F8);
|
||||
}
|
||||
return mobileColorTheme.primary;
|
||||
},
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
mobileColorTheme.onBackground,
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.all(Colors.white),
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
side: MaterialStateProperty.all(
|
||||
BorderSide(
|
||||
color: mobileColorTheme.outline,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
// splash color
|
||||
overlayColor: MaterialStateProperty.all(
|
||||
Colors.grey[100],
|
||||
),
|
||||
),
|
||||
),
|
||||
// text
|
||||
fontFamily: 'Poppins',
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
color: Color(0xFF57B5F8),
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.20,
|
||||
letterSpacing: 0.16,
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
color: Color(0xff2F3030),
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.20,
|
||||
letterSpacing: 0.16,
|
||||
),
|
||||
// H1 Semi 26
|
||||
displaySmall: TextStyle(
|
||||
color: Color(0xFF2F3030),
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.10,
|
||||
letterSpacing: 0.13,
|
||||
),
|
||||
// body2 14 Regular
|
||||
bodyMedium: TextStyle(
|
||||
color: Color(0xFFC5C7CB),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.20,
|
||||
letterSpacing: 0.07,
|
||||
),
|
||||
// blue text button
|
||||
labelMedium: TextStyle(
|
||||
color: Color(0xFF2DA2F6),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.20,
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
width: 2,
|
||||
color: Color(0xFF2DA2F6), //primary 100
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: mobileColorTheme.error),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: mobileColorTheme.error),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Color(0xffBDC0C5), //caption
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
),
|
||||
colorScheme: mobileColorTheme,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
// TODO(yijing): delete this after implementing the real screen inside bottom navigation bar.
|
||||
/// For demonstration purposes
|
||||
class DetailsPlaceholderScreen extends StatefulWidget {
|
||||
/// Constructs a [DetailsScreen].
|
||||
const DetailsPlaceholderScreen({
|
||||
required this.label,
|
||||
this.param,
|
||||
this.extra,
|
||||
this.withScaffold = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The label to display in the center of the screen.
|
||||
final String label;
|
||||
|
||||
/// Optional param
|
||||
final String? param;
|
||||
|
||||
/// Optional extra object
|
||||
final Object? extra;
|
||||
|
||||
/// Wrap in scaffold
|
||||
final bool withScaffold;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => DetailsPlaceholderScreenState();
|
||||
}
|
||||
|
||||
/// The state for DetailsScreen
|
||||
class DetailsPlaceholderScreenState extends State<DetailsPlaceholderScreen> {
|
||||
int _counter = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.withScaffold) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Details Screen - ${widget.label}'),
|
||||
),
|
||||
body: _build(context),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: _build(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Details for ${widget.label} - Counter: $_counter',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_counter++;
|
||||
});
|
||||
},
|
||||
child: const Text('Increment counter'),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
if (widget.param != null)
|
||||
Text(
|
||||
'Parameter: ${widget.param!}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
if (widget.extra != null)
|
||||
Text(
|
||||
'Extra: ${widget.extra!}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
if (!widget.withScaffold) ...<Widget>[
|
||||
const Padding(padding: EdgeInsets.all(16)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'< Back',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export 'mobile_home_page.dart';
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// TODO(yijing): This is just a placeholder for now.
|
||||
class MobileHomeScreen extends StatelessWidget {
|
||||
const MobileHomeScreen({super.key});
|
||||
|
||||
static const routeName = "/MobileHomeScreen";
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: Future.wait([
|
||||
FolderEventGetCurrentWorkspace().send(),
|
||||
getIt<AuthService>().getUser(),
|
||||
]),
|
||||
builder: (context, snapshots) {
|
||||
if (!snapshots.hasData) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final workspaceSetting = snapshots.data?[0].fold(
|
||||
(workspaceSettingPB) {
|
||||
return workspaceSettingPB as WorkspaceSettingPB?;
|
||||
},
|
||||
(error) => null,
|
||||
);
|
||||
final userProfile =
|
||||
snapshots.data?[1].fold((error) => null, (userProfilePB) {
|
||||
return userProfilePB as UserProfilePB?;
|
||||
});
|
||||
// TODO(yijing): implement home page later
|
||||
return Scaffold(
|
||||
key: ValueKey(userProfile?.id),
|
||||
// TODO(yijing):Need to change to workspace when it is ready
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
userProfile?.email.toString() ?? 'No email found',
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// TODO(yijing): Navigate to setting page
|
||||
},
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.m_setting_m,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'User',
|
||||
),
|
||||
Text(
|
||||
userProfile.toString(),
|
||||
),
|
||||
Text('Workspace name: ${workspaceSetting?.workspace.name}'),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await getIt<AuthService>().signOut();
|
||||
runAppFlowy();
|
||||
},
|
||||
child: const Text('Logout'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Builds the "shell" for the app by building a Scaffold with a
|
||||
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
||||
class MobileBottomNavigationBar extends StatelessWidget {
|
||||
/// Constructs an [MobileBottomNavigationBar].
|
||||
const MobileBottomNavigationBar({
|
||||
required this.navigationShell,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The navigation shell and container for the branch Navigators.
|
||||
final StatefulNavigationShell navigationShell;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
body: navigationShell,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
// Here, the items of BottomNavigationBar are hard coded. In a real
|
||||
// world scenario, the items would most likely be generated from the
|
||||
// branches of the shell route, which can be fetched using
|
||||
// `navigationShell.route.branches`.
|
||||
type: BottomNavigationBarType.fixed,
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
// There is no text shown on the bottom navigation bar, but Exception will be thrown if label is null here.
|
||||
label: 'home',
|
||||
icon: const FlowySvg(FlowySvgs.m_home_unselected_lg),
|
||||
activeIcon: FlowySvg(
|
||||
FlowySvgs.m_home_selected_lg,
|
||||
color: style.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
label: 'favorite',
|
||||
icon: FlowySvg(FlowySvgs.m_favorite_unselected_lg),
|
||||
activeIcon: FlowySvg(
|
||||
FlowySvgs.m_favorite_selected_lg,
|
||||
blendMode: null,
|
||||
),
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
label: 'add',
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.m_add_circle_xl,
|
||||
blendMode: null,
|
||||
),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
label: 'search',
|
||||
icon: const FlowySvg(FlowySvgs.m_search_lg),
|
||||
activeIcon: FlowySvg(
|
||||
FlowySvgs.m_search_lg,
|
||||
color: style.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
label: 'notification',
|
||||
icon: const FlowySvg(FlowySvgs.m_notification_unselected_lg),
|
||||
activeIcon: FlowySvg(
|
||||
FlowySvgs.m_notification_selected_lg,
|
||||
color: style.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Navigate to the current location of the branch at the provided index when
|
||||
/// tapping an item in the BottomNavigationBar.
|
||||
void _onTap(BuildContext context, int bottomBarIndex) {
|
||||
// When navigating to a new branch, it's recommended to use the goBranch
|
||||
// method, as doing so makes sure the last navigation state of the
|
||||
// Navigator for the branch is restored.
|
||||
navigationShell.goBranch(
|
||||
bottomBarIndex,
|
||||
// A common pattern when using bottom navigation bars is to support
|
||||
// navigating to the initial location when tapping the item that is
|
||||
// already active. This example demonstrates how to support this behavior,
|
||||
// using the initialLocation parameter of goBranch.
|
||||
initialLocation: bottomBarIndex == navigationShell.currentIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export 'home/home.dart';
|
||||
export 'root_placeholder_page.dart';
|
||||
export 'details_placeholder_page.dart';
|
||||
export 'mobile_bottom_navigation_bar.dart';
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Widget for the root/initial pages in the bottom navigation bar.
|
||||
class RootPlaceholderScreen extends StatelessWidget {
|
||||
/// Creates a RootScreen
|
||||
const RootPlaceholderScreen({
|
||||
required this.label,
|
||||
required this.detailsPath,
|
||||
this.secondDetailsPath,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The label
|
||||
final String label;
|
||||
|
||||
/// The path to the detail page
|
||||
final String detailsPath;
|
||||
|
||||
/// The path to another detail page
|
||||
final String? secondDetailsPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Root of section $label'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text('$label Page', style: Theme.of(context).textTheme.titleLarge),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go(detailsPath, extra: '$label-XYZ');
|
||||
},
|
||||
child: const Text('View details'),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
if (secondDetailsPath != null)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go(secondDetailsPath!);
|
||||
},
|
||||
child: const Text('View more details'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+18
-5
@@ -2,6 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
|
||||
import 'cell_controller.dart';
|
||||
@@ -13,7 +14,8 @@ typedef NumberCellController = CellController<String, String>;
|
||||
typedef SelectOptionCellController
|
||||
= CellController<SelectOptionCellDataPB, String>;
|
||||
typedef ChecklistCellController = CellController<ChecklistCellDataPB, String>;
|
||||
typedef DateCellController = CellController<DateCellDataPB, DateCellData>;
|
||||
typedef DateCellController = CellController<DateCellDataPB, String>;
|
||||
typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
|
||||
typedef URLCellController = CellController<URLCellDataPB, String>;
|
||||
|
||||
class CellControllerBuilder {
|
||||
@@ -41,20 +43,31 @@ class CellControllerBuilder {
|
||||
TextCellDataPersistence(cellContext: _cellContext),
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
final cellDataLoader = CellDataLoader(
|
||||
cellContext: _cellContext,
|
||||
parser: DateCellDataParser(),
|
||||
reloadOnFieldChanged: true,
|
||||
);
|
||||
|
||||
return DateCellController(
|
||||
cellContext: _cellContext,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence:
|
||||
DateCellDataPersistence(cellContext: _cellContext),
|
||||
TextCellDataPersistence(cellContext: _cellContext),
|
||||
);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
final cellDataLoader = CellDataLoader(
|
||||
cellContext: _cellContext,
|
||||
parser: TimestampCellDataParser(),
|
||||
reloadOnFieldChanged: true,
|
||||
);
|
||||
return TimestampCellController(
|
||||
cellContext: _cellContext,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence:
|
||||
TextCellDataPersistence(cellContext: _cellContext),
|
||||
);
|
||||
case FieldType.Number:
|
||||
final cellDataLoader = CellDataLoader(
|
||||
|
||||
+10
@@ -73,6 +73,16 @@ class DateCellDataParser implements CellDataParser<DateCellDataPB> {
|
||||
}
|
||||
}
|
||||
|
||||
class TimestampCellDataParser implements CellDataParser<TimestampCellDataPB> {
|
||||
@override
|
||||
TimestampCellDataPB? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return TimestampCellDataPB.fromBuffer(data);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionCellDataParser
|
||||
implements CellDataParser<SelectOptionCellDataPB> {
|
||||
@override
|
||||
|
||||
+1
-49
@@ -1,7 +1,7 @@
|
||||
part of 'cell_service.dart';
|
||||
|
||||
/// Save the cell data to disk
|
||||
/// You can extend this class to do custom operations. For example, the DateCellDataPersistence.
|
||||
/// You can extend this class to do custom operations.
|
||||
abstract class CellDataPersistence<D> {
|
||||
Future<Option<FlowyError>> save(D data);
|
||||
}
|
||||
@@ -28,51 +28,3 @@ class TextCellDataPersistence implements CellDataPersistence<String> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DateCellData with _$DateCellData {
|
||||
const factory DateCellData({
|
||||
DateTime? dateTime,
|
||||
String? time,
|
||||
required bool includeTime,
|
||||
bool? clearFlag,
|
||||
}) = _DateCellData;
|
||||
}
|
||||
|
||||
class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
|
||||
final DatabaseCellContext cellContext;
|
||||
DateCellDataPersistence({
|
||||
required this.cellContext,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Option<FlowyError>> save(DateCellData data) {
|
||||
final payload = DateChangesetPB.create()
|
||||
..cellId = _makeCellPath(cellContext);
|
||||
if (data.dateTime != null) {
|
||||
final date = (data.dateTime!.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
payload.date = date;
|
||||
}
|
||||
if (data.time != null) {
|
||||
payload.time = data.time!;
|
||||
}
|
||||
if (data.clearFlag != null) {
|
||||
payload.clearFlag = data.clearFlag!;
|
||||
}
|
||||
payload.includeTime = data.includeTime;
|
||||
|
||||
return DatabaseEventUpdateDateCell(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(l) => none(),
|
||||
(err) => Some(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CellIdPB _makeCellPath(DatabaseCellContext cellId) {
|
||||
return CellIdPB.create()
|
||||
..viewId = cellId.viewId
|
||||
..fieldId = cellId.fieldId
|
||||
..rowId = cellId.rowId;
|
||||
}
|
||||
+11
@@ -2,8 +2,10 @@ import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
@@ -72,4 +74,13 @@ class DatabaseCellContext with _$DatabaseCellContext {
|
||||
|
||||
/// Only the primary field can have an emoji.
|
||||
String? get emoji => fieldInfo.field.isPrimary ? rowMeta.icon : null;
|
||||
|
||||
/// Determines whether a database cell context should be visible.
|
||||
/// It will be visible when the field is not hidden or when hidden fields
|
||||
/// should be shown.
|
||||
bool isVisible({bool showHiddenFields = false}) {
|
||||
return fieldInfo.visibility != null &&
|
||||
(showHiddenFields ||
|
||||
fieldInfo.visibility != FieldVisibility.AlwaysHidden);
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
class ChecklistCellBackendService {
|
||||
final String viewId;
|
||||
@@ -52,14 +53,19 @@ class ChecklistCellBackendService {
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> update({
|
||||
Future<Either<Unit, FlowyError>> updateName({
|
||||
required SelectOptionPB option,
|
||||
required name,
|
||||
}) {
|
||||
option.freeze();
|
||||
final newOption = option.rebuild((option) {
|
||||
option.name = name;
|
||||
});
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..updateOptions.add(option);
|
||||
..updateOptions.add(newOption);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
|
||||
final class DateCellBackendService {
|
||||
final CellIdPB cellId;
|
||||
|
||||
DateCellBackendService({
|
||||
required String viewId,
|
||||
required String fieldId,
|
||||
required String rowId,
|
||||
}) : cellId = CellIdPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
|
||||
Future<Either<Unit, FlowyError>> update({
|
||||
DateTime? date,
|
||||
String? time,
|
||||
DateTime? endDate,
|
||||
String? endTime,
|
||||
required includeTime,
|
||||
required isRange,
|
||||
}) {
|
||||
final payload = DateChangesetPB.create()
|
||||
..cellId = cellId
|
||||
..includeTime = includeTime
|
||||
..isRange = isRange;
|
||||
|
||||
if (date != null) {
|
||||
final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000;
|
||||
payload.date = Int64(dateTimestamp);
|
||||
}
|
||||
if (time != null) {
|
||||
payload.time = time;
|
||||
}
|
||||
if (endDate != null) {
|
||||
final dateTimestamp = endDate.millisecondsSinceEpoch ~/ 1000;
|
||||
payload.endDate = Int64(dateTimestamp);
|
||||
}
|
||||
if (endTime != null) {
|
||||
payload.endTime = endTime;
|
||||
}
|
||||
|
||||
return DatabaseEventUpdateDateCell(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> clear() {
|
||||
final payload = DateChangesetPB.create()
|
||||
..cellId = cellId
|
||||
..clearFlag = true;
|
||||
|
||||
return DatabaseEventUpdateDateCell(payload).send();
|
||||
}
|
||||
}
|
||||
+1
-7
@@ -395,13 +395,7 @@ class RowDataBuilder {
|
||||
}
|
||||
|
||||
void insertDate(FieldInfo fieldInfo, DateTime date) {
|
||||
assert(
|
||||
[
|
||||
FieldType.DateTime,
|
||||
FieldType.LastEditedTime,
|
||||
FieldType.CreatedTime,
|
||||
].contains(fieldInfo.fieldType),
|
||||
);
|
||||
assert(FieldType.DateTime == fieldInfo.fieldType);
|
||||
final timestamp = date.millisecondsSinceEpoch ~/ 1000;
|
||||
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
|
||||
}
|
||||
|
||||
+18
-5
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'field_service.dart';
|
||||
@@ -8,16 +10,21 @@ part 'field_action_sheet_bloc.freezed.dart';
|
||||
|
||||
class FieldActionSheetBloc
|
||||
extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
|
||||
final String fieldId;
|
||||
final FieldBackendService fieldService;
|
||||
final FieldSettingsBackendService fieldSettingsService;
|
||||
|
||||
FieldActionSheetBloc({required FieldContext fieldCellContext})
|
||||
: fieldService = FieldBackendService(
|
||||
: fieldId = fieldCellContext.fieldInfo.id,
|
||||
fieldService = FieldBackendService(
|
||||
viewId: fieldCellContext.viewId,
|
||||
fieldId: fieldCellContext.field.id,
|
||||
fieldId: fieldCellContext.fieldInfo.id,
|
||||
),
|
||||
fieldSettingsService =
|
||||
FieldSettingsBackendService(viewId: fieldCellContext.viewId),
|
||||
super(
|
||||
FieldActionSheetState.initial(
|
||||
TypeOptionPB.create()..field_2 = fieldCellContext.field,
|
||||
TypeOptionPB.create()..field_2 = fieldCellContext.fieldInfo.field,
|
||||
),
|
||||
) {
|
||||
on<FieldActionSheetEvent>(
|
||||
@@ -31,14 +38,20 @@ class FieldActionSheetBloc
|
||||
);
|
||||
},
|
||||
hideField: (_HideField value) async {
|
||||
final result = await fieldService.updateField(visibility: false);
|
||||
final result = await fieldSettingsService.updateFieldSettings(
|
||||
fieldId: fieldId,
|
||||
fieldVisibility: FieldVisibility.AlwaysHidden,
|
||||
);
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
showField: (_ShowField value) async {
|
||||
final result = await fieldService.updateField(visibility: true);
|
||||
final result = await fieldSettingsService.updateFieldSettings(
|
||||
fieldId: fieldId,
|
||||
fieldVisibility: FieldVisibility.AlwaysShown,
|
||||
);
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
|
||||
+18
-9
@@ -1,3 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -13,13 +15,14 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
||||
final FieldBackendService _fieldBackendSvc;
|
||||
|
||||
FieldCellBloc({
|
||||
required FieldContext cellContext,
|
||||
}) : _fieldListener = SingleFieldListener(fieldId: cellContext.field.id),
|
||||
required FieldContext fieldContext,
|
||||
}) : _fieldListener =
|
||||
SingleFieldListener(fieldId: fieldContext.fieldInfo.id),
|
||||
_fieldBackendSvc = FieldBackendService(
|
||||
viewId: cellContext.viewId,
|
||||
fieldId: cellContext.field.id,
|
||||
viewId: fieldContext.viewId,
|
||||
fieldId: fieldContext.fieldInfo.id,
|
||||
),
|
||||
super(FieldCellState.initial(cellContext)) {
|
||||
super(FieldCellState.initial(fieldContext)) {
|
||||
on<FieldCellEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
@@ -27,10 +30,13 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveFieldUpdate: (field) {
|
||||
emit(state.copyWith(field: cellContext.field));
|
||||
emit(state.copyWith(field: fieldContext.fieldInfo.field));
|
||||
},
|
||||
onResizeStart: () {
|
||||
emit(state.copyWith(resizeStart: state.width));
|
||||
},
|
||||
startUpdateWidth: (offset) {
|
||||
final width = state.width + offset;
|
||||
final width = max(offset + state.resizeStart, 50).toDouble();
|
||||
emit(state.copyWith(width: width));
|
||||
},
|
||||
endUpdateWidth: () {
|
||||
@@ -66,6 +72,7 @@ class FieldCellEvent with _$FieldCellEvent {
|
||||
const factory FieldCellEvent.initial() = _InitialCell;
|
||||
const factory FieldCellEvent.didReceiveFieldUpdate(FieldPB field) =
|
||||
_DidReceiveFieldUpdate;
|
||||
const factory FieldCellEvent.onResizeStart() = _OnResizeStart;
|
||||
const factory FieldCellEvent.startUpdateWidth(double offset) =
|
||||
_StartUpdateWidth;
|
||||
const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth;
|
||||
@@ -77,11 +84,13 @@ class FieldCellState with _$FieldCellState {
|
||||
required String viewId,
|
||||
required FieldPB field,
|
||||
required double width,
|
||||
required double resizeStart,
|
||||
}) = _FieldCellState;
|
||||
|
||||
factory FieldCellState.initial(FieldContext cellContext) => FieldCellState(
|
||||
viewId: cellContext.viewId,
|
||||
field: cellContext.field,
|
||||
width: cellContext.field.width.toDouble(),
|
||||
field: cellContext.fieldInfo.field,
|
||||
width: cellContext.fieldInfo.field.width.toDouble(),
|
||||
resizeStart: 0,
|
||||
);
|
||||
}
|
||||
+276
-217
@@ -1,24 +1,27 @@
|
||||
import 'dart:collection';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_view_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/filter/filter_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/setting/setting_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/sort/sort_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/sort/sort_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../grid/presentation/widgets/filter/filter_info.dart';
|
||||
import '../../grid/presentation/widgets/sort/sort_info.dart';
|
||||
import '../database_view_service.dart';
|
||||
import '../filter/filter_listener.dart';
|
||||
import '../filter/filter_service.dart';
|
||||
import '../row/row_cache.dart';
|
||||
import '../setting/setting_listener.dart';
|
||||
import '../setting/setting_service.dart';
|
||||
import '../sort/sort_listener.dart';
|
||||
import '../sort/sort_service.dart';
|
||||
|
||||
import 'field_info.dart';
|
||||
import 'field_listener.dart';
|
||||
|
||||
@@ -72,20 +75,23 @@ typedef OnReceiveUpdateFields = void Function(List<FieldInfo>);
|
||||
typedef OnReceiveFields = void Function(List<FieldInfo>);
|
||||
typedef OnReceiveFilters = void Function(List<FilterInfo>);
|
||||
typedef OnReceiveSorts = void Function(List<SortInfo>);
|
||||
typedef OnReceiveFieldSettings = void Function(List<FieldInfo>);
|
||||
|
||||
class FieldController {
|
||||
final String viewId;
|
||||
|
||||
// Listeners
|
||||
final FieldsListener _fieldListener;
|
||||
final DatabaseSettingListener _settingListener;
|
||||
final FiltersListener _filtersListener;
|
||||
final SortsListener _sortsListener;
|
||||
final FieldSettingsListener _fieldSettingsListener;
|
||||
|
||||
// FFI services
|
||||
final DatabaseViewBackendService _databaseViewBackendSvc;
|
||||
final SettingBackendService _settingBackendSvc;
|
||||
final FilterBackendService _filterBackendSvc;
|
||||
final SortBackendService _sortBackendSvc;
|
||||
final FieldSettingsBackendService _fieldSettingsBackendSvc;
|
||||
|
||||
bool _isDisposed = false;
|
||||
|
||||
@@ -97,18 +103,17 @@ class FieldController {
|
||||
final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>
|
||||
_updatedFieldCallbacks = {};
|
||||
|
||||
// Group callbacks
|
||||
final Map<String, GroupSettingPB> _groupConfigurationByFieldId = {};
|
||||
|
||||
// Filter callbacks
|
||||
final Map<OnReceiveFilters, VoidCallback> _filterCallbacks = {};
|
||||
_GridFilterNotifier? _filterNotifier = _GridFilterNotifier();
|
||||
final Map<String, FilterPB> _filterPBByFieldId = {};
|
||||
|
||||
// Sort callbacks
|
||||
final Map<OnReceiveSorts, VoidCallback> _sortCallbacks = {};
|
||||
_GridSortNotifier? _sortNotifier = _GridSortNotifier();
|
||||
final Map<String, SortPB> _sortPBByFieldId = {};
|
||||
|
||||
// Database settings temporary storage
|
||||
final Map<String, GroupSettingPB> _groupConfigurationByFieldId = {};
|
||||
final List<FieldSettingsPB> _fieldSettings = [];
|
||||
|
||||
// Getters
|
||||
List<FieldInfo> get fieldInfos => [..._fieldNotifier.fieldInfos];
|
||||
@@ -116,39 +121,28 @@ class FieldController {
|
||||
List<SortInfo> get sortInfos => [..._sortNotifier?.sorts ?? []];
|
||||
|
||||
FieldInfo? getField(String fieldId) {
|
||||
final fields = _fieldNotifier.fieldInfos
|
||||
.where((element) => element.id == fieldId)
|
||||
.toList();
|
||||
|
||||
if (fields.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
assert(fields.length == 1);
|
||||
return fields.first;
|
||||
return _fieldNotifier.fieldInfos
|
||||
.firstWhereOrNull((element) => element.id == fieldId);
|
||||
}
|
||||
|
||||
FilterInfo? getFilter(String filterId) {
|
||||
final filters = _filterNotifier?.filters
|
||||
.where((element) => element.filter.id == filterId)
|
||||
.toList() ??
|
||||
[];
|
||||
if (filters.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
assert(filters.length == 1);
|
||||
return filters.first;
|
||||
FilterInfo? getFilterByFilterId(String filterId) {
|
||||
return _filterNotifier?.filters
|
||||
.firstWhereOrNull((element) => element.filterId == filterId);
|
||||
}
|
||||
|
||||
SortInfo? getSort(String sortId) {
|
||||
final sorts = _sortNotifier?.sorts
|
||||
.where((element) => element.sortId == sortId)
|
||||
.toList() ??
|
||||
[];
|
||||
if (sorts.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
assert(sorts.length == 1);
|
||||
return sorts.first;
|
||||
FilterInfo? getFilterByFieldId(String fieldId) {
|
||||
return _filterNotifier?.filters
|
||||
.firstWhereOrNull((element) => element.fieldId == fieldId);
|
||||
}
|
||||
|
||||
SortInfo? getSortBySortId(String sortId) {
|
||||
return _sortNotifier?.sorts
|
||||
.firstWhereOrNull((element) => element.sortId == sortId);
|
||||
}
|
||||
|
||||
SortInfo? getSortByFieldId(String fieldId) {
|
||||
return _sortNotifier?.sorts
|
||||
.firstWhereOrNull((element) => element.fieldId == fieldId);
|
||||
}
|
||||
|
||||
FieldController({required this.viewId})
|
||||
@@ -159,34 +153,18 @@ class FieldController {
|
||||
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: viewId),
|
||||
_sortBackendSvc = SortBackendService(viewId: viewId),
|
||||
_sortsListener = SortsListener(viewId: viewId),
|
||||
_settingBackendSvc = SettingBackendService(viewId: viewId) {
|
||||
//Listen on field's changes
|
||||
_fieldSettingsListener = FieldSettingsListener(viewId: viewId),
|
||||
_fieldSettingsBackendSvc = FieldSettingsBackendService(viewId: viewId) {
|
||||
// Start listeners
|
||||
_listenOnFieldChanges();
|
||||
|
||||
//Listen on setting changes
|
||||
_listenOnSettingChanges();
|
||||
|
||||
//Listen on the filter changes
|
||||
_listenOnFilterChanges();
|
||||
|
||||
//Listen on the sort changes
|
||||
_listenOnSortChanged();
|
||||
|
||||
_settingBackendSvc.getSetting().then((result) {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.fold(
|
||||
(setting) => _updateSetting(setting),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
_listenOnFieldSettingsChanged();
|
||||
}
|
||||
|
||||
/// Listen for filter changes in the backend.
|
||||
void _listenOnFilterChanges() {
|
||||
//Listen on the filter changes
|
||||
|
||||
deleteFilterFromChangeset(
|
||||
List<FilterInfo> filters,
|
||||
FilterChangesetNotificationPB changeset,
|
||||
@@ -196,9 +174,6 @@ class FieldController {
|
||||
filters.retainWhere(
|
||||
(element) => !deleteFilterIds.contains(element.filter.id),
|
||||
);
|
||||
|
||||
_filterPBByFieldId
|
||||
.removeWhere((key, value) => deleteFilterIds.contains(value.id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +191,6 @@ class FieldController {
|
||||
fieldType: newFilter.fieldType,
|
||||
);
|
||||
if (fieldInfo != null) {
|
||||
_filterPBByFieldId[fieldInfo.id] = newFilter;
|
||||
filters.add(FilterInfo(viewId, newFilter, fieldInfo));
|
||||
}
|
||||
}
|
||||
@@ -234,8 +208,6 @@ class FieldController {
|
||||
// Remove the old filter
|
||||
if (filterIndex != -1) {
|
||||
filters.removeAt(filterIndex);
|
||||
_filterPBByFieldId
|
||||
.removeWhere((key, value) => value.id == updatedFilter.filterId);
|
||||
}
|
||||
|
||||
// Insert the filter if there is a filter and its field info is
|
||||
@@ -257,7 +229,6 @@ class FieldController {
|
||||
} else {
|
||||
filters.add(filterInfo);
|
||||
}
|
||||
_filterPBByFieldId[fieldInfo.id] = updatedFilter.filter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,16 +243,17 @@ class FieldController {
|
||||
result.fold(
|
||||
(FilterChangesetNotificationPB changeset) {
|
||||
final List<FilterInfo> filters = filterInfos;
|
||||
// Deletes the filters
|
||||
// delete removed filters
|
||||
deleteFilterFromChangeset(filters, changeset);
|
||||
|
||||
// Inserts the new filter if it's not exist
|
||||
// insert new filters
|
||||
insertFilterFromChangeset(filters, changeset);
|
||||
|
||||
// edit modified filters
|
||||
updateFilterFromChangeset(filters, changeset);
|
||||
|
||||
_updateFieldInfos();
|
||||
_filterNotifier?.filters = filters;
|
||||
_updateFieldInfos();
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
@@ -289,6 +261,7 @@ class FieldController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Listen for sort changes in the backend.
|
||||
void _listenOnSortChanged() {
|
||||
deleteSortFromChangeset(
|
||||
List<SortInfo> newSortInfos,
|
||||
@@ -299,9 +272,6 @@ class FieldController {
|
||||
newSortInfos.retainWhere(
|
||||
(element) => !deleteSortIds.contains(element.sortId),
|
||||
);
|
||||
|
||||
_sortPBByFieldId
|
||||
.removeWhere((key, value) => deleteSortIds.contains(value.id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +290,6 @@ class FieldController {
|
||||
);
|
||||
|
||||
if (fieldInfo != null) {
|
||||
_sortPBByFieldId[newSortPB.fieldId] = newSortPB;
|
||||
newSortInfos.add(SortInfo(sortPB: newSortPB, fieldInfo: fieldInfo));
|
||||
}
|
||||
}
|
||||
@@ -356,7 +325,6 @@ class FieldController {
|
||||
} else {
|
||||
newSortInfos.add(newSortInfo);
|
||||
}
|
||||
_sortPBByFieldId[updatedSort.fieldId] = updatedSort;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,7 +341,6 @@ class FieldController {
|
||||
insertSortFromChangeset(newSortInfos, changeset);
|
||||
updateSortFromChangeset(newSortInfos, changeset);
|
||||
|
||||
_updateFieldInfos();
|
||||
_sortNotifier?.sorts = newSortInfos;
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
@@ -382,8 +349,8 @@ class FieldController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Listen for databse setting changes in the backend.
|
||||
void _listenOnSettingChanges() {
|
||||
//Listen on setting changes
|
||||
_settingListener.start(
|
||||
onSettingUpdated: (result) {
|
||||
if (_isDisposed) {
|
||||
@@ -398,19 +365,100 @@ class FieldController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Listen for field changes in the backend.
|
||||
void _listenOnFieldChanges() {
|
||||
//Listen on field's changes
|
||||
Future<FieldInfo> attachFieldSettings(FieldInfo fieldInfo) async {
|
||||
return _fieldSettingsBackendSvc
|
||||
.getFieldSettings(fieldInfo.id)
|
||||
.then((result) {
|
||||
final fieldSettings = result.fold(
|
||||
(fieldSettings) => fieldSettings,
|
||||
(err) => null,
|
||||
);
|
||||
if (fieldSettings == null) {
|
||||
return fieldInfo;
|
||||
}
|
||||
final updatedFieldInfo =
|
||||
fieldInfo.copyWith(fieldSettings: fieldSettings);
|
||||
|
||||
return updatedFieldInfo;
|
||||
});
|
||||
}
|
||||
|
||||
List<FieldInfo> deleteFields(List<FieldIdPB> deletedFields) {
|
||||
if (deletedFields.isEmpty) {
|
||||
return fieldInfos;
|
||||
}
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final Map<String, FieldIdPB> deletedFieldMap = {
|
||||
for (final fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
|
||||
};
|
||||
|
||||
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
|
||||
return newFields;
|
||||
}
|
||||
|
||||
Future<List<FieldInfo>> insertFields(
|
||||
List<IndexFieldPB> insertedFields,
|
||||
List<FieldInfo> fieldInfos,
|
||||
) async {
|
||||
if (insertedFields.isEmpty) {
|
||||
return fieldInfos;
|
||||
}
|
||||
final List<FieldInfo> newFieldInfos = fieldInfos;
|
||||
for (final indexField in insertedFields) {
|
||||
final initial = FieldInfo.initial(indexField.field_1);
|
||||
final fieldInfo = await attachFieldSettings(initial);
|
||||
if (newFieldInfos.length > indexField.index) {
|
||||
newFieldInfos.insert(indexField.index, fieldInfo);
|
||||
} else {
|
||||
newFieldInfos.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
return newFieldInfos;
|
||||
}
|
||||
|
||||
Future<(List<FieldInfo>, List<FieldInfo>)> updateFields(
|
||||
List<FieldPB> updatedFieldPBs,
|
||||
List<FieldInfo> fieldInfos,
|
||||
) async {
|
||||
if (updatedFieldPBs.isEmpty) {
|
||||
return (<FieldInfo>[], fieldInfos);
|
||||
}
|
||||
|
||||
final List<FieldInfo> newFieldInfo = fieldInfos;
|
||||
final List<FieldInfo> updatedFields = [];
|
||||
for (final updatedFieldPB in updatedFieldPBs) {
|
||||
final index =
|
||||
newFieldInfo.indexWhere((field) => field.id == updatedFieldPB.id);
|
||||
if (index != -1) {
|
||||
newFieldInfo.removeAt(index);
|
||||
final initial = FieldInfo.initial(updatedFieldPB);
|
||||
final fieldInfo = await attachFieldSettings(initial);
|
||||
newFieldInfo.insert(index, fieldInfo);
|
||||
updatedFields.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return (updatedFields, newFieldInfo);
|
||||
}
|
||||
|
||||
// Listen on field's changes
|
||||
_fieldListener.start(
|
||||
onFieldsChanged: (result) {
|
||||
onFieldsChanged: (result) async {
|
||||
result.fold(
|
||||
(changeset) {
|
||||
(changeset) async {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
_deleteFields(changeset.deletedFields);
|
||||
_insertFields(changeset.insertedFields);
|
||||
List<FieldInfo> updatedFields;
|
||||
List<FieldInfo> fieldInfos = deleteFields(changeset.deletedFields);
|
||||
fieldInfos =
|
||||
await insertFields(changeset.insertedFields, fieldInfos);
|
||||
(updatedFields, fieldInfos) =
|
||||
await updateFields(changeset.updatedFields, fieldInfos);
|
||||
|
||||
final updatedFields = _updateFields(changeset.updatedFields);
|
||||
_fieldNotifier.fieldInfos = fieldInfos;
|
||||
for (final listener in _updatedFieldCallbacks.values) {
|
||||
listener(updatedFields);
|
||||
}
|
||||
@@ -421,31 +469,70 @@ class FieldController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Listen for field setting changes in the backend.
|
||||
void _listenOnFieldSettingsChanged() {
|
||||
FieldInfo updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
FieldInfo updatedField = newFields[0];
|
||||
|
||||
final index = newFields
|
||||
.indexWhere((field) => field.id == updatedFieldSettings.fieldId);
|
||||
if (index != -1) {
|
||||
newFields[index] =
|
||||
newFields[index].copyWith(fieldSettings: updatedFieldSettings);
|
||||
updatedField = newFields[index];
|
||||
}
|
||||
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
return updatedField;
|
||||
}
|
||||
|
||||
_fieldSettingsListener.start(
|
||||
onFieldSettingsChanged: (result) {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
result.fold(
|
||||
(fieldSettings) {
|
||||
final updatedFieldInfo = updateFieldSettings(fieldSettings);
|
||||
for (final listener in _updatedFieldCallbacks.values) {
|
||||
listener([updatedFieldInfo]);
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates sort, filter, group and field info from `DatabaseViewSettingPB`
|
||||
void _updateSetting(DatabaseViewSettingPB setting) {
|
||||
_groupConfigurationByFieldId.clear();
|
||||
for (final configuration in setting.groupSettings.items) {
|
||||
_groupConfigurationByFieldId[configuration.fieldId] = configuration;
|
||||
}
|
||||
|
||||
for (final filter in setting.filters.items) {
|
||||
_filterPBByFieldId[filter.fieldId] = filter;
|
||||
}
|
||||
_filterNotifier?.filters = _filterInfoListFromPBs(setting.filters.items);
|
||||
|
||||
for (final sort in setting.sorts.items) {
|
||||
_sortPBByFieldId[sort.fieldId] = sort;
|
||||
}
|
||||
_sortNotifier?.sorts = _sortInfoListFromPBs(setting.sorts.items);
|
||||
|
||||
_fieldSettings.clear();
|
||||
_fieldSettings.addAll(setting.fieldSettings.items);
|
||||
|
||||
_updateFieldInfos();
|
||||
}
|
||||
|
||||
/// Attach sort, filter, group information and field settings to `FieldInfo`
|
||||
void _updateFieldInfos() {
|
||||
final List<FieldInfo> newFieldInfos = [];
|
||||
for (final field in _fieldNotifier.fieldInfos) {
|
||||
newFieldInfos.add(
|
||||
field.copyWith(
|
||||
fieldSettings: _fieldSettings
|
||||
.firstWhereOrNull((setting) => setting.fieldId == field.id),
|
||||
isGroupField: _groupConfigurationByFieldId[field.id] != null,
|
||||
hasFilter: _filterPBByFieldId[field.id] != null,
|
||||
hasSort: _sortPBByFieldId[field.id] != null,
|
||||
hasFilter: getFilterByFieldId(field.id) != null,
|
||||
hasSort: getSortByFieldId(field.id) != null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -453,52 +540,27 @@ class FieldController {
|
||||
_fieldNotifier.fieldInfos = newFieldInfos;
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
if (_isDisposed) {
|
||||
Log.warn('FieldController is already disposed');
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
await _fieldListener.stop();
|
||||
await _filtersListener.stop();
|
||||
await _settingListener.stop();
|
||||
await _sortsListener.stop();
|
||||
|
||||
for (final callback in _fieldCallbacks.values) {
|
||||
_fieldNotifier.removeListener(callback);
|
||||
}
|
||||
_fieldNotifier.dispose();
|
||||
|
||||
for (final callback in _filterCallbacks.values) {
|
||||
_filterNotifier?.removeListener(callback);
|
||||
}
|
||||
for (final callback in _sortCallbacks.values) {
|
||||
_sortNotifier?.removeListener(callback);
|
||||
}
|
||||
|
||||
_filterNotifier?.dispose();
|
||||
_filterNotifier = null;
|
||||
|
||||
_sortNotifier?.dispose();
|
||||
_sortNotifier = null;
|
||||
}
|
||||
|
||||
/// Load all of the fields. This is required when opening the database
|
||||
Future<Either<Unit, FlowyError>> loadFields({
|
||||
required List<FieldIdPB> fieldIds,
|
||||
}) async {
|
||||
final result = await _databaseViewBackendSvc.getFields(fieldIds: fieldIds);
|
||||
return Future(
|
||||
() => result.fold(
|
||||
(newFields) {
|
||||
(newFields) async {
|
||||
if (_isDisposed) {
|
||||
return left(unit);
|
||||
}
|
||||
|
||||
_fieldNotifier.fieldInfos =
|
||||
newFields.map((field) => FieldInfo.initial(field)).toList();
|
||||
_loadFilters();
|
||||
_loadSorts();
|
||||
await Future.wait([
|
||||
_loadFilters(),
|
||||
_loadSorts(),
|
||||
_loadAllFieldSettings(),
|
||||
]);
|
||||
_updateFieldInfos();
|
||||
|
||||
return left(unit);
|
||||
},
|
||||
(err) => right(err),
|
||||
@@ -506,24 +568,12 @@ class FieldController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Load all the filters from the backend. Required by `loadFields`
|
||||
Future<Either<Unit, FlowyError>> _loadFilters() async {
|
||||
return _filterBackendSvc.getAllFilters().then((result) {
|
||||
return result.fold(
|
||||
(filterPBs) {
|
||||
final List<FilterInfo> filters = [];
|
||||
for (final filterPB in filterPBs) {
|
||||
final fieldInfo = _findFieldInfo(
|
||||
fieldInfos: fieldInfos,
|
||||
fieldId: filterPB.fieldId,
|
||||
fieldType: filterPB.fieldType,
|
||||
);
|
||||
if (fieldInfo != null) {
|
||||
final filterInfo = FilterInfo(viewId, filterPB, fieldInfo);
|
||||
filters.add(filterInfo);
|
||||
}
|
||||
}
|
||||
|
||||
_filterNotifier?.filters = filters;
|
||||
_filterNotifier?.filters = _filterInfoListFromPBs(filterPBs);
|
||||
return left(unit);
|
||||
},
|
||||
(err) => right(err),
|
||||
@@ -531,26 +581,12 @@ class FieldController {
|
||||
});
|
||||
}
|
||||
|
||||
/// Load all the sorts from the backend. Required by `loadFields`
|
||||
Future<Either<Unit, FlowyError>> _loadSorts() async {
|
||||
return _sortBackendSvc.getAllSorts().then((result) {
|
||||
return result.fold(
|
||||
(sortPBs) {
|
||||
final List<SortInfo> sortInfos = [];
|
||||
for (final sortPB in sortPBs) {
|
||||
final fieldInfo = _findFieldInfo(
|
||||
fieldInfos: fieldInfos,
|
||||
fieldId: sortPB.fieldId,
|
||||
fieldType: sortPB.fieldType,
|
||||
);
|
||||
|
||||
if (fieldInfo != null) {
|
||||
final sortInfo = SortInfo(sortPB: sortPB, fieldInfo: fieldInfo);
|
||||
sortInfos.add(sortInfo);
|
||||
}
|
||||
}
|
||||
|
||||
_updateFieldInfos();
|
||||
_sortNotifier?.sorts = sortInfos;
|
||||
_sortNotifier?.sorts = _sortInfoListFromPBs(sortPBs);
|
||||
return left(unit);
|
||||
},
|
||||
(err) => right(err),
|
||||
@@ -558,6 +594,56 @@ class FieldController {
|
||||
});
|
||||
}
|
||||
|
||||
/// Load all the field settings from the backend. Required by `loadFields`
|
||||
Future<Either<Unit, FlowyError>> _loadAllFieldSettings() async {
|
||||
return _fieldSettingsBackendSvc.getAllFieldSettings().then((result) {
|
||||
return result.fold(
|
||||
(fieldSettingsList) {
|
||||
_fieldSettings.clear();
|
||||
_fieldSettings.addAll(fieldSettingsList);
|
||||
return left(unit);
|
||||
},
|
||||
(err) => right(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Attach corresponding `FieldInfo`s to the `FilterPB`s
|
||||
List<FilterInfo> _filterInfoListFromPBs(List<FilterPB> filterPBs) {
|
||||
FilterInfo? getFilterInfo(FilterPB filterPB) {
|
||||
final fieldInfo = _findFieldInfo(
|
||||
fieldInfos: fieldInfos,
|
||||
fieldId: filterPB.fieldId,
|
||||
fieldType: filterPB.fieldType,
|
||||
);
|
||||
return fieldInfo != null ? FilterInfo(viewId, filterPB, fieldInfo) : null;
|
||||
}
|
||||
|
||||
return filterPBs
|
||||
.map((filterPB) => getFilterInfo(filterPB))
|
||||
.whereType<FilterInfo>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Attach corresponding `FieldInfo`s to the `SortPB`s
|
||||
List<SortInfo> _sortInfoListFromPBs(List<SortPB> sortPBs) {
|
||||
SortInfo? getSortInfo(SortPB sortPB) {
|
||||
final fieldInfo = _findFieldInfo(
|
||||
fieldInfos: fieldInfos,
|
||||
fieldId: sortPB.fieldId,
|
||||
fieldType: sortPB.fieldType,
|
||||
);
|
||||
return fieldInfo != null
|
||||
? SortInfo(sortPB: sortPB, fieldInfo: fieldInfo)
|
||||
: null;
|
||||
}
|
||||
|
||||
return sortPBs
|
||||
.map((sortPB) => getSortInfo(sortPB))
|
||||
.whereType<SortInfo>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
void addListener({
|
||||
OnReceiveFields? onReceiveFields,
|
||||
OnReceiveUpdateFields? onFieldsChanged,
|
||||
@@ -640,57 +726,35 @@ class FieldController {
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteFields(List<FieldIdPB> deletedFields) {
|
||||
if (deletedFields.isEmpty) {
|
||||
/// Stop listeners, dispose notifiers and clear listener callbacks
|
||||
Future<void> dispose() async {
|
||||
if (_isDisposed) {
|
||||
Log.warn('FieldController is already disposed');
|
||||
return;
|
||||
}
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final Map<String, FieldIdPB> deletedFieldMap = {
|
||||
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
|
||||
};
|
||||
_isDisposed = true;
|
||||
await _fieldListener.stop();
|
||||
await _filtersListener.stop();
|
||||
await _settingListener.stop();
|
||||
await _sortsListener.stop();
|
||||
await _fieldSettingsListener.stop();
|
||||
|
||||
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
}
|
||||
for (final callback in _fieldCallbacks.values) {
|
||||
_fieldNotifier.removeListener(callback);
|
||||
}
|
||||
_fieldNotifier.dispose();
|
||||
|
||||
void _insertFields(List<IndexFieldPB> insertedFields) {
|
||||
if (insertedFields.isEmpty) {
|
||||
return;
|
||||
for (final callback in _filterCallbacks.values) {
|
||||
_filterNotifier?.removeListener(callback);
|
||||
}
|
||||
final List<FieldInfo> newFieldInfos = fieldInfos;
|
||||
for (final indexField in insertedFields) {
|
||||
final fieldInfo = FieldInfo.initial(indexField.field_1);
|
||||
if (newFieldInfos.length > indexField.index) {
|
||||
newFieldInfos.insert(indexField.index, fieldInfo);
|
||||
} else {
|
||||
newFieldInfos.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
_fieldNotifier.fieldInfos = newFieldInfos;
|
||||
}
|
||||
_filterNotifier?.dispose();
|
||||
_filterNotifier = null;
|
||||
|
||||
List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {
|
||||
if (updatedFieldPBs.isEmpty) {
|
||||
return [];
|
||||
for (final callback in _sortCallbacks.values) {
|
||||
_sortNotifier?.removeListener(callback);
|
||||
}
|
||||
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final List<FieldInfo> updatedFields = [];
|
||||
for (final updatedFieldPB in updatedFieldPBs) {
|
||||
final index =
|
||||
newFields.indexWhere((field) => field.id == updatedFieldPB.id);
|
||||
if (index != -1) {
|
||||
newFields.removeAt(index);
|
||||
final fieldInfo = FieldInfo.initial(updatedFieldPB);
|
||||
newFields.insert(index, fieldInfo);
|
||||
updatedFields.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedFields.isNotEmpty) {
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
}
|
||||
return updatedFields;
|
||||
_sortNotifier?.dispose();
|
||||
_sortNotifier = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,12 +791,7 @@ FieldInfo? _findFieldInfo({
|
||||
required String fieldId,
|
||||
required FieldType fieldType,
|
||||
}) {
|
||||
final fieldIndex = fieldInfos.indexWhere((element) {
|
||||
return element.id == fieldId && element.fieldType == fieldType;
|
||||
});
|
||||
if (fieldIndex != -1) {
|
||||
return fieldInfos[fieldIndex];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return fieldInfos.firstWhereOrNull(
|
||||
(element) => element.id == fieldId && element.fieldType == fieldType,
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'field_info.freezed.dart';
|
||||
|
||||
@@ -8,6 +9,7 @@ class FieldInfo with _$FieldInfo {
|
||||
|
||||
const factory FieldInfo({
|
||||
required FieldPB field,
|
||||
required FieldSettingsPB? fieldSettings,
|
||||
required bool isGroupField,
|
||||
required bool hasFilter,
|
||||
required bool hasSort,
|
||||
@@ -21,8 +23,11 @@ class FieldInfo with _$FieldInfo {
|
||||
|
||||
bool get isPrimary => field.isPrimary;
|
||||
|
||||
FieldVisibility? get visibility => fieldSettings?.visibility;
|
||||
|
||||
factory FieldInfo.initial(FieldPB field) => FieldInfo(
|
||||
field: field,
|
||||
fieldSettings: null,
|
||||
hasFilter: false,
|
||||
hasSort: false,
|
||||
isGroupField: false,
|
||||
|
||||
+2
-6
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
@@ -30,7 +31,6 @@ class FieldBackendService {
|
||||
Future<Either<Unit, FlowyError>> updateField({
|
||||
String? name,
|
||||
bool? frozen,
|
||||
bool? visibility,
|
||||
double? width,
|
||||
}) {
|
||||
final payload = FieldChangesetPB.create()
|
||||
@@ -45,10 +45,6 @@ class FieldBackendService {
|
||||
payload.frozen = frozen;
|
||||
}
|
||||
|
||||
if (visibility != null) {
|
||||
payload.visibility = visibility;
|
||||
}
|
||||
|
||||
if (width != null) {
|
||||
payload.width = width.toInt();
|
||||
}
|
||||
@@ -113,6 +109,6 @@ class FieldBackendService {
|
||||
class FieldContext with _$FieldContext {
|
||||
const factory FieldContext({
|
||||
required String viewId,
|
||||
required FieldPB field,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _FieldCellContext;
|
||||
}
|
||||
-10
@@ -27,13 +27,6 @@ class DateTypeOptionBloc
|
||||
),
|
||||
);
|
||||
},
|
||||
includeTime: (_IncludeTime value) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
typeOption: _updateTypeOption(includeTime: value.includeTime),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -42,7 +35,6 @@ class DateTypeOptionBloc
|
||||
DateTypeOptionPB _updateTypeOption({
|
||||
DateFormatPB? dateFormat,
|
||||
TimeFormatPB? timeFormat,
|
||||
bool? includeTime,
|
||||
}) {
|
||||
state.typeOption.freeze();
|
||||
return state.typeOption.rebuild((typeOption) {
|
||||
@@ -63,8 +55,6 @@ class DateTypeOptionEvent with _$DateTypeOptionEvent {
|
||||
_DidSelectDateFormat;
|
||||
const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) =
|
||||
_DidSelectTimeFormat;
|
||||
const factory DateTypeOptionEvent.includeTime(bool includeTime) =
|
||||
_IncludeTime;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
import 'type_option_context.dart';
|
||||
part 'timestamp_bloc.freezed.dart';
|
||||
|
||||
class TimestampTypeOptionBloc
|
||||
extends Bloc<TimestampTypeOptionEvent, TimestampTypeOptionState> {
|
||||
TimestampTypeOptionBloc({
|
||||
required TimestampTypeOptionContext typeOptionContext,
|
||||
}) : super(TimestampTypeOptionState.initial(typeOptionContext.typeOption)) {
|
||||
on<TimestampTypeOptionEvent>(
|
||||
(event, emit) async {
|
||||
event.map(
|
||||
didSelectDateFormat: (_DidSelectDateFormat value) {
|
||||
_updateTypeOption(dateFormat: value.format, emit: emit);
|
||||
},
|
||||
didSelectTimeFormat: (_DidSelectTimeFormat value) {
|
||||
_updateTypeOption(timeFormat: value.format, emit: emit);
|
||||
},
|
||||
includeTime: (_IncludeTime value) {
|
||||
_updateTypeOption(includeTime: value.includeTime, emit: emit);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateTypeOption({
|
||||
DateFormatPB? dateFormat,
|
||||
TimeFormatPB? timeFormat,
|
||||
bool? includeTime,
|
||||
required Emitter<TimestampTypeOptionState> emit,
|
||||
}) {
|
||||
state.typeOption.freeze();
|
||||
final newTypeOption = state.typeOption.rebuild((typeOption) {
|
||||
if (dateFormat != null) {
|
||||
typeOption.dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
if (timeFormat != null) {
|
||||
typeOption.timeFormat = timeFormat;
|
||||
}
|
||||
|
||||
if (includeTime != null) {
|
||||
typeOption.includeTime = includeTime;
|
||||
}
|
||||
});
|
||||
emit(state.copyWith(typeOption: newTypeOption));
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampTypeOptionEvent with _$TimestampTypeOptionEvent {
|
||||
const factory TimestampTypeOptionEvent.didSelectDateFormat(
|
||||
DateFormatPB format,
|
||||
) = _DidSelectDateFormat;
|
||||
const factory TimestampTypeOptionEvent.didSelectTimeFormat(
|
||||
TimeFormatPB format,
|
||||
) = _DidSelectTimeFormat;
|
||||
const factory TimestampTypeOptionEvent.includeTime(bool includeTime) =
|
||||
_IncludeTime;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampTypeOptionState with _$TimestampTypeOptionState {
|
||||
const factory TimestampTypeOptionState({
|
||||
required TimestampTypeOptionPB typeOption,
|
||||
}) = _TimestampTypeOptionState;
|
||||
|
||||
factory TimestampTypeOptionState.initial(TimestampTypeOptionPB typeOption) =>
|
||||
TimestampTypeOptionState(typeOption: typeOption);
|
||||
}
|
||||
+13
-1
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
@@ -58,7 +59,7 @@ class URLTypeOptionWidgetDataParser extends TypeOptionParser<URLTypeOptionPB> {
|
||||
}
|
||||
}
|
||||
|
||||
// Date
|
||||
// DateTime
|
||||
typedef DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
|
||||
|
||||
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
||||
@@ -68,6 +69,17 @@ class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
||||
}
|
||||
}
|
||||
|
||||
// LastModified and CreatedAt
|
||||
typedef TimestampTypeOptionContext = TypeOptionContext<TimestampTypeOptionPB>;
|
||||
|
||||
class TimestampTypeOptionDataParser
|
||||
extends TypeOptionParser<TimestampTypeOptionPB> {
|
||||
@override
|
||||
TimestampTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
return TimestampTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
// SingleSelect
|
||||
typedef SingleSelectTypeOptionContext
|
||||
= TypeOptionContext<SingleSelectTypeOptionPB>;
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
|
||||
typedef FieldSettingsValue = Either<FieldSettingsPB, FlowyError>;
|
||||
|
||||
class FieldSettingsListener {
|
||||
final String viewId;
|
||||
PublishNotifier<FieldSettingsValue>? _fieldSettingsNotifier =
|
||||
PublishNotifier();
|
||||
DatabaseNotificationListener? _listener;
|
||||
|
||||
FieldSettingsListener({required this.viewId});
|
||||
|
||||
void start({
|
||||
required void Function(FieldSettingsValue) onFieldSettingsChanged,
|
||||
}) {
|
||||
_fieldSettingsNotifier?.addPublishListener(onFieldSettingsChanged);
|
||||
_listener = DatabaseNotificationListener(
|
||||
objectId: viewId,
|
||||
handler: _handler,
|
||||
);
|
||||
}
|
||||
|
||||
void _handler(
|
||||
DatabaseNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case DatabaseNotification.DidUpdateFieldSettings:
|
||||
result.fold(
|
||||
(payload) => _fieldSettingsNotifier?.value =
|
||||
left(FieldSettingsPB.fromBuffer(payload)),
|
||||
(error) => _fieldSettingsNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_fieldSettingsNotifier?.dispose();
|
||||
_fieldSettingsNotifier = null;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
class FieldSettingsBackendService {
|
||||
final String viewId;
|
||||
FieldSettingsBackendService({required this.viewId});
|
||||
|
||||
Future<Either<FieldSettingsPB, FlowyError>> getFieldSettings(
|
||||
String fieldId,
|
||||
) {
|
||||
final id = FieldIdPB(fieldId: fieldId);
|
||||
final ids = RepeatedFieldIdPB()..items.add(id);
|
||||
final payload = FieldIdsPB()
|
||||
..viewId = viewId
|
||||
..fieldIds = ids;
|
||||
|
||||
return DatabaseEventGetFieldSettings(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(repeatedFieldSettings) {
|
||||
final fieldSetting = repeatedFieldSettings.items.first;
|
||||
if (!fieldSetting.hasVisibility()) {
|
||||
fieldSetting.visibility = FieldVisibility.AlwaysShown;
|
||||
}
|
||||
|
||||
return left(fieldSetting);
|
||||
},
|
||||
(r) => right(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Either<List<FieldSettingsPB>, FlowyError>> getAllFieldSettings() {
|
||||
final payload = DatabaseViewIdPB()..value = viewId;
|
||||
|
||||
return DatabaseEventGetAllFieldSettings(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(repeatedFieldSettings) {
|
||||
final fieldSettings = <FieldSettingsPB>[];
|
||||
|
||||
for (final fieldSetting in repeatedFieldSettings.items) {
|
||||
if (!fieldSetting.hasVisibility()) {
|
||||
fieldSetting.visibility = FieldVisibility.AlwaysShown;
|
||||
}
|
||||
fieldSettings.add(fieldSetting);
|
||||
}
|
||||
|
||||
return left(fieldSettings);
|
||||
},
|
||||
(r) => right(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> updateFieldSettings({
|
||||
required String fieldId,
|
||||
FieldVisibility? fieldVisibility,
|
||||
}) {
|
||||
final FieldSettingsChangesetPB payload = FieldSettingsChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId;
|
||||
|
||||
if (fieldVisibility != null) {
|
||||
payload.visibility = fieldVisibility;
|
||||
}
|
||||
|
||||
return DatabaseEventUpdateFieldSettings(payload).send();
|
||||
}
|
||||
}
|
||||
+1
-5
@@ -32,11 +32,7 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
|
||||
await _listenRowMeteChanged();
|
||||
},
|
||||
didReceiveRowMeta: (RowMetaPB rowMeta) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
rowMeta: rowMeta,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(rowMeta: rowMeta));
|
||||
},
|
||||
setCover: (String coverURL) {
|
||||
_updateMeta(coverURL: coverURL);
|
||||
|
||||
@@ -74,6 +74,7 @@ class RowCache {
|
||||
final rowInfo = buildGridRow(row);
|
||||
_rowList.add(rowInfo);
|
||||
}
|
||||
_changedNotifier.receive(const ChangedReason.setInitialRows());
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
@@ -245,13 +246,11 @@ class RowCache {
|
||||
// ignore: prefer_collection_literals
|
||||
final cellContextMap = CellContextByFieldId();
|
||||
for (final fieldInfo in _fieldDelegate.fieldInfos) {
|
||||
if (fieldInfo.field.visibility) {
|
||||
cellContextMap[fieldInfo.id] = DatabaseCellContext(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
fieldInfo: fieldInfo,
|
||||
);
|
||||
}
|
||||
cellContextMap[fieldInfo.id] = DatabaseCellContext(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
fieldInfo: fieldInfo,
|
||||
);
|
||||
}
|
||||
return cellContextMap;
|
||||
}
|
||||
@@ -281,6 +280,7 @@ class RowChangesetNotifier extends ChangeNotifier {
|
||||
initial: (_) {},
|
||||
reorderRows: (_) => notifyListeners(),
|
||||
reorderSingleRow: (_) => notifyListeners(),
|
||||
setInitialRows: (_) => notifyListeners(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -313,6 +313,7 @@ class ChangedReason with _$ChangedReason {
|
||||
ReorderSingleRowPB reorderRow,
|
||||
RowInfo rowInfo,
|
||||
) = _ReorderSingleRow;
|
||||
const factory ChangedReason.setInitialRows() = _SetInitialRows;
|
||||
}
|
||||
|
||||
class InsertedIndex {
|
||||
|
||||
+7
-5
@@ -1,6 +1,8 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@@ -28,13 +30,13 @@ class DatabasePropertyBloc
|
||||
_startListening();
|
||||
},
|
||||
setFieldVisibility: (_SetFieldVisibility value) async {
|
||||
final fieldBackendSvc = FieldBackendService(
|
||||
final fieldSettingsSvc = FieldSettingsBackendService(
|
||||
viewId: viewId,
|
||||
fieldId: value.fieldId,
|
||||
);
|
||||
|
||||
final result = await fieldBackendSvc.updateField(
|
||||
visibility: value.visibility,
|
||||
final result = await fieldSettingsSvc.updateFieldSettings(
|
||||
fieldId: value.fieldId,
|
||||
fieldVisibility: value.visibility,
|
||||
);
|
||||
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
@@ -84,7 +86,7 @@ class DatabasePropertyEvent with _$DatabasePropertyEvent {
|
||||
const factory DatabasePropertyEvent.initial() = _Initial;
|
||||
const factory DatabasePropertyEvent.setFieldVisibility(
|
||||
String fieldId,
|
||||
bool visibility,
|
||||
FieldVisibility visibility,
|
||||
) = _SetFieldVisibility;
|
||||
const factory DatabasePropertyEvent.didReceiveFieldUpdate(
|
||||
List<FieldInfo> fields,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'dart:io';
|
||||
import 'package:appflowy/workspace/application/settings/share/export_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
part 'share_bloc.freezed.dart';
|
||||
|
||||
class DatabaseShareBloc extends Bloc<DatabaseShareEvent, DatabaseShareState> {
|
||||
DatabaseShareBloc({
|
||||
required this.view,
|
||||
}) : super(const DatabaseShareState.initial()) {
|
||||
on<ShareCSV>(_onShareCSV);
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
Future<void> _onShareCSV(
|
||||
ShareCSV event,
|
||||
Emitter<DatabaseShareState> emit,
|
||||
) async {
|
||||
emit(const DatabaseShareState.loading());
|
||||
|
||||
final result = await BackendExportService.exportDatabaseAsCSV(view.id);
|
||||
result.fold(
|
||||
(l) => _saveCSVToPath(l.data, event.path),
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
|
||||
emit(
|
||||
DatabaseShareState.finish(
|
||||
result.fold(
|
||||
(l) {
|
||||
_saveCSVToPath(l.data, event.path);
|
||||
return left(unit);
|
||||
},
|
||||
(r) => right(r),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ExportDataPB _saveCSVToPath(String markdown, String path) {
|
||||
File(path).writeAsStringSync(markdown);
|
||||
return ExportDataPB()
|
||||
..data = markdown
|
||||
..exportType = ExportType.Markdown;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DatabaseShareEvent with _$DatabaseShareEvent {
|
||||
const factory DatabaseShareEvent.shareCSV(String path) = ShareCSV;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DatabaseShareState with _$DatabaseShareState {
|
||||
const factory DatabaseShareState.initial() = _Initial;
|
||||
const factory DatabaseShareState.loading() = _Loading;
|
||||
const factory DatabaseShareState.finish(
|
||||
Either<Unit, FlowyError> successOrFail,
|
||||
) = _Finish;
|
||||
}
|
||||
+4
-6
@@ -381,12 +381,10 @@ Widget? _buildHeaderIcon(GroupData customData) {
|
||||
switch (customData.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
final group = customData.asCheckboxGroup()!;
|
||||
if (group.isCheck) {
|
||||
widget =
|
||||
const FlowySvg(FlowySvgs.check_filled_s, blendMode: BlendMode.dst,);
|
||||
} else {
|
||||
widget = const FlowySvg(FlowySvgs.uncheck_s);
|
||||
}
|
||||
widget = FlowySvg(
|
||||
group.isCheck ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
break;
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
|
||||
+7
@@ -63,6 +63,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||
createEvent: (DateTime date) async {
|
||||
await _createEvent(date);
|
||||
},
|
||||
newEventPopupDisplayed: () {
|
||||
emit(state.copyWith(editingEvent: null));
|
||||
},
|
||||
moveEvent: (CalendarDayEvent event, DateTime date) async {
|
||||
await _moveEvent(event, date);
|
||||
},
|
||||
@@ -378,6 +381,10 @@ class CalendarEvent with _$CalendarEvent {
|
||||
CalendarEventData<CalendarDayEvent> event,
|
||||
) = _DidReceiveNewEvent;
|
||||
|
||||
// Called after creating a new event
|
||||
const factory CalendarEvent.newEventPopupDisplayed() =
|
||||
_NewEventPopupDisplayed;
|
||||
|
||||
// Called when receive a new event
|
||||
const factory CalendarEvent.didReceiveEvent(
|
||||
CalendarEventData<CalendarDayEvent> event,
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'calendar_event_editor_bloc.freezed.dart';
|
||||
|
||||
class CalendarEventEditorBloc
|
||||
extends Bloc<CalendarEventEditorEvent, CalendarEventEditorState> {
|
||||
final RowController rowController;
|
||||
final CalendarLayoutSettingPB layoutSettings;
|
||||
final RowBackendService _rowService;
|
||||
|
||||
CalendarEventEditorBloc({
|
||||
required this.rowController,
|
||||
required this.layoutSettings,
|
||||
}) : _rowService = RowBackendService(viewId: rowController.viewId),
|
||||
super(CalendarEventEditorState.initial()) {
|
||||
on<CalendarEventEditorEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
final cells = rowController
|
||||
.loadData()
|
||||
.values
|
||||
.where((cellContext) => cellContext.isVisible())
|
||||
.toList();
|
||||
if (!isClosed) {
|
||||
add(
|
||||
CalendarEventEditorEvent.didReceiveCellDatas(cells),
|
||||
);
|
||||
}
|
||||
},
|
||||
didReceiveCellDatas: (cells) {
|
||||
emit(state.copyWith(cells: cells));
|
||||
},
|
||||
delete: () async {
|
||||
final result = await _rowService.deleteRow(rowController.rowId);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
rowController.addListener(
|
||||
onRowChanged: (cells, reason) {
|
||||
if (!isClosed) {
|
||||
final cellData = cells.values
|
||||
.where((cellContext) => cellContext.isVisible())
|
||||
.toList();
|
||||
add(
|
||||
CalendarEventEditorEvent.didReceiveCellDatas(cellData),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
rowController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CalendarEventEditorEvent with _$CalendarEventEditorEvent {
|
||||
const factory CalendarEventEditorEvent.initial() = _Initial;
|
||||
const factory CalendarEventEditorEvent.didReceiveCellDatas(
|
||||
List<DatabaseCellContext> cells,
|
||||
) = _DidReceiveCellDatas;
|
||||
const factory CalendarEventEditorEvent.delete() = _Delete;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CalendarEventEditorState with _$CalendarEventEditorState {
|
||||
const factory CalendarEventEditorState({
|
||||
required List<DatabaseCellContext> cells,
|
||||
}) = _CalendarEventEditorState;
|
||||
|
||||
factory CalendarEventEditorState.initial() =>
|
||||
CalendarEventEditorState(cells: List.empty());
|
||||
}
|
||||
+4
-1
@@ -58,9 +58,12 @@ class UnscheduleEventsBloc
|
||||
);
|
||||
},
|
||||
didReceiveEvent: (CalendarEventPB event) {
|
||||
final events = [...state.allEvents, event];
|
||||
emit(
|
||||
state.copyWith(
|
||||
allEvents: [...state.allEvents, event],
|
||||
allEvents: events,
|
||||
unscheduleEvents:
|
||||
events.where((element) => !element.isScheduled).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+102
-56
@@ -1,13 +1,14 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import 'package:flowy_infra/size.dart';
|
||||
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
import '../../grid/presentation/layout/sizes.dart';
|
||||
import '../application/calendar_bloc.dart';
|
||||
@@ -18,7 +19,7 @@ class CalendarDayCard extends StatelessWidget {
|
||||
final bool isToday;
|
||||
final bool isInMonth;
|
||||
final DateTime date;
|
||||
final RowCache _rowCache;
|
||||
final RowCache rowCache;
|
||||
final List<CalendarDayEvent> events;
|
||||
final void Function(DateTime) onCreateEvent;
|
||||
|
||||
@@ -28,18 +29,21 @@ class CalendarDayCard extends StatelessWidget {
|
||||
required this.isInMonth,
|
||||
required this.date,
|
||||
required this.onCreateEvent,
|
||||
required RowCache rowCache,
|
||||
required this.rowCache,
|
||||
required this.events,
|
||||
Key? key,
|
||||
}) : _rowCache = rowCache,
|
||||
super(key: key);
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color backgroundColor = Theme.of(context).colorScheme.surface;
|
||||
if (!isInMonth) {
|
||||
backgroundColor = AFThemeExtension.of(context).lightGreyHover;
|
||||
Color backgroundColor = Colors.transparent;
|
||||
if (date.isWeekend) {
|
||||
backgroundColor = AFThemeExtension.of(context).calendarWeekendBGColor;
|
||||
}
|
||||
final hoverBackgroundColor =
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Colors.transparent;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
@@ -57,14 +61,14 @@ class CalendarDayCard extends StatelessWidget {
|
||||
),
|
||||
|
||||
// Add a separator between the header and the content.
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
const VSpace(6.0),
|
||||
|
||||
// List of cards or empty space
|
||||
if (events.isNotEmpty)
|
||||
_EventList(
|
||||
events: events,
|
||||
viewId: viewId,
|
||||
rowCache: _rowCache,
|
||||
rowCache: rowCache,
|
||||
constraints: constraints,
|
||||
),
|
||||
],
|
||||
@@ -79,34 +83,29 @@ class CalendarDayCard extends StatelessWidget {
|
||||
DragTarget<CalendarDayEvent>(
|
||||
builder: (context, candidate, __) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (candidate.isNotEmpty)
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color:
|
||||
candidate.isEmpty ? null : hoverBackgroundColor,
|
||||
padding: const EdgeInsets.only(top: 5.0),
|
||||
child: child,
|
||||
)
|
||||
),
|
||||
if (candidate.isEmpty)
|
||||
NewEventButton(onCreate: () => onCreateEvent(date)),
|
||||
],
|
||||
);
|
||||
},
|
||||
onWillAccept: (CalendarDayEvent? event) {
|
||||
if (event == null) {
|
||||
return false;
|
||||
}
|
||||
return !isSameDay(event.date, date);
|
||||
},
|
||||
onAccept: (CalendarDayEvent event) {
|
||||
if (event.date == date) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<CalendarBloc>()
|
||||
.add(CalendarEvent.moveEvent(event, date));
|
||||
},
|
||||
),
|
||||
NewEventButton(onCreate: () => onCreateEvent(date)),
|
||||
MouseRegion(
|
||||
onEnter: (p) => notifyEnter(context, true),
|
||||
onExit: (p) => notifyEnter(context, false),
|
||||
@@ -143,7 +142,7 @@ class _Header extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: _DayBadge(
|
||||
isToday: isToday,
|
||||
isInMonth: isInMonth,
|
||||
@@ -166,7 +165,7 @@ class NewEventButton extends StatelessWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: FlowyIconButton(
|
||||
onPressed: onCreate,
|
||||
iconPadding: EdgeInsets.zero,
|
||||
@@ -174,6 +173,35 @@ class NewEventButton extends StatelessWidget {
|
||||
fillColor: Theme.of(context).colorScheme.background,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
width: 22,
|
||||
tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0xffd0d3d6)
|
||||
: const Color(0xff59647a),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
borderRadius: Corners.s5Border,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
spreadRadius: -2,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 2,
|
||||
),
|
||||
BoxShadow(
|
||||
spreadRadius: 0,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 4,
|
||||
),
|
||||
BoxShadow(
|
||||
spreadRadius: 2,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -195,37 +223,49 @@ class _DayBadge extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color dayTextColor = Theme.of(context).colorScheme.onBackground;
|
||||
Color monthTextColor = Theme.of(context).colorScheme.onBackground;
|
||||
final String monthString =
|
||||
DateFormat("MMM ", context.locale.toLanguageTag()).format(date);
|
||||
final String dayString = date.day.toString();
|
||||
|
||||
if (!isInMonth) {
|
||||
dayTextColor = Theme.of(context).disabledColor;
|
||||
monthTextColor = Theme.of(context).disabledColor;
|
||||
}
|
||||
if (isToday) {
|
||||
dayTextColor = Theme.of(context).colorScheme.onPrimary;
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (date.day == 1) FlowyText.medium(monthString),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? Theme.of(context).colorScheme.primary : null,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
width: isToday ? 26 : null,
|
||||
height: isToday ? 26 : null,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
child: Center(
|
||||
child: FlowyText.medium(
|
||||
dayString,
|
||||
color: dayTextColor,
|
||||
return SizedBox(
|
||||
height: 18,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (date.day == 1)
|
||||
FlowyText.medium(
|
||||
monthString,
|
||||
fontSize: 11,
|
||||
color: monthTextColor,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? Theme.of(context).colorScheme.primary : null,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
width: isToday ? 18 : null,
|
||||
height: isToday ? 18 : null,
|
||||
// padding: GridSize.typeOptionContentInsets,
|
||||
child: Center(
|
||||
child: FlowyText.medium(
|
||||
dayString,
|
||||
fontSize: 11,
|
||||
color: dayTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -246,20 +286,26 @@ class _EventList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final editingEvent = context.watch<CalendarBloc>().state.editingEvent;
|
||||
return Flexible(
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
scrollbars: true,
|
||||
),
|
||||
child: ListView.separated(
|
||||
itemBuilder: (BuildContext context, int index) => EventCard(
|
||||
event: events[index],
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
constraints: constraints,
|
||||
),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final autoEdit =
|
||||
editingEvent?.event?.eventId == events[index].eventId;
|
||||
return EventCard(
|
||||
event: events[index],
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
constraints: constraints,
|
||||
autoEdit: autoEdit,
|
||||
);
|
||||
},
|
||||
itemCount: events.length,
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
|
||||
padding: const EdgeInsets.fromLTRB(4.0, 0, 4.0, 4.0),
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
|
||||
+129
-36
@@ -1,47 +1,73 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/bloc/text_card_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/number_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/url_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../application/calendar_bloc.dart';
|
||||
import 'calendar_page.dart';
|
||||
import 'calendar_event_editor.dart';
|
||||
|
||||
class EventCard extends StatelessWidget {
|
||||
class EventCard extends StatefulWidget {
|
||||
final CalendarDayEvent event;
|
||||
final String viewId;
|
||||
final RowCache rowCache;
|
||||
final BoxConstraints constraints;
|
||||
final bool autoEdit;
|
||||
|
||||
const EventCard({
|
||||
required this.event,
|
||||
required this.viewId,
|
||||
required this.rowCache,
|
||||
required this.constraints,
|
||||
required this.autoEdit,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EventCard> createState() => _EventCardState();
|
||||
}
|
||||
|
||||
class _EventCardState extends State<EventCard> {
|
||||
late final PopoverController _popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_popoverController = PopoverController();
|
||||
if (widget.autoEdit) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_popoverController.show();
|
||||
context
|
||||
.read<CalendarBloc>()
|
||||
.add(const CalendarEvent.newEventPopupDisplayed());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rowInfo = rowCache.getRow(event.eventId);
|
||||
final rowInfo = widget.rowCache.getRow(widget.event.eventId);
|
||||
if (rowInfo == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final styles = <FieldType, CardCellStyle>{
|
||||
FieldType.Number: NumberCardCellStyle(10),
|
||||
FieldType.URL: URLCardCellStyle(10),
|
||||
};
|
||||
final cellBuilder = CardCellBuilder<CalendarDayEvent>(
|
||||
rowCache.cellCache,
|
||||
widget.rowCache.cellCache,
|
||||
styles: styles,
|
||||
);
|
||||
final renderHook = _calendarEventCardRenderHook(context);
|
||||
@@ -49,24 +75,22 @@ class EventCard extends StatelessWidget {
|
||||
final card = RowCard<CalendarDayEvent>(
|
||||
// Add the key here to make sure the card is rebuilt when the cells
|
||||
// in this row are updated.
|
||||
key: ValueKey(event.eventId),
|
||||
rowMeta: rowInfo!.rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
cardData: event,
|
||||
key: ValueKey(widget.event.eventId),
|
||||
rowMeta: rowInfo.rowMeta,
|
||||
viewId: widget.viewId,
|
||||
rowCache: widget.rowCache,
|
||||
cardData: widget.event,
|
||||
isEditing: false,
|
||||
cellBuilder: cellBuilder,
|
||||
openCard: (context) => showEventDetails(
|
||||
context: context,
|
||||
event: event.event,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
),
|
||||
openCard: (_) => _popoverController.show(),
|
||||
styleConfiguration: RowCardStyleConfiguration(
|
||||
showAccessory: false,
|
||||
cellPadding: EdgeInsets.zero,
|
||||
cardPadding: const EdgeInsets.all(6),
|
||||
hoverStyle: HoverStyle(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0x0F1F2329)
|
||||
: const Color(0x0FEFF4FB),
|
||||
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
@@ -76,28 +100,76 @@ class EventCard extends StatelessWidget {
|
||||
);
|
||||
|
||||
final decoration = BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).dividerColor),
|
||||
BorderSide(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0xffd0d3d6)
|
||||
: const Color(0xff59647a),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
spreadRadius: -2,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 2,
|
||||
),
|
||||
BoxShadow(
|
||||
spreadRadius: 0,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 4,
|
||||
),
|
||||
BoxShadow(
|
||||
spreadRadius: 2,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Draggable<CalendarDayEvent>(
|
||||
data: event,
|
||||
data: widget.event,
|
||||
feedback: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth - 16.0,
|
||||
maxWidth: widget.constraints.maxWidth - 8.0,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: decoration.copyWith(
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
child: Opacity(
|
||||
opacity: 0.6,
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: card,
|
||||
),
|
||||
child: card,
|
||||
),
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: card,
|
||||
child: AppFlowyPopover(
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.rightWithCenterAligned,
|
||||
controller: _popoverController,
|
||||
constraints: const BoxConstraints(maxWidth: 360, maxHeight: 348),
|
||||
asBarrier: true,
|
||||
margin: EdgeInsets.zero,
|
||||
offset: const Offset(10.0, 0),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
final settings = context.watch<CalendarBloc>().state.settings.fold(
|
||||
() => null,
|
||||
(layoutSettings) => layoutSettings,
|
||||
);
|
||||
if (settings == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return CalendarEventEditor(
|
||||
rowCache: widget.rowCache,
|
||||
rowMeta: widget.event.event.rowMeta,
|
||||
viewId: widget.viewId,
|
||||
layoutSettings: settings,
|
||||
);
|
||||
},
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: card,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -107,13 +179,10 @@ class EventCard extends StatelessWidget {
|
||||
) {
|
||||
final renderHook = RowCardRenderHook<CalendarDayEvent>();
|
||||
renderHook.addTextCellHook((cellData, eventData, _) {
|
||||
return BlocBuilder<TextCardCellBloc, TextCardCellState>(
|
||||
return BlocBuilder<TextCellBloc, TextCellState>(
|
||||
builder: (context, state) {
|
||||
final isTitle = context
|
||||
.read<TextCardCellBloc>()
|
||||
.cellController
|
||||
.fieldInfo
|
||||
.isPrimary;
|
||||
final isTitle =
|
||||
context.read<TextCellBloc>().cellController.fieldInfo.isPrimary;
|
||||
final text = isTitle && cellData.isEmpty
|
||||
? LocaleKeys.grid_row_titlePlaceholder.tr()
|
||||
: cellData;
|
||||
@@ -127,8 +196,9 @@ class EventCard extends StatelessWidget {
|
||||
child: FlowyText.medium(
|
||||
text,
|
||||
textAlign: TextAlign.left,
|
||||
fontSize: 11,
|
||||
maxLines: null, // Enable multiple lines
|
||||
fontSize: isTitle ? 11 : 10,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -167,6 +237,29 @@ class EventCard extends StatelessWidget {
|
||||
);
|
||||
});
|
||||
|
||||
renderHook.addTimestampCellHook((cellData, cardData, _) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: FlowyText.regular(
|
||||
cellData.dateTime,
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
renderHook.addSelectOptionHook((selectedOptions, cardData, _) {
|
||||
if (selectedOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_event_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class CalendarEventEditor extends StatelessWidget {
|
||||
final RowController rowController;
|
||||
final CalendarLayoutSettingPB layoutSettings;
|
||||
final GridCellBuilder cellBuilder;
|
||||
|
||||
CalendarEventEditor({
|
||||
super.key,
|
||||
required RowCache rowCache,
|
||||
required RowMetaPB rowMeta,
|
||||
required String viewId,
|
||||
required this.layoutSettings,
|
||||
}) : rowController = RowController(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
),
|
||||
cellBuilder = GridCellBuilder(cellCache: rowCache.cellCache);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<CalendarEventEditorBloc>(
|
||||
create: (context) => CalendarEventEditorBloc(
|
||||
rowController: rowController,
|
||||
layoutSettings: layoutSettings,
|
||||
)..add(const CalendarEventEditorEvent.initial()),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
EventEditorControls(rowController: rowController),
|
||||
Flexible(
|
||||
child: EventPropertyList(
|
||||
dateFieldId: layoutSettings.fieldId,
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EventEditorControls extends StatelessWidget {
|
||||
final RowController rowController;
|
||||
const EventEditorControls({
|
||||
super.key,
|
||||
required this.rowController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FlowyIconButton(
|
||||
width: 20,
|
||||
icon: const FlowySvg(FlowySvgs.delete_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
|
||||
onPressed: () => context
|
||||
.read<CalendarEventEditorBloc>()
|
||||
.add(const CalendarEventEditorEvent.delete()),
|
||||
),
|
||||
const HSpace(8.0),
|
||||
FlowyIconButton(
|
||||
width: 20,
|
||||
icon: const FlowySvg(FlowySvgs.full_view_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
|
||||
onPressed: () {
|
||||
PopoverContainer.of(context).close();
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(
|
||||
cellCache: rowController.cellCache,
|
||||
),
|
||||
rowController: rowController,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EventPropertyList extends StatelessWidget {
|
||||
final String dateFieldId;
|
||||
final GridCellBuilder cellBuilder;
|
||||
const EventPropertyList({
|
||||
super.key,
|
||||
required this.dateFieldId,
|
||||
required this.cellBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CalendarEventEditorBloc, CalendarEventEditorState>(
|
||||
builder: (context, state) {
|
||||
final reorderedList = List<DatabaseCellContext>.from(state.cells)
|
||||
..retainWhere((cell) => !cell.fieldInfo.isPrimary);
|
||||
|
||||
final primaryCellContext =
|
||||
state.cells.firstWhereOrNull((cell) => cell.fieldInfo.isPrimary);
|
||||
final dateFieldIndex =
|
||||
reorderedList.indexWhere((cell) => cell.fieldId == dateFieldId);
|
||||
if (primaryCellContext == null || dateFieldIndex == -1) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
reorderedList.insert(0, reorderedList.removeAt(dateFieldIndex));
|
||||
|
||||
final children = <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
|
||||
child: cellBuilder.build(
|
||||
primaryCellContext,
|
||||
style: GridTextCellStyle(
|
||||
cellPadding: EdgeInsets.zero,
|
||||
placeholder: LocaleKeys.calendar_defaultNewCalendarTitle.tr(),
|
||||
textStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontSize: 11),
|
||||
autofocus: true,
|
||||
useRoundedBorder: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
...reorderedList.map(
|
||||
(cell) => PropertyCell(cellContext: cell, cellBuilder: cellBuilder),
|
||||
),
|
||||
];
|
||||
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PropertyCell extends StatefulWidget {
|
||||
final DatabaseCellContext cellContext;
|
||||
final GridCellBuilder cellBuilder;
|
||||
const PropertyCell({
|
||||
required this.cellContext,
|
||||
required this.cellBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _PropertyCellState();
|
||||
}
|
||||
|
||||
class _PropertyCellState extends State<PropertyCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = _customCellStyle(widget.cellContext.fieldType);
|
||||
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
||||
|
||||
final gesture = GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => cell.requestFocus.notify(),
|
||||
child: AccessoryHover(child: cell),
|
||||
);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
constraints: const BoxConstraints(minHeight: 28),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 88,
|
||||
height: 28,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
widget.cellContext.fieldType.icon(),
|
||||
color: Theme.of(context).hintColor,
|
||||
size: const Size.square(14),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
FlowyText.regular(
|
||||
widget.cellContext.fieldInfo.name,
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 11,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(child: gesture),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return GridCheckboxCellStyle(
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
return DateCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
alignment: Alignment.centerLeft,
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return TimestampCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
alignment: Alignment.centerLeft,
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
);
|
||||
case FieldType.Checklist:
|
||||
return ChecklistCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
);
|
||||
case FieldType.Number:
|
||||
return GridNumberCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
case FieldType.RichText:
|
||||
return GridTextCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
);
|
||||
case FieldType.URL:
|
||||
return GridURLCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
accessoryTypes: [
|
||||
GridURLCellAccessoryType.copyURL,
|
||||
GridURLCellAccessoryType.visitURL,
|
||||
],
|
||||
);
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
}
|
||||
+196
-48
@@ -2,11 +2,15 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -119,19 +123,6 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
BlocListener<CalendarBloc, CalendarState>(
|
||||
listenWhen: (p, c) => p.editingEvent != c.editingEvent,
|
||||
listener: (context, state) {
|
||||
if (state.editingEvent != null) {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: state.editingEvent!.event!.event,
|
||||
viewId: widget.view.id,
|
||||
rowCache: _calendarBloc.rowCache,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
BlocListener<CalendarBloc, CalendarState>(
|
||||
// Event create by click the + button or double click on the
|
||||
// calendar
|
||||
@@ -211,40 +202,49 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
}
|
||||
|
||||
Widget _headerNavigatorBuilder(DateTime currentMonth) {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
DateFormat('MMMM y', context.locale.toLanguageTag())
|
||||
.format(currentMonth),
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyIconButton(
|
||||
width: CalendarSize.navigatorButtonWidth,
|
||||
height: CalendarSize.navigatorButtonHeight,
|
||||
icon: const FlowySvg(FlowySvgs.arrow_left_s),
|
||||
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onPressed: () => _calendarState?.currentState?.previousPage(),
|
||||
),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.calendar_navigation_today.tr(),
|
||||
fillColor: Colors.transparent,
|
||||
fontWeight: FontWeight.w500,
|
||||
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onPressed: () =>
|
||||
_calendarState?.currentState?.animateToMonth(DateTime.now()),
|
||||
),
|
||||
FlowyIconButton(
|
||||
width: CalendarSize.navigatorButtonWidth,
|
||||
height: CalendarSize.navigatorButtonHeight,
|
||||
icon: const FlowySvg(FlowySvgs.arrow_right_s),
|
||||
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onPressed: () => _calendarState?.currentState?.nextPage(),
|
||||
),
|
||||
],
|
||||
return SizedBox(
|
||||
height: 24,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
DateFormat('MMMM y', context.locale.toLanguageTag())
|
||||
.format(currentMonth),
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyIconButton(
|
||||
width: CalendarSize.navigatorButtonWidth,
|
||||
height: CalendarSize.navigatorButtonHeight,
|
||||
icon: const FlowySvg(FlowySvgs.arrow_left_s),
|
||||
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onPressed: () => _calendarState?.currentState?.previousPage(),
|
||||
),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.calendar_navigation_today.tr(),
|
||||
fillColor: Colors.transparent,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 10,
|
||||
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onPressed: () =>
|
||||
_calendarState?.currentState?.animateToMonth(DateTime.now()),
|
||||
),
|
||||
FlowyIconButton(
|
||||
width: CalendarSize.navigatorButtonWidth,
|
||||
height: CalendarSize.navigatorButtonHeight,
|
||||
icon: const FlowySvg(FlowySvgs.arrow_right_s),
|
||||
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onPressed: () => _calendarState?.currentState?.nextPage(),
|
||||
),
|
||||
const HSpace(6.0),
|
||||
UnscheduledEventsButton(
|
||||
databaseController: widget.databaseController,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,8 +255,9 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: CalendarSize.daysOfWeekInsets,
|
||||
child: FlowyText.medium(
|
||||
child: FlowyText.regular(
|
||||
weekDayString,
|
||||
fontSize: 9,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
@@ -321,3 +322,150 @@ void showEventDetails({
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class UnscheduledEventsButton extends StatefulWidget {
|
||||
final DatabaseController databaseController;
|
||||
|
||||
const UnscheduledEventsButton({super.key, required this.databaseController});
|
||||
|
||||
@override
|
||||
State<UnscheduledEventsButton> createState() =>
|
||||
_UnscheduledEventsButtonState();
|
||||
}
|
||||
|
||||
class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
|
||||
late final PopoverController _popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_popoverController = PopoverController();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<UnscheduleEventsBloc>(
|
||||
create: (_) =>
|
||||
UnscheduleEventsBloc(databaseController: widget.databaseController)
|
||||
..add(const UnscheduleEventsEvent.initial()),
|
||||
child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
controller: _popoverController,
|
||||
offset: const Offset(0, 8),
|
||||
constraints: const BoxConstraints(maxWidth: 282, maxHeight: 600),
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
side:
|
||||
BorderSide(color: Theme.of(context).dividerColor, width: 1),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onPressed: () {
|
||||
if (state.unscheduleEvents.isNotEmpty) {
|
||||
_popoverController.show();
|
||||
}
|
||||
},
|
||||
child: FlowyText.regular(
|
||||
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
popupBuilder: (context) {
|
||||
return UnscheduleEventsList(
|
||||
viewId: widget.databaseController.viewId,
|
||||
rowCache: widget.databaseController.rowCache,
|
||||
unscheduleEvents: state.unscheduleEvents,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnscheduleEventsList extends StatelessWidget {
|
||||
final String viewId;
|
||||
final RowCache rowCache;
|
||||
final List<CalendarEventPB> unscheduleEvents;
|
||||
const UnscheduleEventsList({
|
||||
required this.viewId,
|
||||
required this.unscheduleEvents,
|
||||
required this.rowCache,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cells = <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.calendar_settings_clickToAdd.tr(),
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
...unscheduleEvents.map(
|
||||
(e) => UnscheduledEventCell(
|
||||
event: e,
|
||||
onPressed: () {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: e,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnscheduledEventCell extends StatelessWidget {
|
||||
final CalendarEventPB event;
|
||||
final VoidCallback onPressed;
|
||||
const UnscheduledEventCell({
|
||||
required this.event,
|
||||
required this.onPressed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 26,
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
text: FlowyText.medium(
|
||||
event.title.isEmpty
|
||||
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
|
||||
: event.title,
|
||||
fontSize: 11,
|
||||
),
|
||||
onTap: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -15,7 +15,7 @@ class CalendarSize {
|
||||
|
||||
static double get scrollBarSize => 8 * scale;
|
||||
static double get navigatorButtonWidth => 20 * scale;
|
||||
static double get navigatorButtonHeight => 25 * scale;
|
||||
static double get navigatorButtonHeight => 24 * scale;
|
||||
static EdgeInsets get daysOfWeekInsets =>
|
||||
EdgeInsets.symmetric(vertical: 10.0 * scale);
|
||||
EdgeInsets.only(top: 12.0 * scale, bottom: 5.0 * scale);
|
||||
}
|
||||
-159
@@ -1,18 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class CalendarSettingBar extends StatelessWidget {
|
||||
final DatabaseController databaseController;
|
||||
@@ -28,8 +16,6 @@ class CalendarSettingBar extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
UnscheduleEventsButton(databaseController: databaseController),
|
||||
const HSpace(2),
|
||||
SettingButton(
|
||||
databaseController: databaseController,
|
||||
),
|
||||
@@ -38,148 +24,3 @@ class CalendarSettingBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnscheduleEventsButton extends StatefulWidget {
|
||||
final DatabaseController databaseController;
|
||||
const UnscheduleEventsButton({
|
||||
required this.databaseController,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<UnscheduleEventsButton> createState() => _UnscheduleEventsButtonState();
|
||||
}
|
||||
|
||||
class _UnscheduleEventsButtonState extends State<UnscheduleEventsButton> {
|
||||
late final PopoverController _popoverController;
|
||||
late final UnscheduleEventsBloc _bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = UnscheduleEventsBloc(databaseController: widget.databaseController)
|
||||
..add(const UnscheduleEventsEvent.initial());
|
||||
_popoverController = PopoverController();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
controller: _popoverController,
|
||||
offset: const Offset(0, 8),
|
||||
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
|
||||
child: BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.unscheduleEvents.length !=
|
||||
current.unscheduleEvents.length,
|
||||
builder: (context, state) {
|
||||
return FlowyTextButton(
|
||||
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
|
||||
fontSize: FontSizes.s11,
|
||||
fontColor: AFThemeExtension.of(context).textColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: GridSize.toolbarSettingButtonInsets,
|
||||
radius: Corners.s4Border,
|
||||
onPressed: () => _popoverController.show(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
popupBuilder: (context) {
|
||||
return UnscheduleEventsList(
|
||||
viewId: _bloc.viewId,
|
||||
rowCache: _bloc.rowCache,
|
||||
controller: _popoverController,
|
||||
unscheduleEvents: _bloc.state.unscheduleEvents,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnscheduleEventsList extends StatelessWidget {
|
||||
final String viewId;
|
||||
final RowCache rowCache;
|
||||
final PopoverController controller;
|
||||
final List<CalendarEventPB> unscheduleEvents;
|
||||
const UnscheduleEventsList({
|
||||
required this.viewId,
|
||||
required this.controller,
|
||||
required this.unscheduleEvents,
|
||||
required this.rowCache,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cells = <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.calendar_settings_clickToAdd.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const VSpace(6),
|
||||
...unscheduleEvents.map(
|
||||
(e) => UnscheduledEventCell(
|
||||
event: e,
|
||||
onPressed: () {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: e,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnscheduledEventCell extends StatelessWidget {
|
||||
final CalendarEventPB event;
|
||||
final VoidCallback onPressed;
|
||||
const UnscheduledEventCell({
|
||||
required this.event,
|
||||
required this.onPressed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
event.title.isEmpty
|
||||
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
|
||||
: event.title,
|
||||
),
|
||||
onTap: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+11
-7
@@ -2,6 +2,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@@ -16,18 +17,25 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
|
||||
GridHeaderBloc({
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
}) : super(GridHeaderState.initial(fieldController.fieldInfos)) {
|
||||
}) : super(GridHeaderState.initial()) {
|
||||
on<GridHeaderEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_InitialHeader value) async {
|
||||
_startListening();
|
||||
add(
|
||||
GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
|
||||
);
|
||||
},
|
||||
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
fields: value.fields
|
||||
.where((element) => element.field.visibility)
|
||||
.where(
|
||||
(element) =>
|
||||
element.visibility != null &&
|
||||
element.visibility != FieldVisibility.AlwaysHidden,
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
@@ -83,9 +91,5 @@ class GridHeaderState with _$GridHeaderState {
|
||||
const factory GridHeaderState({required List<FieldInfo> fields}) =
|
||||
_GridHeaderState;
|
||||
|
||||
factory GridHeaderState.initial(List<FieldInfo> fields) {
|
||||
// final List<FieldPB> newFields = List.from(fields);
|
||||
// newFields.retainWhere((field) => field.visibility);
|
||||
return GridHeaderState(fields: fields);
|
||||
}
|
||||
factory GridHeaderState.initial() => const GridHeaderState(fields: []);
|
||||
}
|
||||
+14
-10
@@ -39,6 +39,8 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
||||
_rowBackendSvc.createRowAfterRow(rowId);
|
||||
},
|
||||
didReceiveCells: (cellByFieldId, reason) async {
|
||||
cellByFieldId
|
||||
.removeWhere((_, cellContext) => !cellContext.isVisible());
|
||||
final cells = cellByFieldId.values
|
||||
.map((e) => GridCellEquatable(e.fieldInfo))
|
||||
.toList();
|
||||
@@ -106,16 +108,18 @@ class RowState with _$RowState {
|
||||
|
||||
factory RowState.initial(
|
||||
CellContextByFieldId cellByFieldId,
|
||||
) =>
|
||||
RowState(
|
||||
cellByFieldId: cellByFieldId,
|
||||
cells: UnmodifiableListView(
|
||||
cellByFieldId.values
|
||||
.map((e) => GridCellEquatable(e.fieldInfo))
|
||||
.toList(),
|
||||
),
|
||||
rowSource: const RowSourece.disk(),
|
||||
);
|
||||
) {
|
||||
cellByFieldId.removeWhere((_, cellContext) => !cellContext.isVisible());
|
||||
return RowState(
|
||||
cellByFieldId: cellByFieldId,
|
||||
cells: UnmodifiableListView(
|
||||
cellByFieldId.values
|
||||
.map((e) => GridCellEquatable(e.fieldInfo))
|
||||
.toList(),
|
||||
),
|
||||
rowSource: const RowSourece.disk(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GridCellEquatable extends Equatable {
|
||||
|
||||
+151
-44
@@ -1,53 +1,85 @@
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import '../../../application/cell/cell_service.dart';
|
||||
import '../../../application/field/field_service.dart';
|
||||
import '../../../application/row/row_controller.dart';
|
||||
|
||||
part 'row_detail_bloc.freezed.dart';
|
||||
|
||||
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
final RowBackendService rowService;
|
||||
final RowController dataController;
|
||||
final RowController rowController;
|
||||
|
||||
RowDetailBloc({
|
||||
required this.dataController,
|
||||
}) : rowService = RowBackendService(viewId: dataController.viewId),
|
||||
super(RowDetailState.initial()) {
|
||||
required this.rowController,
|
||||
}) : super(RowDetailState.initial(rowController.loadData())) {
|
||||
on<RowDetailEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
await _startListening();
|
||||
final cells = dataController.loadData();
|
||||
if (!isClosed) {
|
||||
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
|
||||
}
|
||||
},
|
||||
didReceiveCellDatas: (cells) {
|
||||
emit(state.copyWith(cells: cells));
|
||||
didReceiveCellDatas: (visibleCells, allCells, numHiddenFields) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
visibleCells: visibleCells,
|
||||
allCells: allCells,
|
||||
numHiddenFields: numHiddenFields,
|
||||
),
|
||||
);
|
||||
},
|
||||
deleteField: (fieldId) {
|
||||
_fieldBackendService(fieldId).deleteField();
|
||||
final fieldService = FieldBackendService(
|
||||
viewId: rowController.viewId,
|
||||
fieldId: fieldId,
|
||||
);
|
||||
fieldService.deleteField();
|
||||
},
|
||||
hideField: (fieldId) async {
|
||||
final result = await _fieldBackendService(fieldId).updateField(
|
||||
visibility: false,
|
||||
toggleFieldVisibility: (fieldId) async {
|
||||
final fieldInfo = state.allCells
|
||||
.where((cellContext) => cellContext.fieldId == fieldId)
|
||||
.first
|
||||
.fieldInfo;
|
||||
final fieldVisibility =
|
||||
fieldInfo.visibility == FieldVisibility.AlwaysShown
|
||||
? FieldVisibility.AlwaysHidden
|
||||
: FieldVisibility.AlwaysShown;
|
||||
final result =
|
||||
await FieldSettingsBackendService(viewId: rowController.viewId)
|
||||
.updateFieldSettings(
|
||||
fieldId: fieldId,
|
||||
fieldVisibility: fieldVisibility,
|
||||
);
|
||||
result.fold(
|
||||
(l) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
deleteRow: (rowId) async {
|
||||
await rowService.deleteRow(rowId);
|
||||
reorderField:
|
||||
(reorderedFieldId, targetFieldId, fromIndex, toIndex) async {
|
||||
await _reorderField(
|
||||
reorderedFieldId,
|
||||
targetFieldId,
|
||||
fromIndex,
|
||||
toIndex,
|
||||
emit,
|
||||
);
|
||||
},
|
||||
duplicateRow: (String rowId, String? groupId) async {
|
||||
await rowService.duplicateRow(
|
||||
rowId: rowId,
|
||||
groupId: groupId,
|
||||
toggleHiddenFieldVisibility: () {
|
||||
final showHiddenFields = !state.showHiddenFields;
|
||||
final visibleCells = List<DatabaseCellContext>.from(state.allCells);
|
||||
visibleCells.retainWhere(
|
||||
(cellContext) =>
|
||||
!cellContext.fieldInfo.isPrimary &&
|
||||
cellContext.isVisible(showHiddenFields: showHiddenFields),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
showHiddenFields: showHiddenFields,
|
||||
visibleCells: visibleCells,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -57,25 +89,68 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
dataController.dispose();
|
||||
rowController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _startListening() async {
|
||||
dataController.addListener(
|
||||
onRowChanged: (cells, reason) {
|
||||
if (!isClosed) {
|
||||
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
|
||||
rowController.addListener(
|
||||
onRowChanged: (cellMap, reason) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
final allCells = cellMap.values.toList();
|
||||
int numHiddenFields = 0;
|
||||
final visibleCells = <DatabaseCellContext>[];
|
||||
for (final cell in allCells) {
|
||||
final isPrimary = cell.fieldInfo.isPrimary;
|
||||
|
||||
if (cell.isVisible(showHiddenFields: state.showHiddenFields) &&
|
||||
!isPrimary) {
|
||||
visibleCells.add(cell);
|
||||
}
|
||||
|
||||
if (!cell.isVisible() && !isPrimary) {
|
||||
numHiddenFields++;
|
||||
}
|
||||
}
|
||||
|
||||
add(
|
||||
RowDetailEvent.didReceiveCellDatas(
|
||||
visibleCells,
|
||||
allCells,
|
||||
numHiddenFields,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
FieldBackendService _fieldBackendService(String fieldId) {
|
||||
return FieldBackendService(
|
||||
viewId: dataController.viewId,
|
||||
fieldId: fieldId,
|
||||
Future<void> _reorderField(
|
||||
String reorderedFieldId,
|
||||
String targetFieldId,
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
Emitter<RowDetailState> emit,
|
||||
) async {
|
||||
final cells = List<DatabaseCellContext>.from(state.visibleCells);
|
||||
cells.insert(toIndex, cells.removeAt(fromIndex));
|
||||
emit(state.copyWith(visibleCells: cells));
|
||||
|
||||
final fromIndexInAllFields =
|
||||
state.allCells.indexWhere((cell) => cell.fieldId == reorderedFieldId);
|
||||
final toIndexInAllFields =
|
||||
state.allCells.indexWhere((cell) => cell.fieldId == targetFieldId);
|
||||
|
||||
final fieldService = FieldBackendService(
|
||||
viewId: rowController.viewId,
|
||||
fieldId: reorderedFieldId,
|
||||
);
|
||||
final result = await fieldService.moveField(
|
||||
fromIndexInAllFields,
|
||||
toIndexInAllFields,
|
||||
);
|
||||
result.fold((l) {}, (err) => Log.error(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,22 +158,54 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
class RowDetailEvent with _$RowDetailEvent {
|
||||
const factory RowDetailEvent.initial() = _Initial;
|
||||
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
|
||||
const factory RowDetailEvent.hideField(String fieldId) = _HideField;
|
||||
const factory RowDetailEvent.deleteRow(String rowId) = _DeleteRow;
|
||||
const factory RowDetailEvent.duplicateRow(String rowId, String? groupId) =
|
||||
_DuplicateRow;
|
||||
const factory RowDetailEvent.toggleFieldVisibility(String fieldId) =
|
||||
_ToggleFieldVisibility;
|
||||
const factory RowDetailEvent.reorderField(
|
||||
String reorderFieldID,
|
||||
String targetFieldID,
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
) = _ReorderField;
|
||||
const factory RowDetailEvent.toggleHiddenFieldVisibility() =
|
||||
_ToggleHiddenFieldVisibility;
|
||||
const factory RowDetailEvent.didReceiveCellDatas(
|
||||
List<DatabaseCellContext> gridCells,
|
||||
List<DatabaseCellContext> visibleCells,
|
||||
List<DatabaseCellContext> allCells,
|
||||
int numHiddenFields,
|
||||
) = _DidReceiveCellDatas;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RowDetailState with _$RowDetailState {
|
||||
const factory RowDetailState({
|
||||
required List<DatabaseCellContext> cells,
|
||||
required List<DatabaseCellContext> visibleCells,
|
||||
required List<DatabaseCellContext> allCells,
|
||||
required bool showHiddenFields,
|
||||
required int numHiddenFields,
|
||||
}) = _RowDetailState;
|
||||
|
||||
factory RowDetailState.initial() => RowDetailState(
|
||||
cells: List.empty(),
|
||||
);
|
||||
factory RowDetailState.initial(CellContextByFieldId cellByFieldId) {
|
||||
final allCells = cellByFieldId.values.toList();
|
||||
int numHiddenFields = 0;
|
||||
final visibleCells = <DatabaseCellContext>[];
|
||||
for (final cell in allCells) {
|
||||
final isVisible = cell.isVisible();
|
||||
final isPrimary = cell.fieldInfo.isPrimary;
|
||||
|
||||
if (isVisible && !isPrimary) {
|
||||
visibleCells.add(cell);
|
||||
}
|
||||
|
||||
if (!isVisible && !isPrimary) {
|
||||
numHiddenFields++;
|
||||
}
|
||||
}
|
||||
|
||||
return RowDetailState(
|
||||
visibleCells: visibleCells,
|
||||
allCells: allCells,
|
||||
showHiddenFields: false,
|
||||
numHiddenFields: numHiddenFields,
|
||||
);
|
||||
}
|
||||
}
|
||||
+50
-66
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
|
||||
@@ -7,10 +5,10 @@ import 'package:appflowy/plugins/database_view/tar_bar/setting_menu.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -257,59 +255,38 @@ class _GridRows extends StatelessWidget {
|
||||
GridState state,
|
||||
List<RowInfo> rowInfos,
|
||||
) {
|
||||
if (Platform.isWindows) {
|
||||
// Workaround: On Windows, the focusing of the text cell is not working
|
||||
// properly when the list is reorderable. So using the ListView instead.
|
||||
return ListView.builder(
|
||||
controller: scrollController.verticalController,
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: false,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return ReorderableListView.builder(
|
||||
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
|
||||
/// This is a workaround related to
|
||||
/// https://github.com/flutter/flutter/issues/25652
|
||||
cacheExtent: 5000,
|
||||
scrollController: scrollController.verticalController,
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.white.withOpacity(.1),
|
||||
child: Opacity(opacity: .5, child: child),
|
||||
),
|
||||
onReorder: (fromIndex, newIndex) {
|
||||
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
|
||||
if (fromIndex == toIndex) {
|
||||
return;
|
||||
}
|
||||
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
|
||||
},
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: state.reorderable,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
);
|
||||
}
|
||||
return ReorderableListView.builder(
|
||||
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
|
||||
/// This is a workaround related to
|
||||
/// https://github.com/flutter/flutter/issues/25652
|
||||
cacheExtent: 5000,
|
||||
scrollController: scrollController.verticalController,
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.white.withOpacity(.1),
|
||||
child: Opacity(opacity: .5, child: child),
|
||||
),
|
||||
onReorder: (fromIndex, newIndex) {
|
||||
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
|
||||
if (fromIndex == toIndex) {
|
||||
return;
|
||||
}
|
||||
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
|
||||
},
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: state.reorderable,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderRow(
|
||||
@@ -433,14 +410,21 @@ class _GridFooter extends StatelessWidget {
|
||||
builder: (context, rowCount) {
|
||||
return Padding(
|
||||
padding: GridSize.contentInsets,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
rowCountString(rowCount),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: rowCountString(),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' $rowCount',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: AFThemeExtension.of(context).gridRowCountColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -448,6 +432,6 @@ class _GridFooter extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String rowCountString(int count) {
|
||||
return '${LocaleKeys.grid_row_count.tr()} : $count';
|
||||
String rowCountString() {
|
||||
return '${LocaleKeys.grid_row_count.tr()} :';
|
||||
}
|
||||
+2
-2
@@ -114,7 +114,7 @@ class _GridCreateFilterListState extends State<GridCreateFilterList> {
|
||||
class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
_FilterTextFieldDelegate();
|
||||
|
||||
double fixHeight = 46;
|
||||
double fixHeight = 36;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
@@ -123,7 +123,7 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
bool overlapsContent,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
height: fixHeight,
|
||||
child: FlowyTextField(
|
||||
hintText: LocaleKeys.grid_settings_filterBy.tr(),
|
||||
|
||||
+4
@@ -22,6 +22,10 @@ class FilterInfo {
|
||||
);
|
||||
}
|
||||
|
||||
String get filterId => filter.id;
|
||||
|
||||
String get fieldId => filter.fieldId;
|
||||
|
||||
DateFilterPB? dateFilter() {
|
||||
if (![
|
||||
FieldType.DateTime,
|
||||
|
||||
-1
@@ -105,7 +105,6 @@ class _AddFilterButtonState extends State<AddFilterButton> {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
constraints: BoxConstraints.loose(const Size(200, 300)),
|
||||
margin: const EdgeInsets.all(6),
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
child: child,
|
||||
popupBuilder: (BuildContext context) {
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class GridRowBottomBar extends StatelessWidget {
|
||||
return Container(
|
||||
padding: GridSize.footerContentInsets,
|
||||
height: GridSize.footerHeight,
|
||||
margin: const EdgeInsets.only(bottom: 200),
|
||||
margin: const EdgeInsets.only(bottom: 200, top: 8),
|
||||
child: const GridAddRowButton(),
|
||||
);
|
||||
}
|
||||
|
||||
+15
-11
@@ -38,7 +38,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
return FieldCellBloc(cellContext: widget.cellContext);
|
||||
return FieldCellBloc(fieldContext: widget.cellContext);
|
||||
},
|
||||
child: BlocBuilder<FieldCellBloc, FieldCellState>(
|
||||
builder: (context, state) {
|
||||
@@ -54,7 +54,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
|
||||
);
|
||||
},
|
||||
child: FieldCellButton(
|
||||
field: widget.cellContext.field,
|
||||
field: widget.cellContext.fieldInfo.field,
|
||||
onTap: () => popoverController.show(),
|
||||
),
|
||||
);
|
||||
@@ -125,10 +125,15 @@ class _DragToExpandLine extends StatelessWidget {
|
||||
onTap: () {},
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragStart: (details) {
|
||||
context
|
||||
.read<FieldCellBloc>()
|
||||
.add(const FieldCellEvent.onResizeStart());
|
||||
},
|
||||
onHorizontalDragUpdate: (value) {
|
||||
context
|
||||
.read<FieldCellBloc>()
|
||||
.add(FieldCellEvent.startUpdateWidth(value.delta.dx));
|
||||
.add(FieldCellEvent.startUpdateWidth(value.localPosition.dx));
|
||||
},
|
||||
onHorizontalDragEnd: (end) {
|
||||
context
|
||||
@@ -154,34 +159,33 @@ class FieldCellButton extends StatelessWidget {
|
||||
final FieldPB field;
|
||||
final int? maxLines;
|
||||
final BorderRadius? radius;
|
||||
final EdgeInsets? margin;
|
||||
const FieldCellButton({
|
||||
required this.field,
|
||||
required this.onTap,
|
||||
this.maxLines = 1,
|
||||
this.radius = BorderRadius.zero,
|
||||
this.margin,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Using this technique to have proper text ellipsis
|
||||
// https://github.com/flutter/flutter/issues/18761#issuecomment-812390920
|
||||
final text = Characters(field.name)
|
||||
.replaceAll(Characters(''), Characters('\u{200B}'))
|
||||
.toString();
|
||||
return FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).greyHover,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onTap: onTap,
|
||||
leftIcon: FlowySvg(
|
||||
field.fieldType.icon(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
radius: radius,
|
||||
text: FlowyText.medium(
|
||||
text,
|
||||
field.name,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
margin: GridSize.cellContentInsets,
|
||||
margin: margin ?? GridSize.cellContentInsets,
|
||||
);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 964 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user