Initial implementation of quill in Action Taken

This commit is contained in:
2026-02-21 16:12:04 +08:00
parent 1478667bbf
commit 2aeb73d5de
3 changed files with 290 additions and 51 deletions
+99 -42
View File
@@ -10,6 +10,7 @@ import '../../models/ticket_message.dart';
import '../../providers/notifications_provider.dart';
import 'dart:async';
import 'dart:convert';
import 'package:flutter_quill/flutter_quill.dart' as quill;
import '../../providers/supabase_provider.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import '../../providers/profile_provider.dart';
@@ -52,8 +53,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
final _requestedController = TextEditingController();
final _notedController = TextEditingController();
final _receivedController = TextEditingController();
// Plain text controller for Action taken (fallback from rich editor)
TextEditingController? _actionController;
// Rich text editor for Action taken
quill.QuillController? _actionController;
Timer? _actionDebounce;
Timer? _requestedDebounce;
Timer? _notedDebounce;
@@ -99,7 +100,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
CurvedAnimation(parent: _saveAnimController, curve: Curves.easeInOut),
);
// create an empty action controller by default; will seed per-task later
_actionController = TextEditingController();
_actionController = quill.QuillController.basic();
}
@override
@@ -138,26 +139,6 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
}
}
// Convert stored Quill delta JSON (or other simple structures) to plain text.
String _deltaJsonToPlainText(dynamic json) {
try {
if (json is String) return json;
if (json is List) {
final buffer = StringBuffer();
for (final item in json) {
if (item is Map && item.containsKey('insert')) {
final ins = item['insert'];
if (ins is String) buffer.write(ins);
}
}
return buffer.toString();
}
} catch (_) {
// ignore and fallthrough
}
return '';
}
@override
Widget build(BuildContext context) {
final tasksAsync = ref.watch(tasksProvider);
@@ -228,18 +209,23 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
try {
_actionDebounce?.cancel();
_actionController?.dispose();
_actionController = TextEditingController();
if (task.actionTaken != null && task.actionTaken!.isNotEmpty) {
try {
final decoded = jsonDecode(task.actionTaken!);
final plain = _deltaJsonToPlainText(decoded);
_actionController!.text = plain;
final docJson =
jsonDecode(task.actionTaken!) as List<dynamic>;
final doc = quill.Document.fromJson(docJson);
_actionController = quill.QuillController(
document: doc,
selection: const TextSelection.collapsed(offset: 0),
);
} catch (_) {
_actionController!.text = task.actionTaken!;
_actionController = quill.QuillController.basic();
}
} else {
_actionController = quill.QuillController.basic();
}
} catch (_) {
_actionController = TextEditingController();
_actionController = quill.QuillController.basic();
}
// Attach auto-save listener for action taken (debounced)
@@ -248,15 +234,19 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
_actionDebounce = Timer(
const Duration(milliseconds: 700),
() async {
final plain = _actionController?.text.trim() ?? '';
final plain =
_actionController?.document.toPlainText().trim() ?? '';
setState(() {
_actionSaving = true;
_actionSaved = false;
});
try {
final deltaJson = jsonEncode(
_actionController?.document.toDelta().toJson(),
);
await ref
.read(tasksControllerProvider)
.updateTask(taskId: task.id, actionTaken: plain);
.updateTask(taskId: task.id, actionTaken: deltaJson);
setState(() {
_actionSaved = plain.isNotEmpty;
});
@@ -1363,19 +1353,86 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
children: [
Column(
children: [
// Plain multiline editor for Action taken
Row(
children: [
IconButton(
tooltip: 'Bold',
icon: const Icon(
Icons.format_bold,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.bold,
),
),
IconButton(
tooltip: 'Italic',
icon: const Icon(
Icons.format_italic,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.italic,
),
),
IconButton(
tooltip: 'Underline',
icon: const Icon(
Icons.format_underlined,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.underline,
),
),
IconButton(
tooltip: 'Bullet list',
icon: const Icon(
Icons
.format_list_bulleted,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.ul,
),
),
IconButton(
tooltip: 'Numbered list',
icon: const Icon(
Icons
.format_list_numbered,
),
onPressed: () =>
_actionController
?.formatSelection(
quill
.Attribute
.ol,
),
),
],
),
const SizedBox(height: 6),
Expanded(
child: TextFormField(
controller:
_actionController,
readOnly: !canUpdateStatus,
maxLines: null,
expands: true,
decoration:
const InputDecoration.collapsed(
hintText:
'Describe the action taken...',
child: MouseRegion(
cursor:
SystemMouseCursors.text,
child:
quill.QuillEditor.basic(
controller:
_actionController!,
),
),
),