mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-09-01 12:26:08 +08:00
feat: allow for using image urls and editing source code
This commit is contained in:
parent
5404984850
commit
1c91855e74
4 changed files with 140 additions and 30 deletions
|
@ -170,6 +170,8 @@
|
|||
"hour_one": "{{count}} Hour",
|
||||
"hour_other": "{{count}} Hours",
|
||||
"images": "Images",
|
||||
"image_upload": "Click or drag file in to upload a new image. Note, files are uploaded at their original resolution and filesize.",
|
||||
"image_url": "Enter an external URL for use for your image.",
|
||||
"import_users": "Import Users",
|
||||
"in_timezone": "In Timezone",
|
||||
"integrations": "Integrations",
|
||||
|
|
|
@ -6,13 +6,22 @@ import { ProjectContext } from '../../contexts'
|
|||
import api from '../../api'
|
||||
import './ImageGalleryModal.css'
|
||||
import { Image } from '../../types'
|
||||
import { Tabs } from '../../ui'
|
||||
import TextInput from '../../ui/form/TextInput'
|
||||
import FormWrapper from '../../ui/form/FormWrapper'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface ImageGalleryProps extends ModalStateProps {
|
||||
onInsert?: (image: Image) => void
|
||||
export type ImageUpload = Pick<Image, 'url' | 'alt' | 'name'>
|
||||
|
||||
interface OnInsert {
|
||||
onInsert: (image: ImageUpload) => void
|
||||
}
|
||||
|
||||
export default function ImageGalleryModal({ open, onClose, onInsert }: ImageGalleryProps) {
|
||||
type ImageGalleryProps = ModalStateProps & OnInsert
|
||||
|
||||
const Gallery = ({ onInsert }: OnInsert) => {
|
||||
const [project] = useContext(ProjectContext)
|
||||
const { t } = useTranslation()
|
||||
const { reload, results } = useSearchTableState(useCallback(async params => await api.images.search(project.id, params), [project]))
|
||||
const [upload, setUpload] = useState<FileList | undefined>()
|
||||
|
||||
|
@ -23,33 +32,80 @@ export default function ImageGalleryModal({ open, onClose, onInsert }: ImageGall
|
|||
setUpload(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="image-gallery">
|
||||
<p>{t('image_upload')}</p>
|
||||
<UploadField
|
||||
value={upload}
|
||||
onChange={uploadImage}
|
||||
name="file"
|
||||
label="File"
|
||||
isUploading={upload !== undefined}
|
||||
accept={'image/*'}
|
||||
required />
|
||||
|
||||
{results && <div className="images">
|
||||
{results.results.map(image => <>
|
||||
<div className="image"
|
||||
key={`image-${image.id}`}
|
||||
onClick={() => onInsert?.(image) }>
|
||||
<img src={image.url} alt={image.alt} />
|
||||
</div>
|
||||
</>)}
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface URLParams {
|
||||
url: string
|
||||
}
|
||||
|
||||
const URL = ({ onInsert }: OnInsert) => {
|
||||
const { t } = useTranslation()
|
||||
const handleSubmit = async (params: URLParams) => {
|
||||
onInsert({
|
||||
url: params.url,
|
||||
alt: '',
|
||||
name: params.url.split('/').pop() ?? 'Image',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="image-gallery">
|
||||
<FormWrapper<URLParams>
|
||||
onSubmit={handleSubmit}
|
||||
submitLabel={t('save')}>
|
||||
{form => <>
|
||||
<p>{t('image_url')}</p>
|
||||
<TextInput.Field form={form} name="url" label="Image URL" />
|
||||
</>}
|
||||
</FormWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ImageGalleryModal({ open, onClose, onInsert }: ImageGalleryProps) {
|
||||
const tabs = [
|
||||
{
|
||||
key: 'gallery',
|
||||
label: 'Gallery',
|
||||
children: <Gallery onInsert={onInsert} />,
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
label: 'URL',
|
||||
children: <URL onInsert={onInsert} />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Images"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
size="large">
|
||||
<div className="image-gallery">
|
||||
<p>Click or drag file in to upload a new image. Note, files are uploaded at their original resolution and filesize.</p>
|
||||
<UploadField
|
||||
value={upload}
|
||||
onChange={uploadImage}
|
||||
name="file"
|
||||
label="File"
|
||||
isUploading={upload !== undefined}
|
||||
accept={'image/*'}
|
||||
required />
|
||||
|
||||
{results && <div className="images">
|
||||
{results.results.map(image => <>
|
||||
<div className="image"
|
||||
key={`image-${image.id}`}
|
||||
onClick={() => onInsert?.(image) }>
|
||||
<img src={image.url} alt={image.alt} />
|
||||
</div>
|
||||
</>)}
|
||||
</div>}
|
||||
</div>
|
||||
<Tabs tabs={tabs} />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useState } from 'react'
|
||||
import { Image, Template } from '../../../types'
|
||||
import { Template } from '../../../types'
|
||||
import { editor as Editor } from 'monaco-editor'
|
||||
import Button from '../../../ui/Button'
|
||||
import ImageGalleryModal from '../ImageGalleryModal'
|
||||
import ImageGalleryModal, { ImageUpload } from '../ImageGalleryModal'
|
||||
import Preview from '../../../ui/Preview'
|
||||
import Tabs from '../../../ui/Tabs'
|
||||
import { ImageIcon } from '../../../ui/icons'
|
||||
|
@ -33,7 +33,7 @@ export default function HtmlEditor({ template, setTemplate }: { template: Templa
|
|||
setTemplate(template)
|
||||
}
|
||||
|
||||
function handleImageInsert(image: Image) {
|
||||
function handleImageInsert(image: ImageUpload) {
|
||||
setShowImages(false)
|
||||
handleEditorInsert(`<img src="${image.url}" alt="${image.alt || image.name}" />`)
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ import './VisualEditor.css'
|
|||
import grapesJS, { Editor } from 'grapesjs'
|
||||
import grapesJSMJML from 'grapesjs-mjml'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Font, Image, Resource, Template } from '../../../types'
|
||||
import ImageGalleryModal from '../ImageGalleryModal'
|
||||
import { Font, Resource, Template } from '../../../types'
|
||||
import ImageGalleryModal, { ImageUpload } from '../ImageGalleryModal'
|
||||
|
||||
interface GrapesAssetManagerProps {
|
||||
event: 'open' | 'close'
|
||||
|
@ -70,6 +70,57 @@ function GrapesReact({ id, mjml, onChange, setAssetState, fonts = [] }: GrapesRe
|
|||
updateHead(editor, fonts)
|
||||
}
|
||||
|
||||
const registerSourcePlugin = (editor: Editor) => {
|
||||
editor.Commands.add('edit-raw-source', {
|
||||
run(editor) {
|
||||
const escapeHtml = (str: string): string => {
|
||||
const div = document.createElement('div')
|
||||
div.textContent = str
|
||||
return div.innerHTML
|
||||
}
|
||||
|
||||
const modal = editor.Modal
|
||||
const html = editor.getHtml()
|
||||
const css = editor.getCss()
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.style.padding = '8px'
|
||||
container.innerHTML = `
|
||||
<div style="margin-bottom: 6px">HTML</div>
|
||||
<textarea id="raw-html" style="width:100%;height:220px">${escapeHtml(html)}</textarea>
|
||||
<div style="margin: 10px 0 6px">CSS</div>
|
||||
<textarea id="raw-css" style="width:100%;height:140px">${css}</textarea>
|
||||
<button id="apply-code" class="gjs-btn gjs-btn-prim" style="margin-top:10px">Apply</button>
|
||||
`
|
||||
modal.open({ title: 'Edit Source', content: container })
|
||||
|
||||
const htmlEl = container.querySelector<HTMLTextAreaElement>('#raw-html')
|
||||
const cssEl = container.querySelector<HTMLTextAreaElement>('#raw-css')
|
||||
const applyBtn = container.querySelector<HTMLButtonElement>('#apply-code')
|
||||
|
||||
applyBtn?.addEventListener('click', () => {
|
||||
const newHtml = htmlEl?.value ?? ''
|
||||
const newCss = cssEl?.value ?? ''
|
||||
editor.setComponents(newHtml)
|
||||
editor.setStyle(newCss)
|
||||
modal.close()
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
editor.Panels.addButton('options', {
|
||||
id: 'edit-raw-source',
|
||||
label: '<i class="fa fa-code"></i>',
|
||||
command: 'edit-raw-source',
|
||||
togglable: false,
|
||||
})
|
||||
|
||||
editor.Panels.getButton('options', 'export-template')?.set({
|
||||
className: 'fa fa-solid fa-html5',
|
||||
attributes: { title: 'View MJML & HTML' }, // Optional tooltip
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
const editor = grapesJS.init({
|
||||
|
@ -99,6 +150,7 @@ function GrapesReact({ id, mjml, onChange, setAssetState, fonts = [] }: GrapesRe
|
|||
editor.on('load', () => {
|
||||
editor.Panels.getButton('views', 'open-blocks')
|
||||
?.set('active', true)
|
||||
registerSourcePlugin(editor)
|
||||
setLoaded(true)
|
||||
})
|
||||
editor.render()
|
||||
|
@ -130,7 +182,7 @@ export default function VisualEditor({ template, setTemplate, resources }: Visua
|
|||
setTemplate({ ...template, data: { ...template.data, mjml, html } })
|
||||
}
|
||||
|
||||
function handleImageInsert(image: Image) {
|
||||
function handleImageInsert(image: ImageUpload) {
|
||||
assetManager?.select({ src: image.url })
|
||||
handleHideImages()
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue