You've already forked AstralRinth
refactor: align files tab with content tab design (#5621)
* fix: files.vue bugs before styling changes * feat: move files tab to shared layout structure * fix: qa * fix: qa * fix: bugs * fix: lint * fix: admonition cleanup with progress + actions * fix: cleanup * fix: modals * fix: admon title * fix: i18n standard * fix: lint + i18n pass * fix: remove transition * fix: type errors * feat: files tab in app * fix: qa * fix: backup item minmax * fix: use ContentPageHeader for server panel * fix: lint * fix: lint * fix: lint * feat: page leave safety * fix: lint * fix: cargo fmt fix * fix: blank in prod * fix: content card table stuff * Revert "fix: blank in prod" This reverts commit 74758fe185cf85a4a20355857f889cb091b97ace. * fix: import * feat: browse worlds/servers flow * fix: worlds tab parity with content tab * fix: perf bug + shader filter pill copy * feat: singleplayer filter * fix: ordering * fix: breadcrumbs * fix: lint * fix: qa * feat: store server proj id when adding to a non-linked instance * fix: lint * fix: i18n + qa * fix: conflict * qa: already installed modal + placeholders not server-specific * fix: qa * fix: add + edit server modals * fix: qa * fix: security * fix: devin flags * fix: lint * chore: change file to break build cache * fix: admon * fix: import path stuff * feat: qa * fix: fmt fmt idiot --------- Signed-off-by: Calum H. <calum@modrinth.com>
This commit is contained in:
+73
-27
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
BoxIcon,
|
||||
FilterIcon,
|
||||
GlassesIcon,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
SearchIcon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import Fuse from 'fuse.js'
|
||||
import { computed, nextTick, ref, watchSyncEffect } from 'vue'
|
||||
|
||||
@@ -18,7 +18,12 @@ import type { Option as OverflowMenuOption } from '#ui/components/base/OverflowM
|
||||
import StyledInput from '#ui/components/base/StyledInput.vue'
|
||||
import NewModal from '#ui/components/modal/NewModal.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
import {
|
||||
commonMessages,
|
||||
commonProjectTypeCategoryMessages,
|
||||
commonProjectTypeTitleMessages,
|
||||
normalizeProjectType,
|
||||
} from '#ui/utils/common-messages'
|
||||
|
||||
import { isClientOnlyEnvironment } from '../../composables/content-filtering'
|
||||
import type { ContentCardTableItem, ContentItem } from '../../types'
|
||||
@@ -32,6 +37,7 @@ interface Props {
|
||||
modpackIconUrl?: string
|
||||
enableToggle?: boolean
|
||||
getOverflowOptions?: (item: ContentItem) => OverflowMenuOption[]
|
||||
switchVersion?: (item: ContentItem) => void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -39,6 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
modpackIconUrl: undefined,
|
||||
enableToggle: false,
|
||||
getOverflowOptions: undefined,
|
||||
switchVersion: undefined,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -72,14 +79,6 @@ const messages = defineMessages({
|
||||
id: 'instances.modpack-content-modal.no-results',
|
||||
defaultMessage: 'No projects match your search.',
|
||||
},
|
||||
allFilter: {
|
||||
id: 'instances.modpack-content-modal.filter-all',
|
||||
defaultMessage: 'All',
|
||||
},
|
||||
copyLink: {
|
||||
id: 'instances.modpack-content-modal.copy-link',
|
||||
defaultMessage: 'Copy link',
|
||||
},
|
||||
})
|
||||
|
||||
export interface ModpackContentModalState {
|
||||
@@ -133,25 +132,36 @@ watchSyncEffect(() => fuse.setCollection(items.value))
|
||||
const filterOptions = computed(() => {
|
||||
const frequency = items.value.reduce(
|
||||
(map, item) => {
|
||||
map[item.project_type] = (map[item.project_type] || 0) + 1
|
||||
const normalized = normalizeProjectType(item.project_type)
|
||||
map[normalized] = (map[normalized] || 0) + 1
|
||||
return map
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
)
|
||||
|
||||
// Sort by frequency (most common first)
|
||||
return Object.entries(frequency)
|
||||
const options = Object.entries(frequency)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([type]) => ({
|
||||
id: type,
|
||||
label: formatProjectType(type) + 's',
|
||||
}))
|
||||
.map(([type]) => {
|
||||
const msg =
|
||||
commonProjectTypeCategoryMessages[type as keyof typeof commonProjectTypeCategoryMessages]
|
||||
return {
|
||||
id: type,
|
||||
label: msg ? formatMessage(msg) : type.charAt(0).toUpperCase() + type.slice(1) + 's',
|
||||
}
|
||||
})
|
||||
|
||||
if (items.value.some((item) => !item.enabled)) {
|
||||
options.push({ id: 'disabled', label: 'Disabled' })
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const stats = computed(() => {
|
||||
const counts: Record<string, number> = {}
|
||||
for (const item of items.value) {
|
||||
counts[item.project_type] = (counts[item.project_type] || 0) + 1
|
||||
const normalized = normalizeProjectType(item.project_type)
|
||||
counts[normalized] = (counts[normalized] || 0) + 1
|
||||
}
|
||||
return counts
|
||||
})
|
||||
@@ -165,9 +175,18 @@ function toggleFilter(filterId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const attributeFilterIds = new Set(['disabled'])
|
||||
|
||||
const typeFilteredCount = computed(() => {
|
||||
if (selectedFilters.value.length === 0) return items.value.length
|
||||
return items.value.filter((item) => selectedFilters.value.includes(item.project_type)).length
|
||||
const typeFilters = selectedFilters.value.filter((f) => !attributeFilterIds.has(f))
|
||||
const hasDisabledFilter = selectedFilters.value.includes('disabled')
|
||||
return items.value.filter((item) => {
|
||||
if (typeFilters.length > 0 && !typeFilters.includes(normalizeProjectType(item.project_type)))
|
||||
return false
|
||||
if (hasDisabledFilter && item.enabled) return false
|
||||
return true
|
||||
}).length
|
||||
})
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
@@ -184,9 +203,15 @@ const filteredItems = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// Apply type filters
|
||||
if (selectedFilters.value.length > 0) {
|
||||
result = result.filter((item) => selectedFilters.value.includes(item.project_type))
|
||||
const typeFilters = selectedFilters.value.filter((f) => !attributeFilterIds.has(f))
|
||||
const hasDisabledFilter = selectedFilters.value.includes('disabled')
|
||||
result = result.filter((item) => {
|
||||
if (typeFilters.length > 0 && !typeFilters.includes(normalizeProjectType(item.project_type)))
|
||||
return false
|
||||
if (hasDisabledFilter && item.enabled) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -216,7 +241,18 @@ const tableItems = computed<ContentCardTableItem[]>(() =>
|
||||
...(props.enableToggle ? { enabled: item.enabled } : {}),
|
||||
isClientOnly: isClientOnlyEnvironment(item.environment),
|
||||
disabled: disabledIds.value.has(item.file_name),
|
||||
overflowOptions: props.getOverflowOptions?.(item),
|
||||
overflowOptions: [
|
||||
...(props.switchVersion
|
||||
? [
|
||||
{
|
||||
id: formatMessage(commonMessages.switchVersionButton),
|
||||
icon: ArrowLeftRightIcon,
|
||||
action: () => props.switchVersion!(item),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(props.getOverflowOptions?.(item) ?? []),
|
||||
],
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -344,7 +380,7 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem })
|
||||
/>
|
||||
|
||||
<!-- Filters -->
|
||||
<div v-if="filterOptions.length > 1" class="flex items-center gap-2">
|
||||
<div v-if="filterOptions.length > 0" class="flex items-center gap-2">
|
||||
<FilterIcon class="size-5 text-secondary shrink-0" />
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<button
|
||||
@@ -357,7 +393,7 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem })
|
||||
"
|
||||
@click="selectedFilters = []"
|
||||
>
|
||||
{{ formatMessage(messages.allFilter) }}
|
||||
{{ formatMessage(commonMessages.allProjectType) }}
|
||||
</button>
|
||||
<button
|
||||
v-for="option in filterOptions"
|
||||
@@ -416,7 +452,7 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem })
|
||||
class="flex min-w-0 items-center gap-4"
|
||||
:class="
|
||||
props.enableToggle
|
||||
? 'flex-1 @[800px]:w-[350px] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
? 'flex-1 @[800px]:w-[45%] @[800px]:shrink-0 @[800px]:flex-none'
|
||||
: 'flex-1'
|
||||
"
|
||||
>
|
||||
@@ -434,7 +470,7 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem })
|
||||
</div>
|
||||
<div
|
||||
class="hidden @[800px]:flex"
|
||||
:class="props.enableToggle ? 'w-[335px] min-w-0' : 'flex-1'"
|
||||
:class="props.enableToggle ? 'flex-1 min-w-0' : 'flex-1'"
|
||||
>
|
||||
<span class="font-semibold text-secondary">{{
|
||||
formatMessage(commonMessages.versionLabel)
|
||||
@@ -475,7 +511,17 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem })
|
||||
<div class="flex items-center gap-1.5">
|
||||
<component :is="getTypeIcon(type as string)" class="size-5 text-secondary" />
|
||||
<span class="font-medium text-primary">
|
||||
{{ count }} {{ formatProjectType(type as string) }}{{ count !== 1 ? 's' : '' }}
|
||||
{{ count }}
|
||||
{{
|
||||
formatMessage(
|
||||
commonProjectTypeTitleMessages[
|
||||
normalizeProjectType(
|
||||
type as string,
|
||||
) as keyof typeof commonProjectTypeTitleMessages
|
||||
] ?? commonProjectTypeTitleMessages.project,
|
||||
{ count },
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user