@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.
Files changed (110) hide show
  1. package/.silex.js +38 -8
  2. package/dist/client/client/config.d.ts +11 -41
  3. package/dist/client/client/grapesjs/LoginDialog.d.ts +13 -0
  4. package/dist/client/client/grapesjs/PublicationManager.d.ts +69 -0
  5. package/dist/client/client/grapesjs/PublicationUi.d.ts +58 -0
  6. package/dist/client/client/grapesjs/index.d.ts +18 -44
  7. package/dist/client/client/grapesjs/storage.d.ts +1 -0
  8. package/dist/client/client/services.d.ts +35 -0
  9. package/dist/client/constants.d.ts +17 -0
  10. package/dist/client/css/admin.css +86 -3
  11. package/dist/client/js/main.js +201 -90
  12. package/dist/client/js/main.js.LICENSE.txt +6 -0
  13. package/dist/client/types.d.ts +200 -29
  14. package/dist/constants.d.ts +21 -0
  15. package/dist/constants.js +38 -1
  16. package/dist/constants.js.map +1 -0
  17. package/dist/events.d.ts +2 -0
  18. package/dist/events.js +17 -0
  19. package/dist/events.js.map +1 -0
  20. package/dist/plugins/AuthPlugin.d.ts +1 -0
  21. package/dist/plugins/AuthPlugin.js +38 -0
  22. package/dist/plugins/AuthPlugin.js.map +1 -0
  23. package/dist/plugins/Cache.d.ts +2 -0
  24. package/dist/plugins/Cache.js +32 -0
  25. package/dist/plugins/Cache.js.map +1 -0
  26. package/dist/plugins/FtpBackend.d.ts +92 -0
  27. package/dist/plugins/FtpBackend.js +604 -0
  28. package/dist/plugins/FtpBackend.js.map +1 -0
  29. package/dist/plugins/HooksPlugin.d.ts +7 -0
  30. package/dist/plugins/HooksPlugin.js +76 -0
  31. package/dist/plugins/HooksPlugin.js.map +1 -0
  32. package/dist/plugins/PublicationPlugin.d.ts +17 -0
  33. package/dist/plugins/PublicationPlugin.js +170 -0
  34. package/dist/plugins/PublicationPlugin.js.map +1 -0
  35. package/dist/plugins/SslPlugin.d.ts +1 -0
  36. package/dist/plugins/SslPlugin.js +86 -0
  37. package/dist/plugins/SslPlugin.js.map +1 -0
  38. package/dist/plugins/StaticPlugin.d.ts +1 -0
  39. package/dist/plugins/StaticPlugin.js +66 -0
  40. package/dist/plugins/StaticPlugin.js.map +1 -0
  41. package/dist/plugins/WebsitePlugin.d.ts +14 -0
  42. package/dist/plugins/WebsitePlugin.js +302 -0
  43. package/dist/plugins/WebsitePlugin.js.map +1 -0
  44. package/dist/server/backends/FsBackend.d.ts +42 -0
  45. package/dist/server/backends/FsBackend.js +269 -0
  46. package/dist/server/backends/FsBackend.js.map +1 -0
  47. package/dist/server/backends/index.d.ts +95 -0
  48. package/dist/server/backends/index.js +255 -0
  49. package/dist/server/backends/index.js.map +1 -0
  50. package/dist/server/cli.js +0 -1
  51. package/dist/server/config.d.ts +58 -0
  52. package/dist/server/config.js +128 -24
  53. package/dist/server/config.js.map +1 -0
  54. package/dist/server/express.js +31 -3
  55. package/dist/server/index.js +9 -20
  56. package/dist/server/jobs.d.ts +6 -0
  57. package/dist/server/jobs.js +75 -0
  58. package/dist/server/jobs.js.map +1 -0
  59. package/dist/server/utils/validation.d.ts +7 -0
  60. package/dist/server/utils/validation.js +37 -0
  61. package/dist/server/utils/validation.js.map +1 -0
  62. package/dist/types.d.ts +270 -0
  63. package/dist/types.js +35 -28
  64. package/dist/types.js.map +1 -0
  65. package/package.json +19 -8
  66. package/public/silex.js +17 -0
  67. package/src/css/dialog.scss +9 -2
  68. package/src/css/grapesjs.scss +79 -0
  69. package/src/css/project-bar.scss +2 -2
  70. package/src/example-client-config.js +17 -0
  71. package/src/html/templates/includes.pug +0 -1
  72. package/src/ts/client/config.ts +7 -6
  73. package/src/ts/client/grapesjs/LoginDialog.ts +190 -0
  74. package/src/ts/client/grapesjs/PublicationManager.ts +333 -0
  75. package/src/ts/client/grapesjs/PublicationUi.ts +320 -0
  76. package/src/ts/client/grapesjs/index.ts +88 -85
  77. package/src/ts/client/grapesjs/storage.ts +110 -0
  78. package/src/ts/client/index.ts +5 -7
  79. package/src/ts/client/services.ts +129 -0
  80. package/src/ts/constants.ts +21 -0
  81. package/src/ts/plugins/AuthPlugin.ts +38 -0
  82. package/src/ts/plugins/Cache.ts +28 -0
  83. package/src/ts/plugins/FtpBackend.ts +689 -0
  84. package/src/ts/plugins/HooksPlugin.ts +79 -0
  85. package/src/ts/plugins/PublicationPlugin.ts +170 -0
  86. package/src/{plugins/SslPlugin.js → ts/plugins/SslPlugin.ts} +24 -6
  87. package/src/ts/plugins/StaticPlugin.ts +68 -0
  88. package/src/ts/plugins/WebsitePlugin.ts +318 -0
  89. package/src/ts/server/backends/FsBackend.ts +266 -0
  90. package/src/ts/server/backends/index.ts +309 -0
  91. package/src/ts/server/cli.ts +0 -2
  92. package/src/ts/server/config.ts +103 -33
  93. package/src/ts/server/express.ts +37 -3
  94. package/src/ts/server/index.ts +24 -14
  95. package/src/ts/server/jobs.test.ts +74 -0
  96. package/src/ts/server/jobs.ts +77 -0
  97. package/src/ts/server/utils/validation.ts +33 -0
  98. package/src/ts/types.ts +167 -59
  99. package/dist/client/client/grapesjs/publish.d.ts +0 -18
  100. package/dist/page.js +0 -15
  101. package/src/plugins/AuthPlugin.js +0 -21
  102. package/src/plugins/Cache.js +0 -11
  103. package/src/plugins/DefaultBackend.js +0 -149
  104. package/src/plugins/ExpressPlugin.js +0 -39
  105. package/src/plugins/HooksPlugin.js +0 -58
  106. package/src/plugins/PublishPlugin.js +0 -69
  107. package/src/plugins/StaticPlugin.js +0 -48
  108. package/src/plugins/WebsitePlugin.js +0 -207
  109. package/src/ts/client/grapesjs/publish.ts +0 -403
  110. /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
+ }