add and edit
This commit is contained in:
@@ -24,7 +24,7 @@ interface ItemForLane {
|
||||
}
|
||||
|
||||
function assignLane(
|
||||
existing: ItemForLane[],
|
||||
existing: Array<ItemForLane>,
|
||||
newStart: Date,
|
||||
newEnd: Date | null
|
||||
): number {
|
||||
@@ -57,7 +57,7 @@ async function main() {
|
||||
|
||||
if (items.length === 0) continue;
|
||||
|
||||
const assigned: ItemForLane[] = [];
|
||||
const assigned: Array<ItemForLane> = [];
|
||||
let updated = 0;
|
||||
|
||||
for (const item of items) {
|
||||
|
||||
@@ -125,6 +125,14 @@ class _MainAppState extends State<MainApp> {
|
||||
return (start: earliest.subtract(padding), end: latest.add(padding));
|
||||
}
|
||||
|
||||
void _onEntrySelected(TimelineEntry entry) {
|
||||
emitEvent('item_selected', {'itemId': entry.id});
|
||||
}
|
||||
|
||||
void _onBackgroundTap() {
|
||||
emitEvent('item_deselected');
|
||||
}
|
||||
|
||||
void _onEntryMoved(
|
||||
TimelineEntry entry,
|
||||
DateTime newStart,
|
||||
@@ -346,6 +354,7 @@ class _MainAppState extends State<MainApp> {
|
||||
const ZTimelineTieredHeader(),
|
||||
Expanded(
|
||||
child: ZTimelineInteractor(
|
||||
onBackgroundTap: _onBackgroundTap,
|
||||
child: ZTimelineView(
|
||||
groups: _groups,
|
||||
entries: _entries,
|
||||
@@ -355,6 +364,8 @@ class _MainAppState extends State<MainApp> {
|
||||
enableDrag: true,
|
||||
onEntryMoved: _onEntryMoved,
|
||||
onEntryResized: _onEntryResized,
|
||||
onEntrySelected: _onEntrySelected,
|
||||
selectedEntryId: _state?.selectedItemId,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,10 +5,16 @@ import '../constants.dart';
|
||||
/// A standalone event pill widget with entry color, rounded corners,
|
||||
/// and auto text color based on brightness.
|
||||
class EventPill extends StatelessWidget {
|
||||
const EventPill({required this.color, required this.label, super.key});
|
||||
const EventPill({
|
||||
required this.color,
|
||||
required this.label,
|
||||
this.isSelected = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -17,6 +23,8 @@ class EventPill extends StatelessWidget {
|
||||
? Colors.white
|
||||
: Colors.black87;
|
||||
|
||||
final primaryColor = Theme.of(context).colorScheme.primary;
|
||||
|
||||
return Container(
|
||||
padding: ZTimelineConstants.pillPadding,
|
||||
decoration: BoxDecoration(
|
||||
@@ -24,6 +32,9 @@ class EventPill extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(
|
||||
ZTimelineConstants.pillBorderRadius,
|
||||
),
|
||||
border: isSelected
|
||||
? Border.all(color: primaryColor, width: 2)
|
||||
: null,
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
|
||||
@@ -10,11 +10,13 @@ class EventPoint extends StatelessWidget {
|
||||
required this.color,
|
||||
required this.label,
|
||||
this.maxTextWidth,
|
||||
this.isSelected = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
|
||||
/// Maximum width for the label text. When provided, text truncates with
|
||||
/// ellipsis. When null the text sizes naturally.
|
||||
@@ -49,6 +51,12 @@ class EventPoint extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: isSelected
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (showText) ...[
|
||||
|
||||
@@ -31,6 +31,8 @@ class InteractiveEventPill extends StatefulWidget {
|
||||
this.allEntries = const [],
|
||||
this.onEntryResized,
|
||||
this.onEntryMoved,
|
||||
this.onEntrySelected,
|
||||
this.selectedEntryId,
|
||||
this.groupRegistry,
|
||||
this.nextEntryStartX,
|
||||
super.key,
|
||||
@@ -46,6 +48,8 @@ class InteractiveEventPill extends StatefulWidget {
|
||||
final List<TimelineEntry> allEntries;
|
||||
final OnEntryResized? onEntryResized;
|
||||
final OnEntryMoved? onEntryMoved;
|
||||
final OnEntrySelected? onEntrySelected;
|
||||
final String? selectedEntryId;
|
||||
final TimelineGroupRegistry? groupRegistry;
|
||||
|
||||
/// Normalized startX of the next entry in the same lane, used to compute
|
||||
@@ -285,6 +289,7 @@ class _InteractiveEventPillState extends State<InteractiveEventPill> {
|
||||
final isPoint = !widget.entry.entry.hasEnd;
|
||||
final color = widget.colorBuilder(widget.entry.entry);
|
||||
final label = widget.labelBuilder(widget.entry.entry);
|
||||
final isSelected = widget.entry.entry.id == widget.selectedEntryId;
|
||||
|
||||
final top = LayoutCoordinateService.laneToY(
|
||||
lane: widget.entry.entry.lane,
|
||||
@@ -312,6 +317,7 @@ class _InteractiveEventPillState extends State<InteractiveEventPill> {
|
||||
color: color,
|
||||
label: label,
|
||||
maxTextWidth: maxTextWidth > 0 ? maxTextWidth : 0,
|
||||
isSelected: isSelected,
|
||||
);
|
||||
|
||||
if (!widget.enableDrag) {
|
||||
@@ -355,7 +361,7 @@ class _InteractiveEventPillState extends State<InteractiveEventPill> {
|
||||
}
|
||||
|
||||
// Range event (hasEnd == true)
|
||||
final pill = EventPill(color: color, label: label);
|
||||
final pill = EventPill(color: color, label: label, isSelected: isSelected);
|
||||
final width = _pillWidth;
|
||||
|
||||
if (!widget.enableDrag) {
|
||||
@@ -434,6 +440,7 @@ class _InteractiveEventPillState extends State<InteractiveEventPill> {
|
||||
onExit: (_) => _onHoverExit(),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => widget.onEntrySelected?.call(widget.entry.entry),
|
||||
onPanDown: _onPanDown,
|
||||
onPanStart: _onPanStart,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
@@ -453,6 +460,7 @@ class _InteractiveEventPillState extends State<InteractiveEventPill> {
|
||||
onExit: (_) => _onHoverExit(),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => widget.onEntrySelected?.call(widget.entry.entry),
|
||||
onPanDown: _onPanDown,
|
||||
onPanStart: _onPanStart,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
|
||||
@@ -27,6 +27,7 @@ class ZTimelineInteractor extends StatefulWidget {
|
||||
const ZTimelineInteractor({
|
||||
required this.child,
|
||||
this.autofocus = true,
|
||||
this.onBackgroundTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -36,6 +37,9 @@ class ZTimelineInteractor extends StatefulWidget {
|
||||
/// Whether to automatically focus this widget for keyboard input.
|
||||
final bool autofocus;
|
||||
|
||||
/// Called when the user taps empty space (not on a pill/entry).
|
||||
final VoidCallback? onBackgroundTap;
|
||||
|
||||
@override
|
||||
State<ZTimelineInteractor> createState() => _ZTimelineInteractorState();
|
||||
}
|
||||
@@ -231,6 +235,7 @@ class _ZTimelineInteractorState extends State<ZTimelineInteractor> {
|
||||
);
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: widget.onBackgroundTap,
|
||||
onScaleStart: _handleScaleStart,
|
||||
onScaleUpdate: _handleScaleUpdate,
|
||||
onScaleEnd: _handleScaleEnd,
|
||||
|
||||
@@ -33,6 +33,9 @@ typedef OnEntryResized =
|
||||
int newLane,
|
||||
);
|
||||
|
||||
/// Callback signature for when an entry is tapped / selected.
|
||||
typedef OnEntrySelected = void Function(TimelineEntry entry);
|
||||
|
||||
/// Base timeline view: renders groups with between-group headers and
|
||||
/// lane rows containing event pills.
|
||||
class ZTimelineView extends StatelessWidget {
|
||||
@@ -47,7 +50,9 @@ class ZTimelineView extends StatelessWidget {
|
||||
this.groupHeaderHeight = ZTimelineConstants.groupHeaderHeight,
|
||||
this.onEntryMoved,
|
||||
this.onEntryResized,
|
||||
this.onEntrySelected,
|
||||
this.enableDrag = true,
|
||||
this.selectedEntryId,
|
||||
});
|
||||
|
||||
final List<TimelineGroup> groups;
|
||||
@@ -67,9 +72,15 @@ class ZTimelineView extends StatelessWidget {
|
||||
/// Callback invoked when an entry is resized via edge drag.
|
||||
final OnEntryResized? onEntryResized;
|
||||
|
||||
/// Callback invoked when an entry is tapped / selected.
|
||||
final OnEntrySelected? onEntrySelected;
|
||||
|
||||
/// Whether drag-and-drop is enabled.
|
||||
final bool enableDrag;
|
||||
|
||||
/// ID of the currently selected entry, if any.
|
||||
final String? selectedEntryId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
@@ -112,6 +123,8 @@ class ZTimelineView extends StatelessWidget {
|
||||
enableDrag: enableDrag,
|
||||
onEntryResized: onEntryResized,
|
||||
onEntryMoved: onEntryMoved,
|
||||
onEntrySelected: onEntrySelected,
|
||||
selectedEntryId: selectedEntryId,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -171,6 +184,8 @@ class _GroupLanes extends StatefulWidget {
|
||||
required this.enableDrag,
|
||||
this.onEntryResized,
|
||||
this.onEntryMoved,
|
||||
this.onEntrySelected,
|
||||
this.selectedEntryId,
|
||||
});
|
||||
|
||||
final TimelineGroup group;
|
||||
@@ -185,6 +200,8 @@ class _GroupLanes extends StatefulWidget {
|
||||
final bool enableDrag;
|
||||
final OnEntryResized? onEntryResized;
|
||||
final OnEntryMoved? onEntryMoved;
|
||||
final OnEntrySelected? onEntrySelected;
|
||||
final String? selectedEntryId;
|
||||
|
||||
@override
|
||||
State<_GroupLanes> createState() => _GroupLanesState();
|
||||
@@ -330,6 +347,8 @@ class _GroupLanesState extends State<_GroupLanes> {
|
||||
allEntries: widget.allEntries,
|
||||
onEntryResized: widget.onEntryResized,
|
||||
onEntryMoved: widget.onEntryMoved,
|
||||
onEntrySelected: widget.onEntrySelected,
|
||||
selectedEntryId: widget.selectedEntryId,
|
||||
groupRegistry: scope?.groupRegistry,
|
||||
nextEntryStartX: _findNextEntryStartX(
|
||||
widget.entries,
|
||||
|
||||
@@ -269,9 +269,11 @@
|
||||
}
|
||||
case "item_selected":
|
||||
currentState.selectedItemId = event.payload.itemId;
|
||||
pushState(currentState);
|
||||
break;
|
||||
case "item_deselected":
|
||||
currentState.selectedItemId = null;
|
||||
pushState(currentState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user