mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
feat: hook up validators with input fields
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
:disabled="hasHitLimit"
|
||||
@update:model-value="updatedName()"
|
||||
/>
|
||||
<ValidationMessage :check="nameValidation" />
|
||||
</div>
|
||||
<label for="slug" class="flex flex-col gap-2.5">
|
||||
<span class="text-md font-semibold text-contrast">
|
||||
@@ -108,6 +109,7 @@
|
||||
:placeholder="formatMessage(messages.summaryPlaceholder)"
|
||||
:disabled="hasHitLimit"
|
||||
/>
|
||||
<ValidationMessage :check="summaryValidation" />
|
||||
<span>{{ formatMessage(messages.summaryDescription) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
@@ -148,6 +150,8 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { computed, defineAsyncComponent, h } from 'vue'
|
||||
|
||||
import ValidationMessage from '~/components/ValidationMessage.vue'
|
||||
import { validateProjectText } from '~/composables/project-text-validation'
|
||||
import { generateUrlSlug } from '~/utils/slugs'
|
||||
|
||||
import CreateLimitAlert from './CreateLimitAlert.vue'
|
||||
@@ -303,8 +307,12 @@ const visibilities = ref<VisibilityOption[]>([
|
||||
])
|
||||
const visibility = ref<VisibilityOption>(visibilities.value[0])
|
||||
|
||||
const nameValidation = computed(() => validateProjectText(name.value))
|
||||
const summaryValidation = computed(() => validateProjectText(description.value))
|
||||
|
||||
const disableCreate = computed(() => {
|
||||
if (hasHitLimit.value) return true
|
||||
if (nameValidation.value || summaryValidation.value) return true
|
||||
if (!name.value.trim() || !slug.value.trim()) return true
|
||||
if (description.value.trim().length < 3) return true
|
||||
if (owner.value !== 'self' && !organizations.value.find((org) => org.id === owner.value))
|
||||
@@ -392,6 +400,7 @@ async function fetchOrganizations() {
|
||||
}
|
||||
|
||||
async function createProject() {
|
||||
if (disableCreate.value) return
|
||||
startLoading()
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { validateNonStandardText, validateProfanity } from '@modrinth/moderation'
|
||||
import { defineMessages, type MessageDescriptor } from '@modrinth/ui'
|
||||
|
||||
export interface ProjectTextValidationResult {
|
||||
severity: 'error'
|
||||
message: MessageDescriptor
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
slur: {
|
||||
id: 'project.text-validation.slur',
|
||||
defaultMessage: 'Slurs are not allowed.',
|
||||
},
|
||||
profanity: {
|
||||
id: 'project.text-validation.profanity',
|
||||
defaultMessage: 'Profanity is not allowed.',
|
||||
},
|
||||
nonStandardText: {
|
||||
id: 'project.text-validation.non-standard-text',
|
||||
defaultMessage: 'Non-standard text characters are not allowed.',
|
||||
},
|
||||
})
|
||||
|
||||
export function validateProjectText(
|
||||
text: string | null | undefined,
|
||||
): ProjectTextValidationResult | null {
|
||||
if (!text) return null
|
||||
|
||||
const profanity = validateProfanity(text)
|
||||
if (profanity.slurCount > 0) {
|
||||
return { severity: 'error', message: messages.slur }
|
||||
}
|
||||
if (profanity.profanityCount > 0) {
|
||||
return { severity: 'error', message: messages.profanity }
|
||||
}
|
||||
|
||||
if (!validateNonStandardText(text).valid) {
|
||||
return { severity: 'error', message: messages.nonStandardText }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -17,13 +17,10 @@
|
||||
</div>
|
||||
<MarkdownEditor
|
||||
v-model="current.description"
|
||||
:disabled="
|
||||
!currentMember ||
|
||||
(currentMember?.permissions! & TeamMemberPermission.EDIT_BODY) !==
|
||||
TeamMemberPermission.EDIT_BODY
|
||||
"
|
||||
:disabled="!hasPermission"
|
||||
:on-image-upload="onUploadHandler"
|
||||
/>
|
||||
<ValidationMessage :check="descriptionValidation" class="mt-2" />
|
||||
<div v-if="descriptionWarning" class="mt-2">
|
||||
<SettingsInlineWarning>
|
||||
{{ descriptionWarning }}
|
||||
@@ -34,6 +31,7 @@
|
||||
:original="saved"
|
||||
:modified="current"
|
||||
:saving="saving"
|
||||
:can-save="canSave"
|
||||
@reset="reset"
|
||||
@save="save"
|
||||
/>
|
||||
@@ -56,7 +54,9 @@ import { TeamMemberPermission } from '@modrinth/utils'
|
||||
import { computed, useTemplateRef } from 'vue'
|
||||
|
||||
import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue'
|
||||
import ValidationMessage from '~/components/ValidationMessage.vue'
|
||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||
import { validateProjectText } from '~/composables/project-text-validation'
|
||||
import { fileDeclaresAi } from '~/helpers/c2pa'
|
||||
|
||||
const { projectV2: project, currentMember, patchProject } = injectProjectPageContext()
|
||||
@@ -64,7 +64,14 @@ const aiImageWarningModal = useTemplateRef('aiImageWarningModal')
|
||||
|
||||
useProjectSettingsHeadTitle(commonProjectSettingsMessages.description)
|
||||
|
||||
const { saved, current, saving, hasChanges, reset, save } = useSavable(
|
||||
const {
|
||||
saved,
|
||||
current,
|
||||
saving,
|
||||
hasChanges,
|
||||
reset,
|
||||
save: saveForm,
|
||||
} = useSavable(
|
||||
() => ({ description: project.value.body }),
|
||||
async ({ description }) => {
|
||||
await patchProject({ body: description })
|
||||
@@ -73,6 +80,20 @@ const { saved, current, saving, hasChanges, reset, save } = useSavable(
|
||||
|
||||
const { confirmLeaveModal } = usePageLeaveSafety(hasChanges)
|
||||
|
||||
const hasPermission = computed(
|
||||
() =>
|
||||
!!currentMember.value &&
|
||||
(currentMember.value.permissions & TeamMemberPermission.EDIT_BODY) ===
|
||||
TeamMemberPermission.EDIT_BODY,
|
||||
)
|
||||
const descriptionValidation = computed(() => validateProjectText(current.value.description))
|
||||
const canSave = computed(() => hasPermission.value && !descriptionValidation.value)
|
||||
|
||||
async function save() {
|
||||
if (!canSave.value) return
|
||||
await saveForm()
|
||||
}
|
||||
|
||||
const descriptionWarning = computed(() => {
|
||||
const text = current.value.description?.trim() || ''
|
||||
const charCount = countText(text)
|
||||
|
||||
@@ -14,13 +14,23 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import ValidationMessage from '~/components/ValidationMessage.vue'
|
||||
import { validateProjectText } from '~/composables/project-text-validation'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const { projectV2: project, patchProject } = injectProjectPageContext()
|
||||
|
||||
useProjectSettingsHeadTitle(commonProjectSettingsMessages.general)
|
||||
|
||||
const { saved, current, saving, hasChanges, reset, save } = useSavable(
|
||||
const {
|
||||
saved,
|
||||
current,
|
||||
saving,
|
||||
hasChanges,
|
||||
reset,
|
||||
save: saveForm,
|
||||
} = useSavable(
|
||||
() => ({
|
||||
title: project.value.title,
|
||||
tagline: project.value.description,
|
||||
@@ -38,6 +48,15 @@ const { saved, current, saving, hasChanges, reset, save } = useSavable(
|
||||
|
||||
const { confirmLeaveModal } = usePageLeaveSafety(hasChanges)
|
||||
|
||||
const titleValidation = computed(() => validateProjectText(current.value.title))
|
||||
const taglineValidation = computed(() => validateProjectText(current.value.tagline))
|
||||
const canSave = computed(() => !titleValidation.value && !taglineValidation.value)
|
||||
|
||||
async function save() {
|
||||
if (!canSave.value) return
|
||||
await saveForm()
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
nameTitle: {
|
||||
id: 'project.settings.general.name.title',
|
||||
@@ -129,6 +148,7 @@ const placeholder = computed(() => placeholders[placeholderIndex.value] ?? place
|
||||
:original="saved"
|
||||
:modified="current"
|
||||
:saving="saving"
|
||||
:can-save="canSave"
|
||||
@reset="reset"
|
||||
@save="save"
|
||||
/>
|
||||
@@ -152,6 +172,7 @@ const placeholder = computed(() => placeholders[placeholderIndex.value] ?? place
|
||||
wrapper-class="flex-grow"
|
||||
/>
|
||||
</div>
|
||||
<ValidationMessage :check="titleValidation" class="mt-2" />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<SettingsLabel
|
||||
@@ -167,6 +188,7 @@ const placeholder = computed(() => placeholders[placeholderIndex.value] ?? place
|
||||
:maxlength="120"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<ValidationMessage :check="taglineValidation" class="mt-2" />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<SettingsLabel id="project-url" :title="messages.urlTitle" />
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
:maxlength="2048"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<ValidationMessage :check="nameValidation" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -60,6 +61,7 @@
|
||||
:disabled="!hasPermission"
|
||||
resize="vertical"
|
||||
/>
|
||||
<ValidationMessage :check="summaryValidation" class="mt-2" />
|
||||
<div v-if="summaryWarning" class="my-2">
|
||||
<SettingsInlineWarning>
|
||||
{{ summaryWarning }}
|
||||
@@ -289,6 +291,7 @@
|
||||
:original="original"
|
||||
:modified="modified"
|
||||
:saving="saving"
|
||||
:can-save="canSave"
|
||||
@reset="resetChanges"
|
||||
@save="handleSave"
|
||||
/>
|
||||
@@ -325,7 +328,9 @@ import {
|
||||
import { fileIsValid, formatProjectStatus } from '@modrinth/utils'
|
||||
|
||||
import AiImageWarningModal from '~/components/ui/AiImageWarningModal.vue'
|
||||
import ValidationMessage from '~/components/ValidationMessage.vue'
|
||||
import { useAuth } from '~/composables/auth.js'
|
||||
import { validateProjectText } from '~/composables/project-text-validation'
|
||||
import { fileDeclaresAi } from '~/helpers/c2pa'
|
||||
import { getProjectTypeForUrl } from '~/helpers/projects.js'
|
||||
|
||||
@@ -397,6 +402,12 @@ const hasPermission = computed(() => {
|
||||
return ((currentMember.value?.permissions ?? 0) & EDIT_DETAILS) === EDIT_DETAILS
|
||||
})
|
||||
|
||||
const nameValidation = computed(() => validateProjectText(name.value))
|
||||
const summaryValidation = computed(() => validateProjectText(summary.value))
|
||||
const canSave = computed(
|
||||
() => hasPermission.value && !nameValidation.value && !summaryValidation.value,
|
||||
)
|
||||
|
||||
const monetizationToggleDisabled = computed(() => !hasPermission.value || isForceDemonetized.value)
|
||||
|
||||
const hasDeletePermission = computed(() => {
|
||||
@@ -529,6 +540,7 @@ async function updateMonetizationStatus(status) {
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!canSave.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
const hasPatchChanges = Object.keys(basePatchData.value).length > 0
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
:disabled="!hasPermission || licenseId === 'LicenseRef-Unknown'"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<LinkCheckMessage :check="effectiveLicenseCheck" />
|
||||
<ValidationMessage :check="effectiveLicenseCheck" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -165,7 +165,7 @@ import {
|
||||
import { builtinLicenses, formatProjectType, TeamMemberPermission } from '@modrinth/utils'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import LinkCheckMessage from '@/components/LinkCheckMessage.vue'
|
||||
import ValidationMessage from '@/components/ValidationMessage.vue'
|
||||
|
||||
const { projectV2: project, currentMember, patchProject } = injectProjectPageContext()
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
maxlength="2048"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<LinkCheckMessage :check="siteCheck" />
|
||||
<ValidationMessage :check="siteCheck" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label id="server-store" title="Your server's store page.">
|
||||
@@ -32,7 +32,7 @@
|
||||
maxlength="2048"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<LinkCheckMessage :check="storeCheck" />
|
||||
<ValidationMessage :check="storeCheck" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label
|
||||
@@ -52,7 +52,7 @@
|
||||
maxlength="2048"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<LinkCheckMessage :check="wikiCheck" />
|
||||
<ValidationMessage :check="wikiCheck" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label id="server-discord" title="An invitation link to your Discord server.">
|
||||
@@ -67,7 +67,7 @@
|
||||
maxlength="2048"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<LinkCheckMessage :check="discordInviteCheck" />
|
||||
<ValidationMessage :check="discordInviteCheck" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
:maxlength="2048"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<LinkCheckMessage :check="issuesCheck" />
|
||||
<ValidationMessage :check="issuesCheck" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label
|
||||
@@ -112,7 +112,7 @@
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<LinkCheckMessage :check="sourceCheck" />
|
||||
<ValidationMessage :check="sourceCheck" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label
|
||||
@@ -132,7 +132,7 @@
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<LinkCheckMessage :check="wikiCheck" />
|
||||
<ValidationMessage :check="wikiCheck" />
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label id="project-discord-invite" title="An invitation link to your Discord server.">
|
||||
@@ -147,7 +147,7 @@
|
||||
placeholder="Enter a valid URL"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<LinkCheckMessage :check="discordInviteCheck" />
|
||||
<ValidationMessage :check="discordInviteCheck" />
|
||||
</div>
|
||||
<span class="label">
|
||||
<span class="label__title">Donation links</span>
|
||||
@@ -179,7 +179,7 @@
|
||||
class="platform-selector !w-80"
|
||||
@update:model-value="updateDonationLinks"
|
||||
/>
|
||||
<LinkCheckMessage :check="donationCheckState(donationLink, index)" />
|
||||
<ValidationMessage :check="donationCheckState(donationLink, index)" />
|
||||
</div>
|
||||
</section>
|
||||
<UnsavedChangesPopup
|
||||
@@ -214,7 +214,7 @@ import {
|
||||
useSavable,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import LinkCheckMessage from '@/components/LinkCheckMessage.vue'
|
||||
import ValidationMessage from '@/components/ValidationMessage.vue'
|
||||
|
||||
const tags = useGeneratedState()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user