Wire Backlog nav, Break up, Settings dialog + merge Backlog Board feature #20
12 changed files with 548 additions and 0 deletions
|
|
@ -23,6 +23,16 @@ part 'application_management/codes/owner_settings_warning_code.dart';
|
|||
part 'application_management/requests/get_backlog_request.dart';
|
||||
part 'application_management/read_models/backlog_item_read_model.dart';
|
||||
part 'application_management/read_models/backlog_query_result.dart';
|
||||
part 'application_management/read_models/backlog_board/backlog_board_column_read_model.dart';
|
||||
part 'application_management/read_models/backlog_board/backlog_board_item_read_model.dart';
|
||||
part 'application_management/read_models/backlog_board/backlog_board_query_result.dart';
|
||||
part 'application_management/read_models/backlog_board/backlog_board_summary_read_model.dart';
|
||||
part 'application_management/read_models/backlog_board/backlog_child_preview_read_model.dart';
|
||||
part 'application_management/read_models/backlog_board/backlog_distribution_entry_read_model.dart';
|
||||
part 'application_management/read_models/backlog_board/backlog_distribution_read_model.dart';
|
||||
part 'application_management/read_models/backlog_board/backlog_task_detail_read_model.dart';
|
||||
part 'application_management/read_models/backlog_board/project_option_read_model.dart';
|
||||
part 'application_management/read_models/backlog_board/suggested_slot_read_model.dart';
|
||||
part 'application_management/requests/get_project_defaults_request.dart';
|
||||
part 'application_management/read_models/project_defaults_query_result.dart';
|
||||
part 'application_management/drafts/project_profile_draft.dart';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
part of '../../../application_management.dart';
|
||||
|
||||
/// One column in the Backlog board.
|
||||
class BacklogBoardColumnReadModel {
|
||||
/// Creates a board column with immutable [items].
|
||||
BacklogBoardColumnReadModel({
|
||||
required this.bucket,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.accentToken,
|
||||
required List<BacklogBoardItemReadModel> items,
|
||||
int? count,
|
||||
}) : items = List<BacklogBoardItemReadModel>.unmodifiable(items),
|
||||
count = count ?? items.length;
|
||||
|
||||
/// Bucket represented by this column.
|
||||
final BacklogBoardBucket bucket;
|
||||
|
||||
/// Display title for the column header.
|
||||
final String title;
|
||||
|
||||
/// Short display subtitle for the column header.
|
||||
final String subtitle;
|
||||
|
||||
/// UI color token used by Flutter to style the column accent.
|
||||
final String accentToken;
|
||||
|
||||
/// Count shown in the column header.
|
||||
final int count;
|
||||
|
||||
/// Items rendered in this column.
|
||||
final List<BacklogBoardItemReadModel> items;
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
part of '../../../application_management.dart';
|
||||
|
||||
/// Card-level read model for one Backlog board task.
|
||||
class BacklogBoardItemReadModel {
|
||||
/// Creates a Backlog board item from display-ready task metadata.
|
||||
BacklogBoardItemReadModel({
|
||||
required this.taskId,
|
||||
required this.title,
|
||||
required this.projectId,
|
||||
required this.projectName,
|
||||
required this.projectColorToken,
|
||||
required this.reward,
|
||||
required this.difficulty,
|
||||
required this.bucket,
|
||||
required this.bucketReason,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required List<BacklogChildPreviewReadModel> childPreview,
|
||||
required List<String> tags,
|
||||
this.subtitle,
|
||||
this.durationMinutes,
|
||||
this.priority,
|
||||
bool? hasChildren,
|
||||
}) : childPreview =
|
||||
List<BacklogChildPreviewReadModel>.unmodifiable(childPreview),
|
||||
tags = List<String>.unmodifiable(tags),
|
||||
hasChildren = hasChildren ?? childPreview.isNotEmpty;
|
||||
|
||||
/// Stable task id used for selection and commands.
|
||||
final String taskId;
|
||||
|
||||
/// User-facing task title.
|
||||
final String title;
|
||||
|
||||
/// Optional subtitle or notes preview for card display.
|
||||
final String? subtitle;
|
||||
|
||||
/// Owning project id.
|
||||
final String projectId;
|
||||
|
||||
/// Display name for the owning project.
|
||||
final String projectName;
|
||||
|
||||
/// UI color token for the owning project.
|
||||
final String projectColorToken;
|
||||
|
||||
/// Estimated task duration in minutes, if captured.
|
||||
final int? durationMinutes;
|
||||
|
||||
/// Optional task priority.
|
||||
final PriorityLevel? priority;
|
||||
|
||||
/// Expected reward or payoff.
|
||||
final RewardLevel reward;
|
||||
|
||||
/// Expected activation difficulty.
|
||||
final DifficultyLevel difficulty;
|
||||
|
||||
/// Board bucket selected by scheduler policy.
|
||||
final BacklogBoardBucket bucket;
|
||||
|
||||
/// User-facing reason for [bucket].
|
||||
final String bucketReason;
|
||||
|
||||
/// Original task creation timestamp.
|
||||
final DateTime createdAt;
|
||||
|
||||
/// Last task update timestamp.
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Whether the task has child task metadata.
|
||||
final bool hasChildren;
|
||||
|
||||
/// Preview rows for direct child tasks.
|
||||
final List<BacklogChildPreviewReadModel> childPreview;
|
||||
|
||||
/// Display-ready task-local tag labels.
|
||||
final List<String> tags;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
part of '../../../application_management.dart';
|
||||
|
||||
/// Query result for the Backlog board surface.
|
||||
class BacklogBoardQueryResult {
|
||||
/// Creates a Backlog board result from summary, columns, and project options.
|
||||
BacklogBoardQueryResult({
|
||||
required this.ownerId,
|
||||
required this.localDate,
|
||||
required this.summary,
|
||||
required List<BacklogBoardColumnReadModel> columns,
|
||||
required List<ProjectOptionReadModel> projectOptions,
|
||||
}) : columns = List<BacklogBoardColumnReadModel>.unmodifiable(columns),
|
||||
projectOptions = List<ProjectOptionReadModel>.unmodifiable(
|
||||
projectOptions,
|
||||
);
|
||||
|
||||
/// Authorization-neutral owner scope used for the read.
|
||||
final String ownerId;
|
||||
|
||||
/// Owner-local planning date used for suggested slot previews.
|
||||
final CivilDate localDate;
|
||||
|
||||
/// Summary counts and planning copy derived from the same filtered item set.
|
||||
final BacklogBoardSummaryReadModel summary;
|
||||
|
||||
/// Stable ordered board columns.
|
||||
final List<BacklogBoardColumnReadModel> columns;
|
||||
|
||||
/// Project choices available to board and drawer controls.
|
||||
final List<ProjectOptionReadModel> projectOptions;
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
part of '../../../application_management.dart';
|
||||
|
||||
/// Summary panel read model for the Backlog board.
|
||||
class BacklogBoardSummaryReadModel {
|
||||
/// Creates a Backlog summary from count, estimate, and distribution data.
|
||||
const BacklogBoardSummaryReadModel({
|
||||
required this.totalTaskCount,
|
||||
required this.totalEstimatedMinutes,
|
||||
required this.priorityDistribution,
|
||||
required this.projectDistribution,
|
||||
required this.durationDistribution,
|
||||
required this.planningTip,
|
||||
});
|
||||
|
||||
/// Total number of tasks represented by the filtered board.
|
||||
final int totalTaskCount;
|
||||
|
||||
/// Sum of captured duration estimates across the filtered board.
|
||||
final int totalEstimatedMinutes;
|
||||
|
||||
/// Count distribution by priority label.
|
||||
final BacklogDistributionReadModel priorityDistribution;
|
||||
|
||||
/// Count distribution by project label.
|
||||
final BacklogDistributionReadModel projectDistribution;
|
||||
|
||||
/// Count and percentage distribution by duration bucket.
|
||||
final BacklogDistributionReadModel durationDistribution;
|
||||
|
||||
/// Display copy for the planning tip panel.
|
||||
final String planningTip;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
part of '../../../application_management.dart';
|
||||
|
||||
/// Compact preview for a direct child task.
|
||||
class BacklogChildPreviewReadModel {
|
||||
/// Creates a child task preview row.
|
||||
const BacklogChildPreviewReadModel({
|
||||
required this.taskId,
|
||||
required this.title,
|
||||
required this.isCompleted,
|
||||
this.durationMinutes,
|
||||
this.statusLabel,
|
||||
});
|
||||
|
||||
/// Stable child task id.
|
||||
final String taskId;
|
||||
|
||||
/// User-facing child task title.
|
||||
final String title;
|
||||
|
||||
/// Estimated child task duration in minutes, if captured.
|
||||
final int? durationMinutes;
|
||||
|
||||
/// Whether the child task is completed.
|
||||
final bool isCompleted;
|
||||
|
||||
/// Optional display label for the child task lifecycle state.
|
||||
final String? statusLabel;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
part of '../../../application_management.dart';
|
||||
|
||||
/// One entry in a Backlog summary distribution.
|
||||
class BacklogDistributionEntryReadModel {
|
||||
/// Creates a distribution entry.
|
||||
const BacklogDistributionEntryReadModel({
|
||||
required this.id,
|
||||
required this.label,
|
||||
required this.count,
|
||||
required this.percentage,
|
||||
this.colorToken,
|
||||
});
|
||||
|
||||
/// Stable entry id for tests and UI keys.
|
||||
final String id;
|
||||
|
||||
/// Display label for the entry.
|
||||
final String label;
|
||||
|
||||
/// Number of tasks in this entry.
|
||||
final int count;
|
||||
|
||||
/// Share of the parent distribution as a value from 0.0 to 1.0.
|
||||
final double percentage;
|
||||
|
||||
/// Optional UI color token for the entry.
|
||||
final String? colorToken;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
part of '../../../application_management.dart';
|
||||
|
||||
/// Named distribution used by Backlog summary panels.
|
||||
class BacklogDistributionReadModel {
|
||||
/// Creates a distribution with immutable [entries].
|
||||
BacklogDistributionReadModel({
|
||||
required this.title,
|
||||
required List<BacklogDistributionEntryReadModel> entries,
|
||||
int? totalCount,
|
||||
}) : entries = List<BacklogDistributionEntryReadModel>.unmodifiable(
|
||||
entries,
|
||||
),
|
||||
totalCount = totalCount ??
|
||||
entries.fold<int>(0, (sum, entry) => sum + entry.count);
|
||||
|
||||
/// Display title for this distribution.
|
||||
final String title;
|
||||
|
||||
/// Total item count represented by [entries].
|
||||
final int totalCount;
|
||||
|
||||
/// Ordered display entries.
|
||||
final List<BacklogDistributionEntryReadModel> entries;
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
part of '../../../application_management.dart';
|
||||
|
||||
/// Drawer-level read model for a selected Backlog task.
|
||||
class BacklogTaskDetailReadModel {
|
||||
/// Creates a task detail result for the Backlog drawer.
|
||||
BacklogTaskDetailReadModel({
|
||||
required this.item,
|
||||
required this.addedLabel,
|
||||
required List<String> tags,
|
||||
required List<ProjectOptionReadModel> projectOptions,
|
||||
required List<SuggestedSlotReadModel> suggestedSlots,
|
||||
this.notes,
|
||||
this.suggestedSlotUnavailableReason,
|
||||
}) : tags = List<String>.unmodifiable(tags),
|
||||
projectOptions = List<ProjectOptionReadModel>.unmodifiable(
|
||||
projectOptions,
|
||||
),
|
||||
suggestedSlots = List<SuggestedSlotReadModel>.unmodifiable(
|
||||
suggestedSlots,
|
||||
);
|
||||
|
||||
/// Board card model for the selected task.
|
||||
final BacklogBoardItemReadModel item;
|
||||
|
||||
/// Optional long-form notes for the drawer.
|
||||
final String? notes;
|
||||
|
||||
/// Display label describing when the task was added to Backlog.
|
||||
final String addedLabel;
|
||||
|
||||
/// Display-ready task-local tag labels.
|
||||
final List<String> tags;
|
||||
|
||||
/// Available project choices for the selected task.
|
||||
final List<ProjectOptionReadModel> projectOptions;
|
||||
|
||||
/// Non-mutating suggested schedule slots.
|
||||
final List<SuggestedSlotReadModel> suggestedSlots;
|
||||
|
||||
/// Display reason when slot suggestions are unavailable.
|
||||
final String? suggestedSlotUnavailableReason;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
part of '../../../application_management.dart';
|
||||
|
||||
/// Project option exposed to Backlog board and drawer controls.
|
||||
class ProjectOptionReadModel {
|
||||
/// Creates a project option read model.
|
||||
const ProjectOptionReadModel({
|
||||
required this.projectId,
|
||||
required this.name,
|
||||
required this.colorToken,
|
||||
this.isArchived = false,
|
||||
});
|
||||
|
||||
/// Stable project id.
|
||||
final String projectId;
|
||||
|
||||
/// User-facing project name.
|
||||
final String name;
|
||||
|
||||
/// UI color token for the project.
|
||||
final String colorToken;
|
||||
|
||||
/// Whether this project is archived.
|
||||
final bool isArchived;
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
part of '../../../application_management.dart';
|
||||
|
||||
/// Preview row for a possible Backlog scheduling slot.
|
||||
class SuggestedSlotReadModel {
|
||||
/// Creates a suggested slot preview.
|
||||
const SuggestedSlotReadModel({
|
||||
required this.id,
|
||||
required this.localDateLabel,
|
||||
required this.startText,
|
||||
required this.endText,
|
||||
required this.durationMinutes,
|
||||
required this.fitLabel,
|
||||
required this.fitSeverityToken,
|
||||
required this.isPrimary,
|
||||
});
|
||||
|
||||
/// Stable suggestion id.
|
||||
final String id;
|
||||
|
||||
/// Display label for the owner-local date.
|
||||
final String localDateLabel;
|
||||
|
||||
/// Display-ready start time.
|
||||
final String startText;
|
||||
|
||||
/// Display-ready end time.
|
||||
final String endText;
|
||||
|
||||
/// Suggested slot length in minutes.
|
||||
final int durationMinutes;
|
||||
|
||||
/// Display label such as `Great Fit` or `Okay`.
|
||||
final String fitLabel;
|
||||
|
||||
/// UI severity token for the fit label.
|
||||
final String fitSeverityToken;
|
||||
|
||||
/// Whether this is the primary/default scheduling target.
|
||||
final bool isPrimary;
|
||||
}
|
||||
|
|
@ -70,6 +70,153 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('backlog board read models snapshot collection inputs', () {
|
||||
final childPreviews = [
|
||||
const BacklogChildPreviewReadModel(
|
||||
taskId: 'child-1',
|
||||
title: 'Gather statements',
|
||||
isCompleted: false,
|
||||
durationMinutes: 15,
|
||||
statusLabel: 'Backlog',
|
||||
),
|
||||
];
|
||||
final itemTags = ['finance'];
|
||||
final item = BacklogBoardItemReadModel(
|
||||
taskId: 'review-budget',
|
||||
title: 'Review Q3 budget',
|
||||
subtitle: 'Check the draft before planning.',
|
||||
projectId: 'work',
|
||||
projectName: 'Work',
|
||||
projectColorToken: 'project-work',
|
||||
durationMinutes: 60,
|
||||
priority: PriorityLevel.high,
|
||||
reward: RewardLevel.medium,
|
||||
difficulty: DifficultyLevel.easy,
|
||||
bucket: BacklogBoardBucket.needTimeBlock,
|
||||
bucketReason: 'Needs a focused time block.',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
childPreview: childPreviews,
|
||||
tags: itemTags,
|
||||
);
|
||||
|
||||
childPreviews.clear();
|
||||
itemTags.add('late');
|
||||
|
||||
expect(item.childPreview, hasLength(1));
|
||||
expect(item.tags, ['finance']);
|
||||
expect(() => item.tags.add('new'), throwsUnsupportedError);
|
||||
|
||||
final items = [item];
|
||||
final column = BacklogBoardColumnReadModel(
|
||||
bucket: BacklogBoardBucket.needTimeBlock,
|
||||
title: 'Need Time Block',
|
||||
subtitle: 'Plan deliberately.',
|
||||
accentToken: 'backlog-time-block',
|
||||
items: items,
|
||||
);
|
||||
items.clear();
|
||||
|
||||
expect(column.count, 1);
|
||||
expect(column.items, [item]);
|
||||
expect(() => column.items.clear(), throwsUnsupportedError);
|
||||
|
||||
final entries = [
|
||||
const BacklogDistributionEntryReadModel(
|
||||
id: 'high',
|
||||
label: 'High',
|
||||
count: 2,
|
||||
percentage: 1,
|
||||
colorToken: 'priority-high',
|
||||
),
|
||||
];
|
||||
final priorityDistribution = BacklogDistributionReadModel(
|
||||
title: 'Priority',
|
||||
entries: entries,
|
||||
);
|
||||
entries.clear();
|
||||
|
||||
expect(priorityDistribution.totalCount, 2);
|
||||
expect(priorityDistribution.entries, hasLength(1));
|
||||
expect(
|
||||
() => priorityDistribution.entries.clear(),
|
||||
throwsUnsupportedError,
|
||||
);
|
||||
|
||||
final projects = [
|
||||
const ProjectOptionReadModel(
|
||||
projectId: 'work',
|
||||
name: 'Work',
|
||||
colorToken: 'project-work',
|
||||
),
|
||||
];
|
||||
final summary = BacklogBoardSummaryReadModel(
|
||||
totalTaskCount: 1,
|
||||
totalEstimatedMinutes: 60,
|
||||
priorityDistribution: priorityDistribution,
|
||||
projectDistribution: BacklogDistributionReadModel(
|
||||
title: 'Project',
|
||||
entries: const [
|
||||
BacklogDistributionEntryReadModel(
|
||||
id: 'work',
|
||||
label: 'Work',
|
||||
count: 1,
|
||||
percentage: 1,
|
||||
colorToken: 'project-work',
|
||||
),
|
||||
],
|
||||
),
|
||||
durationDistribution: BacklogDistributionReadModel(
|
||||
title: 'Duration',
|
||||
entries: const [
|
||||
BacklogDistributionEntryReadModel(
|
||||
id: '30-60',
|
||||
label: '30-60',
|
||||
count: 1,
|
||||
percentage: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
planningTip: 'Pick one focused task before adding more.',
|
||||
);
|
||||
final result = BacklogBoardQueryResult(
|
||||
ownerId: 'owner-1',
|
||||
localDate: CivilDate(2026, 6, 25),
|
||||
summary: summary,
|
||||
columns: [column],
|
||||
projectOptions: projects,
|
||||
);
|
||||
projects.clear();
|
||||
|
||||
expect(result.columns, [column]);
|
||||
expect(result.projectOptions, hasLength(1));
|
||||
expect(() => result.projectOptions.clear(), throwsUnsupportedError);
|
||||
|
||||
final detail = BacklogTaskDetailReadModel(
|
||||
item: item,
|
||||
addedLabel: 'Added today',
|
||||
tags: const ['finance'],
|
||||
projectOptions: result.projectOptions,
|
||||
suggestedSlots: const [
|
||||
SuggestedSlotReadModel(
|
||||
id: 'today-1',
|
||||
localDateLabel: 'Today',
|
||||
startText: '2:00 PM',
|
||||
endText: '3:00 PM',
|
||||
durationMinutes: 60,
|
||||
fitLabel: 'Great Fit',
|
||||
fitSeverityToken: 'fit-great',
|
||||
isPrimary: true,
|
||||
),
|
||||
],
|
||||
notes: 'Review budget categories before scheduling.',
|
||||
);
|
||||
|
||||
expect(detail.tags, ['finance']);
|
||||
expect(detail.suggestedSlots.single.fitLabel, 'Great Fit');
|
||||
expect(() => detail.suggestedSlots.clear(), throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test('keeps configured project defaults separate from learned suggestions',
|
||||
() async {
|
||||
final project = ProjectProfile(
|
||||
|
|
|
|||
Loading…
Reference in a new issue