You've already forked AstralRinth
feat: various analytics updates (#6330)
* feat: add button to view user analytics * feat: add "Your projects" preset selection * feat: fix revenue rounding for values under 1 and show full values for all statcards with tooltip * fix: sum rounded value instead of raw value for tooltip total if it's under 1 * fix: show decimal in playtime statcard if under 1 hrs * feat: disable playtime statcard for purely plugin projects * refactor: pnpm prepr
This commit is contained in:
+9
-1
@@ -168,6 +168,14 @@ const {
|
||||
onRangeSelected: (start, end, groupBy) => emit('range-select', start, end, groupBy),
|
||||
})
|
||||
|
||||
function getTooltipTotalMetricValue(value: number): number {
|
||||
if (props.activeStat === 'revenue' && Math.abs(value) < 1) {
|
||||
return Math.round(value * 100) / 100
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const hoverTotalValue = computed(() => {
|
||||
if (hoverState.sliceIndex === null) return 0
|
||||
const sliceIndex = hoverState.sliceIndex
|
||||
@@ -176,7 +184,7 @@ const hoverTotalValue = computed(() => {
|
||||
return props.currentLegendEntries.reduce((sum, legendEntry) => {
|
||||
if (legendEntry.hidden) return sum
|
||||
const dataset = props.chartDatasetById.get(legendEntry.id)
|
||||
return sum + (dataset?.data[sliceIndex] ?? 0)
|
||||
return sum + getTooltipTotalMetricValue(dataset?.data[sliceIndex] ?? 0)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
|
||||
@@ -39,6 +39,14 @@ export const analyticsMessages = defineMessages({
|
||||
id: 'analytics.project.all',
|
||||
defaultMessage: 'All projects',
|
||||
},
|
||||
yourProjects: {
|
||||
id: 'analytics.project.your',
|
||||
defaultMessage: 'Your projects',
|
||||
},
|
||||
userProjects: {
|
||||
id: 'analytics.project.user',
|
||||
defaultMessage: "{username}'s projects",
|
||||
},
|
||||
selectProjects: {
|
||||
id: 'analytics.project.select',
|
||||
defaultMessage: 'Select projects',
|
||||
|
||||
@@ -146,6 +146,7 @@ const QUERY_KEY_TABLE_SORT = 'a_table_sort'
|
||||
const QUERY_KEY_TABLE_SORT_DIRECTION = 'a_table_sort_direction'
|
||||
const QUERY_KEY_LEGACY_GRAPH_TOP_BREAKDOWN_FILTER = 'a_top_breakdown'
|
||||
const QUERY_KEY_LEGACY_GRAPH_LEGEND_EXPANSION = 'a_legend_expanded'
|
||||
const PROJECT_SELECTION_ALL_QUERY_VALUE = 'all'
|
||||
|
||||
const URL_FILTER_CATEGORIES: Exclude<AnalyticsQueryFilterCategory, 'project'>[] = [
|
||||
'project_status',
|
||||
@@ -405,9 +406,10 @@ export function buildDefaultAnalyticsGraphState(
|
||||
|
||||
export function buildDefaultAnalyticsQueryBuilderState(
|
||||
availableProjectIds: string[],
|
||||
defaultProjectIds: string[] = availableProjectIds,
|
||||
): AnalyticsQueryBuilderState {
|
||||
return {
|
||||
selectedProjectIds: [...availableProjectIds],
|
||||
selectedProjectIds: [...defaultProjectIds],
|
||||
selectedTimeframeMode: DEFAULT_TIMEFRAME_MODE,
|
||||
selectedTimeframe: DEFAULT_TIMEFRAME_PRESET,
|
||||
selectedLastTimeframeAmount: DEFAULT_LAST_TIMEFRAME_AMOUNT,
|
||||
@@ -415,7 +417,7 @@ export function buildDefaultAnalyticsQueryBuilderState(
|
||||
selectedCustomTimeframeStartDate: getDefaultCustomStartDate(),
|
||||
selectedCustomTimeframeEndDate: getDefaultCustomEndDate(),
|
||||
selectedGroupBy: DEFAULT_GROUP_BY_PRESET,
|
||||
selectedBreakdowns: getDefaultAnalyticsBreakdownPresets(availableProjectIds),
|
||||
selectedBreakdowns: getDefaultAnalyticsBreakdownPresets(defaultProjectIds),
|
||||
selectedFilters: buildEmptySelectedFilters(),
|
||||
}
|
||||
}
|
||||
@@ -475,12 +477,16 @@ export function getAnalyticsBreakdownPresetForProjectSelection(
|
||||
export function isAnalyticsQueryBuilderStateDefault(
|
||||
state: AnalyticsQueryBuilderState,
|
||||
availableProjectIds: string[],
|
||||
defaultProjectIds: string[] = availableProjectIds,
|
||||
): boolean {
|
||||
const defaultState = buildDefaultAnalyticsQueryBuilderState(availableProjectIds)
|
||||
const defaultState = buildDefaultAnalyticsQueryBuilderState(
|
||||
availableProjectIds,
|
||||
defaultProjectIds,
|
||||
)
|
||||
const areDefaultProjectsSelected =
|
||||
availableProjectIds.length === 0
|
||||
defaultProjectIds.length === 0
|
||||
? state.selectedProjectIds.length === 0
|
||||
: areAllProjectsSelected(state.selectedProjectIds, availableProjectIds)
|
||||
: areAllProjectsSelected(state.selectedProjectIds, defaultProjectIds)
|
||||
|
||||
return (
|
||||
areDefaultProjectsSelected &&
|
||||
@@ -666,13 +672,19 @@ export function readAnalyticsTableSortState(
|
||||
export function readAnalyticsQueryBuilderState(
|
||||
query: LocationQuery,
|
||||
availableProjectIds: string[],
|
||||
defaultProjectIds: string[] = availableProjectIds,
|
||||
): AnalyticsQueryBuilderState {
|
||||
const defaultState = buildDefaultAnalyticsQueryBuilderState(availableProjectIds)
|
||||
const defaultState = buildDefaultAnalyticsQueryBuilderState(
|
||||
availableProjectIds,
|
||||
defaultProjectIds,
|
||||
)
|
||||
const selectedProjectIdsFromQuery = parseListQueryValue(query[QUERY_KEY_PROJECT_IDS])
|
||||
const selectedProjectIds =
|
||||
selectedProjectIdsFromQuery.length > 0
|
||||
? selectedProjectIdsFromQuery
|
||||
: defaultState.selectedProjectIds
|
||||
let selectedProjectIds = defaultState.selectedProjectIds
|
||||
if (selectedProjectIdsFromQuery.includes(PROJECT_SELECTION_ALL_QUERY_VALUE)) {
|
||||
selectedProjectIds = [...availableProjectIds]
|
||||
} else if (selectedProjectIdsFromQuery.length > 0) {
|
||||
selectedProjectIds = selectedProjectIdsFromQuery
|
||||
}
|
||||
|
||||
const selectedFilters = buildEmptySelectedFilters()
|
||||
for (const category of URL_FILTER_CATEGORIES) {
|
||||
@@ -779,14 +791,17 @@ export function buildAnalyticsQueryBuilderRouteQuery(
|
||||
state: AnalyticsQueryBuilderState,
|
||||
availableProjectIds: string[],
|
||||
graphState?: AnalyticsGraphState,
|
||||
defaultProjectIds: string[] = availableProjectIds,
|
||||
): MutableRouteQuery {
|
||||
const nextRouteQuery = {
|
||||
...currentRouteQuery,
|
||||
} as MutableRouteQuery
|
||||
|
||||
const projectIdsQueryValue = areAllProjectsSelected(state.selectedProjectIds, availableProjectIds)
|
||||
const projectIdsQueryValue = areAllProjectsSelected(state.selectedProjectIds, defaultProjectIds)
|
||||
? undefined
|
||||
: serializeListQueryValue(state.selectedProjectIds)
|
||||
: areAllProjectsSelected(state.selectedProjectIds, availableProjectIds)
|
||||
? PROJECT_SELECTION_ALL_QUERY_VALUE
|
||||
: serializeListQueryValue(state.selectedProjectIds)
|
||||
const isCustomTimeframeMode =
|
||||
state.selectedTimeframeMode === 'custom_range' ||
|
||||
state.selectedTimeframeMode === 'custom_datetime_range'
|
||||
|
||||
@@ -69,6 +69,29 @@
|
||||
<template v-if="hasProjectOptions" #top>
|
||||
<div>
|
||||
<button
|
||||
v-if="showProjectPresets"
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center gap-1.5 border-0 bg-surface-4 px-4 py-3 text-left shadow-none transition-all duration-150 hover:brightness-[115%] focus:brightness-[115%]"
|
||||
:aria-selected="isUserProjectsOptionSelected"
|
||||
:class="isUserProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
|
||||
role="option"
|
||||
@click="selectUserProjectsMode"
|
||||
@keydown.enter.stop
|
||||
@keydown.space.stop
|
||||
>
|
||||
<LayersIcon
|
||||
class="h-5 w-5 shrink-0 text-primary"
|
||||
:class="isUserProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 font-semibold leading-tight">
|
||||
{{ userProjectsLabel }}
|
||||
</span>
|
||||
<span class="flex shrink-0 items-center justify-center text-brand">
|
||||
<CheckIcon v-if="isUserProjectsOptionSelected" aria-hidden="true" class="size-5" />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!showProjectPresets || showAllProjectsPreset"
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center gap-1.5 border-0 bg-surface-4 px-4 py-3 text-left shadow-none transition-all duration-150 hover:brightness-[115%] focus:brightness-[115%]"
|
||||
:aria-selected="isAllProjectsOptionSelected"
|
||||
@@ -210,7 +233,11 @@
|
||||
decoding="async"
|
||||
/>
|
||||
<LayersIcon
|
||||
v-else-if="isAllProjectsOptionSelected || areAllProjectsSelected"
|
||||
v-else-if="
|
||||
isUserProjectsOptionSelected ||
|
||||
isAllProjectsOptionSelected ||
|
||||
areAllProjectRowsSelected
|
||||
"
|
||||
class="size-5 shrink-0 text-primary"
|
||||
/>
|
||||
<BoxIcon v-else class="size-5 shrink-0 text-primary" />
|
||||
@@ -253,6 +280,33 @@
|
||||
<template v-if="hasProjectOptions" #top>
|
||||
<div>
|
||||
<button
|
||||
v-if="showProjectPresets"
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center gap-2 border-0 bg-surface-4 px-4 py-3 text-left shadow-none transition-all duration-150 hover:brightness-[115%] focus:brightness-[115%]"
|
||||
:aria-selected="isUserProjectsOptionSelected"
|
||||
:class="isUserProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
|
||||
role="option"
|
||||
@click="selectUserProjectsMode"
|
||||
@keydown.enter.stop
|
||||
@keydown.space.stop
|
||||
>
|
||||
<LayersIcon
|
||||
class="h-5 w-5 shrink-0 text-primary"
|
||||
:class="isUserProjectsOptionSelected ? 'text-contrast' : 'text-primary'"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 font-semibold leading-tight">
|
||||
{{ userProjectsLabel }}
|
||||
</span>
|
||||
<span class="flex shrink-0 items-center justify-center text-brand">
|
||||
<CheckIcon
|
||||
v-if="isUserProjectsOptionSelected"
|
||||
aria-hidden="true"
|
||||
class="size-5"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!showProjectPresets || showAllProjectsPreset"
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center gap-2 border-0 bg-surface-4 px-4 py-3 text-left shadow-none transition-all duration-150 hover:brightness-[115%] focus:brightness-[115%]"
|
||||
:aria-selected="isAllProjectsOptionSelected"
|
||||
@@ -449,11 +503,17 @@ const QUERY_BUILDER_DROPDOWN_MIN_WIDTH = '12rem'
|
||||
const analyticsQueryChipTriggerClass = 'h-10 '
|
||||
const analyticsQueryAddFilterButtonClass = '!h-10 max-w-full !w-max !px-3.5 flex !gap-2'
|
||||
const projectOptionCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
|
||||
type ProjectSelectionPreset = 'user' | 'all'
|
||||
|
||||
const {
|
||||
hasProjectContext,
|
||||
projectGroups,
|
||||
projects,
|
||||
dashboardUserProjectIds,
|
||||
dashboardOrganizationProjectIds,
|
||||
defaultProjectIds,
|
||||
isUsingDashboardUserOverride,
|
||||
dashboardProjectUserName,
|
||||
selectedProjectIds,
|
||||
selectedTimeframeMode,
|
||||
selectedTimeframe,
|
||||
@@ -467,7 +527,7 @@ const {
|
||||
activeStat,
|
||||
showPreviousPeriod,
|
||||
projectStatusById,
|
||||
projectDownloadsById,
|
||||
availableProjectDownloadsById,
|
||||
queryResetToken,
|
||||
refreshAnalyticsQuery,
|
||||
setFetchRequest,
|
||||
@@ -535,12 +595,25 @@ const projectSelectOptions = computed<MultiSelectItem<string>[]>(() => {
|
||||
|
||||
const allProjectIds = computed(() => projectOptions.value.map((project) => project.value))
|
||||
const hasProjectOptions = computed(() => projectOptions.value.length > 0)
|
||||
const userProjectIds = computed(() =>
|
||||
dashboardOrganizationProjectIds.value.length > 0
|
||||
? dashboardUserProjectIds.value
|
||||
: defaultProjectIds.value,
|
||||
)
|
||||
const showProjectPresets = computed(
|
||||
() =>
|
||||
hasProjectOptions.value &&
|
||||
dashboardUserProjectIds.value.length > 0 &&
|
||||
dashboardOrganizationProjectIds.value.length > 0,
|
||||
)
|
||||
const showAllProjectsPreset = computed(() => dashboardOrganizationProjectIds.value.length > 0)
|
||||
const noProjectsMessage = computed(() =>
|
||||
hasProjectContext.value
|
||||
? formatMessage(analyticsMessages.noDataAvailableForAnalytics)
|
||||
: formatMessage(analyticsMessages.noProjectsAvailable),
|
||||
)
|
||||
const isProjectSelectOpen = ref(false)
|
||||
const draftProjectSelectionPreset = ref<ProjectSelectionPreset | null>(null)
|
||||
const draftSelectedProjectIds = ref<string[]>([...selectedProjectIds.value])
|
||||
const projectDownloadsThreshold = ref<number | null>(null)
|
||||
const projectDownloadsThresholdProjectIds = ref<string[] | null>(null)
|
||||
@@ -558,15 +631,48 @@ function normalizeProjectSelection(projectIds: string[]) {
|
||||
return projectIds.length > 0 ? [...projectIds] : [...allProjectIds.value]
|
||||
}
|
||||
|
||||
function getProjectSelectionPreset(projectIds: string[]): ProjectSelectionPreset | null {
|
||||
if (!showProjectPresets.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isSameProjectSelection(projectIds, userProjectIds.value)) {
|
||||
return 'user'
|
||||
}
|
||||
|
||||
if (isSameProjectSelection(projectIds, allProjectIds.value)) {
|
||||
return 'all'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function setDraftProjectSelection(projectIds: string[]) {
|
||||
const preset = getProjectSelectionPreset(projectIds)
|
||||
draftProjectSelectionPreset.value = preset
|
||||
if (preset) {
|
||||
draftSelectedProjectIds.value = []
|
||||
return
|
||||
}
|
||||
|
||||
draftSelectedProjectIds.value = isSameProjectSelection(projectIds, allProjectIds.value)
|
||||
? []
|
||||
: [...projectIds]
|
||||
}
|
||||
|
||||
watch(selectedProjectIds, (nextSelectedProjectIds) => {
|
||||
if (isProjectSelectOpen.value) {
|
||||
return
|
||||
}
|
||||
|
||||
draftSelectedProjectIds.value = [...nextSelectedProjectIds]
|
||||
setDraftProjectSelection(nextSelectedProjectIds)
|
||||
})
|
||||
|
||||
watch(draftSelectedProjectIds, (nextSelectedProjectIds) => {
|
||||
if (draftProjectSelectionPreset.value && nextSelectedProjectIds.length > 0) {
|
||||
draftProjectSelectionPreset.value = null
|
||||
}
|
||||
|
||||
if (projectDownloadsThreshold.value === null) {
|
||||
return
|
||||
}
|
||||
@@ -587,25 +693,40 @@ watch(queryResetToken, () => {
|
||||
isBreakdownSelectOpen.value = false
|
||||
draftSelectedBreakdowns.value = [...selectedBreakdowns.value]
|
||||
clearProjectDownloadsThreshold()
|
||||
draftSelectedProjectIds.value = isSameProjectSelection(
|
||||
selectedProjectIds.value,
|
||||
allProjectIds.value,
|
||||
)
|
||||
? []
|
||||
: [...selectedProjectIds.value]
|
||||
setDraftProjectSelection(selectedProjectIds.value)
|
||||
})
|
||||
|
||||
const areAllProjectsSelected = computed(() => {
|
||||
const areAllProjectRowsSelected = computed(() => {
|
||||
return isSameProjectSelection(draftSelectedProjectIds.value, allProjectIds.value)
|
||||
})
|
||||
const isAllProjectsOptionSelected = computed(() => draftSelectedProjectIds.value.length === 0)
|
||||
const isAllProjectsOptionSelected = computed(() =>
|
||||
showProjectPresets.value
|
||||
? draftProjectSelectionPreset.value === 'all'
|
||||
: draftSelectedProjectIds.value.length === 0,
|
||||
)
|
||||
const isUserProjectsOptionSelected = computed(() => {
|
||||
return showProjectPresets.value && draftProjectSelectionPreset.value === 'user'
|
||||
})
|
||||
const userProjectsLabel = computed(() => {
|
||||
if (isUsingDashboardUserOverride.value) {
|
||||
return formatMessage(analyticsMessages.userProjects, {
|
||||
username: dashboardProjectUserName.value,
|
||||
})
|
||||
}
|
||||
|
||||
return formatMessage(analyticsMessages.yourProjects)
|
||||
})
|
||||
|
||||
const selectedProjectLabel = computed(() => {
|
||||
if (!hasProjectOptions.value) {
|
||||
return noProjectsMessage.value
|
||||
}
|
||||
|
||||
if (isAllProjectsOptionSelected.value || areAllProjectsSelected.value) {
|
||||
if (isUserProjectsOptionSelected.value) {
|
||||
return userProjectsLabel.value
|
||||
}
|
||||
|
||||
if (isAllProjectsOptionSelected.value || areAllProjectRowsSelected.value) {
|
||||
return formatMessage(analyticsMessages.allProjects)
|
||||
}
|
||||
|
||||
@@ -623,8 +744,9 @@ const selectedProjectLabel = computed(() => {
|
||||
|
||||
const selectedProjectIconUrl = computed(() => {
|
||||
if (
|
||||
isUserProjectsOptionSelected.value ||
|
||||
isAllProjectsOptionSelected.value ||
|
||||
areAllProjectsSelected.value ||
|
||||
areAllProjectRowsSelected.value ||
|
||||
draftSelectedProjectIds.value.length !== 1
|
||||
) {
|
||||
return undefined
|
||||
@@ -639,12 +761,7 @@ function getProjectIconUrl(projectId: string): string | undefined {
|
||||
|
||||
function handleProjectSelectOpen() {
|
||||
isProjectSelectOpen.value = true
|
||||
draftSelectedProjectIds.value = isSameProjectSelection(
|
||||
selectedProjectIds.value,
|
||||
allProjectIds.value,
|
||||
)
|
||||
? []
|
||||
: [...selectedProjectIds.value]
|
||||
setDraftProjectSelection(selectedProjectIds.value)
|
||||
}
|
||||
|
||||
function handleProjectSelectClose(
|
||||
@@ -657,9 +774,14 @@ function handleProjectSelectClose(
|
||||
function commitDraftSelectedProjects(
|
||||
nextSelectedProjectIds: string[] = draftSelectedProjectIds.value,
|
||||
) {
|
||||
const nextProjectIds = normalizeProjectSelection(nextSelectedProjectIds)
|
||||
const nextProjectIds =
|
||||
draftProjectSelectionPreset.value === 'user'
|
||||
? [...userProjectIds.value]
|
||||
: draftProjectSelectionPreset.value === 'all'
|
||||
? [...allProjectIds.value]
|
||||
: normalizeProjectSelection(nextSelectedProjectIds)
|
||||
|
||||
draftSelectedProjectIds.value = [...nextProjectIds]
|
||||
setDraftProjectSelection(nextProjectIds)
|
||||
if (!isSameProjectSelection(selectedProjectIds.value, nextProjectIds)) {
|
||||
if (isSameProjectSelection(nextProjectIds, allProjectIds.value)) {
|
||||
showPreviousPeriod.value = false
|
||||
@@ -670,6 +792,17 @@ function commitDraftSelectedProjects(
|
||||
|
||||
function selectAllProjectsMode() {
|
||||
clearProjectDownloadsThreshold()
|
||||
if (showProjectPresets.value) {
|
||||
draftProjectSelectionPreset.value = 'all'
|
||||
} else {
|
||||
draftProjectSelectionPreset.value = null
|
||||
}
|
||||
draftSelectedProjectIds.value = []
|
||||
}
|
||||
|
||||
function selectUserProjectsMode() {
|
||||
clearProjectDownloadsThreshold()
|
||||
draftProjectSelectionPreset.value = 'user'
|
||||
draftSelectedProjectIds.value = []
|
||||
}
|
||||
|
||||
@@ -753,9 +886,10 @@ function applyProjectDownloadsThreshold(threshold: number | null) {
|
||||
}
|
||||
|
||||
const projectIds = projects.value
|
||||
.filter((project) => (projectDownloadsById.value.get(project.id) ?? 0) > threshold)
|
||||
.filter((project) => (availableProjectDownloadsById.value.get(project.id) ?? 0) > threshold)
|
||||
.map((project) => project.id)
|
||||
|
||||
draftProjectSelectionPreset.value = null
|
||||
projectDownloadsThresholdProjectIds.value = projectIds
|
||||
draftSelectedProjectIds.value = projectIds
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div
|
||||
class="text-2xl font-semibold leading-none md:text-4xl"
|
||||
v-tooltip="!disabled ? statTooltip : undefined"
|
||||
class="w-fit text-2xl font-semibold leading-none md:text-4xl"
|
||||
:class="{
|
||||
'text-primary': disabled,
|
||||
'text-contrast': !disabled,
|
||||
@@ -114,6 +115,7 @@ import { analyticsStatCardMessages } from '../analytics-messages'
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
statLabel: string
|
||||
statTooltip?: string
|
||||
vsPrevPeriodPercent: string | null
|
||||
icon: string
|
||||
active?: boolean
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
:key="card.key"
|
||||
:label="card.label"
|
||||
:stat-label="card.statLabel"
|
||||
:stat-tooltip="card.statTooltip"
|
||||
:vs-prev-period-percent="card.vsPrevPeriodPercent"
|
||||
:icon="card.icon"
|
||||
:active="activeStat === card.key"
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
} from '~/providers/analytics/analytics'
|
||||
|
||||
import { analyticsStatCardMessages, formatAnalyticsStatLabel } from '../analytics-messages.ts'
|
||||
import { formatAnalyticsTableFullPlaytime } from '../analytics-table/analytics-table-formatting.ts'
|
||||
import StatCard from './StatCard.vue'
|
||||
|
||||
const MONETIZATION_BANNER_DISMISSED_KEY = 'analytics-monetization-banner-dismissed'
|
||||
@@ -77,6 +79,38 @@ const compactNumberFormatter = computed(
|
||||
}),
|
||||
)
|
||||
|
||||
const underDollarRevenueFormatter = computed(
|
||||
() =>
|
||||
new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}),
|
||||
)
|
||||
|
||||
const preciseRevenueFormatter = computed(
|
||||
() =>
|
||||
new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 5,
|
||||
maximumFractionDigits: 5,
|
||||
}),
|
||||
)
|
||||
|
||||
const tooltipRevenueFormatter = computed(
|
||||
() =>
|
||||
new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}),
|
||||
)
|
||||
|
||||
const underHourPlaytimeFormatter = computed(
|
||||
() =>
|
||||
new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}),
|
||||
)
|
||||
|
||||
function formatStatNumber(value: number): string {
|
||||
const rounded = Math.round(value)
|
||||
|
||||
@@ -87,6 +121,45 @@ function formatStatNumber(value: number): string {
|
||||
return formatNumber(rounded)
|
||||
}
|
||||
|
||||
function formatFullStatNumber(value: number): string {
|
||||
return formatNumber(Math.round(value))
|
||||
}
|
||||
|
||||
function formatRevenueNumber(value: number): string {
|
||||
if (Math.abs(value) > 0 && Math.abs(value) < 1) {
|
||||
return underDollarRevenueFormatter.value.format(value)
|
||||
}
|
||||
|
||||
return formatStatNumber(value)
|
||||
}
|
||||
|
||||
function formatRevenueValue(value: number): string {
|
||||
return formatMessage(analyticsStatCardMessages.revenueValue, {
|
||||
value: formatRevenueNumber(value),
|
||||
})
|
||||
}
|
||||
|
||||
function formatPreciseRevenueValue(value: number): string {
|
||||
return formatMessage(analyticsStatCardMessages.revenueValue, {
|
||||
value:
|
||||
Math.abs(value) < 1
|
||||
? preciseRevenueFormatter.value.format(value)
|
||||
: tooltipRevenueFormatter.value.format(value),
|
||||
})
|
||||
}
|
||||
|
||||
function formatPlaytimeTooltip(value: number): string {
|
||||
return formatAnalyticsTableFullPlaytime(value, formatMessage)
|
||||
}
|
||||
|
||||
function formatPlaytimeNumber(value: number): string {
|
||||
if (Math.abs(value) > 0 && Math.abs(value) < 1) {
|
||||
return underHourPlaytimeFormatter.value.format(value)
|
||||
}
|
||||
|
||||
return formatStatNumber(value)
|
||||
}
|
||||
|
||||
function formatPercent(value: number): string {
|
||||
const rounded = Math.round(value * 10) / 10
|
||||
if (rounded === 0) {
|
||||
@@ -105,7 +178,7 @@ function formatSignedStatNumber(value: number): string {
|
||||
function formatSignedRevenue(value: number): string {
|
||||
const signPrefix = value > 0 ? '+' : value < 0 ? '-' : ''
|
||||
return `${signPrefix}${formatMessage(analyticsStatCardMessages.revenueValue, {
|
||||
value: formatStatNumber(Math.abs(value)),
|
||||
value: formatRevenueNumber(Math.abs(value)),
|
||||
})}`
|
||||
}
|
||||
|
||||
@@ -169,6 +242,7 @@ const statCards = computed<
|
||||
key: AnalyticsDashboardStat
|
||||
label: string
|
||||
statLabel: string
|
||||
statTooltip?: string
|
||||
vsPrevPeriodPercent: string | null
|
||||
icon: string
|
||||
disabled: boolean
|
||||
@@ -178,6 +252,7 @@ const statCards = computed<
|
||||
key: 'views',
|
||||
label: formatAnalyticsStatLabel('views', formatMessage),
|
||||
statLabel: formatStatNumber(currentTotals.value.views),
|
||||
statTooltip: formatFullStatNumber(currentTotals.value.views),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodComparison(
|
||||
'views',
|
||||
percentChanges.value.views,
|
||||
@@ -191,6 +266,7 @@ const statCards = computed<
|
||||
key: 'downloads',
|
||||
label: formatAnalyticsStatLabel('downloads', formatMessage),
|
||||
statLabel: formatStatNumber(currentTotals.value.downloads),
|
||||
statTooltip: formatFullStatNumber(currentTotals.value.downloads),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodComparison(
|
||||
'downloads',
|
||||
percentChanges.value.downloads,
|
||||
@@ -203,9 +279,8 @@ const statCards = computed<
|
||||
{
|
||||
key: 'revenue',
|
||||
label: formatAnalyticsStatLabel('revenue', formatMessage),
|
||||
statLabel: formatMessage(analyticsStatCardMessages.revenueValue, {
|
||||
value: formatStatNumber(currentTotals.value.revenue),
|
||||
}),
|
||||
statLabel: formatRevenueValue(currentTotals.value.revenue),
|
||||
statTooltip: formatPreciseRevenueValue(currentTotals.value.revenue),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodComparison(
|
||||
'revenue',
|
||||
percentChanges.value.revenue,
|
||||
@@ -219,8 +294,9 @@ const statCards = computed<
|
||||
key: 'playtime',
|
||||
label: formatAnalyticsStatLabel('playtime', formatMessage),
|
||||
statLabel: formatMessage(analyticsStatCardMessages.playtimeHours, {
|
||||
hours: formatStatNumber(currentTotals.value.playtime / 3600),
|
||||
hours: formatPlaytimeNumber(currentTotals.value.playtime / 3600),
|
||||
}),
|
||||
statTooltip: formatPlaytimeTooltip(currentTotals.value.playtime),
|
||||
vsPrevPeriodPercent: formatPreviousPeriodComparison(
|
||||
'playtime',
|
||||
percentChanges.value.playtime,
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface UseAnalyticsRouteSyncOptions {
|
||||
queryBuilder: AnalyticsQueryBuilderRefs
|
||||
graph: AnalyticsGraphRefs
|
||||
availableProjectIds: Ref<string[]>
|
||||
defaultProjectIds: Ref<string[]>
|
||||
sanitizeSelectedFilters: (
|
||||
breakdowns: readonly AnalyticsBreakdownPreset[],
|
||||
filters: AnalyticsSelectedFilters,
|
||||
@@ -60,7 +61,8 @@ export interface UseAnalyticsRouteSyncOptions {
|
||||
}
|
||||
|
||||
export function useAnalyticsRouteSync(options: UseAnalyticsRouteSyncOptions) {
|
||||
const { queryBuilder, graph, availableProjectIds, sanitizeSelectedFilters } = options
|
||||
const { queryBuilder, graph, availableProjectIds, defaultProjectIds, sanitizeSelectedFilters } =
|
||||
options
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -116,6 +118,7 @@ export function useAnalyticsRouteSync(options: UseAnalyticsRouteSyncOptions) {
|
||||
getSelectedAnalyticsQueryBuilderState(),
|
||||
availableProjectIds.value,
|
||||
getSelectedAnalyticsGraphState(),
|
||||
defaultProjectIds.value,
|
||||
)
|
||||
|
||||
const hasAnalyticsQueryChange = hasAnalyticsQueryBuilderRouteChange(route.query, nextRouteQuery)
|
||||
@@ -144,7 +147,11 @@ export function useAnalyticsRouteSync(options: UseAnalyticsRouteSyncOptions) {
|
||||
}
|
||||
|
||||
function applyRouteQueryToState(nextQuery: LocationQuery) {
|
||||
const nextQueryState = readAnalyticsQueryBuilderState(nextQuery, availableProjectIds.value)
|
||||
const nextQueryState = readAnalyticsQueryBuilderState(
|
||||
nextQuery,
|
||||
availableProjectIds.value,
|
||||
defaultProjectIds.value,
|
||||
)
|
||||
const availableProjectIdSet = new Set(availableProjectIds.value)
|
||||
const nextSelectedProjectIds = nextQueryState.selectedProjectIds.filter((projectId) =>
|
||||
availableProjectIdSet.has(projectId),
|
||||
|
||||
@@ -350,6 +350,12 @@
|
||||
"analytics.project.select": {
|
||||
"message": "Select projects"
|
||||
},
|
||||
"analytics.project.user": {
|
||||
"message": "{username}'s projects"
|
||||
},
|
||||
"analytics.project.your": {
|
||||
"message": "Your projects"
|
||||
},
|
||||
"analytics.query.filter.add": {
|
||||
"message": "Add filter"
|
||||
},
|
||||
@@ -2846,6 +2852,9 @@
|
||||
"profile.bio.fallback.user": {
|
||||
"message": "A Modrinth user."
|
||||
},
|
||||
"profile.button.analytics": {
|
||||
"message": "View user analytics"
|
||||
},
|
||||
"profile.button.billing": {
|
||||
"message": "Manage user billing"
|
||||
},
|
||||
|
||||
@@ -263,6 +263,15 @@
|
||||
action: () => $refs.userDetailsModal.show(),
|
||||
shown: auth.user && isStaff(auth.user),
|
||||
},
|
||||
{
|
||||
id: 'open-analytics',
|
||||
action: () =>
|
||||
navigateTo({
|
||||
path: '/dashboard/analytics',
|
||||
query: { user: user.username || user.id },
|
||||
}),
|
||||
shown: auth.user && isAdmin(auth.user),
|
||||
},
|
||||
{
|
||||
id: 'edit-role',
|
||||
action: () => openRoleEditModal(),
|
||||
@@ -297,6 +306,10 @@
|
||||
<InfoIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.infoButton) }}
|
||||
</template>
|
||||
<template #open-analytics>
|
||||
<ChartIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.analyticsButton) }}
|
||||
</template>
|
||||
<template #toggle-affiliate>
|
||||
<AffiliateIcon aria-hidden="true" />
|
||||
{{
|
||||
@@ -500,6 +513,7 @@ import {
|
||||
BadgeCheckIcon,
|
||||
BoxIcon,
|
||||
CalendarIcon,
|
||||
ChartIcon,
|
||||
CheckIcon,
|
||||
ClipboardCopyIcon,
|
||||
CurrencyIcon,
|
||||
@@ -692,6 +706,10 @@ const messages = defineMessages({
|
||||
id: 'profile.button.info',
|
||||
defaultMessage: 'View user details',
|
||||
},
|
||||
analyticsButton: {
|
||||
id: 'profile.button.analytics',
|
||||
defaultMessage: 'View user analytics',
|
||||
},
|
||||
setAffiliateButton: {
|
||||
id: 'profile.button.set-affiliate',
|
||||
defaultMessage: 'Set as affiliate',
|
||||
|
||||
@@ -11,9 +11,29 @@ import type {
|
||||
} from './analytics-types'
|
||||
|
||||
const MINECRAFT_JAVA_SERVER_PROJECT_TYPE = 'minecraft_java_server'
|
||||
const PLUGIN_PROJECT_TYPE = 'plugin'
|
||||
|
||||
export const UNKNOWN_ORGANIZATION_NAME = 'Organization'
|
||||
|
||||
function getProjectTypes(project: ProjectTypeMetadata): string[] {
|
||||
const projectTypes = new Set<string>()
|
||||
const projectType = project.project_type?.trim()
|
||||
if (projectType) {
|
||||
projectTypes.add(projectType)
|
||||
}
|
||||
|
||||
for (const types of [project.project_types, project.projectTypes]) {
|
||||
for (const type of types ?? []) {
|
||||
const projectType = type.trim()
|
||||
if (projectType) {
|
||||
projectTypes.add(projectType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...projectTypes]
|
||||
}
|
||||
|
||||
function isServerProject(project: ProjectTypeMetadata): boolean {
|
||||
if (project.project_type === MINECRAFT_JAVA_SERVER_PROJECT_TYPE) {
|
||||
return true
|
||||
@@ -28,6 +48,11 @@ export function isAnalyticsEligibleProject(
|
||||
return !isServerProject(project) && getProjectStatusFilterValue(project.status) !== 'draft'
|
||||
}
|
||||
|
||||
export function isPluginProject(project: ProjectTypeMetadata): boolean {
|
||||
const projectTypes = getProjectTypes(project)
|
||||
return projectTypes.length > 0 && projectTypes.every((type) => type === PLUGIN_PROJECT_TYPE)
|
||||
}
|
||||
|
||||
export function getSingleQueryValue(value: unknown): string | undefined {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined
|
||||
@@ -47,6 +72,7 @@ export function toAnalyticsDashboardProject(
|
||||
downloads: project.downloads ?? 0,
|
||||
status: getProjectStatusFilterValue(project.status),
|
||||
publishedAt: project.published ?? undefined,
|
||||
projectTypes: getProjectTypes(project),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ export type MutableRouteQuery = Record<
|
||||
export type ProjectTypeMetadata = {
|
||||
project_type?: string | null
|
||||
project_types?: readonly string[] | null
|
||||
projectTypes?: readonly string[] | null
|
||||
}
|
||||
|
||||
export type AnalyticsProjectFetchRequest = Labrinth.Analytics.v3.FetchRequest & {
|
||||
@@ -123,6 +124,7 @@ export interface AnalyticsDashboardProject {
|
||||
downloads: number
|
||||
status: ProjectStatusFilterValue
|
||||
publishedAt?: string
|
||||
projectTypes: string[]
|
||||
}
|
||||
|
||||
export interface AnalyticsDashboardProjectGroup {
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
getSingleQueryValue,
|
||||
getUniqueAnalyticsDashboardProjects,
|
||||
isAnalyticsEligibleProject,
|
||||
isPluginProject,
|
||||
toAnalyticsDashboardProject,
|
||||
UNKNOWN_ORGANIZATION_NAME,
|
||||
} from './analytics-project-utils'
|
||||
@@ -169,6 +170,11 @@ export interface AnalyticsDashboardContextValue {
|
||||
hasProjectContext: ComputedRef<boolean>
|
||||
projectGroups: ComputedRef<AnalyticsDashboardProjectGroup[]>
|
||||
projects: ComputedRef<AnalyticsDashboardProject[]>
|
||||
dashboardUserProjectIds: ComputedRef<string[]>
|
||||
dashboardOrganizationProjectIds: ComputedRef<string[]>
|
||||
defaultProjectIds: ComputedRef<string[]>
|
||||
isUsingDashboardUserOverride: ComputedRef<boolean>
|
||||
dashboardProjectUserName: ComputedRef<string>
|
||||
selectedProjectIds: Ref<string[]>
|
||||
selectedTimeframeMode: Ref<AnalyticsTimeframeMode>
|
||||
selectedTimeframe: Ref<AnalyticsTimeframePreset>
|
||||
@@ -198,6 +204,7 @@ export interface AnalyticsDashboardContextValue {
|
||||
versionProjectIconUrlsById: ComputedRef<Map<string, string>>
|
||||
projectStatusById: ComputedRef<Map<string, ProjectStatusFilterValue>>
|
||||
availableProjectStatuses: ComputedRef<ProjectStatusFilterValue[]>
|
||||
availableProjectDownloadsById: ComputedRef<Map<string, number>>
|
||||
projectDownloadsById: ComputedRef<Map<string, number>>
|
||||
projectVersionDownloadsById: ComputedRef<Map<string, number>>
|
||||
gameVersionDownloadsByVersion: ComputedRef<Map<string, number>>
|
||||
@@ -519,6 +526,45 @@ export function createAnalyticsDashboardContext(
|
||||
)
|
||||
|
||||
const availableProjectIds = computed(() => projects.value.map((project) => project.id))
|
||||
const dashboardUserProjectIds = computed(() => {
|
||||
if (!shouldFetchDashboardAllProjects.value) {
|
||||
return [...availableProjectIds.value]
|
||||
}
|
||||
|
||||
const response = dashboardAllProjects.value
|
||||
if (!response) {
|
||||
return []
|
||||
}
|
||||
|
||||
const availableProjectIdSet = new Set(availableProjectIds.value)
|
||||
return response.projects
|
||||
.filter(
|
||||
(project) => !getProjectOrganizationId(project) && availableProjectIdSet.has(project.id),
|
||||
)
|
||||
.map((project) => project.id)
|
||||
})
|
||||
const dashboardOrganizationProjectIds = computed(() => {
|
||||
if (!shouldFetchDashboardAllProjects.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
const response = dashboardAllProjects.value
|
||||
if (!response) {
|
||||
return []
|
||||
}
|
||||
|
||||
const availableProjectIdSet = new Set(availableProjectIds.value)
|
||||
return response.projects
|
||||
.filter(
|
||||
(project) => getProjectOrganizationId(project) && availableProjectIdSet.has(project.id),
|
||||
)
|
||||
.map((project) => project.id)
|
||||
})
|
||||
const defaultProjectIds = computed(() =>
|
||||
dashboardOrganizationProjectIds.value.length > 0 && dashboardUserProjectIds.value.length > 0
|
||||
? dashboardUserProjectIds.value
|
||||
: availableProjectIds.value,
|
||||
)
|
||||
const projectNamesById = computed(
|
||||
() => new Map(projects.value.map((project) => [project.id, project.name])),
|
||||
)
|
||||
@@ -537,6 +583,7 @@ export function createAnalyticsDashboardContext(
|
||||
const presentStatuses = new Set(projects.value.map((project) => project.status))
|
||||
return PROJECT_STATUS_FILTER_VALUES.filter((status) => presentStatuses.has(status))
|
||||
})
|
||||
const sortedAvailableProjectIds = computed(() => sortStringValues(availableProjectIds.value))
|
||||
const sortedSelectedProjectIds = computed(() => sortStringValues(selectedProjectIds.value))
|
||||
const filterOptionProjectSources = computed<AnalyticsProjectVersionSource[] | null>(() => {
|
||||
if (hasProjectContext.value && options.projectPageContext) {
|
||||
@@ -642,6 +689,7 @@ export function createAnalyticsDashboardContext(
|
||||
selectedFilters: selectedFilters.value,
|
||||
},
|
||||
availableProjectIds.value,
|
||||
defaultProjectIds.value,
|
||||
)
|
||||
const isGraphDefault = isAnalyticsGraphStateDefault(
|
||||
{
|
||||
@@ -674,17 +722,35 @@ export function createAnalyticsDashboardContext(
|
||||
allTimeStartTimestamp: analyticsAllTimeStartDate.value.getTime(),
|
||||
}) > REVENUE_MIN_TIMEFRAME_MS,
|
||||
)
|
||||
const isPlaytimeAvailableForProjectSelection = computed(() => {
|
||||
const selectedProjectIdSet = new Set(selectedProjectIds.value)
|
||||
const selectedProjects = projects.value.filter((project) =>
|
||||
selectedProjectIdSet.has(project.id),
|
||||
)
|
||||
|
||||
return (
|
||||
selectedProjects.length === 0 || selectedProjects.some((project) => !isPluginProject(project))
|
||||
)
|
||||
})
|
||||
|
||||
function isAnalyticsDashboardStatAvailableForTimeframe(stat: AnalyticsDashboardStat): boolean {
|
||||
return stat !== 'revenue' || isRevenueTimeframeAvailable.value
|
||||
}
|
||||
|
||||
function isAnalyticsDashboardStatAvailableForProjectSelection(
|
||||
stat: AnalyticsDashboardStat,
|
||||
): boolean {
|
||||
return stat !== 'playtime' || isPlaytimeAvailableForProjectSelection.value
|
||||
}
|
||||
|
||||
function getRelevantAnalyticsDashboardStats(
|
||||
breakdowns: readonly AnalyticsBreakdownPreset[],
|
||||
filters: AnalyticsSelectedFilters = selectedFilters.value,
|
||||
): readonly AnalyticsDashboardStat[] {
|
||||
return getEnabledAnalyticsStatsForState(breakdowns, filters).filter((stat) =>
|
||||
isAnalyticsDashboardStatAvailableForTimeframe(stat),
|
||||
return getEnabledAnalyticsStatsForState(breakdowns, filters).filter(
|
||||
(stat) =>
|
||||
isAnalyticsDashboardStatAvailableForTimeframe(stat) &&
|
||||
isAnalyticsDashboardStatAvailableForProjectSelection(stat),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -741,6 +807,7 @@ export function createAnalyticsDashboardContext(
|
||||
selectedGraphDatasetIds,
|
||||
},
|
||||
availableProjectIds,
|
||||
defaultProjectIds,
|
||||
sanitizeSelectedFilters: sanitizeAnalyticsSelectedFiltersForContext,
|
||||
})
|
||||
|
||||
@@ -768,7 +835,13 @@ export function createAnalyticsDashboardContext(
|
||||
}
|
||||
|
||||
watch(
|
||||
[selectedBreakdowns, selectedFilters, activeStat, isRevenueTimeframeAvailable],
|
||||
[
|
||||
selectedBreakdowns,
|
||||
selectedFilters,
|
||||
activeStat,
|
||||
isRevenueTimeframeAvailable,
|
||||
isPlaytimeAvailableForProjectSelection,
|
||||
],
|
||||
([nextBreakdowns, nextFilters, nextActiveStat]) => {
|
||||
if (isAnalyticsDashboardStatRelevant(nextActiveStat, nextBreakdowns, nextFilters)) {
|
||||
return
|
||||
@@ -846,9 +919,10 @@ export function createAnalyticsDashboardContext(
|
||||
return
|
||||
}
|
||||
|
||||
const availableProjectIds = new Set(nextProjects.map((project) => project.id))
|
||||
const nextAvailableProjectIds = nextProjects.map((project) => project.id)
|
||||
const availableProjectIds = new Set(nextAvailableProjectIds)
|
||||
if (!hasExplicitProjectSelectionQuery.value) {
|
||||
const nextSelectedProjectIds = nextProjects.map((project) => project.id)
|
||||
const nextSelectedProjectIds = [...defaultProjectIds.value]
|
||||
syncSelectedBreakdownsForProjectSelection(nextSelectedProjectIds)
|
||||
syncProjectEventsVisibilityForProjectSelection(nextSelectedProjectIds)
|
||||
if (!areStringArraysEqual(selectedProjectIds.value, nextSelectedProjectIds)) {
|
||||
@@ -858,9 +932,14 @@ export function createAnalyticsDashboardContext(
|
||||
return
|
||||
}
|
||||
|
||||
const retainedSelection = selectedProjectIds.value.filter((id) => availableProjectIds.has(id))
|
||||
const queryProjectSelection = readAnalyticsQueryBuilderState(
|
||||
route.query,
|
||||
nextAvailableProjectIds,
|
||||
defaultProjectIds.value,
|
||||
).selectedProjectIds
|
||||
const retainedSelection = queryProjectSelection.filter((id) => availableProjectIds.has(id))
|
||||
const nextSelectedProjectIds =
|
||||
retainedSelection.length > 0 ? retainedSelection : nextProjects.map((project) => project.id)
|
||||
retainedSelection.length > 0 ? retainedSelection : [...defaultProjectIds.value]
|
||||
|
||||
syncSelectedBreakdownsForProjectSelection(nextSelectedProjectIds)
|
||||
syncProjectEventsVisibilityForProjectSelection(nextSelectedProjectIds)
|
||||
@@ -916,6 +995,7 @@ export function createAnalyticsDashboardContext(
|
||||
selectedBreakdowns,
|
||||
selectedFilters,
|
||||
availableProjectIds,
|
||||
defaultProjectIds,
|
||||
],
|
||||
() => {
|
||||
syncQueryBuilderRouteQuery()
|
||||
@@ -1090,6 +1170,19 @@ export function createAnalyticsDashboardContext(
|
||||
|
||||
return buildAnalyticsFacetsRequest(sortedSelectedProjectIds.value, nextFetchRequest.time_range)
|
||||
})
|
||||
const availableProjectDownloadCountRequest = computed<Labrinth.Analytics.v3.FetchRequest | null>(
|
||||
() => {
|
||||
const nextFetchRequest = fetchRequest.value
|
||||
if (!nextFetchRequest || sortedAvailableProjectIds.value.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return buildAnalyticsFacetsRequest(
|
||||
sortedAvailableProjectIds.value,
|
||||
nextFetchRequest.time_range,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const {
|
||||
data: analyticsFacetsData,
|
||||
@@ -1148,6 +1241,36 @@ export function createAnalyticsDashboardContext(
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const { data: availableProjectDownloadCountTimeSlices } = useQuery({
|
||||
queryKey: computed(() => [
|
||||
'analytics',
|
||||
'dashboard',
|
||||
analyticsQueryUserId.value,
|
||||
'filter-options',
|
||||
'available-project-download-counts',
|
||||
availableProjectDownloadCountRequest.value,
|
||||
queryRefreshTimestamp.value,
|
||||
]),
|
||||
queryFn: () => {
|
||||
const nextRequest = availableProjectDownloadCountRequest.value
|
||||
if (!isAnalyticsFetchRequestReady(nextRequest)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return fetchAnalyticsTimeSlices(nextRequest, (request) =>
|
||||
client.labrinth.analytics_v3.fetch(request),
|
||||
)
|
||||
},
|
||||
enabled: computed(
|
||||
() =>
|
||||
hasCompletedAnalyticsLoading.value &&
|
||||
isAnalyticsFetchRequestReady(availableProjectDownloadCountRequest.value),
|
||||
),
|
||||
placeholderData: [],
|
||||
gcTime: ANALYTICS_FILTER_OPTIONS_GC_TIME_MS,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const { data: analyticsDownloadCountTimeSlices } = useQuery({
|
||||
queryKey: computed(() => [
|
||||
'analytics',
|
||||
@@ -1390,6 +1513,9 @@ export function createAnalyticsDashboardContext(
|
||||
const countTimeSlices = analyticsDownloadCountTimeSlices.value ?? []
|
||||
return countTimeSlices.length > 0 ? countTimeSlices : timeSlices.value
|
||||
})
|
||||
const availableProjectDownloadsById = computed(() =>
|
||||
getProjectDownloadsByIdFromTimeSlices(availableProjectDownloadCountTimeSlices.value ?? []),
|
||||
)
|
||||
const projectDownloadsById = computed(() =>
|
||||
getProjectDownloadsByIdFromTimeSlices(downloadCountTimeSlices.value),
|
||||
)
|
||||
@@ -1484,7 +1610,10 @@ export function createAnalyticsDashboardContext(
|
||||
return
|
||||
}
|
||||
|
||||
const defaultQueryState = buildDefaultAnalyticsQueryBuilderState(availableProjectIds.value)
|
||||
const defaultQueryState = buildDefaultAnalyticsQueryBuilderState(
|
||||
availableProjectIds.value,
|
||||
defaultProjectIds.value,
|
||||
)
|
||||
const defaultGraphState = buildDefaultAnalyticsGraphState(defaultQueryState.selectedProjectIds)
|
||||
|
||||
selectedProjectIds.value = defaultQueryState.selectedProjectIds
|
||||
@@ -1550,6 +1679,11 @@ export function createAnalyticsDashboardContext(
|
||||
hasProjectContext,
|
||||
projectGroups,
|
||||
projects,
|
||||
dashboardUserProjectIds,
|
||||
dashboardOrganizationProjectIds,
|
||||
defaultProjectIds,
|
||||
isUsingDashboardUserOverride,
|
||||
dashboardProjectUserName: effectiveUsername,
|
||||
selectedProjectIds,
|
||||
selectedTimeframeMode,
|
||||
selectedTimeframe,
|
||||
@@ -1579,6 +1713,7 @@ export function createAnalyticsDashboardContext(
|
||||
versionProjectIconUrlsById,
|
||||
projectStatusById,
|
||||
availableProjectStatuses,
|
||||
availableProjectDownloadsById,
|
||||
projectDownloadsById,
|
||||
projectVersionDownloadsById,
|
||||
gameVersionDownloadsByVersion,
|
||||
|
||||
Reference in New Issue
Block a user