Merge tag 'v0.14.7' into beta

v0.14.7
This commit is contained in:
2026-06-18 03:27:02 +03:00
65 changed files with 2223 additions and 355 deletions
@@ -0,0 +1,189 @@
<template>
<div class="relative overflow-clip rounded-xl bg-bg px-4 py-3">
<div
class="absolute bottom-0 left-0 top-0 w-1"
:class="
charge.type === 'refund' ? 'bg-purple' : (chargeStatuses[charge.status]?.color ?? 'bg-blue')
"
/>
<div class="grid w-full grid-cols-[1fr_auto] items-center gap-4">
<div class="flex flex-col gap-2">
<span>
<span class="font-bold text-contrast">
<template v-if="charge.status === 'succeeded'"> Succeeded </template>
<template v-else-if="charge.status === 'failed'"> Failed </template>
<template v-else-if="charge.status === 'cancelled'"> Cancelled </template>
<template v-else-if="charge.status === 'processing'"> Processing </template>
<template v-else-if="charge.status === 'open'"> Upcoming </template>
<template v-else-if="charge.status === 'expiring'"> Expiring </template>
<template v-else> {{ charge.status }} </template>
</span>
<span class="text-secondary opacity-50"></span>
<span>
<template v-if="charge.type === 'refund'"> Refund </template>
<template v-else-if="charge.type === 'subscription'">
<template v-if="charge.status === 'cancelled'"> Subscription </template>
<template v-else-if="isLatestCharge"> Started subscription </template>
<template v-else> Subscription renewal </template>
</template>
<template v-else-if="charge.type === 'one-time'"> One-time charge </template>
<template v-else-if="charge.type === 'proration'"> Proration charge </template>
<template v-else> {{ charge.status }} </template>
</span>
<template v-if="charge.status !== 'cancelled'">
<span class="text-secondary opacity-50"></span>
{{ formatPrice(charge.amount, charge.currency_code) }}
</template>
</span>
<span
v-if="productMetadata && productMetadata.type === 'pyro'"
class="flex items-center gap-1 text-sm text-secondary"
>
<span class="font-bold">Product:</span>
<span v-if="productMetadata.ram">{{ productMetadata.ram / 1024 }}GB RAM</span>
<span v-else>Unknown RAM</span>
<span class="text-secondary opacity-50"></span>
<span v-if="productMetadata.cpu">{{ productMetadata.cpu }} vCPU</span>
<span v-else>Unknown CPU</span>
<span class="text-secondary opacity-50"></span>
<span v-if="productMetadata.storage">{{ productMetadata.storage / 1024 }}GB Storage</span>
<span v-else>Unknown Storage</span>
<span class="text-secondary opacity-50"></span>
<span v-if="productMetadata.swap">{{ productMetadata.swap }}MB Swap</span>
<span v-else>Unknown Swap</span>
</span>
<span class="text-sm text-secondary">
<span
v-if="charge.status === 'cancelled' && dayjs(charge.due).isBefore(dayjs())"
class="font-bold"
>
Ended:
</span>
<span v-else-if="charge.status === 'cancelled'" class="font-bold">Ends:</span>
<span v-else-if="charge.type === 'refund'" class="font-bold">Issued:</span>
<span v-else class="font-bold">Due:</span>
{{ formatDateTime(charge.due) }}
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
</span>
<span v-if="charge.last_attempt != null" class="text-sm text-secondary">
<span v-if="charge.status === 'failed'" class="font-bold">Last attempt:</span>
<span v-else class="font-bold">Charged:</span>
{{ formatDateTime(charge.last_attempt) }}
<span class="text-secondary">({{ formatRelativeTime(charge.last_attempt) }}) </span>
</span>
<div class="flex w-full items-center gap-1 text-xs text-secondary">
{{ charge.status }}
<span class="text-secondary opacity-50"></span>
{{ charge.type }}
<span class="text-secondary opacity-50"></span>
{{ formatPrice(charge.amount, charge.currency_code) }}
<span class="text-secondary opacity-50"></span>
{{ formatDateTimeShort(charge.due) }}
<template v-if="charge.subscription_interval">
<span class="text-secondary opacity-50"></span>
{{ charge.subscription_interval }}
</template>
</div>
</div>
<div class="flex gap-2">
<ButtonStyled v-if="isRefunded">
<div class="button-like disabled"><CheckIcon /> Charge refunded</div>
</ButtonStyled>
<ButtonStyled
v-else-if="charge.status === 'succeeded' && charge.type !== 'refund'"
color="red"
color-fill="text"
>
<button @click="emit('refund', charge)">
<CurrencyIcon />
Refund options
</button>
</ButtonStyled>
<ButtonStyled
v-else-if="charge.status === 'failed' || charge.status === 'open'"
color="red"
color-fill="text"
>
<button @click="emit('modify', charge, subscription)">
<CurrencyIcon />
Modify charge
</button>
</ButtonStyled>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { CheckIcon, CurrencyIcon } from '@modrinth/assets'
import { ButtonStyled, useFormatDateTime, useFormatPrice, useRelativeTime } from '@modrinth/ui'
import dayjs from 'dayjs'
import { products } from '~/generated/state.json'
const props = defineProps<{
charge: Labrinth.Billing.Internal.Charge
subscription: Labrinth.Billing.Internal.UserSubscription
allCharges: Labrinth.Billing.Internal.Charge[]
chargeIndex: number
chargeCount: number
}>()
const emit = defineEmits<{
refund: [charge: Labrinth.Billing.Internal.Charge]
modify: [
charge: Labrinth.Billing.Internal.Charge,
subscription: Labrinth.Billing.Internal.UserSubscription,
]
}>()
const formatPrice = useFormatPrice()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const formatDateTimeShort = useFormatDateTime({
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
})
const formatRelativeTime = useRelativeTime()
const isLatestCharge = computed(() => props.chargeIndex === props.chargeCount - 1)
const isRefunded = computed(() =>
props.allCharges.some(
(charge) => charge.type === 'refund' && charge.parent_charge_id === props.charge.id,
),
)
const productMetadata = computed(
() =>
products.find((product) => product.prices.some((price) => price.id === props.charge.price_id))
?.metadata,
)
const chargeStatuses = {
open: {
color: 'bg-blue',
},
processing: {
color: 'bg-orange',
},
succeeded: {
color: 'bg-green',
},
failed: {
color: 'bg-red',
},
cancelled: {
color: 'bg-red',
},
expiring: {
color: 'bg-orange',
},
}
</script>
@@ -132,23 +132,39 @@ export function createLoaderParsers(
),
}
},
// Fabric
// Fabric (or Babric for mc version beta 1.7.3)
'fabric.mod.json': (file: string): InferredVersionInfo => {
const metadata = JSON.parse(file) as any
const detectedGameVersions = metadata.depends
const mcDependency = metadata.depends?.minecraft
const mcDependencies = Array.isArray(mcDependency) ? mcDependency : [mcDependency]
let detectedGameVersions = metadata.depends
? getGameVersionsMatchingSemverRange(metadata.depends.minecraft, simplifiedGameVersions)
: []
const loaders: string[] = []
// Detect Beta 1.7.3 -> Babric
const hasBabricVersion = mcDependencies.some(
(version: string | undefined) => version?.includes('1.0.0-beta.7.3'), // this is fabric's normalized mc version format
)
// Detect 1.3-1.13 -> legacy-fabric
const hasLegacyVersions = detectedGameVersions.some((version) => {
const match = version.match(/^1\.(\d+)/)
return match && parseInt(match[1]) >= 3 && parseInt(match[1]) <= 13
})
if (hasLegacyVersions) loaders.push('legacy-fabric')
else loaders.push('fabric')
if (hasBabricVersion) {
loaders.push('babric')
detectedGameVersions = gameVersions
.filter((version) => version.version === 'b1.7.3')
.map((version) => version.version)
} else if (hasLegacyVersions) {
loaders.push('legacy-fabric')
} else {
loaders.push('fabric')
}
return {
name: `${project.title} ${metadata.version}`,
@@ -50,15 +50,15 @@ export function getGameVersionsMatchingMavenRange(
ranges.push(range)
}
const LESS_THAN_EQUAL = /^\(,(.*)]$/
const LESS_THAN = /^\(,(.*)\)$/
const EQUAL = /^\[(.*)]$/
const GREATER_THAN_EQUAL = /^\[(.*),\)$/
const GREATER_THAN = /^\((.*),\)$/
const BETWEEN = /^\((.*),(.*)\)$/
const BETWEEN_EQUAL = /^\[(.*),(.*)]$/
const BETWEEN_LESS_THAN_EQUAL = /^\((.*),(.*)]$/
const BETWEEN_GREATER_THAN_EQUAL = /^\[(.*),(.*)\)$/
const LESS_THAN_EQUAL = /^\(,([^,]*)]$/
const LESS_THAN = /^\(,([^,]*)\)$/
const EQUAL = /^\[([^,]*)]$/
const GREATER_THAN_EQUAL = /^\[([^,]*),\)$/
const GREATER_THAN = /^\(([^,]*),\)$/
const BETWEEN = /^\(([^,]*),([^,]*)\)$/
const BETWEEN_EQUAL = /^\[([^,]*),([^,]*)]$/
const BETWEEN_LESS_THAN_EQUAL = /^\(([^,]*),([^,]*)]$/
const BETWEEN_GREATER_THAN_EQUAL = /^\[([^,]*),([^,]*)\)$/
const semverRanges = []
@@ -198,115 +198,17 @@
</div>
</div>
<div class="flex flex-col gap-2">
<div
<AdminBillingChargeCard
v-for="(charge, index) in subscription.charges"
:key="charge.id"
class="relative overflow-clip rounded-xl bg-bg px-4 py-3"
>
<div
class="absolute bottom-0 left-0 top-0 w-1"
:class="
charge.type === 'refund'
? 'bg-purple'
: (chargeStatuses[charge.status]?.color ?? 'bg-blue')
"
/>
<div class="grid w-full grid-cols-[1fr_auto] items-center gap-4">
<div class="flex flex-col gap-2">
<span>
<span class="font-bold text-contrast">
<template v-if="charge.status === 'succeeded'"> Succeeded </template>
<template v-else-if="charge.status === 'failed'"> Failed </template>
<template v-else-if="charge.status === 'cancelled'"> Cancelled </template>
<template v-else-if="charge.status === 'processing'"> Processing </template>
<template v-else-if="charge.status === 'open'"> Upcoming </template>
<template v-else-if="charge.status === 'expiring'"> Expiring </template>
<template v-else> {{ charge.status }} </template>
</span>
<span>
<template v-if="charge.type === 'refund'"> Refund </template>
<template v-else-if="charge.type === 'subscription'">
<template v-if="charge.status === 'cancelled'"> Subscription </template>
<template v-else-if="index === subscription.charges.length - 1">
Started subscription
</template>
<template v-else> Subscription renewal </template>
</template>
<template v-else-if="charge.type === 'one-time'"> One-time charge </template>
<template v-else-if="charge.type === 'proration'"> Proration charge </template>
<template v-else> {{ charge.status }} </template>
</span>
<template v-if="charge.status !== 'cancelled'">
{{ formatPrice(charge.amount, charge.currency_code) }}
</template>
</span>
<span class="text-sm text-secondary">
<span
v-if="charge.status === 'cancelled' && $dayjs(charge.due).isBefore($dayjs())"
class="font-bold"
>
Ended:
</span>
<span v-else-if="charge.status === 'cancelled'" class="font-bold">Ends:</span>
<span v-else-if="charge.type === 'refund'" class="font-bold">Issued:</span>
<span v-else class="font-bold">Due:</span>
{{ formatDateTime(charge.due) }}
<span class="text-secondary">({{ formatRelativeTime(charge.due) }}) </span>
</span>
<span v-if="charge.last_attempt != null" class="text-sm text-secondary">
<span v-if="charge.status === 'failed'" class="font-bold">Last attempt:</span>
<span v-else class="font-bold">Charged:</span>
{{ formatDateTime(charge.last_attempt) }}
<span class="text-secondary"
>({{ formatRelativeTime(charge.last_attempt) }})
</span>
</span>
<div class="flex w-full items-center gap-1 text-xs text-secondary">
{{ charge.status }}
{{ charge.type }}
{{ formatPrice(charge.amount, charge.currency_code) }}
{{ formatDateTimeShort(charge.due) }}
<template v-if="charge.subscription_interval">
{{ charge.subscription_interval }}
</template>
</div>
</div>
<div class="flex gap-2">
<ButtonStyled
v-if="
charges.some((x) => x.type === 'refund' && x.parent_charge_id === charge.id)
"
>
<div class="button-like disabled"><CheckIcon /> Charge refunded</div>
</ButtonStyled>
<ButtonStyled
v-else-if="charge.status === 'succeeded' && charge.type !== 'refund'"
color="red"
color-fill="text"
>
<button @click="showRefundModal(charge)">
<CurrencyIcon />
Refund options
</button>
</ButtonStyled>
<ButtonStyled
v-else-if="charge.status === 'failed' || charge.status === 'open'"
color="red"
color-fill="text"
>
<button @click="showModifyModal(charge, subscription)">
<CurrencyIcon />
Modify charge
</button>
</ButtonStyled>
</div>
</div>
</div>
:charge="charge"
:subscription="subscription"
:all-charges="charges"
:charge-index="index"
:charge-count="subscription.charges.length"
@refund="showRefundModal"
@modify="showModifyModal"
/>
</div>
</div>
</div>
@@ -334,7 +236,6 @@ import {
StyledInput,
Toggle,
useFormatDateTime,
useFormatPrice,
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
@@ -344,21 +245,14 @@ import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import ModrinthServersIcon from '~/components/brand/ModrinthServersIcon.vue'
import AdminBillingChargeCard from '~/components/ui/admin/AdminBillingChargeCard.vue'
const { addNotification } = injectNotificationManager()
const { labrinth } = injectModrinthClient()
const formatPrice = useFormatPrice()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const formatDateTimeShort = useFormatDateTime({
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
})
const vintl = useVIntl()
@@ -372,15 +266,15 @@ const messages = defineMessages({
},
})
const chargeId = useRouteId('charge')
const userId = useRouteId('user')
const {
data: user,
error: userError,
suspense: userSuspense,
} = useQuery({
queryKey: ['user', chargeId],
queryFn: () => labrinth.users_v2.get(chargeId),
queryKey: ['user', userId],
queryFn: () => labrinth.users_v2.get(userId),
})
onServerPrefetch(userSuspense)
@@ -533,27 +427,6 @@ async function modifyCharge() {
}
modifying.value = false
}
const chargeStatuses = {
open: {
color: 'bg-blue',
},
processing: {
color: 'bg-orange',
},
succeeded: {
color: 'bg-green',
},
failed: {
color: 'bg-red',
},
cancelled: {
color: 'bg-red',
},
expiring: {
color: 'bg-orange',
},
}
</script>
<style scoped>
.page {
@@ -1,7 +1,13 @@
<script setup lang="ts">
import { GitGraphIcon, RssIcon } from '@modrinth/assets'
import { articles as rawArticles } from '@modrinth/blog'
import { Avatar, ButtonStyled, injectModrinthClient, useFormatDateTime } from '@modrinth/ui'
import {
ArticleBody,
Avatar,
ButtonStyled,
injectModrinthClient,
useFormatDateTime,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import dayjs from 'dayjs'
import { computed, onMounted } from 'vue'
@@ -169,7 +175,7 @@ onMounted(() => {
class="aspect-video w-full rounded-xl border-[1px] border-solid border-button-border object-cover sm:rounded-2xl"
:alt="article.title"
/>
<div class="markdown-body" v-html="article.html" />
<ArticleBody :html="article.html" />
<h3
class="mb-0 mt-4 border-0 border-t-[1px] border-solid border-divider pt-4 text-base font-extrabold sm:text-lg"
>
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

@@ -1,5 +1,12 @@
{
"articles": [
{
"title": "Modrinth joins Spark Universe",
"summary": "The next chapter. What it means and why we think its right for Modrinth.",
"thumbnail": "https://modrinth.com/news/article/joining-spark-universe/thumbnail.webp",
"date": "2026-06-15T14:00:00.000Z",
"link": "https://modrinth.com/news/article/joining-spark-universe"
},
{
"title": "Manage servers together",
"summary": "Add other users to your server, assign roles, and track whats changed.",
File diff suppressed because one or more lines are too long