refactor(itinerary): replace anchor-based positioning with day/sortOrder
Replace complex anchor system (anchorType, anchorId, anchorDay) with simple (day: Int, sortOrder: Double) positioning for custom items. Changes: - CustomItineraryItem: Remove anchor fields, add day and sortOrder - CKModels: Add migration fallback from old CloudKit fields - ItineraryTableViewController: Add calculateSortOrder() for midpoint insertion - TripDetailView: Simplify callbacks, itinerarySections, and routeWaypoints - AddItemSheet: Take simple day parameter instead of anchor - SavedTrip: Update LocalCustomItem SwiftData model Benefits: - Items freely movable via drag-and-drop - Route waypoints follow exact visual order - Simpler mental model: position = (day, sortOrder) - Midpoint insertion allows unlimited reordering Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -119,9 +119,7 @@ struct TripDetailView: View {
|
||||
.sheet(item: $addItemAnchor) { anchor in
|
||||
AddItemSheet(
|
||||
tripId: trip.id,
|
||||
anchorDay: anchor.day,
|
||||
anchorType: anchor.type,
|
||||
anchorId: anchor.anchorId,
|
||||
day: anchor.day,
|
||||
existingItem: nil
|
||||
) { item in
|
||||
Task { await saveCustomItem(item) }
|
||||
@@ -130,9 +128,7 @@ struct TripDetailView: View {
|
||||
.sheet(item: $editingItem) { item in
|
||||
AddItemSheet(
|
||||
tripId: trip.id,
|
||||
anchorDay: item.anchorDay,
|
||||
anchorType: item.anchorType,
|
||||
anchorId: item.anchorId,
|
||||
day: item.day,
|
||||
existingItem: item
|
||||
) { updatedItem in
|
||||
Task { await saveCustomItem(updatedItem) }
|
||||
@@ -158,7 +154,7 @@ struct TripDetailView: View {
|
||||
// Recalculate routes when custom items change (mappable items affect route)
|
||||
print("🗺️ [MapUpdate] customItems changed, count: \(newItems.count)")
|
||||
for item in newItems where item.isMappable {
|
||||
print("🗺️ [MapUpdate] Mappable: \(item.title) on day \(item.anchorDay), anchor: \(item.anchorType.rawValue)")
|
||||
print("🗺️ [MapUpdate] Mappable: \(item.title) on day \(item.day), sortOrder: \(item.sortOrder)")
|
||||
}
|
||||
Task {
|
||||
updateMapRegion()
|
||||
@@ -224,10 +220,10 @@ struct TripDetailView: View {
|
||||
await saveTravelDayOverride(travelAnchorId: travelId, displayDay: newDay)
|
||||
}
|
||||
},
|
||||
onCustomItemMoved: { itemId, day, anchorType, anchorId in
|
||||
onCustomItemMoved: { itemId, day, sortOrder in
|
||||
Task { @MainActor in
|
||||
guard let item = customItems.first(where: { $0.id == itemId }) else { return }
|
||||
await moveItemToBeginning(item, toDay: day, anchorType: anchorType, anchorId: anchorId)
|
||||
await moveItem(item, toDay: day, sortOrder: sortOrder)
|
||||
}
|
||||
},
|
||||
onCustomItemTapped: { item in
|
||||
@@ -236,8 +232,8 @@ struct TripDetailView: View {
|
||||
onCustomItemDeleted: { item in
|
||||
Task { await deleteCustomItem(item) }
|
||||
},
|
||||
onAddButtonTapped: { day, anchorType, anchorId in
|
||||
addItemAnchor = AddItemAnchor(day: day, type: anchorType, anchorId: anchorId)
|
||||
onAddButtonTapped: { day in
|
||||
addItemAnchor = AddItemAnchor(day: day)
|
||||
}
|
||||
)
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
@@ -605,13 +601,13 @@ struct TripDetailView: View {
|
||||
handleCustomItemDrop(providers: providers, targetItem: item)
|
||||
}
|
||||
|
||||
case .addButton(let day, let anchorType, let anchorId):
|
||||
case .addButton(let day):
|
||||
VStack(spacing: 0) {
|
||||
if isDropTarget {
|
||||
DropTargetIndicator()
|
||||
}
|
||||
InlineAddButton {
|
||||
addItemAnchor = AddItemAnchor(day: day, type: anchorType, anchorId: anchorId)
|
||||
addItemAnchor = AddItemAnchor(day: day)
|
||||
}
|
||||
}
|
||||
.onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: Binding(
|
||||
@@ -628,7 +624,7 @@ struct TripDetailView: View {
|
||||
}
|
||||
}
|
||||
)) { providers in
|
||||
handleAddButtonDrop(providers: providers, day: day, anchorType: anchorType, anchorId: anchorId)
|
||||
handleAddButtonDrop(providers: providers, day: day)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,8 +648,12 @@ struct TripDetailView: View {
|
||||
|
||||
Task { @MainActor in
|
||||
let day = self.findDayForTravelSegment(segment)
|
||||
let stableAnchorId = self.stableTravelAnchorId(segment)
|
||||
await self.moveItemToBeginning(droppedItem, toDay: day, anchorType: .afterTravel, anchorId: stableAnchorId)
|
||||
// Place at beginning of day (sortOrder before existing items)
|
||||
let minSortOrder = self.customItems
|
||||
.filter { $0.day == day && $0.id != droppedItem.id }
|
||||
.map { $0.sortOrder }
|
||||
.min() ?? 1.0
|
||||
await self.moveItem(droppedItem, toDay: day, sortOrder: minSortOrder / 2.0)
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -676,13 +676,19 @@ struct TripDetailView: View {
|
||||
droppedItem.id != targetItem.id else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
await self.moveItem(droppedItem, toDay: targetItem.anchorDay, anchorType: targetItem.anchorType, anchorId: targetItem.anchorId, beforeItem: targetItem)
|
||||
// Place before target item using midpoint insertion
|
||||
let itemsInDay = self.customItems.filter { $0.day == targetItem.day && $0.id != droppedItem.id }
|
||||
.sorted { $0.sortOrder < $1.sortOrder }
|
||||
let targetIdx = itemsInDay.firstIndex(where: { $0.id == targetItem.id }) ?? 0
|
||||
let prevSortOrder = targetIdx > 0 ? itemsInDay[targetIdx - 1].sortOrder : 0.0
|
||||
let newSortOrder = (prevSortOrder + targetItem.sortOrder) / 2.0
|
||||
await self.moveItem(droppedItem, toDay: targetItem.day, sortOrder: newSortOrder)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func handleAddButtonDrop(providers: [NSItemProvider], day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?) -> Bool {
|
||||
private func handleAddButtonDrop(providers: [NSItemProvider], day: Int) -> Bool {
|
||||
guard let provider = providers.first, provider.canLoadObject(ofClass: NSString.self) else {
|
||||
return false
|
||||
}
|
||||
@@ -698,7 +704,12 @@ struct TripDetailView: View {
|
||||
let droppedItem = self.customItems.first(where: { $0.id == itemId }) else { return }
|
||||
|
||||
Task { @MainActor in
|
||||
await self.moveItemToBeginning(droppedItem, toDay: day, anchorType: anchorType, anchorId: anchorId)
|
||||
// Calculate sortOrder: append at end of day's items
|
||||
let maxSortOrder = self.customItems
|
||||
.filter { $0.day == day && $0.id != droppedItem.id }
|
||||
.map { $0.sortOrder }
|
||||
.max() ?? 0.0
|
||||
await self.moveItem(droppedItem, toDay: day, sortOrder: maxSortOrder + 1.0)
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -719,8 +730,8 @@ struct TripDetailView: View {
|
||||
return "travel-\(segment.fromLocation.name)-\(segment.toLocation.name)"
|
||||
case .customItem(let item):
|
||||
return "item-\(item.id.uuidString)"
|
||||
case .addButton(let day, let anchorType, _):
|
||||
return "add-\(day)-\(anchorType.rawValue)"
|
||||
case .addButton(let day):
|
||||
return "add-\(day)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,120 +758,26 @@ struct TripDetailView: View {
|
||||
return "travel:\(from)->\(to)"
|
||||
}
|
||||
|
||||
private func moveItem(_ item: CustomItineraryItem, toDay day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?, beforeItem: CustomItineraryItem? = nil) async {
|
||||
/// Move item to a new day and sortOrder position
|
||||
private func moveItem(_ item: CustomItineraryItem, toDay day: Int, sortOrder: Double) async {
|
||||
var updated = item
|
||||
updated.anchorDay = day
|
||||
updated.anchorType = anchorType
|
||||
updated.anchorId = anchorId
|
||||
updated.day = day
|
||||
updated.sortOrder = sortOrder
|
||||
updated.modifiedAt = Date()
|
||||
|
||||
// Calculate sortOrder
|
||||
let itemsAtSameAnchor = customItems.filter {
|
||||
$0.anchorDay == day &&
|
||||
$0.anchorType == anchorType &&
|
||||
$0.anchorId == anchorId &&
|
||||
$0.id != item.id
|
||||
}.sorted { $0.sortOrder < $1.sortOrder }
|
||||
|
||||
var itemsToSync: [CustomItineraryItem] = []
|
||||
|
||||
print("📍 [Move] itemsAtSameAnchor: \(itemsAtSameAnchor.map { "\($0.title) (id: \($0.id.uuidString.prefix(8)))" })")
|
||||
if let beforeItem = beforeItem {
|
||||
print("📍 [Move] beforeItem: \(beforeItem.title) (id: \(beforeItem.id.uuidString.prefix(8)))")
|
||||
}
|
||||
|
||||
if let beforeItem = beforeItem,
|
||||
let beforeIndex = itemsAtSameAnchor.firstIndex(where: { $0.id == beforeItem.id }) {
|
||||
updated.sortOrder = beforeIndex
|
||||
print("📍 [Move] Setting \(item.title) sortOrder to \(beforeIndex) (before \(beforeItem.title))")
|
||||
|
||||
// Shift other items and track them for syncing
|
||||
for i in beforeIndex..<itemsAtSameAnchor.count {
|
||||
if var shiftItem = customItems.first(where: { $0.id == itemsAtSameAnchor[i].id }) {
|
||||
shiftItem.sortOrder = i + 1
|
||||
shiftItem.modifiedAt = Date()
|
||||
print("📍 [Move] Shifting \(shiftItem.title) sortOrder to \(i + 1)")
|
||||
if let idx = customItems.firstIndex(where: { $0.id == shiftItem.id }) {
|
||||
customItems[idx] = shiftItem
|
||||
}
|
||||
itemsToSync.append(shiftItem)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updated.sortOrder = itemsAtSameAnchor.count
|
||||
print("📍 [Move] Setting \(item.title) sortOrder to \(itemsAtSameAnchor.count) (end of list)")
|
||||
}
|
||||
print("📍 [Move] Moving \(item.title) to day \(day), sortOrder: \(sortOrder)")
|
||||
|
||||
// Update local state
|
||||
if let idx = customItems.firstIndex(where: { $0.id == item.id }) {
|
||||
customItems[idx] = updated
|
||||
}
|
||||
|
||||
// Sync moved item and all shifted items to CloudKit
|
||||
do {
|
||||
_ = try await CustomItemService.shared.updateItem(updated)
|
||||
print("✅ [Move] Synced \(updated.title) to day \(day), anchor: \(anchorType.rawValue), sortOrder: \(updated.sortOrder)")
|
||||
|
||||
// Also sync shifted items
|
||||
for shiftItem in itemsToSync {
|
||||
_ = try await CustomItemService.shared.updateItem(shiftItem)
|
||||
print("✅ [Move] Synced shifted item \(shiftItem.title) with sortOrder: \(shiftItem.sortOrder)")
|
||||
}
|
||||
} catch {
|
||||
print("❌ [Move] Failed to sync: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Move item to the beginning of an anchor position (sortOrder 0), shifting existing items down
|
||||
private func moveItemToBeginning(_ item: CustomItineraryItem, toDay day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?) async {
|
||||
var updated = item
|
||||
updated.anchorDay = day
|
||||
updated.anchorType = anchorType
|
||||
updated.anchorId = anchorId
|
||||
updated.sortOrder = 0 // Insert at beginning
|
||||
updated.modifiedAt = Date()
|
||||
|
||||
// Get existing items at this anchor position (excluding the moved item)
|
||||
let existingItems = customItems.filter {
|
||||
$0.anchorDay == day &&
|
||||
$0.anchorType == anchorType &&
|
||||
$0.anchorId == anchorId &&
|
||||
$0.id != item.id
|
||||
}.sorted { $0.sortOrder < $1.sortOrder }
|
||||
|
||||
print("📍 [MoveToBeginning] Moving \(item.title) to beginning of day \(day), anchor: \(anchorType.rawValue)")
|
||||
print("📍 [MoveToBeginning] Existing items to shift: \(existingItems.map { "\($0.title) (order: \($0.sortOrder))" })")
|
||||
|
||||
// Shift all existing items down by 1
|
||||
var itemsToSync: [CustomItineraryItem] = []
|
||||
for (index, existingItem) in existingItems.enumerated() {
|
||||
if var shiftItem = customItems.first(where: { $0.id == existingItem.id }) {
|
||||
shiftItem.sortOrder = index + 1 // Shift down
|
||||
shiftItem.modifiedAt = Date()
|
||||
print("📍 [MoveToBeginning] Shifting \(shiftItem.title) sortOrder to \(index + 1)")
|
||||
if let idx = customItems.firstIndex(where: { $0.id == shiftItem.id }) {
|
||||
customItems[idx] = shiftItem
|
||||
}
|
||||
itemsToSync.append(shiftItem)
|
||||
}
|
||||
}
|
||||
|
||||
// Update local state for the moved item
|
||||
if let idx = customItems.firstIndex(where: { $0.id == item.id }) {
|
||||
customItems[idx] = updated
|
||||
}
|
||||
|
||||
// Sync to CloudKit
|
||||
do {
|
||||
_ = try await CustomItemService.shared.updateItem(updated)
|
||||
print("✅ [MoveToBeginning] Synced \(updated.title) with sortOrder: 0")
|
||||
|
||||
for shiftItem in itemsToSync {
|
||||
_ = try await CustomItemService.shared.updateItem(shiftItem)
|
||||
print("✅ [MoveToBeginning] Synced shifted item \(shiftItem.title) with sortOrder: \(shiftItem.sortOrder)")
|
||||
}
|
||||
print("✅ [Move] Synced \(updated.title) with day: \(day), sortOrder: \(sortOrder)")
|
||||
} catch {
|
||||
print("❌ [MoveToBeginning] Failed to sync: \(error)")
|
||||
print("❌ [Move] Failed to sync: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -909,60 +826,25 @@ struct TripDetailView: View {
|
||||
for (index, dayDate) in days.enumerated() {
|
||||
let dayNum = index + 1
|
||||
let gamesOnDay = gamesOn(date: dayDate)
|
||||
let isRestDay = gamesOnDay.isEmpty
|
||||
|
||||
// Travel for this day (if any)
|
||||
// Travel for this day (if any) - appears before day header
|
||||
if let travelSegment = travelByDay[dayNum] {
|
||||
sections.append(.travel(travelSegment))
|
||||
|
||||
if allowCustomItems {
|
||||
let stableId = stableTravelAnchorId(travelSegment)
|
||||
|
||||
// Add button after travel
|
||||
sections.append(.addButton(day: dayNum, anchorType: .afterTravel, anchorId: stableId))
|
||||
|
||||
// Custom items after this travel (sorted by sortOrder)
|
||||
let itemsAfterTravel = customItems.filter {
|
||||
$0.anchorType == .afterTravel && $0.anchorId == stableId
|
||||
}.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in itemsAfterTravel {
|
||||
sections.append(.customItem(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom items at start of day (before games or as main content for rest days)
|
||||
if allowCustomItems {
|
||||
let itemsAtStart = customItems.filter {
|
||||
$0.anchorDay == dayNum && $0.anchorType == .startOfDay
|
||||
}.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in itemsAtStart {
|
||||
sections.append(.customItem(item))
|
||||
}
|
||||
}
|
||||
|
||||
// Day section - shows games or minimal rest day display
|
||||
sections.append(.day(dayNumber: dayNum, date: dayDate, games: gamesOnDay))
|
||||
|
||||
// Add button after day (different anchor for game days vs rest days)
|
||||
// Custom items for this day (sorted by sortOrder)
|
||||
if allowCustomItems {
|
||||
if isRestDay {
|
||||
// Rest day: add button anchored to start of day (no games to anchor to)
|
||||
sections.append(.addButton(day: dayNum, anchorType: .startOfDay, anchorId: nil))
|
||||
} else if let lastGame = gamesOnDay.last {
|
||||
// Game day: add button anchored after last game
|
||||
sections.append(.addButton(day: dayNum, anchorType: .afterGame, anchorId: lastGame.game.id))
|
||||
|
||||
// Custom items after this game (sorted by sortOrder)
|
||||
let itemsAfterGame = customItems.filter {
|
||||
$0.anchorDay == dayNum &&
|
||||
$0.anchorType == .afterGame &&
|
||||
$0.anchorId == lastGame.game.id
|
||||
}.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in itemsAfterGame {
|
||||
sections.append(.customItem(item))
|
||||
}
|
||||
let dayItems = customItems.filter { $0.day == dayNum }
|
||||
.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in dayItems {
|
||||
sections.append(.customItem(item))
|
||||
}
|
||||
|
||||
// One add button per day
|
||||
sections.append(.addButton(day: dayNum))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1153,13 +1035,13 @@ struct TripDetailView: View {
|
||||
/// Route waypoints including both game stops and mappable custom items in itinerary order
|
||||
private var routeWaypoints: [(name: String, coordinate: CLLocationCoordinate2D, isCustomItem: Bool)] {
|
||||
// Build an ordered list combining game stops and mappable custom items
|
||||
// Group custom items by day
|
||||
let itemsByDay = Dictionary(grouping: mappableCustomItems) { $0.anchorDay }
|
||||
// Items are ordered by (day, sortOrder) - visual order matches route order
|
||||
let itemsByDay = Dictionary(grouping: mappableCustomItems) { $0.day }
|
||||
|
||||
print("🗺️ [Waypoints] Building waypoints. Mappable items by day:")
|
||||
for (day, items) in itemsByDay.sorted(by: { $0.key < $1.key }) {
|
||||
for item in items {
|
||||
print("🗺️ [Waypoints] Day \(day): \(item.title), anchor: \(item.anchorType.rawValue)")
|
||||
for item in items.sorted(by: { $0.sortOrder < $1.sortOrder }) {
|
||||
print("🗺️ [Waypoints] Day \(day): \(item.title), sortOrder: \(item.sortOrder)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1183,18 +1065,6 @@ struct TripDetailView: View {
|
||||
|
||||
print("🗺️ [Waypoints] Day \(dayNumber): city=\(dayCity ?? "none"), games=\(gamesOnDay.count)")
|
||||
|
||||
// Custom items at start of day (before games)
|
||||
if let items = itemsByDay[dayNumber] {
|
||||
let startItems = items.filter { $0.anchorType == .startOfDay }
|
||||
.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in startItems {
|
||||
if let coord = item.coordinate {
|
||||
print("🗺️ [Waypoints] Adding \(item.title) (startOfDay)")
|
||||
waypoints.append((item.title, coord, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Game stop for this day (only add once per city to avoid duplicates)
|
||||
if let city = dayCity {
|
||||
// Check if we already have this city in waypoints (by city name or stadium name)
|
||||
@@ -1227,23 +1097,12 @@ struct TripDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Custom items after games
|
||||
// Custom items for this day (ordered by sortOrder - visual order matches route)
|
||||
if let items = itemsByDay[dayNumber] {
|
||||
let afterGameItems = items.filter { $0.anchorType == .afterGame }
|
||||
.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in afterGameItems {
|
||||
let sortedItems = items.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in sortedItems {
|
||||
if let coord = item.coordinate {
|
||||
print("🗺️ [Waypoints] Adding \(item.title) (afterGame)")
|
||||
waypoints.append((item.title, coord, true))
|
||||
}
|
||||
}
|
||||
|
||||
// Custom items after travel
|
||||
let afterTravelItems = items.filter { $0.anchorType == .afterTravel }
|
||||
.sorted { $0.sortOrder < $1.sortOrder }
|
||||
for item in afterTravelItems {
|
||||
if let coord = item.coordinate {
|
||||
print("🗺️ [Waypoints] Adding \(item.title) (afterTravel)")
|
||||
print("🗺️ [Waypoints] Adding \(item.title) (sortOrder: \(item.sortOrder))")
|
||||
waypoints.append((item.title, coord, true))
|
||||
}
|
||||
}
|
||||
@@ -1437,7 +1296,7 @@ struct TripDetailView: View {
|
||||
let isUpdate = customItems.contains(where: { $0.id == item.id })
|
||||
print("💾 [CustomItems] Saving item: '\(item.title)' (isUpdate: \(isUpdate))")
|
||||
print(" - tripId: \(item.tripId)")
|
||||
print(" - anchorDay: \(item.anchorDay), anchorType: \(item.anchorType.rawValue)")
|
||||
print(" - day: \(item.day), sortOrder: \(item.sortOrder)")
|
||||
|
||||
// Update local state immediately for responsive UI
|
||||
if isUpdate {
|
||||
@@ -1518,13 +1377,12 @@ struct TripDetailView: View {
|
||||
guard let itemId = UUID(uuidString: droppedId),
|
||||
let item = self.customItems.first(where: { $0.id == itemId }) else { return }
|
||||
|
||||
// For game days, anchor to last game; for rest days, anchor to start of day
|
||||
if let lastGame = gamesOnDay.last {
|
||||
await self.moveItemToBeginning(item, toDay: dayNumber, anchorType: .afterGame, anchorId: lastGame.game.id)
|
||||
} else {
|
||||
// Rest day - anchor to start of day
|
||||
await self.moveItemToBeginning(item, toDay: dayNumber, anchorType: .startOfDay, anchorId: nil)
|
||||
}
|
||||
// Append at end of day's items
|
||||
let maxSortOrder = self.customItems
|
||||
.filter { $0.day == dayNumber && $0.id != item.id }
|
||||
.map { $0.sortOrder }
|
||||
.max() ?? 0.0
|
||||
await self.moveItem(item, toDay: dayNumber, sortOrder: maxSortOrder + 1.0)
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -1556,7 +1414,7 @@ enum ItinerarySection {
|
||||
case day(dayNumber: Int, date: Date, games: [RichGame])
|
||||
case travel(TravelSegment)
|
||||
case customItem(CustomItineraryItem)
|
||||
case addButton(day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?)
|
||||
case addButton(day: Int)
|
||||
|
||||
var isCustomItem: Bool {
|
||||
if case .customItem = self { return true }
|
||||
@@ -1569,8 +1427,6 @@ enum ItinerarySection {
|
||||
struct AddItemAnchor: Identifiable {
|
||||
let id = UUID()
|
||||
let day: Int
|
||||
let type: CustomItineraryItem.AnchorType
|
||||
let anchorId: String?
|
||||
}
|
||||
|
||||
// MARK: - Inline Add Button
|
||||
|
||||
Reference in New Issue
Block a user