Support delete for more objects #110
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
gtfs.zone/coloring-book#110
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Stops already support deletion (with cascade to stop_times). This feature extends the same pattern — delete button in the detail view, cascade-delete confirmation modal, atomic batch patch for undo/redo — to routes, services, agencies, and trips. Each object has a well-defined cascade chain: trips → stop_times; routes → trips → stop_times; services → trips + stop_times + calendar_dates; agencies → routes → trips → stop_times. The implementation is intentionally repetitive (no premature abstraction) to match the existing stop pattern.
Relevant Context
Existing pattern (stop deletion):
src/modules/stop-view-controller.ts(lines 87–91, 346–368) via.delete-stop-btnclass and AbortController-based event delegationhandleDeleteStop(stop_id)insrc/modules/page-content-renderer.ts(lines 860–922): queries dependents, showsshowModal()if any exist, runsdb.deleteRow()calls in order (dependents first), records singlepm.recordBatchDelete(ops, label), then navigates homeonDeleteStopis wired insrc/index.tsfrom page-content-renderer to stop-view-controllerKey utilities:
db.deleteRow(table, key)/db.deleteRows(table, keys)—src/modules/gtfs-database.tspm.recordDelete(table, id, record)/pm.recordBatchDelete(ops, label)—src/modules/patch-manager.tsshowModal({ title, body, actions, enterAction, escapeAction })—src/modules/modal-utils.tsdb.queryRows(table, filter)— used to find cascade targetsView rendering locations:
renderRoute()insrc/modules/page-content-renderer.ts(around line 482) — no separate controller, button goes directly in the rendered HTMLsrc/modules/agency-view-controller.ts— usesonDeleteAgencycallback injected via constructor depssrc/modules/service-view-controller.ts— usesonDeleteServicecallback injected via constructor depsrenderTimetableHeader()insrc/modules/timetable-renderer.ts(line 342) — delete button in each trip-header cell; handler lives insrc/modules/schedule-controller.tsCascade chains:
trip_id)route_id) → stop_times (bytrip_id)service_id) → stop_times (bytrip_id), calendar_dates (byservice_id)agency_id) → trips (byroute_id) → stop_times (bytrip_id)Gotcha:
deleteRowsbatches in 500-row chunks. Prefer it over loopingdeleteRowwhen deleting many stop_times. For the batch patch, flatten all delete ops into onerecordBatchDeletecall so undo is a single step.Navigation after delete:
navigateToHome()helper is already used inhandleDeleteStop(). Use the same for route, agency, service. For trips, stay on the current timetable view by re-rendering (call whatever refresh method scheduleController uses after a trip is modified).Phase 1: Trip deletion (from timetable)
Trips are the simplest cascade (one level: stop_times). They have no standalone detail page — deletion is triggered from the timetable column header.
timetable-renderer.tsrenderTimetableHeader(), add a small delete button inside eachtrip-header<td>, below the trip_id text:<button class="btn btn-xs btn-error btn-outline delete-trip-btn mt-1" data-trip-id="${trip_id}">Delete</button>schedule-controller.ts, add an event listener (AbortController pattern, re-added on each re-render) that delegates clicks on.delete-trip-btnto a newhandleDeleteTrip(trip_id)methodhandleDeleteTrip(trip_id)inschedule-controller.ts:stop_timesbytrip_id"X stop_times") and offer "Delete trip + X stop_times" (btn-error) or Canceldb.deleteRows('stop_times', keys)thendb.deleteRow('trips', trip_id), thenpm.recordBatchDelete([...stop_time_ops, trip_op], label)gtfsDatabase,patchManager) to schedule-controller if not already present — check constructor (both already present; addeddeleteRow/deleteRowsandrecordBatchDeleteto local interfaces)Gotcha: The timetable re-renders frequently; make sure the AbortController is properly reset on each render to avoid stale listeners accumulating.
Phase 2: Route deletion
Routes cascade to trips → stop_times. The route detail is rendered inline in
page-content-renderer.ts, so no callback indirection is needed.page-content-renderer.tsrenderRoute(), add a delete button to the route properties header area:<button class="btn btn-sm btn-error btn-outline delete-route-btn" data-route-id="${route_id}">Delete Route</button>addEventListeners()(or route-specific listener setup), wire.delete-route-btn→handleDeleteRoute(route_id)handleDeleteRoute(route_id)inpage-content-renderer.ts:tripsbyroute_idto get all tripsstop_timesbytrip_id(or query all stop_times then filter — but per-trip is fine at this scale)"X trips, Y stop_times"db.deleteRows/db.deleteRow), thenpm.recordBatchDelete(allOps, label), thennavigateToHome()[PageContentRenderer] Deleted route ${route_id} + ${n} trips + ${m} stop_timesPhase 3: Service deletion
Services cascade to trips → stop_times, and also to calendar_dates (same service_id).
service-view-controller.ts, add anonDeleteService?: (service_id: string) => voidcallback to theServiceViewControllerDepsinterface (or equivalent deps type)service-view-controller.ts:<button class="btn btn-sm btn-error btn-outline delete-service-btn" data-service-id="${service_id}">Delete Service</button>.delete-service-btnclick →dependencies.onDeleteService(service_id)inaddEventListeners()handleDeleteService(service_id)inpage-content-renderer.ts:tripsbyservice_idstop_timesbytrip_idcalendar_datesbyservice_idcalendarrecord for the service_id (for the patch inverse; may not exist for calendar_dates-only services)"X trips, Y stop_times, Z calendar_dates"pm.recordBatchDelete(allOps, label);navigateToHome()onDeleteServiceinsrc/index.tswhen constructingServiceViewController(wired inpage-content-renderer.tsconstructor, not in index.ts — ServiceViewController is managed entirely inside PageContentRenderer)Gotcha: A service may exist only in
calendar_dates(nocalendarrow). Do not try todeleteRow('calendar', id)if the record doesn't exist — check first withdb.getRow('calendar', id).Discovery:
ServiceViewControlleris instantiated insidePageContentRenderer's constructor (not inindex.ts), so theonDeleteServicecallback was wired there — noindex.tschange needed. Thecalendar_datestable uses a composite key(service_id, date)sogenerateCompositeKeyFromRecordhandles it correctly.Phase 4: Agency deletion
Agencies have the deepest cascade: agency → routes → trips → stop_times. Agency detail is rendered in
agency-view-controller.ts.onDeleteAgency?: (agency_id: string) => voidtoAgencyViewControllerDeps(or equivalent)agency-view-controller.tsagency properties header:<button class="btn btn-sm btn-error btn-outline delete-agency-btn" data-agency-id="${agency_id}">Delete Agency</button>.delete-agency-btn→dependencies.onDeleteAgency(agency_id)inaddEventListeners()handleDeleteAgency(agency_id)inpage-content-renderer.ts:routesbyagency_idtripsbyroute_idstop_timesbytrip_id"X routes, Y trips, Z stop_times"pm.recordBatchDelete(allOps, label);navigateToHome()onDeleteAgencyinpage-content-renderer.tsconstructor (AgencyViewController is managed inside PageContentRenderer, same as ServiceViewController — no index.ts change needed)Gotcha: Agencies without routes are a valid (if unusual) state — the simple-confirm modal path (no dependents) should still work.
Discovery: Like ServiceViewController,
AgencyViewControlleris also instantiated insidePageContentRenderer's constructor, soonDeleteAgencywas wired there — noindex.tschange needed. Theagency_idfor the button comes fromthis.currentAgencyIdset at render time inrenderAgencyView().Phase 5: Replace text delete buttons with trash icon
All four new delete buttons (route, service, agency, trip) still use plain text labels, while the stop delete button already uses a trash SVG icon. This phase makes them consistent by extracting the icon into a shared helper and updating all five call sites.
Shared helper location:
src/modules/modal-utils.ts— already imported by all relevant modules forshowModal, so adding a small HTML helper there avoids a new import in each file.src/modules/modal-utils.ts, export arenderTrashIcon(sizeClass = 'h-4 w-4')function that returns the trash SVG string (same path data as the existing stop button:M19 7l-.867 12.142...)src/modules/stop-view-controller.ts: remove the inline SVG from the delete button and replace with${renderTrashIcon()}(importrenderTrashIconfrommodal-utils.ts)src/modules/page-content-renderer.ts(renderRoute()): replaceDelete Routebutton text with${renderTrashIcon()}(already imports frommodal-utils.ts)src/modules/service-view-controller.ts: replaceDelete Servicebutton text with${renderTrashIcon()}(addrenderTrashIconto existingmodal-utils.tsimport)src/modules/agency-view-controller.ts: replaceDelete Agencybutton text with${renderTrashIcon()}(addrenderTrashIconto existingmodal-utils.tsimport)src/modules/timetable-renderer.ts: replaceDeletebutton text with${renderTrashIcon('h-3 w-3')}to match thebtn-xsbutton size (addrenderTrashIconto existingmodal-utils.tsimport)title="Delete"attribute to each icon-only button so it has a tooltip for accessibilityGotcha: The timetable trip header uses
btn-xs— userenderTrashIcon('h-3 w-3')there; all others usebtn-smand the defaulth-4 w-4.Original Issue
Right now it's only supported for stops in #48. Lets extend this to all simple objects