fix: convert false value in attributes to null (#7135)

This commit is contained in:
Lucas 2025-01-03 15:55:25 +08:00 committed by Lucas.Xu
parent db11886e5f
commit 0c1eb7306a
3 changed files with 176 additions and 26 deletions

View file

@ -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 {

View file

@ -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(

View file

@ -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;
});
});
}