mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-11-07 22:18:14 +00:00
fix: convert false value in attributes to null (#7135)
This commit is contained in:
parent
c7e0e36902
commit
552c59218c
@ -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/plugins/document/presentation/editor_plugins/openai/widgets/ask_ai_block_component.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
show
|
|
||||||
EditorState,
|
|
||||||
Transaction,
|
|
||||||
Operation,
|
|
||||||
InsertOperation,
|
|
||||||
UpdateOperation,
|
|
||||||
DeleteOperation,
|
|
||||||
PathExtensions,
|
|
||||||
Node,
|
|
||||||
Path,
|
|
||||||
Delta,
|
|
||||||
composeAttributes,
|
|
||||||
blockComponentDelta;
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:nanoid/nanoid.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.
|
// create the external text if the node contains the delta in its data.
|
||||||
final prevDelta = oldAttributes[blockComponentDelta];
|
final prevDelta = oldAttributes[blockComponentDelta];
|
||||||
final delta = attributes[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 composedAttributes = composeAttributes(oldAttributes, attributes);
|
||||||
final composedDelta = composedAttributes?[blockComponentDelta];
|
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.
|
// to be compatible with the old version, we create a new text id if the text id is empty.
|
||||||
final textId = nanoid(6);
|
final textId = nanoid(6);
|
||||||
final textDelta = composedDelta ?? delta ?? prevDelta;
|
final textDelta = composedDelta ?? delta ?? prevDelta;
|
||||||
final textDeltaPayloadPB = textDelta == null
|
final correctedTextDelta =
|
||||||
|
textDelta != null ? _correctAttributes(textDelta) : null;
|
||||||
|
|
||||||
|
final textDeltaPayloadPB = correctedTextDelta == null
|
||||||
? null
|
? null
|
||||||
: TextDeltaPayloadPB(
|
: TextDeltaPayloadPB(
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
textId: textId,
|
textId: textId,
|
||||||
delta: jsonEncode(textDelta),
|
delta: jsonEncode(correctedTextDelta),
|
||||||
);
|
);
|
||||||
|
|
||||||
node.externalValues = ExternalValues(
|
node.externalValues = ExternalValues(
|
||||||
@ -342,12 +327,20 @@ extension on UpdateOperation {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} 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
|
? null
|
||||||
: TextDeltaPayloadPB(
|
: TextDeltaPayloadPB(
|
||||||
documentId: documentId,
|
documentId: documentId,
|
||||||
textId: textId,
|
textId: textId,
|
||||||
delta: jsonEncode(diff),
|
delta: jsonEncode(correctedDiff),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (enableDocumentInternalLog) {
|
if (enableDocumentInternalLog) {
|
||||||
@ -370,6 +363,58 @@ extension on UpdateOperation {
|
|||||||
|
|
||||||
return actions;
|
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 {
|
extension on DeleteOperation {
|
||||||
|
|||||||
@ -2,10 +2,6 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:appflowy/util/theme_extension.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
|
|
||||||
import 'package:appflowy/core/frameless_window.dart';
|
import 'package:appflowy/core/frameless_window.dart';
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
@ -13,6 +9,7 @@ import 'package:appflowy/plugins/blank/blank.dart';
|
|||||||
import 'package:appflowy/shared/window_title_bar.dart';
|
import 'package:appflowy/shared/window_title_bar.dart';
|
||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/util/theme_extension.dart';
|
||||||
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
|
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
@ -25,6 +22,8 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:time/time.dart';
|
import 'package:time/time.dart';
|
||||||
@ -697,6 +696,10 @@ class PageManager {
|
|||||||
value: _notifier,
|
value: _notifier,
|
||||||
child: Consumer<PageNotifier>(
|
child: Consumer<PageNotifier>(
|
||||||
builder: (_, notifier, __) {
|
builder: (_, notifier, __) {
|
||||||
|
if (notifier.plugin.pluginType == PluginType.blank) {
|
||||||
|
return const BlankPage();
|
||||||
|
}
|
||||||
|
|
||||||
return FadingIndexedStack(
|
return FadingIndexedStack(
|
||||||
index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
|
index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
|
||||||
children: getIt<PluginSandbox>().supportPluginTypes.map(
|
children: getIt<PluginSandbox>().supportPluginTypes.map(
|
||||||
|
|||||||
@ -290,5 +290,107 @@ void main() {
|
|||||||
await editorState.apply(transaction);
|
await editorState.apply(transaction);
|
||||||
await completer.future;
|
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…
x
Reference in New Issue
Block a user