feat: add past task push menu
This commit is contained in:
parent
f3795e8be1
commit
4d373ce250
16 changed files with 608 additions and 8 deletions
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
# Task Pushing 01 Block 03 — Push Button Card UI
|
# Task Pushing 01 Block 03 — Push Button Card UI
|
||||||
|
|
||||||
**Status:** Planned.
|
**Status:** Complete.
|
||||||
**Goal:** Render the full right-side Push button on past incomplete cards.
|
**Goal:** Render the full right-side Push button on past incomplete cards.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -15,6 +15,16 @@ After this block, past incomplete planned flexible cards visibly show a `Push`
|
||||||
button beside the reward/difficulty icons. The menu may still be stubbed until
|
button beside the reward/difficulty icons. The menu may still be stubbed until
|
||||||
Block 04.
|
Block 04.
|
||||||
|
|
||||||
|
## Implementation notes
|
||||||
|
|
||||||
|
1. `TaskTimelineCard` renders a full Push button beside reward/difficulty
|
||||||
|
controls when `TimelineCardModel.showPastTaskPushButton` is true.
|
||||||
|
2. Text reserves additional right-side space while Push is visible, and trailing
|
||||||
|
controls remain above the text layer.
|
||||||
|
3. Hover quick actions are not built while the full Push button is visible.
|
||||||
|
4. Widget tests cover render state, completed/future exclusions, hover
|
||||||
|
suppression, and callback activation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chunk 3.1 — Place the Push button in trailing controls
|
## Chunk 3.1 — Place the Push button in trailing controls
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
# Task Pushing 01 Block 04 — Push Menu Destinations
|
# Task Pushing 01 Block 04 — Push Menu Destinations
|
||||||
|
|
||||||
**Status:** Planned.
|
**Status:** Complete.
|
||||||
**Goal:** Open a destination menu from the Push button and route each choice to a
|
**Goal:** Open a destination menu from the Push button and route each choice to a
|
||||||
callback.
|
callback.
|
||||||
|
|
||||||
|
|
@ -16,6 +16,17 @@ After this block, clicking `Push` opens a menu with the three requested choices.
|
||||||
The choices may call stubbed/controller callbacks until Block 05 wires real
|
The choices may call stubbed/controller callbacks until Block 05 wires real
|
||||||
commands.
|
commands.
|
||||||
|
|
||||||
|
## Implementation notes
|
||||||
|
|
||||||
|
1. The Push button opens a compact popup menu with exactly `Push to next`,
|
||||||
|
`Push to tomorrow`, and `Push to backlog`.
|
||||||
|
2. Destination-specific callbacks are threaded from the home scaffold through
|
||||||
|
`TimelineView` to `TaskTimelineCard`.
|
||||||
|
3. The card prevents duplicate destination submission while a push callback is
|
||||||
|
still running.
|
||||||
|
4. Widget tests cover menu labels, destination callbacks, outside-click
|
||||||
|
dismissal, and one-shot running behavior.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chunk 4.1 — Add destination menu UI
|
## Chunk 4.1 — Add destination menu UI
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ class DemoSchedulerComposition implements SchedulerAppComposition {
|
||||||
return TodayScreenController(
|
return TodayScreenController(
|
||||||
selectedDates: selectedDates,
|
selectedDates: selectedDates,
|
||||||
dateLabelFor: dateLabelFor,
|
dateLabelFor: dateLabelFor,
|
||||||
|
now: () => readAt,
|
||||||
read: (date) {
|
read: (date) {
|
||||||
return todayQuery.execute(
|
return todayQuery.execute(
|
||||||
GetTodayStateRequest(
|
GetTodayStateRequest(
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,30 @@ class _FocusFlowHomeState extends State<FocusFlowHome> {
|
||||||
await commandController.addGenericFlexibleTask();
|
await commandController.addGenericFlexibleTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pushes [card] to the next available slot after the current clock time.
|
||||||
|
Future<void> _pushCardToNext(TimelineCardModel card) async {
|
||||||
|
await commandController.pushTaskToNextAvailableSlot(
|
||||||
|
taskId: card.id,
|
||||||
|
expectedUpdatedAt: card.expectedUpdatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes [card] to tomorrow's first available slot.
|
||||||
|
Future<void> _pushCardToTomorrow(TimelineCardModel card) async {
|
||||||
|
await commandController.pushTaskToTomorrow(
|
||||||
|
taskId: card.id,
|
||||||
|
expectedUpdatedAt: card.expectedUpdatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes [card] back to Backlog.
|
||||||
|
Future<void> _pushCardToBacklog(TimelineCardModel card) async {
|
||||||
|
await commandController.pushTaskToBacklog(
|
||||||
|
taskId: card.id,
|
||||||
|
expectedUpdatedAt: card.expectedUpdatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Moves the visible planning date backward by one day.
|
/// Moves the visible planning date backward by one day.
|
||||||
void _selectPreviousDate() {
|
void _selectPreviousDate() {
|
||||||
selectedDateController.selectPreviousDay();
|
selectedDateController.selectPreviousDay();
|
||||||
|
|
@ -114,6 +138,9 @@ class _FocusFlowHomeState extends State<FocusFlowHome> {
|
||||||
selectedCard: controller.selectedCard,
|
selectedCard: controller.selectedCard,
|
||||||
onSelectCard: controller.selectCard,
|
onSelectCard: controller.selectCard,
|
||||||
onCompleteCard: _toggleCardCompletion,
|
onCompleteCard: _toggleCardCompletion,
|
||||||
|
onPushToNext: _pushCardToNext,
|
||||||
|
onPushToTomorrow: _pushCardToTomorrow,
|
||||||
|
onPushToBacklog: _pushCardToBacklog,
|
||||||
onAddTask: _addGenericFlexibleTask,
|
onAddTask: _addGenericFlexibleTask,
|
||||||
onClearSelection: controller.clearSelection,
|
onClearSelection: controller.clearSelection,
|
||||||
onPreviousDate: _selectPreviousDate,
|
onPreviousDate: _selectPreviousDate,
|
||||||
|
|
@ -127,6 +154,9 @@ class _FocusFlowHomeState extends State<FocusFlowHome> {
|
||||||
selectedCard: controller.selectedCard,
|
selectedCard: controller.selectedCard,
|
||||||
onSelectCard: controller.selectCard,
|
onSelectCard: controller.selectCard,
|
||||||
onCompleteCard: _toggleCardCompletion,
|
onCompleteCard: _toggleCardCompletion,
|
||||||
|
onPushToNext: _pushCardToNext,
|
||||||
|
onPushToTomorrow: _pushCardToTomorrow,
|
||||||
|
onPushToBacklog: _pushCardToBacklog,
|
||||||
onAddTask: _addGenericFlexibleTask,
|
onAddTask: _addGenericFlexibleTask,
|
||||||
onClearSelection: controller.clearSelection,
|
onClearSelection: controller.clearSelection,
|
||||||
onPreviousDate: _selectPreviousDate,
|
onPreviousDate: _selectPreviousDate,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ class _TodayScreenScaffold extends StatefulWidget {
|
||||||
required this.selectedCard,
|
required this.selectedCard,
|
||||||
required this.onSelectCard,
|
required this.onSelectCard,
|
||||||
required this.onCompleteCard,
|
required this.onCompleteCard,
|
||||||
|
required this.onPushToNext,
|
||||||
|
required this.onPushToTomorrow,
|
||||||
|
required this.onPushToBacklog,
|
||||||
required this.onAddTask,
|
required this.onAddTask,
|
||||||
required this.onClearSelection,
|
required this.onClearSelection,
|
||||||
required this.onPreviousDate,
|
required this.onPreviousDate,
|
||||||
|
|
@ -36,6 +39,18 @@ class _TodayScreenScaffold extends StatefulWidget {
|
||||||
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
final Future<void> Function(TimelineCardModel card) onCompleteCard;
|
final Future<void> Function(TimelineCardModel card) onCompleteCard;
|
||||||
|
|
||||||
|
/// Stores the `onPushToNext` value for this object.
|
||||||
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
|
final Future<void> Function(TimelineCardModel card) onPushToNext;
|
||||||
|
|
||||||
|
/// Stores the `onPushToTomorrow` value for this object.
|
||||||
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
|
final Future<void> Function(TimelineCardModel card) onPushToTomorrow;
|
||||||
|
|
||||||
|
/// Stores the `onPushToBacklog` value for this object.
|
||||||
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
|
final Future<void> Function(TimelineCardModel card) onPushToBacklog;
|
||||||
|
|
||||||
/// Stores the `onAddTask` value for this object.
|
/// Stores the `onAddTask` value for this object.
|
||||||
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
final Future<void> Function() onAddTask;
|
final Future<void> Function() onAddTask;
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,9 @@ class _TodayScreenScaffoldState extends State<_TodayScreenScaffold> {
|
||||||
onCardSelected: widget.onSelectCard,
|
onCardSelected: widget.onSelectCard,
|
||||||
onAddTask: widget.onAddTask,
|
onAddTask: widget.onAddTask,
|
||||||
onCardCompleted: widget.onCompleteCard,
|
onCardCompleted: widget.onCompleteCard,
|
||||||
|
onPushToNext: widget.onPushToNext,
|
||||||
|
onPushToTomorrow: widget.onPushToTomorrow,
|
||||||
|
onPushToBacklog: widget.onPushToBacklog,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,68 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pushes a past flexible task to the next available slot after now.
|
||||||
|
Future<void> pushTaskToNextAvailableSlot({
|
||||||
|
required String taskId,
|
||||||
|
DateTime? expectedUpdatedAt,
|
||||||
|
}) async {
|
||||||
|
final commandAt = now();
|
||||||
|
await _run(
|
||||||
|
label: 'Pushing task',
|
||||||
|
successMessage: 'Task pushed',
|
||||||
|
refreshAfterSuccess: true,
|
||||||
|
action: (operationId) {
|
||||||
|
return commands.pushFlexibleToNextAvailableSlot(
|
||||||
|
context: contextFor(operationId, now: commandAt),
|
||||||
|
localDate: _civilDateFor(commandAt),
|
||||||
|
taskId: taskId,
|
||||||
|
expectedUpdatedAt: expectedUpdatedAt,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes a past flexible task to the first available slot tomorrow.
|
||||||
|
Future<void> pushTaskToTomorrow({
|
||||||
|
required String taskId,
|
||||||
|
DateTime? expectedUpdatedAt,
|
||||||
|
}) async {
|
||||||
|
final commandAt = now();
|
||||||
|
await _run(
|
||||||
|
label: 'Pushing task',
|
||||||
|
successMessage: 'Task moved to tomorrow',
|
||||||
|
refreshAfterSuccess: true,
|
||||||
|
action: (operationId) {
|
||||||
|
return commands.pushFlexibleToTomorrowTopOfQueue(
|
||||||
|
context: contextFor(operationId, now: commandAt),
|
||||||
|
targetDate: _civilDateFor(commandAt).addDays(1),
|
||||||
|
taskId: taskId,
|
||||||
|
expectedUpdatedAt: expectedUpdatedAt,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes a past flexible task back to Backlog.
|
||||||
|
Future<void> pushTaskToBacklog({
|
||||||
|
required String taskId,
|
||||||
|
DateTime? expectedUpdatedAt,
|
||||||
|
}) async {
|
||||||
|
final commandAt = now();
|
||||||
|
await _run(
|
||||||
|
label: 'Moving task to backlog',
|
||||||
|
successMessage: 'Task moved to backlog',
|
||||||
|
refreshAfterSuccess: true,
|
||||||
|
action: (operationId) {
|
||||||
|
return commands.moveFlexibleToBacklog(
|
||||||
|
context: contextFor(operationId, now: commandAt),
|
||||||
|
taskId: taskId,
|
||||||
|
expectedUpdatedAt: expectedUpdatedAt,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs the `_run` operation and completes after its asynchronous work finishes.
|
/// Runs the `_run` operation and completes after its asynchronous work finishes.
|
||||||
/// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
|
/// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
|
||||||
Future<void> _run({
|
Future<void> _run({
|
||||||
|
|
@ -238,4 +300,11 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
/// Runs the `_utcNow` helper used inside this library.
|
/// Runs the `_utcNow` helper used inside this library.
|
||||||
/// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
|
/// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
|
||||||
static DateTime _utcNow() => DateTime.now().toUtc();
|
static DateTime _utcNow() => DateTime.now().toUtc();
|
||||||
|
|
||||||
|
/// Runs the `_civilDateFor` helper used inside this library.
|
||||||
|
/// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
|
||||||
|
static CivilDate _civilDateFor(DateTime instant) {
|
||||||
|
final utc = instant.toUtc();
|
||||||
|
return CivilDate(utc.year, utc.month, utc.day);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,18 @@ class _CardMetrics {
|
||||||
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
||||||
double get actionsWidth => actionButtonSize * 4 + actionGap;
|
double get actionsWidth => actionButtonSize * 4 + actionGap;
|
||||||
|
|
||||||
|
/// Returns the derived `pushButtonHeight` value for this object.
|
||||||
|
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
||||||
|
double get pushButtonHeight => (28 * scale).clamp(18.0, 24.0).toDouble();
|
||||||
|
|
||||||
|
/// Returns the derived `pushButtonWidth` value for this object.
|
||||||
|
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
||||||
|
double get pushButtonWidth => (62 * scale).clamp(42.0, 58.0).toDouble();
|
||||||
|
|
||||||
|
/// Returns the derived `pushButtonGap` value for this object.
|
||||||
|
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
||||||
|
double get pushButtonGap => math.max(5, 8 * scale);
|
||||||
|
|
||||||
/// Returns the derived `indicatorTop` value for this object.
|
/// Returns the derived `indicatorTop` value for this object.
|
||||||
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
||||||
double get indicatorTop => isCompact ? 0 : math.max(1, 2 * scale);
|
double get indicatorTop => isCompact ? 0 : math.max(1, 2 * scale);
|
||||||
|
|
@ -158,10 +170,23 @@ class _CardMetrics {
|
||||||
double get expandedTrailingReserve =>
|
double get expandedTrailingReserve =>
|
||||||
indicatorWidth + math.max(8, 12 * scale);
|
indicatorWidth + math.max(8, 12 * scale);
|
||||||
|
|
||||||
|
/// Returns the derived `expandedPushTrailingReserve` value for this object.
|
||||||
|
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
||||||
|
double get expandedPushTrailingReserve =>
|
||||||
|
indicatorWidth +
|
||||||
|
pushButtonGap +
|
||||||
|
pushButtonWidth +
|
||||||
|
math.max(8, 12 * scale);
|
||||||
|
|
||||||
/// Returns the derived `compactTrailingReserve` value for this object.
|
/// Returns the derived `compactTrailingReserve` value for this object.
|
||||||
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
||||||
double get compactTrailingReserve => indicatorWidth + math.max(6, 8 * scale);
|
double get compactTrailingReserve => indicatorWidth + math.max(6, 8 * scale);
|
||||||
|
|
||||||
|
/// Returns the derived `compactPushTrailingReserve` value for this object.
|
||||||
|
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
||||||
|
double get compactPushTrailingReserve =>
|
||||||
|
indicatorWidth + pushButtonGap + pushButtonWidth + math.max(6, 8 * scale);
|
||||||
|
|
||||||
/// Returns the derived `difficultySize` value for this object.
|
/// Returns the derived `difficultySize` value for this object.
|
||||||
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
||||||
Size get difficultySize => Size(26, 14);
|
Size get difficultySize => Size(26, 14);
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,11 @@ class _CompactCardText extends StatelessWidget {
|
||||||
style: metaStyle,
|
style: metaStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: metrics.compactTrailingReserve),
|
SizedBox(
|
||||||
|
width: card.showPastTaskPushButton
|
||||||
|
? metrics.compactPushTrailingReserve
|
||||||
|
: metrics.compactTrailingReserve,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
if (!constraints.hasBoundedWidth || !constraints.hasBoundedHeight) {
|
if (!constraints.hasBoundedWidth || !constraints.hasBoundedHeight) {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,11 @@ class _ExpandedCardContent extends StatelessWidget {
|
||||||
],
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(right: metrics.expandedTrailingReserve),
|
padding: EdgeInsets.only(
|
||||||
|
right: card.showPastTaskPushButton
|
||||||
|
? metrics.expandedPushTrailingReserve
|
||||||
|
: metrics.expandedTrailingReserve,
|
||||||
|
),
|
||||||
child: _CardText(
|
child: _CardText(
|
||||||
card: card,
|
card: card,
|
||||||
color: colors.accent,
|
color: colors.accent,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ class _TaskTimelineCardState extends State<TaskTimelineCard> {
|
||||||
/// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
|
/// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
|
||||||
bool _isHovered = false;
|
bool _isHovered = false;
|
||||||
|
|
||||||
|
/// Private state stored as `_isPushRunning` for this implementation.
|
||||||
|
/// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
|
||||||
|
bool _isPushRunning = false;
|
||||||
|
|
||||||
/// Builds the widget subtree for this component.
|
/// Builds the widget subtree for this component.
|
||||||
/// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data.
|
/// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data.
|
||||||
@override
|
@override
|
||||||
|
|
@ -48,6 +52,16 @@ class _TaskTimelineCardState extends State<TaskTimelineCard> {
|
||||||
colors: colors,
|
colors: colors,
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
actionsVisible: card.showsQuickActions && _isHovered,
|
actionsVisible: card.showsQuickActions && _isHovered,
|
||||||
|
pushRunning: _isPushRunning,
|
||||||
|
onPushToNext: widget.onPushToNext == null
|
||||||
|
? null
|
||||||
|
: () => _runPush(widget.onPushToNext!),
|
||||||
|
onPushToTomorrow: widget.onPushToTomorrow == null
|
||||||
|
? null
|
||||||
|
: () => _runPush(widget.onPushToTomorrow!),
|
||||||
|
onPushToBacklog: widget.onPushToBacklog == null
|
||||||
|
? null
|
||||||
|
: () => _runPush(widget.onPushToBacklog!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -96,4 +110,26 @@ class _TaskTimelineCardState extends State<TaskTimelineCard> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs the `_runPush` operation and completes after its asynchronous work finishes.
|
||||||
|
/// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
|
||||||
|
Future<void> _runPush(
|
||||||
|
Future<void> Function(TimelineCardModel card) callback,
|
||||||
|
) async {
|
||||||
|
if (_isPushRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isPushRunning = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await callback(widget.card);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isPushRunning = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ class _TrailingControls extends StatelessWidget {
|
||||||
required this.colors,
|
required this.colors,
|
||||||
required this.metrics,
|
required this.metrics,
|
||||||
required this.actionsVisible,
|
required this.actionsVisible,
|
||||||
|
required this.pushRunning,
|
||||||
|
this.onPushToNext,
|
||||||
|
this.onPushToTomorrow,
|
||||||
|
this.onPushToBacklog,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Stores the `card` value for this object.
|
/// Stores the `card` value for this object.
|
||||||
|
|
@ -31,6 +35,19 @@ class _TrailingControls extends StatelessWidget {
|
||||||
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
final bool actionsVisible;
|
final bool actionsVisible;
|
||||||
|
|
||||||
|
/// Stores the `pushRunning` value for this object.
|
||||||
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
|
final bool pushRunning;
|
||||||
|
|
||||||
|
/// Callback invoked when Push to next is selected.
|
||||||
|
final Future<void> Function()? onPushToNext;
|
||||||
|
|
||||||
|
/// Callback invoked when Push to tomorrow is selected.
|
||||||
|
final Future<void> Function()? onPushToTomorrow;
|
||||||
|
|
||||||
|
/// Callback invoked when Push to backlog is selected.
|
||||||
|
final Future<void> Function()? onPushToBacklog;
|
||||||
|
|
||||||
/// Builds the widget subtree for this component.
|
/// Builds the widget subtree for this component.
|
||||||
/// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data.
|
/// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data.
|
||||||
@override
|
@override
|
||||||
|
|
@ -45,6 +62,19 @@ class _TrailingControls extends StatelessWidget {
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
visible: actionsVisible,
|
visible: actionsVisible,
|
||||||
),
|
),
|
||||||
|
if (card.showPastTaskPushButton) ...[
|
||||||
|
_PastTaskPushButton(
|
||||||
|
card: card,
|
||||||
|
color: colors.accent,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
metrics: metrics,
|
||||||
|
isRunning: pushRunning,
|
||||||
|
onPushToNext: onPushToNext,
|
||||||
|
onPushToTomorrow: onPushToTomorrow,
|
||||||
|
onPushToBacklog: onPushToBacklog,
|
||||||
|
),
|
||||||
|
SizedBox(width: metrics.pushButtonGap),
|
||||||
|
],
|
||||||
RewardIcon(color: colors.reward, size: metrics.rewardIconSize),
|
RewardIcon(color: colors.reward, size: metrics.rewardIconSize),
|
||||||
SizedBox(width: metrics.indicatorGap),
|
SizedBox(width: metrics.indicatorGap),
|
||||||
DifficultyBars(
|
DifficultyBars(
|
||||||
|
|
@ -56,3 +86,145 @@ class _TrailingControls extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Menu destinations exposed by the overdue-task Push button.
|
||||||
|
enum _PastTaskPushDestination {
|
||||||
|
/// Pushes the task to the next available slot.
|
||||||
|
next,
|
||||||
|
|
||||||
|
/// Pushes the task to tomorrow's first available slot.
|
||||||
|
tomorrow,
|
||||||
|
|
||||||
|
/// Pushes the task to Backlog.
|
||||||
|
backlog,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Private implementation type for `_PastTaskPushButton` in this library.
|
||||||
|
/// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly.
|
||||||
|
class _PastTaskPushButton extends StatelessWidget {
|
||||||
|
/// Creates a `_PastTaskPushButton` 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.
|
||||||
|
const _PastTaskPushButton({
|
||||||
|
required this.card,
|
||||||
|
required this.color,
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.metrics,
|
||||||
|
required this.isRunning,
|
||||||
|
this.onPushToNext,
|
||||||
|
this.onPushToTomorrow,
|
||||||
|
this.onPushToBacklog,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Stores the `card` value for this object.
|
||||||
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
|
final TimelineCardModel card;
|
||||||
|
|
||||||
|
/// Stores the `color` value for this object.
|
||||||
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// Stores the `backgroundColor` value for this object.
|
||||||
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
/// Stores the `metrics` value for this object.
|
||||||
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
|
final _CardMetrics metrics;
|
||||||
|
|
||||||
|
/// Stores the `isRunning` value for this object.
|
||||||
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
|
final bool isRunning;
|
||||||
|
|
||||||
|
/// Callback invoked when Push to next is selected.
|
||||||
|
final Future<void> Function()? onPushToNext;
|
||||||
|
|
||||||
|
/// Callback invoked when Push to tomorrow is selected.
|
||||||
|
final Future<void> Function()? onPushToTomorrow;
|
||||||
|
|
||||||
|
/// Callback invoked when Push to backlog is selected.
|
||||||
|
final Future<void> Function()? onPushToBacklog;
|
||||||
|
|
||||||
|
/// Builds the widget subtree for this component.
|
||||||
|
/// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data.
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final enabled =
|
||||||
|
!isRunning &&
|
||||||
|
(onPushToNext != null ||
|
||||||
|
onPushToTomorrow != null ||
|
||||||
|
onPushToBacklog != null);
|
||||||
|
return PopupMenuButton<_PastTaskPushDestination>(
|
||||||
|
key: ValueKey('${card.id}-push-button'),
|
||||||
|
tooltip: card.pastTaskPushSemanticLabel,
|
||||||
|
enabled: enabled,
|
||||||
|
color: const Color(0xFF122018),
|
||||||
|
elevation: 8,
|
||||||
|
position: PopupMenuPosition.under,
|
||||||
|
onSelected: (destination) {
|
||||||
|
switch (destination) {
|
||||||
|
case _PastTaskPushDestination.next:
|
||||||
|
final callback = onPushToNext;
|
||||||
|
if (callback != null) {
|
||||||
|
unawaited(callback());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case _PastTaskPushDestination.tomorrow:
|
||||||
|
final callback = onPushToTomorrow;
|
||||||
|
if (callback != null) {
|
||||||
|
unawaited(callback());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case _PastTaskPushDestination.backlog:
|
||||||
|
final callback = onPushToBacklog;
|
||||||
|
if (callback != null) {
|
||||||
|
unawaited(callback());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem<_PastTaskPushDestination>(
|
||||||
|
value: _PastTaskPushDestination.next,
|
||||||
|
enabled: onPushToNext != null,
|
||||||
|
child: const Text('Push to next'),
|
||||||
|
),
|
||||||
|
PopupMenuItem<_PastTaskPushDestination>(
|
||||||
|
value: _PastTaskPushDestination.tomorrow,
|
||||||
|
enabled: onPushToTomorrow != null,
|
||||||
|
child: const Text('Push to tomorrow'),
|
||||||
|
),
|
||||||
|
PopupMenuItem<_PastTaskPushDestination>(
|
||||||
|
value: _PastTaskPushDestination.backlog,
|
||||||
|
enabled: onPushToBacklog != null,
|
||||||
|
child: const Text('Push to backlog'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Semantics(
|
||||||
|
button: true,
|
||||||
|
enabled: enabled,
|
||||||
|
label: card.pastTaskPushSemanticLabel,
|
||||||
|
child: Container(
|
||||||
|
width: metrics.pushButtonWidth,
|
||||||
|
height: metrics.pushButtonHeight,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor.withValues(alpha: 0.96),
|
||||||
|
border: Border.all(color: color, width: 1),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Push',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: (11 * metrics.scale).clamp(8.0, 11.0).toDouble(),
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
/// Renders Task Timeline Card pieces for the FocusFlow Flutter timeline.
|
/// Renders Task Timeline Card pieces for the FocusFlow Flutter timeline.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -31,6 +32,9 @@ class TaskTimelineCard extends StatefulWidget {
|
||||||
required this.card,
|
required this.card,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.onStatusPressed,
|
this.onStatusPressed,
|
||||||
|
this.onPushToNext,
|
||||||
|
this.onPushToTomorrow,
|
||||||
|
this.onPushToBacklog,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -43,6 +47,15 @@ class TaskTimelineCard extends StatefulWidget {
|
||||||
/// Callback invoked when the left status control is clicked.
|
/// Callback invoked when the left status control is clicked.
|
||||||
final VoidCallback? onStatusPressed;
|
final VoidCallback? onStatusPressed;
|
||||||
|
|
||||||
|
/// Callback invoked when the card requests Push to next.
|
||||||
|
final Future<void> Function(TimelineCardModel card)? onPushToNext;
|
||||||
|
|
||||||
|
/// Callback invoked when the card requests Push to tomorrow.
|
||||||
|
final Future<void> Function(TimelineCardModel card)? onPushToTomorrow;
|
||||||
|
|
||||||
|
/// Callback invoked when the card requests Push to backlog.
|
||||||
|
final Future<void> Function(TimelineCardModel card)? onPushToBacklog;
|
||||||
|
|
||||||
/// Creates the mutable state object used by this stateful widget.
|
/// Creates the mutable state object used by this stateful widget.
|
||||||
/// Flutter calls this once for each mounted widget instance before lifecycle callbacks and builds begin.
|
/// Flutter calls this once for each mounted widget instance before lifecycle callbacks and builds begin.
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ class TimelineView extends StatefulWidget {
|
||||||
required this.onCardSelected,
|
required this.onCardSelected,
|
||||||
this.onAddTask,
|
this.onAddTask,
|
||||||
this.onCardCompleted,
|
this.onCardCompleted,
|
||||||
|
this.onPushToNext,
|
||||||
|
this.onPushToTomorrow,
|
||||||
|
this.onPushToBacklog,
|
||||||
this.now,
|
this.now,
|
||||||
this.scrollTargetCardId,
|
this.scrollTargetCardId,
|
||||||
this.scrollRequest = 0,
|
this.scrollRequest = 0,
|
||||||
|
|
@ -47,6 +50,15 @@ class TimelineView extends StatefulWidget {
|
||||||
/// Callback invoked when a task card status control requests completion.
|
/// Callback invoked when a task card status control requests completion.
|
||||||
final ValueChanged<TimelineCardModel>? onCardCompleted;
|
final ValueChanged<TimelineCardModel>? onCardCompleted;
|
||||||
|
|
||||||
|
/// Callback invoked when a task card requests Push to next.
|
||||||
|
final Future<void> Function(TimelineCardModel card)? onPushToNext;
|
||||||
|
|
||||||
|
/// Callback invoked when a task card requests Push to tomorrow.
|
||||||
|
final Future<void> Function(TimelineCardModel card)? onPushToTomorrow;
|
||||||
|
|
||||||
|
/// Callback invoked when a task card requests Push to backlog.
|
||||||
|
final Future<void> Function(TimelineCardModel card)? onPushToBacklog;
|
||||||
|
|
||||||
/// Clock used to center the initial scroll position.
|
/// Clock used to center the initial scroll position.
|
||||||
final DateTime Function()? now;
|
final DateTime Function()? now;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,9 @@ class _TimelineViewState extends State<TimelineView> {
|
||||||
: () => widget.onCardCompleted!(
|
: () => widget.onCardCompleted!(
|
||||||
placement.card,
|
placement.card,
|
||||||
),
|
),
|
||||||
|
onPushToNext: widget.onPushToNext,
|
||||||
|
onPushToTomorrow: widget.onPushToTomorrow,
|
||||||
|
onPushToBacklog: widget.onPushToBacklog,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
/// Tests Widget behavior in the FocusFlow Flutter app.
|
/// Tests Widget behavior in the FocusFlow Flutter app.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
@ -110,7 +112,7 @@ void main() {
|
||||||
expect(find.text('Reward level 2'), findsOneWidget);
|
expect(find.text('Reward level 2'), findsOneWidget);
|
||||||
expect(find.text('Medium effort'), findsOneWidget);
|
expect(find.text('Medium effort'), findsOneWidget);
|
||||||
expect(find.text('Done'), findsOneWidget);
|
expect(find.text('Done'), findsOneWidget);
|
||||||
expect(find.text('Push'), findsOneWidget);
|
expect(find.text('Push'), findsWidgets);
|
||||||
expect(find.text('Move to Backlog'), findsOneWidget);
|
expect(find.text('Move to Backlog'), findsOneWidget);
|
||||||
expect(find.text('Break up'), findsOneWidget);
|
expect(find.text('Break up'), findsOneWidget);
|
||||||
|
|
||||||
|
|
@ -546,6 +548,185 @@ void main() {
|
||||||
await gesture.removePointer();
|
await gesture.removePointer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('past incomplete flexible card renders Push', (tester) async {
|
||||||
|
await _pumpTimelineCard(
|
||||||
|
tester,
|
||||||
|
card: _timelineCard(
|
||||||
|
id: 'past-flexible',
|
||||||
|
title: 'Past flexible task',
|
||||||
|
showPastTaskPushButton: true,
|
||||||
|
),
|
||||||
|
size: const Size(360, 88),
|
||||||
|
onPushToNext: (_) async {},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Push'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.byKey(const ValueKey('past-flexible-push-button')),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('completed and future cards do not render Push', (tester) async {
|
||||||
|
await _pumpTimelineCard(
|
||||||
|
tester,
|
||||||
|
card: _timelineCard(
|
||||||
|
id: 'completed-flexible',
|
||||||
|
title: 'Completed flexible task',
|
||||||
|
isCompleted: true,
|
||||||
|
showsQuickActions: false,
|
||||||
|
),
|
||||||
|
size: const Size(360, 88),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Push'), findsNothing);
|
||||||
|
|
||||||
|
await _pumpTimelineCard(
|
||||||
|
tester,
|
||||||
|
card: _timelineCard(id: 'future-flexible', title: 'Future flexible task'),
|
||||||
|
size: const Size(360, 88),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Push'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('past incomplete flexible card suppresses hover quick actions', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await _pumpTimelineCard(
|
||||||
|
tester,
|
||||||
|
card: _timelineCard(
|
||||||
|
id: 'past-no-hover',
|
||||||
|
title: 'Past task',
|
||||||
|
showPastTaskPushButton: true,
|
||||||
|
),
|
||||||
|
size: const Size(360, 88),
|
||||||
|
onPushToNext: (_) async {},
|
||||||
|
);
|
||||||
|
|
||||||
|
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
|
await gesture.addPointer(
|
||||||
|
location: tester.getCenter(find.byKey(const ValueKey('past-no-hover'))),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AnimatedOpacity), findsNothing);
|
||||||
|
expect(find.byIcon(Icons.arrow_forward), findsNothing);
|
||||||
|
|
||||||
|
await gesture.removePointer();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Push menu exposes destination labels and callbacks', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
var nextCount = 0;
|
||||||
|
var tomorrowCount = 0;
|
||||||
|
var backlogCount = 0;
|
||||||
|
|
||||||
|
Future<void> openMenu() async {
|
||||||
|
await tester.tap(find.byKey(const ValueKey('push-menu-push-button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _pumpTimelineCard(
|
||||||
|
tester,
|
||||||
|
card: _timelineCard(
|
||||||
|
id: 'push-menu',
|
||||||
|
title: 'Past task',
|
||||||
|
showPastTaskPushButton: true,
|
||||||
|
),
|
||||||
|
size: const Size(420, 88),
|
||||||
|
onPushToNext: (_) async {
|
||||||
|
nextCount += 1;
|
||||||
|
},
|
||||||
|
onPushToTomorrow: (_) async {
|
||||||
|
tomorrowCount += 1;
|
||||||
|
},
|
||||||
|
onPushToBacklog: (_) async {
|
||||||
|
backlogCount += 1;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await openMenu();
|
||||||
|
|
||||||
|
expect(find.text('Push to next'), findsOneWidget);
|
||||||
|
expect(find.text('Push to tomorrow'), findsOneWidget);
|
||||||
|
expect(find.text('Push to backlog'), findsOneWidget);
|
||||||
|
expect(find.textContaining('Push to '), findsNWidgets(3));
|
||||||
|
|
||||||
|
await tester.tap(find.text('Push to next'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(nextCount, 1);
|
||||||
|
|
||||||
|
await openMenu();
|
||||||
|
await tester.tap(find.text('Push to tomorrow'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tomorrowCount, 1);
|
||||||
|
|
||||||
|
await openMenu();
|
||||||
|
await tester.tap(find.text('Push to backlog'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(backlogCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Push menu dismisses on outside click without callback', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
var count = 0;
|
||||||
|
await _pumpTimelineCard(
|
||||||
|
tester,
|
||||||
|
card: _timelineCard(
|
||||||
|
id: 'push-dismiss',
|
||||||
|
title: 'Past task',
|
||||||
|
showPastTaskPushButton: true,
|
||||||
|
),
|
||||||
|
size: const Size(420, 88),
|
||||||
|
onPushToNext: (_) async {
|
||||||
|
count += 1;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const ValueKey('push-dismiss-push-button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tapAt(Offset.zero);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Push to next'), findsNothing);
|
||||||
|
expect(count, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Push selection is one shot while command is running', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final completer = Completer<void>();
|
||||||
|
var count = 0;
|
||||||
|
await _pumpTimelineCard(
|
||||||
|
tester,
|
||||||
|
card: _timelineCard(
|
||||||
|
id: 'push-running',
|
||||||
|
title: 'Past task',
|
||||||
|
showPastTaskPushButton: true,
|
||||||
|
),
|
||||||
|
size: const Size(420, 88),
|
||||||
|
onPushToNext: (_) async {
|
||||||
|
count += 1;
|
||||||
|
await completer.future;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const ValueKey('push-running-push-button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Push to next'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.byKey(const ValueKey('push-running-push-button')));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(count, 1);
|
||||||
|
|
||||||
|
completer.complete();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('short task card keeps time inline with title', (tester) async {
|
testWidgets('short task card keeps time inline with title', (tester) async {
|
||||||
await _pumpTimelineCard(
|
await _pumpTimelineCard(
|
||||||
tester,
|
tester,
|
||||||
|
|
@ -745,8 +926,13 @@ Future<void> _pumpTimelineCard(
|
||||||
WidgetTester tester, {
|
WidgetTester tester, {
|
||||||
required TimelineCardModel card,
|
required TimelineCardModel card,
|
||||||
required Size size,
|
required Size size,
|
||||||
|
Future<void> Function(TimelineCardModel card)? onPushToNext,
|
||||||
|
Future<void> Function(TimelineCardModel card)? onPushToTomorrow,
|
||||||
|
Future<void> Function(TimelineCardModel card)? onPushToBacklog,
|
||||||
}) async {
|
}) async {
|
||||||
await tester.binding.setSurfaceSize(Size(size.width + 40, size.height + 40));
|
await tester.binding.setSurfaceSize(
|
||||||
|
Size(size.width + 120, size.height + 220),
|
||||||
|
);
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
|
|
@ -759,6 +945,9 @@ Future<void> _pumpTimelineCard(
|
||||||
card: card,
|
card: card,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
onStatusPressed: () {},
|
onStatusPressed: () {},
|
||||||
|
onPushToNext: onPushToNext,
|
||||||
|
onPushToTomorrow: onPushToTomorrow,
|
||||||
|
onPushToBacklog: onPushToBacklog,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -802,6 +991,8 @@ TimelineCardModel _timelineCard({
|
||||||
TaskVisualKind visualKind = TaskVisualKind.flexible,
|
TaskVisualKind visualKind = TaskVisualKind.flexible,
|
||||||
TaskType? taskType,
|
TaskType? taskType,
|
||||||
bool showsQuickActions = true,
|
bool showsQuickActions = true,
|
||||||
|
bool showPastTaskPushButton = false,
|
||||||
|
bool isCompleted = false,
|
||||||
String? completedTimeText,
|
String? completedTimeText,
|
||||||
}) {
|
}) {
|
||||||
final resolvedTaskType =
|
final resolvedTaskType =
|
||||||
|
|
@ -826,9 +1017,10 @@ TimelineCardModel _timelineCard({
|
||||||
rewardIconToken: TimelineRewardIconToken.medium,
|
rewardIconToken: TimelineRewardIconToken.medium,
|
||||||
difficultyIconToken: TimelineDifficultyIconToken.medium,
|
difficultyIconToken: TimelineDifficultyIconToken.medium,
|
||||||
completedTimeText: completedTimeText,
|
completedTimeText: completedTimeText,
|
||||||
isCompleted: false,
|
isCompleted: isCompleted,
|
||||||
isSelectable: true,
|
isSelectable: true,
|
||||||
showsQuickActions: showsQuickActions,
|
showsQuickActions: showPastTaskPushButton ? false : showsQuickActions,
|
||||||
|
showPastTaskPushButton: showPastTaskPushButton,
|
||||||
durationMinutes: endMinutes - startMinutes,
|
durationMinutes: endMinutes - startMinutes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue