mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 09:04:55 +00:00
bunch more fixes lol
This commit is contained in:
@@ -433,7 +433,7 @@ function dismissInfoBanner() {
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
</IntlFormatted>
|
</IntlFormatted>
|
||||||
<template #actions>
|
<template v-if="false" #actions>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ButtonStyled color="blue">
|
<ButtonStyled color="blue">
|
||||||
<a> {{ formatMessage(messages.learnMore) }} <RightArrowIcon /> </a>
|
<a> {{ formatMessage(messages.learnMore) }} <RightArrowIcon /> </a>
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
<VersionPage
|
<VersionPage
|
||||||
:version="version"
|
:version="version"
|
||||||
:enrichment="enrichment"
|
:enrichment="enrichment"
|
||||||
|
:enrichment-loading="dependenciesLoading"
|
||||||
:members="members"
|
:members="members"
|
||||||
:user-link-creator="(user) => (moderator ? `/user/${user.id}` : undefined)"
|
:user-link-creator="(user) => (moderator ? `/user/${user.id}` : undefined)"
|
||||||
:dependency-link-creator="createDependencyLink"
|
:dependency-link-creator="createDependencyLink"
|
||||||
@@ -400,7 +401,7 @@
|
|||||||
</pre
|
</pre
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else-if="showVersionSkeleton">
|
||||||
<div class="flex flex-col gap-4 pb-[30rem]">
|
<div class="flex flex-col gap-4 pb-[30rem]">
|
||||||
<div
|
<div
|
||||||
class="mt-4 flex h-[8rem] w-full animate-pulse items-center justify-center rounded-2xl bg-surface-3"
|
class="mt-4 flex h-[8rem] w-full animate-pulse items-center justify-center rounded-2xl bg-surface-3"
|
||||||
@@ -466,10 +467,12 @@ import {
|
|||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { isStaff } from '@modrinth/utils'
|
import { isStaff } from '@modrinth/utils'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import { onServerPrefetch } from 'vue'
|
||||||
|
|
||||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||||
import { STALE_TIME } from '~/composables/queries/project'
|
import { projectQueryOptions, STALE_TIME } from '~/composables/queries/project'
|
||||||
|
import { versionQueryOptions } from '~/composables/queries/version'
|
||||||
import { createDataPackVersion } from '~/helpers/package.js'
|
import { createDataPackVersion } from '~/helpers/package.js'
|
||||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||||
|
|
||||||
@@ -499,6 +502,7 @@ const {
|
|||||||
versionsLoading,
|
versionsLoading,
|
||||||
loadVersions,
|
loadVersions,
|
||||||
dependencies: contextDependencies,
|
dependencies: contextDependencies,
|
||||||
|
dependenciesLoading,
|
||||||
loadDependencies,
|
loadDependencies,
|
||||||
invalidate,
|
invalidate,
|
||||||
cdnDownloadReason,
|
cdnDownloadReason,
|
||||||
@@ -522,26 +526,31 @@ const signInRouteObj = computed(() => getSignInRouteObj(route))
|
|||||||
const versionRouteParam = computed(() => route.params.version as string)
|
const versionRouteParam = computed(() => route.params.version as string)
|
||||||
const isLatestRoute = computed(() => versionRouteParam.value === 'latest')
|
const isLatestRoute = computed(() => versionRouteParam.value === 'latest')
|
||||||
|
|
||||||
|
function filterVersionsForLatestRoute(allVersions: Labrinth.Versions.v3.Version[]) {
|
||||||
|
let filtered = allVersions
|
||||||
|
|
||||||
|
const loaderFilter = route.query.loader
|
||||||
|
if (typeof loaderFilter === 'string') {
|
||||||
|
filtered = filtered.filter((x) => x.loaders.includes(loaderFilter))
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameVersionFilter = route.query.version
|
||||||
|
if (typeof gameVersionFilter === 'string') {
|
||||||
|
filtered = filtered.filter((x) => x.game_versions.includes(gameVersionFilter))
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
const latestVersionId = computed(() => {
|
const latestVersionId = computed(() => {
|
||||||
if (!isLatestRoute.value) {
|
if (!isLatestRoute.value) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let allVersions = versions.value ?? []
|
const filtered = filterVersionsForLatestRoute(versions.value ?? [])
|
||||||
|
if (filtered.length === 0) return null
|
||||||
|
|
||||||
const loaderFilter = route.query.loader
|
return filtered.reduce((a, b) => (a.date_published > b.date_published ? a : b)).id
|
||||||
if (typeof loaderFilter === 'string') {
|
|
||||||
allVersions = allVersions.filter((x) => x.loaders.includes(loaderFilter))
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameVersionFilter = route.query.version
|
|
||||||
if (typeof gameVersionFilter === 'string') {
|
|
||||||
allVersions = allVersions.filter((x) => x.game_versions.includes(gameVersionFilter))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allVersions.length === 0) return null
|
|
||||||
|
|
||||||
return allVersions.reduce((a, b) => (a.date_published > b.date_published ? a : b)).id
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const versionLookupKey = computed(() =>
|
const versionLookupKey = computed(() =>
|
||||||
@@ -552,6 +561,7 @@ const {
|
|||||||
data: version,
|
data: version,
|
||||||
refetch: refetchVersion,
|
refetch: refetchVersion,
|
||||||
error: versionError,
|
error: versionError,
|
||||||
|
isPending: versionPending,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: computed(
|
queryKey: computed(
|
||||||
() => ['project', project.value.id, 'version', 'v3', versionLookupKey.value] as const,
|
() => ['project', project.value.id, 'version', 'v3', versionLookupKey.value] as const,
|
||||||
@@ -562,6 +572,30 @@ const {
|
|||||||
staleTime: STALE_TIME,
|
staleTime: STALE_TIME,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const showVersionSkeleton = computed(() => import.meta.client && versionPending.value)
|
||||||
|
|
||||||
|
onServerPrefetch(async () => {
|
||||||
|
if (!project.value.id) return
|
||||||
|
|
||||||
|
let lookupKey = versionRouteParam.value
|
||||||
|
|
||||||
|
if (isLatestRoute.value) {
|
||||||
|
loadVersions()
|
||||||
|
const versionsData = await queryClient.ensureQueryData(
|
||||||
|
projectQueryOptions.versionsV3(project.value.id, client),
|
||||||
|
)
|
||||||
|
const filtered = filterVersionsForLatestRoute(versionsData ?? [])
|
||||||
|
if (filtered.length === 0) return
|
||||||
|
lookupKey = filtered.reduce((a, b) => (a.date_published > b.date_published ? a : b)).id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lookupKey || lookupKey === 'latest') return
|
||||||
|
|
||||||
|
await queryClient.ensureQueryData(
|
||||||
|
versionQueryOptions.fromProject(project.value.id, lookupKey, client),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
versionError,
|
versionError,
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<div class="group relative float-end ml-4">
|
<div class="group relative float-end ml-4">
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
v-tooltip="formatMessage(messages.editIconButton)"
|
v-tooltip="formatMessage(messages.editIconButton)"
|
||||||
|
:dropdown-id="`${baseId}-edit-icon`"
|
||||||
class="m-0 cursor-pointer appearance-none border-none bg-transparent p-0 transition-transform group-active:scale-95"
|
class="m-0 cursor-pointer appearance-none border-none bg-transparent p-0 transition-transform group-active:scale-95"
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
@@ -138,7 +139,7 @@
|
|||||||
</NewModal>
|
</NewModal>
|
||||||
<NormalPage :sidebar="cosmetics.leftContentLayout ? 'left' : 'right'">
|
<NormalPage :sidebar="cosmetics.leftContentLayout ? 'left' : 'right'">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-4">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-if="returnLink"
|
v-if="returnLink"
|
||||||
@@ -201,19 +202,32 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2 flex gap-2 sm:col-span-1">
|
<div class="col-span-2 flex items-center gap-2 sm:col-span-1">
|
||||||
<template v-if="canEdit">
|
<template v-if="canEdit">
|
||||||
<ButtonStyled>
|
<ButtonStyled size="large">
|
||||||
<button @click="openEditModal">
|
<button @click="openEditModal">
|
||||||
<EditIcon aria-hidden="true" />
|
<EditIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.editButton) }}
|
{{ formatMessage(commonMessages.editButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled color="red" color-fill="text">
|
<ButtonStyled size="large" circular type="transparent">
|
||||||
<button @click="() => $refs.deleteModal.show()">
|
<OverflowMenu
|
||||||
<TrashIcon aria-hidden="true" />
|
:dropdown-id="`${baseId}-more-options`"
|
||||||
{{ formatMessage(commonMessages.deleteLabel) }}
|
:options="[
|
||||||
</button>
|
{
|
||||||
|
id: 'delete',
|
||||||
|
color: 'red',
|
||||||
|
action: () => deleteModal?.show(),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:aria-label="formatMessage(commonMessages.moreOptionsButton)"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
|
<template #delete>
|
||||||
|
<TrashIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(commonMessages.deleteLabel) }}
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -226,7 +240,12 @@
|
|||||||
v-if="collection.description"
|
v-if="collection.description"
|
||||||
:title="formatMessage(commonMessages.descriptionLabel)"
|
:title="formatMessage(commonMessages.descriptionLabel)"
|
||||||
>
|
>
|
||||||
<p class="m-0 break-words">{{ collection.description }}</p>
|
<div
|
||||||
|
v-if="supportsMarkdown"
|
||||||
|
class="description-body"
|
||||||
|
v-html="renderString(collection.description)"
|
||||||
|
/>
|
||||||
|
<p v-else class="m-0 break-words">{{ collection.description }}</p>
|
||||||
</SidebarCard>
|
</SidebarCard>
|
||||||
<SidebarCard
|
<SidebarCard
|
||||||
v-if="collection.id !== 'following'"
|
v-if="collection.id !== 'following'"
|
||||||
@@ -238,9 +257,7 @@
|
|||||||
>
|
>
|
||||||
<Avatar :src="creator.avatar_url" :alt="creator.username" size="32px" circle />
|
<Avatar :src="creator.avatar_url" :alt="creator.username" size="32px" circle />
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span
|
<span class="flex w-full flex-nowrap items-center gap-1 group-hover:underline">
|
||||||
class="grid w-full grid-cols-[1fr_auto] flex-nowrap items-center gap-1 group-hover:underline"
|
|
||||||
>
|
|
||||||
<span class="min-w-0 overflow-hidden truncate">{{ creator.username }}</span>
|
<span class="min-w-0 overflow-hidden truncate">{{ creator.username }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -373,6 +390,7 @@ import {
|
|||||||
HeartMinusIcon,
|
HeartMinusIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
SaveIcon,
|
SaveIcon,
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
@@ -411,9 +429,10 @@ import {
|
|||||||
useSavable,
|
useSavable,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { isAdmin } from '@modrinth/utils'
|
import { isAdmin, renderString } from '@modrinth/utils'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import { onServerPrefetch } from 'vue'
|
||||||
|
|
||||||
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
|
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
|
||||||
|
|
||||||
@@ -431,6 +450,32 @@ const route = useNativeRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = await useAuth()
|
const auth = await useAuth()
|
||||||
const cosmetics = useCosmetics()
|
const cosmetics = useCosmetics()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const baseId = useId()
|
||||||
|
|
||||||
|
async function fetchProjectsByIds(projectIds) {
|
||||||
|
const segmentSize = 800
|
||||||
|
const segments = []
|
||||||
|
for (let i = 0; i < projectIds.length; i += segmentSize) {
|
||||||
|
segments.push(projectIds.slice(i, i + segmentSize))
|
||||||
|
}
|
||||||
|
const results = await Promise.all(
|
||||||
|
segments.map((ids) => api.labrinth.projects_v2.getMultiple(ids)),
|
||||||
|
)
|
||||||
|
const projects = results.flat()
|
||||||
|
for (const project of projects) {
|
||||||
|
project.categories = project.categories.concat(project.loaders)
|
||||||
|
}
|
||||||
|
return projects
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFollowedProjects(userId) {
|
||||||
|
const projects = await api.labrinth.users_v2.getFollowedProjects(userId)
|
||||||
|
for (const project of projects) {
|
||||||
|
project.categories = project.categories.concat(project.loaders)
|
||||||
|
}
|
||||||
|
return projects
|
||||||
|
}
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
collectionDescription: {
|
collectionDescription: {
|
||||||
@@ -595,6 +640,8 @@ const creator = computed(() =>
|
|||||||
isFollowingCollection.value ? auth.value.user : fetchedCreator.value,
|
isFollowingCollection.value ? auth.value.user : fetchedCreator.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const supportsMarkdown = computed(() => creator.value?.id === '2REoufqX')
|
||||||
|
|
||||||
// Query for followed projects
|
// Query for followed projects
|
||||||
const {
|
const {
|
||||||
data: followedProjects,
|
data: followedProjects,
|
||||||
@@ -602,13 +649,7 @@ const {
|
|||||||
isFetching: followedProjectsIsFetching,
|
isFetching: followedProjectsIsFetching,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: computed(() => ['user', auth.value.user?.id, 'follows']),
|
queryKey: computed(() => ['user', auth.value.user?.id, 'follows']),
|
||||||
queryFn: async () => {
|
queryFn: async () => fetchFollowedProjects(auth.value.user.id),
|
||||||
const projects = await api.labrinth.users_v2.getFollowedProjects(auth.value.user.id)
|
|
||||||
for (const project of projects) {
|
|
||||||
project.categories = project.categories.concat(project.loaders)
|
|
||||||
}
|
|
||||||
return projects
|
|
||||||
},
|
|
||||||
enabled: computed(() => isFollowingCollection.value && !!auth.value.user?.id),
|
enabled: computed(() => isFollowingCollection.value && !!auth.value.user?.id),
|
||||||
placeholderData: [],
|
placeholderData: [],
|
||||||
})
|
})
|
||||||
@@ -620,22 +661,7 @@ const {
|
|||||||
isFetching: collectionProjectsIsFetching,
|
isFetching: collectionProjectsIsFetching,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: computed(() => ['projects', collection.value?.projects]),
|
queryKey: computed(() => ['projects', collection.value?.projects]),
|
||||||
queryFn: async () => {
|
queryFn: () => fetchProjectsByIds(collection.value.projects),
|
||||||
const projectIds = collection.value.projects
|
|
||||||
const segmentSize = 800
|
|
||||||
const segments = []
|
|
||||||
for (let i = 0; i < projectIds.length; i += segmentSize) {
|
|
||||||
segments.push(projectIds.slice(i, i + segmentSize))
|
|
||||||
}
|
|
||||||
const results = await Promise.all(
|
|
||||||
segments.map((ids) => api.labrinth.projects_v2.getMultiple(ids)),
|
|
||||||
)
|
|
||||||
const projects = results.flat()
|
|
||||||
for (const project of projects) {
|
|
||||||
project.categories = project.categories.concat(project.loaders)
|
|
||||||
}
|
|
||||||
return projects
|
|
||||||
},
|
|
||||||
enabled: computed(() => !isFollowingCollection.value && !!collection.value?.projects?.length),
|
enabled: computed(() => !isFollowingCollection.value && !!collection.value?.projects?.length),
|
||||||
placeholderData: [],
|
placeholderData: [],
|
||||||
})
|
})
|
||||||
@@ -647,12 +673,48 @@ const projects = computed(() =>
|
|||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
const isLoading = computed(() => {
|
const isLoading = computed(() => {
|
||||||
|
if (!import.meta.client) return false
|
||||||
|
|
||||||
if (isFollowingCollection.value) {
|
if (isFollowingCollection.value) {
|
||||||
return followedProjectsIsFetching.value
|
return followedProjectsIsFetching.value
|
||||||
}
|
}
|
||||||
return collectionIsPending.value || creatorIsPending.value || collectionProjectsIsFetching.value
|
return collectionIsPending.value || creatorIsPending.value || collectionProjectsIsFetching.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onServerPrefetch(async () => {
|
||||||
|
if (isFollowingCollection.value) {
|
||||||
|
const userId = auth.value.user?.id
|
||||||
|
if (!userId) return
|
||||||
|
|
||||||
|
await queryClient.ensureQueryData({
|
||||||
|
queryKey: ['user', userId, 'follows'],
|
||||||
|
queryFn: () => fetchFollowedProjects(userId),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!collectionId) return
|
||||||
|
|
||||||
|
const collectionData = await queryClient.ensureQueryData({
|
||||||
|
queryKey: ['collection', collectionId],
|
||||||
|
queryFn: () => api.labrinth.collections.get(collectionId),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (collectionData?.user) {
|
||||||
|
await queryClient.ensureQueryData({
|
||||||
|
queryKey: ['user', collectionData.user],
|
||||||
|
queryFn: () => api.labrinth.users_v2.get(collectionData.user),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectionData?.projects?.length) {
|
||||||
|
await queryClient.ensureQueryData({
|
||||||
|
queryKey: ['projects', collectionData.projects],
|
||||||
|
queryFn: () => fetchProjectsByIds(collectionData.projects),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[collection, creator],
|
[collection, creator],
|
||||||
([col, cre]) => {
|
([col, cre]) => {
|
||||||
@@ -716,6 +778,7 @@ const showUpdatedDate = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const editModal = ref(null)
|
const editModal = ref(null)
|
||||||
|
const deleteModal = ref(null)
|
||||||
const iconInputRef = ref(null)
|
const iconInputRef = ref(null)
|
||||||
const icon = ref(null)
|
const icon = ref(null)
|
||||||
const deletedIcon = ref(false)
|
const deletedIcon = ref(false)
|
||||||
@@ -881,4 +944,15 @@ function openEditModal(event) {
|
|||||||
// Omorphia's dropdowns are harcoded in width, so we need to override that
|
// Omorphia's dropdowns are harcoded in width, so we need to override that
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.description-body) {
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-brand);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -146,9 +146,8 @@
|
|||||||
<div class="normal-page__sidebar">
|
<div class="normal-page__sidebar">
|
||||||
<AdPlaceholder v-if="!auth.user" />
|
<AdPlaceholder v-if="!auth.user" />
|
||||||
|
|
||||||
<div class="card flex-card">
|
<SidebarCard title="Members">
|
||||||
<h2>Members</h2>
|
<div class="flex flex-col gap-3 font-semibold">
|
||||||
<div class="details-list">
|
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-for="member in acceptedMembers"
|
v-for="member in acceptedMembers"
|
||||||
:key="`member-${member?.user?.id}`"
|
:key="`member-${member?.user?.id}`"
|
||||||
@@ -163,22 +162,20 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="flex w-full flex-nowrap items-center gap-1 group-hover:underline">
|
<span class="flex w-full flex-nowrap items-center gap-1 group-hover:underline">
|
||||||
<span class="min-w-0 overflow-hidden truncate font-normal text-contrast">{{
|
<span class="min-w-0 overflow-hidden truncate">{{ member.user.username }}</span>
|
||||||
member.user.username
|
|
||||||
}}</span>
|
|
||||||
<CrownIcon
|
<CrownIcon
|
||||||
v-if="member.is_owner"
|
v-if="member.is_owner"
|
||||||
v-tooltip="'Organization owner'"
|
v-tooltip="'Organization owner'"
|
||||||
class="text-brand-orange"
|
class="text-brand-orange"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm font-normal">
|
<span class="text-sm font-normal text-secondary">
|
||||||
{{ member?.role ? member.role : 'Member' }}
|
{{ member?.role ? member.role : 'Member' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SidebarCard>
|
||||||
</div>
|
</div>
|
||||||
<div class="normal-page__content">
|
<div class="normal-page__content">
|
||||||
<div v-if="isInvited" class="universal-card information invited">
|
<div v-if="isInvited" class="universal-card information invited">
|
||||||
@@ -317,6 +314,7 @@ import {
|
|||||||
PROJECT_DEP_MARKER_QUERY,
|
PROJECT_DEP_MARKER_QUERY,
|
||||||
ProjectCard,
|
ProjectCard,
|
||||||
ProjectCardList,
|
ProjectCardList,
|
||||||
|
SidebarCard,
|
||||||
useCompactNumber,
|
useCompactNumber,
|
||||||
useFormatNumber,
|
useFormatNumber,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
@@ -774,4 +772,8 @@ async function copyPermalink() {
|
|||||||
.popout-checkbox {
|
.popout-checkbox {
|
||||||
padding: var(--gap-sm) var(--gap-md);
|
padding: var(--gap-sm) var(--gap-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-page {
|
||||||
|
column-gap: 1.5rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -477,7 +477,7 @@
|
|||||||
v-if="organizations?.length > 0"
|
v-if="organizations?.length > 0"
|
||||||
class="mb-4 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4 pt-3"
|
class="mb-4 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4 pt-3"
|
||||||
>
|
>
|
||||||
<h2 class="m-0 mb-2 text-lg text-contrast">
|
<h2 class="m-0 mb-2 text-lg font-semibold text-contrast">
|
||||||
{{ formatMessage(messages.profileOrganizations) }}
|
{{ formatMessage(messages.profileOrganizations) }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
@@ -1040,4 +1040,8 @@ export default defineNuxtComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-page {
|
||||||
|
column-gap: 1.5rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const { hierarchicalSidebarAvailable } = injectPageContext()
|
|||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.ui-normal-page {
|
.ui-normal-page {
|
||||||
@apply grid gap-6 mx-auto py-4;
|
@apply grid gap-x-6 gap-y-4 mx-auto py-4;
|
||||||
width: min(calc(100% - 2rem), calc(80rem - 3rem));
|
width: min(calc(100% - 2rem), calc(80rem - 3rem));
|
||||||
|
|
||||||
grid-template:
|
grid-template:
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-3 p-4"
|
class="flex flex-col gap-3 p-4 pt-3"
|
||||||
:class="{
|
:class="{
|
||||||
'card-shadow mb-4 last:mb-0 rounded-2xl bg-bg-raised': !hierarchicalSidebarAvailable,
|
'card-shadow mb-4 last:mb-0 rounded-2xl bg-surface-3 border border-solid border-surface-4':
|
||||||
|
!hierarchicalSidebarAvailable,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span class="font-semibold">{{ title }}</span>
|
<h2 class="text-lg font-semibold m-0">{{ title }}</h2>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
>
|
>
|
||||||
<Avatar :src="organization.icon_url" :alt="organization.name" size="32px" />
|
<Avatar :src="organization.icon_url" :alt="organization.name" size="32px" />
|
||||||
<div class="flex flex-col flex-nowrap justify-center">
|
<div class="flex flex-col flex-nowrap justify-center">
|
||||||
<span class="group-hover:underline font-normal text-contrast">
|
<span class="group-hover:underline font-medium">
|
||||||
{{ organization.name }}
|
{{ organization.name }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm font-normal flex items-center gap-1"
|
<span class="text-sm font-normal text-secondary flex items-center gap-1"
|
||||||
><OrganizationIcon /> {{ formatMessage(messages.organization) }}</span
|
><OrganizationIcon /> {{ formatMessage(messages.organization) }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,9 +30,7 @@
|
|||||||
<Avatar :src="member.user.avatar_url" :alt="member.user.username" size="32px" circle />
|
<Avatar :src="member.user.avatar_url" :alt="member.user.username" size="32px" circle />
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="flex w-full flex-nowrap items-center gap-1 group-hover:underline">
|
<span class="flex w-full flex-nowrap items-center gap-1 group-hover:underline">
|
||||||
<span class="min-w-0 overflow-hidden font-normal text-contrast truncate">{{
|
<span class="min-w-0 overflow-hidden truncate">{{ member.user.username }}</span>
|
||||||
member.user.username
|
|
||||||
}}</span>
|
|
||||||
<CrownIcon
|
<CrownIcon
|
||||||
v-if="member.is_owner"
|
v-if="member.is_owner"
|
||||||
v-tooltip="formatMessage(messages.owner)"
|
v-tooltip="formatMessage(messages.owner)"
|
||||||
@@ -40,7 +38,7 @@
|
|||||||
/>
|
/>
|
||||||
<ExternalIcon v-if="linkTarget === '_blank'" />
|
<ExternalIcon v-if="linkTarget === '_blank'" />
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm font-normal">{{ member.role }}</span>
|
<span class="text-sm font-normal text-secondary">{{ member.role }}</span>
|
||||||
</div>
|
</div>
|
||||||
</AutoLink>
|
</AutoLink>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ const earnedBadges = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="earnedBadges.length > 0 || !!downloadsBadge" class="flex flex-col">
|
<div v-if="earnedBadges.length > 0 || !!downloadsBadge" class="flex flex-col">
|
||||||
<h2 class="text-lg text-contrast m-0 mb-2">
|
<h2 class="text-lg font-semibold text-contrast m-0 mb-2">
|
||||||
{{ formatMessage(messages.title) }}
|
{{ formatMessage(messages.title) }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-[repeat(auto-fill,minmax(64px,1fr))] gap-2">
|
<div class="grid grid-cols-[repeat(auto-fill,minmax(64px,1fr))] gap-2">
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Labrinth } from '@modrinth/api-client'
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
import { DownloadIcon, FileIcon, SearchIcon } from '@modrinth/assets'
|
import { DownloadIcon, ExternalIcon, FileIcon, SearchIcon } from '@modrinth/assets'
|
||||||
import { capitalizeString, renderHighlightedString } from '@modrinth/utils'
|
import {
|
||||||
|
capitalizeString,
|
||||||
|
formatVersionsForDisplay,
|
||||||
|
type GameVersionTag,
|
||||||
|
renderHighlightedString,
|
||||||
|
} from '@modrinth/utils'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
@@ -11,6 +16,7 @@ import { useCompactNumber, useFormatNumber } from '#ui/composables/format-number
|
|||||||
import { useRelativeTime } from '#ui/composables/how-ago.ts'
|
import { useRelativeTime } from '#ui/composables/how-ago.ts'
|
||||||
import { defineMessage, defineMessages, useVIntl } from '#ui/composables/i18n.ts'
|
import { defineMessage, defineMessages, useVIntl } from '#ui/composables/i18n.ts'
|
||||||
import { injectModrinthClient } from '#ui/providers/api-client.ts'
|
import { injectModrinthClient } from '#ui/providers/api-client.ts'
|
||||||
|
import { injectTags } from '#ui/providers/tags.ts'
|
||||||
import {
|
import {
|
||||||
commonMessages,
|
commonMessages,
|
||||||
fileTypeMessages,
|
fileTypeMessages,
|
||||||
@@ -41,12 +47,23 @@ const { formatCompactNumber } = useCompactNumber()
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
version: Labrinth.Versions.v3.Version
|
version: Labrinth.Versions.v3.Version
|
||||||
enrichment?: Labrinth.Projects.v2.DependencyInfo
|
enrichment?: Labrinth.Projects.v2.DependencyInfo
|
||||||
|
enrichmentLoading?: boolean
|
||||||
dependencyLinkCreator: (context: DependencyContext) => string | undefined
|
dependencyLinkCreator: (context: DependencyContext) => string | undefined
|
||||||
members?: Labrinth.Projects.v3.TeamMember[]
|
members?: Labrinth.Projects.v3.TeamMember[]
|
||||||
userLinkCreator?: (user: Labrinth.Users.v3.User) => string | undefined
|
userLinkCreator?: (user: Labrinth.Users.v3.User) => string | undefined
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const api = injectModrinthClient()
|
const api = injectModrinthClient()
|
||||||
|
const tags = injectTags(null)
|
||||||
|
|
||||||
|
const gameVersionsToDisplay = computed(() =>
|
||||||
|
tags?.gameVersions.value?.length
|
||||||
|
? formatVersionsForDisplay(
|
||||||
|
props.version.game_versions,
|
||||||
|
tags.gameVersions.value as GameVersionTag[],
|
||||||
|
)
|
||||||
|
: props.version.game_versions,
|
||||||
|
)
|
||||||
|
|
||||||
const versionNumber = computed(() => props.version.version_number)
|
const versionNumber = computed(() => props.version.version_number)
|
||||||
const versionSubtitle = computed(() => props.version.name)
|
const versionSubtitle = computed(() => props.version.name)
|
||||||
@@ -101,22 +118,82 @@ const optionalContent = computed(() =>
|
|||||||
const incompatibleContent = computed(() =>
|
const incompatibleContent = computed(() =>
|
||||||
dependencies.value.filter((dep) => dep.dependency.dependency_type === 'incompatible'),
|
dependencies.value.filter((dep) => dep.dependency.dependency_type === 'incompatible'),
|
||||||
)
|
)
|
||||||
const includedContent = computed(() =>
|
const embeddedDependencies = computed(() =>
|
||||||
dependencies.value
|
dependencies.value.filter((context) => context.dependency.dependency_type === 'embedded'),
|
||||||
.filter((context) => context.dependency.dependency_type === 'embedded')
|
|
||||||
.map((context) => ({
|
|
||||||
icon_url: context.project
|
|
||||||
? context.project.icon_url
|
|
||||||
: context.dependency.attribution?.icon_url,
|
|
||||||
name: context.project ? context.project.title : context.dependency.file_name,
|
|
||||||
version: context.version ? context.version.version_number : undefined,
|
|
||||||
link: context.project
|
|
||||||
? props.dependencyLinkCreator(context)
|
|
||||||
: context.dependency.attribution?.link,
|
|
||||||
hasProject: !!context.project,
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const includedContentLoading = computed(
|
||||||
|
() =>
|
||||||
|
!!props.enrichmentLoading &&
|
||||||
|
embeddedDependencies.value.some((context) => context.dependency.project_id && !context.project),
|
||||||
|
)
|
||||||
|
|
||||||
|
function getEmbeddedDependencyId(context: DependencyContext) {
|
||||||
|
return (
|
||||||
|
context.dependency.project_id ??
|
||||||
|
context.dependency.file_name ??
|
||||||
|
context.dependency.version_id ??
|
||||||
|
'included'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncludedContentLoadingRow = { loading: true; id: string }
|
||||||
|
type IncludedContentRow = {
|
||||||
|
id: string
|
||||||
|
icon_url?: string
|
||||||
|
name?: string
|
||||||
|
version?: string
|
||||||
|
link?: string
|
||||||
|
hasProject: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIncludedContentLoadingRow(
|
||||||
|
row: IncludedContentLoadingRow | IncludedContentRow,
|
||||||
|
): row is IncludedContentLoadingRow {
|
||||||
|
return 'loading' in row
|
||||||
|
}
|
||||||
|
|
||||||
|
const includedContent = computed((): (IncludedContentLoadingRow | IncludedContentRow)[] => {
|
||||||
|
if (includedContentLoading.value) {
|
||||||
|
return embeddedDependencies.value.map((context) => ({
|
||||||
|
loading: true as const,
|
||||||
|
id: getEmbeddedDependencyId(context),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return embeddedDependencies.value
|
||||||
|
.map((context) => {
|
||||||
|
const resolution = context.dependency.attribution?.resolution
|
||||||
|
return {
|
||||||
|
id: getEmbeddedDependencyId(context),
|
||||||
|
icon_url: context.project
|
||||||
|
? context.project.icon_url
|
||||||
|
: context.dependency.attribution?.flame_project?.icon_url,
|
||||||
|
name: context.project ? context.project.title : context.dependency.file_name,
|
||||||
|
version: context.version ? context.version.version_number : undefined,
|
||||||
|
link: context.project
|
||||||
|
? props.dependencyLinkCreator(context)
|
||||||
|
: resolution && 'link_to_work' in resolution
|
||||||
|
? resolution.link_to_work
|
||||||
|
: undefined,
|
||||||
|
hasProject: !!context.project,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const priority = (row: typeof a) => {
|
||||||
|
if (row.hasProject) return 0
|
||||||
|
if (row.link) return 1
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
const priorityDiff = priority(a) - priority(b)
|
||||||
|
if (priorityDiff !== 0) {
|
||||||
|
return priorityDiff
|
||||||
|
}
|
||||||
|
const sortName = (name: string | undefined) => (name ?? '').replace(/[^a-zA-Z0-9]/g, '')
|
||||||
|
return sortName(a.name).localeCompare(sortName(b.name))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const contentTableColumns = computed(() => {
|
const contentTableColumns = computed(() => {
|
||||||
const cols = [
|
const cols = [
|
||||||
{
|
{
|
||||||
@@ -128,7 +205,11 @@ const contentTableColumns = computed(() => {
|
|||||||
label: formatMessage(defineMessage({ id: 'version.content.name', defaultMessage: 'Name' })),
|
label: formatMessage(defineMessage({ id: 'version.content.name', defaultMessage: 'Name' })),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
if (includedContent.value.some((x) => x.version)) {
|
if (
|
||||||
|
includedContentLoading.value
|
||||||
|
? embeddedDependencies.value.some((context) => context.dependency.version_id)
|
||||||
|
: includedContent.value.some((row) => 'version' in row && row.version)
|
||||||
|
) {
|
||||||
cols.push({
|
cols.push({
|
||||||
key: 'version',
|
key: 'version',
|
||||||
label: formatMessage(
|
label: formatMessage(
|
||||||
@@ -318,7 +399,7 @@ const authorLink = computed(() =>
|
|||||||
{{ formatMessage(projectCompatibilityMessages.minecraftJava) }}
|
{{ formatMessage(projectCompatibilityMessages.minecraftJava) }}
|
||||||
<div class="flex gap-1 flex-wrap mt-2">
|
<div class="flex gap-1 flex-wrap mt-2">
|
||||||
<TagItem
|
<TagItem
|
||||||
v-for="gameVersion in version.game_versions"
|
v-for="gameVersion in gameVersionsToDisplay"
|
||||||
:key="`version-compat-game-version-${gameVersion}`"
|
:key="`version-compat-game-version-${gameVersion}`"
|
||||||
>{{ gameVersion }}</TagItem
|
>{{ gameVersion }}</TagItem
|
||||||
>
|
>
|
||||||
@@ -414,7 +495,7 @@ const authorLink = computed(() =>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section v-if="includedContent?.length > 0" id="content">
|
<section v-if="embeddedDependencies.length > 0" id="content">
|
||||||
<h3 class="mt-0 mb-2 text-lg font-semibold">
|
<h3 class="mt-0 mb-2 text-lg font-semibold">
|
||||||
{{
|
{{
|
||||||
formatMessage(
|
formatMessage(
|
||||||
@@ -422,36 +503,60 @@ const authorLink = computed(() =>
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</h3>
|
</h3>
|
||||||
<Table :columns="contentTableColumns" :data="includedContent">
|
<Table :columns="contentTableColumns" :data="includedContent" row-key="id">
|
||||||
<template #header-actions>
|
<template #header-actions>
|
||||||
<StyledInput
|
<StyledInput
|
||||||
v-model="contentSearchQuery"
|
v-model="contentSearchQuery"
|
||||||
:icon="SearchIcon"
|
:icon="SearchIcon"
|
||||||
:placeholder="formatMessage(messages.searchContent)"
|
:placeholder="formatMessage(messages.searchContent)"
|
||||||
|
:disabled="includedContentLoading"
|
||||||
clearable
|
clearable
|
||||||
wrapper-class="w-full sm:w-64"
|
wrapper-class="w-full sm:w-64"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #cell-icon="{ row }">
|
<template #cell-icon="{ row }">
|
||||||
<AutoLink :to="row.link" tabindex="-1" class="flex" target="_blank">
|
<div
|
||||||
<Avatar v-if="row.icon_url" :src="row.icon_url" alt="" size="2rem" />
|
v-if="isIncludedContentLoadingRow(row)"
|
||||||
|
class="size-[2rem] shrink-0 rounded-[16%] bg-surface-3 animate-pulse"
|
||||||
|
/>
|
||||||
|
<AutoLink v-else :to="row.link" tabindex="-1" class="flex" target="_blank">
|
||||||
|
<Avatar v-if="row.hasProject || row.icon_url" :src="row.icon_url" alt="" size="2rem" />
|
||||||
<div v-else class="size-[2rem] flex items-center justify-center">
|
<div v-else class="size-[2rem] flex items-center justify-center">
|
||||||
<FileIcon class="size-5 text-secondary" />
|
<FileIcon class="size-5 text-secondary" />
|
||||||
</div>
|
</div>
|
||||||
</AutoLink>
|
</AutoLink>
|
||||||
</template>
|
</template>
|
||||||
<template #cell-name="{ row }">
|
<template #cell-name="{ row }">
|
||||||
|
<div
|
||||||
|
v-if="isIncludedContentLoadingRow(row)"
|
||||||
|
class="h-4 max-w-[12rem] rounded-lg bg-surface-3 animate-pulse"
|
||||||
|
/>
|
||||||
<AutoLink
|
<AutoLink
|
||||||
|
v-else
|
||||||
:to="row.link"
|
:to="row.link"
|
||||||
class="flex w-fit"
|
class="flex w-fit"
|
||||||
link-class="hover:underline hover:text-contrast"
|
link-class="hover:underline hover:text-contrast"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{{ row.name }}
|
{{ row.name }}
|
||||||
|
<ExternalIcon v-if="row.link?.startsWith('http')" class="shrink-0 ml-2" />
|
||||||
</AutoLink>
|
</AutoLink>
|
||||||
</template>
|
</template>
|
||||||
<template #cell-version="{ row }">
|
<template #cell-version="{ row }">
|
||||||
{{ (row.version ?? row.hasProject) ? formatMessage(commonMessages.unknownLabel) : '—' }}
|
<div
|
||||||
|
v-if="isIncludedContentLoadingRow(row)"
|
||||||
|
class="h-4 w-16 rounded-lg bg-surface-3 animate-pulse"
|
||||||
|
/>
|
||||||
|
<AutoLink
|
||||||
|
v-else
|
||||||
|
:to="row.version ? row.link : undefined"
|
||||||
|
class="flex w-fit"
|
||||||
|
tabindex="-1"
|
||||||
|
link-class="hover:underline hover:text-contrast"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ row.version ?? '—' }}
|
||||||
|
</AutoLink>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user