Envs v3 frontend (#4267)

* New envs frontend

* lint fix

* Add blog post, user-facing changes, dashboard warning, project page member warning, and migration reviewing. maybe some other misc stuff

* lint

* lint

* ignore .data in .prettierignore

* i18n as fuck

* fix proj page

* Improve news markdown rendering

* improve phrasing of initial paragraph

* Fix environments not reloading after save

* index.ts instead of underscored name

* shrink-0 back on these icons
This commit is contained in:
Prospector
2025-08-28 15:11:35 -07:00
committed by GitHub
parent 0ac42344e7
commit 46c325f78a
49 changed files with 2509 additions and 397 deletions

View File

@@ -1,6 +1,7 @@
**/.nuxt
**/dist
**/.output
**/.data
src/generated/**
src/locales/**
src/public/news/feed

View File

@@ -7,10 +7,30 @@
</template>
<script setup lang="ts">
import { NotificationPanel, provideNotificationManager } from '@modrinth/ui'
import { provideApi } from '@modrinth/ui/src/providers/api.ts'
import { RestModrinthApi } from '@modrinth/utils'
import ModrinthLoadingIndicator from '~/components/ui/modrinth-loading-indicator.ts'
import { FrontendNotificationManager } from './providers/frontend-notifications.ts'
provideNotificationManager(new FrontendNotificationManager())
provideApi(
new RestModrinthApi((url: string, options?: object) => {
const match = url.match(/^\/v(\d+)\/(.+)$/)
if (match) {
const apiVersion = Number(match[1])
const path = match[2]
return useBaseFetch(path, {
...options,
apiVersion,
}) as Promise<Response>
} else {
throw new Error('Invalid format')
}
}),
)
</script>

View File

@@ -42,6 +42,7 @@
padding: 0 1.5rem;
grid-template:
'header'
'sidebar'
'content'
'info'

View File

@@ -1,23 +1,18 @@
<template>
<NuxtLink v-if="link !== null" class="nav-link button-base" :to="link">
<div class="nav-content">
<slot />
<span>{{ label }}</span>
<span v-if="beta" class="beta-badge">BETA</span>
<span v-if="chevron" class="chevron"><ChevronRightIcon /></span>
</div>
<NuxtLink v-if="link !== null" :to="link" class="nav-item">
<slot />
<span>{{ label }}</span>
<span v-if="badge" class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand">{{
badge
}}</span>
<span v-if="chevron" class="ml-auto"><ChevronRightIcon /></span>
</NuxtLink>
<button
v-else-if="action"
class="nav-link button-base"
:class="{ 'danger-button': danger }"
@click="action"
>
<span class="nav-content">
<slot />
<span>{{ label }}</span>
<span v-if="beta" class="beta-badge">BETA</span>
</span>
<button v-else-if="action" class="nav-item" :class="{ 'danger-button': danger }" @click="action">
<slot />
<span>{{ label }}</span>
<span v-if="badge" class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand">{{
badge
}}</span>
</button>
<span v-else>i forgor 💀</span>
</template>
@@ -42,9 +37,9 @@ export default {
required: true,
type: String,
},
beta: {
default: false,
type: Boolean,
badge: {
default: null,
type: String,
},
chevron: {
default: false,
@@ -59,58 +54,11 @@ export default {
</script>
<style lang="scss" scoped>
.nav-link {
font-weight: var(--font-weight-bold);
background-color: transparent;
color: var(--text-color);
position: relative;
display: flex;
flex-direction: row;
gap: 0.25rem;
box-shadow: none;
padding: 0;
width: 100%;
outline: none;
.nav-item {
@apply flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2 text-left font-semibold text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97];
}
:where(.nav-link) {
--text-color: var(--color-text);
--background-color: var(--color-raised-bg);
}
.nav-content {
box-sizing: border-box;
padding: 0.5rem 0.75rem;
border-radius: var(--size-rounded-sm);
display: flex;
align-items: center;
gap: 0.4rem;
flex-grow: 1;
background-color: var(--background-color);
}
&:focus-visible {
.nav-content {
border-radius: 0.25rem;
}
}
&.router-link-exact-active {
outline: 2px solid transparent;
border-radius: 0.25rem;
.nav-content {
color: var(--color-button-text-active);
background-color: var(--color-button-bg);
box-shadow: none;
}
}
.beta-badge {
margin: 0;
}
.chevron {
margin-left: auto;
}
.router-link-exact-active.nav-item {
@apply bg-button-bgSelected text-button-textSelected;
}
</style>

View File

@@ -7,17 +7,17 @@
</h2>
<div class="flex flex-row gap-2">
<div class="flex items-center gap-1">
<AsteriskIcon class="size-4 text-red" />
<AsteriskIcon class="size-4 shrink-0 text-red" />
<span class="text-secondary">{{ getFormattedMessage(messages.required) }}</span>
</div>
|
<div class="flex items-center gap-1">
<TriangleAlertIcon class="size-4 text-orange" />
<TriangleAlertIcon class="size-4 shrink-0 text-orange" />
<span class="text-secondary">{{ getFormattedMessage(messages.warning) }}</span>
</div>
|
<div class="flex items-center gap-1">
<LightBulbIcon class="size-4 text-purple" />
<LightBulbIcon class="size-4 shrink-0 text-purple" />
<span class="text-secondary">{{ getFormattedMessage(messages.suggestion) }}</span>
</div>
</div>

View File

@@ -35,6 +35,8 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
showProjectPageDownloadModalServersPromo: false,
showProjectPageCreateServersTooltip: true,
showProjectPageQuickServerButton: false,
newProjectGeneralSettings: false,
newProjectEnvironmentSettings: true,
// advancedRendering: true,
// externalLinksNewTab: true,
// notUsingBlockers: false,

View File

@@ -77,6 +77,7 @@ export const initUserProjects = async () => {
if (auth.user && auth.user.id) {
try {
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`)
user.projectsV3 = await useBaseFetch(`user/${auth.user.id}/projects`, { apiVersion: 3 })
} catch (err) {
console.error(err)
}

View File

@@ -683,21 +683,231 @@
"project.about.details.updated": {
"message": "Updated {date}"
},
"project.actions.create-server": {
"message": "Create a server"
},
"project.actions.create-server-tooltip": {
"message": "Create a server"
},
"project.actions.dont-show-again": {
"message": "Don't show again"
},
"project.actions.review-project": {
"message": "Review project"
},
"project.actions.servers-promo.description": {
"message": "Modrinth Servers is the easiest way to play with your friends without hassle!"
},
"project.actions.servers-promo.monthly": {
"message": " / month"
},
"project.actions.servers-promo.pricing": {
"message": "Starting at $5{monthly}"
},
"project.actions.servers-promo.title": {
"message": "Create a server"
},
"project.collections.create-new": {
"message": "Create new collection"
},
"project.collections.none-found": {
"message": "No collections found."
},
"project.description.title": {
"message": "Description"
},
"project.details.licensed": {
"message": "Licensed"
},
"project.download.game-version": {
"message": "Game version: {version}"
},
"project.download.game-version-error": {
"message": "Error: no game versions found"
},
"project.download.game-version-tooltip": {
"message": "{title} is only available for {version}"
},
"project.download.game-version-unsupported-tooltip": {
"message": "{title} does not support {gameVersion} for {platform}"
},
"project.download.install-with-app": {
"message": "Install with Modrinth App"
},
"project.download.no-app": {
"message": "Don't have Modrinth App?"
},
"project.download.no-versions-available": {
"message": "No versions available for {gameVersion} and {platform}."
},
"project.download.platform": {
"message": "Platform: {platform}"
},
"project.download.platform-error": {
"message": "Error: no platforms found"
},
"project.download.platform-tooltip": {
"message": "{title} is only available for {platform}"
},
"project.download.platform-unsupported-tooltip": {
"message": "{title} does not support {platform} for {gameVersion}"
},
"project.download.search-game-versions": {
"message": "Search game versions..."
},
"project.download.search-game-versions-label": {
"message": "Search game versions..."
},
"project.download.select-game-version": {
"message": "Select game version"
},
"project.download.select-platform": {
"message": "Select platform"
},
"project.download.show-all-versions": {
"message": "Show all versions"
},
"project.download.title": {
"message": "Download {title}"
},
"project.environment.migration.message": {
"message": "We've just overhauled the Environments system on Modrinth and new options are now available. Please visit your project's settings and verify that the metadata is correct."
},
"project.environment.migration.review-button": {
"message": "Review environment settings"
},
"project.environment.migration.title": {
"message": "Please review environment metadata"
},
"project.error.loading": {
"message": "Error loading project data{message}"
},
"project.error.page-not-found": {
"message": "The page could not be found"
},
"project.error.project-not-found": {
"message": "Project not found"
},
"project.gallery.title": {
"message": "Gallery"
},
"project.license.error": {
"message": "License text could not be retrieved."
},
"project.license.loading": {
"message": "Loading license text..."
},
"project.license.title": {
"message": "License"
},
"project.moderation.title": {
"message": "Moderation"
},
"project.navigation.changelog": {
"message": "Changelog"
},
"project.notification.icon-updated.message": {
"message": "Your project's icon has been updated."
},
"project.notification.icon-updated.title": {
"message": "Project icon updated"
},
"project.notification.updated.message": {
"message": "Your project has been updated."
},
"project.notification.updated.title": {
"message": "Project updated"
},
"project.settings.environment.notice.missing-env.description": {
"message": "Your project is missing environment metadata, please select the appropriate option below."
},
"project.settings.environment.notice.missing-env.title": {
"message": "Please select an environment for your project"
},
"project.settings.environment.notice.multiple-environments.description": {
"message": "Different versions of your project have different environments selected, so you can't edit them globally at this time."
},
"project.settings.environment.notice.multiple-environments.title": {
"message": "Your project has multiple environments"
},
"project.settings.environment.notice.review-options.description": {
"message": "We've just overhauled the Environments system on Modrinth and new options are now available. Please ensure the correct option is selected below and then click 'Verify' when you're done!"
},
"project.settings.environment.notice.review-options.title": {
"message": "Please review the options below"
},
"project.settings.environment.notice.wrong-project-type.description": {
"message": "Only mod or modpack projects can have environment metadata."
},
"project.settings.environment.notice.wrong-project-type.title": {
"message": "This project type does not support environment metadata"
},
"project.settings.environment.verification.verify-button": {
"message": "Verify"
},
"project.settings.environment.verification.verify-text": {
"message": "Verify that this project's environment is set correctly."
},
"project.settings.general.name.description": {
"message": "Avoid prefixes, suffixes, parentheticals, or added descriptions—just the project's actual name."
},
"project.settings.general.name.placeholder.1": {
"message": "e.g. Nether Overhaul 2"
},
"project.settings.general.name.placeholder.2": {
"message": "e.g. Construction Equipment"
},
"project.settings.general.name.placeholder.3": {
"message": "e.g. Better than Caving"
},
"project.settings.general.name.placeholder.4": {
"message": "e.g. Enhanced Portals"
},
"project.settings.general.name.placeholder.5": {
"message": "e.g. Dangerous Mobs"
},
"project.settings.general.name.title": {
"message": "Name"
},
"project.settings.general.tagline.description": {
"message": "Summarize your project in no more than one sentence."
},
"project.settings.general.tagline.placeholder.1": {
"message": "e.g. Overhauls game progression to revolve around the Nether."
},
"project.settings.general.tagline.placeholder.2": {
"message": "e.g. Adds wearable construction gear."
},
"project.settings.general.tagline.placeholder.3": {
"message": "e.g. Adds realistic mineshaft-building mechanics."
},
"project.settings.general.tagline.placeholder.4": {
"message": "e.g. Improves how Nether portals link to each other."
},
"project.settings.general.tagline.placeholder.5": {
"message": "e.g. Adds powerful boss versions of the normal mobs to encounter in the night."
},
"project.settings.general.tagline.title": {
"message": "Tagline"
},
"project.settings.general.url.title": {
"message": "URL"
},
"project.settings.title": {
"message": "Settings"
},
"project.settings.visit-dashboard": {
"message": "Visit projects dashboard"
},
"project.stats.downloads-label": {
"message": "download{count, plural, one {} other {s}}"
},
"project.stats.followers-label": {
"message": "follower{count, plural, one {} other {s}}"
},
"project.status.archived.message": {
"message": "{title} has been archived. {title} will not receive any further updates unless the author decides to unarchive the project."
},
"project.version.all-versions": {
"message": "All versions"
},

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,162 @@
<script setup lang="ts">
import {
AlignLeftIcon,
BookTextIcon,
ChartIcon,
GlobeIcon,
ImageIcon,
InfoIcon,
LinkIcon,
TagsIcon,
UsersIcon,
VersionIcon,
} from '@modrinth/assets'
import { commonMessages, commonProjectSettingsMessages } from '@modrinth/ui'
import type { Project, ProjectV3Partial } from '@modrinth/utils'
import { useVIntl } from '@vintl/vintl'
import NavStack from '~/components/ui/NavStack.vue'
import NavStackItem from '~/components/ui/NavStackItem.vue'
const { formatMessage } = useVIntl()
defineProps<{
currentMember: any
patchProject: any
patchIcon: any
resetProject: any
resetOrganization: any
resetMembers: any
}>()
const flags = useFeatureFlags()
const project = defineModel<Project>('project', { required: true })
const projectV3 = defineModel<ProjectV3Partial>('projectV3', { required: true })
const versions = defineModel<any>('versions')
const featuredVersions = defineModel<any>('featuredVersions')
const members = defineModel<any>('members')
const allMembers = defineModel<any>('allMembers')
const dependencies = defineModel<any>('dependencies')
const organization = defineModel<any>('organization')
</script>
<template>
<div class="experimental-styles-within grid grid-cols-[1fr_3fr] gap-4">
<div>
<aside class="universal-card">
<NavStack>
<NavStackItem
:link="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
:label="formatMessage(commonProjectSettingsMessages.general)"
>
<InfoIcon aria-hidden="true" />
</NavStackItem>
<NavStackItem
v-if="flags.newProjectGeneralSettings"
:link="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings/general`"
:label="formatMessage(commonProjectSettingsMessages.general)"
:badge="formatMessage(commonMessages.newBadge)"
>
<InfoIcon aria-hidden="true" />
</NavStackItem>
<NavStackItem
v-if="
flags.newProjectEnvironmentSettings &&
projectV3.project_types.some((type) => ['mod', 'modpack'].includes(type))
"
:link="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/settings/environment`"
:label="formatMessage(commonProjectSettingsMessages.environment)"
:badge="formatMessage(commonMessages.newBadge)"
>
<GlobeIcon aria-hidden="true" />
</NavStackItem>
<NavStackItem
:link="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/settings/tags`"
:label="formatMessage(commonProjectSettingsMessages.tags)"
>
<TagsIcon aria-hidden="true" />
</NavStackItem>
<NavStackItem
:link="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/settings/description`"
:label="formatMessage(commonProjectSettingsMessages.description)"
>
<AlignLeftIcon aria-hidden="true" />
</NavStackItem>
<NavStackItem
:link="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/settings/license`"
:label="formatMessage(commonProjectSettingsMessages.license)"
>
<BookTextIcon aria-hidden="true" />
</NavStackItem>
<NavStackItem
:link="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/settings/links`"
:label="formatMessage(commonProjectSettingsMessages.links)"
>
<LinkIcon aria-hidden="true" />
</NavStackItem>
<NavStackItem
:link="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/settings/members`"
:label="formatMessage(commonProjectSettingsMessages.members)"
>
<UsersIcon aria-hidden="true" />
</NavStackItem>
<h3>{{ formatMessage(commonProjectSettingsMessages.view) }}</h3>
<NavStackItem
:link="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/settings/analytics`"
:label="formatMessage(commonProjectSettingsMessages.analytics)"
chevron
>
<ChartIcon aria-hidden="true" />
</NavStackItem>
<h3>{{ formatMessage(commonProjectSettingsMessages.upload) }}</h3>
<NavStackItem
:link="`/${project.project_type}/${project.slug ? project.slug : project.id}/gallery`"
:label="formatMessage(commonProjectSettingsMessages.gallery)"
chevron
>
<ImageIcon aria-hidden="true" />
</NavStackItem>
<NavStackItem
:link="`/${project.project_type}/${project.slug ? project.slug : project.id}/versions`"
:label="formatMessage(commonProjectSettingsMessages.versions)"
chevron
>
<VersionIcon aria-hidden="true" />
</NavStackItem>
</NavStack>
</aside>
</div>
<div>
<NuxtPage
v-model:project="project"
v-model:project-v3="projectV3"
v-model:versions="versions"
v-model:featured-versions="featuredVersions"
v-model:members="members"
v-model:all-members="allMembers"
v-model:dependencies="dependencies"
v-model:organization="organization"
:current-member="currentMember"
:patch-project="patchProject"
:patch-icon="patchIcon"
:reset-project="resetProject"
:reset-organization="resetOrganization"
:reset-members="resetMembers"
/>
</div>
</div>
</template>

View File

@@ -0,0 +1,165 @@
<script setup lang="ts">
import { CheckIcon } from '@modrinth/assets'
import {
Admonition,
commonProjectSettingsMessages,
injectNotificationManager,
injectProjectPageContext,
ProjectSettingsEnvSelector,
UnsavedChangesPopup,
useSavable,
} from '@modrinth/ui'
import { injectApi } from '@modrinth/ui/src/providers/api.ts'
import { defineMessages, useVIntl } from '@vintl/vintl'
const { formatMessage } = useVIntl()
const { projectV2, projectV3, refreshProject } = injectProjectPageContext()
const { handleError } = injectNotificationManager()
const api = injectApi()
const saving = ref(false)
const supportsEnvironment = computed(() =>
projectV3.value.project_types.some((type) => ['mod', 'modpack'].includes(type)),
)
const needsToVerify = computed(
() =>
projectV3.value.side_types_migration_review_status === 'pending' &&
projectV3.value.environment?.length > 0 &&
projectV3.value.environment?.[0] !== 'unknown' &&
supportsEnvironment.value,
)
function getInitialEnv() {
return projectV3.value.environment?.length === 1 ? projectV3.value.environment[0] : undefined
}
const { saved, current, reset, save } = useSavable(
() => ({
environment: getInitialEnv(),
side_types_migration_review_status: projectV3.value.side_types_migration_review_status,
}),
({ environment, side_types_migration_review_status }) => {
saving.value = true
side_types_migration_review_status = 'reviewed'
api.projects
.editV3(projectV2.value.id, { environment, side_types_migration_review_status })
.then(() => refreshProject().then(reset))
.catch(handleError)
.finally(() => (saving.value = false))
},
)
// Set current to reviewed, which will trigger unsaved changes popup.
// It should not be possible to save without reviewing it.
const originalEnv = getInitialEnv()
if (originalEnv && originalEnv !== 'unknown') {
current.value.side_types_migration_review_status = 'reviewed'
}
const messages = defineMessages({
verifyButton: {
id: 'project.settings.environment.verification.verify-button',
defaultMessage: 'Verify',
},
verifyLabel: {
id: 'project.settings.environment.verification.verify-text',
defaultMessage: `Verify that this project's environment is set correctly.`,
},
wrongProjectTypeTitle: {
id: 'project.settings.environment.notice.wrong-project-type.title',
defaultMessage: `This project type does not support environment metadata`,
},
wrongProjectTypeDescription: {
id: 'project.settings.environment.notice.wrong-project-type.description',
defaultMessage: `Only mod or modpack projects can have environment metadata.`,
},
missingEnvTitle: {
id: 'project.settings.environment.notice.missing-env.title',
defaultMessage: `Please select an environment for your project`,
},
missingEnvDescription: {
id: 'project.settings.environment.notice.missing-env.description',
defaultMessage: `Your project is missing environment metadata, please select the appropriate option below.`,
},
multipleEnvironmentsTitle: {
id: 'project.settings.environment.notice.multiple-environments.title',
defaultMessage: 'Your project has multiple environments',
},
multipleEnvironmentsDescription: {
id: 'project.settings.environment.notice.multiple-environments.description',
defaultMessage:
"Different versions of your project have different environments selected, so you can't edit them globally at this time.",
},
reviewOptionsTitle: {
id: 'project.settings.environment.notice.review-options.title',
defaultMessage: 'Please review the options below',
},
reviewOptionsDescription: {
id: 'project.settings.environment.notice.review-options.description',
defaultMessage:
"We've just overhauled the Environments system on Modrinth and new options are now available. Please ensure the correct option is selected below and then click 'Verify' when you're done!",
},
})
</script>
<template>
<div>
<UnsavedChangesPopup
v-if="supportsEnvironment"
:original="saved"
:modified="current"
:saving="saving"
:can-reset="!needsToVerify"
:text="needsToVerify ? messages.verifyLabel : undefined"
:save-label="needsToVerify ? messages.verifyButton : undefined"
:save-icon="needsToVerify ? CheckIcon : undefined"
@reset="reset"
@save="save"
/>
<div class="card experimental-styles-within">
<h2 class="m-0 mb-2 block text-lg font-extrabold text-contrast">
{{ formatMessage(commonProjectSettingsMessages.environment) }}
</h2>
<Admonition
v-if="!supportsEnvironment"
type="critical"
:header="formatMessage(messages.wrongProjectTypeTitle)"
class="mb-3"
>
{{ formatMessage(messages.wrongProjectTypeDescription) }}
</Admonition>
<template v-else>
<Admonition
v-if="
!projectV3.environment ||
projectV3.environment.length === 0 ||
projectV3.environment[0] === 'unknown'
"
type="critical"
:header="formatMessage(messages.missingEnvTitle)"
class="mb-3"
>
{{ formatMessage(messages.missingEnvDescription) }}
</Admonition>
<Admonition
v-else-if="projectV3.environment.length > 1"
type="info"
:header="formatMessage(messages.multipleEnvironmentsTitle)"
class="mb-3"
>
{{ formatMessage(messages.multipleEnvironmentsDescription) }}
</Admonition>
<Admonition
v-else-if="needsToVerify"
type="warning"
:header="formatMessage(messages.reviewOptionsTitle)"
class="mb-3"
>
{{ formatMessage(messages.reviewOptionsDescription) }}
</Admonition>
<ProjectSettingsEnvSelector v-model="current.environment" />
</template>
</div>
</div>
</template>

View File

@@ -0,0 +1,192 @@
<script setup lang="ts">
import {
IconSelect,
injectNotificationManager,
injectProjectPageContext,
SettingsLabel,
UnsavedChangesPopup,
useSavable,
} from '@modrinth/ui'
import { injectApi } from '@modrinth/ui/src/providers/api.ts'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
const { formatMessage } = useVIntl()
const { projectV2: project, refreshProject } = injectProjectPageContext()
const { handleError } = injectNotificationManager()
const api = injectApi()
const saving = ref(false)
const { saved, current, reset, save } = useSavable(
() => ({
title: project.value.title,
tagline: project.value.description,
url: project.value.slug,
icon: project.value.icon_url,
}),
({ title, tagline, url }) => {
const data: Record<string, string> = {
...(title !== undefined && { title }),
...(tagline !== undefined && { description: tagline }),
...(url !== undefined && { slug: url }),
}
if (data) {
saving.value = true
api.projects
.edit(project.value.id, { title, description: tagline, slug: url })
.then(() => refreshProject().then(reset))
.catch(handleError)
.finally(() => (saving.value = false))
}
},
)
const messages = defineMessages({
nameTitle: {
id: 'project.settings.general.name.title',
defaultMessage: 'Name',
},
nameDescription: {
id: 'project.settings.general.name.description',
defaultMessage:
"Avoid prefixes, suffixes, parentheticals, or added descriptions—just the project's actual name.",
},
taglineTitle: {
id: 'project.settings.general.tagline.title',
defaultMessage: 'Tagline',
},
taglineDescription: {
id: 'project.settings.general.tagline.description',
defaultMessage: 'Summarize your project in no more than one sentence.',
},
urlTitle: {
id: 'project.settings.general.url.title',
defaultMessage: 'URL',
},
})
const placeholders: { name: MessageDescriptor; tagline: MessageDescriptor }[] = [
defineMessages({
name: {
id: 'project.settings.general.name.placeholder.1',
defaultMessage: 'e.g. Nether Overhaul 2',
},
tagline: {
id: 'project.settings.general.tagline.placeholder.1',
defaultMessage: 'e.g. Overhauls game progression to revolve around the Nether.',
},
}),
defineMessages({
name: {
id: 'project.settings.general.name.placeholder.2',
defaultMessage: 'e.g. Construction Equipment',
},
tagline: {
id: 'project.settings.general.tagline.placeholder.2',
defaultMessage: 'e.g. Adds wearable construction gear.',
},
}),
defineMessages({
name: {
id: 'project.settings.general.name.placeholder.3',
defaultMessage: 'e.g. Better than Caving',
},
tagline: {
id: 'project.settings.general.tagline.placeholder.3',
defaultMessage: 'e.g. Adds realistic mineshaft-building mechanics.',
},
}),
defineMessages({
name: {
id: 'project.settings.general.name.placeholder.4',
defaultMessage: 'e.g. Enhanced Portals',
},
tagline: {
id: 'project.settings.general.tagline.placeholder.4',
defaultMessage: 'e.g. Improves how Nether portals link to each other.',
},
}),
defineMessages({
name: {
id: 'project.settings.general.name.placeholder.5',
defaultMessage: 'e.g. Dangerous Mobs',
},
tagline: {
id: 'project.settings.general.tagline.placeholder.5',
defaultMessage:
'e.g. Adds powerful boss versions of the normal mobs to encounter in the night.',
},
}),
]
const placeholderIndex = useState<number>('project-settings-random-placeholder', () =>
Math.floor(Math.random() * (placeholders.length + 1)),
)
const placeholder = computed(() => placeholders[placeholderIndex.value] ?? placeholders[0])
</script>
<template>
<div>
<UnsavedChangesPopup
:original="saved"
:modified="current"
:saving="saving"
@reset="reset"
@save="save"
/>
<div class="base-card block">
<div class="group relative float-end ml-4">
<IconSelect v-model="current.icon" />
</div>
<div>
<SettingsLabel
id="project-name"
:title="messages.nameTitle"
:description="messages.nameDescription"
/>
<div class="flex">
<input
id="project-name"
v-model="current.title"
:placeholder="formatMessage(placeholder.name)"
autocomplete="off"
maxlength="50"
class="flex-grow"
type="text"
/>
</div>
</div>
<div class="mt-4">
<SettingsLabel
id="project-tagline"
:title="messages.taglineTitle"
:description="messages.taglineDescription"
/>
<input
id="project-tagline"
v-model="current.tagline"
:placeholder="formatMessage(placeholder.tagline)"
autocomplete="off"
maxlength="120"
class="w-full"
type="text"
/>
</div>
<div class="mt-4">
<SettingsLabel id="project-url" :title="messages.urlTitle" />
<div class="text-input-wrapper">
<div class="text-input-wrapper__before">https://modrinth.com/project/</div>
<input
id="project-url"
v-model="current.url"
type="text"
maxlength="64"
autocomplete="off"
/>
</div>
</div>
</div>
</div>
</template>

View File

@@ -96,6 +96,7 @@
</div>
<template
v-if="
!flags.newProjectEnvironmentSettings &&
project.versions?.length !== 0 &&
project.project_type !== 'resourcepack' &&
project.project_type !== 'plugin' &&
@@ -258,15 +259,23 @@ import { formatProjectStatus, formatProjectType } from '@modrinth/utils'
import { Multiselect } from 'vue-multiselect'
import FileInput from '~/components/ui/FileInput.vue'
import { useFeatureFlags } from '~/composables/featureFlags.ts'
const { addNotification } = injectNotificationManager()
const flags = useFeatureFlags()
const props = defineProps({
project: {
type: Object,
required: true,
default: () => ({}),
},
projectV3: {
type: Object,
required: true,
default: () => ({}),
},
currentMember: {
type: Object,
required: true,

View File

@@ -282,9 +282,24 @@
<ProjectStatusBadge v-if="project.status" :status="project.status" />
</div>
<div>
<div class="flex !flex-row items-center !justify-end gap-2">
<ButtonStyled
v-if="projectsWithMigrationWarning.includes(project.id)"
circular
color="orange"
>
<nuxt-link
v-tooltip="'Please review environment metadata'"
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
project.slug ? project.slug : project.id
}/settings/environment`"
>
<TriangleAlertIcon />
</nuxt-link>
</ButtonStyled>
<ButtonStyled circular>
<nuxt-link
v-tooltip="formatMessage(commonMessages.settingsLabel)"
:to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${
project.slug ? project.slug : project.id
}/settings`"
@@ -310,6 +325,7 @@ import {
SortAscIcon,
SortDescIcon,
TrashIcon,
TriangleAlertIcon,
XIcon,
} from '@modrinth/assets'
import {
@@ -344,6 +360,7 @@ const { formatMessage } = useVIntl()
const user = await useUser()
const projects = ref([])
const projectsWithMigrationWarning = ref([])
const selectedProjects = ref([])
const sortBy = ref('Name')
const descending = ref(false)
@@ -437,6 +454,15 @@ async function bulkEditLinks() {
await initUserProjects()
if (user.value?.projects) {
projects.value = updateSort(user.value.projects, 'Name', false)
user.value?.projectsV3?.forEach((project) => {
if (
(project.side_types_migration_review_status === 'pending' &&
project.project_types.includes('mod')) ||
project.project_types.includes('modpack')
) {
projectsWithMigrationWarning.value.push(project.id)
}
})
}
</script>
<style lang="scss" scoped>

View File

@@ -181,7 +181,8 @@ useSeoMeta({
padding: 0;
}
ul > li:not(:last-child) {
ul,
ol > li:not(:last-child) {
margin-bottom: 0.5rem;
}
@@ -203,7 +204,7 @@ useSeoMeta({
h1,
h2,
h3 {
margin-bottom: 0.25rem;
margin-bottom: 0.5rem;
}
h1 {

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,5 +1,12 @@
{
"articles": [
{
"title": "Creators: Verify Your Environment Metadata",
"summary": "We've overhauled the environment metadata on Modrinth, and all creators must verify their settings.",
"thumbnail": "https://modrinth.com/news/article/new-environments/thumbnail.webp",
"date": "2025-08-28T07:00:00.000Z",
"link": "https://modrinth.com/news/article/new-environments"
},
{
"title": "Get a Free Modrinth Server",
"summary": "In partnership with Medal.tv, get a 5-day free trial for Modrinth Servers",

File diff suppressed because one or more lines are too long