@tinacms/app 0.0.24 → 0.0.26
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/appFiles/package.json +3 -1
- package/appFiles/src/App.tsx +25 -1
- package/appFiles/src/fields/rich-text/index.tsx +15 -0
- package/appFiles/src/fields/rich-text/monaco/error-message.tsx +126 -0
- package/appFiles/src/fields/rich-text/monaco/index.tsx +246 -0
- package/appFiles/src/fields/rich-text/monaco/mdx.js +31455 -0
- package/appFiles/src/fields/rich-text/monaco/use-debounce.ts +37 -0
- package/appFiles/src/lib/machines/document-machine.ts +1 -2
- package/appFiles/src/lib/machines/query-machine.ts +12 -4
- package/appFiles/src/preview.tsx +10 -3
- package/dist/index.js +27 -18
- package/dist/test-utils.d.ts +1 -0
- package/dist/test-utils.js +294 -0
- package/package.json +16 -3
package/appFiles/package.json
CHANGED
package/appFiles/src/App.tsx
CHANGED
|
@@ -11,7 +11,8 @@ See the License for the specific language governing permissions and
|
|
|
11
11
|
limitations under the License.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import React, { Suspense } from 'react'
|
|
15
|
+
import TinaCMS, { TinaAdmin, useCMS, MdxFieldPluginExtendible } from 'tinacms'
|
|
15
16
|
import { TinaEditProvider, useEditState } from 'tinacms/dist/edit-state'
|
|
16
17
|
import { Preview } from './preview'
|
|
17
18
|
|
|
@@ -19,9 +20,32 @@ import { Preview } from './preview'
|
|
|
19
20
|
// @ts-expect-error
|
|
20
21
|
import config from 'TINA_IMPORT'
|
|
21
22
|
|
|
23
|
+
const RawEditor = React.lazy(() => import('./fields/rich-text'))
|
|
24
|
+
|
|
25
|
+
const Editor = (props) => {
|
|
26
|
+
const [rawMode, setRawMode] = React.useState(false)
|
|
27
|
+
return (
|
|
28
|
+
<MdxFieldPluginExtendible.Component
|
|
29
|
+
rawMode={rawMode}
|
|
30
|
+
setRawMode={setRawMode}
|
|
31
|
+
{...props}
|
|
32
|
+
rawEditor={
|
|
33
|
+
<Suspense fallback={<div>Loading raw editor...</div>}>
|
|
34
|
+
<RawEditor {...props} setRawMode={setRawMode} rawMode={rawMode} />
|
|
35
|
+
</Suspense>
|
|
36
|
+
}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
const SetPreview = ({ outputFolder }: { outputFolder: string }) => {
|
|
23
42
|
const cms = useCMS()
|
|
24
43
|
cms.flags.set('tina-preview', outputFolder)
|
|
44
|
+
// Override original 'rich-text' field with one that has raw mode support
|
|
45
|
+
cms.fields.add({
|
|
46
|
+
...MdxFieldPluginExtendible,
|
|
47
|
+
Component: Editor,
|
|
48
|
+
})
|
|
25
49
|
return null
|
|
26
50
|
}
|
|
27
51
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Copyright 2021 Forestry.io Holdings, Inc.
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software
|
|
8
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
9
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
|
+
See the License for the specific language governing permissions and
|
|
11
|
+
limitations under the License.
|
|
12
|
+
*/
|
|
13
|
+
import RawEditor from './monaco'
|
|
14
|
+
|
|
15
|
+
export default RawEditor
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
|
|
3
|
+
Copyright 2021 Forestry.io Holdings, Inc.
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
|
|
17
|
+
*/
|
|
18
|
+
import React from 'react'
|
|
19
|
+
import { XCircleIcon } from '@heroicons/react/solid'
|
|
20
|
+
import { Popover, Transition } from '@headlessui/react'
|
|
21
|
+
import { Fragment } from 'react'
|
|
22
|
+
// import { InvalidMarkdownElement } from '@tinacms/mdx/src/parse/plate'
|
|
23
|
+
export type EmptyTextElement = { type: 'text'; text: '' }
|
|
24
|
+
export type PositionItem = {
|
|
25
|
+
line?: number | null
|
|
26
|
+
column?: number | null
|
|
27
|
+
offset?: number | null
|
|
28
|
+
_index?: number | null
|
|
29
|
+
_bufferIndex?: number | null
|
|
30
|
+
}
|
|
31
|
+
export type Position = {
|
|
32
|
+
start: PositionItem
|
|
33
|
+
end: PositionItem
|
|
34
|
+
}
|
|
35
|
+
export type InvalidMarkdownElement = {
|
|
36
|
+
type: 'invalid_markdown'
|
|
37
|
+
value: string
|
|
38
|
+
message: string
|
|
39
|
+
position?: Position
|
|
40
|
+
children: [EmptyTextElement]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ErrorType = {
|
|
44
|
+
message: string
|
|
45
|
+
position?: {
|
|
46
|
+
startColumn: number
|
|
47
|
+
endColumn: number
|
|
48
|
+
startLineNumber: number
|
|
49
|
+
endLineNumber: number
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export const buildError = (element: InvalidMarkdownElement): ErrorType => {
|
|
53
|
+
return {
|
|
54
|
+
message: element.message,
|
|
55
|
+
position: element.position && {
|
|
56
|
+
endColumn: element.position.end.column,
|
|
57
|
+
startColumn: element.position.start.column,
|
|
58
|
+
startLineNumber: element.position.start.line,
|
|
59
|
+
endLineNumber: element.position.end.line,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export const buildErrorMessage = (element: InvalidMarkdownElement): string => {
|
|
64
|
+
if (!element) {
|
|
65
|
+
return ''
|
|
66
|
+
}
|
|
67
|
+
const errorMessage = buildError(element)
|
|
68
|
+
const message = errorMessage
|
|
69
|
+
? `${errorMessage.message}${
|
|
70
|
+
errorMessage.position
|
|
71
|
+
? ` at line: ${errorMessage.position.startLineNumber}, column: ${errorMessage.position.startColumn}`
|
|
72
|
+
: ''
|
|
73
|
+
}`
|
|
74
|
+
: null
|
|
75
|
+
return message
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function ErrorMessage({ error }: { error: InvalidMarkdownElement }) {
|
|
79
|
+
const message = buildErrorMessage(error)
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Popover className="relative">
|
|
83
|
+
{() => (
|
|
84
|
+
<>
|
|
85
|
+
<Popover.Button
|
|
86
|
+
className={`p-2 shaodw-lg border ${
|
|
87
|
+
error ? '' : ' opacity-0 hidden '
|
|
88
|
+
}`}
|
|
89
|
+
>
|
|
90
|
+
<span className="sr-only">Errors</span>
|
|
91
|
+
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
|
|
92
|
+
</Popover.Button>
|
|
93
|
+
<Transition
|
|
94
|
+
as={Fragment}
|
|
95
|
+
enter="transition ease-out duration-200"
|
|
96
|
+
enterFrom="opacity-0 translate-y-1"
|
|
97
|
+
enterTo="opacity-100 translate-y-0"
|
|
98
|
+
leave="transition ease-in duration-150"
|
|
99
|
+
leaveFrom="opacity-100 translate-y-0"
|
|
100
|
+
leaveTo="opacity-0 translate-y-1"
|
|
101
|
+
>
|
|
102
|
+
<Popover.Panel className="absolute top-8 w-[300px] -right-3 z-10 mt-3 px-4 sm:px-0">
|
|
103
|
+
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
|
|
104
|
+
<div className="rounded-md bg-red-50 p-4">
|
|
105
|
+
<div className="flex">
|
|
106
|
+
<div className="flex-shrink-0">
|
|
107
|
+
<XCircleIcon
|
|
108
|
+
className="h-5 w-5 text-red-400"
|
|
109
|
+
aria-hidden="true"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="ml-3">
|
|
113
|
+
<h3 className="text-sm font-medium text-red-800 whitespace-pre-wrap">
|
|
114
|
+
{message}
|
|
115
|
+
</h3>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</Popover.Panel>
|
|
121
|
+
</Transition>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</Popover>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
|
|
3
|
+
Copyright 2021 Forestry.io Holdings, Inc.
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import React from 'react'
|
|
20
|
+
import MonacoEditor, { useMonaco, loader } from '@monaco-editor/react'
|
|
21
|
+
/**
|
|
22
|
+
* MDX is built directly to the app because of how we load dependencies.
|
|
23
|
+
* Since we drop the package.json in to the end users folder, we can't
|
|
24
|
+
* easily install the current version of the mdx package in all scenarios
|
|
25
|
+
* (when we're working in the monorepo, or working with a tagged npm version)
|
|
26
|
+
*/
|
|
27
|
+
import { parseMDX, stringifyMDX } from './mdx'
|
|
28
|
+
import { useDebounce } from './use-debounce'
|
|
29
|
+
import type * as monaco from 'monaco-editor'
|
|
30
|
+
import {
|
|
31
|
+
buildError,
|
|
32
|
+
ErrorMessage,
|
|
33
|
+
InvalidMarkdownElement,
|
|
34
|
+
} from './error-message'
|
|
35
|
+
import { RichTextType } from 'tinacms'
|
|
36
|
+
|
|
37
|
+
export const uuid = () => {
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
|
40
|
+
(
|
|
41
|
+
c ^
|
|
42
|
+
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
|
43
|
+
).toString(16)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Monaco = typeof monaco
|
|
48
|
+
|
|
49
|
+
// 0.33.0 has a bug https://github.com/microsoft/monaco-editor/issues/2947
|
|
50
|
+
loader.config({
|
|
51
|
+
paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.31.1/min/vs' },
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Since monaco lazy-loads we may have a delay from when the block is inserted
|
|
56
|
+
* to when monaco has intantiated, keep trying to focus on it.
|
|
57
|
+
*
|
|
58
|
+
* Will try for 3 seconds before moving on
|
|
59
|
+
*/
|
|
60
|
+
let retryCount = 0
|
|
61
|
+
const retryFocus = (ref) => {
|
|
62
|
+
if (ref.current) {
|
|
63
|
+
ref.current.focus()
|
|
64
|
+
} else {
|
|
65
|
+
if (retryCount < 30) {
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
retryCount = retryCount + 1
|
|
68
|
+
retryFocus(ref)
|
|
69
|
+
}, 100)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const RawEditor = (props: RichTextType) => {
|
|
75
|
+
const monaco = useMonaco() as Monaco
|
|
76
|
+
const monacoEditorRef =
|
|
77
|
+
React.useRef<monaco.editor.IStandaloneCodeEditor>(null)
|
|
78
|
+
const [height, setHeight] = React.useState(100)
|
|
79
|
+
const id = React.useMemo(() => uuid(), [])
|
|
80
|
+
const field = props.field
|
|
81
|
+
const inputValue = React.useMemo(() => {
|
|
82
|
+
// @ts-ignore no access to the rich-text type from this package
|
|
83
|
+
const res = stringifyMDX(props.input.value, field, (value) => value)
|
|
84
|
+
return typeof props.input.value === 'string' ? props.input.value : res
|
|
85
|
+
}, [])
|
|
86
|
+
const [value, setValue] = React.useState(inputValue)
|
|
87
|
+
const [error, setError] = React.useState<InvalidMarkdownElement>(null)
|
|
88
|
+
|
|
89
|
+
const debouncedValue = useDebounce(value, 500)
|
|
90
|
+
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
// @ts-ignore no access to the rich-text type from this package
|
|
93
|
+
const parsedValue = parseMDX(value, field, (value) => value)
|
|
94
|
+
if (parsedValue.children[0]) {
|
|
95
|
+
if (parsedValue.children[0].type === 'invalid_markdown') {
|
|
96
|
+
const invalidMarkdown = parsedValue.children[0]
|
|
97
|
+
setError(invalidMarkdown)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
props.input.onChange(parsedValue)
|
|
102
|
+
setError(null)
|
|
103
|
+
}, [JSON.stringify(debouncedValue)])
|
|
104
|
+
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
if (monacoEditorRef.current) {
|
|
107
|
+
if (error) {
|
|
108
|
+
const errorMessage = buildError(error)
|
|
109
|
+
monaco.editor.setModelMarkers(monacoEditorRef.current.getModel(), id, [
|
|
110
|
+
{
|
|
111
|
+
...errorMessage.position,
|
|
112
|
+
message: errorMessage.message,
|
|
113
|
+
severity: 8,
|
|
114
|
+
},
|
|
115
|
+
])
|
|
116
|
+
} else {
|
|
117
|
+
monaco.editor.setModelMarkers(
|
|
118
|
+
monacoEditorRef.current.getModel(),
|
|
119
|
+
id,
|
|
120
|
+
[]
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, [JSON.stringify(error), monacoEditorRef.current])
|
|
125
|
+
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
if (monaco) {
|
|
128
|
+
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
|
|
129
|
+
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
|
|
130
|
+
// disable errors
|
|
131
|
+
noSemanticValidation: true,
|
|
132
|
+
noSyntaxValidation: true,
|
|
133
|
+
})
|
|
134
|
+
// TODO: autocomplete suggestions
|
|
135
|
+
// monaco.languages.registerCompletionItemProvider('markdown', {
|
|
136
|
+
// provideCompletionItems: function (model, position) {
|
|
137
|
+
// const word = model.getWordUntilPosition(position)
|
|
138
|
+
// const range = {
|
|
139
|
+
// startLineNumber: position.lineNumber,
|
|
140
|
+
// endLineNumber: position.lineNumber,
|
|
141
|
+
// startColumn: word.startColumn,
|
|
142
|
+
// endColumn: word.endColumn,
|
|
143
|
+
// }
|
|
144
|
+
// return {
|
|
145
|
+
// suggestions: [
|
|
146
|
+
// {
|
|
147
|
+
// label: '<DateTime />',
|
|
148
|
+
// insertText: '<DateTime format="iso" />',
|
|
149
|
+
// kind: 0,
|
|
150
|
+
// range,
|
|
151
|
+
// },
|
|
152
|
+
// ],
|
|
153
|
+
// }
|
|
154
|
+
// },
|
|
155
|
+
// })
|
|
156
|
+
}
|
|
157
|
+
}, [monaco])
|
|
158
|
+
|
|
159
|
+
function handleEditorDidMount(
|
|
160
|
+
monacoEditor: monaco.editor.IStandaloneCodeEditor,
|
|
161
|
+
monaco: Monaco
|
|
162
|
+
) {
|
|
163
|
+
monacoEditorRef.current = monacoEditor
|
|
164
|
+
monacoEditor.onDidContentSizeChange(() => {
|
|
165
|
+
// FIXME: if the window is too tall the performance degrades, come up with a nice
|
|
166
|
+
// balance between the two
|
|
167
|
+
setHeight(Math.min(Math.max(100, monacoEditor.getContentHeight()), 1000))
|
|
168
|
+
monacoEditor.layout()
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className="relative">
|
|
174
|
+
<div className="sticky -top-4 w-full flex justify-between mb-2 z-50 max-w-full">
|
|
175
|
+
<Button onClick={() => props.setRawMode(false)}>
|
|
176
|
+
View in rich-text editor
|
|
177
|
+
</Button>
|
|
178
|
+
<ErrorMessage error={error} />
|
|
179
|
+
</div>
|
|
180
|
+
<div style={{ height: `${height}px` }}>
|
|
181
|
+
<MonacoEditor
|
|
182
|
+
path={id}
|
|
183
|
+
onMount={handleEditorDidMount}
|
|
184
|
+
// Setting a custom theme is kind of buggy because it doesn't get defined until monaco has mounted.
|
|
185
|
+
// So we end up with the default (light) theme in some scenarios. Seems like a race condition.
|
|
186
|
+
// theme="vs-dark"
|
|
187
|
+
options={{
|
|
188
|
+
scrollBeyondLastLine: false,
|
|
189
|
+
tabSize: 2,
|
|
190
|
+
disableLayerHinting: true,
|
|
191
|
+
accessibilitySupport: 'off',
|
|
192
|
+
codeLens: false,
|
|
193
|
+
wordWrap: 'on',
|
|
194
|
+
minimap: {
|
|
195
|
+
enabled: false,
|
|
196
|
+
},
|
|
197
|
+
fontSize: 14,
|
|
198
|
+
lineHeight: 2,
|
|
199
|
+
formatOnPaste: true,
|
|
200
|
+
lineNumbers: 'off',
|
|
201
|
+
formatOnType: true,
|
|
202
|
+
fixedOverflowWidgets: true,
|
|
203
|
+
// Takes too much horizontal space for iframe
|
|
204
|
+
folding: false,
|
|
205
|
+
renderLineHighlight: 'none',
|
|
206
|
+
scrollbar: {
|
|
207
|
+
verticalScrollbarSize: 1,
|
|
208
|
+
horizontalScrollbarSize: 1,
|
|
209
|
+
// https://github.com/microsoft/monaco-editor/issues/2007#issuecomment-644425664
|
|
210
|
+
alwaysConsumeMouseWheel: false,
|
|
211
|
+
},
|
|
212
|
+
}}
|
|
213
|
+
language={'markdown'}
|
|
214
|
+
value={value}
|
|
215
|
+
onChange={(value) => {
|
|
216
|
+
try {
|
|
217
|
+
setValue(value)
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.log('error', e)
|
|
220
|
+
}
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const Button = (props) => {
|
|
229
|
+
return (
|
|
230
|
+
<button
|
|
231
|
+
className={`${
|
|
232
|
+
props.align === 'left'
|
|
233
|
+
? 'rounded-l-md border-r-0'
|
|
234
|
+
: 'rounded-r-md border-l-0'
|
|
235
|
+
} shadow rounded-md bg-white cursor-pointer relative inline-flex items-center px-2 py-2 border border-gray-200 hover:text-white text-sm font-medium transition-all ease-out duration-150 hover:bg-blue-500 focus:z-10 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500`}
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={props.onClick}
|
|
238
|
+
>
|
|
239
|
+
<span className="text-sm font-semibold tracking-wide align-baseline mr-1">
|
|
240
|
+
{props.children}
|
|
241
|
+
</span>
|
|
242
|
+
</button>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export default RawEditor
|