@silexlabs/silex 3.0.3 → 3.0.5
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/.env.default +4 -4
- package/.silex.js +8 -0
- package/dist/client/client/assetUrl.d.ts +1 -9
- package/dist/client/client/grapesjs/PublicationManager.d.ts +1 -0
- package/dist/client/client/grapesjs/storage.d.ts +2 -1
- package/dist/client/css/admin.css +22 -0
- package/dist/client/css/admin.css.map +1 -1
- package/dist/client/js/main.js +10 -10
- package/dist/client/silex-lib/src/ts/client/assetUrl.d.ts +1 -9
- package/dist/client/silex-lib/src/ts/client/grapesjs/PublicationManager.d.ts +1 -0
- package/dist/client/silex-lib/src/ts/client/grapesjs/storage.d.ts +2 -1
- package/dist/plugins/server/plugins/server/GitlabConnector.d.ts +1 -0
- package/dist/plugins/server/plugins/server/GitlabConnector.js +16 -1
- package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
- package/dist/server/server/api/connectorApi.js +13 -5
- package/package.json +5 -6
- package/src/css/grapesjs.scss +20 -0
- package/src/ts/client/assetUrl.ts +1 -73
- package/src/ts/client/grapesjs/PublicationManager.ts +39 -20
- package/src/ts/client/grapesjs/index.ts +3 -19
- package/src/ts/client/grapesjs/storage.ts +38 -12
- package/src/ts/client/index.ts +38 -0
- package/src/ts/client/publication-transformers.ts +12 -1
- package/src/ts/plugins/server/GitlabConnector.ts +17 -1
- package/src/ts/server/api/connectorApi.ts +15 -7
|
@@ -18,12 +18,19 @@
|
|
|
18
18
|
import { ConnectorId, WebsiteId, WebsiteData, ConnectorUser, ConnectorType, ApiError } from '../../types'
|
|
19
19
|
import { websiteLoad, websiteSave } from '../api'
|
|
20
20
|
import { cmdLogin, eventLoggedIn, eventLoggedOut, getCurrentUser, updateUser } from './LoginDialog'
|
|
21
|
-
import {
|
|
22
|
-
import { PublicationStatus } from './PublicationManager'
|
|
21
|
+
import { addTempDataToAssetUrl, addTempDataToStyles, removeTempDataFromAssetUrl, removeTempDataFromStyles } from '../assetUrl'
|
|
22
|
+
import { PublicationStatus, PublishableEditor } from './PublicationManager'
|
|
23
23
|
import { ClientEvent } from '../events'
|
|
24
|
+
import { ProjectData } from 'grapesjs'
|
|
24
25
|
|
|
25
26
|
export const cmdPauseAutoSave = 'pause-auto-save'
|
|
26
27
|
|
|
28
|
+
// Wait for the next frame to avoid blocking the main thread
|
|
29
|
+
function nextFrame(): Promise<void> {
|
|
30
|
+
const fn = window.requestIdleCallback ?? window.requestAnimationFrame
|
|
31
|
+
return new Promise(resolve => fn(() => resolve()))
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
// Mechanism to keep only the last save during publication
|
|
28
35
|
let lastPendingSaving: WebsiteData = null
|
|
29
36
|
|
|
@@ -31,7 +38,7 @@ let lastPendingSaving: WebsiteData = null
|
|
|
31
38
|
// Note: there is already such a mechanism in grapesjs but it fails when the save is delayed by the publication
|
|
32
39
|
let isSaving = false
|
|
33
40
|
|
|
34
|
-
export const storagePlugin = (editor) => {
|
|
41
|
+
export const storagePlugin = (editor: PublishableEditor) => {
|
|
35
42
|
// Add useful commands
|
|
36
43
|
editor.Commands.add(cmdPauseAutoSave, {
|
|
37
44
|
run: () => {
|
|
@@ -46,25 +53,24 @@ export const storagePlugin = (editor) => {
|
|
|
46
53
|
})
|
|
47
54
|
// Add the storage connector
|
|
48
55
|
editor.Storage.add('connector', {
|
|
49
|
-
async load(options: { id: WebsiteId, connectorId: ConnectorId }): Promise<
|
|
56
|
+
async load(options: { id: WebsiteId, connectorId: ConnectorId }): Promise<ProjectData> {
|
|
50
57
|
try {
|
|
51
58
|
const user: ConnectorUser = await getCurrentUser(editor) ?? await updateUser(editor, ConnectorType.STORAGE, options.connectorId)
|
|
52
59
|
if (!user) throw new ApiError('Not logged in', 401)
|
|
53
60
|
const data = await websiteLoad({ websiteId: options.id, connectorId: user.storage.connectorId }) as WebsiteData
|
|
54
61
|
if (data.assets) data.assets = addTempDataToAssetUrl(data.assets, options.id, user.storage.connectorId)
|
|
55
|
-
if (data.pages) data.pages = addTempDataToPages(data.pages, options.id, user.storage.connectorId)
|
|
56
62
|
if (data.styles) data.styles = addTempDataToStyles(data.styles, options.id, user.storage.connectorId)
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
//setTimeout(() => progressiveLoadPages(editor, data))
|
|
64
|
+
await progressiveLoadPages(editor, data)
|
|
65
|
+
return {}
|
|
59
66
|
} catch (err) {
|
|
60
67
|
editor.UndoManager.clear()
|
|
61
68
|
if (err.httpStatusCode === 401) {
|
|
62
69
|
editor.once(eventLoggedIn, async () => {
|
|
63
70
|
try {
|
|
64
71
|
editor.runCommand(cmdPauseAutoSave)
|
|
65
|
-
|
|
72
|
+
await editor.Storage.load(options)
|
|
66
73
|
editor.once('canvas:frame:load', () => editor.stopCommand(cmdPauseAutoSave))
|
|
67
|
-
editor.loadProjectData(data)
|
|
68
74
|
} catch (err) {
|
|
69
75
|
console.error('connectorPlugin load error', err)
|
|
70
76
|
throw err
|
|
@@ -94,7 +100,7 @@ export const storagePlugin = (editor) => {
|
|
|
94
100
|
},
|
|
95
101
|
|
|
96
102
|
async doStore(data, options) {
|
|
97
|
-
if (editor.PublicationManager
|
|
103
|
+
if (editor.PublicationManager.status === PublicationStatus.STATUS_PENDING) {
|
|
98
104
|
// Publication is pending, save is delayed
|
|
99
105
|
return new Promise((resolve) => {
|
|
100
106
|
editor.once(ClientEvent.PUBLISH_END, () => {
|
|
@@ -115,7 +121,6 @@ export const storagePlugin = (editor) => {
|
|
|
115
121
|
const user = await getCurrentUser(editor)
|
|
116
122
|
if (user) {
|
|
117
123
|
data.assets = removeTempDataFromAssetUrl(data.assets)
|
|
118
|
-
data.pages = removeTempDataFromPages(data.pages)
|
|
119
124
|
data.styles = removeTempDataFromStyles(data.styles)
|
|
120
125
|
await websiteSave({ websiteId: options.id, connectorId: user.storage.connectorId, data })
|
|
121
126
|
isSaving = false
|
|
@@ -123,7 +128,7 @@ export const storagePlugin = (editor) => {
|
|
|
123
128
|
// Not logged in save is delayed
|
|
124
129
|
editor.once(eventLoggedIn, () => {
|
|
125
130
|
isSaving = false
|
|
126
|
-
return editor.Storage.store()
|
|
131
|
+
return editor.Storage.store(data, options)
|
|
127
132
|
})
|
|
128
133
|
editor.Commands.run(cmdLogin)
|
|
129
134
|
}
|
|
@@ -185,3 +190,24 @@ export const storagePlugin = (editor) => {
|
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
192
|
}
|
|
193
|
+
|
|
194
|
+
async function progressiveLoadPages(editor: PublishableEditor, data: ProjectData) {
|
|
195
|
+
editor.Pages.getAll().forEach(page => editor.Pages.remove(page))
|
|
196
|
+
let i = 0
|
|
197
|
+
for (const page of data.pages) {
|
|
198
|
+
document.querySelector('.silex-loader').innerHTML = `Loading page ${++i} / ${data.pages.length}`
|
|
199
|
+
await nextFrame()
|
|
200
|
+
const newPage = editor.Pages.add({
|
|
201
|
+
...page,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Charger les styles, assets, etc. après les pages
|
|
206
|
+
document.querySelector('.silex-loader').innerHTML = 'Loading styles and assets'
|
|
207
|
+
if (data.styles) editor.setStyle(data.styles)
|
|
208
|
+
if (data.assets) editor.AssetManager.add(data.assets)
|
|
209
|
+
|
|
210
|
+
// Sélectionner la première page
|
|
211
|
+
const firstPage = editor.Pages.getAll()[0]
|
|
212
|
+
if (firstPage) editor.Pages.select(firstPage)
|
|
213
|
+
}
|
package/src/ts/client/index.ts
CHANGED
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
*
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { Component, ComponentView } from 'grapesjs'
|
|
24
25
|
import { DEV_MESSAGE } from '../constants'
|
|
25
26
|
import { ClientConfig } from './config'
|
|
26
27
|
import { ClientEvent } from './events'
|
|
27
28
|
import { initEditor, getEditor } from './grapesjs/index'
|
|
28
29
|
import { cmdPauseAutoSave } from './grapesjs/storage'
|
|
30
|
+
import { displayedToStored, storedToDisplayed } from './assetUrl'
|
|
29
31
|
|
|
30
32
|
// Expose API to calling app as window.silex
|
|
31
33
|
export * from './expose'
|
|
@@ -83,6 +85,42 @@ export async function start(options = {}): Promise<void> {
|
|
|
83
85
|
// Add default plugins
|
|
84
86
|
await config.addDefaultPlugins()
|
|
85
87
|
|
|
88
|
+
const domc = editor.DomComponents
|
|
89
|
+
const imgType = domc.getType('image')
|
|
90
|
+
domc.addType('image', {
|
|
91
|
+
...imgType,
|
|
92
|
+
view: {
|
|
93
|
+
...imgType.view,
|
|
94
|
+
onRender() {
|
|
95
|
+
const view = this as ComponentView
|
|
96
|
+
imgType.view.onRender && imgType.view.onRender.call(this) // call original
|
|
97
|
+
const el = view.el
|
|
98
|
+
const elImg = view.el as HTMLImageElement
|
|
99
|
+
const src = this.model.getAttributes().src
|
|
100
|
+
if (src && !src.startsWith('http')) {
|
|
101
|
+
elImg.src = storedToDisplayed(src, config.websiteId, config.storageId)
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
model: {
|
|
106
|
+
...imgType.model,
|
|
107
|
+
init() {
|
|
108
|
+
const model = this as Component
|
|
109
|
+
imgType.model.init && imgType.model.init.call(this) // call original
|
|
110
|
+
// Handle changes by the AssetManager
|
|
111
|
+
model.on('change', () => {
|
|
112
|
+
if(model.get('src')) {
|
|
113
|
+
const newSrc = displayedToStored(model.get('src'))
|
|
114
|
+
if(newSrc !== model.get('src')) {
|
|
115
|
+
model.set('src', newSrc)
|
|
116
|
+
model.view.render()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
|
|
86
124
|
// Load the site
|
|
87
125
|
try {
|
|
88
126
|
editor.runCommand(cmdPauseAutoSave)
|
|
@@ -198,7 +198,18 @@ function doTransformPermalink(editor: Editor, cssValue: string): string {
|
|
|
198
198
|
export function transformBgImage(editor: Editor, style: StyleProps): StyleProps {
|
|
199
199
|
const cssValue = style['background-image']
|
|
200
200
|
if (cssValue) {
|
|
201
|
-
|
|
201
|
+
let newCssValue
|
|
202
|
+
if (Array.isArray(cssValue)) {
|
|
203
|
+
newCssValue = cssValue
|
|
204
|
+
.map(value => doTransformPermalink(editor, value))
|
|
205
|
+
.join(', ')
|
|
206
|
+
} else if (typeof cssValue === 'string') {
|
|
207
|
+
newCssValue = doTransformPermalink(editor, cssValue)
|
|
208
|
+
} else if (newCssValue === cssValue) {
|
|
209
|
+
// No change
|
|
210
|
+
return style
|
|
211
|
+
}
|
|
212
|
+
// Set the new value
|
|
202
213
|
return {
|
|
203
214
|
...style,
|
|
204
215
|
'background-image': newCssValue,
|
|
@@ -22,6 +22,7 @@ import { ApiError, ConnectorType, ConnectorUser, WebsiteData, WebsiteId, Website
|
|
|
22
22
|
import fetch from 'node-fetch'
|
|
23
23
|
import crypto, { createHash } from 'crypto'
|
|
24
24
|
import { join } from 'path'
|
|
25
|
+
import { Agent } from 'https'
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Gitlab connector
|
|
@@ -247,6 +248,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
247
248
|
// Call the API
|
|
248
249
|
const url = `${this.options.domain}/api/v4/projects/${websiteId}/repository/files/${safePath}?ref=${this.options.branch}&access_token=${this.getSessionToken(session).token?.access_token}`
|
|
249
250
|
const response = await fetch(url, {
|
|
251
|
+
agent: this.getAgent(),
|
|
250
252
|
method: 'GET',
|
|
251
253
|
headers: {
|
|
252
254
|
'Content-Type': 'application/json',
|
|
@@ -305,10 +307,12 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
305
307
|
let response
|
|
306
308
|
try {
|
|
307
309
|
response = await fetch(url, body && method !== 'GET' ? {
|
|
310
|
+
agent: this.getAgent(),
|
|
308
311
|
method,
|
|
309
312
|
headers,
|
|
310
313
|
body: body ? JSON.stringify(body) : undefined
|
|
311
314
|
} : {
|
|
315
|
+
agent: this.getAgent(),
|
|
312
316
|
method,
|
|
313
317
|
headers,
|
|
314
318
|
})
|
|
@@ -332,6 +336,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
332
336
|
client_secret: this.options.clientSecret,
|
|
333
337
|
}
|
|
334
338
|
const response = await fetch(this.options.domain + '/oauth/token', {
|
|
339
|
+
agent: this.getAgent(),
|
|
335
340
|
method: 'POST',
|
|
336
341
|
headers: {
|
|
337
342
|
'Content-Type': 'application/json',
|
|
@@ -399,6 +404,16 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
399
404
|
return `${this.config.url}${API_PATH}${API_CONNECTOR_PATH}${API_CONNECTOR_LOGIN_CALLBACK}?${params}`
|
|
400
405
|
}
|
|
401
406
|
|
|
407
|
+
// Force IPv4 when running locally
|
|
408
|
+
private getAgent(): Agent | undefined {
|
|
409
|
+
if (this.config.url.startsWith('http://localhost')) {
|
|
410
|
+
return new Agent({
|
|
411
|
+
family: 4,
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
return undefined
|
|
415
|
+
}
|
|
416
|
+
|
|
402
417
|
/**
|
|
403
418
|
* Get the OAuth URL to redirect the user to
|
|
404
419
|
* The URL should look like
|
|
@@ -471,6 +486,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
471
486
|
}
|
|
472
487
|
|
|
473
488
|
const response = await fetch(this.options.domain + '/oauth/token', {
|
|
489
|
+
agent: this.getAgent(),
|
|
474
490
|
method: 'POST',
|
|
475
491
|
headers: {
|
|
476
492
|
'Content-Type': 'application/json',
|
|
@@ -489,8 +505,8 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
489
505
|
|
|
490
506
|
// Store the token in the session
|
|
491
507
|
this.setSessionToken(session, {
|
|
492
|
-
token,
|
|
493
508
|
...this.getSessionToken(session),
|
|
509
|
+
token,
|
|
494
510
|
})
|
|
495
511
|
|
|
496
512
|
// We need to get the user ID for listWebsites
|
|
@@ -49,17 +49,24 @@ export default function(config: ServerConfig) {
|
|
|
49
49
|
* Method to validate HTTP status codes
|
|
50
50
|
* Returns a suitable error code if it is not valid
|
|
51
51
|
*/
|
|
52
|
-
function validateStatus(status: number, _default = 500): number {
|
|
52
|
+
function validateStatus(status: number | string, _default = 500): number {
|
|
53
53
|
if(!status) {
|
|
54
54
|
console.warn(`Status code is undefined, returning default ${_default}`)
|
|
55
55
|
return _default
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
switch (status) {
|
|
58
|
+
case 'ETIMEDOUT':
|
|
59
|
+
case 'ECONNREFUSED':
|
|
60
|
+
case 'ECONNRESET':
|
|
61
|
+
console.warn(`Connection error ${status}, returning default ${_default}`)
|
|
62
|
+
return _default
|
|
63
|
+
}
|
|
64
|
+
// Make sure it is a number
|
|
65
|
+
const statusNum = parseInt(status.toString())
|
|
59
66
|
// Check the status code
|
|
60
|
-
if (
|
|
67
|
+
if (statusNum >= 100 && statusNum < 600) return statusNum
|
|
61
68
|
// Invalid status code
|
|
62
|
-
console.warn(`Invalid status code ${status}, returning default ${_default}
|
|
69
|
+
console.warn(`Invalid status code ${statusNum} (${status.toString()}, ${status}), returning default ${_default}`)
|
|
63
70
|
return _default
|
|
64
71
|
}
|
|
65
72
|
|
|
@@ -241,7 +248,7 @@ async function routeLogin(req: Request, res: Response) {
|
|
|
241
248
|
async function routeLoginSuccess(req: Request, res: Response) {
|
|
242
249
|
try {
|
|
243
250
|
const query = req.query as ApiConnectorLoginCbkQuery
|
|
244
|
-
if(query.error) throw new Error(query.error)
|
|
251
|
+
if (query.error) throw new Error(query.error)
|
|
245
252
|
|
|
246
253
|
const body = req.body as ApiConnectorLoginCbkBody
|
|
247
254
|
const session = requiredParam(req['session'], 'Session object')
|
|
@@ -265,9 +272,10 @@ async function routeLoginSuccess(req: Request, res: Response) {
|
|
|
265
272
|
// End the auth flow
|
|
266
273
|
res.send(getEndAuthHtml('Logged in', false, connectorId, type, connector.getOptions(body)))
|
|
267
274
|
} catch (error) {
|
|
275
|
+
console.error('Error in the login callback', error, error?.code, error?.httpStatusCode)
|
|
268
276
|
res
|
|
269
277
|
.status(validateStatus(error?.code ?? error?.httpStatusCode, 500))
|
|
270
|
-
.send(getEndAuthHtml(error
|
|
278
|
+
.send(getEndAuthHtml(`${error?.message} ${error?.code ?? error?.httpStatusCode}`, true, req.query.connectorId as ConnectorId, req.query.type as ConnectorType))
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
|