Add PM extension (Projects, Work Logs, Comments) + Quill rich-text UI
Projects: Brick-backed model with list/detail screens and nav entry. Work Logs: per-contributor time ledger, assignee running-clock + helper logged minutes with Hours/Minutes input; pause-after-save and helper-deduct prompts. Comments: threaded comments with file attachments and Quill composer. Detail screen: tabbed layout (Work Log / Comments / Activity), editable title + description via edit dialog, Quill-rendered descriptions with legacy plain-text fallback. Create dialog: fixed-width (480px max), Quill description editor. Shared QuillFieldEditor + QuillReadOnly widgets extracted for reuse. RLS: hardened activity-log actor binding, immutable creator_id/ created_at triggers, SECURITY INVOKER visibility helper, append-only work-log and scoped comment policies. Brick migration for project_id FK on programmer_tasks. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
/// One file attached to a comment (stored in the task_attachments bucket).
|
||||
class ProgrammerCommentAttachment {
|
||||
final String path;
|
||||
final String name;
|
||||
|
||||
const ProgrammerCommentAttachment({required this.path, required this.name});
|
||||
|
||||
Map<String, dynamic> toJson() => {'path': path, 'name': name};
|
||||
|
||||
factory ProgrammerCommentAttachment.fromJson(Map<String, dynamic> json) {
|
||||
return ProgrammerCommentAttachment(
|
||||
path: json['path']?.toString() ?? '',
|
||||
name: json['name']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A comment on a programmer task (plain, online-first model). Any user who can
|
||||
/// see the task may comment and attach files, whether or not they are the
|
||||
/// assignee.
|
||||
class ProgrammerTaskComment {
|
||||
final String id;
|
||||
final String taskId;
|
||||
final String authorId;
|
||||
final String body;
|
||||
final List<ProgrammerCommentAttachment> attachments;
|
||||
final DateTime createdAt;
|
||||
|
||||
ProgrammerTaskComment({
|
||||
required this.id,
|
||||
required this.taskId,
|
||||
required this.authorId,
|
||||
required this.body,
|
||||
this.attachments = const [],
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ProgrammerTaskComment.fromMap(Map<String, dynamic> map) {
|
||||
return ProgrammerTaskComment(
|
||||
id: map['id'].toString(),
|
||||
taskId: map['task_id'].toString(),
|
||||
authorId: map['author_id'].toString(),
|
||||
body: map['body'] as String? ?? '',
|
||||
attachments: _parseAttachments(map['attachments']),
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
static List<ProgrammerCommentAttachment> _parseAttachments(dynamic raw) {
|
||||
List<dynamic> list;
|
||||
if (raw is List) {
|
||||
list = raw;
|
||||
} else if (raw is String && raw.isNotEmpty) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
list = decoded is List ? decoded : const [];
|
||||
} catch (_) {
|
||||
list = const [];
|
||||
}
|
||||
} else {
|
||||
return const [];
|
||||
}
|
||||
return list
|
||||
.whereType<Map>()
|
||||
.map((e) => ProgrammerCommentAttachment.fromJson(
|
||||
e.map((k, v) => MapEntry(k.toString(), v)),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user