@silexlabs/silex 3.0.0-alpha.39 → 3.0.0-alpha.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.silex.js +38 -8
- package/dist/client/client/config.d.ts +11 -41
- package/dist/client/client/grapesjs/LoginDialog.d.ts +13 -0
- package/dist/client/client/grapesjs/PublicationManager.d.ts +69 -0
- package/dist/client/client/grapesjs/PublicationUi.d.ts +58 -0
- package/dist/client/client/grapesjs/index.d.ts +18 -44
- package/dist/client/client/grapesjs/storage.d.ts +1 -0
- package/dist/client/client/services.d.ts +35 -0
- package/dist/client/constants.d.ts +17 -0
- package/dist/client/css/admin.css +86 -3
- package/dist/client/js/main.js +201 -90
- package/dist/client/js/main.js.LICENSE.txt +6 -0
- package/dist/client/types.d.ts +200 -29
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +38 -1
- package/dist/constants.js.map +1 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +17 -0
- package/dist/events.js.map +1 -0
- package/dist/plugins/AuthPlugin.d.ts +1 -0
- package/dist/plugins/AuthPlugin.js +38 -0
- package/dist/plugins/AuthPlugin.js.map +1 -0
- package/dist/plugins/Cache.d.ts +2 -0
- package/dist/plugins/Cache.js +32 -0
- package/dist/plugins/Cache.js.map +1 -0
- package/dist/plugins/FtpBackend.d.ts +92 -0
- package/dist/plugins/FtpBackend.js +604 -0
- package/dist/plugins/FtpBackend.js.map +1 -0
- package/dist/plugins/HooksPlugin.d.ts +7 -0
- package/dist/plugins/HooksPlugin.js +76 -0
- package/dist/plugins/HooksPlugin.js.map +1 -0
- package/dist/plugins/PublicationPlugin.d.ts +17 -0
- package/dist/plugins/PublicationPlugin.js +170 -0
- package/dist/plugins/PublicationPlugin.js.map +1 -0
- package/dist/plugins/SslPlugin.d.ts +1 -0
- package/dist/plugins/SslPlugin.js +86 -0
- package/dist/plugins/SslPlugin.js.map +1 -0
- package/dist/plugins/StaticPlugin.d.ts +1 -0
- package/dist/plugins/StaticPlugin.js +66 -0
- package/dist/plugins/StaticPlugin.js.map +1 -0
- package/dist/plugins/WebsitePlugin.d.ts +14 -0
- package/dist/plugins/WebsitePlugin.js +302 -0
- package/dist/plugins/WebsitePlugin.js.map +1 -0
- package/dist/server/backends/FsBackend.d.ts +42 -0
- package/dist/server/backends/FsBackend.js +269 -0
- package/dist/server/backends/FsBackend.js.map +1 -0
- package/dist/server/backends/index.d.ts +95 -0
- package/dist/server/backends/index.js +255 -0
- package/dist/server/backends/index.js.map +1 -0
- package/dist/server/cli.js +0 -1
- package/dist/server/config.d.ts +58 -0
- package/dist/server/config.js +128 -24
- package/dist/server/config.js.map +1 -0
- package/dist/server/express.js +31 -3
- package/dist/server/index.js +9 -20
- package/dist/server/jobs.d.ts +6 -0
- package/dist/server/jobs.js +75 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/utils/validation.d.ts +7 -0
- package/dist/server/utils/validation.js +37 -0
- package/dist/server/utils/validation.js.map +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.js +35 -28
- package/dist/types.js.map +1 -0
- package/package.json +19 -8
- package/public/silex.js +17 -0
- package/src/css/dialog.scss +9 -2
- package/src/css/grapesjs.scss +79 -0
- package/src/css/project-bar.scss +2 -2
- package/src/example-client-config.js +17 -0
- package/src/html/templates/includes.pug +0 -1
- package/src/ts/client/config.ts +7 -6
- package/src/ts/client/grapesjs/LoginDialog.ts +190 -0
- package/src/ts/client/grapesjs/PublicationManager.ts +333 -0
- package/src/ts/client/grapesjs/PublicationUi.ts +320 -0
- package/src/ts/client/grapesjs/index.ts +88 -85
- package/src/ts/client/grapesjs/storage.ts +110 -0
- package/src/ts/client/index.ts +5 -7
- package/src/ts/client/services.ts +129 -0
- package/src/ts/constants.ts +21 -0
- package/src/ts/plugins/AuthPlugin.ts +38 -0
- package/src/ts/plugins/Cache.ts +28 -0
- package/src/ts/plugins/FtpBackend.ts +689 -0
- package/src/ts/plugins/HooksPlugin.ts +79 -0
- package/src/ts/plugins/PublicationPlugin.ts +170 -0
- package/src/{plugins/SslPlugin.js → ts/plugins/SslPlugin.ts} +24 -6
- package/src/ts/plugins/StaticPlugin.ts +68 -0
- package/src/ts/plugins/WebsitePlugin.ts +318 -0
- package/src/ts/server/backends/FsBackend.ts +266 -0
- package/src/ts/server/backends/index.ts +309 -0
- package/src/ts/server/cli.ts +0 -2
- package/src/ts/server/config.ts +103 -33
- package/src/ts/server/express.ts +37 -3
- package/src/ts/server/index.ts +24 -14
- package/src/ts/server/jobs.test.ts +74 -0
- package/src/ts/server/jobs.ts +77 -0
- package/src/ts/server/utils/validation.ts +33 -0
- package/src/ts/types.ts +167 -59
- package/dist/client/client/grapesjs/publish.d.ts +0 -18
- package/dist/page.js +0 -15
- package/src/plugins/AuthPlugin.js +0 -21
- package/src/plugins/Cache.js +0 -11
- package/src/plugins/DefaultBackend.js +0 -149
- package/src/plugins/ExpressPlugin.js +0 -39
- package/src/plugins/HooksPlugin.js +0 -58
- package/src/plugins/PublishPlugin.js +0 -69
- package/src/plugins/StaticPlugin.js +0 -48
- package/src/plugins/WebsitePlugin.js +0 -207
- package/src/ts/client/grapesjs/publish.ts +0 -403
- /package/.silex/default/{.silex.data.json → website.json} +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Silex website builder, free/libre no-code tool for makers.
|
|
3
|
+
* Copyright (c) 2023 lexoyo and Silex Labs foundation
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { getPageSlug } from '../../page'
|
|
19
|
+
import { onAll } from '../utils'
|
|
20
|
+
import { ApiBackendLoggedInPostMessage, ApiPublicationPublishRequestBody, ApiPublicationPublishRequestQuery, ApiPublicationPublishResponse, ApiPublicationStatusRequestQuery, ApiPublicationStatusResponse, BackendData, BackendType, JobData, JobStatus, PublicationData, PublicationJobData, PublicationSettings, WebsiteData, WebsiteId, WebsiteSettings } from '../../types'
|
|
21
|
+
import { Editor, ProjectData } from 'grapesjs'
|
|
22
|
+
import e from 'express'
|
|
23
|
+
import { PublicationUi } from './PublicationUi'
|
|
24
|
+
import { LoginStatus, loginStatus, logout, publicationStatus, publish } from '../services'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @fileoverview Publication manager for Silex
|
|
28
|
+
* This plugin adds a publication feature to Silex
|
|
29
|
+
* It lets the user publish the website to a hosting service
|
|
30
|
+
* It can optionally display a button and a dialog
|
|
31
|
+
* Useful commands:
|
|
32
|
+
* - publish: starts the publication process and optionally open the dialog
|
|
33
|
+
* - publish-login: open the login dialog
|
|
34
|
+
* - publish-logout: logout from the hosting service and let the user choose a hosting service again
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Constants
|
|
38
|
+
export const cmdPublicationStart = 'publish'
|
|
39
|
+
export const cmdPublicationLogin = 'publish-login'
|
|
40
|
+
export const cmdPublicationLogout = 'publish-logout'
|
|
41
|
+
|
|
42
|
+
// Types
|
|
43
|
+
export type PublishableEditor = Editor & {
|
|
44
|
+
PublicationManager: PublicationManager
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export enum PublicationStatus {
|
|
48
|
+
STATUS_NONE = 'STATUS_NONE',
|
|
49
|
+
STATUS_PENDING = 'STATUS_PENDING',
|
|
50
|
+
STATUS_ERROR = 'STATUS_ERROR',
|
|
51
|
+
STATUS_SUCCESS = 'STATUS_SUCCESS',
|
|
52
|
+
STATUS_LOGGED_OUT = 'STATUS_AUTH_ERROR',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type PublicationManagerOptions = {
|
|
56
|
+
appendTo?: string // If not provided, no button nor dialog will be created
|
|
57
|
+
websiteId: WebsiteId
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// plugin init cod
|
|
61
|
+
export default function publishPlugin(editor, opts) {
|
|
62
|
+
(editor as PublishableEditor).PublicationManager = new PublicationManager(editor, opts)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function jobStatusToPublicationStatus(status: JobStatus): PublicationStatus {
|
|
66
|
+
switch (status) {
|
|
67
|
+
case JobStatus.IN_PROGRESS:
|
|
68
|
+
return PublicationStatus.STATUS_PENDING
|
|
69
|
+
case JobStatus.ERROR:
|
|
70
|
+
return PublicationStatus.STATUS_ERROR
|
|
71
|
+
case JobStatus.SUCCESS:
|
|
72
|
+
return PublicationStatus.STATUS_SUCCESS
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Unknown job status ${status}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The publication manager class
|
|
78
|
+
// This class is responsible for the publication dialog and for the publication process
|
|
79
|
+
// It is added to the editor instance as editor.PublicationManager
|
|
80
|
+
export class PublicationManager {
|
|
81
|
+
/**
|
|
82
|
+
* Publication dialog
|
|
83
|
+
* This class is responsible for the UI
|
|
84
|
+
*/
|
|
85
|
+
dialog?: PublicationUi
|
|
86
|
+
/**
|
|
87
|
+
* Publication settings
|
|
88
|
+
* This is the data which is stored in the website settings
|
|
89
|
+
*/
|
|
90
|
+
settings: PublicationSettings
|
|
91
|
+
/**
|
|
92
|
+
* Plugin options
|
|
93
|
+
* This is the data which is passed to the plugin by grapesjs
|
|
94
|
+
*/
|
|
95
|
+
options: PublicationManagerOptions
|
|
96
|
+
/**
|
|
97
|
+
* Publication job during the publication process
|
|
98
|
+
*/
|
|
99
|
+
job: PublicationJobData | null = null
|
|
100
|
+
/**
|
|
101
|
+
* Publication state
|
|
102
|
+
* This is the state of the publication process
|
|
103
|
+
*/
|
|
104
|
+
status: PublicationStatus = PublicationStatus.STATUS_NONE
|
|
105
|
+
|
|
106
|
+
constructor(private editor: PublishableEditor, opts: PublicationManagerOptions) {
|
|
107
|
+
this.options = {
|
|
108
|
+
appendTo: 'options',
|
|
109
|
+
...opts,
|
|
110
|
+
} as PublicationManagerOptions
|
|
111
|
+
// Save the publication settings in the website settings
|
|
112
|
+
editor.on('storage:start:store', (data) => {
|
|
113
|
+
data.publication = this.settings
|
|
114
|
+
})
|
|
115
|
+
// load publication settings from the website
|
|
116
|
+
editor.on('storage:end:load', (data) => {
|
|
117
|
+
this.settings = data.publication ?? {}
|
|
118
|
+
})
|
|
119
|
+
// Add the publish command to the editor
|
|
120
|
+
editor.Commands.add(cmdPublicationStart, () => this.startPublication())
|
|
121
|
+
editor.Commands.add(cmdPublicationLogin, (editor: PublishableEditor, sender: any, provider: BackendData) => this.goLogin(provider))
|
|
122
|
+
editor.Commands.add(cmdPublicationLogout, () => this.goLogout())
|
|
123
|
+
// Add the publication dialog to the editor
|
|
124
|
+
if (this.options.appendTo) {
|
|
125
|
+
this.dialog = new PublicationUi(editor, {
|
|
126
|
+
appendTo: this.options.appendTo,
|
|
127
|
+
})
|
|
128
|
+
} else {
|
|
129
|
+
console.info('PublicationUi is disabled because no appendTo option is set')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async goLogin(provider: BackendData) {
|
|
134
|
+
window.open(provider.authUrl, '_blank')
|
|
135
|
+
return new Promise(resolve => {
|
|
136
|
+
const onMessage = async (event) => {
|
|
137
|
+
const data = event.data as ApiBackendLoggedInPostMessage
|
|
138
|
+
if (data?.type === 'login') {
|
|
139
|
+
window.removeEventListener('message', onMessage)
|
|
140
|
+
if (data.error) {
|
|
141
|
+
this.status = PublicationStatus.STATUS_LOGGED_OUT
|
|
142
|
+
this.settings.backend = null
|
|
143
|
+
this.dialog && this.dialog.displayError(data.message, this.job, this.status, this.settings)
|
|
144
|
+
} else {
|
|
145
|
+
this.editor.trigger('publish:login')
|
|
146
|
+
const { backend/*, user, websiteMeta */ }: LoginStatus = await loginStatus(data.backendId, provider.type, this.options.websiteId)
|
|
147
|
+
this.settings.backend = backend
|
|
148
|
+
await this.startPublication()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
window.addEventListener('message', onMessage, false)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async goLogout() {
|
|
157
|
+
try {
|
|
158
|
+
await logout(this.settings.backend.backendId, BackendType.HOSTING)
|
|
159
|
+
this.settings.backend = null
|
|
160
|
+
this.dialog && this.dialog.displayPending(this.job, this.status, this.settings)
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.error('logout error', e)
|
|
163
|
+
this.status = PublicationStatus.STATUS_ERROR
|
|
164
|
+
this.dialog && this.dialog.displayError(e.message, this.job, this.status, this.settings)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async startPublication() {
|
|
169
|
+
if (this.status === PublicationStatus.STATUS_PENDING) throw new Error('Publication is already in progress')
|
|
170
|
+
this.status = PublicationStatus.STATUS_PENDING
|
|
171
|
+
if (!this.settings?.backend?.isLoggedIn) {
|
|
172
|
+
this.status = PublicationStatus.STATUS_LOGGED_OUT
|
|
173
|
+
this.dialog && this.dialog.displayError('Please login', this.job, this.status, this.settings)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
this.dialog && this.dialog.displayPending(this.job, this.status, this.settings)
|
|
177
|
+
this.editor.trigger('publish:before')
|
|
178
|
+
const projectData = this.editor.getProjectData() as WebsiteData
|
|
179
|
+
const siteSettings = this.editor.getModel().get('settings') as WebsiteSettings
|
|
180
|
+
// Update assets URL to display outside the editor
|
|
181
|
+
const assetsFolderUrl = this.settings?.assets?.url
|
|
182
|
+
if (assetsFolderUrl) {
|
|
183
|
+
const publishedUrl = path => `${assetsFolderUrl}/${path.split('/').pop()}`
|
|
184
|
+
// New URLs for assets, according to site config
|
|
185
|
+
onAll(this.editor, c => {
|
|
186
|
+
// Attributes
|
|
187
|
+
if (c.get('type') === 'image') {
|
|
188
|
+
const path = c.get('src')
|
|
189
|
+
c.set('tmp-src', path)
|
|
190
|
+
c.set('src', publishedUrl(path))
|
|
191
|
+
}
|
|
192
|
+
//// Inline styles
|
|
193
|
+
//// This is handled by the editor.Css.getAll loop
|
|
194
|
+
//const bgUrl = c.getStyle()['background-image']?.match(/url\('(.*)'\)/)?.pop()
|
|
195
|
+
//if(bgUrl) {
|
|
196
|
+
// c.set('tmp-bg-url', bgUrl)
|
|
197
|
+
// c.setStyle({
|
|
198
|
+
// ...c.getStyle(),
|
|
199
|
+
// 'background-image': `url('${publishedUrl(bgUrl)}')`,
|
|
200
|
+
// })
|
|
201
|
+
//}
|
|
202
|
+
})
|
|
203
|
+
this.editor.Css.getAll()
|
|
204
|
+
.forEach(c => {
|
|
205
|
+
const bgUrl = c.getStyle()['background-image']?.match(/url\('(.*)'\)/)?.pop()
|
|
206
|
+
if (bgUrl) {
|
|
207
|
+
c.setStyle({
|
|
208
|
+
...c.getStyle(),
|
|
209
|
+
'background-image': `url('${publishedUrl(bgUrl)}')`,
|
|
210
|
+
})
|
|
211
|
+
c.set('tmp-bg-url-css' as any, bgUrl)
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
// Build the files structure
|
|
216
|
+
const files = await this.getFiles(siteSettings)
|
|
217
|
+
|
|
218
|
+
// Create the data to send to the server
|
|
219
|
+
const data: PublicationData = {
|
|
220
|
+
...projectData,
|
|
221
|
+
settings: siteSettings,
|
|
222
|
+
publication: this.settings,
|
|
223
|
+
files,
|
|
224
|
+
}
|
|
225
|
+
// Reset asset URLs
|
|
226
|
+
if (assetsFolderUrl) {
|
|
227
|
+
onAll(this.editor, c => {
|
|
228
|
+
if (c.get('type') === 'image' && c.has('tmp-src')) {
|
|
229
|
+
c.set('src', c.get('tmp-src'))
|
|
230
|
+
c.set('tmp-src')
|
|
231
|
+
}
|
|
232
|
+
//// This is handled by the editor.Css.getAll loop
|
|
233
|
+
//if(c.getStyle()['background-image'] && c.has('tmp-bg-url')) {
|
|
234
|
+
// c.setStyle({
|
|
235
|
+
// ...c.getStyle(),
|
|
236
|
+
// 'background-image': `url('${c.get('tmp-bg-url')}')`,
|
|
237
|
+
// })
|
|
238
|
+
// c.set('tmp-bg-url')
|
|
239
|
+
//}
|
|
240
|
+
})
|
|
241
|
+
this.editor.Css.getAll()
|
|
242
|
+
.forEach(c => {
|
|
243
|
+
if (c.has('tmp-bg-url-css' as any)) {
|
|
244
|
+
c.setStyle({
|
|
245
|
+
...c.getStyle(),
|
|
246
|
+
'background-image': `url('${c.get('tmp-bg-url-css' as any)}')`,
|
|
247
|
+
})
|
|
248
|
+
c.set('tmp-bg-url-css' as any)
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
this.editor.trigger('publish:start', data)
|
|
253
|
+
try {
|
|
254
|
+
const id = this.options.websiteId
|
|
255
|
+
const [url, job] = await publish(
|
|
256
|
+
id,
|
|
257
|
+
data as ApiPublicationPublishRequestBody
|
|
258
|
+
)
|
|
259
|
+
this.job = job
|
|
260
|
+
this.status = jobStatusToPublicationStatus(this.job.status)
|
|
261
|
+
// Save the publication settings
|
|
262
|
+
if(this.settings.url !== url) {
|
|
263
|
+
this.settings.url = url
|
|
264
|
+
await this.editor.store(null)
|
|
265
|
+
}
|
|
266
|
+
this.trackProgress()
|
|
267
|
+
} catch (e) {
|
|
268
|
+
console.error('publish error', e)
|
|
269
|
+
if(e.code === 401) {
|
|
270
|
+
this.status = PublicationStatus.STATUS_LOGGED_OUT
|
|
271
|
+
this.settings.backend = null
|
|
272
|
+
this.dialog && this.dialog.displayError('Please login.', this.job, this.status, this.settings)
|
|
273
|
+
} else {
|
|
274
|
+
this.status = PublicationStatus.STATUS_ERROR
|
|
275
|
+
this.dialog && this.dialog.displayError(`An error occured, your site is not published. ${e.message}`, this.job, this.status, this.settings)
|
|
276
|
+
}
|
|
277
|
+
this.editor.trigger('publish:stop', { success: false, message: e.message })
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async getFiles(siteSettings: WebsiteSettings) {
|
|
283
|
+
return this.editor.Pages.getAll().map(page => {
|
|
284
|
+
const pageSettings = page.get('settings') as WebsiteSettings
|
|
285
|
+
const pageName = this.settings?.autoHomePage !== false && page.get('type') === 'main' ? 'index' : (page.get('name') || page.get('type'))
|
|
286
|
+
function getSetting(name) {
|
|
287
|
+
return (pageSettings || {})[name] || (siteSettings || [])[name] || ''
|
|
288
|
+
}
|
|
289
|
+
const component = page.getMainComponent()
|
|
290
|
+
const slug = getPageSlug(pageName)
|
|
291
|
+
return {
|
|
292
|
+
html: `
|
|
293
|
+
<!DOCTYPE html>
|
|
294
|
+
<html lang="${getSetting('lang')}">
|
|
295
|
+
<head>
|
|
296
|
+
<link rel="stylesheet" href="${this.settings?.css?.url || ''}/${slug}.css" />
|
|
297
|
+
${siteSettings?.head || ''}
|
|
298
|
+
${pageSettings?.head || ''}
|
|
299
|
+
<title>${getSetting('title')}</title>
|
|
300
|
+
<link rel="icon" href="${getSetting('favicon')}" />
|
|
301
|
+
${['description', 'og:title', 'og:description', 'og:image']
|
|
302
|
+
.map(prop => `<meta property="${prop}" content="${getSetting(prop)}"/>`)
|
|
303
|
+
.join('\n')
|
|
304
|
+
}
|
|
305
|
+
</head>
|
|
306
|
+
${this.editor.getHtml({ component })}
|
|
307
|
+
</html>
|
|
308
|
+
`,
|
|
309
|
+
css: this.editor.getCss({ component }),
|
|
310
|
+
cssPath: `${this.settings?.css?.path || ''}/${slug}${this.settings?.css?.ext || '.css'}`,
|
|
311
|
+
htmlPath: `${this.settings?.html?.path || ''}/${slug}${this.settings?.html?.ext || '.html'}`,
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async trackProgress() {
|
|
317
|
+
try {
|
|
318
|
+
this.job = await publicationStatus(this.job.jobId)
|
|
319
|
+
this.status = jobStatusToPublicationStatus(this.job.status)
|
|
320
|
+
} catch (e) {
|
|
321
|
+
this.status = PublicationStatus.STATUS_ERROR
|
|
322
|
+
this.dialog && this.dialog.displayError(`An error occured, your site is not published. ${e.message}`, this.job, this.status, this.settings)
|
|
323
|
+
this.editor.trigger('publish:stop', { success: false, message: e.message })
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
if (this.job.status === JobStatus.IN_PROGRESS) {
|
|
327
|
+
setTimeout(() => this.trackProgress(), 2000)
|
|
328
|
+
} else {
|
|
329
|
+
this.editor.trigger('publish:stop', { success: this.job.status === JobStatus.SUCCESS, message: this.job.message })
|
|
330
|
+
}
|
|
331
|
+
this.dialog && this.dialog.displayPending(this.job, this.status, this.settings)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Silex website builder, free/libre no-code tool for makers.
|
|
3
|
+
* Copyright (c) 2023 lexoyo and Silex Labs foundation
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { html, render, TemplateResult } from 'lit-html'
|
|
19
|
+
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'
|
|
20
|
+
//import { map } from 'lit-html/directives/map.js'
|
|
21
|
+
import { cmdPublicationLogin, cmdPublicationLogout, cmdPublicationStart, PublicationStatus, PublishableEditor } from './PublicationManager'
|
|
22
|
+
import { BackendData, BackendType, PublicationJobData, PublicationSettings } from '../../types'
|
|
23
|
+
import { backendList } from '../services'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @fileoverview define the publication dialog
|
|
27
|
+
* This is the UI of the publication feature
|
|
28
|
+
* It is a dialog which allows the user to login to a backend and publish the website
|
|
29
|
+
* It also displays the publication status and logs during the publication process
|
|
30
|
+
* This class is used by the PublicationManager
|
|
31
|
+
* This is optional, you can use the PublicationManager without this UI
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// **
|
|
35
|
+
// Constants
|
|
36
|
+
export const cmdPublish = 'publish-open-dialog'
|
|
37
|
+
|
|
38
|
+
// **
|
|
39
|
+
// Types
|
|
40
|
+
export type PublicationDialogOptions = {
|
|
41
|
+
appendTo: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// **
|
|
45
|
+
// Utils
|
|
46
|
+
function cleanupLogEntry(arr: string[][]): string {
|
|
47
|
+
return arr[arr.length - 1]
|
|
48
|
+
?.map(str => str.replace(/\[.*\]/g, '').trim())
|
|
49
|
+
?.filter(str => !!str)
|
|
50
|
+
?.join('\n')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Class to manage the publication dialog
|
|
55
|
+
*/
|
|
56
|
+
export class PublicationUi {
|
|
57
|
+
/**
|
|
58
|
+
* Dialog state
|
|
59
|
+
*/
|
|
60
|
+
private isOpen = false
|
|
61
|
+
/**
|
|
62
|
+
* Dialog content
|
|
63
|
+
*/
|
|
64
|
+
private errorMessage = ''
|
|
65
|
+
/**
|
|
66
|
+
* Dialog element
|
|
67
|
+
* This is the DOM element of the dialog
|
|
68
|
+
*/
|
|
69
|
+
private el: HTMLElement
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Initialize the dialog and the publish button
|
|
73
|
+
*/
|
|
74
|
+
constructor(private editor: PublishableEditor, private options: PublicationDialogOptions) {
|
|
75
|
+
// Reference to the dialog element
|
|
76
|
+
this.el = this.createDialogElements()
|
|
77
|
+
// Add the publish command to the editor
|
|
78
|
+
const openDialog = () => this.openDialog()
|
|
79
|
+
const closeDialog = () => this.closeDialog()
|
|
80
|
+
editor.Commands.add(cmdPublish, {
|
|
81
|
+
run(editor: PublishableEditor) { openDialog() },
|
|
82
|
+
stop(editor: PublishableEditor) { closeDialog() },
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// **
|
|
87
|
+
// Convenient state checkers
|
|
88
|
+
isSuccess(status: PublicationStatus) {
|
|
89
|
+
return status === PublicationStatus.STATUS_SUCCESS
|
|
90
|
+
}
|
|
91
|
+
isPending(status: PublicationStatus) {
|
|
92
|
+
return status === PublicationStatus.STATUS_PENDING
|
|
93
|
+
}
|
|
94
|
+
isError(status: PublicationStatus) {
|
|
95
|
+
return status === PublicationStatus.STATUS_ERROR
|
|
96
|
+
}
|
|
97
|
+
isLoggedOut(status: PublicationStatus) {
|
|
98
|
+
return status === PublicationStatus.STATUS_LOGGED_OUT
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// **
|
|
102
|
+
// Functions to open and close the dialog
|
|
103
|
+
getDialogElements(): { el: HTMLElement, primary: HTMLElement, secondary: HTMLElement } {
|
|
104
|
+
const el = document.querySelector('#publish-dialog') as HTMLElement
|
|
105
|
+
const primary = el?.querySelector('#publish-button--primary') as HTMLElement
|
|
106
|
+
const secondary = el?.querySelector('#publish-button--secondary') as HTMLElement
|
|
107
|
+
if(!el || !primary || !secondary) throw new Error('Publication dialog elements not found')
|
|
108
|
+
return { el, primary, secondary }
|
|
109
|
+
}
|
|
110
|
+
createDialogElements(): HTMLElement {
|
|
111
|
+
// Create the dialog element
|
|
112
|
+
const el = document.createElement('div')
|
|
113
|
+
el.id = 'publish-dialog'
|
|
114
|
+
el.className = 'silex-dialog-inline silex-dialog gjs-two-color'
|
|
115
|
+
document.body.append(el)
|
|
116
|
+
// Create the publish button
|
|
117
|
+
this.editor.Panels.addButton(this.options.appendTo, {
|
|
118
|
+
id: 'publish-button',
|
|
119
|
+
className: 'silex-button--size publish-button',
|
|
120
|
+
command: cmdPublish,
|
|
121
|
+
attributes: { title: 'Publish' },
|
|
122
|
+
label: '<span class="fa-solid fa-upload"></span><span class="silex-button--small">Publish</span>',
|
|
123
|
+
})
|
|
124
|
+
return el
|
|
125
|
+
}
|
|
126
|
+
move(rect) {
|
|
127
|
+
Object.keys(rect).forEach(key => this.el.style[key] = rect[key] + 'px')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// **
|
|
131
|
+
// Functions to render the dialog
|
|
132
|
+
private async renderDialog(job: PublicationJobData, status: PublicationStatus, settings: PublicationSettings) {
|
|
133
|
+
try {
|
|
134
|
+
if(this.isOpen && (!status || !settings)) throw new Error('PublicationUi: open but no status or settings')
|
|
135
|
+
render(html`
|
|
136
|
+
${!this.isOpen ? '' : settings.backend ? await this.renderOpenDialog(job, status, settings) : await this.renderLoginDialog(status, settings)}
|
|
137
|
+
`, this.el)
|
|
138
|
+
if (this.isOpen) {
|
|
139
|
+
this.el.classList.remove('silex-dialog-hide')
|
|
140
|
+
} else {
|
|
141
|
+
this.el.classList.add('silex-dialog-hide')
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error('Error while rendering the dialog', err)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async renderOpenDialog(job: PublicationJobData, status: PublicationStatus, settings: PublicationSettings): Promise<TemplateResult> {
|
|
148
|
+
return html`
|
|
149
|
+
<main>
|
|
150
|
+
${this.isPending(status) ? html`
|
|
151
|
+
<p>Publication in progress</p>
|
|
152
|
+
` : ''}
|
|
153
|
+
${this.isSuccess(status) ? html`
|
|
154
|
+
<p>Publication success</p>
|
|
155
|
+
${settings.url ? html`<a href="${settings.url}" target="_blank">Click here to view the published website</a>` : ''}
|
|
156
|
+
` : ''}
|
|
157
|
+
${this.isError(status) || this.isLoggedOut(status) ? html`
|
|
158
|
+
<p>Publication error</p>
|
|
159
|
+
<div>${unsafeHTML(this.errorMessage)}</div>
|
|
160
|
+
` : ''}
|
|
161
|
+
${job && job.message ? html`
|
|
162
|
+
<div>${unsafeHTML(job.message)}</div>
|
|
163
|
+
` : ''}
|
|
164
|
+
${this.isPending(status) ? html`
|
|
165
|
+
<progress
|
|
166
|
+
value=""
|
|
167
|
+
style="width: 100%;"
|
|
168
|
+
></progress>
|
|
169
|
+
` : ''}
|
|
170
|
+
${job && job.logs?.length ? html`
|
|
171
|
+
<br><details>
|
|
172
|
+
<summary>Logs</summary>
|
|
173
|
+
<pre style="
|
|
174
|
+
max-width: 100%;
|
|
175
|
+
max-height: 50vh;
|
|
176
|
+
overflow: auto;
|
|
177
|
+
font-size: x-small;
|
|
178
|
+
"
|
|
179
|
+
>${unsafeHTML(cleanupLogEntry(job.logs))}
|
|
180
|
+
</pre>
|
|
181
|
+
</details>
|
|
182
|
+
` : ''}
|
|
183
|
+
${job && job.errors?.length ? html`
|
|
184
|
+
<br><details>
|
|
185
|
+
<summary>Errors</summary>
|
|
186
|
+
<pre style="
|
|
187
|
+
max-width: 100%;
|
|
188
|
+
max-height: 50vh;
|
|
189
|
+
overflow: auto;
|
|
190
|
+
font-size: x-small;
|
|
191
|
+
"
|
|
192
|
+
>${unsafeHTML(cleanupLogEntry(job.errors))}
|
|
193
|
+
</pre>
|
|
194
|
+
</details>
|
|
195
|
+
` : ''}
|
|
196
|
+
</main>
|
|
197
|
+
<footer>
|
|
198
|
+
${this.isPending(status) || this.isLoggedOut(status) ? '' : html`
|
|
199
|
+
<button
|
|
200
|
+
class="silex-button silex-button--primary"
|
|
201
|
+
id="publish-button--primary"
|
|
202
|
+
@click=${() => this.editor.Commands.run(cmdPublicationStart)}
|
|
203
|
+
>Publish</button>
|
|
204
|
+
`}
|
|
205
|
+
${this.isLoggedOut(status) ? html`
|
|
206
|
+
<button
|
|
207
|
+
class="silex-button silex-button--primary"
|
|
208
|
+
id="publish-button--primary"
|
|
209
|
+
@click=${() => this.editor.Commands.run(cmdPublicationLogin)}
|
|
210
|
+
>Login</button>
|
|
211
|
+
`: settings.backend.disableLogout ? '' : html`
|
|
212
|
+
<button
|
|
213
|
+
class="silex-button silex-button--secondary"
|
|
214
|
+
id="publish-button--secondary"
|
|
215
|
+
@click=${() => this.editor.Commands.run(cmdPublicationLogout)}
|
|
216
|
+
>Logout</button>
|
|
217
|
+
`}
|
|
218
|
+
<button
|
|
219
|
+
class="silex-button silex-button--secondary"
|
|
220
|
+
id="publish-button--secondary"
|
|
221
|
+
@click=${() => this.editor.Commands.stop(cmdPublish)}
|
|
222
|
+
>Close</button>
|
|
223
|
+
</footer>
|
|
224
|
+
`
|
|
225
|
+
}
|
|
226
|
+
async renderLoginDialog(status: PublicationStatus, settings: PublicationSettings): Promise<TemplateResult> {
|
|
227
|
+
try {
|
|
228
|
+
const hostingProviders = await backendList(BackendType.HOSTING)
|
|
229
|
+
const loggedProvider: BackendData = hostingProviders.find(provider => provider.isLoggedIn)
|
|
230
|
+
|
|
231
|
+
if (loggedProvider) {
|
|
232
|
+
settings.backend = loggedProvider
|
|
233
|
+
this.editor.Commands.run(cmdPublicationStart)
|
|
234
|
+
return html``
|
|
235
|
+
}
|
|
236
|
+
return html`
|
|
237
|
+
<main>
|
|
238
|
+
<p>You need to login to publish your website</p>
|
|
239
|
+
${this.isError(status) || this.isLoggedOut(status) ? html`
|
|
240
|
+
<p>Login error</p>
|
|
241
|
+
<div>${unsafeHTML(this.errorMessage)}</div>
|
|
242
|
+
` : ''}
|
|
243
|
+
${hostingProviders.map(backend => html`
|
|
244
|
+
<button
|
|
245
|
+
class="silex-button silex-button--primary"
|
|
246
|
+
id="publish-button--primary"
|
|
247
|
+
@click=${() => this.editor.Commands.run(cmdPublicationLogin, backend)}
|
|
248
|
+
>Login with ${backend.backendId}</button>
|
|
249
|
+
`)}
|
|
250
|
+
</main>
|
|
251
|
+
<footer>
|
|
252
|
+
<button
|
|
253
|
+
class="silex-button silex-button--secondary"
|
|
254
|
+
id="publish-button--secondary"
|
|
255
|
+
@click=${() => this.editor.Commands.stop(cmdPublish)}
|
|
256
|
+
>Close</button>
|
|
257
|
+
</footer>
|
|
258
|
+
`
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error(err)
|
|
261
|
+
return html`
|
|
262
|
+
<header>
|
|
263
|
+
<h3>Oops</h3>
|
|
264
|
+
</header>
|
|
265
|
+
<main>
|
|
266
|
+
<p>Unable to load hosting providers</p>
|
|
267
|
+
<p>Something went wrong: ${err.message}</p>
|
|
268
|
+
</main>
|
|
269
|
+
<footer>
|
|
270
|
+
<button
|
|
271
|
+
class="silex-button silex-button--secondary"
|
|
272
|
+
id="publish-button--secondary"
|
|
273
|
+
@click=${() => this.editor.Commands.stop(cmdPublish)}
|
|
274
|
+
>Close</button>
|
|
275
|
+
</footer>
|
|
276
|
+
`
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
displayPending(job: PublicationJobData, status: PublicationStatus, settings: PublicationSettings) {
|
|
280
|
+
this.errorMessage = null
|
|
281
|
+
this.renderDialog(job, status, settings)
|
|
282
|
+
}
|
|
283
|
+
displayError(message: string, job: PublicationJobData, status: PublicationStatus, settings: PublicationSettings) {
|
|
284
|
+
console.error(message, job?.message)
|
|
285
|
+
this.errorMessage = message
|
|
286
|
+
this.renderDialog(job, status, settings)
|
|
287
|
+
}
|
|
288
|
+
async closeDialog() {
|
|
289
|
+
this.isOpen = false
|
|
290
|
+
this.renderDialog(null, null, null)
|
|
291
|
+
}
|
|
292
|
+
async toggleDialog() {
|
|
293
|
+
if (this.isOpen) this.closeDialog()
|
|
294
|
+
else this.openDialog()
|
|
295
|
+
}
|
|
296
|
+
async openDialog() {
|
|
297
|
+
this.isOpen = true
|
|
298
|
+
|
|
299
|
+
// Position
|
|
300
|
+
const buttonEl = this.editor.Panels.getPanel('options')?.view.el.querySelector('.publish-button')
|
|
301
|
+
if(buttonEl) {
|
|
302
|
+
const rect = buttonEl.getBoundingClientRect()
|
|
303
|
+
|
|
304
|
+
const width = 450
|
|
305
|
+
const padding = 10 * 2
|
|
306
|
+
const minHeight = 50
|
|
307
|
+
this.move({
|
|
308
|
+
left: rect.right - width - padding,
|
|
309
|
+
top: rect.bottom + 10,
|
|
310
|
+
width,
|
|
311
|
+
minHeight,
|
|
312
|
+
})
|
|
313
|
+
} else {
|
|
314
|
+
console.error('Unable to find publish button')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Publication
|
|
318
|
+
this.editor.Commands.run(cmdPublicationStart)
|
|
319
|
+
}
|
|
320
|
+
}
|