mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: row document (#2792)
* chore: create orphan view handler * feat: save icon url and cover url in view * feat: implement emoji picker UI * chore: config ui * chore: config ui again * chore: replace RowPB with RowMetaPB to exposing more row information * fix: compile error * feat: show emoji in row * chore: update * test: insert emoji test * test: add update emoji test * test: add remove emoji test * test: add create field tests * test: add create row and delete row integration tests * test: add create row from row menu * test: document in row detail page * test: delete, duplicate row in row detail page * test: check the row count displayed in grid page * test: rename existing field in grid page * test: update field type of exisiting field in grid page * test: delete field test * test: add duplicate field test * test: add hide field test * test: add edit text cell test * test: add insert text to text cell test * test: add edit number cell test * test: add edit multiple number cells * test: add edit checkbox cell test * feat: integrate editor into database row * test: add edit create time and last edit time cell test * test: add edit date cell by selecting a date test * chore: remove unused code * chore: update checklist bg color * test: add update database layout test --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
parent
b8983e4466
commit
27dd719aa8
@ -0,0 +1,185 @@
|
|||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import 'util/database_test_op.dart';
|
||||||
|
import 'util/util.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('grid cell', () {
|
||||||
|
const location = 'appflowy';
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
await TestFolder.setTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() async {
|
||||||
|
await TestFolder.cleanTestLocation(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('edit text cell', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 0,
|
||||||
|
fieldType: FieldType.RichText,
|
||||||
|
input: 'hello world',
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.assertCellContent(
|
||||||
|
rowIndex: 0,
|
||||||
|
fieldType: FieldType.RichText,
|
||||||
|
content: 'hello world',
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('edit number cell', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
const fieldType = FieldType.Number;
|
||||||
|
|
||||||
|
// Create a number field
|
||||||
|
await tester.createField(fieldType, fieldType.name);
|
||||||
|
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 0,
|
||||||
|
fieldType: fieldType,
|
||||||
|
input: '-1',
|
||||||
|
);
|
||||||
|
// edit the next cell to force the previous cell at row 0 to lose focus
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 1,
|
||||||
|
fieldType: fieldType,
|
||||||
|
input: '0.2',
|
||||||
|
);
|
||||||
|
// -1 -> -1
|
||||||
|
await tester.assertCellContent(
|
||||||
|
rowIndex: 0,
|
||||||
|
fieldType: fieldType,
|
||||||
|
content: '-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
// edit the next cell to force the previous cell at row 1 to lose focus
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 2,
|
||||||
|
fieldType: fieldType,
|
||||||
|
input: '.1',
|
||||||
|
);
|
||||||
|
// 0.2 -> 0.2
|
||||||
|
await tester.assertCellContent(
|
||||||
|
rowIndex: 1,
|
||||||
|
fieldType: fieldType,
|
||||||
|
content: '0.2',
|
||||||
|
);
|
||||||
|
|
||||||
|
// edit the next cell to force the previous cell at row 2 to lose focus
|
||||||
|
await tester.editCell(
|
||||||
|
rowIndex: 0,
|
||||||
|
fieldType: fieldType,
|
||||||
|
input: '',
|
||||||
|
);
|
||||||
|
// .1 -> 0.1
|
||||||
|
await tester.assertCellContent(
|
||||||
|
rowIndex: 2,
|
||||||
|
fieldType: fieldType,
|
||||||
|
content: '0.1',
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('edit checkbox cell', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
await tester.assertCheckboxCell(rowIndex: 0, isSelected: false);
|
||||||
|
await tester.tapCheckboxCellInGrid(rowIndex: 0);
|
||||||
|
await tester.assertCheckboxCell(rowIndex: 0, isSelected: true);
|
||||||
|
|
||||||
|
await tester.tapCheckboxCellInGrid(rowIndex: 1);
|
||||||
|
await tester.tapCheckboxCellInGrid(rowIndex: 2);
|
||||||
|
await tester.assertCheckboxCell(rowIndex: 1, isSelected: true);
|
||||||
|
await tester.assertCheckboxCell(rowIndex: 2, isSelected: true);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('edit create time cell', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
const fieldType = FieldType.CreatedTime;
|
||||||
|
// Create a create time field
|
||||||
|
// The create time field is not editable
|
||||||
|
await tester.createField(fieldType, fieldType.name);
|
||||||
|
|
||||||
|
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||||
|
|
||||||
|
await tester.findDateEditor(findsNothing);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('edit last time cell', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
const fieldType = FieldType.LastEditedTime;
|
||||||
|
// Create a last time field
|
||||||
|
// The last time field is not editable
|
||||||
|
await tester.createField(fieldType, fieldType.name);
|
||||||
|
|
||||||
|
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||||
|
|
||||||
|
await tester.findDateEditor(findsNothing);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('edit time cell', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
const fieldType = FieldType.DateTime;
|
||||||
|
await tester.createField(fieldType, fieldType.name);
|
||||||
|
|
||||||
|
// Tap the cell to invoke the field editor
|
||||||
|
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
|
||||||
|
await tester.findDateEditor(findsOneWidget);
|
||||||
|
|
||||||
|
// Select the date
|
||||||
|
await tester.selectDay(content: 3);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,202 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import 'util/database_test_op.dart';
|
||||||
|
import 'util/util.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('grid page', () {
|
||||||
|
const location = 'appflowy';
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
await TestFolder.setTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() async {
|
||||||
|
await TestFolder.cleanTestLocation(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('rename existing field', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// Invoke the field editor
|
||||||
|
await tester.tapGridFieldWithName('Name');
|
||||||
|
await tester.tapEditPropertyButton();
|
||||||
|
|
||||||
|
await tester.renameField('hello world');
|
||||||
|
await tester.dismissFieldEditor();
|
||||||
|
|
||||||
|
await tester.tapGridFieldWithName('hello world');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('update field type of existing field', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// Invoke the field editor
|
||||||
|
await tester.tapGridFieldWithName('Type');
|
||||||
|
await tester.tapEditPropertyButton();
|
||||||
|
|
||||||
|
await tester.tapTypeOptionButton();
|
||||||
|
await tester.selectFieldType(FieldType.Checkbox);
|
||||||
|
await tester.dismissFieldEditor();
|
||||||
|
|
||||||
|
await tester.assertFieldTypeWithFieldName(
|
||||||
|
'Type',
|
||||||
|
FieldType.Checkbox,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('create a field and rename it', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// create a new grid
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// create a field
|
||||||
|
await tester.createField(FieldType.Checklist, 'checklist');
|
||||||
|
|
||||||
|
// check the field is created successfully
|
||||||
|
await tester.findFieldWithName('checklist');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('delete field', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// create a field
|
||||||
|
await tester.createField(FieldType.Checkbox, 'New field 1');
|
||||||
|
|
||||||
|
// Delete the field
|
||||||
|
await tester.tapGridFieldWithName('New field 1');
|
||||||
|
await tester.tapDeletePropertyButton();
|
||||||
|
|
||||||
|
// confirm delete
|
||||||
|
await tester.tapDialogOkButton();
|
||||||
|
|
||||||
|
await tester.noFieldWithName('New field 1');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('duplicate field', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// create a field
|
||||||
|
await tester.scrollToRight(find.byType(GridPage));
|
||||||
|
await tester.tapNewPropertyButton();
|
||||||
|
await tester.renameField('New field 1');
|
||||||
|
await tester.dismissFieldEditor();
|
||||||
|
await tester.createField(FieldType.RichText, 'New field 1');
|
||||||
|
|
||||||
|
// Delete the field
|
||||||
|
await tester.tapGridFieldWithName('New field 1');
|
||||||
|
await tester.tapDuplicatePropertyButton();
|
||||||
|
|
||||||
|
await tester.findFieldWithName('New field 1 (copy)');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('hide field', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// create a field
|
||||||
|
await tester.scrollToRight(find.byType(GridPage));
|
||||||
|
await tester.tapNewPropertyButton();
|
||||||
|
await tester.renameField('New field 1');
|
||||||
|
await tester.dismissFieldEditor();
|
||||||
|
|
||||||
|
// Delete the field
|
||||||
|
await tester.tapGridFieldWithName('New field 1');
|
||||||
|
await tester.tapHidePropertyButton();
|
||||||
|
|
||||||
|
await tester.noFieldWithName('New field 1');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('create checklist field ', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
await tester.scrollToRight(find.byType(GridPage));
|
||||||
|
await tester.tapNewPropertyButton();
|
||||||
|
|
||||||
|
// Open the type option menu
|
||||||
|
await tester.tapTypeOptionButton();
|
||||||
|
|
||||||
|
await tester.selectFieldType(FieldType.Checklist);
|
||||||
|
|
||||||
|
// After update the field type, the cells should be updated
|
||||||
|
await tester.findCellByFieldType(FieldType.Checklist);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('create list of fields', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
for (final fieldType in [
|
||||||
|
FieldType.Checklist,
|
||||||
|
FieldType.DateTime,
|
||||||
|
FieldType.Number,
|
||||||
|
FieldType.URL,
|
||||||
|
FieldType.MultiSelect,
|
||||||
|
FieldType.LastEditedTime,
|
||||||
|
FieldType.CreatedTime,
|
||||||
|
FieldType.Checkbox,
|
||||||
|
]) {
|
||||||
|
await tester.scrollToRight(find.byType(GridPage));
|
||||||
|
await tester.tapNewPropertyButton();
|
||||||
|
await tester.renameField(fieldType.name);
|
||||||
|
|
||||||
|
// Open the type option menu
|
||||||
|
await tester.tapTypeOptionButton();
|
||||||
|
|
||||||
|
await tester.selectFieldType(fieldType);
|
||||||
|
await tester.dismissFieldEditor();
|
||||||
|
|
||||||
|
// After update the field type, the cells should be updated
|
||||||
|
await tester.findCellByFieldType(fieldType);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,241 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import 'util/database_test_op.dart';
|
||||||
|
import 'util/ime.dart';
|
||||||
|
import 'util/util.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('grid', () {
|
||||||
|
const location = 'appflowy';
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
await TestFolder.setTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() async {
|
||||||
|
await TestFolder.cleanTestLocation(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('open first row of the grid', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// Create a new grid
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// Hover first row and then open the row page
|
||||||
|
await tester.openFirstRowDetailPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('insert emoji in the row detail page', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// Create a new grid
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// Hover first row and then open the row page
|
||||||
|
await tester.openFirstRowDetailPage();
|
||||||
|
|
||||||
|
await tester.hoverRowBanner();
|
||||||
|
|
||||||
|
await tester.openEmojiPicker();
|
||||||
|
await tester.switchToEmojiList();
|
||||||
|
await tester.tapEmoji('😀');
|
||||||
|
|
||||||
|
// After select the emoji, the EmojiButton will show up
|
||||||
|
await tester.tapButton(find.byType(EmojiButton));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('update emoji in the row detail page', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// Create a new grid
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// Hover first row and then open the row page
|
||||||
|
await tester.openFirstRowDetailPage();
|
||||||
|
await tester.hoverRowBanner();
|
||||||
|
await tester.openEmojiPicker();
|
||||||
|
await tester.switchToEmojiList();
|
||||||
|
await tester.tapEmoji('😀');
|
||||||
|
|
||||||
|
// Update existing selected emoji
|
||||||
|
await tester.tapButton(find.byType(EmojiButton));
|
||||||
|
await tester.switchToEmojiList();
|
||||||
|
await tester.tapEmoji('😅');
|
||||||
|
|
||||||
|
// The emoji already displayed in the row banner
|
||||||
|
final emojiText = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is FlowyText && widget.title == '😅',
|
||||||
|
);
|
||||||
|
|
||||||
|
// The number of emoji should be two. One in the row displayed in the grid
|
||||||
|
// one in the row detail page.
|
||||||
|
expect(emojiText, findsNWidgets(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('remove emoji in the row detail page', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// Create a new grid
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// Hover first row and then open the row page
|
||||||
|
await tester.openFirstRowDetailPage();
|
||||||
|
await tester.hoverRowBanner();
|
||||||
|
await tester.openEmojiPicker();
|
||||||
|
await tester.switchToEmojiList();
|
||||||
|
await tester.tapEmoji('😀');
|
||||||
|
|
||||||
|
// Remove the emoji
|
||||||
|
await tester.tapButton(find.byType(RemoveEmojiButton));
|
||||||
|
final emojiText = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is FlowyText && widget.title == '😀',
|
||||||
|
);
|
||||||
|
expect(emojiText, findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('create list of fields in row detail page', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// Create a new grid
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// Hover first row and then open the row page
|
||||||
|
await tester.openFirstRowDetailPage();
|
||||||
|
|
||||||
|
for (final fieldType in [
|
||||||
|
FieldType.Checklist,
|
||||||
|
FieldType.DateTime,
|
||||||
|
FieldType.Number,
|
||||||
|
FieldType.URL,
|
||||||
|
FieldType.MultiSelect,
|
||||||
|
FieldType.LastEditedTime,
|
||||||
|
FieldType.CreatedTime,
|
||||||
|
FieldType.Checkbox,
|
||||||
|
]) {
|
||||||
|
await tester.tapRowDetailPageCreatePropertyButton();
|
||||||
|
await tester.renameField(fieldType.name);
|
||||||
|
|
||||||
|
// Open the type option menu
|
||||||
|
await tester.tapTypeOptionButton();
|
||||||
|
|
||||||
|
await tester.selectFieldType(fieldType);
|
||||||
|
await tester.dismissFieldEditor();
|
||||||
|
|
||||||
|
// After update the field type, the cells should be updated
|
||||||
|
await tester.findCellByFieldType(fieldType);
|
||||||
|
await tester.scrollRowDetailByOffset(const Offset(0, -50));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('check document is exist in row detail page', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// Create a new grid
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// 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 content of the document and re-open it',
|
||||||
|
(tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// Create a new grid
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// Hover first row and then open the row page
|
||||||
|
await tester.openFirstRowDetailPage();
|
||||||
|
|
||||||
|
// Wait for the document to be loaded
|
||||||
|
await tester.wait(500);
|
||||||
|
|
||||||
|
// Focus on the editor
|
||||||
|
final textBlock = find.byType(TextBlockComponentWidget);
|
||||||
|
await tester.tapAt(tester.getCenter(textBlock));
|
||||||
|
|
||||||
|
// Input some text
|
||||||
|
const inputText = 'Hello world';
|
||||||
|
await tester.ime.insertText(inputText);
|
||||||
|
expect(
|
||||||
|
find.textContaining(inputText, findRichText: true),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tap outside to dismiss the field
|
||||||
|
await tester.tapAt(Offset.zero);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Re-open the document
|
||||||
|
await tester.openFirstRowDetailPage();
|
||||||
|
expect(
|
||||||
|
find.textContaining(inputText, findRichText: true),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('delete row in row detail page', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// Create a new grid
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// Hover first row and then open the row page
|
||||||
|
await tester.openFirstRowDetailPage();
|
||||||
|
|
||||||
|
await tester.tapRowDetailPageDeleteRowButton();
|
||||||
|
await tester.tapEscButton();
|
||||||
|
|
||||||
|
await tester.assertNumberOfRowsInGridPage(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('duplicate row in row detail page', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
// Create a new grid
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// Hover first row and then open the row page
|
||||||
|
await tester.openFirstRowDetailPage();
|
||||||
|
|
||||||
|
await tester.tapRowDetailPageDuplicateRowButton();
|
||||||
|
await tester.tapEscButton();
|
||||||
|
|
||||||
|
await tester.assertNumberOfRowsInGridPage(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import 'util/database_test_op.dart';
|
||||||
|
import 'util/util.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('grid', () {
|
||||||
|
const location = 'appflowy';
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
await TestFolder.setTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() async {
|
||||||
|
await TestFolder.cleanTestLocation(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('create row of the grid', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
await tester.tapCreateRowButtonInGrid();
|
||||||
|
|
||||||
|
// The initial number of rows is 3
|
||||||
|
await tester.assertNumberOfRowsInGridPage(4);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('create row from row menu of the grid', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
await tester.hoverOnFirstRowOfGrid();
|
||||||
|
|
||||||
|
await tester.tapCreateRowButtonInRowMenuOfGrid();
|
||||||
|
|
||||||
|
// The initial number of rows is 3
|
||||||
|
await tester.assertNumberOfRowsInGridPage(4);
|
||||||
|
await tester.assertRowCountInGridPage(4);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('delete row of the grid', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
await tester.hoverOnFirstRowOfGrid();
|
||||||
|
|
||||||
|
// Open the row menu and then click the delete
|
||||||
|
await tester.tapRowMenuButtonInGrid();
|
||||||
|
await tester.tapDeleteOnRowMenu();
|
||||||
|
|
||||||
|
// The initial number of rows is 3
|
||||||
|
await tester.assertNumberOfRowsInGridPage(2);
|
||||||
|
await tester.assertRowCountInGridPage(2);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('check number of row indicator in the initial grid',
|
||||||
|
(tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
await tester.assertRowCountInGridPage(3);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import 'util/database_test_op.dart';
|
||||||
|
import 'util/util.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('grid', () {
|
||||||
|
const location = 'appflowy';
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
await TestFolder.setTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await TestFolder.cleanTestLocation(location);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() async {
|
||||||
|
await TestFolder.cleanTestLocation(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('update layout', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// open setting
|
||||||
|
await tester.tapDatabaseSettingButton();
|
||||||
|
// select the layout
|
||||||
|
await tester.tapDatabaseLayoutButton();
|
||||||
|
// select layout by board
|
||||||
|
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||||
|
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('update layout multiple times', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await tester.tapAddButton();
|
||||||
|
await tester.tapCreateGridButton();
|
||||||
|
|
||||||
|
// open setting
|
||||||
|
await tester.tapDatabaseSettingButton();
|
||||||
|
await tester.tapDatabaseLayoutButton();
|
||||||
|
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||||
|
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);
|
||||||
|
|
||||||
|
await tester.tapDatabaseSettingButton();
|
||||||
|
await tester.tapDatabaseLayoutButton();
|
||||||
|
await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Calendar);
|
||||||
|
await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Calendar);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||||
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
|
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart';
|
||||||
@ -39,6 +40,13 @@ extension CommonOperations on WidgetTester {
|
|||||||
await tapButtonWithName(LocaleKeys.document_menuName.tr());
|
await tapButtonWithName(LocaleKeys.document_menuName.tr());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tap the create grid button.
|
||||||
|
///
|
||||||
|
/// Must call [tapAddButton] first.
|
||||||
|
Future<void> tapCreateGridButton() async {
|
||||||
|
await tapButtonWithName(LocaleKeys.grid_menuName.tr());
|
||||||
|
}
|
||||||
|
|
||||||
/// Tap the import button.
|
/// Tap the import button.
|
||||||
///
|
///
|
||||||
/// Must call [tapAddButton] first.
|
/// Must call [tapAddButton] first.
|
||||||
@ -105,12 +113,14 @@ extension CommonOperations on WidgetTester {
|
|||||||
Finder finder, {
|
Finder finder, {
|
||||||
Offset? offset,
|
Offset? offset,
|
||||||
}) async {
|
}) async {
|
||||||
|
try {
|
||||||
final gesture = await createGesture(kind: PointerDeviceKind.mouse);
|
final gesture = await createGesture(kind: PointerDeviceKind.mouse);
|
||||||
await gesture.addPointer(location: Offset.zero);
|
await gesture.addPointer(location: Offset.zero);
|
||||||
addTearDown(gesture.removePointer);
|
addTearDown(gesture.removePointer);
|
||||||
await pump();
|
await pump();
|
||||||
await gesture.moveTo(offset ?? getCenter(finder));
|
await gesture.moveTo(offset ?? getCenter(finder));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hover on the page name.
|
/// Hover on the page name.
|
||||||
|
@ -0,0 +1,481 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
|
|
||||||
|
import 'base.dart';
|
||||||
|
import 'common_operations.dart';
|
||||||
|
|
||||||
|
extension AppFlowyDatabaseTest on WidgetTester {
|
||||||
|
Future<void> hoverOnFirstRowOfGrid() async {
|
||||||
|
final findRow = find.byType(GridRow);
|
||||||
|
expect(findRow, findsWidgets);
|
||||||
|
|
||||||
|
final firstRow = findRow.first;
|
||||||
|
await hoverOnWidget(firstRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> editCell({
|
||||||
|
required int rowIndex,
|
||||||
|
required FieldType fieldType,
|
||||||
|
required String input,
|
||||||
|
}) async {
|
||||||
|
final findRow = find.byType(GridRow);
|
||||||
|
final findCell = finderForFieldType(fieldType);
|
||||||
|
|
||||||
|
final cell = find.descendant(
|
||||||
|
of: findRow.at(rowIndex),
|
||||||
|
matching: findCell,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cell, findsOneWidget);
|
||||||
|
await enterText(cell, input);
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapCheckboxCellInGrid({
|
||||||
|
required int rowIndex,
|
||||||
|
}) async {
|
||||||
|
final findRow = find.byType(GridRow);
|
||||||
|
final findCell = finderForFieldType(FieldType.Checkbox);
|
||||||
|
|
||||||
|
final cell = find.descendant(
|
||||||
|
of: findRow.at(rowIndex),
|
||||||
|
matching: findCell,
|
||||||
|
);
|
||||||
|
|
||||||
|
final button = find.descendant(
|
||||||
|
of: cell,
|
||||||
|
matching: find.byType(FlowyIconButton),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cell, findsOneWidget);
|
||||||
|
await tapButton(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> assertCheckboxCell({
|
||||||
|
required int rowIndex,
|
||||||
|
required bool isSelected,
|
||||||
|
}) async {
|
||||||
|
final findRow = find.byType(GridRow);
|
||||||
|
final findCell = finderForFieldType(FieldType.Checkbox);
|
||||||
|
|
||||||
|
final cell = find.descendant(
|
||||||
|
of: findRow.at(rowIndex),
|
||||||
|
matching: findCell,
|
||||||
|
);
|
||||||
|
|
||||||
|
var finder = find.byType(CheckboxCellUncheck);
|
||||||
|
if (isSelected) {
|
||||||
|
finder = find.byType(CheckboxCellCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: cell,
|
||||||
|
matching: finder,
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapCellInGrid({
|
||||||
|
required int rowIndex,
|
||||||
|
required FieldType fieldType,
|
||||||
|
}) async {
|
||||||
|
final findRow = find.byType(GridRow);
|
||||||
|
final findCell = finderForFieldType(fieldType);
|
||||||
|
|
||||||
|
final cell = find.descendant(
|
||||||
|
of: findRow.at(rowIndex),
|
||||||
|
matching: findCell,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cell, findsOneWidget);
|
||||||
|
await tapButton(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> assertCellContent({
|
||||||
|
required int rowIndex,
|
||||||
|
required FieldType fieldType,
|
||||||
|
required String content,
|
||||||
|
}) async {
|
||||||
|
final findRow = find.byType(GridRow);
|
||||||
|
final findCell = finderForFieldType(fieldType);
|
||||||
|
final cell = find.descendant(
|
||||||
|
of: findRow.at(rowIndex),
|
||||||
|
matching: findCell,
|
||||||
|
);
|
||||||
|
|
||||||
|
final findContent = find.descendant(
|
||||||
|
of: cell,
|
||||||
|
matching: find.text(content),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(findContent, findsOneWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> selectDay({
|
||||||
|
required int content,
|
||||||
|
}) async {
|
||||||
|
final findCalendar = find.byType(TableCalendar);
|
||||||
|
final findDay = find.text(content.toString());
|
||||||
|
|
||||||
|
final finder = find.descendant(
|
||||||
|
of: findCalendar,
|
||||||
|
matching: findDay,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tapButton(finder);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> openFirstRowDetailPage() async {
|
||||||
|
await hoverOnFirstRowOfGrid();
|
||||||
|
|
||||||
|
final expandButton = find.byType(PrimaryCellAccessory);
|
||||||
|
expect(expandButton, findsOneWidget);
|
||||||
|
await tapButton(expandButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> hoverRowBanner() async {
|
||||||
|
final banner = find.byType(RowBanner);
|
||||||
|
expect(banner, findsOneWidget);
|
||||||
|
|
||||||
|
await startGesture(
|
||||||
|
getTopLeft(banner),
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
|
);
|
||||||
|
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> openEmojiPicker() async {
|
||||||
|
await tapButton(find.byType(EmojiPickerButton));
|
||||||
|
await tapButton(find.byType(EmojiSelectionMenu));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Must call [openEmojiPicker] first
|
||||||
|
Future<void> switchToEmojiList() async {
|
||||||
|
final icon = find.byIcon(Icons.tag_faces);
|
||||||
|
await tapButton(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapEmoji(String emoji) async {
|
||||||
|
final emojiWidget = find.text(emoji);
|
||||||
|
await tapButton(emojiWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> scrollGridByOffset(Offset offset) async {
|
||||||
|
await drag(find.byType(GridPage), offset);
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> scrollRowDetailByOffset(Offset offset) async {
|
||||||
|
await drag(find.byType(RowDetailPage), offset);
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> scrollToRight(Finder find) async {
|
||||||
|
final size = getSize(find);
|
||||||
|
await drag(find, Offset(-size.width, 0));
|
||||||
|
await pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapNewPropertyButton() async {
|
||||||
|
await tapButtonWithName(LocaleKeys.grid_field_newProperty.tr());
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapGridFieldWithName(String name) async {
|
||||||
|
final field = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is FieldCellButton && widget.field.name == name,
|
||||||
|
);
|
||||||
|
await tapButton(field);
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should call [tapGridFieldWithName] first.
|
||||||
|
Future<void> tapEditPropertyButton() async {
|
||||||
|
await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr());
|
||||||
|
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should call [tapGridFieldWithName] first.
|
||||||
|
Future<void> tapDeletePropertyButton() async {
|
||||||
|
final field = find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is FieldActionCell && widget.action == FieldAction.delete,
|
||||||
|
);
|
||||||
|
await tapButton(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should call [tapGridFieldWithName] first.
|
||||||
|
Future<void> tapDialogOkButton() async {
|
||||||
|
final field = find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is PrimaryTextButton &&
|
||||||
|
widget.label == LocaleKeys.button_OK.tr(),
|
||||||
|
);
|
||||||
|
await tapButton(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should call [tapGridFieldWithName] first.
|
||||||
|
Future<void> tapDuplicatePropertyButton() async {
|
||||||
|
final field = find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is FieldActionCell && widget.action == FieldAction.duplicate,
|
||||||
|
);
|
||||||
|
await tapButton(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should call [tapGridFieldWithName] first.
|
||||||
|
Future<void> tapHidePropertyButton() async {
|
||||||
|
final field = find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is FieldActionCell && widget.action == FieldAction.hide,
|
||||||
|
);
|
||||||
|
await tapButton(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapRowDetailPageCreatePropertyButton() async {
|
||||||
|
await tapButton(find.byType(CreateRowFieldButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapRowDetailPageDeleteRowButton() async {
|
||||||
|
await tapButton(find.byType(RowDetailPageDeleteButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapRowDetailPageDuplicateRowButton() async {
|
||||||
|
await tapButton(find.byType(RowDetailPageDuplicateButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapTypeOptionButton() async {
|
||||||
|
await tapButton(find.byType(SwitchFieldButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapEscButton() async {
|
||||||
|
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Must call [tapTypeOptionButton] first.
|
||||||
|
Future<void> selectFieldType(FieldType fieldType) async {
|
||||||
|
final fieldTypeButton = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is FlowyText && widget.title == fieldType.title(),
|
||||||
|
);
|
||||||
|
await tapButton(fieldTypeButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
final finder = finderForFieldType(fieldType);
|
||||||
|
expect(finder, findsWidgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> assertNumberOfFieldsInGridPage(int num) async {
|
||||||
|
expect(find.byType(GridFieldCell), findsNWidgets(num));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> assertNumberOfRowsInGridPage(int num) async {
|
||||||
|
expect(find.byType(GridRow), findsNWidgets(num));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> assertDocumentExistInRowDetailPage() async {
|
||||||
|
expect(find.byType(RowDocument), findsOneWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the field type of the [FieldCellButton] is the same as the name.
|
||||||
|
Future<void> assertFieldTypeWithFieldName(
|
||||||
|
String name,
|
||||||
|
FieldType fieldType,
|
||||||
|
) async {
|
||||||
|
final field = find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is FieldCellButton &&
|
||||||
|
widget.field.fieldType == fieldType &&
|
||||||
|
widget.field.name == name,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(field, findsOneWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> findFieldWithName(String name) async {
|
||||||
|
final field = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is FieldCellButton && widget.field.name == name,
|
||||||
|
);
|
||||||
|
expect(field, findsOneWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> noFieldWithName(String name) async {
|
||||||
|
final field = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is FieldCellButton && widget.field.name == name,
|
||||||
|
);
|
||||||
|
expect(field, findsNothing);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> renameField(String newName) async {
|
||||||
|
final textField = find.byType(FieldNameTextField);
|
||||||
|
expect(textField, findsOneWidget);
|
||||||
|
await enterText(textField, newName);
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dismissFieldEditor() async {
|
||||||
|
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||||
|
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||||
|
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> findFieldEditor(dynamic matcher) async {
|
||||||
|
final finder = find.byType(FieldEditor);
|
||||||
|
expect(finder, matcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> findDateEditor(dynamic matcher) async {
|
||||||
|
final finder = find.byType(DateCellEditor);
|
||||||
|
expect(finder, matcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapCreateRowButtonInGrid() async {
|
||||||
|
await tapButton(find.byType(GridAddRowButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapCreateRowButtonInRowMenuOfGrid() async {
|
||||||
|
await tapButton(find.byType(InsertRowButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapRowMenuButtonInGrid() async {
|
||||||
|
await tapButton(find.byType(RowMenuButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should call [tapRowMenuButtonInGrid] first.
|
||||||
|
Future<void> tapDeleteOnRowMenu() async {
|
||||||
|
await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> assertRowCountInGridPage(int num) async {
|
||||||
|
final text = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is FlowyText && widget.title == rowCountString(num),
|
||||||
|
);
|
||||||
|
expect(text, findsOneWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createField(FieldType fieldType, String name) async {
|
||||||
|
await scrollToRight(find.byType(GridPage));
|
||||||
|
await tapNewPropertyButton();
|
||||||
|
await renameField(name);
|
||||||
|
await tapTypeOptionButton();
|
||||||
|
await selectFieldType(fieldType);
|
||||||
|
await dismissFieldEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tapDatabaseSettingButton() async {
|
||||||
|
await tapButton(find.byType(SettingButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should call [tapDatabaseSettingButton] first.
|
||||||
|
Future<void> tapDatabaseLayoutButton() async {
|
||||||
|
final findSettingItem = find.byType(DatabaseSettingItem);
|
||||||
|
final findLayoutButton = find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is FlowyText &&
|
||||||
|
widget.title == DatabaseSettingAction.showLayout.title(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final button = find.descendant(
|
||||||
|
of: findSettingItem,
|
||||||
|
matching: findLayoutButton,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tapButton(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
|
||||||
|
final findLayoutCell = find.byType(DatabaseViewLayoutCell);
|
||||||
|
final findText = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is FlowyText && widget.title == layout.layoutName(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final button = find.descendant(
|
||||||
|
of: findLayoutCell,
|
||||||
|
matching: findText,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tapButton(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> assertCurrentDatabaseLayoutType(DatabaseLayoutPB layout) async {
|
||||||
|
expect(finderForDatabaseLayoutType(layout), findsOneWidget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
|
||||||
|
switch (layout) {
|
||||||
|
case DatabaseLayoutPB.Board:
|
||||||
|
return find.byType(BoardPage);
|
||||||
|
case DatabaseLayoutPB.Calendar:
|
||||||
|
return find.byType(CalendarPage);
|
||||||
|
case DatabaseLayoutPB.Grid:
|
||||||
|
return find.byType(GridPage);
|
||||||
|
default:
|
||||||
|
throw Exception('Unknown database layout type: $layout');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Finder finderForFieldType(FieldType fieldType) {
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldType.Checkbox:
|
||||||
|
return find.byType(GridCheckboxCell);
|
||||||
|
case FieldType.DateTime:
|
||||||
|
return find.byType(GridDateCell);
|
||||||
|
case FieldType.LastEditedTime:
|
||||||
|
case FieldType.CreatedTime:
|
||||||
|
return find.byType(GridDateCell);
|
||||||
|
case FieldType.SingleSelect:
|
||||||
|
return find.byType(GridSingleSelectCell);
|
||||||
|
case FieldType.MultiSelect:
|
||||||
|
return find.byType(GridMultiSelectCell);
|
||||||
|
case FieldType.Checklist:
|
||||||
|
return find.byType(GridChecklistCell);
|
||||||
|
case FieldType.Number:
|
||||||
|
return find.byType(GridNumberCell);
|
||||||
|
case FieldType.RichText:
|
||||||
|
return find.byType(GridTextCell);
|
||||||
|
case FieldType.URL:
|
||||||
|
return find.byType(GridURLCell);
|
||||||
|
default:
|
||||||
|
throw Exception('Unknown field type: $fieldType');
|
||||||
|
}
|
||||||
|
}
|
54
frontend/appflowy_flutter/integration_test/util/ime.dart
Normal file
54
frontend/appflowy_flutter/integration_test/util/ime.dart
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
extension IME on WidgetTester {
|
||||||
|
IMESimulator get ime => IMESimulator(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class IMESimulator {
|
||||||
|
IMESimulator(this.tester) {
|
||||||
|
client = findDeltaTextInputClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
final WidgetTester tester;
|
||||||
|
late final DeltaTextInputClient client;
|
||||||
|
|
||||||
|
Future<void> insertText(String text) async {
|
||||||
|
for (final c in text.characters) {
|
||||||
|
await insertCharacter(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> insertCharacter(String character) async {
|
||||||
|
final value = client.currentTextEditingValue;
|
||||||
|
if (value == null) {
|
||||||
|
assert(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final deltas = [
|
||||||
|
TextEditingDeltaInsertion(
|
||||||
|
textInserted: character,
|
||||||
|
oldText: value.text.replaceRange(
|
||||||
|
value.selection.start,
|
||||||
|
value.selection.end,
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
insertionOffset: value.selection.baseOffset,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: value.selection.baseOffset + 1,
|
||||||
|
),
|
||||||
|
composing: TextRange.empty,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
client.updateEditingValueWithDeltas(deltas);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
DeltaTextInputClient findDeltaTextInputClient() {
|
||||||
|
final finder = find.byType(KeyboardServiceWidget);
|
||||||
|
final KeyboardServiceWidgetState state = tester.state(finder);
|
||||||
|
return state.textInputService as DeltaTextInputClient;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
|
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/row/row_meta_listener.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||||
import 'package:appflowy_backend/log.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/field_entities.pbenum.dart';
|
||||||
@ -22,39 +23,45 @@ import 'cell_service.dart';
|
|||||||
///
|
///
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class CellController<T, D> extends Equatable {
|
class CellController<T, D> extends Equatable {
|
||||||
final DatabaseCellContext cellContext;
|
DatabaseCellContext _cellContext;
|
||||||
final CellCache _cellCache;
|
final CellCache _cellCache;
|
||||||
final CellCacheKey _cacheKey;
|
final CellCacheKey _cacheKey;
|
||||||
final FieldBackendService _fieldBackendSvc;
|
final FieldBackendService _fieldBackendSvc;
|
||||||
final SingleFieldListener _fieldListener;
|
|
||||||
final CellDataLoader<T> _cellDataLoader;
|
final CellDataLoader<T> _cellDataLoader;
|
||||||
final CellDataPersistence<D> _cellDataPersistence;
|
final CellDataPersistence<D> _cellDataPersistence;
|
||||||
|
|
||||||
CellListener? _cellListener;
|
CellListener? _cellListener;
|
||||||
|
RowMetaListener? _rowMetaListener;
|
||||||
|
SingleFieldListener? _fieldListener;
|
||||||
CellDataNotifier<T?>? _cellDataNotifier;
|
CellDataNotifier<T?>? _cellDataNotifier;
|
||||||
|
|
||||||
VoidCallback? _onCellFieldChanged;
|
VoidCallback? _onCellFieldChanged;
|
||||||
|
VoidCallback? _onRowMetaChanged;
|
||||||
Timer? _loadDataOperation;
|
Timer? _loadDataOperation;
|
||||||
Timer? _saveDataOperation;
|
Timer? _saveDataOperation;
|
||||||
|
|
||||||
String get viewId => cellContext.viewId;
|
String get viewId => _cellContext.viewId;
|
||||||
|
|
||||||
RowId get rowId => cellContext.rowId;
|
RowId get rowId => _cellContext.rowId;
|
||||||
|
|
||||||
String get fieldId => cellContext.fieldInfo.id;
|
String get fieldId => _cellContext.fieldInfo.id;
|
||||||
|
|
||||||
FieldInfo get fieldInfo => cellContext.fieldInfo;
|
FieldInfo get fieldInfo => _cellContext.fieldInfo;
|
||||||
|
|
||||||
FieldType get fieldType => cellContext.fieldInfo.fieldType;
|
FieldType get fieldType => _cellContext.fieldInfo.fieldType;
|
||||||
|
|
||||||
|
String? get emoji => _cellContext.emoji;
|
||||||
|
|
||||||
CellController({
|
CellController({
|
||||||
required this.cellContext,
|
required DatabaseCellContext cellContext,
|
||||||
required CellCache cellCache,
|
required CellCache cellCache,
|
||||||
required CellDataLoader<T> cellDataLoader,
|
required CellDataLoader<T> cellDataLoader,
|
||||||
required CellDataPersistence<D> cellDataPersistence,
|
required CellDataPersistence<D> cellDataPersistence,
|
||||||
}) : _cellCache = cellCache,
|
}) : _cellContext = cellContext,
|
||||||
|
_cellCache = cellCache,
|
||||||
_cellDataLoader = cellDataLoader,
|
_cellDataLoader = cellDataLoader,
|
||||||
_cellDataPersistence = cellDataPersistence,
|
_cellDataPersistence = cellDataPersistence,
|
||||||
|
_rowMetaListener = RowMetaListener(cellContext.rowId),
|
||||||
_fieldListener = SingleFieldListener(fieldId: cellContext.fieldId),
|
_fieldListener = SingleFieldListener(fieldId: cellContext.fieldId),
|
||||||
_fieldBackendSvc = FieldBackendService(
|
_fieldBackendSvc = FieldBackendService(
|
||||||
viewId: cellContext.viewId,
|
viewId: cellContext.viewId,
|
||||||
@ -84,10 +91,8 @@ 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(
|
_fieldListener?.start(
|
||||||
onFieldChanged: (result) {
|
onFieldChanged: (fieldPB) {
|
||||||
result.fold(
|
|
||||||
(fieldPB) {
|
|
||||||
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
|
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
|
||||||
/// For example:
|
/// For example:
|
||||||
/// ¥12 -> $12
|
/// ¥12 -> $12
|
||||||
@ -96,8 +101,12 @@ class CellController<T, D> extends Equatable {
|
|||||||
}
|
}
|
||||||
_onCellFieldChanged?.call();
|
_onCellFieldChanged?.call();
|
||||||
},
|
},
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_rowMetaListener?.start(
|
||||||
|
callback: (newRowMeta) {
|
||||||
|
_cellContext = _cellContext.copyWith(rowMeta: newRowMeta);
|
||||||
|
_onRowMetaChanged?.call();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -105,9 +114,11 @@ class CellController<T, D> extends Equatable {
|
|||||||
/// Listen on the cell content or field changes
|
/// Listen on the cell content or field changes
|
||||||
VoidCallback? startListening({
|
VoidCallback? startListening({
|
||||||
required void Function(T?) onCellChanged,
|
required void Function(T?) onCellChanged,
|
||||||
|
VoidCallback? onRowMetaChanged,
|
||||||
VoidCallback? onCellFieldChanged,
|
VoidCallback? onCellFieldChanged,
|
||||||
}) {
|
}) {
|
||||||
_onCellFieldChanged = onCellFieldChanged;
|
_onCellFieldChanged = onCellFieldChanged;
|
||||||
|
_onRowMetaChanged = onRowMetaChanged;
|
||||||
|
|
||||||
/// Notify the listener, the cell data was changed.
|
/// Notify the listener, the cell data was changed.
|
||||||
onCellChangedFn() => onCellChanged(_cellDataNotifier?.value);
|
onCellChangedFn() => onCellChanged(_cellDataNotifier?.value);
|
||||||
@ -186,18 +197,26 @@ class CellController<T, D> extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
|
await _rowMetaListener?.stop();
|
||||||
|
_rowMetaListener = null;
|
||||||
|
|
||||||
await _cellListener?.stop();
|
await _cellListener?.stop();
|
||||||
|
_cellListener = null;
|
||||||
|
|
||||||
|
await _fieldListener?.stop();
|
||||||
|
_fieldListener = null;
|
||||||
|
|
||||||
_loadDataOperation?.cancel();
|
_loadDataOperation?.cancel();
|
||||||
_saveDataOperation?.cancel();
|
_saveDataOperation?.cancel();
|
||||||
_cellDataNotifier?.dispose();
|
_cellDataNotifier?.dispose();
|
||||||
await _fieldListener.stop();
|
|
||||||
_cellDataNotifier = null;
|
_cellDataNotifier = null;
|
||||||
|
_onRowMetaChanged = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [
|
List<Object> get props => [
|
||||||
_cellCache.get(_cacheKey) ?? "",
|
_cellCache.get(_cacheKey) ?? "",
|
||||||
cellContext.rowId + cellContext.fieldInfo.id
|
_cellContext.rowId + _cellContext.fieldInfo.id
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
|
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/date_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
@ -52,18 +53,23 @@ class CellBackendService {
|
|||||||
class DatabaseCellContext with _$DatabaseCellContext {
|
class DatabaseCellContext with _$DatabaseCellContext {
|
||||||
const factory DatabaseCellContext({
|
const factory DatabaseCellContext({
|
||||||
required String viewId,
|
required String viewId,
|
||||||
required RowId rowId,
|
required RowMetaPB rowMeta,
|
||||||
required FieldInfo fieldInfo,
|
required FieldInfo fieldInfo,
|
||||||
}) = _DatabaseCellContext;
|
}) = _DatabaseCellContext;
|
||||||
|
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
const DatabaseCellContext._();
|
const DatabaseCellContext._();
|
||||||
|
|
||||||
|
String get rowId => rowMeta.id;
|
||||||
|
|
||||||
String get fieldId => fieldInfo.id;
|
String get fieldId => fieldInfo.id;
|
||||||
|
|
||||||
FieldType get fieldType => fieldInfo.fieldType;
|
FieldType get fieldType => fieldInfo.fieldType;
|
||||||
|
|
||||||
ValueKey key() {
|
ValueKey key() {
|
||||||
return ValueKey("$rowId$fieldId${fieldInfo.fieldType}");
|
return ValueKey("${rowMeta.id}$fieldId${fieldInfo.fieldType}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Only the primary field can have an emoji.
|
||||||
|
String? get emoji => fieldInfo.isPrimary ? rowMeta.icon : null;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
|
||||||
@ -7,17 +6,23 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
class ChecklistCellBackendService {
|
class ChecklistCellBackendService {
|
||||||
final DatabaseCellContext cellContext;
|
final String viewId;
|
||||||
|
final String fieldId;
|
||||||
|
final String rowId;
|
||||||
|
|
||||||
ChecklistCellBackendService({required this.cellContext});
|
ChecklistCellBackendService({
|
||||||
|
required this.viewId,
|
||||||
|
required this.fieldId,
|
||||||
|
required this.rowId,
|
||||||
|
});
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> create({
|
Future<Either<Unit, FlowyError>> create({
|
||||||
required String name,
|
required String name,
|
||||||
}) {
|
}) {
|
||||||
final payload = ChecklistCellDataChangesetPB.create()
|
final payload = ChecklistCellDataChangesetPB.create()
|
||||||
..viewId = cellContext.viewId
|
..viewId = viewId
|
||||||
..fieldId = cellContext.fieldInfo.id
|
..fieldId = fieldId
|
||||||
..rowId = cellContext.rowId
|
..rowId = rowId
|
||||||
..insertOptions.add(name);
|
..insertOptions.add(name);
|
||||||
|
|
||||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||||
@ -27,9 +32,9 @@ class ChecklistCellBackendService {
|
|||||||
required List<String> optionIds,
|
required List<String> optionIds,
|
||||||
}) {
|
}) {
|
||||||
final payload = ChecklistCellDataChangesetPB.create()
|
final payload = ChecklistCellDataChangesetPB.create()
|
||||||
..viewId = cellContext.viewId
|
..viewId = viewId
|
||||||
..fieldId = cellContext.fieldInfo.id
|
..fieldId = fieldId
|
||||||
..rowId = cellContext.rowId
|
..rowId = rowId
|
||||||
..deleteOptionIds.addAll(optionIds);
|
..deleteOptionIds.addAll(optionIds);
|
||||||
|
|
||||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||||
@ -39,9 +44,9 @@ class ChecklistCellBackendService {
|
|||||||
required String optionId,
|
required String optionId,
|
||||||
}) {
|
}) {
|
||||||
final payload = ChecklistCellDataChangesetPB.create()
|
final payload = ChecklistCellDataChangesetPB.create()
|
||||||
..viewId = cellContext.viewId
|
..viewId = viewId
|
||||||
..fieldId = cellContext.fieldInfo.id
|
..fieldId = fieldId
|
||||||
..rowId = cellContext.rowId
|
..rowId = rowId
|
||||||
..selectedOptionIds.add(optionId);
|
..selectedOptionIds.add(optionId);
|
||||||
|
|
||||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||||
@ -51,9 +56,9 @@ class ChecklistCellBackendService {
|
|||||||
required SelectOptionPB option,
|
required SelectOptionPB option,
|
||||||
}) {
|
}) {
|
||||||
final payload = ChecklistCellDataChangesetPB.create()
|
final payload = ChecklistCellDataChangesetPB.create()
|
||||||
..viewId = cellContext.viewId
|
..viewId = viewId
|
||||||
..fieldId = cellContext.fieldInfo.id
|
..fieldId = fieldId
|
||||||
..rowId = cellContext.rowId
|
..rowId = rowId
|
||||||
..updateOptions.add(option);
|
..updateOptions.add(option);
|
||||||
|
|
||||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||||
@ -61,10 +66,9 @@ class ChecklistCellBackendService {
|
|||||||
|
|
||||||
Future<Either<ChecklistCellDataPB, FlowyError>> getCellData() {
|
Future<Either<ChecklistCellDataPB, FlowyError>> getCellData() {
|
||||||
final payload = CellIdPB.create()
|
final payload = CellIdPB.create()
|
||||||
..fieldId = cellContext.fieldInfo.id
|
..viewId = viewId
|
||||||
..viewId = cellContext.viewId
|
..fieldId = fieldId
|
||||||
..rowId = cellContext.rowId
|
..rowId = rowId;
|
||||||
..rowId = cellContext.rowId;
|
|
||||||
|
|
||||||
return DatabaseEventGetChecklistCellData(payload).send();
|
return DatabaseEventGetChecklistCellData(payload).send();
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
|
||||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
@ -8,12 +6,15 @@ 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/cell_entities.pb.dart';
|
||||||
|
|
||||||
class SelectOptionCellBackendService {
|
class SelectOptionCellBackendService {
|
||||||
final DatabaseCellContext cellContext;
|
final String viewId;
|
||||||
SelectOptionCellBackendService({required this.cellContext});
|
final String fieldId;
|
||||||
|
final String rowId;
|
||||||
|
|
||||||
String get viewId => cellContext.viewId;
|
SelectOptionCellBackendService({
|
||||||
String get fieldId => cellContext.fieldInfo.id;
|
required this.viewId,
|
||||||
RowId get rowId => cellContext.rowId;
|
required this.fieldId,
|
||||||
|
required this.rowId,
|
||||||
|
});
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> create({
|
Future<Either<Unit, FlowyError>> create({
|
||||||
required String name,
|
required String name,
|
||||||
|
@ -160,7 +160,7 @@ class DatabaseController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<RowPB, FlowyError>> createRow({
|
Future<Either<RowMetaPB, FlowyError>> createRow({
|
||||||
RowId? startRowId,
|
RowId? startRowId,
|
||||||
String? groupId,
|
String? groupId,
|
||||||
void Function(RowDataBuilder builder)? withCells,
|
void Function(RowDataBuilder builder)? withCells,
|
||||||
@ -181,9 +181,9 @@ class DatabaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> moveGroupRow({
|
Future<Either<Unit, FlowyError>> moveGroupRow({
|
||||||
required RowPB fromRow,
|
required RowMetaPB fromRow,
|
||||||
required String groupId,
|
required String groupId,
|
||||||
RowPB? toRow,
|
RowMetaPB? toRow,
|
||||||
}) {
|
}) {
|
||||||
return _databaseViewBackendSvc.moveGroupRow(
|
return _databaseViewBackendSvc.moveGroupRow(
|
||||||
fromRowId: fromRow.id,
|
fromRowId: fromRow.id,
|
||||||
@ -193,12 +193,12 @@ class DatabaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> moveRow({
|
Future<Either<Unit, FlowyError>> moveRow({
|
||||||
required RowPB fromRow,
|
required String fromRowId,
|
||||||
required RowPB toRow,
|
required String toRowId,
|
||||||
}) {
|
}) {
|
||||||
return _databaseViewBackendSvc.moveRow(
|
return _databaseViewBackendSvc.moveRow(
|
||||||
fromRowId: fromRow.id,
|
fromRowId: fromRowId,
|
||||||
toRowId: toRow.id,
|
toRowId: toRowId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,8 +269,8 @@ class DatabaseController {
|
|||||||
onRowsDeleted: (ids) {
|
onRowsDeleted: (ids) {
|
||||||
_databaseCallbacks?.onRowsDeleted?.call(ids);
|
_databaseCallbacks?.onRowsDeleted?.call(ids);
|
||||||
},
|
},
|
||||||
onRowsUpdated: (ids) {
|
onRowsUpdated: (ids, reason) {
|
||||||
_databaseCallbacks?.onRowsUpdated?.call(ids);
|
_databaseCallbacks?.onRowsUpdated?.call(ids, reason);
|
||||||
},
|
},
|
||||||
onRowsCreated: (ids) {
|
onRowsCreated: (ids) {
|
||||||
_databaseCallbacks?.onRowsCreated?.call(ids);
|
_databaseCallbacks?.onRowsCreated?.call(ids);
|
||||||
|
@ -30,7 +30,7 @@ class DatabaseViewBackendService {
|
|||||||
return DatabaseEventGetDatabase(payload).send();
|
return DatabaseEventGetDatabase(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<RowPB, FlowyError>> createRow({
|
Future<Either<RowMetaPB, FlowyError>> createRow({
|
||||||
RowId? startRowId,
|
RowId? startRowId,
|
||||||
String? groupId,
|
String? groupId,
|
||||||
Map<String, String>? cellDataByFieldId,
|
Map<String, String>? cellDataByFieldId,
|
||||||
|
@ -13,7 +13,10 @@ typedef OnFiltersChanged = void Function(List<FilterInfo>);
|
|||||||
typedef OnDatabaseChanged = void Function(DatabasePB);
|
typedef OnDatabaseChanged = void Function(DatabasePB);
|
||||||
|
|
||||||
typedef OnRowsCreated = void Function(List<RowId> ids);
|
typedef OnRowsCreated = void Function(List<RowId> ids);
|
||||||
typedef OnRowsUpdated = void Function(List<RowId> ids);
|
typedef OnRowsUpdated = void Function(
|
||||||
|
List<RowId> ids,
|
||||||
|
RowsChangedReason reason,
|
||||||
|
);
|
||||||
typedef OnRowsDeleted = void Function(List<RowId> ids);
|
typedef OnRowsDeleted = void Function(List<RowId> ids);
|
||||||
typedef OnNumOfRowsChanged = void Function(
|
typedef OnNumOfRowsChanged = void Function(
|
||||||
UnmodifiableListView<RowInfo> rows,
|
UnmodifiableListView<RowInfo> rows,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
@ -52,14 +51,11 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
|||||||
|
|
||||||
void _startListening() {
|
void _startListening() {
|
||||||
_fieldListener.start(
|
_fieldListener.start(
|
||||||
onFieldChanged: (result) {
|
onFieldChanged: (updatedField) {
|
||||||
if (isClosed) {
|
if (isClosed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
result.fold(
|
add(FieldCellEvent.didReceiveFieldUpdate(updatedField));
|
||||||
(field) => add(FieldCellEvent.didReceiveFieldUpdate(field)),
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';
|
||||||
@ -7,12 +8,11 @@ import 'dart:async';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||||
|
|
||||||
typedef UpdateFieldNotifiedValue = Either<FieldPB, FlowyError>;
|
typedef UpdateFieldNotifiedValue = FieldPB;
|
||||||
|
|
||||||
class SingleFieldListener {
|
class SingleFieldListener {
|
||||||
final String fieldId;
|
final String fieldId;
|
||||||
PublishNotifier<UpdateFieldNotifiedValue>? _updateFieldNotifier =
|
void Function(UpdateFieldNotifiedValue)? _updateFieldNotifier;
|
||||||
PublishNotifier();
|
|
||||||
DatabaseNotificationListener? _listener;
|
DatabaseNotificationListener? _listener;
|
||||||
|
|
||||||
SingleFieldListener({required this.fieldId});
|
SingleFieldListener({required this.fieldId});
|
||||||
@ -20,7 +20,7 @@ class SingleFieldListener {
|
|||||||
void start({
|
void start({
|
||||||
required void Function(UpdateFieldNotifiedValue) onFieldChanged,
|
required void Function(UpdateFieldNotifiedValue) onFieldChanged,
|
||||||
}) {
|
}) {
|
||||||
_updateFieldNotifier?.addPublishListener(onFieldChanged);
|
_updateFieldNotifier = onFieldChanged;
|
||||||
_listener = DatabaseNotificationListener(
|
_listener = DatabaseNotificationListener(
|
||||||
objectId: fieldId,
|
objectId: fieldId,
|
||||||
handler: _handler,
|
handler: _handler,
|
||||||
@ -34,9 +34,8 @@ class SingleFieldListener {
|
|||||||
switch (ty) {
|
switch (ty) {
|
||||||
case DatabaseNotification.DidUpdateField:
|
case DatabaseNotification.DidUpdateField:
|
||||||
result.fold(
|
result.fold(
|
||||||
(payload) =>
|
(payload) => _updateFieldNotifier?.call(FieldPB.fromBuffer(payload)),
|
||||||
_updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)),
|
(error) => Log.error(error),
|
||||||
(error) => _updateFieldNotifier?.value = right(error),
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -46,7 +45,6 @@ class SingleFieldListener {
|
|||||||
|
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
await _listener?.stop();
|
await _listener?.stop();
|
||||||
_updateFieldNotifier?.dispose();
|
|
||||||
_updateFieldNotifier = null;
|
_updateFieldNotifier = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,6 +99,14 @@ class FieldBackendService {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -0,0 +1,163 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_listener.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/prelude.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-folder2/view.pb.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import 'row_meta_listener.dart';
|
||||||
|
|
||||||
|
part 'row_banner_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {
|
||||||
|
final String viewId;
|
||||||
|
final RowBackendService _rowBackendSvc;
|
||||||
|
final RowMetaListener _metaListener;
|
||||||
|
SingleFieldListener? _fieldListener;
|
||||||
|
|
||||||
|
RowBannerBloc({
|
||||||
|
required this.viewId,
|
||||||
|
required RowMetaPB rowMeta,
|
||||||
|
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||||
|
_metaListener = RowMetaListener(rowMeta.id),
|
||||||
|
super(RowBannerState.initial(rowMeta)) {
|
||||||
|
on<RowBannerEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
event.when(
|
||||||
|
initial: () async {
|
||||||
|
_loadPrimaryField();
|
||||||
|
await _listenRowMeteChanged();
|
||||||
|
},
|
||||||
|
didReceiveRowMeta: (RowMetaPB rowMeta) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
rowMeta: rowMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setCover: (String coverURL) {
|
||||||
|
_updateMeta(coverURL: coverURL);
|
||||||
|
},
|
||||||
|
setIcon: (String iconURL) {
|
||||||
|
_updateMeta(iconURL: iconURL);
|
||||||
|
},
|
||||||
|
didReceiveFieldUpdate: (updatedField) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
primaryField: updatedField,
|
||||||
|
loadingState: const LoadingState.finish(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _metaListener.stop();
|
||||||
|
await _fieldListener?.stop();
|
||||||
|
_fieldListener = null;
|
||||||
|
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPrimaryField() async {
|
||||||
|
final fieldOrError =
|
||||||
|
await FieldBackendService.getPrimaryField(viewId: viewId);
|
||||||
|
fieldOrError.fold(
|
||||||
|
(primaryField) {
|
||||||
|
if (!isClosed) {
|
||||||
|
_fieldListener = SingleFieldListener(fieldId: primaryField.id);
|
||||||
|
_fieldListener?.start(
|
||||||
|
onFieldChanged: (updatedField) {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(RowBannerEvent.didReceiveFieldUpdate(updatedField));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
add(RowBannerEvent.didReceiveFieldUpdate(primaryField));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(r) => Log.error(r),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listen the changes of the row meta and then update the banner
|
||||||
|
Future<void> _listenRowMeteChanged() async {
|
||||||
|
_metaListener.start(
|
||||||
|
callback: (rowMeta) {
|
||||||
|
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(
|
||||||
|
iconURL: iconURL,
|
||||||
|
coverURL: coverURL,
|
||||||
|
rowId: state.rowMeta.id,
|
||||||
|
)
|
||||||
|
.then((result) {
|
||||||
|
result.fold(
|
||||||
|
(l) => null,
|
||||||
|
(err) => Log.error(err),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the icon and cover of the view
|
||||||
|
ViewBackendService.updateView(
|
||||||
|
viewId: viewId,
|
||||||
|
iconURL: iconURL,
|
||||||
|
coverURL: coverURL,
|
||||||
|
).then((result) {
|
||||||
|
result.fold(
|
||||||
|
(l) => null,
|
||||||
|
(err) => Log.error(err),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class RowBannerEvent with _$RowBannerEvent {
|
||||||
|
const factory RowBannerEvent.initial() = _Initial;
|
||||||
|
const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) =
|
||||||
|
_DidReceiveRowMeta;
|
||||||
|
const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) =
|
||||||
|
_DidReceiveFieldUdate;
|
||||||
|
const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon;
|
||||||
|
const factory RowBannerEvent.setCover(String coverURL) = _SetCover;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class RowBannerState with _$RowBannerState {
|
||||||
|
const factory RowBannerState({
|
||||||
|
ViewPB? view,
|
||||||
|
FieldPB? primaryField,
|
||||||
|
required RowMetaPB rowMeta,
|
||||||
|
required LoadingState loadingState,
|
||||||
|
}) = _RowBannerState;
|
||||||
|
|
||||||
|
factory RowBannerState.initial(RowMetaPB rowMetaPB) => RowBannerState(
|
||||||
|
rowMeta: rowMetaPB,
|
||||||
|
loadingState: const LoadingState.loading(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class LoadingState with _$LoadingState {
|
||||||
|
const factory LoadingState.loading() = _Loading;
|
||||||
|
const factory LoadingState.finish() = _Finish;
|
||||||
|
}
|
@ -38,17 +38,21 @@ class RowCache {
|
|||||||
final RowCacheDelegate _delegate;
|
final RowCacheDelegate _delegate;
|
||||||
final RowChangesetNotifier _rowChangeReasonNotifier;
|
final RowChangesetNotifier _rowChangeReasonNotifier;
|
||||||
|
|
||||||
|
/// Returns a unmodifiable list of RowInfo
|
||||||
UnmodifiableListView<RowInfo> get rowInfos {
|
UnmodifiableListView<RowInfo> get rowInfos {
|
||||||
final visibleRows = [..._rowList.rows];
|
final visibleRows = [..._rowList.rows];
|
||||||
return UnmodifiableListView(visibleRows);
|
return UnmodifiableListView(visibleRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a unmodifiable map of rowId to RowInfo
|
||||||
UnmodifiableMapView<RowId, RowInfo> get rowByRowId {
|
UnmodifiableMapView<RowId, RowInfo> get rowByRowId {
|
||||||
return UnmodifiableMapView(_rowList.rowInfoByRowId);
|
return UnmodifiableMapView(_rowList.rowInfoByRowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
CellCache get cellCache => _cellCache;
|
CellCache get cellCache => _cellCache;
|
||||||
|
|
||||||
|
RowsChangedReason get changeReason => _rowChangeReasonNotifier.reason;
|
||||||
|
|
||||||
RowCache({
|
RowCache({
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
required RowFieldsDelegate fieldsDelegate,
|
required RowFieldsDelegate fieldsDelegate,
|
||||||
@ -70,7 +74,7 @@ class RowCache {
|
|||||||
return _rowList.get(rowId);
|
return _rowList.get(rowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setInitialRows(List<RowPB> rows) {
|
void setInitialRows(List<RowMetaPB> rows) {
|
||||||
for (final row in rows) {
|
for (final row in rows) {
|
||||||
final rowInfo = buildGridRow(row);
|
final rowInfo = buildGridRow(row);
|
||||||
_rowList.add(rowInfo);
|
_rowList.add(rowInfo);
|
||||||
@ -128,7 +132,7 @@ class RowCache {
|
|||||||
void _insertRows(List<InsertedRowPB> insertRows) {
|
void _insertRows(List<InsertedRowPB> insertRows) {
|
||||||
for (final insertedRow in insertRows) {
|
for (final insertedRow in insertRows) {
|
||||||
final insertedIndex =
|
final insertedIndex =
|
||||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
|
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
|
||||||
if (insertedIndex != null) {
|
if (insertedIndex != null) {
|
||||||
_rowChangeReasonNotifier
|
_rowChangeReasonNotifier
|
||||||
.receive(RowsChangedReason.insert(insertedIndex));
|
.receive(RowsChangedReason.insert(insertedIndex));
|
||||||
@ -138,20 +142,23 @@ class RowCache {
|
|||||||
|
|
||||||
void _updateRows(List<UpdatedRowPB> updatedRows) {
|
void _updateRows(List<UpdatedRowPB> updatedRows) {
|
||||||
if (updatedRows.isEmpty) return;
|
if (updatedRows.isEmpty) return;
|
||||||
final List<RowPB> rowPBs = [];
|
final List<RowMetaPB> updatedList = [];
|
||||||
for (final updatedRow in updatedRows) {
|
for (final updatedRow in updatedRows) {
|
||||||
for (final fieldId in updatedRow.fieldIds) {
|
for (final fieldId in updatedRow.fieldIds) {
|
||||||
final key = CellCacheKey(
|
final key = CellCacheKey(
|
||||||
fieldId: fieldId,
|
fieldId: fieldId,
|
||||||
rowId: updatedRow.row.id,
|
rowId: updatedRow.rowId,
|
||||||
);
|
);
|
||||||
_cellCache.remove(key);
|
_cellCache.remove(key);
|
||||||
}
|
}
|
||||||
rowPBs.add(updatedRow.row);
|
if (updatedRow.hasRowMeta()) {
|
||||||
|
updatedList.add(updatedRow.rowMeta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final updatedIndexs =
|
final updatedIndexs =
|
||||||
_rowList.updateRows(rowPBs, (rowPB) => buildGridRow(rowPB));
|
_rowList.updateRows(updatedList, (rowId) => buildGridRow(rowId));
|
||||||
|
|
||||||
if (updatedIndexs.isNotEmpty) {
|
if (updatedIndexs.isNotEmpty) {
|
||||||
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
|
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
|
||||||
}
|
}
|
||||||
@ -169,7 +176,7 @@ class RowCache {
|
|||||||
void _showRows(List<InsertedRowPB> visibleRows) {
|
void _showRows(List<InsertedRowPB> visibleRows) {
|
||||||
for (final insertedRow in visibleRows) {
|
for (final insertedRow in visibleRows) {
|
||||||
final insertedIndex =
|
final insertedIndex =
|
||||||
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
|
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));
|
||||||
if (insertedIndex != null) {
|
if (insertedIndex != null) {
|
||||||
_rowChangeReasonNotifier
|
_rowChangeReasonNotifier
|
||||||
.receive(RowsChangedReason.insert(insertedIndex));
|
.receive(RowsChangedReason.insert(insertedIndex));
|
||||||
@ -197,8 +204,9 @@ class RowCache {
|
|||||||
if (onCellUpdated != null) {
|
if (onCellUpdated != null) {
|
||||||
final rowInfo = _rowList.get(rowId);
|
final rowInfo = _rowList.get(rowId);
|
||||||
if (rowInfo != null) {
|
if (rowInfo != null) {
|
||||||
final CellContextByFieldId cellDataMap =
|
final CellContextByFieldId cellDataMap = _makeGridCells(
|
||||||
_makeGridCells(rowId, rowInfo.rowPB);
|
rowInfo.rowMeta,
|
||||||
|
);
|
||||||
onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
|
onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -220,12 +228,12 @@ class RowCache {
|
|||||||
_rowChangeReasonNotifier.removeListener(callback);
|
_rowChangeReasonNotifier.removeListener(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
CellContextByFieldId loadGridCells(RowId rowId) {
|
CellContextByFieldId loadGridCells(RowMetaPB rowMeta) {
|
||||||
final RowPB? data = _rowList.get(rowId)?.rowPB;
|
final rowInfo = _rowList.get(rowMeta.id);
|
||||||
if (data == null) {
|
if (rowInfo == null) {
|
||||||
_loadRow(rowId);
|
_loadRow(rowMeta.id);
|
||||||
}
|
}
|
||||||
return _makeGridCells(rowId, data);
|
return _makeGridCells(rowMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadRow(RowId rowId) async {
|
Future<void> _loadRow(RowId rowId) async {
|
||||||
@ -233,57 +241,51 @@ class RowCache {
|
|||||||
..viewId = viewId
|
..viewId = viewId
|
||||||
..rowId = rowId;
|
..rowId = rowId;
|
||||||
|
|
||||||
final result = await DatabaseEventGetRow(payload).send();
|
final result = await DatabaseEventGetRowMeta(payload).send();
|
||||||
result.fold(
|
result.fold(
|
||||||
(optionRow) => _refreshRow(optionRow),
|
(rowMetaPB) {
|
||||||
|
final rowInfo = _rowList.get(rowMetaPB.id);
|
||||||
|
final rowIndex = _rowList.indexOfRow(rowMetaPB.id);
|
||||||
|
if (rowInfo != null && rowIndex != null) {
|
||||||
|
final updatedRowInfo = rowInfo.copyWith(rowMeta: rowMetaPB);
|
||||||
|
_rowList.remove(rowMetaPB.id);
|
||||||
|
_rowList.insert(rowIndex, updatedRowInfo);
|
||||||
|
|
||||||
|
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
|
||||||
|
updatedIndexs[rowMetaPB.id] = UpdatedIndex(
|
||||||
|
index: rowIndex,
|
||||||
|
rowId: rowMetaPB.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
_rowChangeReasonNotifier
|
||||||
|
.receive(RowsChangedReason.update(updatedIndexs));
|
||||||
|
}
|
||||||
|
},
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
CellContextByFieldId _makeGridCells(RowId rowId, RowPB? row) {
|
CellContextByFieldId _makeGridCells(RowMetaPB rowMeta) {
|
||||||
// ignore: prefer_collection_literals
|
// ignore: prefer_collection_literals
|
||||||
final cellDataMap = CellContextByFieldId();
|
final cellContextMap = CellContextByFieldId();
|
||||||
for (final field in _delegate.fields) {
|
for (final field in _delegate.fields) {
|
||||||
if (field.visibility) {
|
if (field.visibility) {
|
||||||
cellDataMap[field.id] = DatabaseCellContext(
|
cellContextMap[field.id] = DatabaseCellContext(
|
||||||
rowId: rowId,
|
rowMeta: rowMeta,
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
fieldInfo: field,
|
fieldInfo: field,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cellDataMap;
|
return cellContextMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _refreshRow(OptionalRowPB optionRow) {
|
RowInfo buildGridRow(RowMetaPB rowMetaPB) {
|
||||||
if (!optionRow.hasRow()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final updatedRow = optionRow.row;
|
|
||||||
updatedRow.freeze();
|
|
||||||
|
|
||||||
final rowInfo = _rowList.get(updatedRow.id);
|
|
||||||
final rowIndex = _rowList.indexOfRow(updatedRow.id);
|
|
||||||
if (rowInfo != null && rowIndex != null) {
|
|
||||||
final updatedRowInfo = rowInfo.copyWith(rowPB: updatedRow);
|
|
||||||
_rowList.remove(updatedRow.id);
|
|
||||||
_rowList.insert(rowIndex, updatedRowInfo);
|
|
||||||
|
|
||||||
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
|
|
||||||
updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
|
|
||||||
index: rowIndex,
|
|
||||||
rowId: updatedRowInfo.rowPB.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowInfo buildGridRow(RowPB rowPB) {
|
|
||||||
return RowInfo(
|
return RowInfo(
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
fields: _delegate.fields,
|
fields: _delegate.fields,
|
||||||
rowPB: rowPB,
|
rowId: rowMetaPB.id,
|
||||||
|
rowMeta: rowMetaPB,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,9 +312,10 @@ class RowChangesetNotifier extends ChangeNotifier {
|
|||||||
@unfreezed
|
@unfreezed
|
||||||
class RowInfo with _$RowInfo {
|
class RowInfo with _$RowInfo {
|
||||||
factory RowInfo({
|
factory RowInfo({
|
||||||
|
required String rowId,
|
||||||
required String viewId,
|
required String viewId,
|
||||||
required UnmodifiableListView<FieldInfo> fields,
|
required UnmodifiableListView<FieldInfo> fields,
|
||||||
required RowPB rowPB,
|
required RowMetaPB rowMeta,
|
||||||
}) = _RowInfo;
|
}) = _RowInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../cell/cell_service.dart';
|
import '../cell/cell_service.dart';
|
||||||
import 'row_cache.dart';
|
import 'row_cache.dart';
|
||||||
import 'row_service.dart';
|
|
||||||
|
|
||||||
typedef OnRowChanged = void Function(CellContextByFieldId, RowsChangedReason);
|
typedef OnRowChanged = void Function(CellContextByFieldId, RowsChangedReason);
|
||||||
|
|
||||||
class RowController {
|
class RowController {
|
||||||
final RowId rowId;
|
final RowMetaPB rowMeta;
|
||||||
final String? groupId;
|
final String? groupId;
|
||||||
final String viewId;
|
final String viewId;
|
||||||
final List<VoidCallback> _onRowChangedListeners = [];
|
final List<VoidCallback> _onRowChangedListeners = [];
|
||||||
@ -14,24 +14,27 @@ class RowController {
|
|||||||
|
|
||||||
get cellCache => _rowCache.cellCache;
|
get cellCache => _rowCache.cellCache;
|
||||||
|
|
||||||
|
get rowId => rowMeta.id;
|
||||||
|
|
||||||
RowController({
|
RowController({
|
||||||
required this.rowId,
|
required this.rowMeta,
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
required RowCache rowCache,
|
required RowCache rowCache,
|
||||||
this.groupId,
|
this.groupId,
|
||||||
}) : _rowCache = rowCache;
|
}) : _rowCache = rowCache;
|
||||||
|
|
||||||
CellContextByFieldId loadData() {
|
CellContextByFieldId loadData() {
|
||||||
return _rowCache.loadGridCells(rowId);
|
return _rowCache.loadGridCells(rowMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addListener({OnRowChanged? onRowChanged}) {
|
void addListener({OnRowChanged? onRowChanged}) {
|
||||||
_onRowChangedListeners.add(
|
final fn = _rowCache.addListener(
|
||||||
_rowCache.addListener(
|
rowId: rowMeta.id,
|
||||||
rowId: rowId,
|
|
||||||
onCellUpdated: onRowChanged,
|
onCellUpdated: onRowChanged,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add the listener to the list so that we can remove it later.
|
||||||
|
_onRowChangedListeners.add(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
@ -25,10 +25,9 @@ class RowList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void add(RowInfo rowInfo) {
|
void add(RowInfo rowInfo) {
|
||||||
final rowId = rowInfo.rowPB.id;
|
final rowId = rowInfo.rowId;
|
||||||
if (contains(rowId)) {
|
if (contains(rowId)) {
|
||||||
final index =
|
final index = _rowInfos.indexWhere((element) => element.rowId == rowId);
|
||||||
_rowInfos.indexWhere((element) => element.rowPB.id == rowId);
|
|
||||||
_rowInfos.removeAt(index);
|
_rowInfos.removeAt(index);
|
||||||
_rowInfos.insert(index, rowInfo);
|
_rowInfos.insert(index, rowInfo);
|
||||||
} else {
|
} else {
|
||||||
@ -38,7 +37,7 @@ class RowList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InsertedIndex? insert(int index, RowInfo rowInfo) {
|
InsertedIndex? insert(int index, RowInfo rowInfo) {
|
||||||
final rowId = rowInfo.rowPB.id;
|
final rowId = rowInfo.rowId;
|
||||||
var insertedIndex = index;
|
var insertedIndex = index;
|
||||||
if (_rowInfos.length <= insertedIndex) {
|
if (_rowInfos.length <= insertedIndex) {
|
||||||
insertedIndex = _rowInfos.length;
|
insertedIndex = _rowInfos.length;
|
||||||
@ -62,7 +61,7 @@ class RowList {
|
|||||||
if (rowInfo != null) {
|
if (rowInfo != null) {
|
||||||
final index = _rowInfos.indexOf(rowInfo);
|
final index = _rowInfos.indexOf(rowInfo);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
rowInfoByRowId.remove(rowInfo.rowPB.id);
|
rowInfoByRowId.remove(rowInfo.rowId);
|
||||||
_rowInfos.remove(rowInfo);
|
_rowInfos.remove(rowInfo);
|
||||||
}
|
}
|
||||||
return DeletedIndex(index: index, rowInfo: rowInfo);
|
return DeletedIndex(index: index, rowInfo: rowInfo);
|
||||||
@ -73,23 +72,23 @@ class RowList {
|
|||||||
|
|
||||||
InsertedIndexs insertRows(
|
InsertedIndexs insertRows(
|
||||||
List<InsertedRowPB> insertedRows,
|
List<InsertedRowPB> insertedRows,
|
||||||
RowInfo Function(RowPB) builder,
|
RowInfo Function(RowMetaPB) builder,
|
||||||
) {
|
) {
|
||||||
final InsertedIndexs insertIndexs = [];
|
final InsertedIndexs insertIndexs = [];
|
||||||
for (final insertRow in insertedRows) {
|
for (final insertRow in insertedRows) {
|
||||||
final isContains = contains(insertRow.row.id);
|
final isContains = contains(insertRow.rowMeta.id);
|
||||||
|
|
||||||
var index = insertRow.index;
|
var index = insertRow.index;
|
||||||
if (_rowInfos.length < index) {
|
if (_rowInfos.length < index) {
|
||||||
index = _rowInfos.length;
|
index = _rowInfos.length;
|
||||||
}
|
}
|
||||||
insert(index, builder(insertRow.row));
|
insert(index, builder(insertRow.rowMeta));
|
||||||
|
|
||||||
if (!isContains) {
|
if (!isContains) {
|
||||||
insertIndexs.add(
|
insertIndexs.add(
|
||||||
InsertedIndex(
|
InsertedIndex(
|
||||||
index: index,
|
index: index,
|
||||||
rowId: insertRow.row.id,
|
rowId: insertRow.rowMeta.id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -105,10 +104,10 @@ class RowList {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_rowInfos.asMap().forEach((index, RowInfo rowInfo) {
|
_rowInfos.asMap().forEach((index, RowInfo rowInfo) {
|
||||||
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
|
if (deletedRowByRowId[rowInfo.rowId] == null) {
|
||||||
newRows.add(rowInfo);
|
newRows.add(rowInfo);
|
||||||
} else {
|
} else {
|
||||||
rowInfoByRowId.remove(rowInfo.rowPB.id);
|
rowInfoByRowId.remove(rowInfo.rowId);
|
||||||
deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
|
deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -117,19 +116,21 @@ class RowList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UpdatedIndexMap updateRows(
|
UpdatedIndexMap updateRows(
|
||||||
List<RowPB> updatedRows,
|
List<RowMetaPB> rowMetas,
|
||||||
RowInfo Function(RowPB) builder,
|
RowInfo Function(RowMetaPB) builder,
|
||||||
) {
|
) {
|
||||||
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
|
final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
|
||||||
for (final RowPB updatedRow in updatedRows) {
|
for (final rowMeta in rowMetas) {
|
||||||
final rowId = updatedRow.id;
|
|
||||||
final index = _rowInfos.indexWhere(
|
final index = _rowInfos.indexWhere(
|
||||||
(rowInfo) => rowInfo.rowPB.id == rowId,
|
(rowInfo) => rowInfo.rowId == rowMeta.id,
|
||||||
);
|
);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
final rowInfo = builder(updatedRow);
|
final rowInfo = builder(rowMeta);
|
||||||
insert(index, rowInfo);
|
insert(index, rowInfo);
|
||||||
updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
|
updatedIndexs[rowMeta.id] = UpdatedIndex(
|
||||||
|
index: index,
|
||||||
|
rowId: rowMeta.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return updatedIndexs;
|
return updatedIndexs;
|
||||||
@ -148,7 +149,7 @@ class RowList {
|
|||||||
|
|
||||||
void moveRow(RowId rowId, int oldIndex, int newIndex) {
|
void moveRow(RowId rowId, int oldIndex, int newIndex) {
|
||||||
final index = _rowInfos.indexWhere(
|
final index = _rowInfos.indexWhere(
|
||||||
(rowInfo) => rowInfo.rowPB.id == rowId,
|
(rowInfo) => rowInfo.rowId == rowId,
|
||||||
);
|
);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
final rowInfo = remove(rowId)!.rowInfo;
|
final rowInfo = remove(rowId)!.rowInfo;
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
|
typedef RowMetaCallback = void Function(RowMetaPB);
|
||||||
|
|
||||||
|
class RowMetaListener {
|
||||||
|
final String rowId;
|
||||||
|
RowMetaCallback? _callback;
|
||||||
|
DatabaseNotificationListener? _listener;
|
||||||
|
RowMetaListener(this.rowId);
|
||||||
|
|
||||||
|
void start({required RowMetaCallback callback}) {
|
||||||
|
_callback = callback;
|
||||||
|
_listener = DatabaseNotificationListener(
|
||||||
|
objectId: rowId,
|
||||||
|
handler: _handler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handler(
|
||||||
|
DatabaseNotification ty,
|
||||||
|
Either<Uint8List, FlowyError> result,
|
||||||
|
) {
|
||||||
|
switch (ty) {
|
||||||
|
case DatabaseNotification.DidUpdateRowMeta:
|
||||||
|
result.fold(
|
||||||
|
(payload) {
|
||||||
|
if (_callback != null) {
|
||||||
|
_callback!(RowMetaPB.fromBuffer(payload));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => Log.error(error),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop() async {
|
||||||
|
await _listener?.stop();
|
||||||
|
_callback = null;
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@ class RowBackendService {
|
|||||||
required this.viewId,
|
required this.viewId,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<Either<RowPB, FlowyError>> createRow(RowId rowId) {
|
Future<Either<RowMetaPB, FlowyError>> createRowAfterRow(RowId rowId) {
|
||||||
final payload = CreateRowPayloadPB.create()
|
final payload = CreateRowPayloadPB.create()
|
||||||
..viewId = viewId
|
..viewId = viewId
|
||||||
..startRowId = rowId;
|
..startRowId = rowId;
|
||||||
@ -28,6 +28,33 @@ class RowBackendService {
|
|||||||
return DatabaseEventGetRow(payload).send();
|
return DatabaseEventGetRow(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Either<RowMetaPB, FlowyError>> getRowMeta(RowId rowId) {
|
||||||
|
final payload = RowIdPB.create()
|
||||||
|
..viewId = viewId
|
||||||
|
..rowId = rowId;
|
||||||
|
|
||||||
|
return DatabaseEventGetRowMeta(payload).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Either<Unit, FlowyError>> updateMeta({
|
||||||
|
required String rowId,
|
||||||
|
String? iconURL,
|
||||||
|
String? coverURL,
|
||||||
|
}) {
|
||||||
|
final payload = UpdateRowMetaChangesetPB.create()
|
||||||
|
..viewId = viewId
|
||||||
|
..id = rowId;
|
||||||
|
|
||||||
|
if (iconURL != null) {
|
||||||
|
payload.iconUrl = iconURL;
|
||||||
|
}
|
||||||
|
if (coverURL != null) {
|
||||||
|
payload.coverUrl = coverURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DatabaseEventUpdateRowMeta(payload).send();
|
||||||
|
}
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> deleteRow(RowId rowId) {
|
Future<Either<Unit, FlowyError>> deleteRow(RowId rowId) {
|
||||||
final payload = RowIdPB.create()
|
final payload = RowIdPB.create()
|
||||||
..viewId = viewId
|
..viewId = viewId
|
||||||
|
@ -65,14 +65,16 @@ class DatabaseViewCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (changeset.updatedRows.isNotEmpty) {
|
if (changeset.updatedRows.isNotEmpty) {
|
||||||
_callbacks?.onRowsUpdated
|
_callbacks?.onRowsUpdated?.call(
|
||||||
?.call(changeset.updatedRows.map((e) => e.row.id).toList());
|
changeset.updatedRows.map((e) => e.rowId).toList(),
|
||||||
|
_rowCache.changeReason,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changeset.insertedRows.isNotEmpty) {
|
if (changeset.insertedRows.isNotEmpty) {
|
||||||
_callbacks?.onRowsCreated?.call(
|
_callbacks?.onRowsCreated?.call(
|
||||||
changeset.insertedRows
|
changeset.insertedRows
|
||||||
.map((insertedRow) => insertedRow.row.id)
|
.map((insertedRow) => insertedRow.rowMeta.id)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
|
void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) {
|
||||||
final fieldInfo = fieldController.getField(group.fieldId);
|
final fieldInfo = fieldController.getField(group.fieldId);
|
||||||
if (fieldInfo == null) {
|
if (fieldInfo == null) {
|
||||||
Log.warn("fieldInfo should not be null");
|
Log.warn("fieldInfo should not be null");
|
||||||
@ -302,12 +302,12 @@ class BoardEvent with _$BoardEvent {
|
|||||||
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
|
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
|
||||||
const factory BoardEvent.didCreateRow(
|
const factory BoardEvent.didCreateRow(
|
||||||
GroupPB group,
|
GroupPB group,
|
||||||
RowPB row,
|
RowMetaPB row,
|
||||||
int? index,
|
int? index,
|
||||||
) = _DidCreateRow;
|
) = _DidCreateRow;
|
||||||
const factory BoardEvent.startEditingRow(
|
const factory BoardEvent.startEditingRow(
|
||||||
GroupPB group,
|
GroupPB group,
|
||||||
RowPB row,
|
RowMetaPB row,
|
||||||
) = _StartEditRow;
|
) = _StartEditRow;
|
||||||
const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow;
|
const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow;
|
||||||
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
|
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
|
||||||
@ -371,7 +371,7 @@ class GridFieldEquatable extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GroupItem extends AppFlowyGroupItem {
|
class GroupItem extends AppFlowyGroupItem {
|
||||||
final RowPB row;
|
final RowMetaPB row;
|
||||||
final FieldInfo fieldInfo;
|
final FieldInfo fieldInfo;
|
||||||
|
|
||||||
GroupItem({
|
GroupItem({
|
||||||
@ -389,7 +389,7 @@ class GroupItem extends AppFlowyGroupItem {
|
|||||||
class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||||
final FieldController fieldController;
|
final FieldController fieldController;
|
||||||
final AppFlowyBoardController controller;
|
final AppFlowyBoardController controller;
|
||||||
final void Function(String, RowPB, int?) onNewColumnItem;
|
final void Function(String, RowMetaPB, int?) onNewColumnItem;
|
||||||
|
|
||||||
GroupControllerDelegateImpl({
|
GroupControllerDelegateImpl({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
@ -398,7 +398,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void insertRow(GroupPB group, RowPB row, int? index) {
|
void insertRow(GroupPB group, RowMetaPB row, int? index) {
|
||||||
final fieldInfo = fieldController.getField(group.fieldId);
|
final fieldInfo = fieldController.getField(group.fieldId);
|
||||||
if (fieldInfo == null) {
|
if (fieldInfo == null) {
|
||||||
Log.warn("fieldInfo should not be null");
|
Log.warn("fieldInfo should not be null");
|
||||||
@ -426,7 +426,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void updateRow(GroupPB group, RowPB row) {
|
void updateRow(GroupPB group, RowMetaPB row) {
|
||||||
final fieldInfo = fieldController.getField(group.fieldId);
|
final fieldInfo = fieldController.getField(group.fieldId);
|
||||||
if (fieldInfo == null) {
|
if (fieldInfo == null) {
|
||||||
Log.warn("fieldInfo should not be null");
|
Log.warn("fieldInfo should not be null");
|
||||||
@ -442,7 +442,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addNewRow(GroupPB group, RowPB row, int? index) {
|
void addNewRow(GroupPB group, RowMetaPB row, int? index) {
|
||||||
final fieldInfo = fieldController.getField(group.fieldId);
|
final fieldInfo = fieldController.getField(group.fieldId);
|
||||||
if (fieldInfo == null) {
|
if (fieldInfo == null) {
|
||||||
Log.warn("fieldInfo should not be null");
|
Log.warn("fieldInfo should not be null");
|
||||||
@ -465,7 +465,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
|||||||
|
|
||||||
class BoardEditingRow {
|
class BoardEditingRow {
|
||||||
GroupPB group;
|
GroupPB group;
|
||||||
RowPB row;
|
RowMetaPB row;
|
||||||
int? index;
|
int? index;
|
||||||
|
|
||||||
BoardEditingRow({
|
BoardEditingRow({
|
||||||
|
@ -12,9 +12,9 @@ typedef OnGroupError = void Function(FlowyError);
|
|||||||
|
|
||||||
abstract class GroupControllerDelegate {
|
abstract class GroupControllerDelegate {
|
||||||
void removeRow(GroupPB group, RowId rowId);
|
void removeRow(GroupPB group, RowId rowId);
|
||||||
void insertRow(GroupPB group, RowPB row, int? index);
|
void insertRow(GroupPB group, RowMetaPB row, int? index);
|
||||||
void updateRow(GroupPB group, RowPB row);
|
void updateRow(GroupPB group, RowMetaPB row);
|
||||||
void addNewRow(GroupPB group, RowPB row, int? index);
|
void addNewRow(GroupPB group, RowMetaPB row, int? index);
|
||||||
}
|
}
|
||||||
|
|
||||||
class GroupController {
|
class GroupController {
|
||||||
@ -28,7 +28,7 @@ class GroupController {
|
|||||||
required this.delegate,
|
required this.delegate,
|
||||||
}) : _listener = SingleGroupListener(group);
|
}) : _listener = SingleGroupListener(group);
|
||||||
|
|
||||||
RowPB? rowAtIndex(int index) {
|
RowMetaPB? rowAtIndex(int index) {
|
||||||
if (index < group.rows.length) {
|
if (index < group.rows.length) {
|
||||||
return group.rows[index];
|
return group.rows[index];
|
||||||
} else {
|
} else {
|
||||||
@ -36,7 +36,7 @@ class GroupController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowPB? lastRow() {
|
RowMetaPB? lastRow() {
|
||||||
if (group.rows.isEmpty) return null;
|
if (group.rows.isEmpty) return null;
|
||||||
return group.rows.last;
|
return group.rows.last;
|
||||||
}
|
}
|
||||||
@ -55,15 +55,15 @@ class GroupController {
|
|||||||
final index = insertedRow.hasIndex() ? insertedRow.index : null;
|
final index = insertedRow.hasIndex() ? insertedRow.index : null;
|
||||||
if (insertedRow.hasIndex() &&
|
if (insertedRow.hasIndex() &&
|
||||||
group.rows.length > insertedRow.index) {
|
group.rows.length > insertedRow.index) {
|
||||||
group.rows.insert(insertedRow.index, insertedRow.row);
|
group.rows.insert(insertedRow.index, insertedRow.rowMeta);
|
||||||
} else {
|
} else {
|
||||||
group.rows.add(insertedRow.row);
|
group.rows.add(insertedRow.rowMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (insertedRow.isNew) {
|
if (insertedRow.isNew) {
|
||||||
delegate.addNewRow(group, insertedRow.row, index);
|
delegate.addNewRow(group, insertedRow.rowMeta, index);
|
||||||
} else {
|
} else {
|
||||||
delegate.insertRow(group, insertedRow.row, index);
|
delegate.insertRow(group, insertedRow.rowMeta, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,8 +45,12 @@ class BoardPlugin extends Plugin {
|
|||||||
BoardPlugin({
|
BoardPlugin({
|
||||||
required ViewPB view,
|
required ViewPB view,
|
||||||
required PluginType pluginType,
|
required PluginType pluginType,
|
||||||
|
bool listenOnViewChanged = false,
|
||||||
}) : _pluginType = pluginType,
|
}) : _pluginType = pluginType,
|
||||||
notifier = ViewPluginNotifier(view: view);
|
notifier = ViewPluginNotifier(
|
||||||
|
view: view,
|
||||||
|
listenOnViewChanged: listenOnViewChanged,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PluginWidgetBuilder get widgetBuilder =>
|
PluginWidgetBuilder get widgetBuilder =>
|
||||||
|
@ -231,7 +231,7 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
) {
|
) {
|
||||||
final groupItem = afGroupItem as GroupItem;
|
final groupItem = afGroupItem as GroupItem;
|
||||||
final groupData = afGroupData.customData as GroupData;
|
final groupData = afGroupData.customData as GroupData;
|
||||||
final rowPB = groupItem.row;
|
final rowMeta = groupItem.row;
|
||||||
final rowCache = context.read<BoardBloc>().getRowCache();
|
final rowCache = context.read<BoardBloc>().getRowCache();
|
||||||
|
|
||||||
/// Return placeholder widget if the rowCache is null.
|
/// Return placeholder widget if the rowCache is null.
|
||||||
@ -255,7 +255,7 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
margin: config.cardPadding,
|
margin: config.cardPadding,
|
||||||
decoration: _makeBoxDecoration(context),
|
decoration: _makeBoxDecoration(context),
|
||||||
child: RowCard<String>(
|
child: RowCard<String>(
|
||||||
row: rowPB,
|
rowMeta: rowMeta,
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
cardData: groupData.group.groupId,
|
cardData: groupData.group.groupId,
|
||||||
@ -267,7 +267,7 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
viewId,
|
viewId,
|
||||||
groupData.group.groupId,
|
groupData.group.groupId,
|
||||||
fieldController,
|
fieldController,
|
||||||
rowPB,
|
rowMeta,
|
||||||
rowCache,
|
rowCache,
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
@ -305,18 +305,19 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
String viewId,
|
String viewId,
|
||||||
String groupId,
|
String groupId,
|
||||||
FieldController fieldController,
|
FieldController fieldController,
|
||||||
RowPB rowPB,
|
RowMetaPB rowMetaPB,
|
||||||
RowCache rowCache,
|
RowCache rowCache,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) {
|
) {
|
||||||
final rowInfo = RowInfo(
|
final rowInfo = RowInfo(
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
fields: UnmodifiableListView(fieldController.fieldInfos),
|
fields: UnmodifiableListView(fieldController.fieldInfos),
|
||||||
rowPB: rowPB,
|
rowMeta: rowMetaPB,
|
||||||
|
rowId: rowMetaPB.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
final dataController = RowController(
|
final dataController = RowController(
|
||||||
rowId: rowInfo.rowPB.id,
|
rowMeta: rowInfo.rowMeta,
|
||||||
viewId: rowInfo.viewId,
|
viewId: rowInfo.viewId,
|
||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
|
@ -268,7 +268,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
|
|
||||||
final eventData = CalendarDayEvent(
|
final eventData = CalendarDayEvent(
|
||||||
event: eventPB,
|
event: eventPB,
|
||||||
eventId: eventPB.rowId,
|
eventId: eventPB.rowMeta.id,
|
||||||
dateFieldId: eventPB.dateFieldId,
|
dateFieldId: eventPB.dateFieldId,
|
||||||
date: date,
|
date: date,
|
||||||
);
|
);
|
||||||
@ -310,7 +310,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
}
|
}
|
||||||
add(CalendarEvent.didDeleteEvents(rowIds));
|
add(CalendarEvent.didDeleteEvents(rowIds));
|
||||||
},
|
},
|
||||||
onRowsUpdated: (rowIds) async {
|
onRowsUpdated: (rowIds, reason) async {
|
||||||
if (isClosed) {
|
if (isClosed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -45,8 +45,12 @@ class CalendarPlugin extends Plugin {
|
|||||||
CalendarPlugin({
|
CalendarPlugin({
|
||||||
required ViewPB view,
|
required ViewPB view,
|
||||||
required PluginType pluginType,
|
required PluginType pluginType,
|
||||||
|
bool listenOnViewChanged = false,
|
||||||
}) : _pluginType = pluginType,
|
}) : _pluginType = pluginType,
|
||||||
notifier = ViewPluginNotifier(view: view);
|
notifier = ViewPluginNotifier(
|
||||||
|
view: view,
|
||||||
|
listenOnViewChanged: listenOnViewChanged,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PluginWidgetBuilder get widgetBuilder =>
|
PluginWidgetBuilder get widgetBuilder =>
|
||||||
|
@ -301,7 +301,7 @@ class _EventCard extends StatelessWidget {
|
|||||||
// Add the key here to make sure the card is rebuilt when the cells
|
// Add the key here to make sure the card is rebuilt when the cells
|
||||||
// in this row are updated.
|
// in this row are updated.
|
||||||
key: ValueKey(event.eventId),
|
key: ValueKey(event.eventId),
|
||||||
row: rowInfo!.rowPB,
|
rowMeta: rowInfo!.rowMeta,
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
cardData: event.dateFieldId,
|
cardData: event.dateFieldId,
|
||||||
|
@ -243,7 +243,7 @@ void showEventDetails({
|
|||||||
required RowCache rowCache,
|
required RowCache rowCache,
|
||||||
}) {
|
}) {
|
||||||
final dataController = RowController(
|
final dataController = RowController(
|
||||||
rowId: event.eventId,
|
rowMeta: event.event.rowMeta,
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
);
|
);
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
|
||||||
import 'package:appflowy/startup/startup.dart';
|
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
|
||||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
|
||||||
import '../../workspace/presentation/home/home_stack.dart';
|
|
||||||
|
|
||||||
/// [DatabaseViewPlugin] is used to build the grid, calendar, and board.
|
|
||||||
/// It is a wrapper of the [Plugin] class. The underlying [Plugin] is
|
|
||||||
/// determined by the [ViewPB.pluginType] field.
|
|
||||||
///
|
|
||||||
class DatabaseViewPlugin extends Plugin {
|
|
||||||
final ViewListener _viewListener;
|
|
||||||
ViewPB _view;
|
|
||||||
Plugin _innerPlugin;
|
|
||||||
|
|
||||||
DatabaseViewPlugin({
|
|
||||||
required ViewPB view,
|
|
||||||
}) : _view = view,
|
|
||||||
_innerPlugin = _makeInnerPlugin(view),
|
|
||||||
_viewListener = ViewListener(view: view) {
|
|
||||||
_listenOnLayoutChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginId get id => _innerPlugin.id;
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginType get pluginType => _innerPlugin.pluginType;
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginWidgetBuilder get widgetBuilder => _innerPlugin.widgetBuilder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_viewListener.stop();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenOnLayoutChanged() {
|
|
||||||
_viewListener.start(
|
|
||||||
onViewUpdated: (result) {
|
|
||||||
result.fold(
|
|
||||||
(updatedView) {
|
|
||||||
if (_view.layout != updatedView.layout) {
|
|
||||||
_innerPlugin = _makeInnerPlugin(updatedView);
|
|
||||||
|
|
||||||
getIt<HomeStackManager>().setPlugin(_innerPlugin);
|
|
||||||
}
|
|
||||||
_view = updatedView;
|
|
||||||
},
|
|
||||||
(r) => null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Plugin _makeInnerPlugin(ViewPB view) {
|
|
||||||
return makePlugin(pluginType: view.pluginType, data: view);
|
|
||||||
}
|
|
@ -33,18 +33,18 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
|||||||
final rowService = RowBackendService(
|
final rowService = RowBackendService(
|
||||||
viewId: rowInfo.viewId,
|
viewId: rowInfo.viewId,
|
||||||
);
|
);
|
||||||
await rowService.deleteRow(rowInfo.rowPB.id);
|
await rowService.deleteRow(rowInfo.rowId);
|
||||||
},
|
},
|
||||||
moveRow: (int from, int to) {
|
moveRow: (int from, int to) {
|
||||||
final List<RowInfo> rows = [...state.rowInfos];
|
final List<RowInfo> rows = [...state.rowInfos];
|
||||||
|
|
||||||
final fromRow = rows[from].rowPB;
|
final fromRow = rows[from].rowId;
|
||||||
final toRow = rows[to].rowPB;
|
final toRow = rows[to].rowId;
|
||||||
|
|
||||||
rows.insert(to, rows.removeAt(from));
|
rows.insert(to, rows.removeAt(from));
|
||||||
emit(state.copyWith(rowInfos: rows));
|
emit(state.copyWith(rowInfos: rows));
|
||||||
|
|
||||||
databaseController.moveRow(fromRow: fromRow, toRow: toRow);
|
databaseController.moveRow(fromRowId: fromRow, toRowId: toRow);
|
||||||
},
|
},
|
||||||
didReceiveGridUpdate: (grid) {
|
didReceiveGridUpdate: (grid) {
|
||||||
emit(state.copyWith(grid: Some(grid)));
|
emit(state.copyWith(grid: Some(grid)));
|
||||||
@ -56,7 +56,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
didReceiveRowUpdate: (newRowInfos, reason) {
|
didLoadRows: (newRowInfos, reason) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
rowInfos: newRowInfos,
|
rowInfos: newRowInfos,
|
||||||
@ -76,7 +76,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
|||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
RowCache? getRowCache(RowId rowId) {
|
RowCache getRowCache(RowId rowId) {
|
||||||
return databaseController.rowCache;
|
return databaseController.rowCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,9 +89,14 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
|||||||
},
|
},
|
||||||
onNumOfRowsChanged: (rowInfos, _, reason) {
|
onNumOfRowsChanged: (rowInfos, _, reason) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
|
add(GridEvent.didLoadRows(rowInfos, reason));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onRowsUpdated: (rows, reason) {
|
||||||
|
add(
|
||||||
|
GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
|
||||||
|
);
|
||||||
|
},
|
||||||
onFieldsChanged: (fields) {
|
onFieldsChanged: (fields) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(GridEvent.didReceiveFieldUpdate(fields));
|
add(GridEvent.didReceiveFieldUpdate(fields));
|
||||||
@ -122,9 +127,9 @@ class GridEvent with _$GridEvent {
|
|||||||
const factory GridEvent.createRow() = _CreateRow;
|
const factory GridEvent.createRow() = _CreateRow;
|
||||||
const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
|
const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
|
||||||
const factory GridEvent.moveRow(int from, int to) = _MoveRow;
|
const factory GridEvent.moveRow(int from, int to) = _MoveRow;
|
||||||
const factory GridEvent.didReceiveRowUpdate(
|
const factory GridEvent.didLoadRows(
|
||||||
List<RowInfo> rows,
|
List<RowInfo> rows,
|
||||||
RowsChangedReason listState,
|
RowsChangedReason reason,
|
||||||
) = _DidReceiveRowUpdate;
|
) = _DidReceiveRowUpdate;
|
||||||
const factory GridEvent.didReceiveFieldUpdate(
|
const factory GridEvent.didReceiveFieldUpdate(
|
||||||
List<FieldInfo> fields,
|
List<FieldInfo> fields,
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
import '../../../application/row/row_cache.dart';
|
|
||||||
import '../../../application/row/row_service.dart';
|
import '../../../application/row/row_service.dart';
|
||||||
|
|
||||||
part 'row_action_sheet_bloc.freezed.dart';
|
part 'row_action_sheet_bloc.freezed.dart';
|
||||||
@ -13,19 +12,20 @@ class RowActionSheetBloc
|
|||||||
extends Bloc<RowActionSheetEvent, RowActionSheetState> {
|
extends Bloc<RowActionSheetEvent, RowActionSheetState> {
|
||||||
final RowBackendService _rowService;
|
final RowBackendService _rowService;
|
||||||
|
|
||||||
RowActionSheetBloc({required RowInfo rowInfo})
|
RowActionSheetBloc({
|
||||||
: _rowService = RowBackendService(viewId: rowInfo.viewId),
|
required String viewId,
|
||||||
super(RowActionSheetState.initial(rowInfo)) {
|
required RowId rowId,
|
||||||
|
}) : _rowService = RowBackendService(viewId: viewId),
|
||||||
|
super(RowActionSheetState.initial(rowId)) {
|
||||||
on<RowActionSheetEvent>(
|
on<RowActionSheetEvent>(
|
||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
deleteRow: () async {
|
deleteRow: () async {
|
||||||
final result = await _rowService.deleteRow(state.rowData.rowPB.id);
|
final result = await _rowService.deleteRow(state.rowId);
|
||||||
logResult(result);
|
logResult(result);
|
||||||
},
|
},
|
||||||
duplicateRow: () async {
|
duplicateRow: () async {
|
||||||
final result =
|
final result = await _rowService.duplicateRow(rowId: state.rowId);
|
||||||
await _rowService.duplicateRow(rowId: state.rowData.rowPB.id);
|
|
||||||
logResult(result);
|
logResult(result);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -47,10 +47,10 @@ class RowActionSheetEvent with _$RowActionSheetEvent {
|
|||||||
@freezed
|
@freezed
|
||||||
class RowActionSheetState with _$RowActionSheetState {
|
class RowActionSheetState with _$RowActionSheetState {
|
||||||
const factory RowActionSheetState({
|
const factory RowActionSheetState({
|
||||||
required RowInfo rowData,
|
required RowId rowId,
|
||||||
}) = _RowActionSheetState;
|
}) = _RowActionSheetState;
|
||||||
|
|
||||||
factory RowActionSheetState.initial(RowInfo rowData) => RowActionSheetState(
|
factory RowActionSheetState.initial(RowId rowId) => RowActionSheetState(
|
||||||
rowData: rowData,
|
rowId: rowId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,16 @@ part 'row_bloc.freezed.dart';
|
|||||||
class RowBloc extends Bloc<RowEvent, RowState> {
|
class RowBloc extends Bloc<RowEvent, RowState> {
|
||||||
final RowBackendService _rowBackendSvc;
|
final RowBackendService _rowBackendSvc;
|
||||||
final RowController _dataController;
|
final RowController _dataController;
|
||||||
|
final String viewId;
|
||||||
|
final String rowId;
|
||||||
|
|
||||||
RowBloc({
|
RowBloc({
|
||||||
required RowInfo rowInfo,
|
required this.rowId,
|
||||||
|
required this.viewId,
|
||||||
required RowController dataController,
|
required RowController dataController,
|
||||||
}) : _rowBackendSvc = RowBackendService(viewId: rowInfo.viewId),
|
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||||
_dataController = dataController,
|
_dataController = dataController,
|
||||||
super(RowState.initial(rowInfo, dataController.loadData())) {
|
super(RowState.initial(dataController.loadData())) {
|
||||||
on<RowEvent>(
|
on<RowEvent>(
|
||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
@ -29,7 +32,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
|||||||
await _startListening();
|
await _startListening();
|
||||||
},
|
},
|
||||||
createRow: () {
|
createRow: () {
|
||||||
_rowBackendSvc.createRow(rowInfo.rowPB.id);
|
_rowBackendSvc.createRowAfterRow(rowId);
|
||||||
},
|
},
|
||||||
didReceiveCells: (cellByFieldId, reason) async {
|
didReceiveCells: (cellByFieldId, reason) async {
|
||||||
final cells = cellByFieldId.values
|
final cells = cellByFieldId.values
|
||||||
@ -78,18 +81,15 @@ class RowEvent with _$RowEvent {
|
|||||||
@freezed
|
@freezed
|
||||||
class RowState with _$RowState {
|
class RowState with _$RowState {
|
||||||
const factory RowState({
|
const factory RowState({
|
||||||
required RowInfo rowInfo,
|
|
||||||
required CellContextByFieldId cellByFieldId,
|
required CellContextByFieldId cellByFieldId,
|
||||||
required UnmodifiableListView<GridCellEquatable> cells,
|
required UnmodifiableListView<GridCellEquatable> cells,
|
||||||
RowsChangedReason? changeReason,
|
RowsChangedReason? changeReason,
|
||||||
}) = _RowState;
|
}) = _RowState;
|
||||||
|
|
||||||
factory RowState.initial(
|
factory RowState.initial(
|
||||||
RowInfo rowInfo,
|
|
||||||
CellContextByFieldId cellByFieldId,
|
CellContextByFieldId cellByFieldId,
|
||||||
) =>
|
) =>
|
||||||
RowState(
|
RowState(
|
||||||
rowInfo: rowInfo,
|
|
||||||
cellByFieldId: cellByFieldId,
|
cellByFieldId: cellByFieldId,
|
||||||
cells: UnmodifiableListView(
|
cells: UnmodifiableListView(
|
||||||
cellByFieldId.values
|
cellByFieldId.values
|
||||||
|
@ -27,7 +27,7 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
didReceiveCellDatas: (cells) {
|
didReceiveCellDatas: (cells) {
|
||||||
emit(state.copyWith(gridCells: cells));
|
emit(state.copyWith(cells: cells));
|
||||||
},
|
},
|
||||||
deleteField: (fieldId) {
|
deleteField: (fieldId) {
|
||||||
_fieldBackendService(fieldId).deleteField();
|
_fieldBackendService(fieldId).deleteField();
|
||||||
@ -95,10 +95,10 @@ class RowDetailEvent with _$RowDetailEvent {
|
|||||||
@freezed
|
@freezed
|
||||||
class RowDetailState with _$RowDetailState {
|
class RowDetailState with _$RowDetailState {
|
||||||
const factory RowDetailState({
|
const factory RowDetailState({
|
||||||
required List<DatabaseCellContext> gridCells,
|
required List<DatabaseCellContext> cells,
|
||||||
}) = _RowDetailState;
|
}) = _RowDetailState;
|
||||||
|
|
||||||
factory RowDetailState.initial() => RowDetailState(
|
factory RowDetailState.initial() => RowDetailState(
|
||||||
gridCells: List.empty(),
|
cells: List.empty(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,126 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
|
|
||||||
|
import '../../../application/row/row_service.dart';
|
||||||
|
|
||||||
|
part 'row_document_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class RowDocumentBloc extends Bloc<RowDocumentEvent, RowDocumentState> {
|
||||||
|
final String rowId;
|
||||||
|
final RowBackendService _rowBackendSvc;
|
||||||
|
|
||||||
|
RowDocumentBloc({
|
||||||
|
required this.rowId,
|
||||||
|
required String viewId,
|
||||||
|
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||||
|
super(RowDocumentState.initial()) {
|
||||||
|
on<RowDocumentEvent>(
|
||||||
|
(event, emit) async {
|
||||||
|
await event.when(
|
||||||
|
initial: () async {
|
||||||
|
_getRowDocumentView();
|
||||||
|
},
|
||||||
|
didReceiveRowDocument: (view) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
viewPB: view,
|
||||||
|
loadingState: const LoadingState.finish(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
didReceiveError: (FlowyError error) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
loadingState: LoadingState.error(error),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _getRowDocumentView() async {
|
||||||
|
final rowDetailOrError = await _rowBackendSvc.getRowMeta(rowId);
|
||||||
|
rowDetailOrError.fold(
|
||||||
|
(RowMetaPB rowMeta) async {
|
||||||
|
final viewsOrError =
|
||||||
|
await ViewBackendService.getView(rowMeta.documentId);
|
||||||
|
|
||||||
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewsOrError.fold(
|
||||||
|
(view) => add(RowDocumentEvent.didReceiveRowDocument(view)),
|
||||||
|
(error) async {
|
||||||
|
if (error.code == ErrorCode.RecordNotFound.value) {
|
||||||
|
// By default, the document of the row is not exist. So creating a
|
||||||
|
// new document for the given document id of the row.
|
||||||
|
final documentView =
|
||||||
|
await _createRowDocumentView(rowMeta.documentId);
|
||||||
|
if (documentView != null) {
|
||||||
|
add(RowDocumentEvent.didReceiveRowDocument(documentView));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
add(RowDocumentEvent.didReceiveError(error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(err) => Log.error('Failed to get row detail: $err'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ViewPB?> _createRowDocumentView(String viewId) async {
|
||||||
|
final result = await ViewBackendService.createOrphanView(
|
||||||
|
viewId: viewId,
|
||||||
|
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||||
|
desc: '',
|
||||||
|
layoutType: ViewLayoutPB.Document,
|
||||||
|
);
|
||||||
|
return result.fold(
|
||||||
|
(view) => view,
|
||||||
|
(error) {
|
||||||
|
Log.error(error);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class RowDocumentEvent with _$RowDocumentEvent {
|
||||||
|
const factory RowDocumentEvent.initial() = _InitialRow;
|
||||||
|
const factory RowDocumentEvent.didReceiveRowDocument(ViewPB view) =
|
||||||
|
_DidReceiveRowDocument;
|
||||||
|
const factory RowDocumentEvent.didReceiveError(FlowyError error) =
|
||||||
|
_DidReceiveError;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class RowDocumentState with _$RowDocumentState {
|
||||||
|
const factory RowDocumentState({
|
||||||
|
ViewPB? viewPB,
|
||||||
|
required LoadingState loadingState,
|
||||||
|
}) = _RowDocumentState;
|
||||||
|
|
||||||
|
factory RowDocumentState.initial() => const RowDocumentState(
|
||||||
|
loadingState: LoadingState.loading(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class LoadingState with _$LoadingState {
|
||||||
|
const factory LoadingState.loading() = _Loading;
|
||||||
|
const factory LoadingState.error(FlowyError error) = _Error;
|
||||||
|
const factory LoadingState.finish() = _Finish;
|
||||||
|
}
|
@ -45,8 +45,12 @@ class GridPlugin extends Plugin {
|
|||||||
GridPlugin({
|
GridPlugin({
|
||||||
required ViewPB view,
|
required ViewPB view,
|
||||||
required PluginType pluginType,
|
required PluginType pluginType,
|
||||||
|
bool listenOnViewChanged = false,
|
||||||
}) : _pluginType = pluginType,
|
}) : _pluginType = pluginType,
|
||||||
notifier = ViewPluginNotifier(view: view);
|
notifier = ViewPluginNotifier(
|
||||||
|
view: view,
|
||||||
|
listenOnViewChanged: listenOnViewChanged,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PluginWidgetBuilder get widgetBuilder =>
|
PluginWidgetBuilder get widgetBuilder =>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||||
@ -78,7 +80,11 @@ class _GridPageState extends State<GridPage> {
|
|||||||
loading: (_) =>
|
loading: (_) =>
|
||||||
const Center(child: CircularProgressIndicator.adaptive()),
|
const Center(child: CircularProgressIndicator.adaptive()),
|
||||||
finish: (result) => result.successOrFail.fold(
|
finish: (result) => result.successOrFail.fold(
|
||||||
(_) => const GridShortcuts(child: FlowyGrid()),
|
(_) => GridShortcuts(
|
||||||
|
child: FlowyGrid(
|
||||||
|
viewId: widget.view.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
(err) => FlowyErrorPage(err.toString()),
|
(err) => FlowyErrorPage(err.toString()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -89,7 +95,9 @@ class _GridPageState extends State<GridPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FlowyGrid extends StatefulWidget {
|
class FlowyGrid extends StatefulWidget {
|
||||||
|
final String viewId;
|
||||||
const FlowyGrid({
|
const FlowyGrid({
|
||||||
|
required this.viewId,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -125,6 +133,7 @@ class _FlowyGridState extends State<FlowyGrid> {
|
|||||||
scrollController: _scrollController,
|
scrollController: _scrollController,
|
||||||
contentWidth: contentWidth,
|
contentWidth: contentWidth,
|
||||||
child: _GridRows(
|
child: _GridRows(
|
||||||
|
viewId: widget.viewId,
|
||||||
verticalScrollController: _scrollController.verticalController,
|
verticalScrollController: _scrollController.verticalController,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -155,7 +164,9 @@ class _FlowyGridState extends State<FlowyGrid> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _GridRows extends StatelessWidget {
|
class _GridRows extends StatelessWidget {
|
||||||
|
final String viewId;
|
||||||
const _GridRows({
|
const _GridRows({
|
||||||
|
required this.viewId,
|
||||||
required this.verticalScrollController,
|
required this.verticalScrollController,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,7 +218,7 @@ class _GridRows extends StatelessWidget {
|
|||||||
final rowInfo = rowInfos[index];
|
final rowInfo = rowInfos[index];
|
||||||
return _renderRow(
|
return _renderRow(
|
||||||
context,
|
context,
|
||||||
rowInfo,
|
rowInfo.rowId,
|
||||||
index: index,
|
index: index,
|
||||||
isSortEnabled: sortState.sortInfos.isNotEmpty,
|
isSortEnabled: sortState.sortInfos.isNotEmpty,
|
||||||
isFilterEnabled: filterState.filters.isNotEmpty,
|
isFilterEnabled: filterState.filters.isNotEmpty,
|
||||||
@ -223,38 +234,38 @@ class _GridRows extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _renderRow(
|
Widget _renderRow(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
RowInfo rowInfo, {
|
RowId rowId, {
|
||||||
int? index,
|
int? index,
|
||||||
bool isSortEnabled = false,
|
bool isSortEnabled = false,
|
||||||
bool isFilterEnabled = false,
|
bool isFilterEnabled = false,
|
||||||
Animation<double>? animation,
|
Animation<double>? animation,
|
||||||
}) {
|
}) {
|
||||||
final rowCache = context.read<GridBloc>().getRowCache(
|
final rowCache = context.read<GridBloc>().getRowCache(rowId);
|
||||||
rowInfo.rowPB.id,
|
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
|
||||||
);
|
|
||||||
|
|
||||||
/// Return placeholder widget if the rowCache is null.
|
/// Return placeholder widget if the rowMeta is null.
|
||||||
if (rowCache == null) return const SizedBox.shrink();
|
if (rowMeta == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
final fieldController =
|
final fieldController =
|
||||||
context.read<GridBloc>().databaseController.fieldController;
|
context.read<GridBloc>().databaseController.fieldController;
|
||||||
final dataController = RowController(
|
final dataController = RowController(
|
||||||
rowId: rowInfo.rowPB.id,
|
viewId: viewId,
|
||||||
viewId: rowInfo.viewId,
|
rowMeta: rowMeta,
|
||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
);
|
);
|
||||||
|
|
||||||
final child = GridRow(
|
final child = GridRow(
|
||||||
key: ValueKey(rowInfo.rowPB.id),
|
key: ValueKey(rowMeta.id),
|
||||||
|
rowId: rowId,
|
||||||
|
viewId: viewId,
|
||||||
index: index,
|
index: index,
|
||||||
isDraggable: !isSortEnabled && !isFilterEnabled,
|
isDraggable: !isSortEnabled && !isFilterEnabled,
|
||||||
rowInfo: rowInfo,
|
|
||||||
dataController: dataController,
|
dataController: dataController,
|
||||||
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
||||||
openDetailPage: (context, cellBuilder) {
|
openDetailPage: (context, cellBuilder) {
|
||||||
_openRowDetailPage(
|
_openRowDetailPage(
|
||||||
context,
|
context,
|
||||||
rowInfo,
|
rowId,
|
||||||
fieldController,
|
fieldController,
|
||||||
rowCache,
|
rowCache,
|
||||||
cellBuilder,
|
cellBuilder,
|
||||||
@ -274,14 +285,17 @@ class _GridRows extends StatelessWidget {
|
|||||||
|
|
||||||
void _openRowDetailPage(
|
void _openRowDetailPage(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
RowInfo rowInfo,
|
RowId rowId,
|
||||||
FieldController fieldController,
|
FieldController fieldController,
|
||||||
RowCache rowCache,
|
RowCache rowCache,
|
||||||
GridCellBuilder cellBuilder,
|
GridCellBuilder cellBuilder,
|
||||||
) {
|
) {
|
||||||
|
final rowMeta = rowCache.getRow(rowId)?.rowMeta;
|
||||||
|
// Most of the cases, the rowMeta should not be null.
|
||||||
|
if (rowMeta != null) {
|
||||||
final dataController = RowController(
|
final dataController = RowController(
|
||||||
viewId: rowInfo.viewId,
|
viewId: viewId,
|
||||||
rowId: rowInfo.rowPB.id,
|
rowMeta: rowMeta,
|
||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -294,6 +308,9 @@ class _GridRows extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
Log.warn('RowMeta is null for rowId: $rowId');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,10 +374,9 @@ class _RowCountBadge extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
FlowyText.medium(
|
FlowyText.medium(
|
||||||
'${LocaleKeys.grid_row_count.tr()} : ',
|
rowCountString(rowCount),
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
),
|
),
|
||||||
FlowyText.medium(rowCount.toString()),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -368,3 +384,7 @@ class _RowCountBadge extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String rowCountString(int count) {
|
||||||
|
return '${LocaleKeys.grid_row_count.tr()} : $count';
|
||||||
|
}
|
||||||
|
@ -52,11 +52,11 @@ class _FieldEditorState extends State<FieldEditor> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<Widget> children = [
|
final List<Widget> children = [
|
||||||
_FieldNameTextField(popoverMutex: popoverMutex),
|
FieldNameTextField(popoverMutex: popoverMutex),
|
||||||
if (widget.onDeleted != null) _addDeleteFieldButton(),
|
if (widget.onDeleted != null) _addDeleteFieldButton(),
|
||||||
if (widget.onHidden != null) _addHideFieldButton(),
|
if (widget.onHidden != null) _addHideFieldButton(),
|
||||||
if (!widget.typeOptionLoader.field.isPrimary)
|
if (!widget.typeOptionLoader.field.isPrimary)
|
||||||
_FieldTypeOptionCell(popoverMutex: popoverMutex),
|
FieldTypeOptionCell(popoverMutex: popoverMutex),
|
||||||
];
|
];
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) {
|
create: (context) {
|
||||||
@ -116,10 +116,10 @@ class _FieldEditorState extends State<FieldEditor> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FieldTypeOptionCell extends StatelessWidget {
|
class FieldTypeOptionCell extends StatelessWidget {
|
||||||
final PopoverMutex popoverMutex;
|
final PopoverMutex popoverMutex;
|
||||||
|
|
||||||
const _FieldTypeOptionCell({
|
const FieldTypeOptionCell({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.popoverMutex,
|
required this.popoverMutex,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@ -130,7 +130,7 @@ class _FieldTypeOptionCell extends StatelessWidget {
|
|||||||
buildWhen: (p, c) => p.field != c.field,
|
buildWhen: (p, c) => p.field != c.field,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return state.field.fold(
|
return state.field.fold(
|
||||||
() => const SizedBox(),
|
() => const SizedBox.shrink(),
|
||||||
(fieldInfo) {
|
(fieldInfo) {
|
||||||
final dataController =
|
final dataController =
|
||||||
context.read<FieldEditorBloc>().dataController;
|
context.read<FieldEditorBloc>().dataController;
|
||||||
@ -145,18 +145,18 @@ class _FieldTypeOptionCell extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FieldNameTextField extends StatefulWidget {
|
class FieldNameTextField extends StatefulWidget {
|
||||||
final PopoverMutex popoverMutex;
|
final PopoverMutex popoverMutex;
|
||||||
const _FieldNameTextField({
|
const FieldNameTextField({
|
||||||
required this.popoverMutex,
|
required this.popoverMutex,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_FieldNameTextField> createState() => _FieldNameTextFieldState();
|
State<FieldNameTextField> createState() => _FieldNameTextFieldState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FieldNameTextFieldState extends State<_FieldNameTextField> {
|
class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||||
final textController = TextEditingController();
|
final textController = TextEditingController();
|
||||||
FocusNode focusNode = FocusNode();
|
FocusNode focusNode = FocusNode();
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ class FieldTypeOptionEditor extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final List<Widget> children = [
|
final List<Widget> children = [
|
||||||
_SwitchFieldButton(popoverMutex: popoverMutex),
|
SwitchFieldButton(popoverMutex: popoverMutex),
|
||||||
if (typeOptionWidget != null) typeOptionWidget
|
if (typeOptionWidget != null) typeOptionWidget
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -73,9 +73,9 @@ class FieldTypeOptionEditor extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SwitchFieldButton extends StatelessWidget {
|
class SwitchFieldButton extends StatelessWidget {
|
||||||
final PopoverMutex popoverMutex;
|
final PopoverMutex popoverMutex;
|
||||||
const _SwitchFieldButton({
|
const SwitchFieldButton({
|
||||||
required this.popoverMutex,
|
required this.popoverMutex,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
@ -14,13 +14,18 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import '../../layout/sizes.dart';
|
import '../../layout/sizes.dart';
|
||||||
|
|
||||||
class RowActions extends StatelessWidget {
|
class RowActions extends StatelessWidget {
|
||||||
final RowInfo rowData;
|
final String viewId;
|
||||||
const RowActions({required this.rowData, Key? key}) : super(key: key);
|
final RowId rowId;
|
||||||
|
const RowActions({
|
||||||
|
required this.viewId,
|
||||||
|
required this.rowId,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => RowActionSheetBloc(rowInfo: rowData),
|
create: (context) => RowActionSheetBloc(viewId: viewId, rowId: rowId),
|
||||||
child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
|
child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final cells = _RowAction.values
|
final cells = _RowAction.values
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
@ -20,7 +20,8 @@ import "package:appflowy/generated/locale_keys.g.dart";
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
class GridRow extends StatefulWidget {
|
class GridRow extends StatefulWidget {
|
||||||
final RowInfo rowInfo;
|
final RowId viewId;
|
||||||
|
final RowId rowId;
|
||||||
final RowController dataController;
|
final RowController dataController;
|
||||||
final GridCellBuilder cellBuilder;
|
final GridCellBuilder cellBuilder;
|
||||||
final void Function(BuildContext, GridCellBuilder) openDetailPage;
|
final void Function(BuildContext, GridCellBuilder) openDetailPage;
|
||||||
@ -30,7 +31,8 @@ class GridRow extends StatefulWidget {
|
|||||||
|
|
||||||
const GridRow({
|
const GridRow({
|
||||||
super.key,
|
super.key,
|
||||||
required this.rowInfo,
|
required this.viewId,
|
||||||
|
required this.rowId,
|
||||||
required this.dataController,
|
required this.dataController,
|
||||||
required this.cellBuilder,
|
required this.cellBuilder,
|
||||||
required this.openDetailPage,
|
required this.openDetailPage,
|
||||||
@ -49,8 +51,9 @@ class _GridRowState extends State<GridRow> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_rowBloc = RowBloc(
|
_rowBloc = RowBloc(
|
||||||
rowInfo: widget.rowInfo,
|
rowId: widget.rowId,
|
||||||
dataController: widget.dataController,
|
dataController: widget.dataController,
|
||||||
|
viewId: widget.viewId,
|
||||||
);
|
);
|
||||||
_rowBloc.add(const RowEvent.initial());
|
_rowBloc.add(const RowEvent.initial());
|
||||||
}
|
}
|
||||||
@ -61,7 +64,8 @@ class _GridRowState extends State<GridRow> {
|
|||||||
value: _rowBloc,
|
value: _rowBloc,
|
||||||
child: _RowEnterRegion(
|
child: _RowEnterRegion(
|
||||||
child: BlocBuilder<RowBloc, RowState>(
|
child: BlocBuilder<RowBloc, RowState>(
|
||||||
buildWhen: (p, c) => p.rowInfo.rowPB.height != c.rowInfo.rowPB.height,
|
// The row need to rebuild when the cell count changes.
|
||||||
|
buildWhen: (p, c) => p.cellByFieldId.length != c.cellByFieldId.length,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final content = Expanded(
|
final content = Expanded(
|
||||||
child: RowContent(
|
child: RowContent(
|
||||||
@ -126,7 +130,11 @@ class _RowLeadingState extends State<_RowLeading> {
|
|||||||
direction: PopoverDirection.rightWithCenterAligned,
|
direction: PopoverDirection.rightWithCenterAligned,
|
||||||
margin: const EdgeInsets.all(6),
|
margin: const EdgeInsets.all(6),
|
||||||
popupBuilder: (BuildContext popoverContext) {
|
popupBuilder: (BuildContext popoverContext) {
|
||||||
return RowActions(rowData: context.read<RowBloc>().state.rowInfo);
|
final bloc = context.read<RowBloc>();
|
||||||
|
return RowActions(
|
||||||
|
viewId: bloc.viewId,
|
||||||
|
rowId: bloc.rowId,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Consumer<RegionStateNotifier>(
|
child: Consumer<RegionStateNotifier>(
|
||||||
builder: (context, state, _) {
|
builder: (context, state, _) {
|
||||||
@ -143,11 +151,11 @@ class _RowLeadingState extends State<_RowLeading> {
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const _InsertButton(),
|
const InsertRowButton(),
|
||||||
if (isDraggable) ...[
|
if (isDraggable) ...[
|
||||||
ReorderableDragStartListener(
|
ReorderableDragStartListener(
|
||||||
index: widget.index!,
|
index: widget.index!,
|
||||||
child: _MenuButton(
|
child: RowMenuButton(
|
||||||
isDragEnabled: isDraggable,
|
isDragEnabled: isDraggable,
|
||||||
openMenu: () {
|
openMenu: () {
|
||||||
popoverController.show();
|
popoverController.show();
|
||||||
@ -155,7 +163,7 @@ class _RowLeadingState extends State<_RowLeading> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
_MenuButton(
|
RowMenuButton(
|
||||||
openMenu: () {
|
openMenu: () {
|
||||||
popoverController.show();
|
popoverController.show();
|
||||||
},
|
},
|
||||||
@ -168,8 +176,8 @@ class _RowLeadingState extends State<_RowLeading> {
|
|||||||
bool get isDraggable => widget.index != null && widget.isDraggable;
|
bool get isDraggable => widget.index != null && widget.isDraggable;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InsertButton extends StatelessWidget {
|
class InsertRowButton extends StatelessWidget {
|
||||||
const _InsertButton({Key? key}) : super(key: key);
|
const InsertRowButton({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -188,20 +196,21 @@ class _InsertButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MenuButton extends StatefulWidget {
|
class RowMenuButton extends StatefulWidget {
|
||||||
final VoidCallback openMenu;
|
final VoidCallback openMenu;
|
||||||
final bool isDragEnabled;
|
final bool isDragEnabled;
|
||||||
|
|
||||||
const _MenuButton({
|
const RowMenuButton({
|
||||||
required this.openMenu,
|
required this.openMenu,
|
||||||
this.isDragEnabled = false,
|
this.isDragEnabled = false,
|
||||||
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_MenuButton> createState() => _MenuButtonState();
|
State<RowMenuButton> createState() => _RowMenuButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MenuButtonState extends State<_MenuButton> {
|
class _RowMenuButtonState extends State<RowMenuButton> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyIconButton(
|
return FlowyIconButton(
|
||||||
|
@ -17,7 +17,7 @@ import 'container/card_container.dart';
|
|||||||
|
|
||||||
/// Edit a database row with card style widget
|
/// Edit a database row with card style widget
|
||||||
class RowCard<CustomCardData> extends StatefulWidget {
|
class RowCard<CustomCardData> extends StatefulWidget {
|
||||||
final RowPB row;
|
final RowMetaPB rowMeta;
|
||||||
final String viewId;
|
final String viewId;
|
||||||
final String? groupingFieldId;
|
final String? groupingFieldId;
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ class RowCard<CustomCardData> extends StatefulWidget {
|
|||||||
final RowCardStyleConfiguration styleConfiguration;
|
final RowCardStyleConfiguration styleConfiguration;
|
||||||
|
|
||||||
const RowCard({
|
const RowCard({
|
||||||
required this.row,
|
required this.rowMeta,
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
this.groupingFieldId,
|
this.groupingFieldId,
|
||||||
required this.isEditing,
|
required this.isEditing,
|
||||||
@ -81,7 +81,7 @@ class _RowCardState<T> extends State<RowCard<T>> {
|
|||||||
viewId: widget.viewId,
|
viewId: widget.viewId,
|
||||||
groupFieldId: widget.groupingFieldId,
|
groupFieldId: widget.groupingFieldId,
|
||||||
isEditing: widget.isEditing,
|
isEditing: widget.isEditing,
|
||||||
row: widget.row,
|
rowMeta: widget.rowMeta,
|
||||||
rowCache: widget.rowCache,
|
rowCache: widget.rowCache,
|
||||||
)..add(const RowCardEvent.initial());
|
)..add(const RowCardEvent.initial());
|
||||||
|
|
||||||
@ -178,7 +178,8 @@ class _RowCardState<T> extends State<RowCard<T>> {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
case AccessoryType.more:
|
case AccessoryType.more:
|
||||||
return RowActions(
|
return RowActions(
|
||||||
rowData: context.read<CardBloc>().rowInfo(),
|
viewId: context.read<CardBloc>().viewId,
|
||||||
|
rowId: context.read<CardBloc>().rowMeta.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,24 +12,24 @@ import '../../application/row/row_service.dart';
|
|||||||
part 'card_bloc.freezed.dart';
|
part 'card_bloc.freezed.dart';
|
||||||
|
|
||||||
class CardBloc extends Bloc<RowCardEvent, RowCardState> {
|
class CardBloc extends Bloc<RowCardEvent, RowCardState> {
|
||||||
final RowPB row;
|
final RowMetaPB rowMeta;
|
||||||
final String? groupFieldId;
|
final String? groupFieldId;
|
||||||
final RowBackendService _rowBackendSvc;
|
final RowBackendService _rowBackendSvc;
|
||||||
final RowCache _rowCache;
|
final RowCache _rowCache;
|
||||||
VoidCallback? _rowCallback;
|
VoidCallback? _rowCallback;
|
||||||
|
final String viewId;
|
||||||
|
|
||||||
CardBloc({
|
CardBloc({
|
||||||
required this.row,
|
required this.rowMeta,
|
||||||
required this.groupFieldId,
|
required this.groupFieldId,
|
||||||
required String viewId,
|
required this.viewId,
|
||||||
required RowCache rowCache,
|
required RowCache rowCache,
|
||||||
required bool isEditing,
|
required bool isEditing,
|
||||||
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
}) : _rowBackendSvc = RowBackendService(viewId: viewId),
|
||||||
_rowCache = rowCache,
|
_rowCache = rowCache,
|
||||||
super(
|
super(
|
||||||
RowCardState.initial(
|
RowCardState.initial(
|
||||||
row,
|
_makeCells(groupFieldId, rowCache.loadGridCells(rowMeta)),
|
||||||
_makeCells(groupFieldId, rowCache.loadGridCells(row.id)),
|
|
||||||
isEditing,
|
isEditing,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
@ -70,13 +70,14 @@ class CardBloc extends Bloc<RowCardEvent, RowCardState> {
|
|||||||
fields: UnmodifiableListView(
|
fields: UnmodifiableListView(
|
||||||
state.cells.map((cell) => cell.fieldInfo).toList(),
|
state.cells.map((cell) => cell.fieldInfo).toList(),
|
||||||
),
|
),
|
||||||
rowPB: state.rowPB,
|
rowId: rowMeta.id,
|
||||||
|
rowMeta: rowMeta,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startListening() async {
|
Future<void> _startListening() async {
|
||||||
_rowCallback = _rowCache.addListener(
|
_rowCallback = _rowCache.addListener(
|
||||||
rowId: row.id,
|
rowId: rowMeta.id,
|
||||||
onCellUpdated: (cellMap, reason) {
|
onCellUpdated: (cellMap, reason) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
final cells = _makeCells(groupFieldId, cellMap);
|
final cells = _makeCells(groupFieldId, cellMap);
|
||||||
@ -118,19 +119,16 @@ class RowCardEvent with _$RowCardEvent {
|
|||||||
@freezed
|
@freezed
|
||||||
class RowCardState with _$RowCardState {
|
class RowCardState with _$RowCardState {
|
||||||
const factory RowCardState({
|
const factory RowCardState({
|
||||||
required RowPB rowPB,
|
|
||||||
required List<DatabaseCellContext> cells,
|
required List<DatabaseCellContext> cells,
|
||||||
required bool isEditing,
|
required bool isEditing,
|
||||||
RowsChangedReason? changeReason,
|
RowsChangedReason? changeReason,
|
||||||
}) = _RowCardState;
|
}) = _RowCardState;
|
||||||
|
|
||||||
factory RowCardState.initial(
|
factory RowCardState.initial(
|
||||||
RowPB rowPB,
|
|
||||||
List<DatabaseCellContext> cells,
|
List<DatabaseCellContext> cells,
|
||||||
bool isEditing,
|
bool isEditing,
|
||||||
) =>
|
) =>
|
||||||
RowCardState(
|
RowCardState(
|
||||||
rowPB: rowPB,
|
|
||||||
cells: cells,
|
cells: cells,
|
||||||
isEditing: isEditing,
|
isEditing: isEditing,
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
@ -47,27 +46,20 @@ class _DatabaseViewWidgetState extends State<DatabaseViewWidget> {
|
|||||||
return ValueListenableBuilder<ViewLayoutPB>(
|
return ValueListenableBuilder<ViewLayoutPB>(
|
||||||
valueListenable: _layoutTypeChangeNotifier,
|
valueListenable: _layoutTypeChangeNotifier,
|
||||||
builder: (_, __, ___) {
|
builder: (_, __, ___) {
|
||||||
return makePlugin(pluginType: view.pluginType, data: view)
|
return view.plugin().widgetBuilder.buildWidget();
|
||||||
.widgetBuilder
|
|
||||||
.buildWidget();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenOnViewUpdated() {
|
void _listenOnViewUpdated() {
|
||||||
_listener = ViewListener(view: widget.view)
|
_listener = ViewListener(viewId: widget.view.id)
|
||||||
..start(
|
..start(
|
||||||
onViewUpdated: (result) {
|
onViewUpdated: (updatedView) {
|
||||||
result.fold(
|
|
||||||
(updatedView) {
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
view = updatedView;
|
view = updatedView;
|
||||||
_layoutTypeChangeNotifier.value = view.layout;
|
_layoutTypeChangeNotifier.value = view.layout;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(r) => null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
_layoutTypeChangeNotifier = ValueNotifier(widget.view.layout);
|
_layoutTypeChangeNotifier = ValueNotifier(widget.view.layout);
|
||||||
|
@ -14,6 +14,7 @@ import 'cells/select_option_cell/select_option_cell.dart';
|
|||||||
import 'cells/text_cell/text_cell.dart';
|
import 'cells/text_cell/text_cell.dart';
|
||||||
import 'cells/url_cell/url_cell.dart';
|
import 'cells/url_cell/url_cell.dart';
|
||||||
|
|
||||||
|
/// Build the cell widget in Grid style.
|
||||||
class GridCellBuilder {
|
class GridCellBuilder {
|
||||||
final CellCache cellCache;
|
final CellCache cellCache;
|
||||||
GridCellBuilder({
|
GridCellBuilder({
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
export 'checkbox_cell/checkbox_cell.dart';
|
||||||
|
export 'checklist_cell/checklist_cell.dart';
|
||||||
|
export 'date_cell/date_cell.dart';
|
||||||
|
export 'number_cell/number_cell.dart';
|
||||||
|
export 'select_option_cell/select_option_cell.dart';
|
||||||
|
export 'text_cell/text_cell.dart';
|
||||||
|
export 'url_cell/url_cell.dart';
|
@ -40,8 +40,8 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
|
|||||||
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
|
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final icon = state.isSelected
|
final icon = state.isSelected
|
||||||
? svgWidget('editor/editor_check')
|
? const CheckboxCellCheck()
|
||||||
: svgWidget('editor/editor_uncheck');
|
: const CheckboxCellUncheck();
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -82,3 +82,21 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CheckboxCellCheck extends StatelessWidget {
|
||||||
|
const CheckboxCellCheck({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return svgWidget('editor/editor_check');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckboxCellUncheck extends StatelessWidget {
|
||||||
|
const CheckboxCellUncheck({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return svgWidget('editor/editor_uncheck');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -16,7 +16,9 @@ class ChecklistCardCellBloc
|
|||||||
ChecklistCardCellBloc({
|
ChecklistCardCellBloc({
|
||||||
required this.cellController,
|
required this.cellController,
|
||||||
}) : _checklistCellSvc = ChecklistCellBackendService(
|
}) : _checklistCellSvc = ChecklistCellBackendService(
|
||||||
cellContext: cellController.cellContext,
|
viewId: cellController.viewId,
|
||||||
|
fieldId: cellController.fieldId,
|
||||||
|
rowId: cellController.rowId,
|
||||||
),
|
),
|
||||||
super(ChecklistCellState.initial(cellController)) {
|
super(ChecklistCellState.initial(cellController)) {
|
||||||
on<ChecklistCellEvent>(
|
on<ChecklistCellEvent>(
|
||||||
|
@ -19,7 +19,9 @@ class ChecklistCellEditorBloc
|
|||||||
ChecklistCellEditorBloc({
|
ChecklistCellEditorBloc({
|
||||||
required this.cellController,
|
required this.cellController,
|
||||||
}) : _checklistCellService = ChecklistCellBackendService(
|
}) : _checklistCellService = ChecklistCellBackendService(
|
||||||
cellContext: cellController.cellContext,
|
viewId: cellController.viewId,
|
||||||
|
fieldId: cellController.fieldId,
|
||||||
|
rowId: cellController.rowId,
|
||||||
),
|
),
|
||||||
super(ChecklistCellEditorState.initial(cellController)) {
|
super(ChecklistCellEditorState.initial(cellController)) {
|
||||||
on<ChecklistCellEditorEvent>(
|
on<ChecklistCellEditorEvent>(
|
||||||
|
@ -22,7 +22,7 @@ class ChecklistProgressBar extends StatelessWidget {
|
|||||||
percent: percent,
|
percent: percent,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
progressColor: percent < 1.0
|
progressColor: percent < 1.0
|
||||||
? SelectOptionColorPB.Blue.toColor(context)
|
? SelectOptionColorPB.Purple.toColor(context)
|
||||||
: SelectOptionColorPB.Green.toColor(context),
|
: SelectOptionColorPB.Green.toColor(context),
|
||||||
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
|
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
|
||||||
barRadius: const Radius.circular(5),
|
barRadius: const Radius.circular(5),
|
||||||
|
@ -17,7 +17,9 @@ class SelectOptionCellEditorBloc
|
|||||||
SelectOptionCellEditorBloc({
|
SelectOptionCellEditorBloc({
|
||||||
required this.cellController,
|
required this.cellController,
|
||||||
}) : _selectOptionService = SelectOptionCellBackendService(
|
}) : _selectOptionService = SelectOptionCellBackendService(
|
||||||
cellContext: cellController.cellContext,
|
viewId: cellController.viewId,
|
||||||
|
fieldId: cellController.fieldId,
|
||||||
|
rowId: cellController.rowId,
|
||||||
),
|
),
|
||||||
super(SelectOptionEditorState.initial(cellController)) {
|
super(SelectOptionEditorState.initial(cellController)) {
|
||||||
on<SelectOptionEditorEvent>(
|
on<SelectOptionEditorEvent>(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../../../../grid/presentation/layout/sizes.dart';
|
import '../../../../grid/presentation/layout/sizes.dart';
|
||||||
@ -10,17 +11,23 @@ class GridTextCellStyle extends GridCellStyle {
|
|||||||
String? placeholder;
|
String? placeholder;
|
||||||
TextStyle? textStyle;
|
TextStyle? textStyle;
|
||||||
bool? autofocus;
|
bool? autofocus;
|
||||||
|
double emojiFontSize;
|
||||||
|
double emojiHPadding;
|
||||||
|
bool showEmoji;
|
||||||
|
|
||||||
GridTextCellStyle({
|
GridTextCellStyle({
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.textStyle,
|
this.textStyle,
|
||||||
this.autofocus,
|
this.autofocus,
|
||||||
|
this.showEmoji = true,
|
||||||
|
this.emojiFontSize = 16,
|
||||||
|
this.emojiHPadding = 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class GridTextCell extends GridCellWidget {
|
class GridTextCell extends GridCellWidget {
|
||||||
final CellControllerBuilder cellControllerBuilder;
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
late final GridTextCellStyle? cellStyle;
|
late final GridTextCellStyle cellStyle;
|
||||||
GridTextCell({
|
GridTextCell({
|
||||||
required this.cellControllerBuilder,
|
required this.cellControllerBuilder,
|
||||||
GridCellStyle? style,
|
GridCellStyle? style,
|
||||||
@ -29,7 +36,7 @@ class GridTextCell extends GridCellWidget {
|
|||||||
if (style != null) {
|
if (style != null) {
|
||||||
cellStyle = (style as GridTextCellStyle);
|
cellStyle = (style as GridTextCellStyle);
|
||||||
} else {
|
} else {
|
||||||
cellStyle = null;
|
cellStyle = GridTextCellStyle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,23 +73,41 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
|
|||||||
left: GridSize.cellContentInsets.left,
|
left: GridSize.cellContentInsets.left,
|
||||||
right: GridSize.cellContentInsets.right,
|
right: GridSize.cellContentInsets.right,
|
||||||
),
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (widget.cellStyle.showEmoji)
|
||||||
|
// Only build the emoji when it changes
|
||||||
|
BlocBuilder<TextCellBloc, TextCellState>(
|
||||||
|
buildWhen: (p, c) => p.emoji != c.emoji,
|
||||||
|
builder: (context, state) => Center(
|
||||||
|
child: FlowyText(
|
||||||
|
state.emoji,
|
||||||
|
fontSize: widget.cellStyle.emojiFontSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
HSpace(widget.cellStyle.emojiHPadding),
|
||||||
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
style: widget.cellStyle?.textStyle ??
|
style: widget.cellStyle.textStyle ??
|
||||||
Theme.of(context).textTheme.bodyMedium,
|
Theme.of(context).textTheme.bodyMedium,
|
||||||
autofocus: widget.cellStyle?.autofocus ?? false,
|
autofocus: widget.cellStyle.autofocus ?? false,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: EdgeInsets.only(
|
contentPadding: EdgeInsets.only(
|
||||||
top: GridSize.cellContentInsets.top,
|
top: GridSize.cellContentInsets.top,
|
||||||
bottom: GridSize.cellContentInsets.bottom,
|
bottom: GridSize.cellContentInsets.bottom,
|
||||||
),
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
hintText: widget.cellStyle?.placeholder,
|
hintText: widget.cellStyle.placeholder,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -26,6 +26,9 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
|||||||
didReceiveCellUpdate: (content) {
|
didReceiveCellUpdate: (content) {
|
||||||
emit(state.copyWith(content: content));
|
emit(state.copyWith(content: content));
|
||||||
},
|
},
|
||||||
|
didUpdateEmoji: (String emoji) {
|
||||||
|
emit(state.copyWith(emoji: emoji));
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -48,6 +51,11 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
|||||||
add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
onRowMetaChanged: () {
|
||||||
|
if (!isClosed) {
|
||||||
|
add(TextCellEvent.didUpdateEmoji(cellController.emoji ?? ""));
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,15 +66,18 @@ class TextCellEvent with _$TextCellEvent {
|
|||||||
const factory TextCellEvent.didReceiveCellUpdate(String cellContent) =
|
const factory TextCellEvent.didReceiveCellUpdate(String cellContent) =
|
||||||
_DidReceiveCellUpdate;
|
_DidReceiveCellUpdate;
|
||||||
const factory TextCellEvent.updateText(String text) = _UpdateText;
|
const factory TextCellEvent.updateText(String text) = _UpdateText;
|
||||||
|
const factory TextCellEvent.didUpdateEmoji(String emoji) = _UpdateEmoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class TextCellState with _$TextCellState {
|
class TextCellState with _$TextCellState {
|
||||||
const factory TextCellState({
|
const factory TextCellState({
|
||||||
required String content,
|
required String content,
|
||||||
|
required String emoji,
|
||||||
}) = _TextCellState;
|
}) = _TextCellState;
|
||||||
|
|
||||||
factory TextCellState.initial(TextCellController context) => TextCellState(
|
factory TextCellState.initial(TextCellController context) => TextCellState(
|
||||||
content: context.getCellData() ?? "",
|
content: context.getCellData() ?? "",
|
||||||
|
emoji: context.emoji ?? "",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,174 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/image.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class RowActionList extends StatelessWidget {
|
||||||
|
final RowController rowController;
|
||||||
|
const RowActionList({
|
||||||
|
required String viewId,
|
||||||
|
required this.rowController,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 10),
|
||||||
|
child: FlowyText(LocaleKeys.grid_row_action.tr()),
|
||||||
|
),
|
||||||
|
const VSpace(15),
|
||||||
|
RowDetailPageDeleteButton(rowId: rowController.rowId),
|
||||||
|
RowDetailPageDuplicateButton(
|
||||||
|
rowId: rowController.rowId,
|
||||||
|
groupId: rowController.groupId,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RowDetailPageDeleteButton extends StatelessWidget {
|
||||||
|
final String rowId;
|
||||||
|
const RowDetailPageDeleteButton({required this.rowId, Key? key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: GridSize.popoverItemHeight,
|
||||||
|
child: FlowyButton(
|
||||||
|
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
||||||
|
leftIcon: const FlowySvg(name: "home/trash"),
|
||||||
|
onTap: () {
|
||||||
|
context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
|
||||||
|
FlowyOverlay.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RowDetailPageDuplicateButton extends StatelessWidget {
|
||||||
|
final String rowId;
|
||||||
|
final String? groupId;
|
||||||
|
const RowDetailPageDuplicateButton({
|
||||||
|
required this.rowId,
|
||||||
|
this.groupId,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: GridSize.popoverItemHeight,
|
||||||
|
child: FlowyButton(
|
||||||
|
text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
|
||||||
|
leftIcon: const FlowySvg(name: "grid/duplicate"),
|
||||||
|
onTap: () {
|
||||||
|
context
|
||||||
|
.read<RowDetailBloc>()
|
||||||
|
.add(RowDetailEvent.duplicateRow(rowId, groupId));
|
||||||
|
FlowyOverlay.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateRowFieldButton extends StatefulWidget {
|
||||||
|
final String viewId;
|
||||||
|
|
||||||
|
const CreateRowFieldButton({
|
||||||
|
required this.viewId,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateRowFieldButton> createState() => _CreateRowFieldButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
|
||||||
|
late PopoverController popoverController;
|
||||||
|
late TypeOptionPB typeOption;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
popoverController = PopoverController();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppFlowyPopover(
|
||||||
|
constraints: BoxConstraints.loose(const Size(240, 200)),
|
||||||
|
controller: popoverController,
|
||||||
|
direction: PopoverDirection.topWithLeftAligned,
|
||||||
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: FlowyButton(
|
||||||
|
text: FlowyText.medium(
|
||||||
|
LocaleKeys.grid_field_newProperty.tr(),
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
onTap: () async {
|
||||||
|
final result = await TypeOptionBackendService.createFieldTypeOption(
|
||||||
|
viewId: widget.viewId,
|
||||||
|
);
|
||||||
|
result.fold(
|
||||||
|
(l) {
|
||||||
|
typeOption = l;
|
||||||
|
popoverController.show();
|
||||||
|
},
|
||||||
|
(r) => Log.error("Failed to create field type option: $r"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
leftIcon: svgWidget(
|
||||||
|
"home/add",
|
||||||
|
color: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
popupBuilder: (BuildContext popOverContext) {
|
||||||
|
return FieldEditor(
|
||||||
|
viewId: widget.viewId,
|
||||||
|
typeOptionLoader: FieldTypeOptionLoader(
|
||||||
|
viewId: widget.viewId,
|
||||||
|
field: typeOption.field_2,
|
||||||
|
),
|
||||||
|
onDeleted: (fieldId) {
|
||||||
|
popoverController.close();
|
||||||
|
NavigatorAlertDialog(
|
||||||
|
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||||
|
confirm: () {
|
||||||
|
context
|
||||||
|
.read<RowDetailBloc>()
|
||||||
|
.add(RowDetailEvent.deleteField(fieldId));
|
||||||
|
},
|
||||||
|
).show(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,268 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
typedef RowBannerCellBuilder = Widget Function(String fieldId);
|
||||||
|
|
||||||
|
class RowBanner extends StatefulWidget {
|
||||||
|
final String viewId;
|
||||||
|
final RowMetaPB rowMeta;
|
||||||
|
final RowBannerCellBuilder cellBuilder;
|
||||||
|
const RowBanner({
|
||||||
|
required this.viewId,
|
||||||
|
required this.rowMeta,
|
||||||
|
required this.cellBuilder,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RowBanner> createState() => _RowBannerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RowBannerState extends State<RowBanner> {
|
||||||
|
final _isHovering = ValueNotifier(false);
|
||||||
|
final popoverController = PopoverController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider<RowBannerBloc>(
|
||||||
|
create: (context) => RowBannerBloc(
|
||||||
|
viewId: widget.viewId,
|
||||||
|
rowMeta: widget.rowMeta,
|
||||||
|
)..add(const RowBannerEvent.initial()),
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (event) => _isHovering.value = true,
|
||||||
|
onExit: (event) => _isHovering.value = false,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 80,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 30,
|
||||||
|
child: _BannerAction(
|
||||||
|
isHovering: _isHovering,
|
||||||
|
popoverController: popoverController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_BannerTitle(
|
||||||
|
cellBuilder: widget.cellBuilder,
|
||||||
|
popoverController: popoverController,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BannerAction extends StatelessWidget {
|
||||||
|
final ValueNotifier<bool> isHovering;
|
||||||
|
final PopoverController popoverController;
|
||||||
|
const _BannerAction({
|
||||||
|
required this.isHovering,
|
||||||
|
required this.popoverController,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: isHovering,
|
||||||
|
builder: (BuildContext context, bool value, Widget? child) {
|
||||||
|
if (value) {
|
||||||
|
return BlocBuilder<RowBannerBloc, RowBannerState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final children = <Widget>[];
|
||||||
|
final rowMeta = state.rowMeta;
|
||||||
|
if (rowMeta.icon.isEmpty) {
|
||||||
|
children.add(
|
||||||
|
EmojiPickerButton(
|
||||||
|
showEmojiPicker: () => popoverController.show(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
children.add(
|
||||||
|
RemoveEmojiButton(
|
||||||
|
onRemoved: () {
|
||||||
|
context
|
||||||
|
.read<RowBannerBloc>()
|
||||||
|
.add(const RowBannerEvent.setIcon(''));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox(height: _kBannerActionHeight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BannerTitle extends StatefulWidget {
|
||||||
|
final RowBannerCellBuilder cellBuilder;
|
||||||
|
final PopoverController popoverController;
|
||||||
|
const _BannerTitle({
|
||||||
|
required this.cellBuilder,
|
||||||
|
required this.popoverController,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_BannerTitle> createState() => _BannerTitleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BannerTitleState extends State<_BannerTitle> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<RowBannerBloc, RowBannerState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final children = <Widget>[];
|
||||||
|
|
||||||
|
if (state.rowMeta.icon.isNotEmpty) {
|
||||||
|
children.add(
|
||||||
|
EmojiButton(
|
||||||
|
emoji: state.rowMeta.icon,
|
||||||
|
showEmojiPicker: () => widget.popoverController.show(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.primaryField != null) {
|
||||||
|
children.add(
|
||||||
|
Expanded(
|
||||||
|
child: widget.cellBuilder(state.primaryField!.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppFlowyPopover(
|
||||||
|
controller: widget.popoverController,
|
||||||
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
|
popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) {
|
||||||
|
context
|
||||||
|
.read<RowBannerBloc>()
|
||||||
|
.add(RowBannerEvent.setIcon(emoji.emoji));
|
||||||
|
widget.popoverController.close();
|
||||||
|
}),
|
||||||
|
child: Row(children: children),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef OnSubmittedEmoji = void Function(Emoji emoji);
|
||||||
|
const _kBannerActionHeight = 40.0;
|
||||||
|
|
||||||
|
class EmojiButton extends StatelessWidget {
|
||||||
|
final String emoji;
|
||||||
|
final VoidCallback showEmojiPicker;
|
||||||
|
|
||||||
|
const EmojiButton({
|
||||||
|
required this.emoji,
|
||||||
|
required this.showEmojiPicker,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: _kBannerActionHeight,
|
||||||
|
width: _kBannerActionHeight,
|
||||||
|
child: FlowyButton(
|
||||||
|
margin: const EdgeInsets.all(4),
|
||||||
|
text: FlowyText.medium(
|
||||||
|
emoji,
|
||||||
|
fontSize: 30,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
onTap: showEmojiPicker,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmojiPickerButton extends StatefulWidget {
|
||||||
|
final VoidCallback showEmojiPicker;
|
||||||
|
const EmojiPickerButton({
|
||||||
|
super.key,
|
||||||
|
required this.showEmojiPicker,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EmojiPickerButton> createState() => _EmojiPickerButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EmojiPickerButtonState extends State<EmojiPickerButton> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 26,
|
||||||
|
width: 160,
|
||||||
|
child: FlowyButton(
|
||||||
|
text: FlowyText.medium(
|
||||||
|
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||||
|
),
|
||||||
|
leftIcon: const Icon(
|
||||||
|
Icons.emoji_emotions,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
onTap: widget.showEmojiPicker,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoveEmojiButton extends StatelessWidget {
|
||||||
|
final VoidCallback onRemoved;
|
||||||
|
RemoveEmojiButton({
|
||||||
|
super.key,
|
||||||
|
required this.onRemoved,
|
||||||
|
});
|
||||||
|
|
||||||
|
final popoverController = PopoverController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 26,
|
||||||
|
width: 160,
|
||||||
|
child: FlowyButton(
|
||||||
|
text: FlowyText.medium(
|
||||||
|
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||||
|
),
|
||||||
|
leftIcon: const Icon(
|
||||||
|
Icons.emoji_emotions,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
onTap: onRemoved,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 250,
|
||||||
|
child: EmojiSelectionMenu(
|
||||||
|
onSubmitted: onSubmitted,
|
||||||
|
onExit: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
@ -1,31 +1,20 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
|
||||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
|
||||||
|
|
||||||
import '../../grid/presentation/layout/sizes.dart';
|
|
||||||
import 'accessory/cell_accessory.dart';
|
|
||||||
import 'cell_builder.dart';
|
import 'cell_builder.dart';
|
||||||
import 'cells/date_cell/date_cell.dart';
|
|
||||||
import 'cells/select_option_cell/select_option_cell.dart';
|
|
||||||
import 'cells/text_cell/text_cell.dart';
|
import 'cells/text_cell/text_cell.dart';
|
||||||
import 'cells/url_cell/url_cell.dart';
|
import 'row_action.dart';
|
||||||
import '../../grid/presentation/widgets/header/field_cell.dart';
|
import 'row_banner.dart';
|
||||||
import '../../grid/presentation/widgets/header/field_editor.dart';
|
import 'row_property.dart';
|
||||||
|
|
||||||
class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
|
class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
|
||||||
final RowController rowController;
|
final RowController rowController;
|
||||||
@ -46,6 +35,14 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RowDetailPageState extends State<RowDetailPage> {
|
class _RowDetailPageState extends State<RowDetailPage> {
|
||||||
|
final scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyDialog(
|
return FlowyDialog(
|
||||||
@ -55,43 +52,91 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
|||||||
..add(const RowDetailEvent.initial());
|
..add(const RowDetailEvent.initial());
|
||||||
},
|
},
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
controller: scrollController,
|
||||||
children: [
|
children: [
|
||||||
// using ListView here for future expansion:
|
_rowBanner(),
|
||||||
// - header and cover image
|
|
||||||
// - lower rich text area
|
|
||||||
IntrinsicHeight(child: _responsiveRowInfo()),
|
IntrinsicHeight(child: _responsiveRowInfo()),
|
||||||
const Divider(height: 1.0),
|
const Divider(height: 1.0),
|
||||||
const SizedBox(height: 10),
|
const VSpace(10),
|
||||||
|
RowDocument(
|
||||||
|
viewId: widget.rowController.viewId,
|
||||||
|
rowId: widget.rowController.rowId,
|
||||||
|
scrollController: scrollController,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _rowBanner() {
|
||||||
|
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final paddingOffset = getHorizontalPadding(context);
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: paddingOffset,
|
||||||
|
right: paddingOffset,
|
||||||
|
top: 20,
|
||||||
|
),
|
||||||
|
child: RowBanner(
|
||||||
|
rowMeta: widget.rowController.rowMeta,
|
||||||
|
viewId: widget.rowController.viewId,
|
||||||
|
cellBuilder: (fieldId) {
|
||||||
|
final fieldInfo = state.cells
|
||||||
|
.firstWhereOrNull(
|
||||||
|
(e) => e.fieldInfo.field.id == fieldId,
|
||||||
|
)
|
||||||
|
?.fieldInfo;
|
||||||
|
|
||||||
|
if (fieldInfo != null) {
|
||||||
|
final style = GridTextCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||||
|
showEmoji: false,
|
||||||
|
autofocus: true,
|
||||||
|
);
|
||||||
|
final cellContext = DatabaseCellContext(
|
||||||
|
viewId: widget.rowController.viewId,
|
||||||
|
rowMeta: widget.rowController.rowMeta,
|
||||||
|
fieldInfo: fieldInfo,
|
||||||
|
);
|
||||||
|
return widget.cellBuilder.build(cellContext, style: style);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _responsiveRowInfo() {
|
Widget _responsiveRowInfo() {
|
||||||
final rowDataColumn = _PropertyColumn(
|
final rowDataColumn = RowPropertyList(
|
||||||
cellBuilder: widget.cellBuilder,
|
cellBuilder: widget.cellBuilder,
|
||||||
viewId: widget.rowController.viewId,
|
viewId: widget.rowController.viewId,
|
||||||
);
|
);
|
||||||
final rowOptionColumn = _RowOptionColumn(
|
final rowOptionColumn = RowActionList(
|
||||||
viewId: widget.rowController.viewId,
|
viewId: widget.rowController.viewId,
|
||||||
rowController: widget.rowController,
|
rowController: widget.rowController,
|
||||||
);
|
);
|
||||||
|
final paddingOffset = getHorizontalPadding(context);
|
||||||
if (MediaQuery.of(context).size.width > 800) {
|
if (MediaQuery.of(context).size.width > 800) {
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 4,
|
flex: 3,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(50, 50, 20, 20),
|
padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
|
||||||
child: rowDataColumn,
|
child: rowDataColumn,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const VerticalDivider(width: 1.0),
|
const VerticalDivider(width: 1.0),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
|
padding: EdgeInsets.fromLTRB(20, 0, paddingOffset, 0),
|
||||||
child: rowOptionColumn,
|
child: rowOptionColumn,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -103,12 +148,12 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 50, 20, 20),
|
padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20),
|
||||||
child: rowDataColumn,
|
child: rowDataColumn,
|
||||||
),
|
),
|
||||||
const Divider(height: 1.0),
|
const Divider(height: 1.0),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: EdgeInsets.symmetric(horizontal: paddingOffset),
|
||||||
child: rowOptionColumn,
|
child: rowOptionColumn,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -117,352 +162,10 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PropertyColumn extends StatelessWidget {
|
double getHorizontalPadding(BuildContext context) {
|
||||||
final String viewId;
|
if (MediaQuery.of(context).size.width > 800) {
|
||||||
final GridCellBuilder cellBuilder;
|
return 50;
|
||||||
const _PropertyColumn({
|
} else {
|
||||||
required this.viewId,
|
return 20;
|
||||||
required this.cellBuilder,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
|
||||||
buildWhen: (previous, current) => previous.gridCells != current.gridCells,
|
|
||||||
builder: (context, state) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_RowTitle(
|
|
||||||
cellContext: state.gridCells
|
|
||||||
.firstWhereOrNull((e) => e.fieldInfo.isPrimary),
|
|
||||||
cellBuilder: cellBuilder,
|
|
||||||
),
|
|
||||||
const VSpace(20),
|
|
||||||
...state.gridCells
|
|
||||||
.where((element) => !element.fieldInfo.isPrimary)
|
|
||||||
.map(
|
|
||||||
(cell) => Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 4.0),
|
|
||||||
child: _PropertyCell(
|
|
||||||
cellContext: cell,
|
|
||||||
cellBuilder: cellBuilder,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
const VSpace(20),
|
|
||||||
_CreatePropertyButton(viewId: viewId),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RowTitle extends StatelessWidget {
|
|
||||||
final DatabaseCellContext? cellContext;
|
|
||||||
final GridCellBuilder cellBuilder;
|
|
||||||
const _RowTitle({this.cellContext, required this.cellBuilder, Key? key})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (cellContext == null) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
final style = GridTextCellStyle(
|
|
||||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
||||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
|
||||||
autofocus: true,
|
|
||||||
);
|
|
||||||
return cellBuilder.build(cellContext!, style: style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CreatePropertyButton extends StatefulWidget {
|
|
||||||
final String viewId;
|
|
||||||
|
|
||||||
const _CreatePropertyButton({
|
|
||||||
required this.viewId,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_CreatePropertyButton> createState() => _CreatePropertyButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CreatePropertyButtonState extends State<_CreatePropertyButton> {
|
|
||||||
late PopoverController popoverController;
|
|
||||||
late TypeOptionPB typeOption;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
popoverController = PopoverController();
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AppFlowyPopover(
|
|
||||||
constraints: BoxConstraints.loose(const Size(240, 200)),
|
|
||||||
controller: popoverController,
|
|
||||||
direction: PopoverDirection.topWithLeftAligned,
|
|
||||||
triggerActions: PopoverTriggerFlags.none,
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 40,
|
|
||||||
child: FlowyButton(
|
|
||||||
text: FlowyText.medium(
|
|
||||||
LocaleKeys.grid_field_newProperty.tr(),
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
),
|
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
|
||||||
onTap: () async {
|
|
||||||
final result = await TypeOptionBackendService.createFieldTypeOption(
|
|
||||||
viewId: widget.viewId,
|
|
||||||
);
|
|
||||||
result.fold(
|
|
||||||
(l) {
|
|
||||||
typeOption = l;
|
|
||||||
popoverController.show();
|
|
||||||
},
|
|
||||||
(r) => Log.error("Failed to create field type option: $r"),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
leftIcon: svgWidget(
|
|
||||||
"home/add",
|
|
||||||
color: AFThemeExtension.of(context).textColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
popupBuilder: (BuildContext popOverContext) {
|
|
||||||
return FieldEditor(
|
|
||||||
viewId: widget.viewId,
|
|
||||||
typeOptionLoader: FieldTypeOptionLoader(
|
|
||||||
viewId: widget.viewId,
|
|
||||||
field: typeOption.field_2,
|
|
||||||
),
|
|
||||||
onDeleted: (fieldId) {
|
|
||||||
popoverController.close();
|
|
||||||
NavigatorAlertDialog(
|
|
||||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
|
||||||
confirm: () {
|
|
||||||
context
|
|
||||||
.read<RowDetailBloc>()
|
|
||||||
.add(RowDetailEvent.deleteField(fieldId));
|
|
||||||
},
|
|
||||||
).show(context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PropertyCell extends StatefulWidget {
|
|
||||||
final DatabaseCellContext cellContext;
|
|
||||||
final GridCellBuilder cellBuilder;
|
|
||||||
const _PropertyCell({
|
|
||||||
required this.cellContext,
|
|
||||||
required this.cellBuilder,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _PropertyCellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PropertyCellState extends State<_PropertyCell> {
|
|
||||||
final PopoverController popover = PopoverController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final style = _customCellStyle(widget.cellContext.fieldType);
|
|
||||||
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
|
||||||
|
|
||||||
final gesture = GestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
onTap: () => cell.beginFocus.notify(),
|
|
||||||
child: AccessoryHover(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
|
|
||||||
child: cell,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return IntrinsicHeight(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(minHeight: 30),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
AppFlowyPopover(
|
|
||||||
controller: popover,
|
|
||||||
constraints: BoxConstraints.loose(const Size(240, 600)),
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
triggerActions: PopoverTriggerFlags.none,
|
|
||||||
popupBuilder: (popoverContext) => buildFieldEditor(),
|
|
||||||
child: SizedBox(
|
|
||||||
width: 150,
|
|
||||||
child: FieldCellButton(
|
|
||||||
field: widget.cellContext.fieldInfo.field,
|
|
||||||
onTap: () => popover.show(),
|
|
||||||
radius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const HSpace(10),
|
|
||||||
Expanded(child: gesture),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildFieldEditor() {
|
|
||||||
return FieldEditor(
|
|
||||||
viewId: widget.cellContext.viewId,
|
|
||||||
isGroupingField: widget.cellContext.fieldInfo.isGroupField,
|
|
||||||
typeOptionLoader: FieldTypeOptionLoader(
|
|
||||||
viewId: widget.cellContext.viewId,
|
|
||||||
field: widget.cellContext.fieldInfo.field,
|
|
||||||
),
|
|
||||||
onHidden: (fieldId) {
|
|
||||||
popover.close();
|
|
||||||
context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
|
|
||||||
},
|
|
||||||
onDeleted: (fieldId) {
|
|
||||||
popover.close();
|
|
||||||
|
|
||||||
NavigatorAlertDialog(
|
|
||||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
|
||||||
confirm: () {
|
|
||||||
context
|
|
||||||
.read<RowDetailBloc>()
|
|
||||||
.add(RowDetailEvent.deleteField(fieldId));
|
|
||||||
},
|
|
||||||
).show(context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
|
||||||
switch (fieldType) {
|
|
||||||
case FieldType.Checkbox:
|
|
||||||
return null;
|
|
||||||
case FieldType.DateTime:
|
|
||||||
case FieldType.LastEditedTime:
|
|
||||||
case FieldType.CreatedTime:
|
|
||||||
return DateCellStyle(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
);
|
|
||||||
case FieldType.MultiSelect:
|
|
||||||
return SelectOptionCellStyle(
|
|
||||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
||||||
);
|
|
||||||
case FieldType.Checklist:
|
|
||||||
return SelectOptionCellStyle(
|
|
||||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
||||||
);
|
|
||||||
case FieldType.Number:
|
|
||||||
return null;
|
|
||||||
case FieldType.RichText:
|
|
||||||
return GridTextCellStyle(
|
|
||||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
||||||
);
|
|
||||||
case FieldType.SingleSelect:
|
|
||||||
return SelectOptionCellStyle(
|
|
||||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
||||||
);
|
|
||||||
|
|
||||||
case FieldType.URL:
|
|
||||||
return GridURLCellStyle(
|
|
||||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
|
||||||
accessoryTypes: [
|
|
||||||
GridURLCellAccessoryType.copyURL,
|
|
||||||
GridURLCellAccessoryType.visitURL,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw UnimplementedError;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RowOptionColumn extends StatelessWidget {
|
|
||||||
final RowController rowController;
|
|
||||||
const _RowOptionColumn({
|
|
||||||
required String viewId,
|
|
||||||
required this.rowController,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 10),
|
|
||||||
child: FlowyText(LocaleKeys.grid_row_action.tr()),
|
|
||||||
),
|
|
||||||
const VSpace(15),
|
|
||||||
_DeleteButton(rowId: rowController.rowId),
|
|
||||||
_DuplicateButton(
|
|
||||||
rowId: rowController.rowId,
|
|
||||||
groupId: rowController.groupId,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DeleteButton extends StatelessWidget {
|
|
||||||
final String rowId;
|
|
||||||
const _DeleteButton({required this.rowId, Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: GridSize.popoverItemHeight,
|
|
||||||
child: FlowyButton(
|
|
||||||
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
|
||||||
leftIcon: const FlowySvg(name: "home/trash"),
|
|
||||||
onTap: () {
|
|
||||||
context.read<RowDetailBloc>().add(RowDetailEvent.deleteRow(rowId));
|
|
||||||
FlowyOverlay.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DuplicateButton extends StatelessWidget {
|
|
||||||
final String rowId;
|
|
||||||
final String? groupId;
|
|
||||||
const _DuplicateButton({
|
|
||||||
required this.rowId,
|
|
||||||
this.groupId,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: GridSize.popoverItemHeight,
|
|
||||||
child: FlowyButton(
|
|
||||||
text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
|
|
||||||
leftIcon: const FlowySvg(name: "grid/duplicate"),
|
|
||||||
onTap: () {
|
|
||||||
context
|
|
||||||
.read<RowDetailBloc>()
|
|
||||||
.add(RowDetailEvent.duplicateRow(rowId, groupId));
|
|
||||||
FlowyOverlay.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,120 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_document_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class RowDocument extends StatelessWidget {
|
||||||
|
const RowDocument({
|
||||||
|
super.key,
|
||||||
|
required this.viewId,
|
||||||
|
required this.rowId,
|
||||||
|
required this.scrollController,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String viewId;
|
||||||
|
final String rowId;
|
||||||
|
final ScrollController scrollController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider<RowDocumentBloc>(
|
||||||
|
create: (context) => RowDocumentBloc(
|
||||||
|
viewId: viewId,
|
||||||
|
rowId: rowId,
|
||||||
|
)..add(
|
||||||
|
const RowDocumentEvent.initial(),
|
||||||
|
),
|
||||||
|
child: BlocBuilder<RowDocumentBloc, RowDocumentState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return state.loadingState.when(
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
error: (error) => FlowyErrorPage(
|
||||||
|
error.toString(),
|
||||||
|
),
|
||||||
|
finish: () => RowEditor(
|
||||||
|
viewPB: state.viewPB!,
|
||||||
|
scrollController: scrollController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RowEditor extends StatefulWidget {
|
||||||
|
const RowEditor({
|
||||||
|
super.key,
|
||||||
|
required this.viewPB,
|
||||||
|
required this.scrollController,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewPB viewPB;
|
||||||
|
final ScrollController scrollController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RowEditor> createState() => _RowEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RowEditorState extends State<RowEditor> {
|
||||||
|
late final DocumentBloc documentBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
documentBloc = DocumentBloc(view: widget.viewPB)
|
||||||
|
..add(const DocumentEvent.initial());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
documentBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(create: (_) => DocumentAppearanceCubit()),
|
||||||
|
BlocProvider.value(value: documentBloc),
|
||||||
|
],
|
||||||
|
child: BlocBuilder<DocumentBloc, DocumentState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return state.loadingState.when(
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
finish: (result) {
|
||||||
|
return result.fold(
|
||||||
|
(error) => FlowyErrorPage(
|
||||||
|
error.toString(),
|
||||||
|
),
|
||||||
|
(_) {
|
||||||
|
final editorState = documentBloc.editorState;
|
||||||
|
if (editorState == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return IntrinsicHeight(
|
||||||
|
child: AppFlowyEditorPage(
|
||||||
|
shrinkWrap: true,
|
||||||
|
autoFocus: false,
|
||||||
|
editorState: editorState,
|
||||||
|
scrollController: widget.scrollController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,192 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import 'accessory/cell_accessory.dart';
|
||||||
|
import 'cell_builder.dart';
|
||||||
|
import 'cells/date_cell/date_cell.dart';
|
||||||
|
import 'cells/select_option_cell/select_option_cell.dart';
|
||||||
|
import 'cells/text_cell/text_cell.dart';
|
||||||
|
import 'cells/url_cell/url_cell.dart';
|
||||||
|
|
||||||
|
/// Display the row properties in a list. Only use this widget in the
|
||||||
|
/// [RowDetailPage].
|
||||||
|
///
|
||||||
|
class RowPropertyList extends StatelessWidget {
|
||||||
|
final String viewId;
|
||||||
|
final GridCellBuilder cellBuilder;
|
||||||
|
const RowPropertyList({
|
||||||
|
required this.viewId,
|
||||||
|
required this.cellBuilder,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
||||||
|
buildWhen: (previous, current) => previous.cells != current.cells,
|
||||||
|
builder: (context, state) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// The rest of the fields are displayed in the order of the field
|
||||||
|
// list
|
||||||
|
...state.cells
|
||||||
|
.where((element) => !element.fieldInfo.isPrimary)
|
||||||
|
.map(
|
||||||
|
(cell) => _PropertyCell(
|
||||||
|
cellContext: cell,
|
||||||
|
cellBuilder: cellBuilder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
const VSpace(20),
|
||||||
|
|
||||||
|
// Create a new property(field) button
|
||||||
|
CreateRowFieldButton(viewId: viewId),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PropertyCell extends StatefulWidget {
|
||||||
|
final DatabaseCellContext cellContext;
|
||||||
|
final GridCellBuilder cellBuilder;
|
||||||
|
const _PropertyCell({
|
||||||
|
required this.cellContext,
|
||||||
|
required this.cellBuilder,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _PropertyCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PropertyCellState extends State<_PropertyCell> {
|
||||||
|
final PopoverController popover = PopoverController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final style = _customCellStyle(widget.cellContext.fieldType);
|
||||||
|
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
||||||
|
|
||||||
|
final gesture = GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: () => cell.beginFocus.notify(),
|
||||||
|
child: AccessoryHover(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3),
|
||||||
|
child: cell,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return IntrinsicHeight(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minHeight: 30),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
AppFlowyPopover(
|
||||||
|
controller: popover,
|
||||||
|
constraints: BoxConstraints.loose(const Size(240, 600)),
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
|
popupBuilder: (popoverContext) => buildFieldEditor(),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 150,
|
||||||
|
child: FieldCellButton(
|
||||||
|
field: widget.cellContext.fieldInfo.field,
|
||||||
|
onTap: () => popover.show(),
|
||||||
|
radius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: gesture),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildFieldEditor() {
|
||||||
|
return FieldEditor(
|
||||||
|
viewId: widget.cellContext.viewId,
|
||||||
|
isGroupingField: widget.cellContext.fieldInfo.isGroupField,
|
||||||
|
typeOptionLoader: FieldTypeOptionLoader(
|
||||||
|
viewId: widget.cellContext.viewId,
|
||||||
|
field: widget.cellContext.fieldInfo.field,
|
||||||
|
),
|
||||||
|
onHidden: (fieldId) {
|
||||||
|
popover.close();
|
||||||
|
context.read<RowDetailBloc>().add(RowDetailEvent.hideField(fieldId));
|
||||||
|
},
|
||||||
|
onDeleted: (fieldId) {
|
||||||
|
popover.close();
|
||||||
|
|
||||||
|
NavigatorAlertDialog(
|
||||||
|
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||||
|
confirm: () {
|
||||||
|
context
|
||||||
|
.read<RowDetailBloc>()
|
||||||
|
.add(RowDetailEvent.deleteField(fieldId));
|
||||||
|
},
|
||||||
|
).show(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldType.Checkbox:
|
||||||
|
return null;
|
||||||
|
case FieldType.DateTime:
|
||||||
|
case FieldType.LastEditedTime:
|
||||||
|
case FieldType.CreatedTime:
|
||||||
|
return DateCellStyle(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
);
|
||||||
|
case FieldType.MultiSelect:
|
||||||
|
return SelectOptionCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
);
|
||||||
|
case FieldType.Checklist:
|
||||||
|
return SelectOptionCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
);
|
||||||
|
case FieldType.Number:
|
||||||
|
return null;
|
||||||
|
case FieldType.RichText:
|
||||||
|
return GridTextCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
);
|
||||||
|
case FieldType.SingleSelect:
|
||||||
|
return SelectOptionCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
);
|
||||||
|
|
||||||
|
case FieldType.URL:
|
||||||
|
return GridURLCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
accessoryTypes: [
|
||||||
|
GridURLCellAccessoryType.copyURL,
|
||||||
|
GridURLCellAccessoryType.visitURL,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw UnimplementedError;
|
||||||
|
}
|
@ -23,7 +23,7 @@ class DatabaseSettingList extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cells = actionsForDatabaseLayout(databaseContoller.databaseLayout)
|
final cells = actionsForDatabaseLayout(databaseContoller.databaseLayout)
|
||||||
.map((action) {
|
.map((action) {
|
||||||
return _SettingItem(
|
return DatabaseSettingItem(
|
||||||
action: action,
|
action: action,
|
||||||
onAction: (action) => onAction(action, databaseContoller),
|
onAction: (action) => onAction(action, databaseContoller),
|
||||||
);
|
);
|
||||||
@ -44,11 +44,11 @@ class DatabaseSettingList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SettingItem extends StatelessWidget {
|
class DatabaseSettingItem extends StatelessWidget {
|
||||||
final DatabaseSettingAction action;
|
final DatabaseSettingAction action;
|
||||||
final Function(DatabaseSettingAction) onAction;
|
final Function(DatabaseSettingAction) onAction;
|
||||||
|
|
||||||
const _SettingItem({
|
const DatabaseSettingItem({
|
||||||
required this.action,
|
required this.action,
|
||||||
required this.onAction,
|
required this.onAction,
|
||||||
Key? key,
|
Key? key,
|
||||||
|
@ -23,7 +23,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
DocumentBloc({
|
DocumentBloc({
|
||||||
required this.view,
|
required this.view,
|
||||||
}) : _documentListener = DocumentListener(id: view.id),
|
}) : _documentListener = DocumentListener(id: view.id),
|
||||||
_viewListener = ViewListener(view: view),
|
_viewListener = ViewListener(viewId: view.id),
|
||||||
_documentService = DocumentService(),
|
_documentService = DocumentService(),
|
||||||
_trashService = TrashService(),
|
_trashService = TrashService(),
|
||||||
super(DocumentState.initial()) {
|
super(DocumentState.initial()) {
|
||||||
|
@ -48,8 +48,12 @@ class DocumentPlugin extends Plugin<int> {
|
|||||||
DocumentPlugin({
|
DocumentPlugin({
|
||||||
required PluginType pluginType,
|
required PluginType pluginType,
|
||||||
required ViewPB view,
|
required ViewPB view,
|
||||||
|
bool listenOnViewChanged = false,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : notifier = ViewPluginNotifier(view: view) {
|
}) : notifier = ViewPluginNotifier(
|
||||||
|
view: view,
|
||||||
|
listenOnViewChanged: listenOnViewChanged,
|
||||||
|
) {
|
||||||
_pluginType = pluginType;
|
_pluginType = pluginType;
|
||||||
_documentAppearanceCubit.fetch();
|
_documentAppearanceCubit.fetch();
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,23 @@ class AppFlowyEditorPage extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
this.header,
|
this.header,
|
||||||
|
this.shrinkWrap = false,
|
||||||
|
this.scrollController,
|
||||||
|
this.autoFocus,
|
||||||
});
|
});
|
||||||
|
|
||||||
final EditorState editorState;
|
|
||||||
final Widget? header;
|
final Widget? header;
|
||||||
|
final EditorState editorState;
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
final bool shrinkWrap;
|
||||||
|
final bool? autoFocus;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
|
State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||||
final scrollController = ScrollController();
|
late final ScrollController effectiveScrollController;
|
||||||
|
|
||||||
final List<CommandShortcutEvent> commandShortcutEvents = [
|
final List<CommandShortcutEvent> commandShortcutEvents = [
|
||||||
...codeBlockCommands,
|
...codeBlockCommands,
|
||||||
@ -90,6 +96,20 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
);
|
);
|
||||||
DocumentBloc get documentBloc => context.read<DocumentBloc>();
|
DocumentBloc get documentBloc => context.read<DocumentBloc>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
effectiveScrollController = widget.scrollController ?? ScrollController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (widget.scrollController == null) {
|
||||||
|
effectiveScrollController.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final (bool autoFocus, Selection? selection) =
|
final (bool autoFocus, Selection? selection) =
|
||||||
@ -98,9 +118,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
final editor = AppFlowyEditor.custom(
|
final editor = AppFlowyEditor.custom(
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
editable: true,
|
editable: true,
|
||||||
scrollController: scrollController,
|
shrinkWrap: widget.shrinkWrap,
|
||||||
|
scrollController: effectiveScrollController,
|
||||||
// setup the auto focus parameters
|
// setup the auto focus parameters
|
||||||
autoFocus: autoFocus,
|
autoFocus: widget.autoFocus ?? autoFocus,
|
||||||
focusedSelection: selection,
|
focusedSelection: selection,
|
||||||
// setup the theme
|
// setup the theme
|
||||||
editorStyle: styleCustomizer.style(),
|
editorStyle: styleCustomizer.style(),
|
||||||
@ -122,7 +143,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
style: styleCustomizer.floatingToolbarStyleBuilder(),
|
style: styleCustomizer.floatingToolbarStyleBuilder(),
|
||||||
items: toolbarItems,
|
items: toolbarItems,
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
scrollController: scrollController,
|
scrollController: effectiveScrollController,
|
||||||
child: editor,
|
child: editor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -61,6 +61,7 @@ extension InsertDatabase on EditorState {
|
|||||||
).then((value) => value.swap().toOption().toNullable());
|
).then((value) => value.swap().toOption().toNullable());
|
||||||
|
|
||||||
// TODO(a-wallen): Show error dialog here.
|
// TODO(a-wallen): Show error dialog here.
|
||||||
|
// Maybe extend the FlowyErrorPage.
|
||||||
if (ref == null) {
|
if (ref == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ class ShareActionList extends StatefulWidget {
|
|||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
class ShareActionListState extends State<ShareActionList> {
|
class ShareActionListState extends State<ShareActionList> {
|
||||||
late String name;
|
late String name;
|
||||||
late final ViewListener viewListener = ViewListener(view: widget.view);
|
late final ViewListener viewListener = ViewListener(viewId: widget.view.id);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -134,7 +134,7 @@ class ShareActionListState extends State<ShareActionList> {
|
|||||||
name = widget.view.name;
|
name = widget.view.name;
|
||||||
viewListener.start(
|
viewListener.start(
|
||||||
onViewUpdated: (view) {
|
onViewUpdated: (view) {
|
||||||
name = view.fold((l) => l.name, (r) => '');
|
name = view.name;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../workspace/presentation/home/home_stack.dart';
|
||||||
|
|
||||||
class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
|
class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
|
||||||
final ViewListener? _viewListener;
|
final ViewListener? _viewListener;
|
||||||
ViewPB view;
|
ViewPB view;
|
||||||
@ -12,21 +16,23 @@ class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
|
|||||||
@override
|
@override
|
||||||
final ValueNotifier<Option<DeletedViewPB>> isDeleted = ValueNotifier(none());
|
final ValueNotifier<Option<DeletedViewPB>> isDeleted = ValueNotifier(none());
|
||||||
|
|
||||||
@override
|
|
||||||
final ValueNotifier<int> isDisplayChanged = ValueNotifier(0);
|
|
||||||
|
|
||||||
ViewPluginNotifier({
|
ViewPluginNotifier({
|
||||||
required this.view,
|
required this.view,
|
||||||
}) : _viewListener = ViewListener(view: view) {
|
required bool listenOnViewChanged,
|
||||||
|
}) : _viewListener = ViewListener(viewId: view.id) {
|
||||||
|
if (listenOnViewChanged) {
|
||||||
_viewListener?.start(
|
_viewListener?.start(
|
||||||
onViewUpdated: (result) {
|
onViewUpdated: (updatedView) {
|
||||||
result.fold(
|
// If the layout is changed, we need to create a new plugin for it.
|
||||||
(updatedView) {
|
if (view.layout != updatedView.layout) {
|
||||||
view = updatedView;
|
getIt<HomeStackManager>().setPlugin(
|
||||||
isDisplayChanged.value = updatedView.hashCode;
|
updatedView.plugin(
|
||||||
},
|
listenOnViewChanged: listenOnViewChanged,
|
||||||
(err) => Log.error(err),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
view = updatedView;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onViewMoveToTrash: (result) {
|
onViewMoveToTrash: (result) {
|
||||||
result.fold(
|
result.fold(
|
||||||
@ -36,11 +42,11 @@ class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
isDeleted.dispose();
|
isDeleted.dispose();
|
||||||
isDisplayChanged.dispose();
|
|
||||||
_viewListener?.stop();
|
_viewListener?.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,11 +122,6 @@ void _resolveFolderDeps(GetIt getIt) {
|
|||||||
WorkspaceListener(user: user, workspaceId: workspaceId),
|
WorkspaceListener(user: user, workspaceId: workspaceId),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ViewPB
|
|
||||||
getIt.registerFactoryParam<ViewListener, ViewPB, void>(
|
|
||||||
(view, _) => ViewListener(view: view),
|
|
||||||
);
|
|
||||||
|
|
||||||
getIt.registerFactoryParam<ViewBloc, ViewPB, void>(
|
getIt.registerFactoryParam<ViewBloc, ViewPB, void>(
|
||||||
(view, _) => ViewBloc(
|
(view, _) => ViewBloc(
|
||||||
view: view,
|
view: view,
|
||||||
|
@ -37,9 +37,6 @@ abstract class PluginNotifier<T> {
|
|||||||
/// Notify if the plugin get deleted
|
/// Notify if the plugin get deleted
|
||||||
ValueNotifier<T> get isDeleted;
|
ValueNotifier<T> get isDeleted;
|
||||||
|
|
||||||
/// Notify if the [PluginWidgetBuilder]'s content was changed
|
|
||||||
ValueNotifier<int> get isDisplayChanged;
|
|
||||||
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,14 +16,14 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
|||||||
ViewBloc({
|
ViewBloc({
|
||||||
required this.view,
|
required this.view,
|
||||||
}) : viewBackendSvc = ViewBackendService(),
|
}) : viewBackendSvc = ViewBackendService(),
|
||||||
listener = ViewListener(view: view),
|
listener = ViewListener(viewId: view.id),
|
||||||
super(ViewState.init(view)) {
|
super(ViewState.init(view)) {
|
||||||
on<ViewEvent>((event, emit) async {
|
on<ViewEvent>((event, emit) async {
|
||||||
await event.map(
|
await event.map(
|
||||||
initial: (e) {
|
initial: (e) {
|
||||||
listener.start(
|
listener.start(
|
||||||
onViewUpdated: (result) {
|
onViewUpdated: (result) {
|
||||||
add(ViewEvent.viewDidUpdate(result));
|
add(ViewEvent.viewDidUpdate(left(result)));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
emit(state);
|
emit(state);
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import 'package:appflowy/plugins/database_view/database_view.dart';
|
import 'package:appflowy/plugins/database_view/board/board.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/calendar/calendar.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/grid.dart';
|
||||||
|
import 'package:appflowy/plugins/document/document.dart';
|
||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
import 'package:flowy_infra/image.dart';
|
import 'package:flowy_infra/image.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
@ -56,14 +59,32 @@ extension ViewExtension on ViewPB {
|
|||||||
throw UnimplementedError;
|
throw UnimplementedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin plugin() {
|
Plugin plugin({bool listenOnViewChanged = false}) {
|
||||||
switch (layout) {
|
switch (layout) {
|
||||||
case ViewLayoutPB.Board:
|
case ViewLayoutPB.Board:
|
||||||
|
return BoardPlugin(
|
||||||
|
view: this,
|
||||||
|
pluginType: pluginType,
|
||||||
|
listenOnViewChanged: listenOnViewChanged,
|
||||||
|
);
|
||||||
case ViewLayoutPB.Calendar:
|
case ViewLayoutPB.Calendar:
|
||||||
|
return CalendarPlugin(
|
||||||
|
view: this,
|
||||||
|
pluginType: pluginType,
|
||||||
|
listenOnViewChanged: listenOnViewChanged,
|
||||||
|
);
|
||||||
case ViewLayoutPB.Grid:
|
case ViewLayoutPB.Grid:
|
||||||
return DatabaseViewPlugin(view: this);
|
return GridPlugin(
|
||||||
|
view: this,
|
||||||
|
pluginType: pluginType,
|
||||||
|
listenOnViewChanged: listenOnViewChanged,
|
||||||
|
);
|
||||||
case ViewLayoutPB.Document:
|
case ViewLayoutPB.Document:
|
||||||
return makePlugin(pluginType: pluginType, data: this);
|
return DocumentPlugin(
|
||||||
|
view: this,
|
||||||
|
pluginType: pluginType,
|
||||||
|
listenOnViewChanged: listenOnViewChanged,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw UnimplementedError;
|
throw UnimplementedError;
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:appflowy/core/notification/folder_notification.dart';
|
import 'package:appflowy/core/notification/folder_notification.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart';
|
||||||
import 'package:appflowy_backend/rust_stream.dart';
|
import 'package:appflowy_backend/rust_stream.dart';
|
||||||
import 'package:flowy_infra/notifier.dart';
|
|
||||||
|
|
||||||
// Delete the view from trash, which means the view was deleted permanently
|
// Delete the view from trash, which means the view was deleted permanently
|
||||||
typedef DeleteViewNotifyValue = Either<ViewPB, FlowyError>;
|
typedef DeleteViewNotifyValue = Either<ViewPB, FlowyError>;
|
||||||
// The view get updated
|
// The view get updated
|
||||||
typedef UpdateViewNotifiedValue = Either<ViewPB, FlowyError>;
|
typedef UpdateViewNotifiedValue = ViewPB;
|
||||||
// Restore the view from trash
|
// Restore the view from trash
|
||||||
typedef RestoreViewNotifiedValue = Either<ViewPB, FlowyError>;
|
typedef RestoreViewNotifiedValue = Either<ViewPB, FlowyError>;
|
||||||
// Move the view to trash
|
// Move the view to trash
|
||||||
@ -20,15 +20,17 @@ typedef MoveToTrashNotifiedValue = Either<DeletedViewPB, FlowyError>;
|
|||||||
|
|
||||||
class ViewListener {
|
class ViewListener {
|
||||||
StreamSubscription<SubscribeObject>? _subscription;
|
StreamSubscription<SubscribeObject>? _subscription;
|
||||||
final _updatedViewNotifier = PublishNotifier<UpdateViewNotifiedValue>();
|
void Function(UpdateViewNotifiedValue)? _updatedViewNotifier;
|
||||||
final _deletedNotifier = PublishNotifier<DeleteViewNotifyValue>();
|
void Function(DeleteViewNotifyValue)? _deletedNotifier;
|
||||||
final _restoredNotifier = PublishNotifier<RestoreViewNotifiedValue>();
|
void Function(RestoreViewNotifiedValue)? _restoredNotifier;
|
||||||
final _moveToTrashNotifier = PublishNotifier<MoveToTrashNotifiedValue>();
|
void Function(MoveToTrashNotifiedValue)? _moveToTrashNotifier;
|
||||||
|
bool _isDisposed = false;
|
||||||
|
|
||||||
FolderNotificationParser? _parser;
|
FolderNotificationParser? _parser;
|
||||||
ViewPB view;
|
final String viewId;
|
||||||
|
|
||||||
ViewListener({
|
ViewListener({
|
||||||
required this.view,
|
required this.viewId,
|
||||||
});
|
});
|
||||||
|
|
||||||
void start({
|
void start({
|
||||||
@ -37,32 +39,18 @@ class ViewListener {
|
|||||||
void Function(RestoreViewNotifiedValue)? onViewRestored,
|
void Function(RestoreViewNotifiedValue)? onViewRestored,
|
||||||
void Function(MoveToTrashNotifiedValue)? onViewMoveToTrash,
|
void Function(MoveToTrashNotifiedValue)? onViewMoveToTrash,
|
||||||
}) {
|
}) {
|
||||||
if (onViewUpdated != null) {
|
if (_isDisposed) {
|
||||||
_updatedViewNotifier.addListener(() {
|
Log.warn("ViewListener is already disposed");
|
||||||
onViewUpdated(_updatedViewNotifier.currentValue!);
|
return;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onViewDeleted != null) {
|
_updatedViewNotifier = onViewUpdated;
|
||||||
_deletedNotifier.addListener(() {
|
_deletedNotifier = onViewDeleted;
|
||||||
onViewDeleted(_deletedNotifier.currentValue!);
|
_restoredNotifier = onViewRestored;
|
||||||
});
|
_moveToTrashNotifier = onViewMoveToTrash;
|
||||||
}
|
|
||||||
|
|
||||||
if (onViewRestored != null) {
|
|
||||||
_restoredNotifier.addListener(() {
|
|
||||||
onViewRestored(_restoredNotifier.currentValue!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onViewMoveToTrash != null) {
|
|
||||||
_moveToTrashNotifier.addListener(() {
|
|
||||||
onViewMoveToTrash(_moveToTrashNotifier.currentValue!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_parser = FolderNotificationParser(
|
_parser = FolderNotificationParser(
|
||||||
id: view.id,
|
id: viewId,
|
||||||
callback: (ty, result) {
|
callback: (ty, result) {
|
||||||
_handleObservableType(ty, result);
|
_handleObservableType(ty, result);
|
||||||
},
|
},
|
||||||
@ -81,30 +69,29 @@ class ViewListener {
|
|||||||
result.fold(
|
result.fold(
|
||||||
(payload) {
|
(payload) {
|
||||||
final view = ViewPB.fromBuffer(payload);
|
final view = ViewPB.fromBuffer(payload);
|
||||||
_updatedViewNotifier.value = left(view);
|
_updatedViewNotifier?.call(view);
|
||||||
},
|
},
|
||||||
(error) => _updatedViewNotifier.value = right(error),
|
(error) => Log.error(error),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case FolderNotification.DidDeleteView:
|
case FolderNotification.DidDeleteView:
|
||||||
result.fold(
|
result.fold(
|
||||||
(payload) =>
|
(payload) => _deletedNotifier?.call(left(ViewPB.fromBuffer(payload))),
|
||||||
_deletedNotifier.value = left(ViewPB.fromBuffer(payload)),
|
(error) => _deletedNotifier?.call(right(error)),
|
||||||
(error) => _deletedNotifier.value = right(error),
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case FolderNotification.DidRestoreView:
|
case FolderNotification.DidRestoreView:
|
||||||
result.fold(
|
result.fold(
|
||||||
(payload) =>
|
(payload) =>
|
||||||
_restoredNotifier.value = left(ViewPB.fromBuffer(payload)),
|
_restoredNotifier?.call(left(ViewPB.fromBuffer(payload))),
|
||||||
(error) => _restoredNotifier.value = right(error),
|
(error) => _restoredNotifier?.call(right(error)),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case FolderNotification.DidMoveViewToTrash:
|
case FolderNotification.DidMoveViewToTrash:
|
||||||
result.fold(
|
result.fold(
|
||||||
(payload) => _moveToTrashNotifier.value =
|
(payload) => _moveToTrashNotifier
|
||||||
left(DeletedViewPB.fromBuffer(payload)),
|
?.call(left(DeletedViewPB.fromBuffer(payload))),
|
||||||
(error) => _moveToTrashNotifier.value = right(error),
|
(error) => _moveToTrashNotifier?.call(right(error)),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -113,10 +100,11 @@ class ViewListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
|
_isDisposed = true;
|
||||||
_parser = null;
|
_parser = null;
|
||||||
await _subscription?.cancel();
|
await _subscription?.cancel();
|
||||||
_updatedViewNotifier.dispose();
|
_updatedViewNotifier = null;
|
||||||
_deletedNotifier.dispose();
|
_deletedNotifier = null;
|
||||||
_restoredNotifier.dispose();
|
_restoredNotifier = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,29 @@ class ViewBackendService {
|
|||||||
return FolderEventCreateView(payload).send();
|
return FolderEventCreateView(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The orphan view is meant to be a view that is not attached to any parent view. By default, this
|
||||||
|
/// view will not be shown in the view list unless it is attached to a parent view that is shown in
|
||||||
|
/// the view list.
|
||||||
|
static Future<Either<ViewPB, FlowyError>> createOrphanView({
|
||||||
|
required String viewId,
|
||||||
|
required ViewLayoutPB layoutType,
|
||||||
|
required String name,
|
||||||
|
String? desc,
|
||||||
|
|
||||||
|
/// The initial data should be a JSON that represent the DocumentDataPB.
|
||||||
|
/// Currently, only support create document with initial data.
|
||||||
|
List<int>? initialDataBytes,
|
||||||
|
}) {
|
||||||
|
final payload = CreateOrphanViewPayloadPB.create()
|
||||||
|
..viewId = viewId
|
||||||
|
..name = name
|
||||||
|
..desc = desc ?? ""
|
||||||
|
..layout = layoutType
|
||||||
|
..initialData = initialDataBytes ?? [];
|
||||||
|
|
||||||
|
return FolderEventCreateOrphanView(payload).send();
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Either<ViewPB, FlowyError>> createDatabaseReferenceView({
|
static Future<Either<ViewPB, FlowyError>> createDatabaseReferenceView({
|
||||||
required String parentViewId,
|
required String parentViewId,
|
||||||
required String databaseId,
|
required String databaseId,
|
||||||
@ -98,12 +121,23 @@ class ViewBackendService {
|
|||||||
static Future<Either<ViewPB, FlowyError>> updateView({
|
static Future<Either<ViewPB, FlowyError>> updateView({
|
||||||
required String viewId,
|
required String viewId,
|
||||||
String? name,
|
String? name,
|
||||||
|
String? iconURL,
|
||||||
|
String? coverURL,
|
||||||
}) {
|
}) {
|
||||||
final payload = UpdateViewPayloadPB.create()..viewId = viewId;
|
final payload = UpdateViewPayloadPB.create()..viewId = viewId;
|
||||||
|
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
payload.name = name;
|
payload.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (iconURL != null) {
|
||||||
|
payload.iconUrl = iconURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coverURL != null) {
|
||||||
|
payload.coverUrl = coverURL;
|
||||||
|
}
|
||||||
|
|
||||||
return FolderEventUpdateView(payload).send();
|
return FolderEventUpdateView(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +178,7 @@ class ViewBackendService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<ViewPB, FlowyError>> getView(
|
static Future<Either<ViewPB, FlowyError>> getView(
|
||||||
String viewID,
|
String viewID,
|
||||||
) async {
|
) async {
|
||||||
final payload = ViewIdPB.create()..value = viewID;
|
final payload = ViewIdPB.create()..value = viewID;
|
||||||
|
@ -78,11 +78,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
// All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
|
// All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
|
||||||
if (getIt<HomeStackManager>().plugin.pluginType ==
|
if (getIt<HomeStackManager>().plugin.pluginType ==
|
||||||
PluginType.blank) {
|
PluginType.blank) {
|
||||||
final plugin = makePlugin(
|
getIt<HomeStackManager>().setPlugin(
|
||||||
pluginType: view.pluginType,
|
view.plugin(listenOnViewChanged: true),
|
||||||
data: view,
|
|
||||||
);
|
);
|
||||||
getIt<HomeStackManager>().setPlugin(plugin);
|
|
||||||
getIt<MenuSharedState>().latestOpenView = view;
|
getIt<MenuSharedState>().latestOpenView = view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -282,12 +280,10 @@ class HomeScreenStackAdaptor extends HomeStackDelegate {
|
|||||||
lastView = views[index - 1];
|
lastView = views[index - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
final plugin = makePlugin(
|
|
||||||
pluginType: lastView.pluginType,
|
|
||||||
data: lastView,
|
|
||||||
);
|
|
||||||
getIt<MenuSharedState>().latestOpenView = lastView;
|
getIt<MenuSharedState>().latestOpenView = lastView;
|
||||||
getIt<HomeStackManager>().setPlugin(plugin);
|
getIt<HomeStackManager>().setPlugin(
|
||||||
|
lastView.plugin(listenOnViewChanged: true),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
getIt<MenuSharedState>().latestOpenView = null;
|
getIt<MenuSharedState>().latestOpenView = null;
|
||||||
getIt<HomeStackManager>().setPlugin(BlankPagePlugin());
|
getIt<HomeStackManager>().setPlugin(BlankPagePlugin());
|
||||||
|
@ -121,14 +121,12 @@ class HomeStackNotifier extends ChangeNotifier {
|
|||||||
/// This is the only place where the plugin is set.
|
/// This is the only place where the plugin is set.
|
||||||
/// No need compare the old plugin with the new plugin. Just set it.
|
/// No need compare the old plugin with the new plugin. Just set it.
|
||||||
set plugin(Plugin newPlugin) {
|
set plugin(Plugin newPlugin) {
|
||||||
_plugin.notifier?.isDisplayChanged.addListener(notifyListeners);
|
|
||||||
_plugin.dispose();
|
_plugin.dispose();
|
||||||
|
|
||||||
/// Set the plugin view as the latest view.
|
/// Set the plugin view as the latest view.
|
||||||
FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send();
|
FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send();
|
||||||
|
|
||||||
_plugin = newPlugin;
|
_plugin = newPlugin;
|
||||||
_plugin.notifier?.isDisplayChanged.removeListener(notifyListeners);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,9 @@ class ViewSection extends StatelessWidget {
|
|||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.selectedView != null) {
|
if (state.selectedView != null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
getIt<HomeStackManager>().setPlugin(state.selectedView!.plugin());
|
getIt<HomeStackManager>().setPlugin(
|
||||||
|
state.selectedView!.plugin(listenOnViewChanged: true),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -25,11 +24,9 @@ class _ViewLeftBarItemState extends State<ViewLeftBarItem> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
view = widget.view;
|
view = widget.view;
|
||||||
_focusNode.addListener(_handleFocusChanged);
|
_focusNode.addListener(_handleFocusChanged);
|
||||||
_viewListener = ViewListener(view: widget.view);
|
_viewListener = ViewListener(viewId: widget.view.id);
|
||||||
_viewListener.start(
|
_viewListener.start(
|
||||||
onViewUpdated: (result) {
|
onViewUpdated: (updatedView) {
|
||||||
result.fold(
|
|
||||||
(updatedView) {
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
view = updatedView;
|
view = updatedView;
|
||||||
@ -37,9 +34,6 @@ class _ViewLeftBarItemState extends State<ViewLeftBarItem> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
_controller.text = view.name;
|
_controller.text = view.name;
|
||||||
}
|
}
|
||||||
|
@ -53,11 +53,11 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "7fe5bb8"
|
ref: "23bc6d2"
|
||||||
resolved-ref: "7fe5bb85d455416ddbce4bbf2afed1c434466eeb"
|
resolved-ref: "23bc6d2f58ab7ab4ff21c507d53753de35094ec0"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.0"
|
version: "1.0.2"
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -42,11 +42,11 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||||
ref: a183c57
|
ref: a183c57
|
||||||
# appflowy_editor: ^1.0.0
|
# appflowy_editor: ^1.0.2
|
||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: 7fe5bb8
|
ref: 23bc6d2
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
path: packages/appflowy_popover
|
path: packages/appflowy_popover
|
||||||
|
|
||||||
|
@ -106,13 +106,14 @@ class BoardTestContext {
|
|||||||
|
|
||||||
final rowDataController = RowController(
|
final rowDataController = RowController(
|
||||||
viewId: rowInfo.viewId,
|
viewId: rowInfo.viewId,
|
||||||
rowId: rowInfo.rowPB.id,
|
rowMeta: rowInfo.rowMeta,
|
||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
);
|
);
|
||||||
|
|
||||||
final rowBloc = RowBloc(
|
final rowBloc = RowBloc(
|
||||||
rowInfo: rowInfo,
|
viewId: rowInfo.viewId,
|
||||||
dataController: rowDataController,
|
dataController: rowDataController,
|
||||||
|
rowId: rowInfo.rowMeta.id,
|
||||||
)..add(const RowEvent.initial());
|
)..add(const RowEvent.initial());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
|
@ -63,16 +63,16 @@ void main() {
|
|||||||
act: (bloc) async {
|
act: (bloc) async {
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
firstId = bloc.state.rowInfos[0].rowPB.id;
|
firstId = bloc.state.rowInfos[0].rowId;
|
||||||
secondId = bloc.state.rowInfos[1].rowPB.id;
|
secondId = bloc.state.rowInfos[1].rowId;
|
||||||
thirdId = bloc.state.rowInfos[2].rowPB.id;
|
thirdId = bloc.state.rowInfos[2].rowId;
|
||||||
|
|
||||||
bloc.add(const GridEvent.moveRow(0, 2));
|
bloc.add(const GridEvent.moveRow(0, 2));
|
||||||
},
|
},
|
||||||
verify: (bloc) {
|
verify: (bloc) {
|
||||||
expect(secondId, bloc.state.rowInfos[0].rowPB.id);
|
expect(secondId, bloc.state.rowInfos[0].rowId);
|
||||||
expect(thirdId, bloc.state.rowInfos[1].rowPB.id);
|
expect(thirdId, bloc.state.rowInfos[1].rowId);
|
||||||
expect(firstId, bloc.state.rowInfos[2].rowPB.id);
|
expect(firstId, bloc.state.rowInfos[2].rowId);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -35,7 +35,7 @@ class GridTestContext {
|
|||||||
return gridController.fieldController;
|
return gridController.fieldController;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<RowPB, FlowyError>> createRow() async {
|
Future<Either<RowMetaPB, FlowyError>> createRow() async {
|
||||||
return gridController.createRow();
|
return gridController.createRow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,14 +55,15 @@ class GridTestContext {
|
|||||||
final rowCache = gridController.rowCache;
|
final rowCache = gridController.rowCache;
|
||||||
|
|
||||||
final rowDataController = RowController(
|
final rowDataController = RowController(
|
||||||
rowId: rowInfo.rowPB.id,
|
rowMeta: rowInfo.rowMeta,
|
||||||
viewId: rowInfo.viewId,
|
viewId: rowInfo.viewId,
|
||||||
rowCache: rowCache,
|
rowCache: rowCache,
|
||||||
);
|
);
|
||||||
|
|
||||||
final rowBloc = RowBloc(
|
final rowBloc = RowBloc(
|
||||||
rowInfo: rowInfo,
|
viewId: rowInfo.viewId,
|
||||||
dataController: rowDataController,
|
dataController: rowDataController,
|
||||||
|
rowId: rowInfo.rowMeta.id,
|
||||||
)..add(const RowEvent.initial());
|
)..add(const RowEvent.initial());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
|
28
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
28
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -99,7 +99,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "appflowy-integrate"
|
name = "appflowy-integrate"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -1024,7 +1024,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -1042,7 +1042,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-client-ws"
|
name = "collab-client-ws"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab-sync",
|
"collab-sync",
|
||||||
@ -1060,7 +1060,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1086,7 +1086,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-derive"
|
name = "collab-derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1098,7 +1098,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -1109,13 +1109,14 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1135,7 +1136,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-persistence"
|
name = "collab-persistence"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1155,7 +1156,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1186,7 +1187,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-sync"
|
name = "collab-sync"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab",
|
"collab",
|
||||||
@ -5006,6 +5007,12 @@ dependencies = [
|
|||||||
"digest 0.10.6",
|
"digest 0.10.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1_smol"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@ -6206,6 +6213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.9",
|
"getrandom 0.2.9",
|
||||||
|
"sha1_smol",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -34,12 +34,12 @@ default = ["custom-protocol"]
|
|||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
|
|
||||||
#collab = { path = "../../AppFlowy-Collab/collab" }
|
#collab = { path = "../../AppFlowy-Collab/collab" }
|
||||||
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
|
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { DatabaseNotification, FlowyError, GroupPB, GroupRowsNotificationPB, RowPB } from '@/services/backend';
|
import {
|
||||||
|
DatabaseNotification,
|
||||||
|
FlowyError,
|
||||||
|
GroupPB,
|
||||||
|
GroupRowsNotificationPB,
|
||||||
|
RowMetaPB,
|
||||||
|
RowPB,
|
||||||
|
} from '@/services/backend';
|
||||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||||
import { None, Ok, Option, Result, Some } from 'ts-results';
|
import { None, Ok, Option, Result, Some } from 'ts-results';
|
||||||
import { DatabaseNotificationObserver } from '../notifications/observer';
|
import { DatabaseNotificationObserver } from '../notifications/observer';
|
||||||
@ -7,10 +14,10 @@ import { DatabaseBackendService } from '../database_bd_svc';
|
|||||||
|
|
||||||
export type GroupDataCallbacks = {
|
export type GroupDataCallbacks = {
|
||||||
onRemoveRow: (groupId: string, rowId: string) => void;
|
onRemoveRow: (groupId: string, rowId: string) => void;
|
||||||
onInsertRow: (groupId: string, row: RowPB, index?: number) => void;
|
onInsertRow: (groupId: string, row: RowMetaPB, index?: number) => void;
|
||||||
onUpdateRow: (groupId: string, row: RowPB) => void;
|
onUpdateRow: (groupId: string, row: RowMetaPB) => void;
|
||||||
|
|
||||||
onCreateRow: (groupId: string, row: RowPB) => void;
|
onCreateRow: (groupId: string, row: RowMetaPB) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class DatabaseGroupController {
|
export class DatabaseGroupController {
|
||||||
@ -37,7 +44,7 @@ export class DatabaseGroupController {
|
|||||||
this.group = group;
|
this.group = group;
|
||||||
};
|
};
|
||||||
|
|
||||||
rowAtIndex = (index: number): Option<RowPB> => {
|
rowAtIndex = (index: number): Option<RowMetaPB> => {
|
||||||
if (this.group.rows.length < index) {
|
if (this.group.rows.length < index) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -59,16 +66,16 @@ export class DatabaseGroupController {
|
|||||||
changeset.inserted_rows.forEach((insertedRow) => {
|
changeset.inserted_rows.forEach((insertedRow) => {
|
||||||
let index: number | undefined = insertedRow.index;
|
let index: number | undefined = insertedRow.index;
|
||||||
if (insertedRow.has_index && this.group.rows.length > insertedRow.index) {
|
if (insertedRow.has_index && this.group.rows.length > insertedRow.index) {
|
||||||
this.group.rows.splice(index, 0, insertedRow.row);
|
this.group.rows.splice(index, 0, insertedRow.row_meta);
|
||||||
} else {
|
} else {
|
||||||
index = undefined;
|
index = undefined;
|
||||||
this.group.rows.push(insertedRow.row);
|
this.group.rows.push(insertedRow.row_meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (insertedRow.is_new) {
|
if (insertedRow.is_new) {
|
||||||
this.callbacks?.onCreateRow(this.group.group_id, insertedRow.row);
|
this.callbacks?.onCreateRow(this.group.group_id, insertedRow.row_meta);
|
||||||
} else {
|
} else {
|
||||||
this.callbacks?.onInsertRow(this.group.group_id, insertedRow.row, index);
|
this.callbacks?.onInsertRow(this.group.group_id, insertedRow.row_meta, index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,12 +7,13 @@ import {
|
|||||||
RowsChangePB,
|
RowsChangePB,
|
||||||
RowsVisibilityChangePB,
|
RowsVisibilityChangePB,
|
||||||
ReorderSingleRowPB,
|
ReorderSingleRowPB,
|
||||||
|
RowMetaPB,
|
||||||
} from '@/services/backend';
|
} from '@/services/backend';
|
||||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||||
import { FieldInfo } from '../field/field_controller';
|
import { FieldInfo } from '../field/field_controller';
|
||||||
import { CellCache, CellCacheKey } from '../cell/cell_cache';
|
import { CellCache, CellCacheKey } from '../cell/cell_cache';
|
||||||
import { CellIdentifier } from '../cell/cell_bd_svc';
|
import { CellIdentifier } from '../cell/cell_bd_svc';
|
||||||
import { DatabaseEventGetRow } from '@/services/backend/events/flowy-database2';
|
import { DatabaseEventGetRow, DatabaseEventGetRowMeta } from '@/services/backend/events/flowy-database2';
|
||||||
import { None, Option, Some } from 'ts-results';
|
import { None, Option, Some } from 'ts-results';
|
||||||
import { Log } from '$app/utils/log';
|
import { Log } from '$app/utils/log';
|
||||||
|
|
||||||
@ -75,7 +76,7 @@ export class RowCache {
|
|||||||
this.notifier.withChange(RowChangedReason.FieldDidChanged);
|
this.notifier.withChange(RowChangedReason.FieldDidChanged);
|
||||||
};
|
};
|
||||||
|
|
||||||
initializeRows = (rows: RowPB[]) => {
|
initializeRows = (rows: RowMetaPB[]) => {
|
||||||
rows.forEach((rowPB) => {
|
rows.forEach((rowPB) => {
|
||||||
this.rowList.push(this._toRowInfo(rowPB));
|
this.rowList.push(this._toRowInfo(rowPB));
|
||||||
});
|
});
|
||||||
@ -106,11 +107,7 @@ export class RowCache {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private _refreshRow = (opRow: OptionalRowPB) => {
|
private _refreshRow = (updatedRow: RowMetaPB) => {
|
||||||
if (!opRow.has_row) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const updatedRow = opRow.row;
|
|
||||||
const option = this.rowList.getRowWithIndex(updatedRow.id);
|
const option = this.rowList.getRowWithIndex(updatedRow.id);
|
||||||
if (option.some) {
|
if (option.some) {
|
||||||
const { rowInfo, index } = option.val;
|
const { rowInfo, index } = option.val;
|
||||||
@ -124,7 +121,7 @@ export class RowCache {
|
|||||||
|
|
||||||
private _loadRow = (rowId: string) => {
|
private _loadRow = (rowId: string) => {
|
||||||
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId });
|
||||||
return DatabaseEventGetRow(payload);
|
return DatabaseEventGetRowMeta(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
private _deleteRows = (rowIds: string[]) => {
|
private _deleteRows = (rowIds: string[]) => {
|
||||||
@ -138,7 +135,7 @@ export class RowCache {
|
|||||||
|
|
||||||
private _insertRows = (rows: InsertedRowPB[]) => {
|
private _insertRows = (rows: InsertedRowPB[]) => {
|
||||||
rows.forEach((insertedRow) => {
|
rows.forEach((insertedRow) => {
|
||||||
const rowInfo = this._toRowInfo(insertedRow.row);
|
const rowInfo = this._toRowInfo(insertedRow.row_meta);
|
||||||
const insertedIndex = this.rowList.insert(insertedRow.index, rowInfo);
|
const insertedIndex = this.rowList.insert(insertedRow.index, rowInfo);
|
||||||
if (insertedIndex !== undefined) {
|
if (insertedIndex !== undefined) {
|
||||||
this.notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId);
|
this.notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId);
|
||||||
@ -154,11 +151,11 @@ export class RowCache {
|
|||||||
const rowInfos: RowInfo[] = [];
|
const rowInfos: RowInfo[] = [];
|
||||||
updatedRows.forEach((updatedRow) => {
|
updatedRows.forEach((updatedRow) => {
|
||||||
updatedRow.field_ids.forEach((fieldId) => {
|
updatedRow.field_ids.forEach((fieldId) => {
|
||||||
const key = new CellCacheKey(fieldId, updatedRow.row.id);
|
const key = new CellCacheKey(fieldId, updatedRow.row_meta.id);
|
||||||
this.cellCache.remove(key);
|
this.cellCache.remove(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
rowInfos.push(this._toRowInfo(updatedRow.row));
|
rowInfos.push(this._toRowInfo(updatedRow.row_meta));
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedIndexs = this.rowList.insertRows(rowInfos);
|
const updatedIndexs = this.rowList.insertRows(rowInfos);
|
||||||
@ -178,7 +175,7 @@ export class RowCache {
|
|||||||
|
|
||||||
private _displayRows = (insertedRows: InsertedRowPB[]) => {
|
private _displayRows = (insertedRows: InsertedRowPB[]) => {
|
||||||
insertedRows.forEach((insertedRow) => {
|
insertedRows.forEach((insertedRow) => {
|
||||||
const insertedIndex = this.rowList.insert(insertedRow.index, this._toRowInfo(insertedRow.row));
|
const insertedIndex = this.rowList.insert(insertedRow.index, this._toRowInfo(insertedRow.row_meta));
|
||||||
|
|
||||||
if (insertedIndex !== undefined) {
|
if (insertedIndex !== undefined) {
|
||||||
this.notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId);
|
this.notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId);
|
||||||
@ -190,7 +187,7 @@ export class RowCache {
|
|||||||
this.notifier.dispose();
|
this.notifier.dispose();
|
||||||
};
|
};
|
||||||
|
|
||||||
private _toRowInfo = (rowPB: RowPB) => {
|
private _toRowInfo = (rowPB: RowMetaPB) => {
|
||||||
return new RowInfo(this.viewId, this.getFieldInfos(), rowPB);
|
return new RowInfo(this.viewId, this.getFieldInfos(), rowPB);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -338,10 +335,10 @@ export class RowInfo {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly viewId: string,
|
public readonly viewId: string,
|
||||||
public readonly fieldInfos: readonly FieldInfo[],
|
public readonly fieldInfos: readonly FieldInfo[],
|
||||||
public readonly row: RowPB
|
public readonly row: RowMetaPB
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
copyWith = (params: { row?: RowPB; fieldInfos?: readonly FieldInfo[] }) => {
|
copyWith = (params: { row?: RowMetaPB; fieldInfos?: readonly FieldInfo[] }) => {
|
||||||
return new RowInfo(this.viewId, params.fieldInfos || this.fieldInfos, params.row || this.row);
|
return new RowInfo(this.viewId, params.fieldInfos || this.fieldInfos, params.row || this.row);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { DatabaseViewRowsObserver } from './view_row_observer';
|
import { DatabaseViewRowsObserver } from './view_row_observer';
|
||||||
import { RowCache, RowInfo } from '../row/row_cache';
|
import { RowCache, RowInfo } from '../row/row_cache';
|
||||||
import { FieldController } from '../field/field_controller';
|
import { FieldController } from '../field/field_controller';
|
||||||
import { RowPB } from '@/services/backend';
|
import { RowMetaPB, RowPB } from '@/services/backend';
|
||||||
|
|
||||||
export class DatabaseViewCache {
|
export class DatabaseViewCache {
|
||||||
private readonly rowsObserver: DatabaseViewRowsObserver;
|
private readonly rowsObserver: DatabaseViewRowsObserver;
|
||||||
@ -20,7 +20,7 @@ export class DatabaseViewCache {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeWithRows = (rows: RowPB[]) => {
|
initializeWithRows = (rows: RowMetaPB[]) => {
|
||||||
this.rowCache.initializeRows(rows);
|
this.rowCache.initializeRows(rows);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
28
frontend/rust-lib/Cargo.lock
generated
28
frontend/rust-lib/Cargo.lock
generated
@ -85,7 +85,7 @@ checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "appflowy-integrate"
|
name = "appflowy-integrate"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -887,7 +887,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -905,7 +905,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-client-ws"
|
name = "collab-client-ws"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab-sync",
|
"collab-sync",
|
||||||
@ -923,7 +923,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-database"
|
name = "collab-database"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -949,7 +949,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-derive"
|
name = "collab-derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -961,7 +961,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-document"
|
name = "collab-document"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collab",
|
"collab",
|
||||||
@ -972,13 +972,14 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-folder"
|
name = "collab-folder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -998,7 +999,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-persistence"
|
name = "collab-persistence"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
@ -1018,7 +1019,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-plugins"
|
name = "collab-plugins"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -1049,7 +1050,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collab-sync"
|
name = "collab-sync"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec"
|
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"collab",
|
"collab",
|
||||||
@ -4176,6 +4177,12 @@ dependencies = [
|
|||||||
"digest 0.10.6",
|
"digest 0.10.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1_smol"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@ -5029,6 +5036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.9",
|
"getrandom 0.2.9",
|
||||||
|
"sha1_smol",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -33,11 +33,11 @@ opt-level = 3
|
|||||||
incremental = false
|
incremental = false
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" }
|
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" }
|
||||||
|
|
||||||
#collab = { path = "../AppFlowy-Collab/collab" }
|
#collab = { path = "../AppFlowy-Collab/collab" }
|
||||||
#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
|
#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
|
||||||
|
@ -2,6 +2,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
|||||||
use flowy_error::ErrorCode;
|
use flowy_error::ErrorCode;
|
||||||
|
|
||||||
use crate::entities::parser::NotEmptyStr;
|
use crate::entities::parser::NotEmptyStr;
|
||||||
|
use crate::entities::RowMetaPB;
|
||||||
use crate::services::setting::{CalendarLayout, CalendarLayoutSetting};
|
use crate::services::setting::{CalendarLayout, CalendarLayoutSetting};
|
||||||
|
|
||||||
use super::CellIdPB;
|
use super::CellIdPB;
|
||||||
@ -99,7 +100,7 @@ impl TryInto<CalendarEventRequestParams> for CalendarEventRequestPB {
|
|||||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||||
pub struct CalendarEventPB {
|
pub struct CalendarEventPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub row_id: String,
|
pub row_meta: RowMetaPB,
|
||||||
|
|
||||||
#[pb(index = 2)]
|
#[pb(index = 2)]
|
||||||
pub date_field_id: String,
|
pub date_field_id: String,
|
||||||
|
@ -6,7 +6,7 @@ use flowy_derive::ProtoBuf;
|
|||||||
use flowy_error::{ErrorCode, FlowyError};
|
use flowy_error::{ErrorCode, FlowyError};
|
||||||
|
|
||||||
use crate::entities::parser::NotEmptyStr;
|
use crate::entities::parser::NotEmptyStr;
|
||||||
use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowPB};
|
use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowMetaPB};
|
||||||
use crate::services::database::CreateDatabaseViewParams;
|
use crate::services::database::CreateDatabaseViewParams;
|
||||||
|
|
||||||
/// [DatabasePB] describes how many fields and blocks the grid has
|
/// [DatabasePB] describes how many fields and blocks the grid has
|
||||||
@ -19,7 +19,7 @@ pub struct DatabasePB {
|
|||||||
pub fields: Vec<FieldIdPB>,
|
pub fields: Vec<FieldIdPB>,
|
||||||
|
|
||||||
#[pb(index = 3)]
|
#[pb(index = 3)]
|
||||||
pub rows: Vec<RowPB>,
|
pub rows: Vec<RowMetaPB>,
|
||||||
|
|
||||||
#[pb(index = 4)]
|
#[pb(index = 4)]
|
||||||
pub layout_type: DatabaseLayoutPB,
|
pub layout_type: DatabaseLayoutPB,
|
||||||
|
@ -4,7 +4,7 @@ use flowy_derive::ProtoBuf;
|
|||||||
use flowy_error::ErrorCode;
|
use flowy_error::ErrorCode;
|
||||||
|
|
||||||
use crate::entities::parser::NotEmptyStr;
|
use crate::entities::parser::NotEmptyStr;
|
||||||
use crate::entities::{FieldType, RowPB};
|
use crate::entities::{FieldType, RowMetaPB};
|
||||||
use crate::services::group::{GroupChangeset, GroupData, GroupSetting};
|
use crate::services::group::{GroupChangeset, GroupData, GroupSetting};
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
|
||||||
@ -79,7 +79,7 @@ pub struct GroupPB {
|
|||||||
pub group_name: String,
|
pub group_name: String,
|
||||||
|
|
||||||
#[pb(index = 4)]
|
#[pb(index = 4)]
|
||||||
pub rows: Vec<RowPB>,
|
pub rows: Vec<RowMetaPB>,
|
||||||
|
|
||||||
#[pb(index = 5)]
|
#[pb(index = 5)]
|
||||||
pub is_default: bool,
|
pub is_default: bool,
|
||||||
@ -94,7 +94,11 @@ impl std::convert::From<GroupData> for GroupPB {
|
|||||||
field_id: group_data.field_id,
|
field_id: group_data.field_id,
|
||||||
group_id: group_data.id,
|
group_id: group_data.id,
|
||||||
group_name: group_data.name,
|
group_name: group_data.name,
|
||||||
rows: group_data.rows.into_iter().map(RowPB::from).collect(),
|
rows: group_data
|
||||||
|
.rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row_detail| RowMetaPB::from(row_detail.meta))
|
||||||
|
.collect(),
|
||||||
is_default: group_data.is_default,
|
is_default: group_data.is_default,
|
||||||
is_visible: group_data.is_visible,
|
is_visible: group_data.is_visible,
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use flowy_derive::ProtoBuf;
|
|||||||
use flowy_error::ErrorCode;
|
use flowy_error::ErrorCode;
|
||||||
|
|
||||||
use crate::entities::parser::NotEmptyStr;
|
use crate::entities::parser::NotEmptyStr;
|
||||||
use crate::entities::{GroupPB, InsertedRowPB, RowPB};
|
use crate::entities::{GroupPB, InsertedRowPB, RowMetaPB};
|
||||||
|
|
||||||
#[derive(Debug, Default, ProtoBuf)]
|
#[derive(Debug, Default, ProtoBuf)]
|
||||||
pub struct GroupRowsNotificationPB {
|
pub struct GroupRowsNotificationPB {
|
||||||
@ -21,7 +21,7 @@ pub struct GroupRowsNotificationPB {
|
|||||||
pub deleted_rows: Vec<String>,
|
pub deleted_rows: Vec<String>,
|
||||||
|
|
||||||
#[pb(index = 5)]
|
#[pb(index = 5)]
|
||||||
pub updated_rows: Vec<RowPB>,
|
pub updated_rows: Vec<RowMetaPB>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for GroupRowsNotificationPB {
|
impl std::fmt::Display for GroupRowsNotificationPB {
|
||||||
@ -29,7 +29,7 @@ impl std::fmt::Display for GroupRowsNotificationPB {
|
|||||||
for inserted_row in &self.inserted_rows {
|
for inserted_row in &self.inserted_rows {
|
||||||
f.write_fmt(format_args!(
|
f.write_fmt(format_args!(
|
||||||
"Insert: {} row at {:?}",
|
"Insert: {} row at {:?}",
|
||||||
inserted_row.row.id, inserted_row.index
|
inserted_row.row_meta.id, inserted_row.index
|
||||||
))?;
|
))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ impl GroupRowsNotificationPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(group_id: String, updated_rows: Vec<RowPB>) -> Self {
|
pub fn update(group_id: String, updated_rows: Vec<RowMetaPB>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
group_id,
|
group_id,
|
||||||
updated_rows,
|
updated_rows,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use collab_database::rows::{Row, RowId};
|
use collab_database::rows::{Row, RowId, RowMeta};
|
||||||
use collab_database::views::RowOrder;
|
use collab_database::views::RowOrder;
|
||||||
|
|
||||||
use flowy_derive::ProtoBuf;
|
use flowy_derive::ProtoBuf;
|
||||||
@ -36,6 +36,7 @@ impl std::convert::From<Row> for RowPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RowOrder> for RowPB {
|
impl From<RowOrder> for RowPB {
|
||||||
fn from(data: RowOrder) -> Self {
|
fn from(data: RowOrder) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -45,6 +46,153 @@ impl From<RowOrder> for RowPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, ProtoBuf)]
|
||||||
|
pub struct RowMetaPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub document_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 3, one_of)]
|
||||||
|
pub icon: Option<String>,
|
||||||
|
|
||||||
|
#[pb(index = 4, one_of)]
|
||||||
|
pub cover: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<&RowMeta> for RowMetaPB {
|
||||||
|
fn from(row_meta: &RowMeta) -> Self {
|
||||||
|
Self {
|
||||||
|
id: row_meta.row_id.clone(),
|
||||||
|
document_id: row_meta.document_id.clone(),
|
||||||
|
icon: row_meta.icon_url.clone(),
|
||||||
|
cover: row_meta.cover_url.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::From<RowMeta> for RowMetaPB {
|
||||||
|
fn from(row_meta: RowMeta) -> Self {
|
||||||
|
Self {
|
||||||
|
id: row_meta.row_id,
|
||||||
|
document_id: row_meta.document_id,
|
||||||
|
icon: row_meta.icon_url,
|
||||||
|
cover: row_meta.cover_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, ProtoBuf)]
|
||||||
|
pub struct UpdateRowMetaChangesetPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub view_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 3, one_of)]
|
||||||
|
pub icon_url: Option<String>,
|
||||||
|
|
||||||
|
#[pb(index = 4, one_of)]
|
||||||
|
pub cover_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UpdateRowMetaParams {
|
||||||
|
pub id: String,
|
||||||
|
pub view_id: String,
|
||||||
|
pub icon_url: Option<String>,
|
||||||
|
pub cover_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryInto<UpdateRowMetaParams> for UpdateRowMetaChangesetPB {
|
||||||
|
type Error = ErrorCode;
|
||||||
|
|
||||||
|
fn try_into(self) -> Result<UpdateRowMetaParams, Self::Error> {
|
||||||
|
let row_id = NotEmptyStr::parse(self.id)
|
||||||
|
.map_err(|_| ErrorCode::RowIdIsEmpty)?
|
||||||
|
.0;
|
||||||
|
|
||||||
|
let view_id = NotEmptyStr::parse(self.view_id)
|
||||||
|
.map_err(|_| ErrorCode::ViewIdIsInvalid)?
|
||||||
|
.0;
|
||||||
|
Ok(UpdateRowMetaParams {
|
||||||
|
id: row_id,
|
||||||
|
view_id,
|
||||||
|
icon_url: self.icon_url,
|
||||||
|
cover_url: self.cover_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, ProtoBuf)]
|
||||||
|
pub struct UpdateRowPayloadPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub row_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2, one_of)]
|
||||||
|
pub insert_document: Option<bool>,
|
||||||
|
|
||||||
|
#[pb(index = 3, one_of)]
|
||||||
|
pub insert_comment: Option<RowCommentPayloadPB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct UpdateRowParams {
|
||||||
|
pub row_id: String,
|
||||||
|
pub insert_comment: Option<RowCommentParams>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryInto<UpdateRowParams> for UpdateRowPayloadPB {
|
||||||
|
type Error = ErrorCode;
|
||||||
|
|
||||||
|
fn try_into(self) -> Result<UpdateRowParams, Self::Error> {
|
||||||
|
let row_id = NotEmptyStr::parse(self.row_id)
|
||||||
|
.map_err(|_| ErrorCode::RowIdIsEmpty)?
|
||||||
|
.0;
|
||||||
|
let insert_comment = self
|
||||||
|
.insert_comment
|
||||||
|
.map(|comment| comment.try_into())
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
Ok(UpdateRowParams {
|
||||||
|
row_id,
|
||||||
|
insert_comment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, ProtoBuf)]
|
||||||
|
pub struct RowCommentPayloadPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub uid: String,
|
||||||
|
|
||||||
|
#[pb(index = 2)]
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct RowCommentParams {
|
||||||
|
pub uid: String,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryInto<RowCommentParams> for RowCommentPayloadPB {
|
||||||
|
type Error = ErrorCode;
|
||||||
|
|
||||||
|
fn try_into(self) -> Result<RowCommentParams, Self::Error> {
|
||||||
|
let uid = NotEmptyStr::parse(self.uid)
|
||||||
|
.map_err(|_| ErrorCode::RowIdIsEmpty)?
|
||||||
|
.0;
|
||||||
|
let comment = NotEmptyStr::parse(self.comment)
|
||||||
|
.map_err(|_| ErrorCode::RowIdIsEmpty)?
|
||||||
|
.0;
|
||||||
|
|
||||||
|
Ok(RowCommentParams { uid, comment })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, ProtoBuf)]
|
#[derive(Debug, Default, ProtoBuf)]
|
||||||
pub struct OptionalRowPB {
|
pub struct OptionalRowPB {
|
||||||
#[pb(index = 1, one_of)]
|
#[pb(index = 1, one_of)]
|
||||||
@ -66,7 +214,7 @@ impl std::convert::From<Vec<RowPB>> for RepeatedRowPB {
|
|||||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||||
pub struct InsertedRowPB {
|
pub struct InsertedRowPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub row: RowPB,
|
pub row_meta: RowMetaPB,
|
||||||
|
|
||||||
#[pb(index = 2, one_of)]
|
#[pb(index = 2, one_of)]
|
||||||
pub index: Option<i32>,
|
pub index: Option<i32>,
|
||||||
@ -76,9 +224,9 @@ pub struct InsertedRowPB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InsertedRowPB {
|
impl InsertedRowPB {
|
||||||
pub fn new(row: RowPB) -> Self {
|
pub fn new(row_meta: RowMetaPB) -> Self {
|
||||||
Self {
|
Self {
|
||||||
row,
|
row_meta,
|
||||||
index: None,
|
index: None,
|
||||||
is_new: false,
|
is_new: false,
|
||||||
}
|
}
|
||||||
@ -90,26 +238,20 @@ impl InsertedRowPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::From<RowPB> for InsertedRowPB {
|
impl std::convert::From<RowMetaPB> for InsertedRowPB {
|
||||||
fn from(row: RowPB) -> Self {
|
fn from(row_meta: RowMetaPB) -> Self {
|
||||||
Self {
|
Self {
|
||||||
row,
|
row_meta,
|
||||||
index: None,
|
index: None,
|
||||||
is_new: false,
|
is_new: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::From<&Row> for InsertedRowPB {
|
|
||||||
fn from(row: &Row) -> Self {
|
|
||||||
Self::from(RowPB::from(row))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<InsertedRow> for InsertedRowPB {
|
impl From<InsertedRow> for InsertedRowPB {
|
||||||
fn from(data: InsertedRow) -> Self {
|
fn from(data: InsertedRow) -> Self {
|
||||||
Self {
|
Self {
|
||||||
row: data.row.into(),
|
row_meta: data.row_meta.into(),
|
||||||
index: data.index,
|
index: data.index,
|
||||||
is_new: data.is_new,
|
is_new: data.is_new,
|
||||||
}
|
}
|
||||||
@ -119,18 +261,24 @@ impl From<InsertedRow> for InsertedRowPB {
|
|||||||
#[derive(Debug, Clone, Default, ProtoBuf)]
|
#[derive(Debug, Clone, Default, ProtoBuf)]
|
||||||
pub struct UpdatedRowPB {
|
pub struct UpdatedRowPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
pub row: RowPB,
|
pub row_id: String,
|
||||||
|
|
||||||
// Indicates the field ids of the cells that were updated in this row.
|
// Indicates the field ids of the cells that were updated in this row.
|
||||||
#[pb(index = 2)]
|
#[pb(index = 2)]
|
||||||
pub field_ids: Vec<String>,
|
pub field_ids: Vec<String>,
|
||||||
|
|
||||||
|
/// The meta of row was updated if this is Some.
|
||||||
|
#[pb(index = 3, one_of)]
|
||||||
|
pub row_meta: Option<RowMetaPB>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<UpdatedRow> for UpdatedRowPB {
|
impl From<UpdatedRow> for UpdatedRowPB {
|
||||||
fn from(data: UpdatedRow) -> Self {
|
fn from(data: UpdatedRow) -> Self {
|
||||||
|
let row_meta = data.row_meta.map(RowMetaPB::from);
|
||||||
Self {
|
Self {
|
||||||
row: data.row.into(),
|
row_id: data.row_id,
|
||||||
field_ids: data.field_ids,
|
field_ids: data.field_ids,
|
||||||
|
row_meta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,6 +133,34 @@ pub(crate) async fn get_fields_handler(
|
|||||||
data_result_ok(fields)
|
data_result_ok(fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(data, manager), err)]
|
||||||
|
pub(crate) async fn get_primary_field_handler(
|
||||||
|
data: AFPluginData<DatabaseViewIdPB>,
|
||||||
|
manager: AFPluginState<Arc<DatabaseManager2>>,
|
||||||
|
) -> DataResult<FieldPB, FlowyError> {
|
||||||
|
let view_id = data.into_inner().value;
|
||||||
|
let database_editor = manager.get_database_with_view_id(&view_id).await?;
|
||||||
|
let mut fields = database_editor
|
||||||
|
.get_fields(&view_id, None)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|field| field.is_primary)
|
||||||
|
.map(FieldPB::from)
|
||||||
|
.collect::<Vec<FieldPB>>();
|
||||||
|
|
||||||
|
if fields.is_empty() {
|
||||||
|
// The primary field should not be empty. Because it is created when the database is created.
|
||||||
|
// If it is empty, it must be a bug.
|
||||||
|
Err(FlowyError::record_not_found())
|
||||||
|
} else {
|
||||||
|
if fields.len() > 1 {
|
||||||
|
// The primary field should not be more than one. If it is more than one,
|
||||||
|
// it must be a bug.
|
||||||
|
tracing::error!("The primary field is more than one");
|
||||||
|
}
|
||||||
|
data_result_ok(fields.remove(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", skip(data, manager), err)]
|
#[tracing::instrument(level = "trace", skip(data, manager), err)]
|
||||||
pub(crate) async fn update_field_handler(
|
pub(crate) async fn update_field_handler(
|
||||||
data: AFPluginData<FieldChangesetPB>,
|
data: AFPluginData<FieldChangesetPB>,
|
||||||
@ -300,6 +328,29 @@ pub(crate) async fn get_row_handler(
|
|||||||
data_result_ok(OptionalRowPB { row })
|
data_result_ok(OptionalRowPB { row })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_row_meta_handler(
|
||||||
|
data: AFPluginData<RowIdPB>,
|
||||||
|
manager: AFPluginState<Arc<DatabaseManager2>>,
|
||||||
|
) -> DataResult<RowMetaPB, FlowyError> {
|
||||||
|
let params: RowIdParams = data.into_inner().try_into()?;
|
||||||
|
let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?;
|
||||||
|
match database_editor.get_row_meta(¶ms.view_id, ¶ms.row_id) {
|
||||||
|
None => Err(FlowyError::record_not_found()),
|
||||||
|
Some(row) => data_result_ok(row),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_row_meta_handler(
|
||||||
|
data: AFPluginData<UpdateRowMetaChangesetPB>,
|
||||||
|
manager: AFPluginState<Arc<DatabaseManager2>>,
|
||||||
|
) -> FlowyResult<()> {
|
||||||
|
let params: UpdateRowMetaParams = data.into_inner().try_into()?;
|
||||||
|
let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?;
|
||||||
|
let row_id = RowId::from(params.id.clone());
|
||||||
|
database_editor.update_row_meta(&row_id, params).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(data, manager), err)]
|
#[tracing::instrument(level = "debug", skip(data, manager), err)]
|
||||||
pub(crate) async fn delete_row_handler(
|
pub(crate) async fn delete_row_handler(
|
||||||
data: AFPluginData<RowIdPB>,
|
data: AFPluginData<RowIdPB>,
|
||||||
@ -341,7 +392,7 @@ pub(crate) async fn move_row_handler(
|
|||||||
pub(crate) async fn create_row_handler(
|
pub(crate) async fn create_row_handler(
|
||||||
data: AFPluginData<CreateRowPayloadPB>,
|
data: AFPluginData<CreateRowPayloadPB>,
|
||||||
manager: AFPluginState<Arc<DatabaseManager2>>,
|
manager: AFPluginState<Arc<DatabaseManager2>>,
|
||||||
) -> DataResult<RowPB, FlowyError> {
|
) -> DataResult<RowMetaPB, FlowyError> {
|
||||||
let params: CreateRowParams = data.into_inner().try_into()?;
|
let params: CreateRowParams = data.into_inner().try_into()?;
|
||||||
let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?;
|
let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?;
|
||||||
let fields = database_editor.get_fields(¶ms.view_id, None);
|
let fields = database_editor.get_fields(¶ms.view_id, None);
|
||||||
@ -362,7 +413,7 @@ pub(crate) async fn create_row_handler(
|
|||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
None => Err(FlowyError::internal().context("Create row fail")),
|
None => Err(FlowyError::internal().context("Create row fail")),
|
||||||
Some(row) => data_result_ok(RowPB::from(row)),
|
Some(row) => data_result_ok(RowMetaPB::from(row.meta)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ pub fn init(database_manager: Arc<DatabaseManager2>) -> AFPlugin {
|
|||||||
.event(DatabaseEvent::DeleteAllSorts, delete_all_sorts_handler)
|
.event(DatabaseEvent::DeleteAllSorts, delete_all_sorts_handler)
|
||||||
// Field
|
// Field
|
||||||
.event(DatabaseEvent::GetFields, get_fields_handler)
|
.event(DatabaseEvent::GetFields, get_fields_handler)
|
||||||
|
.event(DatabaseEvent::GetPrimaryField, get_primary_field_handler)
|
||||||
.event(DatabaseEvent::UpdateField, update_field_handler)
|
.event(DatabaseEvent::UpdateField, update_field_handler)
|
||||||
.event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler)
|
.event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler)
|
||||||
.event(DatabaseEvent::DeleteField, delete_field_handler)
|
.event(DatabaseEvent::DeleteField, delete_field_handler)
|
||||||
@ -33,6 +34,8 @@ pub fn init(database_manager: Arc<DatabaseManager2>) -> AFPlugin {
|
|||||||
// Row
|
// Row
|
||||||
.event(DatabaseEvent::CreateRow, create_row_handler)
|
.event(DatabaseEvent::CreateRow, create_row_handler)
|
||||||
.event(DatabaseEvent::GetRow, get_row_handler)
|
.event(DatabaseEvent::GetRow, get_row_handler)
|
||||||
|
.event(DatabaseEvent::GetRowMeta, get_row_meta_handler)
|
||||||
|
.event(DatabaseEvent::UpdateRowMeta, update_row_meta_handler)
|
||||||
.event(DatabaseEvent::DeleteRow, delete_row_handler)
|
.event(DatabaseEvent::DeleteRow, delete_row_handler)
|
||||||
.event(DatabaseEvent::DuplicateRow, duplicate_row_handler)
|
.event(DatabaseEvent::DuplicateRow, duplicate_row_handler)
|
||||||
.event(DatabaseEvent::MoveRow, move_row_handler)
|
.event(DatabaseEvent::MoveRow, move_row_handler)
|
||||||
@ -172,6 +175,9 @@ pub enum DatabaseEvent {
|
|||||||
#[event(input = "CreateFieldPayloadPB", output = "TypeOptionPB")]
|
#[event(input = "CreateFieldPayloadPB", output = "TypeOptionPB")]
|
||||||
CreateTypeOption = 24,
|
CreateTypeOption = 24,
|
||||||
|
|
||||||
|
#[event(input = "DatabaseViewIdPB", output = "FieldPB")]
|
||||||
|
GetPrimaryField = 25,
|
||||||
|
|
||||||
/// [CreateSelectOption] event is used to create a new select option. Returns a [SelectOptionPB] if
|
/// [CreateSelectOption] event is used to create a new select option. Returns a [SelectOptionPB] if
|
||||||
/// there are no errors.
|
/// there are no errors.
|
||||||
#[event(input = "CreateSelectOptionPayloadPB", output = "SelectOptionPB")]
|
#[event(input = "CreateSelectOptionPayloadPB", output = "SelectOptionPB")]
|
||||||
@ -195,7 +201,7 @@ pub enum DatabaseEvent {
|
|||||||
#[event(input = "RepeatedSelectOptionPayload")]
|
#[event(input = "RepeatedSelectOptionPayload")]
|
||||||
DeleteSelectOption = 33,
|
DeleteSelectOption = 33,
|
||||||
|
|
||||||
#[event(input = "CreateRowPayloadPB", output = "RowPB")]
|
#[event(input = "CreateRowPayloadPB", output = "RowMetaPB")]
|
||||||
CreateRow = 50,
|
CreateRow = 50,
|
||||||
|
|
||||||
/// [GetRow] event is used to get the row data,[RowPB]. [OptionalRowPB] is a wrapper that enables
|
/// [GetRow] event is used to get the row data,[RowPB]. [OptionalRowPB] is a wrapper that enables
|
||||||
@ -212,6 +218,12 @@ pub enum DatabaseEvent {
|
|||||||
#[event(input = "MoveRowPayloadPB")]
|
#[event(input = "MoveRowPayloadPB")]
|
||||||
MoveRow = 54,
|
MoveRow = 54,
|
||||||
|
|
||||||
|
#[event(input = "RowIdPB", output = "RowMetaPB")]
|
||||||
|
GetRowMeta = 55,
|
||||||
|
|
||||||
|
#[event(input = "UpdateRowMetaChangesetPB")]
|
||||||
|
UpdateRowMeta = 56,
|
||||||
|
|
||||||
#[event(input = "CellIdPB", output = "CellPB")]
|
#[event(input = "CellIdPB", output = "CellPB")]
|
||||||
GetCell = 70,
|
GetCell = 70,
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@ pub enum DatabaseNotification {
|
|||||||
DidReorderRows = 65,
|
DidReorderRows = 65,
|
||||||
/// Trigger after editing the row that hit the sort rule
|
/// Trigger after editing the row that hit the sort rule
|
||||||
DidReorderSingleRow = 66,
|
DidReorderSingleRow = 66,
|
||||||
|
/// Trigger after updating the row meta
|
||||||
|
DidUpdateRowMeta = 67,
|
||||||
/// Trigger when the settings of the database are changed
|
/// Trigger when the settings of the database are changed
|
||||||
DidUpdateSettings = 70,
|
DidUpdateSettings = 70,
|
||||||
// Trigger when the layout setting of the database is updated
|
// Trigger when the layout setting of the database is updated
|
||||||
|
@ -14,19 +14,13 @@ use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
|
|||||||
use flowy_task::TaskDispatcher;
|
use flowy_task::TaskDispatcher;
|
||||||
use lib_infra::future::{to_fut, Fut};
|
use lib_infra::future::{to_fut, Fut};
|
||||||
|
|
||||||
use crate::entities::{
|
use crate::entities::*;
|
||||||
CalendarEventPB, CellChangesetNotifyPB, CellPB, ChecklistCellDataPB, DatabaseFieldChangesetPB,
|
|
||||||
DatabasePB, DatabaseViewSettingPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams,
|
|
||||||
FieldChangesetParams, FieldIdPB, FieldPB, FieldType, GroupPB, IndexFieldPB, InsertedRowPB,
|
|
||||||
LayoutSettingParams, NoDateCalendarEventPB, RepeatedFilterPB, RepeatedGroupPB, RepeatedSortPB,
|
|
||||||
RowPB, RowsChangePB, SelectOptionCellDataPB, SelectOptionPB, UpdateFilterParams,
|
|
||||||
UpdateSortParams, UpdatedRowPB,
|
|
||||||
};
|
|
||||||
use crate::notification::{send_notification, DatabaseNotification};
|
use crate::notification::{send_notification, DatabaseNotification};
|
||||||
use crate::services::cell::{
|
use crate::services::cell::{
|
||||||
apply_cell_changeset, get_cell_protobuf, AnyTypeCache, CellCache, ToCellChangeset,
|
apply_cell_changeset, get_cell_protobuf, AnyTypeCache, CellCache, ToCellChangeset,
|
||||||
};
|
};
|
||||||
use crate::services::database::util::database_view_setting_pb_from_view;
|
use crate::services::database::util::database_view_setting_pb_from_view;
|
||||||
|
use crate::services::database::{RowDetail, UpdatedRow};
|
||||||
use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews};
|
use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews};
|
||||||
use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData};
|
use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData};
|
||||||
use crate::services::field::{
|
use crate::services::field::{
|
||||||
@ -376,8 +370,8 @@ impl DatabaseEditor {
|
|||||||
|
|
||||||
pub async fn move_row(&self, view_id: &str, from: RowId, to: RowId) {
|
pub async fn move_row(&self, view_id: &str, from: RowId, to: RowId) {
|
||||||
let database = self.database.lock();
|
let database = self.database.lock();
|
||||||
if let (Some(row), Some(from_index), Some(to_index)) = (
|
if let (Some(row_meta), Some(from_index), Some(to_index)) = (
|
||||||
database.get_row(&from),
|
database.get_row_meta(&from),
|
||||||
database.index_of_row(view_id, &from),
|
database.index_of_row(view_id, &from),
|
||||||
database.index_of_row(view_id, &to),
|
database.index_of_row(view_id, &to),
|
||||||
) {
|
) {
|
||||||
@ -387,7 +381,7 @@ impl DatabaseEditor {
|
|||||||
drop(database);
|
drop(database);
|
||||||
|
|
||||||
let delete_row_id = from.into_inner();
|
let delete_row_id = from.into_inner();
|
||||||
let insert_row = InsertedRowPB::from(&row).with_index(to_index as i32);
|
let insert_row = InsertedRowPB::new(RowMetaPB::from(&row_meta)).with_index(to_index as i32);
|
||||||
let changes =
|
let changes =
|
||||||
RowsChangePB::from_move(view_id.to_string(), vec![delete_row_id], vec![insert_row]);
|
RowsChangePB::from_move(view_id.to_string(), vec![delete_row_id], vec![insert_row]);
|
||||||
send_notification(view_id, DatabaseNotification::DidUpdateViewRows)
|
send_notification(view_id, DatabaseNotification::DidUpdateViewRows)
|
||||||
@ -401,7 +395,7 @@ impl DatabaseEditor {
|
|||||||
view_id: &str,
|
view_id: &str,
|
||||||
group_id: Option<String>,
|
group_id: Option<String>,
|
||||||
mut params: CreateRowParams,
|
mut params: CreateRowParams,
|
||||||
) -> FlowyResult<Option<Row>> {
|
) -> FlowyResult<Option<RowDetail>> {
|
||||||
for view in self.database_views.editors().await {
|
for view in self.database_views.editors().await {
|
||||||
view.v_will_create_row(&mut params.cells, &group_id).await;
|
view.v_will_create_row(&mut params.cells, &group_id).await;
|
||||||
}
|
}
|
||||||
@ -409,11 +403,13 @@ impl DatabaseEditor {
|
|||||||
if let Some((index, row_order)) = result {
|
if let Some((index, row_order)) = result {
|
||||||
tracing::trace!("create row: {:?} at {}", row_order, index);
|
tracing::trace!("create row: {:?} at {}", row_order, index);
|
||||||
let row = self.database.lock().get_row(&row_order.id);
|
let row = self.database.lock().get_row(&row_order.id);
|
||||||
if let Some(row) = row {
|
let row_meta = self.database.lock().get_row_meta(&row_order.id);
|
||||||
|
if let (Some(row), Some(meta)) = (row, row_meta) {
|
||||||
|
let row_detail = RowDetail { row, meta };
|
||||||
for view in self.database_views.editors().await {
|
for view in self.database_views.editors().await {
|
||||||
view.v_did_create_row(&row, &group_id, index).await;
|
view.v_did_create_row(&row_detail, &group_id, index).await;
|
||||||
}
|
}
|
||||||
return Ok(Some(row));
|
return Ok(Some(row_detail));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,16 +487,42 @@ impl DatabaseEditor {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_rows(&self, view_id: &str) -> FlowyResult<Vec<Arc<Row>>> {
|
pub async fn get_rows(&self, view_id: &str) -> FlowyResult<Vec<Arc<RowDetail>>> {
|
||||||
let view_editor = self.database_views.get_view_editor(view_id).await?;
|
let view_editor = self.database_views.get_view_editor(view_id).await?;
|
||||||
Ok(view_editor.v_get_rows().await)
|
Ok(view_editor.v_get_rows().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_row(&self, view_id: &str, row_id: &RowId) -> Option<Row> {
|
pub fn get_row(&self, view_id: &str, row_id: &RowId) -> Option<Row> {
|
||||||
if self.database.lock().views.is_row_exist(view_id, row_id) {
|
if self.database.lock().views.is_row_exist(view_id, row_id) {
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
self.database.lock().get_row(row_id)
|
self.database.lock().get_row(row_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_row_meta(&self, view_id: &str, row_id: &RowId) -> Option<RowMetaPB> {
|
||||||
|
if self.database.lock().views.is_row_exist(view_id, row_id) {
|
||||||
|
let row_meta = self.database.lock().get_row_meta(row_id)?;
|
||||||
|
Some(RowMetaPB {
|
||||||
|
id: row_id.clone().into_inner(),
|
||||||
|
document_id: row_meta.document_id,
|
||||||
|
icon: row_meta.icon_url,
|
||||||
|
cover: row_meta.cover_url,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
tracing::warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option<RowDetail> {
|
||||||
|
if self.database.lock().views.is_row_exist(view_id, row_id) {
|
||||||
|
let meta = self.database.lock().get_row_meta(row_id)?;
|
||||||
|
let row = self.database.lock().get_row(row_id)?;
|
||||||
|
Some(RowDetail { row, meta })
|
||||||
|
} else {
|
||||||
|
tracing::warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -514,6 +536,28 @@ impl DatabaseEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip_all)]
|
||||||
|
pub async fn update_row_meta(&self, row_id: &RowId, changeset: UpdateRowMetaParams) {
|
||||||
|
self.database.lock().update_row_meta(row_id, |meta_update| {
|
||||||
|
meta_update
|
||||||
|
.insert_cover_if_not_none(changeset.cover_url)
|
||||||
|
.insert_icon_if_not_none(changeset.icon_url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use the temporary row meta to get rid of the lock that not implement the `Send` or 'Sync' trait.
|
||||||
|
let row_meta = self.database.lock().get_row_meta(row_id);
|
||||||
|
if let Some(row_meta) = row_meta {
|
||||||
|
for view in self.database_views.editors().await {
|
||||||
|
view.v_did_update_row_meta(row_id, &row_meta).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notifies the client that the row meta has been updated.
|
||||||
|
send_notification(row_id.as_str(), DatabaseNotification::DidUpdateRowMeta)
|
||||||
|
.payload(RowMetaPB::from(&row_meta))
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_cell(&self, field_id: &str, row_id: &RowId) -> Option<Cell> {
|
pub async fn get_cell(&self, field_id: &str, row_id: &RowId) -> Option<Cell> {
|
||||||
let database = self.database.lock();
|
let database = self.database.lock();
|
||||||
let field = database.fields.get_field(field_id)?;
|
let field = database.fields.get_field(field_id)?;
|
||||||
@ -630,7 +674,7 @@ impl DatabaseEditor {
|
|||||||
new_cell: Cell,
|
new_cell: Cell,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
// Get the old row before updating the cell. It would be better to get the old cell
|
// Get the old row before updating the cell. It would be better to get the old cell
|
||||||
let old_row = { self.database.lock().get_row(&row_id) };
|
let old_row = { self.get_row_detail(view_id, &row_id) };
|
||||||
|
|
||||||
// Get all auto updated fields. It will be used to notify the frontend
|
// Get all auto updated fields. It will be used to notify the frontend
|
||||||
// that the fields have been updated.
|
// that the fields have been updated.
|
||||||
@ -642,19 +686,19 @@ impl DatabaseEditor {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let option_row = self.database.lock().get_row(&row_id);
|
let option_row = self.get_row_detail(view_id, &row_id);
|
||||||
if let Some(new_row) = option_row {
|
if let Some(new_row_detail) = option_row {
|
||||||
let updated_row = UpdatedRowPB {
|
let updated_row =
|
||||||
row: RowPB::from(&new_row),
|
UpdatedRow::new(&new_row_detail.row.id).with_field_ids(vec![field_id.to_string()]);
|
||||||
field_ids: vec![field_id.to_string()],
|
let changes = RowsChangePB::from_update(view_id.to_string(), updated_row.into());
|
||||||
};
|
|
||||||
let changes = RowsChangePB::from_update(view_id.to_string(), updated_row);
|
|
||||||
send_notification(view_id, DatabaseNotification::DidUpdateViewRows)
|
send_notification(view_id, DatabaseNotification::DidUpdateViewRows)
|
||||||
.payload(changes)
|
.payload(changes)
|
||||||
.send();
|
.send();
|
||||||
|
|
||||||
for view in self.database_views.editors().await {
|
for view in self.database_views.editors().await {
|
||||||
view.v_did_update_row(&old_row, &new_row, field_id).await;
|
view
|
||||||
|
.v_did_update_row(&old_row, &new_row_detail, field_id)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -854,23 +898,23 @@ impl DatabaseEditor {
|
|||||||
from_row: RowId,
|
from_row: RowId,
|
||||||
to_row: Option<RowId>,
|
to_row: Option<RowId>,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
let row = self.database.lock().get_row(&from_row);
|
let row_detail = self.get_row_detail(view_id, &from_row);
|
||||||
match row {
|
match row_detail {
|
||||||
None => {
|
None => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Move row between group failed, can not find the row:{}",
|
"Move row between group failed, can not find the row:{}",
|
||||||
from_row
|
from_row
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
Some(row) => {
|
Some(row_detail) => {
|
||||||
let mut row_changeset = RowChangeset::new(row.id.clone());
|
let mut row_changeset = RowChangeset::new(row_detail.row.id.clone());
|
||||||
let view = self.database_views.get_view_editor(view_id).await?;
|
let view = self.database_views.get_view_editor(view_id).await?;
|
||||||
view
|
view
|
||||||
.v_move_group_row(&row, &mut row_changeset, to_group, to_row)
|
.v_move_group_row(&row_detail, &mut row_changeset, to_group, to_row)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
tracing::trace!("Row data changed: {:?}", row_changeset);
|
tracing::trace!("Row data changed: {:?}", row_changeset);
|
||||||
self.database.lock().update_row(&row.id, |row| {
|
self.database.lock().update_row(&row_detail.row.id, |row| {
|
||||||
row.set_cells(Cells::from(row_changeset.cell_by_field_id.clone()));
|
row.set_cells(Cells::from(row_changeset.cell_by_field_id.clone()));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1012,8 +1056,8 @@ impl DatabaseEditor {
|
|||||||
|
|
||||||
let rows = rows
|
let rows = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| RowPB::from(row.as_ref()))
|
.map(|row_detail| RowMetaPB::from(&row_detail.meta))
|
||||||
.collect::<Vec<RowPB>>();
|
.collect::<Vec<RowMetaPB>>();
|
||||||
Ok(DatabasePB {
|
Ok(DatabasePB {
|
||||||
id: database_id,
|
id: database_id,
|
||||||
fields,
|
fields,
|
||||||
@ -1150,20 +1194,37 @@ impl DatabaseViewData for DatabaseViewDataImpl {
|
|||||||
to_fut(async move { index })
|
to_fut(async move { index })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut<Option<(usize, Arc<Row>)>> {
|
fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut<Option<(usize, Arc<RowDetail>)>> {
|
||||||
let index = self.database.lock().index_of_row(view_id, row_id);
|
let index = self.database.lock().index_of_row(view_id, row_id);
|
||||||
let row = self.database.lock().get_row(row_id);
|
let row = self.database.lock().get_row(row_id);
|
||||||
|
let row_meta = self.database.lock().get_row_meta(row_id);
|
||||||
to_fut(async move {
|
to_fut(async move {
|
||||||
match (index, row) {
|
match (index, row, row_meta) {
|
||||||
(Some(index), Some(row)) => Some((index, Arc::new(row))),
|
(Some(index), Some(row), Some(row_meta)) => {
|
||||||
|
let row_detail = RowDetail {
|
||||||
|
row,
|
||||||
|
meta: row_meta,
|
||||||
|
};
|
||||||
|
Some((index, Arc::new(row_detail)))
|
||||||
|
},
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<Row>>> {
|
fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>> {
|
||||||
let rows = self.database.lock().get_rows_for_view(view_id);
|
let database = self.database.lock();
|
||||||
to_fut(async move { rows.into_iter().map(Arc::new).collect() })
|
let rows = database.get_rows_for_view(view_id);
|
||||||
|
let row_details = rows
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|row| {
|
||||||
|
database
|
||||||
|
.get_row_meta(&row.id)
|
||||||
|
.map(|meta| RowDetail { row, meta })
|
||||||
|
})
|
||||||
|
.collect::<Vec<RowDetail>>();
|
||||||
|
|
||||||
|
to_fut(async move { row_details.into_iter().map(Arc::new).collect() })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>> {
|
fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>> {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user