Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fd28721
Begin external projects moderator database frontend
Prospector Mar 28, 2026
804f6ca
add copy link button
Prospector Mar 28, 2026
2d2598b
begin project page permissions settings
Prospector Apr 3, 2026
873a12b
MEL database backend routes
aecsocket Apr 6, 2026
81abcf6
include filename in external files
aecsocket Apr 19, 2026
e1f44b8
wip: when uploading a version file, fetch its overrides as a list
aecsocket Apr 22, 2026
bc9e586
wip: override license checks
aecsocket Apr 23, 2026
8d4076f
improve FileHost ref counting
aecsocket Apr 23, 2026
d846ef7
file host read capability
aecsocket Apr 23, 2026
15f67ea
scan files when inserting version file
aecsocket Apr 23, 2026
796049f
Merge branch 'main' into boris/dev-886-modpack-project
aecsocket May 1, 2026
e33b1dc
add dependency sha1 field
aecsocket May 1, 2026
220733f
clean up version files
aecsocket May 2, 2026
b93f6c6
wip: attributions
aecsocket May 2, 2026
f01d149
update s3 file host
aecsocket May 2, 2026
d983f11
attribution scanning basic works
aecsocket May 2, 2026
1b26f55
works
aecsocket May 2, 2026
e30cf26
insert attribution info after resolving
aecsocket May 3, 2026
d53d5c4
Merge branch 'main' into boris/dev-886-modpack-project
aecsocket May 3, 2026
bfd5924
add routes
aecsocket May 3, 2026
a0471da
Merge remote-tracking branch 'origin/main' into boris/dev-886-modpack…
Prospector May 5, 2026
91d74e7
Merge branch 'main' into boris/dev-886-modpack-project
aecsocket May 6, 2026
84888dc
remove dep sha1 stuff
aecsocket May 6, 2026
1fdaf8e
prepr
Prospector May 7, 2026
9819611
wip: override file sources
aecsocket May 7, 2026
2ee217b
add files_missing_attributions to versions
aecsocket May 7, 2026
b6b5271
return extended version info + attributed at/by
aecsocket May 7, 2026
867aadc
hook up frontend to backend (mostly)
Prospector May 9, 2026
0d488d1
Merge remote-tracking branch 'origin/main' into boris/dev-886-modpack…
Prospector May 9, 2026
c07b7af
expose version date published
aecsocket May 9, 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
4 changes: 4 additions & 0 deletions apps/frontend/.env.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
BASE_URL=http://127.0.0.1:8000/v2/
BROWSER_BASE_URL=http://127.0.0.1:8000/v2/
PYRO_BASE_URL=https://staging-archon.modrinth.com
PROD_OVERRIDE=true
3 changes: 3 additions & 0 deletions apps/frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -2663,6 +2663,9 @@
"project.settings.permissions.learn-more": {
"message": "Learn more"
},
"project.settings.permissions.no-results": {
"message": "No external projects match your search."
},
"project.settings.permissions.search-placeholder": {
"message": "Search {count} {count, plural, one {external project} other {external projects}}..."
},
Expand Down
181 changes: 149 additions & 32 deletions apps/frontend/src/pages/[type]/[id]/settings/permissions.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { RightArrowIcon, SearchIcon, SortAscIcon, SortDescIcon } from '@modrinth/assets'
import {
Admonition,
Expand All @@ -8,15 +9,20 @@ import {
commonMessages,
defineMessages,
EmptyState,
ExternalProjectPermissionsCard,
injectModrinthClient,
injectProjectPageContext,
IntlFormatted,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import ExternalProjectPermissionsCard from '@modrinth/ui/src/components/external_files/ExternalProjectPermissionsCard.vue'
import { ref } from 'vue'
import { useQuery } from '@tanstack/vue-query'
import { computed, ref } from 'vue'

const { formatMessage } = useVIntl()
const flags = useFeatureFlags()
const { projectV2: project } = injectProjectPageContext()
const { labrinth } = injectModrinthClient()

if (!flags.value.modpackPermissionsPage) {
throw createError({
Expand All @@ -25,14 +31,75 @@ if (!flags.value.modpackPermissionsPage) {
})
}

const externalFiles = ref([{}])
type SortType = 'Oldest' | 'Newest'

const searchQuery = ref('')
const currentSortType = ref('Oldest')
const currentSortType = ref<SortType>('Oldest')

const {
data: attributionData,
error: attributionError,
isPending: pending,
} = useQuery({
queryKey: ['project-attribution', project.value.id],
queryFn: () => labrinth.attribution_internal.listProjectAttribution(project.value.id),
})

const sortTypes: ComboboxOption<string>[] = [
const sortTypes: ComboboxOption<SortType>[] = [
{ value: 'Oldest', label: 'Oldest' },
{ value: 'Newest', label: 'Newest' },
]

function isAttributed(group: Labrinth.Attribution.Internal.AttributionGroup): boolean {
return group.attribution !== null && group.attribution !== undefined
}

function isNoPermission(group: Labrinth.Attribution.Internal.AttributionGroup): boolean {
const a = group.attribution
if (!a || typeof a !== 'object') return false
return (a as { type?: string }).type === 'no_permission'
}

const filteredGroups = computed(() => {
const groups = attributionData.value ?? []
const query = searchQuery.value.trim().toLowerCase()
const filtered = query
? groups.filter((group) => {
if (group.flame_project_title?.toLowerCase().includes(query)) return true
return (group.files ?? []).some((file) => file.name.toLowerCase().includes(query))
})
: [...groups]
const direction = currentSortType.value === 'Newest' ? -1 : 1
filtered.sort((a, b) => {
const aTime = a.attributed_at ? Date.parse(a.attributed_at) : 0
const bTime = b.attributed_at ? Date.parse(b.attributed_at) : 0
if (aTime !== bTime) return (aTime - bTime) * direction
return a.id.localeCompare(b.id) * direction
})
return filtered
})

const totalGroups = computed(() => attributionData.value?.length ?? 0)

const stats = computed(() => {
const groups = attributionData.value ?? []
let attributed = 0
let pending = 0
let noPermission = 0
for (const group of groups) {
if (isNoPermission(group)) {
noPermission++
} else if (isAttributed(group)) {
attributed++
} else {
pending++
}
}
return { total: groups.length, attributed, pending, noPermission }
})

const projectIsApproved = computed(() => project.value.status === 'approved')

const messages = defineMessages({
searchPlaceholder: {
id: 'project.settings.permissions.search-placeholder',
Expand Down Expand Up @@ -87,6 +154,10 @@ const messages = defineMessages({
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.`,
},
noResults: {
id: 'project.settings.permissions.no-results',
defaultMessage: 'No external projects match your search.',
},
})

function dismissInfoBanner() {
Expand All @@ -95,44 +166,59 @@ function dismissInfoBanner() {
}
</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>
<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>
</Admonition>
</IntlFormatted>
<template #actions>
<div class="flex">
<ButtonStyled color="blue">
<a> {{ formatMessage(messages.learnMore) }} <RightArrowIcon /> </a>
</ButtonStyled>
</div>
</template>
</Admonition>
<template v-if="pending">
<div class="flex flex-col gap-3">
<div
v-for="i in 3"
:key="i"
class="h-[56px] w-full animate-pulse rounded-2xl bg-surface-3"
></div>
</div>
</template>
<template v-else-if="totalGroups > 0">
<Admonition
v-if="true"
v-if="stats.pending === 0 && stats.noPermission === 0"
type="success"
class="mb-4"
:header="formatMessage(messages.completedTitle)"
:body="formatMessage(messages.completedDescription)"
/>
<Admonition
v-if="true"
v-if="stats.pending > 0"
type="warning"
class="mb-4"
:header="formatMessage(messages.attentionNeededTitle)"
:body="formatMessage(messages.attentionNeededDescriptionDraft)"
:body="
formatMessage(
projectIsApproved
? messages.attentionNeededDescriptionApproved
: messages.attentionNeededDescriptionDraft,
)
"
/>
<Admonition
v-if="true"
v-if="stats.noPermission > 0"
type="critical"
class="mb-4"
:header="formatMessage(messages.failTitle)"
Expand All @@ -144,7 +230,7 @@ function dismissInfoBanner() {
type="search"
:placeholder="
formatMessage(messages.searchPlaceholder, {
count: externalFiles.length,
count: totalGroups,
})
"
:icon="SearchIcon"
Expand All @@ -171,8 +257,24 @@ function dismissInfoBanner() {
</div>
</div>
<div class="mt-4 flex flex-col gap-3">
<ExternalProjectPermissionsCard title="FTB Library" />
<TransitionGroup name="list">
<ExternalProjectPermissionsCard
v-for="group in filteredGroups"
:key="group.id"
:project-id="project.id"
:group="group"
/>
<EmptyState
v-if="filteredGroups.length === 0"
:heading="formatMessage(messages.noResults)"
type="no-search-result"
/>
</TransitionGroup>
</div>

<p v-if="attributionError" class="mt-4 text-sm text-red">
{{ String(attributionError) }}
</p>
</template>
<template v-else>
<EmptyState
Expand All @@ -182,3 +284,18 @@ function dismissInfoBanner() {
/>
</template>
</template>
<style scoped>
.list-enter-from {
opacity: 0;
transform: translateY(-10px);
}

.list-leave-to {
opacity: 0;
transform: translateY(10px);
}

.list-move {
transition: transform 200ms ease-in-out;
}
</style>
4 changes: 3 additions & 1 deletion apps/frontend/src/pages/[type]/[id]/settings/versions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,9 @@ async function deleteVersion() {
stopLoading()
}

const withheldVersions = computed(() => ['4.0.0'])
const withheldVersions = computed(() =>
versions.value.filter((x) => x.files_missing_attribution?.length > 0),
)

const messages = defineMessages({
withheldVersionsWarningTitle: {
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading