refactor: database cell controller and builder (#4398)

* refactor: get row/field data from row cache and field controller in cell controller

* refactor: reorganize cell controller tasks and builder

* refactor: rename cell_builder.dart

* refactor: database editable cell builder

* refactor: database card cell builder

* fix: make it work

* fix: start cell listener and adjust cell style on desktop

* fix: build card cell

* fix: remove unnecessary await in tests

* fix: cell cache validation

* fix: row detail banner bugs

* fix: row detail field doesn't update

* fix: calendar event card

* test: fix integration tests

* fix: adjust cell builders to fix cell controller getting disposed

* chore: code review

* fix: bugs on mobile

* test: add grid header integration tests

* test: suppress warnings, reduce flaky test and group tests
This commit is contained in:
Richard Shiue 2024-01-24 23:59:45 +08:00 committed by GitHub
parent 18a355601a
commit a1abcd7626
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
185 changed files with 6524 additions and 7673 deletions

View File

@ -30,6 +30,7 @@ linter:
rules:
- require_trailing_commas
- prefer_collection_literals
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals

View File

@ -3,7 +3,6 @@ import 'package:appflowy/plugins/database/board/presentation/widgets/board_colum
import 'package:appflowy/plugins/database/widgets/card/container/card_container.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -25,11 +24,11 @@ void main() {
final findFirstCard = find.descendant(
of: find.byType(AppFlowyGroupCard),
matching: find.byType(FlowyText),
matching: find.byType(Text),
);
FlowyText firstCardText = tester.firstWidget(findFirstCard);
expect(firstCardText.text, defaultFirstCardName);
Text firstCardText = tester.firstWidget(findFirstCard);
expect(firstCardText.data, defaultFirstCardName);
await tester.tap(
find
@ -51,13 +50,13 @@ void main() {
),
newCardName,
);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.tap(find.byType(AppFlowyBoard));
await tester.pumpAndSettle();
firstCardText = tester.firstWidget(findFirstCard);
expect(firstCardText.text, newCardName);
expect(firstCardText.data, newCardName);
});
testWidgets('from footer', (tester) async {
@ -68,12 +67,11 @@ void main() {
final findLastCard = find.descendant(
of: find.byType(AppFlowyGroupCard),
matching: find.byType(FlowyText),
matching: find.byType(Text),
);
FlowyText? lastCardText =
tester.widgetList(findLastCard).last as FlowyText;
expect(lastCardText.text, defaultLastCardName);
Text? lastCardText = tester.widgetList(findLastCard).last as Text;
expect(lastCardText.data, defaultLastCardName);
await tester.tap(
find
@ -93,13 +91,13 @@ void main() {
),
newCardName,
);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(milliseconds: 500));
await tester.tap(find.byType(AppFlowyBoard));
await tester.pumpAndSettle();
lastCardText = tester.widgetList(findLastCard).last as FlowyText;
expect(lastCardText.text, newCardName);
lastCardText = tester.widgetList(findLastCard).last as Text;
expect(lastCardText.data, newCardName);
});
});
}

View File

@ -1,7 +1,7 @@
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/row_property.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:appflowy_board/appflowy_board.dart';
@ -19,10 +19,10 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
final card1 = find.ancestor(
of: find.findTextInFlowyText(card1Name),
of: find.text(card1Name),
matching: find.byType(AppFlowyGroupCard),
);
final doingGroup = find.findTextInFlowyText('Doing');
final doingGroup = find.text('Doing');
final doingGroupCenter = tester.getCenter(doingGroup);
final card1Center = tester.getCenter(card1);
@ -39,11 +39,11 @@ void main() {
of: find.byType(RowPropertyList),
matching: find.descendant(
of: find.byType(SelectOptionTag),
matching: find.byType(FlowyText),
matching: find.byType(Text),
),
);
expect(card1StatusFinder, findsNWidgets(1));
final card1StatusText = tester.widget<FlowyText>(card1StatusFinder).text;
final card1StatusText = tester.widget<Text>(card1StatusFinder).data;
expect(card1StatusText, 'Doing');
});
});

View File

@ -20,16 +20,16 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
const name = 'Card 1';
final card1 = find.findTextInFlowyText(name);
final card1 = find.text(name);
await tester.hoverOnWidget(
card1,
onHover: () async {
final moreOption = find.byType(CardMoreOption);
final moreOption = find.byType(MoreCardOptionsAccessory);
await tester.tapButton(moreOption);
},
);
await tester.tapButtonWithName(LocaleKeys.button_delete.tr());
expect(find.findTextInFlowyText(name), findsNothing);
expect(find.text(name), findsNothing);
});
testWidgets('duplicate item in ToDo card', (tester) async {
@ -38,11 +38,11 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
const name = 'Card 1';
final card1 = find.findTextInFlowyText(name);
final card1 = find.text(name);
await tester.hoverOnWidget(
card1,
onHover: () async {
final moreOption = find.byType(CardMoreOption);
final moreOption = find.byType(MoreCardOptionsAccessory);
await tester.tapButton(moreOption);
},
);

View File

@ -216,13 +216,13 @@ void main() {
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('MMM dd, y').format(today),
);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
await tester.findDateEditor(findsOneWidget);
// Toggle include time
final now = DateTime.now();
@ -230,8 +230,9 @@ void main() {
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('MMM dd, y HH:mm').format(now),
);
@ -239,12 +240,14 @@ void main() {
await tester.findDateEditor(findsOneWidget);
// Change date format
await tester.tapChangeDateTimeFormatButton();
await tester.changeDateFormat();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('dd/MM/y HH:mm').format(now),
);
@ -252,12 +255,14 @@ void main() {
await tester.findDateEditor(findsOneWidget);
// Change time format
await tester.tapChangeDateTimeFormatButton();
await tester.changeTimeFormat();
await tester.dismissCellEditor();
await tester.assertDateCellInGrid(
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('dd/MM/y hh:mm a').format(now),
);
@ -267,8 +272,9 @@ void main() {
// Clear the date and time
await tester.clearDate();
await tester.assertDateCellInGrid(
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.DateTime,
content: '',
);

View File

@ -1,10 +1,12 @@
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/select/select_option.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
import '../util/database_test_op.dart';
import '../util/util.dart';
@ -21,7 +23,7 @@ void main() {
// Invoke the field editor
await tester.tapGridFieldWithName('Name');
await tester.tapEditPropertyButton();
await tester.tapEditFieldButton();
await tester.renameField('hello world');
await tester.dismissFieldEditor();
@ -38,9 +40,9 @@ void main() {
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditPropertyButton();
await tester.tapEditFieldButton();
await tester.tapTypeOptionButton();
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Checkbox);
await tester.dismissFieldEditor();
@ -139,7 +141,7 @@ void main() {
await tester.tapNewPropertyButton();
// Open the type option menu
await tester.tapTypeOptionButton();
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Checklist);
@ -170,7 +172,7 @@ void main() {
await tester.renameField(fieldType.name);
// Open the type option menu
await tester.tapTypeOptionButton();
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(fieldType);
await tester.dismissFieldEditor();
@ -181,6 +183,84 @@ void main() {
}
});
testWidgets('field types with empty type option editor', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
for (final fieldType in [
FieldType.RichText,
FieldType.Checkbox,
FieldType.Checklist,
FieldType.URL,
]) {
// create the field
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
await tester.renameField(fieldType.i18n);
// change field type
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(fieldType);
await tester.dismissFieldEditor();
// open the field editor
await tester.tapGridFieldWithName(fieldType.i18n);
await tester.tapEditFieldButton();
// check type option editor is empty
tester.expectEmptyTypeOptionEditor();
await tester.dismissFieldEditor();
}
});
testWidgets('number field type option', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.scrollToRight(find.byType(GridPage));
// create a number field
await tester.tapNewPropertyButton();
await tester.renameField("Number");
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.Number);
await tester.dismissFieldEditor();
// enter some data into the first number cell
await tester.editCell(
rowIndex: 0,
fieldType: FieldType.Number,
input: '123',
);
// edit the next cell to force the previous cell at row 0 to lose focus
await tester.editCell(
rowIndex: 1,
fieldType: FieldType.Number,
input: '0.2',
);
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.Number,
content: '123',
);
// open editor and change number format
await tester.tapGridFieldWithName('Number');
await tester.tapEditFieldButton();
await tester.changeNumberFieldFormat();
await tester.dismissFieldEditor();
// assert number format has been changed
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.Number,
content: '\$123',
);
});
testWidgets('add option', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -189,9 +269,9 @@ void main() {
layout: ViewLayoutPB.Grid,
);
// Invoke the field editor
// invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditPropertyButton();
await tester.tapEditFieldButton();
// tap 'add option' button
await tester.tapAddSelectOptionButton();
@ -203,10 +283,115 @@ void main() {
await tester.enterText(inputField, text);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.pumpAndSettle(const Duration(milliseconds: 500));
// check the result
tester.expectToSeeText(text);
});
testWidgets('date time field type options', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.scrollToRight(find.byType(GridPage));
// create a date field
await tester.tapNewPropertyButton();
await tester.renameField(FieldType.DateTime.i18n);
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.DateTime);
await tester.dismissFieldEditor();
// edit the first date cell
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
await tester.toggleIncludeTime();
final now = DateTime.now();
await tester.selectDay(content: now.day);
await tester.dismissCellEditor();
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('MMM dd, y HH:mm').format(now),
);
// open editor and change date & time format
await tester.tapGridFieldWithName(FieldType.DateTime.i18n);
await tester.tapEditFieldButton();
await tester.changeDateFormat();
await tester.changeTimeFormat();
await tester.dismissFieldEditor();
// assert date format has been changed
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.DateTime,
content: DateFormat('dd/MM/y hh:mm a').format(now),
);
});
testWidgets('last modified and created at field type options',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
final created = DateTime.now();
// create a created at field
await tester.tapNewPropertyButton();
await tester.renameField(FieldType.CreatedTime.i18n);
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.CreatedTime);
await tester.dismissFieldEditor();
// create a last modified field
await tester.tapNewPropertyButton();
await tester.renameField(FieldType.LastEditedTime.i18n);
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.LastEditedTime);
await tester.dismissFieldEditor();
final modified = DateTime.now();
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.CreatedTime,
content: DateFormat('MMM dd, y HH:mm').format(created),
);
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.LastEditedTime,
content: DateFormat('MMM dd, y HH:mm').format(modified),
);
// open field editor and change date & time format
await tester.tapGridFieldWithName(FieldType.LastEditedTime.i18n);
await tester.tapEditFieldButton();
await tester.changeDateFormat();
await tester.changeTimeFormat();
await tester.dismissFieldEditor();
// open field editor and change date & time format
await tester.tapGridFieldWithName(FieldType.CreatedTime.i18n);
await tester.tapEditFieldButton();
await tester.changeDateFormat();
await tester.changeTimeFormat();
await tester.dismissFieldEditor();
// assert format has been changed
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.CreatedTime,
content: DateFormat('dd/MM/y hh:mm a').format(created),
);
tester.assertCellContent(
rowIndex: 0,
fieldType: FieldType.LastEditedTime,
content: DateFormat('dd/MM/y hh:mm a').format(modified),
);
});
});
}

View File

@ -20,10 +20,10 @@ void main() {
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditPropertyButton();
await tester.tapEditFieldButton();
// Change to date type
await tester.tapTypeOptionButton();
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.DateTime);
await tester.dismissFieldEditor();
@ -68,10 +68,10 @@ void main() {
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
await tester.tapEditPropertyButton();
await tester.tapEditFieldButton();
// Change to date type
await tester.tapTypeOptionButton();
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(FieldType.DateTime);
await tester.dismissFieldEditor();

View File

@ -14,8 +14,8 @@ import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
testWidgets('row details page opens', (tester) async {
group('grid row detail page:', () {
testWidgets('opens', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -27,9 +27,12 @@ void main() {
// Make sure that the row page is opened
tester.assertRowDetailPageOpened();
// Each row detail page should have a document
await tester.assertDocumentExistInRowDetailPage();
});
testWidgets('insert emoji in the row detail page', (tester) async {
testWidgets('add emoji', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -48,7 +51,7 @@ void main() {
await tester.tapButton(find.byType(EmojiButton));
});
testWidgets('update emoji in the row detail page', (tester) async {
testWidgets('update emoji', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -75,7 +78,7 @@ void main() {
expect(emojiText, findsNWidgets(2));
});
testWidgets('remove emoji in the row detail page', (tester) async {
testWidgets('remove emoji', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -96,7 +99,7 @@ void main() {
expect(emojiText, findsNothing);
});
testWidgets('create list of fields in row detail page', (tester) async {
testWidgets('create list of fields', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -120,7 +123,7 @@ void main() {
await tester.renameField(fieldType.name);
// Open the type option menu
await tester.tapTypeOptionButton();
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(fieldType);
@ -211,20 +214,6 @@ void main() {
tester.assertToggleShowHiddenFieldsVisibility(false);
});
testWidgets('check document exists in row detail page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// Create a new grid
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
// Each row detail page should have a document
await tester.assertDocumentExistInRowDetailPage();
});
testWidgets('update the contents of the document and re-open it',
(tester) async {
await tester.initializeAppFlowy();
@ -305,7 +294,7 @@ void main() {
expect(tester.takeException(), isNull);
});
testWidgets('delete row in row detail page', (tester) async {
testWidgets('delete row', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -322,7 +311,7 @@ void main() {
await tester.assertNumberOfRowsInGridPage(2);
});
testWidgets('duplicate row in row detail page', (tester) async {
testWidgets('duplicate row', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();

View File

@ -155,8 +155,9 @@ void main() {
'',
];
for (final (index, content) in dateCells.indexed) {
await tester.assertDateCellInGrid(
tester.assertCellContent(
rowIndex: index,
fieldType: FieldType.DateTime,
content: content,
);
}

View File

@ -1,9 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@ -40,7 +40,7 @@ void main() {
Position(path: [1]),
),
);
final gridTextCell = find.byType(GridTextCell).first;
final gridTextCell = find.byType(EditableTextCell).first;
await tester.tapButton(gridTextCell);
expect(tester.editor.getCurrentEditorState().selection, isNull);

View File

@ -1,5 +1,16 @@
import 'dart:io';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/number.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/url.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -40,7 +51,6 @@ import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dar
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_editor.dart';
@ -64,8 +74,6 @@ import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/remi
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:calendar_view/calendar_view.dart';
@ -142,7 +150,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
}
///
Finder cellFinder(int rowIndex, FieldType fieldType, {int cellIndex = 0}) {
final findRow = find.byType(GridRow, skipOffstage: false);
final findCell = finderForFieldType(fieldType);
@ -174,10 +181,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
required bool isSelected,
}) async {
final cell = cellFinder(rowIndex, FieldType.Checkbox);
var finder = find.byType(CheckboxCellUncheck);
if (isSelected) {
finder = find.byType(CheckboxCellCheck);
}
final finder = isSelected
? find.byWidgetPredicate(
(widget) =>
widget is FlowySvg && widget.svg == FlowySvgs.check_filled_s,
)
: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s,
);
expect(
find.descendant(
@ -210,14 +221,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
matching: find.text(content),
skipOffstage: false,
);
final text = find.descendant(
of: find.byType(TextField),
matching: findContent,
skipOffstage: false,
);
expect(text, findsOneWidget);
expect(findContent, findsOneWidget);
}
Future<void> assertSingleSelectOption({
@ -275,44 +279,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
final finder = find.descendant(
of: findCell,
matching: find.byWidgetPredicate(
(widget) {
if (widget is ChecklistProgressBar) {
return widget.percent == percent;
}
return false;
},
(widget) =>
widget is ChecklistProgressBar && widget.percent == percent,
),
);
expect(finder, findsOneWidget);
}
}
Future<void> assertDateCellInGrid({
required int rowIndex,
required String content,
}) async {
final findRow = find.byType(GridRow, skipOffstage: false);
final findCell = find.descendant(
of: findRow.at(rowIndex),
matching: find.byType(GridDateCell),
skipOffstage: false,
);
final text = find.descendant(
of: findCell,
matching: find.byWidgetPredicate(
(widget) {
if (widget is FlowyText) {
return widget.text == content;
}
return false;
},
),
skipOffstage: false,
);
expect(text, findsOneWidget);
}
Future<void> selectDay({
required int content,
}) async {
@ -374,15 +348,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(finder);
}
Future<void> changeDateFormat() async {
final findDateEditor = find.byType(DateCellEditor);
final findDateTimeOptionButton = find.byType(DateTypeOptionButton);
final finder = find.descendant(
of: findDateEditor,
matching: findDateTimeOptionButton,
);
await tapButton(finder);
Future<void> tapChangeDateTimeFormatButton() async {
await tapButton(find.byType(DateTypeOptionButton));
}
Future<void> changeDateFormat() async {
final findDateFormatButton = find.byType(DateFormatButton);
await tapButton(findDateFormatButton);
@ -391,14 +361,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> changeTimeFormat() async {
final findDateEditor = find.byType(DateCellEditor);
final findDateTimeOptionButton = find.byType(DateTypeOptionButton);
final finder = find.descendant(
of: findDateEditor,
matching: findDateTimeOptionButton,
);
await tapButton(finder);
final findDateFormatButton = find.byType(TimeFormatButton);
await tapButton(findDateFormatButton);
@ -512,7 +474,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
void assertChecklistEditorVisible({required bool visible}) {
final editor = find.byType(GridChecklistCellEditor);
final editor = find.byType(ChecklistCellEditor);
if (visible) {
expect(editor, findsOneWidget);
} else {
@ -625,7 +587,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> editTitleInRowDetailPage(String title) async {
final titleField = find.byType(GridTextCell);
final titleField = find.byType(EditableTextCell);
await enterText(titleField, title);
await pumpAndSettle();
}
@ -643,11 +605,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future<void> openEmojiPicker() async {
await tapButton(find.byType(EmojiPickerButton));
await tapButton(find.byType(AddEmojiButton));
}
Future<void> tapDateCellInRowDetailPage() async {
final findDateCell = find.byType(GridDateCell);
final findDateCell = find.byType(EditableDateCell);
await tapButton(findDateCell);
}
@ -738,7 +700,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> scrollToRight(Finder find) async {
final size = getSize(find);
await drag(find, Offset(-size.width, 0));
await drag(find, Offset(-size.width, 0), warnIfMissed: false);
await pumpAndSettle(const Duration(milliseconds: 500));
}
@ -756,7 +718,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
/// Should call [tapGridFieldWithName] first.
Future<void> tapEditPropertyButton() async {
Future<void> tapEditFieldButton() async {
await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr());
await pumpAndSettle(const Duration(milliseconds: 200));
}
@ -770,7 +732,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(field);
}
/// Should call [tapGridFieldWithName] first.
/// A SimpleDialog must be shown first, e.g. when deleting a field.
Future<void> tapDialogOkButton() async {
final field = find.byWidgetPredicate(
(widget) =>
@ -838,7 +800,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(find.byType(RowDetailPageDuplicateButton));
}
Future<void> tapTypeOptionButton() async {
Future<void> tapSwitchFieldTypeButton() async {
await tapButton(find.byType(SwitchFieldButton));
}
@ -846,7 +808,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await sendKeyEvent(LogicalKeyboardKey.escape);
}
/// Must call [tapTypeOptionButton] first.
/// Must call [tapSwitchFieldTypeButton] first.
Future<void> selectFieldType(FieldType fieldType) async {
final fieldTypeCell = find.byType(FieldTypeCell);
final fieldTypeButton = find.descendant(
@ -858,6 +820,17 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(fieldTypeButton);
}
// Use in edit mode of FieldEditor
void expectEmptyTypeOptionEditor() {
expect(
find.descendant(
of: find.byType(FieldTypeOptionEditor),
matching: find.byType(TypeOptionSeparator),
),
findsNothing,
);
}
/// Each field has its own cell, so we can find the corresponding cell by
/// the field type after create a new field.
Future<void> findCellByFieldType(FieldType fieldType) async {
@ -979,7 +952,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await scrollToRight(find.byType(GridPage));
await tapNewPropertyButton();
await renameField(name);
await tapTypeOptionButton();
await tapSwitchFieldTypeButton();
await selectFieldType(fieldType);
await dismissFieldEditor();
}
@ -1407,7 +1380,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> dragDropRescheduleCalendarEvent() async {
final findEventCard = find.byType(EventCard);
await drag(findEventCard.first, const Offset(0, 300));
await pumpAndSettle();
await pumpAndSettle(const Duration(microseconds: 300));
}
Future<void> openUnscheduledEventsPopup() async {
@ -1624,6 +1597,23 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButtonWithName(LocaleKeys.importPanel_database.tr());
}
// Use in edit mode of FieldEditor
Future<void> changeNumberFieldFormat() async {
final changeFormatButton = find.descendant(
of: find.byType(FieldTypeOptionEditor),
matching: find.text("Number"),
);
await tapButton(changeFormatButton);
await tapButton(
find.byWidgetPredicate(
(widget) =>
widget is NumberFormatCell && widget.format == NumberFormatPB.USD,
),
);
}
// Use in edit mode of FieldEditor
Future<void> tapAddSelectOptionButton() async {
await tapButtonWithName(LocaleKeys.grid_field_addSelectOption.tr());
}
@ -1657,24 +1647,45 @@ Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
Finder finderForFieldType(FieldType fieldType) {
switch (fieldType) {
case FieldType.Checkbox:
return find.byType(GridCheckboxCell, skipOffstage: false);
return find.byType(EditableCheckboxCell, skipOffstage: false);
case FieldType.DateTime:
return find.byType(GridDateCell, skipOffstage: false);
return find.byType(EditableDateCell, skipOffstage: false);
case FieldType.LastEditedTime:
return find.byWidgetPredicate(
(widget) =>
widget is EditableTimestampCell &&
widget.fieldType == FieldType.LastEditedTime,
skipOffstage: false,
);
case FieldType.CreatedTime:
return find.byType(GridTimestampCell, skipOffstage: false);
return find.byWidgetPredicate(
(widget) =>
widget is EditableTimestampCell &&
widget.fieldType == FieldType.CreatedTime,
skipOffstage: false,
);
case FieldType.SingleSelect:
return find.byType(GridSingleSelectCell, skipOffstage: false);
return find.byWidgetPredicate(
(widget) =>
widget is EditableSelectOptionCell &&
widget.fieldType == FieldType.SingleSelect,
skipOffstage: false,
);
case FieldType.MultiSelect:
return find.byType(GridMultiSelectCell, skipOffstage: false);
return find.byWidgetPredicate(
(widget) =>
widget is EditableSelectOptionCell &&
widget.fieldType == FieldType.MultiSelect,
skipOffstage: false,
);
case FieldType.Checklist:
return find.byType(GridChecklistCell, skipOffstage: false);
return find.byType(EditableChecklistCell, skipOffstage: false);
case FieldType.Number:
return find.byType(GridNumberCell, skipOffstage: false);
return find.byType(EditableNumberCell, skipOffstage: false);
case FieldType.RichText:
return find.byType(GridTextCell, skipOffstage: false);
return find.byType(EditableTextCell, skipOffstage: false);
case FieldType.URL:
return find.byType(GridURLCell, skipOffstage: false);
return find.byType(EditableURLCell, skipOffstage: false);
default:
throw Exception('Unknown field type: $fieldType');
}

View File

@ -6,8 +6,8 @@ import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
@ -26,28 +26,16 @@ class MobileBoardContent extends StatefulWidget {
}
class _MobileBoardContentState extends State<MobileBoardContent> {
final renderHook = RowCardRenderHook<String>();
late final ScrollController scrollController;
late final AppFlowyBoardScrollController scrollManager;
@override
void initState() {
super.initState();
//mobile may not need this
//scroll to bottom when add a new card
// mobile may not need this
// scroll to bottom when add a new card
scrollManager = AppFlowyBoardScrollController();
scrollController = ScrollController();
renderHook.addSelectOptionHook((options, groupId, _) {
// The cell should hide if the option id is equal to the groupId.
final isInGroup =
options.where((element) => element.id == groupId).isNotEmpty;
if (isInGroup || options.isEmpty) {
return const SizedBox.shrink();
}
return null;
});
}
@override
@ -156,10 +144,10 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox.shrink(key: ObjectKey(groupItem));
final cellCache = rowCache.cellCache;
final viewId = boardBloc.viewId;
final cellBuilder = CardCellBuilder<String>(cellCache);
final cellBuilder =
CardCellBuilder(databaseController: boardBloc.databaseController);
final isEditing = boardBloc.state.isEditingRow &&
boardBloc.state.editingRow?.row.id == groupItem.row.id;
@ -169,15 +157,14 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
key: ValueKey(groupItemId),
margin: cardMargin,
decoration: _makeBoxDecoration(context),
child: RowCard<String>(
child: RowCard(
fieldController: boardBloc.fieldController,
rowMeta: rowMeta,
viewId: viewId,
rowCache: rowCache,
cardData: groupData.group.groupId,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: isEditing,
cellBuilder: cellBuilder,
renderHook: renderHook,
openCard: (context) {
context.push(
MobileRowDetailPage.routeName,
@ -192,11 +179,9 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
.add(BoardEvent.startEditingRow(groupData.group, groupItem.row)),
onEndEditing: () =>
boardBloc.add(BoardEvent.endEditingRow(groupItem.row.id)),
styleConfiguration: const RowCardStyleConfiguration(
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: mobileBoardCardCellStyleMap(context),
showAccessory: false,
hoverStyle: null,
cardPadding: EdgeInsets.all(8),
cellPadding: EdgeInsets.zero,
),
),
);
@ -214,18 +199,15 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
),
)
: null,
boxShadow:
// The shadow is only visible in light mode.
themeMode == ThemeMode.light
? [
BoxShadow(
color:
Theme.of(context).colorScheme.outline.withOpacity(0.5),
blurRadius: 4,
offset: const Offset(0, 2),
),
]
: null,
boxShadow: themeMode == ThemeMode.light
? [
BoxShadow(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
blurRadius: 4,
offset: const Offset(0, 2),
),
]
: null,
);
}
}

View File

@ -3,12 +3,9 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
@ -107,7 +104,6 @@ class MobileHiddenGroupList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = context.read<BoardBloc>();
return BlocBuilder<BoardBloc, BoardState>(
builder: (_, state) => ReorderableListView.builder(
itemCount: state.hiddenGroups.length,
@ -115,7 +111,10 @@ class MobileHiddenGroupList extends StatelessWidget {
key: ValueKey(state.hiddenGroups[index].groupId),
group: state.hiddenGroups[index],
index: index,
bloc: bloc,
),
proxyDecorator: (child, index, animation) => BlocProvider.value(
value: context.read<BoardBloc>(),
child: Material(color: Colors.transparent, child: child),
),
physics: const ClampingScrollPhysics(),
onReorder: (oldIndex, newIndex) {
@ -124,7 +123,9 @@ class MobileHiddenGroupList extends StatelessWidget {
}
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
final toGroupId = state.hiddenGroups[newIndex].groupId;
bloc.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
context
.read<BoardBloc>()
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
},
),
);
@ -136,181 +137,111 @@ class MobileHiddenGroup extends StatelessWidget {
super.key,
required this.group,
required this.index,
required this.bloc,
});
final GroupPB group;
final BoardBloc bloc;
final int index;
@override
Widget build(BuildContext context) {
final databaseController = bloc.databaseController;
final databaseController = context.read<BoardBloc>().databaseController;
final primaryField = databaseController.fieldController.fieldInfos
.firstWhereOrNull((element) => element.isPrimary)!;
return BlocProvider<BoardBloc>.value(
value: bloc,
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == this.group.groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == this.group.groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
return ExpansionTile(
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
title: Row(
children: [
Expanded(
child: Text(
group.groupName,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
child: const Padding(
padding: EdgeInsets.all(4),
child: FlowySvg(
FlowySvgs.hide_m,
size: Size.square(20),
),
),
onTap: () => showFlowyMobileConfirmDialog(
context,
title: LocaleKeys.board_mobile_unhideGroup.tr(),
content: LocaleKeys.board_mobile_unhideGroupContent.tr(),
actionButtonTitle: LocaleKeys.button_yes.tr(),
actionButtonColor: Theme.of(context).colorScheme.primary,
onActionButtonPressed: () => context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility(
group,
true,
),
),
),
),
],
),
children: [
MobileHiddenGroupItemList(
bloc: bloc,
viewId: databaseController.viewId,
groupId: group.groupId,
primaryField: primaryField,
rowCache: databaseController.rowCache,
final cells = group.rows.map(
(item) {
final cellContext =
databaseController.rowCache.loadCells(item)[primaryField.id]!;
return TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.bodyMedium,
foregroundColor: Theme.of(context).colorScheme.onBackground,
visualDensity: VisualDensity.compact,
),
],
);
},
),
);
}
}
class MobileHiddenGroupItemList extends StatelessWidget {
const MobileHiddenGroupItemList({
required this.bloc,
required this.groupId,
required this.viewId,
required this.primaryField,
required this.rowCache,
super.key,
});
final BoardBloc bloc;
final String groupId;
final String viewId;
final FieldInfo primaryField;
final RowCache rowCache;
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: bloc,
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
final cells = <Widget>[
...group.rows.map(
(item) {
final cellContext = rowCache.loadCells(item)[primaryField.id]!;
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, _, __) {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final text = cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
if (text.isEmpty) {
return const SizedBox.shrink();
}
return Row(
children: [
if (!cellContext.rowMeta.isDocumentEmpty) ...[
const FlowySvg(FlowySvgs.notes_s),
const HSpace(4),
],
Expanded(
child: Text(
text,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
);
},
);
});
return TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.bodyMedium,
foregroundColor: Theme.of(context).colorScheme.onBackground,
visualDensity: VisualDensity.compact,
),
child: CardCellBuilder<String>(rowCache.cellCache).buildCell(
cellContext: cellContext,
renderHook: renderHook,
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
),
onPressed: () {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: item.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
child: CardCellBuilder(
databaseController:
context.read<BoardBloc>().databaseController,
).build(
cellContext: cellContext,
styleMap: {FieldType.RichText: _titleCellStyle(context)},
hasNotes: !item.isDocumentEmpty,
),
onPressed: () {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: item.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
),
];
);
},
).toList();
return ListView.builder(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
);
},
),
return ExpansionTile(
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
title: Row(
children: [
Expanded(
child: Text(
group.groupName,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
child: const Padding(
padding: EdgeInsets.all(4),
child: FlowySvg(
FlowySvgs.hide_m,
size: Size.square(20),
),
),
onTap: () => showFlowyMobileConfirmDialog(
context,
title: LocaleKeys.board_mobile_unhideGroup.tr(),
content: LocaleKeys.board_mobile_unhideGroupContent.tr(),
actionButtonTitle: LocaleKeys.button_yes.tr(),
actionButtonColor: Theme.of(context).colorScheme.primary,
onActionButtonPressed: () => context.read<BoardBloc>().add(
BoardEvent.toggleGroupVisibility(
group,
true,
),
),
),
),
],
),
children: cells,
);
},
);
}
TextCardCellStyle _titleCellStyle(BuildContext context) {
return TextCardCellStyle(
padding: EdgeInsets.zero,
textStyle: Theme.of(context).textTheme.bodyMedium!,
maxLines: 2,
titleTextStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 11, overflow: TextOverflow.ellipsis),
);
}
}

View File

@ -1,2 +1,2 @@
export 'card_detail/mobile_card_detail_screen.dart';
export 'card_content/mobile_card_content.dart';
export 'mobile_card_content.dart';

View File

@ -1,9 +0,0 @@
export 'checkbox.dart';
export 'date.dart';
export 'style.dart';
export 'time_stamp.dart';
export 'number.dart';
export 'text.dart';
export 'select_option.dart';
export 'url.dart';
export 'checklist.dart';

View File

@ -1,68 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileCheckboxCardCell extends CardCell {
const MobileCheckboxCardCell({
super.key,
required this.cellControllerBuilder,
});
final CellControllerBuilder cellControllerBuilder;
@override
State<MobileCheckboxCardCell> createState() => _CheckboxCellState();
}
class _CheckboxCellState extends State<MobileCheckboxCardCell> {
late final CheckboxCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCellBloc(cellController: cellController)
..add(const CheckboxCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
buildWhen: (previous, current) =>
previous.isSelected != current.isSelected,
builder: (context, state) {
return Align(
alignment: Alignment.centerLeft,
child: IconButton(
padding: EdgeInsets.zero,
alignment: Alignment.centerLeft,
visualDensity: VisualDensity.compact,
icon: FlowySvg(
state.isSelected
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
size: const Size.square(24),
),
onPressed: () => context
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
),
);
},
),
);
}
}

View File

@ -1,137 +0,0 @@
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/card_cells.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
class MobileChecklistCardCell extends CardCell {
const MobileChecklistCardCell({
super.key,
required this.cellControllerBuilder,
});
final CellControllerBuilder cellControllerBuilder;
@override
State<MobileChecklistCardCell> createState() => _ChecklistCellState();
}
class _ChecklistCellState extends State<MobileChecklistCardCell> {
late final ChecklistCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCellBloc(cellController: cellController)
..add(const ChecklistCellEvent.initial());
}
@override
void dispose() {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.tasks.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: cellStyle.padding,
child: MobileChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
);
},
),
);
}
}
class MobileChecklistProgressBar extends StatefulWidget {
const MobileChecklistProgressBar({
super.key,
required this.tasks,
required this.percent,
});
final List<ChecklistSelectOption> tasks;
final double percent;
final int segmentLimit = 5;
@override
State<MobileChecklistProgressBar> createState() =>
_MobileChecklistProgresssBarState();
}
class _MobileChecklistProgresssBarState
extends State<MobileChecklistProgressBar> {
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
final numFinishedTasks = widget.tasks.where((e) => e.isSelected).length;
final completedTaskColor = numFinishedTasks == widget.tasks.length
? AFThemeExtension.of(context).success
: Theme.of(context).colorScheme.primary;
return Row(
children: [
Expanded(
child: Row(
children: [
if (widget.tasks.isNotEmpty &&
widget.tasks.length <= widget.segmentLimit)
...List<Widget>.generate(
widget.tasks.length,
(index) => Flexible(
child: Container(
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(2)),
color: index < numFinishedTasks
? completedTaskColor
: AFThemeExtension.of(context).progressBarBGColor,
),
margin: const EdgeInsets.symmetric(horizontal: 1),
height: 6.0,
),
),
)
else
Expanded(
child: LinearPercentIndicator(
lineHeight: 6.0,
percent: widget.percent,
padding: EdgeInsets.zero,
progressColor: completedTaskColor,
backgroundColor:
AFThemeExtension.of(context).progressBarBGColor,
barRadius: const Radius.circular(2),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
"${(widget.percent * 100).round()}%",
style: cellStyle.secondaryTextStyle(),
),
),
],
);
}
}

View File

@ -1,74 +0,0 @@
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileDateCardCell<CustomCardData> extends CardCell {
const MobileDateCardCell({
super.key,
required this.cellControllerBuilder,
this.renderHook,
});
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<dynamic, CustomCardData>? renderHook;
@override
State<MobileDateCardCell> createState() => _DateCellState();
}
class _DateCellState extends State<MobileDateCardCell> {
late final DateCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCellBloc(cellController: cellController)
..add(const DateCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<DateCellBloc, DateCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox();
} else {
final Widget? custom = widget.renderHook?.call(
state.data,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Container(
alignment: Alignment.centerLeft,
padding: cellStyle.padding,
child: Text(
state.dateStr,
style: cellStyle.secondaryTextStyle(),
),
);
}
},
),
);
}
}

View File

@ -1,76 +0,0 @@
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileNumberCardCell<CustomCardData> extends CardCell {
const MobileNumberCardCell({
super.key,
required this.cellControllerBuilder,
CustomCardData? cardData,
this.renderHook,
});
final CellRenderHook<String, CustomCardData>? renderHook;
final CellControllerBuilder cellControllerBuilder;
@override
State<MobileNumberCardCell> createState() => _NumberCellState();
}
class _NumberCellState extends State<MobileNumberCardCell> {
late final NumberCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCellBloc(cellController: cellController)
..add(const NumberCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<NumberCellBloc, NumberCellState>(
buildWhen: (previous, current) =>
previous.cellContent != current.cellContent,
builder: (context, state) {
if (state.cellContent.isEmpty) {
return const SizedBox();
} else {
final Widget? custom = widget.renderHook?.call(
state.cellContent,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Container(
alignment: Alignment.centerLeft,
padding: cellStyle.padding,
child: Text(
state.cellContent,
style: cellStyle.primaryTextStyle(),
),
);
}
},
),
);
}
}

View File

@ -1,147 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileSelectOptionCardCell<CustomCardData> extends CardCell {
const MobileSelectOptionCardCell({
super.key,
required this.cellControllerBuilder,
required CustomCardData? cardData,
this.renderHook,
});
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<List<SelectOptionPB>, CustomCardData>? renderHook;
@override
State<MobileSelectOptionCardCell> createState() => _SelectOptionCellState();
}
class _SelectOptionCellState extends State<MobileSelectOptionCardCell> {
late final SelectOptionCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
_cellBloc = SelectOptionCellBloc(cellController: cellController)
..add(const SelectOptionCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
buildWhen: (previous, current) {
return previous.selectedOptions != current.selectedOptions;
},
builder: (context, state) {
final Widget? custom = widget.renderHook?.call(
state.selectedOptions,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
final children = state.selectedOptions
.map(
(option) => MobileSelectOptionTag.fromOption(
context: context,
option: option,
),
)
.toList();
return IntrinsicHeight(
child: Padding(
padding: cellStyle.padding,
child: SizedBox.expand(
child: Wrap(spacing: 4, runSpacing: 2, children: children),
),
),
);
},
),
);
}
}
class MobileSelectOptionTag extends StatelessWidget {
const MobileSelectOptionTag({
super.key,
required this.name,
required this.color,
this.onSelected,
this.onRemove,
});
factory MobileSelectOptionTag.fromOption({
required BuildContext context,
required SelectOptionPB option,
VoidCallback? onSelected,
Function(String)? onRemove,
}) {
return MobileSelectOptionTag(
name: option.name,
color: option.color.toColor(context),
onSelected: onSelected,
onRemove: onRemove,
);
}
final String name;
final Color color;
final VoidCallback? onSelected;
final void Function(String)? onRemove;
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
name,
style: cellStyle.tagTextStyle(),
),
),
if (onRemove != null) ...[
const HSpace(2),
IconButton(
onPressed: () => onRemove?.call(name),
icon: const FlowySvg(
FlowySvgs.close_s,
),
),
],
],
),
);
}
}

View File

@ -1,44 +0,0 @@
import 'package:flutter/material.dart';
class MobileCardCellStyle {
MobileCardCellStyle(this.context);
BuildContext context;
EdgeInsets get padding => const EdgeInsets.symmetric(
vertical: 4,
);
TextStyle? primaryTextStyle() {
final theme = Theme.of(context);
return theme.textTheme.bodyMedium?.copyWith(
fontSize: 16,
color: theme.colorScheme.onBackground,
);
}
TextStyle? secondaryTextStyle() {
final theme = Theme.of(context);
return theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.tertiary,
fontSize: 14,
);
}
TextStyle? tagTextStyle() {
final theme = Theme.of(context);
return theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onBackground,
fontSize: 12,
);
}
TextStyle? urlTextStyle() {
final theme = Theme.of(context);
return theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.primary,
fontSize: 16,
decoration: TextDecoration.underline,
);
}
}

View File

@ -1,79 +0,0 @@
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileTextCardCell<CustomCardData> extends CardCell {
const MobileTextCardCell({
super.key,
required this.cellControllerBuilder,
CustomCardData? cardData,
this.renderHook,
});
final CellRenderHook<String, CustomCardData>? renderHook;
final CellControllerBuilder cellControllerBuilder;
@override
State<MobileTextCardCell> createState() => _MobileTextCardCellState();
}
class _MobileTextCardCellState extends State<MobileTextCardCell> {
late final TextCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<TextCellBloc, TextCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
// return custom widget if render hook is provided(for example, the title of the card on board view)
// if widget.cardData.isEmpty means there is no data for this cell
final Widget? custom = widget.renderHook?.call(
state.content,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
// if there is no render hook
// the empty text cell will be hidden
if (state.content.isEmpty) {
return const SizedBox();
}
return Container(
alignment: Alignment.centerLeft,
padding: cellStyle.padding,
child: Text(
state.content,
style: cellStyle.primaryTextStyle(),
),
);
},
),
);
}
}

View File

@ -1,73 +0,0 @@
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileTimestampCardCell<CustomCardData> extends CardCell {
const MobileTimestampCardCell({
super.key,
required this.cellControllerBuilder,
this.renderHook,
});
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<dynamic, CustomCardData>? renderHook;
@override
State<MobileTimestampCardCell> createState() => _TimestampCellState();
}
class _TimestampCellState extends State<MobileTimestampCardCell> {
late final TimestampCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as TimestampCellController;
_cellBloc = TimestampCellBloc(cellController: cellController)
..add(const TimestampCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox.shrink();
}
final Widget? custom = widget.renderHook?.call(
state.data,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Container(
alignment: Alignment.centerLeft,
padding: cellStyle.padding,
child: Text(
state.dateStr,
style: cellStyle.secondaryTextStyle(),
),
);
},
),
);
}
}

View File

@ -1,63 +0,0 @@
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileURLCardCell<CustomCardData> extends CardCell {
const MobileURLCardCell({
super.key,
required this.cellControllerBuilder,
});
final CellControllerBuilder cellControllerBuilder;
@override
State<MobileURLCardCell> createState() => _URLCellState();
}
class _URLCellState extends State<MobileURLCardCell> {
late final URLCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as URLCellController;
_cellBloc = URLCellBloc(cellController: cellController)
..add(const URLCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cellStyle = MobileCardCellStyle(context);
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<URLCellBloc, URLCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox();
} else {
return Container(
alignment: Alignment.centerLeft,
padding: cellStyle.padding,
child: Text(
state.content,
style: cellStyle.urlTextStyle(),
),
);
}
},
),
);
}
}

View File

@ -1,108 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileCardContent<CustomCardData> extends StatelessWidget {
const MobileCardContent({
super.key,
required this.cellBuilder,
required this.cells,
required this.cardData,
required this.styleConfiguration,
this.renderHook,
});
final CardCellBuilder<CustomCardData> cellBuilder;
final List<DatabaseCellContext> cells;
final RowCardRenderHook<CustomCardData>? renderHook;
final CustomCardData? cardData;
final RowCardStyleConfiguration styleConfiguration;
@override
Widget build(BuildContext context) {
return Padding(
padding: styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _makeCells(context, cells),
),
);
}
List<Widget> _makeCells(
BuildContext context,
List<DatabaseCellContext> cells,
) {
final List<Widget> children = [];
cells.asMap().forEach((int index, DatabaseCellContext cellContext) {
Widget child;
if (index == 0) {
// The title cell UI is different with a normal text cell.
// Use render hook to customize its UI
child = _buildTitleCell(cellContext);
} else {
child = Padding(
key: cellContext.key(),
padding: styleConfiguration.cellPadding,
child: cellBuilder.buildCell(
cellContext: cellContext,
cardData: cardData,
renderHook: renderHook,
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
),
);
}
children.add(child);
});
return children;
}
Widget _buildTitleCell(
DatabaseCellContext cellContext,
) {
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, cardData, context) {
final text = cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
final color = cellData.isEmpty
? Theme.of(context).hintColor
: Theme.of(context).colorScheme.onBackground;
return Row(
children: [
if (!cellContext.rowMeta.isDocumentEmpty) ...[
const FlowySvg(FlowySvgs.notes_s),
const HSpace(4),
],
Expanded(
child: FlowyText.medium(
text,
color: color,
overflow: TextOverflow.ellipsis,
),
),
],
);
});
return Padding(
key: cellContext.key(),
padding: styleConfiguration.cellPadding,
child: CardCellBuilder<String>(cellBuilder.cellCache).buildCell(
cellContext: cellContext,
renderHook: renderHook,
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
),
);
}
}

View File

@ -1,91 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RowDetailCheckboxCell extends GridCellWidget {
RowDetailCheckboxCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
});
final CellControllerBuilder cellControllerBuilder;
@override
GridCellState<RowDetailCheckboxCell> createState() =>
_RowDetailCheckboxCellState();
}
class _RowDetailCheckboxCellState extends GridCellState<RowDetailCheckboxCell> {
late final CheckboxCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCellBloc(cellController: cellController)
..add(const CheckboxCellEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
builder: (context, state) {
return InkWell(
onTap: () => context
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
borderRadius: const BorderRadius.all(Radius.circular(14)),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: FlowySvg(
state.isSelected
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
color: Theme.of(context).colorScheme.onBackground,
blendMode: BlendMode.dst,
size: const Size.square(24),
),
),
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
void requestBeginFocus() {
_cellBloc.add(const CheckboxCellEvent.select());
}
@override
String? onCopy() => _cellBloc.state.isSelected ? "Yes" : "No";
}

View File

@ -1,105 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RowDetailNumberCell extends GridCellWidget {
RowDetailNumberCell({
super.key,
required this.cellControllerBuilder,
this.hintText,
});
final CellControllerBuilder cellControllerBuilder;
final String? hintText;
@override
GridEditableTextCell<RowDetailNumberCell> createState() =>
_RowDetailNumberCellState();
}
class _RowDetailNumberCellState
extends GridEditableTextCell<RowDetailNumberCell> {
late final NumberCellBloc _cellBloc;
late final TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCellBloc(cellController: cellController)
..add(const NumberCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.cellContent);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: MultiBlocListener(
listeners: [
BlocListener<NumberCellBloc, NumberCellState>(
listenWhen: (p, c) => p.cellContent != c.cellContent,
listener: (context, state) => _controller.text = state.cellContent,
),
],
child: TextField(
controller: _controller,
keyboardType: const TextInputType.numberWithOptions(
signed: true,
decimal: true,
),
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16),
decoration: InputDecoration(
enabledBorder:
_getInputBorder(color: Theme.of(context).colorScheme.outline),
focusedBorder:
_getInputBorder(color: Theme.of(context).colorScheme.primary),
hintText: widget.hintText,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
isCollapsed: true,
isDense: true,
constraints: const BoxConstraints(),
),
// close keyboard when tapping outside of the text field
onTapOutside: (event) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
);
}
InputBorder _getInputBorder({Color? color}) {
return OutlineInputBorder(
borderSide: BorderSide(color: color!),
borderRadius: const BorderRadius.all(Radius.circular(14)),
gapPadding: 0,
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Future<void> focusChanged() async {
if (mounted &&
!_cellBloc.isClosed &&
_controller.text != _cellBloc.state.cellContent) {
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
}
}
@override
String? onCopy() => _cellBloc.state.cellContent;
}

View File

@ -1,108 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RowDetailTextCell extends GridCellWidget {
RowDetailTextCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
}) {
if (style != null) {
cellStyle = (style as GridTextCellStyle);
} else {
cellStyle = const GridTextCellStyle();
}
}
final CellControllerBuilder cellControllerBuilder;
late final GridTextCellStyle cellStyle;
@override
GridEditableTextCell<RowDetailTextCell> createState() =>
_RowDetailTextCellState();
}
class _RowDetailTextCellState extends GridEditableTextCell<RowDetailTextCell> {
late final TextCellBloc _cellBloc;
late final TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
child: TextField(
controller: _controller,
focusNode: focusNode,
style: widget.cellStyle.textStyle,
maxLines: null,
decoration: InputDecoration(
enabledBorder:
_getInputBorder(color: Theme.of(context).colorScheme.outline),
focusedBorder:
_getInputBorder(color: Theme.of(context).colorScheme.primary),
hintText: widget.cellStyle.placeholder,
contentPadding: widget.cellStyle.cellPadding ??
const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
isCollapsed: true,
isDense: true,
constraints: const BoxConstraints(minHeight: 48),
hintStyle: widget.cellStyle.textStyle
?.copyWith(color: Theme.of(context).hintColor),
),
onTapOutside: (event) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
);
}
InputBorder _getInputBorder({Color? color}) {
if (!widget.cellStyle.useRoundedBorder) {
return InputBorder.none;
}
return OutlineInputBorder(
borderSide: BorderSide(color: color!),
borderRadius: const BorderRadius.all(Radius.circular(14)),
gapPadding: 0,
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
String? onCopy() => _cellBloc.state.content;
@override
Future<void> focusChanged() {
_cellBloc.add(TextCellEvent.updateText(_controller.text));
return super.focusChanged();
}
}

View File

@ -1,125 +0,0 @@
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher_string.dart';
class RowDetailURLCell extends GridCellWidget {
RowDetailURLCell({
super.key,
required this.cellControllerBuilder,
this.hintText,
});
final CellControllerBuilder cellControllerBuilder;
final String? hintText;
@override
GridCellState<RowDetailURLCell> createState() => _RowDetailURLCellState();
}
class _RowDetailURLCellState extends GridCellState<RowDetailURLCell> {
late final URLCellBloc _cellBloc;
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as URLCellController;
_cellBloc = URLCellBloc(cellController: cellController)
..add(const URLCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocSelector<URLCellBloc, URLCellState, String>(
selector: (state) => state.content,
builder: (context, content) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () {
if (content.isEmpty) {
_showURLEditor(content);
return;
}
final shouldAddScheme = !['http', 'https']
.any((pattern) => content.startsWith(pattern));
final url = shouldAddScheme ? 'http://$content' : content;
canLaunchUrlString(url).then((value) => launchUrlString(url));
},
onLongPress: () => _showURLEditor(content),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
child: Text(
content.isEmpty ? widget.hintText ?? "" : content,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16,
decoration:
content.isEmpty ? null : TextDecoration.underline,
color: content.isEmpty
? Theme.of(context).hintColor
: Theme.of(context).colorScheme.primary,
),
),
),
),
);
},
),
);
}
void _showURLEditor(String content) {
showMobileBottomSheet(
context,
title: LocaleKeys.board_mobile_editURL.tr(),
showHeader: true,
showCloseButton: true,
builder: (_) {
final controller = TextEditingController(text: content);
return TextField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.url,
onEditingComplete: () {
_cellBloc.add(URLCellEvent.updateURL(controller.text));
context.pop();
},
);
},
);
}
@override
void requestBeginFocus() {
_focusNode.requestFocus();
}
}

View File

@ -2,18 +2,19 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/row/row_banner_bloc.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/application/row/mobile_row_detail_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/row_property.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@ -316,7 +317,7 @@ class MobileRowDetailPageContent extends StatefulWidget {
class MobileRowDetailPageContentState
extends State<MobileRowDetailPageContent> {
late final RowController rowController;
late final MobileRowDetailPageCellBuilder cellBuilder;
late final EditableCellBuilder cellBuilder;
String get viewId => widget.databaseController.viewId;
RowCache get rowCache => widget.databaseController.rowCache;
@ -332,16 +333,18 @@ class MobileRowDetailPageContentState
viewId: viewId,
rowCache: rowCache,
);
cellBuilder = MobileRowDetailPageCellBuilder(
cellCache: rowCache.cellCache,
cellBuilder = EditableCellBuilder(
databaseController: widget.databaseController,
);
}
@override
Widget build(BuildContext context) {
return BlocProvider<RowDetailBloc>(
create: (_) => RowDetailBloc(rowController: rowController)
..add(const RowDetailEvent.initial()),
create: (_) => RowDetailBloc(
fieldController: fieldController,
rowController: rowController,
),
child: BlocBuilder<RowDetailBloc, RowDetailState>(
builder: (context, rowDetailState) {
return Column(
@ -354,28 +357,17 @@ class MobileRowDetailPageContentState
child: BlocBuilder<RowBannerBloc, RowBannerState>(
builder: (context, state) {
if (state.primaryField != null) {
final cellStyle = GridTextCellStyle(
placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(),
textStyle:
Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 23,
fontWeight: FontWeight.w500,
),
cellPadding: const EdgeInsets.symmetric(vertical: 9),
useRoundedBorder: false,
final cellContext = CellContext(
rowId: rowController.rowId,
fieldId: state.primaryField!.id,
);
final cellContext = DatabaseCellContext(
viewId: viewId,
rowMeta: rowController.rowMeta,
fieldInfo: FieldInfo.initial(state.primaryField!),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: cellBuilder.build(
child: cellBuilder.buildCustom(
cellContext,
style: cellStyle,
skinMap: EditableCellSkinMap(
textSkin: _TitleSkin(),
),
),
);
}
@ -390,9 +382,8 @@ class MobileRowDetailPageContentState
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: MobileRowPropertyList(
databaseController: widget.databaseController,
cellBuilder: cellBuilder,
viewId: viewId,
fieldController: fieldController,
),
),
Padding(
@ -420,3 +411,35 @@ class MobileRowDetailPageContentState
);
}
}
class _TitleSkin extends IEditableTextCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TextCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 23,
fontWeight: FontWeight.w500,
),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 9),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),
isDense: true,
isCollapsed: true,
),
);
}
}

View File

@ -1,13 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cells.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -15,22 +12,19 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class MobileRowPropertyList extends StatelessWidget {
const MobileRowPropertyList({
super.key,
required this.viewId,
required this.fieldController,
required this.databaseController,
required this.cellBuilder,
});
final String viewId;
final FieldController fieldController;
final MobileRowDetailPageCellBuilder cellBuilder;
final DatabaseController databaseController;
final EditableCellBuilder cellBuilder;
@override
Widget build(BuildContext context) {
return BlocBuilder<RowDetailBloc, RowDetailState>(
builder: (context, state) {
final List<DatabaseCellContext> visibleCells = state.visibleCells
.where((element) => !element.fieldInfo.field.isPrimary)
.toList();
final List<CellContext> visibleCells =
state.visibleCells.where((cell) => !_isCellPrimary(cell)).toList();
return ListView.separated(
shrinkWrap: true,
@ -40,7 +34,7 @@ class MobileRowPropertyList extends StatelessWidget {
itemBuilder: (context, index) => _PropertyCell(
key: ValueKey('row_detail_${visibleCells[index].fieldId}'),
cellContext: visibleCells[index],
fieldController: fieldController,
fieldController: databaseController.fieldController,
cellBuilder: cellBuilder,
),
separatorBuilder: (_, __) => const VSpace(22),
@ -48,6 +42,9 @@ class MobileRowPropertyList extends StatelessWidget {
},
);
}
bool _isCellPrimary(CellContext cell) =>
databaseController.fieldController.getField(cell.fieldId)!.isPrimary;
}
class _PropertyCell extends StatefulWidget {
@ -58,9 +55,9 @@ class _PropertyCell extends StatefulWidget {
required this.cellBuilder,
});
final DatabaseCellContext cellContext;
final CellContext cellContext;
final FieldController fieldController;
final MobileRowDetailPageCellBuilder cellBuilder;
final EditableCellBuilder cellBuilder;
@override
State<StatefulWidget> createState() => _PropertyCellState();
@ -69,8 +66,10 @@ class _PropertyCell extends StatefulWidget {
class _PropertyCellState extends State<_PropertyCell> {
@override
Widget build(BuildContext context) {
final style = _customCellStyle(widget.cellContext.fieldType);
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
final fieldInfo =
widget.fieldController.getField(widget.cellContext.fieldId)!;
final cell = widget.cellBuilder
.buildStyled(widget.cellContext, EditableCellStyle.mobileRowDetail);
return Column(
mainAxisSize: MainAxisSize.min,
@ -78,13 +77,13 @@ class _PropertyCellState extends State<_PropertyCell> {
Row(
children: [
FlowySvg(
widget.cellContext.fieldInfo.field.fieldType.icon(),
fieldInfo.fieldType.icon(),
color: Theme.of(context).hintColor,
),
const HSpace(6),
Expanded(
child: FlowyText.regular(
widget.cellContext.fieldInfo.field.name,
fieldInfo.name,
overflow: TextOverflow.ellipsis,
fontSize: 14,
color: Theme.of(context).hintColor,
@ -98,60 +97,3 @@ class _PropertyCellState extends State<_PropertyCell> {
);
}
}
GridCellStyle? _customCellStyle(FieldType fieldType) {
switch (fieldType) {
case FieldType.Checkbox:
return GridCheckboxCellStyle(
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.DateTime:
return DateCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
useRoundedBorder: true,
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return TimestampCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
useRoundedBorder: true,
);
case FieldType.MultiSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
useRoundedBorder: true,
);
case FieldType.Checklist:
return ChecklistCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
useRoundedBorders: true,
);
case FieldType.Number:
return GridNumberCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
case FieldType.RichText:
return GridTextCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
useRoundedBorder: true,
);
case FieldType.SingleSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
useRoundedBorder: true,
);
case FieldType.URL:
return GridURLCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
accessoryTypes: [],
);
}
throw UnimplementedError;
}

View File

@ -0,0 +1,40 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
class MobileCardContent extends StatelessWidget {
const MobileCardContent({
super.key,
required this.rowMeta,
required this.cellBuilder,
required this.cells,
required this.styleConfiguration,
});
final RowMetaPB rowMeta;
final CardCellBuilder cellBuilder;
final List<CellContext> cells;
final RowCardStyleConfiguration styleConfiguration;
@override
Widget build(BuildContext context) {
return Padding(
padding: styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: cells.map(
(cellContext) {
return cellBuilder.build(
cellContext: cellContext,
styleMap: mobileBoardCardCellStyleMap(context),
hasNotes: !rowMeta.isDocumentEmpty,
);
},
).toList(),
),
);
}
}

View File

@ -1,5 +0,0 @@
export 'mobile_checkbox_cell.dart';
export 'mobile_number_cell.dart';
export 'mobile_text_cell.dart';
export 'mobile_timestamp_cell.dart';
export 'mobile_url_cell.dart';

View File

@ -1,70 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileCheckboxCell extends GridCellWidget {
MobileCheckboxCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
});
final CellControllerBuilder cellControllerBuilder;
@override
GridCellState<MobileCheckboxCell> createState() => _CheckboxCellState();
}
class _CheckboxCellState extends GridCellState<MobileCheckboxCell> {
late final CheckboxCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCellBloc(cellController: cellController)
..add(const CheckboxCellEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
builder: (context, state) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: FlowySvg(
state.isSelected
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
size: const Size.square(24),
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
void requestBeginFocus() {
_cellBloc.add(const CheckboxCellEvent.select());
}
@override
String? onCopy() => _cellBloc.state.isSelected ? "Yes" : "No";
}

View File

@ -1,135 +0,0 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/mobile_checklist_cell_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileChecklistCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder;
late final ChecklistCellStyle cellStyle;
MobileChecklistCell({
required this.cellControllerBuilder,
GridCellStyle? style,
super.key,
}) {
if (style != null) {
cellStyle = (style as ChecklistCellStyle);
} else {
cellStyle = const ChecklistCellStyle();
}
}
@override
GridCellState<MobileChecklistCell> createState() =>
_MobileChecklistCellState();
}
class _MobileChecklistCellState extends GridCellState<MobileChecklistCell> {
late ChecklistCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCellBloc(cellController: cellController)
..add(const ChecklistCellEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (widget.cellStyle.useRoundedBorders) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (context) {
return MobileChecklistCellEditScreen(
cellController: widget.cellControllerBuilder.build()
as ChecklistCellController,
);
},
),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
minWidth: double.infinity,
),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).colorScheme.outline),
),
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Padding(
padding: widget.cellStyle.cellPadding ?? EdgeInsets.zero,
child: Align(
alignment: AlignmentDirectional.centerStart,
child: state.tasks.isEmpty
? FlowyText(
widget.cellStyle.placeholder,
fontSize: 15,
color: Theme.of(context).hintColor,
)
: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
fontSize: 15,
),
),
),
),
);
} else {
return FlowyButton(
radius: BorderRadius.zero,
hoverColor: Colors.transparent,
text: Container(
alignment: Alignment.centerLeft,
padding:
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
child: state.tasks.isEmpty
? FlowyText(
widget.cellStyle.placeholder,
fontSize: 15,
color: Theme.of(context).hintColor,
)
: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
fontSize: 15,
),
),
onTap: () => showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
backgroundColor: Theme.of(context).colorScheme.background,
builder: (context) {
return MobileChecklistCellEditScreen(
cellController: widget.cellControllerBuilder.build()
as ChecklistCellController,
);
},
),
);
}
},
),
);
}
@override
void requestBeginFocus() {}
}

View File

@ -1,87 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileNumberCell extends GridCellWidget {
MobileNumberCell({
super.key,
required this.cellControllerBuilder,
this.hintText,
});
final CellControllerBuilder cellControllerBuilder;
final String? hintText;
@override
GridEditableTextCell<MobileNumberCell> createState() => _NumberCellState();
}
class _NumberCellState extends GridEditableTextCell<MobileNumberCell> {
late final NumberCellBloc _cellBloc;
late final TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCellBloc(cellController: cellController)
..add(const NumberCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.cellContent);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: MultiBlocListener(
listeners: [
BlocListener<NumberCellBloc, NumberCellState>(
listenWhen: (p, c) => p.cellContent != c.cellContent,
listener: (context, state) => _controller.text = state.cellContent,
),
],
child: TextField(
controller: _controller,
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15),
maxLines: 1,
decoration: InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: widget.hintText,
contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
isCollapsed: true,
),
onTapOutside: (event) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
Future<void> focusChanged() async {
if (mounted &&
_cellBloc.isClosed == false &&
_controller.text != _cellBloc.state.cellContent) {
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
}
}
@override
String? onCopy() => _cellBloc.state.cellContent;
}

View File

@ -1,90 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileTextCell extends GridCellWidget {
MobileTextCell({
super.key,
required this.cellControllerBuilder,
GridCellStyle? style,
}) {
if (style != null) {
cellStyle = (style as GridTextCellStyle);
} else {
cellStyle = const GridTextCellStyle();
}
}
final CellControllerBuilder cellControllerBuilder;
late final GridTextCellStyle cellStyle;
@override
GridEditableTextCell<MobileTextCell> createState() => _MobileTextCellState();
}
class _MobileTextCellState extends GridEditableTextCell<MobileTextCell> {
late final TextCellBloc _cellBloc;
late final TextEditingController _controller;
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
child: TextField(
controller: _controller,
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15),
maxLines: 1,
decoration: InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: widget.cellStyle.placeholder,
contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
isCollapsed: true,
),
onTapOutside: (event) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
String? onCopy() => _cellBloc.state.content;
@override
Future<void> focusChanged() {
_cellBloc.add(TextCellEvent.updateText(_controller.text));
return super.focusChanged();
}
}

View File

@ -1,65 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileTimestampCell extends GridCellWidget {
MobileTimestampCell({
super.key,
required this.cellControllerBuilder,
});
final CellControllerBuilder cellControllerBuilder;
@override
GridCellState<MobileTimestampCell> createState() => _TimestampCellState();
}
class _TimestampCellState extends GridCellState<MobileTimestampCell> {
late final TimestampCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as TimestampCellController;
_cellBloc = TimestampCellBloc(cellController: cellController)
..add(const TimestampCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
builder: (context, state) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: FlowyText(
state.dateStr,
fontSize: 15,
maxLines: 1,
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
@override
String? onCopy() => _cellBloc.state.dateStr;
@override
void requestBeginFocus() {}
}

View File

@ -1,128 +0,0 @@
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher_string.dart';
class MobileURLCell extends GridCellWidget {
MobileURLCell({
super.key,
required this.cellControllerBuilder,
this.hintText,
});
final CellControllerBuilder cellControllerBuilder;
final String? hintText;
@override
GridCellState<MobileURLCell> createState() => _GridURLCellState();
}
class _GridURLCellState extends GridCellState<MobileURLCell> {
late final URLCellBloc _cellBloc;
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as URLCellController;
_cellBloc = URLCellBloc(cellController: cellController)
..add(const URLCellEvent.initial());
}
@override
Future<void> dispose() async {
_cellBloc.close();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocSelector<URLCellBloc, URLCellState, String>(
selector: (state) => state.content,
builder: (context, content) {
if (content.isEmpty) {
return TextField(
focusNode: _focusNode,
keyboardType: TextInputType.url,
maxLines: 1,
decoration: InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
hintText: widget.hintText,
contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
isCollapsed: true,
),
onTapOutside: (event) =>
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (value) =>
_cellBloc.add(URLCellEvent.updateURL(value)),
);
}
return Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () {
if (content.isEmpty) {
return;
}
final shouldAddScheme = !['http', 'https']
.any((pattern) => content.startsWith(pattern));
final url = shouldAddScheme ? 'http://$content' : content;
canLaunchUrlString(url).then((value) => launchUrlString(url));
},
onLongPress: () => showMobileBottomSheet(
context,
title: LocaleKeys.board_mobile_editURL.tr(),
showHeader: true,
showCloseButton: true,
builder: (_) {
final controller = TextEditingController(text: content);
return TextField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.url,
onEditingComplete: () {
_cellBloc.add(URLCellEvent.updateURL(controller.text));
context.pop();
},
);
},
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Text(
content,
maxLines: 1,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
decoration: TextDecoration.underline,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
);
},
),
);
}
@override
void requestBeginFocus() {
_focusNode.requestFocus();
}
}

View File

@ -77,7 +77,7 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
}
if (newField.type != widget.field.fieldType) {
await fieldService.updateFieldType(fieldType: newField.type);
await fieldService.updateType(fieldType: newField.type);
}
final data = newField.getTypeOptionData();

View File

@ -1,77 +1,36 @@
part of 'cell_service.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
typedef CellContextByFieldId = LinkedHashMap<String, DatabaseCellContext>;
import 'cell_controller.dart';
class DatabaseCell {
dynamic object;
DatabaseCell({
required this.object,
});
}
/// Use to index the cell in the grid.
/// We use [fieldId + rowId] to identify the cell.
class CellCacheKey {
final String fieldId;
final RowId rowId;
CellCacheKey({
required this.fieldId,
required this.rowId,
});
}
/// GridCellCache is used to cache cell data of each block.
/// We use GridCellCacheKey to index the cell in the cache.
/// CellMemCache is used to cache cell data of each block.
/// We use CellContext to index the cell in the cache.
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid
/// for more information
class CellMemCache {
final String viewId;
/// fieldId: {cacheKey: GridCell}
/// fieldId: {rowId: cellData}
final Map<String, Map<RowId, dynamic>> _cellByFieldId = {};
CellMemCache({
required this.viewId,
});
CellMemCache();
void removeCellWithFieldId(String fieldId) {
_cellByFieldId.remove(fieldId);
}
void remove(CellCacheKey key) {
final map = _cellByFieldId[key.fieldId];
if (map != null) {
map.remove(key.rowId);
}
void remove(CellContext context) {
_cellByFieldId[context.fieldId]?.remove(context.rowId);
}
void insert<T extends DatabaseCell>(CellCacheKey key, T value) {
var map = _cellByFieldId[key.fieldId];
if (map == null) {
_cellByFieldId[key.fieldId] = {};
map = _cellByFieldId[key.fieldId];
}
map![key.rowId] = value.object;
void insert<T>(CellContext context, T data) {
_cellByFieldId.putIfAbsent(context.fieldId, () => {});
_cellByFieldId[context.fieldId]![context.rowId] = data;
}
T? get<T>(CellCacheKey key) {
final map = _cellByFieldId[key.fieldId];
if (map == null) {
return null;
} else {
final value = map[key.rowId];
if (value is T) {
return value;
} else {
if (value != null) {
Log.error("Expected value type: $T, but receive $value");
}
return null;
}
}
T? get<T>(CellContext context) {
final value = _cellByFieldId[context.fieldId]?[context.rowId];
return value is T ? value : null;
}
Future<void> dispose() async {
void dispose() {
_cellByFieldId.clear();
}
}

View File

@ -1,32 +1,47 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/field_listener.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_meta_listener.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.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:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../field/type_option/type_option_data_parser.dart';
import 'cell_cache.dart';
import 'cell_data_loader.dart';
import 'cell_data_persistence.dart';
import 'cell_listener.dart';
import 'cell_service.dart';
/// IGridCellController is used to manipulate the cell and receive notifications.
/// * Read/Write cell data
part 'cell_controller.freezed.dart';
@freezed
class CellContext with _$CellContext {
const factory CellContext({
required String fieldId,
required RowId rowId,
}) = _DatabaseCellContext;
}
/// [CellController] is used to manipulate the cell and receive notifications.
/// The cell data is stored in the [RowCache]'s [CellMemCache].
///
/// * Read/write cell data
/// * Listen on field/cell notifications.
///
/// Generic T represents the type of the cell data.
/// Generic D represents the type of data that will be saved to the disk
///
// ignore: must_be_immutable
class CellController<T, D> extends Equatable {
DatabaseCellContext _cellContext;
final CellMemCache _cellCache;
final CellCacheKey _cacheKey;
/// T represents the type of the cell data.
/// D represents the type of data that will be saved to the disk.
class CellController<T, D> {
final String viewId;
final CellContext _cellContext;
final FieldController _fieldController;
final RowCache _rowCache;
final CellDataLoader<T> _cellDataLoader;
final CellDataPersistence<D> _cellDataPersistence;
@ -35,48 +50,52 @@ class CellController<T, D> extends Equatable {
SingleFieldListener? _fieldListener;
CellDataNotifier<T?>? _cellDataNotifier;
VoidCallback? _onCellFieldChanged;
void Function(FieldPB field)? _onCellFieldChanged;
VoidCallback? _onRowMetaChanged;
Timer? _loadDataOperation;
Timer? _saveDataOperation;
String get viewId => _cellContext.viewId;
RowId get rowId => _cellContext.rowId;
String get fieldId => _cellContext.fieldInfo.id;
FieldInfo get fieldInfo => _cellContext.fieldInfo;
FieldType get fieldType => _cellContext.fieldInfo.fieldType;
String? get emoji => _cellContext.emoji;
String get fieldId => _cellContext.fieldId;
FieldInfo get fieldInfo => _fieldController.getField(_cellContext.fieldId)!;
FieldType get fieldType =>
_fieldController.getField(_cellContext.fieldId)!.fieldType;
RowMetaPB? get rowMeta => _rowCache.getRow(rowId)?.rowMeta;
String? get icon => rowMeta?.icon;
CellMemCache get _cellCache => _rowCache.cellCache;
CellController({
required DatabaseCellContext cellContext,
required CellMemCache cellCache,
required this.viewId,
required FieldController fieldController,
required CellContext cellContext,
required RowCache rowCache,
required CellDataLoader<T> cellDataLoader,
required CellDataPersistence<D> cellDataPersistence,
}) : _cellContext = cellContext,
_cellCache = cellCache,
}) : _fieldController = fieldController,
_cellContext = cellContext,
_rowCache = rowCache,
_cellDataLoader = cellDataLoader,
_cellDataPersistence = cellDataPersistence,
_rowMetaListener = RowMetaListener(cellContext.rowId),
_fieldListener = SingleFieldListener(fieldId: cellContext.fieldId),
_cacheKey = CellCacheKey(
rowId: cellContext.rowId,
fieldId: cellContext.fieldInfo.id,
) {
_cellDataNotifier = CellDataNotifier(value: _cellCache.get(_cacheKey));
_cellDataNotifier =
CellDataNotifier(value: rowCache.cellCache.get(cellContext)) {
_startListening();
}
/// casting method for painless type coersion
CellController<A, B> as<A, B>() => this as CellController<A, B>;
/// Start listening to backend changes
void _startListening() {
_cellListener = CellListener(
rowId: cellContext.rowId,
fieldId: cellContext.fieldInfo.id,
rowId: _cellContext.rowId,
fieldId: _cellContext.fieldId,
);
/// 1.Listen on user edit event and load the new cell data if needed.
/// For example:
/// user input: 12
/// cell display: $12
// 1. Listen on user edit event and load the new cell data if needed.
// For example:
// user input: 12
// cell display: $12
_cellListener?.start(
onCellChanged: (result) {
result.fold(
@ -86,38 +105,36 @@ class CellController<T, D> extends Equatable {
},
);
/// 2.Listen on the field event and load the cell data if needed.
// 2. Listen on the field event and load the cell data if needed.
_fieldListener?.start(
onFieldChanged: (fieldPB) {
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
/// For example:
/// 12 -> $12
if (_cellDataLoader.reloadOnFieldChanged) {
_cellContext = _cellContext.copyWith(
fieldInfo: _cellContext.fieldInfo.copyWith(field: fieldPB),
);
// reloadOnFieldChanged should be true if you want to reload the cell
// data when the corresponding field is changed.
// For example:
// 12 -> $12
if (_cellDataLoader.reloadOnFieldChange) {
_loadData();
}
_onCellFieldChanged?.call();
_onCellFieldChanged?.call(fieldPB);
},
);
// Only the primary can listen on the row meta changes.
if (_cellContext.fieldInfo.field.isPrimary) {
// 3. If the field is primary listen to row meta changes.
if (fieldInfo.field.isPrimary) {
_rowMetaListener = RowMetaListener(_cellContext.rowId);
_rowMetaListener?.start(
callback: (newRowMeta) {
_cellContext = _cellContext.copyWith(rowMeta: newRowMeta);
_onRowMetaChanged?.call();
},
);
}
}
/// Listen on the cell content or field changes
VoidCallback? startListening({
/// Add a new listener
VoidCallback? addListener({
required void Function(T?) onCellChanged,
void Function(FieldPB field)? onCellFieldChanged,
VoidCallback? onRowMetaChanged,
VoidCallback? onCellFieldChanged,
}) {
_onCellFieldChanged = onCellFieldChanged;
_onRowMetaChanged = onRowMetaChanged;
@ -134,11 +151,11 @@ class CellController<T, D> extends Equatable {
_cellDataNotifier?.removeListener(fn);
}
/// Return the cell data.
/// The cell data will be read from the Cache first, and load from disk if it does not exist.
/// You can set [loadIfNotExist] to false (default is true) to disable loading the cell data.
/// Get the cell data. The cell data will be read from the cache first,
/// and load from disk if it doesn't exist. You can set [loadIfNotExist] to
/// false to disable this behavior.
T? getCellData({bool loadIfNotExist = true}) {
final data = _cellCache.get(_cacheKey);
final data = _cellCache.get(_cellContext);
if (data == null && loadIfNotExist) {
_loadData();
}
@ -147,30 +164,34 @@ class CellController<T, D> extends Equatable {
/// Return the TypeOptionPB that can be parsed into corresponding class using the [parser].
/// [PD] is the type that the parser return.
PD getTypeOption<PD, P extends TypeOptionParser>(
P parser,
) {
return parser.fromBuffer(_cellContext.fieldInfo.field.typeOptionData);
PD getTypeOption<PD, P extends TypeOptionParser>(P parser) {
return parser.fromBuffer(fieldInfo.field.typeOptionData);
}
/// Save the cell data to disk
/// You can set [deduplicate] to true (default is false) to reduce the save operation.
/// It's useful when you call this method when user editing the [TextField].
/// The default debounce interval is 300 milliseconds.
/// Saves the cell data to disk. You can set [debounce] to reduce the amount
/// of save operations, which is useful when editing a [TextField].
Future<void> saveCellData(
D data, {
bool deduplicate = false,
bool debounce = false,
void Function(Option<FlowyError>)? onFinish,
}) async {
_loadDataOperation?.cancel();
if (deduplicate) {
if (debounce) {
_saveDataOperation?.cancel();
_saveDataOperation = Timer(const Duration(milliseconds: 300), () async {
final result = await _cellDataPersistence.save(data);
final result = await _cellDataPersistence.save(
viewId: viewId,
cellContext: _cellContext,
data: data,
);
onFinish?.call(result);
});
} else {
final result = await _cellDataPersistence.save(data);
final result = await _cellDataPersistence.save(
viewId: viewId,
cellContext: _cellContext,
data: data,
);
onFinish?.call(result);
}
}
@ -180,11 +201,13 @@ class CellController<T, D> extends Equatable {
_loadDataOperation?.cancel();
_loadDataOperation = Timer(const Duration(milliseconds: 10), () {
_cellDataLoader.loadData().then((data) {
_cellDataLoader
.loadData(viewId: viewId, cellContext: _cellContext)
.then((data) {
if (data != null) {
_cellCache.insert(_cacheKey, DatabaseCell(object: data));
_cellCache.insert(_cellContext, data);
} else {
_cellCache.remove(_cacheKey);
_cellCache.remove(_cellContext);
}
_cellDataNotifier?.value = data;
});
@ -207,12 +230,6 @@ class CellController<T, D> extends Equatable {
_cellDataNotifier = null;
_onRowMetaChanged = null;
}
@override
List<Object> get props => [
_cellCache.get(_cacheKey) ?? "",
_cellContext.rowId + _cellContext.fieldInfo.id,
];
}
class CellDataNotifier<T> extends ChangeNotifier {

View File

@ -1,12 +1,9 @@
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_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 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'cell_controller.dart';
import 'cell_service.dart';
import 'cell_data_loader.dart';
import 'cell_data_persistence.dart';
typedef TextCellController = CellController<String, String>;
typedef CheckboxCellController = CellController<String, String>;
@ -18,125 +15,109 @@ typedef DateCellController = CellController<DateCellDataPB, String>;
typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
typedef URLCellController = CellController<URLCellDataPB, String>;
class CellControllerBuilder {
final DatabaseCellContext _cellContext;
final CellMemCache _cellCache;
CellControllerBuilder({
required DatabaseCellContext cellContext,
required CellMemCache cellCache,
}) : _cellCache = cellCache,
_cellContext = cellContext;
CellController build() {
switch (_cellContext.fieldType) {
case FieldType.Checkbox:
final cellDataLoader = CellDataLoader(
cellContext: _cellContext,
CellController makeCellController(
DatabaseController databaseController,
CellContext cellContext,
) {
final DatabaseController(:viewId, :rowCache, :fieldController) =
databaseController;
final fieldType = fieldController.getField(cellContext.fieldId)!.fieldType;
switch (fieldType) {
case FieldType.Checkbox:
return TextCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: StringCellDataParser(),
);
return TextCellController(
cellContext: _cellContext,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence:
TextCellDataPersistence(cellContext: _cellContext),
);
case FieldType.DateTime:
final cellDataLoader = CellDataLoader(
cellContext: _cellContext,
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.DateTime:
return DateCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: DateCellDataParser(),
reloadOnFieldChanged: true,
);
return DateCellController(
cellContext: _cellContext,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence:
TextCellDataPersistence(cellContext: _cellContext),
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
final cellDataLoader = CellDataLoader(
cellContext: _cellContext,
reloadOnFieldChange: true,
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return TimestampCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: TimestampCellDataParser(),
reloadOnFieldChanged: true,
);
return TimestampCellController(
cellContext: _cellContext,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence:
TextCellDataPersistence(cellContext: _cellContext),
);
case FieldType.Number:
final cellDataLoader = CellDataLoader(
cellContext: _cellContext,
reloadOnFieldChange: true,
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.Number:
return NumberCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: NumberCellDataParser(),
reloadOnFieldChanged: true,
);
return NumberCellController(
cellContext: _cellContext,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence:
TextCellDataPersistence(cellContext: _cellContext),
);
case FieldType.RichText:
final cellDataLoader = CellDataLoader(
cellContext: _cellContext,
reloadOnFieldChange: true,
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.RichText:
return TextCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: StringCellDataParser(),
);
return TextCellController(
cellContext: _cellContext,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence:
TextCellDataPersistence(cellContext: _cellContext),
);
case FieldType.MultiSelect:
case FieldType.SingleSelect:
final cellDataLoader = CellDataLoader(
cellContext: _cellContext,
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.MultiSelect:
case FieldType.SingleSelect:
return SelectOptionCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: SelectOptionCellDataParser(),
reloadOnFieldChanged: true,
);
return SelectOptionCellController(
cellContext: _cellContext,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence:
TextCellDataPersistence(cellContext: _cellContext),
);
case FieldType.Checklist:
final cellDataLoader = CellDataLoader(
cellContext: _cellContext,
reloadOnFieldChange: true,
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.Checklist:
return ChecklistCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: ChecklistCellDataParser(),
reloadOnFieldChanged: true,
);
return ChecklistCellController(
cellContext: _cellContext,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence:
TextCellDataPersistence(cellContext: _cellContext),
);
case FieldType.URL:
final cellDataLoader = CellDataLoader(
cellContext: _cellContext,
reloadOnFieldChange: true,
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.URL:
return URLCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: URLCellDataParser(),
);
return URLCellController(
cellContext: _cellContext,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
cellDataPersistence:
TextCellDataPersistence(cellContext: _cellContext),
);
}
throw UnimplementedError;
),
cellDataPersistence: TextCellDataPersistence(),
);
}
throw UnimplementedError;
}

View File

@ -1,4 +1,10 @@
part of 'cell_service.dart';
import 'dart:convert';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'cell_controller.dart';
import 'cell_service.dart';
abstract class IGridCellDataConfig {
// The cell data will reload if it receives the field's change notification.
@ -10,24 +16,28 @@ abstract class CellDataParser<T> {
}
class CellDataLoader<T> {
final CellBackendService service = CellBackendService();
final DatabaseCellContext cellContext;
final CellDataParser<T> parser;
final bool reloadOnFieldChanged;
/// Reload the cell data if the field is changed.
final bool reloadOnFieldChange;
CellDataLoader({
required this.cellContext,
required this.parser,
this.reloadOnFieldChanged = false,
this.reloadOnFieldChange = false,
});
Future<T?> loadData() {
final fut = service.getCell(cellContext: cellContext);
return fut.then(
Future<T?> loadData({
required String viewId,
required CellContext cellContext,
}) {
return CellBackendService.getCell(
viewId: viewId,
cellContext: cellContext,
).then(
(result) => result.fold(
(CellPB cell) {
try {
// Return null the data of the cell is empty.
// Return null if the data of the cell is empty.
if (cell.data.isEmpty) {
return null;
} else {

View File

@ -1,22 +1,30 @@
part of 'cell_service.dart';
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
import 'package:dartz/dartz.dart';
import 'cell_controller.dart';
import 'cell_service.dart';
/// Save the cell data to disk
/// You can extend this class to do custom operations.
abstract class CellDataPersistence<D> {
Future<Option<FlowyError>> save(D data);
Future<Option<FlowyError>> save({
required String viewId,
required CellContext cellContext,
required D data,
});
}
class TextCellDataPersistence implements CellDataPersistence<String> {
final DatabaseCellContext cellContext;
final _cellBackendSvc = CellBackendService();
TextCellDataPersistence({
required this.cellContext,
});
TextCellDataPersistence();
@override
Future<Option<FlowyError>> save(String data) async {
final fut = _cellBackendSvc.updateCell(
Future<Option<FlowyError>> save({
required String viewId,
required CellContext cellContext,
required String data,
}) async {
final fut = CellBackendService.updateCell(
viewId: viewId,
cellContext: cellContext,
data: data,
);

View File

@ -1,11 +1,12 @@
import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
import 'package:flowy_infra/notifier.dart';
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_infra/notifier.dart';
import '../row/row_service.dart';
typedef UpdateFieldNotifiedValue = Either<Unit, FlowyError>;

View File

@ -1,85 +1,36 @@
import 'dart:async';
import 'dart:collection';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:convert' show utf8;
import '../field/field_info.dart';
import '../row/row_service.dart';
part 'cell_service.freezed.dart';
part 'cell_data_loader.dart';
part 'cell_cache.dart';
part 'cell_data_persistence.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'cell_controller.dart';
class CellBackendService {
CellBackendService();
Future<Either<void, FlowyError>> updateCell({
required DatabaseCellContext cellContext,
static Future<Either<void, FlowyError>> updateCell({
required String viewId,
required CellContext cellContext,
required String data,
}) {
final payload = CellChangesetPB.create()
..viewId = cellContext.viewId
final payload = CellChangesetPB()
..viewId = viewId
..fieldId = cellContext.fieldId
..rowId = cellContext.rowId
..cellChangeset = data;
return DatabaseEventUpdateCell(payload).send();
}
Future<Either<CellPB, FlowyError>> getCell({
required DatabaseCellContext cellContext,
static Future<Either<CellPB, FlowyError>> getCell({
required String viewId,
required CellContext cellContext,
}) {
final payload = CellIdPB.create()
..viewId = cellContext.viewId
final payload = CellIdPB()
..viewId = viewId
..fieldId = cellContext.fieldId
..rowId = cellContext.rowId;
return DatabaseEventGetCell(payload).send();
}
}
/// We can locate the cell by using database + rowId + field.id.
@freezed
class DatabaseCellContext with _$DatabaseCellContext {
const factory DatabaseCellContext({
required String viewId,
required RowMetaPB rowMeta,
required FieldInfo fieldInfo,
}) = _DatabaseCellContext;
// ignore: unused_element
const DatabaseCellContext._();
String get rowId => rowMeta.id;
String get fieldId => fieldInfo.field.id;
FieldType get fieldType => fieldInfo.field.fieldType;
ValueKey key() {
return ValueKey("${rowMeta.id}$fieldId${fieldInfo.field.fieldType}");
}
/// Only the primary field can have an emoji.
String? get emoji => fieldInfo.field.isPrimary ? rowMeta.icon : null;
/// Determines whether a database cell context should be visible.
/// It will be visible when the field is not hidden or when hidden fields
/// should be shown.
bool isVisible({bool showHiddenFields = false}) {
return showHiddenFields ||
fieldInfo.visibility != FieldVisibility.AlwaysHidden;
}
}

View File

@ -26,16 +26,11 @@ class SelectOptionCellBackendService {
(result) {
return result.fold(
(option) {
final payload = RepeatedSelectOptionPayload.create()
final payload = RepeatedSelectOptionPayload()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId;
..rowId = rowId..items.add(option);
if (isSelected) {
payload.items.add(option);
} else {
payload.items.add(option);
}
return DatabaseEventInsertOrUpdateSelectOption(payload).send();
},
(r) => right(r),
@ -47,18 +42,19 @@ class SelectOptionCellBackendService {
Future<Either<Unit, FlowyError>> update({
required SelectOptionPB option,
}) {
final payload = RepeatedSelectOptionPayload.create()
final payload = RepeatedSelectOptionPayload()
..items.add(option)
..viewId = viewId
..fieldId = fieldId
..rowId = rowId;
return DatabaseEventInsertOrUpdateSelectOption(payload).send();
}
Future<Either<Unit, FlowyError>> delete({
required Iterable<SelectOptionPB> options,
}) {
final payload = RepeatedSelectOptionPayload.create()
final payload = RepeatedSelectOptionPayload()
..items.addAll(options)
..viewId = viewId
..fieldId = fieldId
@ -68,7 +64,7 @@ class SelectOptionCellBackendService {
}
Future<Either<SelectOptionCellDataPB, FlowyError>> getCellData() {
final payload = CellIdPB.create()
final payload = CellIdPB()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId;
@ -79,23 +75,25 @@ class SelectOptionCellBackendService {
Future<Either<void, FlowyError>> select({
required Iterable<String> optionIds,
}) {
final payload = SelectOptionCellChangesetPB.create()
final payload = SelectOptionCellChangesetPB()
..cellIdentifier = _cellIdentifier()
..insertOptionIds.addAll(optionIds);
return DatabaseEventUpdateSelectOptionCell(payload).send();
}
Future<Either<void, FlowyError>> unSelect({
required Iterable<String> optionIds,
}) {
final payload = SelectOptionCellChangesetPB.create()
final payload = SelectOptionCellChangesetPB()
..cellIdentifier = _cellIdentifier()
..deleteOptionIds.addAll(optionIds);
return DatabaseEventUpdateSelectOptionCell(payload).send();
}
CellIdPB _cellIdentifier() {
return CellIdPB.create()
return CellIdPB()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId;

View File

@ -1,15 +1,9 @@
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/board_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
import 'package:dartz/dartz.dart';
import 'layout/layout_service.dart';

View File

@ -1,12 +1,14 @@
import 'dart:collection';
// TODO(RS): remove dependency on presentation code
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../grid/presentation/widgets/filter/filter_info.dart';
import 'cell/cell_controller.dart';
import 'field/field_info.dart';
import 'row/row_cache.dart';
import 'row/row_service.dart';
@ -18,20 +20,22 @@ typedef OnFiltersChanged = void Function(List<FilterInfo>);
typedef OnSortsChanged = void Function(List<SortInfo>);
typedef OnDatabaseChanged = void Function(DatabasePB);
typedef OnRowsCreated = void Function(List<RowId> ids);
typedef OnRowsCreated = void Function(List<RowId> rowIds);
typedef OnRowsUpdated = void Function(
List<RowId> ids,
List<RowId> rowIds,
ChangedReason reason,
);
typedef OnRowsDeleted = void Function(List<RowId> ids);
typedef OnRowsDeleted = void Function(List<RowId> rowIds);
typedef OnNumOfRowsChanged = void Function(
UnmodifiableListView<RowInfo> rows,
UnmodifiableMapView<RowId, RowInfo> rowByRowId,
UnmodifiableMapView<RowId, RowInfo> rowById,
ChangedReason reason,
);
typedef OnError = void Function(FlowyError);
typedef CellContextByFieldId = LinkedHashMap<String, CellContext>;
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.idle() = _Idle;

View File

@ -55,7 +55,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
emit(state.copyWith(field: fieldController.getField(fieldId)!));
},
switchFieldType: (fieldType) async {
await fieldService.updateFieldType(fieldType: fieldType);
await fieldService.updateType(fieldType: fieldType);
},
renameField: (newName) async {
final result = await fieldService.updateField(name: newName);
@ -70,14 +70,14 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
_logIfError(result);
},
insertLeft: () async {
final result = await fieldService.insertBefore();
final result = await fieldService.createBefore();
result.fold(
(newField) => onFieldInserted?.call(newField.id),
(err) => Log.error("Failed creating field $err"),
);
},
insertRight: () async {
final result = await fieldService.insertAfter();
final result = await fieldService.createAfter();
result.fold(
(newField) => onFieldInserted?.call(newField.id),
(err) => Log.error("Failed creating field $err"),

View File

@ -1,6 +1,6 @@
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/protobuf.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'field_info.freezed.dart';
@freezed

View File

@ -1,12 +1,12 @@
import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:appflowy_backend/log.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
import 'package:flowy_infra/notifier.dart';
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy/core/notification/grid_notification.dart';
import 'package:appflowy_backend/log.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 UpdateFieldNotifiedValue = FieldPB;

View File

@ -5,16 +5,17 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
/// FieldService consists of lots of event functions. We define the events in the backend(Rust),
/// you can find the corresponding event implementation in event_map.rs of the corresponding crate.
///
/// You could check out the rust-lib/flowy-database/event_map.rs for more information.
/// FieldService provides many field-related interfaces event functions. Check out
/// `rust-lib/flowy-database/event_map.rs` for a list of events and their
/// implementations.
class FieldBackendService {
FieldBackendService({required this.viewId, required this.fieldId});
final String viewId;
final String fieldId;
FieldBackendService({required this.viewId, required this.fieldId});
/// Create a field in a database view. The position will only be applicable
/// in this view; for other views it will be appended to the end
static Future<Either<FieldPB, FlowyError>> createField({
required String viewId,
FieldType fieldType = FieldType.RichText,
@ -33,40 +34,7 @@ class FieldBackendService {
return DatabaseEventCreateField(payload).send();
}
Future<Either<FieldPB, FlowyError>> insertBefore({
FieldType fieldType = FieldType.RichText,
String? fieldName,
Uint8List? typeOptionData,
}) {
return createField(
viewId: viewId,
fieldType: fieldType,
fieldName: fieldName,
typeOptionData: typeOptionData,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.Before,
objectId: fieldId,
),
);
}
Future<Either<FieldPB, FlowyError>> insertAfter({
FieldType fieldType = FieldType.RichText,
String? fieldName,
Uint8List? typeOptionData,
}) {
return createField(
viewId: viewId,
fieldType: fieldType,
fieldName: fieldName,
typeOptionData: typeOptionData,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.After,
objectId: fieldId,
),
);
}
/// Reorder a field within a database view
static Future<Either<Unit, FlowyError>> moveField({
required String viewId,
required String fromFieldId,
@ -81,6 +49,7 @@ class FieldBackendService {
return DatabaseEventMoveField(payload).send();
}
/// Delete a field
static Future<Either<Unit, FlowyError>> deleteField({
required String viewId,
required String fieldId,
@ -93,6 +62,7 @@ class FieldBackendService {
return DatabaseEventDeleteField(payload).send();
}
/// Duplicate a field
static Future<Either<Unit, FlowyError>> duplicateField({
required String viewId,
required String fieldId,
@ -105,6 +75,7 @@ class FieldBackendService {
return DatabaseEventDuplicateField(payload).send();
}
/// Update a field's properties
Future<Either<Unit, FlowyError>> updateField({
String? name,
bool? frozen,
@ -124,6 +95,21 @@ class FieldBackendService {
return DatabaseEventUpdateField(payload).send();
}
/// Change a field's type
static Future<Either<Unit, FlowyError>> updateFieldType({
required String viewId,
required String fieldId,
required FieldType fieldType,
}) {
final payload = UpdateFieldTypePayloadPB()
..viewId = viewId
..fieldId = fieldId
..fieldType = fieldType;
return DatabaseEventUpdateFieldType(payload).send();
}
/// Update a field's type option data
static Future<Either<Unit, FlowyError>> updateFieldTypeOption({
required String viewId,
required String fieldId,
@ -137,15 +123,52 @@ class FieldBackendService {
return DatabaseEventUpdateFieldTypeOption(payload).send();
}
Future<Either<Unit, FlowyError>> updateFieldType({
/// Returns the primary field of the view.
static Future<Either<FieldPB, FlowyError>> getPrimaryField({
required String viewId,
}) {
final payload = DatabaseViewIdPB.create()..value = viewId;
return DatabaseEventGetPrimaryField(payload).send();
}
Future<Either<FieldPB, FlowyError>> createBefore({
FieldType fieldType = FieldType.RichText,
String? fieldName,
Uint8List? typeOptionData,
}) {
return createField(
viewId: viewId,
fieldType: fieldType,
fieldName: fieldName,
typeOptionData: typeOptionData,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.Before,
objectId: fieldId,
),
);
}
Future<Either<FieldPB, FlowyError>> createAfter({
FieldType fieldType = FieldType.RichText,
String? fieldName,
Uint8List? typeOptionData,
}) {
return createField(
viewId: viewId,
fieldType: fieldType,
fieldName: fieldName,
typeOptionData: typeOptionData,
position: OrderObjectPositionPB(
position: OrderObjectPositionTypePB.After,
objectId: fieldId,
),
);
}
Future<Either<Unit, FlowyError>> updateType({
required FieldType fieldType,
}) {
final payload = UpdateFieldTypePayloadPB.create()
..viewId = viewId
..fieldId = fieldId
..fieldType = fieldType;
return DatabaseEventUpdateFieldType(payload).send();
return updateFieldType(viewId: viewId, fieldId: fieldId, fieldType: fieldType);
}
Future<Either<Unit, FlowyError>> delete() {
@ -155,12 +178,4 @@ class FieldBackendService {
Future<Either<Unit, FlowyError>> duplicate() {
return duplicateField(viewId: viewId, fieldId: fieldId);
}
/// Returns the primary field of the view.
static Future<Either<FieldPB, FlowyError>> getPrimaryField({
required String viewId,
}) {
final payload = DatabaseViewIdPB.create()..value = viewId;
return DatabaseEventGetPrimaryField(payload).send();
}
}

View File

@ -2,9 +2,7 @@ import 'package:appflowy/plugins/database/application/field/field_listener.dart'
import 'package:appflowy/plugins/database/application/field/field_service.dart';
import 'package:appflowy/plugins/database/application/row/row_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/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -25,11 +23,11 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
_metaListener = RowMetaListener(rowMeta.id),
super(RowBannerState.initial(rowMeta)) {
on<RowBannerEvent>(
(event, emit) async {
(event, emit) {
event.when(
initial: () async {
initial: () {
_loadPrimaryField();
await _listenRowMeteChanged();
_listenRowMetaChanged();
},
didReceiveRowMeta: (RowMetaPB rowMeta) {
emit(state.copyWith(rowMeta: rowMeta));
@ -84,32 +82,24 @@ class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
}
/// Listen the changes of the row meta and then update the banner
Future<void> _listenRowMeteChanged() async {
void _listenRowMetaChanged() {
_metaListener.start(
callback: (rowMeta) {
add(RowBannerEvent.didReceiveRowMeta(rowMeta));
if (!isClosed) {
add(RowBannerEvent.didReceiveRowMeta(rowMeta));
}
},
);
}
/// Update the meta of the row and the view
Future<void> _updateMeta({
String? iconURL,
String? coverURL,
}) async {
// Most of the time, the result is success, so we don't need to handle it.
await _rowBackendSvc
.updateMeta(
Future<void> _updateMeta({String? iconURL, String? coverURL}) async {
final result = await _rowBackendSvc.updateMeta(
iconURL: iconURL,
coverURL: coverURL,
rowId: state.rowMeta.id,
)
.then((result) {
result.fold(
(l) => null,
(err) => Log.error(err),
);
});
);
result.fold((l) => null, (err) => Log.error(err));
}
}
@ -127,13 +117,13 @@ class RowBannerEvent with _$RowBannerEvent {
@freezed
class RowBannerState with _$RowBannerState {
const factory RowBannerState({
ViewPB? view,
FieldPB? primaryField,
required FieldPB? primaryField,
required RowMetaPB rowMeta,
required LoadingState loadingState,
}) = _RowBannerState;
factory RowBannerState.initial(RowMetaPB rowMetaPB) => RowBannerState(
primaryField: null,
rowMeta: rowMetaPB,
loadingState: const LoadingState.loading(),
);

View File

@ -1,13 +1,14 @@
import 'dart:collection';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../cell/cell_service.dart';
import '../cell/cell_cache.dart';
import '../cell/cell_controller.dart';
import '../defines.dart';
import 'row_list.dart';
import 'row_service.dart';
@ -53,12 +54,12 @@ class RowCache {
required this.viewId,
required RowFieldsDelegate fieldsDelegate,
required RowLifeCycle rowLifeCycle,
}) : _cellMemCache = CellMemCache(viewId: viewId),
}) : _cellMemCache = CellMemCache(),
_changedNotifier = RowChangesetNotifier(),
_rowLifeCycle = rowLifeCycle,
_fieldDelegate = fieldsDelegate {
// Listen on the changed of the fields. If the fields changed, we need to
// clear the cell cache with the given field id.
// Listen to field changes. If a field is deleted, we can safely remove the
// cells corresponding to that field from our cache.
fieldsDelegate.onFieldsChanged((fieldInfos) {
for (final fieldInfo in fieldInfos) {
_cellMemCache.removeCellWithFieldId(fieldInfo.id);
@ -79,10 +80,10 @@ class RowCache {
_changedNotifier.receive(const ChangedReason.setInitialRows());
}
Future<void> dispose() async {
void dispose() {
_rowLifeCycle.onRowDisposed();
_changedNotifier.dispose();
await _cellMemCache.dispose();
_cellMemCache.dispose();
}
void applyRowsChanged(RowsChangePB changeset) {
@ -142,7 +143,7 @@ class RowCache {
final List<RowMetaPB> updatedList = [];
for (final updatedRow in updatedRows) {
for (final fieldId in updatedRow.fieldIds) {
final key = CellCacheKey(
final key = CellContext(
fieldId: fieldId,
rowId: updatedRow.rowId,
);
@ -217,11 +218,7 @@ class RowCache {
}
Future<void> _loadRow(RowId rowId) async {
final payload = RowIdPB.create()
..viewId = viewId
..rowId = rowId;
final result = await DatabaseEventGetRowMeta(payload).send();
final result = await RowBackendService.getRow(viewId: viewId, rowId: rowId);
result.fold(
(rowMetaPB) {
final rowInfo = _rowList.get(rowMetaPB.id);
@ -245,13 +242,12 @@ class RowCache {
}
CellContextByFieldId _makeCells(RowMetaPB rowMeta) {
// ignore: prefer_collection_literals
// TODO(RS): no need to use HashMap
final cellContextMap = CellContextByFieldId();
for (final fieldInfo in _fieldDelegate.fieldInfos) {
cellContextMap[fieldInfo.id] = DatabaseCellContext(
rowMeta: rowMeta,
viewId: viewId,
fieldInfo: fieldInfo,
cellContextMap[fieldInfo.id] = CellContext(
rowId: rowMeta.id,
fieldId: fieldInfo.id,
);
}
return cellContextMap;

View File

@ -1,6 +1,8 @@
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:flutter/material.dart';
import '../cell/cell_service.dart';
import '../cell/cell_cache.dart';
import '../defines.dart';
import 'row_cache.dart';
typedef OnRowChanged = void Function(CellContextByFieldId, ChangedReason);

View File

@ -1,5 +1,7 @@
import 'dart:collection';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'row_cache.dart';
import 'row_service.dart';

View File

@ -61,12 +61,15 @@ class RowBackendService {
);
}
Future<Either<OptionalRowPB, FlowyError>> getRow(RowId rowId) {
final payload = RowIdPB.create()
static Future<Either<RowMetaPB, FlowyError>> getRow({
required String viewId,
required String rowId,
}) {
final payload = RowIdPB()
..viewId = viewId
..rowId = rowId;
return DatabaseEventGetRow(payload).send();
return DatabaseEventGetRowMeta(payload).send();
}
Future<Either<RowMetaPB, FlowyError>> getRowMeta(RowId rowId) {

View File

@ -123,7 +123,7 @@ class DatabaseViewCache {
Future<void> dispose() async {
await _databaseViewListener.stop();
await _rowCache.dispose();
_rowCache.dispose();
_callbacks.clear();
}

View File

@ -8,12 +8,12 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_content.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@ -27,9 +27,7 @@ import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../widgets/card/card.dart';
import '../../widgets/card/card_cell_builder.dart';
import '../../widgets/card/cells/card_cell.dart';
import '../../widgets/row/cell_builder.dart';
import '../../widgets/cell/card_cell_builder.dart';
import '../application/board_bloc.dart';
import 'toolbar/board_setting_bar.dart';
@ -126,7 +124,6 @@ class DesktopBoardContent extends StatefulWidget {
}
class _DesktopBoardContentState extends State<DesktopBoardContent> {
final renderHook = RowCardRenderHook<String>();
final ScrollController scrollController = ScrollController();
final AppFlowyBoardScrollController scrollManager =
AppFlowyBoardScrollController();
@ -140,23 +137,6 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
stretchGroupHeight: false,
);
@override
void initState() {
super.initState();
renderHook.addSelectOptionHook((options, groupId, _) {
// The cell should hide if the option id is equal to the groupId.
final isInGroup =
options.where((element) => element.id == groupId).isNotEmpty;
if (isInGroup || options.isEmpty) {
return const SizedBox.shrink();
}
return null;
});
}
@override
void dispose() {
scrollController.dispose();
@ -257,11 +237,10 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
return SizedBox.shrink(key: ObjectKey(groupItem));
}
final cellCache = rowCache.cellCache;
final fieldController = boardBloc.fieldController;
final databaseController = boardBloc.databaseController;
final viewId = boardBloc.viewId;
final cellBuilder = CardCellBuilder<String>(cellCache);
final cellBuilder = CardCellBuilder(databaseController: databaseController);
final isEditing = boardBloc.state.isEditingRow &&
boardBloc.state.editingRow?.row.id == groupItem.row.id;
@ -272,24 +251,22 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
key: ValueKey(groupItemId),
margin: config.cardMargin,
decoration: _makeBoxDecoration(context),
child: RowCard<String>(
child: RowCard(
fieldController: databaseController.fieldController,
rowMeta: rowMeta,
viewId: viewId,
rowCache: rowCache,
cardData: groupData.group.groupId,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: isEditing,
cellBuilder: cellBuilder,
renderHook: renderHook,
openCard: (context) => _openCard(
context: context,
viewId: viewId,
databaseController: databaseController,
groupId: groupData.group.groupId,
fieldController: fieldController,
rowMeta: rowMeta,
rowCache: rowCache,
),
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: desktopBoardCardCellStyleMap(context),
hoverStyle: HoverStyle(
hoverColor: Theme.of(context).brightness == Brightness.light
? const Color(0x0F1F2329)
@ -333,32 +310,30 @@ class _DesktopBoardContentState extends State<DesktopBoardContent> {
void _openCard({
required BuildContext context,
required String viewId,
required DatabaseController databaseController,
required String groupId,
required FieldController fieldController,
required RowMetaPB rowMeta,
required RowCache rowCache,
}) {
final rowInfo = RowInfo(
viewId: viewId,
fields: UnmodifiableListView(fieldController.fieldInfos),
viewId: databaseController.viewId,
fields:
UnmodifiableListView(databaseController.fieldController.fieldInfos),
rowMeta: rowMeta,
rowId: rowMeta.id,
);
final dataController = RowController(
final rowController = RowController(
rowMeta: rowInfo.rowMeta,
viewId: rowInfo.viewId,
rowCache: rowCache,
rowCache: databaseController.rowCache,
groupId: groupId,
);
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
fieldController: fieldController,
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
rowController: dataController,
databaseController: databaseController,
rowController: rowController,
),
);
}

View File

@ -2,17 +2,14 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@ -126,7 +123,6 @@ class HiddenGroupList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = context.read<BoardBloc>();
return BlocBuilder<BoardBloc, BoardState>(
builder: (_, state) => ReorderableListView.builder(
proxyDecorator: (child, index, animation) => Material(
@ -151,7 +147,7 @@ class HiddenGroupList extends StatelessWidget {
child: HiddenGroupCard(
group: state.hiddenGroups[index],
index: index,
bloc: bloc,
bloc: context.read<BoardBloc>(),
),
),
onReorder: (oldIndex, newIndex) {
@ -160,7 +156,9 @@ class HiddenGroupList extends StatelessWidget {
}
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
final toGroupId = state.hiddenGroups[newIndex].groupId;
bloc.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
context
.read<BoardBloc>()
.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
},
),
);
@ -199,13 +197,17 @@ class _HiddenGroupCardState extends State<HiddenGroupCard> {
direction: PopoverDirection.bottomWithCenterAligned,
triggerActions: PopoverTriggerFlags.none,
constraints: const BoxConstraints(maxWidth: 234, maxHeight: 300),
popupBuilder: (popoverContext) => HiddenGroupPopupItemList(
bloc: widget.bloc,
viewId: databaseController.viewId,
groupId: widget.group.groupId,
primaryField: primaryField,
rowCache: databaseController.rowCache,
),
popupBuilder: (popoverContext) {
return BlocProvider.value(
value: context.read<BoardBloc>(),
child: HiddenGroupPopupItemList(
viewId: databaseController.viewId,
groupId: widget.group.groupId,
primaryFieldId: primaryField.id,
rowCache: databaseController.rowCache,
),
);
},
child: HiddenGroupButtonContent(
popoverController: _popoverController,
groupId: widget.group.groupId,
@ -340,111 +342,81 @@ class HiddenGroupCardActions extends StatelessWidget {
class HiddenGroupPopupItemList extends StatelessWidget {
const HiddenGroupPopupItemList({
required this.bloc,
required this.groupId,
required this.viewId,
required this.primaryField,
required this.primaryFieldId,
required this.rowCache,
super.key,
});
final BoardBloc bloc;
final String groupId;
final String viewId;
final FieldInfo primaryField;
final String primaryFieldId;
final RowCache rowCache;
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: bloc,
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
group.groupName,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final group = state.hiddenGroups.firstWhereOrNull(
(g) => g.groupId == groupId,
);
if (group == null) {
return const SizedBox.shrink();
}
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
group.groupName,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
...group.rows.map(
(item) {
final rowController = RowController(
rowMeta: item,
viewId: viewId,
rowCache: rowCache,
);
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, _, __) {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final text = cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
),
...group.rows.map(
(item) {
final rowController = RowController(
rowMeta: item,
viewId: viewId,
rowCache: rowCache,
);
if (text.isEmpty) {
return const SizedBox.shrink();
}
final databaseController =
context.read<BoardBloc>().databaseController;
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(
text,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
return HiddenGroupPopupItem(
cellContext: rowCache.loadCells(item)[primaryFieldId]!,
rowController: rowController,
rowMeta: item,
cellBuilder: CardCellBuilder(
databaseController: databaseController,
),
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (_) {
return RowDetailPage(
databaseController: databaseController,
rowController: rowController,
);
},
);
});
PopoverContainer.of(context).close();
},
);
},
),
];
return HiddenGroupPopupItem(
cellContext: rowCache.loadCells(item)[primaryField.id]!,
primaryField: primaryField,
rowController: rowController,
cellBuilder: CardCellBuilder<String>(rowController.cellCache),
renderHook: renderHook,
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (_) {
return RowDetailPage(
fieldController:
context.read<BoardBloc>().fieldController,
cellBuilder: GridCellBuilder(
cellCache: rowController.cellCache,
),
rowController: rowController,
);
},
);
PopoverContainer.of(context).close();
},
);
},
),
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
},
),
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
},
);
}
}
@ -452,19 +424,17 @@ class HiddenGroupPopupItemList extends StatelessWidget {
class HiddenGroupPopupItem extends StatelessWidget {
const HiddenGroupPopupItem({
super.key,
required this.rowMeta,
required this.cellContext,
required this.onPressed,
required this.cellBuilder,
required this.rowController,
required this.primaryField,
required this.renderHook,
});
final DatabaseCellContext cellContext;
final FieldInfo primaryField;
final RowMetaPB rowMeta;
final CellContext cellContext;
final RowController rowController;
final CardCellBuilder cellBuilder;
final RowCardRenderHook<String> renderHook;
final VoidCallback onPressed;
@override
@ -473,13 +443,24 @@ class HiddenGroupPopupItem extends StatelessWidget {
height: 26,
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
text: cellBuilder.buildCell(
text: cellBuilder.build(
cellContext: cellContext,
renderHook: renderHook,
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
styleMap: {FieldType.RichText: _titleCellStyle(context)},
hasNotes: !rowMeta.isDocumentEmpty,
),
onTap: onPressed,
),
);
}
TextCardCellStyle _titleCellStyle(BuildContext context) {
return TextCardCellStyle(
padding: EdgeInsets.zero,
textStyle: Theme.of(context).textTheme.bodyMedium!,
titleTextStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: 11, overflow: TextOverflow.ellipsis),
);
}
}

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/cell/cell_cache.dart';
import 'package:appflowy/plugins/database/application/defines.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';

View File

@ -1,6 +1,8 @@
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:bloc/bloc.dart';
@ -10,10 +12,12 @@ part 'calendar_event_editor_bloc.freezed.dart';
class CalendarEventEditorBloc
extends Bloc<CalendarEventEditorEvent, CalendarEventEditorState> {
final FieldController fieldController;
final RowController rowController;
final CalendarLayoutSettingPB layoutSettings;
CalendarEventEditorBloc({
required this.fieldController,
required this.rowController,
required this.layoutSettings,
}) : super(CalendarEventEditorState.initial()) {
@ -21,21 +25,18 @@ class CalendarEventEditorBloc
await event.when(
initial: () {
_startListening();
final primaryFieldId = fieldController.fieldInfos
.firstWhere((fieldInfo) => fieldInfo.isPrimary)
.id;
final cells = rowController
.loadData()
.values
.where(
(cellContext) =>
cellContext.isVisible() ||
cellContext.fieldId == layoutSettings.fieldId ||
cellContext.fieldInfo.isPrimary,
_filterCellContext(cellContext, primaryFieldId),
)
.toList();
if (!isClosed) {
add(
CalendarEventEditorEvent.didReceiveCellDatas(cells),
);
}
add(CalendarEventEditorEvent.didReceiveCellDatas(cells));
},
didReceiveCellDatas: (cells) {
emit(state.copyWith(cells: cells));
@ -54,23 +55,32 @@ class CalendarEventEditorBloc
void _startListening() {
rowController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
final cellData = cells.values
.where(
(cellContext) =>
cellContext.isVisible() ||
cellContext.fieldId == layoutSettings.fieldId ||
cellContext.fieldInfo.isPrimary,
)
.toList();
add(
CalendarEventEditorEvent.didReceiveCellDatas(cellData),
);
if (isClosed) {
return;
}
final primaryFieldId = fieldController.fieldInfos
.firstWhere((fieldInfo) => fieldInfo.isPrimary)
.id;
final cellData = cells.values
.where(
(cellContext) => _filterCellContext(cellContext, primaryFieldId),
)
.toList();
add(CalendarEventEditorEvent.didReceiveCellDatas(cellData));
},
);
}
bool _filterCellContext(CellContext cellContext, String primaryFieldId) {
return fieldController
.getField(cellContext.fieldId)!
.fieldSettings!
.visibility
.isVisibleState() ||
cellContext.fieldId == layoutSettings.fieldId ||
cellContext.fieldId == primaryFieldId;
}
@override
Future<void> close() async {
rowController.dispose();
@ -82,7 +92,7 @@ class CalendarEventEditorBloc
class CalendarEventEditorEvent with _$CalendarEventEditorEvent {
const factory CalendarEventEditorEvent.initial() = _Initial;
const factory CalendarEventEditorEvent.didReceiveCellDatas(
List<DatabaseCellContext> cells,
List<CellContext> cells,
) = _DidReceiveCellDatas;
const factory CalendarEventEditorEvent.delete() = _Delete;
}
@ -90,9 +100,9 @@ class CalendarEventEditorEvent with _$CalendarEventEditorEvent {
@freezed
class CalendarEventEditorState with _$CalendarEventEditorState {
const factory CalendarEventEditorState({
required List<DatabaseCellContext> cells,
required List<CellContext> cells,
}) = _CalendarEventEditorState;
factory CalendarEventEditorState.initial() =>
CalendarEventEditorState(cells: List.empty());
const CalendarEventEditorState(cells: []);
}

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/cell/cell_cache.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';

View File

@ -78,54 +78,58 @@ class CalendarDayCard extends StatelessWidget {
],
);
return MouseRegion(
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
opaque: false,
hitTestBehavior: HitTestBehavior.translucent,
child: GestureDetector(
onDoubleTap: () => onCreateEvent(date),
onTap: PlatformExtension.isMobile
? () => _mobileOnTap(context)
: null,
behavior: HitTestBehavior.deferToChild,
child: Container(
color: date.isWeekend
? AFThemeExtension.of(context).calendarWeekendBGColor
: Colors.transparent,
child: DragTarget<CalendarDayEvent>(
builder: (context, candidate, __) {
return Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: candidate.isEmpty
? null
: hoverBackgroundColor,
border: _borderFromPosition(context, position),
),
padding: const EdgeInsets.only(top: 5.0),
child: child,
return Stack(
children: [
GestureDetector(
onDoubleTap: () => onCreateEvent(date),
onTap: PlatformExtension.isMobile
? () => _mobileOnTap(context)
: null,
child: Container(
decoration: BoxDecoration(
color: date.isWeekend
? AFThemeExtension.of(context).calendarWeekendBGColor
: Colors.transparent,
border: _borderFromPosition(context, position),
),
),
),
DragTarget<CalendarDayEvent>(
hitTestBehavior: HitTestBehavior.translucent,
builder: (context, candidate, __) {
return Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
color:
candidate.isEmpty ? null : hoverBackgroundColor,
padding: const EdgeInsets.only(top: 5.0),
child: child,
),
if (candidate.isEmpty && !PlatformExtension.isMobile)
NewEventButton(
onCreate: () => onCreateEvent(date),
),
if (candidate.isEmpty && !PlatformExtension.isMobile)
NewEventButton(onCreate: () => onCreateEvent(date)),
],
);
},
onAcceptWithDetails: (details) {
final event = details.data;
if (event.date == date) {
return;
}
],
);
},
onAcceptWithDetails: (details) {
final event = details.data;
if (event.date != date) {
context
.read<CalendarBloc>()
.add(CalendarEvent.moveEvent(event, date));
},
),
}
},
),
),
MouseRegion(
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
opaque: false,
hitTestBehavior: HitTestBehavior.translucent,
),
],
);
},
);

View File

@ -1,19 +1,11 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/card_cell.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/number_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/url_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
@ -47,17 +39,14 @@ class EventCard extends StatefulWidget {
}
class _EventCardState extends State<EventCard> {
late final PopoverController _popoverController;
final PopoverController _popoverController = PopoverController();
String get viewId => widget.databaseController.viewId;
RowCache get rowCache => widget.databaseController.rowCache;
FieldController get fieldController =>
widget.databaseController.fieldController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
if (widget.autoEdit) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_popoverController.show();
@ -74,24 +63,19 @@ class _EventCardState extends State<EventCard> {
if (rowInfo == null) {
return const SizedBox.shrink();
}
final styles = <FieldType, CardCellStyle>{
FieldType.Number: NumberCardCellStyle(10),
FieldType.URL: URLCardCellStyle(10),
};
final cellBuilder = CardCellBuilder<CalendarDayEvent>(
rowCache.cellCache,
styles: styles,
);
final renderHook = _calendarEventCardRenderHook(context);
Widget card = RowCard<CalendarDayEvent>(
final cellBuilder = CardCellBuilder(
databaseController: widget.databaseController,
);
Widget card = RowCard(
// Add the key here to make sure the card is rebuilt when the cells
// in this row are updated.
key: ValueKey(widget.event.eventId),
fieldController: widget.databaseController.fieldController,
rowMeta: rowInfo.rowMeta,
viewId: viewId,
rowCache: rowCache,
cardData: widget.event,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) {
@ -109,8 +93,8 @@ class _EventCardState extends State<EventCard> {
}
},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: desktopCalendarCardCellStyleMap(context),
showAccessory: false,
cellPadding: EdgeInsets.zero,
cardPadding: const EdgeInsets.all(6),
hoverStyle: HoverStyle(
hoverColor: Theme.of(context).brightness == Brightness.light
@ -119,7 +103,6 @@ class _EventCardState extends State<EventCard> {
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
),
),
renderHook: renderHook,
onStartEditing: () {},
onEndEditing: () {},
);
@ -171,35 +154,29 @@ class _EventCardState extends State<EventCard> {
return const SizedBox.shrink();
}
return CalendarEventEditor(
fieldController: fieldController,
rowCache: rowCache,
databaseController: widget.databaseController,
rowMeta: widget.event.event.rowMeta,
viewId: viewId,
layoutSettings: settings,
);
},
child: Padding(
child: Container(
padding: widget.padding,
child: DecoratedBox(
decoration: decoration,
child: card,
),
decoration: decoration,
child: card,
),
);
if (widget.isDraggable) {
return Draggable<CalendarDayEvent>(
data: widget.event,
feedback: ConstrainedBox(
feedback: Container(
constraints: BoxConstraints(
maxWidth: widget.constraints.maxWidth - 8.0,
),
decoration: decoration,
child: Opacity(
opacity: 0.6,
child: DecoratedBox(
decoration: decoration,
child: card,
),
child: card,
),
),
child: card,
@ -208,117 +185,4 @@ class _EventCardState extends State<EventCard> {
return card;
}
RowCardRenderHook<CalendarDayEvent> _calendarEventCardRenderHook(
BuildContext context,
) {
final renderHook = RowCardRenderHook<CalendarDayEvent>();
renderHook.addTextCellHook((cellData, eventData, _) {
return BlocBuilder<TextCellBloc, TextCellState>(
builder: (context, state) {
final isTitle =
context.read<TextCellBloc>().cellController.fieldInfo.isPrimary;
final text = isTitle && cellData.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: cellData;
if (text.isEmpty) {
return const SizedBox.shrink();
}
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(
text,
textAlign: TextAlign.left,
fontSize: isTitle ? 11 : 10,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
);
});
renderHook.addDateCellHook((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.date,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
if (cellData.includeTime)
Flexible(
child: FlowyText.regular(
cellData.time,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
});
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();
}
final children = selectedOptions.map(
(option) {
return SelectOptionTag(
option: option,
fontSize: 9,
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
);
},
).toList();
return IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: SizedBox.expand(
child: Wrap(spacing: 4, runSpacing: 4, children: children),
),
),
);
});
return renderHook;
}
}

View File

@ -1,15 +1,19 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/calendar/application/calendar_event_editor_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
@ -19,29 +23,30 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CalendarEventEditor extends StatelessWidget {
final DatabaseController databaseController;
final RowController rowController;
final FieldController fieldController;
final CalendarLayoutSettingPB layoutSettings;
final GridCellBuilder cellBuilder;
final EditableCellBuilder cellBuilder;
CalendarEventEditor({
super.key,
required RowCache rowCache,
required RowMetaPB rowMeta,
required String viewId,
required this.layoutSettings,
required this.fieldController,
required this.databaseController,
}) : rowController = RowController(
rowMeta: rowMeta,
viewId: viewId,
rowCache: rowCache,
viewId: databaseController.viewId,
rowCache: databaseController.rowCache,
),
cellBuilder = GridCellBuilder(cellCache: rowCache.cellCache);
cellBuilder = EditableCellBuilder(
databaseController: databaseController,
);
@override
Widget build(BuildContext context) {
return BlocProvider<CalendarEventEditorBloc>(
create: (context) => CalendarEventEditorBloc(
fieldController: databaseController.fieldController,
rowController: rowController,
layoutSettings: layoutSettings,
)..add(const CalendarEventEditorEvent.initial()),
@ -50,10 +55,11 @@ class CalendarEventEditor extends StatelessWidget {
children: [
EventEditorControls(
rowController: rowController,
fieldController: fieldController,
databaseController: databaseController,
),
Flexible(
child: EventPropertyList(
fieldController: databaseController.fieldController,
dateFieldId: layoutSettings.fieldId,
cellBuilder: cellBuilder,
),
@ -68,11 +74,11 @@ class EventEditorControls extends StatelessWidget {
const EventEditorControls({
super.key,
required this.rowController,
required this.fieldController,
required this.databaseController,
});
final RowController rowController;
final FieldController fieldController;
final DatabaseController databaseController;
@override
Widget build(BuildContext context) {
@ -85,9 +91,13 @@ class EventEditorControls extends StatelessWidget {
width: 20,
icon: const FlowySvg(FlowySvgs.delete_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () => context
.read<CalendarEventEditorBloc>()
.add(const CalendarEventEditorEvent.delete()),
onPressed: () async {
final result = await RowBackendService.deleteRow(
rowController.viewId,
rowController.rowId,
);
result.fold((l) => null, (err) => Log.error(err));
},
),
const HSpace(8.0),
FlowyIconButton(
@ -100,10 +110,7 @@ class EventEditorControls extends StatelessWidget {
context: context,
builder: (BuildContext context) {
return RowDetailPage(
fieldController: fieldController,
cellBuilder: GridCellBuilder(
cellCache: rowController.cellCache,
),
databaseController: databaseController,
rowController: rowController,
);
},
@ -117,10 +124,13 @@ class EventEditorControls extends StatelessWidget {
}
class EventPropertyList extends StatelessWidget {
final FieldController fieldController;
final String dateFieldId;
final GridCellBuilder cellBuilder;
final EditableCellBuilder cellBuilder;
const EventPropertyList({
super.key,
required this.fieldController,
required this.dateFieldId,
required this.cellBuilder,
});
@ -129,11 +139,14 @@ class EventPropertyList extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<CalendarEventEditorBloc, CalendarEventEditorState>(
builder: (context, state) {
final reorderedList = List<DatabaseCellContext>.from(state.cells)
..retainWhere((cell) => !cell.fieldInfo.isPrimary);
final primaryFieldId = fieldController.fieldInfos
.firstWhereOrNull((fieldInfo) => fieldInfo.isPrimary)!
.id;
final reorderedList = List<CellContext>.from(state.cells)
..retainWhere((cell) => cell.fieldId != primaryFieldId);
final primaryCellContext =
state.cells.firstWhereOrNull((cell) => cell.fieldInfo.isPrimary);
final primaryCellContext = state.cells
.firstWhereOrNull((cell) => cell.fieldId == primaryFieldId);
final dateFieldIndex =
reorderedList.indexWhere((cell) => cell.fieldId == dateFieldId);
@ -143,25 +156,20 @@ class EventPropertyList extends StatelessWidget {
reorderedList.insert(0, reorderedList.removeAt(dateFieldIndex));
final children = <Widget>[
final children = [
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: cellBuilder.build(
child: cellBuilder.buildCustom(
primaryCellContext,
style: GridTextCellStyle(
cellPadding: EdgeInsets.zero,
placeholder: LocaleKeys.calendar_defaultNewCalendarTitle.tr(),
textStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontSize: 11, overflow: TextOverflow.ellipsis),
autofocus: true,
useRoundedBorder: true,
),
skinMap: EditableCellSkinMap(textSkin: _TitleTextCellSkin()),
),
),
...reorderedList.map(
(cell) => PropertyCell(cellContext: cell, cellBuilder: cellBuilder),
(cellContext) => PropertyCell(
fieldController: fieldController,
cellContext: cellContext,
cellBuilder: cellBuilder,
),
),
];
@ -176,9 +184,12 @@ class EventPropertyList extends StatelessWidget {
}
class PropertyCell extends StatefulWidget {
final DatabaseCellContext cellContext;
final GridCellBuilder cellBuilder;
final FieldController fieldController;
final CellContext cellContext;
final EditableCellBuilder cellBuilder;
const PropertyCell({
required this.fieldController,
required this.cellContext,
required this.cellBuilder,
super.key,
@ -191,14 +202,16 @@ class PropertyCell extends StatefulWidget {
class _PropertyCellState extends State<PropertyCell> {
@override
Widget build(BuildContext context) {
final style = _customCellStyle(widget.cellContext.fieldType);
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
final fieldInfo =
widget.fieldController.getField(widget.cellContext.fieldId)!;
final cell = widget.cellBuilder
.buildStyled(widget.cellContext, EditableCellStyle.desktopRowDetail);
final gesture = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(
fieldType: widget.cellContext.fieldType,
fieldType: fieldInfo.fieldType,
child: cell,
),
);
@ -218,14 +231,14 @@ class _PropertyCellState extends State<PropertyCell> {
child: Row(
children: [
FlowySvg(
widget.cellContext.fieldType.icon(),
fieldInfo.fieldType.icon(),
color: Theme.of(context).hintColor,
size: const Size.square(14),
),
const HSpace(4.0),
Expanded(
child: FlowyText.regular(
widget.cellContext.fieldInfo.name,
fieldInfo.name,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
fontSize: 11,
@ -241,58 +254,23 @@ class _PropertyCellState extends State<PropertyCell> {
),
);
}
}
GridCellStyle? _customCellStyle(FieldType fieldType) {
switch (fieldType) {
case FieldType.Checkbox:
return GridCheckboxCellStyle(
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.DateTime:
return DateCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
);
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return TimestampCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
alignment: Alignment.centerLeft,
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
);
case FieldType.MultiSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.Checklist:
return ChecklistCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.Number:
return GridNumberCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
case FieldType.RichText:
return GridTextCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
case FieldType.SingleSelect:
return SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
);
case FieldType.URL:
return GridURLCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
accessoryTypes: [
GridURLCellAccessoryType.copyURL,
GridURLCellAccessoryType.visitURL,
],
);
}
throw UnimplementedError;
class _TitleTextCellSkin extends IEditableTextCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TextCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return FlowyTextField(
controller: textEditingController,
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 14),
focusNode: focusNode,
hintText: LocaleKeys.calendar_defaultNewCalendarTitle.tr(),
onChanged: (text) => bloc.add(TextCellEvent.updateText(text)),
);
}
}

View File

@ -6,7 +6,6 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database/calendar/application/unschedule_event_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
@ -24,9 +23,7 @@ import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../application/row/row_cache.dart';
import '../../application/row/row_controller.dart';
import '../../widgets/row/cell_builder.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
@ -349,26 +346,21 @@ class _CalendarPageState extends State<CalendarPage> {
void showEventDetails({
required BuildContext context,
required DatabaseController databaseController,
required CalendarEventPB event,
required String viewId,
required RowCache rowCache,
required FieldController fieldController,
}) {
final dataController = RowController(
final rowController = RowController(
rowMeta: event.rowMeta,
viewId: viewId,
rowCache: rowCache,
viewId: databaseController.viewId,
rowCache: databaseController.rowCache,
);
FlowyOverlay.show(
context: context,
builder: (BuildContext overlayContext) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: rowCache.cellCache,
),
rowController: dataController,
fieldController: fieldController,
rowController: rowController,
databaseController: databaseController,
);
},
);
@ -508,9 +500,7 @@ class UnscheduleEventsList extends StatelessWidget {
showEventDetails(
context: context,
event: event,
viewId: databaseController.viewId,
rowCache: databaseController.rowCache,
fieldController: databaseController.fieldController,
databaseController: databaseController,
);
PopoverContainer.of(context).close();
}

View File

@ -1,13 +1,14 @@
import 'dart:async';
import 'dart:collection';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/row/row_listener.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:equatable/equatable.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/defines.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:flutter/foundation.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/row/row_cache.dart';
import '../../../application/row/row_controller.dart';
import '../../../application/row/row_service.dart';
@ -15,82 +16,81 @@ import '../../../application/row/row_service.dart';
part 'row_bloc.freezed.dart';
class RowBloc extends Bloc<RowEvent, RowState> {
final FieldController fieldController;
final RowBackendService _rowBackendSvc;
final RowController _dataController;
final RowListener _rowListener;
final RowController _rowController;
final String viewId;
final String rowId;
RowBloc({
required this.fieldController,
required this.rowId,
required this.viewId,
required RowController dataController,
required RowController rowController,
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
_dataController = dataController,
_rowListener = RowListener(rowId),
super(RowState.initial(dataController.loadData())) {
_rowController = rowController,
super(RowState.initial()) {
_dispatch();
_startListening();
_init();
}
@override
Future<void> close() async {
_rowController.dispose();
return super.close();
}
void _dispatch() {
on<RowEvent>(
(event, emit) async {
await event.when(
initial: () async {
await _startListening();
},
event.when(
createRow: () {
_rowBackendSvc.createRowAfter(rowId);
},
didReceiveCells: (cellByFieldId, reason) async {
cellByFieldId
.removeWhere((_, cellContext) => !cellContext.isVisible());
final cells = cellByFieldId.values
.map((e) => GridCellEquatable(e.fieldInfo))
.toList();
didReceiveCells: (CellContextByFieldId cellByFieldId, reason) {
cellByFieldId.removeWhere(
(_, cellContext) => !fieldController
.getField(cellContext.fieldId)!
.fieldSettings!
.visibility
.isVisibleState(),
);
emit(
state.copyWith(
cellByFieldId: cellByFieldId,
cells: UnmodifiableListView(cells),
changeReason: reason,
),
);
},
reloadRow: (DidFetchRowPB row) {
emit(state.copyWith(rowSource: RowSourece.remote(row)));
},
);
},
);
}
@override
Future<void> close() async {
_dataController.dispose();
await _rowListener.stop();
return super.close();
}
Future<void> _startListening() async {
_dataController.addListener(
void _startListening() {
_rowController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
add(RowEvent.didReceiveCells(cells, reason));
}
},
);
}
_rowListener.start(
onRowFetched: (fetchRow) {
if (!isClosed) {
add(RowEvent.reloadRow(fetchRow));
}
},
void _init() {
add(
RowEvent.didReceiveCells(
_rowController.loadData(),
const ChangedReason.setInitialRows(),
),
);
}
}
@freezed
class RowEvent with _$RowEvent {
const factory RowEvent.initial() = _InitialRow;
const factory RowEvent.createRow() = _CreateRow;
const factory RowEvent.reloadRow(DidFetchRowPB row) = _ReloadRow;
const factory RowEvent.didReceiveCells(
CellContextByFieldId cellsByFieldId,
ChangedReason reason,
@ -101,45 +101,13 @@ class RowEvent with _$RowEvent {
class RowState with _$RowState {
const factory RowState({
required CellContextByFieldId cellByFieldId,
required UnmodifiableListView<GridCellEquatable> cells,
required RowSourece rowSource,
ChangedReason? changeReason,
}) = _RowState;
factory RowState.initial(
CellContextByFieldId cellByFieldId,
) {
cellByFieldId.removeWhere((_, cellContext) => !cellContext.isVisible());
factory RowState.initial() {
return RowState(
cellByFieldId: cellByFieldId,
cells: UnmodifiableListView(
cellByFieldId.values
.map((e) => GridCellEquatable(e.fieldInfo))
.toList(),
),
rowSource: const RowSourece.disk(),
cellByFieldId: CellContextByFieldId(),
changeReason: null,
);
}
}
class GridCellEquatable extends Equatable {
final FieldInfo _fieldInfo;
const GridCellEquatable(FieldInfo field) : _fieldInfo = field;
@override
List<Object?> get props => [
_fieldInfo.id,
_fieldInfo.fieldType,
_fieldInfo.field.visibility,
_fieldInfo.fieldSettings?.width,
];
}
@freezed
class RowSourece with _$RowSourece {
const factory RowSourece.disk() = _Disk;
const factory RowSourece.remote(
DidFetchRowPB row,
) = _Remote;
}

View File

@ -1,7 +1,10 @@
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/field_service.dart';
import 'package:appflowy/plugins/database/application/field_settings/field_settings_service.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.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';
@ -10,26 +13,41 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'row_detail_bloc.freezed.dart';
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
final FieldController fieldController;
final RowController rowController;
final List<CellContext> allCells = [];
RowDetailBloc({
required this.fieldController,
required this.rowController,
}) : super(RowDetailState.initial(rowController.loadData())) {
}) : super(RowDetailState.initial()) {
_dispatch();
_startListening();
_init();
}
@override
Future<void> close() async {
rowController.dispose();
return super.close();
}
void _dispatch() {
on<RowDetailEvent>(
(event, emit) async {
await event.when(
initial: () async {
await _startListening();
},
didReceiveCellDatas: (visibleCells, allCells, numHiddenFields) {
didReceiveCellDatas: (visibleCells, numHiddenFields) {
emit(
state.copyWith(
visibleCells: visibleCells,
allCells: allCells,
numHiddenFields: numHiddenFields,
),
);
},
didUpdateFields: (fields) {
emit(state.copyWith(fields: fields));
},
deleteField: (fieldId) async {
final result = await FieldBackendService.deleteField(
viewId: rowController.viewId,
@ -38,36 +56,23 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
result.fold((l) {}, (err) => Log.error(err));
},
toggleFieldVisibility: (fieldId) async {
final fieldInfo = state.allCells
.where((cellContext) => cellContext.fieldId == fieldId)
.first
.fieldInfo;
final fieldVisibility =
fieldInfo.visibility == FieldVisibility.AlwaysShown
? FieldVisibility.AlwaysHidden
: FieldVisibility.AlwaysShown;
final result =
await FieldSettingsBackendService(viewId: rowController.viewId)
.updateFieldSettings(
fieldId: fieldId,
fieldVisibility: fieldVisibility,
);
result.fold(
(l) {},
(err) => Log.error(err),
);
await _toggleFieldVisibility(fieldId, emit);
},
reorderField: (fromIndex, toIndex) async {
await _reorderField(fromIndex, toIndex, emit);
},
toggleHiddenFieldVisibility: () {
final showHiddenFields = !state.showHiddenFields;
final visibleCells = List<DatabaseCellContext>.from(state.allCells);
visibleCells.retainWhere(
(cellContext) =>
!cellContext.fieldInfo.isPrimary &&
cellContext.isVisible(showHiddenFields: showHiddenFields),
final visibleCells = List<CellContext>.from(
allCells.where((cellContext) {
final fieldInfo = fieldController.getField(cellContext.fieldId);
return fieldInfo != null &&
!fieldInfo.isPrimary &&
(fieldInfo.visibility!.isVisibleState() ||
showHiddenFields);
}),
);
emit(
state.copyWith(
showHiddenFields: showHiddenFields,
@ -80,30 +85,27 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
);
}
@override
Future<void> close() async {
rowController.dispose();
return super.close();
}
Future<void> _startListening() async {
void _startListening() {
rowController.addListener(
onRowChanged: (cellMap, reason) {
if (isClosed) {
return;
}
final allCells = cellMap.values.toList();
allCells.clear();
allCells.addAll(cellMap.values);
int numHiddenFields = 0;
final visibleCells = <DatabaseCellContext>[];
for (final cell in allCells) {
final isPrimary = cell.fieldInfo.isPrimary;
final visibleCells = <CellContext>[];
if (cell.isVisible(showHiddenFields: state.showHiddenFields) &&
!isPrimary) {
visibleCells.add(cell);
for (final cellContext in allCells) {
final fieldInfo = fieldController.getField(cellContext.fieldId);
if (fieldInfo == null || fieldInfo.isPrimary) {
continue;
}
if (!cell.isVisible() && !isPrimary) {
final isHidden = !fieldInfo.visibility!.isVisibleState();
if (!isHidden || state.showHiddenFields) {
visibleCells.add(cellContext);
}
if (isHidden) {
numHiddenFields++;
}
}
@ -111,12 +113,57 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
add(
RowDetailEvent.didReceiveCellDatas(
visibleCells,
allCells,
numHiddenFields,
),
);
},
);
fieldController.addListener(
onReceiveFields: (fields) => add(RowDetailEvent.didUpdateFields(fields)),
listenWhen: () => !isClosed,
);
}
void _init() {
allCells.addAll(rowController.loadData().values);
int numHiddenFields = 0;
final visibleCells = <CellContext>[];
for (final cell in allCells) {
final fieldInfo = fieldController.getField(cell.fieldId);
if (fieldInfo == null || fieldInfo.isPrimary) {
continue;
}
final isHidden = !fieldInfo.visibility!.isVisibleState();
if (!isHidden) {
visibleCells.add(cell);
} else {
numHiddenFields++;
}
}
add(
RowDetailEvent.didReceiveCellDatas(
visibleCells,
numHiddenFields,
),
);
add(RowDetailEvent.didUpdateFields(fieldController.fieldInfos));
}
Future<void> _toggleFieldVisibility(
String fieldId,
Emitter<RowDetailState> emit,
) async {
final fieldInfo = fieldController.getField(fieldId)!;
final fieldVisibility = fieldInfo.visibility == FieldVisibility.AlwaysShown
? FieldVisibility.AlwaysHidden
: FieldVisibility.AlwaysShown;
final result =
await FieldSettingsBackendService(viewId: rowController.viewId)
.updateFieldSettings(
fieldId: fieldId,
fieldVisibility: fieldVisibility,
);
result.fold((l) {}, (err) => Log.error(err));
}
Future<void> _reorderField(
@ -130,7 +177,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
final fromId = state.visibleCells[fromIndex].fieldId;
final toId = state.visibleCells[toIndex].fieldId;
final cells = List<DatabaseCellContext>.from(state.visibleCells);
final cells = List<CellContext>.from(state.visibleCells);
cells.insert(toIndex, cells.removeAt(fromIndex));
emit(state.copyWith(visibleCells: cells));
@ -145,54 +192,46 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
@freezed
class RowDetailEvent with _$RowDetailEvent {
const factory RowDetailEvent.initial() = _Initial;
const factory RowDetailEvent.didUpdateFields(List<FieldInfo> fields) =
_DidUpdateFields;
/// Triggered by listeners to update row data
const factory RowDetailEvent.didReceiveCellDatas(
List<CellContext> visibleCells,
int numHiddenFields,
) = _DidReceiveCellDatas;
/// Used to delete a field
const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;
/// Used to show/hide a field
const factory RowDetailEvent.toggleFieldVisibility(String fieldId) =
_ToggleFieldVisibility;
/// Used to reorder a field
const factory RowDetailEvent.reorderField(
int fromIndex,
int toIndex,
) = _ReorderField;
/// Used to hide/show the hidden fields in the row detail page
const factory RowDetailEvent.toggleHiddenFieldVisibility() =
_ToggleHiddenFieldVisibility;
const factory RowDetailEvent.didReceiveCellDatas(
List<DatabaseCellContext> visibleCells,
List<DatabaseCellContext> allCells,
int numHiddenFields,
) = _DidReceiveCellDatas;
}
@freezed
class RowDetailState with _$RowDetailState {
const factory RowDetailState({
required List<DatabaseCellContext> visibleCells,
required List<DatabaseCellContext> allCells,
required List<FieldInfo> fields,
required List<CellContext> visibleCells,
required bool showHiddenFields,
required int numHiddenFields,
}) = _RowDetailState;
factory RowDetailState.initial(CellContextByFieldId cellByFieldId) {
final allCells = cellByFieldId.values.toList();
int numHiddenFields = 0;
final visibleCells = <DatabaseCellContext>[];
for (final cell in allCells) {
final isVisible = cell.isVisible();
final isPrimary = cell.fieldInfo.isPrimary;
if (isVisible && !isPrimary) {
visibleCells.add(cell);
}
if (!isVisible && !isPrimary) {
numHiddenFields++;
}
}
return RowDetailState(
visibleCells: visibleCells,
allCells: allCells,
showHiddenFields: false,
numHiddenFields: numHiddenFields,
);
}
factory RowDetailState.initial() => const RowDetailState(
fields: [],
visibleCells: [],
showHiddenFields: false,
numHiddenFields: 0,
);
}

View File

@ -4,8 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy_backend/log.dart';
@ -115,18 +114,11 @@ class _GridPageState extends State<GridPage> {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<NotificationActionBloc>.value(
value: getIt<NotificationActionBloc>(),
),
BlocProvider<GridBloc>(
create: (context) => GridBloc(
view: widget.view,
databaseController: widget.databaseController,
)..add(const GridEvent.initial()),
),
],
return BlocProvider<GridBloc>(
create: (context) => GridBloc(
view: widget.view,
databaseController: widget.databaseController,
)..add(const GridEvent.initial()),
child: BlocListener<NotificationActionBloc, NotificationActionState>(
listener: (context, state) {
final action = state.action;
@ -182,7 +174,6 @@ class _GridPageState extends State<GridPage> {
return;
}
final fieldController = gridBloc.databaseController.fieldController;
final rowController = RowController(
viewId: widget.view.id,
rowMeta: rowMeta,
@ -192,9 +183,8 @@ class _GridPageState extends State<GridPage> {
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
cellBuilder: GridCellBuilder(cellCache: rowController.cellCache),
databaseController: context.read<GridBloc>().databaseController,
rowController: rowController,
fieldController: fieldController,
),
);
});
@ -288,7 +278,7 @@ class _GridRows extends StatelessWidget {
reorderSingleRow: (reorderRow, rowInfo) => true,
delete: (item) => true,
insert: (item) => true,
orElse: () => false,
orElse: () => true,
),
builder: (context, state) {
final rowInfos = state.rowInfos;
@ -312,15 +302,17 @@ class _GridRows extends StatelessWidget {
GridState state,
List<RowInfo> rowInfos,
) {
final children = rowInfos.mapIndexed((index, rowInfo) {
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}).toList()
..add(const GridRowBottomBar(key: Key('gridFooter')));
final children = [
...rowInfos.mapIndexed((index, rowInfo) {
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}),
const GridRowBottomBar(key: Key('gridFooter')),
];
return ReorderableListView.builder(
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
@ -333,12 +325,11 @@ class _GridRows extends StatelessWidget {
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
if (fromIndex != toIndex) {
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
}
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemCount: children.length,
itemBuilder: (context, index) => children[index],
);
}
@ -350,7 +341,8 @@ class _GridRows extends StatelessWidget {
required bool isDraggable,
Animation<double>? animation,
}) {
final rowCache = context.read<GridBloc>().getRowCache(rowId);
final databaseController = context.read<GridBloc>().databaseController;
final DatabaseController(:viewId, :rowCache) = databaseController;
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
/// Return placeholder widget if the rowMeta is null.
@ -358,9 +350,6 @@ class _GridRows extends StatelessWidget {
Log.warn('RowMeta is null for rowId: $rowId');
return const SizedBox.shrink();
}
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
final rowController = RowController(
viewId: viewId,
rowMeta: rowMeta,
@ -369,20 +358,20 @@ class _GridRows extends StatelessWidget {
final child = GridRow(
key: ValueKey(rowMeta.id),
fieldController: databaseController.fieldController,
rowId: rowId,
viewId: viewId,
index: index,
isDraggable: isDraggable,
dataController: rowController,
cellBuilder: GridCellBuilder(cellCache: rowController.cellCache),
rowController: rowController,
cellBuilder: EditableCellBuilder(databaseController: databaseController),
openDetailPage: (context, cellBuilder) {
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: cellBuilder,
rowController: rowController,
fieldController: fieldController,
databaseController: databaseController,
);
},
);

View File

@ -10,9 +10,7 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.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/field_settings_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';

View File

@ -1,15 +1,15 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/defines.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/mobile_cell_container.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -36,9 +36,8 @@ class MobileGridRow extends StatefulWidget {
}
class _MobileGridRowState extends State<MobileGridRow> {
late final RowBloc _rowBloc;
late final RowController _rowController;
late final GridCellBuilder _cellBuilder;
late final EditableCellBuilder _cellBuilder;
String get viewId => widget.databaseController.viewId;
RowCache get rowCache => widget.databaseController.rowCache;
@ -51,27 +50,28 @@ class _MobileGridRowState extends State<MobileGridRow> {
viewId: viewId,
rowCache: rowCache,
);
_rowBloc = RowBloc(
rowId: widget.rowId,
dataController: _rowController,
viewId: viewId,
)..add(const RowEvent.initial());
_cellBuilder = GridCellBuilder(cellCache: rowCache.cellCache);
_cellBuilder = EditableCellBuilder(
databaseController: widget.databaseController,
);
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _rowBloc,
return BlocProvider(
create: (context) => RowBloc(
fieldController: widget.databaseController.fieldController,
rowId: widget.rowId,
rowController: _rowController,
viewId: viewId,
),
child: BlocBuilder<RowBloc, RowState>(
// The row need to rebuild when the cell count changes.
buildWhen: (p, c) => p.rowSource != c.rowSource,
builder: (context, state) {
return Row(
children: [
SizedBox(width: GridSize.leadingHeaderPadding),
Expanded(
child: RowContent(
fieldController: widget.databaseController.fieldController,
builder: _cellBuilder,
onExpand: () => widget.openDetailPage(context),
),
@ -85,7 +85,6 @@ class _MobileGridRowState extends State<MobileGridRow> {
@override
Future<void> dispose() async {
_rowBloc.close();
_rowController.dispose();
super.dispose();
}
@ -112,10 +111,12 @@ class InsertRowButton extends StatelessWidget {
}
class RowContent extends StatelessWidget {
final FieldController fieldController;
final VoidCallback onExpand;
final GridCellBuilder builder;
final EditableCellBuilder builder;
const RowContent({
super.key,
required this.fieldController,
required this.builder,
required this.onExpand,
});
@ -123,8 +124,6 @@ class RowContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<RowBloc, RowState>(
buildWhen: (previous, current) =>
!listEquals(previous.cells, current.cells),
builder: (context, state) {
return SizedBox(
height: 52,
@ -145,11 +144,14 @@ class RowContent extends StatelessWidget {
CellContextByFieldId cellByFieldId,
) {
return cellByFieldId.values.map(
(cellId) {
final GridCellWidget child = builder.build(cellId);
(cellContext) {
final fieldInfo = fieldController.getField(cellContext.fieldId)!;
final EditableCellWidget child = builder.buildStyled(
cellContext,
EditableCellStyle.mobileGrid,
);
return MobileCellContainer(
isPrimary: cellId.fieldInfo.field.isPrimary,
isPrimary: fieldInfo.field.isPrimary,
onPrimaryFieldCellTap: onExpand,
child: child,
);

View File

@ -1,13 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import "package:appflowy/generated/locale_keys.g.dart";
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/defines.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@ -22,20 +22,22 @@ import '../../layout/sizes.dart';
import 'action.dart';
class GridRow extends StatefulWidget {
final FieldController fieldController;
final RowId viewId;
final RowId rowId;
final RowController dataController;
final GridCellBuilder cellBuilder;
final void Function(BuildContext, GridCellBuilder) openDetailPage;
final RowController rowController;
final EditableCellBuilder cellBuilder;
final void Function(BuildContext, EditableCellBuilder) openDetailPage;
final int? index;
final bool isDraggable;
const GridRow({
super.key,
required this.fieldController,
required this.viewId,
required this.rowId,
required this.dataController,
required this.rowController,
required this.cellBuilder,
required this.openDetailPage,
this.index,
@ -47,58 +49,37 @@ class GridRow extends StatefulWidget {
}
class _GridRowState extends State<GridRow> {
late final RowBloc _rowBloc;
@override
void initState() {
super.initState();
_rowBloc = RowBloc(
rowId: widget.rowId,
dataController: widget.dataController,
viewId: widget.viewId,
)..add(const RowEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _rowBloc,
child: BlocBuilder<RowBloc, RowState>(
// The row need to rebuild when the cell count changes.
buildWhen: (p, c) => p.rowSource != c.rowSource,
builder: (context, state) {
final content = Expanded(
child: RowContent(
builder: widget.cellBuilder,
onExpand: () => widget.openDetailPage(
context,
widget.cellBuilder,
return BlocProvider(
create: (_) => RowBloc(
fieldController: widget.fieldController,
rowId: widget.rowId,
rowController: widget.rowController,
viewId: widget.viewId,
),
child: _RowEnterRegion(
child: Row(
children: [
_RowLeading(
index: widget.index,
isDraggable: widget.isDraggable,
),
Expanded(
child: RowContent(
fieldController: widget.fieldController,
cellBuilder: widget.cellBuilder,
onExpand: () => widget.openDetailPage(
context,
widget.cellBuilder,
),
),
),
);
return _RowEnterRegion(
key: ValueKey(state.rowSource),
child: Row(
children: [
_RowLeading(
index: widget.index,
isDraggable: widget.isDraggable,
),
content,
],
),
);
},
],
),
),
);
}
@override
Future<void> dispose() async {
_rowBloc.close();
super.dispose();
}
}
class _RowLeading extends StatefulWidget {
@ -231,18 +212,18 @@ class _RowMenuButtonState extends State<RowMenuButton> {
class RowContent extends StatelessWidget {
const RowContent({
super.key,
required this.builder,
required this.fieldController,
required this.cellBuilder,
required this.onExpand,
});
final GridCellBuilder builder;
final FieldController fieldController;
final VoidCallback onExpand;
final EditableCellBuilder cellBuilder;
@override
Widget build(BuildContext context) {
return BlocBuilder<RowBloc, RowState>(
buildWhen: (previous, current) =>
!listEquals(previous.cells, current.cells),
builder: (context, state) {
return IntrinsicHeight(
child: Row(
@ -262,20 +243,24 @@ class RowContent extends StatelessWidget {
CellContextByFieldId cellByFieldId,
) {
return cellByFieldId.values.map(
(cellId) {
final GridCellWidget child = builder.build(cellId);
(cellContext) {
final fieldInfo = fieldController.getField(cellContext.fieldId)!;
final EditableCellWidget child = cellBuilder.buildStyled(
cellContext,
EditableCellStyle.desktopGrid,
);
return CellContainer(
width: cellId.fieldInfo.fieldSettings!.width.toDouble(),
isPrimary: cellId.fieldInfo.field.isPrimary,
width: fieldInfo.fieldSettings!.width.toDouble(),
isPrimary: fieldInfo.field.isPrimary,
accessoryBuilder: (buildContext) {
final builder = child.accessoryBuilder;
final List<GridCellAccessoryBuilder> accessories = [];
if (cellId.fieldInfo.field.isPrimary) {
if (fieldInfo.field.isPrimary) {
accessories.add(
GridCellAccessoryBuilder(
builder: (key) => PrimaryCellAccessory(
key: key,
onTapCallback: onExpand,
onTap: onExpand,
isCellEditing: buildContext.isCellEditing,
),
),
@ -326,7 +311,7 @@ class RegionStateNotifier extends ChangeNotifier {
class _RowEnterRegion extends StatefulWidget {
final Widget child;
const _RowEnterRegion({required this.child, super.key});
const _RowEnterRegion({required this.child});
@override
State<_RowEnterRegion> createState() => _RowEnterRegionState();

View File

@ -1,11 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/foundation.dart';
@ -13,26 +15,24 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_bloc.dart';
import 'card_cell_builder.dart';
import 'cells/card_cell.dart';
import '../cell/card_cell_builder.dart';
import '../cell/card_cell_skeleton/card_cell.dart';
import 'container/accessory.dart';
import 'container/card_container.dart';
/// Edit a database row with card style widget
class RowCard<CustomCardData> extends StatefulWidget {
class RowCard extends StatefulWidget {
final FieldController fieldController;
final RowMetaPB rowMeta;
final String viewId;
final String? groupingFieldId;
final String? groupId;
/// Allows passing a custom card data object to the card. The card will be
/// returned in the [CardCellBuilder] and can be used to build the card.
final CustomCardData? cardData;
final bool isEditing;
final RowCache rowCache;
/// The [CardCellBuilder] is used to build the card cells.
final CardCellBuilder<CustomCardData> cellBuilder;
final CardCellBuilder cellBuilder;
/// Called when the user taps on the card.
final void Function(BuildContext) openCard;
@ -43,14 +43,11 @@ class RowCard<CustomCardData> extends StatefulWidget {
/// Called when the user ends editing the card.
final VoidCallback onEndEditing;
/// The [RowCardRenderHook] is used to render the card's cell. Other than
/// using the default cell builder. For example the [SelectOptionCardCell]
final RowCardRenderHook<CustomCardData>? renderHook;
final RowCardStyleConfiguration styleConfiguration;
const RowCard({
super.key,
required this.fieldController,
required this.rowMeta,
required this.viewId,
required this.isEditing,
@ -59,41 +56,36 @@ class RowCard<CustomCardData> extends StatefulWidget {
required this.openCard,
required this.onStartEditing,
required this.onEndEditing,
required this.styleConfiguration,
this.groupingFieldId,
this.groupId,
this.cardData,
this.styleConfiguration = const RowCardStyleConfiguration(
showAccessory: true,
),
this.renderHook,
});
@override
State<RowCard<CustomCardData>> createState() =>
_RowCardState<CustomCardData>();
State<RowCard> createState() => _RowCardState();
}
class _RowCardState<T> extends State<RowCard<T>> {
class _RowCardState extends State<RowCard> {
final popoverController = PopoverController();
late final CardBloc _cardBloc;
late final EditableRowNotifier rowNotifier;
AccessoryType? accessoryType;
@override
void initState() {
super.initState();
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
_cardBloc = CardBloc(
fieldController: widget.fieldController,
viewId: widget.viewId,
groupFieldId: widget.groupingFieldId,
isEditing: widget.isEditing,
rowMeta: widget.rowMeta,
rowCache: widget.rowCache,
)..add(const RowCardEvent.initial());
)..add(const CardEvent.initial());
rowNotifier.isEditing.addListener(() {
if (!mounted) return;
_cardBloc.add(RowCardEvent.setIsEditing(rowNotifier.isEditing.value));
_cardBloc.add(CardEvent.setIsEditing(rowNotifier.isEditing.value));
if (rowNotifier.isEditing.value) {
widget.onStartEditing();
@ -103,11 +95,18 @@ class _RowCardState<T> extends State<RowCard<T>> {
});
}
@override
Future<void> dispose() async {
rowNotifier.dispose();
_cardBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cardBloc,
child: BlocBuilder<CardBloc, RowCardState>(
child: BlocBuilder<CardBloc, CardState>(
buildWhen: (previous, current) {
// Rebuild when:
// 1. If the length of the cells is not the same or isEditing changed
@ -119,59 +118,61 @@ class _RowCardState<T> extends State<RowCard<T>> {
// 2. the content of the cells changed
return !listEquals(previous.cells, current.cells);
},
builder: (context, state) {
if (PlatformExtension.isMobile) {
return GestureDetector(
child: MobileCardContent<T>(
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
renderHook: widget.renderHook,
cardData: widget.cardData,
),
onTap: () => widget.openCard(context),
);
}
builder: (context, state) =>
PlatformExtension.isMobile ? _mobile(state) : _desktop(state),
),
);
}
return AppFlowyPopover(
controller: popoverController,
triggerActions: PopoverTriggerFlags.none,
constraints: BoxConstraints.loose(const Size(140, 200)),
direction: PopoverDirection.rightWithCenterAligned,
popupBuilder: (_) {
return RowActionMenu.board(
viewId: _cardBloc.viewId,
rowId: _cardBloc.rowMeta.id,
groupId: widget.groupId,
);
},
child: RowCardContainer(
buildAccessoryWhen: () => state.isEditing == false,
accessories: [
if (widget.styleConfiguration.showAccessory) ...[
_CardEditOption(rowNotifier: rowNotifier),
const CardMoreOption(),
],
],
openAccessory: _handleOpenAccessory,
openCard: (context) => widget.openCard(context),
child: _CardContent<T>(
rowNotifier: rowNotifier,
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
renderHook: widget.renderHook,
cardData: widget.cardData,
),
),
);
},
Widget _mobile(CardState state) {
return GestureDetector(
onTap: () => widget.openCard(context),
behavior: HitTestBehavior.opaque,
child: MobileCardContent(
rowMeta: state.rowMeta,
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
),
);
}
Widget _desktop(CardState state) {
final accessories = widget.styleConfiguration.showAccessory
? <CardAccessory>[
EditCardAccessory(rowNotifier: rowNotifier),
const MoreCardOptionsAccessory(),
]
: null;
return AppFlowyPopover(
controller: popoverController,
triggerActions: PopoverTriggerFlags.none,
constraints: BoxConstraints.loose(const Size(140, 200)),
direction: PopoverDirection.rightWithCenterAligned,
popupBuilder: (_) {
return RowActionMenu.board(
viewId: _cardBloc.viewId,
rowId: _cardBloc.rowId,
groupId: widget.groupId,
);
},
child: RowCardContainer(
buildAccessoryWhen: () => state.isEditing == false,
accessories: accessories ?? [],
openAccessory: _handleOpenAccessory,
openCard: widget.openCard,
child: _CardContent(
rowMeta: state.rowMeta,
rowNotifier: rowNotifier,
cellBuilder: widget.cellBuilder,
styleConfiguration: widget.styleConfiguration,
cells: state.cells,
),
),
);
}
void _handleOpenAccessory(AccessoryType newAccessoryType) {
accessoryType = newAccessoryType;
switch (newAccessoryType) {
case AccessoryType.edit:
break;
@ -180,118 +181,70 @@ class _RowCardState<T> extends State<RowCard<T>> {
break;
}
}
@override
Future<void> dispose() async {
rowNotifier.dispose();
_cardBloc.close();
super.dispose();
}
}
class _CardContent<CustomCardData> extends StatefulWidget {
class _CardContent extends StatelessWidget {
const _CardContent({
super.key,
required this.rowMeta,
required this.rowNotifier,
required this.cellBuilder,
required this.cells,
required this.cardData,
required this.styleConfiguration,
this.renderHook,
});
final RowMetaPB rowMeta;
final EditableRowNotifier rowNotifier;
final CardCellBuilder<CustomCardData> cellBuilder;
final List<DatabaseCellContext> cells;
final CustomCardData? cardData;
final CardCellBuilder cellBuilder;
final List<CellContext> cells;
final RowCardStyleConfiguration styleConfiguration;
final RowCardRenderHook<CustomCardData>? renderHook;
@override
State<_CardContent<CustomCardData>> createState() =>
_CardContentState<CustomCardData>();
}
class _CardContentState<CustomCardData>
extends State<_CardContent<CustomCardData>> {
final List<EditableCardNotifier> _notifiers = [];
@override
void dispose() {
for (final element in _notifiers) {
element.dispose();
}
_notifiers.clear();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.styleConfiguration.hoverStyle != null) {
return FlowyHover(
style: widget.styleConfiguration.hoverStyle,
buildWhenOnHover: () => !widget.rowNotifier.isEditing.value,
child: Padding(
padding: widget.styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _makeCells(context, widget.cells),
),
),
);
}
return Padding(
padding: widget.styleConfiguration.cardPadding,
final child = Padding(
padding: styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _makeCells(context, widget.cells),
children: _makeCells(context, rowMeta, cells),
),
);
return styleConfiguration.hoverStyle == null
? child
: FlowyHover(
style: styleConfiguration.hoverStyle,
buildWhenOnHover: () => !rowNotifier.isEditing.value,
child: child,
);
}
List<Widget> _makeCells(
BuildContext context,
List<DatabaseCellContext> cells,
RowMetaPB rowMeta,
List<CellContext> cells,
) {
final List<Widget> children = [];
// Remove all the cell listeners.
widget.rowNotifier.unbind();
rowNotifier.unbind();
cells.asMap().forEach((int index, DatabaseCellContext cellContext) {
final isEditing = index == 0 ? widget.rowNotifier.isEditing.value : false;
final cellNotifier = EditableCardNotifier(isEditing: isEditing);
return cells.mapIndexed((int index, CellContext cellContext) {
EditableCardNotifier? cellNotifier;
if (index == 0) {
// Only use the first cell to receive user's input when click the edit
// button
widget.rowNotifier.bindCell(cellContext, cellNotifier);
} else {
_notifiers.add(cellNotifier);
cellNotifier =
EditableCardNotifier(isEditing: rowNotifier.isEditing.value);
rowNotifier.bindCell(cellContext, cellNotifier);
}
final child = Padding(
key: cellContext.key(),
padding: widget.styleConfiguration.cellPadding,
child: widget.cellBuilder.buildCell(
cellContext: cellContext,
cellNotifier: cellNotifier,
renderHook: widget.renderHook,
cardData: widget.cardData,
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
),
return cellBuilder.build(
cellContext: cellContext,
cellNotifier: cellNotifier,
styleMap: styleConfiguration.cellStyleMap,
hasNotes: !rowMeta.isDocumentEmpty,
);
children.add(child);
});
return children;
}).toList();
}
}
class CardMoreOption extends StatelessWidget with CardAccessory {
const CardMoreOption({super.key});
@override
AccessoryType get type => AccessoryType.more;
class MoreCardOptionsAccessory extends StatelessWidget with CardAccessory {
const MoreCardOptionsAccessory({super.key});
@override
Widget build(BuildContext context) {
@ -303,11 +256,15 @@ class CardMoreOption extends StatelessWidget with CardAccessory {
),
);
}
@override
AccessoryType get type => AccessoryType.more;
}
class _CardEditOption extends StatelessWidget with CardAccessory {
class EditCardAccessory extends StatelessWidget with CardAccessory {
final EditableRowNotifier rowNotifier;
const _CardEditOption({
const EditCardAccessory({
super.key,
required this.rowNotifier,
});
@ -330,14 +287,14 @@ class _CardEditOption extends StatelessWidget with CardAccessory {
}
class RowCardStyleConfiguration {
final CardCellStyleMap cellStyleMap;
final bool showAccessory;
final EdgeInsets cellPadding;
final EdgeInsets cardPadding;
final HoverStyle? hoverStyle;
const RowCardStyleConfiguration({
required this.cellStyleMap,
this.showAccessory = true,
this.cellPadding = EdgeInsets.zero,
this.cardPadding = const EdgeInsets.all(8),
this.hoverStyle,
});

View File

@ -1,21 +1,22 @@
import 'dart:collection';
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/defines.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_listener.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:flutter/foundation.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/row/row_cache.dart';
import '../../application/row/row_service.dart';
part 'card_bloc.freezed.dart';
class CardBloc extends Bloc<RowCardEvent, RowCardState> {
final RowMetaPB rowMeta;
class CardBloc extends Bloc<CardEvent, CardState> {
final FieldController fieldController;
final String rowId;
final String? groupFieldId;
final RowBackendService _rowBackendSvc;
final RowCache _rowCache;
final String viewId;
final RowListener _rowListener;
@ -23,21 +24,27 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
VoidCallback? _rowCallback;
CardBloc({
required this.rowMeta,
required this.fieldController,
required this.groupFieldId,
required this.viewId,
required RowMetaPB rowMeta,
required RowCache rowCache,
required bool isEditing,
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
}) : rowId = rowMeta.id,
_rowListener = RowListener(rowMeta.id),
_rowCache = rowCache,
super(
RowCardState.initial(
_makeCells(groupFieldId, rowCache.loadCells(rowMeta)),
CardState.initial(
rowMeta,
_makeCells(
fieldController,
groupFieldId,
rowCache.loadCells(rowMeta),
),
isEditing,
),
) {
on<RowCardEvent>(
on<CardEvent>(
(event, emit) async {
await event.when(
initial: () async {
@ -54,15 +61,8 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
setIsEditing: (bool isEditing) {
emit(state.copyWith(isEditing: isEditing));
},
didReceiveRowMeta: (rowMeta) {
final cells = state.cells
.map(
(cell) => cell.rowMeta.id == rowMeta.id
? cell.copyWith(rowMeta: rowMeta)
: cell,
)
.toList();
emit(state.copyWith(cells: cells));
didUpdateRowMeta: (rowMeta) {
emit(state.copyWith(rowMeta: rowMeta));
},
);
},
@ -75,88 +75,75 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
_rowCache.removeRowListener(_rowCallback!);
_rowCallback = null;
}
await _rowListener.stop();
return super.close();
}
RowInfo rowInfo() {
return RowInfo(
viewId: _rowBackendSvc.viewId,
fields: UnmodifiableListView(
state.cells.map((cell) => cell.fieldInfo).toList(),
),
rowId: rowMeta.id,
rowMeta: rowMeta,
);
}
Future<void> _startListening() async {
_rowCallback = _rowCache.addListener(
rowId: rowMeta.id,
rowId: rowId,
onRowChanged: (cellMap, reason) {
if (!isClosed) {
final cells = _makeCells(groupFieldId, cellMap);
add(RowCardEvent.didReceiveCells(cells, reason));
final cells = _makeCells(fieldController, groupFieldId, cellMap);
add(CardEvent.didReceiveCells(cells, reason));
}
},
);
_rowListener.start(
onMetaChanged: (meta) {
onMetaChanged: (rowMeta) {
if (!isClosed) {
add(RowCardEvent.didReceiveRowMeta(meta));
add(CardEvent.didUpdateRowMeta(rowMeta));
}
},
);
}
}
List<DatabaseCellContext> _makeCells(
List<CellContext> _makeCells(
FieldController fieldController,
String? groupFieldId,
CellContextByFieldId originalCellMap,
CellContextByFieldId cellMap,
) {
final List<DatabaseCellContext> cells = [];
originalCellMap
.removeWhere((fieldId, cellContext) => !cellContext.isVisible());
for (final entry in originalCellMap.entries) {
// Filter out the cell if it's fieldId equal to the groupFieldId
if (groupFieldId != null) {
if (entry.value.fieldId == groupFieldId) {
continue;
}
}
cells.add(entry.value);
}
return cells;
// Only show the non-hidden cells and cells that aren't of the grouping field
cellMap.removeWhere((_, cellContext) {
final fieldInfo = fieldController.getField(cellContext.fieldId);
return fieldInfo == null ||
!fieldInfo.fieldSettings!.visibility.isVisibleState() ||
(groupFieldId != null && cellContext.fieldId == groupFieldId);
});
return cellMap.values.toList();
}
@freezed
class RowCardEvent with _$RowCardEvent {
const factory RowCardEvent.initial() = _InitialRow;
const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing;
const factory RowCardEvent.didReceiveCells(
List<DatabaseCellContext> cells,
class CardEvent with _$CardEvent {
const factory CardEvent.initial() = _InitialRow;
const factory CardEvent.setIsEditing(bool isEditing) = _IsEditing;
const factory CardEvent.didReceiveCells(
List<CellContext> cells,
ChangedReason reason,
) = _DidReceiveCells;
const factory RowCardEvent.didReceiveRowMeta(
RowMetaPB meta,
) = _DidReceiveRowMeta;
const factory CardEvent.didUpdateRowMeta(RowMetaPB rowMeta) =
_DidUpdateRowMeta;
}
@freezed
class RowCardState with _$RowCardState {
const factory RowCardState({
required List<DatabaseCellContext> cells,
class CardState with _$CardState {
const factory CardState({
required List<CellContext> cells,
required RowMetaPB rowMeta,
required bool isEditing,
ChangedReason? changeReason,
}) = _RowCardState;
factory RowCardState.initial(
List<DatabaseCellContext> cells,
factory CardState.initial(
RowMetaPB rowMeta,
List<CellContext> cells,
bool isEditing,
) =>
RowCardState(
CardState(
cells: cells,
rowMeta: rowMeta,
isEditing: isEditing,
);
}

View File

@ -1,218 +0,0 @@
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/card_cells.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/card/cells/timestamp_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import '../../application/cell/cell_service.dart';
import 'cells/card_cell.dart';
import 'cells/checkbox_card_cell.dart';
import 'cells/checklist_card_cell.dart';
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/url_card_cell.dart';
// T represents as the Generic card data
class CardCellBuilder<CustomCardData> {
final CellMemCache cellCache;
final Map<FieldType, CardCellStyle>? styles;
CardCellBuilder(this.cellCache, {this.styles});
Widget buildCell({
CustomCardData? cardData,
required DatabaseCellContext cellContext,
EditableCardNotifier? cellNotifier,
RowCardRenderHook<CustomCardData>? renderHook,
required bool hasNotes,
}) {
final cellControllerBuilder = CellControllerBuilder(
cellContext: cellContext,
cellCache: cellCache,
);
final key = cellContext.key();
final style = styles?[cellContext.fieldType];
return PlatformExtension.isMobile
? _getMobileCardCellWidget(
key: key,
cellContext: cellContext,
cellControllerBuilder: cellControllerBuilder,
style: style,
cardData: cardData,
cellNotifier: cellNotifier,
renderHook: renderHook,
hasNotes: hasNotes,
)
: _getDesktopCardCellWidget(
key: key,
cellContext: cellContext,
cellControllerBuilder: cellControllerBuilder,
style: style,
cardData: cardData,
cellNotifier: cellNotifier,
renderHook: renderHook,
hasNotes: hasNotes,
);
}
Widget _getDesktopCardCellWidget({
required Key key,
required DatabaseCellContext cellContext,
required CellControllerBuilder cellControllerBuilder,
CardCellStyle? style,
CustomCardData? cardData,
EditableCardNotifier? cellNotifier,
RowCardRenderHook<CustomCardData>? renderHook,
required bool hasNotes,
}) {
switch (cellContext.fieldType) {
case FieldType.Checkbox:
return CheckboxCardCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
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],
cellControllerBuilder: cellControllerBuilder,
cardData: cardData,
key: key,
);
case FieldType.MultiSelect:
return SelectOptionCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.MultiSelect],
cellControllerBuilder: cellControllerBuilder,
cardData: cardData,
editableNotifier: cellNotifier,
key: key,
);
case FieldType.Checklist:
return ChecklistCardCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.Number:
return NumberCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.Number],
style: isStyleOrNull<NumberCardCellStyle>(style),
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.RichText:
return TextCardCell<CustomCardData>(
key: key,
style: isStyleOrNull<TextCardCellStyle>(style),
cardData: cardData,
renderHook: renderHook?.renderHook[FieldType.RichText],
cellControllerBuilder: cellControllerBuilder,
editableNotifier: cellNotifier,
showNotes: cellContext.fieldInfo.isPrimary && hasNotes,
);
case FieldType.URL:
return URLCardCell<CustomCardData>(
style: isStyleOrNull<URLCardCellStyle>(style),
cellControllerBuilder: cellControllerBuilder,
key: key,
);
}
throw UnimplementedError;
}
Widget _getMobileCardCellWidget({
required Key key,
required DatabaseCellContext cellContext,
required CellControllerBuilder cellControllerBuilder,
CardCellStyle? style,
CustomCardData? cardData,
EditableCardNotifier? cellNotifier,
RowCardRenderHook<CustomCardData>? renderHook,
required bool hasNotes,
}) {
switch (cellContext.fieldType) {
case FieldType.Checkbox:
return MobileCheckboxCardCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
return MobileDateCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.DateTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.LastEditedTime:
return MobileTimestampCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.LastEditedTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.CreatedTime:
return MobileTimestampCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.CreatedTime],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.SingleSelect:
return MobileSelectOptionCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
cellControllerBuilder: cellControllerBuilder,
cardData: cardData,
key: key,
);
case FieldType.MultiSelect:
return MobileSelectOptionCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.MultiSelect],
cellControllerBuilder: cellControllerBuilder,
cardData: cardData,
key: key,
);
case FieldType.Checklist:
return MobileChecklistCardCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.Number:
return MobileNumberCardCell<CustomCardData>(
renderHook: renderHook?.renderHook[FieldType.Number],
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.RichText:
return MobileTextCardCell<CustomCardData>(
key: key,
cardData: cardData,
renderHook: renderHook?.renderHook[FieldType.RichText],
cellControllerBuilder: cellControllerBuilder,
);
case FieldType.URL:
return MobileURLCardCell<CustomCardData>(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
}
throw UnimplementedError;
}
}

View File

@ -1,187 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy_backend/log.dart';
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:flutter/material.dart';
typedef CellRenderHook<C, CustomCardData> = Widget? Function(
C cellData,
CustomCardData cardData,
BuildContext buildContext,
);
typedef RenderHookByFieldType<C> = Map<FieldType, CellRenderHook<dynamic, C>>;
/// The [RowCardRenderHook] is used to customize the rendering of the
/// card cell. Each cell has its own field type. So the [renderHook]
/// is a map of [FieldType] to [CellRenderHook].
class RowCardRenderHook<CustomCardData> {
final RenderHookByFieldType<CustomCardData> renderHook = {};
RowCardRenderHook();
/// Add render hook for the FieldType.SingleSelect and FieldType.MultiSelect
void addSelectOptionHook(
CellRenderHook<List<SelectOptionPB>, CustomCardData?> hook,
) {
final hookFn = _typeSafeHook<List<SelectOptionPB>>(hook);
renderHook[FieldType.SingleSelect] = hookFn;
renderHook[FieldType.MultiSelect] = hookFn;
}
/// Add a render hook for the [FieldType.RichText]
void addTextCellHook(
CellRenderHook<String, CustomCardData?> hook,
) {
renderHook[FieldType.RichText] = _typeSafeHook<String>(hook);
}
/// Add a render hook for the [FieldType.Number]
void addNumberCellHook(
CellRenderHook<String, CustomCardData?> hook,
) {
renderHook[FieldType.Number] = _typeSafeHook<String>(hook);
}
/// Add a render hook for the [FieldType.Date]
void addDateCellHook(
CellRenderHook<DateCellDataPB, CustomCardData?> hook,
) {
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,
) {
Widget? hookFn(cellData, cardData, buildContext) {
if (cellData == null) {
return null;
}
if (cellData is C) {
return hook(cellData, cardData, buildContext);
} else {
Log.debug("Unexpected cellData type: ${cellData.runtimeType}");
return null;
}
}
return hookFn;
}
}
abstract class CardCellStyle {}
S? isStyleOrNull<S>(CardCellStyle? style) {
if (style is S) {
return style as S;
} else {
return null;
}
}
abstract class CardCell<T, S extends CardCellStyle> extends StatefulWidget {
final T? cardData;
final S? style;
const CardCell({super.key, this.cardData, this.style});
}
class EditableCardNotifier {
final ValueNotifier<bool> isCellEditing;
EditableCardNotifier({bool isEditing = false})
: isCellEditing = ValueNotifier(isEditing);
void dispose() {
isCellEditing.dispose();
}
}
class EditableRowNotifier {
final Map<EditableCellId, EditableCardNotifier> _cells = {};
final ValueNotifier<bool> isEditing;
EditableRowNotifier({required bool isEditing})
: isEditing = ValueNotifier(isEditing);
void bindCell(
DatabaseCellContext cellIdentifier,
EditableCardNotifier notifier,
) {
assert(
_cells.values.isEmpty,
'Only one cell can receive the notification',
);
final id = EditableCellId.from(cellIdentifier);
_cells[id]?.dispose();
notifier.isCellEditing.addListener(() {
isEditing.value = notifier.isCellEditing.value;
});
_cells[EditableCellId.from(cellIdentifier)] = notifier;
}
void becomeFirstResponder() {
if (_cells.values.isEmpty) return;
assert(
_cells.values.length == 1,
'Only one cell can receive the notification',
);
_cells.values.first.isCellEditing.value = true;
}
void resignFirstResponder() {
if (_cells.values.isEmpty) return;
assert(
_cells.values.length == 1,
'Only one cell can receive the notification',
);
_cells.values.first.isCellEditing.value = false;
}
void unbind() {
for (final notifier in _cells.values) {
notifier.dispose();
}
_cells.clear();
}
void dispose() {
unbind();
isEditing.dispose();
}
}
abstract mixin class EditableCell {
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
// the row notifier receive its cells event. For example: begin editing the
// cell or end editing the cell.
//
EditableCardNotifier? get editableNotifier;
}
class EditableCellId {
String fieldId;
RowId rowId;
EditableCellId(this.rowId, this.fieldId);
factory EditableCellId.from(DatabaseCellContext cellIdentifier) =>
EditableCellId(
cellIdentifier.rowId,
cellIdentifier.fieldId,
);
}

View File

@ -1,72 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class CheckboxCardCell extends CardCell {
final CellControllerBuilder cellControllerBuilder;
const CheckboxCardCell({
required this.cellControllerBuilder,
super.key,
});
@override
State<CheckboxCardCell> createState() => _CheckboxCellState();
}
class _CheckboxCellState extends State<CheckboxCardCell> {
late CheckboxCellBloc _cellBloc;
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as CheckboxCellController;
_cellBloc = CheckboxCellBloc(cellController: cellController)
..add(const CheckboxCellEvent.initial());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
buildWhen: (previous, current) =>
previous.isSelected != current.isSelected,
builder: (context, state) {
final icon = FlowySvg(
state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
);
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: CardSizes.cardCellPadding,
child: FlowyIconButton(
iconPadding: EdgeInsets.zero,
icon: icon,
width: 20,
onPressed: () => context
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,50 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../row/cells/checklist_cell/checklist_cell_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class ChecklistCardCell extends CardCell {
final CellControllerBuilder cellControllerBuilder;
const ChecklistCardCell({required this.cellControllerBuilder, super.key});
@override
State<ChecklistCardCell> createState() => _ChecklistCellState();
}
class _ChecklistCellState extends State<ChecklistCardCell> {
late ChecklistCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCellBloc(cellController: cellController);
_cellBloc.add(const ChecklistCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.tasks.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: CardSizes.cardCellPadding,
child: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
),
);
},
),
);
}
}

View File

@ -1,78 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class DateCardCell<CustomCardData> extends CardCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<dynamic, CustomCardData>? renderHook;
const DateCardCell({
required this.cellControllerBuilder,
this.renderHook,
super.key,
});
@override
State<DateCardCell> createState() => _DateCellState();
}
class _DateCellState extends State<DateCardCell> {
late DateCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as DateCellController;
_cellBloc = DateCellBloc(cellController: cellController)
..add(const DateCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<DateCellBloc, DateCellState>(
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: CardSizes.cardCellPadding,
child: FlowyText.regular(
state.dateStr,
fontSize: 11,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,87 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class NumberCardCellStyle extends CardCellStyle {
final double fontSize;
NumberCardCellStyle(this.fontSize);
}
class NumberCardCell<CustomCardData>
extends CardCell<CustomCardData, NumberCardCellStyle> {
final CellRenderHook<String, CustomCardData>? renderHook;
final CellControllerBuilder cellControllerBuilder;
const NumberCardCell({
required this.cellControllerBuilder,
super.cardData,
super.style,
this.renderHook,
super.key,
});
@override
State<NumberCardCell> createState() => _NumberCellState();
}
class _NumberCellState extends State<NumberCardCell> {
late NumberCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as NumberCellController;
_cellBloc = NumberCellBloc(cellController: cellController)
..add(const NumberCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<NumberCellBloc, NumberCellState>(
buildWhen: (previous, current) =>
previous.cellContent != current.cellContent,
builder: (context, state) {
if (state.cellContent.isEmpty) {
return const SizedBox.shrink();
}
final Widget? custom = widget.renderHook?.call(
state.cellContent,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: CardSizes.cardCellPadding,
child: FlowyText.regular(
state.cellContent,
fontSize: widget.style?.fontSize ?? 11,
color: Theme.of(context).hintColor,
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,92 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class SelectOptionCardCellStyle extends CardCellStyle {}
class SelectOptionCardCell<CustomCardData>
extends CardCell<CustomCardData, SelectOptionCardCellStyle>
with EditableCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<List<SelectOptionPB>, CustomCardData>? renderHook;
@override
final EditableCardNotifier? editableNotifier;
SelectOptionCardCell({
required this.cellControllerBuilder,
required super.cardData,
this.renderHook,
this.editableNotifier,
super.key,
});
@override
State<SelectOptionCardCell> createState() => _SelectOptionCellState();
}
class _SelectOptionCellState extends State<SelectOptionCardCell> {
late SelectOptionCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as SelectOptionCellController;
_cellBloc = SelectOptionCellBloc(cellController: cellController)
..add(const SelectOptionCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
buildWhen: (previous, current) {
return previous.selectedOptions != current.selectedOptions;
},
builder: (context, state) {
final Widget? custom = widget.renderHook?.call(
state.selectedOptions,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
final children = state.selectedOptions
.map(
(option) => SelectOptionTag(
option: option,
fontSize: 11,
padding:
const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
),
)
.toList();
return Align(
alignment: AlignmentDirectional.topStart,
child: Padding(
padding: CardSizes.cardCellPadding,
child: Wrap(spacing: 4, runSpacing: 2, children: children),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,221 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../row/cell_builder.dart';
import '../define.dart';
import 'card_cell.dart';
class TextCardCellStyle extends CardCellStyle {
final double fontSize;
TextCardCellStyle(this.fontSize);
}
class TextCardCell<CustomCardData>
extends CardCell<CustomCardData, TextCardCellStyle> with EditableCell {
const TextCardCell({
super.key,
super.cardData,
super.style,
required this.cellControllerBuilder,
this.editableNotifier,
this.renderHook,
this.showNotes = false,
});
@override
final EditableCardNotifier? editableNotifier;
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<String, CustomCardData>? renderHook;
final bool showNotes;
@override
State<TextCardCell> createState() => _TextCellState();
}
class _TextCellState extends State<TextCardCell> {
late TextCellBloc _cellBloc;
late TextEditingController _controller;
bool focusWhenInit = false;
final focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false;
if (focusWhenInit) {
focusNode.requestFocus();
}
// If the focusNode lost its focus, the widget's editableNotifier will
// set to false, which will cause the [EditableRowNotifier] to receive
// end edit event.
focusNode.addListener(() {
if (!focusNode.hasFocus) {
focusWhenInit = false;
widget.editableNotifier?.isCellEditing.value = false;
_cellBloc.add(const TextCellEvent.enableEdit(false));
}
});
_bindEditableNotifier();
super.initState();
}
void _bindEditableNotifier() {
widget.editableNotifier?.isCellEditing.addListener(() {
if (!mounted) return;
final isEditing = widget.editableNotifier?.isCellEditing.value ?? false;
if (isEditing) {
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
}
_cellBloc.add(TextCellEvent.enableEdit(isEditing));
});
}
@override
void didUpdateWidget(covariant oldWidget) {
_bindEditableNotifier();
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocListener<TextCellBloc, TextCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
child: BlocBuilder<TextCellBloc, TextCellState>(
buildWhen: (previous, current) {
if (previous.content != current.content &&
_controller.text == current.content &&
current.enableEdit) {
return false;
}
return previous != current;
},
builder: (context, state) {
// Returns a custom render widget
final Widget? custom = widget.renderHook?.call(
state.content,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
final isTitle =
context.read<TextCellBloc>().cellController.fieldInfo.isPrimary;
if (state.content.isEmpty &&
state.enableEdit == false &&
focusWhenInit == false &&
!isTitle) {
return const SizedBox.shrink();
}
final child = state.enableEdit || focusWhenInit
? _buildTextField()
: _buildText(state, isTitle);
return Padding(
padding: CardSizes.cardCellPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.showNotes) ...[
FlowyTooltip(
message: LocaleKeys.board_notesTooltip.tr(),
child: FlowySvg(
FlowySvgs.notes_s,
color: Theme.of(context).hintColor,
),
),
const HSpace(4),
],
Expanded(child: child),
],
),
);
},
),
),
);
}
Future<void> focusChanged() async {
_cellBloc.add(TextCellEvent.updateText(_controller.text));
}
@override
Future<void> dispose() async {
_cellBloc.close();
_controller.dispose();
focusNode.dispose();
super.dispose();
}
Widget _buildText(TextCellState state, bool isTitle) {
final text = state.content.isEmpty
? LocaleKeys.grid_row_titlePlaceholder.tr()
: state.content;
final color = state.content.isEmpty ? Theme.of(context).hintColor : null;
return FlowyText(
text,
fontSize: _fontSize(isTitle),
fontWeight: _fontWeight(isTitle),
color: color,
maxLines: null, // Enable multiple lines
);
}
double _fontSize(bool isTitle) {
return widget.style?.fontSize ?? (isTitle ? 12 : 11);
}
FontWeight _fontWeight(bool isTitle) {
return isTitle ? FontWeight.w500 : FontWeight.w400;
}
Widget _buildTextField() {
return TextField(
controller: _controller,
focusNode: focusNode,
onChanged: (value) => focusChanged(),
onEditingComplete: () => focusNode.unfocus(),
maxLines: null,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontSize: _fontSize(true)),
decoration: InputDecoration(
contentPadding:
EdgeInsets.symmetric(vertical: CardSizes.cardCellPadding.top),
border: InputBorder.none,
isDense: true,
isCollapsed: true,
hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),
),
);
}
}

View File

@ -1,77 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class TimestampCardCell<CustomCardData> extends CardCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<dynamic, CustomCardData>? renderHook;
const TimestampCardCell({
required this.cellControllerBuilder,
this.renderHook,
super.key,
});
@override
State<TimestampCardCell> createState() => _TimestampCellState();
}
class _TimestampCellState extends State<TimestampCardCell> {
late TimestampCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as TimestampCellController;
_cellBloc = TimestampCellBloc(cellController: cellController)
..add(const TimestampCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox.shrink();
}
final Widget? custom = widget.renderHook?.call(
state.data,
widget.cardData,
context,
);
if (custom != null) {
return custom;
}
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: CardSizes.cardCellPadding,
child: FlowyText.regular(
state.dateStr,
fontSize: 11,
color: Theme.of(context).hintColor,
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,78 +0,0 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../define.dart';
import 'card_cell.dart';
class URLCardCellStyle extends CardCellStyle {
final double fontSize;
URLCardCellStyle(this.fontSize);
}
class URLCardCell<CustomCardData>
extends CardCell<CustomCardData, URLCardCellStyle> {
final CellControllerBuilder cellControllerBuilder;
const URLCardCell({
required this.cellControllerBuilder,
super.style,
super.key,
});
@override
State<URLCardCell> createState() => _URLCellState();
}
class _URLCellState extends State<URLCardCell> {
late URLCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as URLCellController;
_cellBloc = URLCellBloc(cellController: cellController);
_cellBloc.add(const URLCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<URLCellBloc, URLCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox();
}
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: CardSizes.cardCellPadding,
child: RichText(
textAlign: TextAlign.left,
text: TextSpan(
text: state.content,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
fontSize: widget.style?.fontSize ?? 11,
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -1,5 +0,0 @@
import 'package:flutter/widgets.dart';
class CardSizes {
static EdgeInsets get cardCellPadding => const EdgeInsets.all(4);
}

View File

@ -0,0 +1,90 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/widgets.dart';
import 'card_cell_skeleton/card_cell.dart';
import 'card_cell_skeleton/checkbox_card_cell.dart';
import 'card_cell_skeleton/checklist_card_cell.dart';
import 'card_cell_skeleton/date_card_cell.dart';
import 'card_cell_skeleton/number_card_cell.dart';
import 'card_cell_skeleton/select_option_card_cell.dart';
import 'card_cell_skeleton/text_card_cell.dart';
import 'card_cell_skeleton/url_card_cell.dart';
typedef CardCellStyleMap = Map<FieldType, CardCellStyle>;
class CardCellBuilder {
final DatabaseController databaseController;
CardCellBuilder({required this.databaseController});
Widget build({
required CellContext cellContext,
required CardCellStyleMap styleMap,
EditableCardNotifier? cellNotifier,
required bool hasNotes,
}) {
final fieldType = databaseController.fieldController
.getField(cellContext.fieldId)!
.fieldType;
final key = ValueKey(
"${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}",
);
final style = styleMap[fieldType];
return switch (fieldType) {
FieldType.Checkbox => CheckboxCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.Checklist => ChecklistCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.DateTime => DateCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.LastEditedTime || FieldType.CreatedTime => TimestampCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.SingleSelect || FieldType.MultiSelect => SelectOptionCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.Number => NumberCardCell(
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
key: key,
),
FieldType.RichText => TextCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
editableNotifier: cellNotifier,
showNotes: hasNotes,
),
FieldType.URL => URLCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
_ => throw UnimplementedError,
};
}
}

View File

@ -0,0 +1,96 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:flutter/material.dart';
abstract class CardCell<T extends CardCellStyle> extends StatefulWidget {
final T style;
const CardCell({super.key, required this.style});
}
abstract class CardCellStyle {
final EdgeInsetsGeometry padding;
const CardCellStyle({required this.padding});
}
S? isStyleOrNull<S>(CardCellStyle? style) {
if (style is S) {
return style as S;
} else {
return null;
}
}
class EditableCardNotifier {
final ValueNotifier<bool> isCellEditing;
EditableCardNotifier({bool isEditing = false})
: isCellEditing = ValueNotifier(isEditing);
void dispose() {
isCellEditing.dispose();
}
}
class EditableRowNotifier {
final Map<CellContext, EditableCardNotifier> _cells = {};
final ValueNotifier<bool> isEditing;
EditableRowNotifier({required bool isEditing})
: isEditing = ValueNotifier(isEditing);
void bindCell(
CellContext cellIdentifier,
EditableCardNotifier notifier,
) {
assert(
_cells.values.isEmpty,
'Only one cell can receive the notification',
);
_cells[cellIdentifier]?.dispose();
notifier.isCellEditing.addListener(() {
isEditing.value = notifier.isCellEditing.value;
});
_cells[cellIdentifier] = notifier;
}
void becomeFirstResponder() {
if (_cells.values.isEmpty) return;
assert(
_cells.values.length == 1,
'Only one cell can receive the notification',
);
_cells.values.first.isCellEditing.value = true;
}
void resignFirstResponder() {
if (_cells.values.isEmpty) return;
assert(
_cells.values.length == 1,
'Only one cell can receive the notification',
);
_cells.values.first.isCellEditing.value = false;
}
void unbind() {
for (final notifier in _cells.values) {
notifier.dispose();
}
_cells.clear();
}
void dispose() {
unbind();
isEditing.dispose();
}
}
abstract mixin class EditableCell {
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
// the row notifier receive its cells event. For example: begin editing the
// cell or end editing the cell.
//
EditableCardNotifier? get editableNotifier;
}

View File

@ -0,0 +1,88 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class CheckboxCardCellStyle extends CardCellStyle {
final Size iconSize;
final bool showFieldName;
final TextStyle? textStyle;
CheckboxCardCellStyle({
required super.padding,
required this.iconSize,
required this.showFieldName,
this.textStyle,
}) : assert(!showFieldName || showFieldName && textStyle != null);
}
class CheckboxCardCell extends CardCell<CheckboxCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const CheckboxCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<CheckboxCardCell> createState() => _CheckboxCellState();
}
class _CheckboxCellState extends State<CheckboxCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) {
return CheckboxCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const CheckboxCellEvent.initial());
},
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
builder: (context, state) {
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Row(
children: [
FlowyIconButton(
iconPadding: EdgeInsets.zero,
icon: FlowySvg(
state.isSelected
? FlowySvgs.check_filled_s
: FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
size: widget.style.iconSize,
),
width: 20,
onPressed: () => context
.read<CheckboxCellBloc>()
.add(const CheckboxCellEvent.select()),
),
if (widget.style.showFieldName) ...[
const HSpace(6.0),
Text(
state.fieldName,
style: widget.style.textStyle,
),
],
],
),
);
},
),
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class ChecklistCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
ChecklistCardCellStyle({
required super.padding,
required this.textStyle,
});
}
class ChecklistCardCell extends CardCell<ChecklistCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const ChecklistCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<ChecklistCardCell> createState() => _ChecklistCellState();
}
class _ChecklistCellState extends State<ChecklistCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) {
return ChecklistCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const ChecklistCellEvent.initial());
},
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
if (state.tasks.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: widget.style.padding,
child: ChecklistProgressBar(
tasks: state.tasks,
percent: state.percent,
textStyle: widget.style.textStyle,
),
);
},
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/date_cell/date_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class DateCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
DateCardCellStyle({
required super.padding,
required this.textStyle,
});
}
class DateCardCell extends CardCell<DateCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const DateCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<DateCardCell> createState() => _DateCellState();
}
class _DateCellState extends State<DateCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return DateCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const DateCellEvent.initial());
},
child: BlocBuilder<DateCellBloc, DateCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: Alignment.centerLeft,
padding: widget.style.padding,
child: Text(
state.dateStr,
style: widget.style.textStyle,
),
);
},
),
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/number_cell/number_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class NumberCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
const NumberCardCellStyle({
required super.padding,
required this.textStyle,
});
}
class NumberCardCell extends CardCell<NumberCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const NumberCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<NumberCardCell> createState() => _NumberCellState();
}
class _NumberCellState extends State<NumberCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return NumberCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const NumberCellEvent.initial());
},
child: BlocBuilder<NumberCellBloc, NumberCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Text(state.content, style: widget.style.textStyle),
);
},
),
);
}
}

View File

@ -0,0 +1,87 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class SelectOptionCardCellStyle extends CardCellStyle {
final double tagFontSize;
final bool wrap;
final EdgeInsets tagPadding;
SelectOptionCardCellStyle({
required super.padding,
required this.tagFontSize,
required this.wrap,
required this.tagPadding,
});
}
class SelectOptionCardCell extends CardCell<SelectOptionCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const SelectOptionCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<SelectOptionCardCell> createState() => _SelectOptionCellState();
}
class _SelectOptionCellState extends State<SelectOptionCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) {
return SelectOptionCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const SelectOptionCellEvent.initial());
},
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
buildWhen: (previous, current) {
return previous.selectedOptions != current.selectedOptions;
},
builder: (context, state) {
if (state.selectedOptions.isEmpty) {
return const SizedBox.shrink();
}
final children = state.selectedOptions
.map(
(option) => SelectOptionTag(
option: option,
fontSize: widget.style.tagFontSize,
padding: widget.style.tagPadding,
),
)
.toList();
return Container(
alignment: AlignmentDirectional.topStart,
padding: widget.style.padding,
child: widget.style.wrap
? Wrap(spacing: 4, runSpacing: 4, children: children)
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: children,
),
),
);
},
),
);
}
}

View File

@ -0,0 +1,203 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_builder.dart';
import 'card_cell.dart';
class TextCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
final TextStyle titleTextStyle;
final int? maxLines;
TextCardCellStyle({
required super.padding,
required this.textStyle,
required this.titleTextStyle,
this.maxLines = 1,
});
}
class TextCardCell extends CardCell<TextCardCellStyle> with EditableCell {
final DatabaseController databaseController;
final CellContext cellContext;
final bool showNotes;
const TextCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
this.editableNotifier,
this.showNotes = false,
});
@override
final EditableCardNotifier? editableNotifier;
@override
State<TextCardCell> createState() => _TextCellState();
}
class _TextCellState extends State<TextCardCell> {
late final cellBloc = TextCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const TextCellEvent.initial());
late final TextEditingController _textEditingController =
TextEditingController(text: cellBloc.state.content);
final focusNode = SingleListenerFocusNode();
bool focusWhenInit = false;
@override
void initState() {
super.initState();
focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false;
if (focusWhenInit) {
focusNode.requestFocus();
}
// If the focusNode lost its focus, the widget's editableNotifier will
// set to false, which will cause the [EditableRowNotifier] to receive
// end edit event.
focusNode.addListener(() {
if (!focusNode.hasFocus) {
focusWhenInit = false;
widget.editableNotifier?.isCellEditing.value = false;
cellBloc.add(const TextCellEvent.enableEdit(false));
}
});
_bindEditableNotifier();
}
void _bindEditableNotifier() {
widget.editableNotifier?.isCellEditing.addListener(() {
if (!mounted) return;
final isEditing = widget.editableNotifier?.isCellEditing.value ?? false;
if (isEditing) {
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
}
cellBloc.add(TextCellEvent.enableEdit(isEditing));
});
}
@override
void didUpdateWidget(covariant oldWidget) {
_bindEditableNotifier();
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocConsumer<TextCellBloc, TextCellState>(
listener: (context, state) {
if (_textEditingController.text != state.content) {
_textEditingController.text = state.content;
}
},
buildWhen: (previous, current) {
if (previous.content != current.content &&
_textEditingController.text == current.content &&
current.enableEdit) {
return false;
}
return previous != current;
},
builder: (context, state) {
final isTitle = cellBloc.cellController.fieldInfo.isPrimary;
if (state.content.isEmpty &&
state.enableEdit == false &&
focusWhenInit == false &&
!isTitle) {
return const SizedBox.shrink();
}
final child = state.enableEdit || focusWhenInit
? _buildTextField()
: _buildText(state, isTitle);
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (isTitle && widget.showNotes)
FlowyTooltip(
message: LocaleKeys.board_notesTooltip.tr(),
child: FlowySvg(
FlowySvgs.notes_s,
color: Theme.of(context).hintColor,
),
),
Expanded(child: child),
],
);
},
),
);
}
@override
void dispose() {
_textEditingController.dispose();
focusNode.dispose();
cellBloc.close();
super.dispose();
}
Widget _buildText(TextCellState state, bool isTitle) {
final text = state.content.isEmpty
? isTitle
? LocaleKeys.grid_row_titlePlaceholder.tr()
: LocaleKeys.grid_row_textPlaceholder.tr()
: state.content;
final color = state.content.isEmpty ? Theme.of(context).hintColor : null;
final textStyle =
isTitle ? widget.style.titleTextStyle : widget.style.textStyle;
return Padding(
padding: widget.style.padding,
child: Text(
text,
style: textStyle.copyWith(color: color),
maxLines: widget.style.maxLines,
),
);
}
Widget _buildTextField() {
final padding =
widget.style.padding.add(const EdgeInsets.symmetric(vertical: 4.0));
return TextField(
controller: _textEditingController,
focusNode: focusNode,
onChanged: (_) =>
cellBloc.add(TextCellEvent.updateText(_textEditingController.text)),
onEditingComplete: () => focusNode.unfocus(),
maxLines: null,
style: widget.style.titleTextStyle,
decoration: InputDecoration(
contentPadding: padding,
border: InputBorder.none,
isDense: true,
isCollapsed: true,
hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class TimestampCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
TimestampCardCellStyle({
required super.padding,
required this.textStyle,
});
}
class TimestampCardCell extends CardCell<TimestampCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const TimestampCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<TimestampCardCell> createState() => _TimestampCellState();
}
class _TimestampCellState extends State<TimestampCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) {
return TimestampCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const TimestampCellEvent.initial());
},
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Text(
state.dateStr,
style: widget.style.textStyle,
),
);
},
),
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/url_cell/url_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell.dart';
class URLCardCellStyle extends CardCellStyle {
final TextStyle textStyle;
URLCardCellStyle({
required super.padding,
required this.textStyle,
});
}
class URLCardCell extends CardCell<URLCardCellStyle> {
final DatabaseController databaseController;
final CellContext cellContext;
const URLCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
@override
State<URLCardCell> createState() => _URLCellState();
}
class _URLCellState extends State<URLCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return URLCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
)..add(const URLCellEvent.initial());
},
child: BlocBuilder<URLCellBloc, URLCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Text(
state.content,
style: widget.style.textStyle,
),
);
},
),
);
}
}

View File

@ -0,0 +1,77 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import '../card_cell_builder.dart';
import '../card_cell_skeleton/checkbox_card_cell.dart';
import '../card_cell_skeleton/checklist_card_cell.dart';
import '../card_cell_skeleton/date_card_cell.dart';
import '../card_cell_skeleton/number_card_cell.dart';
import '../card_cell_skeleton/select_option_card_cell.dart';
import '../card_cell_skeleton/text_card_cell.dart';
import '../card_cell_skeleton/timestamp_card_cell.dart';
import '../card_cell_skeleton/url_card_cell.dart';
CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {
const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 2);
final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 10,
overflow: TextOverflow.ellipsis,
fontWeight: FontWeight.w400,
);
return {
FieldType.Checkbox: CheckboxCardCellStyle(
padding: padding,
iconSize: const Size.square(16),
showFieldName: true,
textStyle: textStyle,
),
FieldType.Checklist: ChecklistCardCellStyle(
padding: padding,
textStyle: textStyle.copyWith(color: Theme.of(context).hintColor),
),
FieldType.CreatedTime: TimestampCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.DateTime: DateCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.LastEditedTime: TimestampCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.MultiSelect: SelectOptionCardCellStyle(
padding: padding,
tagFontSize: 9,
wrap: true,
tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
),
FieldType.Number: NumberCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.RichText: TextCardCellStyle(
padding: padding,
textStyle: textStyle,
titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 11,
overflow: TextOverflow.ellipsis,
),
),
FieldType.SingleSelect: SelectOptionCardCellStyle(
padding: padding,
tagFontSize: 9,
wrap: true,
tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
),
FieldType.URL: URLCardCellStyle(
padding: padding,
textStyle: textStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
};
}

View File

@ -0,0 +1,77 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import '../card_cell_builder.dart';
import '../card_cell_skeleton/checkbox_card_cell.dart';
import '../card_cell_skeleton/checklist_card_cell.dart';
import '../card_cell_skeleton/date_card_cell.dart';
import '../card_cell_skeleton/number_card_cell.dart';
import '../card_cell_skeleton/select_option_card_cell.dart';
import '../card_cell_skeleton/text_card_cell.dart';
import '../card_cell_skeleton/timestamp_card_cell.dart';
import '../card_cell_skeleton/url_card_cell.dart';
CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
const EdgeInsetsGeometry padding = EdgeInsets.all(4);
final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 11,
overflow: TextOverflow.ellipsis,
fontWeight: FontWeight.w400,
);
return {
FieldType.Checkbox: CheckboxCardCellStyle(
padding: padding,
iconSize: const Size.square(16),
showFieldName: true,
textStyle: textStyle,
),
FieldType.Checklist: ChecklistCardCellStyle(
padding: padding,
textStyle: textStyle.copyWith(color: Theme.of(context).hintColor),
),
FieldType.CreatedTime: TimestampCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.DateTime: DateCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.LastEditedTime: TimestampCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.MultiSelect: SelectOptionCardCellStyle(
padding: padding,
tagFontSize: 11,
wrap: true,
tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
),
FieldType.Number: NumberCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.RichText: TextCardCellStyle(
padding: padding,
textStyle: textStyle,
maxLines: null,
titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
overflow: TextOverflow.ellipsis,
),
),
FieldType.SingleSelect: SelectOptionCardCellStyle(
padding: padding,
tagFontSize: 11,
wrap: true,
tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
),
FieldType.URL: URLCardCellStyle(
padding: padding,
textStyle: textStyle.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
),
),
};
}

Some files were not shown because too many files have changed in this diff Show More