diff --git a/apps/frontend/src/components/ui/create-project-version/stages/AddDetailsStage.vue b/apps/frontend/src/components/ui/create-project-version/stages/AddDetailsStage.vue index e757627339..2e508b5897 100644 --- a/apps/frontend/src/components/ui/create-project-version/stages/AddDetailsStage.vue +++ b/apps/frontend/src/components/ui/create-project-version/stages/AddDetailsStage.vue @@ -155,8 +155,9 @@ diff --git a/apps/frontend/src/components/ui/create-project-version/stages/AddEnvironmentStage.vue b/apps/frontend/src/components/ui/create-project-version/stages/AddEnvironmentStage.vue index ee04d8a614..f115f6c0b0 100644 --- a/apps/frontend/src/components/ui/create-project-version/stages/AddEnvironmentStage.vue +++ b/apps/frontend/src/components/ui/create-project-version/stages/AddEnvironmentStage.vue @@ -1,11 +1,11 @@ - diff --git a/apps/frontend/src/pages/[type]/[id]/version/[version].vue b/apps/frontend/src/pages/[type]/[id]/version/[version].vue index 6675d802d0..62399f4a49 100644 --- a/apps/frontend/src/pages/[type]/[id]/version/[version].vue +++ b/apps/frontend/src/pages/[type]/[id]/version/[version].vue @@ -560,6 +560,17 @@ {{ $formatVersion(version.game_versions) }} +
+

Environment

+
+ + + {{ environment.title.defaultMessage }} + +
+

Downloads

{{ version.downloads }} @@ -635,6 +646,7 @@ import { Checkbox, ConfirmModal, CopyCode, + ENVIRONMENTS_COPY, injectNotificationManager, MarkdownEditor, } from '@modrinth/ui' @@ -817,6 +829,12 @@ export default defineNuxtComponent({ if (!version) { version = props.versions.find((x) => x.displayUrlEnding === route.params.version) } + + const versionV3 = await useBaseFetch( + `project/${props.project.id}/version/${route.params.version}`, + { apiVersion: 3 }, + ) + if (versionV3) version.environment = versionV3.environment } if (!version) { @@ -933,6 +951,9 @@ export default defineNuxtComponent({ (a, b) => order.indexOf(a.dependency_type) - order.indexOf(b.dependency_type), ) }, + environment() { + return ENVIRONMENTS_COPY[this.version.environment] + }, }, watch: { '$route.path'() { diff --git a/apps/frontend/src/pages/dashboard/projects.vue b/apps/frontend/src/pages/dashboard/projects.vue index f2259ee00e..2ce08b2ea7 100644 --- a/apps/frontend/src/pages/dashboard/projects.vue +++ b/apps/frontend/src/pages/dashboard/projects.vue @@ -290,7 +290,7 @@ v-tooltip="'Please review environment metadata'" :to="`/${getProjectTypeForUrl(project.project_type, project.loaders)}/${ project.slug ? project.slug : project.id - }/settings/environment`" + }?showEnvironmentMigrationWarning=true`" > diff --git a/packages/ui/src/components/project/ProjectPageVersions.vue b/packages/ui/src/components/project/ProjectPageVersions.vue index 8301ed5cd4..504c2103b1 100644 --- a/packages/ui/src/components/project/ProjectPageVersions.vue +++ b/packages/ui/src/components/project/ProjectPageVersions.vue @@ -42,7 +42,12 @@
@@ -57,6 +62,12 @@ > Platforms
+
+ Environment +
@@ -144,6 +155,24 @@
+
+ + + {{ + ENVIRONMENTS_COPY[version.environment]?.title + ? formatMessage(ENVIRONMENTS_COPY[version.environment].title) + : '' + }} + +
= computed( ) const selectedChannels: Ref = computed(() => versionFilters.value?.selectedChannels ?? []) +const hasMultipleEnvironments = computed(() => { + const environments = new Set(props.versions.map((v) => v.environment).filter(Boolean)) + return environments.size > 1 +}) + const filteredVersions = computed(() => { return props.versions.filter( (version) => @@ -321,6 +357,14 @@ function updateQuery(newQueries: Record diff --git a/packages/ui/src/components/project/ProjectSidebarCompatibility.vue b/packages/ui/src/components/project/ProjectSidebarCompatibility.vue index 9365b15aa3..068837d725 100644 --- a/packages/ui/src/components/project/ProjectSidebarCompatibility.vue +++ b/packages/ui/src/components/project/ProjectSidebarCompatibility.vue @@ -21,7 +21,7 @@ :action="() => router.push(`/${project.project_type}s?g=categories:${platform}`)" :style="`--_color: var(--color-platform-${platform})`" > - + {{ formatCategory(platform) }}
@@ -69,6 +69,7 @@ +import { CheckIcon } from '@modrinth/assets' +import { + Admonition, + commonProjectSettingsMessages, + EnvironmentSelector, + injectModrinthClient, + injectNotificationManager, + injectProjectPageContext, + UnsavedChangesPopup, + useSavable, +} from '@modrinth/ui' +import { defineMessages, useVIntl } from '@vintl/vintl' +import { computed, ref } from 'vue' + +const { formatMessage } = useVIntl() + +const { currentMember, projectV2, projectV3, refreshProject } = injectProjectPageContext() +const { handleError } = injectNotificationManager() +const client = injectModrinthClient() + +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) > 0 && + projectV3.value.environment?.[0] !== 'unknown' && + supportsEnvironment.value, +) + +const hasPermission = computed(() => { + const EDIT_DETAILS = 1 << 2 + return (currentMember.value?.permissions & EDIT_DETAILS) === EDIT_DETAILS +}) + +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' + client.labrinth.projects_v3 + .edit(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!", + }, +}) + + diff --git a/packages/ui/src/components/project/settings/environment/ProjectSettingsEnvSelector.vue b/packages/ui/src/components/project/settings/environment/EnvironmentSelector.vue similarity index 98% rename from packages/ui/src/components/project/settings/environment/ProjectSettingsEnvSelector.vue rename to packages/ui/src/components/project/settings/environment/EnvironmentSelector.vue index 9de9cde182..fc661859f3 100644 --- a/packages/ui/src/components/project/settings/environment/ProjectSettingsEnvSelector.vue +++ b/packages/ui/src/components/project/settings/environment/EnvironmentSelector.vue @@ -33,7 +33,10 @@ const optionLabelFormat = defineMessage({ defaultMessage: '{title}: {description}', }) -const OUTER_OPTIONS = { +const OUTER_OPTIONS: Record< + string, + EnvironmentRadioOption & { suboptions: Record } +> = { client: { title: defineMessage({ id: 'project.settings.environment.client_only.title', @@ -125,10 +128,8 @@ const OUTER_OPTIONS = { }), suboptions: {}, }, -} as const satisfies Record< - string, - EnvironmentRadioOption & { suboptions: Record } -> +} as const + type OuterOptionKey = keyof typeof OUTER_OPTIONS type SubOptionKey = ValidKeys<(typeof OUTER_OPTIONS)[keyof typeof OUTER_OPTIONS]['suboptions']> @@ -248,7 +249,7 @@ const simulateSave = ref(false) :aria-label=" formatMessage(optionLabelFormat, { title: formatMessage(title), - description: formatMessage(description), + description: description ? formatMessage(description) : '', }) " @select=" diff --git a/packages/ui/src/components/project/settings/environment/ProjectEnvironmentModal.vue b/packages/ui/src/components/project/settings/environment/ProjectEnvironmentModal.vue new file mode 100644 index 0000000000..789541dc36 --- /dev/null +++ b/packages/ui/src/components/project/settings/environment/ProjectEnvironmentModal.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/ui/src/components/project/settings/environment/environments.ts b/packages/ui/src/components/project/settings/environment/environments.ts new file mode 100644 index 0000000000..6961a9f510 --- /dev/null +++ b/packages/ui/src/components/project/settings/environment/environments.ts @@ -0,0 +1,128 @@ +import type { Labrinth } from '@modrinth/api-client' +import { ClientIcon, MonitorSmartphoneIcon, ServerIcon, UserIcon } from '@modrinth/assets' +import { defineMessage, type MessageDescriptor } from '@vintl/vintl' +import type { Component } from 'vue' + +export const ENVIRONMENTS_COPY: Record< + Labrinth.Projects.v3.Environment, + { title: MessageDescriptor; description: MessageDescriptor; icon?: Component } +> = { + client_only: { + title: defineMessage({ + id: 'project.environment.client-only.title', + defaultMessage: 'Client-side only', + }), + description: defineMessage({ + id: 'project.environment.client-only.description', + defaultMessage: + 'All functionality is done client-side and is compatible with vanilla servers.', + }), + icon: ClientIcon, + }, + server_only: { + title: defineMessage({ + id: 'project.environment.server-only.title', + defaultMessage: 'Server-side only', + }), + description: defineMessage({ + id: 'project.environment.server-only.description', + defaultMessage: + 'All functionality is done server-side and is compatible with vanilla clients.', + }), + icon: ServerIcon, + }, + singleplayer_only: { + title: defineMessage({ + id: 'project.environment.singleplayer-only.title', + defaultMessage: 'Singleplayer only', + }), + description: defineMessage({ + id: 'project.environment.singleplayer-only.description', + defaultMessage: + 'Only functions in Singleplayer or when not connected to a Multiplayer server.', + }), + icon: UserIcon, + }, + dedicated_server_only: { + title: defineMessage({ + id: 'project.environment.dedicated-server-only.title', + defaultMessage: 'Server-side only', + }), + description: defineMessage({ + id: 'project.environment.dedicated-server-only.description', + defaultMessage: + 'All functionality is done server-side and is compatible with vanilla clients.', + }), + icon: ServerIcon, + }, + client_and_server: { + title: defineMessage({ + id: 'project.environment.client-and-server.title', + defaultMessage: 'Client and server', + }), + description: defineMessage({ + id: 'project.environment.client-and-server.description', + defaultMessage: + 'Has some functionality on both the client and server, even if only partially.', + }), + icon: MonitorSmartphoneIcon, + }, + client_only_server_optional: { + title: defineMessage({ + id: 'project.environment.client-only-server-optional.title', + defaultMessage: 'Client and server', + }), + description: defineMessage({ + id: 'project.environment.client-only-server-optional.description', + defaultMessage: + 'Has some functionality on both the client and server, even if only partially.', + }), + icon: MonitorSmartphoneIcon, + }, + server_only_client_optional: { + title: defineMessage({ + id: 'project.environment.server-only-client-optional.title', + defaultMessage: 'Client and server', + }), + description: defineMessage({ + id: 'project.environment.server-only-client-optional.description', + defaultMessage: + 'Has some functionality on both the client and server, even if only partially.', + }), + icon: MonitorSmartphoneIcon, + }, + client_or_server: { + title: defineMessage({ + id: 'project.environment.client-or-server.title', + defaultMessage: 'Client and server', + }), + description: defineMessage({ + id: 'project.environment.client-or-server.description', + defaultMessage: + 'Has some functionality on both the client and server, even if only partially.', + }), + icon: MonitorSmartphoneIcon, + }, + client_or_server_prefers_both: { + title: defineMessage({ + id: 'project.environment.client-or-server-prefers-both.title', + defaultMessage: 'Client and server', + }), + description: defineMessage({ + id: 'project.environment.client-or-server-prefers-both.description', + defaultMessage: + 'Has some functionality on both the client and server, even if only partially.', + }), + icon: MonitorSmartphoneIcon, + }, + unknown: { + title: defineMessage({ + id: 'project.environment.unknown.title', + defaultMessage: 'Unknown environment', + }), + description: defineMessage({ + id: 'project.environment.unknown.description', + defaultMessage: 'The environment for this version could not be determined.', + }), + }, +} diff --git a/packages/ui/src/components/project/settings/index.ts b/packages/ui/src/components/project/settings/index.ts index dc2fb0c137..d538f8adfe 100644 --- a/packages/ui/src/components/project/settings/index.ts +++ b/packages/ui/src/components/project/settings/index.ts @@ -1,2 +1,4 @@ -// Environment -export { default as ProjectSettingsEnvSelector } from './environment/ProjectSettingsEnvSelector.vue' +export { default as EnvironmentMigration } from './environment/EnvironmentMigration.vue' +export { ENVIRONMENTS_COPY } from './environment/environments' +export { default as EnvironmentSelector } from './environment/EnvironmentSelector.vue' +export { default as ProjectEnvironmentModal } from './environment/ProjectEnvironmentModal.vue' diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json index 5e75d51289..8c8d74e0b4 100644 --- a/packages/ui/src/locales/en-US/index.json +++ b/packages/ui/src/locales/en-US/index.json @@ -677,6 +677,66 @@ "project.about.links.wiki": { "defaultMessage": "Visit wiki" }, + "project.environment.client-and-server.description": { + "defaultMessage": "Has some functionality on both the client and server, even if only partially." + }, + "project.environment.client-and-server.title": { + "defaultMessage": "Client and server" + }, + "project.environment.client-only-server-optional.description": { + "defaultMessage": "Has some functionality on both the client and server, even if only partially." + }, + "project.environment.client-only-server-optional.title": { + "defaultMessage": "Client and server" + }, + "project.environment.client-only.description": { + "defaultMessage": "All functionality is done client-side and is compatible with vanilla servers." + }, + "project.environment.client-only.title": { + "defaultMessage": "Client-side only" + }, + "project.environment.client-or-server-prefers-both.description": { + "defaultMessage": "Has some functionality on both the client and server, even if only partially." + }, + "project.environment.client-or-server-prefers-both.title": { + "defaultMessage": "Client and server" + }, + "project.environment.client-or-server.description": { + "defaultMessage": "Has some functionality on both the client and server, even if only partially." + }, + "project.environment.client-or-server.title": { + "defaultMessage": "Client and server" + }, + "project.environment.dedicated-server-only.description": { + "defaultMessage": "All functionality is done server-side and is compatible with vanilla clients." + }, + "project.environment.dedicated-server-only.title": { + "defaultMessage": "Server-side only" + }, + "project.environment.server-only-client-optional.description": { + "defaultMessage": "Has some functionality on both the client and server, even if only partially." + }, + "project.environment.server-only-client-optional.title": { + "defaultMessage": "Client and server" + }, + "project.environment.server-only.description": { + "defaultMessage": "All functionality is done server-side and is compatible with vanilla clients." + }, + "project.environment.server-only.title": { + "defaultMessage": "Server-side only" + }, + "project.environment.singleplayer-only.description": { + "defaultMessage": "Only functions in Singleplayer or when not connected to a Multiplayer server." + }, + "project.environment.singleplayer-only.title": { + "defaultMessage": "Singleplayer only" + }, + "project.environment.unknown.description": { + "defaultMessage": "The environment for this version could not be determined." + }, + "project.environment.unknown.title": { + "defaultMessage": "Unknown environment" + }, "project.settings.analytics.title": { "defaultMessage": "Analytics" }, @@ -710,6 +770,30 @@ "project.settings.environment.client_only.title": { "defaultMessage": "Client-side only" }, + "project.settings.environment.notice.missing-env.description": { + "defaultMessage": "Your project is missing environment metadata, please select the appropriate option below." + }, + "project.settings.environment.notice.missing-env.title": { + "defaultMessage": "Please select an environment for your project" + }, + "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." + }, + "project.settings.environment.notice.multiple-environments.title": { + "defaultMessage": "Your project has multiple environments" + }, + "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!" + }, + "project.settings.environment.notice.review-options.title": { + "defaultMessage": "Please review the options below" + }, + "project.settings.environment.notice.wrong-project-type.description": { + "defaultMessage": "Only mod or modpack projects can have environment metadata." + }, + "project.settings.environment.notice.wrong-project-type.title": { + "defaultMessage": "This project type does not support environment metadata" + }, "project.settings.environment.server_only.dedicated_only.title": { "defaultMessage": "Dedicated server only" }, @@ -737,6 +821,12 @@ "project.settings.environment.title": { "defaultMessage": "Environment" }, + "project.settings.environment.verification.verify-button": { + "defaultMessage": "Verify" + }, + "project.settings.environment.verification.verify-text": { + "defaultMessage": "Verify that this project's environment is set correctly." + }, "project.settings.gallery.title": { "defaultMessage": "Gallery" },