From b9ba3cd3e8cc3150d472d1a2c89d7711cd90e85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= <7822554+AlexTMjugador@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:19:56 +0200 Subject: [PATCH 01/14] fix(labrinth): use a proper `CDN_URL` for local deployments (#3791) --- apps/frontend/.env.local | 1 - apps/labrinth/.env.local | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/frontend/.env.local b/apps/frontend/.env.local index f764f8513..29aad1d10 100644 --- a/apps/frontend/.env.local +++ b/apps/frontend/.env.local @@ -2,4 +2,3 @@ BASE_URL=http://127.0.0.1:8000/v2/ BROWSER_BASE_URL=http://127.0.0.1:8000/v2/ PYRO_BASE_URL=https://staging-archon.modrinth.com PROD_OVERRIDE=true - diff --git a/apps/labrinth/.env.local b/apps/labrinth/.env.local index 8675bcd69..880ac9385 100644 --- a/apps/labrinth/.env.local +++ b/apps/labrinth/.env.local @@ -3,7 +3,8 @@ RUST_LOG=info,sqlx::query=warn SENTRY_DSN=none SITE_URL=http://localhost:3000 -CDN_URL=https://staging-cdn.modrinth.com +# This CDN URL matches the local storage backend set below, which uses MOCK_FILE_PATH +CDN_URL=file:///tmp/modrinth LABRINTH_ADMIN_KEY=feedbeef RATE_LIMIT_IGNORE_KEY=feedbeef @@ -25,7 +26,6 @@ PUBLIC_DISCORD_WEBHOOK= CLOUDFLARE_INTEGRATION=false STORAGE_BACKEND=local - MOCK_FILE_PATH=/tmp/modrinth BACKBLAZE_KEY_ID=none From c32405720df5ecb63dc932e0687707030b3294a4 Mon Sep 17 00:00:00 2001 From: Prospector Date: Sun, 15 Jun 2025 14:39:11 -0700 Subject: [PATCH 02/14] Fix changelog --- packages/utils/changelog.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/utils/changelog.ts b/packages/utils/changelog.ts index 434b94d65..28fba8329 100644 --- a/packages/utils/changelog.ts +++ b/packages/utils/changelog.ts @@ -11,7 +11,7 @@ export type VersionEntry = { const VERSIONS: VersionEntry[] = [ { - date: `2025-06-14T10:50:00-07:00`, + date: `2025-06-15T14:30:00-07:00`, product: 'servers', body: `### Improvements - Fixed various issues with the panel loading improperly in certain cases. @@ -19,13 +19,13 @@ const VERSIONS: VersionEntry[] = [ - Server panel performance should be a little faster now.`, }, { - date: `2025-06-14T10:50:00-07:00`, + date: `2025-06-15T14:30:00-07:00`, product: 'web', body: `### Improvements -- Creator analytics charts will now show up to 15 projects in a tooltip instead of 5. (Contributed by [Erb3](https://github.com/modrinth/code/pull/2898)) -- Made certain scrollable containers not have a fixed height, and allow them to be smaller if they have fewer items. +- Creator analytics charts will now show up to 15 projects in a tooltip instead of 5. +- Made certain scrollable containers not have a fixed height, and allow them to be smaller if they have fewer items. (Contributed by [Erb3](https://github.com/modrinth/code/pull/2898)) - Made organizations sort consistently alphabetically. (Contributed by [WorldWidePixel](https://github.com/modrinth/code/pull/3755)) -- Clarified the 'File too large' error message when uploading an image larger than 1MiB in the text editor.`, +- Clarified the 'File too large' error message when uploading an image larger than 1MiB in the text editor. (Contributed by [IThundxr](https://github.com/modrinth/code/pull/3774))`, }, { date: `2025-06-03T14:35:00-07:00`, From 2b4319ea55830da0610e7ea70c914a15dd311960 Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Sun, 15 Jun 2025 16:17:38 -0700 Subject: [PATCH 03/14] Servers hotfixes (#3793) * servers: Fix installing modpacks from search * remove console.log * Fix subdomain setting --- .../src/pages/search/[searchProjectType].vue | 1 - .../servers/manage/[id]/options/index.vue | 26 +++++++------------ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/apps/frontend/src/pages/search/[searchProjectType].vue b/apps/frontend/src/pages/search/[searchProjectType].vue index 42f01ad67..c12b92fad 100644 --- a/apps/frontend/src/pages/search/[searchProjectType].vue +++ b/apps/frontend/src/pages/search/[searchProjectType].vue @@ -520,7 +520,6 @@ async function serverInstall(project) { if (projectType.value.id === "modpack") { await server.value.general.reinstall( - server.value.serverId, false, project.project_id, version.id, diff --git a/apps/frontend/src/pages/servers/manage/[id]/options/index.vue b/apps/frontend/src/pages/servers/manage/[id]/options/index.vue index 7ff812b7b..c1c1911c4 100644 --- a/apps/frontend/src/pages/servers/manage/[id]/options/index.vue +++ b/apps/frontend/src/pages/servers/manage/[id]/options/index.vue @@ -155,26 +155,18 @@ const saveGeneral = async () => { if (serverSubdomain.value !== data.value?.net?.domain) { try { // type shit backend makes me do - const response = await props.server.network?.checkSubdomainAvailability( + const available = await props.server.network?.checkSubdomainAvailability( serverSubdomain.value, ); - if (response === undefined) { - throw new Error("Failed to check subdomain availability"); - } - if (typeof response === "object" && response !== null && "available" in response) { - const typedResponse = response as { available: boolean }; - if (!typedResponse.available) { - addNotification({ - group: "serverOptions", - type: "error", - title: "Subdomain not available", - text: "The subdomain you entered is already in use.", - }); - return; - } - } else { - throw new Error("Invalid response format from availability check"); + if (!available) { + addNotification({ + group: "serverOptions", + type: "error", + title: "Subdomain not available", + text: "The subdomain you entered is already in use.", + }); + return; } await props.server.network?.changeSubdomain(serverSubdomain.value); From a08562bfe28905f5ffc566e76cf761532a8364b7 Mon Sep 17 00:00:00 2001 From: Prospector Date: Sun, 15 Jun 2025 16:19:06 -0700 Subject: [PATCH 04/14] Update changelog --- packages/utils/changelog.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/utils/changelog.ts b/packages/utils/changelog.ts index 28fba8329..143428111 100644 --- a/packages/utils/changelog.ts +++ b/packages/utils/changelog.ts @@ -10,6 +10,13 @@ export type VersionEntry = { } const VERSIONS: VersionEntry[] = [ + { + date: `2025-06-15T16:25:00-07:00`, + product: 'servers', + body: `### Improvements +- Fixed installing modpacks from search. +- Fixed setting subdomains.`, + }, { date: `2025-06-15T14:30:00-07:00`, product: 'servers', From 5bdff3929b950584558e2db6dc0a52c7a41767aa Mon Sep 17 00:00:00 2001 From: Emma Alexia Date: Mon, 16 Jun 2025 01:41:07 -0400 Subject: [PATCH 05/14] Allow failed subscriptions to be cancelled (#3795) When a payment for a subscription fails, we continue to try to re-attempt retrieving payment for 30 days. Sometimes making it fail is an intentional choice on the user's part (e.g. Privacy.com card) or other times the user just doesn't want their subscription anymore after it fails. This PR allows users with a failed payment to simply cancel instead of waiting for the 30-day timer to set in. --- apps/frontend/src/pages/settings/billing/index.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/frontend/src/pages/settings/billing/index.vue b/apps/frontend/src/pages/settings/billing/index.vue index 70ec4ee65..96ace83c5 100644 --- a/apps/frontend/src/pages/settings/billing/index.vue +++ b/apps/frontend/src/pages/settings/billing/index.vue @@ -330,8 +330,7 @@ - - +
-
-
-
-
- -
-

Server not found

-
-

- You don't have permission to view this server or it no longer exists. If you believe this - is an error, please contact Modrinth Support. -

-
- - - - - -
+
-
-
-
-
- -
-

Server Node Unavailable

+ + +
-
-
-
-
- -
-

Connection lost

+ + +
{ const stopUptimeUpdates = () => { if (uptimeIntervalId) { clearInterval(uptimeIntervalId); - intervalId = null; + pollingIntervalId = null; } }; @@ -1055,7 +992,7 @@ const notifyError = (title: string, text: string) => { }); }; -let intervalId: ReturnType | null = null; +let pollingIntervalId: ReturnType | null = null; const countdown = ref(15); const formattedTime = computed(() => { @@ -1099,23 +1036,142 @@ const backupInProgress = computed(() => { }); const stopPolling = () => { - if (intervalId) { - clearInterval(intervalId); - intervalId = null; + if (pollingIntervalId) { + clearTimeout(pollingIntervalId); + pollingIntervalId = null; } }; const startPolling = () => { - countdown.value = 15; - intervalId = setInterval(() => { - if (countdown.value <= 0) { - reloadNuxtApp(); - } else { - countdown.value--; + stopPolling(); + + let retryCount = 0; + const maxRetries = 10; + + const poll = async () => { + try { + await server.refresh(["general", "ws"]); + + if (!server.moduleErrors?.general?.error) { + stopPolling(); + connectWebSocket(); + return; + } + + retryCount++; + if (retryCount >= maxRetries) { + console.error("Max retries reached, stopping polling"); + stopPolling(); + return; + } + + // Exponential backoff: 3s, 6s, 12s, 24s, etc. + const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000); + + pollingIntervalId = setTimeout(poll, delay); + } catch (error) { + console.error("Polling failed:", error); + retryCount++; + + if (retryCount < maxRetries) { + const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000); + pollingIntervalId = setTimeout(poll, delay); + } } - }, 1000); + }; + + poll(); }; +const nodeUnavailableDetails = computed(() => [ + { + label: "Server ID", + value: server.serverId, + type: "inline" as const, + }, + { + label: "Node", + value: server.general?.datacenter ?? "Unknown! Please contact support!", + type: "inline" as const, + }, +]); + +const suspendedDescription = computed(() => { + if (serverData.value?.suspension_reason === "cancelled") { + return "Your subscription has been cancelled.\nContact Modrinth Support if you believe this is an error."; + } + if (serverData.value?.suspension_reason) { + return `Your server has been suspended: ${serverData.value.suspension_reason}\nContact Modrinth Support if you believe this is an error.`; + } + return "Your server has been suspended.\nContact Modrinth Support if you believe this is an error."; +}); + +const generalErrorDetails = computed(() => [ + { + label: "Server ID", + value: server.serverId, + type: "inline" as const, + }, + { + label: "Timestamp", + value: String(server.moduleErrors?.general?.timestamp), + type: "inline" as const, + }, + { + label: "Error Name", + value: server.moduleErrors?.general?.error.name, + type: "inline" as const, + }, + { + label: "Error Message", + value: server.moduleErrors?.general?.error.message, + type: "block" as const, + }, + ...(server.moduleErrors?.general?.error.originalError + ? [ + { + label: "Original Error", + value: String(server.moduleErrors.general.error.originalError), + type: "hidden" as const, + }, + ] + : []), + ...(server.moduleErrors?.general?.error.stack + ? [ + { + label: "Stack Trace", + value: server.moduleErrors.general.error.stack, + type: "hidden" as const, + }, + ] + : []), +]); + +const suspendedAction = computed(() => ({ + label: "Go to billing settings", + onClick: () => router.push("/settings/billing"), + color: "brand" as const, +})); + +const generalErrorAction = computed(() => ({ + label: "Go back to all servers", + onClick: () => router.push("/servers/manage"), + color: "brand" as const, +})); + +const nodeUnavailableAction = computed(() => ({ + label: "Join Modrinth Discord", + onClick: () => navigateTo("https://discord.modrinth.com", { external: true }), + color: "standard" as const, +})); + +const connectionLostAction = computed(() => ({ + label: "Reload", + onClick: () => reloadNuxtApp(), + color: "brand" as const, + disabled: formattedTime.value !== "00", +})); + const copyServerDebugInfo = () => { const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`; navigator.clipboard.writeText(debugInfo); diff --git a/packages/ui/src/components/base/ErrorInformationCard.vue b/packages/ui/src/components/base/ErrorInformationCard.vue new file mode 100644 index 000000000..9edfc0263 --- /dev/null +++ b/packages/ui/src/components/base/ErrorInformationCard.vue @@ -0,0 +1,120 @@ + + + diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index a1c39233f..3ca10fb98 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -16,6 +16,7 @@ export { default as DoubleIcon } from './base/DoubleIcon.vue' export { default as DropArea } from './base/DropArea.vue' export { default as DropdownSelect } from './base/DropdownSelect.vue' export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue' +export { default as ErrorInformationCard } from './base/ErrorInformationCard.vue' export { default as FileInput } from './base/FileInput.vue' export { default as FilterBar } from './base/FilterBar.vue' export type { FilterBarOption } from './base/FilterBar.vue' diff --git a/packages/utils/servers/errors/modrinth-server-error.ts b/packages/utils/servers/errors/modrinth-server-error.ts index ece8825c8..c0a548a77 100644 --- a/packages/utils/servers/errors/modrinth-server-error.ts +++ b/packages/utils/servers/errors/modrinth-server-error.ts @@ -54,6 +54,6 @@ export class ModrinthServerError extends Error { } super(errorMessage) - this.name = 'PyroServersFetchError' + this.name = 'ModrinthServersFetchError' } } diff --git a/packages/utils/servers/errors/modrinth-servers-fetch-error.ts b/packages/utils/servers/errors/modrinth-servers-fetch-error.ts index ce01e737f..a2df7baa3 100644 --- a/packages/utils/servers/errors/modrinth-servers-fetch-error.ts +++ b/packages/utils/servers/errors/modrinth-servers-fetch-error.ts @@ -5,6 +5,6 @@ export class ModrinthServersFetchError extends Error { public originalError?: Error, ) { super(message) - this.name = 'PyroFetchError' + this.name = 'ModrinthFetchError' } } From 58495e62768f3d38f9e28ed149c36cf81384ad06 Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 16 Jun 2025 10:59:01 -0700 Subject: [PATCH 10/14] Update changelog --- packages/utils/changelog.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/utils/changelog.ts b/packages/utils/changelog.ts index 143428111..7ab62b90f 100644 --- a/packages/utils/changelog.ts +++ b/packages/utils/changelog.ts @@ -10,6 +10,19 @@ export type VersionEntry = { } const VERSIONS: VersionEntry[] = [ + { + date: `2025-06-16T11:00:00-07:00`, + product: 'web', + body: `### Improvements +- Rolled out hotfixes with the previous days' updates.`, + }, + { + date: `2025-06-16T11:00:00-07:00`, + product: 'servers', + body: `### Improvements +- Improved error handling. +- Rolled out hotfixes with the previous days' updates.'`, + }, { date: `2025-06-15T16:25:00-07:00`, product: 'servers', From 65126b3a231ea1ce8be70e028ecd9db6ca1b7422 Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 16 Jun 2025 11:00:15 -0700 Subject: [PATCH 11/14] Update changelog again --- packages/utils/changelog.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/utils/changelog.ts b/packages/utils/changelog.ts index 7ab62b90f..b0bde3b77 100644 --- a/packages/utils/changelog.ts +++ b/packages/utils/changelog.ts @@ -14,7 +14,8 @@ const VERSIONS: VersionEntry[] = [ date: `2025-06-16T11:00:00-07:00`, product: 'web', body: `### Improvements -- Rolled out hotfixes with the previous days' updates.`, +- Rolled out hotfixes with the previous days' updates. +- Failed subscriptions can now be cancelled.`, }, { date: `2025-06-16T11:00:00-07:00`, From ef04dcc37b7c9ab03846176aa5f7538aaecf6888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= <7822554+AlexTMjugador@users.noreply.github.com> Date: Tue, 17 Jun 2025 00:44:57 +0200 Subject: [PATCH 12/14] feat(labrinth): rework v3 side types to a single `environment` field (#3701) * feat(labrinth): rework v3 side types to a single `environment` field This field is meant to be able to represent the existing v2 side type information and beyond, in a way that may also be slightly easier to comprehend. * chore(labrinth/migrations): use proper val for `HAVING` clause * feat(labrinth): add `side_types_migration_review_status` field to projects --- ...008b0fb78c79976fd858e0599e1ccb7f08b82.json | 15 ++ ...a66c4175ebd1c55e437f187f61efca681c62.json} | 12 +- ...8e62ac199fe5e68275bf1bc7c71ace630702.json} | 7 +- ...23174544_project-versions-environments.sql | 122 +++++++++++++++++ .../src/database/models/project_item.rs | 18 ++- apps/labrinth/src/models/v2/projects.rs | 11 +- apps/labrinth/src/models/v2/search.rs | 22 +-- apps/labrinth/src/models/v3/projects.rs | 35 ++++- apps/labrinth/src/queue/moderation.rs | 2 +- .../src/routes/v2/project_creation.rs | 10 +- apps/labrinth/src/routes/v2/projects.rs | 7 +- .../src/routes/v2/version_creation.rs | 51 ++----- apps/labrinth/src/routes/v2_reroute.rs | 129 +++++++----------- .../src/routes/v3/project_creation.rs | 6 +- apps/labrinth/src/routes/v3/projects.rs | 26 ++++ .../src/search/indexing/local_import.rs | 2 +- apps/labrinth/src/search/indexing/mod.rs | 10 +- .../tests/common/api_v3/request_data.rs | 5 +- apps/labrinth/tests/common/search.rs | 18 ++- apps/labrinth/tests/files/dummy_data.sql | 6 +- apps/labrinth/tests/loader_fields.rs | 42 +++--- apps/labrinth/tests/search.rs | 7 +- 22 files changed, 358 insertions(+), 205 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-374c234b92b838b0bd65de100a9008b0fb78c79976fd858e0599e1ccb7f08b82.json rename apps/labrinth/.sqlx/{query-5f75c0c48083de27f853ee877aac070567fd2ed2be4a9a038821b790dd7cb763.json => query-7a6d6a91e6bd27f7be34b8cc7955a66c4175ebd1c55e437f187f61efca681c62.json} (79%) rename apps/labrinth/.sqlx/{query-bcbcac3c0b2b2b0327577d3095fa744ab42f7f1dcd2b7f3c3dace12b899b3f38.json => query-ee74bbff42dd29ab5a23d5811ea18e62ac199fe5e68275bf1bc7c71ace630702.json} (60%) create mode 100644 apps/labrinth/migrations/20250523174544_project-versions-environments.sql diff --git a/apps/labrinth/.sqlx/query-374c234b92b838b0bd65de100a9008b0fb78c79976fd858e0599e1ccb7f08b82.json b/apps/labrinth/.sqlx/query-374c234b92b838b0bd65de100a9008b0fb78c79976fd858e0599e1ccb7f08b82.json new file mode 100644 index 000000000..4e906d600 --- /dev/null +++ b/apps/labrinth/.sqlx/query-374c234b92b838b0bd65de100a9008b0fb78c79976fd858e0599e1ccb7f08b82.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE mods\n SET side_types_migration_review_status = $1\n WHERE id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "374c234b92b838b0bd65de100a9008b0fb78c79976fd858e0599e1ccb7f08b82" +} diff --git a/apps/labrinth/.sqlx/query-5f75c0c48083de27f853ee877aac070567fd2ed2be4a9a038821b790dd7cb763.json b/apps/labrinth/.sqlx/query-7a6d6a91e6bd27f7be34b8cc7955a66c4175ebd1c55e437f187f61efca681c62.json similarity index 79% rename from apps/labrinth/.sqlx/query-5f75c0c48083de27f853ee877aac070567fd2ed2be4a9a038821b790dd7cb763.json rename to apps/labrinth/.sqlx/query-7a6d6a91e6bd27f7be34b8cc7955a66c4175ebd1c55e437f187f61efca681c62.json index 53b085320..f7cb84084 100644 --- a/apps/labrinth/.sqlx/query-5f75c0c48083de27f853ee877aac070567fd2ed2be4a9a038821b790dd7cb763.json +++ b/apps/labrinth/.sqlx/query-7a6d6a91e6bd27f7be34b8cc7955a66c4175ebd1c55e437f187f61efca681c62.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,\n m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id;\n ", + "query": "\n SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,\n m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n m.side_types_migration_review_status side_types_migration_review_status,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id;\n ", "describe": { "columns": [ { @@ -125,11 +125,16 @@ }, { "ordinal": 24, + "name": "side_types_migration_review_status", + "type_info": "Varchar" + }, + { + "ordinal": 25, "name": "categories", "type_info": "VarcharArray" }, { - "ordinal": 25, + "ordinal": 26, "name": "additional_categories", "type_info": "VarcharArray" } @@ -165,9 +170,10 @@ true, false, false, + false, null, null ] }, - "hash": "5f75c0c48083de27f853ee877aac070567fd2ed2be4a9a038821b790dd7cb763" + "hash": "7a6d6a91e6bd27f7be34b8cc7955a66c4175ebd1c55e437f187f61efca681c62" } diff --git a/apps/labrinth/.sqlx/query-bcbcac3c0b2b2b0327577d3095fa744ab42f7f1dcd2b7f3c3dace12b899b3f38.json b/apps/labrinth/.sqlx/query-ee74bbff42dd29ab5a23d5811ea18e62ac199fe5e68275bf1bc7c71ace630702.json similarity index 60% rename from apps/labrinth/.sqlx/query-bcbcac3c0b2b2b0327577d3095fa744ab42f7f1dcd2b7f3c3dace12b899b3f38.json rename to apps/labrinth/.sqlx/query-ee74bbff42dd29ab5a23d5811ea18e62ac199fe5e68275bf1bc7c71ace630702.json index 1de75299c..af9bf42e5 100644 --- a/apps/labrinth/.sqlx/query-bcbcac3c0b2b2b0327577d3095fa744ab42f7f1dcd2b7f3c3dace12b899b3f38.json +++ b/apps/labrinth/.sqlx/query-ee74bbff42dd29ab5a23d5811ea18e62ac199fe5e68275bf1bc7c71ace630702.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO mods (\n id, team_id, name, summary, description,\n published, downloads, icon_url, raw_icon_url, status, requested_status,\n license_url, license,\n slug, color, monetization_status, organization_id\n )\n VALUES (\n $1, $2, $3, $4, $5, $6,\n $7, $8, $9, $10, $11,\n $12, $13,\n LOWER($14), $15, $16, $17\n )\n ", + "query": "\n INSERT INTO mods (\n id, team_id, name, summary, description,\n published, downloads, icon_url, raw_icon_url, status, requested_status,\n license_url, license,\n slug, color, monetization_status, organization_id,\n side_types_migration_review_status\n )\n VALUES (\n $1, $2, $3, $4, $5, $6,\n $7, $8, $9, $10, $11,\n $12, $13,\n LOWER($14), $15, $16, $17,\n $18\n )\n ", "describe": { "columns": [], "parameters": { @@ -21,10 +21,11 @@ "Text", "Int4", "Varchar", - "Int8" + "Int8", + "Varchar" ] }, "nullable": [] }, - "hash": "bcbcac3c0b2b2b0327577d3095fa744ab42f7f1dcd2b7f3c3dace12b899b3f38" + "hash": "ee74bbff42dd29ab5a23d5811ea18e62ac199fe5e68275bf1bc7c71ace630702" } diff --git a/apps/labrinth/migrations/20250523174544_project-versions-environments.sql b/apps/labrinth/migrations/20250523174544_project-versions-environments.sql new file mode 100644 index 000000000..80b862a7d --- /dev/null +++ b/apps/labrinth/migrations/20250523174544_project-versions-environments.sql @@ -0,0 +1,122 @@ +DO LANGUAGE plpgsql $$ +DECLARE + VAR_env_field_id INT; + VAR_env_field_enum_id INT := 4; -- Known available ID for a new enum type +BEGIN + +-- Define a new loader field for environment +INSERT INTO loader_field_enums (id, enum_name, ordering, hidable) + VALUES (VAR_env_field_enum_id, 'environment', NULL, TRUE); + +INSERT INTO loader_field_enum_values (enum_id, value, ordering, created, metadata) + VALUES + -- Must be installed on both client and (integrated) server + (VAR_env_field_enum_id, 'client_and_server', NULL, NOW(), NULL), + -- Must be installed only on the client + (VAR_env_field_enum_id, 'client_only', NULL, NOW(), NULL), + -- Must be installed on the client, may be installed on a (integrated) server. To be displayed as a + -- client mod + (VAR_env_field_enum_id, 'client_only_server_optional', NULL, NOW(), NULL), + -- Must be installed only on the integrated singleplayer server. To be displayed as a server mod for + -- singleplayer exclusively + (VAR_env_field_enum_id, 'singleplayer_only', NULL, NOW(), NULL), + -- Must be installed only on a (integrated) server + (VAR_env_field_enum_id, 'server_only', NULL, NOW(), NULL), + -- Must be installed on the server, may be installed on the client. To be displayed as a + -- singleplayer-compatible server mod + (VAR_env_field_enum_id, 'server_only_client_optional', NULL, NOW(), NULL), + -- Must be installed only on a dedicated multiplayer server (not the integrated singleplayer server). + -- To be displayed as an server mod for multiplayer exclusively + (VAR_env_field_enum_id, 'dedicated_server_only', NULL, NOW(), NULL), + -- Can be installed on both client and server, with no strong preference for either. To be displayed + -- as both a client and server mod + (VAR_env_field_enum_id, 'client_or_server', NULL, NOW(), NULL), + -- Can be installed on both client and server, with a preference for being installed on both. To be + -- displayed as a client and server mod + (VAR_env_field_enum_id, 'client_or_server_prefers_both', NULL, NOW(), NULL), + (VAR_env_field_enum_id, 'unknown', NULL, NOW(), NULL); + +INSERT INTO loader_fields (field, field_type, enum_type, optional) + VALUES ('environment', 'enum', VAR_env_field_enum_id, FALSE) + RETURNING id INTO VAR_env_field_id; + +-- Update version_fields to have the new environment field, initializing it from the +-- values of the previous fields +INSERT INTO version_fields (version_id, field_id, enum_value) + SELECT vf.version_id, VAR_env_field_id, ( + SELECT id + FROM loader_field_enum_values + WHERE enum_id = VAR_env_field_enum_id + AND value = ( + CASE jsonb_object_agg(lf.field, vf.int_value) + WHEN '{ "server_only": 0, "singleplayer": 0, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_only' + WHEN '{ "server_only": 0, "singleplayer": 0, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'client_and_server' + WHEN '{ "server_only": 0, "singleplayer": 0, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_only_server_optional' + WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 0, "client_only": 0 }'::jsonb THEN 'singleplayer_only' + WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_only' + WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'client_and_server' + WHEN '{ "server_only": 0, "singleplayer": 1, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_only_server_optional' + WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 0, "client_only": 0 }'::jsonb THEN 'server_only' + WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_or_server' + WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'server_only_client_optional' + WHEN '{ "server_only": 1, "singleplayer": 0, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_or_server_prefers_both' + WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 0, "client_only": 0 }'::jsonb THEN 'server_only' + WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 0, "client_only": 1 }'::jsonb THEN 'client_or_server' + WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 1, "client_only": 0 }'::jsonb THEN 'server_only_client_optional' + WHEN '{ "server_only": 1, "singleplayer": 1, "client_and_server": 1, "client_only": 1 }'::jsonb THEN 'client_or_server_prefers_both' + ELSE 'unknown' + END + ) + ) + FROM version_fields vf + JOIN loader_fields lf ON vf.field_id = lf.id + WHERE lf.field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only') + GROUP BY vf.version_id + HAVING COUNT(DISTINCT lf.field) = 4; + +-- Clean up old fields from the project versions +DELETE FROM version_fields + WHERE field_id IN ( + SELECT id + FROM loader_fields + WHERE field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only') + ); + +-- Switch loader fields definitions on the available loaders to use the new environment field +ALTER TABLE loader_fields_loaders DROP CONSTRAINT unique_loader_field; +ALTER TABLE loader_fields_loaders DROP CONSTRAINT loader_fields_loaders_pkey; + +UPDATE loader_fields_loaders + SET loader_field_id = VAR_env_field_id + WHERE loader_field_id IN ( + SELECT id + FROM loader_fields + WHERE field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only') + ); + +-- Remove duplicate (loader_id, loader_field_id) pairs that may have been created due to several +-- old fields being converted to a single new field +DELETE FROM loader_fields_loaders + WHERE ctid NOT IN ( + SELECT MIN(ctid) + FROM loader_fields_loaders + GROUP BY loader_id, loader_field_id + ); + +-- Having both a PK and UNIQUE constraint for the same columns is redundant, so only restore the PK +ALTER TABLE loader_fields_loaders ADD PRIMARY KEY (loader_id, loader_field_id); + +-- Finally, remove the old loader fields +DELETE FROM loader_fields + WHERE field IN ('server_only', 'singleplayer', 'client_and_server', 'client_only'); + +-- Add a field to the projects table to track whether the new environment field value has been +-- reviewed to be appropriate after automated migration +ALTER TABLE mods + ADD COLUMN side_types_migration_review_status VARCHAR(64) NOT NULL DEFAULT 'reviewed' + CHECK (side_types_migration_review_status IN ('reviewed', 'pending')); + +UPDATE mods SET side_types_migration_review_status = 'pending'; + +END; +$$ diff --git a/apps/labrinth/src/database/models/project_item.rs b/apps/labrinth/src/database/models/project_item.rs index 65fe28a6a..362907bda 100644 --- a/apps/labrinth/src/database/models/project_item.rs +++ b/apps/labrinth/src/database/models/project_item.rs @@ -6,7 +6,9 @@ use super::{DBUser, ids::*}; use crate::database::models; use crate::database::models::DatabaseError; use crate::database::redis::RedisPool; -use crate::models::projects::{MonetizationStatus, ProjectStatus}; +use crate::models::projects::{ + MonetizationStatus, ProjectStatus, SideTypesMigrationReviewStatus, +}; use ariadne::ids::base62_impl::parse_base62; use chrono::{DateTime, Utc}; use dashmap::{DashMap, DashSet}; @@ -210,6 +212,8 @@ impl ProjectBuilder { webhook_sent: false, color: self.color, monetization_status: self.monetization_status, + side_types_migration_review_status: + SideTypesMigrationReviewStatus::Reviewed, loaders: vec![], }; project_struct.insert(&mut *transaction).await?; @@ -288,6 +292,7 @@ pub struct DBProject { pub webhook_sent: bool, pub color: Option, pub monetization_status: MonetizationStatus, + pub side_types_migration_review_status: SideTypesMigrationReviewStatus, pub loaders: Vec, } @@ -302,13 +307,15 @@ impl DBProject { id, team_id, name, summary, description, published, downloads, icon_url, raw_icon_url, status, requested_status, license_url, license, - slug, color, monetization_status, organization_id + slug, color, monetization_status, organization_id, + side_types_migration_review_status ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, - LOWER($14), $15, $16, $17 + LOWER($14), $15, $16, $17, + $18 ) ", self.id as DBProjectId, @@ -328,6 +335,7 @@ impl DBProject { self.color.map(|x| x as i32), self.monetization_status.as_str(), self.organization_id.map(|x| x.0 as i64), + self.side_types_migration_review_status.as_str() ) .execute(&mut **transaction) .await?; @@ -770,6 +778,7 @@ impl DBProject { m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body, m.webhook_sent, m.color, t.id thread_id, m.monetization_status monetization_status, + m.side_types_migration_review_status side_types_migration_review_status, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories FROM mods m @@ -835,6 +844,9 @@ impl DBProject { monetization_status: MonetizationStatus::from_string( &m.monetization_status, ), + side_types_migration_review_status: SideTypesMigrationReviewStatus::from_string( + &m.side_types_migration_review_status, + ), loaders, }, categories: m.categories.unwrap_or_default(), diff --git a/apps/labrinth/src/models/v2/projects.rs b/apps/labrinth/src/models/v2/projects.rs index 35a340f0b..0af1e53fe 100644 --- a/apps/labrinth/src/models/v2/projects.rs +++ b/apps/labrinth/src/models/v2/projects.rs @@ -127,7 +127,7 @@ impl LegacyProject { .collect(); if let Some(versions_item) = versions_item { - // Extract side types from remaining fields (singleplayer, client_only, etc) + // Extract side types from remaining fields let fields = versions_item .version_fields .iter() @@ -135,10 +135,11 @@ impl LegacyProject { (f.field_name.clone(), f.value.clone().serialize_internal()) }) .collect::>(); - (client_side, server_side) = v2_reroute::convert_side_types_v2( - &fields, - Some(&*og_project_type), - ); + (client_side, server_side) = + v2_reroute::convert_v3_side_types_to_v2_side_types( + &fields, + Some(&*og_project_type), + ); // - if loader is mrpack, this is a modpack // the loaders are whatever the corresponding loader fields are diff --git a/apps/labrinth/src/models/v2/search.rs b/apps/labrinth/src/models/v2/search.rs index dfc9356b7..1aabaca14 100644 --- a/apps/labrinth/src/models/v2/search.rs +++ b/apps/labrinth/src/models/v2/search.rs @@ -102,28 +102,20 @@ impl LegacyResultSearchProject { let project_loader_fields = result_search_project.project_loader_fields.clone(); - let get_one_bool_loader_field = |key: &str| { + let get_one_string_loader_field = |key: &str| { project_loader_fields .get(key) - .cloned() - .unwrap_or_default() + .map_or(&[][..], |values| values.as_slice()) .first() - .and_then(|s| s.as_bool()) + .and_then(|s| s.as_str()) }; - let singleplayer = get_one_bool_loader_field("singleplayer"); - let client_only = - get_one_bool_loader_field("client_only").unwrap_or(false); - let server_only = - get_one_bool_loader_field("server_only").unwrap_or(false); - let client_and_server = get_one_bool_loader_field("client_and_server"); + let environment = + get_one_string_loader_field("environment").unwrap_or("unknown"); let (client_side, server_side) = - v2_reroute::convert_side_types_v2_bools( - singleplayer, - client_only, - server_only, - client_and_server, + v2_reroute::convert_v3_environment_to_v2_side_types( + environment, Some(&*og_project_type), ); let client_side = client_side.to_string(); diff --git a/apps/labrinth/src/models/v3/projects.rs b/apps/labrinth/src/models/v3/projects.rs index fd43f9fb8..56d241d3b 100644 --- a/apps/labrinth/src/models/v3/projects.rs +++ b/apps/labrinth/src/models/v3/projects.rs @@ -92,6 +92,9 @@ pub struct Project { /// The monetization status of this project pub monetization_status: MonetizationStatus, + /// The status of the manual review of the migration of side types of this project + pub side_types_migration_review_status: SideTypesMigrationReviewStatus, + /// Aggregated loader-fields across its myriad of versions #[serde(flatten)] pub fields: HashMap>, @@ -206,6 +209,8 @@ impl From for Project { color: m.color, thread_id: data.thread_id.into(), monetization_status: m.monetization_status, + side_types_migration_review_status: m + .side_types_migration_review_status, fields, } } @@ -588,6 +593,35 @@ impl MonetizationStatus { } } +/// Represents the status of the manual review of the migration of side types of this +/// project to the new environment field. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum SideTypesMigrationReviewStatus { + /// The project has been reviewed to use the new environment side types appropriately. + Reviewed, + /// The project has been automatically migrated to the new environment side types, but + /// the appropriateness of such migration has not been reviewed. + Pending, +} + +impl SideTypesMigrationReviewStatus { + pub fn as_str(&self) -> &'static str { + match self { + SideTypesMigrationReviewStatus::Reviewed => "reviewed", + SideTypesMigrationReviewStatus::Pending => "pending", + } + } + + pub fn from_string(string: &str) -> SideTypesMigrationReviewStatus { + match string { + "reviewed" => SideTypesMigrationReviewStatus::Reviewed, + "pending" => SideTypesMigrationReviewStatus::Pending, + _ => SideTypesMigrationReviewStatus::Reviewed, + } + } +} + /// A specific version of a project #[derive(Serialize, Deserialize, Clone)] pub struct Version { @@ -846,7 +880,6 @@ impl std::fmt::Display for VersionType { } impl VersionType { - // These are constant, so this can remove unneccessary allocations (`to_string`) pub fn as_str(&self) -> &'static str { match self { VersionType::Release => "release", diff --git a/apps/labrinth/src/queue/moderation.rs b/apps/labrinth/src/queue/moderation.rs index 956ff4dd3..b74315839 100644 --- a/apps/labrinth/src/queue/moderation.rs +++ b/apps/labrinth/src/queue/moderation.rs @@ -244,7 +244,7 @@ impl AutomatedModerationQueue { version_specific: HashMap::new(), }; - if project.project_types.iter().any(|x| ["mod", "modpack"].contains(&&**x)) && !project.aggregate_version_fields.iter().any(|x| ["server_only", "client_only", "client_and_server", "singleplayer"].contains(&&*x.field_name)) { + if project.project_types.iter().any(|x| ["mod", "modpack"].contains(&&**x)) && !project.aggregate_version_fields.iter().any(|x| x.field_name == "environment") { mod_messages.messages.push(ModerationMessage::NoSideTypes); } diff --git a/apps/labrinth/src/routes/v2/project_creation.rs b/apps/labrinth/src/routes/v2/project_creation.rs index 540b3a6e1..9a4562430 100644 --- a/apps/labrinth/src/routes/v2/project_creation.rs +++ b/apps/labrinth/src/routes/v2/project_creation.rs @@ -158,10 +158,12 @@ pub async fn project_create( .into_iter() .map(|v| { let mut fields = HashMap::new(); - fields.extend(v2_reroute::convert_side_types_v3( - client_side, - server_side, - )); + fields.extend( + v2_reroute::convert_v2_side_types_to_v3_side_types( + client_side, + server_side, + ), + ); fields.insert( "game_versions".to_string(), json!(v.game_versions), diff --git a/apps/labrinth/src/routes/v2/projects.rs b/apps/labrinth/src/routes/v2/projects.rs index a29860c97..c4a91b877 100644 --- a/apps/labrinth/src/routes/v2/projects.rs +++ b/apps/labrinth/src/routes/v2/projects.rs @@ -511,6 +511,7 @@ pub async fn project_edit( moderation_message: v2_new_project.moderation_message, moderation_message_body: v2_new_project.moderation_message_body, monetization_status: v2_new_project.monetization_status, + side_types_migration_review_status: None, // Not to be exposed in v2 }; // This returns 204 or failure so we don't need to do anything with it @@ -547,10 +548,12 @@ pub async fn project_edit( let version = Version::from(version); let mut fields = version.fields; let (current_client_side, current_server_side) = - v2_reroute::convert_side_types_v2(&fields, None); + v2_reroute::convert_v3_side_types_to_v2_side_types( + &fields, None, + ); let client_side = client_side.unwrap_or(current_client_side); let server_side = server_side.unwrap_or(current_server_side); - fields.extend(v2_reroute::convert_side_types_v3( + fields.extend(v2_reroute::convert_v2_side_types_to_v3_side_types( client_side, server_side, )); diff --git a/apps/labrinth/src/routes/v2/version_creation.rs b/apps/labrinth/src/routes/v2/version_creation.rs index 824027bbb..9406ce556 100644 --- a/apps/labrinth/src/routes/v2/version_creation.rs +++ b/apps/labrinth/src/routes/v2/version_creation.rs @@ -105,7 +105,7 @@ pub async fn version_create( json!(legacy_create.game_versions), ); - // Get all possible side-types for loaders given- we will use these to check if we need to convert/apply singleplayer, etc. + // Get all possible side-types for loaders given- we will use these to check if we need to convert/apply side types let loaders = match v3::tags::loader_list(client.clone(), redis.clone()) .await @@ -136,53 +136,32 @@ pub async fn version_create( .collect::>(); // Copies side types of another version of the project. - // If no version exists, defaults to all false. + // If no version exists, defaults to an unknown side type. // This is inherently lossy, but not much can be done about it, as side types are no longer associated with projects, - // so the 'missing' ones can't be easily accessed, and versions do need to have these fields explicitly set. - let side_type_loader_field_names = [ - "singleplayer", - "client_and_server", - "client_only", - "server_only", - ]; + // so the 'missing' ones can't be easily accessed, and versions do need to have that field explicitly set. - // Check if loader_fields_aggregate contains any of these side types + // Check if loader_fields_aggregate contains the side types // We assume these four fields are linked together. if loader_fields_aggregate .iter() - .any(|f| side_type_loader_field_names.contains(&f.as_str())) + .any(|field| field == "environment") { - // If so, we get the fields of the example version of the project, and set the side types to match. - fields.extend( - side_type_loader_field_names - .iter() - .map(|f| (f.to_string(), json!(false))), - ); - if let Some(example_version_fields) = + // If so, we get the field of an example version of the project, and set the side types to match. + fields.insert( + "environment".into(), get_example_version_fields( legacy_create.project_id, client, &redis, ) .await? - { - fields.extend( - example_version_fields.into_iter().filter_map( - |f| { - if side_type_loader_field_names - .contains(&f.field_name.as_str()) - { - Some(( - f.field_name, - f.value.serialize_internal(), - )) - } else { - None - } - }, - ), - ); - } + .into_iter() + .flatten() + .find(|f| f.field_name == "environment") + .map_or(json!("unknown"), |f| { + f.value.serialize_internal() + }), + ); } // Handle project type via file extension prediction let mut project_type = None; diff --git a/apps/labrinth/src/routes/v2_reroute.rs b/apps/labrinth/src/routes/v2_reroute.rs index b6a193757..0a99b0796 100644 --- a/apps/labrinth/src/routes/v2_reroute.rs +++ b/apps/labrinth/src/routes/v2_reroute.rs @@ -164,69 +164,46 @@ where Ok(new_multipart) } -// Converts a "client_side" and "server_side" pair into the new v3 corresponding fields -pub fn convert_side_types_v3( +/// Converts V2 side types to V3 side types. +pub fn convert_v2_side_types_to_v3_side_types( client_side: LegacySideType, server_side: LegacySideType, ) -> HashMap { - use LegacySideType::{Optional, Required}; + use LegacySideType::{Optional, Required, Unsupported}; - let singleplayer = client_side == Required - || client_side == Optional - || server_side == Required - || server_side == Optional; - let client_and_server = singleplayer; - let client_only = (client_side == Required || client_side == Optional) - && server_side != Required; - let server_only = (server_side == Required || server_side == Optional) - && client_side != Required; + let environment = match (client_side, server_side) { + (Required, Required) => "client_and_server", // Or "singleplayer_only" + (Required, Unsupported) => "client_only", + (Required, Optional) => "client_only_server_optional", + (Unsupported, Required) => "server_only", // Or "dedicated_server_only" + (Optional, Required) => "server_only_client_optional", + (Optional, Optional) => "client_or_server", // Or "client_or_server_prefers_both" + _ => "unknown", + }; - let mut fields = HashMap::new(); - fields.insert("singleplayer".to_string(), json!(singleplayer)); - fields.insert("client_and_server".to_string(), json!(client_and_server)); - fields.insert("client_only".to_string(), json!(client_only)); - fields.insert("server_only".to_string(), json!(server_only)); - fields + [("environment".to_string(), json!(environment))] + .into_iter() + .collect() } -// Convert search facets from V3 back to v2 -// this is not lossless. (See tests) -pub fn convert_side_types_v2( +/// Converts a V3 side types map into the corresponding V2 side types. +pub fn convert_v3_side_types_to_v2_side_types( side_types: &HashMap, project_type: Option<&str>, ) -> (LegacySideType, LegacySideType) { - let client_and_server = side_types - .get("client_and_server") - .and_then(|x| x.as_bool()) - .unwrap_or(false); - let singleplayer = side_types - .get("singleplayer") - .and_then(|x| x.as_bool()) - .unwrap_or(client_and_server); - let client_only = side_types - .get("client_only") - .and_then(|x| x.as_bool()) - .unwrap_or(false); - let server_only = side_types - .get("server_only") - .and_then(|x| x.as_bool()) - .unwrap_or(false); - - convert_side_types_v2_bools( - Some(singleplayer), - client_only, - server_only, - Some(client_and_server), + convert_v3_environment_to_v2_side_types( + side_types + .get("environment") + .and_then(|x| x.as_str()) + .unwrap_or("unknown"), project_type, ) } -// Client side, server side -pub fn convert_side_types_v2_bools( - singleplayer: Option, - client_only: bool, - server_only: bool, - client_and_server: Option, +/// Converts a V3 environment and project type into the corresponding V2 side types. +/// The first side type is for the client, the second is for the server. +pub fn convert_v3_environment_to_v2_side_types( + environment: &str, project_type: Option<&str>, ) -> (LegacySideType, LegacySideType) { use LegacySideType::{Optional, Required, Unknown, Unsupported}; @@ -236,30 +213,18 @@ pub fn convert_side_types_v2_bools( Some("datapack") => (Optional, Required), Some("shader") => (Required, Unsupported), Some("resourcepack") => (Required, Unsupported), - _ => { - let singleplayer = - singleplayer.or(client_and_server).unwrap_or(false); - - match (singleplayer, client_only, server_only) { - // Only singleplayer - (true, false, false) => (Required, Required), - - // Client only and not server only - (false, true, false) => (Required, Unsupported), - (true, true, false) => (Required, Unsupported), - - // Server only and not client only - (false, false, true) => (Unsupported, Required), - (true, false, true) => (Unsupported, Required), - - // Both server only and client only - (true, true, true) => (Optional, Optional), - (false, true, true) => (Optional, Optional), - - // Bad type - (false, false, false) => (Unknown, Unknown), - } - } + _ => match environment { + "client_and_server" => (Required, Required), + "client_only" => (Required, Unsupported), + "client_only_server_optional" => (Required, Optional), + "singleplayer_only" => (Required, Required), + "server_only" => (Unsupported, Required), + "server_only_client_optional" => (Optional, Required), + "dedicated_server_only" => (Unsupported, Required), + "client_or_server" => (Optional, Optional), + "client_or_server_prefers_both" => (Optional, Optional), + _ => (Unknown, Unknown), // "unknown" + }, } } @@ -279,13 +244,14 @@ mod tests { }; #[test] - fn convert_types() { - // Converting types from V2 to V3 and back should be idempotent- for certain pairs + fn v2_v3_side_type_conversion() { + // Only nonsensical V2 side types cannot be round-tripped from V2 to V3 and back. + // When converting from V3 to V2, only additional information about the + // singleplayer-only, multiplayer-only, or install on both sides nature of the + // project is lost. let lossy_pairs = [ (Optional, Unsupported), (Unsupported, Optional), - (Required, Optional), - (Optional, Required), (Unsupported, Unsupported), ]; @@ -294,10 +260,13 @@ mod tests { if lossy_pairs.contains(&(client_side, server_side)) { continue; } - let side_types = - convert_side_types_v3(client_side, server_side); + let side_types = convert_v2_side_types_to_v3_side_types( + client_side, + server_side, + ); let (client_side2, server_side2) = - convert_side_types_v2(&side_types, None); + convert_v3_side_types_to_v2_side_types(&side_types, None); + assert_eq!(client_side, client_side2); assert_eq!(server_side, server_side2); } diff --git a/apps/labrinth/src/routes/v3/project_creation.rs b/apps/labrinth/src/routes/v3/project_creation.rs index 92d5d258c..e7a9a33cf 100644 --- a/apps/labrinth/src/routes/v3/project_creation.rs +++ b/apps/labrinth/src/routes/v3/project_creation.rs @@ -12,7 +12,8 @@ use crate::models::ids::{ImageId, OrganizationId, ProjectId, VersionId}; use crate::models::images::{Image, ImageContext}; use crate::models::pats::Scopes; use crate::models::projects::{ - License, Link, MonetizationStatus, ProjectStatus, VersionStatus, + License, Link, MonetizationStatus, ProjectStatus, + SideTypesMigrationReviewStatus, VersionStatus, }; use crate::models::teams::{OrganizationPermissions, ProjectPermissions}; use crate::models::threads::ThreadType; @@ -901,6 +902,9 @@ async fn project_create_inner( color: project_builder.color, thread_id: thread_id.into(), monetization_status: MonetizationStatus::Monetized, + // New projects are considered reviewed with respect to side types migrations + side_types_migration_review_status: + SideTypesMigrationReviewStatus::Reviewed, fields: HashMap::new(), // Fields instantiate to empty }; diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 025b991aa..e7f829e57 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -17,6 +17,7 @@ use crate::models::notifications::NotificationBody; use crate::models::pats::Scopes; use crate::models::projects::{ MonetizationStatus, Project, ProjectStatus, SearchRequest, + SideTypesMigrationReviewStatus, }; use crate::models::teams::ProjectPermissions; use crate::models::threads::MessageBody; @@ -247,6 +248,8 @@ pub struct EditProject { #[validate(length(max = 65536))] pub moderation_message_body: Option>, pub monetization_status: Option, + pub side_types_migration_review_status: + Option, } #[allow(clippy::too_many_arguments)] @@ -844,6 +847,29 @@ pub async fn project_edit( .await?; } + if let Some(side_types_migration_review_status) = + &new_project.side_types_migration_review_status + { + if !perms.contains(ProjectPermissions::EDIT_DETAILS) { + return Err(ApiError::CustomAuthentication( + "You do not have the permissions to edit the side types migration review status of this project!" + .to_string(), + )); + } + + sqlx::query!( + " + UPDATE mods + SET side_types_migration_review_status = $1 + WHERE id = $2 + ", + side_types_migration_review_status.as_str(), + id as db_ids::DBProjectId, + ) + .execute(&mut *transaction) + .await?; + } + // check new description and body for links to associated images // if they no longer exist in the description or body, delete them let checkable_strings: Vec<&str> = diff --git a/apps/labrinth/src/search/indexing/local_import.rs b/apps/labrinth/src/search/indexing/local_import.rs index f8ef311ae..4fca8ea33 100644 --- a/apps/labrinth/src/search/indexing/local_import.rs +++ b/apps/labrinth/src/search/indexing/local_import.rs @@ -362,7 +362,7 @@ pub async fn index_local( let (_, v2_og_project_type) = LegacyProject::get_project_type(&project_types); let (client_side, server_side) = - v2_reroute::convert_side_types_v2( + v2_reroute::convert_v3_side_types_to_v2_side_types( &unvectorized_loader_fields, Some(&v2_og_project_type), ); diff --git a/apps/labrinth/src/search/indexing/mod.rs b/apps/labrinth/src/search/indexing/mod.rs index e38864ec5..1ecb70ad7 100644 --- a/apps/labrinth/src/search/indexing/mod.rs +++ b/apps/labrinth/src/search/indexing/mod.rs @@ -350,11 +350,8 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ "color", // Note: loader fields are not here, but are added on as they are needed (so they can be dynamically added depending on which exist). // TODO: remove these- as they should be automatically populated. This is a band-aid fix. - "server_only", - "client_only", + "environment", "game_versions", - "singleplayer", - "client_and_server", "mrpack_loaders", // V2 legacy fields for logical consistency "client_side", @@ -397,11 +394,8 @@ const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[ "color", // Note: loader fields are not here, but are added on as they are needed (so they can be dynamically added depending on which exist). // TODO: remove these- as they should be automatically populated. This is a band-aid fix. - "server_only", - "client_only", + "environment", "game_versions", - "singleplayer", - "client_and_server", "mrpack_loaders", // V2 legacy fields for logical consistency "client_side", diff --git a/apps/labrinth/tests/common/api_v3/request_data.rs b/apps/labrinth/tests/common/api_v3/request_data.rs index 98e2d173d..c9774dd0f 100644 --- a/apps/labrinth/tests/common/api_v3/request_data.rs +++ b/apps/labrinth/tests/common/api_v3/request_data.rs @@ -73,10 +73,7 @@ pub fn get_public_version_creation_data_json( // Loader fields "game_versions": ["1.20.1"], - "singleplayer": true, - "client_and_server": true, - "client_only": true, - "server_only": false, + "environment": "client_only_server_optional", }); if is_modpack { j["mrpack_loaders"] = json!(["fabric"]); diff --git a/apps/labrinth/tests/common/search.rs b/apps/labrinth/tests/common/search.rs index f4d7c60a0..c6c0035ed 100644 --- a/apps/labrinth/tests/common/search.rs +++ b/apps/labrinth/tests/common/search.rs @@ -63,7 +63,7 @@ pub async fn setup_search_projects( let id = 0; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[4..6] }, - { "op": "add", "path": "/initial_versions/0/server_only", "value": true }, + { "op": "add", "path": "/initial_versions/0/environment", "value": "server_only" }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, ])) .unwrap(); @@ -78,7 +78,7 @@ pub async fn setup_search_projects( let id = 1; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] }, - { "op": "add", "path": "/initial_versions/0/client_only", "value": false }, + { "op": "add", "path": "/initial_versions/0/environment", "value": "client_or_server" }, ])) .unwrap(); project_creation_futures.push(create_async_future( @@ -92,7 +92,7 @@ pub async fn setup_search_projects( let id = 2; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] }, - { "op": "add", "path": "/initial_versions/0/server_only", "value": true }, + { "op": "add", "path": "/initial_versions/0/environment", "value": "server_only" }, { "op": "add", "path": "/name", "value": "Mysterious Project" }, ])) .unwrap(); @@ -107,7 +107,7 @@ pub async fn setup_search_projects( let id = 3; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] }, - { "op": "add", "path": "/initial_versions/0/server_only", "value": true }, + { "op": "add", "path": "/initial_versions/0/environment", "value": "server_only" }, { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.4"] }, { "op": "add", "path": "/name", "value": "Mysterious Project" }, { "op": "add", "path": "/license_id", "value": "LicenseRef-All-Rights-Reserved" }, @@ -124,7 +124,7 @@ pub async fn setup_search_projects( let id = 4; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] }, - { "op": "add", "path": "/initial_versions/0/client_only", "value": false }, + { "op": "add", "path": "/initial_versions/0/environment", "value": "client_or_server" }, { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] }, ])) .unwrap(); @@ -139,7 +139,7 @@ pub async fn setup_search_projects( let id = 5; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, - { "op": "add", "path": "/initial_versions/0/client_only", "value": false }, + { "op": "add", "path": "/initial_versions/0/environment", "value": "client_or_server" }, { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, ])) @@ -155,8 +155,7 @@ pub async fn setup_search_projects( let id = 6; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, - { "op": "add", "path": "/initial_versions/0/client_only", "value": false }, - { "op": "add", "path": "/initial_versions/0/server_only", "value": true }, + { "op": "add", "path": "/initial_versions/0/environment", "value": "client_or_server_prefers_both" }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, ])) .unwrap(); @@ -173,8 +172,7 @@ pub async fn setup_search_projects( let id = 7; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, - { "op": "add", "path": "/initial_versions/0/client_only", "value": false }, - { "op": "add", "path": "/initial_versions/0/server_only", "value": true }, + { "op": "add", "path": "/initial_versions/0/environment", "value": "client_or_server_prefers_both" }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, { "op": "add", "path": "/initial_versions/0/loaders", "value": ["forge"] }, { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.2"] }, diff --git a/apps/labrinth/tests/files/dummy_data.sql b/apps/labrinth/tests/files/dummy_data.sql index f3fb1e47d..9866a9d45 100644 --- a/apps/labrinth/tests/files/dummy_data.sql +++ b/apps/labrinth/tests/files/dummy_data.sql @@ -67,8 +67,8 @@ VALUES (2, 'Ordering_Negative1', '{"type":"release","major":false}', -1); INSERT INTO loader_field_enum_values(enum_id, value, metadata, ordering) VALUES (2, 'Ordering_Positive100', '{"type":"release","major":false}', 100); -INSERT INTO loader_fields_loaders(loader_id, loader_field_id) -SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field IN ('game_versions','singleplayer', 'client_and_server', 'client_only', 'server_only') ON CONFLICT DO NOTHING; +INSERT INTO loader_fields_loaders(loader_id, loader_field_id) +SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field IN ('game_versions','environment') ON CONFLICT DO NOTHING; INSERT INTO categories (id, category, project_type) VALUES (51, 'combat', 1), @@ -108,6 +108,6 @@ VALUES ( INSERT INTO oauth_client_redirect_uris (id, client_id, uri) VALUES (1, 1, 'https://modrinth.com/oauth_callback'); -- Create dummy data table to mark that this file has been run -CREATE TABLE dummy_data ( +CREATE TABLE dummy_data ( update_id bigint PRIMARY KEY ); diff --git a/apps/labrinth/tests/loader_fields.rs b/apps/labrinth/tests/loader_fields.rs index 0f73391c1..53a86bfac 100644 --- a/apps/labrinth/tests/loader_fields.rs +++ b/apps/labrinth/tests/loader_fields.rs @@ -114,7 +114,7 @@ async fn creating_loader_fields() { Some( serde_json::from_value(json!([{ "op": "remove", - "path": "/singleplayer" + "path": "/environment" }])) .unwrap(), ), @@ -273,12 +273,8 @@ async fn creating_loader_fields() { "value": ["1.20.1", "1.20.2"] }, { "op": "add", - "path": "/singleplayer", - "value": false - }, { - "op": "add", - "path": "/server_only", - "value": true + "path": "/environment", + "value": "client_or_server_prefers_both" }])) .unwrap(), ), @@ -286,16 +282,17 @@ async fn creating_loader_fields() { ) .await; assert_eq!(&v.fields["game_versions"], &json!(["1.20.1", "1.20.2"])); - assert_eq!(&v.fields["singleplayer"], &json!(false)); - assert_eq!(&v.fields["server_only"], &json!(true)); + assert_eq!( + &v.fields["environment"], + &json!("client_or_server_prefers_both") + ); // - Patch let resp = api .edit_version( alpha_version_id, json!({ "game_versions": ["1.20.1", "1.20.2"], - "singleplayer": false, - "server_only": true + "environment": "client_or_server_prefers_both" }), USER_USER_PAT, ) @@ -320,8 +317,8 @@ async fn creating_loader_fields() { "value": ["1.20.5"] }, { "op": "add", - "path": "/singleplayer", - "value": false + "path": "/environment", + "value": "client_or_server" }])) .unwrap(), ), @@ -357,8 +354,13 @@ async fn creating_loader_fields() { &project.fields["game_versions"], &[json!("1.20.1"), json!("1.20.2"), json!("1.20.5")] ); - assert!(project.fields["singleplayer"].contains(&json!(false))); - assert!(project.fields["singleplayer"].contains(&json!(true))); + assert!( + project.fields["environment"].contains(&json!("client_or_server")) + ); + assert!( + project.fields["environment"] + .contains(&json!("client_or_server_prefers_both")) + ); }) .await } @@ -421,10 +423,7 @@ async fn get_available_loader_fields() { fabric_loader_fields, [ "game_versions", - "singleplayer", - "client_and_server", - "client_only", - "server_only", + "environment", "test_fabric_optional" // exists for testing ] .iter() @@ -444,10 +443,7 @@ async fn get_available_loader_fields() { mrpack_loader_fields, [ "game_versions", - "singleplayer", - "client_and_server", - "client_only", - "server_only", + "environment", // mrpack has all the general fields as well as this "mrpack_loaders" ] diff --git a/apps/labrinth/tests/search.rs b/apps/labrinth/tests/search.rs index d8aefc493..f45c4baf4 100644 --- a/apps/labrinth/tests/search.rs +++ b/apps/labrinth/tests/search.rs @@ -52,8 +52,11 @@ async fn search_projects() { vec![1, 2, 3, 4], ), (json!([["project_types:modpack"]]), vec![4]), - (json!([["client_only:true"]]), vec![0, 2, 3, 7, 9]), - (json!([["server_only:true"]]), vec![0, 2, 3, 6, 7]), + (json!([["environment:server_only"]]), vec![0, 2, 3]), + ( + json!([["environment:client_or_server_prefers_both"]]), + vec![6, 7], + ), (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7, 9]), (json!([["license:MIT"]]), vec![1, 2, 4, 9]), (json!([[r#"name:'Mysterious Project'"#]]), vec![2, 3]), From ba4fecb0cb26fcef13a0579b4f281942ae5a4341 Mon Sep 17 00:00:00 2001 From: Josiah Glosson Date: Wed, 18 Jun 2025 15:49:23 -0500 Subject: [PATCH 13/14] Fix direct lint of `packages/app-lib` (#3808) * Fix theseus lint by adding "unstable" feature to Tauri * Remove "unstable" feature from tauri in apps/app --- apps/app/Cargo.toml | 2 +- packages/app-lib/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/Cargo.toml b/apps/app/Cargo.toml index 5cf03bd01..6601ece6b 100644 --- a/apps/app/Cargo.toml +++ b/apps/app/Cargo.toml @@ -16,7 +16,7 @@ serde_json.workspace = true serde = { workspace = true, features = ["derive"] } serde_with.workspace = true -tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] } +tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset"] } tauri-plugin-window-state.workspace = true tauri-plugin-deep-link.workspace = true tauri-plugin-os.workspace = true diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index 7d00b1559..e9ee7f6bf 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -39,7 +39,7 @@ tracing-error.workspace = true paste.workspace = true -tauri = { workspace = true, optional = true } +tauri = { workspace = true, optional = true, features = ["unstable"] } indicatif = { workspace = true, optional = true } async-tungstenite = { workspace = true, features = ["tokio-runtime", "tokio-rustls-webpki-roots"] } From dbde3c4669af10dd577590ed6980e5bd4552d13c Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:07:15 -0700 Subject: [PATCH 14/14] Remove duplicate components in web frontend Avatar, Badge, CopyCode, and Pagination (#3741) --- apps/frontend/src/components/ui/Avatar.vue | 46 ---- apps/frontend/src/components/ui/Badge.vue | 131 ------------ apps/frontend/src/components/ui/CopyCode.vue | 75 ------- .../src/components/ui/NotificationItem.vue | 11 +- .../frontend/src/components/ui/Pagination.vue | 196 ------------------ .../src/components/ui/ProjectCard.vue | 8 +- .../src/components/ui/report/ReportInfo.vue | 5 +- .../ui/thread/ConversationThread.vue | 3 +- .../components/ui/thread/ThreadMessage.vue | 4 +- apps/frontend/src/pages/[type]/[id].vue | 4 +- .../src/pages/[type]/[id]/settings/index.vue | 3 +- .../pages/[type]/[id]/version/[version].vue | 14 +- apps/frontend/src/pages/app.vue | 4 +- apps/frontend/src/pages/dashboard/index.vue | 2 +- .../src/pages/dashboard/notifications.vue | 7 +- .../frontend/src/pages/dashboard/projects.vue | 35 ++-- apps/frontend/src/pages/index.vue | 3 +- apps/frontend/src/pages/moderation/review.vue | 8 +- apps/frontend/src/pages/settings/pats.vue | 2 +- apps/frontend/src/pages/user/[id].vue | 2 +- 20 files changed, 55 insertions(+), 508 deletions(-) delete mode 100644 apps/frontend/src/components/ui/Avatar.vue delete mode 100644 apps/frontend/src/components/ui/Badge.vue delete mode 100644 apps/frontend/src/components/ui/CopyCode.vue delete mode 100644 apps/frontend/src/components/ui/Pagination.vue diff --git a/apps/frontend/src/components/ui/Avatar.vue b/apps/frontend/src/components/ui/Avatar.vue deleted file mode 100644 index 6216aa5d8..000000000 --- a/apps/frontend/src/components/ui/Avatar.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/Badge.vue b/apps/frontend/src/components/ui/Badge.vue deleted file mode 100644 index 1ebb84510..000000000 --- a/apps/frontend/src/components/ui/Badge.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - - - diff --git a/apps/frontend/src/components/ui/CopyCode.vue b/apps/frontend/src/components/ui/CopyCode.vue deleted file mode 100644 index 98cb14c06..000000000 --- a/apps/frontend/src/components/ui/CopyCode.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - - - diff --git a/apps/frontend/src/components/ui/NotificationItem.vue b/apps/frontend/src/components/ui/NotificationItem.vue index 1da769a34..4a293684a 100644 --- a/apps/frontend/src/components/ui/NotificationItem.vue +++ b/apps/frontend/src/components/ui/NotificationItem.vue @@ -104,13 +104,13 @@ by the moderators. @@ -331,16 +331,13 @@ import { XIcon, ExternalIcon, } from "@modrinth/assets"; -import { useRelativeTime } from "@modrinth/ui"; +import { Avatar, ProjectStatusBadge, CopyCode, useRelativeTime } from "@modrinth/ui"; import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue"; import { getProjectLink, getVersionLink } from "~/helpers/projects.js"; import { getUserLink } from "~/helpers/users.js"; import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js"; import { markAsRead } from "~/helpers/notifications.ts"; import DoubleIcon from "~/components/ui/DoubleIcon.vue"; -import Avatar from "~/components/ui/Avatar.vue"; -import Badge from "~/components/ui/Badge.vue"; -import CopyCode from "~/components/ui/CopyCode.vue"; import Categories from "~/components/ui/search/Categories.vue"; const app = useNuxtApp(); diff --git a/apps/frontend/src/components/ui/Pagination.vue b/apps/frontend/src/components/ui/Pagination.vue deleted file mode 100644 index 99fb555ca..000000000 --- a/apps/frontend/src/components/ui/Pagination.vue +++ /dev/null @@ -1,196 +0,0 @@ - - - - - diff --git a/apps/frontend/src/components/ui/ProjectCard.vue b/apps/frontend/src/components/ui/ProjectCard.vue index fbb147c2a..1be6f86c4 100644 --- a/apps/frontend/src/components/ui/ProjectCard.vue +++ b/apps/frontend/src/components/ui/ProjectCard.vue @@ -29,7 +29,7 @@ {{ author }}

- +

{{ description }} @@ -91,18 +91,16 @@