focus-flow/apps/focus_flow_flutter/lib/app/demo_scheduler_composition.dart
pyr0ball 383130ebdb chore: merge upstream Backlog Board feature from Ashley's instance
Merges 20 commits from Ashley's smolblocks.com instance (main branch),
bringing in the full Backlog Board screen (BacklogBoardController, board
columns/cards, task detail drawer, summary panel, search/filter/sort/group,
Break Up and Someday actions) plus the unified FocusFlowSection navigation
model.

Reconciled with our own in-flight work:
- Replaced our placeholder BacklogPane/createBacklogController wiring with
  Ashley's real BacklogBoardScreen/BacklogBoardController (same problem,
  more complete implementation - avoids two competing Backlog UIs).
- Adopted the FocusFlowSection-based Sidebar API in place of our bespoke
  SidebarScreen enum; the Settings nav item now intercepts
  FocusFlowSection.settings in _selectSection to open our Settings dialog
  instead of falling through to a placeholder screen.
- Gave every sidebar nav item a stable key (not just the active one) so the
  Settings dialog tests can still target it reliably.
- Kept our additive owner-settings read controller, Settings dialog, and
  timeline Break-up wiring; renamed the Today controller field to
  todayController throughout to match upstream's now-multi-controller
  naming.
- Fixed a test-only regression: backlog_board_screen_test.dart's standalone
  SchedulerCommandController construction needed the new `management` param.
- Added .gitleaks.toml allowlisting the local, gitignored
  .claude/settings.local.json path (recorded bash command patterns, not
  repo secrets) that was blocking commits.

Verified via focusflow-flutter-ci:3.44.4 (Flutter 3.44.4/Dart 3.12.2):
flutter analyze clean, dart format clean, 106/106 Flutter tests passing,
348/348 scheduler_core + scheduler_persistence_sqlite tests passing.

Closes: Circuit-Forge/focus-flow#9
2026-07-10 22:46:09 -07:00

248 lines
7.8 KiB
Dart

// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
/// Composes the FocusFlow Flutter application around Demo Scheduler Composition.
library;
import 'package:scheduler_core/scheduler_core.dart';
import '../controllers/backlog_board_controller.dart';
import '../controllers/scheduler_command_controller.dart';
import '../controllers/scheduler_read_controller.dart';
import '../controllers/selected_date_controller.dart';
import '../controllers/today_screen_controller.dart';
import 'scheduler_app_composition.dart';
part 'demo/demo_date_formatter.dart';
part 'demo/demo_seed_tasks.dart';
/// Wires the Flutter demo UI to in-memory scheduler application use cases.
class DemoSchedulerComposition implements SchedulerAppComposition {
/// Creates a `DemoSchedulerComposition._` instance with the values required by its invariants.
/// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
DemoSchedulerComposition._({
required this.store,
required this.todayQuery,
required this.managementUseCases,
required this.commandUseCases,
required this.date,
required this.readAt,
});
/// Fixed owner used by the seeded demo data.
static const ownerId = 'owner-1';
/// Fixed owner time zone used by the seeded demo data.
static const timeZoneId = 'UTC';
/// Fixed project id used by the seeded demo data.
static const projectId = 'home';
/// In-memory application store backing the demo.
final InMemoryApplicationUnitOfWork store;
/// Query object used to read the Today state.
final GetTodayStateQuery todayQuery;
/// Management use cases used by backlog-facing UI surfaces.
final V1ApplicationManagementUseCases managementUseCases;
/// Command use cases used by interactive UI controls.
final V1ApplicationCommandUseCases commandUseCases;
/// Local date represented by the seeded demo.
final CivilDate date;
/// Stable read clock used for deterministic demo operations.
final DateTime readAt;
/// Human-readable label for [date].
@override
String get dateLabel => dateLabelFor(date);
/// Initial selected owner-local planning date.
@override
CivilDate get initialDate => date;
/// Default project id used by quick capture.
@override
String get defaultProjectId => projectId;
/// Formats [date] for compact UI date labels.
@override
String dateLabelFor(CivilDate date) {
return formatSchedulerDateLabel(date);
}
/// Creates the controller that owns the visible planning date.
@override
SelectedDateController createSelectedDateController() {
return SelectedDateController(initialDate: initialDate);
}
/// Creates a deterministic seeded composition for the demo app and tests.
factory DemoSchedulerComposition.seeded({
Clock? clock,
CivilDate? selectedDate,
bool includeTodayItems = true,
bool includeBacklogItems = true,
}) {
final resolvedDate =
selectedDate ??
CivilDate.fromDateTime((clock ?? const SystemClock()).now());
final readAt = _instant(resolvedDate, 15, 55);
final createdAt = _instant(resolvedDate, 8);
final projects = _demoProjects();
final tasks = <Task>[
if (includeTodayItems) ..._todaySeedTasks(resolvedDate, createdAt),
if (includeBacklogItems) ..._backlogSeedTasks(resolvedDate, createdAt),
];
final store = InMemoryApplicationUnitOfWork(
initialProjects: projects,
initialOwnerSettings: [
OwnerSettings(
ownerId: ownerId,
timeZoneId: timeZoneId,
compactModeEnabled: true,
),
],
initialTasks: tasks,
);
return DemoSchedulerComposition._(
store: store,
todayQuery: GetTodayStateQuery(
applicationStore: store,
timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
),
managementUseCases: V1ApplicationManagementUseCases(
applicationStore: store,
),
commandUseCases: V1ApplicationCommandUseCases(
applicationStore: store,
timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
),
date: resolvedDate,
readAt: readAt,
);
}
/// Creates the controller used by the compact Today screen mockup.
@override
TodayScreenController createTodayScreenController({
required SelectedDateController selectedDates,
}) {
return TodayScreenController(
selectedDates: selectedDates,
dateLabelFor: dateLabelFor,
now: () => readAt,
read: (date) {
return todayQuery.execute(
GetTodayStateRequest(
context: context('ui-plan-1-read-today'),
date: date,
includeNextFlexibleItem: true,
),
);
},
);
}
/// Creates the controller used by the Backlog board screen.
@override
BacklogBoardController createBacklogBoardController({
required SelectedDateController selectedDates,
}) {
return BacklogBoardController(
selectedDates: selectedDates,
dateLabelFor: dateLabelFor,
readBoard: (request) {
return managementUseCases.getBacklogBoard(
GetBacklogBoardRequest(
context: context('ui-read-backlog-board'),
localDate: request.localDate,
searchText: request.searchText,
filters: request.filters,
boardFilters: request.boardFilters,
sortKey: request.sortKey,
groupMode: request.groupMode,
),
);
},
readTaskDetail: (taskId, localDate) {
return managementUseCases.getBacklogTaskDetail(
GetBacklogTaskDetailRequest(
context: context('ui-read-backlog-detail'),
localDate: localDate,
taskId: taskId,
),
);
},
);
}
/// Creates a generic read controller for Today-state panes.
UiReadController<TodayState> createTodayController() {
return ApplicationReadController<TodayState>(
read: () {
return todayQuery.execute(
GetTodayStateRequest(
context: context('ui-read-today'),
date: date,
includeNextFlexibleItem: true,
),
);
},
isEmpty: (state) {
return state.timelineItems.isEmpty && state.pendingNotices.isEmpty;
},
);
}
/// Creates a generic read controller for the settings screen.
@override
UiReadController<OwnerSettings> createOwnerSettingsController() {
return ApplicationReadController<OwnerSettings>(
read: () {
return managementUseCases.getOwnerSettings(
GetOwnerSettingsRequest(context: context('ui-read-owner-settings')),
);
},
isEmpty: (_) => false,
);
}
/// Creates the command controller used by interactive scheduler controls.
@override
SchedulerCommandController createCommandController({
required ReadRefresh refreshReads,
required SelectedDateReader selectedDate,
}) {
return SchedulerCommandController(
commands: commandUseCases,
management: managementUseCases,
contextFor: context,
selectedDate: selectedDate,
refreshReads: refreshReads,
quickCaptureProjectId: defaultProjectId,
operationIdPrefix: 'ui-command-${readAt.microsecondsSinceEpoch}',
);
}
/// Creates an application operation context for the supplied [operationId].
ApplicationOperationContext context(String operationId, {DateTime? now}) {
return ApplicationOperationContext.start(
operationId: operationId,
ownerTimeZone: OwnerTimeZoneContext(
ownerId: ownerId,
timeZoneId: timeZoneId,
),
clock: FixedClock(now ?? readAt),
idGenerator: SequentialIdGenerator(prefix: operationId),
);
}
/// Releases demo composition resources.
@override
Future<void> dispose() async {}
}