Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 77 additions & 15 deletions apps/app-frontend/src/providers/setup/server-install-content.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Archon, Labrinth } from '@modrinth/api-client'
import {
addPendingServerContentInstalls,
type BrowseInstallPlan,
type BrowseSelectedProject,
createContext,
Expand All @@ -11,11 +12,12 @@ import {
injectModrinthClient,
injectNotificationManager,
type ModpackSearchResult,
type PendingServerContentInstall,
type PendingServerContentInstallType,
readStoredServerInstallQueue,
removePendingServerContentInstall,
resolveServerAddonInstallPlans,
useServerContextRuntime,
useServerPanelSync,
waitForServerContextRuntimeReady,
writePendingServerContentInstallBaseline,
writeStoredServerInstallQueue,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
Expand All @@ -28,6 +30,7 @@ type InstallableSearchResult = Labrinth.Search.v3.ResultSearchProject & {
installing?: boolean
installed?: boolean
}
type PendingServerContentInstallInput = Omit<PendingServerContentInstall, 'createdAt'>

export interface ServerModpackSelectionRequest {
projectId: string
Expand Down Expand Up @@ -93,6 +96,61 @@ function readQueryString(value: unknown): string | null {
return typeof value === 'string' && value.length > 0 ? value : null
}

function getQueuedInstallOwnerFallback(project: InstallableSearchResult) {
if (project.organization) {
const ownerId = project.organization_id ?? project.organization
return {
id: ownerId,
name: project.organization,
type: 'organization' as const,
link: `https://modrinth.com/organization/${ownerId}`,
}
}

if (!project.author) return null

const ownerId = project.author_id ?? project.author
return {
id: ownerId,
name: project.author,
type: 'user' as const,
link: `/user/${encodeURIComponent(ownerId)}`,
}
}

function getQueuedAddonInstallPlans(
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
) {
return Array.from(plans.values()).filter((plan) => plan.contentType !== 'modpack')
}

function getQueuedInstallPlaceholder(
plan: BrowseInstallPlan<InstallableSearchResult>,
owner: PendingServerContentInstallInput['owner'],
): PendingServerContentInstallInput {
const project = plan.project as InstallableSearchResult & { slug?: string | null }
return {
projectId: plan.projectId,
versionId: plan.versionId,
contentType: plan.contentType as PendingServerContentInstallType,
title: project.name ?? 'Project',
versionName: plan.versionName ?? null,
versionNumber: plan.versionNumber ?? null,
fileName: plan.fileName ?? null,
owner,
slug: project.slug ?? plan.projectId,
iconUrl: project.icon_url ?? null,
}
}

function getQueuedInstallPlaceholderFallbacks(
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
) {
return getQueuedAddonInstallPlans(plans).map((plan) =>
getQueuedInstallPlaceholder(plan, getQueuedInstallOwnerFallback(plan.project)),
)
}

export function createServerInstallContent(opts: {
serverSetupModalRef: Ref<ServerSetupModalHandle | null>
}) {
Expand All @@ -115,7 +173,6 @@ export function createServerInstallContent(opts: {
const isFromWorlds = computed(() => browseFrom.value === 'worlds')
const isServerContext = computed(() => !!serverIdQuery.value)
const isSetupServerContext = computed(() => !!serverIdQuery.value && !!serverFlowFrom.value)
useServerContextRuntime(serverIdQuery)

const serverContextWorldId = ref<string | null>(worldIdQuery.value)

Expand All @@ -142,6 +199,7 @@ export function createServerInstallContent(opts: {
initialServerId ? getCachedServer(initialServerId) : null,
)
const serverContentProjectIds = ref<Set<string>>(new Set())
const serverContentInstallKeys = ref<Set<string>>(new Set())
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<InstallableSearchResult>>>(
new Map(),
)
Expand All @@ -162,10 +220,6 @@ export function createServerInstallContent(opts: {
const isInstallingQueuedServerInstalls = ref(false)
const queuedInstallProgress = ref({ completed: 0, total: 0 })
const effectiveServerWorldId = computed(() => worldIdQuery.value ?? serverContextWorldId.value)
useServerPanelSync({
serverId: serverIdQuery,
worldId: effectiveServerWorldId,
})
const serverBackUrl = computed(() => {
const sid = serverIdQuery.value
if (!sid) return '/hosting/manage'
Expand Down Expand Up @@ -211,7 +265,11 @@ export function createServerInstallContent(opts: {
.map((addon) => addon.project_id)
.filter((projectId): projectId is string => !!projectId),
)
const keys = new Set(
(content.addons ?? []).map((addon) => addon.project_id ?? addon.filename),
)
serverContentProjectIds.value = ids
serverContentInstallKeys.value = keys
} catch (err) {
handleError(err as Error)
}
Expand Down Expand Up @@ -253,12 +311,14 @@ export function createServerInstallContent(opts: {
serverContextWorldId.value = null
serverContextServerData.value = null
serverContentProjectIds.value = new Set()
serverContentInstallKeys.value = new Set()
setQueuedServerInstallPlans(new Map())
return
}

if (sid !== prevSid) {
serverContentProjectIds.value = new Set()
serverContentInstallKeys.value = new Set()
queuedServerInstalls.value = readStoredServerInstallQueue(sid, wid)
serverContextServerData.value = getCachedServer(sid)
try {
Expand Down Expand Up @@ -427,13 +487,6 @@ export function createServerInstallContent(opts: {
const queuedPlans = getStoredServerAddonInstallQueue<InstallableSearchResult>(serverId, worldId)
if (queuedPlans.size === 0) return true

try {
await waitForServerContextRuntimeReady(client, serverId)
} catch (error) {
handleError(error as Error)
return false
}

isInstallingQueuedServerInstalls.value = true
queuedInstallProgress.value = {
completed: 0,
Expand All @@ -454,6 +507,9 @@ export function createServerInstallContent(opts: {
})

if (!result.ok) {
for (const plan of result.attemptedPlans) {
removePendingServerContentInstall(serverId, worldId, plan.projectId)
}
handleError(result.error as Error)
return false
}
Expand All @@ -466,6 +522,10 @@ export function createServerInstallContent(opts: {
...serverContentProjectIds.value,
...result.flushedPlans.map((plan) => plan.projectId),
])
serverContentInstallKeys.value = new Set([
...serverContentInstallKeys.value,
...result.flushedPlans.map((plan) => plan.projectId),
])
if (result.flushedPlans.length > 0) {
await queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] })
}
Expand All @@ -490,6 +550,8 @@ export function createServerInstallContent(opts: {

if (sid && wid) {
writeStoredServerInstallQueue(sid, wid, plans)
writePendingServerContentInstallBaseline(sid, wid, serverContentInstallKeys.value)
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
}
const installed = await flushQueuedServerInstalls(sid, wid)
if (!installed) return false
Expand Down
91 changes: 75 additions & 16 deletions apps/frontend/src/composables/use-server-install-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import type {
CreationFlowContextValue,
EnvironmentSearchOverride,
FilterValue,
PendingServerContentInstall,
PendingServerContentInstallType,
} from '@modrinth/ui'
import {
addPendingServerContentInstalls,
commonMessages,
defineMessages,
flushStoredServerAddonInstallQueue,
Expand All @@ -17,14 +20,13 @@ import {
injectModrinthClient,
injectNotificationManager,
readStoredServerInstallQueue,
removePendingServerContentInstall,
requestInstall,
resolveServerAddonInstallPlans,
stripServerRuntimeInstallFilters,
stripServerRuntimeInstallOverrides,
useServerContextRuntime,
useServerPanelSync,
useVIntl,
waitForServerContextRuntimeReady,
writePendingServerContentInstallBaseline,
writeStoredServerInstallQueue,
} from '@modrinth/ui'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
Expand All @@ -34,6 +36,7 @@ import { computed, nextTick, ref, watch } from 'vue'
import { navigateTo, useRoute } from '#app'
import { queryAsString } from '~/utils/router'

type PendingServerContentInstallInput = Omit<PendingServerContentInstall, 'createdAt'>
type ServerInstallBrowseSearchState = Pick<
BrowseSearchState,
'currentFilters' | 'overriddenProvidedFilterTypes'
Expand Down Expand Up @@ -86,6 +89,34 @@ const messages = defineMessages({
},
})

function getQueuedInstallOwnerFallback(project: ServerInstallSearchResult) {
if (project.organization) {
const ownerId = project.organization_id ?? project.organization
return {
id: ownerId,
name: project.organization,
type: 'organization' as const,
link: `/organization/${ownerId}`,
}
}

if (!project.author) return null

const ownerId = project.author_id ?? project.author
return {
id: ownerId,
name: project.author,
type: 'user' as const,
link: `/user/${ownerId}`,
}
}

function getQueuedAddonInstallPlans(
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
) {
return Array.from(plans.values()).filter((plan) => plan.contentType !== 'modpack')
}

export function useServerInstallContent({
projectType,
onboardingModalRef,
Expand All @@ -106,11 +137,6 @@ export function useServerInstallContent({
const currentServerId = computed(() => queryAsString(route.query.sid) || null)
const fromContext = computed(() => queryAsString(route.query.from) || null)
const currentWorldId = computed(() => queryAsString(route.query.wid) || null)
useServerContextRuntime(currentServerId)
useServerPanelSync({
serverId: currentServerId,
worldId: currentWorldId,
})

const {
data: serverData,
Expand Down Expand Up @@ -199,6 +225,32 @@ export function useServerInstallContent({
writeStoredServerInstallQueue(serverId, worldId, plans)
}

function getQueuedInstallPlaceholder(
plan: BrowseInstallPlan<ServerInstallSearchResult>,
owner: PendingServerContentInstallInput['owner'],
): PendingServerContentInstallInput {
return {
projectId: plan.projectId,
versionId: plan.versionId,
contentType: plan.contentType as PendingServerContentInstallType,
title: getInstallProjectName(plan.project),
versionName: plan.versionName ?? null,
versionNumber: plan.versionNumber ?? null,
fileName: plan.fileName ?? null,
owner,
slug: plan.project.slug ?? plan.projectId,
iconUrl: plan.project.icon_url ?? null,
}
}

function getQueuedInstallPlaceholderFallbacks(
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
) {
return getQueuedAddonInstallPlans(plans).map((plan) =>
getQueuedInstallPlaceholder(plan, getQueuedInstallOwnerFallback(plan.project)),
)
}

function setProjectInstalling(projectId: string, installing: boolean) {
const next = new Set(installingProjectIds.value)
if (installing) {
Expand All @@ -224,6 +276,10 @@ export function useServerInstallContent({
)
}

function getServerInstalledContentKeys(data = serverContentData.value) {
return new Set((data?.addons ?? []).map((addon) => addon.project_id ?? addon.filename))
}

function syncHiddenInstalledProjectIds() {
hiddenInstalledProjectIds.value = new Set([
...getServerInstalledProjectIds(),
Expand Down Expand Up @@ -410,13 +466,6 @@ export function useServerInstallContent({
)
if (queuedPlans.size === 0) return true

try {
await waitForServerContextRuntimeReady(client, serverId)
} catch (error) {
handleError(error as Error)
return false
}

isInstallingQueuedServerInstalls.value = true
queuedInstallProgress.value = {
completed: 0,
Expand All @@ -437,6 +486,9 @@ export function useServerInstallContent({
})

if (!result.ok) {
for (const plan of result.attemptedPlans) {
removePendingServerContentInstall(serverId, worldId, plan.projectId)
}
handleError(result.error as Error)
return false
}
Expand All @@ -449,7 +501,9 @@ export function useServerInstallContent({
total: result.flushedPlans.length,
}
if (result.flushedPlans.length > 0) {
await queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
await queryClient.invalidateQueries({
queryKey: ['content', 'list', 'v1', serverId],
})
}

return true
Expand All @@ -472,6 +526,11 @@ export function useServerInstallContent({

if (sid && wid) {
writeStoredServerInstallQueue(sid, wid, plans)
writePendingServerContentInstallBaseline(sid, wid, [
...getServerInstalledContentKeys(),
...optimisticallyInstalledProjectIds.value,
])
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
}
const installed = await flushQueuedServerInstalls(sid, wid)
if (!installed) return false
Expand Down
Loading
Loading