@nuasite/cms 0.2.2 → 0.3.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/README.md +81 -73
- package/dist/src/build-processor.d.ts.map +1 -1
- package/dist/src/component-registry.d.ts +6 -2
- package/dist/src/component-registry.d.ts.map +1 -1
- package/dist/src/dev-middleware.d.ts.map +1 -1
- package/dist/src/editor/api.d.ts +14 -0
- package/dist/src/editor/api.d.ts.map +1 -1
- package/dist/src/editor/components/ai-chat.d.ts.map +1 -1
- package/dist/src/editor/components/block-editor.d.ts.map +1 -1
- package/dist/src/editor/components/color-toolbar.d.ts.map +1 -1
- package/dist/src/editor/components/editable-highlights.d.ts.map +1 -1
- package/dist/src/editor/components/outline.d.ts.map +1 -1
- package/dist/src/editor/constants.d.ts +1 -0
- package/dist/src/editor/constants.d.ts.map +1 -1
- package/dist/src/editor/dom.d.ts +9 -0
- package/dist/src/editor/dom.d.ts.map +1 -1
- package/dist/src/editor/editor.d.ts.map +1 -1
- package/dist/src/editor/history.d.ts.map +1 -1
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -1
- package/dist/src/editor/index.d.ts.map +1 -1
- package/dist/src/editor/storage.d.ts +2 -0
- package/dist/src/editor/storage.d.ts.map +1 -1
- package/dist/src/handlers/array-ops.d.ts +59 -0
- package/dist/src/handlers/array-ops.d.ts.map +1 -0
- package/dist/src/handlers/component-ops.d.ts +26 -0
- package/dist/src/handlers/component-ops.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/build-processor.ts +27 -0
- package/src/component-registry.ts +125 -76
- package/src/dev-middleware.ts +85 -16
- package/src/editor/api.ts +72 -0
- package/src/editor/components/ai-chat.tsx +0 -1
- package/src/editor/components/block-editor.tsx +92 -17
- package/src/editor/components/color-toolbar.tsx +7 -1
- package/src/editor/components/editable-highlights.tsx +4 -1
- package/src/editor/components/outline.tsx +11 -6
- package/src/editor/constants.ts +1 -0
- package/src/editor/dom.ts +46 -1
- package/src/editor/editor.ts +5 -2
- package/src/editor/history.ts +1 -6
- package/src/editor/hooks/useBlockEditorHandlers.ts +86 -29
- package/src/editor/index.tsx +24 -8
- package/src/editor/storage.ts +24 -0
- package/src/handlers/array-ops.ts +452 -0
- package/src/handlers/component-ops.ts +269 -18
- package/src/handlers/markdown-ops.ts +7 -4
- package/src/handlers/request-utils.ts +1 -1
- package/src/handlers/source-writer.ts +4 -5
- package/src/index.ts +15 -10
- package/src/manifest-writer.ts +1 -1
- package/src/source-finder/cross-file-tracker.ts +1 -1
- package/src/source-finder/search-index.ts +1 -1
package/package.json
CHANGED
package/src/build-processor.ts
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
import { getProjectRoot } from './config'
|
|
7
|
+
import { extractPropsFromSource, findComponentInvocationLine } from './handlers/component-ops'
|
|
7
8
|
import { extractComponentName, processHtml } from './html-processor'
|
|
8
9
|
import type { ManifestWriter } from './manifest-writer'
|
|
9
10
|
import { generateComponentPreviews } from './preview-generator'
|
|
@@ -619,6 +620,32 @@ async function processFile(
|
|
|
619
620
|
result.html = root.toString()
|
|
620
621
|
}
|
|
621
622
|
|
|
623
|
+
// Populate component props from page source invocations
|
|
624
|
+
if (Object.keys(result.components).length > 0) {
|
|
625
|
+
const pageSourcePath = await findPageSource(pagePath)
|
|
626
|
+
if (pageSourcePath) {
|
|
627
|
+
try {
|
|
628
|
+
const pageContent = await fs.readFile(pageSourcePath, 'utf-8')
|
|
629
|
+
const pageLines = pageContent.split('\n')
|
|
630
|
+
|
|
631
|
+
// Track per-component-name occurrence counter
|
|
632
|
+
const occurrenceCounts = new Map<string, number>()
|
|
633
|
+
|
|
634
|
+
for (const comp of Object.values(result.components)) {
|
|
635
|
+
const idx = occurrenceCounts.get(comp.componentName) ?? 0
|
|
636
|
+
occurrenceCounts.set(comp.componentName, idx + 1)
|
|
637
|
+
|
|
638
|
+
const invLine = findComponentInvocationLine(pageLines, comp.componentName, idx)
|
|
639
|
+
if (invLine >= 0) {
|
|
640
|
+
comp.props = extractPropsFromSource(pageLines, invLine, comp.componentName)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
} catch {
|
|
644
|
+
// Could not read page source — leave props empty
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
622
649
|
// Remove CMS ID attributes from HTML for entries that were filtered out
|
|
623
650
|
let finalHtml = result.html
|
|
624
651
|
if (idsToRemove.length > 0) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parse as parseBabel } from '@babel/parser'
|
|
1
2
|
import fs from 'node:fs/promises'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { getProjectRoot } from './config'
|
|
@@ -88,100 +89,148 @@ export class ComponentRegistry {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
|
-
* Parse Props content
|
|
92
|
-
*
|
|
92
|
+
* Parse Props content using @babel/parser AST for correct TypeScript handling.
|
|
93
|
+
* Wraps the content in a synthetic interface and walks TSPropertySignature nodes.
|
|
93
94
|
*/
|
|
94
95
|
private parsePropsContent(propsContent: string): ComponentProp[] {
|
|
95
96
|
const props: ComponentProp[] = []
|
|
96
|
-
let i = 0
|
|
97
|
-
const content = propsContent.trim()
|
|
98
|
-
|
|
99
|
-
while (i < content.length) {
|
|
100
|
-
// Skip whitespace and newlines
|
|
101
|
-
while (i < content.length && /\s/.test(content[i] ?? '')) i++
|
|
102
|
-
if (i >= content.length) break
|
|
103
|
-
|
|
104
|
-
// Skip comments
|
|
105
|
-
if (content[i] === '/' && content[i + 1] === '/') {
|
|
106
|
-
// Skip to end of line
|
|
107
|
-
while (i < content.length && content[i] !== '\n') i++
|
|
108
|
-
continue
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (content[i] === '/' && content[i + 1] === '*') {
|
|
112
|
-
// Skip block comment
|
|
113
|
-
while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) i++
|
|
114
|
-
i += 2
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Extract property name
|
|
119
|
-
const nameStart = i
|
|
120
|
-
while (i < content.length && /\w/.test(content[i] ?? '')) i++
|
|
121
|
-
const name = content.substring(nameStart, i)
|
|
122
|
-
|
|
123
|
-
if (!name) break
|
|
124
|
-
|
|
125
|
-
// Skip whitespace
|
|
126
|
-
while (i < content.length && /\s/.test(content[i] ?? '')) i++
|
|
127
|
-
|
|
128
|
-
// Check for optional marker
|
|
129
|
-
const optional = content[i] === '?'
|
|
130
|
-
if (optional) i++
|
|
131
97
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
98
|
+
// Wrap in an interface so Babel can parse it as valid TypeScript
|
|
99
|
+
const synthetic = `interface _Props {\n${propsContent}\n}`
|
|
100
|
+
let ast: ReturnType<typeof parseBabel>
|
|
101
|
+
try {
|
|
102
|
+
ast = parseBabel(synthetic, {
|
|
103
|
+
sourceType: 'module',
|
|
104
|
+
plugins: ['typescript'],
|
|
105
|
+
errorRecovery: true,
|
|
106
|
+
})
|
|
107
|
+
} catch {
|
|
108
|
+
return props
|
|
109
|
+
}
|
|
138
110
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
111
|
+
const interfaceNode = ast.program.body[0]
|
|
112
|
+
if (!interfaceNode || interfaceNode.type !== 'TSInterfaceDeclaration') return props
|
|
113
|
+
|
|
114
|
+
// Collect leading comments per line for JSDoc / inline descriptions
|
|
115
|
+
const lines = synthetic.split('\n')
|
|
116
|
+
|
|
117
|
+
for (const member of interfaceNode.body.body) {
|
|
118
|
+
if (member.type !== 'TSPropertySignature') continue
|
|
119
|
+
if (member.key.type !== 'Identifier') continue
|
|
120
|
+
|
|
121
|
+
const name = member.key.name
|
|
122
|
+
const optional = !!member.optional
|
|
123
|
+
|
|
124
|
+
// Reconstruct the type string from source text
|
|
125
|
+
let type = 'unknown'
|
|
126
|
+
if (member.typeAnnotation?.typeAnnotation) {
|
|
127
|
+
const ta = member.typeAnnotation.typeAnnotation
|
|
128
|
+
if (ta.loc) {
|
|
129
|
+
// Extract the type text directly from the synthetic source
|
|
130
|
+
const startLine = ta.loc.start.line - 1
|
|
131
|
+
const endLine = ta.loc.end.line - 1
|
|
132
|
+
if (startLine === endLine) {
|
|
133
|
+
type = lines[startLine]!.slice(ta.loc.start.column, ta.loc.end.column).trim()
|
|
134
|
+
} else {
|
|
135
|
+
const parts: string[] = []
|
|
136
|
+
for (let l = startLine; l <= endLine; l++) {
|
|
137
|
+
if (l === startLine) parts.push(lines[l]!.slice(ta.loc.start.column))
|
|
138
|
+
else if (l === endLine) parts.push(lines[l]!.slice(0, ta.loc.end.column))
|
|
139
|
+
else parts.push(lines[l]!)
|
|
140
|
+
}
|
|
141
|
+
type = parts.join('\n').trim()
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
type = this.typeAnnotationToString(ta)
|
|
145
|
+
}
|
|
153
146
|
}
|
|
154
147
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// Skip the semicolon
|
|
158
|
-
if (content[i] === ';') i++
|
|
148
|
+
// Look for description from comments
|
|
149
|
+
let description: string | undefined
|
|
159
150
|
|
|
160
|
-
//
|
|
161
|
-
|
|
151
|
+
// First, check for inline trailing comment on the property's source line
|
|
152
|
+
// (Babel can misattach these as leading comments of the next property)
|
|
153
|
+
if (member.loc) {
|
|
154
|
+
const lineIdx = member.loc.end.line - 1
|
|
155
|
+
const sourceLine = lines[lineIdx]
|
|
156
|
+
if (sourceLine) {
|
|
157
|
+
const commentMatch = sourceLine.match(/\/\/\s*(.+?)\s*$/)
|
|
158
|
+
if (commentMatch?.[1]) {
|
|
159
|
+
description = commentMatch[1]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
162
163
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
// If no inline comment, check for leading JSDoc or standalone line comments
|
|
165
|
+
if (!description && member.leadingComments && member.leadingComments.length > 0) {
|
|
166
|
+
const last = member.leadingComments[member.leadingComments.length - 1]!
|
|
167
|
+
if (last.type === 'CommentBlock') {
|
|
168
|
+
description = last.value
|
|
169
|
+
.split('\n')
|
|
170
|
+
.map((l: string) => l.replace(/^\s*\*\s?/, '').trim())
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.join(' ')
|
|
173
|
+
} else if (last.type === 'CommentLine' && last.loc && member.loc) {
|
|
174
|
+
// Only use line comments on their own line (not inline on previous property)
|
|
175
|
+
const commentLineContent = lines[last.loc.start.line - 1]?.trim()
|
|
176
|
+
if (commentLineContent?.startsWith('//')) {
|
|
177
|
+
description = last.value.trim()
|
|
178
|
+
}
|
|
179
|
+
}
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
if (name && type) {
|
|
173
|
-
props.push({
|
|
174
|
-
name,
|
|
175
|
-
type,
|
|
176
|
-
required: !optional,
|
|
177
|
-
description,
|
|
178
|
-
})
|
|
183
|
+
props.push({ name, type, required: !optional, description })
|
|
179
184
|
}
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
return props
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Fallback: convert a Babel TSType node to a human-readable string
|
|
192
|
+
*/
|
|
193
|
+
private typeAnnotationToString(node: any): string {
|
|
194
|
+
switch (node.type) {
|
|
195
|
+
case 'TSStringKeyword':
|
|
196
|
+
return 'string'
|
|
197
|
+
case 'TSNumberKeyword':
|
|
198
|
+
return 'number'
|
|
199
|
+
case 'TSBooleanKeyword':
|
|
200
|
+
return 'boolean'
|
|
201
|
+
case 'TSAnyKeyword':
|
|
202
|
+
return 'any'
|
|
203
|
+
case 'TSVoidKeyword':
|
|
204
|
+
return 'void'
|
|
205
|
+
case 'TSNullKeyword':
|
|
206
|
+
return 'null'
|
|
207
|
+
case 'TSUndefinedKeyword':
|
|
208
|
+
return 'undefined'
|
|
209
|
+
case 'TSUnknownKeyword':
|
|
210
|
+
return 'unknown'
|
|
211
|
+
case 'TSNeverKeyword':
|
|
212
|
+
return 'never'
|
|
213
|
+
case 'TSObjectKeyword':
|
|
214
|
+
return 'object'
|
|
215
|
+
case 'TSArrayType':
|
|
216
|
+
return `${this.typeAnnotationToString(node.elementType)}[]`
|
|
217
|
+
case 'TSUnionType':
|
|
218
|
+
return node.types.map((t: any) => this.typeAnnotationToString(t)).join(' | ')
|
|
219
|
+
case 'TSIntersectionType':
|
|
220
|
+
return node.types.map((t: any) => this.typeAnnotationToString(t)).join(' & ')
|
|
221
|
+
case 'TSLiteralType':
|
|
222
|
+
if (node.literal.type === 'StringLiteral') return `'${node.literal.value}'`
|
|
223
|
+
return String(node.literal.value)
|
|
224
|
+
case 'TSTypeReference':
|
|
225
|
+
if (node.typeName?.type === 'Identifier') return node.typeName.name
|
|
226
|
+
return 'unknown'
|
|
227
|
+
case 'TSParenthesizedType':
|
|
228
|
+
return `(${this.typeAnnotationToString(node.typeAnnotation)})`
|
|
229
|
+
default:
|
|
230
|
+
return 'unknown'
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
185
234
|
/**
|
|
186
235
|
* Extract content between balanced braces after a pattern match
|
|
187
236
|
* Properly handles nested objects
|
package/src/dev-middleware.ts
CHANGED
|
@@ -2,20 +2,18 @@ import { parse } from 'node-html-parser'
|
|
|
2
2
|
import fs from 'node:fs/promises'
|
|
3
3
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
-
import {
|
|
5
|
+
import { getProjectRoot } from './config'
|
|
6
|
+
import { handleAddArrayItem, handleRemoveArrayItem } from './handlers/array-ops'
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
sendError,
|
|
17
|
-
sendJson,
|
|
18
|
-
} from './handlers/request-utils'
|
|
8
|
+
extractPropsFromSource,
|
|
9
|
+
findComponentInvocationLine,
|
|
10
|
+
getPageFileCandidates,
|
|
11
|
+
handleInsertComponent,
|
|
12
|
+
handleRemoveComponent,
|
|
13
|
+
normalizeFilePath,
|
|
14
|
+
} from './handlers/component-ops'
|
|
15
|
+
import { handleCreateMarkdown, handleGetMarkdownContent, handleUpdateMarkdown } from './handlers/markdown-ops'
|
|
16
|
+
import { handleCors, parseJsonBody, parseMultipartFile, readBody, sendError, sendJson } from './handlers/request-utils'
|
|
19
17
|
import { handleUpdate } from './handlers/source-writer'
|
|
20
18
|
import { processHtml } from './html-processor'
|
|
21
19
|
import type { ManifestWriter } from './manifest-writer'
|
|
@@ -188,7 +186,9 @@ export function createDevMiddleware(
|
|
|
188
186
|
return originalWrite.call(res, chunk, encodingOrCb, cb)
|
|
189
187
|
}
|
|
190
188
|
if (chunk) {
|
|
191
|
-
chunks!.push(
|
|
189
|
+
chunks!.push(
|
|
190
|
+
typeof chunk === 'string' ? Buffer.from(chunk, typeof encodingOrCb === 'string' ? encodingOrCb as BufferEncoding : 'utf-8') : Buffer.from(chunk),
|
|
191
|
+
)
|
|
192
192
|
}
|
|
193
193
|
if (typeof encodingOrCb === 'function') encodingOrCb()
|
|
194
194
|
else if (typeof cb === 'function') cb()
|
|
@@ -272,6 +272,22 @@ async function handleCmsApiRoute(
|
|
|
272
272
|
return
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
// POST /_nua/cms/add-array-item
|
|
276
|
+
if (route === 'add-array-item' && req.method === 'POST') {
|
|
277
|
+
const body = await parseJsonBody<Parameters<typeof handleAddArrayItem>[0]>(req)
|
|
278
|
+
const result = await handleAddArrayItem(body, manifestWriter)
|
|
279
|
+
sendJson(res, result)
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// POST /_nua/cms/remove-array-item
|
|
284
|
+
if (route === 'remove-array-item' && req.method === 'POST') {
|
|
285
|
+
const body = await parseJsonBody<Parameters<typeof handleRemoveArrayItem>[0]>(req)
|
|
286
|
+
const result = await handleRemoveArrayItem(body, manifestWriter)
|
|
287
|
+
sendJson(res, result)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
275
291
|
// GET /_nua/cms/markdown/content?filePath=...
|
|
276
292
|
if (route === 'markdown/content' && req.method === 'GET') {
|
|
277
293
|
const urlObj = new URL(req.url!, `http://${req.headers.host}`)
|
|
@@ -341,8 +357,14 @@ async function handleCmsApiRoute(
|
|
|
341
357
|
|
|
342
358
|
// Validate file content type — allow images, videos, PDFs, and common web assets
|
|
343
359
|
const allowedTypes = [
|
|
344
|
-
'image/jpeg',
|
|
345
|
-
'
|
|
360
|
+
'image/jpeg',
|
|
361
|
+
'image/png',
|
|
362
|
+
'image/gif',
|
|
363
|
+
'image/webp',
|
|
364
|
+
'image/avif',
|
|
365
|
+
'image/x-icon',
|
|
366
|
+
'video/mp4',
|
|
367
|
+
'video/webm',
|
|
346
368
|
'application/pdf',
|
|
347
369
|
]
|
|
348
370
|
// Block SVG (can contain scripts) unless explicitly served with safe headers
|
|
@@ -435,6 +457,53 @@ async function processHtmlForDev(
|
|
|
435
457
|
idGenerator,
|
|
436
458
|
)
|
|
437
459
|
|
|
460
|
+
// Populate component props from source invocations
|
|
461
|
+
const projectRoot = getProjectRoot()
|
|
462
|
+
const fileCache = new Map<string, string[] | null>()
|
|
463
|
+
const readLines = async (filePath: string): Promise<string[] | null> => {
|
|
464
|
+
if (fileCache.has(filePath)) return fileCache.get(filePath)!
|
|
465
|
+
try {
|
|
466
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
467
|
+
const lines = content.split('\n')
|
|
468
|
+
fileCache.set(filePath, lines)
|
|
469
|
+
return lines
|
|
470
|
+
} catch {
|
|
471
|
+
fileCache.set(filePath, null)
|
|
472
|
+
return null
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
for (const comp of Object.values(result.components)) {
|
|
477
|
+
let found = false
|
|
478
|
+
|
|
479
|
+
// Try invocationSourcePath first (may point to a layout, not the page)
|
|
480
|
+
if (comp.invocationSourcePath) {
|
|
481
|
+
const filePath = normalizeFilePath(comp.invocationSourcePath)
|
|
482
|
+
const lines = await readLines(path.resolve(projectRoot, filePath))
|
|
483
|
+
if (lines) {
|
|
484
|
+
const invLine = findComponentInvocationLine(lines, comp.componentName, comp.invocationIndex ?? 0)
|
|
485
|
+
if (invLine >= 0) {
|
|
486
|
+
comp.props = extractPropsFromSource(lines, invLine, comp.componentName)
|
|
487
|
+
found = true
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Fallback: search page source file candidates
|
|
493
|
+
if (!found) {
|
|
494
|
+
for (const candidate of getPageFileCandidates(pagePath)) {
|
|
495
|
+
const lines = await readLines(path.resolve(projectRoot, candidate))
|
|
496
|
+
if (lines) {
|
|
497
|
+
const invLine = findComponentInvocationLine(lines, comp.componentName, comp.invocationIndex ?? 0)
|
|
498
|
+
if (invLine >= 0) {
|
|
499
|
+
comp.props = extractPropsFromSource(lines, invLine, comp.componentName)
|
|
500
|
+
break
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
438
507
|
// Build collection entry if this is a collection page
|
|
439
508
|
let collectionEntry: CollectionEntry | undefined
|
|
440
509
|
if (collectionInfo && mdContent) {
|
package/src/editor/api.ts
CHANGED
|
@@ -247,6 +247,78 @@ export interface RemoveComponentResponse {
|
|
|
247
247
|
error?: string
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
export interface AddArrayItemResponse {
|
|
251
|
+
success: boolean
|
|
252
|
+
message?: string
|
|
253
|
+
sourceFile?: string
|
|
254
|
+
error?: string
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function addArrayItem(
|
|
258
|
+
apiBase: string,
|
|
259
|
+
referenceComponentId: string,
|
|
260
|
+
position: 'before' | 'after',
|
|
261
|
+
props: Record<string, unknown>,
|
|
262
|
+
): Promise<AddArrayItemResponse> {
|
|
263
|
+
const res = await fetchWithTimeout(`${apiBase}/add-array-item`, {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
credentials: 'include',
|
|
266
|
+
headers: {
|
|
267
|
+
'Content-Type': 'application/json',
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
referenceComponentId,
|
|
271
|
+
position,
|
|
272
|
+
props,
|
|
273
|
+
meta: {
|
|
274
|
+
source: 'inline-editor',
|
|
275
|
+
url: window.location.href,
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
if (!res.ok) {
|
|
281
|
+
const text = await res.text().catch(() => '')
|
|
282
|
+
throw new Error(`Add array item failed (${res.status}): ${text || res.statusText}`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return res.json()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface RemoveArrayItemResponse {
|
|
289
|
+
success: boolean
|
|
290
|
+
message?: string
|
|
291
|
+
sourceFile?: string
|
|
292
|
+
error?: string
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function removeArrayItem(
|
|
296
|
+
apiBase: string,
|
|
297
|
+
componentId: string,
|
|
298
|
+
): Promise<RemoveArrayItemResponse> {
|
|
299
|
+
const res = await fetchWithTimeout(`${apiBase}/remove-array-item`, {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
credentials: 'include',
|
|
302
|
+
headers: {
|
|
303
|
+
'Content-Type': 'application/json',
|
|
304
|
+
},
|
|
305
|
+
body: JSON.stringify({
|
|
306
|
+
componentId,
|
|
307
|
+
meta: {
|
|
308
|
+
source: 'inline-editor',
|
|
309
|
+
url: window.location.href,
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
if (!res.ok) {
|
|
315
|
+
const text = await res.text().catch(() => '')
|
|
316
|
+
throw new Error(`Remove array item failed (${res.status}): ${text || res.statusText}`)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return res.json()
|
|
320
|
+
}
|
|
321
|
+
|
|
250
322
|
export async function removeComponent(
|
|
251
323
|
apiBase: string,
|
|
252
324
|
componentId: string,
|
|
@@ -382,7 +382,6 @@ export const AIChat = ({ callbacks }: AIChatProps) => {
|
|
|
382
382
|
? 'bg-cms-primary text-cms-primary-text self-end rounded-cms-lg rounded-br-cms-sm'
|
|
383
383
|
: 'bg-white/10 text-white self-start rounded-cms-lg rounded-bl-cms-sm cms-markdown border border-white/10'
|
|
384
384
|
}`}
|
|
385
|
-
|
|
386
385
|
dangerouslySetInnerHTML={msg.role === 'assistant'
|
|
387
386
|
? { __html: renderMarkdown(msg.content) }
|
|
388
387
|
: undefined}
|