import {html, render} from 'lit-html' import {map} from 'lit-html/directives/map.js' import grapesjs from 'grapesjs/dist/grapes.min.js' import { getPageSlug } from '../../page' import { onAll } from '../utils' // constants const pluginName = 'publish' export const cmdPublish = 'publish-open-dialog' let _token = null let projectId let rootUrl // plugin code export const publishPlugin = grapesjs.plugins.add(pluginName, (editor, opts) => { opts = { appendTo: 'options', ...opts, } // Global config rootUrl = opts.rootUrl projectId = opts.projectId // Keep track of the token editor.on('login:success', async ({ getUser, getToken }) => { _token = await getToken() }) // add publication settings to the website editor.on('storage:start:store', (data) => { data.publication = editor.getModel().get('publication') }) editor.on('storage:end:load', (data) => { const model = editor.getModel() model.set('publication', data.publication || {}) }) editor.Panels.addButton(opts.appendTo, { id: 'publish-button', className: 'silex-button--size publish-button', command: cmdPublish, attributes: { title: 'Publish' }, label: 'Publish', }) editor.Commands.add(cmdPublish, { run(editor) { openDialog(editor) }, stop(editor) { closeDialog(editor) }, }) }) export function getDialogElements() { const el = document.querySelector('#publish-dialog') return el ? { dialog: el, primary: el.querySelector('#publish-button--primary'), secondary: el.querySelector('#publish-button--secondary'), } : null } function createDialogElements() { const el = document.createElement('div') el.id = 'publish-dialog' el.className = 'silex-dialog-inline silex-dialog gjs-two-color' document.body.append(el) return getDialogElements() } function move(rect) { Object.keys(rect).forEach(key => dialog.style[key] = rect[key] + 'px') } function update(editor) { render(html` ${ open ? html`
${ status === STATUS_PENDING ? html`

Publication in progress

` : ''} ${ status === STATUS_SUCCESS ? html`

Publication success

${ url ? html`Click here to view the published website` : ''} ` : ''} ${ status === STATUS_ERROR ? html`

Publication error

${ errorMessage }
` : ''} ${ state?.running ? html` ` : ''} ${ state.logs?.length ? html`
Logs
${ cleanup(state.logs) }
            
` : '' } ${ state.errors?.length ? html`
Errors
${ cleanup(state.errors) }
            
` : '' }
` : ''} `, dialog) if(open) { dialog.classList.remove('silex-dialog-hide') } else { dialog.classList.add('silex-dialog-hide') } } export const STATUS_NONE='STATUS_NONE' export const STATUS_PENDING='STATUS_PENDING' export const STATUS_ERROR='STATUS_ERROR' export const STATUS_SUCCESS='STATUS_SUCCESS' export let status = STATUS_NONE export let open = false let errorMessage = '' let dialog let url // from the result of the first fetch when publishing let state = { queued: false, error: false, running: false, logs: [], errors: [], } function cleanup(arr: string[][]): string { return arr[arr.length-1] ?.map(str => str.replace(/\[.*\]/g, '').trim()) ?.filter(str => !!str) ?.join('\n') } function displayError(editor, message) { console.error(message) errorMessage = message status = STATUS_ERROR update(editor) } export async function closeDialog(editor) { open = false update(editor) } export async function toggleDialog(editor) { if(open) closeDialog(editor) else openDialog(editor) } export async function openDialog(editor) { open = true // Position const buttonEl = editor.Panels.getPanel('options').view.el .querySelector('.publish-button') const rect = buttonEl.getBoundingClientRect() const width = 450 const padding = 10 * 2 const minHeight = 50 if(!dialog) dialog = createDialogElements().dialog move({ left: rect.right - width - padding, top: rect.bottom + 10, width, minHeight, }) // Publication if(status === STATUS_NONE) { startPublication(editor) } else { update(editor) } } export async function startPublication(editor) { if(status === STATUS_PENDING) throw new Error('Publication is already in progress') status = STATUS_PENDING update(editor) editor.trigger('publish:before') const projectData = editor.getProjectData() const siteSettings = editor.getModel().get('settings') const publicationSettings = editor.getModel().get('publication') // Update assets URL to display outside the editor const assetsFolderUrl = publicationSettings?.assets?.url if(assetsFolderUrl) { const publishedUrl = path => `${assetsFolderUrl}/${path.split('/').pop()}` // New URLs for assets, according to site config onAll(editor, c => { // Attributes if(c.get('type') === 'image') { const path = c.get('src') c.set('tmp-src', path) c.set('src', publishedUrl(path)) } //// Inline styles //// This is handled by the editor.Css.getAll loop //const bgUrl = c.getStyle()['background-image']?.match(/url\('(.*)'\)/)?.pop() //if(bgUrl) { // c.set('tmp-bg-url', bgUrl) // c.setStyle({ // ...c.getStyle(), // 'background-image': `url('${publishedUrl(bgUrl)}')`, // }) //} }) editor.Css.getAll() .forEach(c => { const bgUrl = c.getStyle()['background-image']?.match(/url\('(.*)'\)/)?.pop() if(bgUrl) { c.setStyle({ ...c.getStyle(), 'background-image': `url('${publishedUrl(bgUrl)}')`, }) c.set('tmp-bg-url-css', bgUrl) } }) } // Build the files structure const files = await getFiles(editor, {siteSettings, publicationSettings}) // Create the data to send to the server const data = { ...projectData, settings: siteSettings, publication: publicationSettings, projectId, files, } // Reset asset URLs if(assetsFolderUrl) { onAll(editor, c => { if(c.get('type') === 'image' && c.has('tmp-src')) { c.set('src', c.get('tmp-src')) c.set('tmp-src') } //// This is handled by the editor.Css.getAll loop //if(c.getStyle()['background-image'] && c.has('tmp-bg-url')) { // c.setStyle({ // ...c.getStyle(), // 'background-image': `url('${c.get('tmp-bg-url')}')`, // }) // c.set('tmp-bg-url') //} }) editor.Css.getAll() .forEach(c => { if(c.has('tmp-bg-url-css')) { c.setStyle({ ...c.getStyle(), 'background-image': `url('${c.get('tmp-bg-url-css')}')`, }) c.set('tmp-bg-url-css') } }) } editor.trigger('publish:start', data) let res let json try { res = await fetch(`${ rootUrl }/publish`, { method: 'POST', body: JSON.stringify({ data, token: _token, }), headers: { 'Content-Type': 'application/json' }, }) } catch(e) { displayError(editor, `An error occured, your site is not published. ${e.message}`) editor.trigger('publish:stop', {success: false, message: e.message}) return } try { json = await res.json() } catch(e) { displayError(editor, `Could not parse the server response, your site may be published. ${e.message}`) editor.trigger('publish:stop', {success: false, message: e.message}) return } if(!res.ok) { displayError(editor, `An network error occured, your site is not published. ${json.message}`) editor.trigger('publish:stop', {success: false, message: json.message}) return } url = json.url if(json.statusUrl) { trackProgress(editor, json.statusUrl) } else { status = STATUS_SUCCESS update(editor) editor.trigger('publish:stop', {success: true}) } } async function getFiles(editor, {siteSettings, publicationSettings, }) { return editor.Pages.getAll().map(page => { const pageSettings = page.get('settings') const pageName = publicationSettings?.autoHomePage !== false && page.get('type') === 'main' ? 'index' : (page.get('name') || page.get('type')) function getSetting(name) { return (pageSettings || {})[name] || (siteSettings || [])[name] || '' } const component = page.getMainComponent() const slug = getPageSlug(pageName) return { html: ` ${ siteSettings?.head || '' } ${ pageSettings?.head || '' } ${ getSetting('title') } ${ ['description', 'og:title', 'og:description', 'og:image'] .map(prop => ``) .join('\n') } ${ editor.getHtml({ component }) } `, css: editor.getCss({ component }), cssPath: `${ publicationSettings?.css?.path || '' }/${slug}${publicationSettings?.css?.ext || '.css'}`, htmlPath: `${ publicationSettings?.html?.path || '' }/${slug}${publicationSettings?.html?.ext || '.html'}`, } }) } export async function trackProgress(editor, statusUrl) { let res let json try { res = await fetch(statusUrl) } catch(e) { displayError(editor, `An error occured, your site is not published. ${e.message}`) editor.trigger('publish:stop', {success: false, message: e.message}) return } try { state = await res.json() } catch(e) { displayError(editor, `Could not parse the server response, your site may be published. ${e.message}`) editor.trigger('publish:stop', {success: false, message: e.message}) return } if(!res.ok) { displayError(editor, `An network error occured, your site is not published. ${res.statusText}`) editor.trigger('publish:stop', {success: false, message: `An network error occured, your site is not published. ${res.statusText}`}) return } if(state.running) { setTimeout(() => trackProgress(editor, statusUrl), 2000) } else { status = state.error ? STATUS_ERROR : STATUS_SUCCESS editor.trigger('publish:stop', {success: state.error}) } update(editor) }