feat: Added "Upload file" option to the super note Insert menu and block picker menu (#2869)
This commit is contained in:
@@ -41,7 +41,7 @@ export default function useModal(): [
|
||||
title: string,
|
||||
// eslint-disable-next-line no-shadow
|
||||
getContent: (onClose: () => void) => JSX.Element,
|
||||
closeOnClickOutside = false,
|
||||
closeOnClickOutside = true,
|
||||
) => {
|
||||
setModalContent({
|
||||
closeOnClickOutside,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -7,10 +7,11 @@
|
||||
*/
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import './Modal.css'
|
||||
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
|
||||
function PortalImpl({
|
||||
onClose,
|
||||
@@ -33,42 +34,51 @@ function PortalImpl({
|
||||
|
||||
useEffect(() => {
|
||||
let modalOverlayElement: HTMLElement | null = null
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.keyCode === 27) {
|
||||
|
||||
const keydownHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === KeyboardKey.Escape) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const clickOutsideHandler = (event: MouseEvent) => {
|
||||
const target = event.target
|
||||
if (modalRef.current !== null && !modalRef.current.contains(target as Node) && closeOnClickOutside) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
if (modalRef.current !== null) {
|
||||
modalOverlayElement = modalRef.current?.parentElement
|
||||
modalOverlayElement = modalRef.current.parentElement
|
||||
if (modalOverlayElement !== null) {
|
||||
modalOverlayElement?.addEventListener('click', clickOutsideHandler)
|
||||
modalOverlayElement.addEventListener('click', clickOutsideHandler)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handler)
|
||||
window.addEventListener('keydown', keydownHandler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handler)
|
||||
window.removeEventListener('keydown', keydownHandler)
|
||||
if (modalOverlayElement !== null) {
|
||||
modalOverlayElement?.removeEventListener('click', clickOutsideHandler)
|
||||
modalOverlayElement.removeEventListener('click', clickOutsideHandler)
|
||||
}
|
||||
}
|
||||
}, [closeOnClickOutside, onClose])
|
||||
|
||||
return (
|
||||
<div className="Modal__overlay" role="dialog">
|
||||
<div className="Modal__modal" tabIndex={-1} ref={modalRef}>
|
||||
<h2 className="Modal__title">{title}</h2>
|
||||
<button className="Modal__closeButton" aria-label="Close modal" type="button" onClick={onClose}>
|
||||
✕
|
||||
</button>
|
||||
<div className="Modal__content">{children}</div>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[rgba(0,0,0,0.65)]" role="dialog">
|
||||
<div
|
||||
className="relative flex min-w-[min(80vw,_20rem)] flex-col rounded border border-border bg-default"
|
||||
tabIndex={-1}
|
||||
ref={modalRef}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
@@ -78,7 +88,7 @@ export default function Modal({
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
closeOnClickOutside = false,
|
||||
closeOnClickOutside = true,
|
||||
}: {
|
||||
children: ReactNode
|
||||
closeOnClickOutside?: boolean
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -16,15 +16,16 @@ import {
|
||||
URL_MATCHER,
|
||||
} from '@lexical/react/LexicalAutoEmbedPlugin'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import * as ReactDOM from 'react-dom'
|
||||
|
||||
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_YOUTUBE_COMMAND } from '../YouTubePlugin'
|
||||
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 {
|
||||
// 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 (
|
||||
<div className="w-[600px] max-w-[90vw]">
|
||||
<div className="Input__wrapper">
|
||||
<input
|
||||
type="text"
|
||||
className="Input__input"
|
||||
placeholder={embedConfig.exampleUrl}
|
||||
<>
|
||||
<label className="flex flex-col gap-1.5">
|
||||
URL:
|
||||
<DecoratedInput
|
||||
value={text}
|
||||
data-test-id={`${embedConfig.type}-embed-modal-url`}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target
|
||||
setText(value)
|
||||
validateText(value)
|
||||
onChange={(text) => {
|
||||
setText(text)
|
||||
validateText(text)
|
||||
}}
|
||||
ref={focusOnMount}
|
||||
/>
|
||||
</div>
|
||||
<DialogActions>
|
||||
<Button disabled={!embedResult} onClick={onClick} data-test-id={`${embedConfig.type}-embed-modal-submit-btn`}>
|
||||
</label>
|
||||
<div className="mt-2.5 flex justify-end">
|
||||
<Button disabled={!embedResult} onClick={onClick} small={isMobileScreen()}>
|
||||
Embed
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import { GetQuoteBlockOption } from '../Blocks/Quote'
|
||||
import { GetDividerBlockOption } from '../Blocks/Divider'
|
||||
import { GetCollapsibleBlockOption } from '../Blocks/Collapsible'
|
||||
import { GetEmbedsBlockOptions } from '../Blocks/Embeds'
|
||||
import { GetUploadFileOption } from '../Blocks/File'
|
||||
|
||||
export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex?: string }): JSX.Element {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
@@ -57,6 +58,7 @@ export default function BlockPickerMenuPlugin({ popoverZIndex }: { popoverZIndex
|
||||
GetRemoteImageBlockOption(() => {
|
||||
showModal('Insert image from URL', (onClose) => <InsertRemoteImageDialog onClose={onClose} />)
|
||||
}),
|
||||
GetUploadFileOption(editor),
|
||||
GetNumberedListBlockOption(editor),
|
||||
GetBulletedListBlockOption(editor),
|
||||
GetChecklistBlockOption(editor),
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { INSERT_FILE_COMMAND } from '../Commands'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FileNode } from './Nodes/FileNode'
|
||||
import {
|
||||
$createParagraphNode,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
PASTE_COMMAND,
|
||||
$isRootOrShadowRoot,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { $createFileNode } from './Nodes/FileUtils'
|
||||
import { mergeRegister, $wrapNodeInElement } from '@lexical/utils'
|
||||
@@ -18,6 +19,70 @@ import { FilesControllerEvent } from '@/Controllers/FilesController'
|
||||
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
|
||||
import { useApplication } from '@/Components/ApplicationProvider'
|
||||
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 {
|
||||
const application = useApplication()
|
||||
@@ -25,6 +90,8 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
||||
const filesController = useFilesController()
|
||||
const linkingController = useLinkingController()
|
||||
|
||||
const [showFileUploadModal, setShowFileUploadModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([FileNode])) {
|
||||
throw new Error('FilePlugin: FileNode not registered on editor')
|
||||
@@ -63,6 +130,14 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
OPEN_FILE_UPLOAD_MODAL_COMMAND,
|
||||
() => {
|
||||
setShowFileUploadModal(true)
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_NORMAL,
|
||||
),
|
||||
editor.registerCommand(
|
||||
PASTE_COMMAND,
|
||||
(payload) => {
|
||||
@@ -103,5 +178,13 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
|
||||
return disposer
|
||||
}, [filesController, editor])
|
||||
|
||||
if (showFileUploadModal) {
|
||||
return (
|
||||
<Modal onClose={() => setShowFileUploadModal(false)} title="Upload File">
|
||||
<UploadFileDialog currentNote={currentNote} onClose={() => setShowFileUploadModal(false)} />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $createParagraphNode, $insertNodes, $isRootOrShadowRoot, COMMAND_PRIORITY_NORMAL } from 'lexical'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Button from '../../Lexical/UI/Button'
|
||||
import { DialogActions } from '../../Lexical/UI/Dialog'
|
||||
import TextInput from '../../Lexical/UI/TextInput'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { INSERT_REMOTE_IMAGE_COMMAND } from '../Commands'
|
||||
import { $createRemoteImageNode, RemoteImageNode } from './RemoteImageNode'
|
||||
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 }) {
|
||||
const [url, setURL] = useState('')
|
||||
@@ -21,12 +21,23 @@ export function InsertRemoteImageDialog({ onClose }: { onClose: () => void }) {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const focusOnMount = useCallback((element: HTMLInputElement | null) => {
|
||||
if (element) {
|
||||
setTimeout(() => element.focus())
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput label="URL:" onChange={setURL} value={url} />
|
||||
<DialogActions>
|
||||
<Button onClick={onClick}>Confirm</Button>
|
||||
</DialogActions>
|
||||
<label className="flex flex-col gap-1.5">
|
||||
URL:
|
||||
<DecoratedInput value={url} onChange={setURL} ref={focusOnMount} />
|
||||
</label>
|
||||
<div className="mt-2.5 flex justify-end">
|
||||
<Button onClick={onClick} disabled={!url} small={isMobileScreen()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
import { INSERT_TABLE_COMMAND, TableNode, TableRowNode } from '@lexical/table'
|
||||
import { $createParagraphNode, LexicalEditor } from 'lexical'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Button from '../Lexical/UI/Button'
|
||||
import { DialogActions } from '../Lexical/UI/Dialog'
|
||||
import TextInput from '../Lexical/UI/TextInput'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { isMobileScreen } from '../../../Utils'
|
||||
|
||||
export function InsertTableDialog({
|
||||
activeEditor,
|
||||
@@ -30,13 +30,27 @@ export function InsertTableDialog({
|
||||
onClose()
|
||||
}
|
||||
|
||||
const focusOnMount = useCallback((element: HTMLInputElement | null) => {
|
||||
if (element) {
|
||||
setTimeout(() => element.focus())
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput label="Number of rows" onChange={setRows} value={rows} />
|
||||
<TextInput label="Number of columns" onChange={setColumns} value={columns} />
|
||||
<DialogActions data-test-id="table-model-confirm-insert">
|
||||
<Button onClick={onClick}>Confirm</Button>
|
||||
</DialogActions>
|
||||
<label className="mb-2.5 flex items-center justify-between gap-3">
|
||||
Rows:
|
||||
<DecoratedInput type="number" value={rows} onChange={setRows} ref={focusOnMount} />
|
||||
</label>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ import usePreference from '@/Hooks/usePreference'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
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')
|
||||
|
||||
@@ -1048,6 +1049,11 @@ const ToolbarPlugin = () => {
|
||||
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
|
||||
name="Image from URL"
|
||||
iconName="image"
|
||||
|
||||
Reference in New Issue
Block a user