Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d36a846425 | ||
|
|
a02ec98e9c | ||
|
|
32c06cf1bf | ||
|
|
cf1096b516 | ||
|
|
938953d7b3 | ||
|
|
949dab9afe | ||
|
|
0996f5fd24 | ||
|
|
6e887f1f12 | ||
|
|
ab9338eb89 | ||
|
|
fd4088aed0 | ||
|
|
375ff5ed6d | ||
|
|
847290d9a4 | ||
|
|
56687581f2 | ||
|
|
3be39f5e7b | ||
|
|
1997231adf | ||
|
|
89face4f02 | ||
|
|
37ddce3a29 | ||
|
|
593df96b10 | ||
|
|
1b966171c4 | ||
|
|
047f1a0b39 | ||
|
|
f2f3506b29 | ||
|
|
0ccced4b54 | ||
|
|
f762a564b5 | ||
|
|
57b78ee3c6 | ||
|
|
5dfc470873 | ||
|
|
d5f8ce5435 | ||
|
|
3c65a96b04 | ||
|
|
6ba7fc0317 | ||
|
|
37a5054ebd | ||
|
|
048434024b | ||
|
|
2c757e9b6c | ||
|
|
9c59e1487e | ||
|
|
4b9b723521 | ||
|
|
a389836897 | ||
|
|
eec7723ce0 | ||
|
|
2f938a331f | ||
|
|
847d7cb35a | ||
|
|
094f8fc307 | ||
|
|
e3f11ea9c0 | ||
|
|
124d435f09 | ||
|
|
b700f95c7f | ||
|
|
daf9b48c73 | ||
|
|
0ea5b3c483 | ||
|
|
f3148640eb | ||
|
|
1c84ee1d53 | ||
|
|
cecd4f48ab | ||
|
|
1e001640aa | ||
|
|
3426e5b53e | ||
|
|
936b49a63f | ||
|
|
f6f80b48c9 | ||
|
|
759cb27a43 | ||
|
|
26a2bffcd1 | ||
|
|
fe55452c79 | ||
|
|
16adacac38 | ||
|
|
d86531ac92 | ||
|
|
9231455f0d | ||
|
|
0c6a1d4ae7 | ||
|
|
524efc2620 | ||
|
|
50a4f03931 | ||
|
|
a4681a4042 | ||
|
|
1ca130d7de | ||
|
|
ef6f9a3175 | ||
|
|
aa25aa4474 | ||
|
|
c7af04b317 | ||
|
|
9565173baf | ||
|
|
5f4e3ecc76 | ||
|
|
043c68a333 | ||
|
|
f639d51c11 | ||
|
|
71071b60b2 | ||
|
|
608255fa0f | ||
|
|
f8b0a26acc | ||
|
|
133dab934b | ||
|
|
a81670c447 | ||
|
|
35f84e42cc | ||
|
|
2f6df6a17d | ||
|
|
8bcc6384f2 | ||
|
|
a0fc4b86b0 | ||
|
|
790580eea4 |
No files matched your search
@@ -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:
|
||||
@@ -385,6 +389,7 @@ jobs:
|
||||
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 -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:
|
||||
|
||||
@@ -32,6 +32,26 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Cache Rust Dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: rust-dependencies-${{ runner.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
frontend/appflowy_tauri/src-tauri
|
||||
|
||||
- name: Cache Node.js dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ runner.os }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_tauri/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -40,13 +60,6 @@ jobs:
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: 'ubuntu-latest-tauri'
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
frontend/appflowy_tauri/src-tauri
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
working-directory: frontend
|
||||
@@ -75,15 +88,23 @@ jobs:
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
npm install -g pnpm@${{ env.PNPM_VERSION }}
|
||||
|
||||
|
||||
- name: build
|
||||
- name: Build
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
cargo make --cwd .. tauri_build
|
||||
pnpm test
|
||||
pnpm test:errors
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
run: |
|
||||
diff_files=$(git status --porcelain)
|
||||
if [ -n "$diff_files" ]; then
|
||||
echo "There are uncommitted changes in the working tree. Please commit them before pushing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -41,3 +41,5 @@ pubspec.lock
|
||||
# ignore the deb filegit
|
||||
frontend/package
|
||||
frontend/*.deb
|
||||
|
||||
**/Cargo.toml.bak
|
||||
@@ -1,5 +1,36 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.3.3 - 09/24/2023
|
||||
|
||||
### New Features
|
||||
- Added an end date field to the time cell in the database.
|
||||
- Added Support for customizing the font family from GoogleFonts in the editor.
|
||||
- Set the uploaded image to cover by default.
|
||||
- Added Support for resetting the user icon on settings page
|
||||
- Add Urdu language translations.
|
||||
|
||||
### Bug fixes
|
||||
- Default colors for the blocks except for the callout were not transparent.
|
||||
- Option/Alt + click to add a block above didn't work on the first line.
|
||||
- Unable to paste HTML content containing `<mark>` tag.
|
||||
- Unable to select the text from anywhere in the line.
|
||||
- The selection in the editor didn't clear when editing the inline database.
|
||||
- Added a bottom border to new property column in the database.
|
||||
- Set minimum width of 50px for grid fields.
|
||||
|
||||
## Version 0.3.2 - 09/18/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Improve the performance of the editor, now it is much faster when editing a large document.
|
||||
- Support for reordering the rows of the database on Windows.
|
||||
- Revamp the row detail page of the database.
|
||||
- Revamp the checklist cell editor of the database.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Some UI issues
|
||||
|
||||
## Version 0.3.1 - 09/04/2023
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -20,11 +20,12 @@ run_task = { name = ["restore-crate-type"] }
|
||||
|
||||
[env]
|
||||
RUST_LOG = "info"
|
||||
CARGO_PROFILE = "dev"
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.3.1"
|
||||
CURRENT_APP_VERSION = "0.3.3"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
@@ -75,6 +76,7 @@ BUILD_ACTIVE_ARCHS_ONLY = true
|
||||
CRATE_TYPE = "staticlib"
|
||||
|
||||
[env.production-mac-arm64]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "aarch64-apple-darwin"
|
||||
@@ -86,6 +88,7 @@ BUILD_ACTIVE_ARCHS_ONLY = false
|
||||
CRATE_TYPE = "staticlib"
|
||||
|
||||
[env.production-mac-x86_64]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "x86_64-apple-darwin"
|
||||
@@ -97,6 +100,7 @@ BUILD_ACTIVE_ARCHS_ONLY = false
|
||||
CRATE_TYPE = "staticlib"
|
||||
|
||||
[env.production-mac-universal]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "macos"
|
||||
FLUTTER_OUTPUT_DIR = "Release"
|
||||
@@ -114,6 +118,7 @@ CRATE_TYPE = "cdylib"
|
||||
LIB_EXT = "dll"
|
||||
|
||||
[env.production-windows-x86]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "windows"
|
||||
RUST_COMPILE_TARGET = "x86_64-pc-windows-msvc"
|
||||
@@ -133,6 +138,7 @@ LIB_EXT = "so"
|
||||
LINUX_ARCH = "x64"
|
||||
|
||||
[env.production-linux-x86_64]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "linux"
|
||||
RUST_COMPILE_TARGET = "x86_64-unknown-linux-gnu"
|
||||
@@ -153,6 +159,7 @@ LINUX_ARCH = "arm64"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite,openssl_vendored"
|
||||
|
||||
[env.production-linux-aarch64]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "linux"
|
||||
RUST_COMPILE_TARGET = "aarch64-unknown-linux-gnu"
|
||||
@@ -202,6 +209,7 @@ FLUTTER_OUTPUT_DIR = "Debug"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite,openssl_vendored"
|
||||
|
||||
[env.production-android]
|
||||
CARGO_PROFILE = "release"
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "android"
|
||||
CRATE_TYPE = "cdylib"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
@@ -80,7 +80,8 @@ void main() {
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
|
||||
expect(find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
|
||||
expect(
|
||||
find.textContaining(DefaultAppearanceSettings.kDefaultFontFamily),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'util/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('calendar database view', () {
|
||||
group('calendar', () {
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -79,33 +79,33 @@ void main() {
|
||||
// Tap on create new event button
|
||||
await tester.tapAddCalendarEventButton();
|
||||
|
||||
// Make sure that the row details page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
// Make sure that the event editor popup is shown
|
||||
tester.assertEventEditorOpen();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
|
||||
// Dismiss the event editor popup
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Double click on today's calendar cell to create a new event
|
||||
await tester.doubleClickCalendarCell(DateTime.now());
|
||||
|
||||
// Make sure that the row details page is opened
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
// Make sure that the event editor popup is shown
|
||||
tester.assertEventEditorOpen();
|
||||
|
||||
// Make sure that the event is inserted in the cell
|
||||
tester.assertNumberOfEventsInCalendar(2);
|
||||
|
||||
// Dismiss the event editor popup
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
tester.assertRowDetailPageOpened();
|
||||
tester.assertEventEditorOpen();
|
||||
|
||||
// Change the title of the event
|
||||
await tester.editTitleInRowDetailPage('hello world');
|
||||
await tester.dismissRowDetailPage();
|
||||
await tester.editEventTitle('hello world');
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Make sure that the event is edited
|
||||
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
|
||||
@@ -113,9 +113,14 @@ void main() {
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
tester.assertEventEditorOpen();
|
||||
|
||||
// Click on the open icon
|
||||
await tester.openEventToRowDetailPage();
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Duplicate the event
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDuplicateRowButton();
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
@@ -125,11 +130,23 @@ void main() {
|
||||
|
||||
// Delete an event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
await tester.deleteEventFromEventEditor();
|
||||
|
||||
// Check that there is 1 event
|
||||
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
|
||||
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
|
||||
|
||||
// Delete event from row detail page
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
await tester.openEventToRowDetailPage();
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
|
||||
// Check that there is 0 event
|
||||
tester.assertNumberOfEventsInCalendar(0, title: 'hello world');
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now());
|
||||
});
|
||||
|
||||
testWidgets('rescheduling events', (tester) async {
|
||||
@@ -143,7 +160,7 @@ void main() {
|
||||
final today = DateTime.now();
|
||||
final firstOfThisMonth = DateTime(today.year, today.month, 1);
|
||||
await tester.doubleClickCalendarCell(firstOfThisMonth);
|
||||
await tester.dismissRowDetailPage();
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Drag and drop the event onto the next week, same day
|
||||
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
|
||||
@@ -155,12 +172,12 @@ void main() {
|
||||
|
||||
// Delete the event
|
||||
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
await tester.deleteEventFromEventEditor();
|
||||
|
||||
// Create a new event in today's calendar cell
|
||||
await tester.scrollToToday();
|
||||
await tester.doubleClickCalendarCell(today);
|
||||
await tester.dismissRowDetailPage();
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Make sure that the event is today
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, today);
|
||||
@@ -179,12 +196,43 @@ void main() {
|
||||
await tester.selectDay(content: newDate.day);
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// Dismiss the row details page
|
||||
await tester.dismissRowDetailPage();
|
||||
// Dismiss the event editor
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Make sure that the event is edited
|
||||
tester.assertNumberOfEventsInCalendar(1);
|
||||
tester.assertNumberOfEventsOnSpecificDay(1, newDate);
|
||||
|
||||
// Click on the unscheduled events button
|
||||
await tester.openUnscheduledEventsPopup();
|
||||
|
||||
// Assert that nothing shows up
|
||||
tester.findUnscheduledPopup(findsNothing, 0);
|
||||
|
||||
// Click on the event in the calendar
|
||||
await tester.openCalendarEvent(index: 0, date: newDate);
|
||||
|
||||
// Open the date editor of the event
|
||||
await tester.tapDateCellInRowDetailPage();
|
||||
await tester.findDateEditor(findsOneWidget);
|
||||
|
||||
// Clear the date of the event
|
||||
await tester.clearDate();
|
||||
|
||||
// Dismiss the event editor
|
||||
await tester.dismissEventEditor();
|
||||
tester.assertNumberOfEventsInCalendar(0);
|
||||
|
||||
// Click on the unscheduled events button
|
||||
await tester.openUnscheduledEventsPopup();
|
||||
|
||||
// Assert that a popup appears and 1 unscheduled event
|
||||
tester.findUnscheduledPopup(findsOneWidget, 1);
|
||||
|
||||
// Click on the unscheduled event
|
||||
await tester.clickUnscheduledEvent();
|
||||
|
||||
tester.assertRowDetailPageOpened();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -185,7 +185,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit time cell', (tester) async {
|
||||
testWidgets('edit date time cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -218,7 +218,6 @@ void main() {
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('MMM dd, y').format(today),
|
||||
);
|
||||
|
||||
@@ -233,7 +232,6 @@ void main() {
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('MMM dd, y HH:mm').format(now),
|
||||
);
|
||||
|
||||
@@ -247,7 +245,6 @@ void main() {
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('dd/MM/y HH:mm').format(now),
|
||||
);
|
||||
|
||||
@@ -261,7 +258,6 @@ void main() {
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: DateFormat('dd/MM/y hh:mm a').format(now),
|
||||
);
|
||||
|
||||
@@ -273,7 +269,6 @@ void main() {
|
||||
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: 0,
|
||||
fieldType: fieldType,
|
||||
content: '',
|
||||
);
|
||||
|
||||
@@ -442,4 +437,106 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('edit checklist cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
const fieldType = FieldType.Checklist;
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
|
||||
// assert that there is no progress bar in the grid
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
|
||||
// tap on the first checklist cell
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// assert that the checklist editor is shown
|
||||
tester.assertChecklistEditorVisible(visible: true);
|
||||
|
||||
// create a new task with enter
|
||||
await tester.createNewChecklistTask(name: "task 0", enter: true);
|
||||
|
||||
// assert that the task is displayed
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 0",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// update the task's name
|
||||
await tester.renameChecklistTask(index: 0, name: "task 1");
|
||||
|
||||
// assert that the task's name is updated
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 1",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// dismiss new task editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// dismiss checklist cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progress bar is shown in grid at 0%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
|
||||
|
||||
// start editing the first checklist cell again
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// create another task with the create button
|
||||
await tester.createNewChecklistTask(name: "task 2", button: true);
|
||||
|
||||
// assert that the task was inserted
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// mark it as complete
|
||||
await tester.checkChecklistTask(index: 1);
|
||||
|
||||
// assert that the task was checked in the editor
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: true,
|
||||
);
|
||||
|
||||
// dismiss checklist editor
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progressbar is shown in grid at 50%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
|
||||
|
||||
// re-open the cell editor
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// hover over first task and delete it
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
|
||||
// dismiss cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progressbar is shown in grid at 100%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
|
||||
|
||||
// re-open the cell edior
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// delete the remaining task
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
|
||||
// dismiss the cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// check that the progress bar is not viisble
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
});
|
||||
}
|
||||
@@ -135,7 +135,40 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('check document is exist in row detail page', (tester) async {
|
||||
testWidgets('change order of fields and cells', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// Create a new grid
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// Assert that the first field in the row details page is the select
|
||||
// option tyoe
|
||||
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
|
||||
|
||||
// Reorder first field in list
|
||||
final gesture = await tester.hoverOnFieldInRowDetail(index: 0);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.reorderFieldInRowDetail(offset: 30);
|
||||
|
||||
// Orders changed, now the checkbox is first
|
||||
tester.assertFirstFieldInRowDetailByType(FieldType.Checkbox);
|
||||
await gesture.removePointer();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Reorder second field in list
|
||||
await tester.hoverOnFieldInRowDetail(index: 1);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.reorderFieldInRowDetail(offset: -30);
|
||||
|
||||
// First field is now back to select option
|
||||
tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);
|
||||
});
|
||||
|
||||
testWidgets('check document exists in row detail page', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -149,7 +182,7 @@ void main() {
|
||||
await tester.assertDocumentExistInRowDetailPage();
|
||||
});
|
||||
|
||||
testWidgets('update the content of the document and re-open it',
|
||||
testWidgets('update the contents of the document and re-open it',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -239,6 +272,7 @@ void main() {
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
await tester.tapEscButton();
|
||||
|
||||
@@ -255,6 +289,7 @@ void main() {
|
||||
// Hover first row and then open the row page
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDuplicateRowButton();
|
||||
await tester.tapEscButton();
|
||||
|
||||
|
||||
@@ -122,17 +122,17 @@ void main() {
|
||||
}
|
||||
|
||||
// check the checklist cell
|
||||
final List<double> checklistCells = [
|
||||
0.6,
|
||||
0.3,
|
||||
final List<double?> checklistCells = [
|
||||
0.67,
|
||||
0.33,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
];
|
||||
for (final (index, percent) in checklistCells.indexed) {
|
||||
await tester.assertChecklistCellInGrid(
|
||||
@@ -157,7 +157,6 @@ void main() {
|
||||
for (final (index, content) in dateCells.indexed) {
|
||||
await tester.assertDateCellInGrid(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.DateTime,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -250,6 +250,32 @@ void main() {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
extension on WidgetTester {
|
||||
|
||||
@@ -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,6 +5,8 @@ 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_inline_math_equation_test.dart'
|
||||
@@ -29,4 +31,6 @@ 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();
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
@@ -7,7 +8,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -22,14 +23,11 @@ void main() {
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new page
|
||||
const name = 'Hello AppFlowy';
|
||||
await tester.tapNewPageButton();
|
||||
await tester.enterText(find.byType(TextFormField), name);
|
||||
await tester.tapOKButton();
|
||||
|
||||
// expect to see a new document
|
||||
tester.expectToSeePageName(
|
||||
name,
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
);
|
||||
// and with one paragraph block
|
||||
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/user/presentation/sign_in_screen.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/sync_setting_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -7,15 +7,15 @@ import 'base.dart';
|
||||
|
||||
extension AppFlowyAuthTest on WidgetTester {
|
||||
Future<void> tapGoogleLoginInButton() async {
|
||||
await tapButton(find.byType(GoogleSignUpButton));
|
||||
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
|
||||
}
|
||||
|
||||
Future<void> tapSignInAsGuest() async {
|
||||
await tapButton(find.byType(SignInAsGuestButton));
|
||||
await tapButton(find.byType(SignInAnonymousButton));
|
||||
}
|
||||
|
||||
void expectToSeeGoogleLoginButton() {
|
||||
expect(find.byType(GoogleSignUpButton), findsOneWidget);
|
||||
expect(find.byKey(const Key('signInWithGoogleButton')), findsOneWidget);
|
||||
}
|
||||
|
||||
void assertSwitchValue(Finder finder, bool value) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/presentation.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -9,8 +11,8 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class FlowyTestContext {
|
||||
FlowyTestContext({
|
||||
@@ -39,8 +41,8 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
IntegrationMode.integrationTest,
|
||||
);
|
||||
|
||||
await wait(3000);
|
||||
await pumpAndSettle(const Duration(seconds: 2));
|
||||
await waitUntilSignInPageShow();
|
||||
|
||||
return FlowyTestContext(
|
||||
applicationDataDirectory: directory,
|
||||
);
|
||||
@@ -78,6 +80,27 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
return directory.path;
|
||||
}
|
||||
|
||||
Future<void> waitUntilSignInPageShow() async {
|
||||
final finder = find.byType(GoButton);
|
||||
await pumpUntilFound(finder);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> pumpUntilFound(
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
bool timerDone = false;
|
||||
final timer = Timer(timeout, () => timerDone = true);
|
||||
while (timerDone != true) {
|
||||
await pump();
|
||||
if (any(finder)) {
|
||||
timerDone = true;
|
||||
}
|
||||
}
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
Future<void> tapButton(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
@@ -279,7 +282,14 @@ extension CommonOperations on WidgetTester {
|
||||
// create a new page
|
||||
await tapAddViewButton(name: parentName ?? gettingStarted);
|
||||
await tapButtonWithName(layout.menuName);
|
||||
await tapOKButton();
|
||||
final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(
|
||||
KVKeys.showRenameDialogWhenCreatingNewFile,
|
||||
(value) => bool.parse(value),
|
||||
);
|
||||
final showRenameDialog = settingsOrFailure.fold((l) => false, (r) => r);
|
||||
if (showRenameDialog) {
|
||||
await tapOKButton();
|
||||
}
|
||||
await pumpAndSettle();
|
||||
|
||||
// hover on it and change it's name
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
@@ -24,6 +26,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart';
|
||||
@@ -38,6 +41,7 @@ import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'
|
||||
import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
@@ -47,6 +51,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
|
||||
@@ -241,36 +246,43 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
/// null percent means no progress bar should be found
|
||||
Future<void> assertChecklistCellInGrid({
|
||||
required int rowIndex,
|
||||
required double percent,
|
||||
required double? percent,
|
||||
}) async {
|
||||
final findCell = cellFinder(rowIndex, FieldType.Checklist);
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is ChecklistProgressBar) {
|
||||
return widget.percent == percent;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
|
||||
if (percent == null) {
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byType(ChecklistProgressBar),
|
||||
);
|
||||
expect(finder, findsNothing);
|
||||
} else {
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is ChecklistProgressBar) {
|
||||
return widget.percent == percent;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> assertDateCellInGrid({
|
||||
required int rowIndex,
|
||||
required FieldType fieldType,
|
||||
required String content,
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow, skipOffstage: false);
|
||||
final findCell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is GridDateCell && widget.fieldType == fieldType,
|
||||
),
|
||||
matching: find.byType(GridDateCell),
|
||||
skipOffstage: false,
|
||||
);
|
||||
|
||||
@@ -319,7 +331,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,
|
||||
@@ -451,6 +473,106 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(cell, matcher);
|
||||
}
|
||||
|
||||
Future<void> tapChecklistCellInGrid({required int rowIndex}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
final findCell = finderForFieldType(FieldType.Checklist);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
);
|
||||
|
||||
await tapButton(cell);
|
||||
}
|
||||
|
||||
void assertChecklistEditorVisible({required bool visible}) {
|
||||
final editor = find.byType(GridChecklistCellEditor);
|
||||
if (visible) {
|
||||
expect(editor, findsOneWidget);
|
||||
} else {
|
||||
expect(editor, findsNothing);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createNewChecklistTask({
|
||||
required String name,
|
||||
enter = false,
|
||||
button = false,
|
||||
}) async {
|
||||
assert(!(enter && button));
|
||||
final textField = find.descendant(
|
||||
of: find.byType(NewTaskItem),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
|
||||
await enterText(textField, name);
|
||||
await pumpAndSettle();
|
||||
if (enter) {
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle();
|
||||
} else {
|
||||
await tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(NewTaskItem),
|
||||
matching: find.byType(FlowyTextButton),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void assertChecklistTaskInEditor({
|
||||
required int index,
|
||||
required String name,
|
||||
required bool isChecked,
|
||||
}) {
|
||||
final task = find.byType(ChecklistItem).at(index);
|
||||
|
||||
final widget = this.widget<ChecklistItem>(task);
|
||||
assert(
|
||||
widget.option.data.name == name && widget.option.isSelected == isChecked,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> renameChecklistTask({
|
||||
required int index,
|
||||
required String name,
|
||||
}) async {
|
||||
final textField = find
|
||||
.descendant(
|
||||
of: find.byType(ChecklistItem),
|
||||
matching: find.byType(TextField),
|
||||
)
|
||||
.at(index);
|
||||
|
||||
await enterText(textField, name);
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> checkChecklistTask({required int index}) async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(ChecklistItem).at(index),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s,
|
||||
),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> deleteChecklistTask({required int index}) async {
|
||||
final task = find.byType(ChecklistItem).at(index);
|
||||
|
||||
await startGesture(getCenter(task), kind: PointerDeviceKind.mouse);
|
||||
await pumpAndSettle();
|
||||
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> openFirstRowDetailPage() async {
|
||||
await hoverOnFirstRowOfGrid();
|
||||
|
||||
@@ -487,7 +609,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(banner, findsOneWidget);
|
||||
|
||||
await startGesture(
|
||||
getTopLeft(banner),
|
||||
getCenter(banner),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
|
||||
@@ -526,6 +648,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(deleteButton);
|
||||
}
|
||||
|
||||
Future<TestGesture> hoverOnFieldInRowDetail({required int index}) async {
|
||||
final fieldButtons = find.byType(FieldCellButton);
|
||||
final button = find
|
||||
.descendant(of: find.byType(RowDetailPage), matching: fieldButtons)
|
||||
.at(index);
|
||||
return startGesture(
|
||||
getCenter(button),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> reorderFieldInRowDetail({required double offset}) async {
|
||||
final thumb = find
|
||||
.byWidgetPredicate(
|
||||
(widget) => widget is ReorderableDragStartListener && widget.enabled,
|
||||
)
|
||||
.first;
|
||||
await drag(
|
||||
thumb,
|
||||
Offset(0, offset),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> scrollGridByOffset(Offset offset) async {
|
||||
await drag(find.byType(GridPage), offset);
|
||||
await pumpAndSettle();
|
||||
@@ -603,6 +750,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapRowDetailPageRowActionButton() async {
|
||||
await tapButton(find.byType(RowActionButton));
|
||||
}
|
||||
|
||||
Future<void> tapRowDetailPageCreatePropertyButton() async {
|
||||
await tapButton(find.byType(CreateRowFieldButton));
|
||||
}
|
||||
@@ -672,6 +823,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(field, findsOneWidget);
|
||||
}
|
||||
|
||||
void assertFirstFieldInRowDetailByType(FieldType fieldType) {
|
||||
final firstField = find
|
||||
.descendant(
|
||||
of: find.byType(RowDetailPage),
|
||||
matching: find.byType(FieldCellButton),
|
||||
)
|
||||
.first;
|
||||
|
||||
final widget = this.widget<FieldCellButton>(firstField);
|
||||
expect(widget.field.fieldType, fieldType);
|
||||
}
|
||||
|
||||
Future<void> findFieldWithName(String name) async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) => widget is FieldCellButton && widget.field.name == name,
|
||||
@@ -736,9 +899,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);
|
||||
}
|
||||
|
||||
@@ -1129,12 +1290,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);
|
||||
@@ -1287,7 +1508,7 @@ Finder finderForFieldType(FieldType fieldType) {
|
||||
return find.byType(GridDateCell, skipOffstage: false);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return find.byType(GridDateCell, skipOffstage: false);
|
||||
return find.byType(GridTimestampCell, skipOffstage: false);
|
||||
case FieldType.SingleSelect:
|
||||
return find.byType(GridSingleSelectCell, skipOffstage: false);
|
||||
case FieldType.MultiSelect:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -174,4 +175,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
|
||||
@@ -50,6 +50,8 @@ PODS:
|
||||
- Toast
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- irondash_engine_context (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
@@ -66,6 +68,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- sign_in_with_apple (0.0.1):
|
||||
- Flutter
|
||||
- super_native_extensions (0.0.1):
|
||||
- Flutter
|
||||
- SwiftyGif (5.4.3)
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
@@ -83,11 +87,13 @@ DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
|
||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
|
||||
@@ -119,6 +125,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
irondash_engine_context:
|
||||
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
@@ -129,6 +137,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sign_in_with_apple:
|
||||
:path: ".symlinks/plugins/sign_in_with_apple/ios"
|
||||
super_native_extensions:
|
||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
webview_flutter_wkwebview:
|
||||
@@ -146,6 +156,7 @@ SPEC CHECKSUMS:
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
|
||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
@@ -153,6 +164,7 @@ SPEC CHECKSUMS:
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c
|
||||
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
|
||||
super_native_extensions: 49e897b6039bb784226e7354e502a3c68e1659b5
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
|
||||
@@ -16,6 +16,17 @@
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>appflowy-flutter</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
@@ -7,6 +7,10 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
abstract class KeyValueStorage {
|
||||
Future<void> set(String key, String value);
|
||||
Future<Either<FlowyError, String>> get(String key);
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
);
|
||||
Future<void> remove(String key);
|
||||
Future<void> clear();
|
||||
}
|
||||
@@ -26,6 +30,18 @@ class DartKeyValue implements KeyValueStorage {
|
||||
return Left(FlowyError());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
) async {
|
||||
final value = await get(key);
|
||||
return value.fold(
|
||||
(l) => left(l),
|
||||
(r) => right(formatter(r)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String key) async {
|
||||
await _initSharedPreferencesIfNeeded();
|
||||
@@ -71,6 +87,18 @@ class RustKeyValue implements KeyValueStorage {
|
||||
return response.swap().map((r) => r.value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
) async {
|
||||
final value = await get(key);
|
||||
return value.fold(
|
||||
(l) => left(l),
|
||||
(r) => right(formatter(r)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String key) async {
|
||||
await ConfigEventRemoveKeyValue(
|
||||
|
||||
@@ -29,6 +29,8 @@ class KVKeys {
|
||||
'kDocumentAppearanceFontSize';
|
||||
static const String kDocumentAppearanceFontFamily =
|
||||
'kDocumentAppearanceFontFamily';
|
||||
static const String kDocumentAppearanceDefaultTextDirection =
|
||||
'kDocumentAppearanceDefaultTextDirection';
|
||||
|
||||
/// The key for saving the expanded views
|
||||
///
|
||||
@@ -41,4 +43,10 @@ class KVKeys {
|
||||
/// The value is a json string with the following format:
|
||||
/// {'SidebarFolderCategoryType.value': true}
|
||||
static const String expandedFolders = 'expandedFolders';
|
||||
|
||||
/// The key for saving if showing the rename dialog when creating a new file
|
||||
///
|
||||
/// The value is a boolean string.
|
||||
static const String showRenameDialogWhenCreatingNewFile =
|
||||
'showRenameDialogWhenCreatingNewFile';
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+16
-3
@@ -2,6 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
|
||||
import 'cell_controller.dart';
|
||||
@@ -14,6 +15,7 @@ typedef SelectOptionCellController
|
||||
= CellController<SelectOptionCellDataPB, String>;
|
||||
typedef ChecklistCellController = CellController<ChecklistCellDataPB, String>;
|
||||
typedef DateCellController = CellController<DateCellDataPB, String>;
|
||||
typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
|
||||
typedef URLCellController = CellController<URLCellDataPB, String>;
|
||||
|
||||
class CellControllerBuilder {
|
||||
@@ -41,14 +43,11 @@ class CellControllerBuilder {
|
||||
TextCellDataPersistence(cellContext: _cellContext),
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
final cellDataLoader = CellDataLoader(
|
||||
cellContext: _cellContext,
|
||||
parser: DateCellDataParser(),
|
||||
reloadOnFieldChanged: true,
|
||||
);
|
||||
|
||||
return DateCellController(
|
||||
cellContext: _cellContext,
|
||||
cellCache: _cellCache,
|
||||
@@ -56,6 +55,20 @@ class CellControllerBuilder {
|
||||
cellDataPersistence:
|
||||
TextCellDataPersistence(cellContext: _cellContext),
|
||||
);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
final cellDataLoader = CellDataLoader(
|
||||
cellContext: _cellContext,
|
||||
parser: TimestampCellDataParser(),
|
||||
reloadOnFieldChanged: true,
|
||||
);
|
||||
return TimestampCellController(
|
||||
cellContext: _cellContext,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence:
|
||||
TextCellDataPersistence(cellContext: _cellContext),
|
||||
);
|
||||
case FieldType.Number:
|
||||
final cellDataLoader = CellDataLoader(
|
||||
cellContext: _cellContext,
|
||||
|
||||
+10
@@ -73,6 +73,16 @@ class DateCellDataParser implements CellDataParser<DateCellDataPB> {
|
||||
}
|
||||
}
|
||||
|
||||
class TimestampCellDataParser implements CellDataParser<TimestampCellDataPB> {
|
||||
@override
|
||||
TimestampCellDataPB? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return TimestampCellDataPB.fromBuffer(data);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionCellDataParser
|
||||
implements CellDataParser<SelectOptionCellDataPB> {
|
||||
@override
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
|
||||
+8
-2
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
class ChecklistCellBackendService {
|
||||
final String viewId;
|
||||
@@ -52,14 +53,19 @@ class ChecklistCellBackendService {
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> update({
|
||||
Future<Either<Unit, FlowyError>> updateName({
|
||||
required SelectOptionPB option,
|
||||
required name,
|
||||
}) {
|
||||
option.freeze();
|
||||
final newOption = option.rebuild((option) {
|
||||
option.name = name;
|
||||
});
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..updateOptions.add(option);
|
||||
..updateOptions.add(newOption);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
}
|
||||
|
||||
+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();
|
||||
}
|
||||
|
||||
+1
-7
@@ -395,13 +395,7 @@ class RowDataBuilder {
|
||||
}
|
||||
|
||||
void insertDate(FieldInfo fieldInfo, DateTime date) {
|
||||
assert(
|
||||
[
|
||||
FieldType.DateTime,
|
||||
FieldType.LastEditedTime,
|
||||
FieldType.CreatedTime,
|
||||
].contains(fieldInfo.fieldType),
|
||||
);
|
||||
assert(FieldType.DateTime == fieldInfo.fieldType);
|
||||
final timestamp = date.millisecondsSinceEpoch ~/ 1000;
|
||||
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
|
||||
}
|
||||
|
||||
+9
-1
@@ -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';
|
||||
@@ -29,8 +31,11 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
||||
didReceiveFieldUpdate: (field) {
|
||||
emit(state.copyWith(field: cellContext.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 +71,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 +83,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(),
|
||||
resizeStart: 0,
|
||||
);
|
||||
}
|
||||
-10
@@ -27,13 +27,6 @@ class DateTypeOptionBloc
|
||||
),
|
||||
);
|
||||
},
|
||||
includeTime: (_IncludeTime value) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
typeOption: _updateTypeOption(includeTime: value.includeTime),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -42,7 +35,6 @@ class DateTypeOptionBloc
|
||||
DateTypeOptionPB _updateTypeOption({
|
||||
DateFormatPB? dateFormat,
|
||||
TimeFormatPB? timeFormat,
|
||||
bool? includeTime,
|
||||
}) {
|
||||
state.typeOption.freeze();
|
||||
return state.typeOption.rebuild((typeOption) {
|
||||
@@ -63,8 +55,6 @@ class DateTypeOptionEvent with _$DateTypeOptionEvent {
|
||||
_DidSelectDateFormat;
|
||||
const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) =
|
||||
_DidSelectTimeFormat;
|
||||
const factory DateTypeOptionEvent.includeTime(bool includeTime) =
|
||||
_IncludeTime;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
import 'type_option_context.dart';
|
||||
part 'timestamp_bloc.freezed.dart';
|
||||
|
||||
class TimestampTypeOptionBloc
|
||||
extends Bloc<TimestampTypeOptionEvent, TimestampTypeOptionState> {
|
||||
TimestampTypeOptionBloc({
|
||||
required TimestampTypeOptionContext typeOptionContext,
|
||||
}) : super(TimestampTypeOptionState.initial(typeOptionContext.typeOption)) {
|
||||
on<TimestampTypeOptionEvent>(
|
||||
(event, emit) async {
|
||||
event.map(
|
||||
didSelectDateFormat: (_DidSelectDateFormat value) {
|
||||
_updateTypeOption(dateFormat: value.format, emit: emit);
|
||||
},
|
||||
didSelectTimeFormat: (_DidSelectTimeFormat value) {
|
||||
_updateTypeOption(timeFormat: value.format, emit: emit);
|
||||
},
|
||||
includeTime: (_IncludeTime value) {
|
||||
_updateTypeOption(includeTime: value.includeTime, emit: emit);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateTypeOption({
|
||||
DateFormatPB? dateFormat,
|
||||
TimeFormatPB? timeFormat,
|
||||
bool? includeTime,
|
||||
required Emitter<TimestampTypeOptionState> emit,
|
||||
}) {
|
||||
state.typeOption.freeze();
|
||||
final newTypeOption = state.typeOption.rebuild((typeOption) {
|
||||
if (dateFormat != null) {
|
||||
typeOption.dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
if (timeFormat != null) {
|
||||
typeOption.timeFormat = timeFormat;
|
||||
}
|
||||
|
||||
if (includeTime != null) {
|
||||
typeOption.includeTime = includeTime;
|
||||
}
|
||||
});
|
||||
emit(state.copyWith(typeOption: newTypeOption));
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampTypeOptionEvent with _$TimestampTypeOptionEvent {
|
||||
const factory TimestampTypeOptionEvent.didSelectDateFormat(
|
||||
DateFormatPB format,
|
||||
) = _DidSelectDateFormat;
|
||||
const factory TimestampTypeOptionEvent.didSelectTimeFormat(
|
||||
TimeFormatPB format,
|
||||
) = _DidSelectTimeFormat;
|
||||
const factory TimestampTypeOptionEvent.includeTime(bool includeTime) =
|
||||
_IncludeTime;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampTypeOptionState with _$TimestampTypeOptionState {
|
||||
const factory TimestampTypeOptionState({
|
||||
required TimestampTypeOptionPB typeOption,
|
||||
}) = _TimestampTypeOptionState;
|
||||
|
||||
factory TimestampTypeOptionState.initial(TimestampTypeOptionPB typeOption) =>
|
||||
TimestampTypeOptionState(typeOption: typeOption);
|
||||
}
|
||||
+13
-1
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
@@ -58,7 +59,7 @@ class URLTypeOptionWidgetDataParser extends TypeOptionParser<URLTypeOptionPB> {
|
||||
}
|
||||
}
|
||||
|
||||
// Date
|
||||
// DateTime
|
||||
typedef DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
|
||||
|
||||
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
||||
@@ -68,6 +69,17 @@ class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
||||
}
|
||||
}
|
||||
|
||||
// LastModified and CreatedAt
|
||||
typedef TimestampTypeOptionContext = TypeOptionContext<TimestampTypeOptionPB>;
|
||||
|
||||
class TimestampTypeOptionDataParser
|
||||
extends TypeOptionParser<TimestampTypeOptionPB> {
|
||||
@override
|
||||
TimestampTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
return TimestampTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
// SingleSelect
|
||||
typedef SingleSelectTypeOptionContext
|
||||
= TypeOptionContext<SingleSelectTypeOptionPB>;
|
||||
|
||||
+1
-5
@@ -32,11 +32,7 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
|
||||
await _listenRowMeteChanged();
|
||||
},
|
||||
didReceiveRowMeta: (RowMetaPB rowMeta) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
rowMeta: rowMeta,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(rowMeta: rowMeta));
|
||||
},
|
||||
setCover: (String coverURL) {
|
||||
_updateMeta(coverURL: coverURL);
|
||||
|
||||
+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,
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
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();
|
||||
if (!isClosed) {
|
||||
add(
|
||||
CalendarEventEditorEvent.didReceiveCellDatas(
|
||||
cells.values.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
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) {
|
||||
add(
|
||||
CalendarEventEditorEvent.didReceiveCellDatas(cells.values.toList()),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
rowController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CalendarEventEditorEvent with _$CalendarEventEditorEvent {
|
||||
const factory CalendarEventEditorEvent.initial() = _Initial;
|
||||
const factory CalendarEventEditorEvent.didReceiveCellDatas(
|
||||
List<DatabaseCellContext> cells,
|
||||
) = _DidReceiveCellDatas;
|
||||
const factory CalendarEventEditorEvent.delete() = _Delete;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CalendarEventEditorState with _$CalendarEventEditorState {
|
||||
const factory CalendarEventEditorState({
|
||||
required List<DatabaseCellContext> cells,
|
||||
}) = _CalendarEventEditorState;
|
||||
|
||||
factory CalendarEventEditorState.initial() =>
|
||||
CalendarEventEditorState(cells: List.empty());
|
||||
}
|
||||
+4
-1
@@ -58,9 +58,12 @@ class UnscheduleEventsBloc
|
||||
);
|
||||
},
|
||||
didReceiveEvent: (CalendarEventPB event) {
|
||||
final events = [...state.allEvents, event];
|
||||
emit(
|
||||
state.copyWith(
|
||||
allEvents: [...state.allEvents, event],
|
||||
allEvents: events,
|
||||
unscheduleEvents:
|
||||
events.where((element) => !element.isScheduled).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+102
-56
@@ -1,13 +1,14 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import 'package:flowy_infra/size.dart';
|
||||
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
import '../../grid/presentation/layout/sizes.dart';
|
||||
import '../application/calendar_bloc.dart';
|
||||
@@ -18,7 +19,7 @@ class CalendarDayCard extends StatelessWidget {
|
||||
final bool isToday;
|
||||
final bool isInMonth;
|
||||
final DateTime date;
|
||||
final RowCache _rowCache;
|
||||
final RowCache rowCache;
|
||||
final List<CalendarDayEvent> events;
|
||||
final void Function(DateTime) onCreateEvent;
|
||||
|
||||
@@ -28,18 +29,21 @@ class CalendarDayCard extends StatelessWidget {
|
||||
required this.isInMonth,
|
||||
required this.date,
|
||||
required this.onCreateEvent,
|
||||
required RowCache rowCache,
|
||||
required this.rowCache,
|
||||
required this.events,
|
||||
Key? key,
|
||||
}) : _rowCache = rowCache,
|
||||
super(key: key);
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color backgroundColor = Theme.of(context).colorScheme.surface;
|
||||
if (!isInMonth) {
|
||||
backgroundColor = AFThemeExtension.of(context).lightGreyHover;
|
||||
Color backgroundColor = Colors.transparent;
|
||||
if (date.isWeekend) {
|
||||
backgroundColor = AFThemeExtension.of(context).calendarWeekendBGColor;
|
||||
}
|
||||
final hoverBackgroundColor =
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Colors.transparent;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
@@ -57,14 +61,14 @@ class CalendarDayCard extends StatelessWidget {
|
||||
),
|
||||
|
||||
// Add a separator between the header and the content.
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
const VSpace(6.0),
|
||||
|
||||
// List of cards or empty space
|
||||
if (events.isNotEmpty)
|
||||
_EventList(
|
||||
events: events,
|
||||
viewId: viewId,
|
||||
rowCache: _rowCache,
|
||||
rowCache: rowCache,
|
||||
constraints: constraints,
|
||||
),
|
||||
],
|
||||
@@ -79,34 +83,29 @@ class CalendarDayCard extends StatelessWidget {
|
||||
DragTarget<CalendarDayEvent>(
|
||||
builder: (context, candidate, __) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (candidate.isNotEmpty)
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color:
|
||||
candidate.isEmpty ? null : hoverBackgroundColor,
|
||||
padding: const EdgeInsets.only(top: 5.0),
|
||||
child: child,
|
||||
)
|
||||
),
|
||||
if (candidate.isEmpty)
|
||||
NewEventButton(onCreate: () => onCreateEvent(date)),
|
||||
],
|
||||
);
|
||||
},
|
||||
onWillAccept: (CalendarDayEvent? event) {
|
||||
if (event == null) {
|
||||
return false;
|
||||
}
|
||||
return !isSameDay(event.date, date);
|
||||
},
|
||||
onAccept: (CalendarDayEvent event) {
|
||||
if (event.date == date) {
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<CalendarBloc>()
|
||||
.add(CalendarEvent.moveEvent(event, date));
|
||||
},
|
||||
),
|
||||
NewEventButton(onCreate: () => onCreateEvent(date)),
|
||||
MouseRegion(
|
||||
onEnter: (p) => notifyEnter(context, true),
|
||||
onExit: (p) => notifyEnter(context, false),
|
||||
@@ -143,7 +142,7 @@ class _Header extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: _DayBadge(
|
||||
isToday: isToday,
|
||||
isInMonth: isInMonth,
|
||||
@@ -166,7 +165,7 @@ class NewEventButton extends StatelessWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: FlowyIconButton(
|
||||
onPressed: onCreate,
|
||||
iconPadding: EdgeInsets.zero,
|
||||
@@ -174,6 +173,35 @@ class NewEventButton extends StatelessWidget {
|
||||
fillColor: Theme.of(context).colorScheme.background,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
width: 22,
|
||||
tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0xffd0d3d6)
|
||||
: const Color(0xff59647a),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
borderRadius: Corners.s5Border,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
spreadRadius: -2,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 2,
|
||||
),
|
||||
BoxShadow(
|
||||
spreadRadius: 0,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 4,
|
||||
),
|
||||
BoxShadow(
|
||||
spreadRadius: 2,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -195,37 +223,49 @@ class _DayBadge extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color dayTextColor = Theme.of(context).colorScheme.onBackground;
|
||||
Color monthTextColor = Theme.of(context).colorScheme.onBackground;
|
||||
final String monthString =
|
||||
DateFormat("MMM ", context.locale.toLanguageTag()).format(date);
|
||||
final String dayString = date.day.toString();
|
||||
|
||||
if (!isInMonth) {
|
||||
dayTextColor = Theme.of(context).disabledColor;
|
||||
monthTextColor = Theme.of(context).disabledColor;
|
||||
}
|
||||
if (isToday) {
|
||||
dayTextColor = Theme.of(context).colorScheme.onPrimary;
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (date.day == 1) FlowyText.medium(monthString),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? Theme.of(context).colorScheme.primary : null,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
width: isToday ? 26 : null,
|
||||
height: isToday ? 26 : null,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
child: Center(
|
||||
child: FlowyText.medium(
|
||||
dayString,
|
||||
color: dayTextColor,
|
||||
return SizedBox(
|
||||
height: 18,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (date.day == 1)
|
||||
FlowyText.medium(
|
||||
monthString,
|
||||
fontSize: 11,
|
||||
color: monthTextColor,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isToday ? Theme.of(context).colorScheme.primary : null,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
width: isToday ? 18 : null,
|
||||
height: isToday ? 18 : null,
|
||||
// padding: GridSize.typeOptionContentInsets,
|
||||
child: Center(
|
||||
child: FlowyText.medium(
|
||||
dayString,
|
||||
fontSize: 11,
|
||||
color: dayTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -246,20 +286,26 @@ class _EventList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final editingEvent = context.watch<CalendarBloc>().state.editingEvent;
|
||||
return Flexible(
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
scrollbars: true,
|
||||
),
|
||||
child: ListView.separated(
|
||||
itemBuilder: (BuildContext context, int index) => EventCard(
|
||||
event: events[index],
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
constraints: constraints,
|
||||
),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final autoEdit =
|
||||
editingEvent?.event?.eventId == events[index].eventId;
|
||||
return EventCard(
|
||||
event: events[index],
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
constraints: constraints,
|
||||
autoEdit: autoEdit,
|
||||
);
|
||||
},
|
||||
itemCount: events.length,
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
|
||||
padding: const EdgeInsets.fromLTRB(4.0, 0, 4.0, 4.0),
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
|
||||
+129
-36
@@ -1,47 +1,73 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/bloc/text_card_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/number_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/url_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../application/calendar_bloc.dart';
|
||||
import 'calendar_page.dart';
|
||||
import 'calendar_event_editor.dart';
|
||||
|
||||
class EventCard extends StatelessWidget {
|
||||
class EventCard extends StatefulWidget {
|
||||
final CalendarDayEvent event;
|
||||
final String viewId;
|
||||
final RowCache rowCache;
|
||||
final BoxConstraints constraints;
|
||||
final bool autoEdit;
|
||||
|
||||
const EventCard({
|
||||
required this.event,
|
||||
required this.viewId,
|
||||
required this.rowCache,
|
||||
required this.constraints,
|
||||
required this.autoEdit,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EventCard> createState() => _EventCardState();
|
||||
}
|
||||
|
||||
class _EventCardState extends State<EventCard> {
|
||||
late final PopoverController _popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_popoverController = PopoverController();
|
||||
if (widget.autoEdit) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_popoverController.show();
|
||||
context
|
||||
.read<CalendarBloc>()
|
||||
.add(const CalendarEvent.newEventPopupDisplayed());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rowInfo = rowCache.getRow(event.eventId);
|
||||
final rowInfo = widget.rowCache.getRow(widget.event.eventId);
|
||||
if (rowInfo == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final styles = <FieldType, CardCellStyle>{
|
||||
FieldType.Number: NumberCardCellStyle(10),
|
||||
FieldType.URL: URLCardCellStyle(10),
|
||||
};
|
||||
final cellBuilder = CardCellBuilder<CalendarDayEvent>(
|
||||
rowCache.cellCache,
|
||||
widget.rowCache.cellCache,
|
||||
styles: styles,
|
||||
);
|
||||
final renderHook = _calendarEventCardRenderHook(context);
|
||||
@@ -49,24 +75,22 @@ class EventCard extends StatelessWidget {
|
||||
final card = RowCard<CalendarDayEvent>(
|
||||
// Add the key here to make sure the card is rebuilt when the cells
|
||||
// in this row are updated.
|
||||
key: ValueKey(event.eventId),
|
||||
rowMeta: rowInfo!.rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
cardData: event,
|
||||
key: ValueKey(widget.event.eventId),
|
||||
rowMeta: rowInfo.rowMeta,
|
||||
viewId: widget.viewId,
|
||||
rowCache: widget.rowCache,
|
||||
cardData: widget.event,
|
||||
isEditing: false,
|
||||
cellBuilder: cellBuilder,
|
||||
openCard: (context) => showEventDetails(
|
||||
context: context,
|
||||
event: event.event,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
),
|
||||
openCard: (_) => _popoverController.show(),
|
||||
styleConfiguration: RowCardStyleConfiguration(
|
||||
showAccessory: false,
|
||||
cellPadding: EdgeInsets.zero,
|
||||
cardPadding: const EdgeInsets.all(6),
|
||||
hoverStyle: HoverStyle(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0x0F1F2329)
|
||||
: const Color(0x0FEFF4FB),
|
||||
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
@@ -76,28 +100,76 @@ class EventCard extends StatelessWidget {
|
||||
);
|
||||
|
||||
final decoration = BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).dividerColor),
|
||||
BorderSide(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0xffd0d3d6)
|
||||
: const Color(0xff59647a),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
spreadRadius: -2,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 2,
|
||||
),
|
||||
BoxShadow(
|
||||
spreadRadius: 0,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 4,
|
||||
),
|
||||
BoxShadow(
|
||||
spreadRadius: 2,
|
||||
color: const Color(0xFF1F2329).withOpacity(0.02),
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Draggable<CalendarDayEvent>(
|
||||
data: event,
|
||||
data: widget.event,
|
||||
feedback: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth - 16.0,
|
||||
maxWidth: widget.constraints.maxWidth - 8.0,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: decoration.copyWith(
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
child: Opacity(
|
||||
opacity: 0.6,
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: card,
|
||||
),
|
||||
child: card,
|
||||
),
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: card,
|
||||
child: AppFlowyPopover(
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.rightWithCenterAligned,
|
||||
controller: _popoverController,
|
||||
constraints: const BoxConstraints(maxWidth: 360, maxHeight: 348),
|
||||
asBarrier: true,
|
||||
margin: EdgeInsets.zero,
|
||||
offset: const Offset(10.0, 0),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
final settings = context.watch<CalendarBloc>().state.settings.fold(
|
||||
() => null,
|
||||
(layoutSettings) => layoutSettings,
|
||||
);
|
||||
if (settings == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return CalendarEventEditor(
|
||||
rowCache: widget.rowCache,
|
||||
rowMeta: widget.event.event.rowMeta,
|
||||
viewId: widget.viewId,
|
||||
layoutSettings: settings,
|
||||
);
|
||||
},
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
child: card,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -107,13 +179,10 @@ class EventCard extends StatelessWidget {
|
||||
) {
|
||||
final renderHook = RowCardRenderHook<CalendarDayEvent>();
|
||||
renderHook.addTextCellHook((cellData, eventData, _) {
|
||||
return BlocBuilder<TextCardCellBloc, TextCardCellState>(
|
||||
return BlocBuilder<TextCellBloc, TextCellState>(
|
||||
builder: (context, state) {
|
||||
final isTitle = context
|
||||
.read<TextCardCellBloc>()
|
||||
.cellController
|
||||
.fieldInfo
|
||||
.isPrimary;
|
||||
final isTitle =
|
||||
context.read<TextCellBloc>().cellController.fieldInfo.isPrimary;
|
||||
final text = isTitle && cellData.isEmpty
|
||||
? LocaleKeys.grid_row_titlePlaceholder.tr()
|
||||
: cellData;
|
||||
@@ -127,8 +196,9 @@ class EventCard extends StatelessWidget {
|
||||
child: FlowyText.medium(
|
||||
text,
|
||||
textAlign: TextAlign.left,
|
||||
fontSize: 11,
|
||||
maxLines: null, // Enable multiple lines
|
||||
fontSize: isTitle ? 11 : 10,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -167,6 +237,29 @@ class EventCard extends StatelessWidget {
|
||||
);
|
||||
});
|
||||
|
||||
renderHook.addTimestampCellHook((cellData, cardData, _) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: FlowyText.regular(
|
||||
cellData.dateTime,
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
renderHook.addSelectOptionHook((selectedOptions, cardData, _) {
|
||||
if (selectedOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_event_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class CalendarEventEditor extends StatelessWidget {
|
||||
final RowController rowController;
|
||||
final CalendarLayoutSettingPB layoutSettings;
|
||||
final GridCellBuilder cellBuilder;
|
||||
|
||||
CalendarEventEditor({
|
||||
super.key,
|
||||
required RowCache rowCache,
|
||||
required RowMetaPB rowMeta,
|
||||
required String viewId,
|
||||
required this.layoutSettings,
|
||||
}) : rowController = RowController(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
),
|
||||
cellBuilder = GridCellBuilder(cellCache: rowCache.cellCache);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<CalendarEventEditorBloc>(
|
||||
create: (context) => CalendarEventEditorBloc(
|
||||
rowController: rowController,
|
||||
layoutSettings: layoutSettings,
|
||||
)..add(const CalendarEventEditorEvent.initial()),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
EventEditorControls(rowController: rowController),
|
||||
Flexible(
|
||||
child: EventPropertyList(
|
||||
dateFieldId: layoutSettings.fieldId,
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EventEditorControls extends StatelessWidget {
|
||||
final RowController rowController;
|
||||
const EventEditorControls({
|
||||
super.key,
|
||||
required this.rowController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FlowyIconButton(
|
||||
width: 20,
|
||||
icon: const FlowySvg(FlowySvgs.delete_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
|
||||
onPressed: () => context
|
||||
.read<CalendarEventEditorBloc>()
|
||||
.add(const CalendarEventEditorEvent.delete()),
|
||||
),
|
||||
const HSpace(8.0),
|
||||
FlowyIconButton(
|
||||
width: 20,
|
||||
icon: const FlowySvg(FlowySvgs.full_view_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
|
||||
onPressed: () {
|
||||
PopoverContainer.of(context).close();
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(
|
||||
cellCache: rowController.cellCache,
|
||||
),
|
||||
rowController: rowController,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EventPropertyList extends StatelessWidget {
|
||||
final String dateFieldId;
|
||||
final GridCellBuilder cellBuilder;
|
||||
const EventPropertyList({
|
||||
super.key,
|
||||
required this.dateFieldId,
|
||||
required this.cellBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CalendarEventEditorBloc, CalendarEventEditorState>(
|
||||
builder: (context, state) {
|
||||
final reorderedList = List<DatabaseCellContext>.from(state.cells)
|
||||
..retainWhere((cell) => !cell.fieldInfo.isPrimary);
|
||||
|
||||
final primaryCellContext =
|
||||
state.cells.firstWhereOrNull((cell) => cell.fieldInfo.isPrimary);
|
||||
final dateFieldIndex =
|
||||
reorderedList.indexWhere((cell) => cell.fieldId == dateFieldId);
|
||||
if (primaryCellContext == null || dateFieldIndex == -1) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
reorderedList.insert(0, reorderedList.removeAt(dateFieldIndex));
|
||||
|
||||
final children = <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
|
||||
child: cellBuilder.build(
|
||||
primaryCellContext,
|
||||
style: GridTextCellStyle(
|
||||
cellPadding: EdgeInsets.zero,
|
||||
placeholder: LocaleKeys.calendar_defaultNewCalendarTitle.tr(),
|
||||
textStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontSize: 11),
|
||||
autofocus: true,
|
||||
useRoundedBorder: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
...reorderedList.map(
|
||||
(cell) => PropertyCell(cellContext: cell, cellBuilder: cellBuilder),
|
||||
),
|
||||
];
|
||||
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PropertyCell extends StatefulWidget {
|
||||
final DatabaseCellContext cellContext;
|
||||
final GridCellBuilder cellBuilder;
|
||||
const PropertyCell({
|
||||
required this.cellContext,
|
||||
required this.cellBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _PropertyCellState();
|
||||
}
|
||||
|
||||
class _PropertyCellState extends State<PropertyCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = _customCellStyle(widget.cellContext.fieldType);
|
||||
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
||||
|
||||
final gesture = GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => cell.requestFocus.notify(),
|
||||
child: AccessoryHover(child: cell),
|
||||
);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
constraints: const BoxConstraints(minHeight: 28),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 88,
|
||||
height: 28,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
widget.cellContext.fieldType.icon(),
|
||||
color: Theme.of(context).hintColor,
|
||||
size: const Size.square(14),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
FlowyText.regular(
|
||||
widget.cellContext.fieldInfo.name,
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 11,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(child: gesture),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return GridCheckboxCellStyle(
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
return DateCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
alignment: Alignment.centerLeft,
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return TimestampCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
alignment: Alignment.centerLeft,
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
);
|
||||
case FieldType.Checklist:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+31
-19
@@ -1,23 +1,20 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import '../../../application/cell/cell_service.dart';
|
||||
import '../../../application/field/field_service.dart';
|
||||
import '../../../application/row/row_controller.dart';
|
||||
|
||||
part 'row_detail_bloc.freezed.dart';
|
||||
|
||||
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
final RowBackendService rowService;
|
||||
final RowController rowController;
|
||||
|
||||
RowDetailBloc({
|
||||
required this.rowController,
|
||||
}) : rowService = RowBackendService(viewId: rowController.viewId),
|
||||
super(RowDetailState.initial()) {
|
||||
}) : super(RowDetailState.initial()) {
|
||||
on<RowDetailEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
@@ -58,14 +55,8 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
deleteRow: (rowId) async {
|
||||
await rowService.deleteRow(rowId);
|
||||
},
|
||||
duplicateRow: (String rowId, String? groupId) async {
|
||||
await rowService.duplicateRow(
|
||||
rowId: rowId,
|
||||
groupId: groupId,
|
||||
);
|
||||
reorderField: (fieldId, fromIndex, toIndex) async {
|
||||
await _reorderField(fieldId, fromIndex, toIndex, emit);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -94,6 +85,25 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
fieldId: fieldId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _reorderField(
|
||||
String fieldId,
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
Emitter<RowDetailState> emit,
|
||||
) async {
|
||||
final cells = List<DatabaseCellContext>.from(state.cells);
|
||||
cells.insert(toIndex, cells.removeAt(fromIndex));
|
||||
emit(state.copyWith(cells: cells));
|
||||
|
||||
final fieldService =
|
||||
FieldBackendService(viewId: rowController.viewId, fieldId: fieldId);
|
||||
final result = await fieldService.moveField(
|
||||
fromIndex,
|
||||
toIndex,
|
||||
);
|
||||
result.fold((l) {}, (err) => Log.error(err));
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -102,9 +112,11 @@ class RowDetailEvent with _$RowDetailEvent {
|
||||
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
|
||||
const factory RowDetailEvent.showField(String fieldId) = _ShowField;
|
||||
const factory RowDetailEvent.hideField(String fieldId) = _HideField;
|
||||
const factory RowDetailEvent.deleteRow(String rowId) = _DeleteRow;
|
||||
const factory RowDetailEvent.duplicateRow(String rowId, String? groupId) =
|
||||
_DuplicateRow;
|
||||
const factory RowDetailEvent.reorderField(
|
||||
String fieldId,
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
) = _ReorderField;
|
||||
const factory RowDetailEvent.didReceiveCellDatas(
|
||||
List<DatabaseCellContext> gridCells,
|
||||
) = _DidReceiveCellDatas;
|
||||
|
||||
+50
-66
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
|
||||
@@ -7,10 +5,10 @@ import 'package:appflowy/plugins/database_view/tar_bar/setting_menu.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -257,59 +255,38 @@ class _GridRows extends StatelessWidget {
|
||||
GridState state,
|
||||
List<RowInfo> rowInfos,
|
||||
) {
|
||||
if (Platform.isWindows) {
|
||||
// Workaround: On Windows, the focusing of the text cell is not working
|
||||
// properly when the list is reorderable. So using the ListView instead.
|
||||
return ListView.builder(
|
||||
controller: scrollController.verticalController,
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: false,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return ReorderableListView.builder(
|
||||
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
|
||||
/// This is a workaround related to
|
||||
/// https://github.com/flutter/flutter/issues/25652
|
||||
cacheExtent: 5000,
|
||||
scrollController: scrollController.verticalController,
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.white.withOpacity(.1),
|
||||
child: Opacity(opacity: .5, child: child),
|
||||
),
|
||||
onReorder: (fromIndex, newIndex) {
|
||||
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
|
||||
if (fromIndex == toIndex) {
|
||||
return;
|
||||
}
|
||||
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
|
||||
},
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: state.reorderable,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
);
|
||||
}
|
||||
return ReorderableListView.builder(
|
||||
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
|
||||
/// This is a workaround related to
|
||||
/// https://github.com/flutter/flutter/issues/25652
|
||||
cacheExtent: 5000,
|
||||
scrollController: scrollController.verticalController,
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.white.withOpacity(.1),
|
||||
child: Opacity(opacity: .5, child: child),
|
||||
),
|
||||
onReorder: (fromIndex, newIndex) {
|
||||
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
|
||||
if (fromIndex == toIndex) {
|
||||
return;
|
||||
}
|
||||
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
|
||||
},
|
||||
itemCount: rowInfos.length + 1, // the extra item is the footer
|
||||
itemBuilder: (context, index) {
|
||||
if (index < rowInfos.length) {
|
||||
final rowInfo = rowInfos[index];
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: state.reorderable,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
return const GridRowBottomBar(key: Key('gridFooter'));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderRow(
|
||||
@@ -433,14 +410,21 @@ class _GridFooter extends StatelessWidget {
|
||||
builder: (context, rowCount) {
|
||||
return Padding(
|
||||
padding: GridSize.contentInsets,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
rowCountString(rowCount),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: rowCountString(),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' $rowCount',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: AFThemeExtension.of(context).gridRowCountColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -448,6 +432,6 @@ class _GridFooter extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String rowCountString(int count) {
|
||||
return '${LocaleKeys.grid_row_count.tr()} : $count';
|
||||
String rowCountString() {
|
||||
return '${LocaleKeys.grid_row_count.tr()} :';
|
||||
}
|
||||
+2
-2
@@ -114,7 +114,7 @@ class _GridCreateFilterListState extends State<GridCreateFilterList> {
|
||||
class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
_FilterTextFieldDelegate();
|
||||
|
||||
double fixHeight = 46;
|
||||
double fixHeight = 36;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
@@ -123,7 +123,7 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
bool overlapsContent,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
height: fixHeight,
|
||||
child: FlowyTextField(
|
||||
hintText: LocaleKeys.grid_settings_filterBy.tr(),
|
||||
|
||||
-1
@@ -105,7 +105,6 @@ class _AddFilterButtonState extends State<AddFilterButton> {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
constraints: BoxConstraints.loose(const Size(200, 300)),
|
||||
margin: const EdgeInsets.all(6),
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
child: child,
|
||||
popupBuilder: (BuildContext context) {
|
||||
|
||||
+12
-3
@@ -125,10 +125,15 @@ class _DragToExpandLine extends StatelessWidget {
|
||||
onTap: () {},
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragStart: (details) {
|
||||
context
|
||||
.read<FieldCellBloc>()
|
||||
.add(const FieldCellEvent.onResizeStart());
|
||||
},
|
||||
onHorizontalDragUpdate: (value) {
|
||||
context
|
||||
.read<FieldCellBloc>()
|
||||
.add(FieldCellEvent.startUpdateWidth(value.delta.dx));
|
||||
.add(FieldCellEvent.startUpdateWidth(value.localPosition.dx));
|
||||
},
|
||||
onHorizontalDragEnd: (end) {
|
||||
context
|
||||
@@ -154,29 +159,33 @@ class FieldCellButton extends StatelessWidget {
|
||||
final FieldPB field;
|
||||
final int? maxLines;
|
||||
final BorderRadius? radius;
|
||||
final EdgeInsets? margin;
|
||||
const FieldCellButton({
|
||||
required this.field,
|
||||
required this.onTap,
|
||||
this.maxLines = 1,
|
||||
this.radius = BorderRadius.zero,
|
||||
this.margin,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).greyHover,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onTap: onTap,
|
||||
leftIcon: FlowySvg(
|
||||
field.fieldType.icon(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
radius: radius,
|
||||
text: FlowyText.medium(
|
||||
field.name,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
margin: GridSize.cellContentInsets,
|
||||
margin: margin ?? GridSize.cellContentInsets,
|
||||
);
|
||||
}
|
||||
}
|
||||
+16
@@ -52,8 +52,13 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool requireSpace = widget.onDeleted != null ||
|
||||
widget.onHidden != 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.typeOptionLoader.field.isPrimary)
|
||||
@@ -200,6 +205,17 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.removeListener(() {
|
||||
if (focusNode.hasFocus) {
|
||||
widget.popoverMutex.close();
|
||||
}
|
||||
});
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteFieldButton extends StatelessWidget {
|
||||
|
||||
+18
-4
@@ -8,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:protobuf/protobuf.dart' hide FieldInfo;
|
||||
@@ -20,6 +21,7 @@ import 'multi_select.dart';
|
||||
import 'number.dart';
|
||||
import 'rich_text.dart';
|
||||
import 'single_select.dart';
|
||||
import 'timestamp.dart';
|
||||
import 'url.dart';
|
||||
|
||||
typedef TypeOptionData = Uint8List;
|
||||
@@ -73,8 +75,6 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
|
||||
),
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return DateTypeOptionWidgetBuilder(
|
||||
makeTypeOptionContextWithDataController<DateTypeOptionPB>(
|
||||
viewId: viewId,
|
||||
@@ -83,6 +83,16 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
|
||||
),
|
||||
popoverMutex,
|
||||
);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return TimestampTypeOptionWidgetBuilder(
|
||||
makeTypeOptionContextWithDataController<TimestampTypeOptionPB>(
|
||||
viewId: viewId,
|
||||
fieldType: fieldType,
|
||||
dataController: dataController,
|
||||
),
|
||||
popoverMutex,
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return SingleSelectTypeOptionWidgetBuilder(
|
||||
makeTypeOptionContextWithDataController<SingleSelectTypeOptionPB>(
|
||||
@@ -203,12 +213,16 @@ TypeOptionContext<T>
|
||||
dataParser: CheckboxTypeOptionWidgetDataParser(),
|
||||
) as TypeOptionContext<T>;
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return DateTypeOptionContext(
|
||||
dataController: dataController,
|
||||
dataParser: DateTypeOptionDataParser(),
|
||||
) as TypeOptionContext<T>;
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return TimestampTypeOptionContext(
|
||||
dataController: dataController,
|
||||
dataParser: TimestampTypeOptionDataParser(),
|
||||
) as TypeOptionContext<T>;
|
||||
case FieldType.SingleSelect:
|
||||
return SingleSelectTypeOptionContext(
|
||||
dataController: dataController,
|
||||
|
||||
+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();
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/timestamp_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.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';
|
||||
|
||||
import 'builder.dart';
|
||||
import 'date.dart';
|
||||
|
||||
class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
|
||||
final TimestampTypeOptionWidget _widget;
|
||||
|
||||
TimestampTypeOptionWidgetBuilder(
|
||||
TimestampTypeOptionContext typeOptionContext,
|
||||
PopoverMutex popoverMutex,
|
||||
) : _widget = TimestampTypeOptionWidget(
|
||||
typeOptionContext: typeOptionContext,
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget? build(BuildContext context) {
|
||||
return _widget;
|
||||
}
|
||||
}
|
||||
|
||||
class TimestampTypeOptionWidget extends TypeOptionWidget {
|
||||
final TimestampTypeOptionContext typeOptionContext;
|
||||
final PopoverMutex popoverMutex;
|
||||
const TimestampTypeOptionWidget({
|
||||
required this.typeOptionContext,
|
||||
required this.popoverMutex,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
TimestampTypeOptionBloc(typeOptionContext: typeOptionContext),
|
||||
child: BlocConsumer<TimestampTypeOptionBloc, TimestampTypeOptionState>(
|
||||
listener: (context, state) =>
|
||||
typeOptionContext.typeOption = state.typeOption,
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [
|
||||
const TypeOptionSeparator(),
|
||||
_renderDateFormatButton(context, state.typeOption.dateFormat),
|
||||
_renderTimeFormatButton(context, state.typeOption.timeFormat),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: IncludeTimeButton(
|
||||
onChanged: (value) => context
|
||||
.read<TimestampTypeOptionBloc>()
|
||||
.add(TimestampTypeOptionEvent.includeTime(!value)),
|
||||
value: state.typeOption.includeTime,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
separatorBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
return VSpace(GridSize.typeOptionSeparatorHeight);
|
||||
}
|
||||
},
|
||||
itemCount: children.length,
|
||||
itemBuilder: (BuildContext context, int index) => children[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderDateFormatButton(
|
||||
BuildContext context,
|
||||
DateFormatPB dataFormat,
|
||||
) {
|
||||
return AppFlowyPopover(
|
||||
mutex: popoverMutex,
|
||||
asBarrier: true,
|
||||
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||
offset: const Offset(8, 0),
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
popupBuilder: (popoverContext) {
|
||||
return DateFormatList(
|
||||
selectedFormat: dataFormat,
|
||||
onSelected: (format) {
|
||||
context
|
||||
.read<TimestampTypeOptionBloc>()
|
||||
.add(TimestampTypeOptionEvent.didSelectDateFormat(format));
|
||||
PopoverContainer.of(popoverContext).close();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: DateFormatButton(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderTimeFormatButton(
|
||||
BuildContext context,
|
||||
TimeFormatPB timeFormat,
|
||||
) {
|
||||
return AppFlowyPopover(
|
||||
mutex: popoverMutex,
|
||||
asBarrier: true,
|
||||
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||
offset: const Offset(8, 0),
|
||||
constraints: BoxConstraints.loose(const Size(460, 440)),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return TimeFormatList(
|
||||
selectedFormat: timeFormat,
|
||||
onSelected: (format) {
|
||||
context
|
||||
.read<TimestampTypeOptionBloc>()
|
||||
.add(TimestampTypeOptionEvent.didSelectTimeFormat(format));
|
||||
PopoverContainer.of(popoverContext).close();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: TimeFormatButton(timeFormat: timeFormat),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IncludeTimeButton extends StatelessWidget {
|
||||
final bool value;
|
||||
final Function(bool value) onChanged;
|
||||
const IncludeTimeButton({
|
||||
super.key,
|
||||
required this.onChanged,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: Padding(
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.clock_alarm_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
const HSpace(6),
|
||||
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
|
||||
const Spacer(),
|
||||
Toggle(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
style: ToggleStyle.big,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -40,8 +40,7 @@ class RowActions extends StatelessWidget {
|
||||
.map((action) => _ActionCell(action: action))
|
||||
.toList();
|
||||
|
||||
//
|
||||
final list = ListView.separated(
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
controller: ScrollController(),
|
||||
itemCount: cells.length,
|
||||
@@ -53,7 +52,6 @@ class RowActions extends StatelessWidget {
|
||||
return cells[index];
|
||||
},
|
||||
);
|
||||
return list;
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -70,6 +68,7 @@ class _ActionCell extends StatelessWidget {
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.medium(
|
||||
action.title(),
|
||||
color: action.enable()
|
||||
|
||||
+23
-7
@@ -161,16 +161,12 @@ class _RowLeadingState extends State<_RowLeading> {
|
||||
index: widget.index!,
|
||||
child: RowMenuButton(
|
||||
isDragEnabled: isDraggable,
|
||||
openMenu: () {
|
||||
popoverController.show();
|
||||
},
|
||||
openMenu: popoverController.show,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
RowMenuButton(
|
||||
openMenu: () {
|
||||
popoverController.show();
|
||||
},
|
||||
openMenu: popoverController.show,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -261,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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -306,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 {
|
||||
|
||||
+2
-2
@@ -114,7 +114,7 @@ class _GridCreateSortListState extends State<GridCreateSortList> {
|
||||
class _SortTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
_SortTextFieldDelegate();
|
||||
|
||||
double fixHeight = 46;
|
||||
double fixHeight = 36;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
@@ -123,7 +123,7 @@ class _SortTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
bool overlapsContent,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
height: fixHeight,
|
||||
child: FlowyTextField(
|
||||
hintText: LocaleKeys.grid_settings_sortBy.tr(),
|
||||
|
||||
+1
@@ -210,6 +210,7 @@ class DatabaseDeleteSortButton extends StatelessWidget {
|
||||
context
|
||||
.read<SortEditorBloc>()
|
||||
.add(const SortEditorEvent.deleteAllSorts());
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
leftIcon: const FlowySvg(FlowySvgs.delete_s),
|
||||
),
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
-86
@@ -1,86 +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) {
|
||||
dateStr = "${cellData.date} ${cellData.time}";
|
||||
}
|
||||
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_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();
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -10,6 +10,7 @@ import 'cells/date_card_cell.dart';
|
||||
import 'cells/number_card_cell.dart';
|
||||
import 'cells/select_option_card_cell.dart';
|
||||
import 'cells/text_card_cell.dart';
|
||||
import 'cells/timestamp_card_cell.dart';
|
||||
import 'cells/url_card_cell.dart';
|
||||
|
||||
// T represents as the Generic card data
|
||||
@@ -39,13 +40,23 @@ class CardCellBuilder<CustomCardData> {
|
||||
key: key,
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return DateCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.DateTime],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.LastEditedTime:
|
||||
return TimestampCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.LastEditedTime],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.CreatedTime:
|
||||
return TimestampCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.CreatedTime],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return SelectOptionCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef CellRenderHook<C, CustomCardData> = Widget? Function(
|
||||
@@ -50,6 +51,16 @@ class RowCardRenderHook<CustomCardData> {
|
||||
renderHook[FieldType.DateTime] = _typeSafeHook<DateCellDataPB>(hook);
|
||||
}
|
||||
|
||||
/// Add a render hook for [FieldType.LastEditedTime] and [FieldType.CreatedTime]
|
||||
void addTimestampCellHook(
|
||||
CellRenderHook<TimestampCellDataPB, CustomCardData?> hook,
|
||||
) {
|
||||
renderHook[FieldType.LastEditedTime] =
|
||||
_typeSafeHook<TimestampCellDataPB>(hook);
|
||||
renderHook[FieldType.CreatedTime] =
|
||||
_typeSafeHook<TimestampCellDataPB>(hook);
|
||||
}
|
||||
|
||||
CellRenderHook<dynamic, CustomCardData> _typeSafeHook<C>(
|
||||
CellRenderHook<C, CustomCardData?> hook,
|
||||
) {
|
||||
|
||||
+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()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+15
-9
@@ -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,11 +31,17 @@ class _ChecklistCardCellState extends State<ChecklistCardCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ChecklistProgressBar(percent: state.percent),
|
||||
),
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ChecklistProgressBar(percent: state.percent),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+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,
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.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';
|
||||
|
||||
import '../define.dart';
|
||||
import 'card_cell.dart';
|
||||
|
||||
class TimestampCardCell<CustomCardData> extends CardCell {
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
final CellRenderHook<dynamic, CustomCardData>? renderHook;
|
||||
|
||||
const TimestampCardCell({
|
||||
required this.cellControllerBuilder,
|
||||
this.renderHook,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TimestampCardCell> createState() => _TimestampCellState();
|
||||
}
|
||||
|
||||
class _TimestampCellState extends State<TimestampCardCell> {
|
||||
late TimestampCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as TimestampCellController;
|
||||
|
||||
_cellBloc = TimestampCellBloc(cellController: cellController)
|
||||
..add(const TimestampCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
|
||||
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
|
||||
builder: (context, state) {
|
||||
if (state.dateStr.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final Widget? custom = widget.renderHook?.call(
|
||||
state.data,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: CardSizes.cardCellVPadding,
|
||||
),
|
||||
child: FlowyText.regular(
|
||||
state.dateStr,
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
+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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-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),
|
||||
|
||||
+22
-57
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -91,50 +91,31 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
|
||||
|
||||
class AccessoryHover extends StatefulWidget {
|
||||
final CellAccessory child;
|
||||
final EdgeInsets contentPadding;
|
||||
const AccessoryHover({
|
||||
required this.child,
|
||||
this.contentPadding = EdgeInsets.zero,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
const AccessoryHover({required this.child, super.key});
|
||||
|
||||
@override
|
||||
State<AccessoryHover> createState() => _AccessoryHoverState();
|
||||
}
|
||||
|
||||
class _AccessoryHoverState extends State<AccessoryHover> {
|
||||
late AccessoryHoverState _hoverState;
|
||||
VoidCallback? _listenerFn;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_hoverState = AccessoryHoverState();
|
||||
_listenerFn = () =>
|
||||
_hoverState.onHover = widget.child.onAccessoryHover?.value ?? false;
|
||||
widget.child.onAccessoryHover?.addListener(_listenerFn!);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hoverState.dispose();
|
||||
|
||||
if (_listenerFn != null) {
|
||||
widget.child.onAccessoryHover?.removeListener(_listenerFn!);
|
||||
_listenerFn = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
bool _isHover = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> children = [
|
||||
Padding(padding: widget.contentPadding, child: widget.child),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _isHover
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: Colors.transparent,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
];
|
||||
|
||||
final accessoryBuilder = widget.child.accessoryBuilder;
|
||||
if (accessoryBuilder != null) {
|
||||
if (accessoryBuilder != null && _isHover) {
|
||||
final accessories = accessoryBuilder(
|
||||
(GridCellAccessoryBuildContext(
|
||||
anchorContext: context,
|
||||
@@ -149,36 +130,20 @@ class _AccessoryHoverState extends State<AccessoryHover> {
|
||||
);
|
||||
}
|
||||
|
||||
return ChangeNotifierProvider.value(
|
||||
value: _hoverState,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
opaque: false,
|
||||
onEnter: (p) => setState(() => _hoverState.onHover = true),
|
||||
onExit: (p) => setState(() => _hoverState.onHover = false),
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: children,
|
||||
),
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
opaque: false,
|
||||
onEnter: (p) => setState(() => _isHover = true),
|
||||
onExit: (p) => setState(() => _isHover = false),
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccessoryHoverState extends ChangeNotifier {
|
||||
bool _onHover = false;
|
||||
|
||||
set onHover(bool value) {
|
||||
if (_onHover != value) {
|
||||
_onHover = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get onHover => _onHover;
|
||||
}
|
||||
|
||||
class CellAccessoryContainer extends StatelessWidget {
|
||||
final List<GridCellAccessoryBuilder> accessories;
|
||||
const CellAccessoryContainer({required this.accessories, Key? key})
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'cells/date_cell/date_cell.dart';
|
||||
import 'cells/number_cell/number_cell.dart';
|
||||
import 'cells/select_option_cell/select_option_cell.dart';
|
||||
import 'cells/text_cell/text_cell.dart';
|
||||
import 'cells/timestamp_cell/timestamp_cell.dart';
|
||||
import 'cells/url_cell/url_cell.dart';
|
||||
|
||||
/// Build the cell widget in Grid style.
|
||||
@@ -34,6 +35,7 @@ class GridCellBuilder {
|
||||
case FieldType.Checkbox:
|
||||
return GridCheckboxCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
style: style,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
@@ -41,14 +43,12 @@ class GridCellBuilder {
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
style: style,
|
||||
fieldType: cellContext.fieldType,
|
||||
);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return GridDateCell(
|
||||
return GridTimestampCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
editable: false,
|
||||
style: style,
|
||||
fieldType: cellContext.fieldType,
|
||||
);
|
||||
@@ -67,11 +67,13 @@ class GridCellBuilder {
|
||||
case FieldType.Checklist:
|
||||
return GridChecklistCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
style: style,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.Number:
|
||||
return GridNumberCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
style: style,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.RichText:
|
||||
@@ -245,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';
|
||||
+25
-10
@@ -1,20 +1,36 @@
|
||||
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;
|
||||
|
||||
GridCheckboxCellStyle({
|
||||
this.cellPadding,
|
||||
});
|
||||
}
|
||||
|
||||
class GridCheckboxCell extends GridCellWidget {
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
late final GridCheckboxCellStyle cellStyle;
|
||||
|
||||
GridCheckboxCell({
|
||||
required this.cellControllerBuilder,
|
||||
GridCellStyle? style,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
}) : super(key: key) {
|
||||
if (style != null) {
|
||||
cellStyle = (style as GridCheckboxCellStyle);
|
||||
} else {
|
||||
cellStyle = GridCheckboxCellStyle();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
GridCellState<GridCheckboxCell> createState() => _CheckboxCellState();
|
||||
@@ -27,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();
|
||||
}
|
||||
|
||||
@@ -46,7 +60,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
padding:
|
||||
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
|
||||
child: FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => context
|
||||
|
||||
+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";
|
||||
}
|
||||
+37
-10
@@ -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();
|
||||
}
|
||||
@@ -40,7 +56,7 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
child: AppFlowyPopover(
|
||||
margin: EdgeInsets.zero,
|
||||
controller: _popover,
|
||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
||||
constraints: BoxConstraints.loose(const Size(360, 400)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
popupBuilder: (BuildContext context) {
|
||||
@@ -53,11 +69,22 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
);
|
||||
},
|
||||
onClose: () => widget.onCellFocus.value = false,
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) =>
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+21
-12
@@ -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,
|
||||
@@ -29,13 +28,23 @@ class ChecklistCardCellBloc
|
||||
_loadOptions();
|
||||
},
|
||||
didReceiveOptions: (data) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allOptions: data.options,
|
||||
selectedOptions: data.selectedOptions,
|
||||
percent: data.percentage,
|
||||
),
|
||||
);
|
||||
if (data == null) {
|
||||
emit(
|
||||
const ChecklistCellState(
|
||||
allOptions: [],
|
||||
selectedOptions: [],
|
||||
percent: 0,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allOptions: data.options,
|
||||
selectedOptions: data.selectedOptions,
|
||||
percent: data.percentage,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -58,7 +67,7 @@ class ChecklistCardCellBloc
|
||||
_loadOptions();
|
||||
},
|
||||
onCellChanged: (data) {
|
||||
if (!isClosed && data != null) {
|
||||
if (!isClosed) {
|
||||
add(ChecklistCellEvent.didReceiveOptions(data));
|
||||
}
|
||||
},
|
||||
@@ -81,7 +90,7 @@ class ChecklistCardCellBloc
|
||||
class ChecklistCellEvent with _$ChecklistCellEvent {
|
||||
const factory ChecklistCellEvent.initial() = _InitialCell;
|
||||
const factory ChecklistCellEvent.didReceiveOptions(
|
||||
ChecklistCellDataPB data,
|
||||
ChecklistCellDataPB? data,
|
||||
) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
|
||||
+295
-126
@@ -1,21 +1,25 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
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';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart';
|
||||
import 'package:collection/collection.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/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../grid/presentation/layout/sizes.dart';
|
||||
import '../../../../grid/presentation/widgets/header/type_option/select_option_editor.dart';
|
||||
import 'checklist_cell_editor_bloc.dart';
|
||||
import 'checklist_progress_bar.dart';
|
||||
|
||||
class GridChecklistCellEditor extends StatefulWidget {
|
||||
final ChecklistCellController cellController;
|
||||
const GridChecklistCellEditor({required this.cellController, Key? key})
|
||||
: super(key: key);
|
||||
const GridChecklistCellEditor({required this.cellController, super.key});
|
||||
|
||||
@override
|
||||
State<GridChecklistCellEditor> createState() =>
|
||||
@@ -23,167 +27,332 @@ class GridChecklistCellEditor extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||
late ChecklistCellEditorBloc bloc;
|
||||
late PopoverMutex popoverMutex;
|
||||
late ChecklistCellEditorBloc _bloc;
|
||||
|
||||
/// Focus node for the new task text field
|
||||
late final FocusNode newTaskFocusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverMutex = PopoverMutex();
|
||||
bloc = ChecklistCellEditorBloc(cellController: widget.cellController);
|
||||
bloc.add(const ChecklistCellEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.close();
|
||||
super.dispose();
|
||||
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());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
value: _bloc,
|
||||
child: BlocConsumer<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
listener: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
newTaskFocusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final List<Widget> slivers = [
|
||||
const SliverChecklistProgressBar(),
|
||||
SliverToBoxAdapter(
|
||||
child: ListView.separated(
|
||||
controller: ScrollController(),
|
||||
shrinkWrap: true,
|
||||
itemCount: state.allOptions.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return _ChecklistOptionCell(
|
||||
option: state.allOptions[index],
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return VSpace(GridSize.typeOptionSeparatorHeight);
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ScrollConfiguration(
|
||||
behavior: const ScrollBehavior().copyWith(scrollbars: false),
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: slivers,
|
||||
controller: ScrollController(),
|
||||
physics: StyledScrollPhysics(),
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _ChecklistOptionCell extends StatefulWidget {
|
||||
/// Displays the a list of all the exisiting tasks and an input field to create
|
||||
/// a new task if `isAddingNewTask` is true
|
||||
class ChecklistItemList extends StatefulWidget {
|
||||
final List<ChecklistSelectOption> options;
|
||||
final VoidCallback onUpdateTask;
|
||||
|
||||
const ChecklistItemList({
|
||||
super.key,
|
||||
required this.options,
|
||||
required this.onUpdateTask,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChecklistItemList> createState() => _ChecklistItemListState();
|
||||
}
|
||||
|
||||
class _ChecklistItemListState extends State<ChecklistItemList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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),
|
||||
),
|
||||
)
|
||||
.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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an existing task
|
||||
@visibleForTesting
|
||||
class ChecklistItem extends StatefulWidget {
|
||||
final ChecklistSelectOption option;
|
||||
final PopoverMutex popoverMutex;
|
||||
const _ChecklistOptionCell({
|
||||
final VoidCallback? onSubmitted;
|
||||
const ChecklistItem({
|
||||
required this.option,
|
||||
required this.popoverMutex,
|
||||
Key? key,
|
||||
this.onSubmitted,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_ChecklistOptionCell> createState() => _ChecklistOptionCellState();
|
||||
State<ChecklistItem> createState() => _ChecklistItemState();
|
||||
}
|
||||
|
||||
class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
|
||||
late PopoverController _popoverController;
|
||||
class _ChecklistItemState extends State<ChecklistItem> {
|
||||
late final TextEditingController _textController;
|
||||
late final FocusNode _focusNode;
|
||||
bool _isHovered = false;
|
||||
Timer? _debounceOnChanged;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_popoverController = PopoverController();
|
||||
super.initState();
|
||||
_textController = TextEditingController(text: widget.option.data.name);
|
||||
_focusNode = FocusNode(
|
||||
onKey: (node, event) {
|
||||
if (event is RawKeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
node.unfocus();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = widget.option.isSelected
|
||||
? const FlowySvg(
|
||||
FlowySvgs.check_filled_s,
|
||||
blendMode: BlendMode.dst,
|
||||
)
|
||||
: const FlowySvg(FlowySvgs.uncheck_s);
|
||||
return _wrapPopover(
|
||||
SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText(
|
||||
widget.option.data.name,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: icon,
|
||||
onTap: () => context
|
||||
.read<ChecklistCellEditorBloc>()
|
||||
.add(ChecklistCellEditorEvent.selectOption(widget.option)),
|
||||
final icon = FlowySvg(
|
||||
widget.option.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
return MouseRegion(
|
||||
onEnter: (event) => setState(() => _isHovered = true),
|
||||
onExit: (event) => setState(() => _isHovered = false),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _isHovered
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: Colors.transparent,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
FlowyIconButton(
|
||||
width: 32,
|
||||
icon: icon,
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.selectTask(widget.option.data),
|
||||
),
|
||||
),
|
||||
),
|
||||
_disclosureButton(),
|
||||
],
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
focusNode: _focusNode,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 6.0,
|
||||
horizontal: 2.0,
|
||||
),
|
||||
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
|
||||
),
|
||||
onChanged: _debounceOnChangedText,
|
||||
onSubmitted: (description) {
|
||||
_submitUpdateTaskDescription(description);
|
||||
widget.onSubmitted?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_isHovered)
|
||||
FlowyIconButton(
|
||||
width: 32,
|
||||
icon: const FlowySvg(FlowySvgs.delete_s),
|
||||
hoverColor: Colors.transparent,
|
||||
iconColorOnHover: Theme.of(context).colorScheme.error,
|
||||
onPressed: () => context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.deleteTask(widget.option.data),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _disclosureButton() {
|
||||
return FlowyIconButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
width: 20,
|
||||
onPressed: () => _popoverController.show(),
|
||||
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
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.
|
||||
/// This can be cancelled by pressing escape
|
||||
@visibleForTesting
|
||||
class NewTaskItem extends StatefulWidget {
|
||||
final FocusNode focusNode;
|
||||
const NewTaskItem({super.key, required this.focusNode});
|
||||
|
||||
@override
|
||||
State<NewTaskItem> createState() => _NewTaskItemState();
|
||||
}
|
||||
|
||||
class _NewTaskItemState extends State<NewTaskItem> {
|
||||
late final TextEditingController _textEditingController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController = TextEditingController();
|
||||
if (widget.focusNode.canRequestFocus) {
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
focusNode: widget.focusNode,
|
||||
controller: _textEditingController,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 6.0,
|
||||
horizontal: 2.0,
|
||||
),
|
||||
hintText: LocaleKeys.grid_checklist_addNew.tr(),
|
||||
),
|
||||
onSubmitted: (taskDescription) {
|
||||
if (taskDescription.trim().isNotEmpty) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.newTask(
|
||||
taskDescription.trim(),
|
||||
),
|
||||
);
|
||||
}
|
||||
widget.focusNode.requestFocus();
|
||||
_textEditingController.clear();
|
||||
},
|
||||
onChanged: (value) => setState(() {}),
|
||||
),
|
||||
),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.grid_checklist_submitNewTask.tr(),
|
||||
fontSize: 11,
|
||||
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: () {
|
||||
if (_textEditingController.text.trim().isNotEmpty) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.newTask(
|
||||
_textEditingController.text..trim(),
|
||||
),
|
||||
);
|
||||
}
|
||||
widget.focusNode.requestFocus();
|
||||
_textEditingController.clear();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapPopover(Widget child) {
|
||||
return AppFlowyPopover(
|
||||
controller: _popoverController,
|
||||
offset: const Offset(8, 0),
|
||||
asBarrier: true,
|
||||
constraints: BoxConstraints.loose(const Size(200, 300)),
|
||||
mutex: widget.popoverMutex,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
child: child,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return SelectOptionTypeOptionEditor(
|
||||
option: widget.option.data,
|
||||
onDeleted: () {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.deleteOption(widget.option.data),
|
||||
);
|
||||
|
||||
_popoverController.close();
|
||||
},
|
||||
onUpdated: (updatedOption) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.updateOption(updatedOption),
|
||||
);
|
||||
},
|
||||
showOptions: false,
|
||||
autoFocus: false,
|
||||
// Use ValueKey to refresh the UI, otherwise, it will remain the old value.
|
||||
key: ValueKey(
|
||||
widget.option.data.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+41
-51
@@ -2,15 +2,22 @@ import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/checklist_cell_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'checklist_cell_editor_bloc.freezed.dart';
|
||||
|
||||
class ChecklistSelectOption {
|
||||
final bool isSelected;
|
||||
final SelectOptionPB data;
|
||||
|
||||
ChecklistSelectOption(this.isSelected, this.data);
|
||||
}
|
||||
|
||||
class ChecklistCellEditorBloc
|
||||
extends Bloc<ChecklistCellEditorEvent, ChecklistCellEditorState> {
|
||||
final ChecklistCellBackendService _checklistCellService;
|
||||
@@ -31,33 +38,31 @@ class ChecklistCellEditorBloc
|
||||
_startListening();
|
||||
_loadOptions();
|
||||
},
|
||||
didReceiveOptions: (data) {
|
||||
didReceiveTasks: (data) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allOptions: _makeChecklistSelectOptions(data, state.predicate),
|
||||
percent: data.percentage,
|
||||
allOptions: _makeChecklistSelectOptions(data),
|
||||
percent: data?.percentage ?? 0,
|
||||
),
|
||||
);
|
||||
},
|
||||
newOption: (optionName) {
|
||||
_createOption(optionName);
|
||||
newTask: (optionName) async {
|
||||
await _createOption(optionName);
|
||||
emit(
|
||||
state.copyWith(
|
||||
createOption: Some(optionName),
|
||||
predicate: '',
|
||||
),
|
||||
);
|
||||
},
|
||||
deleteOption: (option) {
|
||||
_deleteOption([option]);
|
||||
deleteTask: (option) async {
|
||||
await _deleteOption([option]);
|
||||
},
|
||||
updateOption: (option) {
|
||||
_updateOption(option);
|
||||
updateTaskName: (option, name) {
|
||||
_updateOption(option, name);
|
||||
},
|
||||
selectOption: (option) async {
|
||||
await _checklistCellService.select(optionId: option.data.id);
|
||||
selectTask: (option) async {
|
||||
await _checklistCellService.select(optionId: option.id);
|
||||
},
|
||||
filterOption: (String predicate) {},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -69,22 +74,21 @@ class ChecklistCellEditorBloc
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _createOption(String name) async {
|
||||
Future<void> _createOption(String name) async {
|
||||
final result = await _checklistCellService.create(name: name);
|
||||
result.fold((l) => {}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
void _deleteOption(List<SelectOptionPB> options) async {
|
||||
Future<void> _deleteOption(List<SelectOptionPB> options) async {
|
||||
final result = await _checklistCellService.delete(
|
||||
optionIds: options.map((e) => e.id).toList(),
|
||||
);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
void _updateOption(SelectOptionPB option) async {
|
||||
final result = await _checklistCellService.update(
|
||||
option: option,
|
||||
);
|
||||
void _updateOption(SelectOptionPB option, String name) async {
|
||||
final result =
|
||||
await _checklistCellService.updateName(option: option, name: name);
|
||||
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
@@ -94,7 +98,7 @@ class ChecklistCellEditorBloc
|
||||
if (isClosed) return;
|
||||
|
||||
return result.fold(
|
||||
(data) => add(ChecklistCellEditorEvent.didReceiveOptions(data)),
|
||||
(data) => add(ChecklistCellEditorEvent.didReceiveTasks(data)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
@@ -103,8 +107,8 @@ class ChecklistCellEditorBloc
|
||||
void _startListening() {
|
||||
cellController.startListening(
|
||||
onCellChanged: ((data) {
|
||||
if (!isClosed && data != null) {
|
||||
add(ChecklistCellEditorEvent.didReceiveOptions(data));
|
||||
if (!isClosed) {
|
||||
add(ChecklistCellEditorEvent.didReceiveTasks(data));
|
||||
}
|
||||
}),
|
||||
onCellFieldChanged: () {
|
||||
@@ -117,20 +121,19 @@ class ChecklistCellEditorBloc
|
||||
@freezed
|
||||
class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent {
|
||||
const factory ChecklistCellEditorEvent.initial() = _Initial;
|
||||
const factory ChecklistCellEditorEvent.didReceiveOptions(
|
||||
ChecklistCellDataPB data,
|
||||
) = _DidReceiveOptions;
|
||||
const factory ChecklistCellEditorEvent.newOption(String optionName) =
|
||||
_NewOption;
|
||||
const factory ChecklistCellEditorEvent.selectOption(
|
||||
ChecklistSelectOption option,
|
||||
) = _SelectOption;
|
||||
const factory ChecklistCellEditorEvent.updateOption(SelectOptionPB option) =
|
||||
_UpdateOption;
|
||||
const factory ChecklistCellEditorEvent.deleteOption(SelectOptionPB option) =
|
||||
_DeleteOption;
|
||||
const factory ChecklistCellEditorEvent.filterOption(String predicate) =
|
||||
_FilterOption;
|
||||
const factory ChecklistCellEditorEvent.didReceiveTasks(
|
||||
ChecklistCellDataPB? data,
|
||||
) = _DidReceiveTasks;
|
||||
const factory ChecklistCellEditorEvent.newTask(String taskName) = _NewOption;
|
||||
const factory ChecklistCellEditorEvent.selectTask(
|
||||
SelectOptionPB option,
|
||||
) = _SelectTask;
|
||||
const factory ChecklistCellEditorEvent.updateTaskName(
|
||||
SelectOptionPB option,
|
||||
String name,
|
||||
) = _UpdateTaskName;
|
||||
const factory ChecklistCellEditorEvent.deleteTask(SelectOptionPB option) =
|
||||
_DeleteTask;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -139,24 +142,21 @@ class ChecklistCellEditorState with _$ChecklistCellEditorState {
|
||||
required List<ChecklistSelectOption> allOptions,
|
||||
required Option<String> createOption,
|
||||
required double percent,
|
||||
required String predicate,
|
||||
}) = _ChecklistCellEditorState;
|
||||
|
||||
factory ChecklistCellEditorState.initial(ChecklistCellController context) {
|
||||
final data = context.getCellData(loadIfNotExist: true);
|
||||
|
||||
return ChecklistCellEditorState(
|
||||
allOptions: _makeChecklistSelectOptions(data, ''),
|
||||
allOptions: _makeChecklistSelectOptions(data),
|
||||
createOption: none(),
|
||||
percent: data?.percentage ?? 0,
|
||||
predicate: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
||||
ChecklistCellDataPB? data,
|
||||
String predicate,
|
||||
) {
|
||||
if (data == null) {
|
||||
return [];
|
||||
@@ -164,9 +164,6 @@ List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
||||
|
||||
final List<ChecklistSelectOption> options = [];
|
||||
final List<SelectOptionPB> allOptions = List.from(data.options);
|
||||
if (predicate.isNotEmpty) {
|
||||
allOptions.retainWhere((element) => element.name.contains(predicate));
|
||||
}
|
||||
final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList();
|
||||
|
||||
for (final option in allOptions) {
|
||||
@@ -177,10 +174,3 @@ List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
class ChecklistSelectOption {
|
||||
final bool isSelected;
|
||||
final SelectOptionPB data;
|
||||
|
||||
ChecklistSelectOption(this.isSelected, this.data);
|
||||
}
|
||||
+25
-110
@@ -1,129 +1,44 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.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';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
|
||||
class ChecklistProgressBar extends StatelessWidget {
|
||||
class ChecklistProgressBar extends StatefulWidget {
|
||||
final double percent;
|
||||
const ChecklistProgressBar({required this.percent, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LinearPercentIndicator(
|
||||
lineHeight: 10.0,
|
||||
percent: percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: percent < 1.0
|
||||
? SelectOptionColorPB.Purple.toColor(context)
|
||||
: SelectOptionColorPB.Green.toColor(context),
|
||||
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
|
||||
barRadius: const Radius.circular(5),
|
||||
);
|
||||
}
|
||||
State<ChecklistProgressBar> createState() => _ChecklistProgressBarState();
|
||||
}
|
||||
|
||||
class SliverChecklistProgressBar extends StatelessWidget {
|
||||
const SliverChecklistProgressBar({Key? key}) : super(key: key);
|
||||
|
||||
class _ChecklistProgressBarState extends State<ChecklistProgressBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _SliverChecklistProgressBarDelegate(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverChecklistProgressBarDelegate
|
||||
extends SliverPersistentHeaderDelegate {
|
||||
_SliverChecklistProgressBarDelegate();
|
||||
|
||||
double fixHeight = 60;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
) {
|
||||
return const _AutoFocusTextField();
|
||||
}
|
||||
|
||||
@override
|
||||
double get maxExtent => fixHeight;
|
||||
|
||||
@override
|
||||
double get minExtent => fixHeight;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class _AutoFocusTextField extends StatefulWidget {
|
||||
const _AutoFocusTextField();
|
||||
|
||||
@override
|
||||
State<_AutoFocusTextField> createState() => _AutoFocusTextFieldState();
|
||||
}
|
||||
|
||||
class _AutoFocusTextFieldState extends State<_AutoFocusTextField> {
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
builder: (context, state) {
|
||||
return BlocListener<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.createOption != current.createOption,
|
||||
listener: (context, state) {
|
||||
if (_focusNode.canRequestFocus) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
color: Theme.of(context).cardColor,
|
||||
child: Padding(
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
child: Column(
|
||||
children: [
|
||||
FlowyTextField(
|
||||
autoFocus: true,
|
||||
focusNode: _focusNode,
|
||||
autoClearWhenDone: true,
|
||||
submitOnLeave: true,
|
||||
hintText: LocaleKeys.grid_checklist_panelTitle.tr(),
|
||||
onChanged: (text) {
|
||||
context
|
||||
.read<ChecklistCellEditorBloc>()
|
||||
.add(ChecklistCellEditorEvent.filterOption(text));
|
||||
},
|
||||
onSubmitted: (text) {
|
||||
context
|
||||
.read<ChecklistCellEditorBloc>()
|
||||
.add(ChecklistCellEditorEvent.newOption(text));
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
child: ChecklistProgressBar(percent: state.percent),
|
||||
),
|
||||
],
|
||||
),
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LinearPercentIndicator(
|
||||
lineHeight: 4.0,
|
||||
percent: widget.percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
|
||||
barRadius: const Radius.circular(5),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 36,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: FlowyText.regular(
|
||||
"${(widget.percent * 100).round()}%",
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+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.toLocal().date,
|
||||
endDate: newEnd.toLocal().date,
|
||||
);
|
||||
} else if (end == null) {
|
||||
emit(state.copyWith(startDay: start, endDay: null));
|
||||
} else {
|
||||
await _updateDateData(
|
||||
date: start!.toLocal().date,
|
||||
endDate: end.toLocal().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);
|
||||
}
|
||||
+49
-50
@@ -1,8 +1,8 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../grid/presentation/layout/sizes.dart';
|
||||
@@ -11,9 +11,15 @@ import 'date_cell_bloc.dart';
|
||||
import 'date_editor.dart';
|
||||
|
||||
class DateCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
Alignment alignment;
|
||||
EdgeInsets? cellPadding;
|
||||
|
||||
DateCellStyle({this.alignment = Alignment.center});
|
||||
DateCellStyle({
|
||||
this.placeholder,
|
||||
this.alignment = Alignment.center,
|
||||
this.cellPadding,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class GridCellDelegate {
|
||||
@@ -22,20 +28,12 @@ abstract class GridCellDelegate {
|
||||
}
|
||||
|
||||
class GridDateCell extends GridCellWidget {
|
||||
final bool editable;
|
||||
|
||||
/// The [GridDateCell] is used by Field Type [FieldType.DateTime],
|
||||
/// [FieldType.CreatedTime], [FieldType.LastEditedTime]. So it needs
|
||||
/// to know the field type.
|
||||
final FieldType fieldType;
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
late final DateCellStyle? cellStyle;
|
||||
|
||||
GridDateCell({
|
||||
GridCellStyle? style,
|
||||
required this.fieldType,
|
||||
required this.cellControllerBuilder,
|
||||
this.editable = true,
|
||||
Key? key,
|
||||
}) : super(key: key) {
|
||||
if (style != null) {
|
||||
@@ -72,33 +70,30 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<DateCellBloc, DateCellState>(
|
||||
builder: (context, state) {
|
||||
Widget dateTextWidget = GridDateCellText(
|
||||
dateStr: state.dateStr,
|
||||
alignment: alignment,
|
||||
return AppFlowyPopover(
|
||||
controller: _popover,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: BoxConstraints.loose(const Size(260, 620)),
|
||||
margin: EdgeInsets.zero,
|
||||
child: GridDateCellText(
|
||||
dateStr: state.dateStr,
|
||||
placeholder: widget.cellStyle?.placeholder ?? "",
|
||||
alignment: alignment,
|
||||
cellPadding:
|
||||
widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContent) {
|
||||
return DateCellEditor(
|
||||
cellController:
|
||||
widget.cellControllerBuilder.build() as DateCellController,
|
||||
onDismissed: () => widget.onCellFocus.value = false,
|
||||
);
|
||||
},
|
||||
onClose: () {
|
||||
widget.onCellFocus.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
// If the cell is editable, wrap it in a popover.
|
||||
if (widget.editable) {
|
||||
dateTextWidget = AppFlowyPopover(
|
||||
controller: _popover,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: BoxConstraints.loose(const Size(260, 520)),
|
||||
margin: EdgeInsets.zero,
|
||||
child: dateTextWidget,
|
||||
popupBuilder: (BuildContext popoverContent) {
|
||||
return DateCellEditor(
|
||||
cellController: widget.cellControllerBuilder.build()
|
||||
as DateCellController,
|
||||
onDismissed: () => widget.onCellFocus.value = false,
|
||||
);
|
||||
},
|
||||
onClose: () {
|
||||
widget.onCellFocus.value = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
return dateTextWidget;
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -113,10 +108,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
_popover.show();
|
||||
|
||||
if (widget.editable) {
|
||||
widget.onCellFocus.value = true;
|
||||
}
|
||||
widget.onCellFocus.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -125,24 +117,31 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
|
||||
class GridDateCellText extends StatelessWidget {
|
||||
final String dateStr;
|
||||
final String placeholder;
|
||||
final Alignment alignment;
|
||||
final EdgeInsets cellPadding;
|
||||
const GridDateCellText({
|
||||
required this.dateStr,
|
||||
required this.placeholder,
|
||||
required this.alignment,
|
||||
required this.cellPadding,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.expand(
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: FlowyText.medium(
|
||||
dateStr,
|
||||
maxLines: null,
|
||||
),
|
||||
final isPlaceholder = dateStr.isEmpty;
|
||||
final text = isPlaceholder ? placeholder : dateStr;
|
||||
return Align(
|
||||
alignment: alignment,
|
||||
child: Padding(
|
||||
padding: cellPadding,
|
||||
child: FlowyText.medium(
|
||||
text,
|
||||
color: isPlaceholder
|
||||
? Theme.of(context).hintColor
|
||||
: AFThemeExtension.of(context).textColor,
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+12
-1
@@ -79,8 +79,19 @@ class DateCellState with _$DateCellState {
|
||||
}
|
||||
|
||||
String _dateStrFromCellData(DateCellDataPB? cellData) {
|
||||
if (cellData == null || !cellData.hasTimestamp()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String dateStr = "";
|
||||
if (cellData != null) {
|
||||
if (cellData.isRange) {
|
||||
if (cellData.includeTime) {
|
||||
dateStr =
|
||||
"${cellData.date} ${cellData.time} → ${cellData.endDate} ${cellData.endTime}";
|
||||
} else {
|
||||
dateStr = "${cellData.date} → ${cellData.endDate}";
|
||||
}
|
||||
} else {
|
||||
if (cellData.includeTime) {
|
||||
dateStr = "${cellData.date} ${cellData.time}";
|
||||
} else {
|
||||
|
||||
+260
-128
@@ -2,6 +2,7 @@ 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';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@@ -11,7 +12,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:dartz/dartz.dart' show Either;
|
||||
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';
|
||||
@@ -52,7 +52,7 @@ class _DateCellEditor extends State<DateCellEditor> {
|
||||
if (snapshot.hasData) {
|
||||
return _buildWidget(snapshot);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -68,7 +68,7 @@ class _DateCellEditor extends State<DateCellEditor> {
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return const SizedBox();
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -106,38 +106,24 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
||||
cellData: widget.cellContext.getCellData(),
|
||||
cellController: widget.cellContext,
|
||||
)..add(const DateCellCalendarEvent.initial()),
|
||||
child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: _buildCalendar(context),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: state.includeTime
|
||||
? _TimeTextField(
|
||||
timeStr: state.time,
|
||||
popoverMutex: popoverMutex,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 18.0, bottom: 12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StartTextField(popoverMutex: popoverMutex),
|
||||
EndTextField(popoverMutex: popoverMutex),
|
||||
const DatePicker(),
|
||||
const TypeOptionSeparator(spacing: 12.0),
|
||||
const EndTimeButton(),
|
||||
const VSpace(4.0),
|
||||
const _IncludeTimeButton(),
|
||||
const TypeOptionSeparator(spacing: 12.0),
|
||||
const TypeOptionSeparator(spacing: 8.0),
|
||||
DateTypeOptionButton(popoverMutex: popoverMutex),
|
||||
const TypeOptionSeparator(spacing: 12.0),
|
||||
const VSpace(4.0),
|
||||
const ClearDateButton(),
|
||||
];
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
controller: ScrollController(),
|
||||
itemCount: children.length,
|
||||
itemBuilder: (BuildContext context, int index) => children[index],
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -147,84 +133,181 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCalendar(BuildContext context) {
|
||||
class StartTextField extends StatelessWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
const StartTextField({super.key, required this.popoverMutex});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
|
||||
builder: (context, state) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: state.includeTime
|
||||
? _TimeTextField(
|
||||
isEndTime: false,
|
||||
timeStr: state.time,
|
||||
popoverMutex: popoverMutex,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EndTextField extends StatelessWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
const EndTextField({super.key, required this.popoverMutex});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
|
||||
builder: (context, state) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: state.includeTime && state.isRange
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: _TimeTextField(
|
||||
isEndTime: true,
|
||||
timeStr: state.endTime,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DatePicker extends StatefulWidget {
|
||||
const DatePicker({super.key});
|
||||
|
||||
@override
|
||||
State<DatePicker> createState() => _DatePickerState();
|
||||
}
|
||||
|
||||
class _DatePickerState extends State<DatePicker> {
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
|
||||
builder: (context, state) {
|
||||
final textStyle = Theme.of(context).textTheme.bodyMedium!;
|
||||
final boxDecoration = BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: Corners.s6Border,
|
||||
color: Theme.of(context).cardColor,
|
||||
shape: BoxShape.circle,
|
||||
);
|
||||
return TableCalendar(
|
||||
firstDay: kFirstDay,
|
||||
lastDay: kLastDay,
|
||||
focusedDay: state.focusedDay,
|
||||
rowHeight: GridSize.popoverItemHeight,
|
||||
calendarFormat: state.format,
|
||||
daysOfWeekHeight: GridSize.popoverItemHeight,
|
||||
headerStyle: HeaderStyle(
|
||||
formatButtonVisible: false,
|
||||
titleCentered: true,
|
||||
titleTextStyle: textStyle,
|
||||
leftChevronMargin: EdgeInsets.zero,
|
||||
leftChevronPadding: EdgeInsets.zero,
|
||||
leftChevronIcon: FlowySvg(
|
||||
FlowySvgs.arrow_left_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TableCalendar(
|
||||
firstDay: kFirstDay,
|
||||
lastDay: kLastDay,
|
||||
focusedDay: _focusedDay,
|
||||
rowHeight: 26.0 + 7.0,
|
||||
calendarFormat: _calendarFormat,
|
||||
daysOfWeekHeight: 17.0 + 8.0,
|
||||
rangeSelectionMode: state.isRange
|
||||
? RangeSelectionMode.enforced
|
||||
: RangeSelectionMode.disabled,
|
||||
rangeStartDay: state.isRange ? state.startDay : null,
|
||||
rangeEndDay: state.isRange ? state.endDay : null,
|
||||
headerStyle: HeaderStyle(
|
||||
formatButtonVisible: false,
|
||||
titleCentered: true,
|
||||
titleTextStyle: textStyle,
|
||||
leftChevronMargin: EdgeInsets.zero,
|
||||
leftChevronPadding: EdgeInsets.zero,
|
||||
leftChevronIcon: FlowySvg(
|
||||
FlowySvgs.arrow_left_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
rightChevronPadding: EdgeInsets.zero,
|
||||
rightChevronMargin: EdgeInsets.zero,
|
||||
rightChevronIcon: FlowySvg(
|
||||
FlowySvgs.arrow_right_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
headerMargin: EdgeInsets.zero,
|
||||
headerPadding: const EdgeInsets.only(bottom: 8.0),
|
||||
),
|
||||
rightChevronPadding: EdgeInsets.zero,
|
||||
rightChevronMargin: EdgeInsets.zero,
|
||||
rightChevronIcon: FlowySvg(
|
||||
FlowySvgs.arrow_right_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
calendarStyle: CalendarStyle(
|
||||
cellMargin: const EdgeInsets.all(3.5),
|
||||
defaultDecoration: boxDecoration,
|
||||
selectedDecoration: boxDecoration.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
todayDecoration: boxDecoration.copyWith(
|
||||
color: Colors.transparent,
|
||||
border:
|
||||
Border.all(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
weekendDecoration: boxDecoration,
|
||||
outsideDecoration: boxDecoration,
|
||||
rangeStartDecoration: boxDecoration.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
rangeEndDecoration: boxDecoration.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
defaultTextStyle: textStyle,
|
||||
weekendTextStyle: textStyle,
|
||||
selectedTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
rangeStartTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
rangeEndTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
todayTextStyle: textStyle,
|
||||
outsideTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
rangeHighlightColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
headerMargin: const EdgeInsets.only(bottom: 8.0),
|
||||
),
|
||||
daysOfWeekStyle: DaysOfWeekStyle(
|
||||
dowTextFormatter: (date, locale) =>
|
||||
DateFormat.E(locale).format(date).toUpperCase(),
|
||||
weekdayStyle: AFThemeExtension.of(context).caption,
|
||||
weekendStyle: AFThemeExtension.of(context).caption,
|
||||
),
|
||||
calendarStyle: CalendarStyle(
|
||||
cellMargin: const EdgeInsets.all(3),
|
||||
defaultDecoration: boxDecoration,
|
||||
selectedDecoration: boxDecoration.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
todayDecoration: boxDecoration.copyWith(
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
weekendDecoration: boxDecoration,
|
||||
outsideDecoration: boxDecoration,
|
||||
defaultTextStyle: textStyle,
|
||||
weekendTextStyle: textStyle,
|
||||
selectedTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
todayTextStyle: textStyle,
|
||||
outsideTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
selectedDayPredicate: (day) => isSameDay(state.dateTime, day),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
context.read<DateCellCalendarBloc>().add(
|
||||
DateCellCalendarEvent.selectDay(selectedDay.toLocal().date),
|
||||
calendarBuilders: CalendarBuilders(
|
||||
dowBuilder: (context, day) {
|
||||
final locale = context.locale.toLanguageTag();
|
||||
final label = DateFormat.E(locale).format(day).substring(0, 2);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: AFThemeExtension.of(context).caption,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setCalFormat(format));
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setFocusedDay(focusedDay));
|
||||
},
|
||||
},
|
||||
),
|
||||
selectedDayPredicate: (day) =>
|
||||
state.isRange ? false : isSameDay(state.dateTime, day),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
context.read<DateCellCalendarBloc>().add(
|
||||
DateCellCalendarEvent.selectDay(selectedDay.toLocal().date),
|
||||
);
|
||||
},
|
||||
onRangeSelected: (start, end, focusedDay) {
|
||||
context.read<DateCellCalendarBloc>().add(
|
||||
DateCellCalendarEvent.selectDateRange(start, end),
|
||||
);
|
||||
},
|
||||
onFormatChanged: (calendarFormat) => setState(() {
|
||||
_calendarFormat = calendarFormat;
|
||||
}),
|
||||
onPageChanged: (focusedDay) => setState(() {
|
||||
_focusedDay = focusedDay;
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -239,6 +322,29 @@ class _IncludeTimeButton extends StatelessWidget {
|
||||
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
|
||||
selector: (state) => state.includeTime,
|
||||
builder: (context, includeTime) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: IncludeTimeButton(
|
||||
onChanged: (value) => context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setIncludeTime(!value)),
|
||||
value: includeTime,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class EndTimeButton extends StatelessWidget {
|
||||
const EndTimeButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
|
||||
selector: (state) => state.isRange,
|
||||
builder: (context, isRange) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: SizedBox(
|
||||
@@ -248,17 +354,17 @@ class _IncludeTimeButton extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.clock_alarm_s,
|
||||
FlowySvgs.date_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
const HSpace(6),
|
||||
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
|
||||
FlowyText.medium(LocaleKeys.grid_field_isRange.tr()),
|
||||
const Spacer(),
|
||||
Toggle(
|
||||
value: includeTime,
|
||||
value: isRange,
|
||||
onChanged: (value) => context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setIncludeTime(!value)),
|
||||
.add(DateCellCalendarEvent.setIsRange(!value)),
|
||||
style: ToggleStyle.big,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -273,12 +379,14 @@ class _IncludeTimeButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _TimeTextField extends StatefulWidget {
|
||||
final bool isEndTime;
|
||||
final String? timeStr;
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
const _TimeTextField({
|
||||
required this.timeStr,
|
||||
required this.popoverMutex,
|
||||
required this.isEndTime,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -313,32 +421,59 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<DateCellCalendarBloc, DateCellCalendarState>(
|
||||
listener: (context, state) => _textController.text = state.time ?? "",
|
||||
listener: (context, state) {
|
||||
if (widget.isEndTime) {
|
||||
_textController.text = state.endTime ?? "";
|
||||
} else {
|
||||
_textController.text = state.time ?? "";
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
const VSpace(12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: FlowyTextField(
|
||||
text: state.time ?? "",
|
||||
focusNode: _focusNode,
|
||||
controller: _textController,
|
||||
submitOnLeave: true,
|
||||
hintText: state.timeHintText,
|
||||
errorText: state.timeFormatError,
|
||||
onSubmitted: (timeStr) {
|
||||
context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setTime(timeStr));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
String text = "";
|
||||
if (!widget.isEndTime && state.time != null) {
|
||||
text = state.time!;
|
||||
} else if (state.endTime != null) {
|
||||
text = state.endTime!;
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: FlowyTextField(
|
||||
text: text,
|
||||
focusNode: _focusNode,
|
||||
controller: _textController,
|
||||
submitOnLeave: true,
|
||||
hintText: state.timeHintText,
|
||||
errorText: widget.isEndTime
|
||||
? state.parseEndTimeError
|
||||
: state.parseTimeError,
|
||||
onSubmitted: (timeStr) {
|
||||
if (widget.isEndTime) {
|
||||
context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setEndTime(timeStr));
|
||||
} else {
|
||||
context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setTime(timeStr));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
_focusNode.removeListener(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
widget.popoverMutex.close();
|
||||
}
|
||||
});
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
@@ -369,7 +504,6 @@ class DateTypeOptionButton extends StatelessWidget {
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(title),
|
||||
margin: GridSize.typeOptionContentInsets,
|
||||
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
||||
),
|
||||
),
|
||||
@@ -481,8 +615,6 @@ class ClearDateButton extends StatelessWidget {
|
||||
.add(const DateCellCalendarEvent.clearDate());
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
leftIcon: const FlowySvg(FlowySvgs.delete_s),
|
||||
margin: GridSize.typeOptionContentInsets,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+24
-3
@@ -7,13 +7,33 @@ import 'number_cell_bloc.dart';
|
||||
import '../../../../grid/presentation/layout/sizes.dart';
|
||||
import '../../cell_builder.dart';
|
||||
|
||||
class GridNumberCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
TextStyle? textStyle;
|
||||
EdgeInsets? cellPadding;
|
||||
|
||||
GridNumberCellStyle({
|
||||
this.placeholder,
|
||||
this.textStyle,
|
||||
this.cellPadding,
|
||||
});
|
||||
}
|
||||
|
||||
class GridNumberCell extends GridCellWidget {
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
late final GridNumberCellStyle cellStyle;
|
||||
|
||||
GridNumberCell({
|
||||
required this.cellControllerBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
required GridCellStyle? style,
|
||||
super.key,
|
||||
}) {
|
||||
if (style != null) {
|
||||
cellStyle = (style as GridNumberCellStyle);
|
||||
} else {
|
||||
cellStyle = GridNumberCellStyle();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
GridEditableTextCell<GridNumberCell> createState() => _NumberCellState();
|
||||
@@ -57,9 +77,10 @@ class _NumberCellState extends GridEditableTextCell<GridNumberCell> {
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
hintText: widget.cellStyle.placeholder,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
|
||||
Loaded 100 of 544 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user