feat: set up typed nag (validators) system

This commit is contained in:
Calum 2025-07-11 16:18:24 +01:00
parent 359fbd4738
commit b98c1fe7b8
4 changed files with 224 additions and 0 deletions

View File

@ -0,0 +1,4 @@
import type { Nag } from '../types/nags'
import { coreNags } from './nags/core'
export default [...coreNags] as Nag[]

View File

@ -0,0 +1,128 @@
import type { Nag, NagContext } from '../../types/nags'
import { formatProjectType } from '@modrinth/utils'
export const coreNags: Nag[] = [
{
id: 'upload-version',
title: 'Upload a version',
description: () => 'At least one version is required for a project to be submitted for review.',
status: 'required',
shouldShow: (context: NagContext) => context.versions.length < 1,
link: {
path: 'versions',
title: 'Visit versions page',
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-versions',
},
},
{
id: 'add-description',
title: 'Add a description',
description: () =>
"A description that clearly describes the project's purpose and function is required.",
status: 'required',
shouldShow: (context: NagContext) =>
context.project.body === '' || context.project.body.startsWith('# Placeholder description'),
link: {
path: 'settings/description',
title: 'Visit description settings',
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
},
},
{
id: 'add-icon',
title: 'Add an icon',
description: () =>
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
status: 'suggestion',
shouldShow: (context: NagContext) => !context.project.icon_url,
link: {
path: 'settings',
title: 'Visit general settings',
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
},
},
{
id: 'feature-gallery-image',
title: 'Feature a gallery image',
description: () => 'Featured gallery images may be the first impression of many users.',
status: 'suggestion',
shouldShow: (context: NagContext) => {
const featuredGalleryImage = context.project.gallery?.find((img) => img.featured)
return context.project?.gallery?.length === 0 || !featuredGalleryImage
},
link: {
path: 'gallery',
title: 'Visit gallery page',
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-gallery',
},
},
{
id: 'select-tags',
title: 'Select tags',
description: () => 'Select all tags that apply to your project.',
status: 'suggestion',
shouldShow: (context: NagContext) =>
context.project.versions.length > 0 && context.project.categories.length < 1,
link: {
path: 'settings/tags',
title: 'Visit tag settings',
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-tags',
},
},
{
id: 'add-links',
title: 'Add external links',
description: () =>
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.',
status: 'suggestion',
shouldShow: (context: NagContext) =>
!(
context.project.issues_url ||
context.project.source_url ||
context.project.wiki_url ||
context.project.discord_url ||
context.project.donation_urls.length > 0
),
link: {
path: 'settings/links',
title: 'Visit links settings',
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
},
},
{
id: 'select-environments',
title: 'Select supported environments',
description: (context: NagContext) =>
`Select if the ${formatProjectType(context.project.project_type).toLowerCase()} functions on the client-side and/or server-side.`,
status: 'required',
shouldShow: (context: NagContext) => {
const excludedTypes = ['resourcepack', 'plugin', 'shader', 'datapack']
return (
context.project.versions.length > 0 &&
!excludedTypes.includes(context.project.project_type) &&
(context.project.client_side === 'unknown' ||
context.project.server_side === 'unknown' ||
(context.project.client_side === 'unsupported' &&
context.project.server_side === 'unsupported'))
)
},
link: {
path: 'settings',
title: 'Visit general settings',
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
},
},
{
id: 'select-license',
title: 'Select license',
description: (context: NagContext) =>
`Select the license your ${formatProjectType(context.project.project_type).toLowerCase()} is distributed under.`,
status: 'required',
shouldShow: (context: NagContext) => context.project.license.id === 'LicenseRef-Unknown',
link: {
path: 'settings/license',
title: 'Visit license settings',
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-license',
},
},
]

View File

@ -2,7 +2,9 @@ export * from './types/actions'
export * from './types/messages' export * from './types/messages'
export * from './types/stage' export * from './types/stage'
export * from './types/keybinds' export * from './types/keybinds'
export * from './types/nags'
export * from './utils' export * from './utils'
export { default as checklist } from './data/checklist' export { default as checklist } from './data/checklist'
export { default as keybinds } from './data/keybinds' export { default as keybinds } from './data/keybinds'
export { default as nags } from './data/nags'

View File

@ -0,0 +1,90 @@
import type { Project, User, Version } from '@modrinth/utils'
import type { FunctionalComponent, SVGAttributes } from 'vue'
/**
* Type which represents the status type of a nag.
*
* - `required` indicates that the nag must be addressed.
* - `warning` indicates that the nag is important but not critical, and can be ignored. It is often used for issues that should be resolved but do not block project submission.
* - `suggestion` indicates that the nag is a recommendation and can be ignored.
*/
export type NagStatus = 'required' | 'warning' | 'suggestion'
/**
* Interface representing the context in which a nag is displayed.
* It includes the project, versions, current member, all members, and the current route.
* This context is used to determine whether a nag or it's link should be shown and how it should be presented.
*/
export interface NagContext {
/**
* The project associated with the nag.
*/
project: Project
/**
* The versions associated with the project.
*/
versions: Version[]
/**
* The current project member viewing the nag.
*/
currentMember: User
/**
* The current route in the application.
*/
currentRoute: string
}
/**
* Interface representing a nag's link.
*/
export interface NagLink {
/**
* A relative path to the nag's link, e.g. '/settings'.
*/
path: string
/**
* The text to display for the nag's link.
*/
title: string
/**
* The status of the nag, which can be 'required', 'warning', or 'suggestion'.
*/
shouldShow?: (context: NagContext) => boolean
}
/**
* Interface representing a nag.
*/
export interface Nag {
/**
* A unique identifier for the nag.
*/
id: string
/**
* The title of the nag.
*/
title: string
/**
* A function that returns the description of the nag.
* It can accept a context to provide dynamic descriptions.
*/
description: (context: NagContext) => string
/**
* The status of the nag, which can be 'required', 'warning', or 'suggestion'.
*/
status: NagStatus
/**
* An optional icon for the nag, usually from `@modrinth/assets`.
* If not specified it will use the default icon associated with the nag status.
*/
icon?: FunctionalComponent<SVGAttributes>
/**
* A function that determines whether the nag should be shown based on the context.
*/
shouldShow?: (context: NagContext) => boolean
/**
* An optional link associated with the nag.
* If provided, it should be displayed alongside the nag.
*/
link?: NagLink
}