Merge remote-tracking branch 'origin/main' into cal/hosting-server-instances

# Conflicts:
#	apps/app-frontend/src/pages/Browse.vue
#	apps/app-frontend/src/pages/hosting/manage/Index.vue
#	apps/app-frontend/src/pages/instance/Index.vue
#	apps/app-frontend/src/pages/project/Index.vue
#	apps/app-frontend/src/routes.js
#	apps/frontend/src/locales/en-US/index.json
#	apps/frontend/src/middleware/project.global.ts
#	apps/frontend/src/pages/[type]/[project].vue
#	apps/frontend/src/pages/discover/[type]/index.vue
#	apps/frontend/src/pages/organization/[organization].vue
#	apps/frontend/src/pages/user/[user].vue
#	packages/ui/src/components/base/FloatingActionBar.vue
#	packages/ui/src/components/base/PageHeader.vue
#	packages/ui/src/components/base/StackedAdmonitions.vue
#	packages/ui/src/components/base/TeleportOverflowMenu.vue
#	packages/ui/src/components/base/index.ts
#	packages/ui/src/components/project/ProjectHeader.vue
#	packages/ui/src/components/servers/server-header/ServerManageHeader.vue
#	packages/ui/src/components/servers/server-header/index.ts
#	packages/ui/src/composables/index.ts
#	packages/ui/src/layouts/shared/browse-tab/composables/use-browse-search.ts
#	packages/ui/src/layouts/shared/browse-tab/header.vue
#	packages/ui/src/layouts/shared/browse-tab/layout.vue
#	packages/ui/src/layouts/shared/content-tab/components/ContentModpackCard.vue
#	packages/ui/src/layouts/shared/content-tab/layout.vue
#	packages/ui/src/layouts/shared/installation-settings/components/ContentDiffModal.vue
#	packages/ui/src/layouts/shared/installation-settings/providers/installation-settings.ts
#	packages/ui/src/layouts/wrapped/hosting/manage/[id]/index.vue
#	packages/ui/src/locales/cs-CZ/index.json
#	packages/ui/src/locales/de-CH/index.json
#	packages/ui/src/locales/de-DE/index.json
#	packages/ui/src/locales/es-419/index.json
#	packages/ui/src/locales/es-ES/index.json
#	packages/ui/src/locales/fr-FR/index.json
#	packages/ui/src/locales/hu-HU/index.json
#	packages/ui/src/locales/id-ID/index.json
#	packages/ui/src/locales/it-IT/index.json
#	packages/ui/src/locales/ja-JP/index.json
#	packages/ui/src/locales/ko-KR/index.json
#	packages/ui/src/locales/ms-MY/index.json
#	packages/ui/src/locales/nl-NL/index.json
#	packages/ui/src/locales/pl-PL/index.json
#	packages/ui/src/locales/pt-BR/index.json
#	packages/ui/src/locales/ru-RU/index.json
#	packages/ui/src/locales/sv-SE/index.json
#	packages/ui/src/locales/tr-TR/index.json
#	packages/ui/src/locales/uk-UA/index.json
#	packages/ui/src/locales/vi-VN/index.json
#	packages/ui/src/locales/zh-CN/index.json
#	packages/ui/src/locales/zh-TW/index.json
#	packages/ui/src/stories/base/PageHeader.stories.ts
#	packages/ui/src/utils/file-extensions.ts
This commit is contained in:
Calum H. (IMB11)
2026-08-03 18:39:37 +01:00
1294 changed files with 151547 additions and 35420 deletions
@@ -33,6 +33,7 @@ import ContentModpackCard from './components/ContentModpackCard.vue'
import ContentSelectionBar from './components/ContentSelectionBar.vue'
import ConfirmBulkUpdateModal from './components/modals/ConfirmBulkUpdateModal.vue'
import ConfirmDeletionModal from './components/modals/ConfirmDeletionModal.vue'
import ConfirmDisableModal from './components/modals/ConfirmDisableModal.vue'
import ConfirmUnlinkModal from './components/modals/ConfirmUnlinkModal.vue'
import ContentDependencyWarningModal from './components/modals/ContentDependencyWarningModal.vue'
import {
@@ -45,7 +46,12 @@ import {
useContentSelection,
} from './composables'
import { injectContentManager } from './providers/content-manager'
import type { BulkOperationStatus, ContentCardTableItem, ContentItem } from './types'
import type {
BulkOperationStatus,
ContentActionWarning,
ContentCardTableItem,
ContentItem,
} from './types'
const { formatMessage } = useVIntl()
const debug = useDebugLogger('ContentPageLayout')
@@ -280,13 +286,14 @@ const tableItems = computed<ContentCardTableItem[]>(() => {
toggleDisabled: ctx.isBusy.value,
toggleDisabledTooltip: ctx.isBusy.value ? (ctx.busyMessage?.value ?? null) : null,
installing: item.installing === true,
hasUpdate: item.has_update,
hasUpdate: base.hasUpdate ?? item.has_update,
isClientOnly:
isClientOnlyEnvironment(item.environment) ||
!!item.pack_client_retained ||
!!item.pack_client_depends,
clientWarning: getClientWarningType(item),
hideSwitchVersion: !base.versionLink,
hideDelete: base.hideDelete,
hideSwitchVersion: base.hideSwitchVersion ?? !base.versionLink,
overflowOptions: ctx.getOverflowOptions?.(item),
}
})
@@ -322,8 +329,12 @@ const hasOutdatedProjects = computed(() => {
// Deletion
const pendingDeletionItems = ref<ContentItem[]>([])
const pendingDeletionWarning = ref<ContentActionWarning | null>(null)
const confirmDeletionModal = ref<InstanceType<typeof ConfirmDeletionModal>>()
const confirmDisableModal = ref<InstanceType<typeof ConfirmDisableModal>>()
const contentDependencyWarningModal = ref<InstanceType<typeof ContentDependencyWarningModal>>()
const pendingDisableItems = ref<ContentItem[]>([])
const pendingDisableWarning = ref<ContentActionWarning | null>(null)
const pendingDependencyWarningItems = ref<ContentCardTableItem[]>([])
const pendingDependencyWarningDependents = ref<
Array<{
@@ -340,17 +351,30 @@ function mapToDisplayItem(item: ContentItem) {
}
}
function canDeleteItem(item: ContentItem) {
return ctx.canDeleteItem?.(item) ?? true
}
function canToggleItem(item: ContentItem) {
return ctx.canToggleItem?.(item) ?? true
}
const deletableSelectedItems = computed(() => selectedItems.value.filter(canDeleteItem))
const toggleableSelectedItems = computed(() => selectedItems.value.filter(canToggleItem))
async function promptDeleteItems(items: ContentItem[], event?: MouseEvent) {
if (items.length === 0) return
pendingDeletionItems.value = items
const deletableItems = items.filter(canDeleteItem)
if (deletableItems.length === 0) return
pendingDeletionItems.value = deletableItems
pendingDeletionWarning.value = ctx.getDeleteWarning?.(deletableItems) ?? null
pendingDependencyWarningItems.value = []
pendingDependencyWarningDependents.value = []
pendingDependencyWarningDisableTargets.value = []
const deletingIds = new Set(items.map(getItemId))
const deletingIds = new Set(deletableItems.map(getItemId))
const warning = ctx.getDeleteDependencyWarning
? await Promise.resolve()
.then(() => ctx.getDeleteDependencyWarning!(items))
.then(() => ctx.getDeleteDependencyWarning!(deletableItems))
.catch(() => null)
: null
if (warning) {
@@ -366,7 +390,7 @@ async function promptDeleteItems(items: ContentItem[], event?: MouseEvent) {
const relevantDependencyIds = new Set(
remainingDependents.flatMap((dependent) => dependent.dependencies.map(getItemId)),
)
const warningItems = items.filter((item) => relevantDependencyIds.has(getItemId(item)))
const warningItems = deletableItems.filter((item) => relevantDependencyIds.has(getItemId(item)))
if (warningItems.length === 0) {
showDeletionConfirmation(event)
return
@@ -390,7 +414,11 @@ async function promptDeleteItems(items: ContentItem[], event?: MouseEvent) {
}
async function showDeletionConfirmation(event?: MouseEvent) {
if ((event?.shiftKey || skipNonEssentialWarnings.value) && !ctx.isBusy.value) {
if (
!pendingDeletionWarning.value &&
(event?.shiftKey || skipNonEssentialWarnings.value) &&
!ctx.isBusy.value
) {
confirmDelete()
} else {
await nextTick()
@@ -419,6 +447,11 @@ async function confirmDependencyWarningDelete(disableDependentsAfterDeleting: bo
pendingDependencyWarningItems.value = []
pendingDependencyWarningDependents.value = []
if (pendingDeletionWarning.value) {
confirmDeletionModal.value?.show()
return
}
await confirmDelete()
}
@@ -427,6 +460,10 @@ async function disablePendingDependencyWarningDependents() {
pendingDependencyWarningDisableTargets.value = []
if (items.length === 0) return
await promptDisableItems(items)
}
async function disableItemsWithoutWarning(items: ContentItem[]) {
if (ctx.bulkDisableItems) {
await ctx.bulkDisableItems(items)
return
@@ -447,6 +484,7 @@ async function confirmDelete() {
if (ctx.isBusy.value) return
const itemsToDelete = [...pendingDeletionItems.value]
pendingDeletionItems.value = []
pendingDeletionWarning.value = null
if (itemsToDelete.length === 0) return
if (ctx.bulkDeleteItems && itemsToDelete.length > 1) {
@@ -495,10 +533,75 @@ async function confirmDelete() {
await disablePendingDependencyWarningDependents()
}
async function promptDisableItems(items: ContentItem[]) {
if (items.length === 0) return
pendingDisableItems.value = items
const warning = ctx.getDisableWarning?.(items) ?? null
if (warning) {
pendingDisableWarning.value = warning
confirmDisableModal.value?.show()
return
}
await confirmDisable()
}
async function confirmDisable() {
if (ctx.isBusy.value) return
const itemsToDisable = [...pendingDisableItems.value]
pendingDisableItems.value = []
pendingDisableWarning.value = null
if (itemsToDisable.length === 0) return
if (ctx.bulkDisableItems && itemsToDisable.length > 1) {
isBulkOperating.value = true
bulkOperation.value = 'disable'
bulkProgress.value = 0
bulkTotal.value = itemsToDisable.length
bulkWaiting.value = true
try {
await disableItemsWithoutWarning(itemsToDisable)
} finally {
clearSelection()
isBulkOperating.value = false
bulkOperation.value = null
bulkProgress.value = 0
bulkTotal.value = 0
bulkWaiting.value = false
}
return
}
if (itemsToDisable.length === 1) {
const item = itemsToDisable[0]
const id = getItemId(item)
markChanging(id)
try {
if (ctx.bulkDisableItems) {
await ctx.bulkDisableItems(itemsToDisable)
} else {
await ctx.toggleEnabled(item)
}
} finally {
unmarkChanging(id)
}
return
}
await runBulk('disable', itemsToDisable, (item) => disableItemsWithoutWarning([item]), {
onComplete: clearSelection,
})
}
async function handleToggleEnabledById(id: string, _value: boolean) {
if (ctx.isBusy.value) return
const item = ctx.items.value.find((i) => getItemId(i) === id)
if (!item) return
if (!canToggleItem(item)) return
if (!_value) {
await promptDisableItems([item])
return
}
markChanging(id)
try {
await ctx.toggleEnabled(item)
@@ -509,7 +612,7 @@ async function handleToggleEnabledById(id: string, _value: boolean) {
async function bulkEnable() {
if (ctx.isBusy.value) return
const items = selectedItems.value.filter((item) => !item.enabled)
const items = toggleableSelectedItems.value.filter((item) => !item.enabled)
if (items.length === 0) return
if (ctx.bulkEnableItems) {
isBulkOperating.value = true
@@ -534,27 +637,9 @@ async function bulkEnable() {
async function bulkDisable() {
if (ctx.isBusy.value) return
const items = selectedItems.value.filter((item) => item.enabled)
const items = toggleableSelectedItems.value.filter((item) => item.enabled)
if (items.length === 0) return
if (ctx.bulkDisableItems) {
isBulkOperating.value = true
bulkOperation.value = 'disable'
bulkProgress.value = 0
bulkTotal.value = items.length
bulkWaiting.value = true
try {
await ctx.bulkDisableItems(items)
} finally {
clearSelection()
isBulkOperating.value = false
bulkOperation.value = null
bulkProgress.value = 0
bulkTotal.value = 0
bulkWaiting.value = false
}
return
}
await runBulk('disable', items, (item) => ctx.toggleEnabled(item), { onComplete: clearSelection })
await promptDisableItems(items)
}
function handleUpdateById(id: string) {
@@ -696,16 +781,12 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
:has-update="ctx.modpack.value.hasUpdate"
:disabled="ctx.modpack.value.disabled"
:disabled-text="ctx.modpack.value.disabledText"
:show-content-hint="
!!(ctx.showContentHint?.value && ctx.modpack.value && ctx.items.value.length === 0)
"
v-on="{
...(ctx.updateModpack ? { update: () => ctx.updateModpack?.() } : {}),
...(ctx.viewModpackContent ? { content: () => ctx.viewModpackContent?.() } : {}),
...(ctx.unlinkModpack ? { unlink: () => confirmUnlinkModal?.show() } : {}),
...(ctx.openSettings ? { settings: () => ctx.openSettings?.() } : {}),
}"
@dismiss-content-hint="ctx.dismissContentHint?.()"
/>
<template v-if="ctx.items.value.length > 0">
@@ -737,20 +818,6 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
/>
<div class="flex gap-2">
<ButtonStyled color="brand">
<button
v-tooltip="
ctx.busyMessage?.value ??
(ctx.disableAddContent?.value ? ctx.disableAddContentTooltip : undefined)
"
:disabled="ctx.isBusy.value || ctx.disableAddContent?.value"
class="!h-10 flex items-center gap-2"
@click="ctx.browse"
>
<CompassIcon class="size-5" />
<span>{{ formatMessage(messages.browseContent) }}</span>
</button>
</ButtonStyled>
<ButtonStyled type="outlined">
<button
v-tooltip="
@@ -765,6 +832,20 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
{{ formatMessage(messages.uploadFiles) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button
v-tooltip="
ctx.busyMessage?.value ??
(ctx.disableAddContent?.value ? ctx.disableAddContentTooltip : undefined)
"
:disabled="ctx.isBusy.value || ctx.disableAddContent?.value"
class="!h-10 flex items-center gap-2"
@click="ctx.browse"
>
<CompassIcon class="size-5" />
<span>{{ formatMessage(messages.browseContent) }}</span>
</button>
</ButtonStyled>
</div>
</div>
@@ -947,6 +1028,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
:bulk-item-count="bulkItemCount"
:aria-label="formatMessage(commonMessages.selectionActionsLabel)"
:get-item-id="getItemId"
:toggle-items="toggleableSelectedItems"
@clear="clearSelection"
@enable="bulkEnable"
@disable="bulkDisable"
@@ -1013,9 +1095,10 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
</template>
<template #actions-end>
<div class="mx-1 h-6 w-px bg-surface-5" />
<div v-if="deletableSelectedItems.length > 0" class="mx-1 h-6 w-px bg-surface-5" />
<ButtonStyled
v-if="deletableSelectedItems.length > 0"
type="transparent"
color="red"
color-fill="text"
@@ -1036,6 +1119,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
ref="confirmDeletionModal"
:count="pendingDeletionItems.length"
:item-type="ctx.contentTypeLabel.value"
:warning="pendingDeletionWarning"
:variant="ctx.deletionContext ?? 'instance'"
:backup-tip="pendingDeletionItems.map((i) => i.project?.title ?? i.file_name).join(', ')"
:action-disabled="ctx.isBusy.value"
@@ -1043,6 +1127,15 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
:target-type="ctx.deletionContext ?? 'instance'"
@delete="confirmDelete"
/>
<ConfirmDisableModal
ref="confirmDisableModal"
:count="pendingDisableItems.length"
:item-type="ctx.contentTypeLabel.value"
:warning="pendingDisableWarning"
:action-disabled="ctx.isBusy.value"
:action-disabled-tooltip="ctx.busyMessage?.value ?? undefined"
@disable="confirmDisable"
/>
<ContentDependencyWarningModal
ref="contentDependencyWarningModal"
:items="pendingDependencyWarningItems"