Moderation Changes Phase 2.-1 (#6833)

* better default state handling

* make next stage button work for going to next project

* fix skill issue

* actually fix initial values this time,
make single line inputs with varying lengths (text input and dropdown) use their widest possible length instead of whatever the hell they used to be

* loaders in progress

* loaders and versions test (probably dont use them as the messages suck and idrk what they should do quick fix/message wise)

* WIP the wip

* good enough for now slug taken improvements

* trim all those extra newlines

* viewedness

* batch prefetch queue + technically slightly faster checklist init

* show all valid alternative project types regardless of current project type

* Hide moderation permissions stage when it should be, and I'm sure this won't cause any issues

* Hide moderation permissions stage when it should be, and I'm sure this won't cause any issues

* gimme them versions

* better moderation permission stage shown check?

* betterer moderation permission stage shown check?

* bettererer moderation permission stage shown check? + also began deleting legacy checklist types

* fix certain stages being able to be considered viewed before being viewed

* more removals + i forgot to remove an import last commit

* changed nothing™️

* remove severities + bandaid post-approval message priorities + dependencies

* fix message preview tooltips

* next stage = next project when done

* insufficient description custom fix

* fix that thing coolbot told me was broken I forgot what it was oops

* oh there was a second bug there

* make alternate versions and incorrect project type required cuz they are

* re-navigate button also damn that's a lot of other stuff that pnpm fix... fixed?

* you cant just quick fix a slug into a taken one

* maybe lets not just try and set a slug to something entirely invalid?

* what if we just didn't drop ur queue :smart:

* more queue upgrades

* tweaks

* I love unbreaking things

* un-nuked renavigate button

* prepr

* prepr worked this time i think

* undo more mistakes

* i love intellij refactoring

* ok i think we're good
This commit is contained in:
chyz
2026-08-07 23:40:11 +00:00
committed by GitHub
parent e4cbec6b2c
commit 5072c1d298
77 changed files with 3596 additions and 3502 deletions
@@ -312,7 +312,7 @@ defineExpose({ open: openMenu, close: closeMenu })
v-if="isOpen"
:id="menuId"
ref="panelElement"
class="fixed isolate z-[9999] flex min-w-48 flex-col gap-1 rounded-[14px] bg-surface-3 p-2 shadow-lg ring-1 ring-surface-5"
class="fixed isolate z-[9999] rounded-[14px] bg-surface-3 shadow-lg ring-1 ring-surface-5"
:style="[panelStyle, { transformOrigin: menuTransformOrigin }]"
role="menu"
:aria-label="props.label"
@@ -326,76 +326,85 @@ defineExpose({ open: openMenu, close: closeMenu })
:data-side="resolvedSide"
:style="anchorStyle"
/>
<template v-for="(option, index) in visibleOptions" :key="option.id ?? `divider-${index}`">
<div v-if="isDivider(option)" role="separator" class="my-1 h-px bg-surface-5" />
<RouterLink
v-else-if="isLink(option) && option.to !== undefined && !option.disabled"
v-tooltip="option.tooltip"
:to="option.to"
:class="menuItemClasses"
:style="getMenuItemStyle(option)"
:data-tone="option.tone && option.tone !== 'default' ? option.tone : undefined"
:data-hover-filled="option.hoverFilled || option.hoverFilledOnly || undefined"
:data-hover-filled-only="option.hoverFilledOnly || undefined"
role="menuitem"
tabindex="-1"
@click="handleLink(option, $event)"
@keydown="handleLinkKeydown(option, $event)"
@focus="selectedIndex = getMenuItems().indexOf($event.currentTarget as HTMLElement)"
<div
data-anchored-scroll-region
class="flex min-w-48 flex-col gap-1 overflow-y-auto p-2"
:style="{ maxHeight: panelStyle.maxHeight }"
>
<template
v-for="(option, index) in visibleOptions"
:key="option.id ?? `divider-${index}`"
>
<slot :name="option.id" :option="option">
<component :is="option.icon" v-if="option.icon" aria-hidden="true" />
{{ option.label }}
</slot>
</RouterLink>
<div v-if="isDivider(option)" role="separator" class="my-1 h-px bg-surface-5" />
<a
v-else-if="isLink(option)"
v-tooltip="option.tooltip"
:href="option.disabled ? undefined : option.href"
:target="option.target"
:rel="option.rel ?? (option.target === '_blank' ? 'noopener noreferrer' : undefined)"
:download="option.download"
:aria-disabled="option.disabled || undefined"
:class="menuItemClasses"
:style="getMenuItemStyle(option)"
:data-tone="option.tone && option.tone !== 'default' ? option.tone : undefined"
:data-hover-filled="option.hoverFilled || option.hoverFilledOnly || undefined"
:data-hover-filled-only="option.hoverFilledOnly || undefined"
role="menuitem"
tabindex="-1"
@click="handleLink(option, $event)"
@keydown="handleLinkKeydown(option, $event)"
@focus="selectedIndex = getMenuItems().indexOf($event.currentTarget as HTMLElement)"
>
<slot :name="option.id" :option="option">
<component :is="option.icon" v-if="option.icon" aria-hidden="true" />
{{ option.label }}
</slot>
</a>
<RouterLink
v-else-if="isLink(option) && option.to !== undefined && !option.disabled"
v-tooltip="option.tooltip"
:to="option.to"
:class="menuItemClasses"
:style="getMenuItemStyle(option)"
:data-tone="option.tone && option.tone !== 'default' ? option.tone : undefined"
:data-hover-filled="option.hoverFilled || option.hoverFilledOnly || undefined"
:data-hover-filled-only="option.hoverFilledOnly || undefined"
role="menuitem"
tabindex="-1"
@click="handleLink(option, $event)"
@keydown="handleLinkKeydown(option, $event)"
@focus="selectedIndex = getMenuItems().indexOf($event.currentTarget as HTMLElement)"
>
<slot :name="option.id" :option="option">
<component :is="option.icon" v-if="option.icon" aria-hidden="true" />
{{ option.label }}
</slot>
</RouterLink>
<button
v-else
v-tooltip="option.tooltip"
type="button"
:aria-disabled="option.disabled || undefined"
:class="menuItemClasses"
:style="getMenuItemStyle(option)"
:data-tone="option.tone && option.tone !== 'default' ? option.tone : undefined"
:data-hover-filled="option.hoverFilled || option.hoverFilledOnly || undefined"
:data-hover-filled-only="option.hoverFilledOnly || undefined"
role="menuitem"
tabindex="-1"
@click="handleAction(option, $event)"
@focus="selectedIndex = getMenuItems().indexOf($event.currentTarget as HTMLElement)"
>
<slot :name="option.id" :option="option">
<component :is="option.icon" v-if="option.icon" aria-hidden="true" />
{{ option.label }}
</slot>
</button>
</template>
<a
v-else-if="isLink(option)"
v-tooltip="option.tooltip"
:href="option.disabled ? undefined : option.href"
:target="option.target"
:rel="option.rel ?? (option.target === '_blank' ? 'noopener noreferrer' : undefined)"
:download="option.download"
:aria-disabled="option.disabled || undefined"
:class="menuItemClasses"
:style="getMenuItemStyle(option)"
:data-tone="option.tone && option.tone !== 'default' ? option.tone : undefined"
:data-hover-filled="option.hoverFilled || option.hoverFilledOnly || undefined"
:data-hover-filled-only="option.hoverFilledOnly || undefined"
role="menuitem"
tabindex="-1"
@click="handleLink(option, $event)"
@keydown="handleLinkKeydown(option, $event)"
@focus="selectedIndex = getMenuItems().indexOf($event.currentTarget as HTMLElement)"
>
<slot :name="option.id" :option="option">
<component :is="option.icon" v-if="option.icon" aria-hidden="true" />
{{ option.label }}
</slot>
</a>
<button
v-else
v-tooltip="option.tooltip"
type="button"
:aria-disabled="option.disabled || undefined"
:class="menuItemClasses"
:style="getMenuItemStyle(option)"
:data-tone="option.tone && option.tone !== 'default' ? option.tone : undefined"
:data-hover-filled="option.hoverFilled || option.hoverFilledOnly || undefined"
:data-hover-filled-only="option.hoverFilledOnly || undefined"
role="menuitem"
tabindex="-1"
@click="handleAction(option, $event)"
@focus="selectedIndex = getMenuItems().indexOf($event.currentTarget as HTMLElement)"
>
<slot :name="option.id" :option="option">
<component :is="option.icon" v-if="option.icon" aria-hidden="true" />
{{ option.label }}
</slot>
</button>
</template>
</div>
</div>
</Transition>
</Teleport>
@@ -128,7 +128,7 @@ defineExpose({ open: openMenu, close: closeMenu })
v-if="isOpen"
:id="panelId"
ref="panelElement"
class="fixed isolate z-[9999] rounded-[14px] bg-surface-3 p-4 text-primary shadow-lg ring-1 ring-surface-5"
class="fixed isolate z-[9999] overflow-y-auto rounded-[14px] bg-surface-3 p-4 text-primary shadow-lg ring-1 ring-surface-5"
:style="panelStyle"
:role="props.panelRole"
:aria-label="props.label"
@@ -474,7 +474,6 @@ import {
import {
type GameVersionTag,
getVersionGroupsForDisplay,
type Version,
type VersionDisplayGroup,
} from '@modrinth/utils'
import { Menu } from 'floating-vue'
@@ -500,15 +499,12 @@ const formatBytes = useFormatBytes()
const MAX_GAME_VERSION_TAGS = 5
const MAX_PLATFORM_TAGS = 3
type VersionWithDisplayUrlEnding = Version & {
type VersionWithDisplayUrlEnding = Labrinth.Versions.v3.Version & {
displayUrlEnding: string
environment?: Labrinth.Projects.v3.Environment
mrpack_loaders?: string[]
}
type DisplayVersion = VersionWithDisplayUrlEnding & {
noModLoader: boolean
files_missing_attribution?: boolean
}
type VersionTableColumn =
@@ -536,7 +532,7 @@ const props = withDefaults(
currentMember?: boolean
loaders: Labrinth.Tags.v2.Loader[]
gameVersions: GameVersionTag[]
versionLink?: (version: Version) => string
versionLink?: (version: Labrinth.Versions.v3.Version) => string
openModal?: () => void
createVersionButtonSecondary?: boolean
}>(),
@@ -0,0 +1,28 @@
<template>
<AutoLink
:to="`/project/${project.slug}`"
:target="newTab ? '_blank' : undefined"
class="group inline-flex min-w-0 max-w-full items-center gap-1"
:style="{ color: `var(--color-${getProjectStatusColor(project.status)})` }"
>
<component :is="getProjectStatusIcon(project.status)" class="size-4 shrink-0" />
<span class="min-w-0 truncate font-medium group-hover:underline">{{ project.name }}</span>
</AutoLink>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { getProjectStatusColor, getProjectStatusIcon } from '../../utils'
import AutoLink from '../base/AutoLink.vue'
withDefaults(
defineProps<{
project: Pick<Labrinth.Projects.v3.Project, 'name' | 'slug' | 'status'>
newTab?: boolean
}>(),
{
newTab: false,
},
)
</script>
@@ -16,4 +16,5 @@ export { default as ProjectSidebarLinks } from './ProjectSidebarLinks.vue'
export { default as ProjectSidebarServerInfo } from './ProjectSidebarServerInfo.vue'
export { default as ProjectSidebarTags } from './ProjectSidebarTags.vue'
export { default as ProjectStatusBadge } from './ProjectStatusBadge.vue'
export { default as ProjectStatusLink } from './ProjectStatusLink.vue'
export { default as TagsOverflow } from './TagsOverflow.vue'
@@ -120,16 +120,17 @@
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { ChevronLeftIcon, FilterIcon, XCircleIcon, XIcon } from '@modrinth/assets'
import type { MultiSelectOption } from '@modrinth/ui'
import { Checkbox, formatLoader, FormattedTag, MultiSelect, TagItem, useVIntl } from '@modrinth/ui'
import type { GameVersionTag, Version } from '@modrinth/utils'
import type { GameVersionTag } from '@modrinth/utils'
import { computed, ref } from 'vue'
import type { LocationQueryValue } from 'vue-router'
import { useRoute } from 'vue-router'
const props = defineProps<{
versions: Version[]
versions: Labrinth.Versions.v3.Version[]
gameVersions: GameVersionTag[]
baseId?: string
}>()
+1 -1
View File
@@ -15,7 +15,7 @@ export interface ProjectPageContext {
allMembers: Ref<Labrinth.Projects.v3.TeamMember[]>
organization: Ref<Labrinth.Projects.v3.Organization | null>
// Lazy version loading (client-side only)
versions: Ref<Labrinth.Versions.v2.Version[] | null>
versions: Ref<Labrinth.Versions.v3.Version[] | null>
versionsLoading: Ref<boolean>
versionsLoaded: Ref<boolean>
// Lazy dependencies loading (client-side only)
+20
View File
@@ -89,6 +89,22 @@ export const PROJECT_STATUS_ICONS: Record<ProjectStatus, Component> = {
unknown: UnknownIcon,
}
// this should probably be abstracted or something idk
export type BadgeColor = 'red' | 'orange' | 'green' | 'blue' | 'purple' | 'gray'
export const PROJECT_STATUS_COLORS: Record<ProjectStatus, BadgeColor> = {
approved: 'blue',
unlisted: 'purple',
withheld: 'red',
private: 'gray',
scheduled: 'orange',
draft: 'gray',
archived: 'gray',
rejected: 'red',
processing: 'orange',
unknown: 'gray',
}
export const DIRECTORY_ICONS: Record<string, Component> = {
config: FolderOpenIcon,
world: FolderOpenIcon,
@@ -124,6 +140,10 @@ export function getProjectStatusIcon(status: ProjectStatus): Component {
return PROJECT_STATUS_ICONS[status] ?? UnknownIcon
}
export function getProjectStatusColor(status: ProjectStatus): BadgeColor {
return PROJECT_STATUS_COLORS[status] ?? `gray`
}
export function getDirectoryIcon(name: string): Component {
return DIRECTORY_ICONS[name.toLowerCase()] ?? DIRECTORY_ICONS._default
}
+40 -14
View File
@@ -4,8 +4,10 @@ import { nextTick, onUnmounted, ref, watch } from 'vue'
export type AnchoredTeleportPlacement =
| 'bottom-start'
| 'bottom-end'
| 'bottom-center'
| 'top-start'
| 'top-end'
| 'top-center'
| 'right-start'
| 'right-end'
| 'left-start'
@@ -37,40 +39,63 @@ export function useAnchoredTeleport(
if (!isOpen.value || !trigger.value || !panel.value) return
const triggerRect = trigger.value.getBoundingClientRect()
const panelWidth = panel.value.offsetWidth
const panelHeight = panel.value.offsetHeight
const scrollRegion =
panel.value.querySelector<HTMLElement>('[data-anchored-scroll-region]') ?? panel.value
const panelRect = { width: panel.value.offsetWidth, height: scrollRegion.scrollHeight }
const offset = distance.value
const alignsEnd = placement.value.endsWith('end')
const alignsCenter = placement.value.endsWith('center')
const isHorizontal = placement.value.startsWith('right') || placement.value.startsWith('left')
let idealTop: number
let idealLeft: number
let maxHeight: number | undefined
if (isHorizontal) {
const prefersRight = placement.value.startsWith('right')
const spaceRight = window.innerWidth - triggerRect.right - viewportPadding
const spaceLeft = triggerRect.left - viewportPadding
const opensRight = prefersRight
? panelWidth + offset <= spaceRight || spaceRight > spaceLeft
: panelWidth + offset > spaceLeft && spaceRight > spaceLeft
? panelRect.width + offset <= spaceRight || spaceRight > spaceLeft
: panelRect.width + offset > spaceLeft && spaceRight > spaceLeft
resolvedSide.value = opensRight ? 'right' : 'left'
idealTop = alignsEnd ? triggerRect.bottom - panelHeight : triggerRect.top
idealLeft = opensRight ? triggerRect.right + offset : triggerRect.left - panelWidth - offset
idealTop = alignsEnd ? triggerRect.bottom - panelRect.height : triggerRect.top
idealLeft = opensRight
? triggerRect.right + offset
: triggerRect.left - panelRect.width - offset
} else {
const prefersTop = placement.value.startsWith('top')
const spaceBelow = window.innerHeight - triggerRect.bottom - viewportPadding
const spaceAbove = triggerRect.top - viewportPadding
const opensAbove = prefersTop
? panelHeight + offset <= spaceAbove || spaceAbove > spaceBelow
: panelHeight + offset > spaceBelow && spaceAbove > spaceBelow
? panelRect.height + offset <= spaceAbove || spaceAbove > spaceBelow
: panelRect.height + offset > spaceBelow && spaceAbove > spaceBelow
resolvedSide.value = opensAbove ? 'top' : 'bottom'
idealTop = opensAbove ? triggerRect.top - panelHeight - offset : triggerRect.bottom + offset
idealLeft = alignsEnd ? triggerRect.right - panelWidth : triggerRect.left
const availableHeight = opensAbove ? spaceAbove : spaceBelow
if (panelRect.height + offset > availableHeight) {
maxHeight = Math.max(0, availableHeight - offset)
}
idealTop = opensAbove
? triggerRect.top - (maxHeight ?? panelRect.height) - offset
: triggerRect.bottom + offset
if (alignsCenter) {
const centered = triggerRect.left + triggerRect.width / 2 - panelRect.width / 2
if (centered < viewportPadding) {
idealLeft = triggerRect.left
} else if (centered + panelRect.width > window.innerWidth - viewportPadding) {
idealLeft = triggerRect.right - panelRect.width
} else {
idealLeft = centered
}
} else {
idealLeft = alignsEnd ? triggerRect.right - panelRect.width : triggerRect.left
}
}
const maxTop = Math.max(viewportPadding, window.innerHeight - panelHeight - viewportPadding)
const maxLeft = Math.max(viewportPadding, window.innerWidth - panelWidth - viewportPadding)
const effectiveHeight = maxHeight ?? panelRect.height
const maxTop = Math.max(viewportPadding, window.innerHeight - effectiveHeight - viewportPadding)
const maxLeft = Math.max(viewportPadding, window.innerWidth - panelRect.width - viewportPadding)
const panelTop = Math.min(Math.max(idealTop, viewportPadding), maxTop)
const panelLeft = Math.min(Math.max(idealLeft, viewportPadding), maxLeft)
@@ -78,18 +103,19 @@ export function useAnchoredTeleport(
top: `${panelTop}px`,
left: `${panelLeft}px`,
visibility: 'visible',
...(maxHeight !== undefined ? { maxHeight: `${maxHeight}px` } : {}),
}
anchorStyle.value = isHorizontal
? {
top: `${Math.min(
Math.max(triggerRect.top + triggerRect.height / 2 - panelTop, anchorPadding),
Math.max(anchorPadding, panelHeight - anchorPadding),
Math.max(anchorPadding, panelRect.height - anchorPadding),
)}px`,
}
: {
left: `${Math.min(
Math.max(triggerRect.left + triggerRect.width / 2 - panelLeft, anchorPadding),
Math.max(anchorPadding, panelWidth - anchorPadding),
Math.max(anchorPadding, panelRect.width - anchorPadding),
)}px`,
}
}