feat: Added "Upload file" option to the super note Insert menu and block picker menu (#2869)

This commit is contained in:
Aman Harwara
2024-04-13 12:39:38 +05:30
committed by GitHub
parent 7f1baf52c1
commit 9800570fb1
16 changed files with 196 additions and 317 deletions

View File

@@ -41,7 +41,7 @@ export default function useModal(): [
title: string, title: string,
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
getContent: (onClose: () => void) => JSX.Element, getContent: (onClose: () => void) => JSX.Element,
closeOnClickOutside = false, closeOnClickOutside = true,
) => { ) => {
setModalContent({ setModalContent({
closeOnClickOutside, closeOnClickOutside,

View File

@@ -1,36 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
*/
.Button__root {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 15px;
padding-right: 15px;
border: 0px;
background-color: var(--sn-stylekit-contrast-background-color);
cursor: pointer;
font-size: 14px;
}
.Button__root:hover {
background-color: var(--sn-stylekit-info-color);
color: var(--sn-stylekit-info-contrast-color);
}
.Button__small {
padding-top: 5px;
padding-bottom: 5px;
padding-left: 10px;
padding-right: 10px;
font-size: 13px;
}
.Button__disabled {
cursor: not-allowed;
}
.Button__disabled:hover {
background-color: var(--sn-stylekit-secondary-background-color);
}

View File

@@ -1,44 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import './Button.css'
import { ReactNode } from 'react'
import joinClasses from '../Utils/join-classes'
export default function Button({
'data-test-id': dataTestId,
children,
className,
onClick,
disabled,
small,
title,
}: {
'data-test-id'?: string
children: ReactNode
className?: string
disabled?: boolean
onClick: () => void
small?: boolean
title?: string
}): JSX.Element {
return (
<button
disabled={disabled}
className={joinClasses('Button__root', disabled && 'Button__disabled', small && 'Button__small', className)}
onClick={onClick}
title={title}
aria-label={title}
{...(dataTestId && { 'data-test-id': dataTestId })}
>
{children}
</button>
)
}

View File

@@ -1,17 +0,0 @@
.DialogActions {
display: flex;
flex-direction: row;
justify-content: right;
margin-top: 20px;
}
.DialogButtonsList {
display: flex;
flex-direction: column;
justify-content: right;
margin-top: 20px;
}
.DialogButtonsList button {
margin-bottom: 20px;
}

View File

@@ -1,28 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import './Dialog.css'
import { ReactNode } from 'react'
type Props = Readonly<{
'data-test-id'?: string
children: ReactNode
}>
export function DialogButtonsList({ children }: Props): JSX.Element {
return <div className="DialogButtonsList">{children}</div>
}
export function DialogActions({ 'data-test-id': dataTestId, children }: Props): JSX.Element {
return (
<div className="DialogActions" data-test-id={dataTestId}>
{children}
</div>
)
}

View File

@@ -1,33 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
*/
.Input__wrapper {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 10px;
}
.Input__label {
display: flex;
flex: 1;
color: #666;
margin-right: 20px;
}
.Input__input {
display: flex;
flex: 2;
border: 1px solid var(--sn-stylekit-contrast-border-color);
background-color: var(--sn-stylekit-contrast-background-color);
padding-top: 7px;
padding-bottom: 7px;
padding-left: 10px;
padding-right: 10px;
font-size: 16px;
min-width: 0;
}

View File

@@ -1,64 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
*/
.Modal__overlay {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
flex-direction: column;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
background-color: rgba(0, 0, 0, 0.7);
flex-grow: 0px;
flex-shrink: 1px;
z-index: 100;
}
.Modal__modal {
padding: 20px;
min-height: 100px;
min-width: 300px;
display: flex;
flex-grow: 0px;
background-color: var(--sn-stylekit-background-color);
flex-direction: column;
position: relative;
box-shadow: 0 0px 0 var(--sn-stylekit-shadow-color);
border-radius: 0px;
}
.Modal__title {
color: var(--sn-stylekit-foreground-color);
margin: 0px;
padding-bottom: 15px;
border-bottom: 1px solid var(--sn-stylekit-border-color);
}
.Modal__closeButton {
border: 0px;
position: absolute;
right: 20px;
top: 15px;
border-radius: 20px;
justify-content: center;
align-items: center;
display: flex;
width: 30px;
height: 30px;
text-align: center;
cursor: pointer;
background-color: var(--sn-stylekit-contrast-background-color);
}
.Modal__closeButton:hover {
background-color: var(--sn-stylekit-info-color);
color: var(--sn-stylekit-info-contrast-color);
}
.Modal__content {
padding-top: 20px;
}

View File

@@ -7,10 +7,11 @@
*/ */
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import './Modal.css'
import { ReactNode, useEffect, useRef, useState } from 'react' import { ReactNode, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import Icon from '@/Components/Icon/Icon'
import { KeyboardKey } from '@standardnotes/ui-services'
function PortalImpl({ function PortalImpl({
onClose, onClose,
@@ -33,42 +34,51 @@ function PortalImpl({
useEffect(() => { useEffect(() => {
let modalOverlayElement: HTMLElement | null = null let modalOverlayElement: HTMLElement | null = null
const handler = (event: KeyboardEvent) => {
if (event.keyCode === 27) { const keydownHandler = (event: KeyboardEvent) => {
if (event.key === KeyboardKey.Escape) {
onClose() onClose()
} }
} }
const clickOutsideHandler = (event: MouseEvent) => { const clickOutsideHandler = (event: MouseEvent) => {
const target = event.target const target = event.target
if (modalRef.current !== null && !modalRef.current.contains(target as Node) && closeOnClickOutside) { if (modalRef.current !== null && !modalRef.current.contains(target as Node) && closeOnClickOutside) {
onClose() onClose()
} }
} }
if (modalRef.current !== null) { if (modalRef.current !== null) {
modalOverlayElement = modalRef.current?.parentElement modalOverlayElement = modalRef.current.parentElement
if (modalOverlayElement !== null) { if (modalOverlayElement !== null) {
modalOverlayElement?.addEventListener('click', clickOutsideHandler) modalOverlayElement.addEventListener('click', clickOutsideHandler)
} }
} }
window.addEventListener('keydown', handler) window.addEventListener('keydown', keydownHandler)
return () => { return () => {
window.removeEventListener('keydown', handler) window.removeEventListener('keydown', keydownHandler)
if (modalOverlayElement !== null) { if (modalOverlayElement !== null) {
modalOverlayElement?.removeEventListener('click', clickOutsideHandler) modalOverlayElement.removeEventListener('click', clickOutsideHandler)
} }
} }
}, [closeOnClickOutside, onClose]) }, [closeOnClickOutside, onClose])
return ( return (
<div className="Modal__overlay" role="dialog"> <div className="absolute inset-0 flex flex-col items-center justify-center bg-[rgba(0,0,0,0.65)]" role="dialog">
<div className="Modal__modal" tabIndex={-1} ref={modalRef}> <div
<h2 className="Modal__title">{title}</h2> className="relative flex min-w-[min(80vw,_20rem)] flex-col rounded border border-border bg-default"
<button className="Modal__closeButton" aria-label="Close modal" type="button" onClick={onClose}> tabIndex={-1}
ref={modalRef}
</button> >
<div className="Modal__content">{children}</div> <div className="flex items-center justify-between border-b border-border px-3.5 py-2">
<div className="text-sm font-semibold">{title}</div>
<button tabIndex={0} className="ml-2 rounded p-1 font-bold hover:bg-contrast" onClick={onClose}>
<Icon type="close" />
</button>
</div>
<div className="px-3.5 py-3">{children}</div>
</div> </div>
</div> </div>
) )
@@ -78,7 +88,7 @@ export default function Modal({
onClose, onClose,
children, children,
title, title,
closeOnClickOutside = false, closeOnClickOutside = true,
}: { }: {
children: ReactNode children: ReactNode
closeOnClickOutside?: boolean closeOnClickOutside?: boolean

View File

@@ -1,41 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import './Input.css'
type Props = Readonly<{
'data-test-id'?: string
label: string
onChange: (val: string) => void
placeholder?: string
value: string
}>
export default function TextInput({
label,
value,
onChange,
placeholder = '',
'data-test-id': dataTestId,
}: Props): JSX.Element {
return (
<div className="Input__wrapper">
<label className="Input__label">{label}</label>
<input
type="text"
className="Input__input"
placeholder={placeholder}
value={value}
onChange={(e) => {
onChange(e.target.value)
}}
data-test-id={dataTestId}
/>
</div>
)
}

View File

@@ -16,15 +16,16 @@ import {
URL_MATCHER, URL_MATCHER,
} from '@lexical/react/LexicalAutoEmbedPlugin' } from '@lexical/react/LexicalAutoEmbedPlugin'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import * as ReactDOM from 'react-dom' import * as ReactDOM from 'react-dom'
import useModal from '../../Lexical/Hooks/useModal' import useModal from '../../Lexical/Hooks/useModal'
import Button from '../../Lexical/UI/Button'
import { DialogActions } from '../../Lexical/UI/Dialog'
import { INSERT_TWEET_COMMAND } from '../TwitterPlugin' import { INSERT_TWEET_COMMAND } from '../TwitterPlugin'
import { INSERT_YOUTUBE_COMMAND } from '../YouTubePlugin' import { INSERT_YOUTUBE_COMMAND } from '../YouTubePlugin'
import { classNames } from '@standardnotes/snjs' import { classNames } from '@standardnotes/snjs'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import Button from '@/Components/Button/Button'
import { isMobileScreen } from '../../../../Utils'
interface PlaygroundEmbedConfig extends EmbedConfig { interface PlaygroundEmbedConfig extends EmbedConfig {
// Human readable name of the embeded content e.g. Tweet or Google Map. // Human readable name of the embeded content e.g. Tweet or Google Map.
@@ -219,28 +220,31 @@ export function AutoEmbedDialog({
} }
} }
const focusOnMount = useCallback((element: HTMLInputElement | null) => {
if (element) {
setTimeout(() => element.focus())
}
}, [])
return ( return (
<div className="w-[600px] max-w-[90vw]"> <>
<div className="Input__wrapper"> <label className="flex flex-col gap-1.5">
<input URL:
type="text" <DecoratedInput
className="Input__input"
placeholder={embedConfig.exampleUrl}
value={text} value={text}
data-test-id={`${embedConfig.type}-embed-modal-url`} onChange={(text) => {
onChange={(e) => { setText(text)
const { value } = e.target validateText(text)
setText(value)
validateText(value)
}} }}
ref={focusOnMount}
/> />
</div> </label>
<DialogActions> <div className="mt-2.5 flex justify-end">
<Button disabled={!embedResult} onClick={onClick} data-test-id={`${embedConfig.type}-embed-modal-submit-btn`}> <Button disabled={!embedResult} onClick={onClick} small={isMobileScreen()}>
Embed Embed
</Button> </Button>
</DialogActions> </div>
</div> </>
) )
} }

View File

@@ -29,6 +29,7 @@ import { GetQuoteBlockOption } from '../Blocks/Quote'
import { GetDividerBlockOption } from '../Blocks/Divider' import { GetDividerBlockOption } from '../Blocks/Divider'
import { GetCollapsibleBlockOption } from '../Blocks/Collapsible' import { GetCollapsibleBlockOption } from '../Blocks/Collapsible'
import { GetEmbedsBlockOptions } from '../Blocks/Embeds' import { GetEmbedsBlockOptions } from '../Blocks/Embeds'
import { GetUploadFileOption } from '../Blocks/File'
export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex?: string }): JSX.Element { export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex?: string }): JSX.Element {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
@@ -57,6 +58,7 @@ export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex
GetRemoteImageBlockOption(() => { GetRemoteImageBlockOption(() => {
showModal('Insert image from URL', (onClose) => <InsertRemoteImageDialog onClose={onClose} />) showModal('Insert image from URL', (onClose) => <InsertRemoteImageDialog onClose={onClose} />)
}), }),
GetUploadFileOption(editor),
GetNumberedListBlockOption(editor), GetNumberedListBlockOption(editor),
GetBulletedListBlockOption(editor), GetBulletedListBlockOption(editor),
GetChecklistBlockOption(editor), GetChecklistBlockOption(editor),

View File

@@ -0,0 +1,12 @@
import { LexicalIconName } from '@/Components/Icon/LexicalIcons'
import { BlockPickerOption } from '../BlockPickerPlugin/BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { OPEN_FILE_UPLOAD_MODAL_COMMAND } from '../EncryptedFilePlugin/FilePlugin'
export function GetUploadFileOption(editor: LexicalEditor) {
return new BlockPickerOption('Upload file', {
iconName: 'file' as LexicalIconName,
keywords: ['image', 'upload', 'file'],
onSelect: () => editor.dispatchCommand(OPEN_FILE_UPLOAD_MODAL_COMMAND, undefined),
})
}

View File

@@ -1,7 +1,7 @@
import { INSERT_FILE_COMMAND } from '../Commands' import { INSERT_FILE_COMMAND } from '../Commands'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import { FileNode } from './Nodes/FileNode' import { FileNode } from './Nodes/FileNode'
import { import {
$createParagraphNode, $createParagraphNode,
@@ -10,6 +10,7 @@ import {
COMMAND_PRIORITY_NORMAL, COMMAND_PRIORITY_NORMAL,
PASTE_COMMAND, PASTE_COMMAND,
$isRootOrShadowRoot, $isRootOrShadowRoot,
createCommand,
} from 'lexical' } from 'lexical'
import { $createFileNode } from './Nodes/FileUtils' import { $createFileNode } from './Nodes/FileUtils'
import { mergeRegister, $wrapNodeInElement } from '@lexical/utils' import { mergeRegister, $wrapNodeInElement } from '@lexical/utils'
@@ -18,6 +19,70 @@ import { FilesControllerEvent } from '@/Controllers/FilesController'
import { useLinkingController } from '@/Controllers/LinkingControllerProvider' import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
import { useApplication } from '@/Components/ApplicationProvider' import { useApplication } from '@/Components/ApplicationProvider'
import { SNNote } from '@standardnotes/snjs' import { SNNote } from '@standardnotes/snjs'
import Spinner from '../../../Spinner/Spinner'
import Modal from '../../Lexical/UI/Modal'
import Button from '@/Components/Button/Button'
import { isMobileScreen } from '../../../../Utils'
export const OPEN_FILE_UPLOAD_MODAL_COMMAND = createCommand('OPEN_FILE_UPLOAD_MODAL_COMMAND')
function UploadFileDialog({ currentNote, onClose }: { currentNote: SNNote; onClose: () => void }) {
const application = useApplication()
const [editor] = useLexicalComposerContext()
const filesController = useFilesController()
const linkingController = useLinkingController()
const [file, setFile] = useState<File>()
const [isUploadingFile, setIsUploadingFile] = useState(false)
const onClick = () => {
if (!file) {
return
}
setIsUploadingFile(true)
filesController
.uploadNewFile(file)
.then((uploadedFile) => {
if (!uploadedFile) {
return
}
editor.dispatchCommand(INSERT_FILE_COMMAND, uploadedFile.uuid)
void linkingController.linkItemToSelectedItem(uploadedFile)
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
mutator.protected = currentNote.protected
})
})
.catch(console.error)
.finally(() => {
setIsUploadingFile(false)
onClose()
})
}
return (
<>
<input
type="file"
onChange={(event) => {
const filesList = event.target.files
if (filesList && filesList.length === 1) {
setFile(filesList[0])
}
}}
/>
<div className="mt-1.5 flex justify-end">
{isUploadingFile ? (
<Spinner className="h-4 w-4" />
) : (
<Button onClick={onClick} disabled={!file} small={isMobileScreen()}>
Upload
</Button>
)}
</div>
</>
)
}
export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JSX.Element | null { export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JSX.Element | null {
const application = useApplication() const application = useApplication()
@@ -25,6 +90,8 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
const filesController = useFilesController() const filesController = useFilesController()
const linkingController = useLinkingController() const linkingController = useLinkingController()
const [showFileUploadModal, setShowFileUploadModal] = useState(false)
useEffect(() => { useEffect(() => {
if (!editor.hasNodes([FileNode])) { if (!editor.hasNodes([FileNode])) {
throw new Error('FilePlugin: FileNode not registered on editor') throw new Error('FilePlugin: FileNode not registered on editor')
@@ -63,6 +130,14 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR,
), ),
editor.registerCommand(
OPEN_FILE_UPLOAD_MODAL_COMMAND,
() => {
setShowFileUploadModal(true)
return true
},
COMMAND_PRIORITY_NORMAL,
),
editor.registerCommand( editor.registerCommand(
PASTE_COMMAND, PASTE_COMMAND,
(payload) => { (payload) => {
@@ -103,5 +178,13 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
return disposer return disposer
}, [filesController, editor]) }, [filesController, editor])
if (showFileUploadModal) {
return (
<Modal onClose={() => setShowFileUploadModal(false)} title="Upload File">
<UploadFileDialog currentNote={currentNote} onClose={() => setShowFileUploadModal(false)} />
</Modal>
)
}
return null return null
} }

View File

@@ -1,12 +1,12 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $createParagraphNode, $insertNodes, $isRootOrShadowRoot, COMMAND_PRIORITY_NORMAL } from 'lexical' import { $createParagraphNode, $insertNodes, $isRootOrShadowRoot, COMMAND_PRIORITY_NORMAL } from 'lexical'
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import Button from '../../Lexical/UI/Button'
import { DialogActions } from '../../Lexical/UI/Dialog'
import TextInput from '../../Lexical/UI/TextInput'
import { INSERT_REMOTE_IMAGE_COMMAND } from '../Commands' import { INSERT_REMOTE_IMAGE_COMMAND } from '../Commands'
import { $createRemoteImageNode, RemoteImageNode } from './RemoteImageNode' import { $createRemoteImageNode, RemoteImageNode } from './RemoteImageNode'
import { mergeRegister, $wrapNodeInElement } from '@lexical/utils' import { mergeRegister, $wrapNodeInElement } from '@lexical/utils'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import Button from '@/Components/Button/Button'
import { isMobileScreen } from '../../../../Utils'
export function InsertRemoteImageDialog({ onClose }: { onClose: () => void }) { export function InsertRemoteImageDialog({ onClose }: { onClose: () => void }) {
const [url, setURL] = useState('') const [url, setURL] = useState('')
@@ -21,12 +21,23 @@ export function InsertRemoteImageDialog({ onClose }: { onClose: () => void }) {
onClose() onClose()
} }
const focusOnMount = useCallback((element: HTMLInputElement | null) => {
if (element) {
setTimeout(() => element.focus())
}
}, [])
return ( return (
<> <>
<TextInput label="URL:" onChange={setURL} value={url} /> <label className="flex flex-col gap-1.5">
<DialogActions> URL:
<Button onClick={onClick}>Confirm</Button> <DecoratedInput value={url} onChange={setURL} ref={focusOnMount} />
</DialogActions> </label>
<div className="mt-2.5 flex justify-end">
<Button onClick={onClick} disabled={!url} small={isMobileScreen()}>
Confirm
</Button>
</div>
</> </>
) )
} }

View File

@@ -8,12 +8,12 @@
import { INSERT_TABLE_COMMAND, TableNode, TableRowNode } from '@lexical/table' import { INSERT_TABLE_COMMAND, TableNode, TableRowNode } from '@lexical/table'
import { $createParagraphNode, LexicalEditor } from 'lexical' import { $createParagraphNode, LexicalEditor } from 'lexical'
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import Button from '../Lexical/UI/Button'
import { DialogActions } from '../Lexical/UI/Dialog'
import TextInput from '../Lexical/UI/TextInput'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import Button from '@/Components/Button/Button'
import { isMobileScreen } from '../../../Utils'
export function InsertTableDialog({ export function InsertTableDialog({
activeEditor, activeEditor,
@@ -30,13 +30,27 @@ export function InsertTableDialog({
onClose() onClose()
} }
const focusOnMount = useCallback((element: HTMLInputElement | null) => {
if (element) {
setTimeout(() => element.focus())
}
}, [])
return ( return (
<> <>
<TextInput label="Number of rows" onChange={setRows} value={rows} /> <label className="mb-2.5 flex items-center justify-between gap-3">
<TextInput label="Number of columns" onChange={setColumns} value={columns} /> Rows:
<DialogActions data-test-id="table-model-confirm-insert"> <DecoratedInput type="number" value={rows} onChange={setRows} ref={focusOnMount} />
<Button onClick={onClick}>Confirm</Button> </label>
</DialogActions> <label className="mb-2.5 flex items-center justify-between gap-3">
Columns:
<DecoratedInput type="number" value={columns} onChange={setColumns} />
</label>
<div className="flex justify-end">
<Button onClick={onClick} small={isMobileScreen()}>
Confirm
</Button>
</div>
</> </>
) )
} }

View File

@@ -76,6 +76,7 @@ import usePreference from '@/Hooks/usePreference'
import { ElementIds } from '@/Constants/ElementIDs' import { ElementIds } from '@/Constants/ElementIDs'
import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode' import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
import LinkViewer from './LinkViewer' import LinkViewer from './LinkViewer'
import { OPEN_FILE_UPLOAD_MODAL_COMMAND } from '../EncryptedFilePlugin/FilePlugin'
const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND') const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND')
@@ -1048,6 +1049,11 @@ const ToolbarPlugin = () => {
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />) showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />)
} }
/> />
<ToolbarMenuItem
name="Upload file"
iconName="file"
onClick={() => activeEditor.dispatchCommand(OPEN_FILE_UPLOAD_MODAL_COMMAND, undefined)}
/>
<ToolbarMenuItem <ToolbarMenuItem
name="Image from URL" name="Image from URL"
iconName="image" iconName="image"