From b98c1fe7b8ebc672db1a675b0e078b70e111d0c1 Mon Sep 17 00:00:00 2001 From: Calum Date: Fri, 11 Jul 2025 16:18:24 +0100 Subject: [PATCH 01/39] feat: set up typed nag (validators) system --- packages/moderation/data/nags.ts | 4 + packages/moderation/data/nags/core.ts | 128 ++++++++++++++++++++++++++ packages/moderation/index.ts | 2 + packages/moderation/types/nags.ts | 90 ++++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 packages/moderation/data/nags.ts create mode 100644 packages/moderation/data/nags/core.ts create mode 100644 packages/moderation/types/nags.ts diff --git a/packages/moderation/data/nags.ts b/packages/moderation/data/nags.ts new file mode 100644 index 000000000..6d009ecc5 --- /dev/null +++ b/packages/moderation/data/nags.ts @@ -0,0 +1,4 @@ +import type { Nag } from '../types/nags' +import { coreNags } from './nags/core' + +export default [...coreNags] as Nag[] diff --git a/packages/moderation/data/nags/core.ts b/packages/moderation/data/nags/core.ts new file mode 100644 index 000000000..ad6d467ff --- /dev/null +++ b/packages/moderation/data/nags/core.ts @@ -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', + }, + }, +] diff --git a/packages/moderation/index.ts b/packages/moderation/index.ts index b0a6afea9..b8f8f1d8c 100644 --- a/packages/moderation/index.ts +++ b/packages/moderation/index.ts @@ -2,7 +2,9 @@ export * from './types/actions' export * from './types/messages' export * from './types/stage' export * from './types/keybinds' +export * from './types/nags' export * from './utils' export { default as checklist } from './data/checklist' export { default as keybinds } from './data/keybinds' +export { default as nags } from './data/nags' diff --git a/packages/moderation/types/nags.ts b/packages/moderation/types/nags.ts new file mode 100644 index 000000000..0a48f8951 --- /dev/null +++ b/packages/moderation/types/nags.ts @@ -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 + /** + * 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 +} From bcfceecf1b0f3a485ec4b842ff952a54328f0fc8 Mon Sep 17 00:00:00 2001 From: Calum Date: Fri, 11 Jul 2025 16:52:49 +0100 Subject: [PATCH 02/39] feat: start on frontend impl --- .../src/components/ui/ProjectMemberHeader.vue | 638 ++++++------------ apps/frontend/src/pages/[type]/[id].vue | 2 + 2 files changed, 214 insertions(+), 426 deletions(-) diff --git a/apps/frontend/src/components/ui/ProjectMemberHeader.vue b/apps/frontend/src/components/ui/ProjectMemberHeader.vue index 9bef20859..917a87d06 100644 --- a/apps/frontend/src/components/ui/ProjectMemberHeader.vue +++ b/apps/frontend/src/components/ui/ProjectMemberHeader.vue @@ -1,9 +1,12 @@ - diff --git a/apps/frontend/src/pages/[type]/[id].vue b/apps/frontend/src/pages/[type]/[id].vue index 0b51f73bb..8936a795f 100644 --- a/apps/frontend/src/pages/[type]/[id].vue +++ b/apps/frontend/src/pages/[type]/[id].vue @@ -1360,6 +1360,8 @@ const currentMember = computed(() => { }; } + console.log("Current member:", val); + return val; }); From b2978096fdccdbc73d0f6a34ada44d9661f704cc Mon Sep 17 00:00:00 2001 From: Calum Date: Fri, 11 Jul 2025 17:13:38 +0100 Subject: [PATCH 03/39] fix: shouldShow issues --- .../src/components/ui/ProjectMemberHeader.vue | 37 +++---------------- packages/moderation/types/nags.ts | 3 +- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/apps/frontend/src/components/ui/ProjectMemberHeader.vue b/apps/frontend/src/components/ui/ProjectMemberHeader.vue index 917a87d06..74077a008 100644 --- a/apps/frontend/src/components/ui/ProjectMemberHeader.vue +++ b/apps/frontend/src/components/ui/ProjectMemberHeader.vue @@ -29,30 +29,6 @@

Publishing checklist

-
- Progress: -
-
- - -
-
-
-
+
- + (() => ({ const applicableNags = computed(() => { return nags.filter((nag) => { - return !nag.shouldShow || nag.shouldShow(nagContext.value); + return nag.shouldShow(nagContext.value); }); }); const isNagComplete = (nag: Nag): boolean => { const context = nagContext.value; + return !nag.shouldShow(context); }; const visibleNags = computed(() => { @@ -179,9 +156,7 @@ const visibleNags = computed(() => { }); const shouldShowLink = (nag: Nag): boolean => { - if (!nag.link) return false; - if (!nag.link.shouldShow) return true; - return nag.link.shouldShow(nagContext.value); + return nag.link?.shouldShow ? nag.link.shouldShow(nagContext.value) : false; }; const getDefaultIcon = (status: NagStatus): Component => { @@ -212,7 +187,7 @@ const getStatusTooltip = (status: NagStatus): string => { const showInvitation = computed(() => { if (props.allMembers && props.auth) { - const member = props.allMembers.find((x) => x.user.id === props.auth.user.id); + const member = props.allMembers.find((x) => x?.user?.id === props.auth.user.id); return !!member && !member.accepted; } return false; diff --git a/packages/moderation/types/nags.ts b/packages/moderation/types/nags.ts index 0a48f8951..0e8075c8c 100644 --- a/packages/moderation/types/nags.ts +++ b/packages/moderation/types/nags.ts @@ -78,10 +78,11 @@ export interface Nag { * If not specified it will use the default icon associated with the nag status. */ icon?: FunctionalComponent + /** * A function that determines whether the nag should be shown based on the context. */ - shouldShow?: (context: NagContext) => boolean + shouldShow: (context: NagContext) => boolean /** * An optional link associated with the nag. * If provided, it should be displayed alongside the nag. From d33b06ea55445d656f5ac166f5c6a732a4058cc5 Mon Sep 17 00:00:00 2001 From: Calum Date: Fri, 11 Jul 2025 20:07:26 +0100 Subject: [PATCH 04/39] feat: continue work --- .../src/components/ui/ProjectMemberHeader.vue | 44 +++++++++++++------ packages/moderation/data/nags/core.ts | 14 ++++++ packages/moderation/data/nags/extended.ts | 0 packages/moderation/types/nags.ts | 2 + 4 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 packages/moderation/data/nags/extended.ts diff --git a/apps/frontend/src/components/ui/ProjectMemberHeader.vue b/apps/frontend/src/components/ui/ProjectMemberHeader.vue index 74077a008..59f08be91 100644 --- a/apps/frontend/src/components/ui/ProjectMemberHeader.vue +++ b/apps/frontend/src/components/ui/ProjectMemberHeader.vue @@ -8,14 +8,18 @@

You've been invited to join this project. Please accept or decline the invitation.

- - + + + + + +
Publishing checklist
- + + +
@@ -80,9 +83,11 @@ import { LightBulbIcon, TriangleAlertIcon, DropdownIcon, + SendIcon, } from "@modrinth/assets"; import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js"; import { nags } from "@modrinth/moderation"; +import { ButtonStyled } from "@modrinth/ui"; import type { Nag, NagContext, NagStatus } from "@modrinth/moderation"; import type { Project, User, Version } from "@modrinth/utils"; import type { Component } from "vue"; @@ -138,8 +143,19 @@ const nagContext = computed(() => ({ versions: props.versions, currentMember: props.currentMember as User, currentRoute: props.routeName, + tags: props.tags, + submitProject: submitForReview, })); +const submitForReview = async () => { + if ( + !props.acknowledgedMessage || + nags.value.filter((x) => x.condition && x.status === "required").length === 0 + ) { + await props.setProcessing(); + } +}; + const applicableNags = computed(() => { return nags.filter((nag) => { return nag.shouldShow(nagContext.value); diff --git a/packages/moderation/data/nags/core.ts b/packages/moderation/data/nags/core.ts index ad6d467ff..4ae32da77 100644 --- a/packages/moderation/data/nags/core.ts +++ b/packages/moderation/data/nags/core.ts @@ -2,6 +2,20 @@ import type { Nag, NagContext } from '../../types/nags' import { formatProjectType } from '@modrinth/utils' export const coreNags: Nag[] = [ + { + id: 'moderator-feedback', + title: 'Review moderator feedback', + description: () => + 'Review any feedback from moderators regarding your project before resubmitting.', + status: 'suggestion', + shouldShow: (context: NagContext) => + context.tags.rejectedStatuses.includes(context.project.status), + link: { + path: 'moderation', + title: 'Visit moderation thread', + shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-moderation', + }, + }, { id: 'upload-version', title: 'Upload a version', diff --git a/packages/moderation/data/nags/extended.ts b/packages/moderation/data/nags/extended.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/moderation/types/nags.ts b/packages/moderation/types/nags.ts index 0e8075c8c..f922fa253 100644 --- a/packages/moderation/types/nags.ts +++ b/packages/moderation/types/nags.ts @@ -32,6 +32,8 @@ export interface NagContext { * The current route in the application. */ currentRoute: string + tags: any + submitProject: (...any: any) => any } /** From 59ad8eb42600d9f754f19c33402f8881426ab098 Mon Sep 17 00:00:00 2001 From: Calum Date: Fri, 11 Jul 2025 21:12:56 +0100 Subject: [PATCH 05/39] feat: re add submitting/re-submit nags --- .../src/components/ui/ProjectMemberHeader.vue | 109 +++++++++++++----- packages/moderation/types/nags.ts | 2 +- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/apps/frontend/src/components/ui/ProjectMemberHeader.vue b/apps/frontend/src/components/ui/ProjectMemberHeader.vue index 59f08be91..daa522f6c 100644 --- a/apps/frontend/src/components/ui/ProjectMemberHeader.vue +++ b/apps/frontend/src/components/ui/ProjectMemberHeader.vue @@ -1,5 +1,5 @@