Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daf9b48c73 | ||
|
|
0ea5b3c483 | ||
|
|
f3148640eb | ||
|
|
1c84ee1d53 | ||
|
|
cecd4f48ab | ||
|
|
1e001640aa | ||
|
|
3426e5b53e | ||
|
|
936b49a63f | ||
|
|
f6f80b48c9 | ||
|
|
759cb27a43 | ||
|
|
26a2bffcd1 | ||
|
|
fe55452c79 | ||
|
|
16adacac38 | ||
|
|
d86531ac92 | ||
|
|
9231455f0d | ||
|
|
0c6a1d4ae7 | ||
|
|
524efc2620 | ||
|
|
50a4f03931 | ||
|
|
a4681a4042 | ||
|
|
1ca130d7de | ||
|
|
ef6f9a3175 | ||
|
|
aa25aa4474 | ||
|
|
c7af04b317 | ||
|
|
9565173baf | ||
|
|
5f4e3ecc76 | ||
|
|
043c68a333 | ||
|
|
f639d51c11 | ||
|
|
71071b60b2 | ||
|
|
608255fa0f | ||
|
|
f8b0a26acc | ||
|
|
133dab934b | ||
|
|
a81670c447 | ||
|
|
35f84e42cc | ||
|
|
2f6df6a17d | ||
|
|
8bcc6384f2 | ||
|
|
a0fc4b86b0 | ||
|
|
790580eea4 | ||
|
|
3db9024a2d | ||
|
|
a821740469 | ||
|
|
af0ae2e375 | ||
|
|
7d6fa0b159 | ||
|
|
b73a34ed94 | ||
|
|
f3aaff77b9 | ||
|
|
9b7ff375b2 | ||
|
|
18498c0479 | ||
|
|
c652c32575 | ||
|
|
df8642d446 | ||
|
|
abb6eff23d | ||
|
|
3b4f8e53a2 | ||
|
|
4847b8b114 | ||
|
|
e8f124b97c | ||
|
|
f73d59fb57 | ||
|
|
897d7980f6 | ||
|
|
b88aed887a | ||
|
|
4e863bc87f | ||
|
|
507554a596 | ||
|
|
0806436c19 | ||
|
|
1205f0ebf7 | ||
|
|
26e56194ea | ||
|
|
4e67282f2b | ||
|
|
41ec2d992e | ||
|
|
f0e4f3db61 | ||
|
|
6634a0ecb3 | ||
|
|
255f30590f | ||
|
|
1ba7224088 |
No files matched your search
@@ -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,34 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.3.2 - 09/18/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Improve the performance of the editor, now it is much faster when editing a large document.
|
||||
- Support for reordering the rows of the database on Windows.
|
||||
- Revamp the row detail page of the database.
|
||||
- Revamp the checklist cell editor of the database.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Some UI issues
|
||||
|
||||
## Version 0.3.1 - 09/04/2023
|
||||
|
||||
### New Features
|
||||
|
||||
- Improve CJK (Chinese, Japanese, Korean) input method support.
|
||||
- Share a database in CSV format.
|
||||
- Support for aligning the block component with the toolbar.
|
||||
- Support for editing name when creating a new page.
|
||||
- Support for inserting a table in the document page.
|
||||
- Database views allow for independent field visibility toggling.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Paste multiple lines in code block.
|
||||
- Some UI issues
|
||||
|
||||
## Version 0.3.0 - 08/22/2023
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -20,11 +20,12 @@ run_task = { name = ["restore-crate-type"] }
|
||||
|
||||
[env]
|
||||
RUST_LOG = "info"
|
||||
CARGO_PROFILE = "dev"
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.3.0"
|
||||
CURRENT_APP_VERSION = "0.3.2"
|
||||
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 |
@@ -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();
|
||||
@@ -116,6 +116,7 @@ void main() {
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
// Duplicate the event
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDuplicateRowButton();
|
||||
await tester.dismissRowDetailPage();
|
||||
|
||||
@@ -125,6 +126,7 @@ void main() {
|
||||
|
||||
// Delete an event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
|
||||
// Check that there is 1 event
|
||||
@@ -155,6 +157,7 @@ void main() {
|
||||
|
||||
// Delete the event
|
||||
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
|
||||
await tester.tapRowDetailPageRowActionButton();
|
||||
await tester.tapRowDetailPageDeleteRowButton();
|
||||
|
||||
// Create a new event in today's calendar cell
|
||||
|
||||
@@ -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,116 @@ 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);
|
||||
|
||||
// assert that new task editor is shown
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: true);
|
||||
|
||||
// create a new task with enter
|
||||
await tester.createNewChecklistTask(name: "task 0", enter: true);
|
||||
|
||||
// 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();
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: false);
|
||||
|
||||
// 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, click on new task button
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: false);
|
||||
await tester.tapChecklistNewTaskButton();
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: true);
|
||||
|
||||
// create another task with the create button
|
||||
await tester.createNewChecklistTask(name: "task 2", button: true);
|
||||
|
||||
// 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);
|
||||
|
||||
// assert that the new task editor is shown
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: true);
|
||||
|
||||
// dismiss the cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// check that the progress bar is not viisble
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('database field settings', () {
|
||||
testWidgets('field visibility', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
|
||||
|
||||
// create a field
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// hide the field
|
||||
await tester.tapGridFieldWithName('New field 1');
|
||||
await tester.tapHidePropertyButton();
|
||||
await tester.noFieldWithName('New field 1');
|
||||
|
||||
// go back to inline database view, expect field to be shown
|
||||
await tester.tapTabBarLinkedViewByViewName('Untitled');
|
||||
await tester.findFieldWithName('New field 1');
|
||||
|
||||
// go back to linked database view, expect field to be hidden
|
||||
await tester.tapTabBarLinkedViewByViewName('grid');
|
||||
await tester.noFieldWithName('New field 1');
|
||||
|
||||
// use the settings button to show the field
|
||||
await tester.tapDatabaseSettingButton();
|
||||
await tester.tapViewPropertiesButton();
|
||||
await tester.tapViewTogglePropertyVisibilityButtonByName('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
await tester.findFieldWithName('New field 1');
|
||||
|
||||
// open first row in popup then hide the field
|
||||
await tester.openFirstRowDetailPage();
|
||||
await tester.tapGridFieldWithNameInRowDetailPage('New field 1');
|
||||
await tester.tapHidePropertyButtonInFieldEditor();
|
||||
await tester.dismissRowDetailPage();
|
||||
await tester.noFieldWithName('New field 1');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -106,26 +106,6 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('hide field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// create a field
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
await tester.tapNewPropertyButton();
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// Delete the field
|
||||
await tester.tapGridFieldWithName('New field 1');
|
||||
await tester.tapHidePropertyButton();
|
||||
|
||||
await tester.noFieldWithName('New field 1');
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create checklist field ', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -135,7 +135,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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document alignment', () {
|
||||
testWidgets('edit alignment in toolbar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
final selection = Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: 1,
|
||||
);
|
||||
// click the first line of the readme
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.updateSelection(selection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the align center
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
|
||||
|
||||
// expect to see the align center
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final first = editorState.getNodeAtPath([0])!;
|
||||
expect(first.attributes[blockComponentAlign], 'center');
|
||||
|
||||
// click the align right
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
|
||||
expect(first.attributes[blockComponentAlign], 'right');
|
||||
|
||||
// click the align left
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s);
|
||||
await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s);
|
||||
expect(first.attributes[blockComponentAlign], 'left');
|
||||
});
|
||||
});
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('paste in codeblock', () {
|
||||
testWidgets('paste multiple lines in codeblock', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName();
|
||||
|
||||
// mock the clipboard
|
||||
const lines = 3;
|
||||
final text = List.generate(lines, (index) => 'line $index').join('\n');
|
||||
AppFlowyClipboard.mockSetData(
|
||||
AppFlowyClipboardData(
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
|
||||
await insertCodeBlockInDocument(tester);
|
||||
|
||||
// paste the text
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
expect(
|
||||
editorState.getNodeAtPath([0])!.delta!.toPlainText(),
|
||||
text,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Inserts an codeBlock in the document
|
||||
Future<void> insertCodeBlockInDocument(WidgetTester tester) async {
|
||||
// open the actions menu and insert the codeBlock
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_selectionMenu_codeBlock.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
@@ -226,6 +226,56 @@ void main() {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
|
||||
testWidgets(
|
||||
'paste the image from clipboard when html and image are both available',
|
||||
(tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
image: ('png', bytes),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(
|
||||
node.attributes[ImageBlockKeys.url],
|
||||
'https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png',
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
extension on WidgetTester {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_alignment_test.dart' as document_alignment_test;
|
||||
import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;
|
||||
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
|
||||
import 'document_create_and_delete_test.dart'
|
||||
as document_create_and_delete_test;
|
||||
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
|
||||
@@ -7,10 +10,9 @@ import 'document_with_database_test.dart' as document_with_database_test;
|
||||
import 'document_with_inline_math_equation_test.dart'
|
||||
as document_with_inline_math_equation_test;
|
||||
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
|
||||
import 'document_with_outline_block_test.dart' as document_with_outline_block;
|
||||
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
|
||||
import 'edit_document_test.dart' as document_edit_test;
|
||||
import 'document_with_outline_block_test.dart' as document_with_outline_block;
|
||||
import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;
|
||||
|
||||
void startTesting() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -25,4 +27,6 @@ void startTesting() {
|
||||
document_with_outline_block.main();
|
||||
document_with_toggle_list_test.main();
|
||||
document_copy_and_paste_test.main();
|
||||
document_codeblock_paste_test.main();
|
||||
document_alignment_test.main();
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:integration_test/integration_test.dart';
|
||||
import 'database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database_cell_test.dart' as database_cell_test;
|
||||
import 'database_field_test.dart' as database_field_test;
|
||||
import 'database_field_settings_test.dart' as database_field_settings_test;
|
||||
import 'database_filter_test.dart' as database_filter_test;
|
||||
import 'database_row_page_test.dart' as database_row_page_test;
|
||||
import 'database_row_test.dart' as database_row_test;
|
||||
@@ -47,6 +48,7 @@ void main() {
|
||||
// Database integration tests
|
||||
database_cell_test.main();
|
||||
database_field_test.main();
|
||||
database_field_settings_test.main();
|
||||
database_share_test.main();
|
||||
database_row_page_test.main();
|
||||
database_row_test.main();
|
||||
|
||||
@@ -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,14 +1,17 @@
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
@@ -279,6 +282,14 @@ extension CommonOperations on WidgetTester {
|
||||
// create a new page
|
||||
await tapAddViewButton(name: parentName ?? gettingStarted);
|
||||
await tapButtonWithName(layout.menuName);
|
||||
final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(
|
||||
KVKeys.showRenameDialogWhenCreatingNewFile,
|
||||
(value) => bool.parse(value),
|
||||
);
|
||||
final showRenameDialog = settingsOrFailure.fold((l) => false, (r) => r);
|
||||
if (showRenameDialog) {
|
||||
await tapOKButton();
|
||||
}
|
||||
await pumpAndSettle();
|
||||
|
||||
// hover on it and change it's name
|
||||
@@ -409,6 +420,14 @@ extension CommonOperations on WidgetTester {
|
||||
await gesture.up();
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
// tap the button with [FlowySvgData]
|
||||
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg.path == svg.path,
|
||||
);
|
||||
await tapButton(button);
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -35,17 +36,21 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
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';
|
||||
@@ -240,36 +245,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,
|
||||
);
|
||||
|
||||
@@ -450,6 +462,119 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
void assertNewCheckListTaskEditorVisible({required bool visible}) {
|
||||
final editor = find.byType(NewTaskItem);
|
||||
if (visible) {
|
||||
expect(editor, findsOneWidget);
|
||||
} else {
|
||||
expect(editor, findsNothing);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createNewChecklistTask({
|
||||
required String name,
|
||||
enter = false,
|
||||
button = false,
|
||||
}) async {
|
||||
assert(!(enter && button));
|
||||
final textField = find.descendant(
|
||||
of: find.byType(NewTaskItem),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
|
||||
await enterText(textField, name);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
if (enter) {
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
} 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(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> tapChecklistNewTaskButton() async {
|
||||
await tapButton(find.byType(ChecklistNewTaskButton));
|
||||
}
|
||||
|
||||
Future<void> checkChecklistTask({required int index}) async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(ChecklistItem).at(index),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s,
|
||||
),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> deleteChecklistTask({required int index}) async {
|
||||
final task = find.byType(ChecklistItem).at(index);
|
||||
|
||||
await startGesture(getCenter(task), kind: PointerDeviceKind.mouse);
|
||||
await pumpAndSettle();
|
||||
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> openFirstRowDetailPage() async {
|
||||
await hoverOnFirstRowOfGrid();
|
||||
|
||||
@@ -486,7 +611,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(banner, findsOneWidget);
|
||||
|
||||
await startGesture(
|
||||
getTopLeft(banner),
|
||||
getCenter(banner),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
|
||||
@@ -503,6 +628,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(findDateCell);
|
||||
}
|
||||
|
||||
Future<void> tapGridFieldWithNameInRowDetailPage(String name) async {
|
||||
final fields = find.byWidgetPredicate(
|
||||
(widget) => widget is FieldCellButton && widget.field.name == name,
|
||||
);
|
||||
final field = find.descendant(
|
||||
of: find.byType(RowDetailPage),
|
||||
matching: fields,
|
||||
);
|
||||
await tapButton(field);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> duplicateRowInRowDetailPage() async {
|
||||
final duplicateButton = find.byType(RowDetailPageDuplicateButton);
|
||||
await tapButton(duplicateButton);
|
||||
@@ -513,6 +650,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();
|
||||
@@ -585,6 +747,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(field);
|
||||
}
|
||||
|
||||
Future<void> tapHidePropertyButtonInFieldEditor() async {
|
||||
final button = find.byType(HideFieldButton);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapRowDetailPageRowActionButton() async {
|
||||
await tapButton(find.byType(RowActionButton));
|
||||
}
|
||||
|
||||
Future<void> tapRowDetailPageCreatePropertyButton() async {
|
||||
await tapButton(find.byType(CreateRowFieldButton));
|
||||
}
|
||||
@@ -654,6 +825,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,
|
||||
@@ -925,6 +1108,23 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Should call [tapDatabaseSettingButton] first.
|
||||
Future<void> tapViewPropertiesButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingItem);
|
||||
final findLayoutButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
widget.text == DatabaseSettingAction.showProperties.title(),
|
||||
);
|
||||
|
||||
final button = find.descendant(
|
||||
of: findSettingItem,
|
||||
matching: findLayoutButton,
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Should call [tapDatabaseSettingButton] first.
|
||||
Future<void> tapDatabaseLayoutButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingItem);
|
||||
@@ -1111,6 +1311,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(findCreateButton);
|
||||
}
|
||||
|
||||
Future<void> tapTabBarLinkedViewByViewName(String name) async {
|
||||
final viewButton = findTabBarLinkViewByViewName(name);
|
||||
await tapButton(viewButton);
|
||||
}
|
||||
|
||||
Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) => widget is TabBarItemButton && widget.view.layout == layout,
|
||||
@@ -1212,6 +1417,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
Future<void> tapAddSelectOptionButton() async {
|
||||
await tapButtonWithName(LocaleKeys.grid_field_addSelectOption.tr());
|
||||
}
|
||||
|
||||
Future<void> tapViewTogglePropertyVisibilityButtonByName(
|
||||
String fieldName,
|
||||
) async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is GridPropertyCell && widget.fieldInfo.name == fieldName,
|
||||
);
|
||||
final toggleVisibilityButton =
|
||||
find.descendant(of: field, matching: find.byType(FlowyIconButton));
|
||||
await tapButton(toggleVisibilityButton);
|
||||
}
|
||||
}
|
||||
|
||||
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
|
||||
@@ -1235,7 +1452,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:
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
+1
-10
@@ -29,21 +29,12 @@ abstract class Env {
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseAnonKey = _Env.supabaseAnonKey;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_JWT_SECRET',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseJwtSecret = _Env.supabaseJwtSecret;
|
||||
}
|
||||
|
||||
bool get isSupabaseEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return Env.supabaseUrl.isNotEmpty &&
|
||||
Env.supabaseAnonKey.isNotEmpty &&
|
||||
Env.supabaseJwtSecret.isNotEmpty;
|
||||
return Env.supabaseUrl.isNotEmpty && Env.supabaseAnonKey.isNotEmpty;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+18
-5
@@ -2,6 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
|
||||
import 'cell_controller.dart';
|
||||
@@ -13,7 +14,8 @@ typedef NumberCellController = CellController<String, String>;
|
||||
typedef SelectOptionCellController
|
||||
= CellController<SelectOptionCellDataPB, String>;
|
||||
typedef ChecklistCellController = CellController<ChecklistCellDataPB, String>;
|
||||
typedef DateCellController = CellController<DateCellDataPB, DateCellData>;
|
||||
typedef DateCellController = CellController<DateCellDataPB, String>;
|
||||
typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
|
||||
typedef URLCellController = CellController<URLCellDataPB, String>;
|
||||
|
||||
class CellControllerBuilder {
|
||||
@@ -41,20 +43,31 @@ class CellControllerBuilder {
|
||||
TextCellDataPersistence(cellContext: _cellContext),
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
final cellDataLoader = CellDataLoader(
|
||||
cellContext: _cellContext,
|
||||
parser: DateCellDataParser(),
|
||||
reloadOnFieldChanged: true,
|
||||
);
|
||||
|
||||
return DateCellController(
|
||||
cellContext: _cellContext,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence:
|
||||
DateCellDataPersistence(cellContext: _cellContext),
|
||||
TextCellDataPersistence(cellContext: _cellContext),
|
||||
);
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
final cellDataLoader = CellDataLoader(
|
||||
cellContext: _cellContext,
|
||||
parser: TimestampCellDataParser(),
|
||||
reloadOnFieldChanged: true,
|
||||
);
|
||||
return TimestampCellController(
|
||||
cellContext: _cellContext,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence:
|
||||
TextCellDataPersistence(cellContext: _cellContext),
|
||||
);
|
||||
case FieldType.Number:
|
||||
final cellDataLoader = CellDataLoader(
|
||||
|
||||
+10
@@ -73,6 +73,16 @@ class DateCellDataParser implements CellDataParser<DateCellDataPB> {
|
||||
}
|
||||
}
|
||||
|
||||
class TimestampCellDataParser implements CellDataParser<TimestampCellDataPB> {
|
||||
@override
|
||||
TimestampCellDataPB? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return TimestampCellDataPB.fromBuffer(data);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionCellDataParser
|
||||
implements CellDataParser<SelectOptionCellDataPB> {
|
||||
@override
|
||||
|
||||
+1
-49
@@ -1,7 +1,7 @@
|
||||
part of 'cell_service.dart';
|
||||
|
||||
/// Save the cell data to disk
|
||||
/// You can extend this class to do custom operations. For example, the DateCellDataPersistence.
|
||||
/// You can extend this class to do custom operations.
|
||||
abstract class CellDataPersistence<D> {
|
||||
Future<Option<FlowyError>> save(D data);
|
||||
}
|
||||
@@ -28,51 +28,3 @@ class TextCellDataPersistence implements CellDataPersistence<String> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DateCellData with _$DateCellData {
|
||||
const factory DateCellData({
|
||||
DateTime? dateTime,
|
||||
String? time,
|
||||
required bool includeTime,
|
||||
bool? clearFlag,
|
||||
}) = _DateCellData;
|
||||
}
|
||||
|
||||
class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
|
||||
final DatabaseCellContext cellContext;
|
||||
DateCellDataPersistence({
|
||||
required this.cellContext,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Option<FlowyError>> save(DateCellData data) {
|
||||
final payload = DateChangesetPB.create()
|
||||
..cellId = _makeCellPath(cellContext);
|
||||
if (data.dateTime != null) {
|
||||
final date = (data.dateTime!.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
payload.date = date;
|
||||
}
|
||||
if (data.time != null) {
|
||||
payload.time = data.time!;
|
||||
}
|
||||
if (data.clearFlag != null) {
|
||||
payload.clearFlag = data.clearFlag!;
|
||||
}
|
||||
payload.includeTime = data.includeTime;
|
||||
|
||||
return DatabaseEventUpdateDateCell(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(l) => none(),
|
||||
(err) => Some(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CellIdPB _makeCellPath(DatabaseCellContext cellId) {
|
||||
return CellIdPB.create()
|
||||
..viewId = cellId.viewId
|
||||
..fieldId = cellId.fieldId
|
||||
..rowId = cellId.rowId;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
|
||||
final class DateCellBackendService {
|
||||
final CellIdPB cellId;
|
||||
|
||||
DateCellBackendService({
|
||||
required String viewId,
|
||||
required String fieldId,
|
||||
required String rowId,
|
||||
}) : cellId = CellIdPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
|
||||
Future<Either<Unit, FlowyError>> update({
|
||||
DateTime? date,
|
||||
String? time,
|
||||
required includeTime,
|
||||
}) {
|
||||
final payload = DateChangesetPB.create()
|
||||
..cellId = cellId
|
||||
..includeTime = includeTime;
|
||||
|
||||
if (date != null) {
|
||||
final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000;
|
||||
payload.date = Int64(dateTimestamp);
|
||||
}
|
||||
if (time != null) {
|
||||
payload.time = time;
|
||||
}
|
||||
|
||||
return DatabaseEventUpdateDateCell(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> clear() {
|
||||
final payload = DateChangesetPB.create()
|
||||
..cellId = cellId
|
||||
..clearFlag = true;
|
||||
|
||||
return DatabaseEventUpdateDateCell(payload).send();
|
||||
}
|
||||
}
|
||||
+1
-7
@@ -395,13 +395,7 @@ class RowDataBuilder {
|
||||
}
|
||||
|
||||
void insertDate(FieldInfo fieldInfo, DateTime date) {
|
||||
assert(
|
||||
[
|
||||
FieldType.DateTime,
|
||||
FieldType.LastEditedTime,
|
||||
FieldType.CreatedTime,
|
||||
].contains(fieldInfo.fieldType),
|
||||
);
|
||||
assert(FieldType.DateTime == fieldInfo.fieldType);
|
||||
final timestamp = date.millisecondsSinceEpoch ~/ 1000;
|
||||
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
|
||||
}
|
||||
|
||||
+16
-3
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'field_service.dart';
|
||||
@@ -8,13 +10,18 @@ part 'field_action_sheet_bloc.freezed.dart';
|
||||
|
||||
class FieldActionSheetBloc
|
||||
extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
|
||||
final String fieldId;
|
||||
final FieldBackendService fieldService;
|
||||
final FieldSettingsBackendService fieldSettingsService;
|
||||
|
||||
FieldActionSheetBloc({required FieldContext fieldCellContext})
|
||||
: fieldService = FieldBackendService(
|
||||
: fieldId = fieldCellContext.field.id,
|
||||
fieldService = FieldBackendService(
|
||||
viewId: fieldCellContext.viewId,
|
||||
fieldId: fieldCellContext.field.id,
|
||||
),
|
||||
fieldSettingsService =
|
||||
FieldSettingsBackendService(viewId: fieldCellContext.viewId),
|
||||
super(
|
||||
FieldActionSheetState.initial(
|
||||
TypeOptionPB.create()..field_2 = fieldCellContext.field,
|
||||
@@ -31,14 +38,20 @@ class FieldActionSheetBloc
|
||||
);
|
||||
},
|
||||
hideField: (_HideField value) async {
|
||||
final result = await fieldService.updateField(visibility: false);
|
||||
final result = await fieldSettingsService.updateFieldSettings(
|
||||
fieldId: fieldId,
|
||||
fieldVisibility: FieldVisibility.AlwaysHidden,
|
||||
);
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
showField: (_ShowField value) async {
|
||||
final result = await fieldService.updateField(visibility: true);
|
||||
final result = await fieldSettingsService.updateFieldSettings(
|
||||
fieldId: fieldId,
|
||||
fieldVisibility: FieldVisibility.AlwaysShown,
|
||||
);
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
|
||||
+271
-217
@@ -1,24 +1,27 @@
|
||||
import 'dart:collection';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_view_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/filter/filter_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/setting/setting_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/sort/sort_listener.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/sort/sort_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../grid/presentation/widgets/filter/filter_info.dart';
|
||||
import '../../grid/presentation/widgets/sort/sort_info.dart';
|
||||
import '../database_view_service.dart';
|
||||
import '../filter/filter_listener.dart';
|
||||
import '../filter/filter_service.dart';
|
||||
import '../row/row_cache.dart';
|
||||
import '../setting/setting_listener.dart';
|
||||
import '../setting/setting_service.dart';
|
||||
import '../sort/sort_listener.dart';
|
||||
import '../sort/sort_service.dart';
|
||||
|
||||
import 'field_info.dart';
|
||||
import 'field_listener.dart';
|
||||
|
||||
@@ -72,20 +75,23 @@ typedef OnReceiveUpdateFields = void Function(List<FieldInfo>);
|
||||
typedef OnReceiveFields = void Function(List<FieldInfo>);
|
||||
typedef OnReceiveFilters = void Function(List<FilterInfo>);
|
||||
typedef OnReceiveSorts = void Function(List<SortInfo>);
|
||||
typedef OnReceiveFieldSettings = void Function(List<FieldInfo>);
|
||||
|
||||
class FieldController {
|
||||
final String viewId;
|
||||
|
||||
// Listeners
|
||||
final FieldsListener _fieldListener;
|
||||
final DatabaseSettingListener _settingListener;
|
||||
final FiltersListener _filtersListener;
|
||||
final SortsListener _sortsListener;
|
||||
final FieldSettingsListener _fieldSettingsListener;
|
||||
|
||||
// FFI services
|
||||
final DatabaseViewBackendService _databaseViewBackendSvc;
|
||||
final SettingBackendService _settingBackendSvc;
|
||||
final FilterBackendService _filterBackendSvc;
|
||||
final SortBackendService _sortBackendSvc;
|
||||
final FieldSettingsBackendService _fieldSettingsBackendSvc;
|
||||
|
||||
bool _isDisposed = false;
|
||||
|
||||
@@ -97,18 +103,17 @@ class FieldController {
|
||||
final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>
|
||||
_updatedFieldCallbacks = {};
|
||||
|
||||
// Group callbacks
|
||||
final Map<String, GroupSettingPB> _groupConfigurationByFieldId = {};
|
||||
|
||||
// Filter callbacks
|
||||
final Map<OnReceiveFilters, VoidCallback> _filterCallbacks = {};
|
||||
_GridFilterNotifier? _filterNotifier = _GridFilterNotifier();
|
||||
final Map<String, FilterPB> _filterPBByFieldId = {};
|
||||
|
||||
// Sort callbacks
|
||||
final Map<OnReceiveSorts, VoidCallback> _sortCallbacks = {};
|
||||
_GridSortNotifier? _sortNotifier = _GridSortNotifier();
|
||||
final Map<String, SortPB> _sortPBByFieldId = {};
|
||||
|
||||
// Database settings temporary storage
|
||||
final Map<String, GroupSettingPB> _groupConfigurationByFieldId = {};
|
||||
final List<FieldSettingsPB> _fieldSettings = [];
|
||||
|
||||
// Getters
|
||||
List<FieldInfo> get fieldInfos => [..._fieldNotifier.fieldInfos];
|
||||
@@ -116,39 +121,28 @@ class FieldController {
|
||||
List<SortInfo> get sortInfos => [..._sortNotifier?.sorts ?? []];
|
||||
|
||||
FieldInfo? getField(String fieldId) {
|
||||
final fields = _fieldNotifier.fieldInfos
|
||||
.where((element) => element.id == fieldId)
|
||||
.toList();
|
||||
|
||||
if (fields.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
assert(fields.length == 1);
|
||||
return fields.first;
|
||||
return _fieldNotifier.fieldInfos
|
||||
.firstWhereOrNull((element) => element.id == fieldId);
|
||||
}
|
||||
|
||||
FilterInfo? getFilter(String filterId) {
|
||||
final filters = _filterNotifier?.filters
|
||||
.where((element) => element.filter.id == filterId)
|
||||
.toList() ??
|
||||
[];
|
||||
if (filters.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
assert(filters.length == 1);
|
||||
return filters.first;
|
||||
FilterInfo? getFilterByFilterId(String filterId) {
|
||||
return _filterNotifier?.filters
|
||||
.firstWhereOrNull((element) => element.filterId == filterId);
|
||||
}
|
||||
|
||||
SortInfo? getSort(String sortId) {
|
||||
final sorts = _sortNotifier?.sorts
|
||||
.where((element) => element.sortId == sortId)
|
||||
.toList() ??
|
||||
[];
|
||||
if (sorts.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
assert(sorts.length == 1);
|
||||
return sorts.first;
|
||||
FilterInfo? getFilterByFieldId(String fieldId) {
|
||||
return _filterNotifier?.filters
|
||||
.firstWhereOrNull((element) => element.fieldId == fieldId);
|
||||
}
|
||||
|
||||
SortInfo? getSortBySortId(String sortId) {
|
||||
return _sortNotifier?.sorts
|
||||
.firstWhereOrNull((element) => element.sortId == sortId);
|
||||
}
|
||||
|
||||
SortInfo? getSortByFieldId(String fieldId) {
|
||||
return _sortNotifier?.sorts
|
||||
.firstWhereOrNull((element) => element.fieldId == fieldId);
|
||||
}
|
||||
|
||||
FieldController({required this.viewId})
|
||||
@@ -159,34 +153,18 @@ class FieldController {
|
||||
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: viewId),
|
||||
_sortBackendSvc = SortBackendService(viewId: viewId),
|
||||
_sortsListener = SortsListener(viewId: viewId),
|
||||
_settingBackendSvc = SettingBackendService(viewId: viewId) {
|
||||
//Listen on field's changes
|
||||
_fieldSettingsListener = FieldSettingsListener(viewId: viewId),
|
||||
_fieldSettingsBackendSvc = FieldSettingsBackendService(viewId: viewId) {
|
||||
// Start listeners
|
||||
_listenOnFieldChanges();
|
||||
|
||||
//Listen on setting changes
|
||||
_listenOnSettingChanges();
|
||||
|
||||
//Listen on the filter changes
|
||||
_listenOnFilterChanges();
|
||||
|
||||
//Listen on the sort changes
|
||||
_listenOnSortChanged();
|
||||
|
||||
_settingBackendSvc.getSetting().then((result) {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.fold(
|
||||
(setting) => _updateSetting(setting),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
_listenOnFieldSettingsChanged();
|
||||
}
|
||||
|
||||
/// Listen for filter changes in the backend.
|
||||
void _listenOnFilterChanges() {
|
||||
//Listen on the filter changes
|
||||
|
||||
deleteFilterFromChangeset(
|
||||
List<FilterInfo> filters,
|
||||
FilterChangesetNotificationPB changeset,
|
||||
@@ -196,9 +174,6 @@ class FieldController {
|
||||
filters.retainWhere(
|
||||
(element) => !deleteFilterIds.contains(element.filter.id),
|
||||
);
|
||||
|
||||
_filterPBByFieldId
|
||||
.removeWhere((key, value) => deleteFilterIds.contains(value.id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +191,6 @@ class FieldController {
|
||||
fieldType: newFilter.fieldType,
|
||||
);
|
||||
if (fieldInfo != null) {
|
||||
_filterPBByFieldId[fieldInfo.id] = newFilter;
|
||||
filters.add(FilterInfo(viewId, newFilter, fieldInfo));
|
||||
}
|
||||
}
|
||||
@@ -234,8 +208,6 @@ class FieldController {
|
||||
// Remove the old filter
|
||||
if (filterIndex != -1) {
|
||||
filters.removeAt(filterIndex);
|
||||
_filterPBByFieldId
|
||||
.removeWhere((key, value) => value.id == updatedFilter.filterId);
|
||||
}
|
||||
|
||||
// Insert the filter if there is a filter and its field info is
|
||||
@@ -257,7 +229,6 @@ class FieldController {
|
||||
} else {
|
||||
filters.add(filterInfo);
|
||||
}
|
||||
_filterPBByFieldId[fieldInfo.id] = updatedFilter.filter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,16 +243,17 @@ class FieldController {
|
||||
result.fold(
|
||||
(FilterChangesetNotificationPB changeset) {
|
||||
final List<FilterInfo> filters = filterInfos;
|
||||
// Deletes the filters
|
||||
// delete removed filters
|
||||
deleteFilterFromChangeset(filters, changeset);
|
||||
|
||||
// Inserts the new filter if it's not exist
|
||||
// insert new filters
|
||||
insertFilterFromChangeset(filters, changeset);
|
||||
|
||||
// edit modified filters
|
||||
updateFilterFromChangeset(filters, changeset);
|
||||
|
||||
_updateFieldInfos();
|
||||
_filterNotifier?.filters = filters;
|
||||
_updateFieldInfos();
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
@@ -289,6 +261,7 @@ class FieldController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Listen for sort changes in the backend.
|
||||
void _listenOnSortChanged() {
|
||||
deleteSortFromChangeset(
|
||||
List<SortInfo> newSortInfos,
|
||||
@@ -299,9 +272,6 @@ class FieldController {
|
||||
newSortInfos.retainWhere(
|
||||
(element) => !deleteSortIds.contains(element.sortId),
|
||||
);
|
||||
|
||||
_sortPBByFieldId
|
||||
.removeWhere((key, value) => deleteSortIds.contains(value.id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +290,6 @@ class FieldController {
|
||||
);
|
||||
|
||||
if (fieldInfo != null) {
|
||||
_sortPBByFieldId[newSortPB.fieldId] = newSortPB;
|
||||
newSortInfos.add(SortInfo(sortPB: newSortPB, fieldInfo: fieldInfo));
|
||||
}
|
||||
}
|
||||
@@ -356,7 +325,6 @@ class FieldController {
|
||||
} else {
|
||||
newSortInfos.add(newSortInfo);
|
||||
}
|
||||
_sortPBByFieldId[updatedSort.fieldId] = updatedSort;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,7 +341,6 @@ class FieldController {
|
||||
insertSortFromChangeset(newSortInfos, changeset);
|
||||
updateSortFromChangeset(newSortInfos, changeset);
|
||||
|
||||
_updateFieldInfos();
|
||||
_sortNotifier?.sorts = newSortInfos;
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
@@ -382,8 +349,8 @@ class FieldController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Listen for databse setting changes in the backend.
|
||||
void _listenOnSettingChanges() {
|
||||
//Listen on setting changes
|
||||
_settingListener.start(
|
||||
onSettingUpdated: (result) {
|
||||
if (_isDisposed) {
|
||||
@@ -398,19 +365,95 @@ class FieldController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Listen for field changes in the backend.
|
||||
void _listenOnFieldChanges() {
|
||||
//Listen on field's changes
|
||||
void deleteFields(List<FieldIdPB> deletedFields) {
|
||||
if (deletedFields.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final Map<String, FieldIdPB> deletedFieldMap = {
|
||||
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
|
||||
};
|
||||
|
||||
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
}
|
||||
|
||||
Future<FieldInfo> attachFieldSettings(FieldInfo fieldInfo) async {
|
||||
return _fieldSettingsBackendSvc
|
||||
.getFieldSettings(fieldInfo.id)
|
||||
.then((result) {
|
||||
final fieldSettings = result.fold(
|
||||
(fieldSettings) => fieldSettings,
|
||||
(err) {
|
||||
return null;
|
||||
},
|
||||
);
|
||||
if (fieldSettings == null) {
|
||||
return fieldInfo;
|
||||
}
|
||||
final updatedFieldInfo =
|
||||
fieldInfo.copyWith(fieldSettings: fieldSettings);
|
||||
|
||||
return updatedFieldInfo;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> insertFields(List<IndexFieldPB> insertedFields) async {
|
||||
if (insertedFields.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final List<FieldInfo> newFieldInfos = fieldInfos;
|
||||
for (final indexField in insertedFields) {
|
||||
final initial = FieldInfo.initial(indexField.field_1);
|
||||
final fieldInfo = await attachFieldSettings(initial);
|
||||
if (newFieldInfos.length > indexField.index) {
|
||||
newFieldInfos.insert(indexField.index, fieldInfo);
|
||||
} else {
|
||||
newFieldInfos.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
_fieldNotifier.fieldInfos = newFieldInfos;
|
||||
}
|
||||
|
||||
Future<List<FieldInfo>> updateFields(List<FieldPB> updatedFieldPBs) async {
|
||||
if (updatedFieldPBs.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final List<FieldInfo> updatedFields = [];
|
||||
for (final updatedFieldPB in updatedFieldPBs) {
|
||||
final index =
|
||||
newFields.indexWhere((field) => field.id == updatedFieldPB.id);
|
||||
if (index != -1) {
|
||||
newFields.removeAt(index);
|
||||
final initial = FieldInfo.initial(updatedFieldPB);
|
||||
final fieldInfo = await attachFieldSettings(initial);
|
||||
newFields.insert(index, fieldInfo);
|
||||
updatedFields.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedFields.isNotEmpty) {
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
}
|
||||
return updatedFields;
|
||||
}
|
||||
|
||||
// Listen on field's changes
|
||||
_fieldListener.start(
|
||||
onFieldsChanged: (result) {
|
||||
onFieldsChanged: (result) async {
|
||||
result.fold(
|
||||
(changeset) {
|
||||
(changeset) async {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
_deleteFields(changeset.deletedFields);
|
||||
_insertFields(changeset.insertedFields);
|
||||
deleteFields(changeset.deletedFields);
|
||||
insertFields(changeset.insertedFields);
|
||||
|
||||
final updatedFields = _updateFields(changeset.updatedFields);
|
||||
final updatedFields = await updateFields(changeset.updatedFields);
|
||||
for (final listener in _updatedFieldCallbacks.values) {
|
||||
listener(updatedFields);
|
||||
}
|
||||
@@ -421,31 +464,70 @@ class FieldController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Listen for field setting changes in the backend.
|
||||
void _listenOnFieldSettingsChanged() {
|
||||
FieldInfo updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
FieldInfo updatedField = newFields[0];
|
||||
|
||||
final index = newFields
|
||||
.indexWhere((field) => field.id == updatedFieldSettings.fieldId);
|
||||
if (index != -1) {
|
||||
newFields[index] =
|
||||
newFields[index].copyWith(fieldSettings: updatedFieldSettings);
|
||||
updatedField = newFields[index];
|
||||
}
|
||||
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
return updatedField;
|
||||
}
|
||||
|
||||
_fieldSettingsListener.start(
|
||||
onFieldSettingsChanged: (result) {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
result.fold(
|
||||
(fieldSettings) {
|
||||
final updatedFieldInfo = updateFieldSettings(fieldSettings);
|
||||
for (final listener in _updatedFieldCallbacks.values) {
|
||||
listener([updatedFieldInfo]);
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates sort, filter, group and field info from `DatabaseViewSettingPB`
|
||||
void _updateSetting(DatabaseViewSettingPB setting) {
|
||||
_groupConfigurationByFieldId.clear();
|
||||
for (final configuration in setting.groupSettings.items) {
|
||||
_groupConfigurationByFieldId[configuration.fieldId] = configuration;
|
||||
}
|
||||
|
||||
for (final filter in setting.filters.items) {
|
||||
_filterPBByFieldId[filter.fieldId] = filter;
|
||||
}
|
||||
_filterNotifier?.filters = _filterInfoListFromPBs(setting.filters.items);
|
||||
|
||||
for (final sort in setting.sorts.items) {
|
||||
_sortPBByFieldId[sort.fieldId] = sort;
|
||||
}
|
||||
_sortNotifier?.sorts = _sortInfoListFromPBs(setting.sorts.items);
|
||||
|
||||
_fieldSettings.clear();
|
||||
_fieldSettings.addAll(setting.fieldSettings.items);
|
||||
|
||||
_updateFieldInfos();
|
||||
}
|
||||
|
||||
/// Attach sort, filter, group information and field settings to `FieldInfo`
|
||||
void _updateFieldInfos() {
|
||||
final List<FieldInfo> newFieldInfos = [];
|
||||
for (final field in _fieldNotifier.fieldInfos) {
|
||||
newFieldInfos.add(
|
||||
field.copyWith(
|
||||
fieldSettings: _fieldSettings
|
||||
.firstWhereOrNull((setting) => setting.fieldId == field.id),
|
||||
isGroupField: _groupConfigurationByFieldId[field.id] != null,
|
||||
hasFilter: _filterPBByFieldId[field.id] != null,
|
||||
hasSort: _sortPBByFieldId[field.id] != null,
|
||||
hasFilter: getFilterByFieldId(field.id) != null,
|
||||
hasSort: getSortByFieldId(field.id) != null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -453,52 +535,27 @@ class FieldController {
|
||||
_fieldNotifier.fieldInfos = newFieldInfos;
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
if (_isDisposed) {
|
||||
Log.warn('FieldController is already disposed');
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
await _fieldListener.stop();
|
||||
await _filtersListener.stop();
|
||||
await _settingListener.stop();
|
||||
await _sortsListener.stop();
|
||||
|
||||
for (final callback in _fieldCallbacks.values) {
|
||||
_fieldNotifier.removeListener(callback);
|
||||
}
|
||||
_fieldNotifier.dispose();
|
||||
|
||||
for (final callback in _filterCallbacks.values) {
|
||||
_filterNotifier?.removeListener(callback);
|
||||
}
|
||||
for (final callback in _sortCallbacks.values) {
|
||||
_sortNotifier?.removeListener(callback);
|
||||
}
|
||||
|
||||
_filterNotifier?.dispose();
|
||||
_filterNotifier = null;
|
||||
|
||||
_sortNotifier?.dispose();
|
||||
_sortNotifier = null;
|
||||
}
|
||||
|
||||
/// Load all of the fields. This is required when opening the database
|
||||
Future<Either<Unit, FlowyError>> loadFields({
|
||||
required List<FieldIdPB> fieldIds,
|
||||
}) async {
|
||||
final result = await _databaseViewBackendSvc.getFields(fieldIds: fieldIds);
|
||||
return Future(
|
||||
() => result.fold(
|
||||
(newFields) {
|
||||
(newFields) async {
|
||||
if (_isDisposed) {
|
||||
return left(unit);
|
||||
}
|
||||
|
||||
_fieldNotifier.fieldInfos =
|
||||
newFields.map((field) => FieldInfo.initial(field)).toList();
|
||||
_loadFilters();
|
||||
_loadSorts();
|
||||
await Future.wait([
|
||||
_loadFilters(),
|
||||
_loadSorts(),
|
||||
_loadAllFieldSettings(),
|
||||
]);
|
||||
_updateFieldInfos();
|
||||
|
||||
return left(unit);
|
||||
},
|
||||
(err) => right(err),
|
||||
@@ -506,24 +563,12 @@ class FieldController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Load all the filters from the backend. Required by `loadFields`
|
||||
Future<Either<Unit, FlowyError>> _loadFilters() async {
|
||||
return _filterBackendSvc.getAllFilters().then((result) {
|
||||
return result.fold(
|
||||
(filterPBs) {
|
||||
final List<FilterInfo> filters = [];
|
||||
for (final filterPB in filterPBs) {
|
||||
final fieldInfo = _findFieldInfo(
|
||||
fieldInfos: fieldInfos,
|
||||
fieldId: filterPB.fieldId,
|
||||
fieldType: filterPB.fieldType,
|
||||
);
|
||||
if (fieldInfo != null) {
|
||||
final filterInfo = FilterInfo(viewId, filterPB, fieldInfo);
|
||||
filters.add(filterInfo);
|
||||
}
|
||||
}
|
||||
|
||||
_filterNotifier?.filters = filters;
|
||||
_filterNotifier?.filters = _filterInfoListFromPBs(filterPBs);
|
||||
return left(unit);
|
||||
},
|
||||
(err) => right(err),
|
||||
@@ -531,26 +576,12 @@ class FieldController {
|
||||
});
|
||||
}
|
||||
|
||||
/// Load all the sorts from the backend. Required by `loadFields`
|
||||
Future<Either<Unit, FlowyError>> _loadSorts() async {
|
||||
return _sortBackendSvc.getAllSorts().then((result) {
|
||||
return result.fold(
|
||||
(sortPBs) {
|
||||
final List<SortInfo> sortInfos = [];
|
||||
for (final sortPB in sortPBs) {
|
||||
final fieldInfo = _findFieldInfo(
|
||||
fieldInfos: fieldInfos,
|
||||
fieldId: sortPB.fieldId,
|
||||
fieldType: sortPB.fieldType,
|
||||
);
|
||||
|
||||
if (fieldInfo != null) {
|
||||
final sortInfo = SortInfo(sortPB: sortPB, fieldInfo: fieldInfo);
|
||||
sortInfos.add(sortInfo);
|
||||
}
|
||||
}
|
||||
|
||||
_updateFieldInfos();
|
||||
_sortNotifier?.sorts = sortInfos;
|
||||
_sortNotifier?.sorts = _sortInfoListFromPBs(sortPBs);
|
||||
return left(unit);
|
||||
},
|
||||
(err) => right(err),
|
||||
@@ -558,6 +589,56 @@ class FieldController {
|
||||
});
|
||||
}
|
||||
|
||||
/// Load all the field settings from the backend. Required by `loadFields`
|
||||
Future<Either<Unit, FlowyError>> _loadAllFieldSettings() async {
|
||||
return _fieldSettingsBackendSvc.getAllFieldSettings().then((result) {
|
||||
return result.fold(
|
||||
(fieldSettingsList) {
|
||||
_fieldSettings.clear();
|
||||
_fieldSettings.addAll(fieldSettingsList);
|
||||
return left(unit);
|
||||
},
|
||||
(err) => right(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Attach corresponding `FieldInfo`s to the `FilterPB`s
|
||||
List<FilterInfo> _filterInfoListFromPBs(List<FilterPB> filterPBs) {
|
||||
FilterInfo? getFilterInfo(FilterPB filterPB) {
|
||||
final fieldInfo = _findFieldInfo(
|
||||
fieldInfos: fieldInfos,
|
||||
fieldId: filterPB.fieldId,
|
||||
fieldType: filterPB.fieldType,
|
||||
);
|
||||
return fieldInfo != null ? FilterInfo(viewId, filterPB, fieldInfo) : null;
|
||||
}
|
||||
|
||||
return filterPBs
|
||||
.map((filterPB) => getFilterInfo(filterPB))
|
||||
.whereType<FilterInfo>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Attach corresponding `FieldInfo`s to the `SortPB`s
|
||||
List<SortInfo> _sortInfoListFromPBs(List<SortPB> sortPBs) {
|
||||
SortInfo? getSortInfo(SortPB sortPB) {
|
||||
final fieldInfo = _findFieldInfo(
|
||||
fieldInfos: fieldInfos,
|
||||
fieldId: sortPB.fieldId,
|
||||
fieldType: sortPB.fieldType,
|
||||
);
|
||||
return fieldInfo != null
|
||||
? SortInfo(sortPB: sortPB, fieldInfo: fieldInfo)
|
||||
: null;
|
||||
}
|
||||
|
||||
return sortPBs
|
||||
.map((sortPB) => getSortInfo(sortPB))
|
||||
.whereType<SortInfo>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
void addListener({
|
||||
OnReceiveFields? onReceiveFields,
|
||||
OnReceiveUpdateFields? onFieldsChanged,
|
||||
@@ -640,57 +721,35 @@ class FieldController {
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteFields(List<FieldIdPB> deletedFields) {
|
||||
if (deletedFields.isEmpty) {
|
||||
/// Stop listeners, dispose notifiers and clear listener callbacks
|
||||
Future<void> dispose() async {
|
||||
if (_isDisposed) {
|
||||
Log.warn('FieldController is already disposed');
|
||||
return;
|
||||
}
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final Map<String, FieldIdPB> deletedFieldMap = {
|
||||
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
|
||||
};
|
||||
_isDisposed = true;
|
||||
await _fieldListener.stop();
|
||||
await _filtersListener.stop();
|
||||
await _settingListener.stop();
|
||||
await _sortsListener.stop();
|
||||
await _fieldSettingsListener.stop();
|
||||
|
||||
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
}
|
||||
for (final callback in _fieldCallbacks.values) {
|
||||
_fieldNotifier.removeListener(callback);
|
||||
}
|
||||
_fieldNotifier.dispose();
|
||||
|
||||
void _insertFields(List<IndexFieldPB> insertedFields) {
|
||||
if (insertedFields.isEmpty) {
|
||||
return;
|
||||
for (final callback in _filterCallbacks.values) {
|
||||
_filterNotifier?.removeListener(callback);
|
||||
}
|
||||
final List<FieldInfo> newFieldInfos = fieldInfos;
|
||||
for (final indexField in insertedFields) {
|
||||
final fieldInfo = FieldInfo.initial(indexField.field_1);
|
||||
if (newFieldInfos.length > indexField.index) {
|
||||
newFieldInfos.insert(indexField.index, fieldInfo);
|
||||
} else {
|
||||
newFieldInfos.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
_fieldNotifier.fieldInfos = newFieldInfos;
|
||||
}
|
||||
_filterNotifier?.dispose();
|
||||
_filterNotifier = null;
|
||||
|
||||
List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {
|
||||
if (updatedFieldPBs.isEmpty) {
|
||||
return [];
|
||||
for (final callback in _sortCallbacks.values) {
|
||||
_sortNotifier?.removeListener(callback);
|
||||
}
|
||||
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final List<FieldInfo> updatedFields = [];
|
||||
for (final updatedFieldPB in updatedFieldPBs) {
|
||||
final index =
|
||||
newFields.indexWhere((field) => field.id == updatedFieldPB.id);
|
||||
if (index != -1) {
|
||||
newFields.removeAt(index);
|
||||
final fieldInfo = FieldInfo.initial(updatedFieldPB);
|
||||
newFields.insert(index, fieldInfo);
|
||||
updatedFields.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedFields.isNotEmpty) {
|
||||
_fieldNotifier.fieldInfos = newFields;
|
||||
}
|
||||
return updatedFields;
|
||||
_sortNotifier?.dispose();
|
||||
_sortNotifier = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,12 +786,7 @@ FieldInfo? _findFieldInfo({
|
||||
required String fieldId,
|
||||
required FieldType fieldType,
|
||||
}) {
|
||||
final fieldIndex = fieldInfos.indexWhere((element) {
|
||||
return element.id == fieldId && element.fieldType == fieldType;
|
||||
});
|
||||
if (fieldIndex != -1) {
|
||||
return fieldInfos[fieldIndex];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return fieldInfos.firstWhereOrNull(
|
||||
(element) => element.id == fieldId && element.fieldType == fieldType,
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'field_info.freezed.dart';
|
||||
|
||||
@@ -8,6 +9,7 @@ class FieldInfo with _$FieldInfo {
|
||||
|
||||
const factory FieldInfo({
|
||||
required FieldPB field,
|
||||
required FieldSettingsPB? fieldSettings,
|
||||
required bool isGroupField,
|
||||
required bool hasFilter,
|
||||
required bool hasSort,
|
||||
@@ -21,8 +23,11 @@ class FieldInfo with _$FieldInfo {
|
||||
|
||||
bool get isPrimary => field.isPrimary;
|
||||
|
||||
FieldVisibility? get visibility => fieldSettings?.visibility;
|
||||
|
||||
factory FieldInfo.initial(FieldPB field) => FieldInfo(
|
||||
field: field,
|
||||
fieldSettings: null,
|
||||
hasFilter: false,
|
||||
hasSort: false,
|
||||
isGroupField: false,
|
||||
|
||||
-5
@@ -30,7 +30,6 @@ class FieldBackendService {
|
||||
Future<Either<Unit, FlowyError>> updateField({
|
||||
String? name,
|
||||
bool? frozen,
|
||||
bool? visibility,
|
||||
double? width,
|
||||
}) {
|
||||
final payload = FieldChangesetPB.create()
|
||||
@@ -45,10 +44,6 @@ class FieldBackendService {
|
||||
payload.frozen = frozen;
|
||||
}
|
||||
|
||||
if (visibility != null) {
|
||||
payload.visibility = visibility;
|
||||
}
|
||||
|
||||
if (width != null) {
|
||||
payload.width = width.toInt();
|
||||
}
|
||||
|
||||
-10
@@ -27,13 +27,6 @@ class DateTypeOptionBloc
|
||||
),
|
||||
);
|
||||
},
|
||||
includeTime: (_IncludeTime value) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
typeOption: _updateTypeOption(includeTime: value.includeTime),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -42,7 +35,6 @@ class DateTypeOptionBloc
|
||||
DateTypeOptionPB _updateTypeOption({
|
||||
DateFormatPB? dateFormat,
|
||||
TimeFormatPB? timeFormat,
|
||||
bool? includeTime,
|
||||
}) {
|
||||
state.typeOption.freeze();
|
||||
return state.typeOption.rebuild((typeOption) {
|
||||
@@ -63,8 +55,6 @@ class DateTypeOptionEvent with _$DateTypeOptionEvent {
|
||||
_DidSelectDateFormat;
|
||||
const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) =
|
||||
_DidSelectTimeFormat;
|
||||
const factory DateTypeOptionEvent.includeTime(bool includeTime) =
|
||||
_IncludeTime;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
import 'type_option_context.dart';
|
||||
part 'timestamp_bloc.freezed.dart';
|
||||
|
||||
class TimestampTypeOptionBloc
|
||||
extends Bloc<TimestampTypeOptionEvent, TimestampTypeOptionState> {
|
||||
TimestampTypeOptionBloc({
|
||||
required TimestampTypeOptionContext typeOptionContext,
|
||||
}) : super(TimestampTypeOptionState.initial(typeOptionContext.typeOption)) {
|
||||
on<TimestampTypeOptionEvent>(
|
||||
(event, emit) async {
|
||||
event.map(
|
||||
didSelectDateFormat: (_DidSelectDateFormat value) {
|
||||
_updateTypeOption(dateFormat: value.format, emit: emit);
|
||||
},
|
||||
didSelectTimeFormat: (_DidSelectTimeFormat value) {
|
||||
_updateTypeOption(timeFormat: value.format, emit: emit);
|
||||
},
|
||||
includeTime: (_IncludeTime value) {
|
||||
_updateTypeOption(includeTime: value.includeTime, emit: emit);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateTypeOption({
|
||||
DateFormatPB? dateFormat,
|
||||
TimeFormatPB? timeFormat,
|
||||
bool? includeTime,
|
||||
required Emitter<TimestampTypeOptionState> emit,
|
||||
}) {
|
||||
state.typeOption.freeze();
|
||||
final newTypeOption = state.typeOption.rebuild((typeOption) {
|
||||
if (dateFormat != null) {
|
||||
typeOption.dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
if (timeFormat != null) {
|
||||
typeOption.timeFormat = timeFormat;
|
||||
}
|
||||
|
||||
if (includeTime != null) {
|
||||
typeOption.includeTime = includeTime;
|
||||
}
|
||||
});
|
||||
emit(state.copyWith(typeOption: newTypeOption));
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampTypeOptionEvent with _$TimestampTypeOptionEvent {
|
||||
const factory TimestampTypeOptionEvent.didSelectDateFormat(
|
||||
DateFormatPB format,
|
||||
) = _DidSelectDateFormat;
|
||||
const factory TimestampTypeOptionEvent.didSelectTimeFormat(
|
||||
TimeFormatPB format,
|
||||
) = _DidSelectTimeFormat;
|
||||
const factory TimestampTypeOptionEvent.includeTime(bool includeTime) =
|
||||
_IncludeTime;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampTypeOptionState with _$TimestampTypeOptionState {
|
||||
const factory TimestampTypeOptionState({
|
||||
required TimestampTypeOptionPB typeOption,
|
||||
}) = _TimestampTypeOptionState;
|
||||
|
||||
factory TimestampTypeOptionState.initial(TimestampTypeOptionPB typeOption) =>
|
||||
TimestampTypeOptionState(typeOption: typeOption);
|
||||
}
|
||||
+13
-1
@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
@@ -58,7 +59,7 @@ class URLTypeOptionWidgetDataParser extends TypeOptionParser<URLTypeOptionPB> {
|
||||
}
|
||||
}
|
||||
|
||||
// Date
|
||||
// DateTime
|
||||
typedef DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
|
||||
|
||||
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
||||
@@ -68,6 +69,17 @@ class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
||||
}
|
||||
}
|
||||
|
||||
// LastModified and CreatedAt
|
||||
typedef TimestampTypeOptionContext = TypeOptionContext<TimestampTypeOptionPB>;
|
||||
|
||||
class TimestampTypeOptionDataParser
|
||||
extends TypeOptionParser<TimestampTypeOptionPB> {
|
||||
@override
|
||||
TimestampTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
return TimestampTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
// SingleSelect
|
||||
typedef SingleSelectTypeOptionContext
|
||||
= TypeOptionContext<SingleSelectTypeOptionPB>;
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
|
||||
typedef FieldSettingsValue = Either<FieldSettingsPB, FlowyError>;
|
||||
|
||||
class FieldSettingsListener {
|
||||
final String viewId;
|
||||
PublishNotifier<FieldSettingsValue>? _fieldSettingsNotifier =
|
||||
PublishNotifier();
|
||||
DatabaseNotificationListener? _listener;
|
||||
|
||||
FieldSettingsListener({required this.viewId});
|
||||
|
||||
void start({
|
||||
required void Function(FieldSettingsValue) onFieldSettingsChanged,
|
||||
}) {
|
||||
_fieldSettingsNotifier?.addPublishListener(onFieldSettingsChanged);
|
||||
_listener = DatabaseNotificationListener(
|
||||
objectId: viewId,
|
||||
handler: _handler,
|
||||
);
|
||||
}
|
||||
|
||||
void _handler(
|
||||
DatabaseNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case DatabaseNotification.DidUpdateFieldSettings:
|
||||
result.fold(
|
||||
(payload) => _fieldSettingsNotifier?.value =
|
||||
left(FieldSettingsPB.fromBuffer(payload)),
|
||||
(error) => _fieldSettingsNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_fieldSettingsNotifier?.dispose();
|
||||
_fieldSettingsNotifier = null;
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
class FieldSettingsBackendService {
|
||||
final String viewId;
|
||||
FieldSettingsBackendService({required this.viewId});
|
||||
|
||||
Future<Either<FieldSettingsPB, FlowyError>> getFieldSettings(
|
||||
String fieldId,
|
||||
) {
|
||||
final id = FieldIdPB(fieldId: fieldId);
|
||||
final ids = RepeatedFieldIdPB()..items.add(id);
|
||||
final payload = FieldIdsPB()
|
||||
..viewId = viewId
|
||||
..fieldIds = ids;
|
||||
|
||||
return DatabaseEventGetFieldSettings(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(fieldSettings) => left(fieldSettings.items.first),
|
||||
(r) => right(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Either<List<FieldSettingsPB>, FlowyError>> getAllFieldSettings() {
|
||||
final payload = DatabaseViewIdPB()..value = viewId;
|
||||
|
||||
return DatabaseEventGetAllFieldSettings(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(fieldSettings) => left(fieldSettings.items),
|
||||
(r) => right(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> updateFieldSettings({
|
||||
required String fieldId,
|
||||
FieldVisibility? fieldVisibility,
|
||||
}) {
|
||||
final FieldSettingsChangesetPB payload = FieldSettingsChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId;
|
||||
|
||||
if (fieldVisibility != null) {
|
||||
payload.visibility = fieldVisibility;
|
||||
}
|
||||
|
||||
return DatabaseEventUpdateFieldSettings(payload).send();
|
||||
}
|
||||
}
|
||||
+1
-5
@@ -32,11 +32,7 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
|
||||
await _listenRowMeteChanged();
|
||||
},
|
||||
didReceiveRowMeta: (RowMetaPB rowMeta) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
rowMeta: rowMeta,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(rowMeta: rowMeta));
|
||||
},
|
||||
setCover: (String coverURL) {
|
||||
_updateMeta(coverURL: coverURL);
|
||||
|
||||
@@ -74,6 +74,7 @@ class RowCache {
|
||||
final rowInfo = buildGridRow(row);
|
||||
_rowList.add(rowInfo);
|
||||
}
|
||||
_changedNotifier.receive(const ChangedReason.setInitialRows());
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
@@ -245,7 +246,8 @@ class RowCache {
|
||||
// ignore: prefer_collection_literals
|
||||
final cellContextMap = CellContextByFieldId();
|
||||
for (final fieldInfo in _fieldDelegate.fieldInfos) {
|
||||
if (fieldInfo.field.visibility) {
|
||||
if (fieldInfo.visibility != null &&
|
||||
fieldInfo.visibility != FieldVisibility.AlwaysHidden) {
|
||||
cellContextMap[fieldInfo.id] = DatabaseCellContext(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
@@ -281,6 +283,7 @@ class RowChangesetNotifier extends ChangeNotifier {
|
||||
initial: (_) {},
|
||||
reorderRows: (_) => notifyListeners(),
|
||||
reorderSingleRow: (_) => notifyListeners(),
|
||||
setInitialRows: (_) => notifyListeners(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -313,6 +316,7 @@ class ChangedReason with _$ChangedReason {
|
||||
ReorderSingleRowPB reorderRow,
|
||||
RowInfo rowInfo,
|
||||
) = _ReorderSingleRow;
|
||||
const factory ChangedReason.setInitialRows() = _SetInitialRows;
|
||||
}
|
||||
|
||||
class InsertedIndex {
|
||||
|
||||
+7
-5
@@ -1,6 +1,8 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@@ -28,13 +30,13 @@ class DatabasePropertyBloc
|
||||
_startListening();
|
||||
},
|
||||
setFieldVisibility: (_SetFieldVisibility value) async {
|
||||
final fieldBackendSvc = FieldBackendService(
|
||||
final fieldSettingsSvc = FieldSettingsBackendService(
|
||||
viewId: viewId,
|
||||
fieldId: value.fieldId,
|
||||
);
|
||||
|
||||
final result = await fieldBackendSvc.updateField(
|
||||
visibility: value.visibility,
|
||||
final result = await fieldSettingsSvc.updateFieldSettings(
|
||||
fieldId: value.fieldId,
|
||||
fieldVisibility: value.visibility,
|
||||
);
|
||||
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
@@ -84,7 +86,7 @@ class DatabasePropertyEvent with _$DatabasePropertyEvent {
|
||||
const factory DatabasePropertyEvent.initial() = _Initial;
|
||||
const factory DatabasePropertyEvent.setFieldVisibility(
|
||||
String fieldId,
|
||||
bool visibility,
|
||||
FieldVisibility visibility,
|
||||
) = _SetFieldVisibility;
|
||||
const factory DatabasePropertyEvent.didReceiveFieldUpdate(
|
||||
List<FieldInfo> fields,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'dart:io';
|
||||
import 'package:appflowy/workspace/application/settings/share/export_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
part 'share_bloc.freezed.dart';
|
||||
|
||||
class DatabaseShareBloc extends Bloc<DatabaseShareEvent, DatabaseShareState> {
|
||||
DatabaseShareBloc({
|
||||
required this.view,
|
||||
}) : super(const DatabaseShareState.initial()) {
|
||||
on<ShareCSV>(_onShareCSV);
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
Future<void> _onShareCSV(
|
||||
ShareCSV event,
|
||||
Emitter<DatabaseShareState> emit,
|
||||
) async {
|
||||
emit(const DatabaseShareState.loading());
|
||||
|
||||
final result = await BackendExportService.exportDatabaseAsCSV(view.id);
|
||||
result.fold(
|
||||
(l) => _saveCSVToPath(l.data, event.path),
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
|
||||
emit(
|
||||
DatabaseShareState.finish(
|
||||
result.fold(
|
||||
(l) {
|
||||
_saveCSVToPath(l.data, event.path);
|
||||
return left(unit);
|
||||
},
|
||||
(r) => right(r),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ExportDataPB _saveCSVToPath(String markdown, String path) {
|
||||
File(path).writeAsStringSync(markdown);
|
||||
return ExportDataPB()
|
||||
..data = markdown
|
||||
..exportType = ExportType.Markdown;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DatabaseShareEvent with _$DatabaseShareEvent {
|
||||
const factory DatabaseShareEvent.shareCSV(String path) = ShareCSV;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DatabaseShareState with _$DatabaseShareState {
|
||||
const factory DatabaseShareState.initial() = _Initial;
|
||||
const factory DatabaseShareState.loading() = _Loading;
|
||||
const factory DatabaseShareState.finish(
|
||||
Either<Unit, FlowyError> successOrFail,
|
||||
) = _Finish;
|
||||
}
|
||||
+23
@@ -167,6 +167,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();
|
||||
|
||||
+11
-7
@@ -2,6 +2,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@@ -16,18 +17,25 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
|
||||
GridHeaderBloc({
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
}) : super(GridHeaderState.initial(fieldController.fieldInfos)) {
|
||||
}) : super(GridHeaderState.initial()) {
|
||||
on<GridHeaderEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_InitialHeader value) async {
|
||||
_startListening();
|
||||
add(
|
||||
GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
|
||||
);
|
||||
},
|
||||
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
fields: value.fields
|
||||
.where((element) => element.field.visibility)
|
||||
.where(
|
||||
(element) =>
|
||||
element.visibility != null &&
|
||||
element.visibility != FieldVisibility.AlwaysHidden,
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
@@ -83,9 +91,5 @@ class GridHeaderState with _$GridHeaderState {
|
||||
const factory GridHeaderState({required List<FieldInfo> fields}) =
|
||||
_GridHeaderState;
|
||||
|
||||
factory GridHeaderState.initial(List<FieldInfo> fields) {
|
||||
// final List<FieldPB> newFields = List.from(fields);
|
||||
// newFields.retainWhere((field) => field.visibility);
|
||||
return GridHeaderState(fields: fields);
|
||||
}
|
||||
factory GridHeaderState.initial() => const GridHeaderState(fields: []);
|
||||
}
|
||||
+57
-27
@@ -1,27 +1,26 @@
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import '../../../application/cell/cell_service.dart';
|
||||
import '../../../application/field/field_service.dart';
|
||||
import '../../../application/row/row_controller.dart';
|
||||
|
||||
part 'row_detail_bloc.freezed.dart';
|
||||
|
||||
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
final RowBackendService rowService;
|
||||
final RowController dataController;
|
||||
final RowController rowController;
|
||||
|
||||
RowDetailBloc({
|
||||
required this.dataController,
|
||||
}) : rowService = RowBackendService(viewId: dataController.viewId),
|
||||
super(RowDetailState.initial()) {
|
||||
required this.rowController,
|
||||
}) : super(RowDetailState.initial()) {
|
||||
on<RowDetailEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
await _startListening();
|
||||
final cells = dataController.loadData();
|
||||
final cells = rowController.loadData();
|
||||
if (!isClosed) {
|
||||
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
|
||||
}
|
||||
@@ -32,23 +31,32 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
deleteField: (fieldId) {
|
||||
_fieldBackendService(fieldId).deleteField();
|
||||
},
|
||||
hideField: (fieldId) async {
|
||||
final result = await _fieldBackendService(fieldId).updateField(
|
||||
visibility: false,
|
||||
showField: (fieldId) async {
|
||||
final result =
|
||||
await FieldSettingsBackendService(viewId: rowController.viewId)
|
||||
.updateFieldSettings(
|
||||
fieldId: fieldId,
|
||||
fieldVisibility: FieldVisibility.AlwaysShown,
|
||||
);
|
||||
result.fold(
|
||||
(l) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
deleteRow: (rowId) async {
|
||||
await rowService.deleteRow(rowId);
|
||||
},
|
||||
duplicateRow: (String rowId, String? groupId) async {
|
||||
await rowService.duplicateRow(
|
||||
rowId: rowId,
|
||||
groupId: groupId,
|
||||
hideField: (fieldId) async {
|
||||
final result =
|
||||
await FieldSettingsBackendService(viewId: rowController.viewId)
|
||||
.updateFieldSettings(
|
||||
fieldId: fieldId,
|
||||
fieldVisibility: FieldVisibility.AlwaysHidden,
|
||||
);
|
||||
result.fold(
|
||||
(l) {},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
reorderField: (fieldId, fromIndex, toIndex) async {
|
||||
await _reorderField(fieldId, fromIndex, toIndex, emit);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -57,12 +65,12 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
dataController.dispose();
|
||||
rowController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _startListening() async {
|
||||
dataController.addListener(
|
||||
rowController.addListener(
|
||||
onRowChanged: (cells, reason) {
|
||||
if (!isClosed) {
|
||||
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
|
||||
@@ -73,20 +81,42 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
||||
|
||||
FieldBackendService _fieldBackendService(String fieldId) {
|
||||
return FieldBackendService(
|
||||
viewId: dataController.viewId,
|
||||
viewId: rowController.viewId,
|
||||
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
|
||||
class RowDetailEvent with _$RowDetailEvent {
|
||||
const factory RowDetailEvent.initial() = _Initial;
|
||||
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
|
||||
const factory RowDetailEvent.showField(String fieldId) = _ShowField;
|
||||
const factory RowDetailEvent.hideField(String fieldId) = _HideField;
|
||||
const factory RowDetailEvent.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;
|
||||
|
||||
+32
-55
@@ -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';
|
||||
@@ -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(
|
||||
|
||||
+2
-2
@@ -114,7 +114,7 @@ class _GridCreateFilterListState extends State<GridCreateFilterList> {
|
||||
class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
_FilterTextFieldDelegate();
|
||||
|
||||
double fixHeight = 46;
|
||||
double fixHeight = 36;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
@@ -123,7 +123,7 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
|
||||
bool overlapsContent,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
height: fixHeight,
|
||||
child: FlowyTextField(
|
||||
hintText: LocaleKeys.grid_settings_filterBy.tr(),
|
||||
|
||||
+4
@@ -22,6 +22,10 @@ class FilterInfo {
|
||||
);
|
||||
}
|
||||
|
||||
String get filterId => filter.id;
|
||||
|
||||
String get fieldId => filter.fieldId;
|
||||
|
||||
DateFilterPB? dateFilter() {
|
||||
if (![
|
||||
FieldType.DateTime,
|
||||
|
||||
-1
@@ -105,7 +105,6 @@ class _AddFilterButtonState extends State<AddFilterButton> {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
constraints: BoxConstraints.loose(const Size(200, 300)),
|
||||
margin: const EdgeInsets.all(6),
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
child: child,
|
||||
popupBuilder: (BuildContext context) {
|
||||
|
||||
+7
-8
@@ -154,34 +154,33 @@ class FieldCellButton extends StatelessWidget {
|
||||
final FieldPB field;
|
||||
final int? maxLines;
|
||||
final BorderRadius? radius;
|
||||
final EdgeInsets? margin;
|
||||
const FieldCellButton({
|
||||
required this.field,
|
||||
required this.onTap,
|
||||
this.maxLines = 1,
|
||||
this.radius = BorderRadius.zero,
|
||||
this.margin,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Using this technique to have proper text ellipsis
|
||||
// https://github.com/flutter/flutter/issues/18761#issuecomment-812390920
|
||||
final text = Characters(field.name)
|
||||
.replaceAll(Characters(''), Characters('\u{200B}'))
|
||||
.toString();
|
||||
return FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).greyHover,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onTap: onTap,
|
||||
leftIcon: FlowySvg(
|
||||
field.fieldType.icon(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
radius: radius,
|
||||
text: FlowyText.medium(
|
||||
text,
|
||||
field.name,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
margin: GridSize.cellContentInsets,
|
||||
margin: margin ?? GridSize.cellContentInsets,
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -102,7 +102,7 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: _HideFieldButton(
|
||||
child: HideFieldButton(
|
||||
popoverMutex: popoverMutex,
|
||||
onHidden: () {
|
||||
state.field.fold(
|
||||
@@ -236,11 +236,12 @@ class _DeleteFieldButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _HideFieldButton extends StatelessWidget {
|
||||
@visibleForTesting
|
||||
class HideFieldButton extends StatelessWidget {
|
||||
final PopoverMutex popoverMutex;
|
||||
final VoidCallback? onHidden;
|
||||
|
||||
const _HideFieldButton({
|
||||
const HideFieldButton({
|
||||
required this.popoverMutex,
|
||||
required this.onHidden,
|
||||
Key? key,
|
||||
|
||||
+2
-5
@@ -42,12 +42,10 @@ class _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
final bloc = getIt<GridHeaderBloc>(
|
||||
return getIt<GridHeaderBloc>(
|
||||
param1: widget.viewId,
|
||||
param2: widget.fieldController,
|
||||
);
|
||||
bloc.add(const GridHeaderEvent.initial());
|
||||
return bloc;
|
||||
)..add(const GridHeaderEvent.initial());
|
||||
},
|
||||
child: BlocBuilder<GridHeaderBloc, GridHeaderState>(
|
||||
buildWhen: (previous, current) =>
|
||||
@@ -97,7 +95,6 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
buildWhen: (previous, current) => previous.fields != current.fields,
|
||||
builder: (context, state) {
|
||||
final cells = state.fields
|
||||
.where((fieldInfo) => fieldInfo.field.visibility)
|
||||
.map(
|
||||
(field) => FieldContext(
|
||||
viewId: widget.viewId,
|
||||
|
||||
+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,
|
||||
|
||||
+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()
|
||||
|
||||
+2
-6
@@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
|
||||
+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),
|
||||
),
|
||||
|
||||
+2
@@ -8,4 +8,6 @@ class SortInfo {
|
||||
SortInfo({required this.sortPB, required this.fieldInfo});
|
||||
|
||||
String get sortId => sortPB.id;
|
||||
|
||||
String get fieldId => sortPB.fieldId;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/share_button.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
@@ -212,4 +213,16 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
|
||||
@override
|
||||
List<NavigationItem> get navigationItems => [this];
|
||||
|
||||
@override
|
||||
Widget? get rightBarItem {
|
||||
return Row(
|
||||
children: [
|
||||
DatabaseShareButton(
|
||||
key: ValueKey(notifier.view.id),
|
||||
view: notifier.view,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -80,7 +80,11 @@ class DateCardCellState with _$DateCardCellState {
|
||||
String _dateStrFromCellData(DateCellDataPB? cellData) {
|
||||
String dateStr = "";
|
||||
if (cellData != null) {
|
||||
dateStr = "${cellData.date} ${cellData.time}";
|
||||
if (cellData.includeTime) {
|
||||
dateStr = '${cellData.date} ${cellData.time}';
|
||||
} else {
|
||||
dateStr = cellData.date;
|
||||
}
|
||||
}
|
||||
return dateStr;
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
||||
part 'timestamp_card_cell_bloc.freezed.dart';
|
||||
|
||||
class TimestampCardCellBloc
|
||||
extends Bloc<TimestampCardCellEvent, TimestampCardCellState> {
|
||||
final TimestampCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
TimestampCardCellBloc({required this.cellController})
|
||||
: super(TimestampCardCellState.initial(cellController)) {
|
||||
on<TimestampCardCellEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () => _startListening(),
|
||||
didReceiveCellUpdate: (TimestampCellDataPB? cellData) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
data: cellData,
|
||||
dateStr: cellData?.dateTime ?? "",
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellChanged: ((data) {
|
||||
if (!isClosed) {
|
||||
add(TimestampCardCellEvent.didReceiveCellUpdate(data));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampCardCellEvent with _$TimestampCardCellEvent {
|
||||
const factory TimestampCardCellEvent.initial() = _InitialCell;
|
||||
const factory TimestampCardCellEvent.didReceiveCellUpdate(
|
||||
TimestampCellDataPB? data,
|
||||
) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampCardCellState with _$TimestampCardCellState {
|
||||
const factory TimestampCardCellState({
|
||||
required TimestampCellDataPB? data,
|
||||
required String dateStr,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _TimestampCardCellState;
|
||||
|
||||
factory TimestampCardCellState.initial(TimestampCellController context) {
|
||||
final cellData = context.getCellData();
|
||||
|
||||
return TimestampCardCellState(
|
||||
fieldInfo: context.fieldInfo,
|
||||
data: cellData,
|
||||
dateStr: cellData?.dateTime ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
+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,
|
||||
) {
|
||||
|
||||
+10
-4
@@ -32,10 +32,16 @@ class _ChecklistCardCellState extends State<ChecklistCardCell> {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ChecklistProgressBar(percent: state.percent),
|
||||
),
|
||||
builder: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ChecklistProgressBar(percent: state.percent),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart';
|
||||
import 'package: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() => _TimestampCardCellState();
|
||||
}
|
||||
|
||||
class _TimestampCardCellState extends State<TimestampCardCell> {
|
||||
late TimestampCardCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as TimestampCellController;
|
||||
|
||||
_cellBloc = TimestampCardCellBloc(cellController: cellController)
|
||||
..add(const TimestampCardCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<TimestampCardCellBloc, TimestampCardCellState>(
|
||||
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();
|
||||
}
|
||||
}
|
||||
+54
-38
@@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ
|
||||
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
@@ -44,7 +45,7 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
|
||||
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
|
||||
builder: (context, state) {
|
||||
final cells = state.fieldContexts.map((field) {
|
||||
return _GridPropertyCell(
|
||||
return GridPropertyCell(
|
||||
key: ValueKey(field.id),
|
||||
viewId: widget.viewId,
|
||||
fieldInfo: field,
|
||||
@@ -75,12 +76,13 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
|
||||
}
|
||||
}
|
||||
|
||||
class _GridPropertyCell extends StatefulWidget {
|
||||
@visibleForTesting
|
||||
class GridPropertyCell extends StatefulWidget {
|
||||
final FieldInfo fieldInfo;
|
||||
final String viewId;
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
const _GridPropertyCell({
|
||||
const GridPropertyCell({
|
||||
super.key,
|
||||
required this.fieldInfo,
|
||||
required this.viewId,
|
||||
@@ -88,26 +90,22 @@ class _GridPropertyCell extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<_GridPropertyCell> createState() => _GridPropertyCellState();
|
||||
State<GridPropertyCell> createState() => _GridPropertyCellState();
|
||||
}
|
||||
|
||||
class _GridPropertyCellState extends State<_GridPropertyCell> {
|
||||
class _GridPropertyCellState extends State<GridPropertyCell> {
|
||||
final PopoverController _popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final checkmark = FlowySvg(
|
||||
widget.fieldInfo.field.visibility ? FlowySvgs.show_m : FlowySvgs.hide_m,
|
||||
final visiblity = widget.fieldInfo.visibility;
|
||||
final visibleIcon = FlowySvg(
|
||||
visiblity != null && visiblity != FieldVisibility.AlwaysHidden
|
||||
? FlowySvgs.show_m
|
||||
: FlowySvgs.hide_m,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: _editFieldButton(context, checkmark),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _editFieldButton(BuildContext context, Widget checkmark) {
|
||||
return AppFlowyPopover(
|
||||
mutex: widget.popoverMutex,
|
||||
controller: _popoverController,
|
||||
@@ -116,30 +114,40 @@ class _GridPropertyCellState extends State<_GridPropertyCell> {
|
||||
constraints: BoxConstraints.loose(const Size(240, 400)),
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
margin: EdgeInsets.zero,
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
widget.fieldInfo.name,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
widget.fieldInfo.fieldType.icon(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
rightIcon: FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () {
|
||||
context.read<DatabasePropertyBloc>().add(
|
||||
DatabasePropertyEvent.setFieldVisibility(
|
||||
widget.fieldInfo.id,
|
||||
!widget.fieldInfo.field.visibility,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: checkmark.padding(all: 6.0),
|
||||
),
|
||||
onTap: () => _popoverController.show(),
|
||||
).padding(horizontal: 6.0),
|
||||
child: SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
widget.fieldInfo.name,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
widget.fieldInfo.fieldType.icon(),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
rightIcon: FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () {
|
||||
if (widget.fieldInfo.fieldSettings == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final newVisiblity = _newFieldVisibility(
|
||||
widget.fieldInfo.fieldSettings!.visibility,
|
||||
);
|
||||
context.read<DatabasePropertyBloc>().add(
|
||||
DatabasePropertyEvent.setFieldVisibility(
|
||||
widget.fieldInfo.id,
|
||||
newVisiblity,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: visibleIcon.padding(all: 6.0),
|
||||
),
|
||||
onTap: () => _popoverController.show(),
|
||||
).padding(horizontal: 6.0),
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return FieldEditor(
|
||||
viewId: widget.viewId,
|
||||
@@ -151,4 +159,12 @@ class _GridPropertyCellState extends State<_GridPropertyCell> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
FieldVisibility _newFieldVisibility(FieldVisibility current) {
|
||||
return switch (current) {
|
||||
FieldVisibility.AlwaysShown => FieldVisibility.AlwaysHidden,
|
||||
FieldVisibility.AlwaysHidden => FieldVisibility.AlwaysShown,
|
||||
_ => FieldVisibility.AlwaysHidden,
|
||||
};
|
||||
}
|
||||
}
|
||||
+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,
|
||||
);
|
||||
@@ -72,6 +72,7 @@ class GridCellBuilder {
|
||||
case FieldType.Number:
|
||||
return GridNumberCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
style: style,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.RichText:
|
||||
|
||||
+20
-2
@@ -9,12 +9,29 @@ 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();
|
||||
@@ -46,7 +63,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
|
||||
|
||||
+7
-3
@@ -40,7 +40,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) {
|
||||
@@ -56,8 +56,12 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) =>
|
||||
ChecklistProgressBar(percent: state.percent),
|
||||
builder: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return ChecklistProgressBar(percent: state.percent);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+19
-9
@@ -29,13 +29,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 +68,7 @@ class ChecklistCardCellBloc
|
||||
_loadOptions();
|
||||
},
|
||||
onCellChanged: (data) {
|
||||
if (!isClosed && data != null) {
|
||||
if (!isClosed) {
|
||||
add(ChecklistCellEvent.didReceiveOptions(data));
|
||||
}
|
||||
},
|
||||
@@ -81,7 +91,7 @@ class ChecklistCardCellBloc
|
||||
class ChecklistCellEvent with _$ChecklistCellEvent {
|
||||
const factory ChecklistCellEvent.initial() = _InitialCell;
|
||||
const factory ChecklistCellEvent.didReceiveOptions(
|
||||
ChecklistCellDataPB data,
|
||||
ChecklistCellDataPB? data,
|
||||
) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
|
||||
+324
-122
@@ -1,21 +1,23 @@
|
||||
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_popover/appflowy_popover.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 +25,367 @@ 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;
|
||||
|
||||
/// A flag that determines whether the new task text field is visible
|
||||
bool _isAddingNewTask = false;
|
||||
|
||||
@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();
|
||||
_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) {
|
||||
setState(() => _isAddingNewTask = true);
|
||||
}
|
||||
},
|
||||
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 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(),
|
||||
),
|
||||
return Focus(
|
||||
onKey: (node, event) {
|
||||
// don't hide new task text field if there are no tasks at all
|
||||
if (state.allOptions.isNotEmpty &&
|
||||
event is RawKeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
setState(() {
|
||||
_isAddingNewTask = false;
|
||||
});
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
physics: StyledScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: state.allOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
child: ChecklistProgressBar(
|
||||
percent: state.percent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ChecklistItemList(
|
||||
options: state.allOptions,
|
||||
newTaskFocusNode: newTaskFocusNode,
|
||||
isAddingNewTask: _isAddingNewTask,
|
||||
onUpdateTask: () => setState(() {
|
||||
_isAddingNewTask = true;
|
||||
newTaskFocusNode.requestFocus();
|
||||
}),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: TypeOptionSeparator(spacing: 0.0),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: ChecklistNewTaskButton(
|
||||
onTap: () => setState(() => _isAddingNewTask = true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@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 FocusNode newTaskFocusNode;
|
||||
final bool isAddingNewTask;
|
||||
final VoidCallback onUpdateTask;
|
||||
|
||||
const ChecklistItemList({
|
||||
super.key,
|
||||
required this.options,
|
||||
required this.onUpdateTask,
|
||||
required this.isAddingNewTask,
|
||||
required this.newTaskFocusNode,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChecklistItemList> createState() => _ChecklistItemListState();
|
||||
}
|
||||
|
||||
class _ChecklistItemListState extends State<ChecklistItemList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final itemList = [
|
||||
const VSpace(6.0),
|
||||
...widget.options.mapIndexed(
|
||||
(index, option) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: ChecklistItem(
|
||||
option: option,
|
||||
onSubmitted:
|
||||
index == widget.options.length - 1 ? widget.onUpdateTask : null,
|
||||
key: ValueKey(option.data.id),
|
||||
// only allow calling the callback for the last task in the list
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: widget.isAddingNewTask
|
||||
? NewTaskItem(focusNode: widget.newTaskFocusNode)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
const VSpace(6.0),
|
||||
];
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) => itemList[index],
|
||||
childCount: itemList.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
@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(),
|
||||
),
|
||||
onSubmitted: (taskDescription) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.updateTaskName(
|
||||
widget.option.data,
|
||||
taskDescription,
|
||||
),
|
||||
);
|
||||
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,
|
||||
/// 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.0),
|
||||
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const FlowyIconButton(
|
||||
width: 32,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
),
|
||||
hoverColor: Colors.transparent,
|
||||
),
|
||||
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_taskHint.tr(),
|
||||
),
|
||||
onSubmitted: (taskDescription) {
|
||||
if (taskDescription.trim().isNotEmpty) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.newTask(
|
||||
taskDescription.trim(),
|
||||
),
|
||||
);
|
||||
}
|
||||
_textEditingController.clear();
|
||||
},
|
||||
),
|
||||
),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.grid_checklist_submitNewTask.tr(),
|
||||
fontSize: 11,
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
hoverColor: 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
_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),
|
||||
);
|
||||
@visibleForTesting
|
||||
class ChecklistNewTaskButton extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
const ChecklistNewTaskButton({super.key, required this.onTap});
|
||||
|
||||
_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,
|
||||
),
|
||||
);
|
||||
},
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||
child: SizedBox(
|
||||
height: 30,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.grid_checklist_addNew.tr()),
|
||||
margin: const EdgeInsets.all(6),
|
||||
leftIcon: const FlowySvg(FlowySvgs.add_s),
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+59
-69
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
|
||||
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/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/date_cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
@@ -19,6 +19,7 @@ part 'date_cal_bloc.freezed.dart';
|
||||
|
||||
class DateCellCalendarBloc
|
||||
extends Bloc<DateCellCalendarEvent, DateCellCalendarState> {
|
||||
final DateCellBackendService _dateCellBackendService;
|
||||
final DateCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@@ -26,18 +27,24 @@ class DateCellCalendarBloc
|
||||
required DateTypeOptionPB dateTypeOptionPB,
|
||||
required DateCellDataPB? cellData,
|
||||
required this.cellController,
|
||||
}) : super(DateCellCalendarState.initial(dateTypeOptionPB, cellData)) {
|
||||
}) : _dateCellBackendService = DateCellBackendService(
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
rowId: cellController.rowId,
|
||||
),
|
||||
super(DateCellCalendarState.initial(dateTypeOptionPB, cellData)) {
|
||||
on<DateCellCalendarEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async => _startListening(),
|
||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
||||
final dateData = _dateDataFromCellData(cellData);
|
||||
final (dateTime, time, includeTime) =
|
||||
_dateDataFromCellData(cellData);
|
||||
emit(
|
||||
state.copyWith(
|
||||
dateTime: dateData.dateTime,
|
||||
time: dateData.time,
|
||||
includeTime: dateData.includeTime,
|
||||
dateTime: dateTime,
|
||||
time: time,
|
||||
includeTime: includeTime,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -45,13 +52,13 @@ class DateCellCalendarBloc
|
||||
emit(state.copyWith(timeFormatError: timeFormatError));
|
||||
},
|
||||
selectDay: (date) async {
|
||||
await _updateDateData(emit, date: date);
|
||||
await _updateDateData(date: date);
|
||||
},
|
||||
setIncludeTime: (includeTime) async {
|
||||
await _updateDateData(emit, includeTime: includeTime);
|
||||
await _updateDateData(includeTime: includeTime);
|
||||
},
|
||||
setTime: (time) async {
|
||||
await _updateDateData(emit, time: time);
|
||||
await _updateDateData(time: time);
|
||||
},
|
||||
setDateFormat: (dateFormat) async {
|
||||
await _updateTypeOption(emit, dateFormat: dateFormat);
|
||||
@@ -66,15 +73,14 @@ class DateCellCalendarBloc
|
||||
emit(state.copyWith(focusedDay: focusedDay));
|
||||
},
|
||||
clearDate: () async {
|
||||
await _clearDate(emit);
|
||||
await _clearDate();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateDateData(
|
||||
Emitter<DateCellCalendarState> emit, {
|
||||
Future<void> _updateDateData({
|
||||
DateTime? date,
|
||||
String? time,
|
||||
bool? includeTime,
|
||||
@@ -86,76 +92,60 @@ class DateCellCalendarBloc
|
||||
date != null && time == null,
|
||||
);
|
||||
final String? newTime = time ?? state.time;
|
||||
DateTime? newDate = _utcToLocalAddTime(date);
|
||||
DateTime? newDate = _utcToLocalAndAddCurrentTime(date);
|
||||
if (time != null && time.isNotEmpty) {
|
||||
newDate = state.dateTime ?? DateTime.now();
|
||||
}
|
||||
|
||||
final DateCellData newDateData = DateCellData(
|
||||
dateTime: newDate,
|
||||
final result = await _dateCellBackendService.update(
|
||||
date: newDate,
|
||||
time: newTime,
|
||||
includeTime: includeTime ?? state.includeTime,
|
||||
);
|
||||
|
||||
cellController.saveCellData(
|
||||
newDateData,
|
||||
onFinish: (result) {
|
||||
result.fold(
|
||||
() {
|
||||
if (!isClosed && state.timeFormatError != null) {
|
||||
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
switch (err.code) {
|
||||
case ErrorCode.InvalidDateTimeFormat:
|
||||
if (isClosed) return;
|
||||
add(
|
||||
DateCellCalendarEvent.didReceiveTimeFormatError(
|
||||
timeFormatPrompt(err),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
Log.error(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
result.fold(
|
||||
(_) {
|
||||
if (!isClosed && state.timeFormatError != null) {
|
||||
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
switch (err.code) {
|
||||
case ErrorCode.InvalidDateTimeFormat:
|
||||
if (isClosed) return;
|
||||
add(
|
||||
DateCellCalendarEvent.didReceiveTimeFormatError(
|
||||
timeFormatPrompt(err),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
Log.error(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _clearDate(Emitter<DateCellCalendarState> emit) async {
|
||||
final DateCellData newDateData = DateCellData(
|
||||
dateTime: null,
|
||||
time: null,
|
||||
includeTime: state.includeTime,
|
||||
clearFlag: true,
|
||||
);
|
||||
|
||||
cellController.saveCellData(
|
||||
newDateData,
|
||||
onFinish: (result) {
|
||||
result.fold(
|
||||
() {
|
||||
if (!isClosed) {
|
||||
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
Future<void> _clearDate() async {
|
||||
final result = await _dateCellBackendService.clear();
|
||||
result.fold(
|
||||
(_) {
|
||||
if (!isClosed) {
|
||||
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? _utcToLocalAddTime(DateTime? date) {
|
||||
DateTime? _utcToLocalAndAddCurrentTime(DateTime? date) {
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
// the incoming date is Utc. this trick converts it into Local
|
||||
// and add the current time, though the time may be overwritten by
|
||||
// explicitly provided time string
|
||||
// the incoming date is Utc. This trick converts it into Local
|
||||
// and add the current time. The time may be overwritten by
|
||||
// explicitly provided time string in the backend though
|
||||
return DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
@@ -285,14 +275,14 @@ class DateCellCalendarState with _$DateCellCalendarState {
|
||||
DateTypeOptionPB dateTypeOptionPB,
|
||||
DateCellDataPB? cellData,
|
||||
) {
|
||||
final dateData = _dateDataFromCellData(cellData);
|
||||
final (dateTime, time, includeTime) = _dateDataFromCellData(cellData);
|
||||
return DateCellCalendarState(
|
||||
dateTypeOptionPB: dateTypeOptionPB,
|
||||
format: CalendarFormat.month,
|
||||
focusedDay: DateTime.now(),
|
||||
dateTime: dateData.dateTime,
|
||||
time: dateData.time,
|
||||
includeTime: dateData.includeTime,
|
||||
dateTime: dateTime,
|
||||
time: time,
|
||||
includeTime: includeTime,
|
||||
timeFormatError: null,
|
||||
timeHintText: _timeHintText(dateTypeOptionPB),
|
||||
);
|
||||
@@ -310,11 +300,11 @@ String _timeHintText(DateTypeOptionPB typeOption) {
|
||||
}
|
||||
}
|
||||
|
||||
DateCellData _dateDataFromCellData(DateCellDataPB? cellData) {
|
||||
(DateTime?, String?, 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
|
||||
if (cellData == null) {
|
||||
return const DateCellData(includeTime: false);
|
||||
return (null, null, false);
|
||||
}
|
||||
|
||||
DateTime? dateTime;
|
||||
@@ -326,5 +316,5 @@ DateCellData _dateDataFromCellData(DateCellDataPB? cellData) {
|
||||
}
|
||||
final bool includeTime = cellData.includeTime;
|
||||
|
||||
return DateCellData(dateTime: dateTime, time: time, includeTime: includeTime);
|
||||
return (dateTime, time, includeTime);
|
||||
}
|
||||
+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, 520)),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+116
-128
@@ -2,8 +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/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||
@@ -11,7 +10,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 +50,7 @@ class _DateCellEditor extends State<DateCellEditor> {
|
||||
if (snapshot.hasData) {
|
||||
return _buildWidget(snapshot);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -68,7 +66,7 @@ class _DateCellEditor extends State<DateCellEditor> {
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return const SizedBox();
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -109,10 +107,6 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
||||
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
|
||||
@@ -122,11 +116,13 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
const TypeOptionSeparator(spacing: 12.0),
|
||||
const DatePicker(),
|
||||
const VSpace(8.0),
|
||||
const TypeOptionSeparator(spacing: 8.0),
|
||||
const _IncludeTimeButton(),
|
||||
const TypeOptionSeparator(spacing: 12.0),
|
||||
const TypeOptionSeparator(spacing: 8.0),
|
||||
DateTypeOptionButton(popoverMutex: popoverMutex),
|
||||
const TypeOptionSeparator(spacing: 12.0),
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
const ClearDateButton(),
|
||||
];
|
||||
|
||||
@@ -135,7 +131,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
||||
controller: ScrollController(),
|
||||
itemCount: children.length,
|
||||
itemBuilder: (BuildContext context, int index) => children[index],
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
padding: const EdgeInsets.only(top: 18.0, bottom: 12.0),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -147,84 +143,103 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCalendar(BuildContext context) {
|
||||
class DatePicker extends StatelessWidget {
|
||||
const DatePicker({super.key});
|
||||
|
||||
@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,
|
||||
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: state.focusedDay,
|
||||
rowHeight: 26.0 + 7.0,
|
||||
calendarFormat: state.format,
|
||||
daysOfWeekHeight: 17.0 + 8.0,
|
||||
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,
|
||||
defaultTextStyle: textStyle,
|
||||
weekendTextStyle: textStyle,
|
||||
selectedTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
todayTextStyle: textStyle,
|
||||
outsideTextStyle: textStyle.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
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) => isSameDay(state.dateTime, day),
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
context.read<DateCellCalendarBloc>().add(
|
||||
DateCellCalendarEvent.selectDay(selectedDay.toLocal().date),
|
||||
);
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setCalFormat(format));
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setFocusedDay(focusedDay));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -241,30 +256,11 @@ class _IncludeTimeButton extends StatelessWidget {
|
||||
builder: (context, includeTime) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: 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: includeTime,
|
||||
onChanged: (value) => context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setIncludeTime(!value)),
|
||||
style: ToggleStyle.big,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: IncludeTimeButton(
|
||||
onChanged: (value) => context
|
||||
.read<DateCellCalendarBloc>()
|
||||
.add(DateCellCalendarEvent.setIncludeTime(!value)),
|
||||
value: includeTime,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -315,26 +311,21 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
||||
return BlocConsumer<DateCellCalendarBloc, DateCellCalendarState>(
|
||||
listener: (context, state) => _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));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.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));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -369,7 +360,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 +471,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,
|
||||
),
|
||||
),
|
||||
|
||||
+4
-2
@@ -93,7 +93,7 @@ class SelectOptionTag extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
EdgeInsets padding =
|
||||
const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0);
|
||||
const EdgeInsets.symmetric(vertical: 1.5, horizontal: 8.0);
|
||||
if (onRemove != null) {
|
||||
padding = padding.copyWith(right: 2.0);
|
||||
}
|
||||
@@ -110,6 +110,7 @@ class SelectOptionTag extends StatelessWidget {
|
||||
Flexible(
|
||||
child: FlowyText.medium(
|
||||
name,
|
||||
fontSize: FontSizes.s11,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
@@ -168,7 +169,8 @@ class SelectOptionTagCell extends StatelessWidget {
|
||||
...children,
|
||||
],
|
||||
),
|
||||
onTap: () => onSelected(option),
|
||||
// TODO(richard): find alternative solution to onTapDown
|
||||
onTapDown: (_) => onSelected(option),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+10
-8
@@ -13,9 +13,11 @@ import 'select_option_editor.dart';
|
||||
|
||||
class SelectOptionCellStyle extends GridCellStyle {
|
||||
String placeholder;
|
||||
EdgeInsets? cellPadding;
|
||||
|
||||
SelectOptionCellStyle({
|
||||
required this.placeholder,
|
||||
this.cellPadding,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -170,10 +172,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
||||
final Widget child = _buildOptions(context);
|
||||
|
||||
final constraints = BoxConstraints.loose(
|
||||
Size(
|
||||
SelectOptionCellEditor.editorPanelWidth,
|
||||
300,
|
||||
),
|
||||
Size(SelectOptionCellEditor.editorPanelWidth, 300),
|
||||
);
|
||||
return AppFlowyPopover(
|
||||
controller: widget.popoverController,
|
||||
@@ -191,7 +190,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
||||
},
|
||||
onClose: () => widget.onCellEditing.value = false,
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
padding: widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
@@ -200,9 +199,12 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
||||
Widget _buildOptions(BuildContext context) {
|
||||
final Widget child;
|
||||
if (widget.selectOptions.isEmpty && widget.cellStyle != null) {
|
||||
child = FlowyText.medium(
|
||||
widget.cellStyle!.placeholder,
|
||||
color: Theme.of(context).hintColor,
|
||||
child = Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: FlowyText.medium(
|
||||
widget.cellStyle!.placeholder,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final children = widget.selectOptions.map(
|
||||
|
||||
+7
-4
@@ -14,6 +14,7 @@ class GridTextCellStyle extends GridCellStyle {
|
||||
double emojiFontSize;
|
||||
double emojiHPadding;
|
||||
bool showEmoji;
|
||||
EdgeInsets? cellPadding;
|
||||
|
||||
GridTextCellStyle({
|
||||
this.placeholder,
|
||||
@@ -22,6 +23,7 @@ class GridTextCellStyle extends GridCellStyle {
|
||||
this.showEmoji = true,
|
||||
this.emojiFontSize = 16,
|
||||
this.emojiHPadding = 0,
|
||||
this.cellPadding,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,10 +74,11 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: GridSize.cellContentInsets.left,
|
||||
right: GridSize.cellContentInsets.right,
|
||||
),
|
||||
padding: widget.cellStyle.cellPadding ??
|
||||
EdgeInsets.only(
|
||||
left: GridSize.cellContentInsets.left,
|
||||
right: GridSize.cellContentInsets.right,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.cellStyle.showEmoji)
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
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/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.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 TimestampCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
Alignment alignment;
|
||||
EdgeInsets? cellPadding;
|
||||
|
||||
TimestampCellStyle({
|
||||
this.placeholder,
|
||||
this.alignment = Alignment.center,
|
||||
this.cellPadding,
|
||||
});
|
||||
}
|
||||
|
||||
class GridTimestampCell extends GridCellWidget {
|
||||
/// The [GridTimestampCell] is used by both [FieldType.CreatedTime]
|
||||
/// and [FieldType.LastEditedTime]. So it needs to know the field type.
|
||||
final FieldType fieldType;
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
late final TimestampCellStyle? cellStyle;
|
||||
|
||||
GridTimestampCell({
|
||||
GridCellStyle? style,
|
||||
required this.fieldType,
|
||||
required this.cellControllerBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key) {
|
||||
if (style != null) {
|
||||
cellStyle = (style as TimestampCellStyle);
|
||||
} else {
|
||||
cellStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
GridCellState<GridTimestampCell> createState() => _TimestampCellState();
|
||||
}
|
||||
|
||||
class _TimestampCellState extends GridCellState<GridTimestampCell> {
|
||||
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) {
|
||||
final alignment = widget.cellStyle?.alignment ?? Alignment.centerLeft;
|
||||
final placeholder = widget.cellStyle?.placeholder ?? "";
|
||||
final padding = widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets;
|
||||
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
|
||||
builder: (context, state) {
|
||||
final isEmpty = state.dateStr.isEmpty;
|
||||
final text = isEmpty ? placeholder : state.dateStr;
|
||||
return Align(
|
||||
alignment: alignment,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: FlowyText.medium(
|
||||
text,
|
||||
color: isEmpty
|
||||
? Theme.of(context).hintColor
|
||||
: AFThemeExtension.of(context).textColor,
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() => _cellBloc.state.dateStr;
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'timestamp_cell_bloc.freezed.dart';
|
||||
|
||||
class TimestampCellBloc extends Bloc<TimestampCellEvent, TimestampCellState> {
|
||||
final TimestampCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
TimestampCellBloc({required this.cellController})
|
||||
: super(TimestampCellState.initial(cellController)) {
|
||||
on<TimestampCellEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () => _startListening(),
|
||||
didReceiveCellUpdate: (TimestampCellDataPB? cellData) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
data: cellData,
|
||||
dateStr: cellData?.dateTime ?? "",
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellChanged: ((data) {
|
||||
if (!isClosed) {
|
||||
add(TimestampCellEvent.didReceiveCellUpdate(data));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampCellEvent with _$TimestampCellEvent {
|
||||
const factory TimestampCellEvent.initial() = _InitialCell;
|
||||
const factory TimestampCellEvent.didReceiveCellUpdate(
|
||||
TimestampCellDataPB? data,
|
||||
) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TimestampCellState with _$TimestampCellState {
|
||||
const factory TimestampCellState({
|
||||
required TimestampCellDataPB? data,
|
||||
required String dateStr,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _TimestampCellState;
|
||||
|
||||
factory TimestampCellState.initial(TimestampCellController context) {
|
||||
final cellData = context.getCellData();
|
||||
|
||||
return TimestampCellState(
|
||||
fieldInfo: context.fieldInfo,
|
||||
data: cellData,
|
||||
dateStr: cellData?.dateTime ?? "",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
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/type_option_context.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.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/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -20,36 +12,39 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
class RowActionList extends StatelessWidget {
|
||||
final RowController rowController;
|
||||
const RowActionList({
|
||||
required String viewId,
|
||||
required this.rowController,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: FlowyText(LocaleKeys.grid_row_action.tr()),
|
||||
return BlocProvider<RowActionSheetBloc>(
|
||||
create: (context) => RowActionSheetBloc(
|
||||
viewId: rowController.viewId,
|
||||
rowId: rowController.rowId,
|
||||
groupId: rowController.groupId,
|
||||
),
|
||||
child: IntrinsicWidth(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RowDetailPageDuplicateButton(
|
||||
rowId: rowController.rowId,
|
||||
groupId: rowController.groupId,
|
||||
),
|
||||
const VSpace(4.0),
|
||||
RowDetailPageDeleteButton(rowId: rowController.rowId),
|
||||
],
|
||||
),
|
||||
const VSpace(15),
|
||||
RowDetailPageDeleteButton(rowId: rowController.rowId),
|
||||
RowDetailPageDuplicateButton(
|
||||
rowId: rowController.rowId,
|
||||
groupId: rowController.groupId,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RowDetailPageDeleteButton extends StatelessWidget {
|
||||
final String rowId;
|
||||
const RowDetailPageDeleteButton({required this.rowId, Key? key})
|
||||
: super(key: key);
|
||||
const RowDetailPageDeleteButton({required this.rowId, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -59,7 +54,9 @@ class RowDetailPageDeleteButton extends StatelessWidget {
|
||||
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
||||
leftIcon: const FlowySvg(FlowySvgs.trash_m),
|
||||
onTap: () {
|
||||
context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
|
||||
context
|
||||
.read<RowActionSheetBloc>()
|
||||
.add(const RowActionSheetEvent.deleteRow());
|
||||
FlowyOverlay.pop(context);
|
||||
},
|
||||
),
|
||||
@@ -73,8 +70,8 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
|
||||
const RowDetailPageDuplicateButton({
|
||||
required this.rowId,
|
||||
this.groupId,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -85,91 +82,11 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
|
||||
leftIcon: const FlowySvg(FlowySvgs.copy_s),
|
||||
onTap: () {
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.duplicateRow(rowId, groupId));
|
||||
.read<RowActionSheetBloc>()
|
||||
.add(const RowActionSheetEvent.duplicateRow());
|
||||
FlowyOverlay.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CreateRowFieldButton extends StatefulWidget {
|
||||
final String viewId;
|
||||
|
||||
const CreateRowFieldButton({
|
||||
required this.viewId,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
|
||||
}
|
||||
|
||||
class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
|
||||
late PopoverController popoverController;
|
||||
late TypeOptionPB typeOption;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.topWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
margin: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_newProperty.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onTap: () async {
|
||||
final result = await TypeOptionBackendService.createFieldTypeOption(
|
||||
viewId: widget.viewId,
|
||||
);
|
||||
result.fold(
|
||||
(l) {
|
||||
typeOption = l;
|
||||
popoverController.show();
|
||||
},
|
||||
(r) => Log.error("Failed to create field type option: $r"),
|
||||
);
|
||||
},
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.add_m,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popOverContext) {
|
||||
return FieldEditor(
|
||||
viewId: widget.viewId,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
viewId: widget.viewId,
|
||||
field: typeOption.field_2,
|
||||
),
|
||||
onDeleted: (fieldId) {
|
||||
popoverController.close();
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.deleteField(fieldId));
|
||||
},
|
||||
).show(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,27 @@
|
||||
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/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_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';
|
||||
|
||||
typedef RowBannerCellBuilder = Widget Function(String fieldId);
|
||||
import 'cell_builder.dart';
|
||||
import 'cells/cells.dart';
|
||||
|
||||
class RowBanner extends StatefulWidget {
|
||||
final String viewId;
|
||||
final RowMetaPB rowMeta;
|
||||
final RowBannerCellBuilder cellBuilder;
|
||||
final RowController rowController;
|
||||
final GridCellBuilder cellBuilder;
|
||||
|
||||
const RowBanner({
|
||||
required this.viewId,
|
||||
required this.rowMeta,
|
||||
required this.rowController,
|
||||
required this.cellBuilder,
|
||||
super.key,
|
||||
});
|
||||
@@ -34,25 +38,39 @@ class _RowBannerState extends State<RowBanner> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<RowBannerBloc>(
|
||||
create: (context) => RowBannerBloc(
|
||||
viewId: widget.viewId,
|
||||
rowMeta: widget.rowMeta,
|
||||
viewId: widget.rowController.viewId,
|
||||
rowMeta: widget.rowController.rowMeta,
|
||||
)..add(const RowBannerEvent.initial()),
|
||||
child: MouseRegion(
|
||||
onEnter: (event) => _isHovering.value = true,
|
||||
onExit: (event) => _isHovering.value = false,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: _BannerAction(
|
||||
isHovering: _isHovering,
|
||||
popoverController: popoverController,
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(60, 34, 60, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: _BannerAction(
|
||||
isHovering: _isHovering,
|
||||
popoverController: popoverController,
|
||||
),
|
||||
),
|
||||
const HSpace(4),
|
||||
_BannerTitle(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
popoverController: popoverController,
|
||||
rowController: widget.rowController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_BannerTitle(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
popoverController: popoverController,
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
child: RowActionButton(rowController: widget.rowController),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -74,50 +92,54 @@ class _BannerAction extends StatelessWidget {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: isHovering,
|
||||
builder: (BuildContext context, bool value, Widget? child) {
|
||||
if (value) {
|
||||
return BlocBuilder<RowBannerBloc, RowBannerState>(
|
||||
builder: (context, state) {
|
||||
final children = <Widget>[];
|
||||
final rowMeta = state.rowMeta;
|
||||
if (rowMeta.icon.isEmpty) {
|
||||
children.add(
|
||||
EmojiPickerButton(
|
||||
showEmojiPicker: () => popoverController.show(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(
|
||||
RemoveEmojiButton(
|
||||
onRemoved: () {
|
||||
context
|
||||
.read<RowBannerBloc>()
|
||||
.add(const RowBannerEvent.setIcon(''));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (!value) {
|
||||
return const SizedBox(height: _kBannerActionHeight);
|
||||
}
|
||||
|
||||
return BlocBuilder<RowBannerBloc, RowBannerState>(
|
||||
builder: (context, state) {
|
||||
final children = <Widget>[];
|
||||
final rowMeta = state.rowMeta;
|
||||
if (rowMeta.icon.isEmpty) {
|
||||
children.add(
|
||||
EmojiPickerButton(
|
||||
showEmojiPicker: () => popoverController.show(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(
|
||||
RemoveEmojiButton(
|
||||
onRemoved: () {
|
||||
context
|
||||
.read<RowBannerBloc>()
|
||||
.add(const RowBannerEvent.setIcon(''));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BannerTitle extends StatefulWidget {
|
||||
final RowBannerCellBuilder cellBuilder;
|
||||
final GridCellBuilder cellBuilder;
|
||||
final PopoverController popoverController;
|
||||
final RowController rowController;
|
||||
|
||||
const _BannerTitle({
|
||||
required this.cellBuilder,
|
||||
required this.popoverController,
|
||||
});
|
||||
required this.rowController,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_BannerTitle> createState() => _BannerTitleState();
|
||||
@@ -139,10 +161,24 @@ class _BannerTitleState extends State<_BannerTitle> {
|
||||
);
|
||||
}
|
||||
|
||||
children.add(const HSpace(4));
|
||||
|
||||
if (state.primaryField != null) {
|
||||
final style = GridTextCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(),
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
showEmoji: false,
|
||||
autofocus: true,
|
||||
cellPadding: EdgeInsets.zero,
|
||||
);
|
||||
final cellContext = DatabaseCellContext(
|
||||
viewId: widget.rowController.viewId,
|
||||
rowMeta: widget.rowController.rowMeta,
|
||||
fieldInfo: FieldInfo.initial(state.primaryField!),
|
||||
);
|
||||
children.add(
|
||||
Expanded(
|
||||
child: widget.cellBuilder(state.primaryField!.id),
|
||||
child: widget.cellBuilder.build(cellContext, style: style),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -211,16 +247,14 @@ class _EmojiPickerButtonState extends State<EmojiPickerButton> {
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 26,
|
||||
width: 160,
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||
),
|
||||
leftIcon: const Icon(
|
||||
Icons.emoji_emotions,
|
||||
size: 16,
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
|
||||
onTap: widget.showEmojiPicker,
|
||||
margin: const EdgeInsets.all(4),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -239,16 +273,14 @@ class RemoveEmojiButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 26,
|
||||
width: 160,
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||
),
|
||||
leftIcon: const Icon(
|
||||
Icons.emoji_emotions,
|
||||
size: 16,
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
|
||||
onTap: onRemoved,
|
||||
margin: const EdgeInsets.all(4),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -263,3 +295,22 @@ Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class RowActionButton extends StatelessWidget {
|
||||
final RowController rowController;
|
||||
const RowActionButton({super.key, required this.rowController});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (context) => RowActionList(rowController: rowController),
|
||||
child: FlowyIconButton(
|
||||
width: 20,
|
||||
height: 20,
|
||||
icon: const FlowySvg(FlowySvgs.details_horizontal_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,12 @@
|
||||
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_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_document.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';
|
||||
|
||||
import 'cell_builder.dart';
|
||||
import 'cells/text_cell/text_cell.dart';
|
||||
import 'row_action.dart';
|
||||
import 'row_banner.dart';
|
||||
import 'row_property.dart';
|
||||
|
||||
@@ -47,17 +41,29 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyDialog(
|
||||
child: BlocProvider(
|
||||
create: (context) {
|
||||
return RowDetailBloc(dataController: widget.rowController)
|
||||
..add(const RowDetailEvent.initial());
|
||||
},
|
||||
create: (context) => RowDetailBloc(rowController: widget.rowController)
|
||||
..add(const RowDetailEvent.initial()),
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
children: [
|
||||
_rowBanner(),
|
||||
IntrinsicHeight(child: _responsiveRowInfo()),
|
||||
const Divider(height: 1.0),
|
||||
const VSpace(10),
|
||||
RowBanner(
|
||||
rowController: widget.rowController,
|
||||
cellBuilder: widget.cellBuilder,
|
||||
),
|
||||
const VSpace(16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 40, right: 60),
|
||||
child: RowPropertyList(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
viewId: widget.rowController.viewId,
|
||||
),
|
||||
),
|
||||
const VSpace(20),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 60),
|
||||
child: Divider(height: 1.0),
|
||||
),
|
||||
const VSpace(20),
|
||||
RowDocument(
|
||||
viewId: widget.rowController.viewId,
|
||||
rowId: widget.rowController.rowId,
|
||||
@@ -68,104 +74,4 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _rowBanner() {
|
||||
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
||||
builder: (context, state) {
|
||||
final paddingOffset = getHorizontalPadding(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: paddingOffset,
|
||||
right: paddingOffset,
|
||||
top: 20,
|
||||
),
|
||||
child: RowBanner(
|
||||
rowMeta: widget.rowController.rowMeta,
|
||||
viewId: widget.rowController.viewId,
|
||||
cellBuilder: (fieldId) {
|
||||
final fieldInfo = state.cells
|
||||
.firstWhereOrNull(
|
||||
(e) => e.fieldInfo.field.id == fieldId,
|
||||
)
|
||||
?.fieldInfo;
|
||||
|
||||
if (fieldInfo != null) {
|
||||
final style = GridTextCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(),
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
showEmoji: false,
|
||||
autofocus: true,
|
||||
);
|
||||
final cellContext = DatabaseCellContext(
|
||||
viewId: widget.rowController.viewId,
|
||||
rowMeta: widget.rowController.rowMeta,
|
||||
fieldInfo: fieldInfo,
|
||||
);
|
||||
return widget.cellBuilder.build(cellContext, style: style);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _responsiveRowInfo() {
|
||||
final rowDataColumn = RowPropertyList(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
viewId: widget.rowController.viewId,
|
||||
);
|
||||
final rowOptionColumn = RowActionList(
|
||||
viewId: widget.rowController.viewId,
|
||||
rowController: widget.rowController,
|
||||
);
|
||||
final paddingOffset = getHorizontalPadding(context);
|
||||
if (MediaQuery.of(context).size.width > 800) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
|
||||
child: rowDataColumn,
|
||||
),
|
||||
),
|
||||
const VerticalDivider(width: 1.0),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(20, 0, paddingOffset, 0),
|
||||
child: rowOptionColumn,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
|
||||
child: rowDataColumn,
|
||||
),
|
||||
const Divider(height: 1.0),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: paddingOffset),
|
||||
child: rowOptionColumn,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double getHorizontalPadding(BuildContext context) {
|
||||
if (MediaQuery.of(context).size.width > 800) {
|
||||
return 50;
|
||||
} else {
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
+208
-42
@@ -1,28 +1,34 @@
|
||||
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/field/type_option/type_option_context.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.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 'accessory/cell_accessory.dart';
|
||||
import 'cell_builder.dart';
|
||||
import 'cells/checkbox_cell/checkbox_cell.dart';
|
||||
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';
|
||||
|
||||
/// Display the row properties in a list. Only use this widget in the
|
||||
/// [RowDetailPage].
|
||||
///
|
||||
class RowPropertyList extends StatelessWidget {
|
||||
final String viewId;
|
||||
final GridCellBuilder cellBuilder;
|
||||
@@ -37,39 +43,88 @@ class RowPropertyList extends StatelessWidget {
|
||||
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
||||
buildWhen: (previous, current) => previous.cells != current.cells,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// The rest of the fields are displayed in the order of the field
|
||||
// list
|
||||
...state.cells
|
||||
.where((element) => !element.fieldInfo.field.isPrimary)
|
||||
.map(
|
||||
(cell) => _PropertyCell(
|
||||
cellContext: cell,
|
||||
cellBuilder: cellBuilder,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
const VSpace(20),
|
||||
|
||||
// Create a new property(field) button
|
||||
CreateRowFieldButton(viewId: viewId),
|
||||
],
|
||||
final children = state.cells
|
||||
.where((element) => !element.fieldInfo.field.isPrimary)
|
||||
.mapIndexed(
|
||||
(index, cell) => _PropertyCell(
|
||||
key: ValueKey('row_detail_${cell.fieldId}'),
|
||||
cellContext: cell,
|
||||
cellBuilder: cellBuilder,
|
||||
index: index,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return ReorderableListView(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
final reorderedField = children[oldIndex].cellContext.fieldId;
|
||||
_reorderField(
|
||||
context,
|
||||
state.cells,
|
||||
reorderedField,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
);
|
||||
},
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
child,
|
||||
const MouseRegion(cursor: SystemMouseCursors.grabbing),
|
||||
],
|
||||
),
|
||||
),
|
||||
footer: Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: CreateRowFieldButton(viewId: viewId),
|
||||
),
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _reorderField(
|
||||
BuildContext context,
|
||||
List<DatabaseCellContext> cells,
|
||||
String reorderedFieldId,
|
||||
int oldIndex,
|
||||
int newIndex,
|
||||
) {
|
||||
// when reorderiing downwards, need to update index
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
|
||||
// also update index when the index is after the index of the primary field
|
||||
// in the original list of DatabaseCellContext's
|
||||
final primaryFieldIndex =
|
||||
cells.indexWhere((element) => element.fieldInfo.isPrimary);
|
||||
if (oldIndex >= primaryFieldIndex) {
|
||||
oldIndex++;
|
||||
}
|
||||
if (newIndex >= primaryFieldIndex) {
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
context.read<RowDetailBloc>().add(
|
||||
RowDetailEvent.reorderField(reorderedFieldId, oldIndex, newIndex),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PropertyCell extends StatefulWidget {
|
||||
final DatabaseCellContext cellContext;
|
||||
final GridCellBuilder cellBuilder;
|
||||
final int index;
|
||||
const _PropertyCell({
|
||||
required this.cellContext,
|
||||
required this.cellBuilder,
|
||||
Key? key,
|
||||
required this.index,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -77,45 +132,65 @@ class _PropertyCell extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PropertyCellState extends State<_PropertyCell> {
|
||||
final PopoverController popover = PopoverController();
|
||||
final PopoverController _popoverController = PopoverController();
|
||||
bool _isFieldHover = false;
|
||||
|
||||
@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(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
|
||||
child: cell,
|
||||
final dragThumb = MouseRegion(
|
||||
cursor: SystemMouseCursors.grab,
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 30,
|
||||
child: _isFieldHover ? const FlowySvg(FlowySvgs.drag_element_s) : null,
|
||||
),
|
||||
);
|
||||
|
||||
return IntrinsicHeight(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 30),
|
||||
final gesture = GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => cell.requestFocus.notify(),
|
||||
child: AccessoryHover(child: cell),
|
||||
);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
constraints: const BoxConstraints(minHeight: 30),
|
||||
child: MouseRegion(
|
||||
onEnter: (event) => setState(() => _isFieldHover = true),
|
||||
onExit: (event) => setState(() => _isFieldHover = false),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ReorderableDragStartListener(
|
||||
index: widget.index,
|
||||
enabled: _isFieldHover,
|
||||
child: dragThumb,
|
||||
),
|
||||
const HSpace(4),
|
||||
AppFlowyPopover(
|
||||
controller: popover,
|
||||
controller: _popoverController,
|
||||
constraints: BoxConstraints.loose(const Size(240, 600)),
|
||||
margin: EdgeInsets.zero,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (popoverContext) => buildFieldEditor(),
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
height: 40,
|
||||
width: 160,
|
||||
height: 30,
|
||||
child: FieldCellButton(
|
||||
field: widget.cellContext.fieldInfo.field,
|
||||
onTap: () => popover.show(),
|
||||
onTap: () => _popoverController.show(),
|
||||
radius: BorderRadius.circular(6),
|
||||
margin:
|
||||
const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(child: gesture),
|
||||
],
|
||||
),
|
||||
@@ -132,11 +207,11 @@ class _PropertyCellState extends State<_PropertyCell> {
|
||||
field: widget.cellContext.fieldInfo.field,
|
||||
),
|
||||
onHidden: (fieldId) {
|
||||
popover.close();
|
||||
_popoverController.close();
|
||||
context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
|
||||
},
|
||||
onDeleted: (fieldId) {
|
||||
popover.close();
|
||||
_popoverController.close();
|
||||
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
@@ -154,23 +229,35 @@ class _PropertyCellState extends State<_PropertyCell> {
|
||||
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return null;
|
||||
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 DateCellStyle(
|
||||
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 null;
|
||||
return GridNumberCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
);
|
||||
case FieldType.RichText:
|
||||
return GridTextCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
@@ -178,6 +265,7 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
case FieldType.SingleSelect:
|
||||
return SelectOptionCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
);
|
||||
|
||||
case FieldType.URL:
|
||||
@@ -191,3 +279,81 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
|
||||
class CreateRowFieldButton extends StatefulWidget {
|
||||
final String viewId;
|
||||
|
||||
const CreateRowFieldButton({required this.viewId, super.key});
|
||||
|
||||
@override
|
||||
State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
|
||||
}
|
||||
|
||||
class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
|
||||
late PopoverController popoverController;
|
||||
late TypeOptionPB typeOption;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.topWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
margin: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 30,
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_newProperty.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onTap: () async {
|
||||
final result = await TypeOptionBackendService.createFieldTypeOption(
|
||||
viewId: widget.viewId,
|
||||
);
|
||||
result.fold(
|
||||
(l) {
|
||||
typeOption = l;
|
||||
popoverController.show();
|
||||
},
|
||||
(r) => Log.error("Failed to create field type option: $r"),
|
||||
);
|
||||
},
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.add_m,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popOverContext) {
|
||||
return FieldEditor(
|
||||
viewId: widget.viewId,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
viewId: widget.viewId,
|
||||
field: typeOption.field_2,
|
||||
),
|
||||
onDeleted: (fieldId) {
|
||||
popoverController.close();
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<RowDetailBloc>()
|
||||
.add(RowDetailEvent.deleteField(fieldId));
|
||||
},
|
||||
).show(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/share_bloc.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DatabaseShareButton extends StatelessWidget {
|
||||
const DatabaseShareButton({
|
||||
super.key,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => DatabaseShareBloc(view: view),
|
||||
child: BlocListener<DatabaseShareBloc, DatabaseShareState>(
|
||||
listener: (context, state) {
|
||||
state.mapOrNull(
|
||||
finish: (state) {
|
||||
state.successOrFail.fold(
|
||||
(data) => _handleExportData(context),
|
||||
_handleExportError,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<DatabaseShareBloc, DatabaseShareState>(
|
||||
builder: (context, state) => ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(
|
||||
height: 30,
|
||||
width: 100,
|
||||
),
|
||||
child: DatabaseShareActionList(view: view),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleExportData(BuildContext context) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleExportError(FlowyError error) {
|
||||
showMessageToast(error.msg);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseShareActionList extends StatefulWidget {
|
||||
const DatabaseShareActionList({
|
||||
super.key,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
State<DatabaseShareActionList> createState() =>
|
||||
DatabaseShareActionListState();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class DatabaseShareActionListState extends State<DatabaseShareActionList> {
|
||||
late String name;
|
||||
late final ViewListener viewListener = ViewListener(viewId: widget.view.id);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
listenOnViewUpdated();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
viewListener.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final databaseShareBloc = context.read<DatabaseShareBloc>();
|
||||
return PopoverActionList<ShareActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 8),
|
||||
actions: ShareAction.values
|
||||
.map((action) => ShareActionWrapper(action))
|
||||
.toList(),
|
||||
buildChild: (controller) {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
onPressed: () => controller.show(),
|
||||
);
|
||||
},
|
||||
onSelected: (action, controller) async {
|
||||
switch (action.inner) {
|
||||
case ShareAction.csv:
|
||||
final exportPath = await getIt<FilePickerService>().saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '${name.toFileName()}.csv',
|
||||
);
|
||||
if (exportPath != null) {
|
||||
databaseShareBloc.add(DatabaseShareEvent.shareCSV(exportPath));
|
||||
}
|
||||
break;
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void listenOnViewUpdated() {
|
||||
name = widget.view.name;
|
||||
viewListener.start(
|
||||
onViewUpdated: (view) {
|
||||
name = view.name;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ShareAction {
|
||||
csv,
|
||||
}
|
||||
|
||||
class ShareActionWrapper extends ActionCell {
|
||||
final ShareAction inner;
|
||||
|
||||
ShareActionWrapper(this.inner);
|
||||
|
||||
Widget? icon(Color iconColor) => null;
|
||||
|
||||
@override
|
||||
String get name {
|
||||
switch (inner) {
|
||||
case ShareAction.csv:
|
||||
return LocaleKeys.shareAction_csv.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import 'package:appflowy/plugins/document/application/document_data_pb_extension
|
||||
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/util/json_print.dart';
|
||||
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||
@@ -122,10 +121,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
/// subscribe to the document content change
|
||||
void _onDocumentChanged() {
|
||||
_documentListener.start(
|
||||
didReceiveUpdate: (docEvent) {
|
||||
// todo: integrate the document change to the editor
|
||||
// prettyPrintJson(docEvent.toProto3Json());
|
||||
},
|
||||
didReceiveUpdate: syncDocumentDataPB,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,10 +139,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
}
|
||||
|
||||
Future<void> _initAppFlowyEditorState(DocumentDataPB data) async {
|
||||
if (kDebugMode) {
|
||||
prettyPrintJson(data.toProto3Json());
|
||||
}
|
||||
|
||||
final document = data.toDocument();
|
||||
if (document == null) {
|
||||
assert(false, 'document is null');
|
||||
@@ -193,6 +185,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
if (lastNode == null || lastNode.delta == null) {
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNode([document.root.children.length], paragraphNode());
|
||||
transaction.afterSelection = transaction.beforeSelection;
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
@@ -212,6 +205,24 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
void syncDocumentDataPB(DocEventPB docEvent) {
|
||||
// prettyPrintJson(docEvent.toProto3Json());
|
||||
// todo: integrate the document change to the editor
|
||||
// for (final event in docEvent.events) {
|
||||
// for (final blockEvent in event.event) {
|
||||
// switch (blockEvent.command) {
|
||||
// case DeltaTypePB.Inserted:
|
||||
// break;
|
||||
// case DeltaTypePB.Updated:
|
||||
// break;
|
||||
// case DeltaTypePB.Removed:
|
||||
// break;
|
||||
// default:
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
class DocumentService {
|
||||
// unused now.
|
||||
@@ -46,4 +45,42 @@ class DocumentService {
|
||||
final result = await DocumentEventApplyAction(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
|
||||
/// Creates a new external text.
|
||||
///
|
||||
/// Normally, it's used to the block that needs sync long text.
|
||||
///
|
||||
/// the delta parameter is the json representation of the delta.
|
||||
Future<Either<FlowyError, Unit>> createExternalText({
|
||||
required String documentId,
|
||||
required String textId,
|
||||
String? delta,
|
||||
}) async {
|
||||
final payload = TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
textId: textId,
|
||||
delta: delta,
|
||||
);
|
||||
final result = await DocumentEventCreateText(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
|
||||
/// Updates the external text.
|
||||
///
|
||||
/// this function is compatible with the [createExternalText] function.
|
||||
///
|
||||
/// the delta parameter is the json representation of the delta too.
|
||||
Future<Either<FlowyError, Unit>> updateExternalText({
|
||||
required String documentId,
|
||||
required String textId,
|
||||
String? delta,
|
||||
}) async {
|
||||
final payload = TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
textId: textId,
|
||||
delta: delta,
|
||||
);
|
||||
final result = await DocumentEventApplyTextDeltaEvent(payload).send();
|
||||
return result.swap();
|
||||
}
|
||||
}
|
||||
+49
-5
@@ -3,10 +3,27 @@ import 'dart:convert';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show Document, Node, Attributes, Delta, ParagraphBlockKeys, NodeIterator;
|
||||
show
|
||||
Document,
|
||||
Node,
|
||||
Attributes,
|
||||
Delta,
|
||||
ParagraphBlockKeys,
|
||||
NodeIterator,
|
||||
NodeExternalValues;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
class ExternalValues extends NodeExternalValues {
|
||||
const ExternalValues({
|
||||
required this.externalId,
|
||||
required this.externalType,
|
||||
});
|
||||
|
||||
final String externalId;
|
||||
final String externalType;
|
||||
}
|
||||
|
||||
extension DocumentDataPBFromTo on DocumentDataPB {
|
||||
static DocumentDataPB? fromDocument(Document document) {
|
||||
final startNode = document.first;
|
||||
@@ -84,24 +101,51 @@ extension DocumentDataPBFromTo on DocumentDataPB {
|
||||
children.addAll(childrenIds.map((e) => buildNode(e)).whereNotNull());
|
||||
}
|
||||
|
||||
return block?.toNode(children: children);
|
||||
return block?.toNode(
|
||||
children: children,
|
||||
meta: meta,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension BlockToNode on BlockPB {
|
||||
Node toNode({
|
||||
Iterable<Node>? children,
|
||||
required MetaPB meta,
|
||||
}) {
|
||||
return Node(
|
||||
final node = Node(
|
||||
id: id,
|
||||
type: ty,
|
||||
attributes: _dataAdapter(ty, data),
|
||||
attributes: _dataAdapter(ty, data, meta),
|
||||
children: children ?? [],
|
||||
);
|
||||
node.externalValues = ExternalValues(
|
||||
externalId: externalId,
|
||||
externalType: externalType,
|
||||
);
|
||||
return node;
|
||||
}
|
||||
|
||||
Attributes _dataAdapter(String ty, String data) {
|
||||
Attributes _dataAdapter(String ty, String data, MetaPB meta) {
|
||||
final map = Attributes.from(jsonDecode(data));
|
||||
|
||||
// it used in the delta case now.
|
||||
final externalType = this.externalType;
|
||||
final externalId = this.externalId;
|
||||
if (externalType.isNotEmpty && externalId.isNotEmpty) {
|
||||
// the 'text' type is the only type that is supported now.
|
||||
if (externalType == 'text') {
|
||||
final deltaString = meta.textMap[externalId];
|
||||
if (deltaString != null) {
|
||||
final delta = jsonDecode(deltaString);
|
||||
map.putIfAbsent(
|
||||
'delta',
|
||||
() => delta,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final adapter = {
|
||||
ParagraphBlockKeys.type: (Attributes map) => map
|
||||
..putIfAbsent(
|
||||
|
||||
+203
-37
@@ -1,5 +1,8 @@
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
@@ -12,10 +15,10 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
DeleteOperation,
|
||||
PathExtensions,
|
||||
Node,
|
||||
Path,
|
||||
Delta,
|
||||
composeAttributes;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
/// Uses to adjust the data structure between the editor and the backend.
|
||||
@@ -31,28 +34,72 @@ class TransactionAdapter {
|
||||
final DocumentService documentService;
|
||||
final String documentId;
|
||||
|
||||
final bool _enableDebug = true;
|
||||
|
||||
Future<void> apply(Transaction transaction, EditorState editorState) async {
|
||||
Log.debug('transaction => ${transaction.toJson()}');
|
||||
final stopwatch = Stopwatch()..start();
|
||||
if (_enableDebug) {
|
||||
Log.debug('transaction => ${transaction.toJson()}');
|
||||
}
|
||||
final actions = transaction.operations
|
||||
.map((op) => op.toBlockAction(editorState))
|
||||
.map((op) => op.toBlockAction(editorState, documentId))
|
||||
.whereNotNull()
|
||||
.expand((element) => element)
|
||||
.toList(growable: false); // avoid lazy evaluation
|
||||
Log.debug('actions => $actions');
|
||||
final textActions = actions.where(
|
||||
(e) =>
|
||||
e.textDeltaType != TextDeltaType.none && e.textDeltaPayloadPB != null,
|
||||
);
|
||||
final actionCostTime = stopwatch.elapsedMilliseconds;
|
||||
for (final textAction in textActions) {
|
||||
final payload = textAction.textDeltaPayloadPB!;
|
||||
final type = textAction.textDeltaType;
|
||||
if (type == TextDeltaType.create) {
|
||||
await documentService.createExternalText(
|
||||
documentId: payload.documentId,
|
||||
textId: payload.textId,
|
||||
delta: payload.delta,
|
||||
);
|
||||
if (_enableDebug) {
|
||||
Log.debug('create external text: ${payload.delta}');
|
||||
}
|
||||
} else if (type == TextDeltaType.update) {
|
||||
await documentService.updateExternalText(
|
||||
documentId: payload.documentId,
|
||||
textId: payload.textId,
|
||||
delta: payload.delta,
|
||||
);
|
||||
if (_enableDebug) {
|
||||
Log.debug('update external text: ${payload.delta}');
|
||||
}
|
||||
}
|
||||
}
|
||||
final blockActions =
|
||||
actions.map((e) => e.blockActionPB).toList(growable: false);
|
||||
await documentService.applyAction(
|
||||
documentId: documentId,
|
||||
actions: actions,
|
||||
actions: blockActions,
|
||||
);
|
||||
final elapsed = stopwatch.elapsedMilliseconds;
|
||||
stopwatch.stop();
|
||||
if (_enableDebug) {
|
||||
Log.debug(
|
||||
'apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on Operation {
|
||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
||||
extension BlockAction on Operation {
|
||||
List<BlockActionWrapper> toBlockAction(
|
||||
EditorState editorState,
|
||||
String documentId,
|
||||
) {
|
||||
final op = this;
|
||||
if (op is InsertOperation) {
|
||||
return op.toBlockAction(editorState);
|
||||
return op.toBlockAction(editorState, documentId);
|
||||
} else if (op is UpdateOperation) {
|
||||
return op.toBlockAction(editorState);
|
||||
return op.toBlockAction(editorState, documentId);
|
||||
} else if (op is DeleteOperation) {
|
||||
return op.toBlockAction(editorState);
|
||||
}
|
||||
@@ -61,48 +108,96 @@ extension on Operation {
|
||||
}
|
||||
|
||||
extension on InsertOperation {
|
||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
||||
final List<BlockActionPB> actions = [];
|
||||
// store the previous node for continuous insertion.
|
||||
// because the backend needs to know the previous node's id.
|
||||
Node? previousNode;
|
||||
List<BlockActionWrapper> toBlockAction(
|
||||
EditorState editorState,
|
||||
String documentId, {
|
||||
Node? previousNode,
|
||||
}) {
|
||||
Path currentPath = path;
|
||||
final List<BlockActionWrapper> actions = [];
|
||||
for (final node in nodes) {
|
||||
final parentId =
|
||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||
final parentId = node.parent?.id ??
|
||||
editorState.getNodeAtPath(currentPath.parent)?.id ??
|
||||
'';
|
||||
var prevId = previousNode?.id ??
|
||||
editorState.getNodeAtPath(path.previous)?.id ??
|
||||
editorState.getNodeAtPath(currentPath.previous)?.id ??
|
||||
'';
|
||||
assert(parentId.isNotEmpty);
|
||||
if (path.equals(path.previous) && !path.equals([0])) {
|
||||
if (currentPath.equals(currentPath.previous) &&
|
||||
!currentPath.equals([0])) {
|
||||
prevId = '';
|
||||
} else {
|
||||
assert(prevId.isNotEmpty && prevId != node.id);
|
||||
}
|
||||
|
||||
// create the external text if the node contains the delta in its data.
|
||||
final delta = node.delta;
|
||||
TextDeltaPayloadPB? textDeltaPayloadPB;
|
||||
if (delta != null) {
|
||||
final textId = nanoid(6);
|
||||
|
||||
textDeltaPayloadPB = TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
textId: textId,
|
||||
delta: jsonEncode(node.delta!.toJson()),
|
||||
);
|
||||
|
||||
// sync the text id to the node
|
||||
node.externalValues = ExternalValues(
|
||||
externalId: textId,
|
||||
externalType: 'text',
|
||||
);
|
||||
}
|
||||
|
||||
// remove the delta from the data when the incremental update is stable.
|
||||
final payload = BlockActionPayloadPB()
|
||||
..block = node.toBlock(childrenId: nanoid(10))
|
||||
..block = node.toBlock(childrenId: nanoid(6))
|
||||
..parentId = parentId
|
||||
..prevId = prevId;
|
||||
|
||||
// pass the external text id to the payload.
|
||||
if (textDeltaPayloadPB != null) {
|
||||
payload.textId = textDeltaPayloadPB.textId;
|
||||
}
|
||||
|
||||
assert(payload.block.childrenId.isNotEmpty);
|
||||
final blockActionPB = BlockActionPB()
|
||||
..action = BlockActionTypePB.Insert
|
||||
..payload = payload;
|
||||
|
||||
actions.add(
|
||||
BlockActionPB()
|
||||
..action = BlockActionTypePB.Insert
|
||||
..payload = payload,
|
||||
BlockActionWrapper(
|
||||
blockActionPB: blockActionPB,
|
||||
textDeltaPayloadPB: textDeltaPayloadPB,
|
||||
textDeltaType: TextDeltaType.create,
|
||||
),
|
||||
);
|
||||
if (node.children.isNotEmpty) {
|
||||
final childrenActions = node.children
|
||||
.map((e) => InsertOperation(e.path, [e]).toBlockAction(editorState))
|
||||
.expand((element) => element);
|
||||
actions.addAll(childrenActions);
|
||||
Node? prevChild;
|
||||
for (final child in node.children) {
|
||||
actions.addAll(
|
||||
InsertOperation(currentPath + child.path, [child]).toBlockAction(
|
||||
editorState,
|
||||
documentId,
|
||||
previousNode: prevChild,
|
||||
),
|
||||
);
|
||||
prevChild = child;
|
||||
}
|
||||
}
|
||||
previousNode = node;
|
||||
currentPath = currentPath.next;
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
extension on UpdateOperation {
|
||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
||||
final List<BlockActionPB> actions = [];
|
||||
List<BlockActionWrapper> toBlockAction(
|
||||
EditorState editorState,
|
||||
String documentId,
|
||||
) {
|
||||
final List<BlockActionWrapper> actions = [];
|
||||
|
||||
// if the attributes are both empty, we don't need to update
|
||||
if (const DeepCollectionEquality().equals(attributes, oldAttributes)) {
|
||||
@@ -116,23 +211,74 @@ extension on UpdateOperation {
|
||||
final parentId =
|
||||
node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';
|
||||
assert(parentId.isNotEmpty);
|
||||
|
||||
// create the external text if the node contains the delta in its data.
|
||||
final prevDelta = oldAttributes['delta'];
|
||||
final delta = attributes['delta'];
|
||||
final diff = prevDelta != null && delta != null
|
||||
? Delta.fromJson(prevDelta).diff(
|
||||
Delta.fromJson(delta),
|
||||
)
|
||||
: null;
|
||||
|
||||
final payload = BlockActionPayloadPB()
|
||||
..block = node.toBlock(
|
||||
parentId: parentId,
|
||||
attributes: composeAttributes(oldAttributes, attributes),
|
||||
)
|
||||
..parentId = parentId;
|
||||
actions.add(
|
||||
BlockActionPB()
|
||||
..action = BlockActionTypePB.Update
|
||||
..payload = payload,
|
||||
);
|
||||
final blockActionPB = BlockActionPB()
|
||||
..action = BlockActionTypePB.Update
|
||||
..payload = payload;
|
||||
|
||||
final textId = (node.externalValues as ExternalValues?)?.externalId;
|
||||
if (textId == null || textId.isEmpty) {
|
||||
// to be compatible with the old version, we create a new text id if the text id is empty.
|
||||
final textId = nanoid(6);
|
||||
final textDeltaPayloadPB = delta == null
|
||||
? null
|
||||
: TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
textId: textId,
|
||||
delta: jsonEncode(delta),
|
||||
);
|
||||
|
||||
node.externalValues = ExternalValues(
|
||||
externalId: textId,
|
||||
externalType: 'text',
|
||||
);
|
||||
|
||||
actions.add(
|
||||
BlockActionWrapper(
|
||||
blockActionPB: blockActionPB,
|
||||
textDeltaPayloadPB: textDeltaPayloadPB,
|
||||
textDeltaType: TextDeltaType.create,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final textDeltaPayloadPB = delta == null
|
||||
? null
|
||||
: TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
textId: textId,
|
||||
delta: jsonEncode(diff),
|
||||
);
|
||||
|
||||
actions.add(
|
||||
BlockActionWrapper(
|
||||
blockActionPB: blockActionPB,
|
||||
textDeltaPayloadPB: textDeltaPayloadPB,
|
||||
textDeltaType: TextDeltaType.update,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
|
||||
extension on DeleteOperation {
|
||||
List<BlockActionPB> toBlockAction(EditorState editorState) {
|
||||
List<BlockActionWrapper> toBlockAction(EditorState editorState) {
|
||||
final List<BlockActionPB> actions = [];
|
||||
for (final node in nodes) {
|
||||
final parentId =
|
||||
@@ -149,6 +295,26 @@ extension on DeleteOperation {
|
||||
..payload = payload,
|
||||
);
|
||||
}
|
||||
return actions;
|
||||
return actions
|
||||
.map((e) => BlockActionWrapper(blockActionPB: e))
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
enum TextDeltaType {
|
||||
none,
|
||||
create,
|
||||
update,
|
||||
}
|
||||
|
||||
class BlockActionWrapper {
|
||||
BlockActionWrapper({
|
||||
required this.blockActionPB,
|
||||
this.textDeltaType = TextDeltaType.none,
|
||||
this.textDeltaPayloadPB,
|
||||
});
|
||||
|
||||
final BlockActionPB blockActionPB;
|
||||
final TextDeltaPayloadPB? textDeltaPayloadPB;
|
||||
final TextDeltaType textDeltaType;
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'share_bloc.freezed.dart';
|
||||
|
||||
class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
||||
|
||||
@@ -102,7 +102,8 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
editorState: editorState!,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 50),
|
||||
// the 44 is the width of the left action list
|
||||
padding: const EdgeInsets.only(left: 40, right: 40 + 44),
|
||||
),
|
||||
header: _buildCoverAndIcon(context),
|
||||
);
|
||||
|
||||
Loaded 100 of 590 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user