Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion packages/wonky_app/lib/l10n/wonky_app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -2052,6 +2052,31 @@
}
}
},
"boardSimilarShow": "Show #{number} {title}",
"@boardSimilarShow": {
"description": "Screen-reader label of a similar issue's title in the report dialog: it opens the issue's details.",
"placeholders": {
"number": {
"type": "int"
},
"title": {
"type": "String"
}
}
},
"boardSimilarLoadFailed": "Couldn't load this issue. Try again later.",
"boardTaskCommentsLabel": "Comments for the prompt",
"boardTaskCommentsHint": "The AI reads the checked comments when it writes the prompt.",
"boardCommentsPromptHint": "Checked comments go into the task prompt when you select this issue for building.",
"boardCommentInPrompt": "Include the comment by {name} in the task prompt",
"@boardCommentInPrompt": {
"description": "Screen-reader label of an admin's checkbox on an issue comment.",
"placeholders": {
"name": {
"type": "String"
}
}
},
"boardReportVoteInstead": "Vote instead",
"boardReportVoted": "Voted",
"boardReportVotedFor": "Voted for #{number}",
Expand Down Expand Up @@ -2388,7 +2413,6 @@
"boardEmptyHint": "Change the filters, or be the first to report one.",
"boardEmptyTasks": "No tasks up for grabs",
"boardEmptyTasksHint": "Admins publish tasks from the most wanted issues. Check back soon.",
"boardDoneTasks": "Finished tasks",
"boardUpvoteLabel": "Upvote, {score} votes",
"@boardUpvoteLabel": {
"description": "Semantics label of the up-vote button.",
Expand Down Expand Up @@ -2496,6 +2520,7 @@
"boardSelectForBuilding": "Select for building",
"boardDeclineEllipsis": "Decline…",
"boardMergeEllipsis": "Merge into…",
"boardLabelsEllipsis": "Labels…",
"boardBlockEllipsis": "Block user from the board…",
"boardEditEllipsis": "Edit…",
"boardDismiss": "Dismiss",
Expand Down
8 changes: 7 additions & 1 deletion packages/wonky_app/lib/l10n/wonky_app_en_XA.arb
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,12 @@
"boardReportBodyTooLong": "[Ţĥé ðéšçŕîþţîöñ îš ţöö ļöñĝ: åţ ɱöšţ {max} çĥåŕåçţéŕš. one two three f]",
"boardReportSimilarTitle": "[Šîɱîļåŕ îššûéš one t]",
"boardReportSimilarVotes": "[{count, plural, =1{1 ṽöţé} other{{count} ṽöţéš}} one]",
"boardSimilarShow": "[Šĥöŵ #{number} {title} on]",
"boardSimilarLoadFailed": "[Çöûļðñ'ţ ļöåð ţĥîš îššûé. Ţŕý åĝåîñ ļåţéŕ. one two three]",
"boardTaskCommentsLabel": "[Çöɱɱéñţš ƒöŕ ţĥé þŕöɱþţ one two]",
"boardTaskCommentsHint": "[Ţĥé ÅÎ ŕéåðš ţĥé çĥéçķéð çöɱɱéñţš ŵĥéñ îţ ŵŕîţéš ţĥé þŕöɱþţ. one two three four]",
"boardCommentsPromptHint": "[Çĥéçķéð çöɱɱéñţš ĝö îñţö ţĥé ţåšķ þŕöɱþţ ŵĥéñ ýöû šéļéçţ ţĥîš îššûé ƒöŕ ƀûîļðîñĝ. one two three four five]",
"boardCommentInPrompt": "[Îñçļûðé ţĥé çöɱɱéñţ ƀý {name} îñ ţĥé ţåšķ þŕöɱþţ one two thre]",
"boardReportVoteInstead": "[Ṽöţé îñšţéåð one]",
"boardReportVoted": "[Ṽöţéð on]",
"boardReportVotedFor": "[Ṽöţéð ƒöŕ #{number} one]",
Expand Down Expand Up @@ -1060,7 +1066,6 @@
"boardEmptyHint": "[Çĥåñĝé ţĥé ƒîļţéŕš, öŕ ƀé ţĥé ƒîŕšţ ţö ŕéþöŕţ öñé. one two three f]",
"boardEmptyTasks": "[Ñö ţåšķš ûþ ƒöŕ ĝŕåƀš one tw]",
"boardEmptyTasksHint": "[Åðɱîñš þûƀļîšĥ ţåšķš ƒŕöɱ ţĥé ɱöšţ ŵåñţéð îššûéš. Çĥéçķ ƀåçķ šööñ. one two three four f]",
"boardDoneTasks": "[Ƒîñîšĥéð ţåšķš one t]",
"boardUpvoteLabel": "[Ûþṽöţé, {score} ṽöţéš one t]",
"boardDownvoteLabel": "[Ðöŵñṽöţé, {score} ṽöţéš one t]",
"boardRemoveUpvote": "[Ŕéɱöṽé ûþṽöţé one t]",
Expand Down Expand Up @@ -1099,6 +1104,7 @@
"boardSelectForBuilding": "[Šéļéçţ ƒöŕ ƀûîļðîñĝ one tw]",
"boardDeclineEllipsis": "[Ðéçļîñé… one]",
"boardMergeEllipsis": "[Ṁéŕĝé îñţö… one]",
"boardLabelsEllipsis": "[Ļåƀéļš… one]",
"boardBlockEllipsis": "[Ɓļöçķ ûšéŕ ƒŕöɱ ţĥé ƀöåŕð… one two]",
"boardEditEllipsis": "[Éðîţ… on]",
"boardDismiss": "[Ðîšɱîšš one]",
Expand Down
63 changes: 59 additions & 4 deletions packages/wonky_app/lib/src/board/board_commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,35 @@ String boardTaskUrl(ShellServices s, int number) =>
String boardPrLines(int task, String code) =>
'Wonky-Task: T-$task\nWonky-Claim: $code';

/// The comments of the issues [numbers] the *Create task* dialog offers for
/// the prompt's context (specs/16 §9.3): the visible ones people wrote, the
/// ones the prompt can include. [first] is issue `numbers.first`, already
/// read. A failing read offers none.
Future<List<BoardTaskComment>> _taskComments(
BoardRepository repo,
List<int> numbers,
BoardPostView first,
) async {
final out = <BoardTaskComment>[];
for (final n in numbers) {
try {
final post = n == first.number ? first : await repo.getPost(n);
for (final c in await repo.listComments(n)) {
if (c.deleted ||
c.systemEvent != null ||
c.moderation != BoardModeration.visible ||
c.body.trim().isEmpty) {
continue;
}
out.add((postNumber: n, postTitle: post.title, comment: c));
}
} on Object {
continue;
}
}
return out;
}

/// `shell.board.*` (specs/16 §14) apart from `report` / `open`, which come
/// with the report dialog. Every state change is undoable on the shell
/// stack through [BoardUndoHandler] (the server inverse); admin commands are
Expand Down Expand Up @@ -921,13 +950,19 @@ List<Command<CommandArgs>> boardCommands(ShellServices s) {
'size': JsonSchema.string(
enumValues: [for (final x in BoardSize.values) x.name],
),
// Comments (uuids) left out of the prompt's context (§9.3).
'excludedComments': const JsonSchema.array(JsonSchema.string()),
},
required: const ['postNumbers'],
),
run: (ctx, a) async {
final numbers = [
for (final x in a.values['postNumbers']! as List) (x as num).toInt(),
];
var excluded = [
for (final x in a.values['excludedComments'] as List? ?? const [])
UuidValue.withValidation('$x'),
];
var title = a.string('title');
var area = switch (a.string('area')) {
final String x => BoardArea.values.byName(x),
Expand All @@ -946,25 +981,45 @@ List<Command<CommandArgs>> boardCommands(ShellServices s) {
area ??= first.area ?? BoardArea.suite;
size ??= first.size ?? BoardSize.small;
} else {
// The issue page passes the comments its admin unchecked: the
// dialog then lists none.
final comments = a.values.containsKey('excludedComments')
? const <BoardTaskComment>[]
: await _taskComments(repo(), numbers, first);
if (!bc.mounted) return const CommandResult.cancelled();
final r = await showBoardTaskDialog(
bc,
count: numbers.length,
title: title ?? first.title,
area: area ?? first.area ?? BoardArea.suite,
size: size ?? first.size ?? BoardSize.small,
comments: comments,
);
if (r == null) return const CommandResult.cancelled();
(title, area, size) = r;
(:title, :area, :size) = (
title: r.title,
area: r.area,
size: r.size,
);
if (comments.isNotEmpty) excluded = r.excludedComments;
}
}
final t = await repo().createTask(numbers, title, area, size);
final t = await repo().createTask(
numbers,
title,
area,
size,
excludedComments: excluded,
);
ctx.record(
entry(BoardUndoOp.createTask, {
'task': t.number,
'posts': numbers,
'title': title,
'area': area.name,
'size': size.name,
if (excluded.isNotEmpty)
'excluded': [for (final u in excluded) u.uuid],
}),
);
hub.changed(task: t.number);
Expand Down Expand Up @@ -1011,7 +1066,7 @@ List<Command<CommandArgs>> boardCommands(ShellServices s) {
size: t.size,
);
if (r == null) return const CommandResult.cancelled();
(title, area, size) = r;
(:title, :area, :size, excludedComments: _) = r;
}
title ??= t.title;
area ??= t.area;
Expand Down Expand Up @@ -1134,7 +1189,7 @@ List<Command<CommandArgs>> boardCommands(ShellServices s) {
size: size ?? first.size,
);
if (r == null) return const CommandResult.cancelled();
(title, area, size) = r;
(:title, :area, :size, excludedComments: _) = r;
}
}
final t = await repo().mergeTasks(numbers, title, area, size);
Expand Down
21 changes: 17 additions & 4 deletions packages/wonky_app/lib/src/board/board_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,16 @@ abstract class BoardRepository {
Future<BoardPostView> reopen(int number);
Future<void> blockUser(int userId, String reason, {DateTime? until});
Future<void> unblockUser(int userId);

/// [excludedComments]: comments of the posts left out of the prompt's
/// context (specs/16 §9.3).
Future<BoardTaskView> createTask(
List<int> postNumbers,
String title,
BoardArea area,
BoardSize size,
);
BoardSize size, {
List<UuidValue> excludedComments = const [],
});
Future<BoardTaskView> updateTask(
int number,
String title,
Expand Down Expand Up @@ -317,8 +321,17 @@ class ServerBoardRepository implements BoardRepository {
List<int> postNumbers,
String title,
BoardArea area,
BoardSize size,
) => api.guard((c) => c.board.createTask(postNumbers, title, area, size));
BoardSize size, {
List<UuidValue> excludedComments = const [],
}) => api.guard(
(c) => c.board.createTask(
postNumbers,
title,
area,
size,
excludedComments: excludedComments.isEmpty ? null : excludedComments,
),
);

@override
Future<BoardTaskView> updateTask(
Expand Down
4 changes: 4 additions & 0 deletions packages/wonky_app/lib/src/board/board_undo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ class BoardUndoHandler extends UndoHandler<BoardUndoEntry> {
d['title']! as String,
BoardArea.values.byName(d['area']! as String),
BoardSize.values.byName(d['size']! as String),
excludedComments: [
for (final u in d['excluded'] as List? ?? const [])
UuidValue.withValidation(u as String),
],
);
_hub.changed(task: t.number);
return e.withData({'task': t.number});
Expand Down
25 changes: 25 additions & 0 deletions packages/wonky_app/lib/src/board/pages/board_comment_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class BoardCommentActions extends CommentThreadActions {
required this.services,
required this.postNumber,
this.open = true,
this.excluded,
this.onSelect,
});

final ShellServices services;
Expand All @@ -84,6 +86,13 @@ class BoardCommentActions extends CommentThreadActions {
/// Whether new comments are accepted (not on removed or deleted posts).
final bool open;

/// Admins choosing the comments for a task prompt (specs/16 §9.3): the
/// uuids left out (unchecked). Null: no checkboxes.
final Set<String>? excluded;

/// A comment was checked (true) or unchecked.
final void Function(String uuid, bool selected)? onSelect;

BoardHub get _hub => BoardHub.of(services);

BoardCommentView? _view(ThreadComment c) =>
Expand Down Expand Up @@ -128,6 +137,22 @@ class BoardCommentActions extends CommentThreadActions {
@override
String? replyHint(WonkyAppLocalizations l) => l.boardCommentHint;

// Only visible comments people wrote can go into a prompt.
@override
bool? selected(ThreadComment c) {
final ex = excluded;
if (ex == null || c.systemText != null) return null;
if (_view(c)?.moderation != BoardModeration.visible) return null;
return !ex.contains(c.id);
}

@override
void select(ThreadComment c, bool selected) => onSelect?.call(c.id, selected);

@override
String selectLabel(WonkyAppLocalizations l, ThreadComment c) =>
l.boardCommentInPrompt(c.authorName ?? l.someone);

@override
Future<void> reply(String body, {List<int> mentions = const []}) =>
_invoke('shell.board.comment.add', {..._target, 'body': body});
Expand Down
60 changes: 55 additions & 5 deletions packages/wonky_app/lib/src/board/pages/board_dialogs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -428,15 +428,45 @@ Future<(BoardPostKind, BoardArea?, BoardSize?)?> showBoardLabelsDialog(
);
}

/// A comment the *Create task* dialog offers for the prompt's context: on
/// issue [postNumber] ([postTitle]).
typedef BoardTaskComment = ({
int postNumber,
String postTitle,
BoardCommentView comment,
});

/// What *Create task* / *Edit task* / *Merge tasks* answered: the title,
/// labels and, when creating, the comments left out of the prompt.
typedef BoardTaskDialogResult = ({
String title,
BoardArea area,
BoardSize size,
List<UuidValue> excludedComments,
});

/// The checklist line of [c]: `Anna Berg: first words of the comment…`.
String boardCommentLine(BoardCommentView c, WonkyAppLocalizations l) {
final text = c.body
.replaceAll(RegExp(r'[*_`~>#\[\]]'), '')
.replaceAll(RegExp(r'\s+'), ' ')
.trim();
final clipped = text.length <= 140 ? text : '${text.substring(0, 139)}…';
return '${c.author?.displayName ?? l.boardDeletedUser}: $clipped';
}

/// *Create task from N issues…* ([count] set), *Merge N tasks…*
/// ([mergeCount] set) or *Edit task*.
Future<(String, BoardArea, BoardSize)?> showBoardTaskDialog(
/// ([mergeCount] set) or *Edit task*. When creating, [comments] are the
/// issues' comments, one checkbox each (checked: in the prompt's context,
/// specs/16 §9.3).
Future<BoardTaskDialogResult?> showBoardTaskDialog(
BuildContext context, {
int? count,
int? mergeCount,
required String title,
required BoardArea area,
required BoardSize size,
List<BoardTaskComment> comments = const [],
}) async {
final l = WonkyAppLocalizations.of(context);
final r = await showWonkyFormDialog(
Expand All @@ -453,6 +483,7 @@ Future<(String, BoardArea, BoardSize)?> showBoardTaskDialog(
_ => null,
},
dialogKey: 'dialog.board.task',
width: comments.isEmpty ? 420 : 520,
fields: [
TextFieldSpec(
id: 'title',
Expand All @@ -476,6 +507,20 @@ Future<(String, BoardArea, BoardSize)?> showBoardTaskDialog(
},
initial: size.name,
),
if (comments.isNotEmpty)
ChecklistFieldSpec(
id: 'comments',
label: l.boardTaskCommentsLabel,
description: l.boardTaskCommentsHint,
items: [
for (final c in comments)
ChecklistItem(
value: c.comment.uuid.uuid,
label: boardCommentLine(c.comment, l),
group: '#${c.postNumber} ${c.postTitle}',
),
],
),
],
buttons: [
DialogButtonSpec(id: 'cancel', label: l.actionCancel),
Expand All @@ -492,10 +537,15 @@ Future<(String, BoardArea, BoardSize)?> showBoardTaskDialog(
),
);
if (r?.buttonId != 'ok') return null;
final kept = r!.checked('comments').toSet();
return (
r!.text('title').trim(),
BoardArea.values.byName(r.text('area')),
BoardSize.values.byName(r.text('size')),
title: r.text('title').trim(),
area: BoardArea.values.byName(r.text('area')),
size: BoardSize.values.byName(r.text('size')),
excludedComments: [
for (final c in comments)
if (!kept.contains(c.comment.uuid.uuid)) c.comment.uuid,
],
);
}

Expand Down
Loading
Loading