Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6243c99
Begin external projects moderator database frontend
Prospector Mar 28, 2026
9da9b42
add copy link button
Prospector Mar 28, 2026
0d7e529
Merge remote-tracking branch 'origin/main' into prospector/external-p…
Prospector Apr 3, 2026
9798d55
begin project page permissions settings
Prospector Apr 3, 2026
35756e4
MEL database backend routes
aecsocket Apr 6, 2026
42052f8
include filename in external files
aecsocket Apr 19, 2026
8d55803
Hook up frontend external license page to backend
aecsocket Apr 24, 2026
9fb40d3
more work on user-facing external projects stuff
Prospector Apr 25, 2026
a8ea2a2
put user-facing stuff behind feature flag
Prospector Apr 25, 2026
639f658
Merge remote-tracking branch 'origin/main' into prospector/external-p…
Prospector Apr 29, 2026
8366af9
prepr
Prospector Apr 29, 2026
d589d63
Merge branch 'main' into prospector/external-projects-frontend
Prospector Apr 29, 2026
b4d27b0
Merge branch 'main' into prospector/external-projects-frontend
Prospector May 1, 2026
1009ab7
Merge branch 'main' into prospector/external-projects-frontend
Prospector May 1, 2026
7653100
Merge branch 'main' into prospector/external-projects-frontend
Prospector May 2, 2026
f0daced
Merge branch 'main' into prospector/external-projects-frontend
Prospector May 3, 2026
e85dfe7
Merge remote-tracking branch 'origin/main' into prospector/external-p…
Prospector May 3, 2026
25ea918
clippy
aecsocket May 3, 2026
aabbaf7
Merge branch 'main' into prospector/external-projects-frontend
Prospector May 3, 2026
3bce0e1
Merge branch 'main' into prospector/external-projects-frontend
Prospector May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/frontend/src/composables/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
showDiscoverProjectButtons: false,
useV1ContentTabAPI: true,
labrinthApiCanary: false,
dismissedExternalProjectsInfo: false,
modpackPermissionsPage: false,
} as const)

export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
Expand Down
13 changes: 13 additions & 0 deletions apps/frontend/src/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@
color: 'orange',
link: '/moderation/reports',
},
{
id: 'external-projects',
color: 'orange',
link: '/moderation/external-projects',
},
{
divider: true,
},
Expand Down Expand Up @@ -377,6 +382,9 @@
<template #review-reports>
<ReportIcon aria-hidden="true" /> {{ formatMessage(messages.reports) }}
</template>
<template #external-projects>
<GlobeIcon aria-hidden="true" /> {{ formatMessage(messages.externalProjects) }}
</template>
<template #user-lookup>
<UserSearchIcon aria-hidden="true" /> {{ formatMessage(messages.lookupByEmail) }}
</template>
Expand Down Expand Up @@ -705,6 +713,7 @@ import {
DropdownIcon,
FileIcon,
GlassesIcon,
GlobeIcon,
HamburgerIcon,
HomeIcon,
IssuesIcon,
Expand Down Expand Up @@ -880,6 +889,10 @@ const messages = defineMessages({
id: 'layout.action.reports',
defaultMessage: 'Review reports',
},
externalProjects: {
id: 'layout.action.external-projects',
defaultMessage: 'External projects',
},
lookupByEmail: {
id: 'layout.action.lookup-by-email',
defaultMessage: 'Lookup by email',
Expand Down
56 changes: 55 additions & 1 deletion apps/frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -1670,6 +1670,9 @@
"layout.action.create-new": {
"message": "Create new..."
},
"layout.action.external-projects": {
"message": "External projects"
},
"layout.action.file-lookup": {
"message": "File lookup"
},
Expand Down Expand Up @@ -1922,14 +1925,17 @@
"moderation.moderate": {
"message": "Moderate"
},
"moderation.page.external-projects": {
"message": "External projects"
},
"moderation.page.projects": {
"message": "Projects"
},
"moderation.page.reports": {
"message": "Reports"
},
"moderation.page.technicalReview": {
"message": "Technical Review"
"message": "Tech review"
},
"muralpay.account-type.checking": {
"message": "Checking"
Expand Down Expand Up @@ -2615,6 +2621,45 @@
"project.settings.general.url.title": {
"message": "URL"
},
"project.settings.permissions.attention-needed.description.proj-approved": {
"message": "Please provide proof that you have permission to redistribute all of the following files and any withheld versions will be automatically published."
},
"project.settings.permissions.attention-needed.description.proj-draft": {
"message": "Please provide proof that you have permission to redistribute all of the following files before you can submit your project for review."
},
"project.settings.permissions.attention-needed.title": {
"message": "Unknown embedded content"
},
"project.settings.permissions.completed.description": {
"message": "All external content has attributions provided."
},
"project.settings.permissions.completed.title": {
"message": "Attributions completed!"
},
"project.settings.permissions.empty-state.description": {
"message": "None of your versions contain external content, so you don't need to worry about obtaining permissions."
},
"project.settings.permissions.empty-state.heading": {
"message": "You're all set!"
},
"project.settings.permissions.fail.description": {
"message": "You don't have permission to redistribute some of the external content you've added. In order to publish on Modrinth, remove the infringing content."
},
"project.settings.permissions.fail.title": {
"message": "Some content can't be included"
},
"project.settings.permissions.info-banner.description": {
"message": "If you include content that isn’t hosted on Modrinth, you need to let us know where it’s from and verify that you have permission to distribute the files. Check out <link>our guide</link> to learn about how to do this properly!"
},
"project.settings.permissions.info-banner.title": {
"message": "Learn how attributions work"
},
"project.settings.permissions.learn-more": {
"message": "Learn more"
},
"project.settings.permissions.search-placeholder": {
"message": "Search {count} {count, plural, one {external project} other {external projects}}..."
},
"project.settings.title": {
"message": "Settings"
},
Expand All @@ -2633,6 +2678,15 @@
"project.versions.title": {
"message": "Versions"
},
"project.versions.withheld-versions-warning.description": {
"message": "{count, plural, one {This version is} other {These versions are}} currently withheld and not publicly listed. Please provide proof that you have permission to redistribute certain files included in the modpack {count, plural, one {version} other {versions}}."
},
"project.versions.withheld-versions-warning.resolve-button": {
"message": "Resolve"
},
"project.versions.withheld-versions-warning.title": {
"message": "{count, plural, one {Version {version_name}} other {Versions}} withheld due to unknown embedded content"
},
"report.already-reported": {
"message": "You've already reported {title}"
},
Expand Down
12 changes: 12 additions & 0 deletions apps/frontend/src/pages/[type]/[id]/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
InfoIcon,
LinkIcon,
ServerIcon,
SignatureIcon,
TagsIcon,
UsersIcon,
VersionIcon,
Expand Down Expand Up @@ -46,6 +47,12 @@ const navItems = computed(() => {
projectV3.value?.project_types?.some((type) => ['mod', 'modpack'].includes(type)) &&
isStaff(currentMember.value?.user)

const hasPermissionsPage = computed(
() =>
flags.value.modpackPermissionsPage &&
projectV3.value?.project_types?.some((type) => ['modpack'].includes(type)),
)

const items = [
{
link: `/${base}/settings`,
Expand Down Expand Up @@ -75,6 +82,11 @@ const navItems = computed(() => {
label: formatMessage(commonProjectSettingsMessages.description),
icon: AlignLeftIcon,
},
hasPermissionsPage.value && {
link: `/${base}/settings/permissions`,
label: formatMessage(commonProjectSettingsMessages.permissions),
icon: SignatureIcon,
},
!isServerProject.value && {
link: `/${base}/settings/versions`,
label: formatMessage(commonProjectSettingsMessages.versions),
Expand Down
184 changes: 184 additions & 0 deletions apps/frontend/src/pages/[type]/[id]/settings/permissions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<script setup lang="ts">
import { RightArrowIcon, SearchIcon, SortAscIcon, SortDescIcon } from '@modrinth/assets'
import {
Admonition,
ButtonStyled,
Combobox,
type ComboboxOption,
commonMessages,
defineMessages,
EmptyState,
IntlFormatted,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import ExternalProjectPermissionsCard from '@modrinth/ui/src/components/external_files/ExternalProjectPermissionsCard.vue'
import { ref } from 'vue'

const { formatMessage } = useVIntl()
const flags = useFeatureFlags()

if (!flags.value.modpackPermissionsPage) {
throw createError({
fatal: true,
statusCode: 404,
})
}

const externalFiles = ref([{}])
const searchQuery = ref('')
const currentSortType = ref('Oldest')

const sortTypes: ComboboxOption<string>[] = [
{ value: 'Oldest', label: 'Oldest' },
{ value: 'Newest', label: 'Newest' },
]
const messages = defineMessages({
searchPlaceholder: {
id: 'project.settings.permissions.search-placeholder',
defaultMessage:
'Search {count} {count, plural, one {external project} other {external projects}}...',
},
infoBannerTitle: {
id: 'project.settings.permissions.info-banner.title',
defaultMessage: 'Learn how attributions work',
},
infoBannerDescription: {
id: 'project.settings.permissions.info-banner.description',
defaultMessage: `If you include content that isn’t hosted on Modrinth, you need to let us know where it’s from and verify that you have permission to distribute the files. Check out <link>our guide</link> to learn about how to do this properly!`,
},
learnMore: {
id: 'project.settings.permissions.learn-more',
defaultMessage: 'Learn more',
},
emptyStateHeading: {
id: 'project.settings.permissions.empty-state.heading',
defaultMessage: `You're all set!`,
},
emptyStateDescription: {
id: 'project.settings.permissions.empty-state.description',
defaultMessage: `None of your versions contain external content, so you don't need to worry about obtaining permissions.`,
},
completedTitle: {
id: 'project.settings.permissions.completed.title',
defaultMessage: `Attributions completed!`,
},
completedDescription: {
id: 'project.settings.permissions.completed.description',
defaultMessage: 'All external content has attributions provided.',
},
failTitle: {
id: 'project.settings.permissions.fail.title',
defaultMessage: `Some content can't be included`,
},
failDescription: {
id: 'project.settings.permissions.fail.description',
defaultMessage: `You don't have permission to redistribute some of the external content you've added. In order to publish on Modrinth, remove the infringing content.`,
},
attentionNeededTitle: {
id: 'project.settings.permissions.attention-needed.title',
defaultMessage: `Unknown embedded content`,
},
attentionNeededDescriptionApproved: {
id: 'project.settings.permissions.attention-needed.description.proj-approved',
defaultMessage: `Please provide proof that you have permission to redistribute all of the following files and any withheld versions will be automatically published.`,
},
attentionNeededDescriptionDraft: {
id: 'project.settings.permissions.attention-needed.description.proj-draft',
defaultMessage: `Please provide proof that you have permission to redistribute all of the following files before you can submit your project for review.`,
},
})

function dismissInfoBanner() {
flags.value.dismissedExternalProjectsInfo = true
saveFeatureFlags()
}
</script>
<template>
<template v-if="externalFiles.length > 0">
<Admonition
v-if="!flags.dismissedExternalProjectsInfo"
type="info"
class="mb-4"
:header="formatMessage(messages.infoBannerTitle)"
dismissible
@dismiss="dismissInfoBanner"
>
<IntlFormatted :message-id="messages.infoBannerDescription">
<template #link="{ children }">
<a class="text-link" target="_blank"> <component :is="() => children" /> </a>
</template>
</IntlFormatted>
<template #actions>
<div class="flex">
<ButtonStyled color="blue">
<a> {{ formatMessage(messages.learnMore) }} <RightArrowIcon /> </a>
</ButtonStyled>
</div>
</template>
</Admonition>
<Admonition
v-if="true"
type="success"
class="mb-4"
:header="formatMessage(messages.completedTitle)"
:body="formatMessage(messages.completedDescription)"
/>
<Admonition
v-if="true"
type="warning"
class="mb-4"
:header="formatMessage(messages.attentionNeededTitle)"
:body="formatMessage(messages.attentionNeededDescriptionDraft)"
/>
<Admonition
v-if="true"
type="critical"
class="mb-4"
:header="formatMessage(messages.failTitle)"
:body="formatMessage(messages.failDescription)"
/>
<div class="grid grid-cols-[1fr_auto] gap-2">
<StyledInput
v-model="searchQuery"
type="search"
:placeholder="
formatMessage(messages.searchPlaceholder, {
count: externalFiles.length,
})
"
:icon="SearchIcon"
input-class="h-[40px]"
/>
<div>
<Combobox
v-model="currentSortType"
class="!w-full flex-grow sm:!w-[150px] sm:flex-grow-0 lg:!w-[150px]"
:options="sortTypes"
:placeholder="formatMessage(commonMessages.sortByLabel)"
>
<template #selected>
<span class="flex flex-row gap-2 align-middle font-semibold">
<SortAscIcon
v-if="currentSortType === 'Oldest'"
class="size-5 flex-shrink-0 text-secondary"
/>
<SortDescIcon v-else class="size-5 flex-shrink-0 text-secondary" />
<span class="truncate text-contrast">{{ currentSortType }}</span>
</span>
</template>
</Combobox>
</div>
</div>
<div class="mt-4 flex flex-col gap-3">
<ExternalProjectPermissionsCard title="FTB Library" />
</div>
</template>
<template v-else>
<EmptyState
:heading="formatMessage(messages.emptyStateHeading)"
:description="formatMessage(messages.emptyStateDescription)"
type="done"
/>
</template>
</template>
Loading
Loading