Skip to content

Commit

Permalink
allow users to create inline links to resources
Browse files Browse the repository at this point in the history
This introduces a new dependency on our custom resource link plugin,
which is based on the code for CKEditor's standard link plugin. This
allows resource links to be inlined in the text rather than being block
nodes, by storing the UUID for the link as an attribute on a range of
text.

Resource links are translated to the `resource_link` shortcode, like
this:

```
{{< resource_link uuidforaresource "text inside the link" >}}
```

This change also required some changes to how we open the resource
picker and some related things. Now instead of having one 'Add resource'
button we now have 'Embed resource' and 'Link resource' buttons.
Together these make it a little less ambiguous for the user (at least,
that is the hope!)

closes #607
closes #612
part of #536
  • Loading branch information
alicewriteswrongs committed Sep 29, 2021
1 parent c3a05ae commit b786ed0
Show file tree
Hide file tree
Showing 21 changed files with 334 additions and 529 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = {
transformIgnorePatterns: [
"/node_modules/(?!(" +
"@ckeditor/*" +
"|@mitodl/ckeditor5-resource-link/*" +
"|ckeditor5/*" +
"|lodash-es" +
")/)"
Expand Down
43 changes: 22 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,31 @@
"dependencies": {
"@babel/core": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"@ckeditor/ckeditor5-adapter-ckfinder": "^29.1.0",
"@ckeditor/ckeditor5-autoformat": "^29.1.0",
"@ckeditor/ckeditor5-basic-styles": "^29.1.0",
"@ckeditor/ckeditor5-block-quote": "^29.1.0",
"@ckeditor/ckeditor5-core": "^29.1.0",
"@ckeditor/ckeditor5-dev-utils": "^25.4.2",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^25.4.2",
"@ckeditor/ckeditor5-easy-image": "^29.1.0",
"@ckeditor/ckeditor5-editor-classic": "^29.1.0",
"@ckeditor/ckeditor5-engine": "^29.1.0",
"@ckeditor/ckeditor5-essentials": "^29.1.0",
"@ckeditor/ckeditor5-heading": "^29.1.0",
"@ckeditor/ckeditor5-image": "^29.1.0",
"@ckeditor/ckeditor5-link": "^29.1.0",
"@ckeditor/ckeditor5-list": "^29.1.0",
"@ckeditor/ckeditor5-markdown-gfm": "^29.1.0",
"@ckeditor/ckeditor5-media-embed": "^29.1.0",
"@ckeditor/ckeditor5-paragraph": "^29.1.0",
"@ckeditor/ckeditor5-adapter-ckfinder": "^29.2.0",
"@ckeditor/ckeditor5-autoformat": "^29.2.0",
"@ckeditor/ckeditor5-basic-styles": "^29.2.0",
"@ckeditor/ckeditor5-block-quote": "^29.2.0",
"@ckeditor/ckeditor5-core": "^29.2.0",
"@ckeditor/ckeditor5-dev-utils": "^25.4.3",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^25.4.3",
"@ckeditor/ckeditor5-easy-image": "^29.2.0",
"@ckeditor/ckeditor5-editor-classic": "^29.2.0",
"@ckeditor/ckeditor5-engine": "^29.2.0",
"@ckeditor/ckeditor5-essentials": "^29.2.0",
"@ckeditor/ckeditor5-heading": "^29.2.0",
"@ckeditor/ckeditor5-image": "^29.2.0",
"@ckeditor/ckeditor5-link": "^29.2.0",
"@ckeditor/ckeditor5-list": "^29.2.0",
"@ckeditor/ckeditor5-markdown-gfm": "^29.2.0",
"@ckeditor/ckeditor5-media-embed": "^29.2.0",
"@ckeditor/ckeditor5-paragraph": "^29.2.0",
"@ckeditor/ckeditor5-react": "^3.0.2",
"@ckeditor/ckeditor5-theme-lark": "^29.1.0",
"@ckeditor/ckeditor5-upload": "^29.1.0",
"@ckeditor/ckeditor5-widget": "^29.1.0",
"@ckeditor/ckeditor5-theme-lark": "^29.2.0",
"@ckeditor/ckeditor5-upload": "^29.2.0",
"@ckeditor/ckeditor5-widget": "^29.2.0",
"@dnd-kit/core": "^3.1.1",
"@dnd-kit/sortable": "^4.0.0",
"@mitodl/ckeditor5-resource-link": "29.2.0",
"@sentry/browser": "^6.10.0",
"@testing-library/react-hooks": "^7.0.1",
"@types/ckeditor__ckeditor5-core": "^11.0.3",
Expand Down
2 changes: 2 additions & 0 deletions scripts/upgrade_ckeditor.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/usr/bin/env node

const fs = require("fs")
const child_process = require("child_process")
const { Command } = require("commander")
Expand Down
74 changes: 37 additions & 37 deletions static/js/components/widgets/MarkdownEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
MinimalEditorConfig
} from "../../lib/ckeditor/CKEditor"
import {
ADD_RESOURCE,
ADD_RESOURCE_EMBED,
ADD_RESOURCE_LINK,
CKEDITOR_RESOURCE_UTILS,
RESOURCE_EMBED,
RESOURCE_LINK
Expand Down Expand Up @@ -65,29 +66,19 @@ describe("MarkdownEditor", () => {
expect(wrapper.find("ResourcePickerDialog").prop("attach")).toBe("resource")
})

//
;[
[RESOURCE_EMBED, "EmbeddedResource"],
[RESOURCE_LINK, "ResourceLink"]
].forEach(([embedType, componentDisplayName]) => {
it(`should render resources with ${embedType} using ${componentDisplayName}`, () => {
const wrapper = render()
const editor = wrapper.find("CKEditor").prop("config")
const el = document.createElement("div")
// @ts-ignore
editor[CKEDITOR_RESOURCE_UTILS].renderResource(
"resource-uuid",
el,
embedType
)
wrapper.update()
expect(
wrapper
.find(componentDisplayName)
.at(0)
.prop("uuid")
).toEqual("resource-uuid")
})
it("should render resources with using EmbeddedResource", () => {
const wrapper = render()
const editor = wrapper.find("CKEditor").prop("config")
const el = document.createElement("div")
// @ts-ignore
editor[CKEDITOR_RESOURCE_UTILS].renderResource("resource-uuid", el)
wrapper.update()
expect(
wrapper
.find("EmbeddedResource")
.at(0)
.prop("uuid")
).toEqual("resource-uuid")
})

//
Expand All @@ -100,22 +91,31 @@ describe("MarkdownEditor", () => {
const wrapper = render(hasAttach ? { attach: "resource" } : {})
const editorConfig = wrapper.find("CKEditor").prop("config")
// @ts-ignore
expect(editorConfig.toolbar.items.includes(ADD_RESOURCE)).toBe(hasAttach)
expect(editorConfig.toolbar.items.includes(ADD_RESOURCE_EMBED)).toBe(
hasAttach
)
// @ts-ignore
expect(editorConfig.toolbar.items.includes(ADD_RESOURCE_LINK)).toBe(
hasAttach
)
})
})

it("should open the resource picker", () => {
const wrapper = render({ attach: "resource" })
const editor = wrapper.find("CKEditor").prop("config")
// @ts-ignore
editor[CKEDITOR_RESOURCE_UTILS].openResourcePicker()
wrapper.update()
expect(
wrapper
.find("ResourcePickerDialog")
.at(0)
.prop("open")
).toBeTruthy()
//
;[RESOURCE_EMBED, RESOURCE_LINK].forEach(resourceNodeType => {
it(`should open the resource picker for ${resourceNodeType}`, () => {
const wrapper = render({ attach: "resource" })
const editor = wrapper.find("CKEditor").prop("config")
// @ts-ignore
editor[CKEDITOR_RESOURCE_UTILS].openResourcePicker(resourceNodeType)
wrapper.update()
expect(
wrapper
.find("ResourcePickerDialog")
.at(0)
.prop("state")
).toBe(resourceNodeType)
})
})

//
Expand Down
60 changes: 33 additions & 27 deletions static/js/components/widgets/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@ import {
} from "../../lib/ckeditor/CKEditor"
import EmbeddedResource from "./EmbeddedResource"
import {
ADD_RESOURCE,
ADD_RESOURCE_LINK,
CKEResourceNodeType,
CKEDITOR_RESOURCE_UTILS,
RenderResourceFunc,
ResourceCommandMap,
RESOURCE_EMBED,
RESOURCE_LINK
ResourceDialogState,
ADD_RESOURCE_EMBED
} from "../../lib/ckeditor/plugins/constants"
import ResourcePickerDialog from "./ResourcePickerDialog"
import ResourceLink from "./ResourceLink"

export interface Props {
value?: string
Expand All @@ -29,7 +28,7 @@ export interface Props {
attach?: string
}

type RenderQueueEntry = [string, HTMLElement, CKEResourceNodeType]
type RenderQueueEntry = [string, HTMLElement]

/**
* A component for editing Markdown using CKEditor.
Expand All @@ -43,7 +42,9 @@ export default function MarkdownEditor(props: Props): JSX.Element {
const setEditorRef = useCallback(editorInstance => {
editor.current = editorInstance
}, [])
const [resourcePickerOpen, setResourcePickerOpen] = useState(false)
const [resourcePickerState, setResourcePickerState] = useState<
ResourceDialogState
>("closed")

const addResourceEmbed = useCallback(
(uuid: string, variant: CKEResourceNodeType) => {
Expand All @@ -59,15 +60,18 @@ export default function MarkdownEditor(props: Props): JSX.Element {
const [renderQueue, setRenderQueue] = useState<RenderQueueEntry[]>([])

const renderResource: RenderResourceFunc = useCallback(
(uuid: string, el: HTMLElement, variant: CKEResourceNodeType) => {
setRenderQueue(xs => [...xs, [uuid, el, variant]])
(uuid: string, el: HTMLElement) => {
setRenderQueue(xs => [...xs, [uuid, el]])
},
[setRenderQueue]
)

const openResourcePicker = useCallback(() => {
setResourcePickerOpen(true)
}, [setResourcePickerOpen])
const openResourcePicker = useCallback(
(resourceDialogType: CKEResourceNodeType) => {
setResourcePickerState(resourceDialogType)
},
[setResourcePickerState]
)

const hasAttach = attach && attach.length > 0

Expand All @@ -80,11 +84,16 @@ export default function MarkdownEditor(props: Props): JSX.Element {
// and then use it to render resources within the editor.
return {
...FullEditorConfig,
[CKEDITOR_RESOURCE_UTILS]: { renderResource, openResourcePicker },
toolbar: {
[CKEDITOR_RESOURCE_UTILS]: {
renderResource,
openResourcePicker
},
toolbar: {
...FullEditorConfig.toolbar,
items: FullEditorConfig.toolbar.items.filter(
item => hasAttach || item !== ADD_RESOURCE
items: FullEditorConfig.toolbar.items.filter(item =>
hasAttach ?
true :
item !== ADD_RESOURCE_LINK && item !== ADD_RESOURCE_EMBED
)
}
}
Expand All @@ -111,6 +120,10 @@ export default function MarkdownEditor(props: Props): JSX.Element {
[onChange, setRenderQueue, name]
)

const closeResourcePicker = useCallback(() => {
setResourcePickerState("closed")
}, [setResourcePickerState])

return (
<>
<CKEditor
Expand All @@ -122,22 +135,15 @@ export default function MarkdownEditor(props: Props): JSX.Element {
/>
{hasAttach ? (
<ResourcePickerDialog
open={resourcePickerOpen}
setOpen={setResourcePickerOpen}
state={resourcePickerState}
closeDialog={closeResourcePicker}
insertEmbed={addResourceEmbed}
attach={attach as string}
/>
) : null}
{renderQueue.map(([uuid, el, variant], idx) => {
if (variant === RESOURCE_EMBED) {
return <EmbeddedResource key={`${uuid}_${idx}`} uuid={uuid} el={el} />
}

if (variant === RESOURCE_LINK) {
return <ResourceLink key={`${uuid}_${idx}`} uuid={uuid} el={el} />
}
return null
})}
{renderQueue.map(([uuid, el], idx) => (
<EmbeddedResource key={`${uuid}_${idx}`} uuid={uuid} el={el} />
))}
</>
)
}
72 changes: 0 additions & 72 deletions static/js/components/widgets/ResourceLink.test.tsx

This file was deleted.

Loading

0 comments on commit b786ed0

Please sign in to comment.