fix: various fixes related to content tab on app and panel (#5605)
* fix: content filtering client only * fix: browse content bug Fixes #5570 * fix: Applying Mods & Updates filters at the same time doesn't work Fixes #5602 * fix: Browsing content: going back resets filters and installed state Fixes #5598 * fix: Mod tile background flickers when toggling enabled/disabled state Fixes #5600 * fix: Overhaul of "Content" tab on instances broke a lot Fixes #5567 * fix: Latest App update replacing all mods icons with a datapack/rescourcepack Fixes #5556 * fix: billing page api-client ditch useBaseFetch * fix: remove org icon from project card items * fix: lint
This commit is contained in:
@@ -109,6 +109,10 @@ const instanceHideInstalled = ref(false)
|
|||||||
const newlyInstalled = ref<string[]>([])
|
const newlyInstalled = ref<string[]>([])
|
||||||
const isServerInstance = ref(false)
|
const isServerInstance = ref(false)
|
||||||
|
|
||||||
|
const allInstalledIds = computed(
|
||||||
|
() => new Set([...newlyInstalled.value, ...(installedProjectIds.value ?? [])]),
|
||||||
|
)
|
||||||
|
|
||||||
const PERSISTENT_QUERY_PARAMS = ['i', 'ai']
|
const PERSISTENT_QUERY_PARAMS = ['i', 'ai']
|
||||||
|
|
||||||
await initInstanceContext()
|
await initInstanceContext()
|
||||||
@@ -485,17 +489,8 @@ async function refreshSearch() {
|
|||||||
link: `/browse/${projectType.value}`,
|
link: `/browse/${projectType.value}`,
|
||||||
query: params,
|
query: params,
|
||||||
})
|
})
|
||||||
const queryString = Object.entries(params)
|
debugLog('updating URL', params)
|
||||||
.flatMap(([key, value]) => {
|
router.replace({ path: route.path, query: params })
|
||||||
const values = Array.isArray(value) ? value : [value]
|
|
||||||
return values
|
|
||||||
.filter((v): v is string => v != null)
|
|
||||||
.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`)
|
|
||||||
})
|
|
||||||
.join('&')
|
|
||||||
const newUrl = `${route.path}${queryString ? '?' + queryString : ''}`
|
|
||||||
debugLog('updating URL', newUrl)
|
|
||||||
window.history.replaceState(window.history.state, '', newUrl)
|
|
||||||
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
debugLog('refreshSearch complete', { version })
|
debugLog('refreshSearch complete', { version })
|
||||||
@@ -947,7 +942,7 @@ previousFilterState.value = JSON.stringify({
|
|||||||
loader.supported_project_types?.includes(projectType),
|
loader.supported_project_types?.includes(projectType),
|
||||||
),
|
),
|
||||||
]"
|
]"
|
||||||
:installed="result.installed || newlyInstalled.includes(result.project_id || '')"
|
:installed="result.installed || allInstalledIds.has(result.project_id || '')"
|
||||||
@install="
|
@install="
|
||||||
(id) => {
|
(id) => {
|
||||||
newlyInstalled.push(id)
|
newlyInstalled.push(id)
|
||||||
|
|||||||
@@ -781,11 +781,17 @@ provideContentManager({
|
|||||||
linkedModpackProject.value
|
linkedModpackProject.value
|
||||||
? {
|
? {
|
||||||
project: linkedModpackProject.value,
|
project: linkedModpackProject.value,
|
||||||
projectLink: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}`,
|
projectLink: {
|
||||||
|
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}`,
|
||||||
|
query: { i: props.instance.path },
|
||||||
|
},
|
||||||
version: linkedModpackVersion.value ?? undefined,
|
version: linkedModpackVersion.value ?? undefined,
|
||||||
versionLink:
|
versionLink:
|
||||||
linkedModpackProject.value && linkedModpackVersion.value
|
linkedModpackProject.value && linkedModpackVersion.value
|
||||||
? `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`
|
? {
|
||||||
|
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`,
|
||||||
|
query: { i: props.instance.path },
|
||||||
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
owner: linkedModpackOwner.value
|
owner: linkedModpackOwner.value
|
||||||
? {
|
? {
|
||||||
@@ -808,7 +814,7 @@ provideContentManager({
|
|||||||
isPackLocked,
|
isPackLocked,
|
||||||
isBusy: isInstanceBusy,
|
isBusy: isInstanceBusy,
|
||||||
isBulkOperating,
|
isBulkOperating,
|
||||||
getItemId: (item) => item.file_name,
|
getItemId: (item) => item.file_path ?? item.file_name,
|
||||||
contentTypeLabel: ref(formatMessage(messages.contentTypeProject)),
|
contentTypeLabel: ref(formatMessage(messages.contentTypeProject)),
|
||||||
toggleEnabled: toggleDisableMod,
|
toggleEnabled: toggleDisableMod,
|
||||||
bulkEnableItems: (items) =>
|
bulkEnableItems: (items) =>
|
||||||
@@ -832,14 +838,16 @@ provideContentManager({
|
|||||||
dismissContentHint,
|
dismissContentHint,
|
||||||
shareItems: handleShareItems,
|
shareItems: handleShareItems,
|
||||||
mapToTableItem: (item) => ({
|
mapToTableItem: (item) => ({
|
||||||
id: item.file_name,
|
id: item.file_path ?? item.file_name,
|
||||||
project: item.project ?? {
|
project: item.project ?? {
|
||||||
id: item.file_name,
|
id: item.file_name,
|
||||||
slug: null,
|
slug: null,
|
||||||
title: item.file_name.replace('.disabled', ''),
|
title: item.file_name.replace('.disabled', ''),
|
||||||
icon_url: null,
|
icon_url: null,
|
||||||
},
|
},
|
||||||
projectLink: item.project?.id ? `/project/${item.project.id}` : undefined,
|
projectLink: item.project?.id
|
||||||
|
? { path: `/project/${item.project.id}`, query: { i: props.instance.path } }
|
||||||
|
: undefined,
|
||||||
version: item.version ?? {
|
version: item.version ?? {
|
||||||
id: item.file_name,
|
id: item.file_name,
|
||||||
version_number: formatMessage(messages.unknownVersion),
|
version_number: formatMessage(messages.unknownVersion),
|
||||||
@@ -847,7 +855,10 @@ provideContentManager({
|
|||||||
},
|
},
|
||||||
versionLink:
|
versionLink:
|
||||||
item.project?.id && item.version?.id
|
item.project?.id && item.version?.id
|
||||||
? `/project/${item.project.id}/version/${item.version.id}`
|
? {
|
||||||
|
path: `/project/${item.project.id}/version/${item.version.id}`,
|
||||||
|
query: { i: props.instance.path },
|
||||||
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
owner: item.owner
|
owner: item.owner
|
||||||
? {
|
? {
|
||||||
@@ -857,6 +868,7 @@ provideContentManager({
|
|||||||
: undefined,
|
: undefined,
|
||||||
enabled: item.enabled,
|
enabled: item.enabled,
|
||||||
}),
|
}),
|
||||||
|
filterPersistKey: props.instance.path,
|
||||||
})
|
})
|
||||||
|
|
||||||
await initProjects()
|
await initProjects()
|
||||||
|
|||||||
@@ -137,12 +137,12 @@
|
|||||||
class="mb-4 flex items-center justify-between border-0 border-b border-solid border-divider pb-4"
|
class="mb-4 flex items-center justify-between border-0 border-b border-solid border-divider pb-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Avatar :src="user.avatar_url" :alt="user.username" size="32px" circle />
|
<Avatar :src="user?.avatar_url" :alt="user?.username" size="32px" circle />
|
||||||
<h1 class="m-0 text-2xl font-extrabold">{{ user.username }}'s subscriptions</h1>
|
<h1 class="m-0 text-2xl font-extrabold">{{ user?.username }}'s subscriptions</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<nuxt-link :to="`/user/${user.id}`">
|
<nuxt-link :to="`/user/${user?.id}`">
|
||||||
<UserIcon aria-hidden="true" />
|
<UserIcon aria-hidden="true" />
|
||||||
User profile
|
User profile
|
||||||
<ExternalIcon class="h-4 w-4" />
|
<ExternalIcon class="h-4 w-4" />
|
||||||
@@ -346,7 +346,7 @@ import dayjs from 'dayjs'
|
|||||||
import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue'
|
import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue'
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const client = injectModrinthClient()
|
const { labrinth } = injectModrinthClient()
|
||||||
const formatPrice = useFormatPrice()
|
const formatPrice = useFormatPrice()
|
||||||
const formatDateTime = useFormatDateTime({
|
const formatDateTime = useFormatDateTime({
|
||||||
timeStyle: 'short',
|
timeStyle: 'short',
|
||||||
@@ -373,11 +373,17 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: user, error: userError } = useQuery({
|
const {
|
||||||
|
data: user,
|
||||||
|
error: userError,
|
||||||
|
suspense: userSuspense,
|
||||||
|
} = useQuery({
|
||||||
queryKey: ['user', route.params.id],
|
queryKey: ['user', route.params.id],
|
||||||
queryFn: () => client.labrinth.users_v2.get(route.params.id),
|
queryFn: () => labrinth.users_v2.get(route.params.id),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onServerPrefetch(userSuspense)
|
||||||
|
|
||||||
watch(userError, (error) => {
|
watch(userError, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
showError({
|
showError({
|
||||||
@@ -390,14 +396,14 @@ watch(userError, (error) => {
|
|||||||
|
|
||||||
const { data: subscriptions } = useQuery({
|
const { data: subscriptions } = useQuery({
|
||||||
queryKey: computed(() => ['billing', 'subscriptions', user.value?.id]),
|
queryKey: computed(() => ['billing', 'subscriptions', user.value?.id]),
|
||||||
queryFn: () => client.labrinth.billing_internal.getSubscriptions(user.value.id),
|
queryFn: () => labrinth.billing_internal.getSubscriptions(user.value?.id),
|
||||||
enabled: computed(() => !!user.value?.id),
|
enabled: computed(() => !!user.value?.id),
|
||||||
placeholderData: [],
|
placeholderData: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: charges, refetch: refreshCharges } = useQuery({
|
const { data: charges, refetch: refreshCharges } = useQuery({
|
||||||
queryKey: computed(() => ['billing', 'payments', user.value?.id]),
|
queryKey: computed(() => ['billing', 'payments', user.value?.id]),
|
||||||
queryFn: () => client.labrinth.billing_internal.getPayments(user.value.id),
|
queryFn: () => labrinth.billing_internal.getPayments(user.value?.id),
|
||||||
enabled: computed(() => !!user.value?.id),
|
enabled: computed(() => !!user.value?.id),
|
||||||
placeholderData: [],
|
placeholderData: [],
|
||||||
})
|
})
|
||||||
@@ -458,7 +464,7 @@ async function applyCredit() {
|
|||||||
crediting.value = true
|
crediting.value = true
|
||||||
try {
|
try {
|
||||||
const daysParsed = Math.max(1, Math.floor(Number(creditDays.value) || 1))
|
const daysParsed = Math.max(1, Math.floor(Number(creditDays.value) || 1))
|
||||||
await client.labrinth.billing_internal.credit({
|
await labrinth.billing_internal.credit({
|
||||||
subscription_ids: [selectedSubscription.value.id],
|
subscription_ids: [selectedSubscription.value.id],
|
||||||
days: daysParsed,
|
days: daysParsed,
|
||||||
send_email: creditSendEmail.value,
|
send_email: creditSendEmail.value,
|
||||||
@@ -492,7 +498,7 @@ async function refundCharge() {
|
|||||||
? { type: 'none', unprovision: unprovision.value }
|
? { type: 'none', unprovision: unprovision.value }
|
||||||
: { type: 'full', unprovision: unprovision.value }
|
: { type: 'full', unprovision: unprovision.value }
|
||||||
|
|
||||||
await client.labrinth.billing_internal.refundCharge(selectedCharge.value.id, payload)
|
await labrinth.billing_internal.refundCharge(selectedCharge.value.id, payload)
|
||||||
await refreshCharges()
|
await refreshCharges()
|
||||||
refundModal.value.hide()
|
refundModal.value.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -508,7 +514,7 @@ async function refundCharge() {
|
|||||||
async function modifyCharge() {
|
async function modifyCharge() {
|
||||||
modifying.value = true
|
modifying.value = true
|
||||||
try {
|
try {
|
||||||
await client.labrinth.billing_internal.editSubscription(selectedSubscription.value.id, {
|
await labrinth.billing_internal.editSubscription(selectedSubscription.value.id, {
|
||||||
cancelled: cancel.value,
|
cancelled: cancel.value,
|
||||||
})
|
})
|
||||||
addNotification({
|
addNotification({
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
OrganizationIcon,
|
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
TrashExclamationIcon,
|
TrashExclamationIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
@@ -88,7 +87,7 @@ const deleteHovered = ref(false)
|
|||||||
:class="{ 'opacity-50': disabled }"
|
:class="{ 'opacity-50': disabled }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex min-w-0 items-center gap-4"
|
class="flex min-w-0 items-center gap-4 transition-[filter,opacity] duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none',
|
hideActions ? 'flex-1' : 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none',
|
||||||
enabled === false && !disabled ? 'grayscale opacity-50' : '',
|
enabled === false && !disabled ? 'grayscale opacity-50' : '',
|
||||||
@@ -158,10 +157,6 @@ const deleteHovered = ref(false)
|
|||||||
class="flex shrink-0 items-center gap-1 !decoration-secondary"
|
class="flex shrink-0 items-center gap-1 !decoration-secondary"
|
||||||
:class="{ 'hover:underline': owner.link }"
|
:class="{ 'hover:underline': owner.link }"
|
||||||
>
|
>
|
||||||
<OrganizationIcon
|
|
||||||
v-if="owner.type === 'organization'"
|
|
||||||
class="size-4 text-secondary"
|
|
||||||
/>
|
|
||||||
<Avatar
|
<Avatar
|
||||||
:src="owner.avatar_url"
|
:src="owner.avatar_url"
|
||||||
:alt="owner.name"
|
:alt="owner.name"
|
||||||
@@ -193,7 +188,7 @@ const deleteHovered = ref(false)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="hidden flex-col gap-0.5 @[800px]:flex"
|
class="hidden flex-col gap-0.5 transition-[filter,opacity] duration-200 @[800px]:flex"
|
||||||
:class="[
|
:class="[
|
||||||
hideActions ? 'flex-1' : 'flex-1 min-w-0',
|
hideActions ? 'flex-1' : 'flex-1 min-w-0',
|
||||||
enabled === false && !disabled ? 'grayscale opacity-50' : '',
|
enabled === false && !disabled ? 'grayscale opacity-50' : '',
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
|
import { useSessionStorage } from '@vueuse/core'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
import type { ContentItem } from '../types'
|
import type { ContentItem } from '../types'
|
||||||
|
|
||||||
const CLIENT_ONLY_ENVIRONMENTS = new Set([
|
const CLIENT_ONLY_ENVIRONMENTS = new Set(['client_only', 'singleplayer_only'])
|
||||||
'client_only',
|
|
||||||
'client_only_server_optional',
|
|
||||||
'singleplayer_only',
|
|
||||||
])
|
|
||||||
|
|
||||||
export function isClientOnlyEnvironment(env?: string | null): boolean {
|
export function isClientOnlyEnvironment(env?: string | null): boolean {
|
||||||
return !!env && CLIENT_ONLY_ENVIRONMENTS.has(env)
|
return !!env && CLIENT_ONLY_ENVIRONMENTS.has(env)
|
||||||
@@ -24,10 +21,13 @@ export interface ContentFilterConfig {
|
|||||||
showClientOnlyFilter?: boolean
|
showClientOnlyFilter?: boolean
|
||||||
isPackLocked?: Ref<boolean>
|
isPackLocked?: Ref<boolean>
|
||||||
formatProjectType?: (type: string) => string
|
formatProjectType?: (type: string) => string
|
||||||
|
persistKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useContentFilters(items: Ref<ContentItem[]>, config?: ContentFilterConfig) {
|
export function useContentFilters(items: Ref<ContentItem[]>, config?: ContentFilterConfig) {
|
||||||
const selectedFilters = ref<string[]>([])
|
const selectedFilters = config?.persistKey
|
||||||
|
? useSessionStorage<string[]>(`content-filters:${config.persistKey}`, [])
|
||||||
|
: ref<string[]>([])
|
||||||
|
|
||||||
const filterOptions = computed<ContentFilterOption[]>(() => {
|
const filterOptions = computed<ContentFilterOption[]>(() => {
|
||||||
const options: ContentFilterOption[] = []
|
const options: ContentFilterOption[] = []
|
||||||
@@ -83,14 +83,23 @@ export function useContentFilters(items: Ref<ContentItem[]>, config?: ContentFil
|
|||||||
|
|
||||||
function applyFilters(source: ContentItem[]): ContentItem[] {
|
function applyFilters(source: ContentItem[]): ContentItem[] {
|
||||||
if (selectedFilters.value.length === 0) return source
|
if (selectedFilters.value.length === 0) return source
|
||||||
|
|
||||||
|
const attributeFilters = new Set(['updates', 'disabled', 'client-only'])
|
||||||
|
const typeFilters = selectedFilters.value.filter((f) => !attributeFilters.has(f))
|
||||||
|
const activeAttributes = selectedFilters.value.filter((f) => attributeFilters.has(f))
|
||||||
|
|
||||||
return source.filter((item) => {
|
return source.filter((item) => {
|
||||||
for (const filter of selectedFilters.value) {
|
if (typeFilters.length > 0 && !typeFilters.includes(item.project_type)) {
|
||||||
if (filter === 'updates' && item.has_update) return true
|
return false
|
||||||
if (filter === 'disabled' && !item.enabled) return true
|
|
||||||
if (filter === 'client-only' && isClientOnlyEnvironment(item.environment)) return true
|
|
||||||
if (item.project_type === filter) return true
|
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
for (const filter of activeAttributes) {
|
||||||
|
if (filter === 'updates' && !item.has_update) return false
|
||||||
|
if (filter === 'disabled' && item.enabled) return false
|
||||||
|
if (filter === 'client-only' && !isClientOnlyEnvironment(item.environment)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ const { selectedFilters, filterOptions, toggleFilter, applyFilters } = useConten
|
|||||||
showClientOnlyFilter: ctx.showClientOnlyFilter ?? false,
|
showClientOnlyFilter: ctx.showClientOnlyFilter ?? false,
|
||||||
isPackLocked: ctx.isPackLocked,
|
isPackLocked: ctx.isPackLocked,
|
||||||
formatProjectType,
|
formatProjectType,
|
||||||
|
persistKey: ctx.filterPersistKey,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ export interface ContentManagerContext {
|
|||||||
|
|
||||||
// Table item mapping (link generation differs per platform)
|
// Table item mapping (link generation differs per platform)
|
||||||
mapToTableItem: (item: ContentItem) => ContentCardTableItem
|
mapToTableItem: (item: ContentItem) => ContentCardTableItem
|
||||||
|
|
||||||
|
// Filter persistence key — when set, selected filters are saved/restored via sessionStorage
|
||||||
|
filterPersistKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const [injectContentManager, provideContentManager] = createContext<ContentManagerContext>(
|
export const [injectContentManager, provideContentManager] = createContext<ContentManagerContext>(
|
||||||
|
|||||||
@@ -872,7 +872,7 @@ provideContentManager({
|
|||||||
})
|
})
|
||||||
return filteredReasons.length > 0 ? formatMessage(filteredReasons[0].reason) : null
|
return filteredReasons.length > 0 ? formatMessage(filteredReasons[0].reason) : null
|
||||||
}),
|
}),
|
||||||
getItemId: (item) => item.file_name,
|
getItemId: (item) => item.file_path ?? item.file_name,
|
||||||
contentTypeLabel: type,
|
contentTypeLabel: type,
|
||||||
toggleEnabled: handleToggleEnabled,
|
toggleEnabled: handleToggleEnabled,
|
||||||
deleteItem: handleDeleteItem,
|
deleteItem: handleDeleteItem,
|
||||||
@@ -898,7 +898,7 @@ provideContentManager({
|
|||||||
mapToTableItem: (item) => {
|
mapToTableItem: (item) => {
|
||||||
const projectType = item.project_type ?? type.value
|
const projectType = item.project_type ?? type.value
|
||||||
return {
|
return {
|
||||||
id: item.file_name,
|
id: item.file_path ?? item.file_name,
|
||||||
project: item.project,
|
project: item.project,
|
||||||
projectLink: item.project?.id ? `/${projectType}/${item.project.id}` : undefined,
|
projectLink: item.project?.id ? `/${projectType}/${item.project.id}` : undefined,
|
||||||
version: item.version,
|
version: item.version,
|
||||||
@@ -912,6 +912,7 @@ provideContentManager({
|
|||||||
enabled: item.enabled,
|
enabled: item.enabled,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
filterPersistKey: `server:${serverId}:${worldId.value}`,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user