Skip to content

Conversation

@larbish
Copy link
Contributor

@larbish larbish commented Dec 24, 2025

Resolves #207

@vercel
Copy link
Contributor

vercel bot commented Dec 24, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
content-studio Ready Ready Preview, Comment Dec 24, 2025 10:03am

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 24, 2025

npm i https://pkg.pr.new/nuxt-content/studio/nuxt-studio@213

commit: 8e3a265

params = {
fsPath: newFsPath,
content: `# ${upperFirst(state.name)} file`,
content: getInitialContent(state.extension!, upperFirst(state.name)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses a non-null assertion on state.extension when calling getInitialContent(), but the extension can be undefined when a default extension is not provided (e.g., in MediaCardForm), causing a type mismatch at runtime.

View Details
📝 Patch Details
diff --git a/src/app/src/components/shared/item/ItemCardForm.vue b/src/app/src/components/shared/item/ItemCardForm.vue
index 9a98d87..16af581 100644
--- a/src/app/src/components/shared/item/ItemCardForm.vue
+++ b/src/app/src/components/shared/item/ItemCardForm.vue
@@ -60,46 +60,61 @@ const fullName = computed(() => {
   return isDirectory.value ? prefixedName : `${prefixedName}.${state.extension}`
 })
 
-const schema = computed(() => z.object({
-  name: z.string()
-    .min(1, t('studio.validation.nameEmpty'))
-    .refine((name: string) => !name.endsWith('.'), t('studio.validation.nameEndsWithDot'))
-    .refine((name: string) => !name.startsWith('/'), t('studio.validation.nameStartsWithSlash')),
-  extension: z.enum([...CONTENT_EXTENSIONS, ...MEDIA_EXTENSIONS] as [string, ...string[]]).nullish(),
-  prefix: z.preprocess(
-    val => val === '' ? null : val,
-    z.string()
-      .regex(/^\d+$/, t('studio.validation.prefixDigitsOnly'))
+const schema = computed(() => {
+  // For file creation/renaming, extension is required; for folders, it's not applicable
+  const extensionSchema = isDirectory.value
+    ? z.enum([...CONTENT_EXTENSIONS, ...MEDIA_EXTENSIONS] as [string, ...string[]]).nullish()
+    : z.union([
+        z.enum([...CONTENT_EXTENSIONS, ...MEDIA_EXTENSIONS] as [string, ...string[]]),
+        z.null(),
+        z.undefined(),
+      ])
       .refine(
-        (prefix: string | null | undefined) => {
-          if (prefix === null || prefix === undefined) {
-            return true
-          }
+        (ext: string | null | undefined) => ext !== null && ext !== undefined,
+        { message: t('studio.validation.extensionRequired') },
+      )
 
-          const num = Number(prefix)
+  return z.object({
+    name: z.string()
+      .min(1, t('studio.validation.nameEmpty'))
+      .refine((name: string) => !name.endsWith('.'), t('studio.validation.nameEndsWithDot'))
+      .refine((name: string) => !name.startsWith('/'), t('studio.validation.nameStartsWithSlash')),
+    extension: extensionSchema,
+    prefix: z.preprocess(
+      val => val === '' ? null : val,
+      z.string()
+        .regex(/^\d+$/, t('studio.validation.prefixDigitsOnly'))
+        .refine(
+          (prefix: string | null | undefined) => {
+            if (prefix === null || prefix === undefined) {
+              return true
+            }
+
+            const num = Number(prefix)
+
+            return Number.isInteger(num) && num >= 0
+          },
+          t('studio.validation.prefixNonNegativeInteger'),
+        )
+        .nullish(),
+    ),
+  }).refine(() => {
+    const siblings = props.parentItem.children?.filter(child => !child.hide) || []
+
+    const isDuplicate = siblings.some((sibling) => {
+      const siblingBaseName = sibling.fsPath.split('/').pop()
+      if (props.renamedItem && sibling.fsPath === props.renamedItem.fsPath) {
+        return false
+      }
+      return siblingBaseName === fullName.value
+    })
 
-          return Number.isInteger(num) && num >= 0
-        },
-        t('studio.validation.prefixNonNegativeInteger'),
-      )
-      .nullish(),
-  ),
-}).refine(() => {
-  const siblings = props.parentItem.children?.filter(child => !child.hide) || []
-
-  const isDuplicate = siblings.some((sibling) => {
-    const siblingBaseName = sibling.fsPath.split('/').pop()
-    if (props.renamedItem && sibling.fsPath === props.renamedItem.fsPath) {
-      return false
-    }
-    return siblingBaseName === fullName.value
+    return !isDuplicate
+  }, {
+    message: t('studio.validation.nameExists'),
+    path: ['name'],
   })
-
-  return !isDuplicate
-}, {
-  message: t('studio.validation.nameExists'),
-  path: ['name'],
-}))
+})
 
 type SchemaType = {
   name: string
@@ -225,7 +240,7 @@ async function onSubmit() {
     case StudioItemActionId.CreateDocument:
       params = {
         fsPath: newFsPath,
-        content: getInitialContent(state.extension!, upperFirst(state.name)),
+        content: getInitialContent(state.extension as string, upperFirst(state.name)),
       }
       break
     case StudioItemActionId.RenameItem:

Analysis

Extension validation missing in ItemCardForm allows undefined extension for file creation

What fails: ItemCardForm accepts undefined extension when creating media files, allowing invalid file creation with extensions like "myvideo.undefined" and passing undefined to getInitialContent() which expects a string parameter.

How to reproduce:

  1. In MediaCardForm (which uses ItemCardForm with config: { allowed: MEDIA_EXTENSIONS, editable: false } - no default)
  2. Attempt to create a new document (not rename, where renamedItem = null)
  3. The extension field is disabled (editable: false), so user cannot select an extension
  4. originalExtension computes to props.config.default which is undefined
  5. The Zod schema allows z.enum(...).nullish() - validation passes with undefined
  6. Submit button is enabled because validation passes
  7. On submit with CreateDocument action, calls getInitialContent(state.extension!, ...) with undefined

Result:

  • File created with name like "myvideo.undefined"
  • getInitialContent receives undefined instead of string
  • Type assertion masks the issue but doesn't prevent runtime problem

Expected:

  • Extension should be required when creating files
  • Form validation should prevent submission without a valid extension
  • Follows the pattern in ContentCardForm which provides a default

Fix applied: Modified src/app/src/components/shared/item/ItemCardForm.vue to conditionally require extension:

  • For file creation/renaming (!isDirectory): extension must be a valid string value
  • For folder creation (isDirectory): extension can be null/undefined
  • Validation now prevents form submission without selecting a valid extension

@larbish larbish merged commit 230e31e into main Dec 24, 2025
6 checks passed
@larbish larbish deleted the fix/file-creation branch December 24, 2025 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong JSON content on file creation

2 participants