Skip to content

Commit 0fa9409

Browse files
authored
Merge pull request #1310 from trycompai/main
[comp] Production Deploy
2 parents aa0fecf + df69e22 commit 0fa9409

File tree

3 files changed

+89
-38
lines changed

3 files changed

+89
-38
lines changed

apps/app/src/app/(app)/[orgId]/policies/[policyId]/editor/components/PolicyDetails.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { PolicyEditor } from '@/components/editor/policy-editor';
4+
import { validateAndFixTipTapContent } from '@comp/ui/editor';
45
import '@comp/ui/editor.css';
56
import type { JSONContent } from '@tiptap/react';
67
import { updatePolicy } from '../actions/update-policy';
@@ -34,6 +35,9 @@ export function PolicyPageEditor({
3435
? [policyContent as JSONContent]
3536
: [];
3637
const sanitizedContent = formattedContent.map(removeUnsupportedMarks);
38+
// Normalize via validator so editor always receives a clean array
39+
const validatedDoc = validateAndFixTipTapContent(sanitizedContent);
40+
const normalizedContent = (validatedDoc.content || []) as JSONContent[];
3741
const handleSavePolicy = async (policyContent: JSONContent[]): Promise<void> => {
3842
if (!policyId) return;
3943

@@ -48,7 +52,7 @@ export function PolicyPageEditor({
4852
return (
4953
<div className="flex h-full flex-col border">
5054
<PolicyEditor
51-
content={sanitizedContent}
55+
content={normalizedContent}
5256
onSave={handleSavePolicy}
5357
readOnly={isPendingApproval}
5458
/>

apps/app/src/components/editor/policy-editor.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22

3+
import { validateAndFixTipTapContent } from '@comp/ui/editor';
34
import type { JSONContent } from '@tiptap/react';
45
import { useState } from 'react';
56
import AdvancedEditor from './advanced-editor';
@@ -13,18 +14,10 @@ interface PolicyEditorProps {
1314
export function PolicyEditor({ content, readOnly = false, onSave }: PolicyEditorProps) {
1415
const [editorContent, setEditorContent] = useState<JSONContent | null>(null);
1516

16-
const documentContent = {
17+
const documentContent = validateAndFixTipTapContent({
1718
type: 'doc',
18-
content:
19-
Array.isArray(content) && content.length > 0
20-
? content
21-
: [
22-
{
23-
type: 'paragraph',
24-
content: [{ type: 'text', text: '' }],
25-
},
26-
],
27-
};
19+
content: Array.isArray(content) && content.length > 0 ? content : [],
20+
});
2821

2922
const handleUpdate = (updatedContent: JSONContent) => {
3023
setEditorContent(updatedContent);
@@ -34,7 +27,8 @@ export function PolicyEditor({ content, readOnly = false, onSave }: PolicyEditor
3427
if (!contentToSave || !onSave) return;
3528

3629
try {
37-
const contentArray = contentToSave.content as JSONContent[];
30+
const fixed = validateAndFixTipTapContent(contentToSave);
31+
const contentArray = (fixed.content || []) as JSONContent[];
3832
await onSave(contentArray);
3933
} catch (error) {
4034
console.error('Error saving policy:', error);

packages/ui/src/components/editor/utils/validate-content.ts

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,9 @@ function fixContentArray(contentArray: any[]): JSONContent[] {
4646
return [createEmptyParagraph()];
4747
}
4848

49-
const fixedContent = contentArray.map(fixNode).filter((node): node is JSONContent => {
50-
if (!node) return false;
51-
if (node.type === 'text') {
52-
const value = typeof (node as any).text === 'string' ? (node as any).text : '';
53-
return isNonEmptyText(value);
54-
}
55-
return true;
56-
}) as JSONContent[];
49+
const fixedContent = contentArray
50+
.map(fixNode)
51+
.filter((node): node is JSONContent => node !== null) as JSONContent[];
5752

5853
// Ensure we have at least one paragraph
5954
if (fixedContent.length === 0) {
@@ -63,11 +58,13 @@ function fixContentArray(contentArray: any[]): JSONContent[] {
6358
return fixedContent;
6459
}
6560

66-
function isNonEmptyText(value: string): boolean {
67-
// Consider normal whitespace, NBSP, zero-width space and narrow no-break space
68-
if (typeof value !== 'string') return false;
69-
const normalized = value.replace(/[\u00A0\u200B\u202F]/g, '');
70-
return normalized.trim().length > 0;
61+
function ensureNonEmptyText(value: unknown): string {
62+
const text = typeof value === 'string' ? value : '';
63+
// Normalize NBSP and narrow no-break space for emptiness checks
64+
const normalized = text.replace(/[\u00A0\u202F]/g, '');
65+
if (normalized.trim().length > 0) return text;
66+
// Return zero-width space to ensure non-empty text node without visual change
67+
return '\u200B';
7168
}
7269

7370
/**
@@ -94,6 +91,12 @@ function fixNode(node: any): JSONContent | null {
9491
return fixList(node);
9592
case 'listItem':
9693
return fixListItem(node);
94+
case 'table':
95+
return fixTable(node);
96+
case 'tableRow':
97+
return fixTableRow(node);
98+
case 'tableCell':
99+
return fixTableCell(node);
97100
case 'text':
98101
return fixTextNode(node);
99102
case 'heading':
@@ -102,6 +105,8 @@ function fixNode(node: any): JSONContent | null {
102105
return fixBlockquote(node);
103106
case 'codeBlock':
104107
return fixCodeBlock(node);
108+
case 'hardBreak':
109+
return { type: 'hardBreak' };
105110
default:
106111
// For other valid nodes, just fix their content if they have any
107112
return {
@@ -130,21 +135,13 @@ function fixParagraph(node: any): JSONContent {
130135
if (item.text && !item.type) {
131136
return {
132137
type: 'text',
133-
text: item.text,
138+
text: ensureNonEmptyText(item.text),
134139
...(item.marks && { marks: fixMarks(item.marks) }),
135140
};
136141
}
137142
return fixNode(item);
138143
})
139-
.filter((n): n is JSONContent => {
140-
if (!n) return false;
141-
// Drop empty text nodes entirely
142-
if (n.type === 'text') {
143-
const txt = typeof (n as any).text === 'string' ? (n as any).text : '';
144-
return txt.trim().length > 0;
145-
}
146-
return true;
147-
});
144+
.filter((n): n is JSONContent => Boolean(n));
148145

149146
// If no valid content, keep an empty paragraph (no empty text nodes)
150147

@@ -216,8 +213,7 @@ function fixListItem(node: any): JSONContent {
216213
function fixTextNode(node: any): JSONContent {
217214
const { text, marks, ...rest } = node;
218215

219-
const value = typeof text === 'string' ? text : '';
220-
// If the resulting text is empty, return a text node with non-empty check handled by callers.
216+
const value = ensureNonEmptyText(text);
221217
return {
222218
type: 'text',
223219
text: value,
@@ -272,6 +268,49 @@ function fixCodeBlock(node: any): JSONContent {
272268
};
273269
}
274270

271+
/**
272+
* Fixes table structures
273+
*/
274+
function fixTable(node: any): JSONContent {
275+
const { content, attrs, ...rest } = node;
276+
let rows: JSONContent[] = [];
277+
if (Array.isArray(content)) {
278+
rows = content
279+
.map((child: any) => (child?.type === 'tableRow' ? fixTableRow(child) : null))
280+
.filter(Boolean) as JSONContent[];
281+
}
282+
if (rows.length === 0) {
283+
rows = [createEmptyTableRow()];
284+
}
285+
return { type: 'table', content: rows, ...(attrs && { attrs }), ...rest };
286+
}
287+
288+
function fixTableRow(node: any): JSONContent {
289+
const { content, attrs, ...rest } = node;
290+
let cells: JSONContent[] = [];
291+
if (Array.isArray(content)) {
292+
cells = content
293+
.map((child: any) => (child?.type === 'tableCell' ? fixTableCell(child) : null))
294+
.filter(Boolean) as JSONContent[];
295+
}
296+
if (cells.length === 0) {
297+
cells = [createEmptyTableCell()];
298+
}
299+
return { type: 'tableRow', content: cells, ...(attrs && { attrs }), ...rest };
300+
}
301+
302+
function fixTableCell(node: any): JSONContent {
303+
const { content, attrs, ...rest } = node;
304+
let blocks: JSONContent[] = [];
305+
if (Array.isArray(content)) {
306+
blocks = fixContentArray(content);
307+
}
308+
if (blocks.length === 0) {
309+
blocks = [createEmptyParagraph()];
310+
}
311+
return { type: 'tableCell', content: blocks, ...(attrs && { attrs }), ...rest };
312+
}
313+
275314
/**
276315
* Fixes marks array
277316
*/
@@ -318,6 +357,20 @@ function createEmptyListItem(): JSONContent {
318357
};
319358
}
320359

360+
function createEmptyTableCell(): JSONContent {
361+
return {
362+
type: 'tableCell',
363+
content: [createEmptyParagraph()],
364+
};
365+
}
366+
367+
function createEmptyTableRow(): JSONContent {
368+
return {
369+
type: 'tableRow',
370+
content: [createEmptyTableCell()],
371+
};
372+
}
373+
321374
/**
322375
* Validates if content is a valid TipTap document structure
323376
*/

0 commit comments

Comments
 (0)