From d324768208518ccb693ae404b575fdc199d3e464 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 16 Jan 2026 09:47:13 +0000 Subject: [PATCH 01/59] feat: base content card component --- .../src/components/instances/ContentCard.vue | 118 ++++ packages/ui/src/components/instances/index.ts | 20 +- .../stories/instances/ContentCard.stories.ts | 666 ++++++++++++++++++ 3 files changed, 786 insertions(+), 18 deletions(-) create mode 100644 packages/ui/src/components/instances/ContentCard.vue create mode 100644 packages/ui/src/stories/instances/ContentCard.stories.ts diff --git a/packages/ui/src/components/instances/ContentCard.vue b/packages/ui/src/components/instances/ContentCard.vue new file mode 100644 index 0000000000..ac377d9eb1 --- /dev/null +++ b/packages/ui/src/components/instances/ContentCard.vue @@ -0,0 +1,118 @@ + + + diff --git a/packages/ui/src/components/instances/index.ts b/packages/ui/src/components/instances/index.ts index fd997a6865..7d9dd736a3 100644 --- a/packages/ui/src/components/instances/index.ts +++ b/packages/ui/src/components/instances/index.ts @@ -1,18 +1,2 @@ -export { default as ContentCardItem } from './ContentCardItem.vue' -export { default as ContentCardTable } from './ContentCardTable.vue' -/** - * @deprecated Use `ContentCardTable` with `ContentCardItem` instead. - * This alias is kept for backwards compatibility and will be removed in a future version. - */ -export { default as ContentCard } from './ContentCardItem.vue' -export { default as ContentModpackCard } from './ContentModpackCard.vue' -// export { default as ContentUpdaterModal } from './modals/ContentUpdaterModal.vue' -export type { - ContentCardProject, - ContentCardTableItem, - ContentCardVersion, - ContentModpackCardCategory, - ContentModpackCardProject, - ContentModpackCardVersion, - ContentOwner, -} from './types' +export type { ContentCardOwner, ContentCardProject, ContentCardVersion } from './ContentCard.vue' +export { default as ContentCard } from './ContentCard.vue' diff --git a/packages/ui/src/stories/instances/ContentCard.stories.ts b/packages/ui/src/stories/instances/ContentCard.stories.ts new file mode 100644 index 0000000000..2188053ece --- /dev/null +++ b/packages/ui/src/stories/instances/ContentCard.stories.ts @@ -0,0 +1,666 @@ +import { EditIcon, EyeIcon, FolderOpenIcon, LinkIcon } from '@modrinth/assets' +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { fn } from 'storybook/test' +import { ref } from 'vue' + +import ButtonStyled from '../../components/base/ButtonStyled.vue' +import type { + ContentCardOwner, + ContentCardProject, + ContentCardVersion, +} from '../../components/instances/ContentCard.vue' +import ContentCard from '../../components/instances/ContentCard.vue' + +// Real project data from Modrinth API +const sodiumProject: ContentCardProject = { + id: 'AANobbMI', + slug: 'sodium', + title: 'Sodium', + icon_url: + 'https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp', +} + +const modMenuProject: ContentCardProject = { + id: 'mOgUt4GM', + slug: 'modmenu', + title: 'Mod Menu', + icon_url: 'https://cdn.modrinth.com/data/mOgUt4GM/5a20ed1450a0e1e79a1fe04e61bb4e5878bf1d20.png', +} + +const fabricApiProject: ContentCardProject = { + id: 'P7dR8mSH', + slug: 'fabric-api', + title: 'Fabric API', + icon_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png', +} + +// Version data +const sodiumVersion: ContentCardVersion = { + id: '59wygFUQ', + version_number: 'mc1.21.11-0.8.2-fabric', + file_name: 'sodium-fabric-0.8.2+mc1.21.11.jar', +} + +const modMenuVersion: ContentCardVersion = { + id: 'QuU0ciaR', + version_number: '16.0.0', + file_name: 'modmenu-16.0.0.jar', +} + +const fabricApiVersion: ContentCardVersion = { + id: 'Lwa1Q6e4', + version_number: '0.141.3+26.1', + file_name: 'fabric-api-0.141.3+26.1.jar', +} + +// Owner data +const sodiumOwner: ContentCardOwner = { + id: 'DzLrfrbK', + name: 'IMS', + avatar_url: 'https://avatars3.githubusercontent.com/u/31803019?v=4', + type: 'user', +} + +const fabricApiOwner: ContentCardOwner = { + id: 'BZoBsPo6', + name: 'FabricMC', + avatar_url: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png', + type: 'organization', +} + +const meta = { + title: 'Instances/ContentCard', + component: ContentCard, + parameters: { + layout: 'padded', + }, + argTypes: { + project: { + control: 'object', + description: 'Project information (id, slug, title, icon_url)', + }, + version: { + control: 'object', + description: 'Version information (id, version_number, file_name)', + }, + owner: { + control: 'object', + description: 'Owner/author information', + }, + enabled: { + control: 'boolean', + description: 'Toggle state - toggle hidden if undefined', + }, + disabled: { + control: 'boolean', + description: 'Grays out the card when true', + }, + overflowOptions: { + control: 'object', + description: 'Options for the overflow menu', + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +// ============================================ +// All Types Overview +// ============================================ + +export const AllTypes: Story = { + args: { + project: sodiumProject, + }, + render: () => ({ + components: { ContentCard }, + setup() { + const toggleOn = ref(true) + const toggleOff = ref(false) + + const cards = [ + { + label: 'Full featured (all actions)', + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + enabled: toggleOn, + hasUpdate: true, + hasDelete: true, + hasOverflow: true, + }, + { + label: 'With toggle only', + project: modMenuProject, + version: modMenuVersion, + owner: { id: 'u2', name: 'Prospector', type: 'user' }, + enabled: toggleOn, + }, + { + label: 'With update available', + project: fabricApiProject, + version: fabricApiVersion, + owner: fabricApiOwner, + hasUpdate: true, + }, + { + label: 'Minimal (project only)', + project: sodiumProject, + }, + { + label: 'With version info only', + project: modMenuProject, + version: modMenuVersion, + }, + { + label: 'With owner only', + project: fabricApiProject, + owner: fabricApiOwner, + }, + { + label: 'Disabled state', + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + enabled: toggleOff, + disabled: true, + }, + { + label: 'Delete button only', + project: modMenuProject, + version: modMenuVersion, + hasDelete: true, + }, + { + label: 'Toggle off', + project: fabricApiProject, + version: fabricApiVersion, + owner: fabricApiOwner, + enabled: toggleOff, + }, + ] + + return { cards } + }, + template: /*html*/ ` +
+ +
+ `, + }), +} + +// ============================================ +// Basic Stories +// ============================================ + +export const Default: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + enabled: true, + overflowOptions: [ + { id: 'view', action: () => console.log('View clicked') }, + { id: 'edit', action: () => console.log('Edit clicked') }, + { divider: true }, + { id: 'remove', action: () => console.log('Remove clicked'), color: 'red' }, + ], + onDelete: fn(), + onUpdate: fn(), + 'onUpdate:enabled': fn(), + }, +} + +export const MinimalProjectOnly: Story = { + args: { + project: sodiumProject, + }, +} + +export const WithVersion: Story = { + args: { + project: modMenuProject, + version: modMenuVersion, + }, +} + +export const WithOwner: Story = { + args: { + project: fabricApiProject, + owner: fabricApiOwner, + }, +} + +export const WithToggle: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + enabled: true, + 'onUpdate:enabled': fn(), + }, +} + +export const ToggleDisabled: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + enabled: false, + 'onUpdate:enabled': fn(), + }, +} + +// ============================================ +// Action Button Stories +// ============================================ + +export const WithDeleteButton: Story = { + args: { + project: modMenuProject, + version: modMenuVersion, + owner: sodiumOwner, + onDelete: fn(), + }, +} + +export const WithUpdateButton: Story = { + args: { + project: fabricApiProject, + version: fabricApiVersion, + owner: fabricApiOwner, + onUpdate: fn(), + }, +} + +export const WithAllActions: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + enabled: true, + onDelete: fn(), + onUpdate: fn(), + 'onUpdate:enabled': fn(), + overflowOptions: [ + { id: 'view', action: () => console.log('View') }, + { id: 'openFolder', action: () => console.log('Open folder') }, + { divider: true }, + { id: 'copyLink', action: () => console.log('Copy link') }, + ], + }, +} + +// ============================================ +// State Stories +// ============================================ + +export const Disabled: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + enabled: false, + disabled: true, + 'onUpdate:enabled': fn(), + }, +} + +export const LongProjectName: Story = { + args: { + project: { + id: 'test123', + slug: 'very-long-project-name', + title: '[EMF] Entity Model Features - The Ultimate Entity Rendering Mod', + icon_url: sodiumProject.icon_url, + }, + version: { + id: 'v1', + version_number: '2.4.1', + file_name: 'Entity_model_features_fabric_1.21.1-2.4.1.jar', + }, + owner: { + id: 'u1', + name: 'Traben', + type: 'user', + }, + enabled: true, + onDelete: fn(), + 'onUpdate:enabled': fn(), + }, +} + +// ============================================ +// Overflow Menu Stories +// ============================================ + +export const WithOverflowMenu: Story = { + render: (args) => ({ + components: { ContentCard, EditIcon, EyeIcon, FolderOpenIcon, LinkIcon }, + setup() { + return { args } + }, + template: /*html*/ ` + + + + + + + `, + }), + args: { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + overflowOptions: [ + { id: 'view', action: () => console.log('View') }, + { id: 'edit', action: () => console.log('Edit') }, + { id: 'openFolder', action: () => console.log('Open folder') }, + { divider: true }, + { id: 'copyLink', action: () => console.log('Copy link') }, + ], + }, +} + +// ============================================ +// Slot Stories +// ============================================ + +export const WithAdditionalButtons: Story = { + render: (args) => ({ + components: { ContentCard, ButtonStyled, EyeIcon, FolderOpenIcon }, + setup() { + return { args } + }, + template: /*html*/ ` + + + + + `, + }), + args: { + project: modMenuProject, + version: modMenuVersion, + enabled: true, + onDelete: fn(), + 'onUpdate:enabled': fn(), + }, +} + +// ============================================ +// Interactive Stories +// ============================================ + +export const InteractiveToggle: Story = { + args: { + project: sodiumProject, + }, + render: () => ({ + components: { ContentCard }, + setup() { + const enabled = ref(true) + return { + enabled, + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + } + }, + template: /*html*/ ` +
+ +
+ Mod is currently: {{ enabled ? 'Enabled' : 'Disabled' }} +
+
+ `, + }), +} + +// ============================================ +// List Stories +// ============================================ + +export const ModList: Story = { + args: { + project: sodiumProject, + }, + render: () => ({ + components: { ContentCard }, + setup() { + const mods = ref([ + { project: sodiumProject, version: sodiumVersion, owner: sodiumOwner, enabled: true }, + { + project: modMenuProject, + version: modMenuVersion, + owner: { id: 'u2', name: 'Prospector', type: 'user' as const }, + enabled: true, + }, + { + project: fabricApiProject, + version: fabricApiVersion, + owner: fabricApiOwner, + enabled: true, + }, + ]) + + const handleDelete = (index: number) => { + mods.value.splice(index, 1) + } + + const handleToggle = (index: number, value: boolean) => { + mods.value[index].enabled = value + } + + return { mods, handleDelete, handleToggle } + }, + template: /*html*/ ` +
+ + + + +
+ `, + }), +} + +export const MixedStates: Story = { + args: { + project: sodiumProject, + }, + render: () => ({ + components: { ContentCard }, + setup() { + return { + sodiumProject, + sodiumVersion, + sodiumOwner, + modMenuProject, + modMenuVersion, + fabricApiProject, + fabricApiVersion, + fabricApiOwner, + } + }, + template: /*html*/ ` +
+ + + + + + + + +
+ `, + }), +} + +// ============================================ +// Responsive Stories +// ============================================ + +export const ResponsiveView: Story = { + args: { + project: sodiumProject, + }, + render: () => ({ + components: { ContentCard }, + setup() { + return { + project: sodiumProject, + version: sodiumVersion, + owner: sodiumOwner, + } + }, + template: /*html*/ ` +
+
+

Desktop (version info visible)

+
+ +
+
+
+

Mobile (<768px - version info hidden)

+
+ +
+
+
+ `, + }), +} + +// ============================================ +// Edge Cases +// ============================================ + +export const NoIcon: Story = { + args: { + project: { + id: 'test', + slug: 'no-icon-mod', + title: 'Mod Without Icon', + icon_url: undefined, + }, + version: { + id: 'v1', + version_number: '1.0.0', + file_name: 'no-icon-mod-1.0.0.jar', + }, + enabled: true, + 'onUpdate:enabled': fn(), + }, +} + +export const NoOwnerAvatar: Story = { + args: { + project: sodiumProject, + version: sodiumVersion, + owner: { + id: 'u1', + name: 'Anonymous', + avatar_url: undefined, + type: 'user', + }, + enabled: true, + 'onUpdate:enabled': fn(), + }, +} From b60127ff5dd864b118cd986e5ee16135bf7c3023 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 16 Jan 2026 09:57:56 +0000 Subject: [PATCH 02/59] fix: tooltips + colors --- .../ui/src/components/instances/ContentCard.vue | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/instances/ContentCard.vue b/packages/ui/src/components/instances/ContentCard.vue index ac377d9eb1..8c275e045b 100644 --- a/packages/ui/src/components/instances/ContentCard.vue +++ b/packages/ui/src/components/instances/ContentCard.vue @@ -85,12 +85,14 @@ const hasUpdateListener = computed(() => !!instance?.vnode.props?.onUpdate) - @@ -100,9 +102,9 @@ const hasUpdateListener = computed(() => !!instance?.vnode.props?.onUpdate) @update:model-value="(val) => emit('update:enabled', val)" /> - - From fd23751bc378f1e1fedcff7c599140eb9fe81700 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 16 Jan 2026 10:06:56 +0000 Subject: [PATCH 03/59] feat: fix orgs --- packages/ui/src/components/instances/ContentCard.vue | 11 +++++++++-- .../ui/src/stories/instances/ContentCard.stories.ts | 11 ++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/instances/ContentCard.vue b/packages/ui/src/components/instances/ContentCard.vue index 8c275e045b..9a2823079a 100644 --- a/packages/ui/src/components/instances/ContentCard.vue +++ b/packages/ui/src/components/instances/ContentCard.vue @@ -1,6 +1,6 @@ + + diff --git a/apps/frontend/src/pages/hosting/manage/[id]/content/index.vue b/apps/frontend/src/pages/hosting/manage/[id]/content/index.vue deleted file mode 100644 index c773d69686..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/content/index.vue +++ /dev/null @@ -1,706 +0,0 @@ - - - - - diff --git a/packages/api-client/src/modules/index.ts b/packages/api-client/src/modules/index.ts index 82880a682d..7827bbcf4f 100644 --- a/packages/api-client/src/modules/index.ts +++ b/packages/api-client/src/modules/index.ts @@ -7,7 +7,7 @@ import { ArchonServersV0Module } from './archon/servers/v0' import { ArchonServersV1Module } from './archon/servers/v1' import { ISO3166Module } from './iso3166' import { KyrosFilesV0Module } from './kyros/files/v0' -import { LabrinthVersionsV3Module } from './labrinth' +import { LabrinthVersionsV2Module, LabrinthVersionsV3Module } from './labrinth' import { LabrinthBillingInternalModule } from './labrinth/billing/internal' import { LabrinthCollectionsModule } from './labrinth/collections' import { LabrinthProjectsV2Module } from './labrinth/projects/v2' @@ -40,6 +40,7 @@ export const MODULE_REGISTRY = { labrinth_projects_v3: LabrinthProjectsV3Module, labrinth_state: LabrinthStateModule, labrinth_tech_review_internal: LabrinthTechReviewInternalModule, + labrinth_versions_v2: LabrinthVersionsV2Module, labrinth_versions_v3: LabrinthVersionsV3Module, } as const satisfies Record diff --git a/packages/api-client/src/modules/labrinth/index.ts b/packages/api-client/src/modules/labrinth/index.ts index 38bc3f22b6..c11216dfe6 100644 --- a/packages/api-client/src/modules/labrinth/index.ts +++ b/packages/api-client/src/modules/labrinth/index.ts @@ -4,4 +4,5 @@ export * from './projects/v2' export * from './projects/v3' export * from './state' export * from './tech-review/internal' +export * from './versions/v2' export * from './versions/v3' diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index 4c391ef553..5d36f76e6e 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -469,6 +469,12 @@ export namespace Labrinth { game_versions: string[] loaders: string[] } + + export interface GetProjectVersionsParams { + game_versions?: string[] + loaders?: string[] + include_changelog?: boolean + } } // TODO: consolidate duplicated types between v2 and v3 versions @@ -484,7 +490,6 @@ export namespace Labrinth { game_versions?: string[] loaders?: string[] include_changelog?: boolean - apiVersion?: 2 | 3 } export type VersionChannel = 'release' | 'beta' | 'alpha' diff --git a/packages/api-client/src/modules/labrinth/versions/v2.ts b/packages/api-client/src/modules/labrinth/versions/v2.ts new file mode 100644 index 0000000000..750d91d53d --- /dev/null +++ b/packages/api-client/src/modules/labrinth/versions/v2.ts @@ -0,0 +1,135 @@ +import { AbstractModule } from '../../../core/abstract-module' +import type { Labrinth } from '../types' + +export class LabrinthVersionsV2Module extends AbstractModule { + public getModuleID(): string { + return 'labrinth_versions_v2' + } + + /** + * Get versions for a project (v2) + * + * @param id - Project ID or slug (e.g., 'sodium' or 'AANobbMI') + * @param options - Optional query parameters to filter versions + * @returns Promise resolving to an array of v2 versions + * + * @example + * ```typescript + * const versions = await client.labrinth.versions_v2.getProjectVersions('sodium') + * const filteredVersions = await client.labrinth.versions_v2.getProjectVersions('sodium', { + * game_versions: ['1.20.1'], + * loaders: ['fabric'], + * include_changelog: false + * }) + * console.log(versions[0].version_number) + * ``` + */ + public async getProjectVersions( + id: string, + options?: Labrinth.Versions.v2.GetProjectVersionsParams, + ): Promise { + const params: Record = {} + if (options?.game_versions?.length) { + params.game_versions = JSON.stringify(options.game_versions) + } + if (options?.loaders?.length) { + params.loaders = JSON.stringify(options.loaders) + } + if (options?.include_changelog === false) { + params.include_changelog = 'false' + } + + return this.client.request(`/project/${id}/version`, { + api: 'labrinth', + version: 2, + method: 'GET', + params: Object.keys(params).length > 0 ? params : undefined, + }) + } + + /** + * Get a specific version by ID (v2) + * + * @param id - Version ID + * @returns Promise resolving to the v2 version data + * + * @example + * ```typescript + * const version = await client.labrinth.versions_v2.getVersion('DXtmvS8i') + * console.log(version.version_number) + * ``` + */ + public async getVersion(id: string): Promise { + return this.client.request(`/version/${id}`, { + api: 'labrinth', + version: 2, + method: 'GET', + }) + } + + /** + * Get multiple versions by IDs (v2) + * + * @param ids - Array of version IDs + * @returns Promise resolving to an array of v2 versions + * + * @example + * ```typescript + * const versions = await client.labrinth.versions_v2.getVersions(['DXtmvS8i', 'abc123']) + * console.log(versions[0].version_number) + * ``` + */ + public async getVersions(ids: string[]): Promise { + return this.client.request(`/versions`, { + api: 'labrinth', + version: 2, + method: 'GET', + params: { ids: JSON.stringify(ids) }, + }) + } + + /** + * Get a version from a project by version ID or number (v2) + * + * @param projectId - Project ID or slug + * @param versionId - Version ID or version number + * @returns Promise resolving to the v2 version data + * + * @example + * ```typescript + * const version = await client.labrinth.versions_v2.getVersionFromIdOrNumber('sodium', 'DXtmvS8i') + * const versionByNumber = await client.labrinth.versions_v2.getVersionFromIdOrNumber('sodium', '0.4.12') + * ``` + */ + public async getVersionFromIdOrNumber( + projectId: string, + versionId: string, + ): Promise { + return this.client.request( + `/project/${projectId}/version/${versionId}`, + { + api: 'labrinth', + version: 2, + method: 'GET', + }, + ) + } + + /** + * Delete a version by ID (v2) + * + * @param versionId - Version ID + * + * @example + * ```typescript + * await client.labrinth.versions_v2.deleteVersion('DXtmvS8i') + * ``` + */ + public async deleteVersion(versionId: string): Promise { + return this.client.request(`/version/${versionId}`, { + api: 'labrinth', + version: 2, + method: 'DELETE', + }) + } +} diff --git a/packages/api-client/src/modules/labrinth/versions/v3.ts b/packages/api-client/src/modules/labrinth/versions/v3.ts index 1a3a023242..7de277f616 100644 --- a/packages/api-client/src/modules/labrinth/versions/v3.ts +++ b/packages/api-client/src/modules/labrinth/versions/v3.ts @@ -35,8 +35,8 @@ export class LabrinthVersionsV3Module extends AbstractModule { if (options?.loaders?.length) { params.loaders = JSON.stringify(options.loaders) } - if (options?.include_changelog !== undefined) { - params.include_changelog = options.include_changelog + if (options?.include_changelog === false) { + params.include_changelog = 'false' } return this.client.request(`/project/${id}/version`, { diff --git a/packages/api-client/src/platform/nuxt.ts b/packages/api-client/src/platform/nuxt.ts index 15a3613399..b74fd0bd4f 100644 --- a/packages/api-client/src/platform/nuxt.ts +++ b/packages/api-client/src/platform/nuxt.ts @@ -13,27 +13,32 @@ import { XHRUploadClient } from './xhr-upload-client' * * This provides cross-request persistence in SSR while also working in client-side. * State is shared between requests in the same Nuxt context. + * + * Note: useState must be called during initialization (in setup context) and cached, + * as it won't work during async operations when the Nuxt context may be lost. */ export class NuxtCircuitBreakerStorage implements CircuitBreakerStorage { - private getState(): Map { + private state: Map + + constructor() { // @ts-expect-error - useState is provided by Nuxt runtime - const state = useState>( + const stateRef = useState>( 'circuit-breaker-state', () => new Map(), ) - return state.value + this.state = stateRef.value } get(key: string): CircuitBreakerState | undefined { - return this.getState().get(key) + return this.state.get(key) } set(key: string, state: CircuitBreakerState): void { - this.getState().set(key, state) + this.state.set(key, state) } clear(key: string): void { - this.getState().delete(key) + this.state.delete(key) } } diff --git a/apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue b/packages/ui/src/components/servers/content/ContentVersionEditModal.vue similarity index 86% rename from apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue rename to packages/ui/src/components/servers/content/ContentVersionEditModal.vue index 0bfe2ba11a..d1a8d3bc7a 100644 --- a/apps/frontend/src/components/ui/servers/ContentVersionEditModal.vue +++ b/packages/ui/src/components/servers/content/ContentVersionEditModal.vue @@ -27,13 +27,13 @@
{{ type }} version
- - +
diff --git a/apps/frontend/src/components/ui/servers/ContentVersionFilter.vue b/packages/ui/src/components/servers/content/ContentVersionFilter.vue similarity index 84% rename from apps/frontend/src/components/ui/servers/ContentVersionFilter.vue rename to packages/ui/src/components/servers/content/ContentVersionFilter.vue index 4e2a503a1a..3ae2a72ab8 100644 --- a/apps/frontend/src/components/ui/servers/ContentVersionFilter.vue +++ b/packages/ui/src/components/servers/content/ContentVersionFilter.vue @@ -57,12 +57,13 @@ - - diff --git a/packages/ui/src/pages/hosting/manage/content.vue b/packages/ui/src/pages/hosting/manage/content.vue new file mode 100644 index 0000000000..7905404cf0 --- /dev/null +++ b/packages/ui/src/pages/hosting/manage/content.vue @@ -0,0 +1,830 @@ + + + + + diff --git a/packages/ui/src/pages/index.ts b/packages/ui/src/pages/index.ts index 528a1761c5..e6ce8a51d9 100644 --- a/packages/ui/src/pages/index.ts +++ b/packages/ui/src/pages/index.ts @@ -1,3 +1,4 @@ export { default as ServersManageBackupsPage } from './hosting/manage/backups.vue' +export { default as ServersManageContentPage } from './hosting/manage/content.vue' export { default as ServersManageFilesPage } from './hosting/manage/files.vue' export { default as ServersManagePageIndex } from './hosting/manage/index.vue' diff --git a/packages/ui/src/utils/auto-icons.ts b/packages/ui/src/utils/auto-icons.ts index cdcb76df9f..102b559de1 100644 --- a/packages/ui/src/utils/auto-icons.ts +++ b/packages/ui/src/utils/auto-icons.ts @@ -32,6 +32,13 @@ import { import type { ProjectStatus, ProjectType } from '@modrinth/utils' import type { Component } from 'vue' +import { + FILE_ARCHIVE_EXTENSIONS, + FILE_CODE_EXTENSIONS, + FILE_IMAGE_EXTENSIONS, + FILE_TEXT_EXTENSIONS, +} from './file-extensions' + export const PROJECT_TYPE_ICONS: Record = { mod: BoxIcon, modpack: PackageOpenIcon, @@ -88,53 +95,6 @@ const BLOCKCHAIN_CONFIG: Record = { polygon: { icon: PolygonIcon, color: 'text-purple' }, } -export const CODE_EXTENSIONS: readonly string[] = [ - 'json', - 'json5', - 'jsonc', - 'java', - 'kt', - 'kts', - 'sh', - 'bat', - 'ps1', - 'yml', - 'yaml', - 'toml', - 'js', - 'ts', - 'py', - 'rb', - 'php', - 'html', - 'css', - 'cpp', - 'c', - 'h', - 'rs', - 'go', -] as const - -export const TEXT_EXTENSIONS: readonly string[] = [ - 'txt', - 'md', - 'log', - 'cfg', - 'conf', - 'properties', - 'ini', - 'sk', -] as const -export const IMAGE_EXTENSIONS: readonly string[] = [ - 'png', - 'jpg', - 'jpeg', - 'gif', - 'svg', - 'webp', -] as const -const ARCHIVE_EXTENSIONS: string[] = ['zip', 'jar', 'tar', 'gz', 'rar', '7z'] as const - export function getProjectTypeIcon(projectType: ProjectType): Component { return PROJECT_TYPE_ICONS[projectType] ?? BoxIcon } @@ -162,16 +122,16 @@ export function getDirectoryIcon(name: string): Component { export function getFileExtensionIcon(extension: string): Component { const ext: string = extension.toLowerCase() - if (CODE_EXTENSIONS.includes(ext)) { + if ((FILE_CODE_EXTENSIONS as readonly string[]).includes(ext)) { return FileCodeIcon } - if (TEXT_EXTENSIONS.includes(ext)) { + if ((FILE_TEXT_EXTENSIONS as readonly string[]).includes(ext)) { return FileTextIcon } - if (IMAGE_EXTENSIONS.includes(ext)) { + if ((FILE_IMAGE_EXTENSIONS as readonly string[]).includes(ext)) { return FileImageIcon } - if (ARCHIVE_EXTENSIONS.includes(ext)) { + if ((FILE_ARCHIVE_EXTENSIONS as readonly string[]).includes(ext)) { return FileArchiveIcon } diff --git a/packages/ui/src/utils/file-extensions.ts b/packages/ui/src/utils/file-extensions.ts index 38f11b974c..7908f52b06 100644 --- a/packages/ui/src/utils/file-extensions.ts +++ b/packages/ui/src/utils/file-extensions.ts @@ -1,5 +1,5 @@ // File extension constants -export const CODE_EXTENSIONS = [ +export const FILE_CODE_EXTENSIONS = [ 'json', 'json5', 'jsonc', @@ -26,7 +26,7 @@ export const CODE_EXTENSIONS = [ 'go', ] as const -export const TEXT_EXTENSIONS = [ +export const FILE_TEXT_EXTENSIONS = [ 'txt', 'md', 'log', @@ -37,15 +37,15 @@ export const TEXT_EXTENSIONS = [ 'sk', ] as const -export const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'] as const +export const FILE_IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'] as const -export const ARCHIVE_EXTENSIONS = ['zip'] as const +export const FILE_ARCHIVE_EXTENSIONS = ['zip', 'jar', 'tar', 'gz', 'rar', '7z'] as const // Type for extension strings -export type CodeExtension = (typeof CODE_EXTENSIONS)[number] -export type TextExtension = (typeof TEXT_EXTENSIONS)[number] -export type ImageExtension = (typeof IMAGE_EXTENSIONS)[number] -export type ArchiveExtension = (typeof ARCHIVE_EXTENSIONS)[number] +export type CodeExtension = (typeof FILE_CODE_EXTENSIONS)[number] +export type TextExtension = (typeof FILE_TEXT_EXTENSIONS)[number] +export type ImageExtension = (typeof FILE_IMAGE_EXTENSIONS)[number] +export type ArchiveExtension = (typeof FILE_ARCHIVE_EXTENSIONS)[number] /** * Extract file extension from filename (lowercase) @@ -58,28 +58,28 @@ export function getFileExtension(filename: string): string { * Check if extension is a code file */ export function isCodeFile(ext: string): boolean { - return (CODE_EXTENSIONS as readonly string[]).includes(ext.toLowerCase()) + return (FILE_CODE_EXTENSIONS as readonly string[]).includes(ext.toLowerCase()) } /** * Check if extension is a text file */ export function isTextFile(ext: string): boolean { - return (TEXT_EXTENSIONS as readonly string[]).includes(ext.toLowerCase()) + return (FILE_TEXT_EXTENSIONS as readonly string[]).includes(ext.toLowerCase()) } /** * Check if extension is an image file */ export function isImageFile(ext: string): boolean { - return (IMAGE_EXTENSIONS as readonly string[]).includes(ext.toLowerCase()) + return (FILE_IMAGE_EXTENSIONS as readonly string[]).includes(ext.toLowerCase()) } /** * Check if extension is an archive file */ export function isArchiveFile(ext: string): boolean { - return (ARCHIVE_EXTENSIONS as readonly string[]).includes(ext.toLowerCase()) + return (FILE_ARCHIVE_EXTENSIONS as readonly string[]).includes(ext.toLowerCase()) } /** diff --git a/packages/ui/src/utils/formatting.ts b/packages/ui/src/utils/formatting.ts new file mode 100644 index 0000000000..45512ea6b0 --- /dev/null +++ b/packages/ui/src/utils/formatting.ts @@ -0,0 +1,212 @@ +import type { Labrinth } from '@modrinth/api-client' + +export function capitalizeString(name: string) { + return name ? name.charAt(0).toUpperCase() + name.slice(1) : name +} + +export function formatCategory(name: string) { + if (name === 'modloader') return "Risugami's ModLoader" + if (name === 'bungeecord') return 'BungeeCord' + if (name === 'liteloader') return 'LiteLoader' + if (name === 'neoforge') return 'NeoForge' + if (name === 'game-mechanics') return 'Game Mechanics' + if (name === 'worldgen') return 'World Generation' + if (name === 'core-shaders') return 'Core Shaders' + if (name === 'gui') return 'GUI' + if (name === '8x-') return '8x or lower' + if (name === '512x+') return '512x or higher' + if (name === 'kitchen-sink') return 'Kitchen Sink' + if (name === 'path-tracing') return 'Path Tracing' + if (name === 'pbr') return 'PBR' + if (name === 'datapack') return 'Data Pack' + if (name === 'colored-lighting') return 'Colored Lighting' + if (name === 'optifine') return 'OptiFine' + if (name === 'bta-babric') return 'BTA (Babric)' + if (name === 'legacy-fabric') return 'Legacy Fabric' + if (name === 'java-agent') return 'Java Agent' + if (name === 'nilloader') return 'NilLoader' + if (name === 'mrpack') return 'Modpack' + if (name === 'minecraft') return 'Resource Pack' + if (name === 'vanilla') return 'Vanilla Shader' + if (name === 'geyser') return 'Geyser Extension' + return capitalizeString(name) +} + +const mcVersionRegex = /^([0-9]+.[0-9]+)(.[0-9]+)?$/ + +type VersionRange = { + major: string + minor: number[] +} + +function groupVersions(versions: string[], consecutive = false) { + return versions + .slice() + .reverse() + .reduce((ranges: VersionRange[], version: string) => { + const matchesVersion = version.match(mcVersionRegex) + + if (matchesVersion) { + const majorVersion = matchesVersion[1] + const minorVersion = matchesVersion[2] + const minorNumeric = minorVersion ? parseInt(minorVersion.replace('.', '')) : 0 + + const prevInRange = ranges.find( + (x) => x.major === majorVersion && (!consecutive || x.minor.at(-1) === minorNumeric - 1), + ) + if (prevInRange) { + prevInRange.minor.push(minorNumeric) + return ranges + } + + return [...ranges, { major: majorVersion, minor: [minorNumeric] }] + } + + return ranges + }, []) + .reverse() +} + +function groupConsecutiveIndices( + versions: string[], + referenceList: Labrinth.Tags.v2.GameVersion[], +) { + if (!versions || versions.length === 0) { + return [] + } + + const referenceMap = new Map() + referenceList.forEach((item, index) => { + referenceMap.set(item.version, index) + }) + + const sortedList: string[] = versions + .slice() + .sort((a, b) => (referenceMap.get(a) ?? 0) - (referenceMap.get(b) ?? 0)) + + const ranges: string[] = [] + let start = sortedList[0] + let previous = sortedList[0] + + for (let i = 1; i < sortedList.length; i++) { + const current = sortedList[i] + if ((referenceMap.get(current) ?? 0) !== (referenceMap.get(previous) ?? 0) + 1) { + ranges.push(validateRange(`${previous}–${start}`)) + start = current + } + previous = current + } + + ranges.push(validateRange(`${previous}–${start}`)) + + return ranges +} + +function validateRange(range: string): string { + switch (range) { + case 'rd-132211–b1.8.1': + return 'All legacy versions' + case 'a1.0.4–b1.8.1': + return 'All alpha and beta versions' + case 'a1.0.4–a1.2.6': + return 'All alpha versions' + case 'b1.0–b1.8.1': + return 'All beta versions' + case 'rd-132211–inf20100618': + return 'All pre-alpha versions' + } + const splitRange = range.split('–') + if (splitRange && splitRange[0] === splitRange[1]) { + return splitRange[0] + } + return range +} + +function formatMinecraftMinorVersion(major: string, minor: number): string { + return minor === 0 ? major : `${major}.${minor}` +} + +export function formatVersionsForDisplay( + gameVersions: string[], + allGameVersions: Labrinth.Tags.v2.GameVersion[], +) { + const inputVersions = gameVersions.slice() + const allVersions = allGameVersions.slice() + + const allSnapshots = allVersions.filter((version) => version.version_type === 'snapshot') + const allReleases = allVersions.filter((version) => version.version_type === 'release') + const allLegacy = allVersions.filter( + (version) => version.version_type !== 'snapshot' && version.version_type !== 'release', + ) + + { + const indices: Record = allVersions.reduce( + (map, gameVersion, index) => { + map[gameVersion.version] = index + return map + }, + {} as Record, + ) + inputVersions.sort((a, b) => indices[a] - indices[b]) + } + + const releaseVersions = inputVersions.filter((projVer) => + allReleases.some((gameVer) => gameVer.version === projVer), + ) + + const dateString = allReleases.find((version) => version.version === releaseVersions[0])?.date + + const latestReleaseVersionDate = dateString ? Date.parse(dateString) : 0 + const latestSnapshot = inputVersions.find((projVer) => + allSnapshots.some( + (gameVer) => + gameVer.version === projVer && + (!latestReleaseVersionDate || latestReleaseVersionDate < Date.parse(gameVer.date)), + ), + ) + + const allReleasesGrouped = groupVersions( + allReleases.map((release) => release.version), + false, + ) + const projectVersionsGrouped = groupVersions(releaseVersions, true) + + const releaseVersionsAsRanges = projectVersionsGrouped.map(({ major, minor }) => { + if (minor.length === 1) { + return formatMinecraftMinorVersion(major, minor[0]) + } + + const range = allReleasesGrouped.find((x) => x.major === major) + + if (range?.minor.every((value, index) => value === minor[index])) { + return `${major}.x` + } + + return `${formatMinecraftMinorVersion(major, minor[0])}–${formatMinecraftMinorVersion(major, minor[minor.length - 1])}` + }) + + const legacyVersionsAsRanges = groupConsecutiveIndices( + inputVersions.filter((projVer) => allLegacy.some((gameVer) => gameVer.version === projVer)), + allLegacy, + ) + + let output = [...legacyVersionsAsRanges] + + // show all snapshots if there's no release versions + if (releaseVersionsAsRanges.length === 0) { + const snapshotVersionsAsRanges = groupConsecutiveIndices( + inputVersions.filter((projVer) => + allSnapshots.some((gameVer) => gameVer.version === projVer), + ), + allSnapshots, + ) + output = [...snapshotVersionsAsRanges, ...output] + } else { + output = [...releaseVersionsAsRanges, ...output] + } + + if (latestSnapshot && !output.includes(latestSnapshot)) { + output = [latestSnapshot, ...output] + } + return output +} diff --git a/packages/ui/src/utils/index.ts b/packages/ui/src/utils/index.ts index 61a2e137a2..b50714b599 100644 --- a/packages/ui/src/utils/index.ts +++ b/packages/ui/src/utils/index.ts @@ -2,6 +2,7 @@ export * from './auto-icons' export * from './common-messages' export * from './events' export * from './file-extensions' +export * from './formatting' export * from './game-modes' export * from './notices' export * from './savable' diff --git a/packages/utils/servers/types/api.ts b/packages/utils/servers/types/api.ts index 8cc6271f10..d589d6034d 100644 --- a/packages/utils/servers/types/api.ts +++ b/packages/utils/servers/types/api.ts @@ -16,4 +16,4 @@ export interface ModuleError { timestamp: number } -export type ModuleName = 'general' | 'content' | 'network' | 'startup' +export type ModuleName = 'general' | 'network' | 'startup' From a27ae0d3e0555940706372772f390ad9c982cf2c Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Sun, 18 Jan 2026 21:01:33 +0000 Subject: [PATCH 05/59] feat: fix invalidmodal --- packages/ui/src/pages/hosting/manage/content.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ui/src/pages/hosting/manage/content.vue b/packages/ui/src/pages/hosting/manage/content.vue index 7905404cf0..573b695be5 100644 --- a/packages/ui/src/pages/hosting/manage/content.vue +++ b/packages/ui/src/pages/hosting/manage/content.vue @@ -491,6 +491,9 @@ const type = computed(() => { // Check if server has a modpack const hasModpack = computed(() => server.value?.upstream?.kind === 'modpack') +// Check if modal cannot be shown (missing required server data) +const invalidModal = computed(() => !server.value?.mc_version || !server.value?.loader) + // Accepted file types for upload const acceptedFileTypes = computed(() => { return type.value.toLowerCase() === 'plugin' ? ['.jar'] : ['.jar', '.zip'] From 513e711646a1850b1f6a4efad09d63c5626e5d50 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 19 Jan 2026 09:32:31 +0000 Subject: [PATCH 06/59] feat: add ContentModpackCard --- .../src/components/instances/ContentCard.vue | 2 +- .../instances/ContentModpackCard.vue | 57 ++++++++------- packages/ui/src/components/instances/index.ts | 7 ++ .../instances/ContentModpackCard.stories.ts | 72 +++++++------------ 4 files changed, 65 insertions(+), 73 deletions(-) diff --git a/packages/ui/src/components/instances/ContentCard.vue b/packages/ui/src/components/instances/ContentCard.vue index 9a2823079a..6389ea9d5a 100644 --- a/packages/ui/src/components/instances/ContentCard.vue +++ b/packages/ui/src/components/instances/ContentCard.vue @@ -106,7 +106,7 @@ const hasUpdateListener = computed(() => !!instance?.vnode.props?.onUpdate) diff --git a/packages/ui/src/components/instances/ContentModpackCard.vue b/packages/ui/src/components/instances/ContentModpackCard.vue index 36373f6202..91f0d16741 100644 --- a/packages/ui/src/components/instances/ContentModpackCard.vue +++ b/packages/ui/src/components/instances/ContentModpackCard.vue @@ -1,4 +1,5 @@ + + diff --git a/packages/ui/src/pages/hosting/manage/content_new.vue b/packages/ui/src/pages/hosting/manage/content_new.vue new file mode 100644 index 0000000000..25b1586d8e --- /dev/null +++ b/packages/ui/src/pages/hosting/manage/content_new.vue @@ -0,0 +1,329 @@ + + + diff --git a/packages/ui/src/pages/index.ts b/packages/ui/src/pages/index.ts index e6ce8a51d9..6fb5fdf83e 100644 --- a/packages/ui/src/pages/index.ts +++ b/packages/ui/src/pages/index.ts @@ -1,4 +1,5 @@ export { default as ServersManageBackupsPage } from './hosting/manage/backups.vue' export { default as ServersManageContentPage } from './hosting/manage/content.vue' +export { default as ServersManageContentNewPage } from './hosting/manage/content_new.vue' export { default as ServersManageFilesPage } from './hosting/manage/files.vue' export { default as ServersManagePageIndex } from './hosting/manage/index.vue' From dd663d63bc8a482e078081c7e109daa7ecaa35fe Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 19 Jan 2026 11:01:31 +0000 Subject: [PATCH 09/59] feat: unlink modal --- .../instances/modals/ModpackUnlinkModal.vue | 63 +++++++++++++++++++ .../src/pages/hosting/manage/content_new.vue | 11 +++- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 packages/ui/src/components/instances/modals/ModpackUnlinkModal.vue diff --git a/packages/ui/src/components/instances/modals/ModpackUnlinkModal.vue b/packages/ui/src/components/instances/modals/ModpackUnlinkModal.vue new file mode 100644 index 0000000000..9c431dcf51 --- /dev/null +++ b/packages/ui/src/components/instances/modals/ModpackUnlinkModal.vue @@ -0,0 +1,63 @@ + + + diff --git a/packages/ui/src/pages/hosting/manage/content_new.vue b/packages/ui/src/pages/hosting/manage/content_new.vue index 25b1586d8e..755f877d9e 100644 --- a/packages/ui/src/pages/hosting/manage/content_new.vue +++ b/packages/ui/src/pages/hosting/manage/content_new.vue @@ -14,6 +14,7 @@ import Combobox, { type ComboboxOption } from '../../../components/base/Combobox import Pagination from '../../../components/base/Pagination.vue' import ContentCard from '../../../components/instances/ContentCard.vue' import ContentModpackCard from '../../../components/instances/ContentModpackCard.vue' +import ModpackUnlinkModal from '../../../components/instances/modals/ModpackUnlinkModal.vue' import type { ContentCardProject, ContentCardVersion, @@ -126,6 +127,8 @@ const sortType = ref('Newest') const currentPage = ref(1) const itemsPerPage = 10 +const modpackUnlinkModal = ref>() + const filterOptions: ComboboxOption[] = [ { value: 'All', label: 'All' }, { value: 'Mods', label: 'Mods' }, @@ -204,7 +207,11 @@ function handleModpackContent() { } function handleModpackUnlink() { - console.log('Modpack unlink') + modpackUnlinkModal.value?.show() +} + +function handleModpackUnlinkConfirm() { + console.log('Modpack unlink confirmed') } function handleBrowseContent() { @@ -325,5 +332,7 @@ function handleUploadFiles() {
+ + From 1beb10b6e9f6c9491b7e411bbf8ace8e8ac56e61 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 19 Jan 2026 11:23:26 +0000 Subject: [PATCH 10/59] feat: impl content tab --- .../src/pages/hosting/manage/[id]/content.vue | 10 +- .../pages/hosting/manage/[id]/content_new.vue | 7 - .../pages/hosting/manage/[id]/content_old.vue | 21 + .../ui/src/pages/hosting/manage/content.vue | 1080 ++++++----------- .../src/pages/hosting/manage/content_new.vue | 338 ------ .../src/pages/hosting/manage/content_old.vue | 833 +++++++++++++ packages/ui/src/pages/index.ts | 2 +- 7 files changed, 1242 insertions(+), 1049 deletions(-) delete mode 100644 apps/frontend/src/pages/hosting/manage/[id]/content_new.vue create mode 100644 apps/frontend/src/pages/hosting/manage/[id]/content_old.vue delete mode 100644 packages/ui/src/pages/hosting/manage/content_new.vue create mode 100644 packages/ui/src/pages/hosting/manage/content_old.vue diff --git a/apps/frontend/src/pages/hosting/manage/[id]/content.vue b/apps/frontend/src/pages/hosting/manage/[id]/content.vue index 2e12aed769..2cf5866be1 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/content.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/content.vue @@ -1,21 +1,13 @@ diff --git a/apps/frontend/src/pages/hosting/manage/[id]/content_new.vue b/apps/frontend/src/pages/hosting/manage/[id]/content_new.vue deleted file mode 100644 index 7953b952c8..0000000000 --- a/apps/frontend/src/pages/hosting/manage/[id]/content_new.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/frontend/src/pages/hosting/manage/[id]/content_old.vue b/apps/frontend/src/pages/hosting/manage/[id]/content_old.vue new file mode 100644 index 0000000000..6e3098125a --- /dev/null +++ b/apps/frontend/src/pages/hosting/manage/[id]/content_old.vue @@ -0,0 +1,21 @@ + + + diff --git a/packages/ui/src/pages/hosting/manage/content.vue b/packages/ui/src/pages/hosting/manage/content.vue index 573b695be5..3a4c547fd0 100644 --- a/packages/ui/src/pages/hosting/manage/content.vue +++ b/packages/ui/src/pages/hosting/manage/content.vue @@ -1,485 +1,44 @@ - - - +
+
+ + + + + + + +
+ + +
+ +
+
+ No content found. +
+ +
+ +
+ +
+ + + + + diff --git a/packages/ui/src/pages/hosting/manage/content_new.vue b/packages/ui/src/pages/hosting/manage/content_new.vue deleted file mode 100644 index 755f877d9e..0000000000 --- a/packages/ui/src/pages/hosting/manage/content_new.vue +++ /dev/null @@ -1,338 +0,0 @@ - - - diff --git a/packages/ui/src/pages/hosting/manage/content_old.vue b/packages/ui/src/pages/hosting/manage/content_old.vue new file mode 100644 index 0000000000..573b695be5 --- /dev/null +++ b/packages/ui/src/pages/hosting/manage/content_old.vue @@ -0,0 +1,833 @@ + + + + + diff --git a/packages/ui/src/pages/index.ts b/packages/ui/src/pages/index.ts index 6fb5fdf83e..75b60b26e9 100644 --- a/packages/ui/src/pages/index.ts +++ b/packages/ui/src/pages/index.ts @@ -1,5 +1,5 @@ export { default as ServersManageBackupsPage } from './hosting/manage/backups.vue' export { default as ServersManageContentPage } from './hosting/manage/content.vue' -export { default as ServersManageContentNewPage } from './hosting/manage/content_new.vue' +export { default as ServersManageContentOldPage } from './hosting/manage/content_old.vue' export { default as ServersManageFilesPage } from './hosting/manage/files.vue' export { default as ServersManagePageIndex } from './hosting/manage/index.vue' From 92c21bc4f4093b195d9e5c3ef8abc37d81d26fe8 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 19 Jan 2026 11:39:48 +0000 Subject: [PATCH 11/59] fix: lint --- packages/ui/src/pages/hosting/manage/content.vue | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/pages/hosting/manage/content.vue b/packages/ui/src/pages/hosting/manage/content.vue index 3a4c547fd0..c9aea2a2f6 100644 --- a/packages/ui/src/pages/hosting/manage/content.vue +++ b/packages/ui/src/pages/hosting/manage/content.vue @@ -33,7 +33,6 @@ import { injectNotificationManager, } from '../../../providers' -// Providers const client = injectModrinthClient() const { server } = injectModrinthServerContext() const { addNotification } = injectNotificationManager() @@ -41,30 +40,25 @@ const route = useRoute() const queryClient = useQueryClient() const serverId = route.params.id as string -// Content type based on server loader const type = computed(() => { const loader = server.value?.loader?.toLowerCase() return loader === 'paper' || loader === 'purpur' ? 'Plugin' : 'Mod' }) -// Check if server has a modpack const hasModpack = computed(() => server.value?.upstream?.kind === 'modpack') -// Fetch modpack project details from Labrinth const { data: modpackProject } = useQuery({ queryKey: computed(() => ['project', server.value?.upstream?.project_id]), queryFn: () => client.labrinth.projects_v3.get(server.value!.upstream!.project_id), enabled: hasModpack, }) -// Fetch modpack version details from Labrinth const { data: modpackVersion } = useQuery({ queryKey: computed(() => ['version', server.value?.upstream?.version_id]), - queryFn: () => client.labrinth.versions_v3.get(server.value!.upstream!.version_id), + queryFn: () => client.labrinth.versions_v3.getVersion(server.value!.upstream!.version_id), enabled: hasModpack, }) -// Computed modpack data for ContentModpackCard const modpack = computed(() => { if (!hasModpack.value || !modpackProject.value) return null @@ -170,7 +164,7 @@ const sortType = ref('Newest') const currentPage = ref(1) const itemsPerPage = 10 -const modpackUnlinkModal = ref>() +const _modpackUnlinkModal = ref>() const filterOptions: ComboboxOption[] = [ { value: 'All', label: 'All' }, @@ -271,7 +265,7 @@ const toggleMutation = useMutation({ }) // Update mutation -const updateMutation = useMutation({ +const _updateMutation = useMutation({ mutationFn: ({ replace, project_id, From 556bdc85fef3d0ad7f96004db6475fcaf01e34e6 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 19 Jan 2026 12:21:05 +0000 Subject: [PATCH 12/59] fix: toggling --- .../ui/src/pages/hosting/manage/content.vue | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/ui/src/pages/hosting/manage/content.vue b/packages/ui/src/pages/hosting/manage/content.vue index c9aea2a2f6..3a5a830f94 100644 --- a/packages/ui/src/pages/hosting/manage/content.vue +++ b/packages/ui/src/pages/hosting/manage/content.vue @@ -223,7 +223,6 @@ function handleSearch() { currentPage.value = 1 } -// Delete mutation const deleteMutation = useMutation({ mutationFn: ({ path }: { path: string; modKey: string }) => client.archon.content_v0.delete(serverId, { path }), @@ -239,20 +238,31 @@ const deleteMutation = useMutation({ }, }) -// Toggle mutation (uses files API to rename) const toggleMutation = useMutation({ - mutationFn: async ({ mod }: { mod: Archon.Content.v0.Mod; modKey: string }) => { - const newFilename = mod.filename.endsWith('.disabled') - ? mod.filename.slice(0, -9) - : `${mod.filename}.disabled` + mutationFn: async ({ mod, modKey }: { mod: Archon.Content.v0.Mod; modKey: string }) => { const folder = `${type.value.toLowerCase()}s` + const currentFilename = mod.disabled ? `${mod.filename}.disabled` : mod.filename + const newFilename = mod.disabled ? mod.filename : `${mod.filename}.disabled` await client.kyros.files_v0.moveFileOrFolder( - `/${folder}/${mod.filename}`, + `/${folder}/${currentFilename}`, `/${folder}/${newFilename}`, ) - return { newFilename } + return { newDisabled: !mod.disabled, modKey } }, - onSuccess: () => { + onSuccess: ({ newDisabled, modKey }) => { + // Optimistically update the local cache immediately + // Archon may take time to sync after Kyros renames the file + queryClient.setQueryData( + contentQueryKey.value, + (oldData: Archon.Content.v0.Mod[] | undefined) => { + if (!oldData) return oldData + return oldData.map((m) => + getStableModKey(m) === modKey ? { ...m, disabled: newDisabled } : m, + ) + }, + ) + changingMods.value.delete(modKey) + // Also invalidate to eventually get the real server state queryClient.invalidateQueries({ queryKey: contentQueryKey.value }) }, onError: (_err, { mod, modKey }) => { @@ -297,14 +307,7 @@ function handleToggleEnabled(item: ContentItem, _value: boolean) { const modKey = getStableModKey(mod) changingMods.value.add(modKey) - toggleMutation.mutate( - { mod, modKey }, - { - onSettled: () => { - changingMods.value.delete(modKey) - }, - }, - ) + toggleMutation.mutate({ mod, modKey }) } function handleDelete(item: ContentItem) { From 9db7dfc59d1c2326ddaf1bd6cc6c048a544b3442 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 19 Jan 2026 15:19:15 +0000 Subject: [PATCH 13/59] temp: disable updating stuff --- .../ui/src/pages/hosting/manage/content.vue | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/ui/src/pages/hosting/manage/content.vue b/packages/ui/src/pages/hosting/manage/content.vue index 3a5a830f94..38ae927c3d 100644 --- a/packages/ui/src/pages/hosting/manage/content.vue +++ b/packages/ui/src/pages/hosting/manage/content.vue @@ -325,24 +325,26 @@ function handleDelete(item: ContentItem) { ) } -function handleUpdate(item: ContentItem) { - const mod = item._mod - if (!mod.project_id || !mod.version_id) { - addNotification({ - type: 'error', - text: 'Cannot update content without project information', - }) - return - } - - // TODO: Implement version selection modal or auto-update to latest - console.log('Update:', item.project.title) -} - -function handleModpackUpdate() { - // TODO: Implement modpack update (needs version selection) - console.log('Modpack update') -} +// TODO: implement update checking +// function handleUpdate(item: ContentItem) { +// const mod = item._mod +// if (!mod.project_id || !mod.version_id) { +// addNotification({ +// type: 'error', +// text: 'Cannot update content without project information', +// }) +// return +// } +// +// // TODO: Implement version selection modal or auto-update to latest +// console.log('Update:', item.project.title) +// } + +// TODO: implement modpack update +// function handleModpackUpdate() { +// // TODO: Implement modpack update (needs version selection) +// console.log('Modpack update') +// } function handleModpackContent() { // Navigate to modpack project page @@ -405,17 +407,16 @@ function handleUploadFiles() { diff --git a/packages/ui/src/components/modal/index.ts b/packages/ui/src/components/modal/index.ts index 89c4cf0266..eef4f9904c 100644 --- a/packages/ui/src/components/modal/index.ts +++ b/packages/ui/src/components/modal/index.ts @@ -1,6 +1,7 @@ export { default as ConfirmModal } from './ConfirmModal.vue' +export { default as InstallToPlayModal } from './InstallToPlayModal.vue' export { default as Modal } from './Modal.vue' export { default as NewModal } from './NewModal.vue' export { default as ShareModal } from './ShareModal.vue' -export type { Tab as TabbedModalTab } from './TabbedModal.vue' export { default as TabbedModal } from './TabbedModal.vue' +export type { Tab as TabbedModalTab } from './TabbedModal.vue' From e7d2f6d719af2f35f689b7e43034c6fbbf255656 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Tue, 20 Jan 2026 17:29:50 +0000 Subject: [PATCH 21/59] fix: events --- packages/ui/src/components/instances/ContentCard.vue | 4 ++-- packages/ui/src/components/instances/ContentModpackCard.vue | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/instances/ContentCard.vue b/packages/ui/src/components/instances/ContentCard.vue index 7697c1f17a..4b7d90d522 100644 --- a/packages/ui/src/components/instances/ContentCard.vue +++ b/packages/ui/src/components/instances/ContentCard.vue @@ -36,8 +36,8 @@ const emit = defineEmits<{ const instance = getCurrentInstance() const hasSelection = computed(() => !!instance?.vnode.props?.['onUpdate:selected']) -const hasDeleteListener = computed(() => !!instance?.vnode.props?.onDelete) -const hasUpdateListener = computed(() => !!instance?.vnode.props?.onUpdate) +const hasDeleteListener = computed(() => typeof instance?.vnode.props?.onDelete === 'function') +const hasUpdateListener = computed(() => typeof instance?.vnode.props?.onUpdate === 'function')