mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 06:37:14 -04:00
fix: convert false value in attributes to null (#7135)
This commit is contained in:
parent
db11886e5f
commit
0c1eb7306a
3 changed files with 176 additions and 26 deletions
|
@ -7,20 +7,7 @@ import 'package:appflowy/plugins/document/application/document_service.dart';
|
|||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/ask_ai_block_component.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show
|
||||
EditorState,
|
||||
Transaction,
|
||||
Operation,
|
||||
InsertOperation,
|
||||
UpdateOperation,
|
||||
DeleteOperation,
|
||||
PathExtensions,
|
||||
Node,
|
||||
Path,
|
||||
Delta,
|
||||
composeAttributes,
|
||||
blockComponentDelta;
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
|
@ -287,11 +274,6 @@ extension on UpdateOperation {
|
|||
// create the external text if the node contains the delta in its data.
|
||||
final prevDelta = oldAttributes[blockComponentDelta];
|
||||
final delta = attributes[blockComponentDelta];
|
||||
final diff = prevDelta != null && delta != null
|
||||
? Delta.fromJson(prevDelta).diff(
|
||||
Delta.fromJson(delta),
|
||||
)
|
||||
: null;
|
||||
|
||||
final composedAttributes = composeAttributes(oldAttributes, attributes);
|
||||
final composedDelta = composedAttributes?[blockComponentDelta];
|
||||
|
@ -312,12 +294,15 @@ extension on UpdateOperation {
|
|||
// to be compatible with the old version, we create a new text id if the text id is empty.
|
||||
final textId = nanoid(6);
|
||||
final textDelta = composedDelta ?? delta ?? prevDelta;
|
||||
final textDeltaPayloadPB = textDelta == null
|
||||
final correctedTextDelta =
|
||||
textDelta != null ? _correctAttributes(textDelta) : null;
|
||||
|
||||
final textDeltaPayloadPB = correctedTextDelta == null
|
||||
? null
|
||||
: TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
textId: textId,
|
||||
delta: jsonEncode(textDelta),
|
||||
delta: jsonEncode(correctedTextDelta),
|
||||
);
|
||||
|
||||
node.externalValues = ExternalValues(
|
||||
|
@ -342,12 +327,20 @@ extension on UpdateOperation {
|
|||
),
|
||||
);
|
||||
} else {
|
||||
final textDeltaPayloadPB = delta == null
|
||||
final diff = prevDelta != null && delta != null
|
||||
? Delta.fromJson(prevDelta).diff(
|
||||
Delta.fromJson(delta),
|
||||
)
|
||||
: null;
|
||||
|
||||
final correctedDiff = diff != null ? _correctDelta(diff) : null;
|
||||
|
||||
final textDeltaPayloadPB = correctedDiff == null
|
||||
? null
|
||||
: TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
textId: textId,
|
||||
delta: jsonEncode(diff),
|
||||
delta: jsonEncode(correctedDiff),
|
||||
);
|
||||
|
||||
if (enableDocumentInternalLog) {
|
||||
|
@ -370,6 +363,58 @@ extension on UpdateOperation {
|
|||
|
||||
return actions;
|
||||
}
|
||||
|
||||
// if the value in Delta's attributes is false, we should set the value to null instead.
|
||||
// because on Yjs, canceling format must use the null value. If we use false, the update will be rejected.
|
||||
List<TextOperation>? _correctDelta(Delta delta) {
|
||||
// if the value in diff's attributes is false, we should set the value to null instead.
|
||||
// because on Yjs, canceling format must use the null value. If we use false, the update will be rejected.
|
||||
final correctedOps = delta.map((op) {
|
||||
final attributes = op.attributes?.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
// if the value is false, we should set the value to null instead.
|
||||
value == false ? null : value,
|
||||
),
|
||||
);
|
||||
|
||||
if (attributes != null) {
|
||||
if (op is TextRetain) {
|
||||
return TextRetain(op.length, attributes: attributes);
|
||||
} else if (op is TextInsert) {
|
||||
return TextInsert(op.text, attributes: attributes);
|
||||
}
|
||||
// ignore the other operations that do not contain attributes.
|
||||
}
|
||||
|
||||
return op;
|
||||
});
|
||||
|
||||
return correctedOps.toList(growable: false);
|
||||
}
|
||||
|
||||
// Refer to [_correctDelta] for more details.
|
||||
List<Map<String, dynamic>> _correctAttributes(
|
||||
List<Map<String, dynamic>> attributes,
|
||||
) {
|
||||
final correctedAttributes = attributes.map((attribute) {
|
||||
return attribute.map((key, value) {
|
||||
if (value is bool) {
|
||||
return MapEntry(key, value == false ? null : value);
|
||||
} else if (value is Map<String, dynamic>) {
|
||||
return MapEntry(
|
||||
key,
|
||||
value.map((key, value) {
|
||||
return MapEntry(key, value == false ? null : value);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return MapEntry(key, value);
|
||||
});
|
||||
}).toList(growable: false);
|
||||
|
||||
return correctedAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
extension on DeleteOperation {
|
||||
|
|
|
@ -2,9 +2,6 @@ import 'dart:async';
|
|||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'package:appflowy/core/frameless_window.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
|
@ -24,6 +21,8 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:time/time.dart';
|
||||
|
@ -585,6 +584,10 @@ class PageManager {
|
|||
value: _notifier,
|
||||
child: Consumer<PageNotifier>(
|
||||
builder: (_, notifier, __) {
|
||||
if (notifier.plugin.pluginType == PluginType.blank) {
|
||||
return const BlankPage();
|
||||
}
|
||||
|
||||
return FadingIndexedStack(
|
||||
index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
|
||||
children: getIt<PluginSandbox>().supportPluginTypes.map(
|
||||
|
|
|
@ -290,5 +290,107 @@ void main() {
|
|||
await editorState.apply(transaction);
|
||||
await completer.future;
|
||||
});
|
||||
|
||||
test('text retain with attributes that are false', () async {
|
||||
final node = paragraphNode(
|
||||
delta: Delta()
|
||||
..insert(
|
||||
'Hello AppFlowy',
|
||||
attributes: {
|
||||
'bold': true,
|
||||
},
|
||||
),
|
||||
);
|
||||
final document = Document(
|
||||
root: pageNode(
|
||||
children: [
|
||||
node,
|
||||
],
|
||||
),
|
||||
);
|
||||
final transactionAdapter = TransactionAdapter(
|
||||
documentId: '',
|
||||
documentService: DocumentService(),
|
||||
);
|
||||
|
||||
final editorState = EditorState(
|
||||
document: document,
|
||||
);
|
||||
|
||||
int counter = 0;
|
||||
final completer = Completer();
|
||||
editorState.transactionStream.listen((event) {
|
||||
final time = event.$1;
|
||||
if (time == TransactionTime.before) {
|
||||
final actions = transactionAdapter.transactionToBlockActions(
|
||||
event.$2,
|
||||
editorState,
|
||||
);
|
||||
final textActions =
|
||||
transactionAdapter.filterTextDeltaActions(actions);
|
||||
final blockActions = transactionAdapter.filterBlockActions(actions);
|
||||
expect(textActions.length, 1);
|
||||
expect(blockActions.length, 1);
|
||||
if (counter == 1) {
|
||||
// check text operation
|
||||
final textAction = textActions.first;
|
||||
final textId = textAction.textDeltaPayloadPB?.textId;
|
||||
{
|
||||
expect(textAction.textDeltaType, TextDeltaType.create);
|
||||
|
||||
expect(textId, isNotEmpty);
|
||||
final delta = textAction.textDeltaPayloadPB?.delta;
|
||||
expect(
|
||||
delta,
|
||||
equals(
|
||||
'[{"insert":"Hello","attributes":{"bold":null}},{"insert":" AppFlowy","attributes":{"bold":true}}]',
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (counter == 3) {
|
||||
final textAction = textActions.first;
|
||||
final textId = textAction.textDeltaPayloadPB?.textId;
|
||||
{
|
||||
expect(textAction.textDeltaType, TextDeltaType.update);
|
||||
|
||||
expect(textId, isNotEmpty);
|
||||
final delta = textAction.textDeltaPayloadPB?.delta;
|
||||
expect(
|
||||
delta,
|
||||
equals(
|
||||
'[{"retain":5,"attributes":{"bold":null}}]',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (time == TransactionTime.after && counter == 3) {
|
||||
completer.complete();
|
||||
}
|
||||
});
|
||||
|
||||
counter = 1;
|
||||
final insertTransaction = editorState.transaction;
|
||||
insertTransaction.formatText(node, 0, 5, {
|
||||
'bold': false,
|
||||
});
|
||||
|
||||
await editorState.apply(insertTransaction);
|
||||
|
||||
counter = 2;
|
||||
final updateTransaction = editorState.transaction;
|
||||
updateTransaction.formatText(node, 0, 5, {
|
||||
'bold': true,
|
||||
});
|
||||
await editorState.apply(updateTransaction);
|
||||
|
||||
counter = 3;
|
||||
final formatTransaction = editorState.transaction;
|
||||
formatTransaction.formatText(node, 0, 5, {
|
||||
'bold': false,
|
||||
});
|
||||
await editorState.apply(formatTransaction);
|
||||
|
||||
await completer.future;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue