+ 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..1b6fa8e747 100644
--- a/apps/app-frontend/src/pages/instance/layout.vue
+++ b/apps/app-frontend/src/pages/instance/layout.vue
@@ -40,7 +40,6 @@
:recent-plays="recentPlays"
:ping="ping"
:minecraft-server="minecraftServer"
- :linked-project-v3="linkedProjectV3"
:shared-instance-manager="sharedInstanceManager"
@repair="() => repairInstance()"
@stop="() => stopInstance('InstancePage')"
@@ -64,10 +63,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 +328,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 +512,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 +789,7 @@ provideInstancePage({
instance: instance as ComputedRef,
linkedProject: linkedProjectV3,
isServerInstance,
+ sharedInstanceUpdateAvailable,
offline,
playing,
loading,
@@ -804,6 +802,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) {
-
,
+) -> HashMap {
+ let (client, server) = match environment
+ .unwrap_or(VersionEnvironment::Unknown)
+ {
+ VersionEnvironment::ClientAndServer
+ | VersionEnvironment::SingleplayerOnly => {
+ (SideType::Required, SideType::Required)
+ }
+ VersionEnvironment::ClientOnly => {
+ (SideType::Required, SideType::Unsupported)
+ }
+ VersionEnvironment::ClientOnlyServerOptional => {
+ (SideType::Required, SideType::Optional)
+ }
+ VersionEnvironment::ServerOnly
+ | VersionEnvironment::DedicatedServerOnly => {
+ (SideType::Unsupported, SideType::Required)
+ }
+ VersionEnvironment::ServerOnlyClientOptional => {
+ (SideType::Optional, SideType::Required)
+ }
+ VersionEnvironment::ClientOrServer
+ | VersionEnvironment::ClientOrServerPrefersBoth => {
+ (SideType::Optional, SideType::Optional)
+ }
+ VersionEnvironment::Unknown => {
+ (SideType::Unknown, SideType::Unknown)
+ }
+ };
+
+ HashMap::from([(EnvType::Client, client), (EnvType::Server, server)])
+}
+
#[tracing::instrument(skip_all)]
pub async fn create_mrpack_json(
metadata: &InstanceMetadata,
@@ -461,9 +497,10 @@ pub async fn create_mrpack_json(
_ => None,
})
.collect::>();
- let versions = CachedEntry::get_version_many(
- &projects.iter().map(|x| &*x.1).collect::>(),
- None,
+ let version_ids = projects.iter().map(|x| &*x.1).collect::>();
+ let versions = CachedEntry::get_version_v3_many(
+ &version_ids,
+ Some(CacheBehaviour::MustRevalidate),
&state.pool,
&state.api_semaphore,
)
@@ -473,9 +510,7 @@ pub async fn create_mrpack_json(
.filter_map(|(path, version_id)| {
if let Some(version) = versions.iter().find(|x| x.id == version_id)
{
- let mut env = HashMap::new();
- env.insert(EnvType::Client, SideType::Required);
- env.insert(EnvType::Server, SideType::Required);
+ let env = get_mrpack_environment(version.environment);
let Some(primary_file) = version.files.first() else {
return Some(Err(crate::ErrorKind::OtherError(format!(
"No primary file found for mod at: {path}"
diff --git a/packages/app-lib/src/state/cache.rs b/packages/app-lib/src/state/cache.rs
index 867fc1b624..051ad5bd31 100644
--- a/packages/app-lib/src/state/cache.rs
+++ b/packages/app-lib/src/state/cache.rs
@@ -21,6 +21,7 @@ pub enum CacheValueType {
Project,
ProjectV3,
Version,
+ VersionV3,
User,
Team,
Organization,
@@ -47,6 +48,7 @@ impl CacheValueType {
CacheValueType::Project => "project",
CacheValueType::ProjectV3 => "project_v3",
CacheValueType::Version => "version",
+ CacheValueType::VersionV3 => "version_v3",
CacheValueType::User => "user",
CacheValueType::Team => "team",
CacheValueType::Organization => "organization",
@@ -72,6 +74,7 @@ impl CacheValueType {
"project" => CacheValueType::Project,
"project_v3" => CacheValueType::ProjectV3,
"version" => CacheValueType::Version,
+ "version_v3" => CacheValueType::VersionV3,
"user" => CacheValueType::User,
"team" => CacheValueType::Team,
"organization" => CacheValueType::Organization,
@@ -134,6 +137,7 @@ impl CacheValueType {
| CacheValueType::GameVersions
| CacheValueType::DonationPlatforms
| CacheValueType::Version
+ | CacheValueType::VersionV3
| CacheValueType::Team
| CacheValueType::File
| CacheValueType::LoaderManifest
@@ -181,6 +185,7 @@ pub struct CachedProjectVersions {
pub enum CacheValue {
Project(Project),
Version(Version),
+ VersionV3(VersionV3),
User(User),
Team(Vec),
Organization(Organization),
@@ -543,6 +548,30 @@ pub struct Version {
pub loaders: Vec,
}
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct VersionV3 {
+ pub id: String,
+ pub files: Vec,
+ #[serde(default)]
+ pub environment: Option,
+}
+
+#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum VersionEnvironment {
+ ClientAndServer,
+ ClientOnly,
+ ClientOnlyServerOptional,
+ SingleplayerOnly,
+ ServerOnly,
+ ServerOnlyClientOptional,
+ DedicatedServerOnly,
+ ClientOrServer,
+ ClientOrServerPrefersBoth,
+ #[serde(other)]
+ Unknown,
+}
+
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct VersionFile {
pub hashes: HashMap,
@@ -661,6 +690,7 @@ impl CacheValue {
CacheValue::Project(_) => CacheValueType::Project,
CacheValue::ProjectV3(_) => CacheValueType::ProjectV3,
CacheValue::Version(_) => CacheValueType::Version,
+ CacheValue::VersionV3(_) => CacheValueType::VersionV3,
CacheValue::User(_) => CacheValueType::User,
CacheValue::Team { .. } => CacheValueType::Team,
CacheValue::Organization(_) => CacheValueType::Organization,
@@ -690,6 +720,7 @@ impl CacheValue {
CacheValue::Project(project) => project.id.clone(),
CacheValue::ProjectV3(project) => project.id.clone(),
CacheValue::Version(version) => version.id.clone(),
+ CacheValue::VersionV3(version) => version.id.clone(),
CacheValue::User(user) => user.id.clone(),
CacheValue::Team(members) => members
.iter()
@@ -746,6 +777,7 @@ impl CacheValue {
| CacheValue::GameVersions(_)
| CacheValue::DonationPlatforms(_)
| CacheValue::Version(_)
+ | CacheValue::VersionV3(_)
| CacheValue::Team { .. }
| CacheValue::File { .. }
| CacheValue::LoaderManifest { .. }
@@ -762,6 +794,7 @@ impl CacheValue {
CacheValue::Project(project) => serde_json::to_value(project),
CacheValue::ProjectV3(project) => serde_json::to_value(project),
CacheValue::Version(version) => serde_json::to_value(version),
+ CacheValue::VersionV3(version) => serde_json::to_value(version),
CacheValue::User(user) => serde_json::to_value(user),
CacheValue::Team(members) => serde_json::to_value(members),
CacheValue::Organization(org) => serde_json::to_value(org),
@@ -898,6 +931,7 @@ impl_cache_methods!(
(Project, Project),
(ProjectV3, ProjectV3),
(Version, Version),
+ (VersionV3, VersionV3),
(User, User),
(Team, Vec),
(Organization, Organization),
@@ -1274,6 +1308,15 @@ impl CachedEntry {
CacheValue::Version
)
}
+ CacheValueType::VersionV3 => {
+ fetch_original_values!(
+ VersionV3,
+ env!("MODRINTH_API_URL_V3"),
+ "versions",
+ Some("/v3/versions"),
+ CacheValue::VersionV3
+ )
+ }
CacheValueType::User => {
fetch_original_values!(
User,
@@ -1976,6 +2019,9 @@ impl CachedEntry {
CacheValueType::Version => {
CacheValue::Version(parse(data, id, "version")?)
}
+ CacheValueType::VersionV3 => {
+ CacheValue::VersionV3(parse(data, id, "version_v3")?)
+ }
CacheValueType::User => CacheValue::User(parse(data, id, "user")?),
CacheValueType::Team => CacheValue::Team(parse(data, id, "team")?),
CacheValueType::Organization => {
diff --git a/packages/ui/src/components/base/FilterPills.vue b/packages/ui/src/components/base/FilterPills.vue
index 4a5d052d98..8a0a4c3682 100644
--- a/packages/ui/src/components/base/FilterPills.vue
+++ b/packages/ui/src/components/base/FilterPills.vue
@@ -1,22 +1,26 @@
-
-
-
-
+
+
+
+
+
+
@@ -36,10 +40,10 @@ defineProps<{
function pillClass(active: boolean) {
return [
- 'cursor-pointer rounded-full border border-solid px-3 py-1.5 text-base font-semibold leading-5 transition-all duration-100 active:scale-[0.97]',
+ 'cursor-pointer rounded-xl border border-solid bg-transparent px-3 py-1.5 text-sm font-medium leading-5 transition-all duration-100 active:scale-[0.97] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-brand-shadow',
active
- ? 'border-brand bg-brand-highlight text-brand'
- : 'border-surface-5 bg-surface-4 text-primary hover:bg-surface-5',
+ ? 'filter-pills__chip--active text-brand'
+ : 'border-surface-5 text-primary hover:bg-surface-3',
]
}
@@ -51,3 +55,14 @@ function toggle(id: string) {
}
}
+
+
diff --git a/packages/ui/src/components/base/buttons/ButtonFrame.vue b/packages/ui/src/components/base/buttons/ButtonFrame.vue
index 81d0c09f8d..0503a3d3d7 100644
--- a/packages/ui/src/components/base/buttons/ButtonFrame.vue
+++ b/packages/ui/src/components/base/buttons/ButtonFrame.vue
@@ -46,7 +46,7 @@ const typeClasses: Record
= {
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 @@
-
-
-
-
-
-
-
-
-
-
-
-
- {{ project.title }}
-
-
- {{ project.filename }}
-
-
-
- {{ project.filename }}
-
-
-
-
- {{ owner.name }}
-
-
-
-
- {{ version.version_number }}
-
-
-
-
-
-
- {{ formatTimeAgo(new Date(version.date_published)) }}
-
-
-
-
-
-
-
-
-
-
- {{
- disabledText ?? formatMessage(commonMessages.updatingLabel)
- }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ formatMessage(commonMessages.contentLabel) }}
-
-
-
- {{ formatMessage(messages.installationSettingsTooltip) }}
-
-
-
-
-
-
-
-
-
-
-
- {{ project.description }}
-
-
-
-
-
- {{ formatCompact(project.downloads) }}
-
-
-
-
- {{ formatCompact(project.followers) }}
-
-
-
-
-
-
-
-
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..20c2c47924
--- /dev/null
+++ b/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/index.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
{{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.managedBy) }}
+
+
+
+
+ {{ data.manager.name }}
+
+
+
+ {{ formatMessage(messages.server) }}
+
+
+
+
+
+
+
+ {{ data.versionNumber }}
+
+
+
+
+
+
+
+ {{ timestampLabel }}
+
+
+
+
+
+
+
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..8e2b96c174
--- /dev/null
+++ b/packages/ui/src/layouts/shared/content-tab/components/managed-content-card/managed-content-card-summary.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+ {{ formatMessage(messages.installing) }}
+
+
+
+
+
+
+
+
+ {{ formatNumber(item.count) }}
+ {{ formatProjectTypeSentence(formatMessage, item.type, item.count) }}
+
+
+
+
+
+
+ {{ formatMessage(messages.empty) }}
+
+
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 87%
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..f619aa1773 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
@@ -3,7 +3,6 @@ import {
ArrowLeftRightIcon,
BoxIcon,
ExternalIcon,
- FilterIcon,
GlassesIcon,
PaintbrushIcon,
SearchIcon,
@@ -17,6 +16,7 @@ import BulletDivider from '#ui/components/base/BulletDivider.vue'
import type { OverflowMenuOption } from '#ui/components/base/buttons'
import { ButtonLink } from '#ui/components/base/buttons'
import Checkbox from '#ui/components/base/Checkbox.vue'
+import FilterPills from '#ui/components/base/FilterPills.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import NewModal from '#ui/components/modal/NewModal.vue'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
@@ -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[]
@@ -193,15 +193,6 @@ const stats = computed(() => {
return counts
})
-function toggleFilter(filterId: string) {
- const index = selectedFilters.value.indexOf(filterId)
- if (index === -1) {
- selectedFilters.value.push(filterId)
- } else {
- selectedFilters.value.splice(index, 1)
- }
-}
-
const attributeFilterIds = new Set(['disabled', 'warnings'])
const typeFilteredCount = computed(() => {
@@ -404,7 +395,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 +405,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 +457,11 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems
>
-
-
{{ formatMessage(messages.loading) }}
-
{{ formatMessage(messages.emptyDescription) }}
-
{{ formatMessage(messages.noResults) }}
-
-
-
diff --git a/packages/ui/src/layouts/shared/content-tab/index.ts b/packages/ui/src/layouts/shared/content-tab/index.ts
index ffb63e9c64..dd8053cdb0 100644
--- a/packages/ui/src/layouts/shared/content-tab/index.ts
+++ b/packages/ui/src/layouts/shared/content-tab/index.ts
@@ -1,7 +1,7 @@
export { default as ContentCardItem } from './components/ContentCardItem.vue'
export { default as ContentCard } from './components/ContentCardItem.vue'
export { default as ContentCardTable } from './components/ContentCardTable.vue'
-export { default as ContentModpackCard } from './components/ContentModpackCard.vue'
+export { default as ManagedContentCard } from './components/managed-content-card/index.vue'
export { default as ConfirmBulkUpdateModal } from './components/modals/ConfirmBulkUpdateModal.vue'
export { default as ConfirmDeletionModal } from './components/modals/ConfirmDeletionModal.vue'
export { default as ConfirmDisableModal } from './components/modals/ConfirmDisableModal.vue'
@@ -18,11 +18,12 @@ export type {
} from './components/modals/ContentInstallModal.vue'
export { default as ContentInstallModal } from './components/modals/ContentInstallModal.vue'
export { default as InlineBackupCreator } from './components/modals/InlineBackupCreator.vue'
-export type { ModpackContentModalState } from './components/modals/ModpackContentModal.vue'
-export { default as ModpackContentModal } from './components/modals/ModpackContentModal.vue'
+export type { ManagedContentModalState } from './components/managed-content-modal/index.vue'
+export { default as ManagedContentModal } from './components/managed-content-modal/index.vue'
export { default as ContentCardLayout } from './layout.vue'
export { default as ContentPageLayout } from './layout.vue'
export * from './providers'
export * from './types'
export * from './utils/update-channels'
+export * from './utils/managed-content'
export { default as ConfirmLeaveModal } from '#ui/components/modal/ConfirmLeaveModal.vue'
diff --git a/packages/ui/src/layouts/shared/content-tab/layout.vue b/packages/ui/src/layouts/shared/content-tab/layout.vue
index 2ec27bb643..b085a52291 100644
--- a/packages/ui/src/layouts/shared/content-tab/layout.vue
+++ b/packages/ui/src/layouts/shared/content-tab/layout.vue
@@ -9,7 +9,6 @@ import {
DownloadIcon,
DropdownIcon,
FileIcon,
- FilterIcon,
FolderOpenIcon,
LinkIcon,
RefreshCwIcon,
@@ -22,13 +21,14 @@ import { computed, nextTick, ref, watch } from 'vue'
import { Button, TeleportOverflowMenu } from '#ui/components/base/buttons'
import EmptyState from '#ui/components/base/EmptyState.vue'
+import FilterPills from '#ui/components/base/FilterPills.vue'
import StyledInput from '#ui/components/base/StyledInput.vue'
import { useDebugLogger } from '#ui/composables/debug-logger'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import { commonMessages, formatContentTypeSentence } from '#ui/utils/common-messages'
import ContentCardTable from './components/ContentCardTable.vue'
-import ContentModpackCard from './components/ContentModpackCard.vue'
+import ManagedContentCard from './components/managed-content-card/index.vue'
import ContentSelectionBar from './components/ContentSelectionBar.vue'
import ConfirmBulkUpdateModal from './components/modals/ConfirmBulkUpdateModal.vue'
import ConfirmDeletionModal from './components/modals/ConfirmDeletionModal.vue'
@@ -236,6 +236,18 @@ const { selectedFilters, filterOptions, toggleFilter, applyFilters } = useConten
},
)
+function updateFilterChips(nextFilters: string[]) {
+ if (nextFilters.length === 0) {
+ selectedFilters.value = []
+ return
+ }
+
+ const changedFilter =
+ nextFilters.find((filter) => !selectedFilters.value.includes(filter)) ??
+ selectedFilters.value.find((filter) => !nextFilters.includes(filter))
+ if (changedFilter) toggleFilter(changedFilter)
+}
+
const { selectedIds, selectedItems, clearSelection, removeFromSelection } = useContentSelection(
ctx.items,
getItemId,
@@ -769,28 +781,22 @@ const confirmUnlinkModal = ref>()
-
-
+
{{ formatMessage(messages.additionalContent) }}
@@ -849,33 +855,15 @@ const confirmUnlinkModal = ref>()
-
-
-
+
+ {{ formatMessage(commonMessages.allProjectType) }}
+
+
`,
}),
}
-// ============================================
-// Filter Demo
-// ============================================
export const FilterDemo: Story = {
render: () => ({
- components: { ModpackContentModal, Button },
+ components: { ManagedContentModal, Button },
setup() {
- const modalRef = ref
| null>(null)
+ const modalRef = ref | null>(null)
const openModal = () => modalRef.value?.show(mixedModpackContent)
return { modalRef, openModal }
},
@@ -564,22 +539,18 @@ export const FilterDemo: Story = {
Click the button and try the filter chips (Mods, Shaders, Resource Packs) to filter content by type.
Test Filters
-
+
`,
}),
}
-// ============================================
-// Mixed Owner Types
-// ============================================
export const MixedOwnerTypes: Story = {
render: () => ({
- components: { ModpackContentModal, Button },
+ components: { ManagedContentModal, Button },
setup() {
- const modalRef = ref
| null>(null)
- // Mix of user and organization owners
+ const modalRef = ref | null>(null)
const mixedContent = [
sodiumItem, // User owner
fabricApiItem, // Organization owner
@@ -595,7 +566,7 @@ export const MixedOwnerTypes: Story = {
Shows content with different owner types: users (circular avatar) and organizations (rounded + icon).
View Mixed Owners
-
+
`,
}),