Skip to content

Commit

Permalink
fix(Insight): CKEditor and various stability fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner committed Dec 29, 2023
1 parent f2124f9 commit 5b3bc6a
Show file tree
Hide file tree
Showing 13 changed files with 9,843 additions and 4,318 deletions.
13,881 changes: 9,636 additions & 4,245 deletions client/package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
"serve": "vite preview"
},
"dependencies": {
"@ckeditor/ckeditor5-react": "^6.2.0",
"@fortawesome/fontawesome-free": "^5.15.4",
"@libretexts/insight-ckeditor5-build": "^1.0.0",
"@tailwindcss/typography": "^0.5.10",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"@types/quill": "^2.0.13",
"axios": "^1.6.0",
"axios": "^1.6.3",
"date-and-time": "^0.14.2",
"date-fns": "^2.29.3",
"date-fns-tz": "^2.0.0",
Expand All @@ -27,7 +29,6 @@
"marked": "^4.0.10",
"prop-types": "^15.7.2",
"query-string": "^6.14.1",
"quill": "^1.3.7",
"react": "^17.0.2",
"react-big-calendar": "^0.38.1",
"react-circular-progressbar": "^2.1.0",
Expand All @@ -40,7 +41,6 @@
"react-mentions": "^4.3.0",
"react-minimal-pie-chart": "^8.2.0",
"react-popper": "^2.3.0",
"react-quill": "^2.0.0",
"react-redux": "^7.2.4",
"react-responsive": "^9.0.0-beta.3",
"react-router-dom": "^5.2.0",
Expand Down
91 changes: 91 additions & 0 deletions client/src/components/kb/KBCKEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import axios from "axios";
import { useRef, useEffect, useState } from "react";
import LoadingSpinner from "../LoadingSpinner";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import InsightEditor from "@libretexts/insight-ckeditor5-build";
import Editor from "@libretexts/insight-ckeditor5-build";
import { Plugin } from "@ckeditor/ckeditor5-core";

interface KBCKEditorProps extends React.HTMLAttributes<HTMLDivElement> {
data: string | undefined;
onDataChange: (newData: string) => void;
pageUUID: string;
}

const KBCKEditor: React.FC<KBCKEditorProps> = ({
data,
onDataChange,
pageUUID,
...rest
}) => {
const [loading, setLoading] = useState(false);

class ImageUploadAdapterPlugin {
private loader: any;
constructor(loader: any) {
this.loader = loader;
}

upload() {
return this.loader.file.then(
(file: any) =>
new Promise((resolve, reject) => {
const myReader = new FileReader();
myReader.addEventListener("loadend", (e: any) => {
this.sendRequest(file).then((res) => {
if (!res || typeof res !== "string") reject("Invalid response");
resolve({ default: res });
});
});
myReader.readAsDataURL(file);
})
);
}

async sendRequest(file: File) {
const formData = new FormData();
formData.append("file", file);

const uploadRes = await axios.post(
`/kb/page/${pageUUID}/files`,
formData,
{
headers: { "Content-Type": "multipart/form-data" },
}
);
if (uploadRes.data.err || !uploadRes.data.url) {
throw new Error(uploadRes.data.errMsg);
}
return uploadRes.data.url;
}
}

return (
<div aria-busy={loading} {...rest}>
{loading && <LoadingSpinner />}
<CKEditor
//@ts-expect-error
editor={InsightEditor}
data={data}
onReady={(editor) => {
editor.plugins.get("FileRepository").createUploadAdapter = (
loader
) => {
return new ImageUploadAdapterPlugin(loader);
};
}}
onChange={(event, editor) => {
const data = editor.data.get();
onDataChange(data);
}}
/>
<p className="text-xs text-gray-500 italic ml-1">
Caution: When using Source mode, ensure you toggle back to WYSIWYG mode
before saving. Otherwise, your changes may be lost. CKEditor will ignore
invalid HTML when toggling back to WYSIWYG mode.
</p>
</div>
);
};

export default KBCKEditor;
107 changes: 64 additions & 43 deletions client/src/components/kb/KBPageEditMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { required } from "../../utils/formRules";
import useQueryParam from "../../utils/useQueryParam";
import PageStatusLabel from "./PageStatusLabel";
import { checkIsUUID, getKBSharingObj } from "../../utils/kbHelpers";
import KBCKEditor from "./KBCKEditor";
const ConfirmDeletePageModal = lazy(() => import("./ConfirmDeletePageModal"));

const KBPageEditMode = ({
Expand Down Expand Up @@ -56,7 +57,9 @@ const KBPageEditMode = ({
const isUUID = checkIsUUID(slug);

setLoading(true);
const res = await axios.get(`/kb/page/${isUUID ? `${slug}` : `slug/${slug}`}`);
const res = await axios.get(
`/kb/page/${isUUID ? `${slug}` : `slug/${slug}`}`
);
if (res.data.err) {
throw new Error(res.data.errMsg);
}
Expand All @@ -73,7 +76,7 @@ const KBPageEditMode = ({
}

async function handleSave(status: "published" | "draft") {
if (!await trigger()) return;
if (!(await trigger())) return;
if (mode === "edit") {
handleUpdate(status);
} else {
Expand Down Expand Up @@ -107,10 +110,10 @@ const KBPageEditMode = ({
async function handleUpdate(status: "published" | "draft") {
try {
setLoading(true);
if (!getValues('uuid')) return;
if (!getValues("uuid")) return;
_checkSlug();

const res = await axios.patch(`/kb/page/${getValues('uuid')}`, {
const res = await axios.patch(`/kb/page/${getValues("uuid")}`, {
...getValues(),
status,
lastEditedBy: user.uuid,
Expand All @@ -132,14 +135,19 @@ const KBPageEditMode = ({
}

function _checkSlug() {
if (getValues('slug') && ['new', 'edit', 'create', 'welcome'].includes(getValues('slug').trim())) {
throw new Error("Slug cannot be reserved word ('new', 'edit', 'create', 'welcome')");
if (
getValues("slug") &&
["new", "edit", "create", "welcome"].includes(getValues("slug").trim())
) {
throw new Error(
"Slug cannot be reserved word ('new', 'edit', 'create', 'welcome')"
);
}
}

const EditorOptions = ({ editMode }: { editMode: boolean }) => {
return (
<div className="flex flex-row">
<div className="flex flex-row h-8">
{editMode && (
<>
<Button
Expand All @@ -162,23 +170,26 @@ const KBPageEditMode = ({
</Button>
</>
)}
{mode === "edit" && (
<Button
color="green"
loading={loading}
onClick={() => handleSave("published")}
size="mini"
>
<Icon name="save" />
Save & Publish
</Button>
)}
<Button
color="blue"
loading={loading}
onClick={() => handleSave("published")}
size="mini"
>
<Icon name="save" />
{mode === "edit" ? "Save & Publish" : "Create & Publish"}
</Button>
<Button
color="green"
color={editMode ? "blue" : "green"}
basic={editMode}
loading={loading}
onClick={() => handleSave("draft")}
size="mini"
>
<Icon name="save" />
{mode === "edit" ? "Save as Draft" : "Create as Draft"}
{editMode ? "Save as Draft" : "Create Draft"}
</Button>
</div>
);
Expand All @@ -194,16 +205,17 @@ const KBPageEditMode = ({
)}
<div className="flex flex-row justify-between">
<div className="flex flex-row items-center">
<p className="text-3xl font-semibold">
{mode === "edit" ? (
<span>
Editing Page: <em>{getValues("title")}</em>
</span>
) : (
<span>Create New Page</span>
)}
</p>
<PageStatusLabel status={getValues("status")} />
<div className="flex flex-row max-w-3xl">
<p className="text-3xl font-semibold">
{mode === "edit" ? (
<span>
Editing Page: <em>{getValues("title")}</em>
</span>
) : (
<span>Create New Page</span>
)}
</p>
</div>
</div>
<EditorOptions editMode={mode === "edit"} />
</div>
Expand All @@ -217,10 +229,13 @@ const KBPageEditMode = ({
)}
</p>
{mode === "edit" && (
<PageLastEditor
lastEditedBy={getValues("lastEditedBy")}
updatedAt={getValues("updatedAt")}
/>
<div className="flex flex-row">
<PageLastEditor
lastEditedBy={getValues("lastEditedBy")}
updatedAt={getValues("updatedAt")}
/>
<PageStatusLabel status={getValues("status")} className="!mt-0.5" />
</div>
)}
<div className="flex flex-col my-8">
<div className="mb-4">
Expand Down Expand Up @@ -255,23 +270,29 @@ const KBPageEditMode = ({
placeholder="URL slug of the page. Leave blank to auto-generate."
fluid
/>
<p className="text-xs text-gray-500 italic">
<p className="text-xs text-gray-500 italic ml-1">
If you leave this blank, the URL slug will be auto-generated based
on the page title. If you provide a slug, it must be unique and
will be parsed and safely encoded by the system. Use caution when
on the page title. If you provide a slug, it must be unique and will
be parsed and safely encoded by the system. Use caution when
changing an existing slug as it may break existing links.
</p>
</div>

<div className="mt-8">
<p className="form-field-label mb-1">Content</p>
<KBQuillEditor
data={watch("body")}
pageUUID={watch("uuid")}
onDataChange={(newData: string) => {
setValue("body", newData);
}}
/>
{watch("uuid") ? (
<KBCKEditor
data={watch("body")}
pageUUID={watch("uuid")}
onDataChange={(newData: string) => {
setValue("body", newData);
}}
/>
) : (
<p className="text-sm text-gray-500">
Save this page first to start editing the content.
</p>
)}
</div>
</div>
<div className="flex flex-row justify-end mt-6">
Expand All @@ -283,7 +304,7 @@ const KBPageEditMode = ({
title={getValues("title")}
content={getValues("body")}
/>
{getValues('uuid') && (
{getValues("uuid") && (
<ConfirmDeletePageModal
open={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
Expand Down
32 changes: 18 additions & 14 deletions client/src/components/kb/KBPageViewMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import KBFooter from "./KBFooter";
import PageStatusLabel from "./PageStatusLabel";
import { checkIsUUID } from "../../utils/kbHelpers";

const KBPageViewMode = ({ slug, canEdit }: { slug?: string | null; canEdit?: boolean }) => {
const KBPageViewMode = ({
slug,
canEdit,
}: {
slug?: string | null;
canEdit?: boolean;
}) => {
const { handleGlobalError } = useGlobalError();
const [loading, setLoading] = useState(false);
const [page, setPage] = useState<KBPage | null>(null);
Expand All @@ -25,7 +31,9 @@ const KBPageViewMode = ({ slug, canEdit }: { slug?: string | null; canEdit?: boo
const isUUID = checkIsUUID(slug);

setLoading(true);
const res = await axios.get(`/kb/page/${isUUID ? `${slug}` : `slug/${slug}`}`);
const res = await axios.get(
`/kb/page/${isUUID ? `${slug}` : `slug/${slug}`}`
);
if (res.data.err) {
throw new Error(res.data.errMsg);
}
Expand All @@ -44,23 +52,19 @@ const KBPageViewMode = ({ slug, canEdit }: { slug?: string | null; canEdit?: boo
<div aria-busy={loading}>
<div className="flex flex-row items-center">
<p className="text-4xl font-semibold">{page?.title}</p>
{
canEdit && (
<PageStatusLabel status={page?.status} />
)
}
</div>
<p>
<p className="max-w-6xl">
<span className="font-medium">Description</span>:{" "}
<span className="italic">{page?.description}</span>
</p>
<div className="border-b">
<PageLastEditor
lastEditedBy={page?.lastEditedBy}
updatedAt={page?.updatedAt}
/>
<div className="flex flex-row border-b pb-2">
<PageLastEditor
lastEditedBy={page?.lastEditedBy}
updatedAt={page?.updatedAt}
/>
{canEdit && <PageStatusLabel status={page?.status} className="!mt-0.5"/>}
</div>

<div className="mt-6 mb-9">
<KBRenderer content={page?.body} />
</div>
Expand Down
Loading

0 comments on commit 5b3bc6a

Please sign in to comment.