mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 05:48:57 +00:00
feat: instance sharing thru shared-instances service (#6569)
* feat: implement instance share page + search_users backend call * feat: invite players modal * feat: use tanstack queries for friends sync across app pages * feat: base shared instances implementation * fix: admon style * feat: impl instance admonitions like server panel * fix: impl get + del usage * feat: support modpack links * feat: invite notif accepting * fix: lint + fmt * feat: impl install to play * feat: impl usage of UpdateToPlayModal * feat: warnings on deleting/disabling shared-instance version content * fix: send instance name * feat: align with backend * feat: shared instances qa * feat: wrong account protection * feat: qa * fix: smartly apply updates * fix: install bug * fix: 401/404 differentiation * fix: fmt+prepr * feat: qa * feat: qa * fix: signing out messes up revoke/deleted checks * feat: qa * fix: fmt + lint * feat: lock content if part of shared instance * fix: lint * [do not merge] feat: rough invite links impl temp (#6666) * fix: wrong cmd * feat: invite page * fix: server-manager DTO mismatch * fix: drop anonymous invite link acceptance * refactor: structured shared-instance unavailable errors * refactor: centralise error presentations * refactor: dedupe shared instance diff detection * fix: logging in reqwests * refactor: move app.vue shared instances into handler * refactor: break up Share.vue * refactor: split up shared instances state outside of instance index * refactor: dedicated shared instances install/update modals + split up page * refactor: centralized managed content * refactor: split up install shared to own runner + shared.rs split up * refactor: dedupe sql for instance metadata enrichmnt * refactor: friends composable + dedupe friends logic across usages * chore: reduced unused code * fix: align with backend * fix: lint * fix: file sha changes * fix: invite links not working due to icon signed * feat: qa * feat: reporting frontend dummy * fix: try use header * remove: file hash field * fix: pin box * feat: malware warning for shared instances * fix: cache rule * feat: config files syncing * feat: disable config sharing * fix: header * fix: use mark ready * fix: dont cause push update for configs * fix: lint * feat: sharing page in settings * feat: move config + change flow * fix: qa * fix: lint prepr * feat: proxy file upload thru shared instances backend * fix: use collapisible * fix: push config * fix: config * feat: swap out sign in modal for new one * fix: report flow * fix: exclude configs.zip from external warnings * fix: nuxi init * fix: config bundle downloading * fix: error notif * fix: polling * fix: qa * fix: lint + prepr * feat: shared instances moderation frontend + hook up report flow * fix: report copy * fix: lint * fix: lint * fix: modrinth ids being undefined * feat: instance quarantining * fix: prepr + fmt * fix: quarantined -> locked terminology * fix: missing endpoint impls + fmt * fix: missing api in build.rs * fix: share tab jittery * fix: fmt *PT bug * fix: invites count as users even if pending * fix: prepr * fix: invite page owner in users list * fix: lint * fix: qa * fix: lint * fix: members stale not clearing * fix: invite use joined_at field * fix: lint * fix: qa --------- Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
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,12 +1119,22 @@ 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"
|
||||
:action-disabled-tooltip="ctx.busyMessage?.value ?? undefined"
|
||||
@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