Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f5d42cb
feat(serialization): add first pass at serialization
tefkah May 7, 2025
ce77fcd
feat(ui): add new show more component
tefkah Apr 30, 2025
204fa4f
feat(serialization): add proper serialization libs
tefkah Apr 30, 2025
c33097f
refactor(serialization): parse and convert prosemirror tree during
tefkah Apr 30, 2025
8f49b17
feat(fts|serialization): properly index html
tefkah May 1, 2025
d9f4f20
fix(serialization): sort of fix loading
tefkah May 7, 2025
e92f261
fix: fix weird redis install issue
tefkah May 7, 2025
550c767
dev: make legacy seed have useful richtext
tefkah May 7, 2025
71439d6
dev: make legacy seed have useful richtext
tefkah May 8, 2025
642bbd1
feat(math): serialize math to mathml and back reliably
tefkah May 8, 2025
9997e8f
feat(serialization): find a happy way to store html and serialize on …
tefkah May 12, 2025
2b3c8b5
chore: remove old validation function, fix test
tefkah May 12, 2025
e5bf99f
fix: fix the stories
tefkah May 12, 2025
b83d855
fix: really fix math, cleanup
tefkah May 12, 2025
e2b5978
feat: add migration to clear current richtext values
tefkah May 12, 2025
276fffe
Merge branch 'main' into tfk/html-richtext
tefkah May 12, 2025
6c740c6
chore: meaningless change to trigger ci
tefkah May 12, 2025
53bb547
fix: fix fts related type issues
tefkah May 12, 2025
162b545
fix: fix contexteditor related type issues
tefkah May 12, 2025
0ced401
chore: don't format html
tefkah May 12, 2025
76e70e8
fix: fix test to work with figures
tefkah May 12, 2025
7831b83
chore: revert and merge with main
tefkah May 15, 2025
ec44cc8
fix: modify tests
tefkah May 15, 2025
39b0628
Merge branch 'main' into tfk/html-richtext
tefkah May 19, 2025
829eb08
chore: remove silly console.log messages in cache layer
tefkah May 20, 2025
b892c62
refactor: move the html/prosemirror boundary to the form
tefkah May 20, 2025
0dded00
chore: merge with main
tefkah May 20, 2025
114206f
fix: handle no value for richtext
tefkah May 21, 2025
e0f038d
fix: use more predictable handling of temp richtext value, add explan…
tefkah May 21, 2025
7a6ea14
refactor: get rid of forward ref, it is cringe
tefkah May 21, 2025
971e977
Merge branch 'main' into tfk/html-richtext
tefkah May 21, 2025
81d60fc
fix: make ref optional
tefkah May 21, 2025
848a258
fix: use consolidate prosemirror/html conversion fns a bit
tefkah May 21, 2025
1bbb209
Merge branch 'main' into tfk/html-richtext
tefkah May 22, 2025
05564bd
docs: update rich text docs
tefkah May 22, 2025
a3d9614
fix: find math display correctly
tefkah May 22, 2025
22c80b2
Merge branch 'main' into tfk/html-richtext
tefkah May 27, 2025
25f761d
fix?: use localhost instead of docker-host for minio init
tefkah May 27, 2025
3edd460
debug: add some logs
tefkah May 27, 2025
4b4c059
fix?: upgrade aws?
tefkah May 27, 2025
7ced2c4
fix(minio): use new minio client syntax after breaking change
tefkah May 27, 2025
243d4ea
Merge branch 'main' into tfk/html-richtext
tefkah May 27, 2025
9881d52
chore: merge
tefkah May 27, 2025
e77d8a5
chore: merge
tefkah May 29, 2025
0aa25f9
fix: cave to erics CRAZY demands
tefkah May 29, 2025
71984ef
fix: remove unnecessary memo
tefkah May 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/.prettierignore
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
17 changes: 13 additions & 4 deletions core/app/c/[communitySlug]/pubs/[pubId]/components/PubValues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { CoreSchemaType } from "db/public";
import { Button } from "ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "ui/collapsible";
import { ChevronDown, ChevronRight } from "ui/icon";
import { ShowMore } from "ui/show-more";

import type { FileUpload } from "~/lib/fields/fileUpload";
import { FileUploadPreview } from "~/app/components/forms/FileUpload";
import { getPubTitle, valuesWithoutTitle } from "~/lib/pubs";

Expand Down Expand Up @@ -207,11 +207,9 @@ const PubValue = ({ value }: { value: FullProcessedPubWithForm["values"][number]
);
}

// Currently, we are only rendering string versions of fields, except for file uploads
// For file uploads, because Unjournal doesn't have schemaNames yet, we check the value structure
const fileUploadSchema = getJsonSchemaByCoreSchemaType(CoreSchemaType.FileUpload);
if (Value.Check(fileUploadSchema, value.value)) {
return <FileUploadPreview files={value.value as FileUpload} />;
return <FileUploadPreview files={value.value} />;
}

if (value.schemaName === CoreSchemaType.DateTime) {
Expand All @@ -221,6 +219,17 @@ const PubValue = ({ value }: { value: FullProcessedPubWithForm["values"][number]
}
}

if (value.schemaName === CoreSchemaType.RichText) {
return (
<ShowMore animate={false}>
<div
className="prose prose-sm"
dangerouslySetInnerHTML={{ __html: value.value as string }}
/>
</ShowMore>
);
}
Comment on lines +223 to +232

Copy link
Copy Markdown
Member Author

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 ReadOnly version of themselves which can be displayed here.

Copy link
Copy Markdown
Member Author

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


const valueAsString = (value.value as JsonValue)?.toString() || "";

let renderedField: ReactNode = valueAsString;
Expand Down
7 changes: 4 additions & 3 deletions core/app/components/ContextEditor/ContextEditorClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const ContextEditorClient = ({
pubId,
pubTypeId,
className,
initialDoc,
initialHtml,
onChange,
disabled,
hideMenu,
Expand All @@ -38,9 +38,10 @@ export const ContextEditorClient = ({
// Might be able to use more of this type in the future—for now, this component is a lil more stricty typed than context-editor
} & Pick<
ContextEditorProps,
"onChange" | "initialDoc" | "className" | "disabled" | "hideMenu"
"onChange" | "initialHtml" | "className" | "disabled" | "hideMenu"
>) => {
const runUpload = useServerAction(upload);

const getPubs = useCallback(
(filter: string) => {
return new Promise<any[]>((resolve, reject) => {
Expand All @@ -65,7 +66,7 @@ export const ContextEditorClient = ({
}}
atomRenderingComponent={ContextAtom}
onChange={onChange}
initialDoc={initialDoc}
initialHtml={initialHtml}
disabled={disabled}
className={className}
hideMenu={hideMenu}
Expand Down
13 changes: 10 additions & 3 deletions core/app/components/forms/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { Button } from "ui/button";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "ui/hover-card";

import type { FileUpload } from "~/lib/fields/fileUpload";

export function FileUploadPreview({ files }: { files: FileUpload }) {
export function FileUploadPreview({
files,
}: {
files: {
fileName: string;
fileSize: number;
fileType: string;
fileUploadUrl: string;
}[];
}) {
return (
<ul>
{files.map((file) => {
Expand Down
143 changes: 63 additions & 80 deletions core/app/components/forms/elements/ContextEditorElement.tsx
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";
Expand All @@ -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"
/>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 RichText field to the prosemirror tree in the form. i thought it a bit too much to convert the prosemirror tree to html on each change.

what's annoying is that this sort of breaks the nice dichotomy i had where only the actual ContextEditor knew that the RichText field should be converted to a ProseMirror tree, everything else just thinks it's HTML. this idea is maybe flawed in general.

</FormControl>
</div>
<FormDescription>{help}</FormDescription>
<FormMessage />
</FormItem>
);
},
(prevProps, nextProps) => {
// delete prevProps.field;
// delete nextProps.field;
return true;
}
);
Comment thread
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)) {
Expand All @@ -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} />
)}
/>
);
};
9 changes: 8 additions & 1 deletion core/app/components/pubs/PubEditor/PubEditorClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const buildDefaultValues = (
if (element.schemaName === CoreSchemaType.DateTime && pubValue) {
defaultValues[element.slug] = new Date(pubValue as string);
}

// There can be multiple relations for a single slug
if (element.isRelation) {
const relatedPubValues = pubValues.filter((v) => v.fieldSlug === element.slug);
Expand Down Expand Up @@ -163,11 +164,17 @@ const createSchemaFromElements = (
return [slug, undefined];
}

const schema = getJsonSchemaByCoreSchemaType(schemaName, config);
let schema = getJsonSchemaByCoreSchemaType(schemaName, config);
if (!schema) {
return [slug, undefined];
}

if (schemaName === CoreSchemaType.RichText) {
// TODO: find a better solution for this
// @ts-expect-error Cant assign any to the schema, but it's fine
schema = Type.Any();
}

// Allow fields to be empty or optional. Special case for empty strings,
// which happens when you enter something in an input field and then delete it
// TODO: reevaluate whether this should be "" or undefined
Expand Down
4 changes: 3 additions & 1 deletion core/cache-handler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down Expand Up @@ -191,6 +191,7 @@ function createRedisHandler({

async function getCacheHandlerPromise() {
let redisClient = null;
console.log("im walkin here");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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({
Expand Down Expand Up @@ -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) {
Expand Down
20 changes: 9 additions & 11 deletions core/lib/__tests__/validateFields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, test } from "vitest";

import { CoreSchemaType } from "db/public";

import { _deprecated_validatePubValuesBySchemaName } from "~/lib/server/validateFields";
import { validatePubValuesBySchemaName } from "~/lib/server/validateFields";

const JSON_EXAMPLE = {
content: [
Expand All @@ -20,19 +20,17 @@ const JSON_EXAMPLE = {
};

test.each([
{ value: "", expected: false },
{ value: 1, expected: false },
{ value: {}, expected: false },
{ value: JSON_EXAMPLE, expected: true },
{ value: JSON_EXAMPLE, expected: `<p>Example document</p>` },
// "" is a valid html value
{ value: "", expected: "" },
])("validating rich text field", ({ value, expected }) => {
const fields = [
{ name: "Rich Text", slug: "community:richtext", schemaName: CoreSchemaType.RichText },
];
const values = { "community:richtext": value };
const result = _deprecated_validatePubValuesBySchemaName({ fields, values });
if (expected) {
expect(result).toStrictEqual({});
const values = [{ slug: "community:richtext", value, schemaName: CoreSchemaType.RichText }];
const result = validatePubValuesBySchemaName(values);
if (expected !== false) {
expect(result.results[0].value).toStrictEqual(expected);
} else {
expect(result[fields[0].slug]).toBeTruthy();
expect(result.errors.length).toBeGreaterThan(0);
}
});
34 changes: 34 additions & 0 deletions core/lib/editor/process-editor-html.ts
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,
};
};
Loading
Loading