mirror of
https://github.com/modrinth/code.git
synced 2026-09-05 06:19:11 +00:00
feat: add publishing checklist storybok
This commit is contained in:
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Generated
+3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user