diff --git a/apps/frontend/src/locales/en-US/index.json b/apps/frontend/src/locales/en-US/index.json index 705bdcb470..53c900ca4f 100644 --- a/apps/frontend/src/locales/en-US/index.json +++ b/apps/frontend/src/locales/en-US/index.json @@ -3494,6 +3494,15 @@ "project.actions.servers-promo.title": { "message": "Create a server" }, + "project.actions.view-dependents": { + "message": "View dependents" + }, + "project.actions.view-modpacks": { + "message": "View modpacks" + }, + "project.actions.view-project-type-dependents": { + "message": "View {projectType} dependents" + }, "project.ai-image-warning-modal.body.1": { "message": "Using AI-generated images to represent your project is not allowed. Attempting to work around detection may lead to your Modrinth account being suspended." }, diff --git a/apps/frontend/src/pages/[type]/[project].vue b/apps/frontend/src/pages/[type]/[project].vue index 26a8e90924..89800df9e5 100644 --- a/apps/frontend/src/pages/[type]/[project].vue +++ b/apps/frontend/src/pages/[type]/[project].vue @@ -564,10 +564,12 @@ import { HeartIcon, LeftArrowIcon, MoreVerticalIcon, + PackageSearchIcon, PlayIcon, ReportIcon, ScaleIcon, ScanEyeIcon, + SearchIcon, ServerPlusIcon, SettingsIcon, XIcon, @@ -582,6 +584,8 @@ import { ButtonLink, commonMessages, defineMessages, + formatDependencyProjectFilterOption, + formatProjectTypeSentence, getActiveDisclosures, IconButton, injectModrinthClient, @@ -846,6 +850,18 @@ const messages = defineMessages({ id: 'project.actions.review-project', defaultMessage: 'Review project', }, + viewDependents: { + id: 'project.actions.view-dependents', + defaultMessage: 'View dependents', + }, + viewProjectTypeDependents: { + id: 'project.actions.view-project-type-dependents', + defaultMessage: 'View {projectType} dependents', + }, + viewModpacks: { + id: 'project.actions.view-modpacks', + defaultMessage: 'View modpacks', + }, rescanModpack: { id: 'project.actions.rescan-modpack', defaultMessage: 'Rescan modpack', @@ -1904,6 +1920,30 @@ async function checkModpackArchives() { const projectHeaderMoreActions = computed(() => { const isStaff = !!(auth.value.user && tags.value.staffRoles.includes(auth.value.user.role)) + const projectId = project.value?.id + const dependentSearchTypes = getDependentSearchTypes() + const dependentSearchActions = dependentSearchTypes + .filter((projectType) => projectType !== 'modpack') + .map((projectType) => ({ + id: `view-${projectType}-dependents`, + label: formatMessage( + dependentSearchTypes.length === 1 + ? messages.viewDependents + : messages.viewProjectTypeDependents, + { + projectType: formatProjectTypeSentence(formatMessage, projectType), + }, + ), + icon: SearchIcon, + type: 'link', + to: { + path: `/discover/${projectType}s`, + query: { + dep: formatDependencyProjectFilterOption(projectId, ['required']), + }, + }, + })) + const isPluginOnly = dependentSearchTypes.length === 1 && dependentSearchTypes[0] === 'plugin' return [ { @@ -1914,7 +1954,21 @@ const projectHeaderMoreActions = computed(() => { to: `${projectPath.value}/settings/analytics`, shown: !!auth.value.user && !!currentMember.value, }, - { type: 'divider', shown: !!auth.value.user && !!currentMember.value }, + ...dependentSearchActions, + { + id: 'view-modpacks', + label: formatMessage(messages.viewModpacks), + icon: PackageSearchIcon, + type: 'link', + to: { + path: '/discover/modpacks', + query: { + dep: formatDependencyProjectFilterOption(projectId, ['required']), + }, + }, + shown: !isPluginOnly && project.value?.actualProjectType !== 'modpack', + }, + { type: 'divider' }, { id: 'moderation-checklist', label: formatMessage(messages.reviewProject), @@ -1977,6 +2031,29 @@ const projectHeaderMoreActions = computed(() => { ] }) +function getDependentSearchTypes() { + if (!project.value) return [] + + if (project.value.actualProjectType !== 'mod') { + return [isServerProject.value ? 'server' : project.value.actualProjectType] + } + + const loaders = project.value.loaders ?? [] + const projectTypes = [] + + if (loaders.some((loader) => tags.value.loaderData.modLoaders.includes(loader))) { + projectTypes.push('mod') + } + if (loaders.some((loader) => tags.value.loaderData.allPluginLoaders.includes(loader))) { + projectTypes.push('plugin') + } + if (loaders.some((loader) => tags.value.loaderData.dataPackLoaders.includes(loader))) { + projectTypes.push('datapack') + } + + return projectTypes.length > 0 ? projectTypes : ['mod'] +} + const createCanonicalUrl = () => project.value ? `https://modrinth.com/project/${project.value.id}` : undefined diff --git a/packages/assets/generated-icons.ts b/packages/assets/generated-icons.ts index 0bdf625b16..b92e881e3a 100644 --- a/packages/assets/generated-icons.ts +++ b/packages/assets/generated-icons.ts @@ -213,6 +213,7 @@ import _PackageIcon from './icons/package.svg?component' import _PackageClosedIcon from './icons/package-closed.svg?component' import _PackageOpenIcon from './icons/package-open.svg?component' import _PackagePlusIcon from './icons/package-plus.svg?component' +import _PackageSearchIcon from './icons/package-search.svg?component' import _PageRoundIcon from './icons/page-round.svg?component' import _PaintbrushIcon from './icons/paintbrush.svg?component' import _PaletteIcon from './icons/palette.svg?component' @@ -656,6 +657,7 @@ export const PackageIcon = _PackageIcon export const PackageClosedIcon = _PackageClosedIcon export const PackageOpenIcon = _PackageOpenIcon export const PackagePlusIcon = _PackagePlusIcon +export const PackageSearchIcon = _PackageSearchIcon export const PageRoundIcon = _PageRoundIcon export const PaintbrushIcon = _PaintbrushIcon export const PaletteIcon = _PaletteIcon diff --git a/packages/assets/icons/package-search.svg b/packages/assets/icons/package-search.svg new file mode 100644 index 0000000000..4b5f1eaa0f --- /dev/null +++ b/packages/assets/icons/package-search.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/packages/ui/src/components/base/Combobox.vue b/packages/ui/src/components/base/Combobox.vue index eddd74c20c..b25d7adfd3 100644 --- a/packages/ui/src/components/base/Combobox.vue +++ b/packages/ui/src/components/base/Combobox.vue @@ -1,10 +1,14 @@