You've already forked AstralRinth
fix: analytics events page not in admin dropdown (#6352)
* fix: analytics events page not in admin dropdown * pnpm prepr * fix: add clearing date picker * fix: date picker positioning not using rendered height
This commit is contained in:
@@ -383,6 +383,12 @@
|
||||
action: (event) => $refs.modal_batch_credit.show(event),
|
||||
shown: isAdmin(auth.user),
|
||||
},
|
||||
{
|
||||
id: 'analytics-events',
|
||||
color: 'primary',
|
||||
link: '/admin/analytics/events',
|
||||
shown: isAdmin(auth.user),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<ModrinthIcon aria-hidden="true" />
|
||||
@@ -417,6 +423,9 @@
|
||||
<template #servers-nodes>
|
||||
<ServerIcon aria-hidden="true" /> Credit server nodes
|
||||
</template>
|
||||
<template #analytics-events>
|
||||
<ChartIcon aria-hidden="true" /> {{ formatMessage(messages.analyticsEvents) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
@@ -954,6 +963,10 @@ const messages = defineMessages({
|
||||
id: 'layout.action.manage-affiliates',
|
||||
defaultMessage: 'Manage affiliate links',
|
||||
},
|
||||
analyticsEvents: {
|
||||
id: 'layout.action.analytics-events',
|
||||
defaultMessage: 'Analytics events',
|
||||
},
|
||||
newProject: {
|
||||
id: 'layout.action.new-project',
|
||||
defaultMessage: 'New project',
|
||||
|
||||
@@ -2315,6 +2315,9 @@
|
||||
"landing.subheading": {
|
||||
"message": "Discover, play, and share Minecraft content through our open-source platform built for the community."
|
||||
},
|
||||
"layout.action.analytics-events": {
|
||||
"message": "Analytics events"
|
||||
},
|
||||
"layout.action.change-theme": {
|
||||
"message": "Change theme"
|
||||
},
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
placeholder="Select start..."
|
||||
input-class="w-full"
|
||||
wrapper-class="w-full"
|
||||
clearable
|
||||
show-today
|
||||
/>
|
||||
</div>
|
||||
@@ -89,6 +90,7 @@
|
||||
placeholder="Select end..."
|
||||
input-class="w-full"
|
||||
wrapper-class="w-full"
|
||||
clearable
|
||||
show-today
|
||||
/>
|
||||
</div>
|
||||
@@ -214,7 +216,11 @@
|
||||
|
||||
<template #empty-state>
|
||||
<div class="flex h-64 items-center justify-center text-secondary">
|
||||
{{ isLoadingEvents ? 'Loading analytics events...' : 'No results.' }}
|
||||
<div v-if="isFetchingEvents" class="flex items-center gap-2">
|
||||
<SpinnerIcon class="size-5 animate-spin" aria-hidden="true" />
|
||||
Loading
|
||||
</div>
|
||||
<template v-else>No results.</template>
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
@@ -224,7 +230,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { EditIcon, ExternalIcon, PlusIcon, SaveIcon, SearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import {
|
||||
EditIcon,
|
||||
ExternalIcon,
|
||||
PlusIcon,
|
||||
SaveIcon,
|
||||
SearchIcon,
|
||||
SpinnerIcon,
|
||||
TrashIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
ConfirmModal,
|
||||
@@ -322,7 +336,7 @@ let resetFormTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
const {
|
||||
data: analyticsEvents,
|
||||
error: eventsError,
|
||||
isLoading: isLoadingEvents,
|
||||
isFetching: isFetchingEvents,
|
||||
} = useQuery({
|
||||
queryKey: analyticsEventsQueryKey,
|
||||
queryFn: () => client.labrinth.analytics_v3.getEvents(),
|
||||
@@ -439,7 +453,7 @@ function openEditModal(event: Labrinth.Analytics.v3.AnalyticsEvent) {
|
||||
title: event.title,
|
||||
announcementUrl: event.announcement_url ?? '',
|
||||
startsAt: getDateTimeInputValue(event.starts),
|
||||
endsAt: getDateTimeInputValue(event.ends),
|
||||
endsAt: isEventDateRange(event) ? getDateTimeInputValue(event.ends) : '',
|
||||
metricKinds: event.for_metric_kind?.length ? [...event.for_metric_kind] : [...allMetricKinds],
|
||||
}
|
||||
committedAnnouncementUrl.value = event.announcement_url ?? ''
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<button
|
||||
v-if="hasClearButton"
|
||||
type="button"
|
||||
class="absolute right-0.5 z-[1] touch-manipulation cursor-pointer select-none border-none bg-transparent p-2 text-secondary transition-colors hover:text-contrast"
|
||||
class="absolute right-0.5 top-px z-[1] touch-manipulation cursor-pointer select-none border-none bg-transparent p-2 text-secondary transition-colors hover:text-contrast"
|
||||
aria-label="Clear date"
|
||||
@click.stop="clearValue"
|
||||
>
|
||||
@@ -161,7 +161,7 @@ const props = withDefaults(
|
||||
mode: 'single',
|
||||
showMonths: 1,
|
||||
time24hr: false,
|
||||
clearable: true,
|
||||
clearable: false,
|
||||
placeholder: 'Enter date',
|
||||
showIcon: true,
|
||||
showToday: false,
|
||||
@@ -199,6 +199,7 @@ let originalInputFocus: HTMLInputElement['focus'] | null = null
|
||||
let suppressNextInputFocusScroll = false
|
||||
const calendarBaseClass = 'modrinth-date-picker-calendar'
|
||||
const twoCalendarClass = 'has-two-calendars'
|
||||
const calendarPositionGap = 2
|
||||
const calendarStateClasses = [
|
||||
'calendar-only',
|
||||
'show-today',
|
||||
@@ -1084,6 +1085,83 @@ function syncInputFocusScrollSuppression() {
|
||||
inputFocusScrollSuppressionTarget = target
|
||||
}
|
||||
|
||||
function setCalendarPositionClass(container: HTMLElement, className: string, isEnabled: boolean) {
|
||||
container.classList.toggle(className, isEnabled)
|
||||
}
|
||||
|
||||
function getCalendarPositionParts() {
|
||||
const parts = props.position.split(' ')
|
||||
return {
|
||||
vertical: parts[0] ?? 'auto',
|
||||
horizontal: parts[1] ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function getCalendarHeight(container: HTMLElement) {
|
||||
const height = container.getBoundingClientRect().height
|
||||
if (height > 0) return height
|
||||
|
||||
return Array.from(container.children).reduce(
|
||||
(total, child) => total + (child instanceof HTMLElement ? child.offsetHeight : 0),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
function positionCalendar(instance: Instance, customPositionElement?: HTMLElement) {
|
||||
const container = instance.calendarContainer
|
||||
const positionElement = customPositionElement ?? instance._positionElement
|
||||
if (!container || !positionElement) return
|
||||
|
||||
const calendarHeight = getCalendarHeight(container)
|
||||
const calendarWidth = container.offsetWidth
|
||||
const { vertical, horizontal } = getCalendarPositionParts()
|
||||
const inputBounds = positionElement.getBoundingClientRect()
|
||||
const distanceFromBottom = window.innerHeight - inputBounds.bottom
|
||||
const showOnTop =
|
||||
vertical === 'above' ||
|
||||
(vertical !== 'below' &&
|
||||
distanceFromBottom < calendarHeight &&
|
||||
inputBounds.top > calendarHeight)
|
||||
|
||||
const top =
|
||||
window.pageYOffset +
|
||||
inputBounds.top +
|
||||
(showOnTop
|
||||
? -calendarHeight - calendarPositionGap
|
||||
: positionElement.offsetHeight + calendarPositionGap)
|
||||
let left = window.pageXOffset + inputBounds.left
|
||||
let isCenter = false
|
||||
let isRight = false
|
||||
|
||||
if (horizontal === 'center') {
|
||||
left -= (calendarWidth - inputBounds.width) / 2
|
||||
isCenter = true
|
||||
} else if (horizontal === 'right') {
|
||||
left -= calendarWidth - inputBounds.width
|
||||
isRight = true
|
||||
}
|
||||
|
||||
const viewportLeft = window.pageXOffset
|
||||
const viewportRight = viewportLeft + document.documentElement.clientWidth
|
||||
const isOverflowingRight = left + calendarWidth > viewportRight
|
||||
const clampedLeft = Math.min(
|
||||
Math.max(viewportLeft, left),
|
||||
Math.max(viewportLeft, viewportRight - calendarWidth),
|
||||
)
|
||||
|
||||
setCalendarPositionClass(container, 'arrowTop', !showOnTop)
|
||||
setCalendarPositionClass(container, 'arrowBottom', showOnTop)
|
||||
setCalendarPositionClass(container, 'arrowLeft', !isCenter && !isRight)
|
||||
setCalendarPositionClass(container, 'arrowCenter', isCenter)
|
||||
setCalendarPositionClass(container, 'arrowRight', isRight)
|
||||
setCalendarPositionClass(container, 'rightMost', isOverflowingRight)
|
||||
setCalendarPositionClass(container, 'centerMost', false)
|
||||
|
||||
container.style.top = `${top}px`
|
||||
container.style.left = `${clampedLeft}px`
|
||||
container.style.right = 'auto'
|
||||
}
|
||||
|
||||
const resolvedDateFormat = computed(
|
||||
() => props.dateFormat ?? (props.enableTime ? 'Y-m-d H:i' : 'Y-m-d'),
|
||||
)
|
||||
@@ -1104,12 +1182,7 @@ const selectedDates = computed(() => {
|
||||
})
|
||||
|
||||
const hasClearButton = computed(
|
||||
() =>
|
||||
!props.calendarOnly &&
|
||||
props.clearable &&
|
||||
!props.disabled &&
|
||||
!props.readonly &&
|
||||
selectedDates.value.length > 0,
|
||||
() => !props.calendarOnly && props.clearable && !props.disabled && selectedDates.value.length > 0,
|
||||
)
|
||||
|
||||
const inputClasses = computed(() => [
|
||||
@@ -1143,6 +1216,7 @@ watch(
|
||||
props.calendarOnly,
|
||||
props.closeOnSelect,
|
||||
props.position,
|
||||
props.clearable,
|
||||
],
|
||||
() => {
|
||||
if (!picker.value) return
|
||||
@@ -1399,7 +1473,7 @@ function flatpickrOptions(): Options {
|
||||
mode: props.mode,
|
||||
noCalendar: false,
|
||||
nextArrow: chevronRightIcon,
|
||||
position: props.position,
|
||||
position: positionCalendar,
|
||||
prevArrow: chevronLeftIcon,
|
||||
showMonths: resolvedShowMonths.value,
|
||||
static: false,
|
||||
@@ -1480,7 +1554,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar.arrowBottom) {
|
||||
margin-top: -2.5rem;
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.modrinth-date-picker.calendar-only {
|
||||
|
||||
@@ -52,17 +52,30 @@ export const Clearable: Story = {
|
||||
render: () => ({
|
||||
components: { DatePicker },
|
||||
setup() {
|
||||
const value = ref('2026-04-27')
|
||||
return { value }
|
||||
const emptyValue = ref(null)
|
||||
const selectedValue = ref('2026-04-27')
|
||||
return { emptyValue, selectedValue }
|
||||
},
|
||||
template: /* html */ `
|
||||
<div class="flex max-w-sm flex-col gap-2">
|
||||
<DatePicker
|
||||
v-model="value"
|
||||
wrapperClass="w-[300px]"
|
||||
placeholder="Select a date..."
|
||||
/>
|
||||
<p class="text-sm text-secondary">Selected value: {{ value || 'None' }}</p>
|
||||
<div class="flex max-w-sm flex-col gap-5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<DatePicker
|
||||
v-model="emptyValue"
|
||||
wrapperClass="w-[300px]"
|
||||
clearable
|
||||
placeholder="Button hidden while empty..."
|
||||
/>
|
||||
<p class="text-sm text-secondary">Empty value: {{ emptyValue || 'None' }}</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<DatePicker
|
||||
v-model="selectedValue"
|
||||
wrapperClass="w-[300px]"
|
||||
clearable
|
||||
placeholder="Select a date..."
|
||||
/>
|
||||
<p class="text-sm text-secondary">Selected value: {{ selectedValue || 'None' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user