From 1eabbf0c28d72d648a32dc9dc06f887718738259 Mon Sep 17 00:00:00 2001 From: chyzman Date: Sat, 2 May 2026 20:53:22 -0400 Subject: [PATCH 01/85] Rule 1.x, 3.2 and 3.3x support --- .../checklist/ModerationChecklist.vue | 41 ++++- .../prohibited-content-header.md | 2 + .../prohibited-content/discriminatory.md | 1 + .../prohibited-content/false-endorsement.md | 1 + .../prohibited-content/harmful.md | 1 + .../prohibited-content/illegal-activity.md | 1 + .../prohibited-content/impersonation.md | 1 + .../prohibited-content/ip-infringement.md | 1 + .../prohibited-content/legal-rights.md | 1 + .../prohibited-content/misleading.md | 1 + .../prohibited-content/mojang-bypass.md | 1 + .../prohibited-content/objectionable.md | 1 + .../prohibited-content/profanity.md | 1 + .../prohibited-content/undisclosed-upload.md | 1 + .../server-side-opt-in-header.md | 2 + .../server-side-opt-in/aim-bot.md | 1 + .../server-side-opt-in/hiding-mods.md | 1 + .../server-side-opt-in/item-duplication.md | 1 + .../server-side-opt-in/movement.md | 1 + .../rule-breaking/server-side-opt-in/pvp.md | 1 + .../rule-breaking/server-side-opt-in/x-ray.md | 1 + .../rule-breaking/server-side-opt-out.md | 2 + .../src/data/stages/rule-following.ts | 167 +++++++++++++++++- packages/moderation/src/types/actions.ts | 6 + 24 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content-header.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in-header.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md create mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-out.md diff --git a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue index 1b737d9e5c..e5e09569af 100644 --- a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue @@ -1735,17 +1735,44 @@ async function processAction( const multiSelectAction = action as MultiSelectChipsAction const selectedIndices = state.value as Set - for (const index of selectedIndices) { - const option = multiSelectAction.options[index] - if (option && 'message' in option && 'weight' in option) { - const message = (await option.message()) as string + if (multiSelectAction.joinWith !== undefined) { + const parts: { weight: number; content: string }[] = [] + for (const index of selectedIndices) { + const option = multiSelectAction.options[index] + if (option && 'message' in option && 'weight' in option) { + parts.push({ + weight: option.weight, + content: processMessage( + (await option.message()) as string, + action, + stageIndex, + textInputValues.value, + ), + }) + } + } + if (parts.length > 0) { + parts.sort((a, b) => a.weight - b.weight) messageParts.push({ - weight: option.weight, - content: processMessage(message, action, stageIndex, textInputValues.value), - actionId: `${actionId}-option-${index}`, + weight: parts[0].weight, + content: parts.map((p) => p.content.trim()).join(multiSelectAction.joinWith), + actionId: `${actionId}-combined`, stageIndex, }) } + } else { + for (const index of selectedIndices) { + const option = multiSelectAction.options[index] + if (option && 'message' in option && 'weight' in option) { + const message = (await option.message()) as string + messageParts.push({ + weight: option.weight, + content: processMessage(message, action, stageIndex, textInputValues.value), + actionId: `${actionId}-option-${index}`, + stageIndex, + }) + } + } } } } diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content-header.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content-header.md new file mode 100644 index 0000000000..11d70d2a79 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content-header.md @@ -0,0 +1,2 @@ +## Prohibited Content +Per section 1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules), this project contains the following prohibited content: diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md new file mode 100644 index 0000000000..99f542f935 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md @@ -0,0 +1 @@ +- Sexually explicit or pornographic material, or content promoting violence or discrimination based on race, sex, gender, religion, nationality, disability, sexual orientation, or age (1.2) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md new file mode 100644 index 0000000000..4f04352b73 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md @@ -0,0 +1 @@ +- Content giving a false impression of endorsement by Modrinth or any other person or entity (1.9) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md new file mode 100644 index 0000000000..b8061b7cfc --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md @@ -0,0 +1 @@ +- Content likely to cause annoyance, harm, embarrassment, alarm, or deception to others (1.6) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md new file mode 100644 index 0000000000..4b554e15dc --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md @@ -0,0 +1 @@ +- Promotion of illegal activity, or advocacy of unlawful acts including real-life drugs or illicit substances (1.5) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md new file mode 100644 index 0000000000..1e2f8f2330 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md @@ -0,0 +1 @@ +- Impersonation of a person, or misrepresentation of identity or affiliation with any person or organization (1.8) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md new file mode 100644 index 0000000000..164fe6a48e --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md @@ -0,0 +1 @@ +- Content infringing patents, trademarks, trade secrets, copyright, or other intellectual property rights (1.3) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md new file mode 100644 index 0000000000..a388b6944a --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md @@ -0,0 +1 @@ +- Content violating the legal rights of others (1.4) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md new file mode 100644 index 0000000000..4148d3b726 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md @@ -0,0 +1 @@ +- Intentionally wrong or misleading claims (1.7) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md new file mode 100644 index 0000000000..324e96e1dd --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md @@ -0,0 +1 @@ +- Content bypassing restrictions placed by Mojang to prevent users from joining certain in-game servers (1.12) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md new file mode 100644 index 0000000000..7114e01b8a --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md @@ -0,0 +1 @@ +- Defamatory, obscene, indecent, abusive, offensive, harassing, violent, hateful, inflammatory, or otherwise objectionable material (1.1) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md new file mode 100644 index 0000000000..10bad943c1 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md @@ -0,0 +1 @@ +- Excessive profanity (1.10) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md new file mode 100644 index 0000000000..de6997d590 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md @@ -0,0 +1 @@ +- Undisclosed upload of data to a remote server without clear disclosure (1.11) diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in-header.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in-header.md new file mode 100644 index 0000000000..906030e71a --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in-header.md @@ -0,0 +1,2 @@ +## Server-side opt-in required +Per section 3.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules), the following feature(s) in this project require a server-side opt-in: diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md new file mode 100644 index 0000000000..5799525eb6 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md @@ -0,0 +1 @@ +- Aim bot or aim assist diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md new file mode 100644 index 0000000000..23c44a91da --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md @@ -0,0 +1 @@ +- Active client-side hiding of third-party modifications that have server-side opt-outs diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md new file mode 100644 index 0000000000..f95c4236d5 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md @@ -0,0 +1 @@ +- Item duplication diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md new file mode 100644 index 0000000000..339d3ce05f --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md @@ -0,0 +1 @@ +- Flight, speed, or other movement modifications diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md new file mode 100644 index 0000000000..b9b9b6f309 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md @@ -0,0 +1 @@ +- Automatic or assisted PvP combat diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md new file mode 100644 index 0000000000..365a8ce258 --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md @@ -0,0 +1 @@ +- X-ray or the ability to see through opaque blocks diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-out.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-out.md new file mode 100644 index 0000000000..1479d7312d --- /dev/null +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-out.md @@ -0,0 +1,2 @@ +## Server-side opt-out required +Per section 3.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules), mods that give an unfair advantage in a multiplayer setting over other players that do not have a comparable modification must have a server-side opt-out. diff --git a/packages/moderation/src/data/stages/rule-following.ts b/packages/moderation/src/data/stages/rule-following.ts index 98761d1a96..57af108630 100644 --- a/packages/moderation/src/data/stages/rule-following.ts +++ b/packages/moderation/src/data/stages/rule-following.ts @@ -1,6 +1,6 @@ import { ListBulletedIcon } from '@modrinth/assets' -import type { ButtonAction } from '../../types/actions' +import type { ButtonAction, MultiSelectChipsAction } from '../../types/actions' import type { Stage } from '../../types/stage' const ruleFollowing: Stage = { @@ -40,6 +40,171 @@ const ruleFollowing: Stage = { }, message: async () => (await import('../messages/paid-access-server.md?raw')).default, }, + { + id: 'prohibited_content', + type: 'button', + label: 'Prohibited content (1)', + weight: 0, + suggestedStatus: 'rejected', + severity: 'critical', + message: async () => + (await import('../messages/rule-breaking/prohibited-content-header.md?raw')).default.trimEnd(), + enablesActions: [ + { + id: 'prohibited_content_options', + type: 'multi-select-chips', + label: 'Which section 1 violations apply?', + joinWith: '\n', + options: [ + { + label: 'Objectionable', + weight: 1, + message: async () => + ( + await import('../messages/rule-breaking/prohibited-content/objectionable.md?raw')).default, + }, + { + label: 'Discriminatory or Explicit', + weight: 2, + message: async () => + ( + await import('../messages/rule-breaking/prohibited-content/discriminatory.md?raw')).default, + }, + { + label: 'IP Infringement', + weight: 3, + message: async () => + ( + await import('../messages/rule-breaking/prohibited-content/ip-infringement.md?raw')).default, + }, + { + label: 'Rights Violation', + weight: 4, + message: async () => + ( + await import('../messages/rule-breaking/prohibited-content/legal-rights.md?raw')).default, + }, + { + label: 'Illegal Activity', + weight: 5, + message: async () => + ( + await import('../messages/rule-breaking/prohibited-content/illegal-activity.md?raw')).default, + }, + { + label: 'Harmful or Deceptive', + weight: 6, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/harmful.md?raw')).default, + }, + { + label: 'Misleading claims', + weight: 7, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/misleading.md?raw')).default, + }, + { + label: 'Impersonation', + weight: 8, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/impersonation.md?raw')).default, + }, + { + label: 'False Endorsement', + weight: 9, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/false-endorsement.md?raw')).default, + }, + { + label: 'Profanity', + weight: 10, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/profanity.md?raw')).default, + }, + { + label: 'Undisclosed Data Upload', + weight: 11, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/undisclosed-upload.md?raw')).default, + }, + { + label: 'Mojang Bypass', + weight: 12, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/mojang-bypass.md?raw')).default, + }, + ], + } as MultiSelectChipsAction, + ], + } as ButtonAction, + { + id: 'server_side_opt_out', + type: 'button', + label: '3.2 (Opt-out)', + weight: 0, + suggestedStatus: 'flagged', + severity: 'high', + message: async () => + (await import('../messages/rule-breaking/server-side-opt-out.md?raw')).default, + } as ButtonAction, + { + id: 'server_side_opt_in', + type: 'button', + label: '3.3 (Opt-in)', + weight: 0, + suggestedStatus: 'flagged', + severity: 'high', + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in-header.md?raw')).default.trimEnd(), + enablesActions: [ + { + id: 'server_side_opt_in_options', + type: 'multi-select-chips', + label: 'Which section 3.3 violations apply?', + joinWith: '\n', + options: [ + { + label: 'X-ray', + weight: 1, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in/x-ray.md?raw')).default, + }, + { + label: 'Aim Assist', + weight: 2, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in/aim-bot.md?raw')).default, + }, + { + label: 'Movement', + weight: 3, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in/movement.md?raw')).default, + }, + { + label: 'PvP', + weight: 4, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in/pvp.md?raw')).default, + }, + { + label: 'Anti 3.x', + weight: 5, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in/hiding-mods.md?raw')).default, + }, + { + label: 'Dupes', + weight: 6, + message: async () => + ( + await import('../messages/rule-breaking/server-side-opt-in/item-duplication.md?raw') + ).default, + }, + ], + } as MultiSelectChipsAction, + ], + } as ButtonAction, { id: 'excessive_languages', type: 'button', diff --git a/packages/moderation/src/types/actions.ts b/packages/moderation/src/types/actions.ts index 3c6877fd95..3f054d133a 100644 --- a/packages/moderation/src/types/actions.ts +++ b/packages/moderation/src/types/actions.ts @@ -222,6 +222,12 @@ export interface MultiSelectChipsAction extends BaseAction { * The options available in the multi-select chips. */ options: MultiSelectChipsOption[] + + /** + * If set, all selected option messages are joined with this string and emitted as a single + * message part rather than individual parts. Useful for building bullet lists under a shared header. + */ + joinWith?: string } export interface AdditionalTextInput { From 792e194d3fa18bf5980a2c4f13ada2cbe420f9f0 Mon Sep 17 00:00:00 2001 From: chyzman Date: Sat, 2 May 2026 23:02:25 -0400 Subject: [PATCH 02/85] reorganize --- .../prohibited-content/discriminatory.md | 2 +- .../prohibited-content/false-endorsement.md | 2 +- .../prohibited-content/harmful.md | 2 +- .../prohibited-content/illegal-activity.md | 2 +- .../prohibited-content/impersonation.md | 2 +- .../prohibited-content/ip-infringement.md | 2 +- .../prohibited-content/legal-rights.md | 2 +- .../prohibited-content/misleading.md | 2 +- .../prohibited-content/mojang-bypass.md | 2 +- .../prohibited-content/objectionable.md | 2 +- .../prohibited-content/profanity.md | 2 +- .../prohibited-content/undisclosed-upload.md | 2 +- .../server-side-opt-in/aim-bot.md | 2 +- .../server-side-opt-in/hiding-mods.md | 2 +- .../server-side-opt-in/item-duplication.md | 2 +- .../server-side-opt-in/movement.md | 2 +- .../rule-breaking/server-side-opt-in/pvp.md | 2 +- .../rule-breaking/server-side-opt-in/x-ray.md | 2 +- .../src/data/stages/rule-following.ts | 198 +++++------------- 19 files changed, 66 insertions(+), 168 deletions(-) diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md index 99f542f935..2ae4a60015 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md @@ -1 +1 @@ -- Sexually explicit or pornographic material, or content promoting violence or discrimination based on race, sex, gender, religion, nationality, disability, sexual orientation, or age (1.2) +- 1.2: Sexually explicit or pornographic material, or content promoting violence or discrimination based on race, sex, gender, religion, nationality, disability, sexual orientation, or age diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md index 4f04352b73..ac6fbde059 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md @@ -1 +1 @@ -- Content giving a false impression of endorsement by Modrinth or any other person or entity (1.9) +- 1.9: Content giving a false impression of endorsement by Modrinth or any other person or entity diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md index b8061b7cfc..9ddaea6e8e 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md @@ -1 +1 @@ -- Content likely to cause annoyance, harm, embarrassment, alarm, or deception to others (1.6) +- 1.6: Content likely to cause annoyance, harm, embarrassment, alarm, or deception to others diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md index 4b554e15dc..aaf276c50b 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md @@ -1 +1 @@ -- Promotion of illegal activity, or advocacy of unlawful acts including real-life drugs or illicit substances (1.5) +- 1.5: Promotion of illegal activity, or advocacy of unlawful acts including real-life drugs or illicit substances diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md index 1e2f8f2330..ab3be29e7c 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md @@ -1 +1 @@ -- Impersonation of a person, or misrepresentation of identity or affiliation with any person or organization (1.8) +- 1.8: Impersonation of a person, or misrepresentation of identity or affiliation with any person or organization diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md index 164fe6a48e..0eee5443a1 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md @@ -1 +1 @@ -- Content infringing patents, trademarks, trade secrets, copyright, or other intellectual property rights (1.3) +- 1.3: Content infringing patents, trademarks, trade secrets, copyright, or other intellectual property rights diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md index a388b6944a..749304631d 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md @@ -1 +1 @@ -- Content violating the legal rights of others (1.4) +- 1.4: Content violating the legal rights of others diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md index 4148d3b726..005d8d88fb 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md @@ -1 +1 @@ -- Intentionally wrong or misleading claims (1.7) +- 1.7: Intentionally wrong or misleading claims diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md index 324e96e1dd..3706c42442 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md @@ -1 +1 @@ -- Content bypassing restrictions placed by Mojang to prevent users from joining certain in-game servers (1.12) +- 1.12: Content bypassing restrictions placed by Mojang to prevent users from joining certain in-game servers diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md index 7114e01b8a..02cc641afb 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md @@ -1 +1 @@ -- Defamatory, obscene, indecent, abusive, offensive, harassing, violent, hateful, inflammatory, or otherwise objectionable material (1.1) +- 1.1: Defamatory, obscene, indecent, abusive, offensive, harassing, violent, hateful, inflammatory, or otherwise objectionable material diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md index 10bad943c1..1e42162eea 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md @@ -1 +1 @@ -- Excessive profanity (1.10) +- 1.10: Excessive profanity diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md index de6997d590..4b0967feab 100644 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md +++ b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md @@ -1 +1 @@ -- Undisclosed upload of data to a remote server without clear disclosure (1.11) +- 1.11: Undisclosed upload of data to a remote server without clear disclosure diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md index 5799525eb6..16f6e2644e 100644 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md @@ -1 +1 @@ -- Aim bot or aim assist +- 3.3b: Aim bot or aim assist diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md index 23c44a91da..a4d4ad5d52 100644 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md @@ -1 +1 @@ -- Active client-side hiding of third-party modifications that have server-side opt-outs +- 3.3e: Active client-side hiding of third-party modifications that have server-side opt-outs diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md index f95c4236d5..feae0986ec 100644 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md @@ -1 +1 @@ -- Item duplication +- 3.3f: Item duplication diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md index 339d3ce05f..8422514558 100644 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md @@ -1 +1 @@ -- Flight, speed, or other movement modifications +- 3.3c: Flight, speed, or other movement modifications diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md index b9b9b6f309..d1cc5099c4 100644 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md @@ -1 +1 @@ -- Automatic or assisted PvP combat +- 3.3d: Automatic or assisted PvP combat diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md index 365a8ce258..a58d28ddd9 100644 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md +++ b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md @@ -1 +1 @@ -- X-ray or the ability to see through opaque blocks +- 3.3a: X-ray or the ability to see through opaque blocks diff --git a/packages/moderation/src/data/stages/rule-following.ts b/packages/moderation/src/data/stages/rule-following.ts index 57af108630..f38d101e6a 100644 --- a/packages/moderation/src/data/stages/rule-following.ts +++ b/packages/moderation/src/data/stages/rule-following.ts @@ -7,27 +7,9 @@ const ruleFollowing: Stage = { title: 'Does this project violate the rules?', id: 'rule-following', icon: ListBulletedIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080709084f6269835607f', + guidance_url: 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080709084f6269835607f', navigate: '/moderation', actions: [ - { - id: 'rule_breaking_yes', - type: 'button', - label: 'Yes', - weight: 0, - suggestedStatus: 'rejected', - severity: 'critical', - message: async () => (await import('../messages/rule-breaking.md?raw')).default, - relevantExtraInput: [ - { - label: 'Please explain to the user how it infringes on our content rules.', - variable: 'MESSAGE', - required: true, - large: true, - }, - ], - } as ButtonAction, { id: 'paid_access_server', type: 'button', @@ -43,96 +25,30 @@ const ruleFollowing: Stage = { { id: 'prohibited_content', type: 'button', - label: 'Prohibited content (1)', - weight: 0, + label: 'Prohibited Content', + weight: 100, suggestedStatus: 'rejected', severity: 'critical', - message: async () => - (await import('../messages/rule-breaking/prohibited-content-header.md?raw')).default.trimEnd(), + message: async () => (await import('../messages/rule-breaking/prohibited-content-header.md?raw')).default.trimEnd(), enablesActions: [ { id: 'prohibited_content_options', type: 'multi-select-chips', - label: 'Which section 1 violations apply?', + label: 'Which Prohibited Content rules does this project violate?', joinWith: '\n', options: [ - { - label: 'Objectionable', - weight: 1, - message: async () => - ( - await import('../messages/rule-breaking/prohibited-content/objectionable.md?raw')).default, - }, - { - label: 'Discriminatory or Explicit', - weight: 2, - message: async () => - ( - await import('../messages/rule-breaking/prohibited-content/discriminatory.md?raw')).default, - }, - { - label: 'IP Infringement', - weight: 3, - message: async () => - ( - await import('../messages/rule-breaking/prohibited-content/ip-infringement.md?raw')).default, - }, - { - label: 'Rights Violation', - weight: 4, - message: async () => - ( - await import('../messages/rule-breaking/prohibited-content/legal-rights.md?raw')).default, - }, - { - label: 'Illegal Activity', - weight: 5, - message: async () => - ( - await import('../messages/rule-breaking/prohibited-content/illegal-activity.md?raw')).default, - }, - { - label: 'Harmful or Deceptive', - weight: 6, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/harmful.md?raw')).default, - }, - { - label: 'Misleading claims', - weight: 7, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/misleading.md?raw')).default, - }, - { - label: 'Impersonation', - weight: 8, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/impersonation.md?raw')).default, - }, - { - label: 'False Endorsement', - weight: 9, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/false-endorsement.md?raw')).default, - }, - { - label: 'Profanity', - weight: 10, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/profanity.md?raw')).default, - }, - { - label: 'Undisclosed Data Upload', - weight: 11, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/undisclosed-upload.md?raw')).default, - }, - { - label: 'Mojang Bypass', - weight: 12, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/mojang-bypass.md?raw')).default, - }, + {label: 'Objectionable', weight: 101, message: async () => (await import('../messages/rule-breaking/prohibited-content/objectionable.md?raw')).default,}, + {label: 'Discriminatory or Explicit', weight: 102, message: async () => (await import('../messages/rule-breaking/prohibited-content/discriminatory.md?raw')).default,}, + {label: 'IP Infringement', weight: 103, message: async () => (await import('../messages/rule-breaking/prohibited-content/ip-infringement.md?raw')).default,}, + {label: 'Rights Violation', weight: 104, message: async () => (await import('../messages/rule-breaking/prohibited-content/legal-rights.md?raw')).default,}, + {label: 'Illegal Activity', weight: 105, message: async () => (await import('../messages/rule-breaking/prohibited-content/illegal-activity.md?raw')).default,}, + {label: 'Harmful or Deceptive', weight: 106, message: async () => (await import('../messages/rule-breaking/prohibited-content/harmful.md?raw')).default,}, + {label: 'Misleading claims', weight: 107, message: async () => (await import('../messages/rule-breaking/prohibited-content/misleading.md?raw')).default,}, + {label: 'Impersonation', weight: 108, message: async () => (await import('../messages/rule-breaking/prohibited-content/impersonation.md?raw')).default,}, + {label: 'False Endorsement', weight: 109, message: async () => (await import('../messages/rule-breaking/prohibited-content/false-endorsement.md?raw')).default,}, + {label: 'Profanity', weight: 110, message: async () => (await import('../messages/rule-breaking/prohibited-content/profanity.md?raw')).default,}, + {label: 'Undisclosed Data Upload', weight: 111, message: async () => (await import('../messages/rule-breaking/prohibited-content/undisclosed-upload.md?raw')).default,}, + {label: 'Mojang Bypass', weight: 112, message: async () => (await import('../messages/rule-breaking/prohibited-content/mojang-bypass.md?raw')).default,}, ], } as MultiSelectChipsAction, ], @@ -140,67 +56,33 @@ const ruleFollowing: Stage = { { id: 'server_side_opt_out', type: 'button', - label: '3.2 (Opt-out)', - weight: 0, + label: 'Opt-out', + weight: 200, suggestedStatus: 'flagged', severity: 'high', - message: async () => - (await import('../messages/rule-breaking/server-side-opt-out.md?raw')).default, + message: async () => (await import('../messages/rule-breaking/server-side-opt-out.md?raw')).default, } as ButtonAction, { id: 'server_side_opt_in', type: 'button', - label: '3.3 (Opt-in)', - weight: 0, + label: 'Opt-in', + weight: 300, suggestedStatus: 'flagged', severity: 'high', - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in-header.md?raw')).default.trimEnd(), + message: async () => (await import('../messages/rule-breaking/server-side-opt-in-header.md?raw')).default.trimEnd(), enablesActions: [ { id: 'server_side_opt_in_options', type: 'multi-select-chips', - label: 'Which section 3.3 violations apply?', + label: 'Which features of this project require a Server-side Opt-in?', joinWith: '\n', options: [ - { - label: 'X-ray', - weight: 1, - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in/x-ray.md?raw')).default, - }, - { - label: 'Aim Assist', - weight: 2, - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in/aim-bot.md?raw')).default, - }, - { - label: 'Movement', - weight: 3, - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in/movement.md?raw')).default, - }, - { - label: 'PvP', - weight: 4, - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in/pvp.md?raw')).default, - }, - { - label: 'Anti 3.x', - weight: 5, - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in/hiding-mods.md?raw')).default, - }, - { - label: 'Dupes', - weight: 6, - message: async () => - ( - await import('../messages/rule-breaking/server-side-opt-in/item-duplication.md?raw') - ).default, - }, + {label: 'X-ray', weight: 301, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/x-ray.md?raw')).default,}, + {label: 'Aim Assist', weight: 302, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/aim-bot.md?raw')).default,}, + {label: 'Movement', weight: 303, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/movement.md?raw')).default,}, + {label: 'PvP', weight: 304, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/pvp.md?raw')).default,}, + {label: 'Anti 3.x', weight: 305, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/hiding-mods.md?raw')).default,}, + {label: 'Dupe', weight: 306, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/item-duplication.md?raw')).default,}, ], } as MultiSelectChipsAction, ], @@ -219,9 +101,25 @@ const ruleFollowing: Stage = { projectV3?.minecraft_server?.languages?.length > 4 ) }, - message: async () => - (await import('../messages/misc-metadata/excessive_languages-server.md?raw')).default, + message: async () => (await import('../messages/misc-metadata/excessive_languages-server.md?raw')).default, }, + { + id: 'rule_breaking_other', + type: 'button', + label: 'Other', + weight: 0, + suggestedStatus: 'rejected', + severity: 'critical', + message: async () => (await import('../messages/rule-breaking.md?raw')).default, + relevantExtraInput: [ + { + label: 'Please explain to the user how it infringes on our content rules.', + variable: 'MESSAGE', + required: true, + large: true, + }, + ], + } as ButtonAction, ], } From f1c6ef599a209586853239d3d8e9c43e67ef47fc Mon Sep 17 00:00:00 2001 From: chyzman Date: Sun, 3 May 2026 01:29:34 -0400 Subject: [PATCH 03/85] Kinda Jank dynamic list stuff that should probably be implemented better but its 1:30 am --- .../checklist/ModerationChecklist.vue | 177 ++++++++++++------ .../src/data/messages/links/misused.md | 4 +- .../messages/versions/incorrect_loader.md | 4 + packages/moderation/src/data/stages/links.ts | 157 +++++++++++++--- .../moderation/src/data/stages/versions.ts | 60 +++++- packages/moderation/src/types/actions.ts | 20 +- packages/moderation/src/utils.ts | 4 +- 7 files changed, 332 insertions(+), 94 deletions(-) create mode 100644 packages/moderation/src/data/messages/versions/incorrect_loader.md diff --git a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue index e5e09569af..955cdc4ef4 100644 --- a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue @@ -257,10 +257,10 @@
{{ action.label }}
+ + + + + + +
+ + + + + + + @@ -617,6 +617,134 @@ const PREFETCH_STALE_MS = 30_000 // 30 seconds const PREFETCH_TARGET_COUNT = 3 // Keep 3 unlocked projects ready const PREFETCH_BATCH_SIZE = 5 // Check 5 at a time in parallel +// Tooltip constants and cache +const BUTTON_TOOLTIP_DELAY_MS = 500 // Show tooltip after 1.5 seconds of hovering +const BUTTON_TOOLTIP_HIDE_DELAY_MS = 0 // Hide immediately on mouseleave +const buttonActionTooltipCache = ref>({}) +const TOOLTIP_CACHE_TTL_MS = 30000 // Cache tooltip text for 30 seconds + +// Helper: compute the message text that a button action would generate +async function getButtonActionTooltipText(action: Action): Promise { + try { + const actionIndex = currentStageObj.value.actions.indexOf(action) + const actionId = getActionId(action, actionIndex) + const state = actionStates.value[actionId] + + if (!state) return '' + + // Build list of selected action IDs (including this action as if selected) + const tempSelectedIds = Object.entries(actionStates.value) + .filter(([_, s]) => s.selected) + .map(([id]) => id) + + if (!tempSelectedIds.includes(actionId)) { + tempSelectedIds.push(actionId) + } + + // Build all valid action IDs across all stages + const allValidActionIds: string[] = [] + checklist.forEach((stage, stageIdx) => { + stage.actions.forEach((stageAction, actionIdx) => { + allValidActionIds.push(getActionIdForStage(stageAction, stageIdx, actionIdx)) + if (stageAction.enablesActions) { + stageAction.enablesActions.forEach((enabledAction, enabledIdx) => { + allValidActionIds.push( + getActionIdForStage(enabledAction, stageIdx, actionIdx, enabledIdx), + ) + }) + } + }) + }) + + let messageText = '' + + if (action.type === 'button' || action.type === 'toggle') { + const buttonAction = action as ButtonAction | ToggleAction + const message = await getActionMessage(buttonAction, tempSelectedIds, allValidActionIds) + if (message) { + messageText = processMessage(message, action, currentStage.value, textInputValues.value) + } + } else if (action.type === 'conditional-button') { + const conditionalAction = action as ConditionalButtonAction + const matchingVariant = findMatchingVariant( + conditionalAction.messageVariants, + tempSelectedIds, + allValidActionIds, + currentStage.value, + ) + + if (matchingVariant) { + const message = (await matchingVariant.message()) as string + messageText = processMessage(message, action, currentStage.value, textInputValues.value) + } else if (conditionalAction.fallbackMessage) { + const message = (await conditionalAction.fallbackMessage()) as string + messageText = processMessage(message, action, currentStage.value, textInputValues.value) + } + } + + return expandVariables( + messageText.trim(), + projectV2.value, + projectV3.value, + variables.value, + ).trim() + } catch (error) { + console.warn('[tooltip] Error computing action message:', error) + return '' + } +} + +// Helper: render markdown tooltip content using the app's markdown renderer +function renderTooltipMarkdownHtml(text: string): string { + const trimmed = text/*.slice(0, 500)*/.trim() + if (!trimmed) return '' + + return `
${renderHighlightedString(trimmed)}
` +} + +// Helper: get tooltip directive config for a button action with caching & delay +function getChecklistButtonTooltipConfig(action: Action): any { + const actionKey = getActionKey(action) + const now = Date.now() + const cached = buttonActionTooltipCache.value[actionKey] + + if (cached && now < cached.expiresAt) { + const renderedHtml = renderTooltipMarkdownHtml(cached.text) + return renderedHtml + ? { + content: renderedHtml, + html: true, + delay: { show: BUTTON_TOOLTIP_DELAY_MS, hide: BUTTON_TOOLTIP_HIDE_DELAY_MS }, + triggers: ['hover', 'focus'], + placement: 'top', + } + : undefined + } + + void (async () => { + const text = await getButtonActionTooltipText(action) + buttonActionTooltipCache.value[actionKey] = { + text, + expiresAt: Date.now() + TOOLTIP_CACHE_TTL_MS, + } + })() + + if (cached) { + const renderedHtml = renderTooltipMarkdownHtml(cached.text) + return renderedHtml + ? { + content: renderedHtml, + html: true, + delay: { show: BUTTON_TOOLTIP_DELAY_MS, hide: BUTTON_TOOLTIP_HIDE_DELAY_MS }, + triggers: ['hover', 'focus'], + placement: 'top', + } + : undefined + } + + return undefined +} + async function handleVisibilityChange() { if (document.visibilityState === 'visible' && lockStatus.value?.isOwnLock) { // Immediately refresh the lock when returning to the tab @@ -2366,4 +2494,50 @@ const stageOptionsForSlots = computed(() => cursor: default; } } + +// Tooltip styling for button action message previews. +// Must use :global since floating-vue teleports tooltips outside the component DOM. +:global(.v-popper--theme-tooltip .v-popper__inner) { + max-width: 400px; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; +} + +:global(.v-popper--theme-tooltip .moderation-tooltip-markdown) { + line-height: 1.45; + font-size: 0.9rem; +} + +:global(.v-popper--theme-tooltip .moderation-tooltip-markdown p) { + margin: 0.35rem 0; +} + +:global(.v-popper--theme-tooltip .moderation-tooltip-markdown ul), +:global(.v-popper--theme-tooltip .moderation-tooltip-markdown ol) { + margin: 0.35rem 0; + padding-left: 1.15rem; +} + +:global(.v-popper--theme-tooltip .moderation-tooltip-markdown pre) { + max-width: 100%; + overflow-x: auto; + margin: 0.4rem 0; +} + +:global(.v-popper--theme-tooltip .moderation-tooltip-markdown code) { + background-color: rgba(255, 255, 255, 0.15); + padding: 0.1rem 0.3rem; + border-radius: 0.25rem; + font-family: monospace; + font-size: 0.85em; +} + +:global(.v-popper--theme-tooltip .moderation-tooltip-markdown strong) { + font-weight: 700; +} + +:global(.v-popper--theme-tooltip .moderation-tooltip-markdown em) { + font-style: italic; +} diff --git a/packages/moderation/src/data/messages/rule-breaking/cheat-or-hack-advertising.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/cheat-or-hack-advertising.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/cheat-or-hack-advertising.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/cheat-or-hack-advertising.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content-header.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content-header.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content-header.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content-header.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/discriminatory.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/discriminatory.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/false-endorsement.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/false-endorsement.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/harmful.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/harmful.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/illegal-activity.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/illegal-activity.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/impersonation.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/impersonation.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/ip-infringement.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/ip-infringement.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/legal-rights.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/legal-rights.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/misleading.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/misleading.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/mojang-bypass.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/mojang-bypass.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/objectionable.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/objectionable.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/profanity.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/profanity.md diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/undisclosed-upload.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/prohibited-content/undisclosed-upload.md diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in-header.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in-header.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/server-side-opt-in-header.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in-header.md diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/aim-bot.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/aim-bot.md diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/hiding-mods.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/hiding-mods.md diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/item-duplication.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/item-duplication.md diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/movement.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/movement.md diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/pvp.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/pvp.md diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/x-ray.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-in/x-ray.md diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-out.md b/packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-out.md similarity index 100% rename from packages/moderation/src/data/messages/rule-breaking/server-side-opt-out.md rename to packages/moderation/src/data/messages/checklist-messages/rule-breaking/server-side-opt-out.md diff --git a/packages/moderation/src/data/messages/checklist-messages/versions/incorrect_loader.md b/packages/moderation/src/data/messages/checklist-messages/versions/incorrect_loader.md new file mode 100644 index 0000000000..17c12c713c --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/versions/incorrect_loader.md @@ -0,0 +1,4 @@ +## Incorrect Loader Labels + +Per section 5.7 of %RULES%, the loader labels on each of your %PROJECT_VERSIONS_FLINK% must accurately reflect what the uploaded files support. +Currently, some of your versions appear to use the following incorrect loader label(s). Please remove these loaders from any affected versions before resubmitting your project: diff --git a/packages/moderation/src/data/stages/description.ts b/packages/moderation/src/data/stages/description.ts index 6422a64862..7c6211a424 100644 --- a/packages/moderation/src/data/stages/description.ts +++ b/packages/moderation/src/data/stages/description.ts @@ -19,7 +19,9 @@ const description: Stage = { suggestedStatus: 'flagged', severity: 'medium', message: async () => - (await import('../messages/description/insufficient/insufficient-header.md?raw')).default, + ( + await import('../messages/checklist-messages/description/insufficient/insufficient-header.md?raw') + ).default, enablesActions: [ { id: 'description_insufficient_options', @@ -33,8 +35,9 @@ const description: Stage = { shouldShow: (project, projectV3) => project.project_type === 'modpack' && !projectV3?.minecraft_server, message: async () => - (await import('../messages/checklist-messages/description/insufficient/insufficient-packs.md?raw')) - .default, + ( + await import('../messages/checklist-messages/description/insufficient/insufficient-packs.md?raw') + ).default, }, { id: 'description_insufficient_projects', @@ -42,34 +45,38 @@ const description: Stage = { weight: 402, shouldShow: (project, projectV3) => project.project_type !== 'modpack' && !projectV3?.minecraft_server, - message: async () =>( - await import('../messages/checklist-messages/description/insufficient/insufficient-projects.md?raw')) - .default, + message: async () => + ( + await import('../messages/checklist-messages/description/insufficient/insufficient-projects.md?raw') + ).default, }, { id: 'description_insufficient_servers', label: 'Missing basic details', weight: 403, shouldShow: (project, projectV3) => !!projectV3?.minecraft_java_server, - message: async () =>( - await import('../messages/checklist-messages/description/insufficient/insufficient-servers.md?raw')) - .default, + message: async () => + ( + await import('../messages/checklist-messages/description/insufficient/insufficient-servers.md?raw') + ).default, }, { id: 'description_insufficient_fork', label: 'Fork', weight: 404, message: async () => - (await import('../messages/description/insufficient/insufficient-fork.md?raw')) - .default, + ( + await import('../messages/checklist-messages/description/insufficient/insufficient-fork.md?raw') + ).default, }, { id: 'description_insufficient_port', label: 'Port', weight: 405, message: async () => - (await import('../messages/description/insufficient/insufficient-port.md?raw')) - .default, + ( + await import('../messages/checklist-messages/description/insufficient/insufficient-port.md?raw') + ).default, }, ], } as MultiSelectChipsAction, @@ -80,8 +87,10 @@ const description: Stage = { weight: 406, suggestedStatus: 'flagged', severity: 'medium', - message: async () =>( - await import('../messages/checklist-messages/description/insufficient/insufficient.md?raw')).default, + message: async () => + ( + await import('../messages/checklist-messages/description/insufficient/insufficient.md?raw') + ).default, relevantExtraInput: [ { label: 'Please elaborate on how the author can improve their description.', diff --git a/packages/moderation/src/data/stages/links.ts b/packages/moderation/src/data/stages/links.ts index 7a445a3ea7..d1345ac3d6 100644 --- a/packages/moderation/src/data/stages/links.ts +++ b/packages/moderation/src/data/stages/links.ts @@ -46,7 +46,8 @@ function getProjectLinkDefinitions(context: ChecklistActionContext): LinkOptionD pushLink( 'source', 'Source', - async () => (await import('../messages/links/not_accessible-source.md?raw')).default, + async () => + (await import('../messages/checklist-messages/links/not_accessible-source.md?raw')).default, ) } @@ -63,7 +64,9 @@ function getProjectLinkDefinitions(context: ChecklistActionContext): LinkOptionD pushLink( 'discord', 'Discord', - async () => (await import('../messages/links/not_accessible-discord.md?raw')).default, + async () => + (await import('../messages/checklist-messages/links/not_accessible-discord.md?raw')) + .default, ) } diff --git a/packages/moderation/src/data/stages/rule-following.ts b/packages/moderation/src/data/stages/rule-following.ts index e3cd506d3f..e915f32d47 100644 --- a/packages/moderation/src/data/stages/rule-following.ts +++ b/packages/moderation/src/data/stages/rule-following.ts @@ -7,7 +7,8 @@ const ruleFollowing: Stage = { title: 'Does this project violate the rules?', id: 'rule-following', icon: ListBulletedIcon, - guidance_url: 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080709084f6269835607f', + guidance_url: + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080709084f6269835607f', navigate: '/moderation', actions: [ { @@ -30,7 +31,10 @@ const ruleFollowing: Stage = { weight: 100, suggestedStatus: 'rejected', severity: 'critical', - message: async () => (await import('../messages/rule-breaking/prohibited-content-header.md?raw')).default.trimEnd(), + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content-header.md?raw') + ).default.trimEnd(), enablesActions: [ { id: 'prohibited_content_options', @@ -38,18 +42,102 @@ const ruleFollowing: Stage = { label: 'Which Prohibited Content rules does this project violate?', joinWith: '\n', options: [ - {label: 'Objectionable', weight: 101, message: async () => (await import('../messages/rule-breaking/prohibited-content/objectionable.md?raw')).default,}, - {label: 'Discriminatory or Explicit', weight: 102, message: async () => (await import('../messages/rule-breaking/prohibited-content/discriminatory.md?raw')).default,}, - {label: 'IP Infringement', weight: 103, message: async () => (await import('../messages/rule-breaking/prohibited-content/ip-infringement.md?raw')).default,}, - {label: 'Rights Violation', weight: 104, message: async () => (await import('../messages/rule-breaking/prohibited-content/legal-rights.md?raw')).default,}, - {label: 'Illegal Activity', weight: 105, message: async () => (await import('../messages/rule-breaking/prohibited-content/illegal-activity.md?raw')).default,}, - {label: 'Harmful or Deceptive', weight: 106, message: async () => (await import('../messages/rule-breaking/prohibited-content/harmful.md?raw')).default,}, - {label: 'Misleading claims', weight: 107, message: async () => (await import('../messages/rule-breaking/prohibited-content/misleading.md?raw')).default,}, - {label: 'Impersonation', weight: 108, message: async () => (await import('../messages/rule-breaking/prohibited-content/impersonation.md?raw')).default,}, - {label: 'False Endorsement', weight: 109, message: async () => (await import('../messages/rule-breaking/prohibited-content/false-endorsement.md?raw')).default,}, - {label: 'Profanity', weight: 110, message: async () => (await import('../messages/rule-breaking/prohibited-content/profanity.md?raw')).default,}, - {label: 'Undisclosed Data Upload', weight: 111, message: async () => (await import('../messages/rule-breaking/prohibited-content/undisclosed-upload.md?raw')).default,}, - {label: 'Mojang Bypass', weight: 112, message: async () => (await import('../messages/rule-breaking/prohibited-content/mojang-bypass.md?raw')).default,}, + { + label: 'Objectionable', + weight: 101, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/objectionable.md?raw') + ).default, + }, + { + label: 'Discriminatory or Explicit', + weight: 102, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/discriminatory.md?raw') + ).default, + }, + { + label: 'IP Infringement', + weight: 103, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/ip-infringement.md?raw') + ).default, + }, + { + label: 'Rights Violation', + weight: 104, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/legal-rights.md?raw') + ).default, + }, + { + label: 'Illegal Activity', + weight: 105, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/illegal-activity.md?raw') + ).default, + }, + { + label: 'Harmful or Deceptive', + weight: 106, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/harmful.md?raw') + ).default, + }, + { + label: 'Misleading claims', + weight: 107, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/misleading.md?raw') + ).default, + }, + { + label: 'Impersonation', + weight: 108, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/impersonation.md?raw') + ).default, + }, + { + label: 'False Endorsement', + weight: 109, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/false-endorsement.md?raw') + ).default, + }, + { + label: 'Profanity', + weight: 110, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/profanity.md?raw') + ).default, + }, + { + label: 'Undisclosed Data Upload', + weight: 111, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/undisclosed-upload.md?raw') + ).default, + }, + { + label: 'Mojang Bypass', + weight: 112, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/prohibited-content/mojang-bypass.md?raw') + ).default, + }, ], } as MultiSelectChipsAction, ], @@ -61,7 +149,10 @@ const ruleFollowing: Stage = { weight: 150, suggestedStatus: 'rejected', severity: 'critical', - message: async () => (await import('../messages/rule-breaking/cheat-or-hack-advertising.md?raw')).default, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/cheat-or-hack-advertising.md?raw') + ).default, } as ButtonAction, { id: 'server_side_opt_out', @@ -70,7 +161,9 @@ const ruleFollowing: Stage = { weight: 200, suggestedStatus: 'flagged', severity: 'high', - message: async () => (await import('../messages/rule-breaking/server-side-opt-out.md?raw')).default, + message: async () => + (await import('../messages/checklist-messages/rule-breaking/server-side-opt-out.md?raw')) + .default, } as ButtonAction, { id: 'server_side_opt_in', @@ -79,7 +172,10 @@ const ruleFollowing: Stage = { weight: 300, suggestedStatus: 'flagged', severity: 'high', - message: async () => (await import('../messages/rule-breaking/server-side-opt-in-header.md?raw')).default.trimEnd(), + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/server-side-opt-in-header.md?raw') + ).default.trimEnd(), enablesActions: [ { id: 'server_side_opt_in_options', @@ -87,12 +183,54 @@ const ruleFollowing: Stage = { label: 'Which features of this project require a Server-side Opt-in?', joinWith: '\n', options: [ - {label: 'X-ray', weight: 301, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/x-ray.md?raw')).default,}, - {label: 'Aim Assist', weight: 302, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/aim-bot.md?raw')).default,}, - {label: 'Movement', weight: 303, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/movement.md?raw')).default,}, - {label: 'PvP', weight: 304, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/pvp.md?raw')).default,}, - {label: 'Anti 3.x', weight: 305, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/hiding-mods.md?raw')).default,}, - {label: 'Dupe', weight: 306, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/item-duplication.md?raw')).default,}, + { + label: 'X-ray', + weight: 301, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/x-ray.md?raw') + ).default, + }, + { + label: 'Aim Assist', + weight: 302, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/aim-bot.md?raw') + ).default, + }, + { + label: 'Movement', + weight: 303, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/movement.md?raw') + ).default, + }, + { + label: 'PvP', + weight: 304, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/pvp.md?raw') + ).default, + }, + { + label: 'Anti 3.x', + weight: 305, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/hiding-mods.md?raw') + ).default, + }, + { + label: 'Dupe', + weight: 306, + message: async () => + ( + await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/item-duplication.md?raw') + ).default, + }, ], } as MultiSelectChipsAction, ], @@ -111,27 +249,29 @@ const ruleFollowing: Stage = { projectV3?.minecraft_server?.languages?.length > 4 ) }, - message: async () => ( + message: async () => + ( await import('../messages/checklist-messages/misc-metadata/excessive_languages-server.md?raw') ).default, }, - { - id: 'rule_breaking_other', - type: 'button', - label: 'Other', - weight: 0, - suggestedStatus: 'rejected', - severity: 'critical', - message: async () => (await import('../messages/rule-breaking.md?raw')).default, - relevantExtraInput: [ - { - label: 'Please explain to the user how it infringes on our content rules.', - variable: 'MESSAGE', - required: true, - large: true, - }, - ], - } as ButtonAction, + { + id: 'rule_breaking_other', + type: 'button', + label: 'Other', + weight: 0, + suggestedStatus: 'rejected', + severity: 'critical', + message: async () => + (await import('../messages/checklist-messages/rule-breaking.md?raw')).default, + relevantExtraInput: [ + { + label: 'Please explain to the user how it infringes on our content rules.', + variable: 'MESSAGE', + required: true, + large: true, + }, + ], + } as ButtonAction, ], } diff --git a/packages/moderation/src/data/stages/versions.ts b/packages/moderation/src/data/stages/versions.ts index a84295af02..446a0321a7 100644 --- a/packages/moderation/src/data/stages/versions.ts +++ b/packages/moderation/src/data/stages/versions.ts @@ -186,7 +186,7 @@ const versions: Stage = { suggestedStatus: 'flagged', severity: 'medium', weight: 1001, - message: async () => (await import('../messages/versions/incorrect_loader.md?raw')).default, + message: async () => (await import('../messages/checklist-messages/versions/incorrect_loader.md?raw')).default, enablesActions: [ { id: 'versions_incorrect_loader_options', From b48db5574ed166b1ed2bfff1a8873f1d9253eefa Mon Sep 17 00:00:00 2001 From: chyzman Date: Thu, 9 Jul 2026 17:53:14 -0400 Subject: [PATCH 14/85] only use indexedDb for moderation storage, localStorage forces a bunch of cringe --- .../services/moderation-checklist-storage.ts | 322 +++--------------- apps/frontend/src/services/moderation-db.ts | 88 +++++ .../src/services/moderation-queue-storage.ts | 198 +---------- 3 files changed, 151 insertions(+), 457 deletions(-) create mode 100644 apps/frontend/src/services/moderation-db.ts diff --git a/apps/frontend/src/services/moderation-checklist-storage.ts b/apps/frontend/src/services/moderation-checklist-storage.ts index a8edd3d0bf..a75dcace80 100644 --- a/apps/frontend/src/services/moderation-checklist-storage.ts +++ b/apps/frontend/src/services/moderation-checklist-storage.ts @@ -4,6 +4,8 @@ import { serializeActionStates, } from '@modrinth/moderation' +import { dbDelete, dbGet, dbPut, dbScan } from './moderation-db.ts' + interface PersistedChecklistValue { version: 1 savedAt: string @@ -15,9 +17,7 @@ export interface ModerationChecklistGeneratedMessageState { message: string } -const DB_NAME = 'modrinth-moderation' -const DB_VERSION = 1 -const STORE_NAME = 'kv' +const STORE = 'kv' const CHECKLIST_OPEN_KEY_PREFIX = 'show-moderation-checklist-' const STAGE_KEY_PREFIX = 'moderation-stage-' const ACTION_STATES_KEY_PREFIX = 'moderation-actions-' @@ -25,7 +25,6 @@ const TEXT_INPUTS_KEY_PREFIX = 'moderation-inputs-' const GENERATED_MESSAGE_KEY_PREFIX = 'moderation-generated-message-' const CHECKLIST_STATE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000 const CHECKLIST_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000 -const CHECKLIST_CLEANUP_LAST_RUN_KEY = 'moderation-checklist-cleanup:last-run' const CHECKLIST_STATE_KEY_PREFIXES = [ CHECKLIST_OPEN_KEY_PREFIX, STAGE_KEY_PREFIX, @@ -33,7 +32,7 @@ const CHECKLIST_STATE_KEY_PREFIXES = [ TEXT_INPUTS_KEY_PREFIX, GENERATED_MESSAGE_KEY_PREFIX, ] -const indexedDbSaveChains = new Map>() +const saveChains = new Map>() let checklistCleanupPromise: Promise | null = null let checklistCleanupLastRunAt = 0 @@ -44,52 +43,6 @@ export function createEmptyGeneratedMessageState(): ModerationChecklistGenerated } } -function hasIndexedDb(): boolean { - return typeof window !== 'undefined' && typeof indexedDB !== 'undefined' -} - -function getLocalStorage(): Storage | null { - if (typeof window === 'undefined') return null - - try { - return window.localStorage - } catch { - return null - } -} - -function openDatabase(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION) - - request.onupgradeneeded = () => { - const db = request.result - if (!db.objectStoreNames.contains(STORE_NAME)) { - db.createObjectStore(STORE_NAME) - } - } - - request.onsuccess = () => resolve(request.result) - request.onerror = () => reject(request.error ?? new Error('Failed to open IndexedDB')) - request.onblocked = () => reject(new Error('IndexedDB open request blocked')) - }) -} - -function requestToPromise(request: IDBRequest): Promise { - return new Promise((resolve, reject) => { - request.onsuccess = () => resolve(request.result) - request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed')) - }) -} - -function wrapValue(value: T, savedAt = new Date().toISOString()): PersistedChecklistValue { - return { - version: 1, - savedAt, - value, - } -} - function isRecord(value: unknown): value is Record { return !!value && typeof value === 'object' } @@ -139,6 +92,10 @@ function sanitizeTextInputs(value: unknown): Record | null { return result } +function wrapValue(value: T, savedAt = new Date().toISOString()): PersistedChecklistValue { + return { version: 1, savedAt, value } +} + function normalizeChecklistOpen(value: unknown): PersistedChecklistValue | null { if (isPersistedValue(value, isBoolean)) return value if (isBoolean(value)) return wrapValue(value, '') @@ -147,10 +104,7 @@ function normalizeChecklistOpen(value: unknown): PersistedChecklistValue | null { if (isPersistedValue(value, isNumber)) { - return { - ...value, - value: sanitizeStage(value.value), - } + return { ...value, value: sanitizeStage(value.value) } } if (isNumber(value)) return wrapValue(sanitizeStage(value), '') return null @@ -188,11 +142,7 @@ function normalizeTextInputs( if (isRecord(value) && value.version === 1 && typeof value.savedAt === 'string') { const textInputs = sanitizeTextInputs(value.value) if (textInputs) { - return { - version: 1, - savedAt: value.savedAt, - value: textInputs, - } + return { version: 1, savedAt: value.savedAt, value: textInputs } } } @@ -213,15 +163,6 @@ function savedAtTime(state: PersistedChecklistValue): number { return Number.isNaN(time) ? 0 : time } -function newestState( - first: PersistedChecklistValue | null, - second: PersistedChecklistValue | null, -): PersistedChecklistValue | null { - if (!first) return second - if (!second) return first - return savedAtTime(second) > savedAtTime(first) ? second : first -} - function isChecklistStateKey(key: string): boolean { return CHECKLIST_STATE_KEY_PREFIXES.some((prefix) => key.startsWith(prefix)) } @@ -245,93 +186,38 @@ function isStaleRawState(value: unknown, now = Date.now()): boolean { return now - savedAt > CHECKLIST_STATE_MAX_AGE_MS } -async function loadFromIndexedDb( - key: string, - normalize: (value: unknown) => PersistedChecklistValue | null, -): Promise | null> { - if (!hasIndexedDb()) return null - - const db = await openDatabase() - try { - const tx = db.transaction(STORE_NAME, 'readonly') - const store = tx.objectStore(STORE_NAME) - return normalize(await requestToPromise(store.get(key))) - } finally { - db.close() - } -} - async function cleanupIndexedDb(now = Date.now()): Promise { - if (!hasIndexedDb()) return - - const db = await openDatabase() - try { - const tx = db.transaction(STORE_NAME, 'readwrite') - const store = tx.objectStore(STORE_NAME) - const request = store.openCursor() - - await new Promise((resolve, reject) => { - request.onsuccess = () => { - const cursor = request.result - if (!cursor) return - - const key = typeof cursor.key === 'string' ? cursor.key : null - if (key && isChecklistStateKey(key) && isStaleRawState(cursor.value, now)) { - cursor.delete() - } - - cursor.continue() - } - request.onerror = () => reject(request.error ?? new Error('IndexedDB cursor failed')) - tx.oncomplete = () => resolve() - tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed')) - }) - } finally { - db.close() - } + const entries = await dbScan(STORE) + const staleKeys = entries + .filter( + ({ key, value }) => + typeof key === 'string' && isChecklistStateKey(key) && isStaleRawState(value, now), + ) + .map(({ key }) => key as string) + await Promise.all(staleKeys.map((key) => dbDelete(STORE, key))) } -async function saveToIndexedDb(key: string, state: PersistedChecklistValue): Promise { - if (!hasIndexedDb()) return - - const db = await openDatabase() - try { - const tx = db.transaction(STORE_NAME, 'readwrite') - tx.objectStore(STORE_NAME).put(state, key) - - await new Promise((resolve, reject) => { - tx.oncomplete = () => resolve() - tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed')) - }) - } finally { - db.close() - } -} +function scheduleStaleChecklistCleanup(): void { + if (!import.meta.client || checklistCleanupPromise) return -async function clearIndexedDbKey(key: string): Promise { - if (!hasIndexedDb()) return + const now = Date.now() + if (now - checklistCleanupLastRunAt < CHECKLIST_CLEANUP_INTERVAL_MS) return - const db = await openDatabase() - try { - const tx = db.transaction(STORE_NAME, 'readwrite') - tx.objectStore(STORE_NAME).delete(key) + checklistCleanupLastRunAt = now - await new Promise((resolve, reject) => { - tx.oncomplete = () => resolve() - tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed')) + checklistCleanupPromise = cleanupIndexedDb(now) + .catch((error) => { + console.debug('Failed to cleanup stale moderation checklist state from IndexedDB:', error) + }) + .finally(() => { + checklistCleanupPromise = null }) - } finally { - db.close() - } } -async function saveToIndexedDbInOrder( - key: string, - state: PersistedChecklistValue, -): Promise { - const run = () => saveToIndexedDb(key, state) - const result = (indexedDbSaveChains.get(key) ?? Promise.resolve()).then(run, run) - indexedDbSaveChains.set( +async function saveInOrder(key: string, value: PersistedChecklistValue): Promise { + const run = () => dbPut(STORE, key, value) + const result = (saveChains.get(key) ?? Promise.resolve()).then(run, run) + saveChains.set( key, result.then( () => undefined, @@ -341,10 +227,10 @@ async function saveToIndexedDbInOrder( return result } -async function clearIndexedDbKeyInOrder(key: string): Promise { - const run = () => clearIndexedDbKey(key) - const result = (indexedDbSaveChains.get(key) ?? Promise.resolve()).then(run, run) - indexedDbSaveChains.set( +async function deleteInOrder(key: string): Promise { + const run = () => dbDelete(STORE, key) + const result = (saveChains.get(key) ?? Promise.resolve()).then(run, run) + saveChains.set( key, result.then( () => undefined, @@ -354,103 +240,6 @@ async function clearIndexedDbKeyInOrder(key: string): Promise { return result } -function loadFromLocalStorage( - key: string, - normalize: (value: unknown) => PersistedChecklistValue | null, -): PersistedChecklistValue | null { - const storage = getLocalStorage() - if (!storage) return null - - const raw = storage.getItem(key) - if (!raw) return null - - try { - const parsed: unknown = JSON.parse(raw) - const state = normalize(parsed) - if (state) return state - } catch (error) { - console.debug('Failed to parse moderation checklist state from localStorage:', error) - } - - try { - storage.removeItem(key) - } catch (error) { - console.debug('Failed to clear moderation checklist state from localStorage:', error) - } - return null -} - -function safeSaveLocalStorage(key: string, state: PersistedChecklistValue): void { - try { - getLocalStorage()?.setItem(key, JSON.stringify(state)) - } catch (error) { - console.debug('Failed to save moderation checklist state to localStorage:', error) - } -} - -function safeClearLocalStorage(key: string): void { - try { - getLocalStorage()?.removeItem(key) - } catch (error) { - console.debug('Failed to clear moderation checklist state from localStorage:', error) - } -} - -function cleanupLocalStorage(now = Date.now()): void { - const storage = getLocalStorage() - if (!storage) return - - const keysToRemove: string[] = [] - for (let index = 0; index < storage.length; index++) { - const key = storage.key(index) - if (!key || !isChecklistStateKey(key)) continue - - const raw = storage.getItem(key) - if (!raw) continue - - try { - if (isStaleRawState(JSON.parse(raw), now)) { - keysToRemove.push(key) - } - } catch { - keysToRemove.push(key) - } - } - - keysToRemove.forEach((key) => safeClearLocalStorage(key)) -} - -function scheduleStaleChecklistCleanup(): void { - if (!import.meta.client || checklistCleanupPromise) return - - const storage = getLocalStorage() - const now = Date.now() - const persistedLastRun = Number(storage?.getItem(CHECKLIST_CLEANUP_LAST_RUN_KEY) ?? 0) - const lastRun = Math.max( - checklistCleanupLastRunAt, - Number.isFinite(persistedLastRun) ? persistedLastRun : 0, - ) - if (Number.isFinite(lastRun) && now - lastRun < CHECKLIST_CLEANUP_INTERVAL_MS) return - - checklistCleanupLastRunAt = now - try { - storage?.setItem(CHECKLIST_CLEANUP_LAST_RUN_KEY, String(now)) - } catch (error) { - console.debug('Failed to save moderation checklist cleanup timestamp:', error) - } - - checklistCleanupPromise = (async () => { - cleanupLocalStorage(now) - try { - await cleanupIndexedDb(now) - } catch (error) { - console.debug('Failed to cleanup stale moderation checklist state from IndexedDB:', error) - } - })().finally(() => { - checklistCleanupPromise = null - }) -} - async function loadState( key: string, normalize: (value: unknown) => PersistedChecklistValue | null, @@ -460,21 +249,14 @@ async function loadState( scheduleStaleChecklistCleanup() - let indexedDbState: PersistedChecklistValue | null = null + let state: PersistedChecklistValue | null = null try { - indexedDbState = await loadFromIndexedDb(key, normalize) + const raw = await dbGet(STORE, key) + state = raw !== null ? normalize(raw) : null } catch (error) { console.debug('Failed to load moderation checklist state from IndexedDB:', error) } - let localStorageState: PersistedChecklistValue | null = null - try { - localStorageState = loadFromLocalStorage(key, normalize) - } catch (error) { - console.debug('Failed to load moderation checklist state from localStorage:', error) - } - - const state = newestState(indexedDbState, localStorageState) if (!state) return null if (isStaleState(state)) { @@ -494,28 +276,20 @@ async function saveState(key: string, value: T): Promise { scheduleStaleChecklistCleanup() - const state = wrapValue(value) - safeSaveLocalStorage(key, state) - - if (hasIndexedDb()) { - try { - await saveToIndexedDbInOrder(key, state) - } catch (error) { - console.debug('Failed to save moderation checklist state to IndexedDB:', error) - } + try { + await saveInOrder(key, wrapValue(value)) + } catch (error) { + console.debug('Failed to save moderation checklist state to IndexedDB:', error) } } async function clearState(key: string): Promise { if (!import.meta.client) return - safeClearLocalStorage(key) - if (hasIndexedDb()) { - try { - await clearIndexedDbKeyInOrder(key) - } catch (error) { - console.debug('Failed to clear moderation checklist state from IndexedDB:', error) - } + try { + await deleteInOrder(key) + } catch (error) { + console.debug('Failed to clear moderation checklist state from IndexedDB:', error) } } diff --git a/apps/frontend/src/services/moderation-db.ts b/apps/frontend/src/services/moderation-db.ts new file mode 100644 index 0000000000..374d3187b7 --- /dev/null +++ b/apps/frontend/src/services/moderation-db.ts @@ -0,0 +1,88 @@ +const DB_NAME = 'modrinth-moderation' +const DB_VERSION = 1 + +function hasIndexedDb(): boolean { + return typeof window !== 'undefined' && typeof indexedDB !== 'undefined' +} + +function openDatabase(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION) + + request.onupgradeneeded = () => { + const db = request.result + if (!db.objectStoreNames.contains('kv')) { + db.createObjectStore('kv') + } + } + + request.onsuccess = () => resolve(request.result) + request.onerror = () => reject(request.error ?? new Error('Failed to open IndexedDB')) + request.onblocked = () => reject(new Error('IndexedDB open request blocked')) + }) +} + +function requestToPromise(request: IDBRequest): Promise { + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result) + request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed')) + }) +} + +export async function dbGet(store: string, key: IDBValidKey): Promise { + if (!hasIndexedDb()) return null + const db = await openDatabase() + try { + const tx = db.transaction(store, 'readonly') + const result = await requestToPromise(tx.objectStore(store).get(key)) + return result ?? null + } finally { + db.close() + } +} + +export async function dbPut(store: string, key: IDBValidKey, value: T): Promise { + if (!hasIndexedDb()) return + const db = await openDatabase() + try { + const tx = db.transaction(store, 'readwrite') + tx.objectStore(store).put(value, key) + await new Promise((resolve, reject) => { + tx.oncomplete = () => resolve() + tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed')) + }) + } finally { + db.close() + } +} + +export async function dbDelete(store: string, key: IDBValidKey): Promise { + if (!hasIndexedDb()) return + const db = await openDatabase() + try { + const tx = db.transaction(store, 'readwrite') + tx.objectStore(store).delete(key) + await new Promise((resolve, reject) => { + tx.oncomplete = () => resolve() + tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed')) + }) + } finally { + db.close() + } +} + +export async function dbScan(store: string): Promise<{ key: IDBValidKey; value: T }[]> { + if (!hasIndexedDb()) return [] + const db = await openDatabase() + try { + const tx = db.transaction(store, 'readonly') + const s = tx.objectStore(store) + const [keys, values] = await Promise.all([ + requestToPromise(s.getAllKeys()), + requestToPromise(s.getAll()), + ]) + return keys.map((key, i) => ({ key, value: values[i] as T })) + } finally { + db.close() + } +} diff --git a/apps/frontend/src/services/moderation-queue-storage.ts b/apps/frontend/src/services/moderation-queue-storage.ts index 2a07f7e486..f59815134a 100644 --- a/apps/frontend/src/services/moderation-queue-storage.ts +++ b/apps/frontend/src/services/moderation-queue-storage.ts @@ -1,3 +1,5 @@ +import { dbDelete, dbGet, dbPut } from './moderation-db.ts' + export interface PersistedModerationQueueState { version: 1 savedAt: string @@ -11,25 +13,9 @@ export interface PersistedModerationQueueState { isQueueMode: boolean } -const DB_NAME = 'modrinth-moderation' -const DB_VERSION = 1 -const STORE_NAME = 'kv' +const STORE = 'kv' export const MODERATION_QUEUE_KEY = 'moderation-queue:v1' -function hasIndexedDb(): boolean { - return typeof window !== 'undefined' && typeof indexedDB !== 'undefined' -} - -function getLocalStorage(): Storage | null { - if (typeof window === 'undefined') return null - - try { - return window.localStorage - } catch { - return null - } -} - function isStringArray(value: unknown): value is string[] { return Array.isArray(value) && value.every((entry) => typeof entry === 'string') } @@ -53,189 +39,35 @@ function isPersistedStateCandidate(value: unknown): value is PersistedModeration return true } -function savedAtTime(state: PersistedModerationQueueState): number { - const time = Date.parse(state.savedAt) - return Number.isNaN(time) ? 0 : time -} - -function newestState( - first: PersistedModerationQueueState | null, - second: PersistedModerationQueueState | null, -): PersistedModerationQueueState | null { - if (!first) return second - if (!second) return first - return savedAtTime(second) > savedAtTime(first) ? second : first -} - -function openDatabase(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION) - - request.onupgradeneeded = () => { - const db = request.result - if (!db.objectStoreNames.contains(STORE_NAME)) { - db.createObjectStore(STORE_NAME) - } - } - - request.onsuccess = () => resolve(request.result) - request.onerror = () => reject(request.error ?? new Error('Failed to open IndexedDB')) - request.onblocked = () => reject(new Error('IndexedDB open request blocked')) - }) -} - -function requestToPromise(request: IDBRequest): Promise { - return new Promise((resolve, reject) => { - request.onsuccess = () => resolve(request.result) - request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed')) - }) -} - -async function loadFromIndexedDb(): Promise { - if (!hasIndexedDb()) return null - - const db = await openDatabase() - try { - const tx = db.transaction(STORE_NAME, 'readonly') - const store = tx.objectStore(STORE_NAME) - const raw = await requestToPromise(store.get(MODERATION_QUEUE_KEY)) - if (!isPersistedStateCandidate(raw)) return null - - return raw - } finally { - db.close() - } -} - -async function saveToIndexedDb(state: PersistedModerationQueueState): Promise { - if (!hasIndexedDb()) return - - const db = await openDatabase() - try { - const tx = db.transaction(STORE_NAME, 'readwrite') - tx.objectStore(STORE_NAME).put(state, MODERATION_QUEUE_KEY) - - await new Promise((resolve, reject) => { - tx.oncomplete = () => resolve() - tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed')) - }) - } finally { - db.close() - } -} - -async function clearIndexedDb(): Promise { - if (!hasIndexedDb()) return - - const db = await openDatabase() - try { - const tx = db.transaction(STORE_NAME, 'readwrite') - tx.objectStore(STORE_NAME).delete(MODERATION_QUEUE_KEY) - - await new Promise((resolve, reject) => { - tx.oncomplete = () => resolve() - tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed')) - }) - } finally { - db.close() - } -} - -function loadFromLocalStorage(): PersistedModerationQueueState | null { - const storage = getLocalStorage() - if (!storage) return null - - const raw = storage.getItem(MODERATION_QUEUE_KEY) - if (!raw) return null - - try { - const parsed: unknown = JSON.parse(raw) - if (isPersistedStateCandidate(parsed)) return parsed - } catch (error) { - console.debug('Failed to parse moderation queue from localStorage:', error) - } - - safeClearLocalStorage() - return null -} - -function saveToLocalStorage(state: PersistedModerationQueueState): void { - const storage = getLocalStorage() - if (!storage) return - storage.setItem(MODERATION_QUEUE_KEY, JSON.stringify(state)) -} - -function clearLocalStorage(): void { - const storage = getLocalStorage() - if (!storage) return - storage.removeItem(MODERATION_QUEUE_KEY) -} - -function safeClearLocalStorage(): void { - try { - clearLocalStorage() - } catch (error) { - console.debug('Failed to clear moderation queue from localStorage:', error) - } -} - -function safeSaveLocalStorage(state: PersistedModerationQueueState): void { - try { - saveToLocalStorage(state) - } catch (error) { - console.debug('Failed to save moderation queue to localStorage:', error) - } -} - export async function loadQueueState(): Promise { if (!import.meta.client) return null - let indexedDbState: PersistedModerationQueueState | null = null try { - indexedDbState = await loadFromIndexedDb() + const raw = await dbGet(STORE, MODERATION_QUEUE_KEY) + if (!isPersistedStateCandidate(raw)) return null + return raw } catch (error) { console.debug('Failed to load moderation queue from IndexedDB:', error) + return null } - - let localStorageState: PersistedModerationQueueState | null = null - try { - localStorageState = loadFromLocalStorage() - } catch (error) { - console.debug('Failed to load moderation queue from localStorage:', error) - } - - return newestState(indexedDbState, localStorageState) } export async function saveQueueState(state: PersistedModerationQueueState): Promise { if (!import.meta.client) return - if (hasIndexedDb()) { - try { - await saveToIndexedDb(state) - safeSaveLocalStorage(state) - return - } catch (error) { - console.debug( - 'Failed to save moderation queue to IndexedDB, using localStorage fallback:', - error, - ) - } + try { + await dbPut(STORE, MODERATION_QUEUE_KEY, state) + } catch (error) { + console.debug('Failed to save moderation queue to IndexedDB:', error) } - - safeSaveLocalStorage(state) } export async function clearQueueState(): Promise { if (!import.meta.client) return - if (hasIndexedDb()) { - try { - await clearIndexedDb() - } catch (error) { - console.debug('Failed to clear moderation queue from IndexedDB:', error) - } + try { + await dbDelete(STORE, MODERATION_QUEUE_KEY) + } catch (error) { + console.debug('Failed to clear moderation queue from IndexedDB:', error) } - - safeClearLocalStorage() } From c2426be85bbe76a1b8f97defdaef289629dc008b Mon Sep 17 00:00:00 2001 From: chyzman Date: Thu, 9 Jul 2026 19:57:15 -0400 Subject: [PATCH 15/85] store moderation stuff separately + compact checklist stuff --- .../checklist/ModerationChecklist.vue | 218 +++++------ apps/frontend/src/pages/[type]/[project].vue | 18 +- .../services/moderation-checklist-storage.ts | 356 +++--------------- apps/frontend/src/services/moderation-db.ts | 14 +- .../src/services/moderation-queue-storage.ts | 2 +- packages/moderation/src/utils.ts | 32 -- 6 files changed, 161 insertions(+), 479 deletions(-) diff --git a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue index 89a7cb3647..380e47830e 100644 --- a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue @@ -176,22 +176,22 @@
@@ -545,23 +545,16 @@ import type { ModerationJudgements, ModerationModpackItem, ProjectStatus } from import { renderHighlightedString } from '@modrinth/utils' import { useQueryClient } from '@tanstack/vue-query' import { computedAsync, useDebounceFn } from '@vueuse/core' +import { toRaw } from 'vue' import type { Component } from 'vue' import { useGeneratedState } from '~/composables/generated' import { useImageUpload } from '~/composables/image-upload.ts' import { getProjectTypeForUrlShorthand } from '~/helpers/projects.js' import { - clearChecklistProgressState, - clearGeneratedMessageState as clearPersistedGeneratedMessageState, - createEmptyGeneratedMessageState, - loadChecklistActionStates, - loadChecklistStage, - loadChecklistTextInputs, - loadGeneratedMessageState, - saveChecklistActionStates, - saveChecklistStage, - saveChecklistTextInputs, - saveGeneratedMessageState, + clearChecklistState, + loadChecklistState, + saveChecklistState, } from '~/services/moderation-checklist-storage.ts' import type { LockAcquireResponse } from '~/services/moderation-queue.ts' import { useModerationQueue } from '~/services/moderation-queue.ts' @@ -839,7 +832,6 @@ function handleLockLost(result: LockAcquireResponse) { function handleLockAcquired() { lockStatus.value = { locked: false, isOwnLock: true } lockError.value = false - initializeAllStages() clearLockCountdown() startLockHeartbeat() maintainPrefetchQueue() // Start prefetching immediately (not debounced) @@ -848,7 +840,6 @@ function handleLockAcquired() { function handleLockUnavailable() { lockError.value = true lockStatus.value = { locked: false, isOwnLock: false } - initializeAllStages() clearLockCountdown() addNotification({ title: 'Lock unavailable', @@ -924,14 +915,12 @@ async function onUploadHandler(file: File) { } const useSimpleEditor = ref(false) -const checklistPersistenceProjectSlug = projectV2.value.slug -const persistedGeneratedMessage = import.meta.client - ? await loadGeneratedMessageState(checklistPersistenceProjectSlug) - : createEmptyGeneratedMessageState() -const message = ref( - typeof persistedGeneratedMessage.message === 'string' ? persistedGeneratedMessage.message : '', -) -const generatedMessage = ref(persistedGeneratedMessage.generated === true) +const checklistPersistenceProjectId = projectV2.value.id +const persistedState = import.meta.client + ? await loadChecklistState(checklistPersistenceProjectId) + : null +const message = ref(persistedState?.message ?? null) +const generatedMessage = computed(() => message.value !== null) const loadingMessage = ref(false) const moderationDecision = ref(null) const loadingModerationDecision = computed(() => moderationDecision.value !== null) @@ -940,22 +929,17 @@ const approveSendStatus = computed(() => { return requested ?? 'approved' }) const done = ref(false) - -function persistGeneratedMessageState() { - void saveGeneratedMessageState(checklistPersistenceProjectSlug, { - generated: generatedMessage.value, - message: message.value, - }) -} +const messageText = computed({ + get: () => message.value ?? '', + set: (v: string) => { + message.value = v + }, +}) function clearGeneratedMessageState() { - generatedMessage.value = false - message.value = '' - void clearPersistedGeneratedMessageState(checklistPersistenceProjectSlug) + message.value = null } -watch([generatedMessage, message], persistGeneratedMessageState, { flush: 'sync' }) - function handleModpackPermissionsComplete() { modpackPermissionsComplete.value = true } @@ -1018,7 +1002,6 @@ async function confirmTakeOverOverride() { function reviewAnyway() { alreadyReviewed.value = false - initializeAllStages() // Start prefetching the next project in the background maintainPrefetchQueue() } @@ -1269,8 +1252,6 @@ function resetProgress() { modpackPermissionsComplete.value = false modpackJudgements.value = {} - - initializeAllStages() } function findFirstValidStage(): number { @@ -1298,11 +1279,10 @@ const checklistTitleText = computed(() => { ? kebabToTitleCase(currentStageObj.value.id) : currentStageObj.value.title }) -const persistedStage = import.meta.client - ? await loadChecklistStage(checklistPersistenceProjectSlug) - : null const currentStage = ref( - persistedStage !== null && checklist[persistedStage] ? persistedStage : findFirstValidStage(), + persistedState?.stage !== undefined && checklist[persistedState.stage] + ? persistedState.stage + : findFirstValidStage(), ) const stageTextExpanded = computedAsync(async () => { @@ -1321,29 +1301,39 @@ const stageTextExpanded = computedAsync(async () => { return null }, null) -const persistedActionStates = import.meta.client - ? await loadChecklistActionStates(checklistPersistenceProjectSlug) - : {} - const router = useRouter() -const persistedTextInputs = import.meta.client - ? await loadChecklistTextInputs(checklistPersistenceProjectSlug) - : {} +const actionStates = ref>(persistedState?.actionStates ?? {}) +const textInputValues = ref>(persistedState?.textInputs ?? {}) -const actionStates = ref>(persistedActionStates) -const textInputValues = ref>(persistedTextInputs) +const initialStage = currentStage.value +let hasMeaningfulState = persistedState !== null const persistState = () => { - void saveChecklistActionStates(checklistPersistenceProjectSlug, actionStates.value) - void saveChecklistTextInputs(checklistPersistenceProjectSlug, textInputValues.value) + if (!hasMeaningfulState) { + hasMeaningfulState = + currentStage.value !== initialStage || + message.value !== null || + Object.values(toRaw(actionStates.value)).some((s) => s.selected) + if (!hasMeaningfulState) return + } + void saveChecklistState(checklistPersistenceProjectId, { + open: !props.collapsed, + stage: currentStage.value, + message: message.value, + actionStates: toRaw(actionStates.value), + textInputs: toRaw(textInputValues.value), + }) } -watch(currentStage, (stage) => { - void saveChecklistStage(checklistPersistenceProjectSlug, stage) -}) +watch(currentStage, persistState) watch(actionStates, persistState, { deep: true }) watch(textInputValues, persistState, { deep: true }) +watch(message, persistState) +watch(() => props.collapsed, (collapsed) => { + if (!collapsed) hasMeaningfulState = true + persistState() +}) interface MessagePart { weight: number @@ -1539,16 +1529,6 @@ onUnmounted(() => { isPrefetching.value = false }) -function initializeAllStages() { - checklist.forEach((stage, stageIndex) => { - initializeStageActions(stage, stageIndex) - }) -} - -function initializeCurrentStage() { - initializeStageActions(currentStageObj.value, currentStage.value) -} - watch( currentStage, (newIndex, oldIndex) => { @@ -1557,8 +1537,6 @@ watch( if (oldIndex !== undefined && newIndex !== oldIndex && stage?.navigate) { router.push(`/${projectV2.value.project_type}/${projectV2.value.slug}${stage.navigate}`) } - - initializeCurrentStage() }, { immediate: true }, ) @@ -1573,26 +1551,6 @@ watch( { immediate: true }, ) -function initializeStageActions(stage: Stage, stageIndex: number) { - stage.actions.forEach((action, index) => { - const actionId = getActionIdForStage(action, stageIndex, index) - if (!actionStates.value[actionId]) { - actionStates.value[actionId] = initializeActionState(action) - } - }) - - stage.actions.forEach((action) => { - if (action.enablesActions) { - action.enablesActions.forEach((enabledAction, index) => { - const actionId = getActionIdForStage(enabledAction, currentStage.value, index) - if (!actionStates.value[actionId]) { - actionStates.value[actionId] = initializeActionState(enabledAction) - } - }) - } - }) -} - function getActionId(action: Action, index?: number): string { // If index is not provided, find it in the current stage's actions if (index === undefined) { @@ -1734,37 +1692,51 @@ function getDropdownValue(action: DropdownAction) { return visibleOptions[0] || null } +function isDefaultActionState(action: Action, state: ActionState): boolean { + const def = initializeActionState(action) + if (state.selected !== def.selected) return false + if (def.value instanceof Set) return !(state.value instanceof Set) || state.value.size === 0 + return state.value === def.value +} + function isActionSelected(action: Action): boolean { const actionIndex = currentStageObj.value.actions.indexOf(action) const actionId = getActionId(action, actionIndex) - return actionStates.value[actionId]?.selected || false + const state = actionStates.value[actionId] + if (state !== undefined) return state.selected + return action.type === 'toggle' ? (action.defaultChecked ?? false) : false } function toggleAction(action: Action) { const actionIndex = currentStageObj.value.actions.indexOf(action) const actionId = getActionId(action, actionIndex) - const state = actionStates.value[actionId] - if (state) { - state.selected = !state.selected - persistState() + const current = actionStates.value[actionId] ?? initializeActionState(action) + const newState: ActionState = { ...current, selected: !current.selected } + if (isDefaultActionState(action, newState)) { + const { [actionId]: _, ...rest } = actionStates.value + actionStates.value = rest + } else { + actionStates.value[actionId] = newState } + persistState() } function selectDropdownOption(action: DropdownAction, selected: any) { + if (selected === undefined || selected === null) return const actionIndex = currentStageObj.value.actions.indexOf(action) const actionId = getActionId(action, actionIndex) - const state = actionStates.value[actionId] - if (state && selected !== undefined && selected !== null) { - const optionIndex = action.options.findIndex( - (opt) => opt === selected || (opt?.label && selected?.label && opt.label === selected.label), - ) - - if (optionIndex !== -1) { - state.value = optionIndex - state.selected = true - persistState() - } + const optionIndex = action.options.findIndex( + (opt) => opt === selected || (opt?.label && selected?.label && opt.label === selected.label), + ) + if (optionIndex === -1) return + const newState: ActionState = { selected: true, value: optionIndex } + if (isDefaultActionState(action, newState)) { + const { [actionId]: _, ...rest } = actionStates.value + actionStates.value = rest + } else { + actionStates.value[actionId] = newState } + persistState() } @@ -1777,21 +1749,21 @@ function isChipSelected(action: MultiSelectChipsAction, option: MultiSelectChips function toggleChip(action: MultiSelectChipsAction, option: MultiSelectChipsOption) { const actionIndex = currentStageObj.value.actions.indexOf(action) const actionId = getActionId(action, actionIndex) - const state = actionStates.value[actionId] - if (state) { - const selectedOptionIds = getSelectedChipIds(action, state) - const optionId = getMultiSelectOptionId(option) - - if (selectedOptionIds.has(optionId)) { - selectedOptionIds.delete(optionId) - } else { - selectedOptionIds.add(optionId) - } - - state.value = selectedOptionIds - state.selected = selectedOptionIds.size > 0 - persistState() + const selectedOptionIds = getSelectedChipIds(action, actionStates.value[actionId]) + const optionId = getMultiSelectOptionId(option) + if (selectedOptionIds.has(optionId)) { + selectedOptionIds.delete(optionId) + } else { + selectedOptionIds.add(optionId) + } + const newState: ActionState = { selected: selectedOptionIds.size > 0, value: selectedOptionIds } + if (isDefaultActionState(action, newState)) { + const { [actionId]: _, ...rest } = actionStates.value + actionStates.value = rest + } else { + actionStates.value[actionId] = newState } + persistState() } const isAnyVisibleInputs = computed(() => { @@ -2133,8 +2105,6 @@ async function generateMessage() { } message.value = fullMessage - - generatedMessage.value = true } catch (error) { console.error('Error generating message:', error) addNotification({ @@ -2388,10 +2358,10 @@ function clearProjectLocalStorage() { sessionStorage.removeItem(`modpack-permissions-permanent-no-${projectV2.value.id}`) sessionStorage.removeItem(`modpack-permissions-updated-${projectV2.value.id}`) - void clearChecklistProgressState(checklistPersistenceProjectSlug) + void clearChecklistState(checklistPersistenceProjectId) actionStates.value = {} textInputValues.value = {} - clearGeneratedMessageState() + message.value = null } const isLastVisibleStage = computed(() => { diff --git a/apps/frontend/src/pages/[type]/[project].vue b/apps/frontend/src/pages/[type]/[project].vue index b21380e860..6c90d735c6 100644 --- a/apps/frontend/src/pages/[type]/[project].vue +++ b/apps/frontend/src/pages/[type]/[project].vue @@ -803,10 +803,7 @@ import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project' import { versionQueryOptions } from '~/composables/queries/version' import { userCollectProject, userFollowProject } from '~/composables/user.js' import { injectCurrentProjectId } from '~/providers/current-project.ts' -import { - loadChecklistOpenState, - saveChecklistOpenState, -} from '~/services/moderation-checklist-storage.ts' +import { loadChecklistState } from '~/services/moderation-checklist-storage.ts' import { useModerationQueue } from '~/services/moderation-queue.ts' import { getReportPath, reportProject } from '~/utils/report-helpers.ts' @@ -2035,9 +2032,6 @@ function consumeShowChecklistHistoryState() { function setModerationChecklistOpen(open, projectId = project.value?.id) { showModerationChecklist.value = open - if (projectId) { - void saveChecklistOpenState(projectId, open) - } } function isProjectInActiveModerationQueue(projectId = project.value?.id) { @@ -2079,21 +2073,17 @@ watch( return } - const storedOpen = await loadChecklistOpenState(projectId) + const storedState = await loadChecklistState(projectId) if (cancelled) return - if (storedOpen !== null) { - showModerationChecklist.value = storedOpen + if (storedState !== null) { + showModerationChecklist.value = storedState.open ?? false return } const shouldRecoverFromQueue = moderationQueue.isQueueMode && moderationQueue.getCurrentProjectId() === projectId showModerationChecklist.value = shouldRecoverFromQueue - - if (shouldRecoverFromQueue) { - void saveChecklistOpenState(projectId, true) - } }, { immediate: true }, ) diff --git a/apps/frontend/src/services/moderation-checklist-storage.ts b/apps/frontend/src/services/moderation-checklist-storage.ts index a75dcace80..1873b8d3e2 100644 --- a/apps/frontend/src/services/moderation-checklist-storage.ts +++ b/apps/frontend/src/services/moderation-checklist-storage.ts @@ -1,199 +1,47 @@ -import { - type ActionState, - deserializeActionStates, - serializeActionStates, -} from '@modrinth/moderation' +import type { ActionState } from '@modrinth/moderation' import { dbDelete, dbGet, dbPut, dbScan } from './moderation-db.ts' -interface PersistedChecklistValue { - version: 1 +export interface PersistedChecklistState { savedAt: string - value: T + open?: boolean + stage: number + message: string | null + actionStates: Record + textInputs: Record } -export interface ModerationChecklistGeneratedMessageState { - generated: boolean - message: string -} - -const STORE = 'kv' -const CHECKLIST_OPEN_KEY_PREFIX = 'show-moderation-checklist-' -const STAGE_KEY_PREFIX = 'moderation-stage-' -const ACTION_STATES_KEY_PREFIX = 'moderation-actions-' -const TEXT_INPUTS_KEY_PREFIX = 'moderation-inputs-' -const GENERATED_MESSAGE_KEY_PREFIX = 'moderation-generated-message-' +const STORE = 'checklist' const CHECKLIST_STATE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000 const CHECKLIST_CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000 -const CHECKLIST_STATE_KEY_PREFIXES = [ - CHECKLIST_OPEN_KEY_PREFIX, - STAGE_KEY_PREFIX, - ACTION_STATES_KEY_PREFIX, - TEXT_INPUTS_KEY_PREFIX, - GENERATED_MESSAGE_KEY_PREFIX, -] -const saveChains = new Map>() +const saveChain = new Map>() let checklistCleanupPromise: Promise | null = null let checklistCleanupLastRunAt = 0 -export function createEmptyGeneratedMessageState(): ModerationChecklistGeneratedMessageState { - return { - generated: false, - message: '', - } -} - -function isRecord(value: unknown): value is Record { - return !!value && typeof value === 'object' -} - -function isPersistedValue( - value: unknown, - isValue: (value: unknown) => value is T, -): value is PersistedChecklistValue { - if (!isRecord(value)) return false - if (value.version !== 1) return false - if (typeof value.savedAt !== 'string') return false - return isValue(value.value) -} - -function isBoolean(value: unknown): value is boolean { - return typeof value === 'boolean' -} - -function isNumber(value: unknown): value is number { - return typeof value === 'number' && Number.isFinite(value) -} - -function isString(value: unknown): value is string { - return typeof value === 'string' -} - -function isGeneratedMessageState( - value: unknown, -): value is ModerationChecklistGeneratedMessageState { - if (!isRecord(value)) return false - return typeof value.generated === 'boolean' && typeof value.message === 'string' -} - -function sanitizeStage(value: number): number { - return Math.max(0, Math.trunc(value)) -} - -function sanitizeTextInputs(value: unknown): Record | null { - if (!isRecord(value)) return null - - const result: Record = {} - for (const [key, entry] of Object.entries(value)) { - if (typeof entry === 'string') { - result[key] = entry - } - } - return result -} - -function wrapValue(value: T, savedAt = new Date().toISOString()): PersistedChecklistValue { - return { version: 1, savedAt, value } +function isPersistedChecklistState(value: unknown): value is PersistedChecklistState { + if (!value || typeof value !== 'object') return false + const v = value as PersistedChecklistState + if (typeof v.savedAt !== 'string') return false + if (typeof v.stage !== 'number' || !Number.isFinite(v.stage)) return false + if (v.message !== null && typeof v.message !== 'string') return false + if (!v.actionStates || typeof v.actionStates !== 'object') return false + if (!v.textInputs || typeof v.textInputs !== 'object') return false + return true } -function normalizeChecklistOpen(value: unknown): PersistedChecklistValue | null { - if (isPersistedValue(value, isBoolean)) return value - if (isBoolean(value)) return wrapValue(value, '') - return null +function isStale(savedAt: string, now = Date.now()): boolean { + const time = Date.parse(savedAt) + return !Number.isNaN(time) && now - time > CHECKLIST_STATE_MAX_AGE_MS } -function normalizeStage(value: unknown): PersistedChecklistValue | null { - if (isPersistedValue(value, isNumber)) { - return { ...value, value: sanitizeStage(value.value) } - } - if (isNumber(value)) return wrapValue(sanitizeStage(value), '') - return null -} - -function normalizeActionStates( - value: unknown, -): PersistedChecklistValue> | null { - if (isRecord(value) && value.version === 1 && typeof value.savedAt === 'string') { - if (isString(value.value)) { - return { - version: 1, - savedAt: value.savedAt, - value: deserializeActionStates(value.value), - } - } - - if (isRecord(value.value)) { - return { - version: 1, - savedAt: value.savedAt, - value: deserializeActionStates(JSON.stringify(value.value)), - } - } - } - - if (isString(value)) return wrapValue(deserializeActionStates(value), '') - if (isRecord(value)) return wrapValue(deserializeActionStates(JSON.stringify(value)), '') - return null -} - -function normalizeTextInputs( - value: unknown, -): PersistedChecklistValue> | null { - if (isRecord(value) && value.version === 1 && typeof value.savedAt === 'string') { - const textInputs = sanitizeTextInputs(value.value) - if (textInputs) { - return { version: 1, savedAt: value.savedAt, value: textInputs } - } - } - - const textInputs = sanitizeTextInputs(value) - return textInputs ? wrapValue(textInputs, '') : null -} - -function normalizeGeneratedMessage( - value: unknown, -): PersistedChecklistValue | null { - if (isPersistedValue(value, isGeneratedMessageState)) return value - if (isGeneratedMessageState(value)) return wrapValue(value, '') - return null -} - -function savedAtTime(state: PersistedChecklistValue): number { - const time = Date.parse(state.savedAt) - return Number.isNaN(time) ? 0 : time -} - -function isChecklistStateKey(key: string): boolean { - return CHECKLIST_STATE_KEY_PREFIXES.some((prefix) => key.startsWith(prefix)) -} - -function isStaleState( - state: PersistedChecklistValue, - now = Date.now(), - maxAgeMs = CHECKLIST_STATE_MAX_AGE_MS, -): boolean { - const savedAt = savedAtTime(state) - if (savedAt === 0) return false - return now - savedAt > maxAgeMs -} - -function isStaleRawState(value: unknown, now = Date.now()): boolean { - if (!isRecord(value)) return false - if (value.version !== 1 || typeof value.savedAt !== 'string') return false - - const savedAt = Date.parse(value.savedAt) - if (Number.isNaN(savedAt)) return false - return now - savedAt > CHECKLIST_STATE_MAX_AGE_MS -} - -async function cleanupIndexedDb(now = Date.now()): Promise { +async function cleanupStaleStates(now = Date.now()): Promise { const entries = await dbScan(STORE) const staleKeys = entries - .filter( - ({ key, value }) => - typeof key === 'string' && isChecklistStateKey(key) && isStaleRawState(value, now), - ) - .map(({ key }) => key as string) + .filter(({ key, value }) => { + if (!isPersistedChecklistState(value)) return false + return isStale(value.savedAt, now) + }) + .map(({ key }) => key) await Promise.all(staleKeys.map((key) => dbDelete(STORE, key))) } @@ -204,21 +52,19 @@ function scheduleStaleChecklistCleanup(): void { if (now - checklistCleanupLastRunAt < CHECKLIST_CLEANUP_INTERVAL_MS) return checklistCleanupLastRunAt = now - - checklistCleanupPromise = cleanupIndexedDb(now) + checklistCleanupPromise = cleanupStaleStates(now) .catch((error) => { - console.debug('Failed to cleanup stale moderation checklist state from IndexedDB:', error) + console.debug('Failed to cleanup stale moderation checklist states from IndexedDB:', error) }) .finally(() => { checklistCleanupPromise = null }) } -async function saveInOrder(key: string, value: PersistedChecklistValue): Promise { - const run = () => dbPut(STORE, key, value) - const result = (saveChains.get(key) ?? Promise.resolve()).then(run, run) - saveChains.set( - key, +async function enqueueOp(projectId: string, op: () => Promise): Promise { + const result = (saveChain.get(projectId) ?? Promise.resolve()).then(op, op) + saveChain.set( + projectId, result.then( () => undefined, () => undefined, @@ -227,144 +73,46 @@ async function saveInOrder(key: string, value: PersistedChecklistValue): P return result } -async function deleteInOrder(key: string): Promise { - const run = () => dbDelete(STORE, key) - const result = (saveChains.get(key) ?? Promise.resolve()).then(run, run) - saveChains.set( - key, - result.then( - () => undefined, - () => undefined, - ), - ) - return result -} - -async function loadState( - key: string, - normalize: (value: unknown) => PersistedChecklistValue | null, - touch = true, -): Promise { +export async function loadChecklistState(projectId: string): Promise { if (!import.meta.client) return null - scheduleStaleChecklistCleanup() - let state: PersistedChecklistValue | null = null try { - const raw = await dbGet(STORE, key) - state = raw !== null ? normalize(raw) : null + const raw = await dbGet(STORE, projectId) + if (!isPersistedChecklistState(raw)) return null + if (isStale(raw.savedAt)) { + await clearChecklistState(projectId) + return null + } + return raw } catch (error) { - console.debug('Failed to load moderation checklist state from IndexedDB:', error) - } - - if (!state) return null - - if (isStaleState(state)) { - await clearState(key) + console.debug('Failed to load checklist state from IndexedDB:', error) return null } - - if (touch) { - void saveState(key, state.value) - } - - return state.value } -async function saveState(key: string, value: T): Promise { +export async function saveChecklistState( + projectId: string, + state: Omit, +): Promise { if (!import.meta.client) return - scheduleStaleChecklistCleanup() + const record: PersistedChecklistState = { ...state, savedAt: new Date().toISOString() } try { - await saveInOrder(key, wrapValue(value)) + await enqueueOp(projectId, () => dbPut(STORE, projectId, record)) } catch (error) { - console.debug('Failed to save moderation checklist state to IndexedDB:', error) + console.debug('Failed to save checklist state to IndexedDB:', error) } } -async function clearState(key: string): Promise { +export async function clearChecklistState(projectId: string): Promise { if (!import.meta.client) return try { - await deleteInOrder(key) + await enqueueOp(projectId, () => dbDelete(STORE, projectId)) } catch (error) { - console.debug('Failed to clear moderation checklist state from IndexedDB:', error) + console.debug('Failed to clear checklist state from IndexedDB:', error) } } -export async function loadChecklistOpenState(projectId: string): Promise { - return loadState(`${CHECKLIST_OPEN_KEY_PREFIX}${projectId}`, normalizeChecklistOpen, false) -} - -export async function saveChecklistOpenState(projectId: string, open: boolean): Promise { - await saveState(`${CHECKLIST_OPEN_KEY_PREFIX}${projectId}`, open) -} - -export async function loadChecklistStage(projectSlug: string): Promise { - return loadState(`${STAGE_KEY_PREFIX}${projectSlug}`, normalizeStage) -} - -export async function saveChecklistStage(projectSlug: string, stage: number): Promise { - await saveState(`${STAGE_KEY_PREFIX}${projectSlug}`, sanitizeStage(stage)) -} - -export async function loadChecklistActionStates( - projectSlug: string, -): Promise> { - const actionStates = - (await loadState(`${ACTION_STATES_KEY_PREFIX}${projectSlug}`, normalizeActionStates, false)) ?? - {} - if (Object.keys(actionStates).length > 0) { - void saveChecklistActionStates(projectSlug, actionStates) - } - return actionStates -} - -export async function saveChecklistActionStates( - projectSlug: string, - actionStates: Record, -): Promise { - await saveState(`${ACTION_STATES_KEY_PREFIX}${projectSlug}`, serializeActionStates(actionStates)) -} - -export async function loadChecklistTextInputs( - projectSlug: string, -): Promise> { - return (await loadState(`${TEXT_INPUTS_KEY_PREFIX}${projectSlug}`, normalizeTextInputs)) ?? {} -} - -export async function saveChecklistTextInputs( - projectSlug: string, - textInputs: Record, -): Promise { - await saveState(`${TEXT_INPUTS_KEY_PREFIX}${projectSlug}`, textInputs) -} - -export async function clearChecklistProgressState(projectSlug: string): Promise { - await Promise.all([ - clearState(`${STAGE_KEY_PREFIX}${projectSlug}`), - clearState(`${ACTION_STATES_KEY_PREFIX}${projectSlug}`), - clearState(`${TEXT_INPUTS_KEY_PREFIX}${projectSlug}`), - ]) -} - -export async function loadGeneratedMessageState( - projectSlug: string, -): Promise { - return ( - (await loadState(`${GENERATED_MESSAGE_KEY_PREFIX}${projectSlug}`, normalizeGeneratedMessage)) ?? - createEmptyGeneratedMessageState() - ) -} - -export async function saveGeneratedMessageState( - projectSlug: string, - state: ModerationChecklistGeneratedMessageState, -): Promise { - await saveState(`${GENERATED_MESSAGE_KEY_PREFIX}${projectSlug}`, state) -} - -export async function clearGeneratedMessageState(projectSlug: string): Promise { - await clearState(`${GENERATED_MESSAGE_KEY_PREFIX}${projectSlug}`) -} diff --git a/apps/frontend/src/services/moderation-db.ts b/apps/frontend/src/services/moderation-db.ts index 374d3187b7..6c00260b0f 100644 --- a/apps/frontend/src/services/moderation-db.ts +++ b/apps/frontend/src/services/moderation-db.ts @@ -1,5 +1,5 @@ const DB_NAME = 'modrinth-moderation' -const DB_VERSION = 1 +const DB_VERSION = 2 function hasIndexedDb(): boolean { return typeof window !== 'undefined' && typeof indexedDB !== 'undefined' @@ -9,10 +9,16 @@ function openDatabase(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION) - request.onupgradeneeded = () => { + request.onupgradeneeded = (event) => { const db = request.result - if (!db.objectStoreNames.contains('kv')) { - db.createObjectStore('kv') + if (event.oldVersion < 2 && db.objectStoreNames.contains('kv')) { + db.deleteObjectStore('kv') + } + if (!db.objectStoreNames.contains('checklist')) { + db.createObjectStore('checklist') + } + if (!db.objectStoreNames.contains('queue')) { + db.createObjectStore('queue') } } diff --git a/apps/frontend/src/services/moderation-queue-storage.ts b/apps/frontend/src/services/moderation-queue-storage.ts index f59815134a..d90ac1121d 100644 --- a/apps/frontend/src/services/moderation-queue-storage.ts +++ b/apps/frontend/src/services/moderation-queue-storage.ts @@ -13,7 +13,7 @@ export interface PersistedModerationQueueState { isQueueMode: boolean } -const STORE = 'kv' +const STORE = 'queue' export const MODERATION_QUEUE_KEY = 'moderation-queue:v1' function isStringArray(value: unknown): value is string[] { diff --git a/packages/moderation/src/utils.ts b/packages/moderation/src/utils.ts index e554d9b1c8..01e02e8710 100644 --- a/packages/moderation/src/utils.ts +++ b/packages/moderation/src/utils.ts @@ -20,10 +20,6 @@ export interface MessagePart { stageIndex: number } -export type SerializedActionState = { - isSet?: boolean -} & ActionState - export function getActionIdForStage( action: Action, stageIndex: number, @@ -50,34 +46,6 @@ export function getActionKey( return `${currentStage}-${index}-${getActionId(action, currentStage)}` } -export function serializeActionStates(states: Record): string { - const serializable: Record = {} - for (const [key, state] of Object.entries(states)) { - serializable[key] = { - selected: state.selected, - value: state.value instanceof Set ? Array.from(state.value) : state.value, - isSet: state.value instanceof Set, - } - } - return JSON.stringify(serializable) -} - -export function deserializeActionStates(data: string): Record { - try { - const parsed = JSON.parse(data) - const states: Record = {} - for (const [key, state] of Object.entries(parsed as Record)) { - states[key] = { - selected: state.selected, - value: state.isSet ? new Set(state.value as unknown[]) : state.value, - } - } - return states - } catch { - return {} - } -} - export function initializeActionState(action: Action): ActionState { if (action.type === 'toggle') { return { From d18eef5a2d925d6a6b1cd4b6fc243920194d04b8 Mon Sep 17 00:00:00 2001 From: chyzman Date: Fri, 10 Jul 2026 14:41:58 -0400 Subject: [PATCH 16/85] give stages actual titles + rename what was previously called titles to hints --- .../checklist/ModerationChecklist.vue | 10 +- .../src/data/modpack-permissions-stage.ts | 2 +- .../moderation/src/data/stages/categories.ts | 3 +- .../moderation/src/data/stages/description.ts | 3 +- .../environment/environment-multiple.ts | 3 +- .../data/stages/environment/environment.ts | 3 +- .../moderation/src/data/stages/gallery.ts | 3 +- .../moderation/src/data/stages/license.ts | 3 +- packages/moderation/src/data/stages/links.ts | 3 +- .../moderation/src/data/stages/permissions.ts | 3 +- .../src/data/stages/post-approval.ts | 3 +- .../moderation/src/data/stages/reupload.ts | 3 +- .../src/data/stages/rule-following.ts | 169 +++++++++++++++--- .../src/data/stages/status-alerts.ts | 3 +- .../moderation/src/data/stages/summary.ts | 3 +- .../moderation/src/data/stages/title-slug.ts | 3 +- .../src/data/stages/undefined-project.ts | 3 +- .../moderation/src/data/stages/versions.ts | 6 +- packages/moderation/src/types/stage.ts | 7 +- 19 files changed, 188 insertions(+), 48 deletions(-) diff --git a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue index 380e47830e..b23292ab3c 100644 --- a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue @@ -205,7 +205,7 @@

- {{ currentStageObj.title }} + {{ currentStageObj.hint }}

@@ -1275,9 +1275,7 @@ const checklistTitleText = computed(() => { if (alreadyReviewed.value || done.value) return 'Moderation' if (generatedMessage.value) return 'Generated Message' - return currentStageObj.value.id - ? kebabToTitleCase(currentStageObj.value.id) - : currentStageObj.value.title + return currentStageObj.value.title ?? kebabToTitleCase(currentStageObj.value.id) }) const currentStage = ref( persistedState?.stage !== undefined && checklist[persistedState.stage] @@ -1353,7 +1351,7 @@ function handleKeybinds(event: KeyboardEvent) { currentStage: currentStage.value, totalStages: checklist.length, currentStageId: currentStageObj.value.id, - currentStageTitle: currentStageObj.value.title, + currentStageTitle: currentStageObj.value.hint, isCollapsed: props.collapsed, isDone: done.value, @@ -2390,7 +2388,7 @@ const stageOptions = computed(() => { return { id: String(index), action: () => (currentStage.value = index), - text: stage.id ? kebabToTitleCase(stage.id) : stage.title, + text: stage.title ?? kebabToTitleCase(stage.id), color: index === currentStage.value && !generatedMessage.value ? 'green' : undefined, hoverFilled: true, icon: stage.icon ? stage.icon : undefined, diff --git a/packages/moderation/src/data/modpack-permissions-stage.ts b/packages/moderation/src/data/modpack-permissions-stage.ts index eb0311977b..8b9f3ef2b4 100644 --- a/packages/moderation/src/data/modpack-permissions-stage.ts +++ b/packages/moderation/src/data/modpack-permissions-stage.ts @@ -6,7 +6,7 @@ import type { Stage } from '../types/stage' export default { id: 'modpack-permissions', - title: 'Modpack Permissions', + hint: 'Modpack Permissions', icon: PackageOpenIcon, // Replace me please. guidance_url: diff --git a/packages/moderation/src/data/stages/categories.ts b/packages/moderation/src/data/stages/categories.ts index 89b37d9ee9..3ad4df2f6a 100644 --- a/packages/moderation/src/data/stages/categories.ts +++ b/packages/moderation/src/data/stages/categories.ts @@ -4,7 +4,8 @@ import type { ButtonAction } from '../../types/actions' import type { Stage } from '../../types/stage' const categories: Stage = { - title: "Are the project's tags accurate?", + title: "Tags", + hint: "Are the project's tags accurate?", id: 'tags', icon: TagsIcon, guidance_url: diff --git a/packages/moderation/src/data/stages/description.ts b/packages/moderation/src/data/stages/description.ts index 7c6211a424..e9411f48c0 100644 --- a/packages/moderation/src/data/stages/description.ts +++ b/packages/moderation/src/data/stages/description.ts @@ -4,7 +4,8 @@ import type { ButtonAction, MultiSelectChipsAction } from '../../types/actions' import type { Stage } from '../../types/stage' const description: Stage = { - title: 'Is the description sufficient, accurate, and accessible?', + title: "Description", + hint: 'Is the description sufficient, accurate, and accessible?', id: 'description', icon: LibraryIcon, guidance_url: diff --git a/packages/moderation/src/data/stages/environment/environment-multiple.ts b/packages/moderation/src/data/stages/environment/environment-multiple.ts index d4f27051e9..0d5aaa1f9b 100644 --- a/packages/moderation/src/data/stages/environment/environment-multiple.ts +++ b/packages/moderation/src/data/stages/environment/environment-multiple.ts @@ -4,7 +4,8 @@ import type { ButtonAction } from '../../../types/actions' import type { Stage } from '../../../types/stage' const environmentMultiple: Stage = { - title: "Is the project's environment information accurate?", + title: "Environment", + hint: "Is the project's environment information accurate?", id: 'environment', navigate: '/settings/versions', icon: GlobeIcon, diff --git a/packages/moderation/src/data/stages/environment/environment.ts b/packages/moderation/src/data/stages/environment/environment.ts index b86e42d9bc..03eaf46c29 100644 --- a/packages/moderation/src/data/stages/environment/environment.ts +++ b/packages/moderation/src/data/stages/environment/environment.ts @@ -4,7 +4,8 @@ import type { ButtonAction } from '../../../types/actions' import type { Stage } from '../../../types/stage' const environment: Stage = { - title: "Is the project's environment information accurate?", + title: "Environment", + hint: "Is the project's environment information accurate?", id: 'environment', navigate: '/settings/environment', icon: GlobeIcon, diff --git a/packages/moderation/src/data/stages/gallery.ts b/packages/moderation/src/data/stages/gallery.ts index e13e0db110..0623364dc1 100644 --- a/packages/moderation/src/data/stages/gallery.ts +++ b/packages/moderation/src/data/stages/gallery.ts @@ -4,7 +4,8 @@ import type { ButtonAction } from '../../types/actions' import type { Stage } from '../../types/stage' const gallery: Stage = { - title: "Are this project's gallery images sufficient?", + title: "Gallery", + hint: "Are this project's gallery images sufficient?", id: 'gallery', icon: ImageIcon, guidance_url: diff --git a/packages/moderation/src/data/stages/license.ts b/packages/moderation/src/data/stages/license.ts index 069b8d670a..7bc9417540 100644 --- a/packages/moderation/src/data/stages/license.ts +++ b/packages/moderation/src/data/stages/license.ts @@ -20,7 +20,8 @@ const licensesNotRequiringSource: string[] = [ ] const licenseStage: Stage = { - title: 'Is this license and link valid?', + title: 'License', + hint: 'Is this license and link valid?', text: async () => (await import('../messages/checklist-text/licensing.md?raw')).default, id: 'license', icon: BookTextIcon, diff --git a/packages/moderation/src/data/stages/links.ts b/packages/moderation/src/data/stages/links.ts index d1345ac3d6..5a1b835de0 100644 --- a/packages/moderation/src/data/stages/links.ts +++ b/packages/moderation/src/data/stages/links.ts @@ -120,7 +120,8 @@ function getInaccessibleLinkOptions(context: ChecklistActionContext): MultiSelec } const links: Stage = { - title: "Are the project's links accurate and accessible?", + title: 'Links', + hint: "Are the project's links accurate and accessible?", id: 'links', icon: LinkIcon, guidance_url: diff --git a/packages/moderation/src/data/stages/permissions.ts b/packages/moderation/src/data/stages/permissions.ts index 0f23c2d464..078a881240 100644 --- a/packages/moderation/src/data/stages/permissions.ts +++ b/packages/moderation/src/data/stages/permissions.ts @@ -3,7 +3,8 @@ import { SignatureIcon } from '@modrinth/assets' import type { Stage } from '../../types/stage' const permissions: Stage = { - title: 'Does this projects external content have any issues?', + title: "Modpack Permissions", + hint: 'Does this projects external content have any issues?', id: 'permissions', icon: SignatureIcon, guidance_url: 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892', diff --git a/packages/moderation/src/data/stages/post-approval.ts b/packages/moderation/src/data/stages/post-approval.ts index 3b5d981490..d8802471c2 100644 --- a/packages/moderation/src/data/stages/post-approval.ts +++ b/packages/moderation/src/data/stages/post-approval.ts @@ -4,7 +4,8 @@ import type { ButtonAction } from '../../types/actions' import type { Stage } from '../../types/stage' const postApproval: Stage = { - title: 'Issue warnings, notices, or takedowns?', + title: 'Post-Approval', + hint: 'Issue warnings, notices, or takedowns?', id: 'post-approval', icon: ScaleIcon, guidance_url: diff --git a/packages/moderation/src/data/stages/reupload.ts b/packages/moderation/src/data/stages/reupload.ts index 8ffb1b1605..8cacfc5032 100644 --- a/packages/moderation/src/data/stages/reupload.ts +++ b/packages/moderation/src/data/stages/reupload.ts @@ -4,7 +4,8 @@ import type { ButtonAction, ToggleAction } from '../../types/actions' import type { Stage } from '../../types/stage' const reupload: Stage = { - title: 'Does the author have proper permissions to post this project?', + title: 'Reupload', + hint: 'Does the author have proper permissions to post this project?', id: 'reupload', icon: CopyrightIcon, guidance_url: diff --git a/packages/moderation/src/data/stages/rule-following.ts b/packages/moderation/src/data/stages/rule-following.ts index 05dcebf016..c71978eaf1 100644 --- a/packages/moderation/src/data/stages/rule-following.ts +++ b/packages/moderation/src/data/stages/rule-following.ts @@ -4,10 +4,12 @@ import type { ButtonAction, MultiSelectChipsAction } from '../../types/actions' import type { Stage } from '../../types/stage' const ruleFollowing: Stage = { - title: 'Does this project violate the rules?', + title: 'Rule Following', + hint: 'Does this project violate the rules?', id: 'rule-following', icon: ListBulletedIcon, - guidance_url: 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080709084f6269835607f', + guidance_url: + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080709084f6269835607f', navigate: '/moderation', actions: [ { @@ -241,7 +243,10 @@ const ruleFollowing: Stage = { weight: 100, suggestedStatus: 'rejected', severity: 'critical', - message: async () => (await import('../messages/rule-breaking/prohibited-content-header.md?raw')).default.trimEnd(), + message: async () => + ( + await import('../messages/rule-breaking/prohibited-content-header.md?raw') + ).default.trimEnd(), enablesActions: [ { id: 'prohibited_content_options', @@ -249,18 +254,94 @@ const ruleFollowing: Stage = { label: 'Which Prohibited Content rules does this project violate?', joinWith: '\n', options: [ - {label: 'Objectionable', weight: 101, message: async () => (await import('../messages/rule-breaking/prohibited-content/objectionable.md?raw')).default,}, - {label: 'Discriminatory or Explicit', weight: 102, message: async () => (await import('../messages/rule-breaking/prohibited-content/discriminatory.md?raw')).default,}, - {label: 'IP Infringement', weight: 103, message: async () => (await import('../messages/rule-breaking/prohibited-content/ip-infringement.md?raw')).default,}, - {label: 'Rights Violation', weight: 104, message: async () => (await import('../messages/rule-breaking/prohibited-content/legal-rights.md?raw')).default,}, - {label: 'Illegal Activity', weight: 105, message: async () => (await import('../messages/rule-breaking/prohibited-content/illegal-activity.md?raw')).default,}, - {label: 'Harmful or Deceptive', weight: 106, message: async () => (await import('../messages/rule-breaking/prohibited-content/harmful.md?raw')).default,}, - {label: 'Misleading claims', weight: 107, message: async () => (await import('../messages/rule-breaking/prohibited-content/misleading.md?raw')).default,}, - {label: 'Impersonation', weight: 108, message: async () => (await import('../messages/rule-breaking/prohibited-content/impersonation.md?raw')).default,}, - {label: 'False Endorsement', weight: 109, message: async () => (await import('../messages/rule-breaking/prohibited-content/false-endorsement.md?raw')).default,}, - {label: 'Profanity', weight: 110, message: async () => (await import('../messages/rule-breaking/prohibited-content/profanity.md?raw')).default,}, - {label: 'Undisclosed Data Upload', weight: 111, message: async () => (await import('../messages/rule-breaking/prohibited-content/undisclosed-upload.md?raw')).default,}, - {label: 'Mojang Bypass', weight: 112, message: async () => (await import('../messages/rule-breaking/prohibited-content/mojang-bypass.md?raw')).default,}, + { + label: 'Objectionable', + weight: 101, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/objectionable.md?raw')) + .default, + }, + { + label: 'Discriminatory or Explicit', + weight: 102, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/discriminatory.md?raw')) + .default, + }, + { + label: 'IP Infringement', + weight: 103, + message: async () => + ( + await import('../messages/rule-breaking/prohibited-content/ip-infringement.md?raw') + ).default, + }, + { + label: 'Rights Violation', + weight: 104, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/legal-rights.md?raw')) + .default, + }, + { + label: 'Illegal Activity', + weight: 105, + message: async () => + ( + await import('../messages/rule-breaking/prohibited-content/illegal-activity.md?raw') + ).default, + }, + { + label: 'Harmful or Deceptive', + weight: 106, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/harmful.md?raw')) + .default, + }, + { + label: 'Misleading claims', + weight: 107, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/misleading.md?raw')) + .default, + }, + { + label: 'Impersonation', + weight: 108, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/impersonation.md?raw')) + .default, + }, + { + label: 'False Endorsement', + weight: 109, + message: async () => + ( + await import('../messages/rule-breaking/prohibited-content/false-endorsement.md?raw') + ).default, + }, + { + label: 'Profanity', + weight: 110, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/profanity.md?raw')) + .default, + }, + { + label: 'Undisclosed Data Upload', + weight: 111, + message: async () => + ( + await import('../messages/rule-breaking/prohibited-content/undisclosed-upload.md?raw') + ).default, + }, + { + label: 'Mojang Bypass', + weight: 112, + message: async () => + (await import('../messages/rule-breaking/prohibited-content/mojang-bypass.md?raw')) + .default, + }, ], } as MultiSelectChipsAction, ], @@ -272,7 +353,8 @@ const ruleFollowing: Stage = { weight: 200, suggestedStatus: 'flagged', severity: 'high', - message: async () => (await import('../messages/rule-breaking/server-side-opt-out.md?raw')).default, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-out.md?raw')).default, } as ButtonAction, { id: 'server_side_opt_in', @@ -281,7 +363,10 @@ const ruleFollowing: Stage = { weight: 300, suggestedStatus: 'flagged', severity: 'high', - message: async () => (await import('../messages/rule-breaking/server-side-opt-in-header.md?raw')).default.trimEnd(), + message: async () => + ( + await import('../messages/rule-breaking/server-side-opt-in-header.md?raw') + ).default.trimEnd(), enablesActions: [ { id: 'server_side_opt_in_options', @@ -289,12 +374,47 @@ const ruleFollowing: Stage = { label: 'Which features of this project require a Server-side Opt-in?', joinWith: '\n', options: [ - {label: 'X-ray', weight: 301, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/x-ray.md?raw')).default,}, - {label: 'Aim Assist', weight: 302, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/aim-bot.md?raw')).default,}, - {label: 'Movement', weight: 303, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/movement.md?raw')).default,}, - {label: 'PvP', weight: 304, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/pvp.md?raw')).default,}, - {label: 'Anti 3.x', weight: 305, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/hiding-mods.md?raw')).default,}, - {label: 'Dupe', weight: 306, message: async () => (await import('../messages/rule-breaking/server-side-opt-in/item-duplication.md?raw')).default,}, + { + label: 'X-ray', + weight: 301, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in/x-ray.md?raw')).default, + }, + { + label: 'Aim Assist', + weight: 302, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in/aim-bot.md?raw')) + .default, + }, + { + label: 'Movement', + weight: 303, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in/movement.md?raw')) + .default, + }, + { + label: 'PvP', + weight: 304, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in/pvp.md?raw')).default, + }, + { + label: 'Anti 3.x', + weight: 305, + message: async () => + (await import('../messages/rule-breaking/server-side-opt-in/hiding-mods.md?raw')) + .default, + }, + { + label: 'Dupe', + weight: 306, + message: async () => + ( + await import('../messages/rule-breaking/server-side-opt-in/item-duplication.md?raw') + ).default, + }, ], } as MultiSelectChipsAction, ], @@ -313,7 +433,8 @@ const ruleFollowing: Stage = { projectV3?.minecraft_server?.languages?.length > 4 ) }, - message: async () => ( + message: async () => + ( await import('../messages/checklist-messages/misc-metadata/excessive_languages-server.md?raw') ).default, }, diff --git a/packages/moderation/src/data/stages/status-alerts.ts b/packages/moderation/src/data/stages/status-alerts.ts index c044010bb4..e2c3414df2 100644 --- a/packages/moderation/src/data/stages/status-alerts.ts +++ b/packages/moderation/src/data/stages/status-alerts.ts @@ -4,7 +4,8 @@ import type { ButtonAction } from '../../types/actions' import type { Stage } from '../../types/stage' const statusAlerts: Stage = { - title: `Is anything else affecting this project's status?`, + title: 'Status Alerts', + hint: `Is anything else affecting this project's status?`, id: 'status-alerts', icon: TriangleAlertIcon, text: async () => (await import('../messages/checklist-text/status-alerts/text.md?raw')).default, diff --git a/packages/moderation/src/data/stages/summary.ts b/packages/moderation/src/data/stages/summary.ts index 6710d0cc65..16981f135e 100644 --- a/packages/moderation/src/data/stages/summary.ts +++ b/packages/moderation/src/data/stages/summary.ts @@ -4,7 +4,8 @@ import type { ButtonAction } from '../../types/actions' import type { Stage } from '../../types/stage' const summary: Stage = { - title: "Is the project's summary sufficient?", + title: 'Summary', + hint: "Is the project's summary sufficient?", text: async () => (await import('../messages/checklist-text/summary/summary.md?raw')).default, id: 'summary', icon: AlignLeftIcon, diff --git a/packages/moderation/src/data/stages/title-slug.ts b/packages/moderation/src/data/stages/title-slug.ts index 4f880423c6..651114fd88 100644 --- a/packages/moderation/src/data/stages/title-slug.ts +++ b/packages/moderation/src/data/stages/title-slug.ts @@ -16,7 +16,8 @@ function hasCustomSlug(project: Labrinth.Projects.v2.Project): boolean { } const titleSlug: Stage = { - title: 'Are the Name and URL accurate and appropriate?', + title: 'Title & Slug', + hint: 'Are the Name and URL accurate and appropriate?', id: 'title-&-slug', text: async (project) => { let text = (await import('../messages/checklist-text/title-slug/title.md?raw')).default diff --git a/packages/moderation/src/data/stages/undefined-project.ts b/packages/moderation/src/data/stages/undefined-project.ts index a996da0f2c..150ac92f6e 100644 --- a/packages/moderation/src/data/stages/undefined-project.ts +++ b/packages/moderation/src/data/stages/undefined-project.ts @@ -3,7 +3,8 @@ import { XIcon } from '@modrinth/assets' import type { Stage } from '../../types/stage' const undefinedProjectStage: Stage = { - title: 'This project is undefined!', + title: 'Undefined Project', + hint: 'This project is undefined!', id: 'undefined-project', icon: XIcon, guidance_url: diff --git a/packages/moderation/src/data/stages/versions.ts b/packages/moderation/src/data/stages/versions.ts index 446a0321a7..175fab278d 100644 --- a/packages/moderation/src/data/stages/versions.ts +++ b/packages/moderation/src/data/stages/versions.ts @@ -42,7 +42,8 @@ function getIncorrectLoaderOptions(context: ChecklistActionContext): MultiSelect } const versions: Stage = { - title: "Are this project's files correct?", + title: 'Versions', + hint: "Are this project's files correct?", id: 'versions', icon: VersionIcon, guidance_url: @@ -186,7 +187,8 @@ const versions: Stage = { suggestedStatus: 'flagged', severity: 'medium', weight: 1001, - message: async () => (await import('../messages/checklist-messages/versions/incorrect_loader.md?raw')).default, + message: async () => + (await import('../messages/checklist-messages/versions/incorrect_loader.md?raw')).default, enablesActions: [ { id: 'versions_incorrect_loader_options', diff --git a/packages/moderation/src/types/stage.ts b/packages/moderation/src/types/stage.ts index 5e7eb946dc..0106f15cf5 100644 --- a/packages/moderation/src/types/stage.ts +++ b/packages/moderation/src/types/stage.ts @@ -8,10 +8,15 @@ import type { Action } from './actions' */ export interface Stage { /** - * The title of the stage, displayed to the moderator. + * The title of the stage, displayed in the checklist header. */ title: string + /** + * The hint for this stage, tells the moderator what to do. + */ + hint: string + /** * An optional description or additional text for the stage. */ From ee6ca6d804484d23ef393d0c249b67cf20bd8aef Mon Sep 17 00:00:00 2001 From: chyzman Date: Fri, 10 Jul 2026 21:52:35 -0400 Subject: [PATCH 17/85] Title & Slug, Summary Description, Links, License, Gallery, Versions and Reupload migrated to new system:tm: --- .../checklist/ModerationChecklist.vue | 982 ++++-------------- .../ui/moderation/checklist/NodeRenderer.vue | 296 ++++++ .../services/moderation-checklist-storage.ts | 16 +- packages/moderation/src/data/checklist.ts | 29 +- .../{insufficient.md => custom.md} | 0 .../{insufficient-fork.md => fork.md} | 0 .../{insufficient-header.md => header.md} | 0 .../insufficient/insufficient-port.md | 1 - .../{insufficient-packs.md => packs.md} | 0 .../{insufficient-projects.md => projects.md} | 0 .../{insufficient-servers.md => servers.md} | 0 .../links/discord/inaccessible.md | 1 + .../links/discord/misused.md | 1 + .../links/donations/inaccessible.md | 1 + .../links/donations/misused.md | 1 + .../links/issues/inaccessible.md | 1 + .../links/issues/misused.md | 1 + .../links/site/inaccessible.md | 1 + .../checklist-messages/links/site/misused.md | 1 + .../links/source/inaccessible.md | 1 + .../links/source/misused.md | 1 + .../links/store/inaccessible.md | 1 + .../checklist-messages/links/store/misused.md | 1 + .../links/wiki/inaccessible.md | 1 + .../checklist-messages/links/wiki/misused.md | 1 + .../prohibited-content-header.md | 2 - .../prohibited-content/discriminatory.md | 1 - .../prohibited-content/false-endorsement.md | 1 - .../prohibited-content/harmful.md | 1 - .../prohibited-content/illegal-activity.md | 1 - .../prohibited-content/impersonation.md | 1 - .../prohibited-content/ip-infringement.md | 1 - .../prohibited-content/legal-rights.md | 1 - .../prohibited-content/misleading.md | 1 - .../prohibited-content/mojang-bypass.md | 1 - .../prohibited-content/objectionable.md | 1 - .../prohibited-content/profanity.md | 1 - .../prohibited-content/undisclosed-upload.md | 1 - .../server-side-opt-in-header.md | 2 - .../server-side-opt-in/aim-bot.md | 1 - .../server-side-opt-in/hiding-mods.md | 1 - .../server-side-opt-in/item-duplication.md | 1 - .../server-side-opt-in/movement.md | 1 - .../rule-breaking/server-side-opt-in/pvp.md | 1 - .../rule-breaking/server-side-opt-in/x-ray.md | 1 - .../rule-breaking/server-side-opt-out.md | 2 - .../src/data/modpack-permissions-stage.ts | 35 - .../moderation/src/data/stages/description.ts | 272 ++--- .../moderation/src/data/stages/gallery.ts | 63 +- .../moderation/src/data/stages/license.ts | 115 +- packages/moderation/src/data/stages/links.ts | 274 ++--- .../moderation/src/data/stages/reupload.ts | 371 +++---- .../moderation/src/data/stages/summary.ts | 112 +- .../moderation/src/data/stages/title-slug.ts | 155 ++- .../moderation/src/data/stages/versions.ts | 367 +++---- packages/moderation/src/index.ts | 2 +- packages/moderation/src/types/node.ts | 225 ++++ 57 files changed, 1302 insertions(+), 2051 deletions(-) create mode 100644 apps/frontend/src/components/ui/moderation/checklist/NodeRenderer.vue rename packages/moderation/src/data/messages/checklist-messages/description/insufficient/{insufficient.md => custom.md} (100%) rename packages/moderation/src/data/messages/checklist-messages/description/insufficient/{insufficient-fork.md => fork.md} (100%) rename packages/moderation/src/data/messages/checklist-messages/description/insufficient/{insufficient-header.md => header.md} (100%) delete mode 100644 packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-port.md rename packages/moderation/src/data/messages/checklist-messages/description/insufficient/{insufficient-packs.md => packs.md} (100%) rename packages/moderation/src/data/messages/checklist-messages/description/insufficient/{insufficient-projects.md => projects.md} (100%) rename packages/moderation/src/data/messages/checklist-messages/description/insufficient/{insufficient-servers.md => servers.md} (100%) create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/discord/inaccessible.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/discord/misused.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/donations/inaccessible.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/donations/misused.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/issues/inaccessible.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/issues/misused.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/site/inaccessible.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/site/misused.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/source/inaccessible.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/source/misused.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/store/inaccessible.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/store/misused.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/wiki/inaccessible.md create mode 100644 packages/moderation/src/data/messages/checklist-messages/links/wiki/misused.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content-header.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in-header.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md delete mode 100644 packages/moderation/src/data/messages/rule-breaking/server-side-opt-out.md delete mode 100644 packages/moderation/src/data/modpack-permissions-stage.ts create mode 100644 packages/moderation/src/types/node.ts diff --git a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue index b23292ab3c..e27e02a756 100644 --- a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue @@ -208,144 +208,13 @@ {{ currentStageObj.hint }} -
-
-
Loading stage content...
-
- - -
- -
-
- -
-
- - -
- -
- - - - - -
- -
-
- -
- - -
- -
+
@@ -498,42 +367,28 @@ import { XIcon, } from '@modrinth/assets' import type { - Action, - ActionState, - ButtonAction, - ChecklistActionContext, - ConditionalButtonAction, - DropdownAction, - MultiSelectChipsAction, - MultiSelectChipsOption, - Stage, - ToggleAction, + ChecklistStage, + Node, + NodeContext, + NodeState, + NodeStateWithChildren, } from '@modrinth/moderation' import { checklist, expandVariables, - finalPermissionMessages, - findMatchingVariant, flattenProjectV3Variables, flattenProjectVariables, flattenStaticVariables, - getActionIdForStage, - getActionMessage, - getVisibleInputs, handleKeybind, - initializeActionState, kebabToTitleCase, keybinds, - processMessage, } from '@modrinth/moderation' import type { OverflowMenuOption } from '@modrinth/ui' import { Avatar, ButtonStyled, - Checkbox, Collapsible, ConfirmModal, - DropdownSelect, injectNotificationManager, injectProjectPageContext, MarkdownEditor, @@ -542,9 +397,8 @@ import { useDebugLogger, } from '@modrinth/ui' import type { ModerationJudgements, ModerationModpackItem, ProjectStatus } from '@modrinth/utils' -import { renderHighlightedString } from '@modrinth/utils' import { useQueryClient } from '@tanstack/vue-query' -import { computedAsync, useDebounceFn } from '@vueuse/core' +import { useDebounceFn } from '@vueuse/core' import { toRaw } from 'vue' import type { Component } from 'vue' @@ -561,6 +415,7 @@ import { useModerationQueue } from '~/services/moderation-queue.ts' import KeybindsModal from './ChecklistKeybindsModal.vue' import ModpackPermissionsFlow from './ModpackPermissionsFlow.vue' +import NodeRenderer from './NodeRenderer.vue' const notifications = injectNotificationManager() const { addNotification } = notifications @@ -610,133 +465,6 @@ const PREFETCH_STALE_MS = 30_000 // 30 seconds const PREFETCH_TARGET_COUNT = 3 // Keep 3 unlocked projects ready const PREFETCH_BATCH_SIZE = 5 // Check 5 at a time in parallel -// Tooltip constants and cache -const BUTTON_TOOLTIP_DELAY_MS = 500 // Show tooltip after 1.5 seconds of hovering -const BUTTON_TOOLTIP_HIDE_DELAY_MS = 0 // Hide immediately on mouseleave -const buttonActionTooltipCache = ref>({}) -const TOOLTIP_CACHE_TTL_MS = 30000 // Cache tooltip text for 30 seconds - -// Helper: compute the message text that a button action would generate -async function getButtonActionTooltipText(action: Action): Promise { - try { - const actionIndex = currentStageObj.value.actions.indexOf(action) - const actionId = getActionId(action, actionIndex) - const state = actionStates.value[actionId] - - if (!state) return '' - - // Build list of selected action IDs (including this action as if selected) - const tempSelectedIds = Object.entries(actionStates.value) - .filter(([_, s]) => s.selected) - .map(([id]) => id) - - if (!tempSelectedIds.includes(actionId)) { - tempSelectedIds.push(actionId) - } - - // Build all valid action IDs across all stages - const allValidActionIds: string[] = [] - checklist.forEach((stage, stageIdx) => { - stage.actions.forEach((stageAction, actionIdx) => { - allValidActionIds.push(getActionIdForStage(stageAction, stageIdx, actionIdx)) - if (stageAction.enablesActions) { - stageAction.enablesActions.forEach((enabledAction, enabledIdx) => { - allValidActionIds.push( - getActionIdForStage(enabledAction, stageIdx, actionIdx, enabledIdx), - ) - }) - } - }) - }) - - let messageText = '' - - if (action.type === 'button' || action.type === 'toggle') { - const buttonAction = action as ButtonAction | ToggleAction - const message = await getActionMessage(buttonAction, tempSelectedIds, allValidActionIds) - if (message) { - messageText = processMessage(message, action, currentStage.value, textInputValues.value) - } - } else if (action.type === 'conditional-button') { - const conditionalAction = action as ConditionalButtonAction - const matchingVariant = findMatchingVariant( - conditionalAction.messageVariants, - tempSelectedIds, - allValidActionIds, - currentStage.value, - ) - - if (matchingVariant) { - const message = (await matchingVariant.message()) as string - messageText = processMessage(message, action, currentStage.value, textInputValues.value) - } else if (conditionalAction.fallbackMessage) { - const message = (await conditionalAction.fallbackMessage()) as string - messageText = processMessage(message, action, currentStage.value, textInputValues.value) - } - } - - return expandVariables( - messageText.trim(), - projectV2.value, - projectV3.value, - variables.value, - ).trim() - } catch (error) { - console.warn('[tooltip] Error computing action message:', error) - return '' - } -} - -// Helper: render markdown tooltip content using the app's markdown renderer -function renderTooltipMarkdownHtml(text: string): string { - const trimmed = text/*.slice(0, 500)*/.trim() - if (!trimmed) return '' - - return `
${renderHighlightedString(trimmed)}
` -} - -// Helper: get tooltip directive config for a button action with caching & delay -function getChecklistButtonTooltipConfig(action: Action): any { - const actionKey = getActionKey(action) - const now = Date.now() - const cached = buttonActionTooltipCache.value[actionKey] - - if (cached && now < cached.expiresAt) { - const renderedHtml = renderTooltipMarkdownHtml(cached.text) - return renderedHtml - ? { - content: renderedHtml, - html: true, - delay: { show: BUTTON_TOOLTIP_DELAY_MS, hide: BUTTON_TOOLTIP_HIDE_DELAY_MS }, - triggers: ['hover', 'focus'], - placement: 'top', - } - : undefined - } - - void (async () => { - const text = await getButtonActionTooltipText(action) - buttonActionTooltipCache.value[actionKey] = { - text, - expiresAt: Date.now() + TOOLTIP_CACHE_TTL_MS, - } - })() - - if (cached) { - const renderedHtml = renderTooltipMarkdownHtml(cached.text) - return renderedHtml - ? { - content: renderedHtml, - html: true, - delay: { show: BUTTON_TOOLTIP_DELAY_MS, hide: BUTTON_TOOLTIP_HIDE_DELAY_MS }, - triggers: ['hover', 'focus'], - placement: 'top', - } - : undefined - } - - return undefined -} async function handleVisibilityChange() { if (document.visibilityState === 'visible' && lockStatus.value?.isOwnLock) { @@ -894,11 +622,6 @@ const variables = computed(() => { } }) -const checklistActionContext = computed(() => ({ - project: projectV2.value, - projectV3: projectV3.value, - versions: versions.value, -})) const modpackPermissionsComplete = ref(false) const modpackJudgements = ref({}) @@ -919,6 +642,7 @@ const checklistPersistenceProjectId = projectV2.value.id const persistedState = import.meta.client ? await loadChecklistState(checklistPersistenceProjectId) : null +const reviewedAnyway = ref(persistedState?.reviewAnyway ?? false) const message = ref(persistedState?.message ?? null) const generatedMessage = computed(() => message.value !== null) const loadingMessage = ref(false) @@ -1002,6 +726,9 @@ async function confirmTakeOverOverride() { function reviewAnyway() { alreadyReviewed.value = false + reviewedAnyway.value = true + hasMeaningfulState = true + persistState() // Start prefetching the next project in the background maintainPrefetchQueue() } @@ -1235,8 +962,7 @@ async function skipToNextProject() { function resetProgress() { currentStage.value = findFirstValidStage() - actionStates.value = {} - textInputValues.value = {} + nodeStates.value = {} done.value = false clearGeneratedMessageState() @@ -1277,33 +1003,13 @@ const checklistTitleText = computed(() => { return currentStageObj.value.title ?? kebabToTitleCase(currentStageObj.value.id) }) -const currentStage = ref( - persistedState?.stage !== undefined && checklist[persistedState.stage] - ? persistedState.stage - : findFirstValidStage(), -) +const nodeStates = ref>>(persistedState?.state ?? {}) -const stageTextExpanded = computedAsync(async () => { - const stageIndex = currentStage.value - const stage = checklist[stageIndex] - if (stage.text) { - return renderHighlightedString( - expandVariables( - await stage.text(projectV2.value, projectV3.value), - projectV2.value, - projectV3.value, - variables.value, - ), - ) - } - return null -}, null) +const restoredStage = persistedState ? checklist.findIndex((s) => s.id === persistedState.stage) : -1 +const currentStage = ref(restoredStage >= 0 ? restoredStage : findFirstValidStage()) const router = useRouter() -const actionStates = ref>(persistedState?.actionStates ?? {}) -const textInputValues = ref>(persistedState?.textInputs ?? {}) - const initialStage = currentStage.value let hasMeaningfulState = persistedState !== null @@ -1312,21 +1018,20 @@ const persistState = () => { hasMeaningfulState = currentStage.value !== initialStage || message.value !== null || - Object.values(toRaw(actionStates.value)).some((s) => s.selected) + Object.values(toRaw(nodeStates.value)).some((s) => Object.keys(s).length > 0) if (!hasMeaningfulState) return } void saveChecklistState(checklistPersistenceProjectId, { open: !props.collapsed, - stage: currentStage.value, + reviewAnyway: reviewedAnyway.value || undefined, + stage: currentStageObj.value.id, message: message.value, - actionStates: toRaw(actionStates.value), - textInputs: toRaw(textInputValues.value), + state: toRaw(nodeStates.value), }) } watch(currentStage, persistState) -watch(actionStates, persistState, { deep: true }) -watch(textInputValues, persistState, { deep: true }) +watch(nodeStates, persistState, { deep: true }) watch(message, persistState) watch(() => props.collapsed, (collapsed) => { if (!collapsed) hasMeaningfulState = true @@ -1336,13 +1041,9 @@ watch(() => props.collapsed, (collapsed) => { interface MessagePart { weight: number content: string - actionId: string - stageIndex: number } function handleKeybinds(event: KeyboardEvent) { - const focusedActionIndex = ref(null) - handleKeybind( event, { @@ -1360,13 +1061,10 @@ function handleKeybinds(event: KeyboardEvent) { isModpackPermissionsStage: isModpackPermissionsStage.value, futureProjectCount: moderationQueue.queueLength, - visibleActionsCount: visibleActions.value.length, + visibleActionsCount: currentStageObj.value.nodes.length, - focusedActionIndex: focusedActionIndex.value, - focusedActionType: - focusedActionIndex.value !== null - ? (visibleActions.value[focusedActionIndex.value]?.type as any) - : null, + focusedActionIndex: null, + focusedActionType: null, }, actions: { tryGoNext: nextStage, @@ -1383,63 +1081,12 @@ function handleKeybinds(event: KeyboardEvent) { tryWithhold: () => sendMessage('withheld'), tryEditMessage: goBackToStages, - tryToggleAction: (actionIndex: number) => { - const action = visibleActions.value[actionIndex] - if (action) { - toggleAction(action) - } - }, - trySelectDropdownOption: (actionIndex: number, optionIndex: number) => { - const action = visibleActions.value[actionIndex] as DropdownAction - if (action && action.type === 'dropdown') { - const visibleOptions = getVisibleDropdownOptions(action) - if (optionIndex < visibleOptions.length) { - selectDropdownOption(action, visibleOptions[optionIndex]) - } - } - }, - tryToggleChip: (actionIndex: number, chipIndex: number) => { - const action = visibleActions.value[actionIndex] as MultiSelectChipsAction - if (action && action.type === 'multi-select-chips') { - const visibleOptions = getVisibleMultiSelectOptions(action) - if (chipIndex < visibleOptions.length) { - toggleChip(action, visibleOptions[chipIndex]) - } - } - }, - - tryFocusNextAction: () => { - if (visibleActions.value.length === 0) return - if (focusedActionIndex.value === null) { - focusedActionIndex.value = 0 - } else { - focusedActionIndex.value = (focusedActionIndex.value + 1) % visibleActions.value.length - } - }, - tryFocusPreviousAction: () => { - if (visibleActions.value.length === 0) return - if (focusedActionIndex.value === null) { - focusedActionIndex.value = visibleActions.value.length - 1 - } else { - focusedActionIndex.value = - focusedActionIndex.value === 0 - ? visibleActions.value.length - 1 - : focusedActionIndex.value - 1 - } - }, - tryActivateFocusedAction: () => { - if (focusedActionIndex.value === null) return - const action = visibleActions.value[focusedActionIndex.value] - if (!action) return - - if ( - action.type === 'button' || - action.type === 'conditional-button' || - action.type === 'toggle' - ) { - toggleAction(action) - } - }, + tryToggleAction: () => {}, + trySelectDropdownOption: () => {}, + tryToggleChip: () => {}, + tryFocusNextAction: () => {}, + tryFocusPreviousAction: () => {}, + tryActivateFocusedAction: () => {}, }, }, keybinds, @@ -1460,7 +1107,7 @@ onMounted(async () => { document.addEventListener('visibilitychange', handleVisibilityChange) notifications.setNotificationLocation('left') - if (projectV2.value.status !== 'processing') { + if (projectV2.value.status !== 'processing' && !reviewedAnyway.value) { alreadyReviewed.value = true return } @@ -1549,228 +1196,27 @@ watch( { immediate: true }, ) -function getActionId(action: Action, index?: number): string { - // If index is not provided, find it in the current stage's actions - if (index === undefined) { - index = currentStageObj.value.actions.indexOf(action) - } - return getActionIdForStage(action, currentStage.value, index) -} - -function getActionKey(action: Action): string { - // Find the actual index of this action in the current stage's actions array - const index = currentStageObj.value.actions.indexOf(action) - return `${currentStage.value}-${index}-${getActionId(action, index)}` -} - -const visibleActions = computed(() => { - const selectedActionIds = Object.entries(actionStates.value) - .filter(([_, state]) => state.selected) - .map(([id]) => id) - - const allActions: Action[] = [] - const actionSources = new Map() - - currentStageObj.value.actions.forEach((action, actionIndex) => { - if (shouldShowAction(action)) { - allActions.push(action) - actionSources.set(action, { actionIndex }) - - if (action.enablesActions) { - action.enablesActions.forEach((enabledAction) => { - if (shouldShowAction(enabledAction)) { - allActions.push(enabledAction) - actionSources.set(enabledAction, { enabledBy: action, actionIndex }) - } - }) - } - } - }) - - return allActions.filter((action) => { - const source = actionSources.get(action) - - if (source?.enabledBy) { - const enablerId = getActionId(source.enabledBy, source.actionIndex) - if (!selectedActionIds.includes(enablerId)) { - return false - } - } - - const disabledByOthers = currentStageObj.value.actions.some((otherAction, otherIndex) => { - const otherId = getActionId(otherAction, otherIndex) - return ( - selectedActionIds.includes(otherId) && - otherAction.disablesActions?.includes( - action.id || `action-${currentStage.value}-${source?.actionIndex}`, - ) - ) - }) - - return !disabledByOthers - }) -}) - -const buttonActions = computed(() => - visibleActions.value.filter( - (action) => action.type === 'button' || action.type === 'conditional-button', - ), -) - -const toggleActions = computed(() => - visibleActions.value.filter((action) => action.type === 'toggle'), -) - -const dropdownActions = computed(() => - visibleActions.value.filter((action) => action.type === 'dropdown'), -) - -const multiSelectActions = computed(() => - visibleActions.value.filter( - (action) => - action.type === 'multi-select-chips' && getVisibleMultiSelectOptions(action).length > 0, - ), -) - -function resolveMultiSelectOptions(action: MultiSelectChipsAction) { - return typeof action.options === 'function' - ? action.options(checklistActionContext.value) - : action.options -} - -function getMultiSelectOptionId(option: MultiSelectChipsOption): string { - return option.id ?? option.label -} - -function getMultiSelectOptionKey( - action: MultiSelectChipsAction, - option: MultiSelectChipsOption, - optionIndex: number, -) { - return `${getActionId(action)}-chip-${getMultiSelectOptionId(option) || optionIndex}` +function getNodeState(stageId: string, nodeId: string): NodeState { + return nodeStates.value[stageId]?.[nodeId] } -function getSelectedChipIds(action: MultiSelectChipsAction, state?: ActionState): Set { - const selectedValues = state?.value - if (!(selectedValues instanceof Set)) { - return new Set() - } - - const optionIds = new Set() - const resolvedOptions = resolveMultiSelectOptions(action) - - for (const selectedValue of selectedValues) { - if (typeof selectedValue === 'string') { - optionIds.add(selectedValue) - } else if (typeof selectedValue === 'number') { - const option = resolvedOptions[selectedValue] - if (option) { - optionIds.add(getMultiSelectOptionId(option)) - } +function setNodeState(stageId: string, nodeId: string, value: NodeState) { + if (value === undefined) { + if (!nodeStates.value[stageId]) return + const { [nodeId]: _, ...rest } = nodeStates.value[stageId] + if (Object.keys(rest).length === 0) { + const { [stageId]: __, ...restStages } = nodeStates.value + nodeStates.value = restStages + } else { + nodeStates.value[stageId] = rest } - } - - return optionIds -} - -function getDropdownValue(action: DropdownAction) { - const actionIndex = currentStageObj.value.actions.indexOf(action) - const actionId = getActionId(action, actionIndex) - const visibleOptions = getVisibleDropdownOptions(action) - const storedValue = actionStates.value[actionId]?.value - const currentValue: number = typeof storedValue === 'number' ? storedValue : (action.defaultOption ?? 0) - - const allOptions = action.options - const storedOption = allOptions.at(currentValue) - - if (storedOption && visibleOptions.includes(storedOption)) { - return storedOption - } - - return visibleOptions[0] || null -} - -function isDefaultActionState(action: Action, state: ActionState): boolean { - const def = initializeActionState(action) - if (state.selected !== def.selected) return false - if (def.value instanceof Set) return !(state.value instanceof Set) || state.value.size === 0 - return state.value === def.value -} - -function isActionSelected(action: Action): boolean { - const actionIndex = currentStageObj.value.actions.indexOf(action) - const actionId = getActionId(action, actionIndex) - const state = actionStates.value[actionId] - if (state !== undefined) return state.selected - return action.type === 'toggle' ? (action.defaultChecked ?? false) : false -} - -function toggleAction(action: Action) { - const actionIndex = currentStageObj.value.actions.indexOf(action) - const actionId = getActionId(action, actionIndex) - const current = actionStates.value[actionId] ?? initializeActionState(action) - const newState: ActionState = { ...current, selected: !current.selected } - if (isDefaultActionState(action, newState)) { - const { [actionId]: _, ...rest } = actionStates.value - actionStates.value = rest - } else { - actionStates.value[actionId] = newState - } - persistState() -} - -function selectDropdownOption(action: DropdownAction, selected: any) { - if (selected === undefined || selected === null) return - const actionIndex = currentStageObj.value.actions.indexOf(action) - const actionId = getActionId(action, actionIndex) - const optionIndex = action.options.findIndex( - (opt) => opt === selected || (opt?.label && selected?.label && opt.label === selected.label), - ) - if (optionIndex === -1) return - const newState: ActionState = { selected: true, value: optionIndex } - if (isDefaultActionState(action, newState)) { - const { [actionId]: _, ...rest } = actionStates.value - actionStates.value = rest } else { - actionStates.value[actionId] = newState + if (!nodeStates.value[stageId]) nodeStates.value[stageId] = {} + nodeStates.value[stageId][nodeId] = value } persistState() } - -function isChipSelected(action: MultiSelectChipsAction, option: MultiSelectChipsOption): boolean { - const actionIndex = currentStageObj.value.actions.indexOf(action) - const actionId = getActionId(action, actionIndex) - return getSelectedChipIds(action, actionStates.value[actionId]).has(getMultiSelectOptionId(option)) -} - -function toggleChip(action: MultiSelectChipsAction, option: MultiSelectChipsOption) { - const actionIndex = currentStageObj.value.actions.indexOf(action) - const actionId = getActionId(action, actionIndex) - const selectedOptionIds = getSelectedChipIds(action, actionStates.value[actionId]) - const optionId = getMultiSelectOptionId(option) - if (selectedOptionIds.has(optionId)) { - selectedOptionIds.delete(optionId) - } else { - selectedOptionIds.add(optionId) - } - const newState: ActionState = { selected: selectedOptionIds.size > 0, value: selectedOptionIds } - if (isDefaultActionState(action, newState)) { - const { [actionId]: _, ...rest } = actionStates.value - actionStates.value = rest - } else { - actionStates.value[actionId] = newState - } - persistState() -} - -const isAnyVisibleInputs = computed(() => { - return visibleActions.value.some((action) => { - const visibleInputs = getVisibleInputs(action, actionStates.value) - return visibleInputs.length > 0 && isActionSelected(action) - }) -}) - function getModpackFilesFromStorage(): { interactive: ModerationModpackItem[] permanentNo: ModerationModpackItem[] @@ -1796,233 +1242,148 @@ function getModpackFilesFromStorage(): { } } -async function assembleFullMessage() { - const messageParts: MessagePart[] = [] - - for (let stageIndex = 0; stageIndex < checklist.length; stageIndex++) { - const stage = checklist[stageIndex] - - await processStageActions(stage, stageIndex, messageParts) +function isNodeActive(node: Node, state: NodeState): boolean { + switch (node.type) { + case 'boolean': { + if (typeof state === 'boolean') return state + if (state && typeof state === 'object' && !(state instanceof Set)) { + const v = (state as NodeStateWithChildren).value + if (typeof v === 'boolean') return v + } + return node.defaultChecked === true + } + case 'multi-select': return state instanceof Set && state.size > 0 + case 'select': return typeof state === 'string' && state !== '' + case 'text': + case 'markdown': return typeof state === 'string' && state !== '' + default: return false } - - messageParts.sort((a, b) => a.weight - b.weight) - - const finalMessage = expandVariables( - messageParts - .map((part) => part.content) - .filter((content) => content.trim().length > 0) - .join('\n\n'), - projectV2.value, - projectV3.value, - ) - - return finalMessage } -async function processStageActions(stage: Stage, stageIndex: number, messageParts: MessagePart[]) { - const selectedActionIds = Object.entries(actionStates.value) - .filter(([_, state]) => state.selected) - .map(([id]) => id) - - for (let actionIndex = 0; actionIndex < stage.actions.length; actionIndex++) { - const action = stage.actions[actionIndex] - const actionId = getActionIdForStage(action, stageIndex, actionIndex) - const state = actionStates.value[actionId] - - if (!state?.selected) continue - - await processAction(action, actionId, state, selectedActionIds, stageIndex, messageParts) - - if (action.enablesActions) { - for (let enabledIndex = 0; enabledIndex < action.enablesActions.length; enabledIndex++) { - const enabledAction = action.enablesActions[enabledIndex] - const enabledActionId = getActionIdForStage( - enabledAction, - stageIndex, - actionIndex, - enabledIndex, - ) - const enabledState = actionStates.value[enabledActionId] - - if (enabledState?.selected) { - await processAction( - enabledAction, - enabledActionId, - enabledState, - selectedActionIds, - stageIndex, - messageParts, - ) - } - } - } +function getBooleanChildState(nodeState: NodeState): Record { + if (nodeState && typeof nodeState === 'object' && !(nodeState instanceof Set)) { + return nodeState as Record } + return {} } -async function processAction( - action: Action, - actionId: string, - state: ActionState, - selectedActionIds: string[], - stageIndex: number, - messageParts: MessagePart[], -) { - const allValidActionIds: string[] = [] - checklist.forEach((stage, stageIdx) => { - stage.actions.forEach((stageAction, actionIdx) => { - allValidActionIds.push(getActionIdForStage(stageAction, stageIdx, actionIdx)) - if (stageAction.enablesActions) { - stageAction.enablesActions.forEach((enabledAction, enabledIdx) => { - allValidActionIds.push( - getActionIdForStage(enabledAction, stageIdx, actionIdx, enabledIdx), - ) - }) - } - }) - }) +function resolveChildren(node: Node, ctx: NodeContext): Node[] { + if (node.childrenFn) return node.childrenFn(ctx) + return node.children ?? [] +} - if (action.type === 'button' || action.type === 'toggle') { - const buttonAction = action as ButtonAction | ToggleAction - const message = await getActionMessage(buttonAction, selectedActionIds, allValidActionIds) - if (message) { - messageParts.push({ - weight: buttonAction.weight, - content: processMessage(message, action, stageIndex, textInputValues.value), - actionId, - stageIndex, - }) +async function collectNodeMessages( + nodes: Node[], + stageState: Record, + ctx: NodeContext, + parts: MessagePart[], +): Promise { + for (const node of nodes) { + if (node.shown && !node.shown(ctx)) continue + + if (node.type === 'group') { + if (node.id) { + const raw = stageState[node.id] + const childState = (raw && typeof raw === 'object' && !(raw instanceof Set)) + ? (raw as Record) + : {} + await collectNodeMessages(resolveChildren(node, ctx), childState, { ...ctx, state: childState }, parts) + } else { + await collectNodeMessages(resolveChildren(node, ctx), stageState, ctx, parts) + } + continue } - } else if (action.type === 'conditional-button') { - const conditionalAction = action as ConditionalButtonAction - const matchingVariant = findMatchingVariant( - conditionalAction.messageVariants, - selectedActionIds, - allValidActionIds, - stageIndex, - ) - let message: string - let weight: number + const nodeState = stageState[node.id!] + const active = isNodeActive(node, nodeState) - if (matchingVariant) { - message = (await matchingVariant.message()) as string - weight = matchingVariant.weight - } else if (conditionalAction.fallbackMessage) { - message = (await conditionalAction.fallbackMessage()) as string - weight = conditionalAction.fallbackWeight ?? 0 - } else { - return + if (active && node.message) { + const nodeCtx: NodeContext = node.type === 'boolean' + ? { ...ctx, state: getBooleanChildState(nodeState) } + : ctx + const msg = await node.message(nodeCtx) + if (msg) parts.push({ weight: node.weight ?? 0, content: msg }) } - messageParts.push({ - weight, - content: processMessage(message, action, stageIndex, textInputValues.value), - actionId, - stageIndex, - }) - } else if (action.type === 'dropdown') { - const dropdownAction = action as DropdownAction - const selectedIndex: number = typeof state.value === 'number' ? state.value : 0 - const selectedOption = dropdownAction.options.at(selectedIndex) - - if (selectedOption && 'message' in selectedOption && 'weight' in selectedOption) { - const message = (await selectedOption.message()) as string - messageParts.push({ - weight: selectedOption.weight, - content: processMessage(message, action, stageIndex, textInputValues.value), - actionId, - stageIndex, - }) - } - } else if (action.type === 'multi-select-chips') { - const multiSelectAction = action as MultiSelectChipsAction - const visibleOptions = getVisibleMultiSelectOptions(multiSelectAction) - const selectedOptionIds = getSelectedChipIds(multiSelectAction, state) - - if (multiSelectAction.joinWith !== undefined) { - const parts: { weight: number; content: string }[] = [] - for (const option of visibleOptions) { - if (selectedOptionIds.has(getMultiSelectOptionId(option))) { - parts.push({ - weight: option.weight, - content: processMessage( - (await option.message()) as string, - action, - stageIndex, - textInputValues.value, - ), - }) + const children = resolveChildren(node, ctx) + if (children.length > 0 && active) { + if (node.type === 'multi-select') { + const selected = nodeState instanceof Set ? nodeState : new Set() + for (const child of children) { + if (!selected.has(child.id!)) continue + if (child.shown && !child.shown(ctx)) continue + if (child.message) { + const msg = await child.message(ctx) + if (msg) parts.push({ weight: child.weight ?? 0, content: msg }) + } + if (child.children) { + await collectNodeMessages(child.children, stageState, ctx, parts) + } } - } - if (parts.length > 0) { - parts.sort((a, b) => a.weight - b.weight) - messageParts.push({ - weight: parts[0].weight, - content: parts.map((p) => p.content.trim()).join(multiSelectAction.joinWith), - actionId: `${actionId}-combined`, - stageIndex, - }) - } - } else { - for (const option of visibleOptions) { - if (selectedOptionIds.has(getMultiSelectOptionId(option))) { - const message = (await option.message()) as string - messageParts.push({ - weight: option.weight, - content: processMessage(message, action, stageIndex, textInputValues.value), - actionId: `${actionId}-option-${getMultiSelectOptionId(option)}`, - stageIndex, - }) + } else if (node.type === 'boolean') { + const childState = getBooleanChildState(nodeState) + await collectNodeMessages(children, childState, { ...ctx, state: childState }, parts) + } else if (node.type === 'select') { + const selectedId = typeof nodeState === 'string' ? nodeState : undefined + if (selectedId) { + for (const child of children) { + if (child.id !== selectedId) continue + if (child.shown && !child.shown(ctx)) continue + if (child.message) { + const msg = await child.message(ctx) + if (msg) parts.push({ weight: child.weight ?? node.weight ?? 0, content: msg }) + } + if (child.children) { + await collectNodeMessages(child.children, stageState, ctx, parts) + } + break + } } + } else { + await collectNodeMessages(children, stageState, ctx, parts) } } } } -function shouldShowStage(stage: Stage): boolean { - let hasVisibleActions = false - - for (const a of stage.actions) { - if (shouldShowAction(a)) { - hasVisibleActions = true - } - } +async function assembleFullMessage() { + const parts: MessagePart[] = [] - if (!hasVisibleActions) { - return false + for (const stage of checklist) { + const stageState = nodeStates.value[stage.id] ?? {} + await collectNodeMessages(stage.nodes, stageState, makeNodeContext(stage), parts) } - if (typeof stage.shouldShow === 'function') { - return stage.shouldShow(projectV2.value, projectV3.value) - } + parts.sort((a, b) => a.weight - b.weight) - return true + return expandVariables( + parts + .map((p) => p.content) + .filter((c) => c.trim().length > 0) + .join('\n\n'), + projectV2.value, + projectV3.value, + ) } -function shouldShowAction(action: Action): boolean { - if (typeof action.shouldShow === 'function') { - return action.shouldShow(projectV2.value, projectV3.value, checklistActionContext.value) +function makeNodeContext(stage: ChecklistStage): NodeContext { + return { + project: projectV3.value, + projectV2: projectV2.value, + state: nodeStates.value[stage.id] ?? {}, + globalState: nodeStates.value, } - - return true } -function getVisibleDropdownOptions(action: DropdownAction) { - return action.options.filter((option) => { - if (typeof option.shouldShow === 'function') { - return option.shouldShow(projectV2.value, projectV3.value, checklistActionContext.value) - } - return true - }) +function isNodeVisible(node: Node, ctx: NodeContext): boolean { + return !node.shown || node.shown(ctx) } -function getVisibleMultiSelectOptions(action: MultiSelectChipsAction) { - return resolveMultiSelectOptions(action).filter((option) => { - if (typeof option.shouldShow === 'function') { - return option.shouldShow(projectV2.value, projectV3.value, checklistActionContext.value) - } - return true - }) +function shouldShowStage(stage: ChecklistStage): boolean { + const ctx = makeNodeContext(stage) + if (!stage.nodes.some((n) => isNodeVisible(n, ctx))) return false + if (stage.shown) return stage.shown(projectV2.value, projectV3.value) + return true } function shouldShowStageIndex(stageIndex: number): boolean { @@ -2115,6 +1476,13 @@ async function generateMessage() { } } +const finalPermissionMessages = { + 'with-attribution': `The following content has attribution requirements, meaning that you must link back to the page where you originally found this content in your Modpack's description or version changelog (e.g. linking a mod's CurseForge page if you got it from CurseForge):`, + no: 'The following content is not allowed in Modrinth modpacks due to licensing restrictions. Please contact the author(s) directly for permission or remove the content from your modpack:', + 'permanent-no': `The following content is not allowed in Modrinth modpacks, regardless of permission obtained. This may be because it breaks Modrinth's content rules or because the authors, upon being contacted for permission, have declined. Please remove the content from your modpack:`, + unidentified: `The following content could not be identified. Please provide proof of its origin along with proof that you have permission to include it:`, +} + function generateModpackMessage(allFiles: { interactive: ModerationModpackItem[] permanentNo: ModerationModpackItem[] @@ -2357,8 +1725,7 @@ function clearProjectLocalStorage() { sessionStorage.removeItem(`modpack-permissions-updated-${projectV2.value.id}`) void clearChecklistState(checklistPersistenceProjectId) - actionStates.value = {} - textInputValues.value = {} + nodeStates.value = {} message.value = null } @@ -2381,17 +1748,20 @@ const hasValidPreviousStage = computed(() => { }) const stageOptions = computed(() => { - const options = checklist + const options = (checklist as ReadonlyArray) .map((stage, index) => { if (!shouldShowStage(stage)) return null return { id: String(index), - action: () => (currentStage.value = index), + action: () => { + clearGeneratedMessageState() + currentStage.value = index + }, text: stage.title ?? kebabToTitleCase(stage.id), color: index === currentStage.value && !generatedMessage.value ? 'green' : undefined, hoverFilled: true, - icon: stage.icon ? stage.icon : undefined, + icon: stage.icon ?? undefined, } as OverflowMenuOption }) .filter((opt): opt is OverflowMenuOption => opt !== null) diff --git a/apps/frontend/src/components/ui/moderation/checklist/NodeRenderer.vue b/apps/frontend/src/components/ui/moderation/checklist/NodeRenderer.vue new file mode 100644 index 0000000000..d6cf593815 --- /dev/null +++ b/apps/frontend/src/components/ui/moderation/checklist/NodeRenderer.vue @@ -0,0 +1,296 @@ + + + diff --git a/apps/frontend/src/services/moderation-checklist-storage.ts b/apps/frontend/src/services/moderation-checklist-storage.ts index 1873b8d3e2..19f5018b81 100644 --- a/apps/frontend/src/services/moderation-checklist-storage.ts +++ b/apps/frontend/src/services/moderation-checklist-storage.ts @@ -1,14 +1,14 @@ -import type { ActionState } from '@modrinth/moderation' +import type { NodeState } from '@modrinth/moderation' import { dbDelete, dbGet, dbPut, dbScan } from './moderation-db.ts' export interface PersistedChecklistState { savedAt: string open?: boolean - stage: number + reviewAnyway?: boolean + stage: string message: string | null - actionStates: Record - textInputs: Record + state: Record> } const STORE = 'checklist' @@ -22,10 +22,9 @@ function isPersistedChecklistState(value: unknown): value is PersistedChecklistS if (!value || typeof value !== 'object') return false const v = value as PersistedChecklistState if (typeof v.savedAt !== 'string') return false - if (typeof v.stage !== 'number' || !Number.isFinite(v.stage)) return false + if (typeof v.stage !== 'string') return false if (v.message !== null && typeof v.message !== 'string') return false - if (!v.actionStates || typeof v.actionStates !== 'object') return false - if (!v.textInputs || typeof v.textInputs !== 'object') return false + if (!v.state || typeof v.state !== 'object') return false return true } @@ -37,7 +36,7 @@ function isStale(savedAt: string, now = Date.now()): boolean { async function cleanupStaleStates(now = Date.now()): Promise { const entries = await dbScan(STORE) const staleKeys = entries - .filter(({ key, value }) => { + .filter(({ value }) => { if (!isPersistedChecklistState(value)) return false return isStale(value.savedAt, now) }) @@ -115,4 +114,3 @@ export async function clearChecklistState(projectId: string): Promise { console.debug('Failed to clear checklist state from IndexedDB:', error) } } - diff --git a/packages/moderation/src/data/checklist.ts b/packages/moderation/src/data/checklist.ts index 8b8bb5d689..343fd6a47b 100644 --- a/packages/moderation/src/data/checklist.ts +++ b/packages/moderation/src/data/checklist.ts @@ -1,36 +1,11 @@ -import type { Stage } from '../types/stage' -import categories from './stages/categories' +import type { ChecklistStage } from '../types/node' import description from './stages/description' -import environment from './stages/environment/environment' -import environmentMultiple from './stages/environment/environment-multiple' import gallery from './stages/gallery' import license from './stages/license' import links from './stages/links' -import permissions from './stages/permissions' -import postApproval from './stages/post-approval' import reupload from './stages/reupload' -import ruleFollowing from './stages/rule-following' -import statusAlerts from './stages/status-alerts' import summary from './stages/summary' import titleSlug from './stages/title-slug' -import undefinedProject from './stages/undefined-project' import versions from './stages/versions' -export default [ - titleSlug, - summary, - description, - links, - license, - categories, - environment, - environmentMultiple, - gallery, - versions, - reupload, - permissions, - ruleFollowing, - statusAlerts, - undefinedProject, - postApproval, -] as ReadonlyArray +export default [titleSlug, summary, description, links, license, gallery, versions, reupload] as ReadonlyArray diff --git a/packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient.md b/packages/moderation/src/data/messages/checklist-messages/description/insufficient/custom.md similarity index 100% rename from packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient.md rename to packages/moderation/src/data/messages/checklist-messages/description/insufficient/custom.md diff --git a/packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-fork.md b/packages/moderation/src/data/messages/checklist-messages/description/insufficient/fork.md similarity index 100% rename from packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-fork.md rename to packages/moderation/src/data/messages/checklist-messages/description/insufficient/fork.md diff --git a/packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-header.md b/packages/moderation/src/data/messages/checklist-messages/description/insufficient/header.md similarity index 100% rename from packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-header.md rename to packages/moderation/src/data/messages/checklist-messages/description/insufficient/header.md diff --git a/packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-port.md b/packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-port.md deleted file mode 100644 index 654fb871a8..0000000000 --- a/packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-port.md +++ /dev/null @@ -1 +0,0 @@ -Since your project is a port, you should briefly describe the function or features of the %PROJECT_TYPE% your project is ported from, in addition to what changes you've made in your version. diff --git a/packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-packs.md b/packages/moderation/src/data/messages/checklist-messages/description/insufficient/packs.md similarity index 100% rename from packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-packs.md rename to packages/moderation/src/data/messages/checklist-messages/description/insufficient/packs.md diff --git a/packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-projects.md b/packages/moderation/src/data/messages/checklist-messages/description/insufficient/projects.md similarity index 100% rename from packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-projects.md rename to packages/moderation/src/data/messages/checklist-messages/description/insufficient/projects.md diff --git a/packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-servers.md b/packages/moderation/src/data/messages/checklist-messages/description/insufficient/servers.md similarity index 100% rename from packages/moderation/src/data/messages/checklist-messages/description/insufficient/insufficient-servers.md rename to packages/moderation/src/data/messages/checklist-messages/description/insufficient/servers.md diff --git a/packages/moderation/src/data/messages/checklist-messages/links/discord/inaccessible.md b/packages/moderation/src/data/messages/checklist-messages/links/discord/inaccessible.md new file mode 100644 index 0000000000..c38dc19e24 --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/discord/inaccessible.md @@ -0,0 +1 @@ +Currently, your Discord link directs to an invalid invite, likely because your invite has expired. Make sure to set your invite link to permanent with unlimited uses before resubmitting your project. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/discord/misused.md b/packages/moderation/src/data/messages/checklist-messages/links/discord/misused.md new file mode 100644 index 0000000000..fcc47ccd1f --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/discord/misused.md @@ -0,0 +1 @@ +Per section 5.4 of %RULES%, all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project. Currently, your **Discord** link appears to be misused or incorrectly labeled. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/donations/inaccessible.md b/packages/moderation/src/data/messages/checklist-messages/links/donations/inaccessible.md new file mode 100644 index 0000000000..305e547c95 --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/donations/inaccessible.md @@ -0,0 +1 @@ +Currently, your **Donation** link(s) are inaccessible. Make sure they point to publicly accessible donation pages before resubmitting your project. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/donations/misused.md b/packages/moderation/src/data/messages/checklist-messages/links/donations/misused.md new file mode 100644 index 0000000000..64cfd11a60 --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/donations/misused.md @@ -0,0 +1 @@ +Per section 5.4 of %RULES%, all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project. Currently, your **Donation** link(s) appear to be misused or incorrectly labeled. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/issues/inaccessible.md b/packages/moderation/src/data/messages/checklist-messages/links/issues/inaccessible.md new file mode 100644 index 0000000000..9cf440d219 --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/issues/inaccessible.md @@ -0,0 +1 @@ +Currently, your **Issues** link is inaccessible. Make sure it points to a publicly accessible issue tracker before resubmitting your project. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/issues/misused.md b/packages/moderation/src/data/messages/checklist-messages/links/issues/misused.md new file mode 100644 index 0000000000..d1ef5b2d52 --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/issues/misused.md @@ -0,0 +1 @@ +Per section 5.4 of %RULES%, all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project. Currently, your **Issues** link appears to be misused or incorrectly labeled. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/site/inaccessible.md b/packages/moderation/src/data/messages/checklist-messages/links/site/inaccessible.md new file mode 100644 index 0000000000..c01fda4c41 --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/site/inaccessible.md @@ -0,0 +1 @@ +Currently, your **Website** link is inaccessible. Make sure it points to a publicly accessible project website before resubmitting your project. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/site/misused.md b/packages/moderation/src/data/messages/checklist-messages/links/site/misused.md new file mode 100644 index 0000000000..42e0e0a6a8 --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/site/misused.md @@ -0,0 +1 @@ +Per section 5.4 of %RULES%, all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project. Currently, your **Website** link appears to be misused or incorrectly labeled. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/source/inaccessible.md b/packages/moderation/src/data/messages/checklist-messages/links/source/inaccessible.md new file mode 100644 index 0000000000..2cf8f2da46 --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/source/inaccessible.md @@ -0,0 +1 @@ +Currently, your Source link directs to a Page Not Found error, likely because your repository is private. Make sure to set your repository to public before resubmitting your project. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/source/misused.md b/packages/moderation/src/data/messages/checklist-messages/links/source/misused.md new file mode 100644 index 0000000000..daa212743c --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/source/misused.md @@ -0,0 +1 @@ +Per section 5.4 of %RULES%, all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project. Currently, your **Source** link appears to be misused or incorrectly labeled. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/store/inaccessible.md b/packages/moderation/src/data/messages/checklist-messages/links/store/inaccessible.md new file mode 100644 index 0000000000..6be832c011 --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/store/inaccessible.md @@ -0,0 +1 @@ +Currently, your **Store** link is inaccessible. Make sure it points to a publicly accessible storefront before resubmitting your project. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/store/misused.md b/packages/moderation/src/data/messages/checklist-messages/links/store/misused.md new file mode 100644 index 0000000000..460fcb8a1f --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/store/misused.md @@ -0,0 +1 @@ +Per section 5.4 of %RULES%, all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project. Currently, your **Store** link appears to be misused or incorrectly labeled. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/wiki/inaccessible.md b/packages/moderation/src/data/messages/checklist-messages/links/wiki/inaccessible.md new file mode 100644 index 0000000000..7b9b5835d5 --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/wiki/inaccessible.md @@ -0,0 +1 @@ +Currently, your **Wiki** link is inaccessible. Make sure it points to a publicly accessible documentation page before resubmitting your project. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/checklist-messages/links/wiki/misused.md b/packages/moderation/src/data/messages/checklist-messages/links/wiki/misused.md new file mode 100644 index 0000000000..1cb1a8d6af --- /dev/null +++ b/packages/moderation/src/data/messages/checklist-messages/links/wiki/misused.md @@ -0,0 +1 @@ +Per section 5.4 of %RULES%, all %PROJECT_LINKS_FLINK% must lead to correctly labeled publicly available resources that are directly related to your project. Currently, your **Wiki** link appears to be misused or incorrectly labeled. \ No newline at end of file diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content-header.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content-header.md deleted file mode 100644 index 11d70d2a79..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content-header.md +++ /dev/null @@ -1,2 +0,0 @@ -## Prohibited Content -Per section 1 of [Modrinth's Content Rules](https://modrinth.com/legal/rules), this project contains the following prohibited content: diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md deleted file mode 100644 index 2ae4a60015..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/discriminatory.md +++ /dev/null @@ -1 +0,0 @@ -- 1.2: Sexually explicit or pornographic material, or content promoting violence or discrimination based on race, sex, gender, religion, nationality, disability, sexual orientation, or age diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md deleted file mode 100644 index ac6fbde059..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/false-endorsement.md +++ /dev/null @@ -1 +0,0 @@ -- 1.9: Content giving a false impression of endorsement by Modrinth or any other person or entity diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md deleted file mode 100644 index 9ddaea6e8e..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/harmful.md +++ /dev/null @@ -1 +0,0 @@ -- 1.6: Content likely to cause annoyance, harm, embarrassment, alarm, or deception to others diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md deleted file mode 100644 index aaf276c50b..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/illegal-activity.md +++ /dev/null @@ -1 +0,0 @@ -- 1.5: Promotion of illegal activity, or advocacy of unlawful acts including real-life drugs or illicit substances diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md deleted file mode 100644 index ab3be29e7c..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/impersonation.md +++ /dev/null @@ -1 +0,0 @@ -- 1.8: Impersonation of a person, or misrepresentation of identity or affiliation with any person or organization diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md deleted file mode 100644 index 0eee5443a1..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/ip-infringement.md +++ /dev/null @@ -1 +0,0 @@ -- 1.3: Content infringing patents, trademarks, trade secrets, copyright, or other intellectual property rights diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md deleted file mode 100644 index 749304631d..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/legal-rights.md +++ /dev/null @@ -1 +0,0 @@ -- 1.4: Content violating the legal rights of others diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md deleted file mode 100644 index 005d8d88fb..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/misleading.md +++ /dev/null @@ -1 +0,0 @@ -- 1.7: Intentionally wrong or misleading claims diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md deleted file mode 100644 index 3706c42442..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/mojang-bypass.md +++ /dev/null @@ -1 +0,0 @@ -- 1.12: Content bypassing restrictions placed by Mojang to prevent users from joining certain in-game servers diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md deleted file mode 100644 index 02cc641afb..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/objectionable.md +++ /dev/null @@ -1 +0,0 @@ -- 1.1: Defamatory, obscene, indecent, abusive, offensive, harassing, violent, hateful, inflammatory, or otherwise objectionable material diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md deleted file mode 100644 index 1e42162eea..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/profanity.md +++ /dev/null @@ -1 +0,0 @@ -- 1.10: Excessive profanity diff --git a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md b/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md deleted file mode 100644 index 4b0967feab..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/prohibited-content/undisclosed-upload.md +++ /dev/null @@ -1 +0,0 @@ -- 1.11: Undisclosed upload of data to a remote server without clear disclosure diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in-header.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in-header.md deleted file mode 100644 index 906030e71a..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in-header.md +++ /dev/null @@ -1,2 +0,0 @@ -## Server-side opt-in required -Per section 3.3 of [Modrinth's Content Rules](https://modrinth.com/legal/rules), the following feature(s) in this project require a server-side opt-in: diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md deleted file mode 100644 index 16f6e2644e..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/aim-bot.md +++ /dev/null @@ -1 +0,0 @@ -- 3.3b: Aim bot or aim assist diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md deleted file mode 100644 index a4d4ad5d52..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/hiding-mods.md +++ /dev/null @@ -1 +0,0 @@ -- 3.3e: Active client-side hiding of third-party modifications that have server-side opt-outs diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md deleted file mode 100644 index feae0986ec..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/item-duplication.md +++ /dev/null @@ -1 +0,0 @@ -- 3.3f: Item duplication diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md deleted file mode 100644 index 8422514558..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/movement.md +++ /dev/null @@ -1 +0,0 @@ -- 3.3c: Flight, speed, or other movement modifications diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md deleted file mode 100644 index d1cc5099c4..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/pvp.md +++ /dev/null @@ -1 +0,0 @@ -- 3.3d: Automatic or assisted PvP combat diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md deleted file mode 100644 index a58d28ddd9..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-in/x-ray.md +++ /dev/null @@ -1 +0,0 @@ -- 3.3a: X-ray or the ability to see through opaque blocks diff --git a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-out.md b/packages/moderation/src/data/messages/rule-breaking/server-side-opt-out.md deleted file mode 100644 index 1479d7312d..0000000000 --- a/packages/moderation/src/data/messages/rule-breaking/server-side-opt-out.md +++ /dev/null @@ -1,2 +0,0 @@ -## Server-side opt-out required -Per section 3.2 of [Modrinth's Content Rules](https://modrinth.com/legal/rules), mods that give an unfair advantage in a multiplayer setting over other players that do not have a comparable modification must have a server-side opt-out. diff --git a/packages/moderation/src/data/modpack-permissions-stage.ts b/packages/moderation/src/data/modpack-permissions-stage.ts deleted file mode 100644 index 8b9f3ef2b4..0000000000 --- a/packages/moderation/src/data/modpack-permissions-stage.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Labrinth } from '@modrinth/api-client' -import { PackageOpenIcon } from '@modrinth/assets' -import type { ModerationModpackPermissionApprovalType } from '@modrinth/utils' - -import type { Stage } from '../types/stage' - -export default { - id: 'modpack-permissions', - hint: 'Modpack Permissions', - icon: PackageOpenIcon, - // Replace me please. - guidance_url: - 'https://www.notion.so/Content-Moderation-Cheat-Sheets-22d5ee711bf081a4920ef08879fe6bf5?source=copy_link#22d5ee711bf08116bd8bc1186f357062', - shouldShow: (project: Labrinth.Projects.v2.Project, projectV3) => - project.project_type === 'modpack' && !projectV3?.minecraft_server, - actions: [ - { - id: 'button', - type: 'button', - label: 'This dummy button must be present or the stage will not appear.', - }, - ], -} as Stage - -export const finalPermissionMessages: Record< - ModerationModpackPermissionApprovalType['id'], - string | undefined -> = { - yes: undefined, - 'with-attribution-and-source': undefined, - 'with-attribution': `The following content has attribution requirements, meaning that you must link back to the page where you originally found this content in your Modpack's description or version changelog (e.g. linking a mod's CurseForge page if you got it from CurseForge):`, - no: 'The following content is not allowed in Modrinth modpacks due to licensing restrictions. Please contact the author(s) directly for permission or remove the content from your modpack:', - 'permanent-no': `The following content is not allowed in Modrinth modpacks, regardless of permission obtained. This may be because it breaks Modrinth's content rules or because the authors, upon being contacted for permission, have declined. Please remove the content from your modpack:`, - unidentified: `The following content could not be identified. Please provide proof of its origin along with proof that you have permission to include it:`, -} diff --git a/packages/moderation/src/data/stages/description.ts b/packages/moderation/src/data/stages/description.ts index e9411f48c0..09ff0e9a76 100644 --- a/packages/moderation/src/data/stages/description.ts +++ b/packages/moderation/src/data/stages/description.ts @@ -1,190 +1,90 @@ import { LibraryIcon } from '@modrinth/assets' -import type { ButtonAction, MultiSelectChipsAction } from '../../types/actions' -import type { Stage } from '../../types/stage' +import { button, group, markdown, mdMsg, select, stage, toggle } from '../../types/node' -const description: Stage = { - title: "Description", - hint: 'Is the description sufficient, accurate, and accessible?', - id: 'description', - icon: LibraryIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf080508042e70089dd787e', - navigate: '/', - actions: [ - { - id: 'description_insufficient', - type: 'button', - label: 'Insufficient', - weight: 400, - suggestedStatus: 'flagged', - severity: 'medium', - message: async () => - ( - await import('../messages/checklist-messages/description/insufficient/insufficient-header.md?raw') - ).default, - enablesActions: [ - { - id: 'description_insufficient_options', - type: 'multi-select-chips', - label: 'Which details are missing from the description?', - options: [ - { - id: 'description_insufficient_packs', - label: 'Missing basic details (modpacks)', - weight: 401, - shouldShow: (project, projectV3) => - project.project_type === 'modpack' && !projectV3?.minecraft_server, - message: async () => - ( - await import('../messages/checklist-messages/description/insufficient/insufficient-packs.md?raw') - ).default, - }, - { - id: 'description_insufficient_projects', - label: 'Missing basic details', - weight: 402, - shouldShow: (project, projectV3) => - project.project_type !== 'modpack' && !projectV3?.minecraft_server, - message: async () => - ( - await import('../messages/checklist-messages/description/insufficient/insufficient-projects.md?raw') - ).default, - }, - { - id: 'description_insufficient_servers', - label: 'Missing basic details', - weight: 403, - shouldShow: (project, projectV3) => !!projectV3?.minecraft_java_server, - message: async () => - ( - await import('../messages/checklist-messages/description/insufficient/insufficient-servers.md?raw') - ).default, - }, - { - id: 'description_insufficient_fork', - label: 'Fork', - weight: 404, - message: async () => - ( - await import('../messages/checklist-messages/description/insufficient/insufficient-fork.md?raw') - ).default, - }, - { - id: 'description_insufficient_port', - label: 'Port', - weight: 405, - message: async () => - ( - await import('../messages/checklist-messages/description/insufficient/insufficient-port.md?raw') - ).default, - }, - ], - } as MultiSelectChipsAction, - { - id: 'description_insufficient_custom', - type: 'button', - label: 'Insufficient (custom)', - weight: 406, - suggestedStatus: 'flagged', - severity: 'medium', - message: async () => - ( - await import('../messages/checklist-messages/description/insufficient/insufficient.md?raw') - ).default, - relevantExtraInput: [ - { - label: 'Please elaborate on how the author can improve their description.', - variable: 'EXPLAINER', - large: true, - required: true, - }, - ], - } as ButtonAction, - ], - } as ButtonAction, - { - id: 'description_non_english', - type: 'button', - label: 'Non-english', - weight: 402, - suggestedStatus: 'flagged', - severity: 'medium', - shouldShow: (project, projectV3) => !projectV3?.minecraft_java_server, - message: async () => - ( - await import('../messages/checklist-messages/description/accessability/non-english/non-english.md?raw') - ).default, - } as ButtonAction, - { - id: 'description_non_english-server', - type: 'button', - label: 'Non-english', - weight: 402, - suggestedStatus: 'flagged', - severity: 'medium', - shouldShow: (project, projectV3) => !!projectV3?.minecraft_java_server, - message: async () => - ( - await import('../messages/checklist-messages/description/accessability/non-english/non-english-server.md?raw') - ).default, - } as ButtonAction, - { - id: 'description_unfinished', - type: 'button', - label: 'Unfinished', - weight: 403, - suggestedStatus: 'flagged', - severity: 'low', - message: async () => - (await import('../messages/checklist-messages/description/unfinished.md?raw')).default, - } as ButtonAction, - { - id: 'description_headers_as_body', - type: 'button', - label: 'Headers as body text', - weight: 404, - suggestedStatus: 'flagged', - severity: 'low', - message: async () => - ( - await import('../messages/checklist-messages/description/accessability/headers-as-body.md?raw') - ).default, - } as ButtonAction, - { - id: 'description_image_only', - type: 'button', - label: 'Image-only', - weight: 405, - suggestedStatus: 'flagged', - severity: 'medium', - message: async () => - (await import('../messages/checklist-messages/description/accessability/image-only.md?raw')) - .default, - } as ButtonAction, - { - id: 'description_non_standard_text', - type: 'button', - label: 'Non-standard text', - weight: 406, - suggestedStatus: 'flagged', - severity: 'medium', - message: async () => - ( - await import('../messages/checklist-messages/description/accessability/non-standard-text.md?raw') - ).default, - } as ButtonAction, - { - id: 'description_clarity', - type: 'button', - label: 'Unclear / Misleading', - weight: 407, - suggestedStatus: 'rejected', - severity: 'high', - message: async () => - (await import('../messages/checklist-messages/description/clarity.md?raw')).default, - } as ButtonAction, - ], -} +export default stage( + 'description', + 'Description', + 'Is the description sufficient, accurate, and accessible?', + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf080508042e70089dd787e', + [ + group().children( + button('insufficient', 'Insufficient') + .weight(400) + .suggestedStatus('flagged') + .severity('medium') + .message(async (ctx) => { + const reason = ctx?.state.reason as string | undefined + if (reason === 'custom') { + return mdMsg('description/insufficient/custom', (c) => ({ + EXPLAINER: c.state.explainer, + }))(ctx) + } + if (reason === 'fork') { + const header = await mdMsg('description/insufficient/header')(ctx) + const detail = await mdMsg('description/insufficient/fork')(ctx) + return `${header}\n\n${detail}` + } + return mdMsg( + `description/insufficient/${ctx?.project?.minecraft_java_server ? 'servers' : ctx?.project?.project_types?.includes('modpack') ? 'packs' : 'projects'}`, + )(ctx) + }) + .children( + select('reason', 'Specific reason?').children( + toggle('fork', 'Fork'), + toggle('custom', 'Custom').children( + markdown( + 'explainer', + 'Please elaborate on how the author can improve their description.', + ).required(), + ), + ), + ), + + button('non_english', 'Non-english') + .shown(({ project }) => !project.minecraft_java_server) + .weight(402) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('description/accessability/non-english/non-english')), + + button('non_english_server', 'Non-english') + .shown(({ project }) => !!project.minecraft_java_server) + .weight(402) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('description/accessability/non-english/non-english-server')), + + button('unfinished', 'Unfinished') + .weight(403) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('description/unfinished')), -export default description + button('headers_as_body', 'Headers as body text') + .weight(404) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('description/accessability/headers-as-body')), + + button('image_only', 'Image-only') + .weight(405) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('description/accessability/image-only')), + + button('non_standard_text', 'Non-standard text') + .weight(406) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('description/accessability/non-standard-text')), + + button('clarity', 'Unclear / Misleading') + .weight(407) + .suggestedStatus('rejected') + .severity('high') + .message(mdMsg('description/clarity')), + ), + ], + { icon: LibraryIcon, navigate: '/' }, +) diff --git a/packages/moderation/src/data/stages/gallery.ts b/packages/moderation/src/data/stages/gallery.ts index 0623364dc1..7117d9805f 100644 --- a/packages/moderation/src/data/stages/gallery.ts +++ b/packages/moderation/src/data/stages/gallery.ts @@ -1,40 +1,31 @@ import { ImageIcon } from '@modrinth/assets' -import type { ButtonAction } from '../../types/actions' -import type { Stage } from '../../types/stage' +import { button, group, mdMsg, stage } from '../../types/node' -const gallery: Stage = { - title: "Gallery", - hint: "Are this project's gallery images sufficient?", - id: 'gallery', - icon: ImageIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf08096828bd1c3f24d8b8e', - navigate: '/gallery', - actions: [ - { - id: 'gallery_insufficient', - type: 'button', - label: 'Insufficient', - weight: 900, - suggestedStatus: 'flagged', - severity: 'low', - shouldShow: (project, projectV3) => !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/gallery/insufficient.md?raw')).default, - } as ButtonAction, - { - id: 'gallery_not_relevant', - type: 'button', - label: 'Not relevant', - weight: 901, - suggestedStatus: 'flagged', - severity: 'low', - shouldShow: (project) => project.gallery && project.gallery.length > 0, - message: async () => - (await import('../messages/checklist-messages/gallery/not-relevant.md?raw')).default, - } as ButtonAction, - ], -} +export default stage( + 'gallery', + 'Gallery', + "Are this project's gallery images sufficient?", + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf08096828bd1c3f24d8b8e', + [ + group().children( + button('insufficient', 'Insufficient') + .shown(({ project }) => !project.minecraft_server) + .weight(900) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('gallery/insufficient')), -export default gallery + button('not_relevant', 'Not relevant') + .shown(({ project }) => project.gallery.length > 0) + .weight(901) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('gallery/not-relevant')), + ), + ], + { + icon: ImageIcon, + navigate: '/gallery', + }, +) diff --git a/packages/moderation/src/data/stages/license.ts b/packages/moderation/src/data/stages/license.ts index 7bc9417540..9c960aee6d 100644 --- a/packages/moderation/src/data/stages/license.ts +++ b/packages/moderation/src/data/stages/license.ts @@ -1,6 +1,6 @@ import { BookTextIcon } from '@modrinth/assets' -import type { Stage } from '../../types/stage' +import { button, group, mdMsg, mdText, prose, stage, toggle } from '../../types/node' const licensesNotRequiringSource: string[] = [ 'LicenseRef-All-Rights-Reserved', @@ -19,77 +19,44 @@ const licensesNotRequiringSource: string[] = [ 'Zlib', ] -const licenseStage: Stage = { - title: 'License', - hint: 'Is this license and link valid?', - text: async () => (await import('../messages/checklist-text/licensing.md?raw')).default, - id: 'license', - icon: BookTextIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf080f8805df7d012a8f770', - navigate: '/settings/license', - shouldShow(project, projectV3) { - return !projectV3?.minecraft_server - }, - actions: [ - { - id: 'license_invalid_link', - type: 'button', - label: 'Invalid Link', - weight: 600, - suggestedStatus: 'flagged', - severity: 'medium', - shouldShow: (project) => Boolean(project.license.url), - message: async () => - (await import('../messages/checklist-messages/license/invalid_link.md?raw')).default, - enablesActions: [ - { - id: 'license_invalid_link-custom_license', - type: 'toggle', - label: 'Invalid Link: Custom License', - weight: 601, - suggestedStatus: 'flagged', - severity: 'medium', - message: async () => - ( - await import('../messages/checklist-messages/license/invalid_link-custom_license.md?raw') - ).default, - }, - ], - }, - { - id: 'license_no_source', - type: 'conditional-button', - label: 'No Source', - suggestedStatus: 'rejected', - severity: 'medium', - shouldShow: (project) => !licensesNotRequiringSource.includes(project.license.id), - messageVariants: [ - { - conditions: { - excludedActions: ['license_no_source-fork'], - }, - weight: 602, - message: async () => - (await import('../messages/checklist-messages/license/no_source.md?raw')).default, - }, - ], - fallbackWeight: 602, - fallbackMessage: async () => '', - enablesActions: [ - { - id: 'license_no_source-fork', - type: 'toggle', - label: 'No Source: Fork', - weight: 602, - suggestedStatus: 'rejected', - severity: 'high', - message: async () => - (await import('../messages/checklist-messages/license/no_source-fork.md?raw')).default, - }, - ], - }, - ], -} +export default stage( + 'license', + 'License', + 'Is this license and link valid?', + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf080f8805df7d012a8f770', + [ + prose(mdText('licensing')), + + group().children( + button('invalid_link', 'Invalid Link') + .shown(({ project }) => !!project.license?.url) + .weight(600) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('license/invalid_link')) + .children( + toggle('custom_license', 'Invalid Link: Custom License') + .weight(601) + .message(mdMsg('license/invalid_link-custom_license')), + ), -export default licenseStage + button('no_source', 'No Source') + .shown(({ project }) => !licensesNotRequiringSource.includes(project.license?.id ?? '')) + .weight(602) + .suggestedStatus('rejected') + .severity('medium') + .message(async (ctx) => { + if (ctx.state.fork) return mdMsg('license/no_source-fork')(ctx) + return mdMsg('license/no_source')(ctx) + }) + .children( + toggle('fork', 'No Source: Fork').severity('high'), + ), + ), + ], + { + icon: BookTextIcon, + navigate: '/settings/license', + shown: (_project, projectV3) => !projectV3?.minecraft_server, + }, +) diff --git a/packages/moderation/src/data/stages/links.ts b/packages/moderation/src/data/stages/links.ts index 5a1b835de0..ca0c6ae426 100644 --- a/packages/moderation/src/data/stages/links.ts +++ b/packages/moderation/src/data/stages/links.ts @@ -1,204 +1,82 @@ import { LinkIcon } from '@modrinth/assets' -import type { - ButtonAction, - ChecklistActionContext, - MultiSelectChipsAction, - MultiSelectChipsOption, -} from '../../types/actions' -import type { Stage } from '../../types/stage' - -interface LinkOptionDefinition { - id: string - label: string - weight: number - message: () => Promise -} - -function formatLabel(value: string): string { - return value - .split(/[-_]/g) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') -} - -function getProjectLinkDefinitions(context: ChecklistActionContext): LinkOptionDefinition[] { - const { project, projectV3 } = context - const donationUrls = project.donation_urls ?? [] - const links: LinkOptionDefinition[] = [] - let weight = 511 - - const pushLink = (id: string, label: string, message: () => Promise) => { - links.push({ id, label, weight, message }) - weight += 1 - } - - if (project.issues_url) { - pushLink( - 'issues', - 'Issues', - async () => - 'Currently, your Issues link is inaccessible. Make sure it points to a publicly accessible issue tracker before resubmitting your project.', - ) - } - - if (project.source_url) { - pushLink( - 'source', - 'Source', - async () => - (await import('../messages/checklist-messages/links/not_accessible-source.md?raw')).default, - ) - } - - if (project.wiki_url) { - pushLink( - 'wiki', - 'Wiki', - async () => - 'Currently, your Wiki link is inaccessible. Make sure it points to a publicly accessible documentation page before resubmitting your project.', - ) - } - - if (project.discord_url) { - pushLink( - 'discord', - 'Discord', - async () => - (await import('../messages/checklist-messages/links/not_accessible-discord.md?raw')) - .default, - ) - } - - if (projectV3?.link_urls?.site?.url) { - pushLink( - 'site', - 'Website', - async () => - 'Currently, your Website link is inaccessible. Make sure it points to a publicly accessible project website before resubmitting your project.', +import type { ContentFn } from '../../types/node' +import { button, group, mdEscape, mdMsg, prose, stage } from '../../types/node' + +function linkSection(id: string, urlLine: ContentFn, baseWeight: number) { + return group(id) + .column() + .children( + prose(urlLine), + group().children( + button('misused', 'Misused') + .weight(baseWeight) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg(`links/${id}/misused`)), + button('inaccessible', 'Inaccessible') + .weight(baseWeight + 1) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg(`links/${id}/inaccessible`)), + ), ) - } - - if (projectV3?.link_urls?.store?.url) { - pushLink( - 'store', - 'Store', - async () => - 'Currently, your Store link is inaccessible. Make sure it points to a publicly accessible storefront before resubmitting your project.', - ) - } - - donationUrls.forEach((donation, index) => { - const donationLabel = `${formatLabel(donation.platform)} Donation` - pushLink( - `donation-${donation.id}-${index}`, - donationLabel, - async () => - `Currently, your ${donationLabel} link is inaccessible. Make sure it points to a publicly accessible donation page before resubmitting your project.`, - ) - }) - - return links -} - -function getMisusedLinkOptions(context: ChecklistActionContext): MultiSelectChipsOption[] { - return getProjectLinkDefinitions(context).map((link, index) => ({ - id: link.id, - label: link.label, - weight: 501 + index, - message: async () => `- ${link.label}`, - })) -} - -function getInaccessibleLinkOptions(context: ChecklistActionContext): MultiSelectChipsOption[] { - return getProjectLinkDefinitions(context).map((link) => ({ - id: link.id, - label: link.label, - weight: link.weight, - message: link.message, - })) } -const links: Stage = { - title: 'Links', - hint: "Are the project's links accurate and accessible?", - id: 'links', - icon: LinkIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf08013b36cd75cbf1a9177', - navigate: '/settings/links', - shouldShow: (project, projectV3) => - Boolean( - project.issues_url || - project.source_url || - project.wiki_url || - project.discord_url || - projectV3?.link_urls?.site || - projectV3?.link_urls?.store || - (project.donation_urls?.length ?? 0) > 0, - ), - text: async (project, projectV3) => { - let text - if (projectV3?.minecraft_server) - text = (await import('../messages/checklist-text/links/server.md?raw')).default - else text = (await import('../messages/checklist-text/links/base.md?raw')).default - - if ((project.donation_urls?.length ?? 0) > 0) { - text += (await import('../messages/checklist-text/links/donation/donations.md?raw')).default - - for (const donation of project.donation_urls ?? []) { - text += (await import(`../messages/checklist-text/links/donation/donation.md?raw`)).default - .replace('{URL}', donation.url) - .replace('{PLATFORM}', donation.platform) - } - } - - return text - }, - actions: [ - { - id: 'links_misused', - type: 'button', - label: 'Links are misused', - weight: 500, - suggestedStatus: 'flagged', - severity: 'low', - message: async () => - (await import('../messages/checklist-messages/links/misused.md?raw')).default, - enablesActions: [ - { - id: 'links_misused_options', - type: 'multi-select-chips', - label: 'Which links are misused?', - joinWith: '\n', - shouldShow: (_project, _projectV3, context) => - Boolean(context && getMisusedLinkOptions(context).length > 0), - options: (context) => getMisusedLinkOptions(context), - }, - ] as MultiSelectChipsAction[], - } as ButtonAction, - { - id: 'links_inaccessible', - type: 'button', - label: 'Links are inaccessible', - weight: 510, - suggestedStatus: 'flagged', - // Theoretically a conditional could go here to prevent overlap of misuse and inaccessible messages repeating while still allowing for a multi-select in each. - // if links_misused was selected, send nothing. - message: async () => - (await import('../messages/checklist-messages/links/not_accessible.md?raw')).default, - enablesActions: [ - { - id: 'links_inaccessible_options', - type: 'multi-select-chips', - label: 'Warn of inaccessible link?', - shouldShow: (_project, _projectV3, context) => - Boolean(context && getInaccessibleLinkOptions(context).length > 0), - options: (context) => getInaccessibleLinkOptions(context), - } as MultiSelectChipsAction, - ], - } as ButtonAction, +export default stage( + 'links', + 'Links', + "Are the project's links accurate and accessible?", + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf08013b36cd75cbf1a9177', + [ + linkSection('issues', ({ project }) => `**Issues:** ${mdEscape(project.link_urls.issues?.url ?? '')}`, 500) + .shown(({ project }) => !!project.link_urls.issues?.url), + + linkSection('source', ({ project }) => `**Source:** ${mdEscape(project.link_urls.source?.url ?? '')}`, 510) + .shown(({ project }) => !!project.link_urls.source?.url), + + linkSection('wiki', ({ project }) => `**Wiki:** ${mdEscape(project.link_urls.wiki?.url ?? '')}`, 520) + .shown(({ project }) => !!project.link_urls.wiki?.url), + + linkSection('discord', ({ project }) => `**Discord:** ${mdEscape(project.link_urls.discord?.url ?? '')}`, 530) + .shown(({ project }) => !!project.link_urls.discord?.url), + + linkSection('site', ({ project }) => `**Website:** ${mdEscape(project.link_urls.site?.url ?? '')}`, 540) + .shown(({ project }) => !!project.link_urls.site?.url), + + linkSection('store', ({ project }) => `**Store:** ${mdEscape(project.link_urls.store?.url ?? '')}`, 550) + .shown(({ project }) => !!project.link_urls.store?.url), + + group('donations') + .column() + .shown(({ project }) => Object.values(project.link_urls).some((l) => l.donation)) + .children( + prose(({ project }) => + Object.values(project.link_urls) + .filter((l) => l.donation) + .map((l) => `**${mdEscape(l.platform)}:** ${mdEscape(l.url)}`) + .join(' \\\n'), + ), + group().children( + button('misused', 'Misused') + .weight(560) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('links/donations/misused')), + button('inaccessible', 'Inaccessible') + .weight(561) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('links/donations/inaccessible')), + ), + ), ], -} - -export default links + { + icon: LinkIcon, + navigate: '/settings/links', + shown: (_project, projectV3) => + Boolean( + projectV3 && Object.keys(projectV3.link_urls).length > 0, + ), + }, +) diff --git a/packages/moderation/src/data/stages/reupload.ts b/packages/moderation/src/data/stages/reupload.ts index 8cacfc5032..15b5a11a81 100644 --- a/packages/moderation/src/data/stages/reupload.ts +++ b/packages/moderation/src/data/stages/reupload.ts @@ -1,259 +1,122 @@ import { CopyrightIcon } from '@modrinth/assets' -import type { ButtonAction, ToggleAction } from '../../types/actions' -import type { Stage } from '../../types/stage' +import type { NodeContext } from '../../types/node' +import { button, group, markdown, mdMsg, stage, text, toggle } from '../../types/node' -const reupload: Stage = { - title: 'Reupload', - hint: 'Does the author have proper permissions to post this project?', - id: 'reupload', - icon: CopyrightIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080d1a0a2cda3ff2ce997', - actions: [ - { - id: 'reupload_reupload', - type: 'button', - label: 'Re-upload', - weight: 1100, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/reupload/reupload.md?raw')).default, - disablesActions: [ - 'reupload_unclear_fork', - 'reupload_insufficient_fork', - 'reupload_request_proof', - 'reupload_identity_verification', - 'reupload_request_proof_server', - 'reupload_identity_verification_server', - ], - relevantExtraInput: [ - { - label: 'What is the title of the original project?', - variable: 'ORIGINAL_PROJECT', - required: true, - suggestions: ['Vanilla Tweaks'], - }, - { - label: 'What is the author of the original project?', - variable: 'ORIGINAL_AUTHOR', - required: true, - suggestions: ['Vanilla Tweaks Team'], - }, - ], - } as ButtonAction, - { - id: 'reupload_unclear_fork', - type: 'button', - label: 'Unclear Fork', - weight: 1100, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/reupload/fork.md?raw')).default, - disablesActions: [ - 'reupload_reupload', - 'reupload_insufficient_fork', - 'reupload_request_proof', - 'reupload_identity_verification', - 'reupload_request_proof_server', - 'reupload_identity_verification_server', - ], - } as ButtonAction, - { - id: 'reupload_insufficient_fork', - type: 'button', - label: 'Insufficient Fork', - weight: 1100, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/reupload/insufficient_fork.md?raw')).default, - disablesActions: [ - 'reupload_unclear_fork', - 'reupload_reupload', - 'reupload_request_proof', - 'reupload_identity_verification', - 'reupload_request_proof_server', - 'reupload_identity_verification_server', - ], - } as ButtonAction, - { - id: 'reupload_request_proof', - type: 'button', - label: 'Proof of permissions', - weight: 1100, - suggestedStatus: 'rejected', - severity: 'high', - message: async () => - (await import('../messages/checklist-messages/reupload/proof_of_permissions.md?raw')) - .default, - disablesActions: [ - 'reupload_reupload', - 'reupload_unclear_fork', - 'reupload_insufficient_fork', - 'reupload_identity_verification', - 'reupload_request_proof_server', - 'reupload_identity_verification_server', - ], - }, - { - id: 'reupload_identity_verification', - type: 'button', - label: 'Verify Identity', - weight: 1100, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => !projectV3?.minecraft_server, - message: async () => - ( - await import('../messages/checklist-messages/reupload/identity-verification/identity_verification.md?raw') - ).default, - relevantExtraInput: [ - { - label: 'Where else can the project be found?', - variable: 'PLATFORM', - required: true, - }, - ], - disablesActions: [ - 'reupload_reupload', - 'reupload_insufficient_fork', - 'reupload_request_proof', - 'reupload_request_proof_server', - ], - }, - { - id: 'reupload_identity_verification_server', - type: 'button', - label: 'Verify Identity', - weight: 1100, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => !!projectV3?.minecraft_server, - message: async () => - ( - await import('../messages/checklist-messages/reupload/identity-verification/identity_verification-server.md?raw') - ).default, - relevantExtraInput: [ - { - label: 'known public contact method', - variable: 'CONTACT', - required: true, - }, - ], - disablesActions: [ - 'reupload_reupload', - 'reupload_insufficient_fork', - 'reupload_request_proof', - 'reupload_request_proof_server', - ], - }, - { - id: 'reupload_request_proof_server', - type: 'button', - label: 'Reuploaded pack', - weight: 1100, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => - !!projectV3?.minecraft_server && - projectV3?.minecraft_java_server?.content?.kind === 'modpack' && - projectV3?.minecraft_java_server?.content?.['project_id'] === project.id, - message: async () => - ( - await import('../messages/checklist-messages/reupload/custom_server/custom_server_permissions.md?raw') - ).default, - disablesActions: [ - 'reupload_reupload', - 'reupload_unclear_fork', - 'reupload_insufficient_fork', - 'reupload_identity_verification', - 'reupload_request_proof', - ], - }, - { - id: 'reupload_custom_pack_verification', - type: 'button', - label: 'Override verification', - weight: 1100, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => - !!projectV3?.minecraft_server && - projectV3?.minecraft_java_server?.content?.kind === 'modpack' && - projectV3?.minecraft_java_server?.content?.['project_id'] === project.id, - message: async () => - ( - await import('../messages/checklist-messages/reupload/custom_server/custom_server_overrides-verification.md?raw') - ).default, - enablesActions: [ - { - id: 'reupload_custom_pack_verification-list', - type: 'toggle', - label: 'List overrides?', - weight: 1101, - message: async () => - ( - await import('../messages/checklist-messages/reupload/custom_server/custom_server_overrides-verification-list.md?raw') - ).default, - relevantExtraInput: [ - { - label: 'Add list of overrides.', - variable: 'OVERRIDES', - large: true, - required: false, - }, - ], - } as ToggleAction, - ], - - disablesActions: [ - 'reupload_reupload', - 'reupload_unclear_fork', - 'reupload_insufficient_fork', - 'reupload_identity_verification', - 'reupload_request_proof', - 'reupload_custom_pack_prohibited', - ], - }, - { - id: 'reupload_custom_pack_prohibited', - type: 'button', - label: 'Forbidden Overrides', - weight: 1100, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => - !!projectV3?.minecraft_server && - projectV3?.minecraft_java_server?.content?.kind === 'modpack' && - projectV3?.minecraft_java_server?.content?.['project_id'] === project.id, - message: async () => - ( - await import('../messages/checklist-messages/reupload/custom_server/custom_server_overrides-prohibited.md?raw') - ).default, - relevantExtraInput: [ - { - label: 'Add list of overrides.', - variable: 'OVERRIDES', - large: true, - required: true, - }, - ], - disablesActions: [ - 'reupload_reupload', - 'reupload_unclear_fork', - 'reupload_insufficient_fork', - 'reupload_identity_verification', - 'reupload_request_proof', - 'reupload_custom_pack_verification', - ], - }, - ], +function isServerModpack({ project }: NodeContext): boolean { + return ( + !!project.minecraft_server && + project.minecraft_java_server?.content?.kind === 'modpack' && + (project.minecraft_java_server?.content as Record)?.project_id === project.id + ) } -export default reupload +export default stage( + 'reupload', + 'Reupload', + 'Does the author have proper permissions to post this project?', + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080d1a0a2cda3ff2ce997', + [ + group().children( + button('reupload', 'Re-upload') + .shown(({ project }) => !project.minecraft_server) + .weight(1100) + .suggestedStatus('rejected') + .severity('high') + .message( + mdMsg('reupload/reupload', (ctx) => ({ + ORIGINAL_PROJECT: ctx.state.original_project, + ORIGINAL_AUTHOR: ctx.state.original_author, + })), + ) + .children( + text('original_project', 'Original project title').required(), + text('original_author', 'Original project author').required(), + ), + + button('unclear_fork', 'Unclear Fork') + .shown(({ project }) => !project.minecraft_server) + .weight(1100) + .suggestedStatus('rejected') + .severity('high') + .message(mdMsg('reupload/fork')), + + button('insufficient_fork', 'Insufficient Fork') + .shown(({ project }) => !project.minecraft_server) + .weight(1100) + .suggestedStatus('rejected') + .severity('high') + .message(mdMsg('reupload/insufficient_fork')), + + button('request_proof', 'Proof of permissions') + .weight(1100) + .suggestedStatus('rejected') + .severity('high') + .message(mdMsg('reupload/proof_of_permissions')), + + button('identity_verification', 'Verify Identity') + .shown(({ project }) => !project.minecraft_server) + .weight(1100) + .suggestedStatus('rejected') + .severity('high') + .message( + mdMsg( + 'reupload/identity-verification/identity_verification', + (ctx) => ({ PLATFORM: ctx.state.platform }), + ), + ) + .children(text('platform', 'Where else can the project be found?').required()), + + button('identity_verification_server', 'Verify Identity') + .shown(({ project }) => !!project.minecraft_server) + .weight(1100) + .suggestedStatus('rejected') + .severity('high') + .message( + mdMsg( + 'reupload/identity-verification/identity_verification-server', + (ctx) => ({ CONTACT: ctx.state.contact }), + ), + ) + .children(text('contact', 'Known public contact method').required()), + + button('request_proof_server', 'Reuploaded pack') + .shown(isServerModpack) + .weight(1100) + .suggestedStatus('rejected') + .severity('high') + .message(mdMsg('reupload/custom_server/custom_server_permissions')), + + button('custom_pack_verification', 'Override verification') + .shown(isServerModpack) + .weight(1100) + .suggestedStatus('rejected') + .severity('high') + .message(mdMsg('reupload/custom_server/custom_server_overrides-verification')) + .children( + toggle('list', 'List overrides?') + .weight(1101) + .message( + mdMsg( + 'reupload/custom_server/custom_server_overrides-verification-list', + (ctx) => ({ OVERRIDES: ctx.state.overrides }), + ), + ) + .children(markdown('overrides', 'Add list of overrides.')), + ), + + button('custom_pack_prohibited', 'Forbidden Overrides') + .shown(isServerModpack) + .weight(1100) + .suggestedStatus('rejected') + .severity('high') + .message( + mdMsg('reupload/custom_server/custom_server_overrides-prohibited', (ctx) => ({ + OVERRIDES: ctx.state.overrides, + })), + ) + .children(markdown('overrides', 'Forbidden overrides list').required()), + ), + ], + { icon: CopyrightIcon }, +) diff --git a/packages/moderation/src/data/stages/summary.ts b/packages/moderation/src/data/stages/summary.ts index 16981f135e..eacb7d930a 100644 --- a/packages/moderation/src/data/stages/summary.ts +++ b/packages/moderation/src/data/stages/summary.ts @@ -1,71 +1,49 @@ import { AlignLeftIcon } from '@modrinth/assets' -import type { ButtonAction } from '../../types/actions' -import type { Stage } from '../../types/stage' +import { button, group, mdMsg, mdText, prose, stage } from '../../types/node' -const summary: Stage = { - title: 'Summary', - hint: "Is the project's summary sufficient?", - text: async () => (await import('../messages/checklist-text/summary/summary.md?raw')).default, - id: 'summary', - icon: AlignLeftIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf080bfb5e5c7c6211c693b', - actions: [ - { - id: 'summary_insufficient', - type: 'button', - label: 'Insufficient', - weight: 300, - suggestedStatus: 'flagged', - severity: 'low', - disablesActions: ['summary_repeat_title'], - message: async () => - (await import('../messages/checklist-messages/summary/insufficient.md?raw')).default, - } as ButtonAction, - { - id: 'summary_repeat_title', - type: 'button', - label: 'Repeat of title', - weight: 300, - suggestedStatus: 'flagged', - severity: 'low', - disablesActions: ['summary_insufficient'], - message: async () => - (await import('../messages/checklist-messages/summary/repeat-title.md?raw')).default, - } as ButtonAction, - { - id: 'summary_formatting', - type: 'button', - label: 'Formatting', - weight: 301, - suggestedStatus: 'flagged', - severity: 'low', - message: async () => - (await import('../messages/checklist-messages/summary/formatting.md?raw')).default, - } as ButtonAction, - { - id: 'summary_non_english', - type: 'button', - label: 'Non-english', - weight: 302, - suggestedStatus: 'flagged', - severity: 'medium', - message: async () => - (await import('../messages/checklist-messages/summary/non-english.md?raw')).default, - } as ButtonAction, - { - id: 'summary_repeat_ip', - type: 'button', - label: 'Repeat of IP', - weight: 303, - suggestedStatus: 'flagged', - severity: 'medium', - shouldShow: (project, projectV3) => !!projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/summary/repeat-ip.md?raw')).default, - }, - ], -} +export default stage( + 'summary', + 'Summary', + "Is the project's summary sufficient?", + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf080bfb5e5c7c6211c693b', + [ + prose(mdText('summary/summary')), + + group().children( + button('insufficient', 'Insufficient') + .enabled(({ state }) => !state['repeat_title']) + .weight(300) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('summary/insufficient')), + + button('repeat_title', 'Repeat of Title') + .enabled(({ state }) => !state['insufficient']) + .weight(300) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('summary/repeat-title')), -export default summary + button('formatting', 'Formatting') + .weight(301) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('summary/formatting')), + + button('non_english', 'Non-english') + .weight(302) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('summary/non-english')), + + button('repeat_ip', 'Repeat of IP') + .weight(303) + .suggestedStatus('flagged') + .severity('medium') + .shown(({ project }) => !!project?.minecraft_server) + .message(mdMsg('summary/repeat-ip')), + ), + ], + { icon: AlignLeftIcon }, +) diff --git a/packages/moderation/src/data/stages/title-slug.ts b/packages/moderation/src/data/stages/title-slug.ts index 651114fd88..6e8e64f119 100644 --- a/packages/moderation/src/data/stages/title-slug.ts +++ b/packages/moderation/src/data/stages/title-slug.ts @@ -1,12 +1,12 @@ import type { Labrinth } from '@modrinth/api-client' import { BookOpenIcon } from '@modrinth/assets' -import type { Stage } from '../../types/stage' +import { button, chips, group, mdMsg, mdText, prose, stage, toggle } from '../../types/node' -function hasCustomSlug(project: Labrinth.Projects.v2.Project): boolean { +function hasCustomSlug(project: Labrinth.Projects.v3.Project): boolean { return ( project.slug !== - project.title + project.name .trim() .toLowerCase() .replaceAll(' ', '-') @@ -15,92 +15,65 @@ function hasCustomSlug(project: Labrinth.Projects.v2.Project): boolean { ) } -const titleSlug: Stage = { - title: 'Title & Slug', - hint: 'Are the Name and URL accurate and appropriate?', - id: 'title-&-slug', - text: async (project) => { - let text = (await import('../messages/checklist-text/title-slug/title.md?raw')).default - if (hasCustomSlug(project)) - text += (await import('../messages/checklist-text/title-slug/slug.md?raw')).default - return text - }, - icon: BookOpenIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf0803c9660e90f0fead705', - actions: [ - { - id: 'title_useless_info', - type: 'button', - label: 'Contains useless info', - weight: 100, - suggestedStatus: 'flagged', - severity: 'low', - message: async () => - (await import('../messages/checklist-messages/title/useless-info.md?raw')).default, - }, - { - id: 'title_minecraft_branding', - type: 'button', - label: 'Minecraft title', - weight: 100, - suggestedStatus: 'flagged', - severity: 'medium', - message: async () => - (await import('../messages/checklist-messages/title/minecraft-branding.md?raw')).default, - }, - { - id: 'title_similarities', - type: 'button', - label: 'Title similarities', - weight: 110, - suggestedStatus: 'flagged', - severity: 'medium', - message: async () => - (await import('../messages/checklist-messages/title/similarities.md?raw')).default, - enablesActions: [ - { - id: 'title_similarities_options', - type: 'multi-select-chips', - label: 'Similarities additional info', - options: [ - { - label: 'Modpack named after mod', - weight: 111, - shouldShow: (project) => project.project_type === 'modpack', - message: async () => - (await import('../messages/checklist-messages/title/similarities-modpack.md?raw')) - .default, - }, - { - label: 'Forked project', - weight: 112, - shouldShow: (project, projectV3) => !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/title/similarities-fork.md?raw')) - .default, - }, - ], - }, - ], - }, - { - id: 'slug_misused_options', - type: 'multi-select-chips', - label: 'Slug issues?', - suggestedStatus: 'rejected', - severity: 'low', - shouldShow: (project) => hasCustomSlug(project), - options: [ - { - label: 'Misused', - weight: 200, - message: async () => - (await import('../messages/checklist-messages/slug/misused.md?raw')).default, - }, - ], - }, - ], -} +export default stage( + 'title-slug', + 'Title & Slug', + 'Are the Name and URL accurate and appropriate?', + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf0803c9660e90f0fead705', + [ + prose(async (ctx) => { + const title = await mdText('title-slug/title')(ctx) + if (!hasCustomSlug(ctx.project)) return title + return title + await mdText('title-slug/slug')(ctx) + }), + + group('title').children( + button('useless_info', 'Contains Useless Info') + .weight(100) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('title/useless-info')), + + button('minecraft_branding', 'Minecraft Title') + .weight(100) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('title/minecraft-branding')), -export default titleSlug + button('similarities', 'Title Similarities') + .weight(110) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('title/similarities')) + .children( + chips('options', 'Similarities Additional Info') + .children( + toggle('modpack_named_after_mod', 'Modpack Named After Mod') + .shown(({ project }) => project.project_types.includes('modpack')) + .weight(111) + .message(mdMsg('title/similarities-modpack')), + + toggle('forked_project', 'Forked Project') + .shown(({ project }) => !project?.minecraft_server) + .weight(112) + .message(mdMsg('title/similarities-fork')), + ), + ), + ), + + group('slug') + .column() + .shown(({ project }) => hasCustomSlug(project)) + .children( + chips('options', 'Slug Issues?') + .suggestedStatus('rejected') + .severity('low') + .children( + toggle('misused', 'Misused') + .weight(200) + .message(mdMsg('slug/misused')), + ), + ), + ], + { icon: BookOpenIcon }, +) diff --git a/packages/moderation/src/data/stages/versions.ts b/packages/moderation/src/data/stages/versions.ts index 175fab278d..c93b0aac6e 100644 --- a/packages/moderation/src/data/stages/versions.ts +++ b/packages/moderation/src/data/stages/versions.ts @@ -1,257 +1,140 @@ import { VersionIcon } from '@modrinth/assets' -import type { - ButtonAction, - ChecklistActionContext, - DropdownAction, - DropdownActionOption, - MultiSelectChipsAction, - MultiSelectChipsOption, -} from '../../types/actions' -import type { Stage } from '../../types/stage' +import { button, chips, group, mdMsg, select, stage, text, toggle } from '../../types/node' const loaderLabels: Record = { - datapack: 'Data Pack', - resourcepack: 'Resource Pack', neoforge: 'NeoForge', liteloader: 'LiteLoader', + datapack: 'Data Pack', + resourcepack: 'Resource Pack', } -function formatLoaderLabel(loader: string): string { - if (loaderLabels[loader]) { - return loaderLabels[loader] - } - - return loader +function formatLoaderLabel(id: string): string { + return loaderLabels[id] ?? id .split(/[-_]/g) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(' ') } -function getIncorrectLoaderOptions(context: ChecklistActionContext): MultiSelectChipsOption[] { - const distinctLoaders = [...new Set((context.versions ?? []).flatMap((version) => version.loaders ?? []))] +export default stage( + 'versions', + 'Versions', + "Are this project's files correct?", + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e25ee711bf0804bad38e9055951ff31', + [ + group().children( + button('incorrect_additional', 'Incorrect additional files') + .weight(1000) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('versions/incorrect_additional_files')), - return distinctLoaders - .sort((a, b) => formatLoaderLabel(a).localeCompare(formatLoaderLabel(b))) - .map((loader, index) => ({ - id: loader, - label: formatLoaderLabel(loader), - weight: 1002 + index, - message: async () => `- ${formatLoaderLabel(loader)}`, - })) -} + button('incorrect_project_type', 'Incorrect Project Type') + .weight(1001) + .suggestedStatus('rejected') + .severity('medium') + .children( + select('type', 'What type should this project be?').children( + toggle('modpack', 'Modpack') + .shown(({ project }) => !project.project_types.includes('modpack')) + .weight(1001) + .message(mdMsg('versions/invalid-modpacks')), + toggle('resourcepack', 'Resource Pack') + .shown(({ project }) => !project.project_types.includes('resourcepack')) + .weight(1001) + .message(mdMsg('versions/invalid-resourcepacks')), + toggle('datapack', 'Data Pack') + .shown(({ project }) => !project.loaders.includes('datapack')) + .weight(1001) + .message(mdMsg('versions/invalid-datapacks')), + ), + ), -const versions: Stage = { - title: 'Versions', - hint: "Are this project's files correct?", - id: 'versions', - icon: VersionIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e25ee711bf0804bad38e9055951ff31', - navigate: '/versions', - shouldShow: (project, projectV3) => !projectV3?.minecraft_server, - actions: [ - { - id: 'versions_incorrect_additional', - type: 'button', - label: 'Incorrect additional files', - weight: 1000, - suggestedStatus: 'flagged', - severity: 'medium', - message: async () => - (await import('../messages/checklist-messages/versions/incorrect_additional_files.md?raw')) - .default, - } as ButtonAction, - { - id: 'versions_incorrect_project_type', - type: 'button', - label: 'Incorrect Project Type', - suggestedStatus: 'rejected', - severity: 'medium', - weight: -999999, - message: async () => '', - enablesActions: [ - { - id: 'versions_incorrect_project_type_options', - type: 'dropdown', - label: 'What type should this project be?', - options: [ - { - label: 'Modpack', - weight: 1001, - shouldShow: (project) => project.project_type !== 'modpack', - message: async () => - (await import('../messages/checklist-messages/versions/invalid-modpacks.md?raw')) - .default, - } as DropdownActionOption, - { - label: 'Resource Pack', - weight: 1001, - shouldShow: (project) => project.project_type !== 'resourcepack', - message: async () => - ( - await import('../messages/checklist-messages/versions/invalid-resourcepacks.md?raw') - ).default, - } as DropdownActionOption, - { - label: 'Data Pack', - weight: 1001, - shouldShow: (project) => !project.loaders.includes('datapack'), - message: async () => - (await import('../messages/checklist-messages/versions/invalid-datapacks.md?raw')) - .default, - } as DropdownActionOption, - ], - } as DropdownAction, - ], - } as ButtonAction, - { - id: 'versions_alternate_versions', - type: 'button', - label: 'Alternate Versions', - suggestedStatus: 'flagged', - severity: 'medium', - weight: -999999, - message: async () => '', - enablesActions: [ - { - id: 'versions_alternate_versions_options', - type: 'dropdown', - label: 'How are the alternate versions distributed?', - options: [ - { - label: 'Primary Files', - weight: 1002, - message: async () => - ( - await import('../messages/checklist-messages/versions/alternate_versions-primary.md?raw') - ).default, - } as DropdownActionOption, - { - label: 'Additional Files', - weight: 1002, - message: async () => - ( - await import('../messages/checklist-messages/versions/alternate_versions-additional.md?raw') - ).default, - } as DropdownActionOption, - { - label: 'Monofile', - weight: 1002, - shouldShow: (project) => - project.project_type === 'resourcepack' || project.loaders.includes('datapack'), - message: async () => - ( - await import('../messages/checklist-messages/versions/alternate_versions-mono.md?raw') - ).default, - } as DropdownActionOption, - { - label: 'Server Files (Primary Files)', - weight: 1002, - shouldShow: (project) => project.project_type === 'modpack', - message: async () => - ( - await import('../messages/checklist-messages/versions/alternate_versions-server.md?raw') - ).default, - } as DropdownActionOption, - { - label: 'Server Files (Additional Files)', - weight: 1002, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project) => project.project_type === 'modpack', - message: async () => - ( - await import('../messages/checklist-messages/versions/alternate_versions-server-additional.md?raw') - ).default, - } as DropdownActionOption, - { - label: 'mods.zip', - weight: 1002, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project) => project.project_type === 'modpack', - message: async () => - ( - await import('../messages/checklist-messages/versions/alternate_versions-zip.md?raw') - ).default, - } as DropdownActionOption, - ], - } as DropdownAction, - ], - } as ButtonAction, - { - id: 'versions_incorrect_loader', - type: 'button', - label: 'Incorrect Loader', - suggestedStatus: 'flagged', - severity: 'medium', - weight: 1001, - message: async () => - (await import('../messages/checklist-messages/versions/incorrect_loader.md?raw')).default, - enablesActions: [ - { - id: 'versions_incorrect_loader_options', - type: 'multi-select-chips', - label: 'Which loader labels are incorrect?', - joinWith: '\n', - shouldShow: (_project, _projectV3, context) => - Boolean(context && getIncorrectLoaderOptions(context).length > 0), - options: (context) => getIncorrectLoaderOptions(context), - } as MultiSelectChipsAction, - ], - } as ButtonAction, - { - id: 'versions_vanilla_assets', - type: 'button', - label: 'Vanilla Assets', - suggestedStatus: `rejected`, - severity: `medium`, - weight: 1003, - shouldShow: (project) => project.project_type === 'resourcepack', - message: async () => - (await import('../messages/checklist-messages/versions/vanilla_assets.md?raw')).default, - } as ButtonAction, - { - id: 'versions_redist_libs', - type: 'button', - label: 'Packed Libs', - suggestedStatus: `rejected`, - severity: `medium`, - weight: 1003, - shouldShow: (project) => project.project_type === 'mod' || project.project_type === 'plugin', - message: async () => - (await import('../messages/checklist-messages/versions/redist_libs.md?raw')).default, - } as ButtonAction, - { - id: 'versions_duplicate_primary_files', - type: 'button', - label: 'Duplicate Primary Files', - suggestedStatus: 'flagged', - severity: `medium`, - weight: 1004, - message: async () => - (await import('../messages/checklist-messages/versions/broken_version.md?raw')).default, - } as ButtonAction, - { - id: 'unsupported_project_type', - type: 'button', - label: `Unsupported`, - suggestedStatus: `rejected`, - severity: `medium`, - weight: 1005, - message: async () => - (await import('../messages/checklist-messages/versions/unsupported_project.md?raw')) - .default, - relevantExtraInput: [ - { - label: 'Project Type', - required: true, - variable: 'INVALID_TYPE', - }, - ], - } as ButtonAction, - ], -} + button('alternate_versions', 'Alternate Versions') + .weight(1002) + .suggestedStatus('rejected') + .severity('high') + .children( + select('distribution', 'How are they distributed?').children( + toggle('primary', 'Primary Files') + .weight(1002) + .message(mdMsg('versions/alternate_versions-primary')), + toggle('additional', 'Additional Files') + .weight(1002) + .message(mdMsg('versions/alternate_versions-additional')), + toggle('mono', 'Monofile') + .shown(({ project }) => project.project_types.includes('resourcepack') || project.loaders.includes('datapack')) + .weight(1002) + .message(mdMsg('versions/alternate_versions-mono')), + toggle('server', 'Server Files (Primary Files)') + .shown(({ project }) => project.project_types.includes('modpack')) + .weight(1002) + .message(mdMsg('versions/alternate_versions-server')), + toggle('server_additional', 'Server Files (Additional Files)') + .shown(({ project }) => project.project_types.includes('modpack')) + .weight(1002) + .message(mdMsg('versions/alternate_versions-server-additional')), + toggle('zip', 'mods.zip') + .shown(({ project }) => project.project_types.includes('modpack')) + .weight(1002) + .message(mdMsg('versions/alternate_versions-zip')), + ), + ), + + button('incorrect_loader', 'Incorrect Loader') + .weight(1003) + .suggestedStatus('flagged') + .severity('medium') + .message(async (ctx) => { + const header = await mdMsg('versions/incorrect_loader')(ctx) + const selected = ctx.state.loaders + if (selected instanceof Set && selected.size > 0) { + const list = [...selected].map((id) => `- ${formatLoaderLabel(id)}`).join('\n') + return `${header}\n${list}` + } + return header + }) + //TODO: different message for empty vs non empty + quick fix? + .children( + chips('loaders', 'Which loader labels are incorrect?') + .childrenFn(({ project }) => project.loaders.map((id) => toggle(id, formatLoaderLabel(id)).build())), + ), + + button('vanilla_assets', 'Vanilla Assets') + .shown(({ project }) => project.project_types.includes('resourcepack')) + .weight(1004) + .suggestedStatus('rejected') + .severity('medium') + .message(mdMsg('versions/vanilla_assets')), -export default versions + button('redist_libs', 'Packed Libs') + .shown(({ project }) => project.project_types.includes('mod') || project.project_types.includes('plugin')) + .weight(1004) + .suggestedStatus('rejected') + .severity('medium') + .message(mdMsg('versions/redist_libs')), + + button('duplicate_primary_files', 'Duplicate Primary Files') + .weight(1005) + .suggestedStatus('flagged') + .severity('medium') + .message(mdMsg('versions/broken_version')), + + button('unsupported', 'Unsupported') + .weight(1006) + .suggestedStatus('rejected') + .severity('medium') + .message(mdMsg('versions/unsupported_project', (ctx) => ({ + INVALID_TYPE: ctx.state.invalid_type, + }))) + .children( + text('invalid_type', 'Project type').required(), + ), + ), + ], + { + icon: VersionIcon, + navigate: '/versions', + shown: (_project, projectV3) => !projectV3?.minecraft_server + }, +) diff --git a/packages/moderation/src/index.ts b/packages/moderation/src/index.ts index 80e08ccf02..d583b48973 100644 --- a/packages/moderation/src/index.ts +++ b/packages/moderation/src/index.ts @@ -1,6 +1,5 @@ export { default as checklist } from './data/checklist' export { default as keybinds } from './data/keybinds' -export { finalPermissionMessages } from './data/modpack-permissions-stage' export { default as nags } from './data/nags' export * from './data/nags/index' export { default as reportQuickReplies } from './data/report-quick-replies' @@ -18,3 +17,4 @@ export * from './types/quick-reply' export * from './types/reports' export * from './types/stage' export * from './utils' +export * from './types/node' diff --git a/packages/moderation/src/types/node.ts b/packages/moderation/src/types/node.ts new file mode 100644 index 0000000000..81f17a065f --- /dev/null +++ b/packages/moderation/src/types/node.ts @@ -0,0 +1,225 @@ +import type { Labrinth } from '@modrinth/api-client' +import type { FunctionalComponent, SVGAttributes } from 'vue' + +import { + expandVariables, + flattenProjectV3Variables, + flattenProjectVariables, + flattenStaticVariables, +} from '../utils' + +export type NodeType = 'boolean' | 'select' | 'multi-select' | 'text' | 'markdown' | 'label' | 'group' | 'prose' +export type NodeVariant = 'button' | 'toggle' +export type ModerationStatus = 'approved' | 'rejected' | 'flagged' +export type ModerationSeverity = 'low' | 'medium' | 'high' | 'critical' + +export type NodeState = boolean | string | number | Set | NodeStateWithChildren | null | undefined + +export interface NodeStateWithChildren { + value?: boolean | string | number | Set | null + [childId: string]: NodeState +} + +export interface NodeContext { + project: Labrinth.Projects.v3.Project + projectV2: Labrinth.Projects.v2.Project + state: Record + globalState: Record> +} + +export type MessageFn = (ctx: NodeContext) => Promise +export type ContentFn = (ctx: NodeContext) => string | Promise +export type ShowFn = (ctx: NodeContext) => boolean +export type ChildrenFn = (ctx: NodeContext) => Node[] + +const messageFiles = import.meta.glob('../data/messages/checklist-messages/**/*.md', { + query: '?raw', + import: 'default', +}) + +export function mdMsg( + path: string, + getVars?: (ctx: NodeContext) => Record, +): MessageFn { + return async (ctx) => { + const loader = messageFiles[`../data/messages/checklist-messages/${path}.md`] + if (!loader) return '' + const raw = (await loader()) as string + if (!getVars) return raw + const vars = getVars(ctx) + return Object.entries(vars).reduce( + (result, [key, value]) => result.replace(new RegExp(`%${key}%`, 'g'), String(value ?? '')), + raw, + ) + } +} + +const textFiles = import.meta.glob('../data/messages/checklist-text/**/*.md', { + query: '?raw', + import: 'default', +}) + +export function mdEscape(text: string): string { + return text.replace(/[\\*_`[~]/g, '\\$&') +} + +const USER_CONTENT_KEYS = ['PROJECT_TITLE', 'PROJECT_SLUG', 'PROJECT_SUMMARY', 'PROJECT_TYPE', 'PROJECT_STATUS'] + +export function mdText(path: string): ContentFn { + return async (ctx) => { + const loader = textFiles[`../data/messages/checklist-text/${path}.md`] + if (!loader) return '' + const raw = (await loader()) as string + const vars: Record = { + ...flattenStaticVariables(), + ...flattenProjectVariables(ctx.projectV2), + ...flattenProjectV3Variables(ctx.project), + } + for (const key of USER_CONTENT_KEYS) { + if (key in vars) vars[key] = mdEscape(vars[key]) + } + return expandVariables(raw, ctx.projectV2, ctx.project, vars) + } +} + +export interface Node { + id?: string + type: NodeType + label?: string + variant?: NodeVariant + layout?: 'flex' | 'column' + weight?: number + children?: Node[] + childrenFn?: ChildrenFn + message?: MessageFn + content?: ContentFn + shown?: ShowFn + enabled?: ShowFn + suggestedStatus?: ModerationStatus + severity?: ModerationSeverity + defaultChecked?: boolean + placeholder?: string + required?: boolean +} + +export interface ChecklistStage { + id: string + title: string + hint: string + guidance_url: string + icon?: FunctionalComponent + navigate?: string + nodes: Node[] + shown?: ( + project: Labrinth.Projects.v2.Project, + projectV3?: Labrinth.Projects.v3.Project, + ) => boolean +} + +type Resolvable = Node | NodeBuilder + +function resolve(n: Resolvable): Node { + return n instanceof NodeBuilder ? n.build() : n +} + +export class NodeBuilder { + private data: Partial + + constructor(data: Partial) { + this.data = { ...data } + } + + weight(w: number): NodeBuilder { + return new NodeBuilder({ ...this.data, weight: w }) + } + + message(fn: MessageFn): NodeBuilder { + return new NodeBuilder({ ...this.data, message: fn }) + } + + content(fn: ContentFn): NodeBuilder { + return new NodeBuilder({ ...this.data, content: fn }) + } + + column(): NodeBuilder { + return new NodeBuilder({ ...this.data, layout: 'column' }) + } + + children(...nodes: Resolvable[]): NodeBuilder { + return new NodeBuilder({ ...this.data, children: nodes.map(resolve) }) + } + + childrenFn(fn: ChildrenFn): NodeBuilder { + return new NodeBuilder({ ...this.data, childrenFn: fn }) + } + + shown(fn: ShowFn): NodeBuilder { + return new NodeBuilder({ ...this.data, shown: fn }) + } + + enabled(fn: ShowFn): NodeBuilder { + return new NodeBuilder({ ...this.data, enabled: fn }) + } + + suggestedStatus(s: ModerationStatus): NodeBuilder { + return new NodeBuilder({ ...this.data, suggestedStatus: s }) + } + + severity(s: ModerationSeverity): NodeBuilder { + return new NodeBuilder({ ...this.data, severity: s }) + } + + defaultChecked(v = true): NodeBuilder { + return new NodeBuilder({ ...this.data, defaultChecked: v }) + } + + placeholder(p: string): NodeBuilder { + return new NodeBuilder({ ...this.data, placeholder: p }) + } + + required(v = true): NodeBuilder { + return new NodeBuilder({ ...this.data, required: v }) + } + + build(): Node { + return { ...this.data } as Node + } +} + +function node(id: string, label: string, type: NodeType, variant?: NodeVariant): NodeBuilder { + return new NodeBuilder({ id, label, type, variant }) +} + +export const button = (id: string, label: string) => node(id, label, 'boolean', 'button') +export const toggle = (id: string, label: string) => node(id, label, 'boolean', 'toggle') +export const select = (id: string, label: string) => node(id, label, 'select') +export const chips = (id: string, label: string) => node(id, label, 'multi-select') +export const text = (id: string, label: string) => node(id, label, 'text') +export const markdown = (id: string, label: string) => node(id, label, 'markdown') +export const label = (id: string, content: string) => node(id, content, 'label') +export const group = (id?: string) => new NodeBuilder({ id, type: 'group' }) +export const prose = (content: ContentFn) => new NodeBuilder({ type: 'prose', content }) + +interface StageOptions { + icon?: ChecklistStage['icon'] + navigate?: string + shown?: ChecklistStage['shown'] +} + +export function stage( + id: string, + title: string, + hint: string, + guidance_url: string, + nodes: Resolvable[], + options: StageOptions = {}, +): ChecklistStage { + return { + id, + title, + hint, + guidance_url, + nodes: nodes.map(resolve), + ...options, + } +} From 81d873fb71ed2016c2c45441421ca36954ee7f94 Mon Sep 17 00:00:00 2001 From: chyzman Date: Fri, 10 Jul 2026 21:59:52 -0400 Subject: [PATCH 18/85] put stageoptions before nodes --- .../moderation/src/data/stages/description.ts | 2 +- .../moderation/src/data/stages/gallery.ts | 8 +-- .../moderation/src/data/stages/license.ts | 14 ++--- packages/moderation/src/data/stages/links.ts | 56 ++++++++++++------- .../moderation/src/data/stages/reupload.ts | 23 ++++---- .../moderation/src/data/stages/summary.ts | 2 +- .../moderation/src/data/stages/title-slug.ts | 31 +++++----- .../moderation/src/data/stages/versions.ts | 38 ++++++++----- packages/moderation/src/types/node.ts | 2 +- 9 files changed, 95 insertions(+), 81 deletions(-) diff --git a/packages/moderation/src/data/stages/description.ts b/packages/moderation/src/data/stages/description.ts index 09ff0e9a76..331a02b117 100644 --- a/packages/moderation/src/data/stages/description.ts +++ b/packages/moderation/src/data/stages/description.ts @@ -7,6 +7,7 @@ export default stage( 'Description', 'Is the description sufficient, accurate, and accessible?', 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf080508042e70089dd787e', + { icon: LibraryIcon, navigate: '/' }, [ group().children( button('insufficient', 'Insufficient') @@ -86,5 +87,4 @@ export default stage( .message(mdMsg('description/clarity')), ), ], - { icon: LibraryIcon, navigate: '/' }, ) diff --git a/packages/moderation/src/data/stages/gallery.ts b/packages/moderation/src/data/stages/gallery.ts index 7117d9805f..2798e5dd94 100644 --- a/packages/moderation/src/data/stages/gallery.ts +++ b/packages/moderation/src/data/stages/gallery.ts @@ -7,6 +7,10 @@ export default stage( 'Gallery', "Are this project's gallery images sufficient?", 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf08096828bd1c3f24d8b8e', + { + icon: ImageIcon, + navigate: '/gallery', + }, [ group().children( button('insufficient', 'Insufficient') @@ -24,8 +28,4 @@ export default stage( .message(mdMsg('gallery/not-relevant')), ), ], - { - icon: ImageIcon, - navigate: '/gallery', - }, ) diff --git a/packages/moderation/src/data/stages/license.ts b/packages/moderation/src/data/stages/license.ts index 9c960aee6d..0f70360dbb 100644 --- a/packages/moderation/src/data/stages/license.ts +++ b/packages/moderation/src/data/stages/license.ts @@ -24,6 +24,11 @@ export default stage( 'License', 'Is this license and link valid?', 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf080f8805df7d012a8f770', + { + icon: BookTextIcon, + navigate: '/settings/license', + shown: (_project, projectV3) => !projectV3?.minecraft_server, + }, [ prose(mdText('licensing')), @@ -49,14 +54,7 @@ export default stage( if (ctx.state.fork) return mdMsg('license/no_source-fork')(ctx) return mdMsg('license/no_source')(ctx) }) - .children( - toggle('fork', 'No Source: Fork').severity('high'), - ), + .children(toggle('fork', 'No Source: Fork').severity('high')), ), ], - { - icon: BookTextIcon, - navigate: '/settings/license', - shown: (_project, projectV3) => !projectV3?.minecraft_server, - }, ) diff --git a/packages/moderation/src/data/stages/links.ts b/packages/moderation/src/data/stages/links.ts index ca0c6ae426..c5228902c3 100644 --- a/packages/moderation/src/data/stages/links.ts +++ b/packages/moderation/src/data/stages/links.ts @@ -28,24 +28,48 @@ export default stage( 'Links', "Are the project's links accurate and accessible?", 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf08013b36cd75cbf1a9177', + { + icon: LinkIcon, + navigate: '/settings/links', + shown: (_project, projectV3) => + Boolean(projectV3 && Object.keys(projectV3.link_urls).length > 0), + }, [ - linkSection('issues', ({ project }) => `**Issues:** ${mdEscape(project.link_urls.issues?.url ?? '')}`, 500) - .shown(({ project }) => !!project.link_urls.issues?.url), + linkSection( + 'issues', + ({ project }) => `**Issues:** ${mdEscape(project.link_urls.issues?.url ?? '')}`, + 500, + ).shown(({ project }) => !!project.link_urls.issues?.url), - linkSection('source', ({ project }) => `**Source:** ${mdEscape(project.link_urls.source?.url ?? '')}`, 510) - .shown(({ project }) => !!project.link_urls.source?.url), + linkSection( + 'source', + ({ project }) => `**Source:** ${mdEscape(project.link_urls.source?.url ?? '')}`, + 510, + ).shown(({ project }) => !!project.link_urls.source?.url), - linkSection('wiki', ({ project }) => `**Wiki:** ${mdEscape(project.link_urls.wiki?.url ?? '')}`, 520) - .shown(({ project }) => !!project.link_urls.wiki?.url), + linkSection( + 'wiki', + ({ project }) => `**Wiki:** ${mdEscape(project.link_urls.wiki?.url ?? '')}`, + 520, + ).shown(({ project }) => !!project.link_urls.wiki?.url), - linkSection('discord', ({ project }) => `**Discord:** ${mdEscape(project.link_urls.discord?.url ?? '')}`, 530) - .shown(({ project }) => !!project.link_urls.discord?.url), + linkSection( + 'discord', + ({ project }) => `**Discord:** ${mdEscape(project.link_urls.discord?.url ?? '')}`, + 530, + ).shown(({ project }) => !!project.link_urls.discord?.url), - linkSection('site', ({ project }) => `**Website:** ${mdEscape(project.link_urls.site?.url ?? '')}`, 540) - .shown(({ project }) => !!project.link_urls.site?.url), + linkSection( + 'site', + ({ project }) => `**Website:** ${mdEscape(project.link_urls.site?.url ?? '')}`, + 540, + ).shown(({ project }) => !!project.link_urls.site?.url), - linkSection('store', ({ project }) => `**Store:** ${mdEscape(project.link_urls.store?.url ?? '')}`, 550) - .shown(({ project }) => !!project.link_urls.store?.url), + linkSection( + 'store', + ({ project }) => `**Store:** ${mdEscape(project.link_urls.store?.url ?? '')}`, + 550, + ).shown(({ project }) => !!project.link_urls.store?.url), group('donations') .column() @@ -71,12 +95,4 @@ export default stage( ), ), ], - { - icon: LinkIcon, - navigate: '/settings/links', - shown: (_project, projectV3) => - Boolean( - projectV3 && Object.keys(projectV3.link_urls).length > 0, - ), - }, ) diff --git a/packages/moderation/src/data/stages/reupload.ts b/packages/moderation/src/data/stages/reupload.ts index 15b5a11a81..9f57f9cb83 100644 --- a/packages/moderation/src/data/stages/reupload.ts +++ b/packages/moderation/src/data/stages/reupload.ts @@ -16,6 +16,7 @@ export default stage( 'Reupload', 'Does the author have proper permissions to post this project?', 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080d1a0a2cda3ff2ce997', + { icon: CopyrightIcon }, [ group().children( button('reupload', 'Re-upload') @@ -60,10 +61,9 @@ export default stage( .suggestedStatus('rejected') .severity('high') .message( - mdMsg( - 'reupload/identity-verification/identity_verification', - (ctx) => ({ PLATFORM: ctx.state.platform }), - ), + mdMsg('reupload/identity-verification/identity_verification', (ctx) => ({ + PLATFORM: ctx.state.platform, + })), ) .children(text('platform', 'Where else can the project be found?').required()), @@ -73,10 +73,9 @@ export default stage( .suggestedStatus('rejected') .severity('high') .message( - mdMsg( - 'reupload/identity-verification/identity_verification-server', - (ctx) => ({ CONTACT: ctx.state.contact }), - ), + mdMsg('reupload/identity-verification/identity_verification-server', (ctx) => ({ + CONTACT: ctx.state.contact, + })), ) .children(text('contact', 'Known public contact method').required()), @@ -97,10 +96,9 @@ export default stage( toggle('list', 'List overrides?') .weight(1101) .message( - mdMsg( - 'reupload/custom_server/custom_server_overrides-verification-list', - (ctx) => ({ OVERRIDES: ctx.state.overrides }), - ), + mdMsg('reupload/custom_server/custom_server_overrides-verification-list', (ctx) => ({ + OVERRIDES: ctx.state.overrides, + })), ) .children(markdown('overrides', 'Add list of overrides.')), ), @@ -118,5 +116,4 @@ export default stage( .children(markdown('overrides', 'Forbidden overrides list').required()), ), ], - { icon: CopyrightIcon }, ) diff --git a/packages/moderation/src/data/stages/summary.ts b/packages/moderation/src/data/stages/summary.ts index eacb7d930a..92cfbac610 100644 --- a/packages/moderation/src/data/stages/summary.ts +++ b/packages/moderation/src/data/stages/summary.ts @@ -7,6 +7,7 @@ export default stage( 'Summary', "Is the project's summary sufficient?", 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf080bfb5e5c7c6211c693b', + { icon: AlignLeftIcon }, [ prose(mdText('summary/summary')), @@ -45,5 +46,4 @@ export default stage( .message(mdMsg('summary/repeat-ip')), ), ], - { icon: AlignLeftIcon }, ) diff --git a/packages/moderation/src/data/stages/title-slug.ts b/packages/moderation/src/data/stages/title-slug.ts index 6e8e64f119..b70f03bae8 100644 --- a/packages/moderation/src/data/stages/title-slug.ts +++ b/packages/moderation/src/data/stages/title-slug.ts @@ -20,11 +20,12 @@ export default stage( 'Title & Slug', 'Are the Name and URL accurate and appropriate?', 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf0803c9660e90f0fead705', + { icon: BookOpenIcon }, [ prose(async (ctx) => { const title = await mdText('title-slug/title')(ctx) if (!hasCustomSlug(ctx.project)) return title - return title + await mdText('title-slug/slug')(ctx) + return title + (await mdText('title-slug/slug')(ctx)) }), group('title').children( @@ -46,18 +47,17 @@ export default stage( .severity('medium') .message(mdMsg('title/similarities')) .children( - chips('options', 'Similarities Additional Info') - .children( - toggle('modpack_named_after_mod', 'Modpack Named After Mod') - .shown(({ project }) => project.project_types.includes('modpack')) - .weight(111) - .message(mdMsg('title/similarities-modpack')), + chips('options', 'Similarities Additional Info').children( + toggle('modpack_named_after_mod', 'Modpack Named After Mod') + .shown(({ project }) => project.project_types.includes('modpack')) + .weight(111) + .message(mdMsg('title/similarities-modpack')), - toggle('forked_project', 'Forked Project') - .shown(({ project }) => !project?.minecraft_server) - .weight(112) - .message(mdMsg('title/similarities-fork')), - ), + toggle('forked_project', 'Forked Project') + .shown(({ project }) => !project?.minecraft_server) + .weight(112) + .message(mdMsg('title/similarities-fork')), + ), ), ), @@ -68,12 +68,7 @@ export default stage( chips('options', 'Slug Issues?') .suggestedStatus('rejected') .severity('low') - .children( - toggle('misused', 'Misused') - .weight(200) - .message(mdMsg('slug/misused')), - ), + .children(toggle('misused', 'Misused').weight(200).message(mdMsg('slug/misused'))), ), ], - { icon: BookOpenIcon }, ) diff --git a/packages/moderation/src/data/stages/versions.ts b/packages/moderation/src/data/stages/versions.ts index c93b0aac6e..72160173c5 100644 --- a/packages/moderation/src/data/stages/versions.ts +++ b/packages/moderation/src/data/stages/versions.ts @@ -21,6 +21,11 @@ export default stage( 'Versions', "Are this project's files correct?", 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e25ee711bf0804bad38e9055951ff31', + { + icon: VersionIcon, + navigate: '/versions', + shown: (_project, projectV3) => !projectV3?.minecraft_server, + }, [ group().children( button('incorrect_additional', 'Incorrect additional files') @@ -63,7 +68,11 @@ export default stage( .weight(1002) .message(mdMsg('versions/alternate_versions-additional')), toggle('mono', 'Monofile') - .shown(({ project }) => project.project_types.includes('resourcepack') || project.loaders.includes('datapack')) + .shown( + ({ project }) => + project.project_types.includes('resourcepack') || + project.loaders.includes('datapack'), + ) .weight(1002) .message(mdMsg('versions/alternate_versions-mono')), toggle('server', 'Server Files (Primary Files)') @@ -96,8 +105,9 @@ export default stage( }) //TODO: different message for empty vs non empty + quick fix? .children( - chips('loaders', 'Which loader labels are incorrect?') - .childrenFn(({ project }) => project.loaders.map((id) => toggle(id, formatLoaderLabel(id)).build())), + chips('loaders', 'Which loader labels are incorrect?').childrenFn(({ project }) => + project.loaders.map((id) => toggle(id, formatLoaderLabel(id)).build()), + ), ), button('vanilla_assets', 'Vanilla Assets') @@ -108,7 +118,10 @@ export default stage( .message(mdMsg('versions/vanilla_assets')), button('redist_libs', 'Packed Libs') - .shown(({ project }) => project.project_types.includes('mod') || project.project_types.includes('plugin')) + .shown( + ({ project }) => + project.project_types.includes('mod') || project.project_types.includes('plugin'), + ) .weight(1004) .suggestedStatus('rejected') .severity('medium') @@ -124,17 +137,12 @@ export default stage( .weight(1006) .suggestedStatus('rejected') .severity('medium') - .message(mdMsg('versions/unsupported_project', (ctx) => ({ - INVALID_TYPE: ctx.state.invalid_type, - }))) - .children( - text('invalid_type', 'Project type').required(), - ), + .message( + mdMsg('versions/unsupported_project', (ctx) => ({ + INVALID_TYPE: ctx.state.invalid_type, + })), + ) + .children(text('invalid_type', 'Project type').required()), ), ], - { - icon: VersionIcon, - navigate: '/versions', - shown: (_project, projectV3) => !projectV3?.minecraft_server - }, ) diff --git a/packages/moderation/src/types/node.ts b/packages/moderation/src/types/node.ts index 81f17a065f..3d5d88cb31 100644 --- a/packages/moderation/src/types/node.ts +++ b/packages/moderation/src/types/node.ts @@ -211,8 +211,8 @@ export function stage( title: string, hint: string, guidance_url: string, - nodes: Resolvable[], options: StageOptions = {}, + nodes: Resolvable[], ): ChecklistStage { return { id, From 8f957ac83cfd10592fcebcaea054fbdc49690d34 Mon Sep 17 00:00:00 2001 From: chyzman Date: Fri, 10 Jul 2026 22:21:23 -0400 Subject: [PATCH 19/85] do state and variablse in tooltips --- .../ui/moderation/checklist/NodeRenderer.vue | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/ui/moderation/checklist/NodeRenderer.vue b/apps/frontend/src/components/ui/moderation/checklist/NodeRenderer.vue index d6cf593815..83dd2a0b11 100644 --- a/apps/frontend/src/components/ui/moderation/checklist/NodeRenderer.vue +++ b/apps/frontend/src/components/ui/moderation/checklist/NodeRenderer.vue @@ -1,7 +1,13 @@ @@ -169,7 +215,11 @@ watchEffect(async () => { v-else-if="node.type === 'boolean' && node.variant === 'button'" :color="getBooleanState(node) ? 'brand' : 'standard'" > - + From 09acaad11888837700521df10d691255983e2c63 Mon Sep 17 00:00:00 2001 From: chyzman Date: Fri, 10 Jul 2026 22:59:54 -0400 Subject: [PATCH 20/85] checklist fully migrated to new system, not much new has been added (yet) --- packages/moderation/src/data/checklist.ts | 27 +- .../moderation/src/data/stages/categories.ts | 107 ++-- .../environment/environment-multiple.ts | 55 +- .../data/stages/environment/environment.ts | 55 +- .../moderation/src/data/stages/permissions.ts | 85 ++- .../src/data/stages/post-approval.ts | 186 ++---- .../src/data/stages/rule-following.ts | 562 ++++-------------- .../src/data/stages/status-alerts.ts | 144 ++--- .../src/data/stages/undefined-project.ts | 43 +- 9 files changed, 397 insertions(+), 867 deletions(-) diff --git a/packages/moderation/src/data/checklist.ts b/packages/moderation/src/data/checklist.ts index 343fd6a47b..d91997d2ad 100644 --- a/packages/moderation/src/data/checklist.ts +++ b/packages/moderation/src/data/checklist.ts @@ -1,11 +1,36 @@ import type { ChecklistStage } from '../types/node' +import categories from './stages/categories' import description from './stages/description' +import environment from './stages/environment/environment' +import environmentMultiple from './stages/environment/environment-multiple' import gallery from './stages/gallery' import license from './stages/license' import links from './stages/links' +import permissions from './stages/permissions' +import postApproval from './stages/post-approval' import reupload from './stages/reupload' +import ruleFollowing from './stages/rule-following' +import statusAlerts from './stages/status-alerts' import summary from './stages/summary' import titleSlug from './stages/title-slug' +import undefinedProject from './stages/undefined-project' import versions from './stages/versions' -export default [titleSlug, summary, description, links, license, gallery, versions, reupload] as ReadonlyArray +export default [ + titleSlug, + summary, + description, + links, + license, + categories, + environment, + environmentMultiple, + gallery, + versions, + reupload, + permissions, + ruleFollowing, + statusAlerts, + undefinedProject, + postApproval, +] as ReadonlyArray diff --git a/packages/moderation/src/data/stages/categories.ts b/packages/moderation/src/data/stages/categories.ts index 3ad4df2f6a..67e54278aa 100644 --- a/packages/moderation/src/data/stages/categories.ts +++ b/packages/moderation/src/data/stages/categories.ts @@ -1,64 +1,53 @@ import { TagsIcon } from '@modrinth/assets' -import type { ButtonAction } from '../../types/actions' -import type { Stage } from '../../types/stage' +import { button, group, mdMsg, mdText, prose, stage } from '../../types/node' -const categories: Stage = { - title: "Tags", - hint: "Are the project's tags accurate?", - id: 'tags', - icon: TagsIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf0802f96aafc0397a9f6d3', - navigate: '/settings/tags', - shouldShow: (project) => - project.categories.length > 0 || project.additional_categories.length > 0, - text: async () => { - return (await import('../messages/checklist-text/categories.md?raw')).default +export default stage( + 'tags', + 'Tags', + "Are the project's tags accurate?", + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e15ee711bf0802f96aafc0397a9f6d3', + { + icon: TagsIcon, + navigate: '/settings/tags', + shown: (_project, projectV3) => + (projectV3?.categories.length ?? 0) > 0 || + (projectV3?.additional_categories.length ?? 0) > 0, }, - actions: [ - { - id: 'categories_inaccurate', - type: 'button', - label: 'Inaccurate', - weight: 700, - suggestedStatus: 'flagged', - severity: 'low', - message: async () => - (await import('../messages/checklist-messages/categories/inaccurate.md?raw')).default, - disablesActions: ['categories_optimization_misused', 'categories_resolutions_misused'], - } as ButtonAction, - { - id: 'categories_optimization_misused', - type: 'button', - label: 'Optimization', - weight: 701, - suggestedStatus: 'flagged', - severity: 'low', - shouldShow: (project) => - project.categories.includes('optimization') || - project.additional_categories.includes('optimization'), - message: async () => - (await import('../messages/checklist-messages/categories/inaccurate.md?raw')).default + - (await import('../messages/checklist-messages/categories/optimization_misused.md?raw')) - .default, - disablesActions: ['categories_inaccurate', 'categories_resolutions_misused'], - } as ButtonAction, - { - id: 'categories_resolutions_misused', - type: 'button', - label: 'Resolutions', - weight: 702, - suggestedStatus: 'flagged', - severity: 'low', - shouldShow: (project) => project.project_type === 'resourcepack', - message: async () => - (await import('../messages/checklist-messages/categories/inaccurate.md?raw')).default + - (await import('../messages/checklist-messages/categories/resolutions_misused.md?raw')) - .default, - disablesActions: ['categories_inaccurate', 'categories_optimization_misused'], - }, - ], -} + [ + prose(mdText('categories')), + + group().children( + button('inaccurate', 'Inaccurate') + .weight(700) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('categories/inaccurate')), -export default categories + button('optimization_misused', 'Optimization') + .shown(({ project }) => + project.categories.includes('optimization') || + project.additional_categories.includes('optimization'), + ) + .weight(701) + .suggestedStatus('flagged') + .severity('low') + .message(async (ctx) => { + const base = await mdMsg('categories/inaccurate')(ctx) + const extra = await mdMsg('categories/optimization_misused')(ctx) + return base + extra + }), + + button('resolutions_misused', 'Resolutions') + .shown(({ project }) => project.project_types.includes('resourcepack')) + .weight(702) + .suggestedStatus('flagged') + .severity('low') + .message(async (ctx) => { + const base = await mdMsg('categories/inaccurate')(ctx) + const extra = await mdMsg('categories/resolutions_misused')(ctx) + return base + extra + }), + ), + ], +) diff --git a/packages/moderation/src/data/stages/environment/environment-multiple.ts b/packages/moderation/src/data/stages/environment/environment-multiple.ts index 0d5aaa1f9b..6e6ffd8db5 100644 --- a/packages/moderation/src/data/stages/environment/environment-multiple.ts +++ b/packages/moderation/src/data/stages/environment/environment-multiple.ts @@ -1,33 +1,30 @@ import { GlobeIcon } from '@modrinth/assets' -import type { ButtonAction } from '../../../types/actions' -import type { Stage } from '../../../types/stage' +import { button, group, mdMsg, mdText, prose, stage } from '../../../types/node' -const environmentMultiple: Stage = { - title: "Environment", - hint: "Is the project's environment information accurate?", - id: 'environment', - navigate: '/settings/versions', - icon: GlobeIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e25ee711bf0802d9a9bdb82dce040eb', - text: async () => - (await import('../../messages/checklist-text/environment/environment-multiple.md?raw')).default, - shouldShow: (project, projectV3) => - (projectV3?.environment?.length ?? 0) !== 1 && !projectV3?.minecraft_server, - actions: [ - { - id: 'side_types_inaccurate', - type: 'button', - label: 'Inaccurate', - weight: 800, - suggestedStatus: 'flagged', - severity: 'low', - shouldShow: (project) => project.project_type === 'mod' || project.project_type === 'modpack', - message: async () => - (await import('../../messages/checklist-messages/environment/inaccurate.md?raw')).default, - } as ButtonAction, - ], -} +export default stage( + 'environment', + 'Environment', + "Is the project's environment information accurate?", + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e25ee711bf0802d9a9bdb82dce040eb', + { + icon: GlobeIcon, + navigate: '/settings/versions', + shown: (_project, projectV3) => + (projectV3?.environment?.length ?? 0) !== 1 && !projectV3?.minecraft_server, + }, + [ + prose(mdText('environment/environment-multiple')), -export default environmentMultiple + group().children( + button('side_types_inaccurate', 'Inaccurate') + .shown(({ project }) => + project.project_types.includes('mod') || project.project_types.includes('modpack'), + ) + .weight(800) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('environment/inaccurate')), + ), + ], +) diff --git a/packages/moderation/src/data/stages/environment/environment.ts b/packages/moderation/src/data/stages/environment/environment.ts index 03eaf46c29..2f8ac67964 100644 --- a/packages/moderation/src/data/stages/environment/environment.ts +++ b/packages/moderation/src/data/stages/environment/environment.ts @@ -1,33 +1,30 @@ import { GlobeIcon } from '@modrinth/assets' -import type { ButtonAction } from '../../../types/actions' -import type { Stage } from '../../../types/stage' +import { button, group, mdMsg, mdText, prose, stage } from '../../../types/node' -const environment: Stage = { - title: "Environment", - hint: "Is the project's environment information accurate?", - id: 'environment', - navigate: '/settings/environment', - icon: GlobeIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e25ee711bf0802d9a9bdb82dce040eb', - text: async () => - (await import('../../messages/checklist-text/environment/environment.md?raw')).default, - shouldShow: (project, projectV3) => - (projectV3?.environment?.length ?? 0) === 1 && !projectV3?.minecraft_server, - actions: [ - { - id: 'side_types_inaccurate', - type: 'button', - label: 'Inaccurate', - weight: 800, - suggestedStatus: 'flagged', - severity: 'low', - shouldShow: (project) => project.project_type === 'mod' || project.project_type === 'modpack', - message: async () => - (await import('../../messages/checklist-messages/environment/inaccurate.md?raw')).default, - } as ButtonAction, - ], -} +export default stage( + 'environment', + 'Environment', + "Is the project's environment information accurate?", + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e25ee711bf0802d9a9bdb82dce040eb', + { + icon: GlobeIcon, + navigate: '/settings/environment', + shown: (_project, projectV3) => + (projectV3?.environment?.length ?? 0) === 1 && !projectV3?.minecraft_server, + }, + [ + prose(mdText('environment/environment')), -export default environment + group().children( + button('side_types_inaccurate', 'Inaccurate') + .shown(({ project }) => + project.project_types.includes('mod') || project.project_types.includes('modpack'), + ) + .weight(800) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('environment/inaccurate')), + ), + ], +) diff --git a/packages/moderation/src/data/stages/permissions.ts b/packages/moderation/src/data/stages/permissions.ts index 078a881240..8fe3319a6a 100644 --- a/packages/moderation/src/data/stages/permissions.ts +++ b/packages/moderation/src/data/stages/permissions.ts @@ -1,55 +1,38 @@ import { SignatureIcon } from '@modrinth/assets' -import type { Stage } from '../../types/stage' +import { button, group, mdMsg, stage } from '../../types/node' -const permissions: Stage = { - title: "Modpack Permissions", - hint: 'Does this projects external content have any issues?', - id: 'permissions', - icon: SignatureIcon, - guidance_url: 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892', - navigate: '/settings/permissions', - actions: [ - { - id: 'invalid-permissions', - type: 'button', - label: 'Invalid permissions', - weight: 2000, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => - projectV3.project_types?.includes('modpack') && !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/externals-permissions/invalid.md?raw')) - .default, - }, - { - id: 'prohibited-extrernal-content', - type: 'button', - label: 'Prohibited externals', - weight: 2001, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => - projectV3.project_types?.includes('modpack') && !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/externals-permissions/prohibited.md?raw')) - .default, - }, - { - id: 'missing-permissions', - type: 'button', - label: 'Missing permissions', - weight: 2002, - suggestedStatus: 'rejected', - severity: 'high', - shouldShow: (project, projectV3) => - projectV3.project_types?.includes('modpack') && !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/externals-permissions/missing.md?raw')) - .default, - }, - ], -} +const isModpack = ({ project }: { project: { project_types: string[]; minecraft_server?: unknown } }) => + project.project_types.includes('modpack') && !project.minecraft_server + +export default stage( + 'permissions', + 'Modpack Permissions', + 'Does this project\'s external content have any issues?', + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892', + { icon: SignatureIcon, navigate: '/settings/permissions' }, + [ + group().children( + button('invalid_permissions', 'Invalid permissions') + .shown(isModpack) + .weight(2000) + .suggestedStatus('rejected') + .severity('high') + .message(mdMsg('externals-permissions/invalid')), -export default permissions + button('prohibited_external_content', 'Prohibited externals') + .shown(isModpack) + .weight(2001) + .suggestedStatus('rejected') + .severity('high') + .message(mdMsg('externals-permissions/prohibited')), + + button('missing_permissions', 'Missing permissions') + .shown(isModpack) + .weight(2002) + .suggestedStatus('rejected') + .severity('high') + .message(mdMsg('externals-permissions/missing')), + ), + ], +) diff --git a/packages/moderation/src/data/stages/post-approval.ts b/packages/moderation/src/data/stages/post-approval.ts index d8802471c2..d2af1d24cd 100644 --- a/packages/moderation/src/data/stages/post-approval.ts +++ b/packages/moderation/src/data/stages/post-approval.ts @@ -1,128 +1,66 @@ import { ScaleIcon } from '@modrinth/assets' -import type { ButtonAction } from '../../types/actions' -import type { Stage } from '../../types/stage' +import { button, group, mdMsg, stage, text } from '../../types/node' -const postApproval: Stage = { - title: 'Post-Approval', - hint: 'Issue warnings, notices, or takedowns?', - id: 'post-approval', - icon: ScaleIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#3475ee711bf080c5a13cda0b1e4ae9ed', - shouldShow: (project) => project.status === 'approved', - actions: [ - { - id: 'issue_warning', - type: 'button', - label: 'Issue warning', - weight: 3000, - suggestedStatus: 'approved', - severity: 'low', - message: async () => - (await import('../messages/checklist-messages/post-approval/issue-warning.md?raw')).default, - }, - { - id: 'missed_deadline', - type: 'button', - label: 'Missed due date', - weight: -999, - suggestedStatus: 'flagged', - severity: 'high', - message: async () => - (await import('../messages/checklist-messages/post-approval/missed-deadline.md?raw')) - .default, - disablesActions: ['issue_warning', 'metadata_issue'], - relevantExtraInput: [ - { - label: 'What status is the project being set to?', - variable: 'STATUS', - required: true, - }, - ], - }, - { - id: 'metadata_issue', - type: 'button', - label: 'Incorrect metadata', - weight: 0, - suggestedStatus: 'approved', - severity: 'low', - message: async () => - (await import('../messages/checklist-messages/post-approval/metadata-issue.md?raw')) - .default, - enablesActions: [ - { - id: 'dependencies', - type: 'button', - label: 'Missing Dependencies', - weight: 1, - severity: 'low', - message: async () => - (await import('../messages/checklist-messages/misc-metadata/dependencies.md?raw')) - .default, - relevantExtraInput: [ - { - label: 'Dependency Name', - variable: 'DEPENDENCY_NAME', - required: true, - }, - { - label: 'Dependency Link', - variable: 'DEPENDENCY_LINK', - required: true, - }, - ], - }, - { - id: 'mc_versions', - type: 'button', - label: 'Game versions', - weight: 2, - severity: 'low', - message: async () => - (await import('../messages/checklist-messages/misc-metadata/mc-versions.md?raw')) - .default, - relevantExtraInput: [ - { - label: 'Provide more details about game versions issue?', - variable: 'SPECIFICS', - required: false, - large: true, - }, - ], - }, - { - id: 'loaders', - type: 'button', - label: 'Loaders', - weight: 3, - severity: 'low', - message: async () => - (await import('../messages/checklist-messages/misc-metadata/loaders.md?raw')).default, - relevantExtraInput: [ - { - label: 'Provide more details about loaders issue?', - variable: 'SPECIFICS', - required: false, - large: true, - }, - ], - }, - { - id: 'license', - type: 'button', - label: 'Inconsistent Licensing', - weight: 4, - severity: 'low', - message: async () => - ( - await import('../messages/checklist-messages/misc-metadata/inconsistent-license.md?raw') - ).default, - }, - ], - } as ButtonAction, - ], -} +export default stage( + 'post-approval', + 'Post-Approval', + 'Issue warnings, notices, or takedowns?', + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#3475ee711bf080c5a13cda0b1e4ae9ed', + { + icon: ScaleIcon, + shown: (project) => project.status === 'approved', + }, + [ + group().children( + button('issue_warning', 'Issue warning') + .weight(3000) + .suggestedStatus('approved') + .severity('low') + .message(mdMsg('post-approval/issue-warning')), + + button('missed_deadline', 'Missed due date') + .weight(-999) + .suggestedStatus('flagged') + .severity('high') + .message(mdMsg('post-approval/missed-deadline', (ctx) => ({ STATUS: ctx.state.status }))) + .children(text('status', 'What status is the project being set to?').required()), + + button('metadata_issue', 'Incorrect metadata') + .weight(0) + .suggestedStatus('approved') + .severity('low') + .message(mdMsg('post-approval/metadata-issue')) + .children( + button('dependencies', 'Missing Dependencies') + .weight(1) + .severity('low') + .message(mdMsg('misc-metadata/dependencies', (ctx) => ({ + DEPENDENCY_NAME: ctx.state.dependency_name, + DEPENDENCY_LINK: ctx.state.dependency_link, + }))) + .children( + text('dependency_name', 'Dependency name').required(), + text('dependency_link', 'Dependency link').required(), + ), -export default postApproval + button('mc_versions', 'Game versions') + .weight(2) + .severity('low') + .message(mdMsg('misc-metadata/mc-versions', (ctx) => ({ SPECIFICS: ctx.state.specifics }))) + .children(text('specifics', 'More details about the game versions issue?')), + + button('loaders', 'Loaders') + .weight(3) + .severity('low') + .message(mdMsg('misc-metadata/loaders', (ctx) => ({ SPECIFICS: ctx.state.specifics }))) + .children(text('specifics', 'More details about the loaders issue?')), + + button('license', 'Inconsistent Licensing') + .weight(4) + .severity('low') + .message(mdMsg('misc-metadata/inconsistent-license')), + ), + ), + ], +) diff --git a/packages/moderation/src/data/stages/rule-following.ts b/packages/moderation/src/data/stages/rule-following.ts index c71978eaf1..6dc7c44f8c 100644 --- a/packages/moderation/src/data/stages/rule-following.ts +++ b/packages/moderation/src/data/stages/rule-following.ts @@ -1,462 +1,110 @@ import { ListBulletedIcon } from '@modrinth/assets' -import type { ButtonAction, MultiSelectChipsAction } from '../../types/actions' -import type { Stage } from '../../types/stage' +import { button, chips, group, markdown, mdMsg, stage, toggle } from '../../types/node' -const ruleFollowing: Stage = { - title: 'Rule Following', - hint: 'Does this project violate the rules?', - id: 'rule-following', - icon: ListBulletedIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080709084f6269835607f', - navigate: '/moderation', - actions: [ - { - id: 'paid_access_server', - type: 'button', - label: 'Paid access server', - weight: 0, - suggestedStatus: 'rejected', - severity: 'critical', - shouldShow(project, projectV3) { - return !!projectV3?.minecraft_server - }, - message: async () => - (await import('../messages/checklist-messages/paid-access-server.md?raw')).default, - }, - { - id: 'prohibited_content', - type: 'button', - label: 'Prohibited Content', - weight: 100, - suggestedStatus: 'rejected', - severity: 'critical', - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content-header.md?raw') - ).default.trimEnd(), - enablesActions: [ - { - id: 'prohibited_content_options', - type: 'multi-select-chips', - label: 'Which Prohibited Content rules does this project violate?', - joinWith: '\n', - options: [ - { - label: 'Objectionable', - weight: 101, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/objectionable.md?raw') - ).default, - }, - { - label: 'Discriminatory or Explicit', - weight: 102, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/discriminatory.md?raw') - ).default, - }, - { - label: 'IP Infringement', - weight: 103, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/ip-infringement.md?raw') - ).default, - }, - { - label: 'Rights Violation', - weight: 104, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/legal-rights.md?raw') - ).default, - }, - { - label: 'Illegal Activity', - weight: 105, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/illegal-activity.md?raw') - ).default, - }, - { - label: 'Harmful or Deceptive', - weight: 106, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/harmful.md?raw') - ).default, - }, - { - label: 'Misleading claims', - weight: 107, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/misleading.md?raw') - ).default, - }, - { - label: 'Impersonation', - weight: 108, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/impersonation.md?raw') - ).default, - }, - { - label: 'False Endorsement', - weight: 109, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/false-endorsement.md?raw') - ).default, - }, - { - label: 'Profanity', - weight: 110, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/profanity.md?raw') - ).default, - }, - { - label: 'Undisclosed Data Upload', - weight: 111, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/undisclosed-upload.md?raw') - ).default, - }, - { - label: 'Mojang Bypass', - weight: 112, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/prohibited-content/mojang-bypass.md?raw') - ).default, - }, - ], - } as MultiSelectChipsAction, - ], - } as ButtonAction, - { - id: 'cheat_or_hack_advertising', - type: 'button', - label: 'Hacks', - weight: 150, - suggestedStatus: 'rejected', - severity: 'critical', - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/cheat-or-hack-advertising.md?raw') - ).default, - } as ButtonAction, - { - id: 'server_side_opt_out', - type: 'button', - label: 'Opt-out', - weight: 200, - suggestedStatus: 'flagged', - severity: 'high', - message: async () => - (await import('../messages/checklist-messages/rule-breaking/server-side-opt-out.md?raw')) - .default, - } as ButtonAction, - { - id: 'server_side_opt_in', - type: 'button', - label: 'Opt-in', - weight: 300, - suggestedStatus: 'flagged', - severity: 'high', - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/server-side-opt-in-header.md?raw') - ).default.trimEnd(), - enablesActions: [ - { - id: 'server_side_opt_in_options', - type: 'multi-select-chips', - label: 'Which features of this project require a Server-side Opt-in?', - joinWith: '\n', - options: [ - { - label: 'X-ray', - weight: 301, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/x-ray.md?raw') - ).default, - }, - { - label: 'Aim Assist', - weight: 302, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/aim-bot.md?raw') - ).default, - }, - { - label: 'Movement', - weight: 303, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/movement.md?raw') - ).default, - }, - { - label: 'PvP', - weight: 304, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/pvp.md?raw') - ).default, - }, - { - label: 'Anti 3.x', - weight: 305, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/hiding-mods.md?raw') - ).default, - }, - { - label: 'Dupe', - weight: 306, - message: async () => - ( - await import('../messages/checklist-messages/rule-breaking/server-side-opt-in/item-duplication.md?raw') - ).default, - }, - ], - } as MultiSelectChipsAction, - ], - } as ButtonAction, - { - id: 'prohibited_content', - type: 'button', - label: 'Prohibited Content', - weight: 100, - suggestedStatus: 'rejected', - severity: 'critical', - message: async () => - ( - await import('../messages/rule-breaking/prohibited-content-header.md?raw') - ).default.trimEnd(), - enablesActions: [ - { - id: 'prohibited_content_options', - type: 'multi-select-chips', - label: 'Which Prohibited Content rules does this project violate?', - joinWith: '\n', - options: [ - { - label: 'Objectionable', - weight: 101, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/objectionable.md?raw')) - .default, - }, - { - label: 'Discriminatory or Explicit', - weight: 102, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/discriminatory.md?raw')) - .default, - }, - { - label: 'IP Infringement', - weight: 103, - message: async () => - ( - await import('../messages/rule-breaking/prohibited-content/ip-infringement.md?raw') - ).default, - }, - { - label: 'Rights Violation', - weight: 104, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/legal-rights.md?raw')) - .default, - }, - { - label: 'Illegal Activity', - weight: 105, - message: async () => - ( - await import('../messages/rule-breaking/prohibited-content/illegal-activity.md?raw') - ).default, - }, - { - label: 'Harmful or Deceptive', - weight: 106, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/harmful.md?raw')) - .default, - }, - { - label: 'Misleading claims', - weight: 107, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/misleading.md?raw')) - .default, - }, - { - label: 'Impersonation', - weight: 108, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/impersonation.md?raw')) - .default, - }, - { - label: 'False Endorsement', - weight: 109, - message: async () => - ( - await import('../messages/rule-breaking/prohibited-content/false-endorsement.md?raw') - ).default, - }, - { - label: 'Profanity', - weight: 110, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/profanity.md?raw')) - .default, - }, - { - label: 'Undisclosed Data Upload', - weight: 111, - message: async () => - ( - await import('../messages/rule-breaking/prohibited-content/undisclosed-upload.md?raw') - ).default, - }, - { - label: 'Mojang Bypass', - weight: 112, - message: async () => - (await import('../messages/rule-breaking/prohibited-content/mojang-bypass.md?raw')) - .default, - }, - ], - } as MultiSelectChipsAction, - ], - } as ButtonAction, - { - id: 'server_side_opt_out', - type: 'button', - label: 'Opt-out', - weight: 200, - suggestedStatus: 'flagged', - severity: 'high', - message: async () => - (await import('../messages/rule-breaking/server-side-opt-out.md?raw')).default, - } as ButtonAction, - { - id: 'server_side_opt_in', - type: 'button', - label: 'Opt-in', - weight: 300, - suggestedStatus: 'flagged', - severity: 'high', - message: async () => - ( - await import('../messages/rule-breaking/server-side-opt-in-header.md?raw') - ).default.trimEnd(), - enablesActions: [ - { - id: 'server_side_opt_in_options', - type: 'multi-select-chips', - label: 'Which features of this project require a Server-side Opt-in?', - joinWith: '\n', - options: [ - { - label: 'X-ray', - weight: 301, - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in/x-ray.md?raw')).default, - }, - { - label: 'Aim Assist', - weight: 302, - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in/aim-bot.md?raw')) - .default, - }, - { - label: 'Movement', - weight: 303, - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in/movement.md?raw')) - .default, - }, - { - label: 'PvP', - weight: 304, - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in/pvp.md?raw')).default, - }, - { - label: 'Anti 3.x', - weight: 305, - message: async () => - (await import('../messages/rule-breaking/server-side-opt-in/hiding-mods.md?raw')) - .default, - }, - { - label: 'Dupe', - weight: 306, - message: async () => - ( - await import('../messages/rule-breaking/server-side-opt-in/item-duplication.md?raw') - ).default, - }, - ], - } as MultiSelectChipsAction, - ], - } as ButtonAction, - { - id: 'excessive_languages', - type: 'button', - label: 'Excessive languages', - weight: 0, - suggestedStatus: 'flagged', - severity: 'low', - shouldShow(project, projectV3) { - return ( - !!projectV3?.minecraft_server && - !!projectV3?.minecraft_server?.languages?.length && - projectV3?.minecraft_server?.languages?.length > 4 +export default stage( + 'rule-following', + 'Rule Following', + 'Does this project violate the rules?', + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080709084f6269835607f', + { + icon: ListBulletedIcon, + navigate: '/moderation', + }, + [ + group().children( + button('paid_access_server', 'Paid access server') + .shown(({ project }) => !!project.minecraft_server) + .weight(0) + .suggestedStatus('rejected') + .severity('critical') + .message(mdMsg('paid-access-server')), + + button('prohibited_content', 'Prohibited Content') + .weight(100) + .suggestedStatus('rejected') + .severity('critical') + .message(async (ctx) => { + const header = (await mdMsg('rule-breaking/prohibited-content-header')(ctx)).trimEnd() + const selected = ctx.state.options + if (!(selected instanceof Set) || selected.size === 0) return header + const items = await Promise.all( + [...selected].map((id) => mdMsg(`rule-breaking/prohibited-content/${id}`)(ctx)), + ) + return `${header}\n${items.join('\n')}` + }) + .children( + chips('options', 'Which Prohibited Content rules does this project violate?').children( + toggle('objectionable', 'Objectionable'), + toggle('discriminatory', 'Discriminatory or Explicit'), + toggle('ip-infringement', 'IP Infringement'), + toggle('legal-rights', 'Rights Violation'), + toggle('illegal-activity', 'Illegal Activity'), + toggle('harmful', 'Harmful or Deceptive'), + toggle('misleading', 'Misleading claims'), + toggle('impersonation', 'Impersonation'), + toggle('false-endorsement', 'False Endorsement'), + toggle('profanity', 'Profanity'), + toggle('undisclosed-upload', 'Undisclosed Data Upload'), + toggle('mojang-bypass', 'Mojang Bypass'), + ), + ), + + button('cheat_or_hack_advertising', 'Hacks') + .weight(150) + .suggestedStatus('rejected') + .severity('critical') + .message(mdMsg('rule-breaking/cheat-or-hack-advertising')), + + button('server_side_opt_out', 'Opt-out') + .weight(200) + .suggestedStatus('flagged') + .severity('high') + .message(mdMsg('rule-breaking/server-side-opt-out')), + + button('server_side_opt_in', 'Opt-in') + .weight(300) + .suggestedStatus('flagged') + .severity('high') + .message(async (ctx) => { + const header = (await mdMsg('rule-breaking/server-side-opt-in-header')(ctx)).trimEnd() + const selected = ctx.state.options + if (!(selected instanceof Set) || selected.size === 0) return header + const items = await Promise.all( + [...selected].map((id) => mdMsg(`rule-breaking/server-side-opt-in/${id}`)(ctx)), + ) + return `${header}\n${items.join('\n')}` + }) + .children( + chips('options', 'Which features require a Server-side Opt-in?').children( + toggle('x-ray', 'X-ray'), + toggle('aim-bot', 'Aim Assist'), + toggle('movement', 'Movement'), + toggle('pvp', 'PvP'), + toggle('hiding-mods', 'Anti 3.x'), + toggle('item-duplication', 'Dupe'), + ), + ), + + button('excessive_languages', 'Excessive languages') + .shown(({ project }) => + !!project.minecraft_server && + !!project.minecraft_server?.languages?.length && + project.minecraft_server.languages.length > 4, ) - }, - message: async () => - ( - await import('../messages/checklist-messages/misc-metadata/excessive_languages-server.md?raw') - ).default, - }, - { - id: 'rule_breaking_other', - type: 'button', - label: 'Other', - weight: 0, - suggestedStatus: 'rejected', - severity: 'critical', - message: async () => - (await import('../messages/checklist-messages/rule-breaking.md?raw')).default, - relevantExtraInput: [ - { - label: 'Please explain to the user how it infringes on our content rules.', - variable: 'MESSAGE', - required: true, - large: true, - }, - ], - } as ButtonAction, - ], -} + .weight(0) + .suggestedStatus('flagged') + .severity('low') + .message(mdMsg('misc-metadata/excessive_languages-server')), -export default ruleFollowing + button('rule_breaking_other', 'Other') + .weight(0) + .suggestedStatus('rejected') + .severity('critical') + .message(mdMsg('rule-breaking', (ctx) => ({ MESSAGE: ctx.state.message }))) + .children( + markdown('message', 'Explain how it infringes on content rules.').required(), + ), + ), + ], +) diff --git a/packages/moderation/src/data/stages/status-alerts.ts b/packages/moderation/src/data/stages/status-alerts.ts index e2c3414df2..0e64c3756d 100644 --- a/packages/moderation/src/data/stages/status-alerts.ts +++ b/packages/moderation/src/data/stages/status-alerts.ts @@ -1,97 +1,55 @@ import { TriangleAlertIcon } from '@modrinth/assets' -import type { ButtonAction } from '../../types/actions' -import type { Stage } from '../../types/stage' +import { button, group, mdMsg, mdText, prose, stage } from '../../types/node' -const statusAlerts: Stage = { - title: 'Status Alerts', - hint: `Is anything else affecting this project's status?`, - id: 'status-alerts', - icon: TriangleAlertIcon, - text: async () => (await import('../messages/checklist-text/status-alerts/text.md?raw')).default, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080968699c397e470eca6', - navigate: '/moderation', - actions: [ - { - id: 'status_corrections_applied', - type: 'button', - label: 'Corrections applied', - weight: -999999, - suggestedStatus: 'approved', - disablesActions: ['status_private_use', 'status_account_issues'], - shouldShow: (project) => project.status !== 'approved', - message: async () => - (await import('../messages/checklist-messages/status-alerts/fixed.md?raw')).default, - } as ButtonAction, - { - id: 'status_corrections_applied-approved', - type: 'button', - label: 'Corrections applied', - weight: -999999, - suggestedStatus: 'approved', - disablesActions: ['status_private_use', 'status_account_issues'], - shouldShow: (project) => project.status === 'approved', - message: async () => - (await import('../messages/checklist-messages/status-alerts/fixed-approved.md?raw')) - .default, - } as ButtonAction, - { - id: 'status_private_use', - type: 'button', - label: 'Private use', - weight: -999999, - suggestedStatus: 'flagged', - disablesActions: ['status_corrections_applied', 'status_account_issues'], - shouldShow: (project, projectV3) => !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/status-alerts/private/private.md?raw')) - .default, - } as ButtonAction, - { - id: 'status_private_use-server', - type: 'button', - label: 'Private community', - weight: -999999, - suggestedStatus: 'flagged', - disablesActions: ['status_corrections_applied', 'status_account_issues'], - shouldShow: (project, projectV3) => !!projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/status-alerts/private/private-server.md?raw')) - .default, - } as ButtonAction, - { - id: 'status_server_use', - type: 'button', - label: 'Server use', - weight: -999999, - shouldShow: (project, projectV3) => - project.project_type === 'modpack' && !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/status-alerts/serverpack.md?raw')).default, - } as ButtonAction, - { - id: 'status_account_issues', - type: 'button', - label: 'Account issues', - weight: -999999, - suggestedStatus: 'rejected', - disablesActions: ['status_corrections_applied', 'status_private_use'], - message: async () => - (await import('../messages/checklist-messages/status-alerts/account_issues.md?raw')) - .default, - } as ButtonAction, - { - id: 'status_automod_confusion', - type: 'button', - label: `Automod confusion`, - weight: -999999, - shouldShow: (project, projectV3) => !projectV3?.minecraft_server, - message: async () => - (await import('../messages/checklist-messages/status-alerts/automod_confusion.md?raw')) - .default, - } as ButtonAction, - ], -} +export default stage( + 'status-alerts', + 'Status Alerts', + `Is anything else affecting this project's status?`, + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#2e35ee711bf080968699c397e470eca6', + { icon: TriangleAlertIcon, navigate: '/moderation' }, + [ + prose(mdText('status-alerts/text')), + + group().children( + button('corrections_applied', 'Corrections applied') + .shown(({ projectV2 }) => projectV2.status !== 'approved') + .weight(-999999) + .suggestedStatus('approved') + .message(mdMsg('status-alerts/fixed')), + + button('corrections_applied_approved', 'Corrections applied') + .shown(({ projectV2 }) => projectV2.status === 'approved') + .weight(-999999) + .suggestedStatus('approved') + .message(mdMsg('status-alerts/fixed-approved')), + + button('private_use', 'Private use') + .shown(({ project }) => !project.minecraft_server) + .weight(-999999) + .suggestedStatus('flagged') + .message(mdMsg('status-alerts/private/private')), -export default statusAlerts + button('private_use_server', 'Private community') + .shown(({ project }) => !!project.minecraft_server) + .weight(-999999) + .suggestedStatus('flagged') + .message(mdMsg('status-alerts/private/private-server')), + + button('server_use', 'Server use') + .shown(({ project }) => project.project_types.includes('modpack') && !project.minecraft_server) + .weight(-999999) + .message(mdMsg('status-alerts/serverpack')), + + button('account_issues', 'Account issues') + .weight(-999999) + .suggestedStatus('rejected') + .message(mdMsg('status-alerts/account_issues')), + + button('automod_confusion', 'Automod confusion') + .shown(({ project }) => !project.minecraft_server) + .weight(-999999) + .message(mdMsg('status-alerts/automod_confusion')), + ), + ], +) diff --git a/packages/moderation/src/data/stages/undefined-project.ts b/packages/moderation/src/data/stages/undefined-project.ts index 150ac92f6e..22797bcd6c 100644 --- a/packages/moderation/src/data/stages/undefined-project.ts +++ b/packages/moderation/src/data/stages/undefined-project.ts @@ -1,28 +1,23 @@ import { XIcon } from '@modrinth/assets' -import type { Stage } from '../../types/stage' +import { button, group, mdMsg, stage } from '../../types/node' -const undefinedProjectStage: Stage = { - title: 'Undefined Project', - hint: 'This project is undefined!', - id: 'undefined-project', - icon: XIcon, - guidance_url: - 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#3475ee711bf080018bf3d822a2f51a35', - navigate: '/versions', - shouldShow: (project, projectV3) => project.versions.length === 0 && !projectV3?.minecraft_server, - actions: [ - { - id: 'undefined_no_versions', - type: 'button', - label: 'No Versions', - weight: -100, - suggestedStatus: 'rejected', - message: async () => - (await import('../messages/checklist-messages/undefined-project/no_versions.md?raw')) - .default, - }, +export default stage( + 'undefined-project', + 'Undefined Project', + 'This project is undefined!', + 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892#3475ee711bf080018bf3d822a2f51a35', + { + icon: XIcon, + navigate: '/versions', + shown: (project, projectV3) => project.versions.length === 0 && !projectV3?.minecraft_server, + }, + [ + group().children( + button('no_versions', 'No Versions') + .weight(-100) + .suggestedStatus('rejected') + .message(mdMsg('undefined-project/no_versions')), + ), ], -} - -export default undefinedProjectStage +) From 6fbb11f51adbe1408958e353b4f5e5b3f8f170fd Mon Sep 17 00:00:00 2001 From: chyzman Date: Fri, 10 Jul 2026 23:02:39 -0400 Subject: [PATCH 21/85] forgot to only do modpack permissions for modpacks --- .../moderation/src/data/stages/permissions.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/moderation/src/data/stages/permissions.ts b/packages/moderation/src/data/stages/permissions.ts index 8fe3319a6a..2ba7a87fb8 100644 --- a/packages/moderation/src/data/stages/permissions.ts +++ b/packages/moderation/src/data/stages/permissions.ts @@ -2,33 +2,32 @@ import { SignatureIcon } from '@modrinth/assets' import { button, group, mdMsg, stage } from '../../types/node' -const isModpack = ({ project }: { project: { project_types: string[]; minecraft_server?: unknown } }) => - project.project_types.includes('modpack') && !project.minecraft_server - export default stage( 'permissions', 'Modpack Permissions', - 'Does this project\'s external content have any issues?', + "Does this project's external content have any issues?", 'https://www.notion.so/2e15ee711bf080e4a41df61bbab49892', - { icon: SignatureIcon, navigate: '/settings/permissions' }, + { + icon: SignatureIcon, + navigate: '/settings/permissions', + shown: (_project, projectV3) => + (projectV3?.project_types?.includes('modpack') ?? false) && !projectV3?.minecraft_server, + }, [ group().children( button('invalid_permissions', 'Invalid permissions') - .shown(isModpack) .weight(2000) .suggestedStatus('rejected') .severity('high') .message(mdMsg('externals-permissions/invalid')), button('prohibited_external_content', 'Prohibited externals') - .shown(isModpack) .weight(2001) .suggestedStatus('rejected') .severity('high') .message(mdMsg('externals-permissions/prohibited')), button('missing_permissions', 'Missing permissions') - .shown(isModpack) .weight(2002) .suggestedStatus('rejected') .severity('high') From b85d56f8055a3dfaaf2864169910d246925dab5a Mon Sep 17 00:00:00 2001 From: chyzman Date: Sat, 11 Jul 2026 13:39:12 -0400 Subject: [PATCH 22/85] Idk why I wasn't just mutating the builder --- packages/moderation/src/types/node.ts | 67 ++++++++++++++++----------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/packages/moderation/src/types/node.ts b/packages/moderation/src/types/node.ts index 3d5d88cb31..3b2e027892 100644 --- a/packages/moderation/src/types/node.ts +++ b/packages/moderation/src/types/node.ts @@ -129,60 +129,73 @@ export class NodeBuilder { this.data = { ...data } } - weight(w: number): NodeBuilder { - return new NodeBuilder({ ...this.data, weight: w }) + weight(w: number): this { + this.data.weight = w + return this } - message(fn: MessageFn): NodeBuilder { - return new NodeBuilder({ ...this.data, message: fn }) + message(fn: MessageFn): this { + this.data.message = fn + return this } - content(fn: ContentFn): NodeBuilder { - return new NodeBuilder({ ...this.data, content: fn }) + content(fn: ContentFn): this { + this.data.content = fn + return this } - column(): NodeBuilder { - return new NodeBuilder({ ...this.data, layout: 'column' }) + column(): this { + this.data.layout = 'column' + return this } - children(...nodes: Resolvable[]): NodeBuilder { - return new NodeBuilder({ ...this.data, children: nodes.map(resolve) }) + children(...nodes: Resolvable[]): this { + this.data.children = nodes.map(resolve) + return this } - childrenFn(fn: ChildrenFn): NodeBuilder { - return new NodeBuilder({ ...this.data, childrenFn: fn }) + childrenFn(fn: ChildrenFn): this { + this.data.childrenFn = fn + return this } - shown(fn: ShowFn): NodeBuilder { - return new NodeBuilder({ ...this.data, shown: fn }) + shown(fn: ShowFn): this { + this.data.shown = fn + return this } - enabled(fn: ShowFn): NodeBuilder { - return new NodeBuilder({ ...this.data, enabled: fn }) + enabled(fn: ShowFn): this { + this.data.enabled = fn + return this } - suggestedStatus(s: ModerationStatus): NodeBuilder { - return new NodeBuilder({ ...this.data, suggestedStatus: s }) + suggestedStatus(s: ModerationStatus): this { + this.data.suggestedStatus = s + return this } - severity(s: ModerationSeverity): NodeBuilder { - return new NodeBuilder({ ...this.data, severity: s }) + severity(s: ModerationSeverity): this { + this.data.severity = s + return this } - defaultChecked(v = true): NodeBuilder { - return new NodeBuilder({ ...this.data, defaultChecked: v }) + defaultChecked(v = true): this { + this.data.defaultChecked = v + return this } - placeholder(p: string): NodeBuilder { - return new NodeBuilder({ ...this.data, placeholder: p }) + placeholder(p: string): this { + this.data.placeholder = p + return this } - required(v = true): NodeBuilder { - return new NodeBuilder({ ...this.data, required: v }) + required(v = true): this { + this.data.required = v + return this } build(): Node { - return { ...this.data } as Node + return this.data as Node } } From e5b24d031af4f54f4e57ed88c0c7452f3f412c66 Mon Sep 17 00:00:00 2001 From: chyzman Date: Sat, 11 Jul 2026 17:29:53 -0400 Subject: [PATCH 23/85] actual node type hierarchy + action abstraction + improve message/text handling --- .../checklist/ModerationChecklist.vue | 133 +++---- .../ui/moderation/checklist/NodeRenderer.vue | 231 +++++++----- packages/moderation/src/data/checklist.ts | 8 +- .../messages}/categories/inaccurate.md | 0 .../categories/optimization_misused.md | 0 .../categories/resolutions_misused.md | 0 .../accessability/headers-as-body.md | 0 .../description/accessability/image-only.md | 0 .../non-english/non-english-server.md | 0 .../accessability/non-english/non-english.md | 0 .../accessability/non-standard-text.md | 0 .../messages}/description/clarity.md | 0 .../description/insufficient/custom.md | 0 .../description/insufficient/fork.md | 0 .../description/insufficient/header.md | 0 .../description/insufficient/packs.md | 0 .../description/insufficient/projects.md | 0 .../description/insufficient/servers.md | 0 .../messages}/description/unfinished.md | 0 .../messages}/environment/inaccurate.md | 0 .../externals-permissions/invalid.md | 0 .../externals-permissions/missing.md | 0 .../externals-permissions/non-commercial.md | 0 .../externals-permissions/prohibited.md | 0 .../messages}/gallery/insufficient.md | 0 .../messages}/gallery/not-relevant.md | 0 .../license/invalid_link-custom_license.md | 0 .../messages}/license/invalid_link.md | 0 .../messages}/license/no_source-fork.md | 0 .../messages}/license/no_source.md | 0 .../messages}/links/discord/inaccessible.md | 0 .../messages}/links/discord/misused.md | 0 .../messages}/links/donations/inaccessible.md | 0 .../messages}/links/donations/misused.md | 0 .../messages}/links/issues/inaccessible.md | 0 .../messages}/links/issues/misused.md | 0 .../messages}/links/misused.md | 0 .../messages}/links/not_accessible-discord.md | 0 .../messages}/links/not_accessible-source.md | 0 .../messages}/links/not_accessible.md | 0 .../messages}/links/site/inaccessible.md | 0 .../messages}/links/site/misused.md | 0 .../messages}/links/source/inaccessible.md | 0 .../messages}/links/source/misused.md | 0 .../messages}/links/store/inaccessible.md | 0 .../messages}/links/store/misused.md | 0 .../messages}/links/wiki/inaccessible.md | 0 .../messages}/links/wiki/misused.md | 0 .../messages}/misc-metadata/dependencies.md | 0 .../excessive_languages-server.md | 0 .../misc-metadata/inconsistent-license.md | 0 .../messages}/misc-metadata/loaders.md | 0 .../messages}/misc-metadata/mc-versions.md | 0 .../messages}/paid-access-server.md | 0 .../messages}/post-approval/issue-warning.md | 0 .../messages}/post-approval/metadata-issue.md | 0 .../post-approval/missed-deadline.md | 0 .../custom_server_overrides-prohibited.md | 0 ...stom_server_overrides-verification-list.md | 0 .../custom_server_overrides-verification.md | 0 .../custom_server_permissions.md | 0 .../messages}/reupload/fork.md | 0 .../identity_verification-server.md | 0 .../identity_verification.md | 0 .../messages}/reupload/insufficient_fork.md | 0 .../reupload/proof_of_permissions.md | 0 .../messages}/reupload/reupload.md | 0 .../messages}/rule-breaking.md | 0 .../cheat-or-hack-advertising.md | 0 .../prohibited-content-header.md | 0 .../prohibited-content/discriminatory.md | 0 .../prohibited-content/false-endorsement.md | 0 .../prohibited-content/harmful.md | 0 .../prohibited-content/illegal-activity.md | 0 .../prohibited-content/impersonation.md | 0 .../prohibited-content/ip-infringement.md | 0 .../prohibited-content/legal-rights.md | 0 .../prohibited-content/misleading.md | 0 .../prohibited-content/mojang-bypass.md | 0 .../prohibited-content/objectionable.md | 0 .../prohibited-content/profanity.md | 0 .../prohibited-content/undisclosed-upload.md | 0 .../server-side-opt-in-header.md | 0 .../server-side-opt-in/aim-bot.md | 0 .../server-side-opt-in/hiding-mods.md | 0 .../server-side-opt-in/item-duplication.md | 0 .../server-side-opt-in/movement.md | 0 .../rule-breaking/server-side-opt-in/pvp.md | 0 .../rule-breaking/server-side-opt-in/x-ray.md | 0 .../rule-breaking/server-side-opt-out.md | 0 .../messages}/slug/misused.md | 0 .../messages}/status-alerts/account_issues.md | 0 .../status-alerts/automod_confusion.md | 0 .../demonetized/demonetized-modpack.md | 0 .../status-alerts/demonetized/demonetized.md | 0 .../messages}/status-alerts/fixed-approved.md | 0 .../messages}/status-alerts/fixed.md | 0 .../status-alerts/private/private-server.md | 0 .../status-alerts/private/private.md | 0 .../messages}/status-alerts/serverpack.md | 0 .../status-alerts/tec/source_request-bins.md | 0 .../status-alerts/tec/source_request-obfs.md | 0 .../messages}/summary/formatting.md | 0 .../messages}/summary/insufficient.md | 0 .../messages}/summary/non-english.md | 0 .../messages}/summary/repeat-ip.md | 0 .../messages}/summary/repeat-title.md | 0 .../messages}/title/minecraft-branding.md | 0 .../messages}/title/similarities-fork.md | 0 .../messages}/title/similarities-modpack.md | 0 .../messages}/title/similarities.md | 0 .../messages}/title/useless-info.md | 0 .../undefined-project/no_versions.md | 0 .../versions/alternate_versions-additional.md | 0 .../versions/alternate_versions-mono.md | 0 .../versions/alternate_versions-primary.md | 0 .../alternate_versions-server-additional.md | 0 .../versions/alternate_versions-server.md | 0 .../versions/alternate_versions-zip.md | 0 .../messages}/versions/broken_version.md | 0 .../versions/incorrect_additional_files.md | 0 .../messages}/versions/incorrect_loader.md | 0 .../messages}/versions/invalid-datapacks.md | 0 .../messages}/versions/invalid-modpacks.md | 0 .../versions/invalid-resourcepacks.md | 0 .../messages}/versions/redist_libs.md | 0 .../messages}/versions/unsupported_project.md | 0 .../messages}/versions/vanilla_assets.md | 0 .../text}/categories.md | 0 .../text}/environment/environment-multiple.md | 0 .../text}/environment/environment.md | 0 .../text}/licensing.md | 0 .../text}/links/base.md | 0 .../text}/links/donation/donation.md | 0 .../text}/links/donation/donations.md | 0 .../text}/links/server.md | 0 .../text}/status-alerts/text.md | 0 .../text}/summary/summary.md | 0 .../text}/title-slug.md | 0 .../text}/title-slug/slug.md | 0 .../text}/title-slug/title.md | 0 .../moderation/src/data/stages/categories.ts | 72 ++-- .../moderation/src/data/stages/description.ts | 129 ++++--- .../moderation/src/data/stages/environment.ts | 37 ++ .../environment/environment-multiple.ts | 30 -- .../data/stages/environment/environment.ts | 30 -- .../moderation/src/data/stages/gallery.ts | 36 +- .../moderation/src/data/stages/license.ts | 58 ++-- packages/moderation/src/data/stages/links.ts | 69 ++-- .../moderation/src/data/stages/permissions.ts | 64 ++-- .../src/data/stages/post-approval.ts | 89 +++-- .../moderation/src/data/stages/reupload.ts | 148 ++++---- .../src/data/stages/rule-following.ts | 123 ++++--- .../src/data/stages/status-alerts.ts | 100 ++++-- .../moderation/src/data/stages/summary.ts | 67 ++-- .../moderation/src/data/stages/title-slug.ts | 81 +++-- .../src/data/stages/undefined-project.ts | 25 +- .../moderation/src/data/stages/versions.ts | 189 ++++++---- packages/moderation/src/types/node.ts | 328 +++++++++++------- 159 files changed, 1206 insertions(+), 841 deletions(-) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/categories/inaccurate.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/categories/optimization_misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/categories/resolutions_misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/accessability/headers-as-body.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/accessability/image-only.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/accessability/non-english/non-english-server.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/accessability/non-english/non-english.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/accessability/non-standard-text.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/clarity.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/insufficient/custom.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/insufficient/fork.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/insufficient/header.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/insufficient/packs.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/insufficient/projects.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/insufficient/servers.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/description/unfinished.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/environment/inaccurate.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/externals-permissions/invalid.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/externals-permissions/missing.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/externals-permissions/non-commercial.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/externals-permissions/prohibited.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/gallery/insufficient.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/gallery/not-relevant.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/license/invalid_link-custom_license.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/license/invalid_link.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/license/no_source-fork.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/license/no_source.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/discord/inaccessible.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/discord/misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/donations/inaccessible.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/donations/misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/issues/inaccessible.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/issues/misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/not_accessible-discord.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/not_accessible-source.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/not_accessible.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/site/inaccessible.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/site/misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/source/inaccessible.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/source/misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/store/inaccessible.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/store/misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/wiki/inaccessible.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/links/wiki/misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/misc-metadata/dependencies.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/misc-metadata/excessive_languages-server.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/misc-metadata/inconsistent-license.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/misc-metadata/loaders.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/misc-metadata/mc-versions.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/paid-access-server.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/post-approval/issue-warning.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/post-approval/metadata-issue.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/post-approval/missed-deadline.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/reupload/custom_server/custom_server_overrides-prohibited.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/reupload/custom_server/custom_server_overrides-verification-list.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/reupload/custom_server/custom_server_overrides-verification.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/reupload/custom_server/custom_server_permissions.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/reupload/fork.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/reupload/identity-verification/identity_verification-server.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/reupload/identity-verification/identity_verification.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/reupload/insufficient_fork.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/reupload/proof_of_permissions.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/reupload/reupload.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/cheat-or-hack-advertising.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content-header.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/discriminatory.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/false-endorsement.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/harmful.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/illegal-activity.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/impersonation.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/ip-infringement.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/legal-rights.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/misleading.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/mojang-bypass.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/objectionable.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/profanity.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/prohibited-content/undisclosed-upload.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/server-side-opt-in-header.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/server-side-opt-in/aim-bot.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/server-side-opt-in/hiding-mods.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/server-side-opt-in/item-duplication.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/server-side-opt-in/movement.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/server-side-opt-in/pvp.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/server-side-opt-in/x-ray.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/rule-breaking/server-side-opt-out.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/slug/misused.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/account_issues.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/automod_confusion.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/demonetized/demonetized-modpack.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/demonetized/demonetized.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/fixed-approved.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/fixed.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/private/private-server.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/private/private.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/serverpack.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/tec/source_request-bins.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/status-alerts/tec/source_request-obfs.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/summary/formatting.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/summary/insufficient.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/summary/non-english.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/summary/repeat-ip.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/summary/repeat-title.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/title/minecraft-branding.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/title/similarities-fork.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/title/similarities-modpack.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/title/similarities.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/title/useless-info.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/undefined-project/no_versions.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/alternate_versions-additional.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/alternate_versions-mono.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/alternate_versions-primary.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/alternate_versions-server-additional.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/alternate_versions-server.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/alternate_versions-zip.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/broken_version.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/incorrect_additional_files.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/incorrect_loader.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/invalid-datapacks.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/invalid-modpacks.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/invalid-resourcepacks.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/redist_libs.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/unsupported_project.md (100%) rename packages/moderation/src/data/messages/{checklist-messages => checklist/messages}/versions/vanilla_assets.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/categories.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/environment/environment-multiple.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/environment/environment.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/licensing.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/links/base.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/links/donation/donation.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/links/donation/donations.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/links/server.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/status-alerts/text.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/summary/summary.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/title-slug.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/title-slug/slug.md (100%) rename packages/moderation/src/data/messages/{checklist-text => checklist/text}/title-slug/title.md (100%) create mode 100644 packages/moderation/src/data/stages/environment.ts delete mode 100644 packages/moderation/src/data/stages/environment/environment-multiple.ts delete mode 100644 packages/moderation/src/data/stages/environment/environment.ts diff --git a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue index 5569eeb77b..c2b9560a1f 100644 --- a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue @@ -12,7 +12,7 @@ />
@@ -48,7 +48,7 @@ - + @@ -69,7 +69,7 @@
- +
@@ -150,7 +150,7 @@