forked from eva/focus-flow
feat: add past task push presentation state
This commit is contained in:
parent
d4c85494c0
commit
f3795e8be1
11 changed files with 232 additions and 9 deletions
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
# Task Pushing 01 Block 02 — Past Task Read Model
|
# Task Pushing 01 Block 02 — Past Task Read Model
|
||||||
|
|
||||||
**Status:** Planned.
|
**Status:** Complete.
|
||||||
**Goal:** Add presentation-model state that tells widgets when to show the full
|
**Goal:** Add presentation-model state that tells widgets when to show the full
|
||||||
Push button.
|
Push button.
|
||||||
|
|
||||||
|
|
@ -15,6 +15,17 @@ Push button.
|
||||||
After this block, `TimelineCardModel` or its successor exposes a clear push-state
|
After this block, `TimelineCardModel` or its successor exposes a clear push-state
|
||||||
field. Leaf widgets should not recompute past/incomplete scheduling conditions.
|
field. Leaf widgets should not recompute past/incomplete scheduling conditions.
|
||||||
|
|
||||||
|
## Implementation notes
|
||||||
|
|
||||||
|
1. `TimelineItem` carries the source task `updatedAt` timestamp for stale-write
|
||||||
|
protection.
|
||||||
|
2. `TodayScreenData` and `TimelineCardModel` receive an injected clock for
|
||||||
|
deterministic past/incomplete detection.
|
||||||
|
3. `TimelineCardModel.showPastTaskPushButton` identifies past incomplete planned
|
||||||
|
flexible cards and suppresses hover quick actions while true.
|
||||||
|
4. Mapper tests cover earlier today, previous day, future day, completed,
|
||||||
|
unsupported task types, and missing schedule placement.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chunk 2.1 — Add push-state presentation fields
|
## Chunk 2.1 — Add push-state presentation fields
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,9 @@ class TodayScreenController extends ChangeNotifier {
|
||||||
required this.read,
|
required this.read,
|
||||||
required this.selectedDates,
|
required this.selectedDates,
|
||||||
required this.dateLabelFor,
|
required this.dateLabelFor,
|
||||||
}) : _state = TodayScreenLoading(dateLabelFor(selectedDates.date)) {
|
DateTime Function()? now,
|
||||||
|
}) : now = now ?? _utcNow,
|
||||||
|
_state = TodayScreenLoading(dateLabelFor(selectedDates.date)) {
|
||||||
selectedDates.addListener(_handleSelectedDateChanged);
|
selectedDates.addListener(_handleSelectedDateChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,6 +86,9 @@ class TodayScreenController extends ChangeNotifier {
|
||||||
/// Formats dates before a backend read has returned screen data.
|
/// Formats dates before a backend read has returned screen data.
|
||||||
final TodayDateLabelFormatter dateLabelFor;
|
final TodayDateLabelFormatter dateLabelFor;
|
||||||
|
|
||||||
|
/// Clock used while mapping time-sensitive presentation state.
|
||||||
|
final DateTime Function() now;
|
||||||
|
|
||||||
/// Private state stored as `_state` for this implementation.
|
/// Private state stored as `_state` for this implementation.
|
||||||
/// 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.
|
||||||
TodayScreenState _state;
|
TodayScreenState _state;
|
||||||
|
|
@ -130,7 +135,10 @@ class TodayScreenController extends ChangeNotifier {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = TodayScreenData.fromTodayState(result.requireValue);
|
final data = TodayScreenData.fromTodayState(
|
||||||
|
result.requireValue,
|
||||||
|
now: now(),
|
||||||
|
);
|
||||||
_syncSelectedCard(data);
|
_syncSelectedCard(data);
|
||||||
_state = data.cards.isEmpty
|
_state = data.cards.isEmpty
|
||||||
? TodayScreenEmpty(data)
|
? TodayScreenEmpty(data)
|
||||||
|
|
@ -215,4 +223,8 @@ class TodayScreenController extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
_selectedCard = null;
|
_selectedCard = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
static DateTime _utcNow() => DateTime.now().toUtc();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,23 @@ class TimelineCardModel {
|
||||||
required this.isCompleted,
|
required this.isCompleted,
|
||||||
required this.isSelectable,
|
required this.isSelectable,
|
||||||
required this.showsQuickActions,
|
required this.showsQuickActions,
|
||||||
|
this.showPastTaskPushButton = false,
|
||||||
this.completionDateContextRequired = false,
|
this.completionDateContextRequired = false,
|
||||||
this.completedTimeText,
|
this.completedTimeText,
|
||||||
|
this.expectedUpdatedAt,
|
||||||
this.durationMinutes,
|
this.durationMinutes,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Maps a scheduler timeline item into card presentation data.
|
/// Maps a scheduler timeline item into card presentation data.
|
||||||
factory TimelineCardModel.fromItem(TodayTimelineItem item) {
|
factory TimelineCardModel.fromItem(
|
||||||
|
TodayTimelineItem item, {
|
||||||
|
required DateTime now,
|
||||||
|
}) {
|
||||||
final start = item.start;
|
final start = item.start;
|
||||||
final end = item.end;
|
final end = item.end;
|
||||||
final visualKind = _visualKindFor(item);
|
final visualKind = _visualKindFor(item);
|
||||||
final isCompleted = item.taskStatus == TaskStatus.completed;
|
final isCompleted = item.taskStatus == TaskStatus.completed;
|
||||||
|
final showPastTaskPushButton = _showPastTaskPushButton(item, now: now);
|
||||||
final title = item.item.displayTitle;
|
final title = item.item.displayTitle;
|
||||||
final duration = item.item.durationMinutes;
|
final duration = item.item.durationMinutes;
|
||||||
final timeText = start == null || end == null
|
final timeText = start == null || end == null
|
||||||
|
|
@ -66,9 +72,14 @@ class TimelineCardModel {
|
||||||
includeDate: completionDateContextRequired,
|
includeDate: completionDateContextRequired,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
expectedUpdatedAt: item.item.updatedAt,
|
||||||
isCompleted: isCompleted,
|
isCompleted: isCompleted,
|
||||||
isSelectable: true,
|
isSelectable: true,
|
||||||
showsQuickActions: !isCompleted && visualKind != TaskVisualKind.freeSlot,
|
showPastTaskPushButton: showPastTaskPushButton,
|
||||||
|
showsQuickActions:
|
||||||
|
!showPastTaskPushButton &&
|
||||||
|
!isCompleted &&
|
||||||
|
visualKind != TaskVisualKind.freeSlot,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +101,9 @@ class TimelineCardModel {
|
||||||
/// Display-ready completion time or date-time, if the task has been completed.
|
/// Display-ready completion time or date-time, if the task has been completed.
|
||||||
final String? completedTimeText;
|
final String? completedTimeText;
|
||||||
|
|
||||||
|
/// Expected update timestamp for stale-write protection.
|
||||||
|
final DateTime? expectedUpdatedAt;
|
||||||
|
|
||||||
/// Whether completion text includes date context because completion crossed dates.
|
/// Whether completion text includes date context because completion crossed dates.
|
||||||
final bool completionDateContextRequired;
|
final bool completionDateContextRequired;
|
||||||
|
|
||||||
|
|
@ -123,6 +137,12 @@ class TimelineCardModel {
|
||||||
/// Whether the card should show compact quick-action controls.
|
/// Whether the card should show compact quick-action controls.
|
||||||
final bool showsQuickActions;
|
final bool showsQuickActions;
|
||||||
|
|
||||||
|
/// Whether the card should show the full overdue-task push control.
|
||||||
|
final bool showPastTaskPushButton;
|
||||||
|
|
||||||
|
/// Accessible label for the full overdue-task push control.
|
||||||
|
String get pastTaskPushSemanticLabel => 'Push overdue task';
|
||||||
|
|
||||||
/// Whether the left status control can toggle this task's completion.
|
/// Whether the left status control can toggle this task's completion.
|
||||||
bool get canToggleCompletion {
|
bool get canToggleCompletion {
|
||||||
return switch (taskType) {
|
return switch (taskType) {
|
||||||
|
|
|
||||||
|
|
@ -101,3 +101,19 @@ bool _completionDateContextRequired(TodayTimelineItem item) {
|
||||||
actualStartDate != null && actualStartDate != scheduledDate ||
|
actualStartDate != null && actualStartDate != scheduledDate ||
|
||||||
actualEndDate != null && actualEndDate != scheduledDate;
|
actualEndDate != null && actualEndDate != scheduledDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Top-level helper that performs the `_showPastTaskPushButton` operation for this file.
|
||||||
|
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
|
||||||
|
bool _showPastTaskPushButton(TodayTimelineItem item, {required DateTime now}) {
|
||||||
|
if (item.source != TodayTimelineItemSource.task) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (item.taskType != TaskType.flexible) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (item.taskStatus != TaskStatus.planned) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final end = item.end;
|
||||||
|
return end != null && end.isBefore(now);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,13 @@ class TodayScreenData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps scheduler core [TodayState] into UI presentation data.
|
/// Maps scheduler core [TodayState] into UI presentation data.
|
||||||
factory TodayScreenData.fromTodayState(TodayState state) {
|
factory TodayScreenData.fromTodayState(
|
||||||
|
TodayState state, {
|
||||||
|
required DateTime now,
|
||||||
|
}) {
|
||||||
final cards = [
|
final cards = [
|
||||||
for (final item in state.timelineItems) TimelineCardModel.fromItem(item),
|
for (final item in state.timelineItems)
|
||||||
|
TimelineCardModel.fromItem(item, now: now),
|
||||||
];
|
];
|
||||||
final nextRequired = state.nextRequiredItem;
|
final nextRequired = state.nextRequiredItem;
|
||||||
return TodayScreenData(
|
return TodayScreenData(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ void main() {
|
||||||
scheduledEnd: DateTime.utc(2026, 1, 3, 9, 30),
|
scheduledEnd: DateTime.utc(2026, 1, 3, 9, 30),
|
||||||
completedAt: DateTime.utc(2026, 1, 3, 9, 25),
|
completedAt: DateTime.utc(2026, 1, 3, 9, 25),
|
||||||
),
|
),
|
||||||
|
now: DateTime.utc(2026, 1, 3, 10),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(card.completionDateContextRequired, isFalse);
|
expect(card.completionDateContextRequired, isFalse);
|
||||||
|
|
@ -32,6 +33,7 @@ void main() {
|
||||||
scheduledEnd: DateTime.utc(2026, 1, 3, 9, 30),
|
scheduledEnd: DateTime.utc(2026, 1, 3, 9, 30),
|
||||||
completedAt: DateTime.utc(2026, 1, 4, 8, 5),
|
completedAt: DateTime.utc(2026, 1, 4, 8, 5),
|
||||||
),
|
),
|
||||||
|
now: DateTime.utc(2026, 1, 4, 9),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(card.completionDateContextRequired, isTrue);
|
expect(card.completionDateContextRequired, isTrue);
|
||||||
|
|
@ -48,6 +50,7 @@ void main() {
|
||||||
actualStart: DateTime.utc(2026, 1, 4, 0, 10),
|
actualStart: DateTime.utc(2026, 1, 4, 0, 10),
|
||||||
actualEnd: DateTime.utc(2026, 1, 4, 0, 20),
|
actualEnd: DateTime.utc(2026, 1, 4, 0, 20),
|
||||||
),
|
),
|
||||||
|
now: DateTime.utc(2026, 1, 4, 1),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(card.completionDateContextRequired, isTrue);
|
expect(card.completionDateContextRequired, isTrue);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/// Tests past-task push presentation mapping.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
|
import 'package:focus_flow_flutter/models/today_screen_models.dart';
|
||||||
|
|
||||||
|
/// Runs past-task push presentation tests.
|
||||||
|
void main() {
|
||||||
|
final now = DateTime.utc(2026, 1, 3, 12);
|
||||||
|
|
||||||
|
test('planned flexible task earlier today shows Push state', () {
|
||||||
|
final updatedAt = DateTime.utc(2026, 1, 3, 8, 45);
|
||||||
|
final card = TimelineCardModel.fromItem(
|
||||||
|
_item(
|
||||||
|
start: DateTime.utc(2026, 1, 3, 9),
|
||||||
|
end: DateTime.utc(2026, 1, 3, 9, 30),
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
),
|
||||||
|
now: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(card.showPastTaskPushButton, isTrue);
|
||||||
|
expect(card.showsQuickActions, isFalse);
|
||||||
|
expect(card.expectedUpdatedAt, updatedAt);
|
||||||
|
expect(card.pastTaskPushSemanticLabel, 'Push overdue task');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('planned flexible task later today does not show Push state', () {
|
||||||
|
final card = TimelineCardModel.fromItem(
|
||||||
|
_item(
|
||||||
|
start: DateTime.utc(2026, 1, 3, 14),
|
||||||
|
end: DateTime.utc(2026, 1, 3, 14, 30),
|
||||||
|
),
|
||||||
|
now: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(card.showPastTaskPushButton, isFalse);
|
||||||
|
expect(card.showsQuickActions, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('planned flexible task on a previous date shows Push state', () {
|
||||||
|
final card = TimelineCardModel.fromItem(
|
||||||
|
_item(
|
||||||
|
start: DateTime.utc(2026, 1, 2, 14),
|
||||||
|
end: DateTime.utc(2026, 1, 2, 14, 30),
|
||||||
|
),
|
||||||
|
now: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(card.showPastTaskPushButton, isTrue);
|
||||||
|
expect(card.showsQuickActions, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('planned flexible task on a future date does not show Push state', () {
|
||||||
|
final card = TimelineCardModel.fromItem(
|
||||||
|
_item(
|
||||||
|
start: DateTime.utc(2026, 1, 4, 9),
|
||||||
|
end: DateTime.utc(2026, 1, 4, 9, 30),
|
||||||
|
),
|
||||||
|
now: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(card.showPastTaskPushButton, isFalse);
|
||||||
|
expect(card.showsQuickActions, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('completed flexible task in the past does not show Push state', () {
|
||||||
|
final card = TimelineCardModel.fromItem(
|
||||||
|
_item(
|
||||||
|
start: DateTime.utc(2026, 1, 3, 9),
|
||||||
|
end: DateTime.utc(2026, 1, 3, 9, 30),
|
||||||
|
status: TaskStatus.completed,
|
||||||
|
),
|
||||||
|
now: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(card.showPastTaskPushButton, isFalse);
|
||||||
|
expect(card.showsQuickActions, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unsupported task types do not show Push state', () {
|
||||||
|
for (final type in [
|
||||||
|
TaskType.critical,
|
||||||
|
TaskType.inflexible,
|
||||||
|
TaskType.locked,
|
||||||
|
TaskType.surprise,
|
||||||
|
TaskType.freeSlot,
|
||||||
|
]) {
|
||||||
|
final card = TimelineCardModel.fromItem(
|
||||||
|
_item(
|
||||||
|
start: DateTime.utc(2026, 1, 3, 9),
|
||||||
|
end: DateTime.utc(2026, 1, 3, 9, 30),
|
||||||
|
type: type,
|
||||||
|
),
|
||||||
|
now: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(card.showPastTaskPushButton, isFalse, reason: type.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('task without schedule placement does not show Push state', () {
|
||||||
|
final card = TimelineCardModel.fromItem(_item(), now: now);
|
||||||
|
|
||||||
|
expect(card.showPastTaskPushButton, isFalse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a timeline item for push-state presentation tests.
|
||||||
|
TodayTimelineItem _item({
|
||||||
|
DateTime? start,
|
||||||
|
DateTime? end,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
TaskType type = TaskType.flexible,
|
||||||
|
TaskStatus status = TaskStatus.planned,
|
||||||
|
}) {
|
||||||
|
return TodayTimelineItem(
|
||||||
|
item: TimelineItem(
|
||||||
|
id: 'task-${type.name}-${status.name}',
|
||||||
|
displayTitle: 'Task',
|
||||||
|
taskType: type,
|
||||||
|
projectColorToken: 'project-home',
|
||||||
|
backgroundToken: TimelineBackgroundToken.flexible,
|
||||||
|
rewardIconToken: TimelineRewardIconToken.medium,
|
||||||
|
difficultyIconToken: TimelineDifficultyIconToken.medium,
|
||||||
|
showsExplicitTime: true,
|
||||||
|
quickActions: const [],
|
||||||
|
category: TimelineItemCategory.taskCard,
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
durationMinutes: start == null || end == null
|
||||||
|
? null
|
||||||
|
: end.difference(start).inMinutes,
|
||||||
|
),
|
||||||
|
source: TodayTimelineItemSource.task,
|
||||||
|
taskStatus: status,
|
||||||
|
taskId: 'task-${type.name}-${status.name}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -169,6 +169,7 @@ void main() {
|
||||||
expect(completed.item.completedAt, isNotNull);
|
expect(completed.item.completedAt, isNotNull);
|
||||||
final completedData = TodayScreenData.fromTodayState(
|
final completedData = TodayScreenData.fromTodayState(
|
||||||
await _readToday(tester, composition, tomorrow),
|
await _readToday(tester, composition, tomorrow),
|
||||||
|
now: DateTime.utc(2026, 1, 4, 8, 20),
|
||||||
);
|
);
|
||||||
final completedCard = completedData.cards.singleWhere(
|
final completedCard = completedData.cards.singleWhere(
|
||||||
(card) => card.title == title,
|
(card) => card.title == title,
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,10 @@ void main() {
|
||||||
'visual token adapter maps seeded items to expected visual kinds',
|
'visual token adapter maps seeded items to expected visual kinds',
|
||||||
() async {
|
() async {
|
||||||
final state = await _readSeededToday();
|
final state = await _readSeededToday();
|
||||||
final data = TodayScreenData.fromTodayState(state);
|
final data = TodayScreenData.fromTodayState(
|
||||||
|
state,
|
||||||
|
now: DateTime.utc(2025, 5, 20, 15, 20),
|
||||||
|
);
|
||||||
|
|
||||||
expect(_kindFor(data, 'clean-coffee-maker'), TaskVisualKind.flexible);
|
expect(_kindFor(data, 'clean-coffee-maker'), TaskVisualKind.flexible);
|
||||||
expect(_kindFor(data, 'sort-mail'), TaskVisualKind.flexible);
|
expect(_kindFor(data, 'sort-mail'), TaskVisualKind.flexible);
|
||||||
|
|
@ -357,7 +360,10 @@ void main() {
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final state = await _readSeededToday();
|
final state = await _readSeededToday();
|
||||||
final data = TodayScreenData.fromTodayState(state);
|
final data = TodayScreenData.fromTodayState(
|
||||||
|
state,
|
||||||
|
now: DateTime.utc(2025, 5, 20, 15, 20),
|
||||||
|
);
|
||||||
|
|
||||||
await _pumpConstrainedWidget(
|
await _pumpConstrainedWidget(
|
||||||
tester,
|
tester,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class TimelineItemMapper {
|
||||||
actualStart: task.actualStart,
|
actualStart: task.actualStart,
|
||||||
actualEnd: task.actualEnd,
|
actualEnd: task.actualEnd,
|
||||||
completedAt: task.completedAt,
|
completedAt: task.completedAt,
|
||||||
|
updatedAt: task.updatedAt,
|
||||||
durationMinutes: _durationMinutesFor(task),
|
durationMinutes: _durationMinutesFor(task),
|
||||||
quickActions: _quickActionsFor(task.type),
|
quickActions: _quickActionsFor(task.type),
|
||||||
category: task.isLocked
|
category: task.isLocked
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class TimelineItem {
|
||||||
this.actualStart,
|
this.actualStart,
|
||||||
this.actualEnd,
|
this.actualEnd,
|
||||||
this.completedAt,
|
this.completedAt,
|
||||||
|
this.updatedAt,
|
||||||
this.durationMinutes,
|
this.durationMinutes,
|
||||||
}) : quickActions = List<TimelineQuickAction>.unmodifiable(quickActions);
|
}) : quickActions = List<TimelineQuickAction>.unmodifiable(quickActions);
|
||||||
|
|
||||||
|
|
@ -65,6 +66,9 @@ class TimelineItem {
|
||||||
/// Completion instant, if the source task has been completed.
|
/// Completion instant, if the source task has been completed.
|
||||||
final DateTime? completedAt;
|
final DateTime? completedAt;
|
||||||
|
|
||||||
|
/// Source task update timestamp, when the item was mapped from a task.
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
/// Duration display value for flexible task cards.
|
/// Duration display value for flexible task cards.
|
||||||
final int? durationMinutes;
|
final int? durationMinutes;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue