TF-3344 Handle separator when pasting list of mail addresses
This commit is contained in:
@@ -1,10 +1,41 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'app_logger.dart';
|
||||||
|
|
||||||
class StringConvert {
|
class StringConvert {
|
||||||
static String? writeEmptyToNull(String text) {
|
static const String separatorPattern = r'[ ,;]+';
|
||||||
|
|
||||||
|
static String? writeEmptyToNull(String text) {
|
||||||
if (text.isEmpty) return null;
|
if (text.isEmpty) return null;
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String writeNullToEmpty(String? text) {
|
static String writeNullToEmpty(String? text) {
|
||||||
return text ?? '';
|
return text ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<String> extractStrings(String input) {
|
||||||
|
try {
|
||||||
|
// Check if the input is URL encoded
|
||||||
|
if (input.contains('%')) {
|
||||||
|
input = Uri.decodeComponent(input); // Decode URL encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the input is Base64 encoded
|
||||||
|
if (input.length % 4 == 0 && RegExp(r'^[A-Za-z0-9+/=]+$').hasMatch(input)) {
|
||||||
|
input = utf8.decode(base64.decode(input)); // Decode Base64 encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
final RegExp separator = RegExp(separatorPattern);
|
||||||
|
final listStrings = input
|
||||||
|
.replaceAll('\n', ' ')
|
||||||
|
.split(separator)
|
||||||
|
.where((value) => value.trim().isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
log('StringConvert::extractStrings:listStrings = $listStrings');
|
||||||
|
return listStrings;
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:core/utils/string_convert.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('StringConvert::extractStrings::', () {
|
||||||
|
group('Basic Functionality', () {
|
||||||
|
test('should extract strings separated by spaces', () {
|
||||||
|
const input = 'user1@example.com user2@example.com';
|
||||||
|
final expected = ['user1@example.com', 'user2@example.com'];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should extract strings separated by commas', () {
|
||||||
|
const input = 'user1@example.com,user2@example.com';
|
||||||
|
final expected = ['user1@example.com', 'user2@example.com'];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should extract strings separated by semicolons', () {
|
||||||
|
const input = 'user1@example.com;user2@example.com';
|
||||||
|
final expected = ['user1@example.com', 'user2@example.com'];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should extract strings separated by mixed separators', () {
|
||||||
|
const input = 'user1@example.com, user2@example.com; user3@example.com';
|
||||||
|
final expected = [
|
||||||
|
'user1@example.com',
|
||||||
|
'user2@example.com',
|
||||||
|
'user3@example.com'
|
||||||
|
];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiple consecutive separators', () {
|
||||||
|
const input =
|
||||||
|
'user1@example.com,,; user2@example.com;; user3@example.com';
|
||||||
|
final expected = [
|
||||||
|
'user1@example.com',
|
||||||
|
'user2@example.com',
|
||||||
|
'user3@example.com'
|
||||||
|
];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Edge Cases', () {
|
||||||
|
test('should handle empty input', () {
|
||||||
|
const input = '';
|
||||||
|
final expected = <String>[];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle input with only separators', () {
|
||||||
|
const input = ',, ;; ';
|
||||||
|
final expected = <String>[];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle trailing and leading separators', () {
|
||||||
|
const input = ', user1@example.com; user2@example.com ,';
|
||||||
|
final expected = ['user1@example.com', 'user2@example.com'];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle extra spaces between separators', () {
|
||||||
|
const input = 'user1@example.com , user2@example.com ; user3@example.com';
|
||||||
|
final expected = [
|
||||||
|
'user1@example.com',
|
||||||
|
'user2@example.com',
|
||||||
|
'user3@example.com'
|
||||||
|
];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Stress Tests', () {
|
||||||
|
test('should handle a large number of strings', () {
|
||||||
|
final input = List.generate(
|
||||||
|
1000,
|
||||||
|
(index) =>
|
||||||
|
'user$index@example.com${index % 3 == 0 ? ',' : index % 3 == 1 ? ';' : ' '}',
|
||||||
|
).join();
|
||||||
|
final expected =
|
||||||
|
List.generate(1000, (index) => 'user$index@example.com');
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Encoded Input Tests', () {
|
||||||
|
test('should handle URL encoded input', () {
|
||||||
|
String input = 'user1%40example.com%20user2%40example.com%20user3%40example.com';
|
||||||
|
final expected = [
|
||||||
|
'user1@example.com',
|
||||||
|
'user2@example.com',
|
||||||
|
'user3@example.com'
|
||||||
|
];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Base64 encoded input', () {
|
||||||
|
String input = 'dXNlcjFAZXhhbXBsZS5jb20gdXNlcjJAZXhhbXBsZS5jb20gdXNlcjNAZXhhbXBsZS5jb20=';
|
||||||
|
final expected = [
|
||||||
|
'user1@example.com',
|
||||||
|
'user2@example.com',
|
||||||
|
'user3@example.com'
|
||||||
|
];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle input with both URL encoding and Base64 encoding', () {
|
||||||
|
String input = Uri.encodeComponent(base64.encode(utf8.encode('user1@example.com user2@example.com user3@example.com')));
|
||||||
|
final expected = [
|
||||||
|
'user1@example.com',
|
||||||
|
'user2@example.com',
|
||||||
|
'user3@example.com'
|
||||||
|
];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Failing Cases', () {
|
||||||
|
test('should return empty list for empty input', () {
|
||||||
|
String input = '';
|
||||||
|
expect(StringConvert.extractStrings(input), equals([]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty list if input is only separators', () {
|
||||||
|
String input = ' , ; ';
|
||||||
|
expect(StringConvert.extractStrings(input), equals([]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct result for input with invalid separators', () {
|
||||||
|
String input = 'user1@example.com,,user2@example.com;;;user3@example.com';
|
||||||
|
final expected = [
|
||||||
|
'user1@example.com',
|
||||||
|
'user2@example.com',
|
||||||
|
'user3@example.com'
|
||||||
|
];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty list for input with only whitespace and separators', () {
|
||||||
|
String input = ' , ; \n';
|
||||||
|
expect(StringConvert.extractStrings(input), equals([]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle input with newline characters', () {
|
||||||
|
// Arrange
|
||||||
|
String input = 'user1@example.com\nuser2@example.com;user3@example.com';
|
||||||
|
final expected = [
|
||||||
|
'user1@example.com',
|
||||||
|
'user2@example.com',
|
||||||
|
'user3@example.com'
|
||||||
|
];
|
||||||
|
expect(StringConvert.extractStrings(input), equals(expected));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1545,15 +1545,30 @@ class ComposerController extends BaseController
|
|||||||
final inputToEmail = toEmailAddressController.text;
|
final inputToEmail = toEmailAddressController.text;
|
||||||
final inputCcEmail = ccEmailAddressController.text;
|
final inputCcEmail = ccEmailAddressController.text;
|
||||||
final inputBccEmail = bccEmailAddressController.text;
|
final inputBccEmail = bccEmailAddressController.text;
|
||||||
|
log('ComposerController::_autoCreateEmailTag:inputToEmail = $inputToEmail | inputCcEmail = $inputCcEmail | inputBccEmail = $inputBccEmail');
|
||||||
if (inputToEmail.isNotEmpty) {
|
if (inputToEmail.trim().isNotEmpty) {
|
||||||
_autoCreateToEmailTag(inputToEmail);
|
_autoCreateEmailTagForRecipientField(
|
||||||
|
prefixEmail: PrefixEmailAddress.to,
|
||||||
|
inputText: inputToEmail,
|
||||||
|
listEmailAddress: listToEmailAddress,
|
||||||
|
keyEmailTagEditor: keyToEmailTagEditor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (inputCcEmail.isNotEmpty) {
|
if (inputCcEmail.trim().isNotEmpty) {
|
||||||
_autoCreateCcEmailTag(inputCcEmail);
|
_autoCreateEmailTagForRecipientField(
|
||||||
|
prefixEmail: PrefixEmailAddress.cc,
|
||||||
|
inputText: inputCcEmail,
|
||||||
|
listEmailAddress: listCcEmailAddress,
|
||||||
|
keyEmailTagEditor: keyCcEmailTagEditor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (inputBccEmail.isNotEmpty) {
|
if (inputBccEmail.trim().isNotEmpty) {
|
||||||
_autoCreateBccEmailTag(inputBccEmail);
|
_autoCreateEmailTagForRecipientField(
|
||||||
|
prefixEmail: PrefixEmailAddress.bcc,
|
||||||
|
inputText: inputBccEmail,
|
||||||
|
listEmailAddress: listBccEmailAddress,
|
||||||
|
keyEmailTagEditor: keyBccEmailTagEditor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1564,47 +1579,53 @@ class ComposerController extends BaseController
|
|||||||
.contains(inputEmail);
|
.contains(inputEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _autoCreateToEmailTag(String inputEmail) {
|
void _autoCreateEmailTagForRecipientField({
|
||||||
if (!_isDuplicatedRecipient(inputEmail, listToEmailAddress)) {
|
required PrefixEmailAddress prefixEmail,
|
||||||
final emailAddress = EmailAddress(null, inputEmail);
|
required String inputText,
|
||||||
listToEmailAddress.add(emailAddress);
|
required List<EmailAddress> listEmailAddress,
|
||||||
isInitialRecipient.value = true;
|
required GlobalKey<TagsEditorState> keyEmailTagEditor,
|
||||||
isInitialRecipient.refresh();
|
}) {
|
||||||
_updateStatusEmailSendButton();
|
log('ComposerController::_autoCreateEmailTagForRecipientField:prefixEmail = $prefixEmail | inputText = $inputText | listEmailAddress = $listEmailAddress');
|
||||||
}
|
switch(prefixEmail) {
|
||||||
log('ComposerController::_autoCreateToEmailTag(): STATE: ${keyToEmailTagEditor.currentState}');
|
case PrefixEmailAddress.to:
|
||||||
keyToEmailTagEditor.currentState?.resetTextField();
|
case PrefixEmailAddress.cc:
|
||||||
Future.delayed(const Duration(milliseconds: 300), () {
|
case PrefixEmailAddress.bcc:
|
||||||
keyToEmailTagEditor.currentState?.closeSuggestionBox();
|
final listString = StringConvert.extractStrings(inputText).toSet();
|
||||||
});
|
if (listString.isEmpty && !_isDuplicatedRecipient(inputText, listEmailAddress)) {
|
||||||
}
|
final emailAddress = EmailAddress(null, inputText);
|
||||||
|
listEmailAddress.add(emailAddress);
|
||||||
|
isInitialRecipient.value = true;
|
||||||
|
isInitialRecipient.refresh();
|
||||||
|
_updateStatusEmailSendButton();
|
||||||
|
keyEmailTagEditor.currentState?.resetTextField();
|
||||||
|
Future.delayed(
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
|
keyEmailTagEditor.currentState?.closeSuggestionBox,
|
||||||
|
);
|
||||||
|
} else if (listString.isNotEmpty) {
|
||||||
|
final listStringNotExist = listString
|
||||||
|
.where((text) => !_isDuplicatedRecipient(text, listEmailAddress))
|
||||||
|
.toList();
|
||||||
|
|
||||||
void _autoCreateCcEmailTag(String inputEmail) {
|
if (listStringNotExist.isNotEmpty) {
|
||||||
if (!_isDuplicatedRecipient(inputEmail, listCcEmailAddress)) {
|
final listAddress = listStringNotExist
|
||||||
final emailAddress = EmailAddress(null, inputEmail);
|
.map((value) => EmailAddress(null, value))
|
||||||
listCcEmailAddress.add(emailAddress);
|
.toList();
|
||||||
isInitialRecipient.value = true;
|
listEmailAddress.addAll(listAddress);
|
||||||
isInitialRecipient.refresh();
|
isInitialRecipient.value = true;
|
||||||
_updateStatusEmailSendButton();
|
isInitialRecipient.refresh();
|
||||||
|
_updateStatusEmailSendButton();
|
||||||
|
keyEmailTagEditor.currentState?.resetTextField();
|
||||||
|
Future.delayed(
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
|
keyEmailTagEditor.currentState?.closeSuggestionBox,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
keyCcEmailTagEditor.currentState?.resetTextField();
|
|
||||||
Future.delayed(const Duration(milliseconds: 300), () {
|
|
||||||
keyCcEmailTagEditor.currentState?.closeSuggestionBox();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _autoCreateBccEmailTag(String inputEmail) {
|
|
||||||
if (!_isDuplicatedRecipient(inputEmail, listBccEmailAddress)) {
|
|
||||||
final emailAddress = EmailAddress(null, inputEmail);
|
|
||||||
listBccEmailAddress.add(emailAddress);
|
|
||||||
isInitialRecipient.value = true;
|
|
||||||
isInitialRecipient.refresh();
|
|
||||||
_updateStatusEmailSendButton();
|
|
||||||
}
|
|
||||||
keyBccEmailTagEditor.currentState?.resetTextField();
|
|
||||||
Future.delayed(const Duration(milliseconds: 300), () {
|
|
||||||
keyBccEmailTagEditor.currentState?.closeSuggestionBox();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _closeSuggestionBox() {
|
void _closeSuggestionBox() {
|
||||||
@@ -1664,22 +1685,37 @@ class ComposerController extends BaseController
|
|||||||
case PrefixEmailAddress.to:
|
case PrefixEmailAddress.to:
|
||||||
toAddressExpandMode.value = ExpandMode.COLLAPSE;
|
toAddressExpandMode.value = ExpandMode.COLLAPSE;
|
||||||
final inputToEmail = toEmailAddressController.text;
|
final inputToEmail = toEmailAddressController.text;
|
||||||
if (inputToEmail.isNotEmpty) {
|
if (inputToEmail.trim().isNotEmpty) {
|
||||||
_autoCreateToEmailTag(inputToEmail);
|
_autoCreateEmailTagForRecipientField(
|
||||||
|
prefixEmail: PrefixEmailAddress.to,
|
||||||
|
inputText: inputToEmail,
|
||||||
|
listEmailAddress: listToEmailAddress,
|
||||||
|
keyEmailTagEditor: keyToEmailTagEditor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case PrefixEmailAddress.cc:
|
case PrefixEmailAddress.cc:
|
||||||
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
|
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
|
||||||
final inputCcEmail = ccEmailAddressController.text;
|
final inputCcEmail = ccEmailAddressController.text;
|
||||||
if (inputCcEmail.isNotEmpty) {
|
if (inputCcEmail.trim().isNotEmpty) {
|
||||||
_autoCreateCcEmailTag(inputCcEmail);
|
_autoCreateEmailTagForRecipientField(
|
||||||
|
prefixEmail: PrefixEmailAddress.cc,
|
||||||
|
inputText: inputCcEmail,
|
||||||
|
listEmailAddress: listCcEmailAddress,
|
||||||
|
keyEmailTagEditor: keyCcEmailTagEditor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case PrefixEmailAddress.bcc:
|
case PrefixEmailAddress.bcc:
|
||||||
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
|
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
|
||||||
final inputBccEmail = bccEmailAddressController.text;
|
final inputBccEmail = bccEmailAddressController.text;
|
||||||
if (inputBccEmail.isNotEmpty) {
|
if (inputBccEmail.trim().isNotEmpty) {
|
||||||
_autoCreateBccEmailTag(inputBccEmail);
|
_autoCreateEmailTagForRecipientField(
|
||||||
|
prefixEmail: PrefixEmailAddress.bcc,
|
||||||
|
inputText: inputBccEmail,
|
||||||
|
listEmailAddress: listBccEmailAddress,
|
||||||
|
keyEmailTagEditor: keyBccEmailTagEditor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
Reference in New Issue
Block a user