-
Notifications
You must be signed in to change notification settings - Fork 10
feat: store richtext data as HTML rather than a Prosemirror tree #1239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 21 commits
f5d42cb
ce77fcd
204fa4f
c33097f
8f49b17
d9f4f20
e92f261
550c767
71439d6
642bbd1
9997e8f
2b3c8b5
e5bf99f
b83d855
e2b5978
276fffe
6c740c6
53bb547
162b545
0ced401
76e70e8
7831b83
ec44cc8
39b0628
829eb08
b892c62
0dded00
114206f
e0f038d
7a6ea14
971e977
81d60fc
848a258
1bbb209
05564bd
a3d9614
22c80b2
25f761d
3edd460
4b4c059
7ced2c4
243d4ea
9881d52
e77d8a5
0aa25f9
71984ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| app/c/\[communitySlug\]/developers/docs/stoplight.styles.css | ||
| vitest-bench.local.json | ||
| vitest-bench.local.json | ||
| **/*.html |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| "use client"; | ||
|
|
||
| import type { Node } from "prosemirror-model"; | ||
| import type { ControllerRenderProps, FieldValues } from "react-hook-form"; | ||
|
|
||
| import { useState } from "react"; | ||
| import { memo, useMemo, useState } from "react"; | ||
| import { Value } from "@sinclair/typebox/value"; | ||
| import { EMPTY_DOC } from "context-editor"; | ||
| import { docHasChanged } from "context-editor/utils"; | ||
| import { useFormContext } from "react-hook-form"; | ||
| import { richTextInputConfigSchema } from "schemas"; | ||
|
|
@@ -17,81 +18,73 @@ import { ContextEditorClient } from "../../ContextEditor/ContextEditorClient"; | |
| import { useContextEditorContext } from "../../ContextEditor/ContextEditorContext"; | ||
| import { useFormElementToggleContext } from "../FormElementToggleContext"; | ||
|
|
||
| const EMPTY_DOC = { | ||
| type: "doc", | ||
| attrs: { | ||
| meta: {}, | ||
| }, | ||
| content: [ | ||
| { | ||
| type: "paragraph", | ||
| attrs: { | ||
| id: null, | ||
| class: null, | ||
| }, | ||
| }, | ||
| ], | ||
| }; | ||
| const EditorFormElement = memo( | ||
| function EditorFormElement({ | ||
| field, | ||
| label, | ||
| help, | ||
| }: { | ||
| field: ControllerRenderProps<FieldValues, string>; | ||
| label: string; | ||
| help?: string; | ||
| }) { | ||
| const formElementToggle = useFormElementToggleContext(); | ||
| const { pubs, pubTypes, pubId, pubTypeId } = useContextEditorContext(); | ||
|
|
||
| const EditorFormElement = ({ | ||
| label, | ||
| help, | ||
| onChange, | ||
| initialValue, | ||
| disabled, | ||
| }: { | ||
| label: string; | ||
| help?: string; | ||
| onChange: (state: any) => void; | ||
| initialValue?: Node; | ||
| disabled?: boolean; | ||
| }) => { | ||
| const { pubs, pubTypes, pubId, pubTypeId } = useContextEditorContext(); | ||
| const [initialDoc] = useState(initialValue); | ||
| const f = useMemo(() => { | ||
| return field; | ||
| }, []); | ||
|
|
||
| if (!pubId || !pubTypeId) { | ||
| return null; | ||
| } | ||
| const [initialHtml] = useState(f.value); | ||
|
|
||
| return ( | ||
| <FormItem> | ||
| <FormLabel className="flex">{label}</FormLabel> | ||
| <div className="w-full"> | ||
| <FormControl> | ||
| <ContextEditorClient | ||
| pubId={pubId} | ||
| pubs={pubs} | ||
| pubTypes={pubTypes} | ||
| pubTypeId={pubTypeId} | ||
| onChange={(state) => { | ||
| // Control changing the state more granularly or else the dirty field will trigger on load | ||
| // Since we can't control the dirty state directly, even this workaround does not handle the case of | ||
| // if someone changes the doc but then reverts it--that will still count as dirty since react-hook-form is tracking that | ||
| const hasChanged = docHasChanged(initialDoc ?? EMPTY_DOC, state); | ||
| if (hasChanged) { | ||
| onChange(state); | ||
| } | ||
| }} | ||
| initialDoc={initialDoc} | ||
| disabled={disabled} | ||
| className="max-h-96 overflow-scroll" | ||
| /> | ||
| </FormControl> | ||
| </div> | ||
| <FormDescription>{help}</FormDescription> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| ); | ||
| }; | ||
| if (!pubId || !pubTypeId) { | ||
| return <></>; | ||
| } | ||
| const disabled = !formElementToggle.isEnabled(f.name); | ||
|
|
||
| return ( | ||
| <FormItem> | ||
| <FormLabel className="flex">{label}</FormLabel> | ||
| <div className="w-full"> | ||
| <FormControl> | ||
| <ContextEditorClient | ||
| pubId={pubId} | ||
| pubs={pubs} | ||
| pubTypes={pubTypes} | ||
| pubTypeId={pubTypeId} | ||
| onChange={(state, initialDoc, initialHtml) => { | ||
| // Control changing the state more granularly or else the dirty field will trigger on load | ||
| // Since we can't control the dirty state directly, even this workaround does not handle the case of | ||
| // if someone changes the doc but then reverts it--that will still count as dirty since react-hook-form is tracking that | ||
| const hasChanged = docHasChanged(initialDoc ?? EMPTY_DOC, state); | ||
| if (hasChanged) { | ||
| f.onChange(serializeProseMirrorDoc(state.doc)); | ||
| } | ||
| }} | ||
| initialHtml={initialHtml} | ||
| disabled={disabled} | ||
| className="max-h-96 overflow-scroll" | ||
| /> | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so this is a bit cringe: we are still intermittently setting the value of a what's annoying is that this sort of breaks the nice dichotomy i had where only the actual |
||
| </FormControl> | ||
| </div> | ||
| <FormDescription>{help}</FormDescription> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| ); | ||
| }, | ||
| (prevProps, nextProps) => { | ||
| // delete prevProps.field; | ||
| // delete nextProps.field; | ||
| return true; | ||
| } | ||
| ); | ||
|
tefkah marked this conversation as resolved.
Outdated
|
||
|
|
||
| export const ContextEditorElement = ({ | ||
| slug, | ||
| label, | ||
| config, | ||
| }: ElementProps<InputComponent.richText>) => { | ||
| const { control } = useFormContext(); | ||
| const formElementToggle = useFormElementToggleContext(); | ||
| const isEnabled = formElementToggle.isEnabled(slug); | ||
|
|
||
| Value.Default(richTextInputConfigSchema, config); | ||
| if (!Value.Check(richTextInputConfigSchema, config)) { | ||
|
|
@@ -102,19 +95,9 @@ export const ContextEditorElement = ({ | |
| <FormField | ||
| control={control} | ||
| name={slug} | ||
| render={({ field }) => { | ||
| return ( | ||
| <EditorFormElement | ||
| label={label} | ||
| help={config.help} | ||
| onChange={(state) => { | ||
| field.onChange(serializeProseMirrorDoc(state.doc)); | ||
| }} | ||
| initialValue={field.value} | ||
| disabled={!isEnabled} | ||
| /> | ||
| ); | ||
| }} | ||
| render={({ field }) => ( | ||
| <EditorFormElement field={field} label={label} help={config.help} /> | ||
| )} | ||
| /> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ function createRedisHandler({ | |
| revalidateTagQuerySize = 1e4, | ||
| }) { | ||
| function assertClientIsReady() { | ||
| if (!client.status === "ready") { | ||
| if (client.status !== "ready") { | ||
| // Throwing here ensures that we immediately fall back to uncached behavior, rather than | ||
| // waiting for the command timeout | ||
| throw new Error("Redis client is not ready yet or connection is lost."); | ||
|
|
@@ -191,6 +191,7 @@ function createRedisHandler({ | |
|
|
||
| async function getCacheHandlerPromise() { | ||
| let redisClient = null; | ||
| console.log("im walkin here"); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whoops, ill remove this |
||
| if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) { | ||
| try { | ||
| redisClient = new Redis({ | ||
|
|
@@ -237,6 +238,7 @@ async function getCacheHandlerPromise() { | |
|
|
||
| // Usual onCreation from @neshca/cache-handler | ||
| CacheHandler.onCreation(() => { | ||
| console.log("im walkin here 2"); | ||
| // Important - It's recommended to use global scope to ensure only one Redis connection is made | ||
| // This ensures only one instance get created | ||
| if (global.cacheHandlerConfig) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import rehypeFormat from "rehype-format"; | ||
| import rehypeParse from "rehype-parse"; | ||
| import rehypeStringify from "rehype-stringify"; | ||
| import { unified } from "unified"; | ||
|
|
||
| export const processEditorHTML = ( | ||
| html: string, | ||
| opts?: { | ||
| plugins?: any[]; | ||
| settings?: { | ||
| fragment?: boolean; | ||
| pretty?: boolean; | ||
| }; | ||
| } | ||
| ) => { | ||
| const processor = unified().use(rehypeParse, opts?.settings); | ||
|
|
||
| if (opts?.settings?.pretty) { | ||
| processor.use(rehypeFormat); | ||
| } | ||
| if (opts?.plugins) { | ||
| opts.plugins.forEach((plugin) => { | ||
| processor.use(plugin); | ||
| }); | ||
| } | ||
|
|
||
| return { | ||
| html: async () => { | ||
| const file = await processor.use(rehypeStringify).process(html); | ||
| return String(file); | ||
| }, | ||
| processor, | ||
| }; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ideally we wouldnt do manual check here, but have InputComponents maybe define some default
ReadOnlyversion of themselves which can be displayed here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i might look into this here #1209