Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Modules/Sources/JetpackStats/Analytics/StatsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public enum StatsEvent {
/// Referrer stats screen shown
case referrerStatsScreenShown

/// UTM metric stats screen shown
case utmMetricStatsScreenShown

// MARK: - Date Range Events

/// Date range preset selected
Expand Down Expand Up @@ -154,6 +157,12 @@ public enum StatsEvent {
/// - "to_breakdown": New breakdown
case deviceBreakdownChanged

/// UTM parameter grouping changed
/// - Parameters:
/// - "from_grouping": Previous grouping ("sourceMedium", "campaignSourceMedium", etc.)
/// - "to_grouping": New grouping
case utmParamGroupingChanged

// MARK: - Navigation Events

/// Stats tab selected
Expand Down Expand Up @@ -229,6 +238,7 @@ extension TopListItemType {
case .searchTerms: "search_terms"
case .fileDownloads: "file_downloads"
case .archive: "archive"
case .utm: "utm"
}
}
}
Expand Down
55 changes: 47 additions & 8 deletions Modules/Sources/JetpackStats/Cards/TopListCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct TopListCard: View {
private let itemLimit: Int
private let reserveSpace: Bool
private let showMoreInline: Bool
private let showItemTypePicker: Bool

@State private var isExpanded = false

Expand All @@ -16,12 +17,14 @@ struct TopListCard: View {
viewModel: TopListViewModel,
itemLimit: Int = 5,
reserveSpace: Bool = true,
showMoreInline: Bool = false
showMoreInline: Bool = false,
showItemTypePicker: Bool = true
) {
self.viewModel = viewModel
self.itemLimit = itemLimit
self.reserveSpace = reserveSpace
self.showMoreInline = showMoreInline
self.showItemTypePicker = showItemTypePicker
}

var body: some View {
Expand Down Expand Up @@ -81,10 +84,14 @@ struct TopListCard: View {

private var cardHeaderView: some View {
HStack {
Menu {
itemTypePicker
} label: {
StatsCardTitleView(title: viewModel.selection.item.localizedTitle, showChevron: true)
if showItemTypePicker {
Menu {
itemTypePicker
} label: {
StatsCardTitleView(title: viewModel.selection.item.localizedTitle, showChevron: true)
}
} else {
StatsCardTitleView(title: viewModel.selection.item.localizedTitle, showChevron: false)
}
Spacer(minLength: 44)
}
Expand Down Expand Up @@ -114,7 +121,7 @@ struct TopListCard: View {

private var listHeaderView: some View {
HStack {
// Left side: Location level for locations, device breakdown for devices, otherwise item type
// Left side: Location level for locations, device breakdown for devices, UTM parameter for UTM, otherwise item type
if viewModel.selection.item == .locations {
Menu {
locationLevelPicker
Expand All @@ -135,6 +142,16 @@ struct TopListCard: View {
.padding(.vertical, Constants.step0_5)
}
.fixedSize()
} else if viewModel.selection.item == .utm {
Menu {
utmParamGroupingPicker
} label: {
InlineValuePickerTitle(title: viewModel.selection.options.utmParamGrouping.localizedTitle)
.foregroundStyle(Color.secondary)
.padding(.top, 6)
.padding(.vertical, Constants.step0_5)
}
.fixedSize()
} else {
makeColumnTitle(viewModel.selection.item.localizedColumnName)
}
Expand Down Expand Up @@ -267,6 +284,28 @@ struct TopListCard: View {
.tint(Color.primary)
}

private var utmParamGroupingPicker: some View {
ForEach(Array(UTMParamGrouping.grouped.enumerated()), id: \.offset) { _, groupings in
Section {
ForEach(groupings) { grouping in
Button {
let previousGrouping = viewModel.selection.options.utmParamGrouping
viewModel.selection.options.utmParamGrouping = grouping

// Track UTM parameter grouping change
viewModel.tracker?.send(.utmParamGroupingChanged, properties: [
"from_grouping": previousGrouping.analyticsName,
"to_grouping": grouping.analyticsName
])
} label: {
Text(grouping.localizedTitle)
}
}
}
}
.tint(Color.primary)
}

@ViewBuilder
private var listContentView: some View {
Group {
Expand Down Expand Up @@ -371,8 +410,8 @@ struct TopListCard: View {

#Preview {
ScrollView {
VStack {
TopListCardPreview(item: .devices)
VStack(spacing: 16) {
TopListCardPreview(item: .utm)
TopListCardPreview(item: .authors)
TopListCardPreview(item: .locations)
}
Expand Down
41 changes: 28 additions & 13 deletions Modules/Sources/JetpackStats/Cards/TopListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ final class TopListViewModel: ObservableObject, TrafficCardViewModel {

enum Filter: Equatable {
case author(userId: String)
case utmMetric(values: [String])
}

var isFirstLoad: Bool { isLoading && data == nil }
Expand Down Expand Up @@ -83,15 +84,16 @@ final class TopListViewModel: ObservableObject, TrafficCardViewModel {
self.data = initialData
self.isLoading = initialData == nil

self.groupedItems = {
let primary = service.supportedItems.filter {
!TopListItemType.secondaryItems.contains($0)
}
let secondary = service.supportedItems.filter {
TopListItemType.secondaryItems.contains($0)
}
return [primary, secondary]
}()
let supportedItems = Set(service.supportedItems)
self.groupedItems = [
TopListItemType.contentItems,
TopListItemType.trafficSourceItems,
TopListItemType.audienceEngagementItems
].map {
$0.filter(supportedItems.contains)
}.filter {
!$0.isEmpty
}
}

func updateConfiguration(_ newConfiguration: TopListCardConfiguration) {
Expand Down Expand Up @@ -209,11 +211,17 @@ final class TopListViewModel: ObservableObject, TrafficCardViewModel {
private func getTopListData(for selection: Selection, dateRange: StatsDateRange) async throws -> TopListData {
let granularity = dateRange.dateInterval.preferredGranularity

// When filter is set for author, we need to fetch authors data
// When filter is set, we need to fetch the appropriate data type
let fetchItem: TopListItemType
if let filter, case .author = filter {
// We have to fake it as "Posts & Pages" does not support filtering
fetchItem = .authors
if let filter {
switch filter {
case .author:
// We have to fake it as "Posts & Pages" does not support filtering
fetchItem = .authors
case .utmMetric:
// Fetch UTM data to get posts for specific campaign
fetchItem = .utm
}
} else {
fetchItem = selection.item
}
Expand Down Expand Up @@ -277,6 +285,13 @@ final class TopListViewModel: ObservableObject, TrafficCardViewModel {
return posts
}
return []
case .utmMetric(let values):
let utmMetrics = items.lazy.compactMap { $0 as? TopListItem.UTMMetric }
if let metric = utmMetrics.first(where: { $0.values == values }),
let posts = metric.posts {
return posts
}
return []
}
}

Expand Down
Loading