From 923410983d4860d3466d9dfe5ffbd74ae6fa55c0 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Thu, 26 Mar 2026 22:15:59 +0000 Subject: [PATCH 1/2] feat: better tooltips for mods in content tab hosting panel --- .../api-client/src/modules/archon/types.ts | 2 ++ .../components/ContentCardItem.vue | 22 +++++++++++++++++-- .../components/ContentCardTable.vue | 3 +++ .../components/modals/ModpackContentModal.vue | 8 +++++-- .../composables/content-filtering.ts | 16 +++++++++----- .../src/layouts/shared/content-tab/layout.vue | 7 +++++- .../src/layouts/shared/content-tab/types.ts | 5 +++++ .../components/ContentDiffModal.vue | 2 +- .../wrapped/hosting/manage/content.vue | 2 ++ packages/ui/src/locales/en-US/index.json | 8 ++++++- packages/ui/src/utils/common-messages.ts | 10 +++++++++ 11 files changed, 72 insertions(+), 13 deletions(-) diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts index 8b74a55d60..0a40f11f73 100644 --- a/packages/api-client/src/modules/archon/types.ts +++ b/packages/api-client/src/modules/archon/types.ts @@ -27,6 +27,8 @@ export namespace Archon { disabled: boolean kind: AddonKind from_modpack: boolean + pack_client_retained: boolean + pack_client_depends: boolean has_update: string | null name: string | null project_id: string | null diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue index 3aaa22023a..2acde1aaea 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardItem.vue @@ -25,7 +25,12 @@ import { useVIntl } from '#ui/composables/i18n' import { commonMessages } from '#ui/utils/common-messages' import { truncatedTooltip } from '#ui/utils/truncate' -import type { ContentCardProject, ContentCardVersion, ContentOwner } from '../types' +import type { + ClientWarningType, + ContentCardProject, + ContentCardVersion, + ContentOwner, +} from '../types' const { formatMessage } = useVIntl() @@ -39,6 +44,7 @@ interface Props { installing?: boolean hasUpdate?: boolean isClientOnly?: boolean + clientWarning?: ClientWarningType | null overflowOptions?: OverflowMenuOption[] disabled?: boolean showCheckbox?: boolean @@ -55,6 +61,7 @@ const props = withDefaults(defineProps(), { installing: false, hasUpdate: false, isClientOnly: false, + clientWarning: null, overflowOptions: undefined, disabled: false, showCheckbox: false, @@ -83,6 +90,17 @@ const fileNameRef = ref(null) const isDisabled = computed(() => props.disabled || props.installing) +const clientWarningMessage = computed(() => { + switch (props.clientWarning) { + case 'retained': + return commonMessages.clientRetainedWarning + case 'depends': + return commonMessages.clientDependsWarning + default: + return commonMessages.clientOnlyWarning + } +}) + const { shift: shiftHeld } = useMagicKeys() const deleteHovered = ref(false) @@ -147,7 +165,7 @@ const deleteHovered = ref(false) diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue index 1dfdf8d64d..d7b61c1783 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/ContentCardTable.vue @@ -276,6 +276,7 @@ function handleSort(column: ContentCardTableSortColumn) { :installing="item.installing" :has-update="item.hasUpdate" :is-client-only="item.isClientOnly" + :client-warning="item.clientWarning" :overflow-options="item.overflowOptions" :disabled="item.disabled" :show-checkbox="showSelection" @@ -329,6 +330,8 @@ function handleSort(column: ContentCardTableSortColumn) { :enabled="item.enabled" :installing="item.installing" :has-update="item.hasUpdate" + :is-client-only="item.isClientOnly" + :client-warning="item.clientWarning" :overflow-options="item.overflowOptions" :disabled="item.disabled" :show-checkbox="showSelection" diff --git a/packages/ui/src/layouts/shared/content-tab/components/modals/ModpackContentModal.vue b/packages/ui/src/layouts/shared/content-tab/components/modals/ModpackContentModal.vue index 55da9642e5..0d2f58f5fc 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/modals/ModpackContentModal.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/modals/ModpackContentModal.vue @@ -25,7 +25,7 @@ import { normalizeProjectType, } from '#ui/utils/common-messages' -import { isClientOnlyEnvironment } from '../../composables/content-filtering' +import { getClientWarningType, isClientOnlyEnvironment } from '../../composables/content-filtering' import type { ContentCardTableItem, ContentItem } from '../../types' import ContentCardTable from '../ContentCardTable.vue' import ContentSelectionBar from '../ContentSelectionBar.vue' @@ -239,7 +239,11 @@ const tableItems = computed(() => } : undefined, ...(props.enableToggle ? { enabled: item.enabled } : {}), - isClientOnly: isClientOnlyEnvironment(item.environment), + isClientOnly: + isClientOnlyEnvironment(item.environment) || + !!item.pack_client_retained || + !!item.pack_client_depends, + clientWarning: getClientWarningType(item), disabled: disabledIds.value.has(item.file_name), overflowOptions: [ ...(props.switchVersion diff --git a/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts b/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts index 7f448e1e18..76957b3748 100644 --- a/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts +++ b/packages/ui/src/layouts/shared/content-tab/composables/content-filtering.ts @@ -5,7 +5,7 @@ import { computed, ref, watch } from 'vue' import { useVIntl } from '#ui/composables/i18n' import { commonProjectTypeCategoryMessages, normalizeProjectType } from '#ui/utils/common-messages' -import type { ContentItem } from '../types' +import type { ClientWarningType, ContentItem } from '../types' const CLIENT_ONLY_ENVIRONMENTS = new Set(['client_only', 'singleplayer_only']) @@ -13,6 +13,13 @@ export function isClientOnlyEnvironment(env?: string | null): boolean { return !!env && CLIENT_ONLY_ENVIRONMENTS.has(env) } +export function getClientWarningType(item: ContentItem): ClientWarningType | null { + if (item.pack_client_retained) return 'retained' + if (item.pack_client_depends) return 'depends' + if (isClientOnlyEnvironment(item.environment)) return 'environment' + return null +} + export interface ContentFilterOption { id: string label: string @@ -59,10 +66,7 @@ export function useContentFilters(items: Ref, config?: ContentFil options.push({ id: 'updates', label: 'Updates' }) } - if ( - config?.showClientOnlyFilter && - items.value.some((m) => isClientOnlyEnvironment(m.environment)) - ) { + if (config?.showClientOnlyFilter && items.value.some((m) => getClientWarningType(m) !== null)) { options.push({ id: 'client-only', label: 'Client-only' }) } @@ -106,7 +110,7 @@ export function useContentFilters(items: Ref, config?: ContentFil for (const filter of activeAttributes) { if (filter === 'updates' && !item.has_update) return false if (filter === 'disabled' && item.enabled) return false - if (filter === 'client-only' && !isClientOnlyEnvironment(item.environment)) return false + if (filter === 'client-only' && getClientWarningType(item) === null) return false } return true diff --git a/packages/ui/src/layouts/shared/content-tab/layout.vue b/packages/ui/src/layouts/shared/content-tab/layout.vue index e1334224aa..8fff08ebe7 100644 --- a/packages/ui/src/layouts/shared/content-tab/layout.vue +++ b/packages/ui/src/layouts/shared/content-tab/layout.vue @@ -40,6 +40,7 @@ import ConfirmBulkUpdateModal from './components/modals/ConfirmBulkUpdateModal.v import ConfirmDeletionModal from './components/modals/ConfirmDeletionModal.vue' import ConfirmUnlinkModal from './components/modals/ConfirmUnlinkModal.vue' import { + getClientWarningType, isClientOnlyEnvironment, useBulkOperation, useChangingItems, @@ -279,7 +280,11 @@ const tableItems = computed(() => { item.installing === true, installing: item.installing === true, hasUpdate: !ctx.isPackLocked.value && item.has_update, - isClientOnly: isClientOnlyEnvironment(item.environment), + isClientOnly: + isClientOnlyEnvironment(item.environment) || + !!item.pack_client_retained || + !!item.pack_client_depends, + clientWarning: getClientWarningType(item), overflowOptions: ctx.getOverflowOptions?.(item), } }) diff --git a/packages/ui/src/layouts/shared/content-tab/types.ts b/packages/ui/src/layouts/shared/content-tab/types.ts index aa55c490dc..618066ab9d 100644 --- a/packages/ui/src/layouts/shared/content-tab/types.ts +++ b/packages/ui/src/layouts/shared/content-tab/types.ts @@ -21,6 +21,8 @@ export interface ContentOwner { link?: string | RouteLocationRaw | (() => void) } +export type ClientWarningType = 'retained' | 'depends' | 'environment' + export interface ContentCardTableItem { id: string project: ContentCardProject @@ -33,6 +35,7 @@ export interface ContentCardTableItem { installing?: boolean hasUpdate?: boolean isClientOnly?: boolean + clientWarning?: ClientWarningType | null overflowOptions?: OverflowMenuOption[] } @@ -53,6 +56,8 @@ export interface ContentItem extends Omit< update_version_id: string | null date_added?: string environment?: string + pack_client_retained?: boolean + pack_client_depends?: boolean installing?: boolean } diff --git a/packages/ui/src/layouts/shared/installation-settings/components/ContentDiffModal.vue b/packages/ui/src/layouts/shared/installation-settings/components/ContentDiffModal.vue index 089312b016..48b81cfa0d 100644 --- a/packages/ui/src/layouts/shared/installation-settings/components/ContentDiffModal.vue +++ b/packages/ui/src/layouts/shared/installation-settings/components/ContentDiffModal.vue @@ -201,7 +201,7 @@ const diffTypeMessages = defineMessages({ }, removed: { id: 'content.diff-modal.diff-type.removed', - defaultMessage: 'Removed', + defaultMessage: 'Disabled', }, updated: { id: 'content.diff-modal.diff-type.updated', diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue index 1206f53089..d91543e753 100644 --- a/packages/ui/src/layouts/wrapped/hosting/manage/content.vue +++ b/packages/ui/src/layouts/wrapped/hosting/manage/content.vue @@ -499,6 +499,8 @@ function addonToContentItem(addon: Archon.Content.v1.Addon): ContentItem { has_update: !!addon.has_update, update_version_id: addon.has_update, environment: addon.version?.environment ?? undefined, + pack_client_retained: addon.pack_client_retained, + pack_client_depends: addon.pack_client_depends, } } diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json index 60c4415618..78548f369f 100644 --- a/packages/ui/src/locales/en-US/index.json +++ b/packages/ui/src/locales/en-US/index.json @@ -282,7 +282,7 @@ "defaultMessage": "Added (dependency)" }, "content.diff-modal.diff-type.removed": { - "defaultMessage": "Removed" + "defaultMessage": "Disabled" }, "content.diff-modal.diff-type.updated": { "defaultMessage": "Updated" @@ -1196,9 +1196,15 @@ "label.changes-saved": { "defaultMessage": "Changes saved" }, + "label.client-depends-warning": { + "defaultMessage": "This mod depends on a client-side mod and may cause issues when starting your server" + }, "label.client-only-warning": { "defaultMessage": "This is a client-side mod and may cause issues. We've kept it enabled because some authors mislabel environments, and the loader should resolve the conflict." }, + "label.client-retained-warning": { + "defaultMessage": "This is a client-side mod that was installed as a dependency and may cause issues when starting your server" + }, "label.collections": { "defaultMessage": "Collections" }, diff --git a/packages/ui/src/utils/common-messages.ts b/packages/ui/src/utils/common-messages.ts index 42237784f3..fe61d1e783 100644 --- a/packages/ui/src/utils/common-messages.ts +++ b/packages/ui/src/utils/common-messages.ts @@ -406,6 +406,16 @@ export const commonMessages = defineMessages({ defaultMessage: "This is a client-side mod and may cause issues. We've kept it enabled because some authors mislabel environments, and the loader should resolve the conflict.", }, + clientRetainedWarning: { + id: 'label.client-retained-warning', + defaultMessage: + 'This is a client-side mod that was installed as a dependency and may cause issues when starting your server', + }, + clientDependsWarning: { + id: 'label.client-depends-warning', + defaultMessage: + 'This mod depends on a client-side mod and may cause issues when starting your server', + }, selectAllLabel: { id: 'label.select-all', defaultMessage: 'Select all', From ffcd2eb80a8daf57e1e97386fd8e1fa91600c046 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Thu, 26 Mar 2026 22:44:13 +0000 Subject: [PATCH 2/2] feat: qa --- .../hosting/manage/[id]/options/loader.vue | 39 ++++++++------ .../api-client/src/modules/archon/types.ts | 11 +++- .../server/ServerModpackContentCard.vue | 13 ++++- .../components/ContentCardItem.vue | 8 ++- .../components/ContentCardTable.vue | 1 + .../components/ContentModpackCard.vue | 36 ++++++++----- .../src/layouts/shared/content-tab/layout.vue | 1 + .../src/layouts/shared/content-tab/types.ts | 9 +++- .../shared/installation-settings/layout.vue | 47 +++++++++------- .../providers/installation-settings.ts | 2 +- .../shared/installation-settings/types.ts | 3 +- .../wrapped/hosting/manage/content.vue | 54 ++++++++++++------- 12 files changed, 151 insertions(+), 73 deletions(-) diff --git a/apps/frontend/src/pages/hosting/manage/[id]/options/loader.vue b/apps/frontend/src/pages/hosting/manage/[id]/options/loader.vue index 368479623b..247ddffd47 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/options/loader.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/options/loader.vue @@ -203,13 +203,18 @@ const addonsQuery = useQuery({ const modpack = computed(() => addonsQuery.data.value?.modpack ?? null) +const modpackProjectId = computed(() => { + const spec = modpack.value?.spec + return spec?.platform === 'modrinth' ? spec.project_id : null +}) + const modpackVersionsQuery = useQuery({ - queryKey: computed(() => ['labrinth', 'versions', 'v2', modpack.value?.spec.project_id]), + queryKey: computed(() => ['labrinth', 'versions', 'v2', modpackProjectId.value]), queryFn: () => - client.labrinth.versions_v2.getProjectVersions(modpack.value!.spec.project_id, { + client.labrinth.versions_v2.getProjectVersions(modpackProjectId.value!, { include_changelog: false, }), - enabled: computed(() => !!modpack.value?.spec.project_id), + enabled: computed(() => !!modpackProjectId.value), }) const auth = await useAuth() @@ -321,17 +326,20 @@ provideInstallationSettings({ }), isLinked: computed(() => { const val = !!modpack.value - debug('isLinked:', val, 'modpack:', modpack.value?.spec?.project_id) + debug('isLinked:', val, 'modpack:', modpackProjectId.value) return val }), isBusy: isInstalling, modpack: computed(() => { if (!modpack.value) return null + const isLocal = modpack.value.spec.platform === 'local_file' return { iconUrl: modpack.value.icon_url, - title: modpack.value.title ?? modpack.value.spec.project_id, - link: `/project/${modpack.value.spec.project_id}`, + title: + modpack.value.title ?? (isLocal ? modpack.value.spec.name : modpack.value.spec.project_id), + link: modpackProjectId.value ? `/project/${modpackProjectId.value}` : undefined, versionNumber: modpack.value.version_number, + filename: isLocal ? modpack.value.spec.filename : undefined, owner: modpack.value.owner ? { id: modpack.value.owner.id, @@ -460,7 +468,7 @@ provideInstallationSettings({ }, async reinstallModpack() { - if (!modpack.value) return + if (!modpack.value || modpack.value.spec.platform !== 'modrinth') return debug( 'reinstallModpack: called, project:', modpack.value.spec.project_id, @@ -531,10 +539,11 @@ provideInstallationSettings({ getCachedModpackVersions: () => modpackVersionsQuery.data.value ?? null, async fetchModpackVersions() { - debug('fetchModpackVersions: called, project:', modpack.value?.spec.project_id) + debug('fetchModpackVersions: called, project:', modpackProjectId.value) + if (!modpackProjectId.value) throw new Error('No modpack project ID') try { const versions = await client.labrinth.versions_v2.getProjectVersions( - modpack.value!.spec.project_id, + modpackProjectId.value, { include_changelog: false, }, @@ -562,7 +571,7 @@ provideInstallationSettings({ }, async onModpackVersionConfirm(version) { - if (!modpack.value) return + if (!modpackProjectId.value) return debug('onModpackVersionConfirm: called, version:', version.id) debug('onModpackVersionConfirm: emitting reinstall before API call') emit('reinstall') @@ -571,7 +580,7 @@ provideInstallationSettings({ content_variant: 'modpack', spec: { platform: 'modrinth', - project_id: modpack.value.spec.project_id, + project_id: modpackProjectId.value, version_id: version.id, }, soft_override: true, @@ -590,18 +599,18 @@ provideInstallationSettings({ updaterModalProps: computed(() => ({ isApp: false, - currentVersionId: modpack.value?.spec.version_id ?? '', + currentVersionId: + modpack.value?.spec.platform === 'modrinth' ? modpack.value.spec.version_id : '', projectIconUrl: modpack.value?.icon_url ?? undefined, projectName: - modpack.value?.title ?? - modpack.value?.spec.project_id ?? - formatMessage(commonMessages.modpackLabel), + modpack.value?.title ?? modpackProjectId.value ?? formatMessage(commonMessages.modpackLabel), currentGameVersion: addonsQuery.data.value?.game_version ?? server.value?.mc_version ?? '', currentLoader: addonsQuery.data.value?.modloader ?? server.value?.loader ?? '', })), isServer: true, isApp: false, + showModpackVersionActions: computed(() => modpack.value?.spec.platform === 'modrinth'), lockPlatform: true, hideLoaderVersion: true, diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts index 0a40f11f73..5c13800371 100644 --- a/packages/api-client/src/modules/archon/types.ts +++ b/packages/api-client/src/modules/archon/types.ts @@ -70,12 +70,21 @@ export namespace Archon { | 'purpur' | 'vanilla' - export type ModpackSpec = { + export type ModpackSpecModrinth = { platform: 'modrinth' project_id: string version_id: string } + export type ModpackSpecLocalFile = { + platform: 'local_file' + filename: string + name: string + description: string | null + } + + export type ModpackSpec = ModpackSpecModrinth | ModpackSpecLocalFile + export type ModpackOwner = { id: string name: string diff --git a/packages/ui/src/components/project/server/ServerModpackContentCard.vue b/packages/ui/src/components/project/server/ServerModpackContentCard.vue index ef66a91567..bc29556cc8 100644 --- a/packages/ui/src/components/project/server/ServerModpackContentCard.vue +++ b/packages/ui/src/components/project/server/ServerModpackContentCard.vue @@ -1,7 +1,7 @@