feat: add publishing checklist storybok

This commit is contained in:
tdgao
2026-09-02 14:13:19 -06:00
parent b889de7799
commit 15fbe1328e
5 changed files with 266 additions and 4 deletions
@@ -0,0 +1,240 @@
import type { Labrinth } from '@modrinth/api-client'
import {
type Nag,
projectDescriptionValidationRules,
projectDisclosureTextValidationRules,
projectDisclosureValidationRules,
projectGalleryTextValidationRules,
projectGalleryValidationRules,
projectIconValidationRules,
projectLicenseValidationRules,
projectLinksValidationRules,
projectModerationValidationRules,
projectNameValidationRules,
projectPermissionsValidationRules,
projectServerSettingsValidationRules,
projectSummaryValidationRules,
projectTagsValidationRules,
projectVersionValidationRules,
toNags,
type ValidationRuleDefinition,
} from '@modrinth/moderation'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ModerationProjectNags from './ModerationProjectNags.vue'
const categories = [
'adventure',
'cursed',
'decoration',
'economy',
'equipment',
'food',
'game-mechanics',
'library',
'magic',
]
function createProject(status: Labrinth.Projects.v2.ProjectStatus): Labrinth.Projects.v2.Project {
return {
id: 'storybook-publishing-checklist',
slug: 'storybook-publishing-checklist',
project_type: 'mod',
actualProjectType: 'mod',
team: 'storybook-team',
organization: null,
title: 'Publishing Checklist Preview',
description: 'A project fixture for previewing the publishing checklist.',
body: '',
published: '2026-01-01T00:00:00.000Z',
updated: '2026-01-01T00:00:00.000Z',
status,
license: {
id: 'MIT',
name: 'MIT License',
},
client_side: 'required',
server_side: 'optional',
downloads: 0,
followers: 0,
categories,
additional_categories: [],
game_versions: [],
loaders: [],
versions: [],
gallery: [],
thread_id: 'storybook-thread',
monetization_status: 'monetized',
}
}
function createProjectV3(status: Labrinth.Projects.v2.ProjectStatus): Labrinth.Projects.v3.Project {
return {
id: 'storybook-publishing-checklist',
slug: 'storybook-publishing-checklist',
project_types: ['mod'],
games: ['minecraft:java-edition'],
team_id: 'storybook-team',
name: 'Publishing Checklist Preview',
summary: 'A short summary.',
description: '',
published: '2026-01-01T00:00:00.000Z',
updated: '2026-01-01T00:00:00.000Z',
status,
license: {
id: 'MIT',
name: 'MIT License',
},
downloads: 0,
followers: 0,
categories,
additional_categories: [],
loaders: [],
mrpack_loaders: [],
versions: [],
link_urls: {},
gallery: [],
thread_id: 'storybook-thread',
monetization_status: 'monetized',
side_types_migration_review_status: 'reviewed',
environment: ['unknown'],
}
}
const tags = {
categories: [],
rejectedStatuses: ['rejected', 'withheld'],
gameVersions: [],
loaders: [],
}
const previewValues = {
count: 3,
domain: 'example.com',
fullUrl: 'https://example.com/prohibited-link',
languageCount: 12,
length: 12,
maxTagCount: 8,
minChars: 50,
projectType: 'mod',
status: 'rejected',
tagCount: 9,
tags: '16x|32x',
totalAvailableTags: 20,
type: 'mod',
url: 'https://example.com/prohibited-link',
value: 'example',
}
function createPreviewNags(rules: Readonly<Record<string, ValidationRuleDefinition>>): Nag[] {
return Object.entries(rules).flatMap(([code, rule]) =>
toNags([
{
code,
message: rule.presentation.message,
rule,
values: previewValues,
},
]),
)
}
const validationRuleSets = [
projectNameValidationRules,
projectSummaryValidationRules,
projectIconValidationRules,
projectGalleryTextValidationRules,
projectGalleryValidationRules,
projectDescriptionValidationRules,
projectLicenseValidationRules,
projectLinksValidationRules,
projectPermissionsValidationRules,
projectServerSettingsValidationRules,
projectTagsValidationRules,
projectVersionValidationRules,
projectDisclosureTextValidationRules,
projectDisclosureValidationRules,
projectModerationValidationRules,
]
const everyNag: Nag[] = [
...validationRuleSets.flatMap(createPreviewNags),
{
id: 'resubmit-for-review-preview',
title: 'Resubmit for review',
description: () =>
"Your project has been rejected by Modrinth's staff. Address the moderation team's feedback before resubmitting.",
status: 'special-submit-action',
shouldShow: () => true,
link: {
path: 'moderation',
title: 'Visit moderation page',
shouldShow: () => true,
},
},
]
const meta = {
title: 'Website/Moderation/PublishingChecklist',
component: ModerationProjectNags,
decorators: [
(story) => ({
components: { story },
template: '<div class="mx-auto w-full p-4" style="max-width: 1100px"><story /></div>',
}),
],
parameters: {
layout: 'fullscreen',
},
args: {
project: createProject('draft'),
projectV3: createProjectV3('draft'),
versions: [],
collapsed: false,
disableHorizontalScroll: true,
routeName: 'type-project',
tags,
},
render: (args) => ({
components: { ModerationProjectNags },
setup() {
const collapsed = ref(args.collapsed)
return { args, collapsed }
},
template: /* html */ `
<ModerationProjectNags
v-bind="args"
:collapsed="collapsed"
@toggle-collapsed="collapsed = !collapsed"
/>
`,
}),
} satisfies Meta<typeof ModerationProjectNags>
export default meta
type Story = StoryObj<typeof meta>
export const EntirePublishingChecklist: Story = {
parameters: {
docs: {
description: {
story: 'The complete expanded checklist for an unfinished draft project.',
},
},
},
}
export const EveryNag: Story = {
args: {
nags: everyNag,
},
parameters: {
docs: {
description: {
story:
'Every publishing-checklist validation nag plus the submit and resubmit actions, including combinations that cannot normally appear together.',
},
},
},
}
@@ -38,13 +38,16 @@
<Accordion :open-by-default="!collapsed" content-class="mt-4">
<div class="relative">
<div
v-if="!disableHorizontalScroll"
class="nag-scroll-shadow-left pointer-events-none absolute bottom-0 left-0 top-0 z-10 w-8 bg-surface-3 transition-opacity duration-200"
:class="showLeftNagShadow ? 'opacity-100' : 'opacity-0'"
/>
<div
ref="nagScroller"
class="flex w-full gap-2 overflow-x-auto overflow-y-hidden pb-2"
class="flex w-full gap-2 pb-2"
:class="{
'grid grid-cols-4': disableHorizontalScroll,
'overflow-x-auto overflow-y-hidden': !disableHorizontalScroll,
'cursor-grab select-none': canScrollNags,
'is-dragging': draggingNags,
}"
@@ -111,6 +114,7 @@
</div>
</div>
<div
v-if="!disableHorizontalScroll"
class="nag-scroll-shadow-right pointer-events-none absolute bottom-0 right-0 top-0 z-10 w-8 bg-surface-3 transition-opacity duration-200"
:class="showRightNagShadow ? 'opacity-100' : 'opacity-0'"
/>
@@ -148,8 +152,10 @@ interface Props {
project: Labrinth.Projects.v2.Project
projectV3: Labrinth.Projects.v3.Project
versions?: Labrinth.Versions.v3.Version[]
nags?: Nag[]
currentMember?: Labrinth.Projects.v3.TeamMember | null
collapsed?: boolean
disableHorizontalScroll?: boolean
routeName?: string
tags: Tags
}
@@ -209,6 +215,7 @@ const props = withDefaults(defineProps<Props>(), {
versions: () => [],
currentMember: null,
collapsed: false,
disableHorizontalScroll: false,
routeName: '',
})
@@ -235,7 +242,7 @@ let suppressNagClickTimeout: ReturnType<typeof setTimeout> | null = null
function updateNagScrollShadows() {
const el = nagScroller.value
if (!el) {
if (!el || props.disableHorizontalScroll) {
canScrollNags.value = false
showLeftNagShadow.value = false
showRightNagShadow.value = false
@@ -250,7 +257,7 @@ function updateNagScrollShadows() {
function onNagWheel(event: WheelEvent) {
const el = nagScroller.value
if (!el || el.scrollWidth <= el.clientWidth) return
if (props.disableHorizontalScroll || !el || el.scrollWidth <= el.clientWidth) return
const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY
event.preventDefault()
@@ -260,6 +267,7 @@ function onNagWheel(event: WheelEvent) {
function onNagPointerDown(event: PointerEvent) {
const el = nagScroller.value
if (
props.disableHorizontalScroll ||
!el ||
el.scrollWidth <= el.clientWidth + 1 ||
event.pointerType === 'touch' ||
@@ -335,6 +343,11 @@ watch(nagScroller, (el, previousEl) => {
nextTick(updateNagScrollShadows)
})
watch(
() => props.disableHorizontalScroll,
() => nextTick(updateNagScrollShadows),
)
const nagContext = computed<NagContext>(() => ({
project: props.project,
projectV3: props.projectV3,
@@ -355,6 +368,8 @@ async function submitForReview() {
}
const applicableNags = computed<Nag[]>(() => {
if (props.nags) return props.nags
return getNags(nagContext.value).filter((nag) => {
return nag.shouldShow(nagContext.value)
})
+1
View File
@@ -15,6 +15,7 @@
"@modrinth/api-client": "workspace:*",
"@modrinth/assets": "workspace:*",
"@modrinth/utils": "workspace:*",
"@tanstack/vue-query": "5.101.4",
"franc-min": "^6.2.0",
"linkify-it": "^5.0.0",
"node-html-parser": "^9.0.1",
+4 -1
View File
@@ -10,7 +10,10 @@ const config: StorybookConfig = {
docgen: false,
},
},
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
stories: [
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
'../../../apps/frontend/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: ['@storybook/addon-themes', '@storybook/addon-a11y'],
viteFinal: async (config) =>
mergeConfig(config, {
+3
View File
@@ -563,6 +563,9 @@ importers:
'@modrinth/utils':
specifier: workspace:*
version: link:../utils
'@tanstack/vue-query':
specifier: 5.101.4
version: 5.101.4(vue@3.5.27(typescript@5.9.3))
franc-min:
specifier: ^6.2.0
version: 6.2.0