From 1be6fcc26833565607e1e0bb51629b5a4b4124e9 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 10 Aug 2026 17:37:25 +0100 Subject: [PATCH 1/4] refactor: managed content card --- .../ui/modal/InstallToPlayModal.vue | 23 +- .../shared-instance-install-modal/index.vue | 10 +- .../src/helpers/instance-content.ts | 47 +- .../instance/components/admonitions/index.vue | 26 - .../components/admonitions/messages.ts | 13 - .../shared-instance-update-available.vue | 32 - .../instance/components/admonitions/types.ts | 1 - .../src/pages/instance/content/index.vue | 308 +++++---- .../src/pages/instance/instance-context.ts | 2 + .../src/pages/instance/layout.vue | 8 +- .../pages/instance/shared-instance-context.ts | 2 + .../SharedInstanceReportContext.vue | 10 +- .../components/base/buttons/ButtonFrame.vue | 2 +- .../components/ContentModpackCard.vue | 306 --------- .../components/managed-content-card/index.vue | 112 ++++ .../managed-content-card-footer.vue | 184 +++++ .../managed-content-card-summary.vue | 92 +++ .../index.vue} | 51 +- .../src/layouts/shared/content-tab/index.ts | 7 +- .../src/layouts/shared/content-tab/layout.vue | 40 +- .../content-tab/providers/content-manager.ts | 28 +- .../src/layouts/shared/content-tab/types.ts | 44 +- .../content-tab/utils/managed-content.ts | 58 ++ .../wrapped/hosting/manage/content.vue | 108 ++- packages/ui/src/providers/content-manager.ts | 2 +- .../instances/ContentModpackCard.stories.ts | 633 ------------------ .../instances/ManagedContentCard.stories.ts | 204 ++++++ ...ries.ts => ManagedContentModal.stories.ts} | 97 +-- 28 files changed, 1057 insertions(+), 1393 deletions(-) delete mode 100644 apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-update-available.vue delete mode 100644 packages/ui/src/layouts/shared/content-tab/components/ContentModpackCard.vue create mode 100644 packages/ui/src/layouts/shared/content-tab/components/managed-content-card/index.vue create mode 100644 packages/ui/src/layouts/shared/content-tab/components/managed-content-card/managed-content-card-footer.vue create mode 100644 packages/ui/src/layouts/shared/content-tab/components/managed-content-card/managed-content-card-summary.vue rename packages/ui/src/layouts/shared/content-tab/components/{modals/ModpackContentModal.vue => managed-content-modal/index.vue} (94%) create mode 100644 packages/ui/src/layouts/shared/content-tab/utils/managed-content.ts delete mode 100644 packages/ui/src/stories/instances/ContentModpackCard.stories.ts create mode 100644 packages/ui/src/stories/instances/ManagedContentCard.stories.ts rename packages/ui/src/stories/instances/{ModpackContentModal.stories.ts => ManagedContentModal.stories.ts} (80%) diff --git a/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue b/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue index 16c229706f..a48a1d4c6a 100644 --- a/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue +++ b/apps/app-frontend/src/components/ui/modal/InstallToPlayModal.vue @@ -141,10 +141,11 @@ - @@ -159,7 +160,7 @@ import { type ContentItem, defineMessages, formatLoader, - ModpackContentModal, + ManagedContentModal, NewModal, Table, type TableColumn, @@ -269,10 +270,10 @@ function handleReport() { } } -const modpackContentModal = ref>() +const managedContentModal = ref>() async function openViewContents() { - modpackContentModal.value?.showLoading() + managedContentModal.value?.showLoading() try { // Ensure version data is available — the useQuery may not have resolved yet const versionId = modpackVersionId.value @@ -330,10 +331,10 @@ async function openViewContents() { } }, ) - modpackContentModal.value?.show(contentItems) + managedContentModal.value?.show(contentItems) } catch (err) { console.error('Failed to load modpack contents:', err) - modpackContentModal.value?.show([]) + managedContentModal.value?.show([]) } } @@ -363,6 +364,10 @@ function hide() { } const messages = defineMessages({ + modpackContent: { + id: 'app.modal.install-to-play.managed-content.modpack-header', + defaultMessage: 'Modpack content', + }, installToPlay: { id: 'app.modal.install-to-play.header', defaultMessage: 'Install to play', diff --git a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue index 9ffa818bcb..15f707ce83 100644 --- a/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue +++ b/apps/app-frontend/src/components/ui/shared-instances/shared-instance-install-modal/index.vue @@ -229,11 +229,11 @@ - @@ -256,7 +256,7 @@ import { injectNotificationManager, IntlFormatted, MarkdownEditor, - ModpackContentModal, + ManagedContentModal, NewModal, Table, type TableColumn, @@ -289,7 +289,7 @@ type SharedInstanceCreator = { } const modal = ref>() -const contentModal = ref>() +const contentModal = ref>() const externalFileTable = ref(null) const preview = ref(null) const creator = ref(null) diff --git a/apps/app-frontend/src/helpers/instance-content.ts b/apps/app-frontend/src/helpers/instance-content.ts index b294603c34..d2a9e08b1e 100644 --- a/apps/app-frontend/src/helpers/instance-content.ts +++ b/apps/app-frontend/src/helpers/instance-content.ts @@ -1,9 +1,7 @@ import type { ContentItem, - ContentModpackCardCategory, - ContentModpackCardProject, - ContentModpackCardVersion, - ContentOwner, + ManagedContentProject, + ManagedContentVersion, } from '@modrinth/ui' import { @@ -11,7 +9,6 @@ import { get_linked_modpack_info, type LinkedModpackInfo, } from '@/helpers/instance' -import { get_categories } from '@/helpers/tags.js' import type { CacheBehaviour } from '@/helpers/types' export type InstanceContentData = { @@ -21,11 +18,8 @@ export type InstanceContentData = { } export type InstanceContentModpackData = { - project: ContentModpackCardProject - version: ContentModpackCardVersion - owner: ContentOwner | null - categories: ContentModpackCardCategory[] - hasUpdate: boolean + project: ManagedContentProject + version: ManagedContentVersion updateVersionId: string | null } @@ -34,19 +28,15 @@ export async function loadInstanceContentData( cacheBehaviour?: CacheBehaviour, onError?: (error: Error) => unknown, ): Promise { - const [contentItems, modpackInfo, allCategories] = await Promise.all([ + const [contentItems, modpackInfo] = await Promise.all([ get_content_items(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)), get_linked_modpack_info(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)), - get_categories().catch((error) => handleLoadError(error, onError)), ]) return { path, contentItems: (contentItems as ContentItem[] | null | undefined) ?? null, - modpack: normalizeLinkedModpackInfo( - modpackInfo as LinkedModpackInfo | null | undefined, - allCategories as ContentModpackCardCategory[] | null | undefined, - ), + modpack: normalizeLinkedModpackInfo(modpackInfo as LinkedModpackInfo | null | undefined), } } @@ -58,7 +48,6 @@ function handleLoadError(error: unknown, onError?: (error: Error) => unknown) { function normalizeLinkedModpackInfo( modpackInfo: LinkedModpackInfo | null | undefined, - allCategories: ContentModpackCardCategory[] | null | undefined, ): InstanceContentModpackData | null { if (!modpackInfo) return null @@ -72,30 +61,6 @@ function normalizeLinkedModpackInfo( ...modpackInfo.version, date_published: modpackInfo.version.date_published.toString(), }, - owner: modpackInfo.owner - ? { - ...modpackInfo.owner, - avatar_url: modpackInfo.owner.avatar_url ?? undefined, - } - : null, - categories: resolveLinkedModpackCategories(modpackInfo, allCategories), - hasUpdate: modpackInfo.has_update, updateVersionId: modpackInfo.update_version_id, } } - -function resolveLinkedModpackCategories( - modpackInfo: LinkedModpackInfo, - allCategories: ContentModpackCardCategory[] | null | undefined, -) { - if (!allCategories || !modpackInfo.project.categories) return [] - - const seen = new Set() - return allCategories.filter((category) => { - if (modpackInfo.project.categories.includes(category.name) && !seen.has(category.name)) { - seen.add(category.name) - return true - } - return false - }) -} diff --git a/apps/app-frontend/src/pages/instance/components/admonitions/index.vue b/apps/app-frontend/src/pages/instance/components/admonitions/index.vue index 5e4d996ad6..f294d38df1 100644 --- a/apps/app-frontend/src/pages/instance/components/admonitions/index.vue +++ b/apps/app-frontend/src/pages/instance/components/admonitions/index.vue @@ -6,11 +6,6 @@ :instance="instance" @published="emit('published')" /> - () const emit = defineEmits<{ published: [] delete: [] - 'review-update': [event: MouseEvent] }>() const sharedInstanceWrongAccount = computed(() => props.sharedInstanceWrongAccount ?? false) @@ -76,15 +68,6 @@ const showSharedInstancePublishAdmonition = computed( props.instance.shared_instance?.role === 'owner' && props.instance.shared_instance.status === 'stale', ) -const showSharedInstanceUpdateAdmonition = computed( - () => - !sharedInstanceWrongAccount.value && - !displayedSharedInstanceUnavailableReason.value && - props.instance.install_stage === 'installed' && - props.sharedInstanceRole === 'member' && - props.sharedInstanceUpdateAvailable === true, -) - const stackItems = computed(() => { const items: InstanceAdmonitionItem[] = [] @@ -120,15 +103,6 @@ const stackItems = computed(() => { }) } - if (showSharedInstanceUpdateAdmonition.value) { - items.push({ - id: 'shared-instance-update-available', - type: 'info', - dismissible: false, - kind: 'shared-instance-update-available', - }) - } - return items }) diff --git a/apps/app-frontend/src/pages/instance/components/admonitions/messages.ts b/apps/app-frontend/src/pages/instance/components/admonitions/messages.ts index 011a57acaf..5191ae9b26 100644 --- a/apps/app-frontend/src/pages/instance/components/admonitions/messages.ts +++ b/apps/app-frontend/src/pages/instance/components/admonitions/messages.ts @@ -21,19 +21,6 @@ export const instanceAdmonitionsMessages = defineMessages({ id: 'app.instance.admonitions.shared-instance.reviewing-button', defaultMessage: 'Reviewing...', }, - sharedInstanceUpdateAvailableHeader: { - id: 'app.instance.admonitions.shared-instance.update-available-header', - defaultMessage: 'An update is available', - }, - sharedInstanceUpdateAvailableBody: { - id: 'app.instance.admonitions.shared-instance.update-available-body', - defaultMessage: - 'An update is required to play {name}. Please update to latest version to launch the game.', - }, - sharedInstanceReviewUpdateButton: { - id: 'app.instance.admonitions.shared-instance.review-update-button', - defaultMessage: 'Review update', - }, sharedInstanceReviewHeader: { id: 'app.instance.admonitions.shared-instance.review-header', defaultMessage: 'Review changes', diff --git a/apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-update-available.vue b/apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-update-available.vue deleted file mode 100644 index 780b51ccec..0000000000 --- a/apps/app-frontend/src/pages/instance/components/admonitions/shared-instance-update-available.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/apps/app-frontend/src/pages/instance/components/admonitions/types.ts b/apps/app-frontend/src/pages/instance/components/admonitions/types.ts index 02948b3e8d..ea7eb81be1 100644 --- a/apps/app-frontend/src/pages/instance/components/admonitions/types.ts +++ b/apps/app-frontend/src/pages/instance/components/admonitions/types.ts @@ -2,7 +2,6 @@ import type { StackedAdmonitionItem } from '@modrinth/ui' export type InstanceAdmonitionKind = | 'shared-instance-stale' - | 'shared-instance-update-available' | 'shared-instance-unavailable' | 'shared-instance-wrong-account' diff --git a/apps/app-frontend/src/pages/instance/content/index.vue b/apps/app-frontend/src/pages/instance/content/index.vue index 080f09026a..7c7e434a15 100644 --- a/apps/app-frontend/src/pages/instance/content/index.vue +++ b/apps/app-frontend/src/pages/instance/content/index.vue @@ -15,27 +15,28 @@ :share-text="formatMessage(messages.shareText)" :open-in-new-tab="false" /> - { if (owner.type === 'user') return `/user/${encodeURIComponent(owner.id)}` @@ -219,6 +231,7 @@ const skipNonEssentialWarnings = computed(() => ) const instancePage = injectInstancePage() +const sharedInstanceState = injectSharedInstance() const instance = instancePage.instance const isServerInstance = instancePage.isServerInstance const openSettings = () => instancePage.openSettings(1) @@ -283,15 +296,12 @@ watch( }, ) -const linkedModpackProject = ref(null) -const linkedModpackVersion = ref(null) -const linkedModpackOwner = ref(null) -const linkedModpackCategories = ref([]) -const linkedModpackHasUpdate = ref(false) +const linkedModpackProject = ref(null) +const linkedModpackVersion = ref(null) const linkedModpackUpdateVersionId = ref(null) const localImportedModpackUnlinked = ref(false) -const localImportedModpackProject = computed(() => { +const localImportedModpackProject = computed(() => { const link = instance.value.link if (localImportedModpackUnlinked.value || link?.type !== 'imported_modpack') return null @@ -300,7 +310,6 @@ const localImportedModpackProject = computed(( slug: link.filename ?? instance.value.id, title: link.name ?? instance.value.name, icon_url: instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : undefined, - description: '', filename: link.filename ?? undefined, } }) @@ -329,10 +338,10 @@ const isPackLocked = computed( const shareModal = ref | null>() const exportModal = ref(null) const contentUpdaterModal = ref | null>() -const modpackContentModal = ref | null>() +const managedContentModal = ref | null>() const modpackUpdateConfirmModal = ref | null>() const sharedDisableConfirmModal = ref | null>() -const pendingModpackDisableItems = ref([]) +const pendingManagedContentDisableItems = ref([]) const unknownFileWarningModal = ref | null>() const unknownFileName = ref('') let resolveUnknownFileConfirmation: ((confirmed: boolean) => void) | null = null @@ -349,6 +358,132 @@ const modpackContentQuery = useQuery({ ), }) +const hasSharedManagedContent = computed(() => { + if (instance.value.shared_instance?.role === 'owner') return false + + const linkType = instance.value.link?.type + return ( + !!instance.value.shared_instance || + linkType === 'server_project' || + linkType === 'server_project_modpack' + ) +}) + +const managedContentItems = computed(() => { + const linkedContent = modpackContentQuery.data.value ?? [] + const sourcedContent = hasSharedManagedContent.value + ? projects.value.filter((item) => + ['server_project', 'shared_instance'].includes(item.source_kind ?? ''), + ) + : [] + + return dedupeManagedContentItems([...linkedContent, ...sourcedContent]) +}) + +const managedContentSummary = computed(() => + modpackContentQuery.isLoading.value && modpackContentQuery.data.value === undefined + ? undefined + : summarizeManagedContent(managedContentItems.value), +) + +const managedContent = computed(() => { + const attachment = instance.value.shared_instance + const sharedManager = sharedInstanceState.manager.value + const linkedProject = instancePage.linkedProject.value + const linkType = instance.value.link?.type + const isSharedOwner = attachment?.role === 'owner' + + if ( + !isSharedOwner && + (attachment || linkType === 'server_project' || linkType === 'server_project_modpack') + ) { + const serverManaged = + sharedManager?.type === 'server' || + !!attachment?.server_manager_name || + linkType === 'server_project' || + linkType === 'server_project_modpack' || + (!attachment && isServerInstance.value) + const managerName = serverManaged + ? (sharedManager?.name ?? + attachment?.server_manager_name ?? + linkedProject?.title ?? + instance.value.name) + : (sharedManager?.name ?? instance.value.name) + const managerIcon = serverManaged + ? (sharedManager?.avatarUrl ?? + attachment?.server_manager_icon_url ?? + linkedProject?.icon_url ?? + undefined) + : (sharedManager?.avatarUrl ?? + (instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : undefined)) + const managerLink = serverManaged + ? linkedProject + ? { + path: `/project/${linkedProject.slug ?? linkedProject.id}`, + query: { i: instancePage.instanceId.value }, + } + : undefined + : sharedManager?.type === 'user' + ? `/user/${encodeURIComponent(sharedManager.name)}` + : undefined + + return { + card: { + kind: serverManaged ? 'server' : 'shared-instance', + manager: { + name: managerName, + iconUrl: managerIcon, + link: managerLink, + }, + summary: managedContentSummary.value, + syncedAt: sharedInstanceState.lastUpdateCheckAt.value, + updateAvailable: instancePage.sharedInstanceUpdateAvailable.value, + }, + disabled: attachment?.status === 'applying' || isInstanceBusy.value, + disabledText: formatMessage(commonMessages.updatingLabel), + } + } + + const project = displayedModpackProject.value + if (!project) return null + + return { + card: { + kind: 'modpack', + manager: { + name: project.title, + iconUrl: project.icon_url ?? undefined, + link: linkedModpackProject.value + ? { + path: `/project/${project.slug ?? project.id}`, + query: { i: instancePage.instanceId.value }, + } + : undefined, + }, + summary: managedContentSummary.value, + versionNumber: linkedModpackVersion.value?.version_number, + versionLink: + linkedModpackProject.value && linkedModpackVersion.value + ? { + path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`, + query: { i: instancePage.instanceId.value }, + } + : undefined, + updatedAt: linkedModpackVersion.value?.date_published, + }, + disabled: isModpackUpdating.value || isInstanceBusy.value, + disabledText: formatMessage(commonMessages.updatingLabel), + } +}) + +const managedContentModalHeader = computed(() => + formatMessage( + managedContent.value?.card.kind === 'modpack' + ? messages.modpackContentHeader + : messages.sharedContentHeader, + ), +) + // TODO: Extract content operation and updater modal state into composables; this page currently owns file mutations, dependency installs, busy flags, and version selection flow. const updatingProject = ref(null) const updatingProjectVersions = ref([]) @@ -420,12 +555,12 @@ function canDeleteContent(item: ContentItem) { function setContentItemBusy(item: ContentItem, busy: boolean, originalFileName = item.file_name) { item.installing = busy - modpackContentModal.value?.updateItem(originalFileName, { + managedContentModal.value?.updateItem(originalFileName, { installing: busy, disabled: busy, }) if (item.file_name !== originalFileName) { - modpackContentModal.value?.updateItem(item.file_name, { + managedContentModal.value?.updateItem(item.file_name, { installing: busy, disabled: busy, }) @@ -640,7 +775,7 @@ async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) { mod.file_path = newPath mod.file_name = newFileName mod.enabled = enabled - modpackContentModal.value?.updateItem(operation.originalFileName, { + managedContentModal.value?.updateItem(operation.originalFileName, { file_path: newPath, file_name: newFileName, enabled, @@ -993,9 +1128,9 @@ async function handleSwitchVersion(item: ContentItem) { updatingProjectVersions.value = versions } -async function handleModpackContentToggle(item: ContentItem, enabled: boolean) { +async function handleManagedContentToggle(item: ContentItem, enabled: boolean) { if (!enabled && managedContentPolicy.disableWarning([item])) { - pendingModpackDisableItems.value = [item] + pendingManagedContentDisableItems.value = [item] sharedDisableConfirmModal.value?.show() return } @@ -1003,47 +1138,47 @@ async function handleModpackContentToggle(item: ContentItem, enabled: boolean) { await toggleDisableDebounced(item, enabled) } -async function handleModpackContentBulkToggle(items: ContentItem[], enabled: boolean) { +async function handleManagedContentBulkToggle(items: ContentItem[], enabled: boolean) { if (!enabled && managedContentPolicy.disableWarning(items)) { - pendingModpackDisableItems.value = items + pendingManagedContentDisableItems.value = items sharedDisableConfirmModal.value?.show() return } - await setModpackContentEnabled(items, enabled) + await setManagedContentEnabled(items, enabled) } -async function confirmPendingModpackContentDisable() { - const items = [...pendingModpackDisableItems.value] - pendingModpackDisableItems.value = [] - await setModpackContentEnabled(items, false) +async function confirmPendingManagedContentDisable() { + const items = [...pendingManagedContentDisableItems.value] + pendingManagedContentDisableItems.value = [] + await setManagedContentEnabled(items, false) } -async function setModpackContentEnabled(items: ContentItem[], enabled: boolean) { +async function setManagedContentEnabled(items: ContentItem[], enabled: boolean) { await Promise.all(items.map((item) => toggleDisableMod(item, enabled))) } -async function handleModpackContent() { +async function handleManagedContent() { if (!instance.value?.id) return - if (modpackContentQuery.data.value?.length) { - modpackContentModal.value?.show(modpackContentQuery.data.value) + if (modpackContentQuery.data.value !== undefined) { + managedContentModal.value?.show(managedContentItems.value) return } - modpackContentModal.value?.showLoading() + managedContentModal.value?.showLoading() const { data, error } = await modpackContentQuery.refetch() if (data !== undefined) { - modpackContentModal.value?.show(data) + managedContentModal.value?.show(managedContentItems.value) } else { if (error) handleError(error) - modpackContentModal.value?.hide() + managedContentModal.value?.hide() } } -async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) { +async function refreshManagedContentItems(cacheBehaviour?: CacheBehaviour) { if (!instance.value?.id) return const contentItems = await queryClient @@ -1054,13 +1189,13 @@ async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) { .catch(handleError) if (contentItems) { - modpackContentModal.value?.setItems(contentItems) + managedContentModal.value?.setItems(managedContentItems.value) } } async function refreshContentState(cacheBehaviour?: CacheBehaviour) { await initProjects(cacheBehaviour) - await refreshModpackContentItems(cacheBehaviour) + await refreshManagedContentItems(cacheBehaviour) } watch( @@ -1093,7 +1228,6 @@ async function handleModpackUpdate() { linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value, linkedModpackProject: linkedModpackProject.value, linkedModpackVersion: linkedModpackVersion.value, - linkedModpackHasUpdate: linkedModpackHasUpdate.value, instance: { path: instance.value.id, name: instance.value.name, @@ -1262,8 +1396,6 @@ async function unpairInstance() { }) linkedModpackProject.value = null linkedModpackVersion.value = null - linkedModpackOwner.value = null - linkedModpackHasUpdate.value = false linkedModpackUpdateVersionId.value = null localImportedModpackUnlinked.value = true await initProjects() @@ -1358,16 +1490,10 @@ function applyContentData(contentData: InstanceContentData) { if (contentData.modpack) { linkedModpackProject.value = contentData.modpack.project linkedModpackVersion.value = contentData.modpack.version - linkedModpackOwner.value = contentData.modpack.owner - linkedModpackCategories.value = contentData.modpack.categories - linkedModpackHasUpdate.value = contentData.modpack.hasUpdate linkedModpackUpdateVersionId.value = contentData.modpack.updateVersionId } else { linkedModpackProject.value = null linkedModpackVersion.value = null - linkedModpackOwner.value = null - linkedModpackCategories.value = [] - linkedModpackHasUpdate.value = false linkedModpackUpdateVersionId.value = null } @@ -1379,51 +1505,7 @@ provideContentManager({ items: mergedProjects, loading, error: ref(null), - modpack: computed(() => { - if (linkedModpackProject.value) { - return { - project: linkedModpackProject.value, - projectLink: { - path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}`, - query: { i: instancePage.instanceId.value }, - }, - version: linkedModpackVersion.value ?? undefined, - versionLink: - linkedModpackProject.value && linkedModpackVersion.value - ? { - path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`, - query: { i: instancePage.instanceId.value }, - } - : undefined, - owner: linkedModpackOwner.value - ? { - ...linkedModpackOwner.value, - link: contentOwnerLink(linkedModpackOwner.value), - } - : undefined, - categories: linkedModpackCategories.value, - hasUpdate: linkedModpackHasUpdate.value, - disabled: isModpackUpdating.value, - disabledText: isModpackUpdating.value - ? formatMessage(commonMessages.updatingLabel) - : formatMessage(commonMessages.installingLabel), - } - } - - if (localImportedModpackProject.value) { - return { - project: localImportedModpackProject.value, - categories: [], - hasUpdate: false, - disabled: isModpackUpdating.value, - disabledText: isModpackUpdating.value - ? formatMessage(commonMessages.updatingLabel) - : formatMessage(commonMessages.installingLabel), - } - } - - return null - }), + managedContent, isPackLocked, isBusy: isInstanceBusy, disableAddContent: isQuarantined, @@ -1459,13 +1541,15 @@ provideContentManager({ updateItem: handleUpdate, bulkUpdateAll: bulkUpdateAllProjects, bulkUpdateItem: updateProject, - updateModpack: - isServerInstance.value || isSharedMember.value || isQuarantined.value - ? undefined - : handleModpackUpdate, - viewModpackContent: handleModpackContent, + runManagedContentPrimaryAction: + instance.value.shared_instance?.role === 'member' + ? instancePage.reviewSharedInstanceUpdate + : instance.value.link?.type === 'modrinth_modpack' && !isQuarantined.value + ? handleModpackUpdate + : undefined, + viewManagedContent: handleManagedContent, unlinkModpack: unpairInstance, - openSettings: openSettings, + openManagedContentSettings: openSettings, switchVersion: handleSwitchVersion, getOverflowOptions, shareItems: handleShareItems, @@ -1511,7 +1595,7 @@ provideContentManager({ type UnlistenFn = () => void const initialContentReady = loadInitialContent() -void initialContentReady.then(restoreModpackContentModalState).catch(handleError) +void initialContentReady.then(restoreManagedContentModalState).catch(handleError) function getInstallRevision() { return installRevisionByInstance.value.get(instance.value.id) ?? 0 @@ -1541,18 +1625,18 @@ watch(contentQuery.error, (error) => { } }) -async function restoreModpackContentModalState() { +async function restoreManagedContentModalState() { if (!savedModalState) return const stateToRestore = savedModalState savedModalState = null await nextTick() - modpackContentModal.value?.restore(stateToRestore) + managedContentModal.value?.restore(stateToRestore) } // Save modal state when navigating away so it can be restored on back const removeBeforeEach = router.beforeEach(() => { - const state = modpackContentModal.value?.getState() + const state = managedContentModal.value?.getState() savedModalState = state ?? null }) diff --git a/apps/app-frontend/src/pages/instance/instance-context.ts b/apps/app-frontend/src/pages/instance/instance-context.ts index 19223b9664..397922e372 100644 --- a/apps/app-frontend/src/pages/instance/instance-context.ts +++ b/apps/app-frontend/src/pages/instance/instance-context.ts @@ -9,6 +9,7 @@ export interface InstancePageContext { readonly instance: ComputedRef readonly linkedProject: ComputedRef readonly isServerInstance: ComputedRef + readonly sharedInstanceUpdateAvailable: ComputedRef readonly offline: Readonly> readonly playing: ComputedRef readonly loading: Readonly> @@ -21,6 +22,7 @@ export interface InstancePageContext { openSettings: (tab?: number) => void browseContent: (projectType?: string) => Promise browseServers: () => Promise + reviewSharedInstanceUpdate: (event?: MouseEvent) => void } export const [injectInstancePage, provideInstancePage] = diff --git a/apps/app-frontend/src/pages/instance/layout.vue b/apps/app-frontend/src/pages/instance/layout.vue index caffa1404a..e5d00bb8d2 100644 --- a/apps/app-frontend/src/pages/instance/layout.vue +++ b/apps/app-frontend/src/pages/instance/layout.vue @@ -64,10 +64,8 @@ :shared-instance-expected-user-id="sharedInstanceExpectedUserId" :shared-instance-role="instance.shared_instance?.role" :shared-instance-signed-out="sharedInstanceSignedOut" - :shared-instance-update-available="showSharedInstanceUpdateAdmonition" @published="refreshInstance" @delete="requestInstanceDeletion" - @review-update="reviewSharedInstanceUpdate" />
@@ -331,7 +329,7 @@ const sharedInstanceUpdateKey = computed(() => { const latestVersion = sharedInstanceUpdatePreview.value?.latestVersion return instanceId && latestVersion !== undefined ? `${instanceId}:${latestVersion}` : null }) -const showSharedInstanceUpdateAdmonition = computed( +const sharedInstanceUpdateAvailable = computed( () => sharedInstanceUpdatePreview.value?.updateAvailable === true && sharedInstanceUpdateKey.value !== hiddenSharedInstanceUpdateKey.value, @@ -515,7 +513,7 @@ async function handleSharedInstanceUnavailable( setSharedInstanceUnavailable(reason) } -function reviewSharedInstanceUpdate(event: MouseEvent) { +function reviewSharedInstanceUpdate(event?: MouseEvent) { const currentInstance = instance.value const preview = sharedInstanceUpdatePreview.value if ( @@ -792,6 +790,7 @@ provideInstancePage({ instance: instance as ComputedRef, linkedProject: linkedProjectV3, isServerInstance, + sharedInstanceUpdateAvailable, offline, playing, loading, @@ -804,6 +803,7 @@ provideInstancePage({ openSettings, browseContent, browseServers, + reviewSharedInstanceUpdate, }) provideInstanceBackup(() => instance.value!) diff --git a/apps/app-frontend/src/pages/instance/shared-instance-context.ts b/apps/app-frontend/src/pages/instance/shared-instance-context.ts index 683a7a0686..64ae41ba5f 100644 --- a/apps/app-frontend/src/pages/instance/shared-instance-context.ts +++ b/apps/app-frontend/src/pages/instance/shared-instance-context.ts @@ -128,6 +128,7 @@ export function createSharedInstanceContext( const updatePreview = computed(() => unavailableReason.value ? null : (updatePreviewQuery.data.value ?? null), ) + const lastUpdateCheckAt = computed(() => updatePreviewQuery.dataUpdatedAt.value || undefined) watch( () => instance.value?.id, @@ -162,6 +163,7 @@ export function createSharedInstanceContext( unavailableManager, manager, updatePreview, + lastUpdateCheckAt, expectedUserId, wrongAccount, signedOut, diff --git a/apps/frontend/src/components/ui/moderation/SharedInstanceReportContext.vue b/apps/frontend/src/components/ui/moderation/SharedInstanceReportContext.vue index 247b350a73..e3839dd4a6 100644 --- a/apps/frontend/src/components/ui/moderation/SharedInstanceReportContext.vue +++ b/apps/frontend/src/components/ui/moderation/SharedInstanceReportContext.vue @@ -17,7 +17,7 @@ import { ConfirmLeaveModal, type ContentItem, injectModrinthClient, - ModpackContentModal, + ManagedContentModal, Table, type TableColumn, useFormatDateTime, @@ -76,7 +76,7 @@ const emit = defineEmits<{ contentError: [error: unknown] }>() -const contentModal = ref | null>(null) +const contentModal = ref | null>(null) const banModal = ref | null>(null) const client = injectModrinthClient() const contentByVersion = new Map() @@ -406,11 +406,11 @@ function formattedLoader(version: SharedInstanceReportVersion) {
- = { colored: 'button-frame--colored bg-[--button-color] text-[var(--color-accent-contrast)] [&>svg]:text-inherit', outlined: - 'button-frame--outlined bg-transparent text-[var(--button-color,var(--color-contrast))] [&>svg]:text-inherit', + 'button-frame--outlined bg-transparent text-[var(--button-color,var(--color-contrast))] [&>svg]:text-[var(--button-color,var(--color-base))]', quiet: 'button-frame--quiet bg-transparent [&>svg]:text-inherit', } diff --git a/packages/ui/src/layouts/shared/content-tab/components/ContentModpackCard.vue b/packages/ui/src/layouts/shared/content-tab/components/ContentModpackCard.vue deleted file mode 100644 index fa36c4de7f..0000000000 --- a/packages/ui/src/layouts/shared/content-tab/components/ContentModpackCard.vue +++ /dev/null @@ -1,306 +0,0 @@ - - - diff --git a/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/index.vue b/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/index.vue new file mode 100644 index 0000000000..e4f1ed44cf --- /dev/null +++ b/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/index.vue @@ -0,0 +1,112 @@ + + + diff --git a/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/managed-content-card-footer.vue b/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/managed-content-card-footer.vue new file mode 100644 index 0000000000..f33bdbe63e --- /dev/null +++ b/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/managed-content-card-footer.vue @@ -0,0 +1,184 @@ + + + diff --git a/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/managed-content-card-summary.vue b/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/managed-content-card-summary.vue new file mode 100644 index 0000000000..34424ee9a2 --- /dev/null +++ b/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/managed-content-card-summary.vue @@ -0,0 +1,92 @@ + + + diff --git a/packages/ui/src/layouts/shared/content-tab/components/modals/ModpackContentModal.vue b/packages/ui/src/layouts/shared/content-tab/components/managed-content-modal/index.vue similarity index 94% rename from packages/ui/src/layouts/shared/content-tab/components/modals/ModpackContentModal.vue rename to packages/ui/src/layouts/shared/content-tab/components/managed-content-modal/index.vue index 2cdf830569..b29b40d3ee 100644 --- a/packages/ui/src/layouts/shared/content-tab/components/modals/ModpackContentModal.vue +++ b/packages/ui/src/layouts/shared/content-tab/components/managed-content-modal/index.vue @@ -38,8 +38,8 @@ const pageContext = injectPageContext(null) interface Props { header?: string - modpackName?: string - modpackIconUrl?: string + sourceName?: string + sourceIconUrl?: string enableToggle?: boolean actionDisabled?: boolean actionDisabledTooltip?: string | null @@ -49,8 +49,8 @@ interface Props { const props = withDefaults(defineProps(), { header: undefined, - modpackName: undefined, - modpackIconUrl: undefined, + sourceName: undefined, + sourceIconUrl: undefined, enableToggle: false, actionDisabled: false, actionDisabledTooltip: undefined, @@ -67,44 +67,44 @@ const emit = defineEmits<{ const messages = defineMessages({ header: { - id: 'instances.modpack-content-modal.header', - defaultMessage: 'Modpack content', + id: 'instances.managed-content-modal.header', + defaultMessage: 'Managed content', }, searchPlaceholder: { - id: 'instances.modpack-content-modal.search-placeholder', + id: 'instances.managed-content-modal.search-placeholder', defaultMessage: 'Search {count, number} {count, plural, one {project} other {projects}}', }, loading: { - id: 'instances.modpack-content-modal.loading', + id: 'instances.managed-content-modal.loading', defaultMessage: 'Loading content...', }, emptyTitle: { - id: 'instances.modpack-content-modal.empty-title', + id: 'instances.managed-content-modal.empty-title', defaultMessage: 'No content found', }, emptyDescription: { - id: 'instances.modpack-content-modal.empty-description', - defaultMessage: 'This modpack does not include any additional content.', + id: 'instances.managed-content-modal.empty-description', + defaultMessage: 'This source does not include any managed content.', }, noResults: { - id: 'instances.modpack-content-modal.no-results', + id: 'instances.managed-content-modal.no-results', defaultMessage: 'No projects match your search.', }, externalContent: { - id: 'instances.modpack-content-modal.external-content', + id: 'instances.managed-content-modal.external-content', defaultMessage: 'External', }, externalContentDescription: { - id: 'instances.modpack-content-modal.external-content-description', + id: 'instances.managed-content-modal.external-content-description', defaultMessage: 'This file is not published on Modrinth.', }, openInSlicer: { - id: 'instances.modpack-content-modal.open-in-slicer', + id: 'instances.managed-content-modal.open-in-slicer', defaultMessage: 'Open in Slicer', }, }) -export interface ModpackContentModalState { +export interface ManagedContentModalState { items: ContentItem[] searchQuery: string selectedFilters: string[] @@ -404,7 +404,7 @@ function handleHide() { emit('hide') } -function getState(): ModpackContentModalState | null { +function getState(): ManagedContentModalState | null { if (!items.value.length) return null return { items: items.value, @@ -414,7 +414,7 @@ function getState(): ModpackContentModalState | null { } } -async function restore(state: ModpackContentModalState) { +async function restore(state: ManagedContentModalState) { items.value = state.items.map((item) => ({ ...item })) searchQuery.value = state.searchQuery selectedFilters.value = state.selectedFilters @@ -466,10 +466,11 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems >