Moderation Changes Phase 2.-1 (#6833)

* better default state handling

* make next stage button work for going to next project

* fix skill issue

* actually fix initial values this time,
make single line inputs with varying lengths (text input and dropdown) use their widest possible length instead of whatever the hell they used to be

* loaders in progress

* loaders and versions test (probably dont use them as the messages suck and idrk what they should do quick fix/message wise)

* WIP the wip

* good enough for now slug taken improvements

* trim all those extra newlines

* viewedness

* batch prefetch queue + technically slightly faster checklist init

* show all valid alternative project types regardless of current project type

* Hide moderation permissions stage when it should be, and I'm sure this won't cause any issues

* Hide moderation permissions stage when it should be, and I'm sure this won't cause any issues

* gimme them versions

* better moderation permission stage shown check?

* betterer moderation permission stage shown check?

* bettererer moderation permission stage shown check? + also began deleting legacy checklist types

* fix certain stages being able to be considered viewed before being viewed

* more removals + i forgot to remove an import last commit

* changed nothing™️

* remove severities + bandaid post-approval message priorities + dependencies

* fix message preview tooltips

* next stage = next project when done

* insufficient description custom fix

* fix that thing coolbot told me was broken I forgot what it was oops

* oh there was a second bug there

* make alternate versions and incorrect project type required cuz they are

* re-navigate button also damn that's a lot of other stuff that pnpm fix... fixed?

* you cant just quick fix a slug into a taken one

* maybe lets not just try and set a slug to something entirely invalid?

* what if we just didn't drop ur queue :smart:

* more queue upgrades

* tweaks

* I love unbreaking things

* un-nuked renavigate button

* prepr

* prepr worked this time i think

* undo more mistakes

* i love intellij refactoring

* ok i think we're good
This commit is contained in:
chyz
2026-08-07 23:40:11 +00:00
committed by GitHub
parent e4cbec6b2c
commit 5072c1d298
77 changed files with 3596 additions and 3502 deletions
+5 -12
View File
@@ -1,8 +1,6 @@
import type { Ref } from 'vue'
import { provide, ref } from 'vue'
import type { NodeState, StageFn, StageNodeBuilder } from '../types/node'
import { group, STAGES_KEY } from '../types/node'
import type { NodeState, StageNode } from '../types/node'
import useCategoriesStage from './stages/categories'
import useDescriptionStage from './stages/description'
import useGalleryStage from './stages/gallery'
@@ -13,7 +11,7 @@ import usePermissionsStage from './stages/permissions'
import usePostApprovalStage from './stages/post-approval'
import useReReviewStage from './stages/re-review'
import useReuploadsStage from './stages/reupload'
import useOtherRulesStage from './stages/other-rules'
import useRulesStage from './stages/rules'
import useStatusAlertsStage from './stages/status-alerts'
import useSummaryStage from './stages/summary'
import useTitleSlugStage from './stages/title-slug'
@@ -22,8 +20,8 @@ import useVersionsStage from './stages/versions'
export function useStages(
globalState: Ref<Record<string, Record<string, NodeState>>>,
): StageNodeBuilder[] {
const mainStages: StageNodeBuilder[] = [
): StageNode[] {
const mainStages: StageNode[] = [
usePostApprovalStage(),
useUndefinedProjectStage(),
useReReviewStage(),
@@ -38,12 +36,7 @@ export function useStages(
useVersionsStage(),
useReuploadsStage(),
usePermissionsStage(),
useOtherRulesStage(),
useRulesStage(),
]
provide(STAGES_KEY, ref(mainStages))
return [...mainStages, useStatusAlertsStage(mainStages, globalState)]
}
export const stages: ReadonlyArray<StageFn> = []
export default group()
+2 -2
View File
@@ -1,6 +1,7 @@
import type { KeybindListener } from '../types/keybinds'
import type { Labrinth } from '@modrinth/api-client'
import type { KeybindListener } from '../types/keybinds'
const copyProjectLink = async (
project: Labrinth.Projects.v2.Project,
permalink: boolean,
@@ -32,7 +33,6 @@ const keybinds: { [id: string]: KeybindListener } = {
keybind: 'ArrowRight',
description: 'Go to next stage',
scope: 'checklist',
enabled: (ctx) => !ctx.state.isDone,
action: (ctx) => ctx.actions.tryGoNext(),
},
'previous-stage': {
@@ -2,5 +2,3 @@
Per section 2.1 of %RULES%, your %PROJECT_DESCRIPTION_FLINK% should clearly inform the reader of the content, purpose, and appeal of your %PROJECT_TYPE%.</br>
Currently, it looks like there are some missing details.
%CUSTOM_ADVICE%
@@ -0,0 +1,4 @@
## Missing Dependencies
Per section 5.6 of %RULES%, it is important that relevant dependencies be listed in the dependencies section of your project.
Please ensure that all relevant dependencies are included in the Dependencies section of each version of your project.
@@ -0,0 +1 @@
This project should have the following game version(s) selected: %GAME_VERSIONS%.
@@ -0,0 +1,5 @@
## Game Version Metadata
Per section 5.1 of %RULES%, it is important that the metadata of your project is accurate, including which Minecraft versions are selected.
%CORRECT%
@@ -0,0 +1 @@
This project should have the following loader(s) selected: %LOADERS%.
@@ -0,0 +1,5 @@
## Loader Metadata
Per section 5.1 of %RULES%, it is important that the metadata of your project is accurate, including which loaders are selected.
%CORRECT%
+1 -2
View File
@@ -1,8 +1,7 @@
import { defineMessage, formatProjectTypeSentence, useVIntl } from '@modrinth/ui'
import type { Nag, NagContext } from '../../types/nags'
import { licenseRequiresSource, licensesRequiringSource, notSourceAsDistributed } from '../../utils'
import license from '../stages/license'
import { licenseRequiresSource, notSourceAsDistributed } from '../../utils'
export const commonLinkDomains = {
source: [
@@ -8,4 +8,5 @@ export const Priorities = new (class extends Priority {
rejected = this.before()
withheld = this.before()
note = this.after()
tempLast = this.after()
})()
@@ -36,7 +36,7 @@ export default function () {
),
group().children(
toggle('inaccurate', 'Inaccurate').suggestedStatus('flagged').severity('low').message(),
toggle('inaccurate', 'Inaccurate').suggestedStatus('flagged').message(),
toggle('optimization-misused', 'Optimization')
.shown(
@@ -47,7 +47,6 @@ export default function () {
),
)
.suggestedStatus('flagged')
.severity('low')
.rawMessage(optimizationMsg)
.fix(
fix().project((patch) => {
@@ -61,7 +60,6 @@ export default function () {
toggle('resolutions-misused', 'Resolutions')
.shown(computed(() => project.value.project_types.includes('resourcepack')))
.suggestedStatus('flagged')
.severity('low')
.rawMessage(resolutionsMsg)
.fix(
fix().project((patch) => {
@@ -15,24 +15,23 @@ export default function () {
'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf080508042e70089dd787e',
)
.icon(LibraryIcon)
.navigate('/')
.navigate()
.children(
group()
.title('Description Issues?')
.children(
toggle('insufficient', 'Insufficient')
.suggestedStatus('flagged')
.severity('medium')
.message('insufficient/header', (s) => ({ CUSTOM_ADVICE: s.custom?.explainer }))
.message('insufficient/header')
.children(
group()
.title('Why is this Description Insufficient?')
.multiSelect('reason')
.children(
toggle('custom', 'Custom').children(
markdown('explainer')
.title('How can the author improve their description?')
.required(),
.required()
.rawMessage((state) => `${state.value}\n\n`),
),
toggle('fork', 'Fork').message('piece/fork'),
toggle('unfinished', 'Unfinished').message('piece/unfinished'),
@@ -44,15 +43,13 @@ export default function () {
`insufficient/default/${project.value?.minecraft_java_server ? 'servers' : project.value?.project_types?.includes('modpack') ? 'packs' : 'projects'}`,
)
.rawMessage(async (state) => {
const reasons = state?.reason instanceof Set ? state.reason : new Set<string>()
return SHOW_SPOILER_ADVICE.some((reason) => reasons.has(reason))
return SHOW_SPOILER_ADVICE.some((reason) => state?.[reason] === true)
? await md('checklist/messages/description/insufficient/piece/spoiler-guide')(state)
: ''
}),
toggle('non-english', 'Non-english')
.suggestedStatus('flagged')
.severity('medium')
.message(() => `non-english${project.value.minecraft_java_server ? '-server' : ''}`)
.shown(
computed(() => {
@@ -63,25 +60,13 @@ export default function () {
}),
),
toggle('headers-as-body', 'Headers as body text')
.suggestedStatus('flagged')
.severity('low')
.message(),
toggle('headers-as-body', 'Headers as body text').suggestedStatus('flagged').message(),
toggle('image-only', 'Image-only')
.suggestedStatus('flagged')
.severity('medium')
.message(),
toggle('image-only', 'Image-only').suggestedStatus('flagged').message(),
toggle('non-standard-text', 'Non-standard text')
.suggestedStatus('flagged')
.severity('medium')
.message(),
toggle('non-standard-text', 'Non-standard text').suggestedStatus('flagged').message(),
toggle('clarity', 'Unclear / Misleading')
.suggestedStatus('rejected')
.severity('high')
.message(),
toggle('clarity', 'Unclear / Misleading').suggestedStatus('rejected').message(),
),
)
}
@@ -16,18 +16,14 @@ export default function () {
.navigate('/gallery')
.children(
group().children(
toggle('insufficient', 'Insufficient').suggestedStatus('flagged').severity('low').message(),
toggle('insufficient', 'Insufficient').suggestedStatus('flagged').message(),
toggle('not-relevant', 'Not relevant')
.shown(computed(() => project.value.gallery.length > 0))
.suggestedStatus('flagged')
.severity('low')
.message(),
toggle('showcase-clarity', 'Showcase Clarity')
.suggestedStatus('rejected')
.severity('high')
.message(),
toggle('showcase-clarity', 'Showcase Clarity').suggestedStatus('rejected').message(),
),
)
}
@@ -53,7 +53,6 @@ export default function () {
toggle('invalid-link', 'Invalid Link')
.shown(computed(() => !!project.value.license?.url))
.suggestedStatus('flagged')
.severity('medium')
.message()
.children(check('custom-license', 'Invalid Link: Custom License').message())
.collect(),
@@ -61,12 +60,11 @@ export default function () {
toggle('no-source', 'No Source')
.shown(needSource)
.suggestedStatus('rejected')
.severity('medium')
.rawMessage(async (state) => {
if (state.fork) return noSourceForkMsg(state)
return noSourceMsg(state)
})
.children(check('fork', 'No Source: Fork').severity('high')),
.children(check('fork', 'No Source: Fork')),
),
)
}
@@ -3,15 +3,15 @@ import { injectProjectPageContext } from '@modrinth/ui'
import type { Ref } from 'vue'
import { computed } from 'vue'
import { group, md, mdOptional, stage, toggle } from '../../types/node'
import type { ChildEntry, GroupNodeBuilder } from '../../types/node'
import { promptSourceRequired } from '../..'
import type { ChildEntry, GroupNode } from '../../types/node'
import { group, md, mdOptional, stage, toggle } from '../../types/node'
export default function () {
const { projectV3: project } = injectProjectPageContext()
const linkNames: Record<string, string> = {}
type LinkSectionBuilder = GroupNodeBuilder & {
type LinkSectionBuilder = GroupNode & {
children(...extras: ChildEntry[]): LinkSectionBuilder
label(badge: Ref<boolean>): LinkSectionBuilder
}
@@ -75,7 +75,6 @@ export default function () {
.navigate('/settings/links')
.shown(computed(() => Object.keys(project.value.link_urls).length > 0))
.suggestedStatus('flagged')
.severity('low')
.rawMessage(async (state) => {
const sections = Object.entries(state).filter(
([, s]) => s && typeof s === 'object' && !(s instanceof Set),
+107 -66
View File
@@ -3,7 +3,16 @@ import { DatabaseIcon } from '@modrinth/assets'
import { ENVIRONMENTS_COPY, injectProjectPageContext, injectTags } from '@modrinth/ui'
import { computed } from 'vue'
import { dropdown, fix, group, md, option, stage, toggle } from '../../types/node'
import {
appComponent as _appComponent,
dropdown,
fix,
group,
md,
option,
stage,
toggle,
} from '../../types/node'
import { requiresEnvironmentInfo } from '../../utils'
const loaderLabels: Record<string, string> = {
@@ -13,7 +22,7 @@ const loaderLabels: Record<string, string> = {
resourcepack: 'Resource Pack',
}
function formatLoaderLabel(id: string): string {
function _formatLoaderLabel(id: string): string {
return (
loaderLabels[id] ??
id
@@ -25,7 +34,11 @@ function formatLoaderLabel(id: string): string {
export default function () {
const { projectV3: project } = injectProjectPageContext()
const { loaders } = injectTags()
const { loaders: _loaders, gameVersions: _gameVersions } = injectTags()
const _currentGameVersions = computed(
() => (project.value.game_versions as string[] | undefined) ?? [],
)
return (
stage('metadata', 'Metadata')
@@ -64,7 +77,6 @@ export default function () {
toggle('environment', 'Environment')
.shown(computed(() => requiresEnvironmentInfo(project.value.project_types)))
.suggestedStatus('flagged')
.severity('low')
.rawMessage(async (state) => {
const correctEnvironment = state?.['correct-environment'] as string | undefined
@@ -94,7 +106,7 @@ export default function () {
.title('Correct Environment')
.children(
dropdown('correct-environment')
.children(
.options(
...(Object.keys(ENVIRONMENTS_COPY) as Labrinth.Projects.v3.Environment[])
.filter((id) => id !== 'unknown')
.map((id) => option(id, ENVIRONMENTS_COPY[id].title.defaultMessage ?? id)),
@@ -103,67 +115,96 @@ export default function () {
.none('Unknown'),
),
),
// TODO: chyz, fix pls (make into single set of buttons where current loaders start selected and non current start non selected
// toggle('loader', `Loader${project.value.loaders.length > 1 ? 's' : ''}`).children(
// group()
// .title('Loader Issues?')
// .action(
// action()
// .suggestedStatus('flagged')
// .severity('medium')
// .message(async (state) => {
// //TODO: chyz
// //TODO: coolbot this one is a bit of a doozy
// const header = await md('checklist/messages/metadata/loader/incorrect')(state)
// const selected = state.loaders
// if (selected instanceof Set && selected.size > 0) {
// const list = [...selected]
// .map((id) => `- ${formatLoaderLabel(id)}`)
// .join('\n')
// return `${header}\n${list}`
// }
// return header
// }),
// )
// .children(
// toggle('incorrect', 'Incorrect').children(
// group()
// .title('Incorrect Loaders')
// .multiSelect('loaders')
// .children(
// ...project.value.loaders.map((id) => option(id, formatLoaderLabel(id))),
// ),
// ),
// TODO: chyz, this should be the same interface as incorrect, as a corrections scheme, with selected loaders default on.
// toggle('missing', 'Missing').children(
// group()
// .title('Missing Loaders')
// .multiSelect('loaders')
// .children(
// ...(() => {
// //TODO: chyz maybe this can be done better
// // (plugin loaders and datapack are marked as valid for mods which makes this suck)
// const existingTypes = new Set(
// loaders.value
// .filter((l) => project.value.loaders.includes(l.name))
// .flatMap((l) => l.supported_project_types),
// )
// const referenceTypes =
// existingTypes.size > 0
// ? existingTypes
// : new Set(project.value.project_types)
// return loaders.value
// .filter(
// (loader) =>
// loader.supported_project_types.every((t) => referenceTypes.has(t)) &&
// !project.value.loaders.includes(loader.name),
// )
// .map((loader) => option(loader.name, formatLoaderLabel(loader.name)))
// })(),
// )/
// ),
// ),
// ),
toggle('dependencies', 'Dependencies').suggestedStatus('flagged').message(),
// toggle('loader', 'Loaders (WIP)')
// .suggestedStatus('flagged')
// .rawMessage(async (state) => {
// const selected =
// state.loaders instanceof Set ? state.loaders : new Set(project.value.loaders)
// const current = new Set(project.value.loaders)
// const isCorrected =
// selected.size !== current.size || [...selected].some((id) => !current.has(id))
//
// let correct = ''
// if (isCorrected) {
// const list = [...selected].map((id) => formatLoaderLabel(id)).join(', ')
// correct = await md('checklist/messages/metadata/loader/correction', () => ({
// LOADERS: list || 'none',
// }))(state)
// }
//
// return md('checklist/messages/metadata/loader/inaccurate', () => ({
// CORRECT: correct,
// }))(state)
// })
// .fix(
// fix().project((patch, state) => {
// const selected =
// state.loaders instanceof Set ? state.loaders : new Set(project.value.loaders)
// const next = [...selected]
// const current = project.value.loaders
// if (next.length === current.length && next.every((id) => current.includes(id)))
// return
// patch.loaders = next
// }),
// )
// .children(
// appComponent('loaders', 'loader-picker')
// .valueKind('set')
// .initial(() => new Set(project.value.loaders))
// .props((ctx) => ({
// loaders: loaders.value,
// toggleLoader: ctx.toggleSetValue,
// })),
// ),
//
// toggle('game-version', 'Game Versions (WIP)')
// .suggestedStatus('flagged')
// .rawMessage(async (state) => {
// const selected =
// state['game-versions'] instanceof Set
// ? state['game-versions']
// : new Set(currentGameVersions.value)
// const current = new Set(currentGameVersions.value)
// const isCorrected =
// selected.size !== current.size || [...selected].some((id) => !current.has(id))
//
// let correct = ''
// if (isCorrected) {
// const list = [...selected].join(', ')
// correct = await md('checklist/messages/metadata/game-version/correction', () => ({
// GAME_VERSIONS: list || 'none',
// }))(state)
// }
//
// return md('checklist/messages/metadata/game-version/inaccurate', () => ({
// CORRECT: correct,
// }))(state)
// })
// .fix(
// fix().project((patch, state) => {
// const selected =
// state['game-versions'] instanceof Set
// ? state['game-versions']
// : new Set(currentGameVersions.value)
// const next = [...selected]
// const current = currentGameVersions.value
// if (next.length === current.length && next.every((id) => current.includes(id)))
// return
// patch.game_versions = next
// }),
// )
// .children(
// appComponent('game-versions', 'game-version-picker')
// .valueKind('set')
// .initial(() => new Set(currentGameVersions.value))
// .props(() => ({
// gameVersions: gameVersions.value,
// noHeader: true,
// })),
// ),
),
)
)
@@ -1,11 +1,30 @@
import type { Labrinth } from '@modrinth/api-client'
import { SignatureIcon } from '@modrinth/assets'
import { injectProjectPageContext } from '@modrinth/ui'
import { injectModrinthClient, injectProjectPageContext } from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { computed } from 'vue'
import { group, stage, toggle } from '../../types/node'
function isResolved(attributionGroup: Labrinth.Attribution.Internal.AttributionGroup): boolean {
const attribution = attributionGroup.attribution
if (!attribution) return false
if (attribution.kind === 'globally_allowed') return true
return attribution.moderation_status?.kind === 'approved'
}
export default function () {
const { projectV3: project } = injectProjectPageContext()
const { labrinth } = injectModrinthClient()
const { data: attributionData } = useQuery({
queryKey: ['project-attribution', project.value.id],
queryFn: () => labrinth.attribution_internal.listProjectAttribution(project.value.id),
})
const unresolvedCount = computed(
() => (attributionData.value ?? []).filter((g) => !isResolved(g)).length,
)
return stage('permissions', 'Modpack Permissions')
.hint("Does this project's external content have any issues?")
@@ -16,30 +35,24 @@ export default function () {
computed(
() =>
(project.value.project_types?.includes('modpack') ?? false) &&
!project.value.minecraft_server,
!project.value.minecraft_server &&
unresolvedCount.value > 0,
),
)
.sticky()
.children(
group().children(
toggle('invalid-permissions', 'Invalid permissions')
.suggestedStatus('rejected')
.severity('high')
.message(),
toggle('invalid-permissions', 'Invalid permissions').suggestedStatus('rejected').message(),
toggle('prohibited-external-content', 'Prohibited externals')
.suggestedStatus('rejected')
.severity('high')
.message(),
toggle('missing-permissions', 'Missing permissions')
.suggestedStatus('rejected')
.severity('high')
.message(),
toggle('missing-permissions', 'Missing permissions').suggestedStatus('rejected').message(),
toggle('non-commercial-external-content', 'Non-commercial externals')
.shown(computed(() => project.value.monetization_status === 'monetized'))
.suggestedStatus('rejected')
.severity('high')
.message(),
),
)
@@ -3,6 +3,7 @@ import { injectProjectPageContext } from '@modrinth/ui'
import { computed } from 'vue'
import { group, stage, text, toggle } from '../../types/node'
import { Priorities } from '../priorities.ts'
//TODO chyz
//TODO coolbot needs discussion
@@ -20,15 +21,15 @@ export default function () {
group().children(
toggle('issue-warning', 'Issue warning')
.suggestedStatus('approved')
.severity('low')
.message(),
.message()
.priority(Priorities.tempLast),
toggle('missed-deadline', 'Missed due date')
.suggestedStatus('flagged')
.severity('high')
.message(undefined, (state) => ({
.message((state) => ({
STATUS: state.status,
}))
.priority(Priorities.tempLast)
.children(
//TODO: chyz this shouldn't need to be provided by moderator
text('status').title('What status is the project being set to?').required(),
@@ -36,35 +37,31 @@ export default function () {
toggle('metadata-issue', 'Incorrect metadata')
.suggestedStatus('approved')
.severity('low')
.message()
.children(
toggle('dependencies', 'Missing Dependencies')
.severity('low')
.message(undefined, (state) => ({
DEPENDENCY_NAME: state['dependency-name'],
DEPENDENCY_LINK: state['dependency-link'],
.message((state) => ({
DEPENDENCY_NAME: state['name'],
DEPENDENCY_LINK: state['link'],
}))
.children(
text('dependency-name').title('Dependency name').required(),
text('dependency-link').title('Dependency link').required(),
text('name').title('Dependency name').required(),
text('link').title('Dependency link').required(),
),
toggle('mc-versions', 'Game versions')
.severity('low')
.message(undefined, (state) => ({
.message((state) => ({
SPECIFICS: state.specifics,
}))
.children(text('specifics').title('More details about the game versions issue?')),
toggle('loaders', 'Loaders')
.severity('low')
.message(undefined, (state) => ({
.message((state) => ({
SPECIFICS: state.specifics,
}))
.children(text('specifics').title('More details about the loaders issue?')),
toggle('license', 'Inconsistent Licensing').severity('low').message(),
toggle('license', 'Inconsistent Licensing').message(),
)
.collect(),
),
@@ -32,13 +32,9 @@ export default function () {
group().children(
toggle('ignored', 'Yes')
.suggestedStatus('flagged')
.severity('medium')
.message()
.children(
toggle('warning', 'Multiple times in a row')
.suggestedStatus('rejected')
.severity('high')
.message(),
toggle('warning', 'Multiple times in a row').suggestedStatus('rejected').message(),
)
.collect(),
),
@@ -20,14 +20,13 @@ export default function () {
'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080d1a0a2cda3ff2ce997',
)
.icon(CopyrightIcon)
.navigate('/')
.navigate()
.children(
group().children(
toggle('reupload', 'Re-upload')
.shown(computed(() => !project.value.minecraft_server))
.suggestedStatus('rejected')
.severity('high')
.message(undefined, (state) => ({
.message((state) => ({
ORIGINAL_PROJECT: state['original-project'],
ORIGINAL_AUTHOR: state['original-author'],
}))
@@ -39,25 +38,19 @@ export default function () {
toggle('unclear-fork', 'Unclear Fork')
.shown(computed(() => !project.value.minecraft_server))
.suggestedStatus('rejected')
.severity('high')
.message(),
toggle('insufficient-fork', 'Insufficient Fork')
.shown(computed(() => !project.value.minecraft_server))
.suggestedStatus('rejected')
.severity('high')
.message(),
toggle('request-proof', 'Proof of permissions')
.suggestedStatus('rejected')
.severity('high')
.message(),
toggle('request-proof', 'Proof of permissions').suggestedStatus('rejected').message(),
toggle('identity-verification', 'Verify Identity')
.shown(computed(() => !project.value.minecraft_server))
.suggestedStatus('rejected')
.severity('high')
.message(undefined, (state) => ({
.message((state) => ({
PLATFORM: state.platform,
}))
.children(text('platform').title('Where else can the project be found?').required()),
@@ -65,8 +58,7 @@ export default function () {
toggle('identity-verification-server', 'Verify Identity')
.shown(computed(() => !!project.value.minecraft_server))
.suggestedStatus('rejected')
.severity('high')
.message(undefined, (state) => ({
.message((state) => ({
CONTACT: state.contact,
}))
.children(text('contact').title('Known public contact method').required()),
@@ -74,17 +66,15 @@ export default function () {
toggle('request-proof-server', 'Reuploaded pack')
.shown(isServerModpack)
.suggestedStatus('rejected')
.severity('high')
.message(),
toggle('custom-pack-verification', 'Override verification')
.shown(isServerModpack)
.suggestedStatus('rejected')
.severity('high')
.message()
.children(
check('list', 'List overrides?')
.message(undefined, (state) => ({
.message((state) => ({
OVERRIDES: state.overrides,
}))
.children(markdown('overrides').title('Add list of overrides.')),
@@ -94,8 +84,7 @@ export default function () {
toggle('custom-pack-prohibited', 'Forbidden Overrides')
.shown(isServerModpack)
.suggestedStatus('rejected')
.severity('high')
.message(undefined, (state) => ({
.message((state) => ({
OVERRIDES: state.overrides,
}))
.children(markdown('overrides').title('Forbidden overrides list').required()),
@@ -2,9 +2,8 @@ import { ListBulletedIcon } from '@modrinth/assets'
import { injectProjectPageContext } from '@modrinth/ui'
import { computed } from 'vue'
import { group, markdown, option, stage, toggle } from '../../types/node'
import { group, markdown, stage, toggle } from '../../types/node'
//TODO: coolbot have fun :3
export default function () {
const { projectV3: project } = injectProjectPageContext()
@@ -20,61 +19,49 @@ export default function () {
toggle('paid-access-server', 'Paid access server')
.shown(computed(() => !!project.value.minecraft_server))
.suggestedStatus('rejected')
.severity('critical')
.message(),
// TODO: chyz, the lists built by these message have empty line gaps.
toggle('prohibited-content', 'Prohibited Content')
.suggestedStatus('rejected')
.severity('critical')
.message('prohibited-content-header')
.collect()
.children(
group()
.multiSelect('options')
.title('Which Prohibited Content rules does this project violate?')
.children(
option('objectionable', 'Objectionable').message(),
option('discriminatory', 'Discriminatory or Explicit').message(),
option('ip-infringement', 'IP Infringement').message(),
option('legal-rights', 'Rights Violation').message(),
option('illegal-activity', 'Illegal Activity').message(),
option('harmful', 'Harmful or Deceptive').message(),
option('misleading', 'Misleading claims').message(),
option('impersonation', 'Impersonation').message(),
option('false-endorsement', 'False Endorsement').message(),
option('profanity', 'Profanity').message(),
option('undisclosed-upload', 'Undisclosed Data Upload').message(),
option('mojang-bypass', 'Mojang Bypass').message(),
toggle('objectionable', 'Objectionable').message(),
toggle('discriminatory', 'Discriminatory or Explicit').message(),
toggle('ip-infringement', 'IP Infringement').message(),
toggle('legal-rights', 'Rights Violation').message(),
toggle('illegal-activity', 'Illegal Activity').message(),
toggle('harmful', 'Harmful or Deceptive').message(),
toggle('misleading', 'Misleading claims').message(),
toggle('impersonation', 'Impersonation').message(),
toggle('false-endorsement', 'False Endorsement').message(),
toggle('profanity', 'Profanity').message(),
toggle('undisclosed-upload', 'Undisclosed Data Upload').message(),
toggle('mojang-bypass', 'Mojang Bypass').message(),
),
),
toggle('cheat-or-hack-advertising', 'Hacks')
.suggestedStatus('rejected')
.severity('critical')
.message(),
toggle('cheat-or-hack-advertising', 'Hacks').suggestedStatus('rejected').message(),
toggle('server-side-opt-out', 'Opt-out')
.suggestedStatus('flagged')
.severity('high')
.message(),
toggle('server-side-opt-out', 'Opt-out').suggestedStatus('flagged').message(),
toggle('server-side-opt-in', 'Opt-in')
.suggestedStatus('flagged')
.severity('high')
.message('server-side-opt-in-header')
.collect()
.children(
group()
.multiSelect('options')
.title('Which features require a Server-side Opt-in?')
.children(
option('x-ray', 'X-ray').message(),
option('aim-bot', 'Aim Assist').message(),
option('movement', 'Movement').message(),
option('pvp', 'PvP').message(),
option('hiding-mods', 'Anti 3.x').message(),
option('item-duplication', 'Dupe').message(),
toggle('x-ray', 'X-ray').message(),
toggle('aim-bot', 'Aim Assist').message(),
toggle('movement', 'Movement').message(),
toggle('pvp', 'PvP').message(),
toggle('hiding-mods', 'Anti 3.x').message(),
toggle('item-duplication', 'Dupe').message(),
),
),
@@ -88,14 +75,11 @@ export default function () {
),
)
.suggestedStatus('flagged')
.severity('low')
.message(),
toggle('rule-breaking-other', 'Other')
// TODO: chyz, the required asterisk is on a separate line
.suggestedStatus('rejected')
.severity('critical')
.message(undefined, (state) => ({ MESSAGE: state.message }))
.message((state) => ({ MESSAGE: state.message }))
.children(
markdown('message').title('Explain how it infringes on content rules.').required(),
),
@@ -3,9 +3,9 @@ import { injectProjectPageContext } from '@modrinth/ui'
import type { Ref } from 'vue'
import { computed } from 'vue'
import type { NodeState, StageNodeBuilder } from '../../types/node'
import type { AnyNode, ChildNode, NodeState, StageNode } from '../../types/node'
import {
NodeBuilder,
externalGroup,
getBooleanChildState,
group,
isNodeActive,
@@ -18,7 +18,7 @@ import {
import { Priorities } from '../priorities.ts'
export default function (
mainStages: StageNodeBuilder[],
mainStages: StageNode[],
globalState: Ref<Record<string, Record<string, NodeState>>>,
) {
const { projectV3: project } = injectProjectPageContext()
@@ -47,23 +47,26 @@ export default function (
.priority(Priorities.alerts)
.applyFixes()
.children(
computed<NodeBuilder | null>(() => {
const fixNodes: NodeBuilder[] = []
computed<AnyNode | null>(() => {
const fixGroups: ChildNode[] = []
walkNodes(
[group().children(...mainStages)],
(globalState.value ?? {}) as unknown as Record<string, NodeState>,
(node, nodeState) => {
if (!node._fixes.length) return
(node, nodeState, _localState, path) => {
if (!('_fixes' in node) || !(node as { _fixes: unknown[] })._fixes.length) return
if (!isNodeActive(node, nodeState)) return
const childState = getBooleanChildState(nodeState)
fixNodes.push(
...resolveChildren(node, childState).filter(
(c): c is NodeBuilder => c instanceof NodeBuilder,
),
const children = resolveChildren(
node as never,
getBooleanChildState(nodeState),
).filter(
(c): c is Exclude<ChildNode, string | (() => unknown)> =>
typeof c === 'object' && c !== null,
)
if (children.length === 0) return
fixGroups.push(externalGroup(path).children(...children))
},
)
return fixNodes.length > 0 ? group().children(...fixNodes) : null
return fixGroups.length > 0 ? group().children(...fixGroups) : null
}),
),
@@ -31,20 +31,17 @@ export default function () {
toggle('insufficient', 'Insufficient')
.enabled((state) => !state['repeat-title'])
.suggestedStatus('flagged')
.severity('low')
.message(),
toggle('repeat-title', 'Repeat of Title')
.enabled((state) => !state.insufficient)
.suggestedStatus('flagged')
.severity('low')
.message(),
toggle('formatting', 'Formatting').suggestedStatus('flagged').severity('low').message(),
toggle('formatting', 'Formatting').suggestedStatus('flagged').message(),
toggle('non-english', 'Non-english')
.suggestedStatus('flagged')
.severity('medium')
.message()
.shown(
computed(() => {
@@ -62,7 +59,6 @@ export default function () {
toggle('repeat-ip', 'Repeat of IP')
.shown(computed(() => !!project.value?.minecraft_server))
.suggestedStatus('flagged')
.severity('medium')
.message(),
),
)
+108 -133
View File
@@ -5,18 +5,23 @@ import {
TagCategoryRefreshCcwIcon,
TagCategoryWandSparklesIcon,
UserPlusIcon,
WrenchIcon,
} from '@modrinth/assets'
import { Alert, injectModrinthClient, injectProjectPageContext } from '@modrinth/ui'
import {
Alert,
injectModrinthClient,
injectProjectPageContext,
ProjectStatusLink,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue'
import { md, type NodeState } from '../../types/node'
import { button, fix, group, option, stage, text, toggle } from '../../types/node'
import { check, fix, group, md, stage, text, toggle } from '../../types/node'
const STALE_TIME = 1000 * 60 * 5
type AutoSlugStatus = 'loading' | 'available' | 'unavailable'
type SlugValidation = 'checking' | 'available' | 'unchanged' | 'taken' | null
type SlugValidation = 'checking' | 'available' | 'unchanged' | 'taken' | 'empty' | 'invalid' | null
//TODO: make this not a copy of frontend/src/utils/slugs.generateUrlSlug
// (as in move the other one so we can use it here)
@@ -46,12 +51,6 @@ export default function () {
const slugValidation = ref<SlugValidation>(null)
let slugDebounceTimer: ReturnType<typeof setTimeout> | undefined
function currentSlug(state: Record<string, NodeState>) {
return (
(state['correct-slug'] as string | undefined) ?? resolvedAutoSlug.value ?? project.value.slug
)
}
async function checkSlugTaken(slug: string): Promise<Labrinth.Projects.v3.Project | null> {
try {
return await queryClient.fetchQuery({
@@ -66,8 +65,13 @@ export default function () {
}
const SlugStatus = () => {
const v = slugValidation.value
if (v === null) return null
const v =
slugValidation.value ??
(autoSlugStatus.value === 'loading'
? 'checking'
: autoSlugStatus.value === 'unavailable'
? 'taken'
: 'available')
if (v === 'checking')
return (
<Alert type="checking" class="w-full">
@@ -86,19 +90,25 @@ export default function () {
Slug is available
</Alert>
)
if (v === 'empty')
return (
<Alert type="error" class="w-full">
Slug cannot be empty
</Alert>
)
if (v === 'invalid')
return (
<Alert type="error" class="w-full">
Invalid Slug
</Alert>
)
const by = correctSlugConflict.value
return (
<Alert type="error" class="w-full">
Slug taken
{by ? (
<>
{' by '}
<a href={`/project/${by.slug}`} target="_blank" class="underline">
{by.name}
</a>
{` (${by.status})`}
</>
) : null}
<div class="flex flex-wrap items-center gap-1.5">
Slug taken
{by ? <ProjectStatusLink project={by} newTab /> : null}
</div>
</Alert>
)
}
@@ -157,7 +167,9 @@ export default function () {
return
}
}
} catch {}
} catch {
// fall through to 'unavailable' below
}
autoSlugStatus.value = 'unavailable'
},
@@ -190,12 +202,7 @@ export default function () {
{autoSlugStatus.value === 'loading' ? (
'...'
) : by ? (
<>
<a href={`/project/${by.slug}`} target="_blank" class="underline">
{by.name}
</a>
{` (${by.status})`}
</>
<ProjectStatusLink project={by} newTab />
) : (
'No'
)}
@@ -206,30 +213,22 @@ export default function () {
group('title')
.title('Title Issues?')
.children(
toggle('useless-info', 'Contains Useless Info')
.suggestedStatus('flagged')
.severity('low')
.message(),
toggle('useless-info', 'Contains Useless Info').suggestedStatus('flagged').message(),
toggle('minecraft-branding', 'Minecraft Title')
.suggestedStatus('flagged')
.severity('medium')
.message(),
toggle('minecraft-branding', 'Minecraft Title').suggestedStatus('flagged').message(),
toggle('similarities', 'Title Similarities')
.suggestedStatus('flagged')
.severity('medium')
.message()
.children(
group()
.title('Similarities Additional Info')
.multiSelect('options')
.children(
option('modpack', 'Modpack Named After Mod')
check('modpack', 'Modpack Named After Mod')
.shown(computed(() => project.value.project_types.includes('modpack')))
.message(),
option('fork', 'Forked Project')
check('fork', 'Forked Project')
.shown(computed(() => !project.value?.minecraft_server))
.message(),
),
@@ -241,100 +240,76 @@ export default function () {
.title('Slug Issues')
.shown(computed(() => hasCustomSlug(project.value)))
.children(
group()
.multiSelect('issues')
.children(
toggle('misused', 'Misused')
.children(
group()
.title('Correct Slug')
.children(
text('correct-slug')
.initial(() => resolvedAutoSlug.value ?? project.value.slug)
.onChange((value, { override }) => {
if (!value) return override(project.value.slug ?? '')
clearTimeout(slugDebounceTimer)
if (value === project.value.slug) {
slugValidation.value = 'unchanged'
return
group().children(
toggle('misused', 'Misused')
.children(
group()
.title('Correct Slug')
.children(
text('correct-slug')
.initial(() => resolvedAutoSlug.value ?? project.value.slug ?? '')
.onChange((value) => {
clearTimeout(slugDebounceTimer)
if (value === project.value.slug) {
slugValidation.value = 'unchanged'
return
}
if (!value) {
slugValidation.value = 'empty'
correctSlugConflict.value = null
return
}
if (generateUrlSlug(value) !== value) {
slugValidation.value = 'invalid'
correctSlugConflict.value = null
return
}
slugValidation.value = 'checking'
slugDebounceTimer = setTimeout(async () => {
const conflict = await checkSlugTaken(value).catch(() => null)
if (conflict !== null && conflict.id !== project.value.id) {
correctSlugConflict.value = conflict
slugValidation.value = 'taken'
} else {
correctSlugConflict.value = null
slugValidation.value = 'available'
}
slugValidation.value = 'checking'
slugDebounceTimer = setTimeout(async () => {
const conflict = await checkSlugTaken(value).catch(() => null)
if (conflict !== null && conflict.id !== project.value.id) {
correctSlugConflict.value = conflict
slugValidation.value = 'taken'
} else {
correctSlugConflict.value = null
slugValidation.value = 'available'
}
}, 400)
}),
}, 400)
})
.tweak(TagCategoryWandSparklesIcon, () =>
autoSlugStatus.value === 'available' ? resolvedAutoSlug.value : null,
)
.tweak(UserPlusIcon, (current) =>
ownerUsername.value && !current?.includes(ownerUsername.value)
? `${current}-${ownerUsername.value}`
: null,
)
.tweak(WrenchIcon, (current) => generateUrlSlug(current ?? ''))
.tweak(TagCategoryRefreshCcwIcon, () => project.value.slug),
button()
.icon(TagCategoryWandSparklesIcon)
.tooltip(computed(() => resolvedAutoSlug.value ?? ''))
.enabled(
(state) =>
autoSlugStatus.value === 'available' &&
resolvedAutoSlug.value !== null &&
resolvedAutoSlug.value !== currentSlug(state),
)
.onClick((state) => {
if (resolvedAutoSlug.value) state['correct-slug'] = resolvedAutoSlug.value
}),
button()
.icon(UserPlusIcon)
.tooltip((state) => {
const current = currentSlug(state)
if (!ownerUsername.value || current?.includes(ownerUsername.value))
return current ?? ''
return `${current}-${ownerUsername.value}`
})
.enabled(
(state) =>
ownerUsername.value !== null &&
!currentSlug(state)?.includes(ownerUsername.value),
)
.onClick((state) => {
state['correct-slug'] = `${currentSlug(state)}-${ownerUsername.value}`
}),
button()
.icon(TagCategoryRefreshCcwIcon)
.tooltip(computed(() => project.value.slug ?? ''))
.enabled((state) => currentSlug(state) !== project.value.slug)
.onClick((state) => {
state['correct-slug'] = project.value.slug
}),
SlugStatus,
),
)
.rawMessage(async (state) => {
let correct = ''
if (slugValidation.value === 'available') {
const slug = state['correct-slug'] as string | undefined
if (slug)
correct = await md('checklist/messages/title-slug/slug/correction', () => ({
SUGGESTED_SLUG: slug,
}))(state)
}
return md('checklist/messages/title-slug/slug/misused', () => ({
CORRECT: correct,
}))(state)
})
.fix(
//TODO chyz think of some way to have initial values actually be reflected in state without having to store them
fix().project((patch, state) => {
const slug =
(state['correct-slug'] as string | undefined) ?? resolvedAutoSlug.value
if (!slug || slug === project.value.slug) return
patch.slug = slug
}),
),
),
SlugStatus,
),
)
.rawMessage(async (state) => {
let correct = ''
if (slugValidation.value === 'available') {
const slug = state['correct-slug'] as string | undefined
if (slug)
correct = await md('checklist/messages/title-slug/slug/correction', () => ({
SUGGESTED_SLUG: slug,
}))(state)
}
return md('checklist/messages/title-slug/slug/misused', () => ({
CORRECT: correct,
}))(state)
})
.fix(
fix().project((patch, state) => {
if (slugValidation.value !== 'available') return
patch.slug = state['correct-slug'] as string
}),
),
),
),
)
}
@@ -19,38 +19,31 @@ export default function () {
group().children(
toggle('incorrect-additional-files', 'Incorrect additional files')
.suggestedStatus('flagged')
.severity('medium')
.message(),
toggle('incorrect-project-type', 'Incorrect Project Type')
.suggestedStatus('rejected')
.severity('medium')
.children(
dropdown('type')
.title('Correct Project Type')
.required()
.none('Unknown')
.children(
option('modpack', 'Modpack')
.shown(computed(() => !project.value.project_types.includes('modpack')))
.message(),
option('resourcepack', 'Resource Pack')
.shown(computed(() => !project.value.project_types.includes('resourcepack')))
.message(),
option('datapack', 'Data Pack')
.shown(computed(() => !project.value.loaders.includes('datapack')))
.message(),
.options(
option('modpack', 'Modpack').message(),
option('resourcepack', 'Resource Pack').message(),
option('datapack', 'Data Pack').message(),
),
)
.collect(),
toggle('alternate-versions', 'Alternate Versions')
.suggestedStatus('rejected')
.severity('high')
.children(
dropdown('distribution')
.title('Distribution Type')
.required()
.none('Unknown')
.children(
.options(
option('primary', 'Primary Files').message(),
option('additional', 'Additional Files').message(),
option('mono', 'Monofile')
@@ -78,7 +71,6 @@ export default function () {
toggle('vanilla-assets', 'Vanilla Assets')
.shown(computed(() => project.value.project_types.includes('resourcepack')))
.suggestedStatus('rejected')
.severity('medium')
.message(),
toggle('redist-libs', 'Packed Libs')
@@ -90,18 +82,15 @@ export default function () {
),
)
.suggestedStatus('rejected')
.severity('medium')
.message(),
toggle('duplicate-primary-files', 'Duplicate Primary Files')
.suggestedStatus('flagged')
.severity('medium')
.message(),
toggle('unsupported', 'Unsupported')
.suggestedStatus('rejected')
.severity('medium')
.message(undefined, (state) => ({
.message((state) => ({
INVALID_TYPE: state['invalid-type'],
}))
.children(text('invalid-type').title('Unsupported Type').required()),