mirror of
https://github.com/modrinth/code.git
synced 2026-09-02 04:56:52 +00:00
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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user