@nuasite/cms 0.20.5 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editor.js +16886 -16613
- package/package.json +1 -1
- package/src/collection-scanner.ts +106 -20
- package/src/dev-middleware.ts +4 -14
- package/src/editor/api.ts +2 -0
- package/src/editor/components/fields.tsx +3 -2
- package/src/editor/components/frontmatter-fields.tsx +31 -20
- package/src/editor/components/markdown-editor-overlay.tsx +20 -3
- package/src/editor/components/prop-editor.tsx +353 -26
- package/src/editor/components/reference-picker.tsx +3 -13
- package/src/editor/components/seo-editor.tsx +0 -2
- package/src/editor/constants.ts +0 -13
- package/src/editor/editor.ts +1 -4
- package/src/editor/hooks/useBlockEditorHandlers.ts +1 -5
- package/src/editor/manifest.ts +10 -0
- package/src/editor/signals.ts +11 -0
- package/src/field-types.ts +42 -0
- package/src/handlers/markdown-ops.ts +13 -1
- package/src/index.ts +11 -0
- package/src/manifest-writer.ts +7 -0
- package/src/prop-types.ts +46 -0
- package/src/types.ts +4 -0
package/package.json
CHANGED
|
@@ -389,11 +389,10 @@ async function scanCollection(collectionPath: string, collectionName: string, co
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
|
-
*
|
|
393
|
-
* Returns
|
|
392
|
+
* Read and parse the Astro content config file, extracting schema blocks for each collection.
|
|
393
|
+
* Returns parsed blocks with collection names and their raw schema bodies.
|
|
394
394
|
*/
|
|
395
|
-
async function
|
|
396
|
-
const result = new Map<string, Map<string, { target: string; isArray: boolean }>>()
|
|
395
|
+
async function parseContentConfigSchemaBlocks(): Promise<Array<{ collectionName: string; schemaBody: string }>> {
|
|
397
396
|
const projectRoot = getProjectRoot()
|
|
398
397
|
|
|
399
398
|
for (const configPath of ['src/content/config.ts', 'src/content.config.ts']) {
|
|
@@ -401,7 +400,6 @@ async function parseContentConfigReferences(): Promise<Map<string, Map<string, {
|
|
|
401
400
|
const fullPath = path.join(projectRoot, configPath)
|
|
402
401
|
const content = await fs.readFile(fullPath, 'utf-8')
|
|
403
402
|
|
|
404
|
-
// Parse defineCollection blocks to extract schema bodies
|
|
405
403
|
const collectionBlocks = content.matchAll(
|
|
406
404
|
/(?:const\s+(\w+)\s*=\s*)?defineCollection\s*\(\s*\{[\s\S]*?schema\s*:\s*z\.object\s*\(\s*\{([\s\S]*?)\}\s*\)/g,
|
|
407
405
|
)
|
|
@@ -416,39 +414,125 @@ async function parseContentConfigReferences(): Promise<Map<string, Map<string, {
|
|
|
416
414
|
}
|
|
417
415
|
}
|
|
418
416
|
|
|
417
|
+
const blocks: Array<{ collectionName: string; schemaBody: string }> = []
|
|
419
418
|
for (const block of collectionBlocks) {
|
|
420
419
|
const varName = block[1]
|
|
421
420
|
const schemaBody = block[2]!
|
|
422
421
|
const collectionName = varName ? varToName.get(varName) : undefined
|
|
423
422
|
if (!collectionName) continue
|
|
424
|
-
|
|
425
|
-
const fields = new Map<string, { target: string; isArray: boolean }>()
|
|
426
|
-
const fieldRefs = schemaBody.matchAll(/(\w+)\s*:\s*(z\.array\s*\(\s*)?reference\s*\(\s*['"](\w+)['"]\s*\)/g)
|
|
427
|
-
for (const m of fieldRefs) {
|
|
428
|
-
fields.set(m[1]!, { target: m[3]!, isArray: !!m[2] })
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (fields.size > 0) {
|
|
432
|
-
result.set(collectionName, fields)
|
|
433
|
-
}
|
|
423
|
+
blocks.push({ collectionName, schemaBody })
|
|
434
424
|
}
|
|
435
425
|
|
|
436
|
-
if (
|
|
426
|
+
if (blocks.length > 0) return blocks
|
|
437
427
|
} catch {
|
|
438
428
|
// File doesn't exist, try next
|
|
439
429
|
}
|
|
440
430
|
}
|
|
431
|
+
return []
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Parse the Astro content config file to extract explicit reference() declarations.
|
|
436
|
+
* Returns a map: collectionName → { fieldName → { target, isArray } }
|
|
437
|
+
*/
|
|
438
|
+
function parseContentConfigReferences(
|
|
439
|
+
schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
|
|
440
|
+
): Map<string, Map<string, { target: string; isArray: boolean }>> {
|
|
441
|
+
const result = new Map<string, Map<string, { target: string; isArray: boolean }>>()
|
|
442
|
+
|
|
443
|
+
for (const { collectionName, schemaBody } of schemaBlocks) {
|
|
444
|
+
const fields = new Map<string, { target: string; isArray: boolean }>()
|
|
445
|
+
const fieldRefs = schemaBody.matchAll(/(\w+)\s*:\s*(z\.array\s*\(\s*)?reference\s*\(\s*['"](\w+)['"]\s*\)/g)
|
|
446
|
+
for (const m of fieldRefs) {
|
|
447
|
+
fields.set(m[1]!, { target: m[3]!, isArray: !!m[2] })
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (fields.size > 0) {
|
|
451
|
+
result.set(collectionName, fields)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
441
454
|
return result
|
|
442
455
|
}
|
|
443
456
|
|
|
457
|
+
/** Valid field type names exported by `field` helper from @nuasite/cms */
|
|
458
|
+
const FIELD_HELPER_TYPES = new Set(['image', 'url', 'email', 'color', 'date', 'datetime', 'time', 'textarea'])
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Parse the content config file to extract explicit field type hints:
|
|
462
|
+
* - `field.image(...)`, `field.url(...)`, etc. from @nuasite/cms
|
|
463
|
+
* - `z.enum([...])` for select options
|
|
464
|
+
*
|
|
465
|
+
* Returns a map: collectionName → fieldName → { type, options? }
|
|
466
|
+
*/
|
|
467
|
+
function parseContentConfigFieldTypes(
|
|
468
|
+
schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
|
|
469
|
+
): Map<string, Map<string, { type: FieldType; options?: string[] }>> {
|
|
470
|
+
const result = new Map<string, Map<string, { type: FieldType; options?: string[] }>>()
|
|
471
|
+
|
|
472
|
+
for (const { collectionName, schemaBody } of schemaBlocks) {
|
|
473
|
+
const fields = new Map<string, { type: FieldType; options?: string[] }>()
|
|
474
|
+
|
|
475
|
+
// Detect field.image(...), field.url(...), etc.
|
|
476
|
+
const fieldHelpers = schemaBody.matchAll(/(\w+)\s*:\s*field\.(\w+)\s*\(/g)
|
|
477
|
+
for (const m of fieldHelpers) {
|
|
478
|
+
const fieldName = m[1]!
|
|
479
|
+
const helperName = m[2]!
|
|
480
|
+
if (FIELD_HELPER_TYPES.has(helperName)) {
|
|
481
|
+
fields.set(fieldName, { type: helperName as FieldType })
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Detect z.enum(['a', 'b', 'c'])
|
|
486
|
+
const enumFields = schemaBody.matchAll(/(\w+)\s*:\s*z\.enum\s*\(\s*\[([\s\S]*?)\]\s*\)/g)
|
|
487
|
+
for (const m of enumFields) {
|
|
488
|
+
const fieldName = m[1]!
|
|
489
|
+
const enumBody = m[2]!
|
|
490
|
+
const options = [...enumBody.matchAll(/['"]([^'"]+)['"]/g)].map(o => o[1]!)
|
|
491
|
+
if (options.length > 0) {
|
|
492
|
+
fields.set(fieldName, { type: 'select', options })
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (fields.size > 0) {
|
|
497
|
+
result.set(collectionName, fields)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return result
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Apply field type overrides from config parsing to scanned collections.
|
|
505
|
+
*/
|
|
506
|
+
function applyConfigFieldTypes(
|
|
507
|
+
collections: Record<string, CollectionDefinition>,
|
|
508
|
+
schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
|
|
509
|
+
): void {
|
|
510
|
+
const configTypes = parseContentConfigFieldTypes(schemaBlocks)
|
|
511
|
+
for (const [collectionName, fieldTypes] of configTypes) {
|
|
512
|
+
const def = collections[collectionName]
|
|
513
|
+
if (!def) continue
|
|
514
|
+
for (const [fieldName, override] of fieldTypes) {
|
|
515
|
+
const field = def.fields.find(f => f.name === fieldName)
|
|
516
|
+
if (!field) continue
|
|
517
|
+
field.type = override.type
|
|
518
|
+
if (override.options) {
|
|
519
|
+
field.options = override.options
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
444
525
|
/**
|
|
445
526
|
* After all collections are scanned, detect reference fields.
|
|
446
527
|
* Prefers explicit reference() declarations from the content config file.
|
|
447
528
|
* Falls back to heuristic slug matching when no config is available.
|
|
448
529
|
*/
|
|
449
|
-
async function detectReferenceFields(
|
|
530
|
+
async function detectReferenceFields(
|
|
531
|
+
collections: Record<string, CollectionDefinition>,
|
|
532
|
+
schemaBlocks: Array<{ collectionName: string; schemaBody: string }>,
|
|
533
|
+
): Promise<void> {
|
|
450
534
|
// Try parsing the content config first — this is the source of truth
|
|
451
|
-
const configRefs =
|
|
535
|
+
const configRefs = parseContentConfigReferences(schemaBlocks)
|
|
452
536
|
if (configRefs.size > 0) {
|
|
453
537
|
for (const [collectionName, fieldRefs] of configRefs) {
|
|
454
538
|
const def = collections[collectionName]
|
|
@@ -673,8 +757,10 @@ export async function scanCollections(contentDir: string = 'src/content'): Promi
|
|
|
673
757
|
// Content directory doesn't exist or isn't readable
|
|
674
758
|
}
|
|
675
759
|
|
|
676
|
-
// Post-scan: detect
|
|
677
|
-
await
|
|
760
|
+
// Post-scan: apply explicit type hints, detect references, and derived fields
|
|
761
|
+
const schemaBlocks = await parseContentConfigSchemaBlocks()
|
|
762
|
+
applyConfigFieldTypes(collections, schemaBlocks)
|
|
763
|
+
await detectReferenceFields(collections, schemaBlocks)
|
|
678
764
|
detectDerivedHrefFields(collections)
|
|
679
765
|
|
|
680
766
|
return collections
|
package/src/dev-middleware.ts
CHANGED
|
@@ -44,9 +44,6 @@ interface ViteDevServerLike {
|
|
|
44
44
|
on: (event: string, listener: (...args: any[]) => void) => any
|
|
45
45
|
removeListener: (event: string, listener: (...args: any[]) => void) => any
|
|
46
46
|
}
|
|
47
|
-
environments?: Record<string, {
|
|
48
|
-
moduleGraph: { invalidateAll: () => void }
|
|
49
|
-
}>
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
/**
|
|
@@ -115,17 +112,6 @@ export function createDevMiddleware(
|
|
|
115
112
|
const route = url.replace('/_nua/cms/', '').split('?')[0]!
|
|
116
113
|
|
|
117
114
|
handleCmsApiRoute(route, req, res, manifestWriter, config.contentDir, options.mediaAdapter)
|
|
118
|
-
.then(() => {
|
|
119
|
-
// Invalidate all Vite environment module caches after content-modifying
|
|
120
|
-
// routes so that a subsequent page reload serves fresh content.
|
|
121
|
-
// In sandboxed environments (e.g. E2B) chokidar doesn't detect file
|
|
122
|
-
// changes, leaving stale modules in Astro's SSR/prerender environments.
|
|
123
|
-
if (req.method === 'POST' && server.environments) {
|
|
124
|
-
for (const env of Object.values(server.environments)) {
|
|
125
|
-
env.moduleGraph.invalidateAll()
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
115
|
.catch((error) => {
|
|
130
116
|
console.error('[astro-cms] API error:', error)
|
|
131
117
|
sendError(res, 'Internal server error', 500)
|
|
@@ -181,6 +167,10 @@ export function createDevMiddleware(
|
|
|
181
167
|
if (Object.keys(collectionDefs).length > 0) {
|
|
182
168
|
manifest.collectionDefinitions = collectionDefs
|
|
183
169
|
}
|
|
170
|
+
const mdxComponents = manifestWriter.getMdxComponents()
|
|
171
|
+
if (mdxComponents) {
|
|
172
|
+
manifest.mdxComponents = mdxComponents
|
|
173
|
+
}
|
|
184
174
|
res.end(JSON.stringify(manifest, null, 2))
|
|
185
175
|
return
|
|
186
176
|
}
|
package/src/editor/api.ts
CHANGED
|
@@ -91,6 +91,8 @@ export async function fetchManifest(): Promise<CmsManifest> {
|
|
|
91
91
|
metadata: pageManifest?.metadata,
|
|
92
92
|
// SEO data from page-specific manifest
|
|
93
93
|
seo: pageManifest?.seo,
|
|
94
|
+
// MDX component allowlist from global manifest
|
|
95
|
+
mdxComponents: globalManifest?.mdxComponents,
|
|
94
96
|
} as CmsManifest
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -42,14 +42,15 @@ export interface TextFieldProps {
|
|
|
42
42
|
onChange: (value: string) => void
|
|
43
43
|
isDirty?: boolean
|
|
44
44
|
onReset?: () => void
|
|
45
|
+
inputType?: string
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
export function TextField({ label, value, placeholder, onChange, isDirty, onReset }: TextFieldProps) {
|
|
48
|
+
export function TextField({ label, value, placeholder, onChange, isDirty, onReset, inputType = 'text' }: TextFieldProps) {
|
|
48
49
|
return (
|
|
49
50
|
<div class="space-y-1.5">
|
|
50
51
|
<FieldLabel label={label} isDirty={isDirty} onReset={onReset} />
|
|
51
52
|
<input
|
|
52
|
-
type=
|
|
53
|
+
type={inputType}
|
|
53
54
|
value={value ?? ''}
|
|
54
55
|
placeholder={placeholder}
|
|
55
56
|
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentChildren } from 'preact'
|
|
2
2
|
import { useEffect, useState } from 'preact/hooks'
|
|
3
|
+
import { getCollectionEntryOptions } from '../manifest'
|
|
3
4
|
import { renameMarkdownPage } from '../markdown-api'
|
|
4
5
|
import {
|
|
5
6
|
config,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
updateMarkdownPageMeta,
|
|
12
13
|
} from '../signals'
|
|
13
14
|
import type { CollectionDefinition, FieldDefinition, MarkdownPageEntry } from '../types'
|
|
14
|
-
import { ComboBoxField, ImageField, MultiSelectField, NumberField, TextField, ToggleField } from './fields'
|
|
15
|
+
import { ColorField, ComboBoxField, ImageField, MultiSelectField, NumberField, TextField, ToggleField } from './fields'
|
|
15
16
|
import { groupFields } from './frontmatter-sidebar'
|
|
16
17
|
|
|
17
18
|
function isArrayOfObjects(value: unknown[]): value is Record<string, unknown>[] {
|
|
@@ -188,7 +189,9 @@ export function CreateModeFrontmatter({
|
|
|
188
189
|
fields,
|
|
189
190
|
onSlugManualEdit,
|
|
190
191
|
}: CreateModeFrontmatterProps) {
|
|
191
|
-
const
|
|
192
|
+
const allFields = fields ?? collectionDefinition.fields
|
|
193
|
+
// In create mode, skip complex fields (arrays, objects) — they can be edited after creation
|
|
194
|
+
const displayFields = allFields.filter(f => f.type !== 'array' && f.type !== 'object')
|
|
192
195
|
const groups = groupFields(displayFields)
|
|
193
196
|
|
|
194
197
|
return (
|
|
@@ -376,20 +379,6 @@ export function EditModeFrontmatter({
|
|
|
376
379
|
)
|
|
377
380
|
}
|
|
378
381
|
|
|
379
|
-
// ============================================================================
|
|
380
|
-
// Collection Reference Helpers
|
|
381
|
-
// ============================================================================
|
|
382
|
-
|
|
383
|
-
function getCollectionEntryOptions(collectionName?: string): Array<{ value: string; label: string }> {
|
|
384
|
-
if (!collectionName) return []
|
|
385
|
-
const def = manifest.value.collectionDefinitions?.[collectionName]
|
|
386
|
-
if (!def?.entries) return []
|
|
387
|
-
return def.entries.map(e => ({
|
|
388
|
-
value: e.slug,
|
|
389
|
-
label: e.title ?? e.slug,
|
|
390
|
-
}))
|
|
391
|
-
}
|
|
392
|
-
|
|
393
382
|
// ============================================================================
|
|
394
383
|
// Schema-aware Frontmatter Field
|
|
395
384
|
// ============================================================================
|
|
@@ -410,12 +399,14 @@ export function SchemaFrontmatterField({
|
|
|
410
399
|
switch (field.type) {
|
|
411
400
|
case 'text':
|
|
412
401
|
case 'url':
|
|
402
|
+
case 'email':
|
|
413
403
|
return (
|
|
414
404
|
<TextField
|
|
415
405
|
label={label}
|
|
416
406
|
value={(value as string) ?? ''}
|
|
417
407
|
placeholder={getPlaceholder(field)}
|
|
418
408
|
onChange={(v) => onChange(v)}
|
|
409
|
+
inputType={field.type === 'text' ? undefined : field.type}
|
|
419
410
|
/>
|
|
420
411
|
)
|
|
421
412
|
|
|
@@ -434,6 +425,16 @@ export function SchemaFrontmatterField({
|
|
|
434
425
|
/>
|
|
435
426
|
)
|
|
436
427
|
|
|
428
|
+
case 'color':
|
|
429
|
+
return (
|
|
430
|
+
<ColorField
|
|
431
|
+
label={label}
|
|
432
|
+
value={(value as string) ?? ''}
|
|
433
|
+
placeholder={getPlaceholder(field)}
|
|
434
|
+
onChange={(v) => onChange(v)}
|
|
435
|
+
/>
|
|
436
|
+
)
|
|
437
|
+
|
|
437
438
|
case 'textarea':
|
|
438
439
|
return (
|
|
439
440
|
<div class="flex flex-col gap-1 col-span-2" data-cms-ui>
|
|
@@ -450,11 +451,13 @@ export function SchemaFrontmatterField({
|
|
|
450
451
|
)
|
|
451
452
|
|
|
452
453
|
case 'date':
|
|
454
|
+
case 'datetime':
|
|
455
|
+
case 'time':
|
|
453
456
|
return (
|
|
454
457
|
<div class="flex flex-col gap-1" data-cms-ui>
|
|
455
458
|
<label class="text-xs text-white/60 font-medium">{label}</label>
|
|
456
459
|
<input
|
|
457
|
-
type=
|
|
460
|
+
type={field.type === 'datetime' ? 'datetime-local' : field.type}
|
|
458
461
|
value={(value as string) ?? ''}
|
|
459
462
|
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
|
|
460
463
|
class="px-3 py-2 text-sm bg-white/10 border border-white/20 rounded-cms-sm text-white focus:outline-none focus:border-white/40"
|
|
@@ -496,7 +499,7 @@ export function SchemaFrontmatterField({
|
|
|
496
499
|
)
|
|
497
500
|
|
|
498
501
|
case 'reference': {
|
|
499
|
-
const refOptions = getCollectionEntryOptions(field.collection)
|
|
502
|
+
const refOptions = getCollectionEntryOptions(manifest.value, field.collection)
|
|
500
503
|
return (
|
|
501
504
|
<ComboBoxField
|
|
502
505
|
label={label}
|
|
@@ -512,7 +515,7 @@ export function SchemaFrontmatterField({
|
|
|
512
515
|
const items = Array.isArray(value) ? value : []
|
|
513
516
|
// Array of references — show multiselect with collection entries
|
|
514
517
|
if (field.itemType === 'reference' && field.collection) {
|
|
515
|
-
const refEntries = getCollectionEntryOptions(field.collection)
|
|
518
|
+
const refEntries = getCollectionEntryOptions(manifest.value, field.collection)
|
|
516
519
|
return (
|
|
517
520
|
<div class="col-span-2" data-cms-ui>
|
|
518
521
|
<MultiSelectField
|
|
@@ -536,7 +539,7 @@ export function SchemaFrontmatterField({
|
|
|
536
539
|
</div>
|
|
537
540
|
)
|
|
538
541
|
}
|
|
539
|
-
if (isArrayOfObjects(items)) {
|
|
542
|
+
if (field.itemType === 'object' || isArrayOfObjects(items)) {
|
|
540
543
|
return (
|
|
541
544
|
<ArrayOfObjectsField
|
|
542
545
|
label={label}
|
|
@@ -835,10 +838,18 @@ export function getPlaceholder(field: FieldDefinition): string {
|
|
|
835
838
|
switch (field.type) {
|
|
836
839
|
case 'url':
|
|
837
840
|
return 'https://...'
|
|
841
|
+
case 'email':
|
|
842
|
+
return 'name@example.com'
|
|
838
843
|
case 'image':
|
|
839
844
|
return '/images/...'
|
|
845
|
+
case 'color':
|
|
846
|
+
return '#000000'
|
|
840
847
|
case 'date':
|
|
841
848
|
return 'YYYY-MM-DD'
|
|
849
|
+
case 'datetime':
|
|
850
|
+
return 'YYYY-MM-DDTHH:MM'
|
|
851
|
+
case 'time':
|
|
852
|
+
return 'HH:MM'
|
|
842
853
|
default:
|
|
843
854
|
return `Enter ${formatFieldLabel(field.name).toLowerCase()}...`
|
|
844
855
|
}
|
|
@@ -2,13 +2,14 @@ import { type Editor, editorViewCtx } from '@milkdown/core'
|
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
3
3
|
import { slugify } from '../../shared'
|
|
4
4
|
import { updateMarkdownPage } from '../api'
|
|
5
|
-
import {
|
|
5
|
+
import { STORAGE_KEYS, Z_INDEX } from '../constants'
|
|
6
6
|
import { createMarkdownPage } from '../markdown-api'
|
|
7
7
|
import {
|
|
8
8
|
config,
|
|
9
9
|
currentMarkdownPage,
|
|
10
10
|
isMarkdownPreview,
|
|
11
11
|
markdownEditorState,
|
|
12
|
+
pendingCollectionEntries,
|
|
12
13
|
resetMarkdownEditorState,
|
|
13
14
|
showToast,
|
|
14
15
|
startRedirectCountdown,
|
|
@@ -89,6 +90,22 @@ export function MarkdownEditorOverlay() {
|
|
|
89
90
|
}
|
|
90
91
|
}, [])
|
|
91
92
|
|
|
93
|
+
/** Create any collection entries that were queued during component insertion */
|
|
94
|
+
const flushPendingEntries = useCallback(async () => {
|
|
95
|
+
const entries = pendingCollectionEntries.value
|
|
96
|
+
if (entries.length === 0) return
|
|
97
|
+
pendingCollectionEntries.value = []
|
|
98
|
+
await Promise.all(entries.map(entry =>
|
|
99
|
+
createMarkdownPage(config.value, {
|
|
100
|
+
collection: entry.collection,
|
|
101
|
+
slug: entry.slug,
|
|
102
|
+
title: entry.title,
|
|
103
|
+
frontmatter: entry.frontmatter as any,
|
|
104
|
+
fileExtension: entry.fileExtension,
|
|
105
|
+
})
|
|
106
|
+
))
|
|
107
|
+
}, [])
|
|
108
|
+
|
|
92
109
|
const handleSave = useCallback(
|
|
93
110
|
async (content: string) => {
|
|
94
111
|
if (isSaving) return
|
|
@@ -97,6 +114,7 @@ export function MarkdownEditorOverlay() {
|
|
|
97
114
|
|
|
98
115
|
setIsSaving(true)
|
|
99
116
|
try {
|
|
117
|
+
await flushPendingEntries()
|
|
100
118
|
const result = await updateMarkdownPage(config.value.apiBase, {
|
|
101
119
|
filePath: currentPage.filePath,
|
|
102
120
|
content,
|
|
@@ -127,8 +145,6 @@ export function MarkdownEditorOverlay() {
|
|
|
127
145
|
// Clear pending entry navigation so editor doesn't auto-open after save
|
|
128
146
|
sessionStorage.removeItem(STORAGE_KEYS.PENDING_ENTRY_NAVIGATION)
|
|
129
147
|
resetMarkdownEditorState()
|
|
130
|
-
|
|
131
|
-
schedulePageReload()
|
|
132
148
|
} else {
|
|
133
149
|
showToast(result.error || 'Failed to save markdown', 'error')
|
|
134
150
|
setIsSaving(false)
|
|
@@ -162,6 +178,7 @@ export function MarkdownEditorOverlay() {
|
|
|
162
178
|
|
|
163
179
|
setIsSaving(true)
|
|
164
180
|
try {
|
|
181
|
+
await flushPendingEntries()
|
|
165
182
|
const isData = opts.collectionDefinition.type === 'data'
|
|
166
183
|
|
|
167
184
|
// Build frontmatter — for data collections include all fields; for markdown exclude title
|