mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
18a355601a
commit
a1abcd7626
@ -30,6 +30,7 @@ linter:
|
||||
rules:
|
||||
- require_trailing_commas
|
||||
|
||||
- prefer_collection_literals
|
||||
- prefer_final_fields
|
||||
- prefer_final_in_for_each
|
||||
- prefer_final_locals
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
|
@ -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: '',
|
||||
);
|
||||
|
||||
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
export 'card_detail/mobile_card_detail_screen.dart';
|
||||
export 'card_content/mobile_card_content.dart';
|
||||
export 'mobile_card_content.dart';
|
||||
|
@ -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';
|
@ -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()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
@ -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";
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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>;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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"),
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -123,7 +123,7 @@ class DatabaseViewCache {
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _databaseViewListener.stop();
|
||||
await _rowCache.dispose();
|
||||
_rowCache.dispose();
|
||||
_callbacks.clear();
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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: []);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CardSizes {
|
||||
static EdgeInsets get cardCellPadding => const EdgeInsets.all(4);
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user