@open-mercato/ui 0.6.5-develop.5048.1.fd82f4ae17 → 0.6.5-develop.5116.1.f0af9e5080
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/.turbo/turbo-build.log +1 -1
- package/dist/backend/CrudForm.js +7 -57
- package/dist/backend/CrudForm.js.map +3 -3
- package/dist/backend/detail/InlineEditors.js +2 -51
- package/dist/backend/detail/InlineEditors.js.map +3 -3
- package/dist/backend/inputs/MarkdownField.js +20 -0
- package/dist/backend/inputs/MarkdownField.js.map +7 -0
- package/dist/backend/inputs/MdxEditorImpl.js +98 -0
- package/dist/backend/inputs/MdxEditorImpl.js.map +7 -0
- package/dist/backend/inputs/SwitchableMarkdownInput.js +6 -38
- package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
- package/dist/backend/markdown/markdownToPlainText.js +29 -0
- package/dist/backend/markdown/markdownToPlainText.js.map +2 -2
- package/dist/types/css.d.js +1 -0
- package/dist/types/css.d.js.map +7 -0
- package/package.json +4 -3
- package/src/backend/CrudForm.tsx +3 -67
- package/src/backend/__tests__/CrudForm.custom-fields.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.datetime.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.defaults.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.dotPath.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.doubleSubmit.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.navigation.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.optimisticLock.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.render.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.sortable.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.transformData.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.unsavedNavigation.test.tsx +0 -1
- package/src/backend/__tests__/CrudForm.validation.test.tsx +0 -1
- package/src/backend/__tests__/CustomDataSection.test.tsx +0 -1
- package/src/backend/__tests__/lazy-heavy-libraries.test.ts +16 -3
- package/src/backend/detail/InlineEditors.tsx +14 -76
- package/src/backend/inputs/MarkdownField.tsx +34 -0
- package/src/backend/inputs/MdxEditorImpl.tsx +113 -0
- package/src/backend/inputs/SwitchableMarkdownInput.tsx +5 -54
- package/src/backend/markdown/__tests__/markdownToPlainText.test.ts +13 -0
- package/src/backend/markdown/markdownToPlainText.ts +41 -0
- package/src/types/css.d.ts +5 -0
|
@@ -29,11 +29,8 @@
|
|
|
29
29
|
"use client"
|
|
30
30
|
|
|
31
31
|
import * as React from 'react'
|
|
32
|
-
import dynamic from 'next/dynamic'
|
|
33
32
|
import type { PluggableList } from 'unified'
|
|
34
|
-
import
|
|
35
|
-
import { useMarkdownRemarkPlugins } from '../markdown/useMarkdownRemarkPlugins'
|
|
36
|
-
import { useTheme } from '../../theme'
|
|
33
|
+
import MarkdownField from './MarkdownField'
|
|
37
34
|
|
|
38
35
|
/** @deprecated See module-level JSDoc. */
|
|
39
36
|
export type SwitchableMarkdownInputProps = {
|
|
@@ -53,75 +50,29 @@ export type SwitchableMarkdownInputProps = {
|
|
|
53
50
|
remarkPlugins?: PluggableList
|
|
54
51
|
}
|
|
55
52
|
|
|
56
|
-
type UiMarkdownEditorProps = {
|
|
57
|
-
value?: string
|
|
58
|
-
height?: number
|
|
59
|
-
onChange?: (value?: string) => void
|
|
60
|
-
previewOptions?: { remarkPlugins?: unknown[] }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const isTestEnv =
|
|
64
|
-
typeof process !== 'undefined' &&
|
|
65
|
-
(process.env.NODE_ENV === 'test' || typeof process.env.JEST_WORKER_ID !== 'undefined')
|
|
66
|
-
|
|
67
|
-
const MarkdownEditorTestStub: React.ComponentType<UiMarkdownEditorProps> = ({ value, onChange }) => (
|
|
68
|
-
<textarea
|
|
69
|
-
className="min-h-[160px] w-full rounded border px-3 py-2 text-sm"
|
|
70
|
-
value={value ?? ''}
|
|
71
|
-
onChange={(event) => onChange?.(event.target.value)}
|
|
72
|
-
/>
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
const UiMarkdownEditor = isTestEnv
|
|
76
|
-
? MarkdownEditorTestStub
|
|
77
|
-
: (dynamic(() => import('@uiw/react-md-editor'), {
|
|
78
|
-
ssr: false,
|
|
79
|
-
loading: () => (
|
|
80
|
-
<LoadingMessage
|
|
81
|
-
label="Loading editor..."
|
|
82
|
-
className="min-h-[220px] justify-center"
|
|
83
|
-
/>
|
|
84
|
-
),
|
|
85
|
-
}) as unknown as React.ComponentType<UiMarkdownEditorProps>)
|
|
86
|
-
|
|
87
53
|
/** @deprecated Use `RichEditor` from `@open-mercato/ui/primitives/rich-editor` for new code. See module JSDoc. */
|
|
88
54
|
export function SwitchableMarkdownInput({
|
|
89
55
|
value,
|
|
90
56
|
onChange,
|
|
91
57
|
isMarkdownEnabled,
|
|
92
58
|
disableMarkdown,
|
|
93
|
-
height = 220,
|
|
94
59
|
placeholder,
|
|
95
60
|
rows = 3,
|
|
96
61
|
textareaRef,
|
|
97
62
|
onTextareaInput,
|
|
98
63
|
textareaClassName,
|
|
99
|
-
editorWrapperClassName,
|
|
100
|
-
editorClassName,
|
|
101
64
|
disabled,
|
|
102
|
-
remarkPlugins,
|
|
103
65
|
}: SwitchableMarkdownInputProps) {
|
|
104
|
-
const resolvedPlugins = useMarkdownRemarkPlugins(remarkPlugins)
|
|
105
|
-
const { resolvedTheme } = useTheme()
|
|
106
|
-
const editorWrapperClasses =
|
|
107
|
-
editorWrapperClassName ?? 'w-full rounded-lg border border-muted-foreground/20 bg-background p-2'
|
|
108
|
-
const editorClasses = editorClassName ?? 'w-full'
|
|
109
66
|
const textareaClasses =
|
|
110
67
|
textareaClassName
|
|
111
68
|
?? 'w-full resize-none overflow-hidden rounded-lg border border-muted-foreground/20 bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
|
112
69
|
|
|
113
70
|
if (isMarkdownEnabled && !disableMarkdown) {
|
|
114
71
|
return (
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
height={height}
|
|
120
|
-
onChange={(nextValue) => onChange(typeof nextValue === 'string' ? nextValue : '')}
|
|
121
|
-
previewOptions={resolvedPlugins.length ? { remarkPlugins: resolvedPlugins } : undefined}
|
|
122
|
-
/>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
72
|
+
<MarkdownField
|
|
73
|
+
value={value}
|
|
74
|
+
onChange={(nextValue) => onChange(typeof nextValue === 'string' ? nextValue : '')}
|
|
75
|
+
/>
|
|
125
76
|
)
|
|
126
77
|
}
|
|
127
78
|
|
|
@@ -26,4 +26,17 @@ describe('markdownToPlainText', () => {
|
|
|
26
26
|
const input = '> quoted text\n\n~~gone~~\n\n| a | b |\n| - | - |\n| 1 | 2 |'
|
|
27
27
|
expect(markdownToPlainText(input)).toBe('quoted text gone a b - - 1 2')
|
|
28
28
|
})
|
|
29
|
+
|
|
30
|
+
it('strips legacy HTML tags and separates block content', () => {
|
|
31
|
+
expect(markdownToPlainText('<span style="font-weight:bold">Backend</span> and frontend')).toBe(
|
|
32
|
+
'Backend and frontend',
|
|
33
|
+
)
|
|
34
|
+
expect(markdownToPlainText('<p>Hello</p><p>World</p>')).toBe('Hello World')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('decodes named, decimal, and hex HTML entities', () => {
|
|
38
|
+
expect(markdownToPlainText('Tom & Jerry')).toBe('Tom & Jerry')
|
|
39
|
+
expect(markdownToPlainText('it's — done')).toBe("it's — done")
|
|
40
|
+
expect(markdownToPlainText('a b')).toBe('a b')
|
|
41
|
+
})
|
|
29
42
|
})
|
|
@@ -1,7 +1,47 @@
|
|
|
1
|
+
const NAMED_HTML_ENTITIES: Record<string, string> = {
|
|
2
|
+
amp: '&',
|
|
3
|
+
lt: '<',
|
|
4
|
+
gt: '>',
|
|
5
|
+
quot: '"',
|
|
6
|
+
apos: "'",
|
|
7
|
+
nbsp: ' ',
|
|
8
|
+
hellip: '…',
|
|
9
|
+
mdash: '—',
|
|
10
|
+
ndash: '–',
|
|
11
|
+
rsquo: '’',
|
|
12
|
+
lsquo: '‘',
|
|
13
|
+
rdquo: '”',
|
|
14
|
+
ldquo: '“',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function decodeHtmlEntities(value: string): string {
|
|
18
|
+
return value
|
|
19
|
+
.replace(/&#(\d+);/g, (match, dec: string) => {
|
|
20
|
+
const code = Number(dec)
|
|
21
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : match
|
|
22
|
+
})
|
|
23
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (match, hex: string) => {
|
|
24
|
+
const code = parseInt(hex, 16)
|
|
25
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : match
|
|
26
|
+
})
|
|
27
|
+
.replace(/&([a-zA-Z][a-zA-Z0-9]*);/g, (match, name: string) =>
|
|
28
|
+
Object.prototype.hasOwnProperty.call(NAMED_HTML_ENTITIES, name) ? NAMED_HTML_ENTITIES[name] : match,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Converts a Markdown (or legacy HTML) string into a plain-text preview suitable for compact,
|
|
34
|
+
* read-only contexts such as DataTable list cells. Strips Markdown syntax, strips HTML tags, and
|
|
35
|
+
* decodes HTML entities, then collapses whitespace. Handling legacy HTML keeps list previews clean
|
|
36
|
+
* for records authored before the editor moved back to Markdown — no data migration required.
|
|
37
|
+
*
|
|
38
|
+
* This is NOT a sanitizer — render rich content with the Markdown components, not this helper.
|
|
39
|
+
*/
|
|
1
40
|
export function markdownToPlainText(input: string | null | undefined): string {
|
|
2
41
|
if (!input) return ''
|
|
3
42
|
let text = String(input)
|
|
4
43
|
text = text.replace(/```[\s\S]*?```/g, ' ')
|
|
44
|
+
text = text.replace(/<[^>]+>/g, ' ')
|
|
5
45
|
text = text.replace(/`([^`]+)`/g, '$1')
|
|
6
46
|
text = text.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
|
|
7
47
|
text = text.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
@@ -15,6 +55,7 @@ export function markdownToPlainText(input: string | null | undefined): string {
|
|
|
15
55
|
text = text.replace(/\*([^*]+)\*/g, '$1')
|
|
16
56
|
text = text.replace(/_([^_]+)_/g, '$1')
|
|
17
57
|
text = text.replace(/\|/g, ' ')
|
|
58
|
+
text = decodeHtmlEntities(text)
|
|
18
59
|
text = text.replace(/\s+/g, ' ').trim()
|
|
19
60
|
return text
|
|
20
61
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Ambient declaration so side-effect CSS imports from third-party packages
|
|
2
|
+
// (e.g. `import '@mdxeditor/editor/style.css'`) type-check. Packages that consume
|
|
3
|
+
// @open-mercato/ui as source type-check this file's `.tsx` too, so MdxEditorImpl.tsx
|
|
4
|
+
// pulls this declaration into their program via a `/// <reference path>` directive.
|
|
5
|
+
declare module '*.css'
|