Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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.
|
||||
@@ -88,7 +88,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
|
||||
|
||||
@@ -85,7 +85,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:
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
# Release Notes
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,7 +25,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.3.2"
|
||||
CURRENT_APP_VERSION = "0.3.5"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -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).
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,6 +113,10 @@ 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
|
||||
@@ -126,12 +130,23 @@ void main() {
|
||||
|
||||
// Delete an event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
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 {
|
||||
@@ -145,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);
|
||||
@@ -157,13 +172,12 @@ void main() {
|
||||
|
||||
// Delete the event
|
||||
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
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);
|
||||
@@ -182,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();
|
||||
|
||||
@@ -456,9 +456,6 @@ void main() {
|
||||
// assert that the checklist editor is shown
|
||||
tester.assertChecklistEditorVisible(visible: true);
|
||||
|
||||
// assert that new task editor is shown
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: true);
|
||||
|
||||
// create a new task with enter
|
||||
await tester.createNewChecklistTask(name: "task 0", enter: true);
|
||||
|
||||
@@ -481,7 +478,6 @@ void main() {
|
||||
|
||||
// dismiss new task editor
|
||||
await tester.dismissCellEditor();
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: false);
|
||||
|
||||
// dismiss checklist cell editor
|
||||
await tester.dismissCellEditor();
|
||||
@@ -489,11 +485,8 @@ void main() {
|
||||
// assert that progress bar is shown in grid at 0%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
|
||||
|
||||
// start editing the first checklist cell again, click on new task button
|
||||
// start editing the first checklist cell again
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: false);
|
||||
await tester.tapChecklistNewTaskButton();
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: true);
|
||||
|
||||
// create another task with the create button
|
||||
await tester.createNewChecklistTask(name: "task 2", button: true);
|
||||
@@ -540,9 +533,6 @@ void main() {
|
||||
// delete the remaining task
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
|
||||
// assert that the new task editor is shown
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: true);
|
||||
|
||||
// dismiss the cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ void main() {
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// Assert that the first field in the row details page is the select
|
||||
// option tyoe
|
||||
// option type
|
||||
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
|
||||
|
||||
// Reorder first field in list
|
||||
@@ -168,6 +168,54 @@ void main() {
|
||||
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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5,8 +5,11 @@ 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;
|
||||
@@ -29,4 +32,7 @@ void startTesting() {
|
||||
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/appearance.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 {
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
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: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);
|
||||
|
||||
await tester.tapButton(find.byType(ImagePlaceholder));
|
||||
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);
|
||||
|
||||
await tester.tapButton(find.byType(ImagePlaceholder));
|
||||
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);
|
||||
|
||||
await tester.tapButton(find.byType(ImagePlaceholder));
|
||||
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');
|
||||
@@ -22,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.
|
||||
///
|
||||
@@ -67,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",
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database_view/board/presentation/board_page.dar
|
||||
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';
|
||||
@@ -25,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';
|
||||
@@ -45,7 +47,6 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart';
|
||||
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';
|
||||
@@ -53,7 +54,8 @@ 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';
|
||||
@@ -330,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,
|
||||
@@ -483,15 +495,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
void assertNewCheckListTaskEditorVisible({required bool visible}) {
|
||||
final editor = find.byType(NewTaskItem);
|
||||
if (visible) {
|
||||
expect(editor, findsOneWidget);
|
||||
} else {
|
||||
expect(editor, findsNothing);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createNewChecklistTask({
|
||||
required String name,
|
||||
enter = false,
|
||||
@@ -504,10 +507,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
);
|
||||
|
||||
await enterText(textField, name);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
await pumpAndSettle();
|
||||
if (enter) {
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
await pumpAndSettle();
|
||||
} else {
|
||||
await tapButton(
|
||||
find.descendant(
|
||||
@@ -544,11 +547,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
await enterText(textField, name);
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> tapChecklistNewTaskButton() async {
|
||||
await tapButton(find.byType(ChecklistNewTaskButton));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> checkChecklistTask({required int index}) async {
|
||||
@@ -675,6 +674,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
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();
|
||||
@@ -748,7 +772,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> tapHidePropertyButtonInFieldEditor() async {
|
||||
final button = find.byType(HideFieldButton);
|
||||
final button = find.byType(FieldVisibilityToggleButton);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
@@ -901,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);
|
||||
}
|
||||
|
||||
@@ -1294,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);
|
||||
|
||||
@@ -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/appearance.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
|
||||
@@ -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'));
|
||||
});
|
||||
}
|
||||
}
|
||||
+82
-2
@@ -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',
|
||||
@@ -31,11 +62,60 @@ abstract class Env {
|
||||
static final String supabaseAnonKey = _Env.supabaseAnonKey;
|
||||
}
|
||||
|
||||
bool get isSupabaseEnabled {
|
||||
bool get isCloudEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return Env.supabaseUrl.isNotEmpty && Env.supabaseAnonKey.isNotEmpty;
|
||||
return currentCloudType().isEnabled;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isSupabaseEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
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;
|
||||
}
|
||||
+14
-2
@@ -23,7 +23,20 @@ ThemeData getMobileThemeData() {
|
||||
primaryColor: mobileColorTheme.primary, //primary 100
|
||||
primaryColorLight: const Color(0xFF57B5F8), //primary 80
|
||||
dividerColor: mobileColorTheme.outline, //caption
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
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(
|
||||
@@ -72,7 +85,6 @@ ThemeData getMobileThemeData() {
|
||||
displayLarge: TextStyle(
|
||||
color: Color(0xFF57B5F8),
|
||||
fontSize: 32,
|
||||
fontFamily: 'Poppins',
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.20,
|
||||
letterSpacing: 0.16,
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.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';
|
||||
|
||||
class MobileHomeScreen extends StatelessWidget {
|
||||
const MobileHomeScreen({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
required this.workspaceSetting,
|
||||
});
|
||||
|
||||
static const routeName = "/MobileHomeScreen";
|
||||
final UserProfilePB userProfile;
|
||||
final WorkspaceSettingPB workspaceSetting;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("MobileHomeScreen"),
|
||||
),
|
||||
// TODO(yijing): implement home page later
|
||||
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,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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+10
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -73,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);
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -20,11 +20,15 @@ final class DateCellBackendService {
|
||||
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;
|
||||
..includeTime = includeTime
|
||||
..isRange = isRange;
|
||||
|
||||
if (date != null) {
|
||||
final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000;
|
||||
@@ -33,6 +37,13 @@ final class DateCellBackendService {
|
||||
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();
|
||||
}
|
||||
|
||||
+3
-3
@@ -15,16 +15,16 @@ class FieldActionSheetBloc
|
||||
final FieldSettingsBackendService fieldSettingsService;
|
||||
|
||||
FieldActionSheetBloc({required FieldContext fieldCellContext})
|
||||
: fieldId = fieldCellContext.field.id,
|
||||
: 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>(
|
||||
|
||||
+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,
|
||||
);
|
||||
}
|
||||
+37
-32
@@ -367,28 +367,13 @@ class FieldController {
|
||||
|
||||
/// Listen for field changes in the backend.
|
||||
void _listenOnFieldChanges() {
|
||||
void deleteFields(List<FieldIdPB> deletedFields) {
|
||||
if (deletedFields.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final Map<String, FieldIdPB> deletedFieldMap = {
|
||||
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
|
||||
};
|
||||
|
||||
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
}
|
||||
|
||||
Future<FieldInfo> attachFieldSettings(FieldInfo fieldInfo) async {
|
||||
return _fieldSettingsBackendSvc
|
||||
.getFieldSettings(fieldInfo.id)
|
||||
.then((result) {
|
||||
final fieldSettings = result.fold(
|
||||
(fieldSettings) => fieldSettings,
|
||||
(err) {
|
||||
return null;
|
||||
},
|
||||
(err) => null,
|
||||
);
|
||||
if (fieldSettings == null) {
|
||||
return fieldInfo;
|
||||
@@ -400,9 +385,25 @@ class FieldController {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> insertFields(List<IndexFieldPB> insertedFields) async {
|
||||
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;
|
||||
return fieldInfos;
|
||||
}
|
||||
final List<FieldInfo> newFieldInfos = fieldInfos;
|
||||
for (final indexField in insertedFields) {
|
||||
@@ -414,32 +415,32 @@ class FieldController {
|
||||
newFieldInfos.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
_fieldNotifier.fieldInfos = newFieldInfos;
|
||||
return newFieldInfos;
|
||||
}
|
||||
|
||||
Future<List<FieldInfo>> updateFields(List<FieldPB> updatedFieldPBs) async {
|
||||
Future<(List<FieldInfo>, List<FieldInfo>)> updateFields(
|
||||
List<FieldPB> updatedFieldPBs,
|
||||
List<FieldInfo> fieldInfos,
|
||||
) async {
|
||||
if (updatedFieldPBs.isEmpty) {
|
||||
return [];
|
||||
return (<FieldInfo>[], fieldInfos);
|
||||
}
|
||||
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final List<FieldInfo> newFieldInfo = fieldInfos;
|
||||
final List<FieldInfo> updatedFields = [];
|
||||
for (final updatedFieldPB in updatedFieldPBs) {
|
||||
final index =
|
||||
newFields.indexWhere((field) => field.id == updatedFieldPB.id);
|
||||
newFieldInfo.indexWhere((field) => field.id == updatedFieldPB.id);
|
||||
if (index != -1) {
|
||||
newFields.removeAt(index);
|
||||
newFieldInfo.removeAt(index);
|
||||
final initial = FieldInfo.initial(updatedFieldPB);
|
||||
final fieldInfo = await attachFieldSettings(initial);
|
||||
newFields.insert(index, fieldInfo);
|
||||
newFieldInfo.insert(index, fieldInfo);
|
||||
updatedFields.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedFields.isNotEmpty) {
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
}
|
||||
return updatedFields;
|
||||
return (updatedFields, newFieldInfo);
|
||||
}
|
||||
|
||||
// Listen on field's changes
|
||||
@@ -450,10 +451,14 @@ class FieldController {
|
||||
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 = await updateFields(changeset.updatedFields);
|
||||
_fieldNotifier.fieldInfos = fieldInfos;
|
||||
for (final listener in _updatedFieldCallbacks.values) {
|
||||
listener(updatedFields);
|
||||
}
|
||||
|
||||
+2
-1
@@ -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';
|
||||
@@ -108,6 +109,6 @@ class FieldBackendService {
|
||||
class FieldContext with _$FieldContext {
|
||||
const factory FieldContext({
|
||||
required String viewId,
|
||||
required FieldPB field,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _FieldCellContext;
|
||||
}
|
||||
+20
-2
@@ -20,7 +20,14 @@ class FieldSettingsBackendService {
|
||||
|
||||
return DatabaseEventGetFieldSettings(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(fieldSettings) => left(fieldSettings.items.first),
|
||||
(repeatedFieldSettings) {
|
||||
final fieldSetting = repeatedFieldSettings.items.first;
|
||||
if (!fieldSetting.hasVisibility()) {
|
||||
fieldSetting.visibility = FieldVisibility.AlwaysShown;
|
||||
}
|
||||
|
||||
return left(fieldSetting);
|
||||
},
|
||||
(r) => right(r),
|
||||
);
|
||||
});
|
||||
@@ -31,7 +38,18 @@ class FieldSettingsBackendService {
|
||||
|
||||
return DatabaseEventGetAllFieldSettings(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(fieldSettings) => left(fieldSettings.items),
|
||||
(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),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -246,14 +246,11 @@ class RowCache {
|
||||
// ignore: prefer_collection_literals
|
||||
final cellContextMap = CellContextByFieldId();
|
||||
for (final fieldInfo in _fieldDelegate.fieldInfos) {
|
||||
if (fieldInfo.visibility != null &&
|
||||
fieldInfo.visibility != FieldVisibility.AlwaysHidden) {
|
||||
cellContextMap[fieldInfo.id] = DatabaseCellContext(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
fieldInfo: fieldInfo,
|
||||
);
|
||||
}
|
||||
cellContextMap[fieldInfo.id] = DatabaseCellContext(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
fieldInfo: fieldInfo,
|
||||
);
|
||||
}
|
||||
return cellContextMap;
|
||||
}
|
||||
|
||||
+4
-2
@@ -382,8 +382,10 @@ Widget? _buildHeaderIcon(GroupData customData) {
|
||||
case FieldType.Checkbox:
|
||||
final group = customData.asCheckboxGroup()!;
|
||||
if (group.isCheck) {
|
||||
widget =
|
||||
const FlowySvg(FlowySvgs.check_filled_s, blendMode: BlendMode.dst,);
|
||||
widget = const FlowySvg(
|
||||
FlowySvgs.check_filled_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
} else {
|
||||
widget = const FlowySvg(FlowySvgs.uncheck_s);
|
||||
}
|
||||
|
||||
+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,
|
||||
|
||||
+106
-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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+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 {
|
||||
|
||||
+124
-47
@@ -14,49 +14,73 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
|
||||
RowDetailBloc({
|
||||
required this.rowController,
|
||||
}) : super(RowDetailState.initial()) {
|
||||
}) : super(RowDetailState.initial(rowController.loadData())) {
|
||||
on<RowDetailEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
await _startListening();
|
||||
final cells = rowController.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();
|
||||
},
|
||||
showField: (fieldId) async {
|
||||
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.AlwaysShown,
|
||||
fieldVisibility: fieldVisibility,
|
||||
);
|
||||
result.fold(
|
||||
(l) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
hideField: (fieldId) async {
|
||||
final result =
|
||||
await FieldSettingsBackendService(viewId: rowController.viewId)
|
||||
.updateFieldSettings(
|
||||
fieldId: fieldId,
|
||||
fieldVisibility: FieldVisibility.AlwaysHidden,
|
||||
);
|
||||
result.fold(
|
||||
(l) {},
|
||||
(err) => Log.error(err),
|
||||
reorderField:
|
||||
(reorderedFieldId, targetFieldId, fromIndex, toIndex) async {
|
||||
await _reorderField(
|
||||
reorderedFieldId,
|
||||
targetFieldId,
|
||||
fromIndex,
|
||||
toIndex,
|
||||
emit,
|
||||
);
|
||||
},
|
||||
reorderField: (fieldId, fromIndex, toIndex) async {
|
||||
await _reorderField(fieldId, fromIndex, toIndex, emit);
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -71,36 +95,60 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
|
||||
Future<void> _startListening() async {
|
||||
rowController.addListener(
|
||||
onRowChanged: (cells, reason) {
|
||||
if (!isClosed) {
|
||||
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
|
||||
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: rowController.viewId,
|
||||
fieldId: fieldId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _reorderField(
|
||||
String fieldId,
|
||||
String reorderedFieldId,
|
||||
String targetFieldId,
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
Emitter<RowDetailState> emit,
|
||||
) async {
|
||||
final cells = List<DatabaseCellContext>.from(state.cells);
|
||||
final cells = List<DatabaseCellContext>.from(state.visibleCells);
|
||||
cells.insert(toIndex, cells.removeAt(fromIndex));
|
||||
emit(state.copyWith(cells: cells));
|
||||
emit(state.copyWith(visibleCells: cells));
|
||||
|
||||
final fieldService =
|
||||
FieldBackendService(viewId: rowController.viewId, fieldId: fieldId);
|
||||
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(
|
||||
fromIndex,
|
||||
toIndex,
|
||||
fromIndexInAllFields,
|
||||
toIndexInAllFields,
|
||||
);
|
||||
result.fold((l) {}, (err) => Log.error(err));
|
||||
}
|
||||
@@ -110,25 +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.showField(String fieldId) = _ShowField;
|
||||
const factory RowDetailEvent.hideField(String fieldId) = _HideField;
|
||||
const factory RowDetailEvent.toggleFieldVisibility(String fieldId) =
|
||||
_ToggleFieldVisibility;
|
||||
const factory RowDetailEvent.reorderField(
|
||||
String fieldId,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+18
-11
@@ -5,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';
|
||||
@@ -410,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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -425,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()} :';
|
||||
}
|
||||
+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(),
|
||||
);
|
||||
}
|
||||
|
||||
+8
-3
@@ -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
|
||||
|
||||
+11
-11
@@ -34,14 +34,14 @@ class _GridFieldCellActionSheetState extends State<GridFieldCellActionSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_showFieldEditor) {
|
||||
final field = widget.cellContext.field;
|
||||
return SizedBox(
|
||||
width: 400,
|
||||
child: FieldEditor(
|
||||
viewId: widget.cellContext.viewId,
|
||||
fieldInfo: widget.cellContext.fieldInfo,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
viewId: widget.cellContext.viewId,
|
||||
field: field,
|
||||
field: widget.cellContext.fieldInfo.field,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -96,8 +96,8 @@ class _EditFieldButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _FieldOperationList extends StatelessWidget {
|
||||
final FieldContext fieldInfo;
|
||||
const _FieldOperationList(this.fieldInfo, {Key? key}) : super(key: key);
|
||||
final FieldContext fieldContext;
|
||||
const _FieldOperationList(this.fieldContext, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -128,7 +128,7 @@ class _FieldOperationList extends StatelessWidget {
|
||||
bool enable = true;
|
||||
|
||||
// If the field is primary, delete and duplicate are disabled.
|
||||
if (fieldInfo.field.isPrimary) {
|
||||
if (fieldContext.fieldInfo.isPrimary) {
|
||||
switch (action) {
|
||||
case FieldAction.hide:
|
||||
break;
|
||||
@@ -145,7 +145,7 @@ class _FieldOperationList extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FieldActionCell(
|
||||
fieldInfo: fieldInfo,
|
||||
fieldInfo: fieldContext,
|
||||
action: action,
|
||||
enable: enable,
|
||||
),
|
||||
@@ -217,7 +217,7 @@ extension _FieldActionExtension on FieldAction {
|
||||
}
|
||||
}
|
||||
|
||||
void run(BuildContext context, FieldContext fieldInfo) {
|
||||
void run(BuildContext context, FieldContext fieldContext) {
|
||||
switch (this) {
|
||||
case FieldAction.hide:
|
||||
context
|
||||
@@ -228,8 +228,8 @@ extension _FieldActionExtension on FieldAction {
|
||||
PopoverContainer.of(context).close();
|
||||
|
||||
FieldBackendService(
|
||||
viewId: fieldInfo.viewId,
|
||||
fieldId: fieldInfo.field.id,
|
||||
viewId: fieldContext.viewId,
|
||||
fieldId: fieldContext.fieldInfo.id,
|
||||
).duplicateField();
|
||||
|
||||
break;
|
||||
@@ -240,8 +240,8 @@ extension _FieldActionExtension on FieldAction {
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
FieldBackendService(
|
||||
viewId: fieldInfo.viewId,
|
||||
fieldId: fieldInfo.field.id,
|
||||
viewId: fieldContext.viewId,
|
||||
fieldId: fieldContext.fieldInfo.field.id,
|
||||
).deleteField();
|
||||
},
|
||||
).show(context);
|
||||
|
||||
+50
-22
@@ -1,6 +1,8 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
@@ -19,17 +21,19 @@ class FieldEditor extends StatefulWidget {
|
||||
final String viewId;
|
||||
final bool isGroupingField;
|
||||
final Function(String)? onDeleted;
|
||||
final Function(String)? onHidden;
|
||||
final Function(String)? onToggleVisibility;
|
||||
final FieldTypeOptionLoader typeOptionLoader;
|
||||
final FieldInfo? fieldInfo;
|
||||
|
||||
const FieldEditor({
|
||||
required this.viewId,
|
||||
required this.typeOptionLoader,
|
||||
this.fieldInfo,
|
||||
this.isGroupingField = false,
|
||||
this.onDeleted,
|
||||
this.onHidden,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
this.onToggleVisibility,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FieldEditorState();
|
||||
@@ -52,10 +56,15 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool requireSpace = widget.onDeleted != null ||
|
||||
widget.onToggleVisibility != null ||
|
||||
!widget.typeOptionLoader.field.isPrimary;
|
||||
|
||||
final List<Widget> children = [
|
||||
FieldNameTextField(popoverMutex: popoverMutex),
|
||||
if (requireSpace) const VSpace(4),
|
||||
if (widget.onDeleted != null) _addDeleteFieldButton(),
|
||||
if (widget.onHidden != null) _addHideFieldButton(),
|
||||
if (widget.onToggleVisibility != null) _addHideFieldButton(),
|
||||
if (!widget.typeOptionLoader.field.isPrimary)
|
||||
FieldTypeOptionCell(popoverMutex: popoverMutex),
|
||||
];
|
||||
@@ -83,7 +92,7 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: _DeleteFieldButton(
|
||||
child: DeleteFieldButton(
|
||||
popoverMutex: popoverMutex,
|
||||
onDeleted: () {
|
||||
state.field.fold(
|
||||
@@ -102,12 +111,14 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: HideFieldButton(
|
||||
child: FieldVisibilityToggleButton(
|
||||
isFieldHidden:
|
||||
widget.fieldInfo!.visibility == FieldVisibility.AlwaysHidden,
|
||||
popoverMutex: popoverMutex,
|
||||
onHidden: () {
|
||||
onTap: () {
|
||||
state.field.fold(
|
||||
() => Log.error('Can not hidden the field'),
|
||||
(field) => widget.onHidden?.call(field.id),
|
||||
(field) => widget.onToggleVisibility?.call(field.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -200,17 +211,29 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.removeListener(() {
|
||||
if (focusNode.hasFocus) {
|
||||
widget.popoverMutex.close();
|
||||
}
|
||||
});
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteFieldButton extends StatelessWidget {
|
||||
@visibleForTesting
|
||||
class DeleteFieldButton extends StatelessWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
final VoidCallback? onDeleted;
|
||||
|
||||
const _DeleteFieldButton({
|
||||
const DeleteFieldButton({
|
||||
required this.popoverMutex,
|
||||
required this.onDeleted,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -237,15 +260,17 @@ class _DeleteFieldButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class HideFieldButton extends StatelessWidget {
|
||||
class FieldVisibilityToggleButton extends StatelessWidget {
|
||||
final bool isFieldHidden;
|
||||
final PopoverMutex popoverMutex;
|
||||
final VoidCallback? onHidden;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const HideFieldButton({
|
||||
const FieldVisibilityToggleButton({
|
||||
required this.isFieldHidden,
|
||||
required this.popoverMutex,
|
||||
required this.onHidden,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -254,10 +279,13 @@ class HideFieldButton extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
final Widget button = FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_hide.tr(),
|
||||
isFieldHidden
|
||||
? LocaleKeys.grid_field_show.tr()
|
||||
: LocaleKeys.grid_field_hide.tr(),
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.hide_s),
|
||||
onTap: () => onHidden?.call(),
|
||||
leftIcon:
|
||||
FlowySvg(isFieldHidden ? FlowySvgs.show_m : FlowySvgs.hide_m),
|
||||
onTap: onTap,
|
||||
onHover: (_) => popoverMutex.close(),
|
||||
);
|
||||
return SizedBox(height: GridSize.popoverItemHeight, child: button);
|
||||
|
||||
+3
-3
@@ -98,12 +98,12 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
.map(
|
||||
(field) => FieldContext(
|
||||
viewId: widget.viewId,
|
||||
field: field.field,
|
||||
fieldInfo: field,
|
||||
),
|
||||
)
|
||||
.map(
|
||||
(ctx) => GridFieldCell(
|
||||
key: _getKeyById(ctx.field.id),
|
||||
key: _getKeyById(ctx.fieldInfo.id),
|
||||
cellContext: ctx,
|
||||
),
|
||||
)
|
||||
@@ -136,7 +136,7 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
int newIndex,
|
||||
) {
|
||||
if (cells.length > oldIndex) {
|
||||
final field = cells[oldIndex].cellContext.field;
|
||||
final field = cells[oldIndex].cellContext.fieldInfo.field;
|
||||
context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.moveField(field, oldIndex, newIndex));
|
||||
|
||||
+1
@@ -73,6 +73,7 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
|
||||
height: GridSize.popoverItemHeight,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FlowyText.medium(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
LocaleKeys.grid_field_numberFormat.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
+11
-1
@@ -306,7 +306,6 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: FlowyTextField(
|
||||
autoClearWhenDone: true,
|
||||
maxLength: 30,
|
||||
text: text,
|
||||
focusNode: _focusNode,
|
||||
onCanceled: () {
|
||||
@@ -325,4 +324,15 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
widget.popoverMutex?.close();
|
||||
}
|
||||
});
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -224,7 +224,7 @@ class _SelectOptionColorCell extends StatelessWidget {
|
||||
|
||||
final colorIcon = SizedBox.square(
|
||||
dimension: 16,
|
||||
child: Container(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color.toColor(context),
|
||||
shape: BoxShape.circle,
|
||||
|
||||
+21
-1
@@ -257,7 +257,10 @@ class RowContent extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _makeCells(context, state.cellByFieldId),
|
||||
children: [
|
||||
..._makeCells(context, state.cellByFieldId),
|
||||
_finalCellDecoration(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -302,6 +305,23 @@ class RowContent extends StatelessWidget {
|
||||
},
|
||||
).toList();
|
||||
}
|
||||
|
||||
Widget _finalCellDecoration(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
child: Container(
|
||||
width: GridSize.trailHeaderPadding,
|
||||
padding: GridSize.headerContentInsets,
|
||||
constraints: const BoxConstraints(minHeight: 46),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
),
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RegionStateNotifier extends ChangeNotifier {
|
||||
|
||||
@@ -161,6 +161,7 @@ enum AddButtonAction {
|
||||
case AddButtonAction.board:
|
||||
return FlowySvgs.board_s;
|
||||
case AddButtonAction.calendar:
|
||||
return FlowySvgs.date_s;
|
||||
case AddButtonAction.grid:
|
||||
return FlowySvgs.grid_s;
|
||||
}
|
||||
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import '../../../application/cell/cell_controller_builder.dart';
|
||||
|
||||
part 'checkbox_card_cell_bloc.freezed.dart';
|
||||
|
||||
class CheckboxCardCellBloc
|
||||
extends Bloc<CheckboxCardCellEvent, CheckboxCardCellState> {
|
||||
final CheckboxCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
CheckboxCardCellBloc({
|
||||
required this.cellController,
|
||||
}) : super(CheckboxCardCellState.initial(cellController)) {
|
||||
on<CheckboxCardCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(state.copyWith(isSelected: _isSelected(cellData)));
|
||||
},
|
||||
select: () async {
|
||||
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellChanged: ((cellContent) {
|
||||
if (!isClosed) {
|
||||
add(CheckboxCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CheckboxCardCellEvent with _$CheckboxCardCellEvent {
|
||||
const factory CheckboxCardCellEvent.initial() = _InitialCell;
|
||||
const factory CheckboxCardCellEvent.select() = _Selected;
|
||||
const factory CheckboxCardCellEvent.didReceiveCellUpdate(String cellContent) =
|
||||
_DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CheckboxCardCellState with _$CheckboxCardCellState {
|
||||
const factory CheckboxCardCellState({
|
||||
required bool isSelected,
|
||||
}) = _CheckboxCellState;
|
||||
|
||||
factory CheckboxCardCellState.initial(TextCellController context) {
|
||||
return CheckboxCardCellState(
|
||||
isSelected: _isSelected(context.getCellData()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isSelected(String? cellData) {
|
||||
// The backend use "Yes" and "No" to represent the checkbox cell data.
|
||||
return cellData == "Yes";
|
||||
}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import '../../../application/cell/cell_controller_builder.dart';
|
||||
part 'date_card_cell_bloc.freezed.dart';
|
||||
|
||||
class DateCardCellBloc extends Bloc<DateCardCellEvent, DateCardCellState> {
|
||||
final DateCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
DateCardCellBloc({required this.cellController})
|
||||
: super(DateCardCellState.initial(cellController)) {
|
||||
on<DateCardCellEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () => _startListening(),
|
||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
data: cellData,
|
||||
dateStr: _dateStrFromCellData(cellData),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellChanged: ((data) {
|
||||
if (!isClosed) {
|
||||
add(DateCardCellEvent.didReceiveCellUpdate(data));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DateCardCellEvent with _$DateCardCellEvent {
|
||||
const factory DateCardCellEvent.initial() = _InitialCell;
|
||||
const factory DateCardCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
|
||||
_DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DateCardCellState with _$DateCardCellState {
|
||||
const factory DateCardCellState({
|
||||
required DateCellDataPB? data,
|
||||
required String dateStr,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _DateCardCellState;
|
||||
|
||||
factory DateCardCellState.initial(DateCellController context) {
|
||||
final cellData = context.getCellData();
|
||||
|
||||
return DateCardCellState(
|
||||
fieldInfo: context.fieldInfo,
|
||||
data: cellData,
|
||||
dateStr: _dateStrFromCellData(cellData),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _dateStrFromCellData(DateCellDataPB? cellData) {
|
||||
String dateStr = "";
|
||||
if (cellData != null) {
|
||||
if (cellData.includeTime) {
|
||||
dateStr = '${cellData.date} ${cellData.time}';
|
||||
} else {
|
||||
dateStr = cellData.date;
|
||||
}
|
||||
}
|
||||
return dateStr;
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import '../../../application/cell/cell_controller_builder.dart';
|
||||
|
||||
part 'number_card_cell_bloc.freezed.dart';
|
||||
|
||||
class NumberCardCellBloc
|
||||
extends Bloc<NumberCardCellEvent, NumberCardCellState> {
|
||||
final NumberCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
NumberCardCellBloc({
|
||||
required this.cellController,
|
||||
}) : super(NumberCardCellState.initial(cellController)) {
|
||||
on<NumberCardCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveCellUpdate: (content) {
|
||||
emit(state.copyWith(content: content));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellChanged: ((cellContent) {
|
||||
if (!isClosed) {
|
||||
add(NumberCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class NumberCardCellEvent with _$NumberCardCellEvent {
|
||||
const factory NumberCardCellEvent.initial() = _InitialCell;
|
||||
const factory NumberCardCellEvent.didReceiveCellUpdate(String cellContent) =
|
||||
_DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class NumberCardCellState with _$NumberCardCellState {
|
||||
const factory NumberCardCellState({
|
||||
required String content,
|
||||
}) = _NumberCardCellState;
|
||||
|
||||
factory NumberCardCellState.initial(TextCellController context) =>
|
||||
NumberCardCellState(
|
||||
content: context.getCellData() ?? "",
|
||||
);
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'select_option_card_cell_bloc.freezed.dart';
|
||||
|
||||
class SelectOptionCardCellBloc
|
||||
extends Bloc<SelectOptionCardCellEvent, SelectOptionCardCellState> {
|
||||
final SelectOptionCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
SelectOptionCardCellBloc({
|
||||
required this.cellController,
|
||||
}) : super(SelectOptionCardCellState.initial(cellController)) {
|
||||
on<SelectOptionCardCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveOptions: (List<SelectOptionPB> selectedOptions) {
|
||||
emit(state.copyWith(selectedOptions: selectedOptions));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellChanged: ((selectOptionContext) {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
SelectOptionCardCellEvent.didReceiveOptions(
|
||||
selectOptionContext?.selectOptions ?? [],
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionCardCellEvent with _$SelectOptionCardCellEvent {
|
||||
const factory SelectOptionCardCellEvent.initial() = _InitialCell;
|
||||
const factory SelectOptionCardCellEvent.didReceiveOptions(
|
||||
List<SelectOptionPB> selectedOptions,
|
||||
) = _DidReceiveOptions;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionCardCellState with _$SelectOptionCardCellState {
|
||||
const factory SelectOptionCardCellState({
|
||||
required List<SelectOptionPB> selectedOptions,
|
||||
}) = _SelectOptionCardCellState;
|
||||
|
||||
factory SelectOptionCardCellState.initial(
|
||||
SelectOptionCellController context,
|
||||
) {
|
||||
final data = context.getCellData();
|
||||
return SelectOptionCardCellState(
|
||||
selectedOptions: data?.selectOptions ?? [],
|
||||
);
|
||||
}
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
||||
part 'text_card_cell_bloc.freezed.dart';
|
||||
|
||||
class TextCardCellBloc extends Bloc<TextCardCellEvent, TextCardCellState> {
|
||||
final TextCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
TextCardCellBloc({
|
||||
required this.cellController,
|
||||
}) : super(TextCardCellState.initial(cellController)) {
|
||||
on<TextCardCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveCellUpdate: (content) {
|
||||
emit(state.copyWith(content: content));
|
||||
},
|
||||
updateText: (text) {
|
||||
if (text != state.content) {
|
||||
cellController.saveCellData(text);
|
||||
emit(state.copyWith(content: text));
|
||||
}
|
||||
},
|
||||
enableEdit: (bool enabled) {
|
||||
emit(state.copyWith(enableEdit: enabled));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellChanged: ((cellContent) {
|
||||
if (!isClosed) {
|
||||
add(TextCardCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TextCardCellEvent with _$TextCardCellEvent {
|
||||
const factory TextCardCellEvent.initial() = _InitialCell;
|
||||
const factory TextCardCellEvent.updateText(String text) = _UpdateContent;
|
||||
const factory TextCardCellEvent.enableEdit(bool enabled) = _EnableEdit;
|
||||
const factory TextCardCellEvent.didReceiveCellUpdate(String cellContent) =
|
||||
_DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TextCardCellState with _$TextCardCellState {
|
||||
const factory TextCardCellState({
|
||||
required String content,
|
||||
required bool enableEdit,
|
||||
}) = _TextCardCellState;
|
||||
|
||||
factory TextCardCellState.initial(TextCellController context) =>
|
||||
TextCardCellState(
|
||||
content: context.getCellData() ?? "",
|
||||
enableEdit: false,
|
||||
);
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.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 'dart:async';
|
||||
|
||||
part 'timestamp_card_cell_bloc.freezed.dart';
|
||||
|
||||
class TimestampCardCellBloc
|
||||
extends Bloc<TimestampCardCellEvent, TimestampCardCellState> {
|
||||
final TimestampCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
TimestampCardCellBloc({required this.cellController})
|
||||
: super(TimestampCardCellState.initial(cellController)) {
|
||||
on<TimestampCardCellEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () => _startListening(),
|
||||
didReceiveCellUpdate: (TimestampCellDataPB? cellData) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
data: cellData,
|
||||
dateStr: cellData?.dateTime ?? "",
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellChanged: ((data) {
|
||||
if (!isClosed) {
|
||||
add(TimestampCardCellEvent.didReceiveCellUpdate(data));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampCardCellEvent with _$TimestampCardCellEvent {
|
||||
const factory TimestampCardCellEvent.initial() = _InitialCell;
|
||||
const factory TimestampCardCellEvent.didReceiveCellUpdate(
|
||||
TimestampCellDataPB? data,
|
||||
) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampCardCellState with _$TimestampCardCellState {
|
||||
const factory TimestampCardCellState({
|
||||
required TimestampCellDataPB? data,
|
||||
required String dateStr,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _TimestampCardCellState;
|
||||
|
||||
factory TimestampCardCellState.initial(TimestampCellController context) {
|
||||
final cellData = context.getCellData();
|
||||
|
||||
return TimestampCardCellState(
|
||||
fieldInfo: context.fieldInfo,
|
||||
data: cellData,
|
||||
dateStr: cellData?.dateTime ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
||||
part 'url_card_cell_bloc.freezed.dart';
|
||||
|
||||
class URLCardCellBloc extends Bloc<URLCardCellEvent, URLCardCellState> {
|
||||
final URLCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
URLCardCellBloc({
|
||||
required this.cellController,
|
||||
}) : super(URLCardCellState.initial(cellController)) {
|
||||
on<URLCardCellEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
content: cellData?.content ?? "",
|
||||
url: cellData?.url ?? "",
|
||||
),
|
||||
);
|
||||
},
|
||||
updateURL: (String url) {
|
||||
cellController.saveCellData(url, deduplicate: true);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellChanged: ((cellData) {
|
||||
if (!isClosed) {
|
||||
add(URLCardCellEvent.didReceiveCellUpdate(cellData));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class URLCardCellEvent with _$URLCardCellEvent {
|
||||
const factory URLCardCellEvent.initial() = _InitialCell;
|
||||
const factory URLCardCellEvent.updateURL(String url) = _UpdateURL;
|
||||
const factory URLCardCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
|
||||
_DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class URLCardCellState with _$URLCardCellState {
|
||||
const factory URLCardCellState({
|
||||
required String content,
|
||||
required String url,
|
||||
}) = _URLCardCellState;
|
||||
|
||||
factory URLCardCellState.initial(URLCellController context) {
|
||||
final cellData = context.getCellData();
|
||||
return URLCardCellState(
|
||||
content: cellData?.content ?? "",
|
||||
url: cellData?.url ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'bloc/number_card_cell_bloc.dart';
|
||||
import 'define.dart';
|
||||
|
||||
class BoardNumberCell extends StatefulWidget {
|
||||
final String groupId;
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
|
||||
const BoardNumberCell({
|
||||
required this.groupId,
|
||||
required this.cellControllerBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<BoardNumberCell> createState() => _NumberCardCellState();
|
||||
}
|
||||
|
||||
class _NumberCardCellState extends State<BoardNumberCell> {
|
||||
late NumberCardCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as NumberCellController;
|
||||
|
||||
_cellBloc = NumberCardCellBloc(cellController: cellController)
|
||||
..add(const NumberCardCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<NumberCardCellBloc, NumberCardCellState>(
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
builder: (context, state) {
|
||||
if (state.content.isEmpty) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: CardSizes.cardCellVPadding,
|
||||
),
|
||||
child: FlowyText.medium(
|
||||
state.content,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,8 @@ List<DatabaseCellContext> _makeCells(
|
||||
CellContextByFieldId originalCellMap,
|
||||
) {
|
||||
final List<DatabaseCellContext> cells = [];
|
||||
originalCellMap
|
||||
.removeWhere((fieldId, cellContext) => !cellContext.isVisible());
|
||||
for (final entry in originalCellMap.entries) {
|
||||
// Filter out the cell if it's fieldId equal to the groupFieldId
|
||||
if (groupFieldId != null) {
|
||||
|
||||
+9
-10
@@ -1,11 +1,10 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../bloc/checkbox_card_cell_bloc.dart';
|
||||
import 'card_cell.dart';
|
||||
|
||||
class CheckboxCardCell extends CardCell {
|
||||
@@ -17,18 +16,18 @@ class CheckboxCardCell extends CardCell {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CheckboxCardCell> createState() => _CheckboxCardCellState();
|
||||
State<CheckboxCardCell> createState() => _CheckboxCellState();
|
||||
}
|
||||
|
||||
class _CheckboxCardCellState extends State<CheckboxCardCell> {
|
||||
late CheckboxCardCellBloc _cellBloc;
|
||||
class _CheckboxCellState extends State<CheckboxCardCell> {
|
||||
late CheckboxCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as CheckboxCellController;
|
||||
_cellBloc = CheckboxCardCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const CheckboxCardCellEvent.initial());
|
||||
_cellBloc = CheckboxCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const CheckboxCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -36,7 +35,7 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<CheckboxCardCellBloc, CheckboxCardCellState>(
|
||||
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.isSelected != current.isSelected,
|
||||
builder: (context, state) {
|
||||
@@ -55,8 +54,8 @@ class _CheckboxCardCellState extends State<CheckboxCardCell> {
|
||||
icon: icon,
|
||||
width: 20,
|
||||
onPressed: () => context
|
||||
.read<CheckboxCardCellBloc>()
|
||||
.add(const CheckboxCardCellEvent.select()),
|
||||
.read<CheckboxCellBloc>()
|
||||
.add(const CheckboxCellEvent.select()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+5
-5
@@ -12,17 +12,17 @@ class ChecklistCardCell extends CardCell {
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<ChecklistCardCell> createState() => _ChecklistCardCellState();
|
||||
State<ChecklistCardCell> createState() => _ChecklistCellState();
|
||||
}
|
||||
|
||||
class _ChecklistCardCellState extends State<ChecklistCardCell> {
|
||||
late ChecklistCardCellBloc _cellBloc;
|
||||
class _ChecklistCellState extends State<ChecklistCardCell> {
|
||||
late ChecklistCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as ChecklistCellController;
|
||||
_cellBloc = ChecklistCardCellBloc(cellController: cellController);
|
||||
_cellBloc = ChecklistCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const ChecklistCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class _ChecklistCardCellState extends State<ChecklistCardCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../bloc/date_card_cell_bloc.dart';
|
||||
import '../define.dart';
|
||||
import 'card_cell.dart';
|
||||
|
||||
@@ -18,19 +18,19 @@ class DateCardCell<CustomCardData> extends CardCell {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DateCardCell> createState() => _DateCardCellState();
|
||||
State<DateCardCell> createState() => _DateCellState();
|
||||
}
|
||||
|
||||
class _DateCardCellState extends State<DateCardCell> {
|
||||
late DateCardCellBloc _cellBloc;
|
||||
class _DateCellState extends State<DateCardCell> {
|
||||
late DateCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as DateCellController;
|
||||
|
||||
_cellBloc = DateCardCellBloc(cellController: cellController)
|
||||
..add(const DateCardCellEvent.initial());
|
||||
_cellBloc = DateCellBloc(cellController: cellController)
|
||||
..add(const DateCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class _DateCardCellState extends State<DateCardCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<DateCardCellBloc, DateCardCellState>(
|
||||
child: BlocBuilder<DateCellBloc, DateCellState>(
|
||||
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
|
||||
builder: (context, state) {
|
||||
if (state.dateStr.isEmpty) {
|
||||
|
||||
+12
-11
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../bloc/number_card_cell_bloc.dart';
|
||||
import '../define.dart';
|
||||
import 'card_cell.dart';
|
||||
|
||||
@@ -27,19 +27,19 @@ class NumberCardCell<CustomCardData>
|
||||
}) : super(key: key, style: style, cardData: cardData);
|
||||
|
||||
@override
|
||||
State<NumberCardCell> createState() => _NumberCardCellState();
|
||||
State<NumberCardCell> createState() => _NumberCellState();
|
||||
}
|
||||
|
||||
class _NumberCardCellState extends State<NumberCardCell> {
|
||||
late NumberCardCellBloc _cellBloc;
|
||||
class _NumberCellState extends State<NumberCardCell> {
|
||||
late NumberCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as NumberCellController;
|
||||
|
||||
_cellBloc = NumberCardCellBloc(cellController: cellController)
|
||||
..add(const NumberCardCellEvent.initial());
|
||||
_cellBloc = NumberCellBloc(cellController: cellController)
|
||||
..add(const NumberCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -47,14 +47,15 @@ class _NumberCardCellState extends State<NumberCardCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<NumberCardCellBloc, NumberCardCellState>(
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
child: BlocBuilder<NumberCellBloc, NumberCellState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.cellContent != current.cellContent,
|
||||
builder: (context, state) {
|
||||
if (state.content.isEmpty) {
|
||||
if (state.cellContent.isEmpty) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
final Widget? custom = widget.renderHook?.call(
|
||||
state.content,
|
||||
state.cellContent,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
@@ -69,7 +70,7 @@ class _NumberCardCellState extends State<NumberCardCell> {
|
||||
vertical: CardSizes.cardCellVPadding,
|
||||
),
|
||||
child: FlowyText.medium(
|
||||
state.content,
|
||||
state.cellContent,
|
||||
fontSize: widget.style?.fontSize ?? 14,
|
||||
),
|
||||
),
|
||||
|
||||
+8
-7
@@ -1,12 +1,13 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../bloc/select_option_card_cell_bloc.dart';
|
||||
|
||||
import 'card_cell.dart';
|
||||
|
||||
class SelectOptionCardCellStyle extends CardCellStyle {}
|
||||
@@ -29,11 +30,11 @@ class SelectOptionCardCell<CustomCardData>
|
||||
}) : super(key: key, cardData: cardData);
|
||||
|
||||
@override
|
||||
State<SelectOptionCardCell> createState() => _SelectOptionCardCellState();
|
||||
State<SelectOptionCardCell> createState() => _SelectOptionCellState();
|
||||
}
|
||||
|
||||
class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
|
||||
late SelectOptionCardCellBloc _cellBloc;
|
||||
class _SelectOptionCellState extends State<SelectOptionCardCell> {
|
||||
late SelectOptionCellBloc _cellBloc;
|
||||
late PopoverController _popover;
|
||||
|
||||
@override
|
||||
@@ -41,8 +42,8 @@ class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
|
||||
_popover = PopoverController();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as SelectOptionCellController;
|
||||
_cellBloc = SelectOptionCardCellBloc(cellController: cellController)
|
||||
..add(const SelectOptionCardCellEvent.initial());
|
||||
_cellBloc = SelectOptionCellBloc(cellController: cellController)
|
||||
..add(const SelectOptionCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ class _SelectOptionCardCellState extends State<SelectOptionCardCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<SelectOptionCardCellBloc, SelectOptionCardCellState>(
|
||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
buildWhen: (previous, current) {
|
||||
return previous.selectedOptions != current.selectedOptions;
|
||||
},
|
||||
|
||||
+12
-12
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../row/cell_builder.dart';
|
||||
import '../bloc/text_card_cell_bloc.dart';
|
||||
import '../define.dart';
|
||||
import 'card_cell.dart';
|
||||
|
||||
@@ -30,11 +30,11 @@ class TextCardCell<CustomCardData>
|
||||
}) : super(key: key, style: style, cardData: cardData);
|
||||
|
||||
@override
|
||||
State<TextCardCell> createState() => _TextCardCellState();
|
||||
State<TextCardCell> createState() => _TextCellState();
|
||||
}
|
||||
|
||||
class _TextCardCellState extends State<TextCardCell> {
|
||||
late TextCardCellBloc _cellBloc;
|
||||
class _TextCellState extends State<TextCardCell> {
|
||||
late TextCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
bool focusWhenInit = false;
|
||||
final focusNode = SingleListenerFocusNode();
|
||||
@@ -43,8 +43,8 @@ class _TextCardCellState extends State<TextCardCell> {
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as TextCellController;
|
||||
_cellBloc = TextCardCellBloc(cellController: cellController)
|
||||
..add(const TextCardCellEvent.initial());
|
||||
_cellBloc = TextCellBloc(cellController: cellController)
|
||||
..add(const TextCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false;
|
||||
if (focusWhenInit) {
|
||||
@@ -58,7 +58,7 @@ class _TextCardCellState extends State<TextCardCell> {
|
||||
if (!focusNode.hasFocus) {
|
||||
focusWhenInit = false;
|
||||
widget.editableNotifier?.isCellEditing.value = false;
|
||||
_cellBloc.add(const TextCardCellEvent.enableEdit(false));
|
||||
_cellBloc.add(const TextCellEvent.enableEdit(false));
|
||||
}
|
||||
});
|
||||
_bindEditableNotifier();
|
||||
@@ -75,7 +75,7 @@ class _TextCardCellState extends State<TextCardCell> {
|
||||
focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
_cellBloc.add(TextCardCellEvent.enableEdit(isEditing));
|
||||
_cellBloc.add(TextCellEvent.enableEdit(isEditing));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,13 +89,13 @@ class _TextCardCellState extends State<TextCardCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocListener<TextCardCellBloc, TextCardCellState>(
|
||||
child: BlocListener<TextCellBloc, TextCellState>(
|
||||
listener: (context, state) {
|
||||
if (_controller.text != state.content) {
|
||||
_controller.text = state.content;
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<TextCardCellBloc, TextCardCellState>(
|
||||
child: BlocBuilder<TextCellBloc, TextCellState>(
|
||||
buildWhen: (previous, current) {
|
||||
if (previous.content != current.content &&
|
||||
_controller.text == current.content &&
|
||||
@@ -137,7 +137,7 @@ class _TextCardCellState extends State<TextCardCell> {
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {
|
||||
_cellBloc.add(TextCardCellEvent.updateText(_controller.text));
|
||||
_cellBloc.add(TextCellEvent.updateText(_controller.text));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -156,7 +156,7 @@ class _TextCardCellState extends State<TextCardCell> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildText(TextCardCellState state) {
|
||||
Widget _buildText(TextCellState state) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: CardSizes.cardCellVPadding,
|
||||
|
||||
+7
-7
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -18,19 +18,19 @@ class TimestampCardCell<CustomCardData> extends CardCell {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TimestampCardCell> createState() => _TimestampCardCellState();
|
||||
State<TimestampCardCell> createState() => _TimestampCellState();
|
||||
}
|
||||
|
||||
class _TimestampCardCellState extends State<TimestampCardCell> {
|
||||
late TimestampCardCellBloc _cellBloc;
|
||||
class _TimestampCellState extends State<TimestampCardCell> {
|
||||
late TimestampCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as TimestampCellController;
|
||||
|
||||
_cellBloc = TimestampCardCellBloc(cellController: cellController)
|
||||
..add(const TimestampCardCellEvent.initial());
|
||||
_cellBloc = TimestampCellBloc(cellController: cellController)
|
||||
..add(const TimestampCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class _TimestampCardCellState extends State<TimestampCardCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<TimestampCardCellBloc, TimestampCardCellState>(
|
||||
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
|
||||
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
|
||||
builder: (context, state) {
|
||||
if (state.dateStr.isEmpty) {
|
||||
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../bloc/url_card_cell_bloc.dart';
|
||||
import '../define.dart';
|
||||
import 'card_cell.dart';
|
||||
|
||||
@@ -24,18 +24,18 @@ class URLCardCell<CustomCardData>
|
||||
}) : super(key: key, style: style);
|
||||
|
||||
@override
|
||||
State<URLCardCell> createState() => _URLCardCellState();
|
||||
State<URLCardCell> createState() => _URLCellState();
|
||||
}
|
||||
|
||||
class _URLCardCellState extends State<URLCardCell> {
|
||||
late URLCardCellBloc _cellBloc;
|
||||
class _URLCellState extends State<URLCardCell> {
|
||||
late URLCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as URLCellController;
|
||||
_cellBloc = URLCardCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const URLCardCellEvent.initial());
|
||||
_cellBloc = URLCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const URLCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class _URLCardCellState extends State<URLCardCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<URLCardCellBloc, URLCardCellState>(
|
||||
child: BlocBuilder<URLCellBloc, URLCellState>(
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
builder: (context, state) {
|
||||
if (state.content.isEmpty) {
|
||||
|
||||
+4
-1
@@ -66,7 +66,10 @@ class CardAccessoryContainer extends StatelessWidget {
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: decoration,
|
||||
child: child,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -143,7 +143,7 @@ class _GridPropertyCellState extends State<GridPropertyCell> {
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: visibleIcon.padding(all: 6.0),
|
||||
icon: visibleIcon.padding(all: 4.0),
|
||||
),
|
||||
onTap: () => _popoverController.show(),
|
||||
).padding(horizontal: 6.0),
|
||||
@@ -151,6 +151,7 @@ class _GridPropertyCellState extends State<GridPropertyCell> {
|
||||
popupBuilder: (BuildContext context) {
|
||||
return FieldEditor(
|
||||
viewId: widget.viewId,
|
||||
fieldInfo: widget.fieldInfo,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
viewId: widget.viewId,
|
||||
field: widget.fieldInfo.field,
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@@ -66,7 +67,7 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
|
||||
with GridCellAccessoryState {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
return FlowyTooltip.delayed(
|
||||
message: LocaleKeys.tooltip_openAsPage.tr(),
|
||||
child: SizedBox(
|
||||
width: 26,
|
||||
|
||||
@@ -67,6 +67,7 @@ class GridCellBuilder {
|
||||
case FieldType.Checklist:
|
||||
return GridChecklistCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
style: style,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.Number:
|
||||
@@ -246,7 +247,9 @@ class RequestFocusListener extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class GridCellStyle {}
|
||||
abstract class GridCellStyle {
|
||||
const GridCellStyle();
|
||||
}
|
||||
|
||||
class SingleListenerFocusNode extends FocusNode {
|
||||
VoidCallback? _listener;
|
||||
|
||||
@@ -4,4 +4,5 @@ export 'date_cell/date_cell.dart';
|
||||
export 'number_cell/number_cell.dart';
|
||||
export 'select_option_cell/select_option_cell.dart';
|
||||
export 'text_cell/text_cell.dart';
|
||||
export 'timestamp_cell/timestamp_cell.dart';
|
||||
export 'url_cell/url_cell.dart';
|
||||
+5
-8
@@ -1,13 +1,12 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'checkbox_cell_bloc.dart';
|
||||
import '../../../../grid/presentation/layout/sizes.dart';
|
||||
import '../../cell_builder.dart';
|
||||
|
||||
class GridCheckboxCellStyle extends GridCellStyle {
|
||||
EdgeInsets? cellPadding;
|
||||
@@ -44,10 +43,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as CheckboxCellController;
|
||||
_cellBloc = CheckboxCellBloc(
|
||||
service: CellBackendService(),
|
||||
cellController: cellController,
|
||||
)..add(const CheckboxCellEvent.initial());
|
||||
_cellBloc = CheckboxCellBloc(cellController: cellController)
|
||||
..add(const CheckboxCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -1,5 +1,4 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@@ -11,7 +10,6 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
CheckboxCellBloc({
|
||||
required CellBackendService service,
|
||||
required this.cellController,
|
||||
}) : super(CheckboxCellState.initial(cellController)) {
|
||||
on<CheckboxCellEvent>(
|
||||
@@ -20,12 +18,12 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
select: () async {
|
||||
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
|
||||
},
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(state.copyWith(isSelected: _isSelected(cellData)));
|
||||
},
|
||||
select: () async {
|
||||
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -73,5 +71,6 @@ class CheckboxCellState with _$CheckboxCellState {
|
||||
}
|
||||
|
||||
bool _isSelected(String? cellData) {
|
||||
// The backend use "Yes" and "No" to represent the checkbox cell data.
|
||||
return cellData == "Yes";
|
||||
}
|
||||
+36
-13
@@ -10,17 +10,33 @@ import 'checklist_cell_bloc.dart';
|
||||
import 'checklist_cell_editor.dart';
|
||||
import 'checklist_progress_bar.dart';
|
||||
|
||||
class ChecklistCellStyle extends GridCellStyle {
|
||||
String placeholder;
|
||||
EdgeInsets? cellPadding;
|
||||
|
||||
ChecklistCellStyle({
|
||||
required this.placeholder,
|
||||
this.cellPadding,
|
||||
});
|
||||
}
|
||||
|
||||
class GridChecklistCell extends GridCellWidget {
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
GridChecklistCell({required this.cellControllerBuilder, Key? key})
|
||||
: super(key: key);
|
||||
late final ChecklistCellStyle? cellStyle;
|
||||
GridChecklistCell({
|
||||
required this.cellControllerBuilder,
|
||||
GridCellStyle? style,
|
||||
super.key,
|
||||
}) {
|
||||
cellStyle = style as ChecklistCellStyle?;
|
||||
}
|
||||
|
||||
@override
|
||||
GridCellState<GridChecklistCell> createState() => GridChecklistCellState();
|
||||
}
|
||||
|
||||
class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
late ChecklistCardCellBloc _cellBloc;
|
||||
late ChecklistCellBloc _cellBloc;
|
||||
late final PopoverController _popover;
|
||||
|
||||
@override
|
||||
@@ -28,7 +44,7 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
_popover = PopoverController();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as ChecklistCellController;
|
||||
_cellBloc = ChecklistCardCellBloc(cellController: cellController);
|
||||
_cellBloc = ChecklistCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const ChecklistCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
@@ -53,15 +69,22 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
);
|
||||
},
|
||||
onClose: () => widget.onCellFocus.value = false,
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return ChecklistProgressBar(percent: state.percent);
|
||||
},
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding:
|
||||
widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
return FlowyText.medium(
|
||||
widget.cellStyle?.placeholder ?? "",
|
||||
color: Theme.of(context).hintColor,
|
||||
);
|
||||
}
|
||||
return ChecklistProgressBar(percent: state.percent);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+2
-3
@@ -8,12 +8,11 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
part 'checklist_cell_bloc.freezed.dart';
|
||||
|
||||
class ChecklistCardCellBloc
|
||||
extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
final ChecklistCellController cellController;
|
||||
final ChecklistCellBackendService _checklistCellSvc;
|
||||
void Function()? _onCellChangedFn;
|
||||
ChecklistCardCellBloc({
|
||||
ChecklistCellBloc({
|
||||
required this.cellController,
|
||||
}) : _checklistCellSvc = ChecklistCellBackendService(
|
||||
viewId: cellController.viewId,
|
||||
|
||||
+87
-120
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
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_controller_builder.dart';
|
||||
@@ -30,13 +32,19 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||
/// Focus node for the new task text field
|
||||
late final FocusNode newTaskFocusNode;
|
||||
|
||||
/// A flag that determines whether the new task text field is visible
|
||||
bool _isAddingNewTask = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
newTaskFocusNode = FocusNode();
|
||||
newTaskFocusNode = FocusNode(
|
||||
onKey: (node, event) {
|
||||
if (event is RawKeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
node.unfocus();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
_bloc = ChecklistCellEditorBloc(cellController: widget.cellController)
|
||||
..add(const ChecklistCellEditorEvent.initial());
|
||||
}
|
||||
@@ -48,59 +56,35 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||
child: BlocConsumer<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
listener: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
setState(() => _isAddingNewTask = true);
|
||||
newTaskFocusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Focus(
|
||||
onKey: (node, event) {
|
||||
// don't hide new task text field if there are no tasks at all
|
||||
if (state.allOptions.isNotEmpty &&
|
||||
event is RawKeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
setState(() {
|
||||
_isAddingNewTask = false;
|
||||
});
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
physics: StyledScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: state.allOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
child: ChecklistProgressBar(
|
||||
percent: state.percent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ChecklistItemList(
|
||||
options: state.allOptions,
|
||||
newTaskFocusNode: newTaskFocusNode,
|
||||
isAddingNewTask: _isAddingNewTask,
|
||||
onUpdateTask: () => setState(() {
|
||||
_isAddingNewTask = true;
|
||||
newTaskFocusNode.requestFocus();
|
||||
}),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: TypeOptionSeparator(spacing: 0.0),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: ChecklistNewTaskButton(
|
||||
onTap: () => setState(() => _isAddingNewTask = true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: state.allOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
child: ChecklistProgressBar(
|
||||
percent: state.percent,
|
||||
),
|
||||
),
|
||||
),
|
||||
ChecklistItemList(
|
||||
options: state.allOptions,
|
||||
onUpdateTask: () => newTaskFocusNode.requestFocus(),
|
||||
),
|
||||
if (state.allOptions.isNotEmpty)
|
||||
const TypeOptionSeparator(spacing: 0.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: NewTaskItem(focusNode: newTaskFocusNode),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -118,16 +102,12 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||
/// a new task if `isAddingNewTask` is true
|
||||
class ChecklistItemList extends StatefulWidget {
|
||||
final List<ChecklistSelectOption> options;
|
||||
final FocusNode newTaskFocusNode;
|
||||
final bool isAddingNewTask;
|
||||
final VoidCallback onUpdateTask;
|
||||
|
||||
const ChecklistItemList({
|
||||
super.key,
|
||||
required this.options,
|
||||
required this.onUpdateTask,
|
||||
required this.isAddingNewTask,
|
||||
required this.newTaskFocusNode,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -137,32 +117,28 @@ class ChecklistItemList extends StatefulWidget {
|
||||
class _ChecklistItemListState extends State<ChecklistItemList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final itemList = [
|
||||
const VSpace(6.0),
|
||||
...widget.options.mapIndexed(
|
||||
(index, option) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: ChecklistItem(
|
||||
if (widget.options.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final itemList = widget.options
|
||||
.mapIndexed(
|
||||
(index, option) => ChecklistItem(
|
||||
option: option,
|
||||
onSubmitted:
|
||||
index == widget.options.length - 1 ? widget.onUpdateTask : null,
|
||||
key: ValueKey(option.data.id),
|
||||
// only allow calling the callback for the last task in the list
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: widget.isAddingNewTask
|
||||
? NewTaskItem(focusNode: widget.newTaskFocusNode)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
const VSpace(6.0),
|
||||
];
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) => itemList[index],
|
||||
childCount: itemList.length,
|
||||
)
|
||||
.toList();
|
||||
|
||||
return Flexible(
|
||||
child: ListView.separated(
|
||||
itemBuilder: (context, index) => itemList[index],
|
||||
separatorBuilder: (context, index) => const VSpace(4),
|
||||
itemCount: itemList.length,
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -187,6 +163,7 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
late final TextEditingController _textController;
|
||||
late final FocusNode _focusNode;
|
||||
bool _isHovered = false;
|
||||
Timer? _debounceOnChanged;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -249,13 +226,9 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
),
|
||||
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
|
||||
),
|
||||
onSubmitted: (taskDescription) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.updateTaskName(
|
||||
widget.option.data,
|
||||
taskDescription,
|
||||
),
|
||||
);
|
||||
onChanged: _debounceOnChangedText,
|
||||
onSubmitted: (description) {
|
||||
_submitUpdateTaskDescription(description);
|
||||
widget.onSubmitted?.call();
|
||||
},
|
||||
),
|
||||
@@ -276,6 +249,22 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _debounceOnChangedText(String text) {
|
||||
_debounceOnChanged?.cancel();
|
||||
_debounceOnChanged = Timer(const Duration(milliseconds: 300), () {
|
||||
_submitUpdateTaskDescription(text);
|
||||
});
|
||||
}
|
||||
|
||||
void _submitUpdateTaskDescription(String description) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.updateTaskName(
|
||||
widget.option.data,
|
||||
description,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new task after entering the description and pressing enter.
|
||||
@@ -304,19 +293,12 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const FlowyIconButton(
|
||||
width: 32,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
),
|
||||
hoverColor: Colors.transparent,
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
focusNode: widget.focusNode,
|
||||
@@ -330,7 +312,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
vertical: 6.0,
|
||||
horizontal: 2.0,
|
||||
),
|
||||
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
|
||||
hintText: LocaleKeys.grid_checklist_addNew.tr(),
|
||||
),
|
||||
onSubmitted: (taskDescription) {
|
||||
if (taskDescription.trim().isNotEmpty) {
|
||||
@@ -340,15 +322,21 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
),
|
||||
);
|
||||
}
|
||||
widget.focusNode.requestFocus();
|
||||
_textEditingController.clear();
|
||||
},
|
||||
onChanged: (value) => setState(() {}),
|
||||
),
|
||||
),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.grid_checklist_submitNewTask.tr(),
|
||||
fontSize: 11,
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
hoverColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
fillColor: _textEditingController.text.isEmpty
|
||||
? Theme.of(context).disabledColor
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
hoverColor: _textEditingController.text.isEmpty
|
||||
? Theme.of(context).disabledColor
|
||||
: Theme.of(context).colorScheme.primaryContainer,
|
||||
fontColor: Theme.of(context).colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
onPressed: () {
|
||||
@@ -359,6 +347,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
),
|
||||
);
|
||||
}
|
||||
widget.focusNode.requestFocus();
|
||||
_textEditingController.clear();
|
||||
},
|
||||
),
|
||||
@@ -367,25 +356,3 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class ChecklistNewTaskButton extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
const ChecklistNewTaskButton({super.key, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||
child: SizedBox(
|
||||
height: 30,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.grid_checklist_addNew.tr()),
|
||||
margin: const EdgeInsets.all(6),
|
||||
leftIcon: const FlowySvg(FlowySvgs.add_s),
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+138
-49
@@ -10,10 +10,10 @@ import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart'
|
||||
show StringTranslateExtension;
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
part 'date_cal_bloc.freezed.dart';
|
||||
|
||||
@@ -38,40 +38,75 @@ class DateCellCalendarBloc
|
||||
await event.when(
|
||||
initial: () async => _startListening(),
|
||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
||||
final (dateTime, time, includeTime) =
|
||||
final (dateTime, endDateTime, time, endTime, includeTime, isRange) =
|
||||
_dateDataFromCellData(cellData);
|
||||
final endDay =
|
||||
isRange == state.isRange && isRange ? endDateTime : null;
|
||||
emit(
|
||||
state.copyWith(
|
||||
dateTime: dateTime,
|
||||
time: time,
|
||||
endDateTime: endDateTime,
|
||||
endTime: endTime,
|
||||
includeTime: includeTime,
|
||||
isRange: isRange,
|
||||
startDay: isRange ? dateTime : null,
|
||||
endDay: endDay,
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveTimeFormatError: (String? timeFormatError) {
|
||||
emit(state.copyWith(timeFormatError: timeFormatError));
|
||||
didReceiveTimeFormatError:
|
||||
(String? parseTimeError, String? parseEndTimeError) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
parseTimeError: parseTimeError,
|
||||
parseEndTimeError: parseEndTimeError,
|
||||
),
|
||||
);
|
||||
},
|
||||
selectDay: (date) async {
|
||||
if (state.isRange) {
|
||||
return;
|
||||
}
|
||||
await _updateDateData(date: date);
|
||||
},
|
||||
setIncludeTime: (includeTime) async {
|
||||
await _updateDateData(includeTime: includeTime);
|
||||
},
|
||||
setIsRange: (isRange) async {
|
||||
await _updateDateData(isRange: isRange);
|
||||
},
|
||||
setTime: (time) async {
|
||||
await _updateDateData(time: time);
|
||||
},
|
||||
selectDateRange: (DateTime? start, DateTime? end) async {
|
||||
if (end == null && state.startDay != null && state.endDay == null) {
|
||||
final (newStart, newEnd) = state.startDay!.isBefore(start!)
|
||||
? (state.startDay!, start)
|
||||
: (start, state.startDay!);
|
||||
emit(state.copyWith(startDay: null, endDay: null));
|
||||
await _updateDateData(
|
||||
date: newStart.date,
|
||||
endDate: newEnd.date,
|
||||
);
|
||||
} else if (end == null) {
|
||||
emit(state.copyWith(startDay: start, endDay: null));
|
||||
} else {
|
||||
await _updateDateData(
|
||||
date: start!.date,
|
||||
endDate: end.date,
|
||||
);
|
||||
}
|
||||
},
|
||||
setEndTime: (String endTime) async {
|
||||
await _updateDateData(endTime: endTime);
|
||||
},
|
||||
setDateFormat: (dateFormat) async {
|
||||
await _updateTypeOption(emit, dateFormat: dateFormat);
|
||||
},
|
||||
setTimeFormat: (timeFormat) async {
|
||||
await _updateTypeOption(emit, timeFormat: timeFormat);
|
||||
},
|
||||
setCalFormat: (format) {
|
||||
emit(state.copyWith(format: format));
|
||||
},
|
||||
setFocusedDay: (focusedDay) {
|
||||
emit(state.copyWith(focusedDay: focusedDay));
|
||||
},
|
||||
clearDate: () async {
|
||||
await _clearDate();
|
||||
},
|
||||
@@ -83,39 +118,66 @@ class DateCellCalendarBloc
|
||||
Future<void> _updateDateData({
|
||||
DateTime? date,
|
||||
String? time,
|
||||
DateTime? endDate,
|
||||
String? endTime,
|
||||
bool? includeTime,
|
||||
bool? isRange,
|
||||
}) async {
|
||||
// make sure that not both date and time are updated at the same time
|
||||
assert(
|
||||
date == null && time == null ||
|
||||
date == null && time != null ||
|
||||
date != null && time == null,
|
||||
!(date != null && time != null) || !(endDate != null && endTime != null),
|
||||
);
|
||||
|
||||
// if not updating the time, use the old time in the state
|
||||
final String? newTime = time ?? state.time;
|
||||
DateTime? newDate = _utcToLocalAndAddCurrentTime(date);
|
||||
DateTime? newDate;
|
||||
if (time != null && time.isNotEmpty) {
|
||||
newDate = state.dateTime ?? DateTime.now();
|
||||
} else {
|
||||
newDate = _utcToLocalAndAddCurrentTime(date);
|
||||
}
|
||||
|
||||
// if not updating the time, use the old time in the state
|
||||
final String? newEndTime = endTime ?? state.endTime;
|
||||
DateTime? newEndDate;
|
||||
if (endTime != null && endTime.isNotEmpty) {
|
||||
newEndDate = state.endDateTime ?? DateTime.now();
|
||||
} else {
|
||||
newEndDate = _utcToLocalAndAddCurrentTime(endDate);
|
||||
}
|
||||
|
||||
final result = await _dateCellBackendService.update(
|
||||
date: newDate,
|
||||
time: newTime,
|
||||
endDate: newEndDate,
|
||||
endTime: newEndTime,
|
||||
includeTime: includeTime ?? state.includeTime,
|
||||
isRange: isRange ?? state.isRange,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(_) {
|
||||
if (!isClosed && state.timeFormatError != null) {
|
||||
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
|
||||
if (!isClosed &&
|
||||
(state.parseEndTimeError != null || state.parseTimeError != null)) {
|
||||
add(
|
||||
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null),
|
||||
);
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
switch (err.code) {
|
||||
case ErrorCode.InvalidDateTimeFormat:
|
||||
if (isClosed) return;
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
// to determine which textfield should show error
|
||||
final (startError, endError) = newDate != null
|
||||
? (timeFormatPrompt(err), null)
|
||||
: (null, timeFormatPrompt(err));
|
||||
add(
|
||||
DateCellCalendarEvent.didReceiveTimeFormatError(
|
||||
timeFormatPrompt(err),
|
||||
startError,
|
||||
endError,
|
||||
),
|
||||
);
|
||||
break;
|
||||
@@ -130,9 +192,13 @@ class DateCellCalendarBloc
|
||||
final result = await _dateCellBackendService.clear();
|
||||
result.fold(
|
||||
(_) {
|
||||
if (!isClosed) {
|
||||
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
add(
|
||||
const DateCellCalendarEvent.didReceiveTimeFormatError(null, null),
|
||||
);
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
@@ -157,18 +223,13 @@ class DateCellCalendarBloc
|
||||
}
|
||||
|
||||
String timeFormatPrompt(FlowyError error) {
|
||||
String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}.";
|
||||
switch (state.dateTypeOptionPB.timeFormat) {
|
||||
case TimeFormatPB.TwelveHour:
|
||||
msg = "$msg e.g. 01:00 PM";
|
||||
break;
|
||||
case TimeFormatPB.TwentyFourHour:
|
||||
msg = "$msg e.g. 13:00";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return msg;
|
||||
return switch (state.dateTypeOptionPB.timeFormat) {
|
||||
TimeFormatPB.TwelveHour =>
|
||||
"${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 01:00 PM",
|
||||
TimeFormatPB.TwentyFourHour =>
|
||||
"${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 13:00",
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -235,19 +296,21 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
|
||||
DateCellDataPB? data,
|
||||
) = _DidReceiveCellUpdate;
|
||||
const factory DateCellCalendarEvent.didReceiveTimeFormatError(
|
||||
String? timeformatError,
|
||||
String? parseTimeError,
|
||||
String? parseEndTimeError,
|
||||
) = _DidReceiveTimeFormatError;
|
||||
|
||||
// table calendar's UI settings
|
||||
const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay;
|
||||
const factory DateCellCalendarEvent.setCalFormat(CalendarFormat format) =
|
||||
_CalendarFormat;
|
||||
|
||||
// date cell data is modified
|
||||
const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay;
|
||||
const factory DateCellCalendarEvent.selectDateRange(
|
||||
DateTime? start,
|
||||
DateTime? end,
|
||||
) = _SelectDateRange;
|
||||
const factory DateCellCalendarEvent.setTime(String time) = _Time;
|
||||
const factory DateCellCalendarEvent.setEndTime(String endTime) = _EndTime;
|
||||
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) =
|
||||
_IncludeTime;
|
||||
const factory DateCellCalendarEvent.setIsRange(bool isRange) = _IsRange;
|
||||
|
||||
// date field type options are modified
|
||||
const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) =
|
||||
@@ -261,13 +324,24 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
|
||||
@freezed
|
||||
class DateCellCalendarState with _$DateCellCalendarState {
|
||||
const factory DateCellCalendarState({
|
||||
// the date field's type option
|
||||
required DateTypeOptionPB dateTypeOptionPB,
|
||||
required CalendarFormat format,
|
||||
required DateTime focusedDay,
|
||||
|
||||
// used when selecting a date range
|
||||
required DateTime? startDay,
|
||||
required DateTime? endDay,
|
||||
|
||||
// cell data from the backend
|
||||
required DateTime? dateTime,
|
||||
required DateTime? endDateTime,
|
||||
required String? time,
|
||||
required String? endTime,
|
||||
required bool includeTime,
|
||||
required String? timeFormatError,
|
||||
required bool isRange,
|
||||
|
||||
// error and hint text
|
||||
required String? parseTimeError,
|
||||
required String? parseEndTimeError,
|
||||
required String timeHintText,
|
||||
}) = _DateCellCalendarState;
|
||||
|
||||
@@ -275,15 +349,20 @@ class DateCellCalendarState with _$DateCellCalendarState {
|
||||
DateTypeOptionPB dateTypeOptionPB,
|
||||
DateCellDataPB? cellData,
|
||||
) {
|
||||
final (dateTime, time, includeTime) = _dateDataFromCellData(cellData);
|
||||
final (dateTime, endDateTime, time, endTime, includeTime, isRange) =
|
||||
_dateDataFromCellData(cellData);
|
||||
return DateCellCalendarState(
|
||||
dateTypeOptionPB: dateTypeOptionPB,
|
||||
format: CalendarFormat.month,
|
||||
focusedDay: DateTime.now(),
|
||||
startDay: isRange ? dateTime : null,
|
||||
endDay: isRange ? endDateTime : null,
|
||||
dateTime: dateTime,
|
||||
endDateTime: endDateTime,
|
||||
time: time,
|
||||
endTime: endTime,
|
||||
includeTime: includeTime,
|
||||
timeFormatError: null,
|
||||
isRange: isRange,
|
||||
parseTimeError: null,
|
||||
parseEndTimeError: null,
|
||||
timeHintText: _timeHintText(dateTypeOptionPB),
|
||||
);
|
||||
}
|
||||
@@ -300,21 +379,31 @@ String _timeHintText(DateTypeOptionPB typeOption) {
|
||||
}
|
||||
}
|
||||
|
||||
(DateTime?, String?, bool) _dateDataFromCellData(DateCellDataPB? cellData) {
|
||||
(DateTime?, DateTime?, String?, String?, bool, bool) _dateDataFromCellData(
|
||||
DateCellDataPB? cellData,
|
||||
) {
|
||||
// a null DateCellDataPB may be returned, indicating that all the fields are
|
||||
// at their default values: empty strings and false booleans
|
||||
// their default values: empty strings and false booleans
|
||||
if (cellData == null) {
|
||||
return (null, null, false);
|
||||
return (null, null, null, null, false, false);
|
||||
}
|
||||
|
||||
DateTime? dateTime;
|
||||
String? time;
|
||||
DateTime? endDateTime;
|
||||
String? endTime;
|
||||
if (cellData.hasTimestamp()) {
|
||||
final timestamp = cellData.timestamp * 1000;
|
||||
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
|
||||
time = cellData.time;
|
||||
if (cellData.hasEndTimestamp()) {
|
||||
final endTimestamp = cellData.endTimestamp * 1000;
|
||||
endDateTime = DateTime.fromMillisecondsSinceEpoch(endTimestamp.toInt());
|
||||
endTime = cellData.endTime;
|
||||
}
|
||||
}
|
||||
final bool includeTime = cellData.includeTime;
|
||||
final bool isRange = cellData.isRange;
|
||||
|
||||
return (dateTime, time, includeTime);
|
||||
return (dateTime, endDateTime, time, endTime, includeTime, isRange);
|
||||
}
|
||||
+1
-1
@@ -74,7 +74,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
controller: _popover,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: BoxConstraints.loose(const Size(260, 520)),
|
||||
constraints: BoxConstraints.loose(const Size(260, 620)),
|
||||
margin: EdgeInsets.zero,
|
||||
child: GridDateCellText(
|
||||
dateStr: state.dateStr,
|
||||
|
||||
Loaded 100 of 514 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user