Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions apps/frontend/src/composables/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
showDiscoverProjectButtons: false,
useV1ContentTabAPI: true,
labrinthApiCanary: false,
dismissedExternalProjectsInfo: 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 @@ -877,6 +886,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
47 changes: 46 additions & 1 deletion apps/frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -1748,17 +1748,62 @@
"layout.nav.upgrade-to-modrinth-plus": {
"message": "Upgrade to Modrinth+"
},
"moderation.external-projects.empty-description": {
"message": "Type at least 3 characters of a project’s title to begin browsing."
},
"moderation.external-projects.empty-flame-id-description": {
"message": "Type the numeric project ID, then use lookup for an exact match."
},
"moderation.external-projects.empty-flame-id-title": {
"message": "Enter a CurseForge project ID"
},
"moderation.external-projects.empty-sha1-description": {
"message": "Paste the full 40-character hex hash, then use lookup for an exact match."
},
"moderation.external-projects.empty-sha1-title": {
"message": "Enter a SHA-1 hash"
},
"moderation.external-projects.empty-title": {
"message": "Enter a search term to get started"
},
"moderation.external-projects.lookup-curseforge-id": {
"message": "Lookup CurseForge ID"
},
"moderation.external-projects.lookup-sha1-hash": {
"message": "Lookup SHA-1"
},
"moderation.external-projects.no-results-by-flame-id": {
"message": "No external project has that CurseForge ID"
},
"moderation.external-projects.no-results-by-sha1": {
"message": "No external file has that SHA-1 hash"
},
"moderation.external-projects.no-results-by-title": {
"message": "No projects matched that title search"
},
"moderation.external-projects.page-title": {
"message": "External projects - Modrinth"
},
"moderation.external-projects.search-by-title": {
"message": "Search by title"
},
"moderation.external-projects.search-placeholder": {
"message": "Search external projects..."
},
"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
10 changes: 10 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,10 @@ const navItems = computed(() => {
projectV3.value?.project_types?.some((type) => ['mod', 'modpack'].includes(type)) &&
isStaff(currentMember.value?.user)
const hasPermissionsPage = computed(() =>
projectV3.value?.project_types?.some((type) => ['modpack'].includes(type)),
)
const items = [
{
link: `/${base}/settings`,
Expand Down Expand Up @@ -75,6 +80,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
173 changes: 173 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,173 @@
<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 { ref } from 'vue'

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

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>
</template>
<template v-else>
<EmptyState
:heading="formatMessage(messages.emptyStateHeading)"
:description="formatMessage(messages.emptyStateDescription)"
type="done"
/>
</template>
</template>
Loading
Loading