@silexlabs/silex 3.3.1-4 → 3.3.1-6
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/README.md +27 -2
- package/dist/client/index.html +2 -2
- package/dist/client/js/main.js +214 -214
- package/dist/client/js/main.js.LICENSE.txt +1 -14
- package/dist/plugins/server/plugins/server/FtpConnector.js +53 -8
- package/dist/plugins/server/plugins/server/FtpConnector.js.map +1 -1
- package/dist/plugins/server/plugins/server/GitlabConnector.js +21 -66
- package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
- package/dist/plugins/server/server/connectors/FsStorage.js +50 -4
- package/dist/plugins/server/server/connectors/FsStorage.js.map +1 -1
- package/dist/plugins/server/server/utils/websiteDataSerialization.d.ts +20 -0
- package/dist/plugins/server/server/utils/websiteDataSerialization.js +127 -0
- package/dist/plugins/server/server/utils/websiteDataSerialization.js.map +1 -0
- package/dist/server/page.js +14 -0
- package/dist/server/server/connectors/FsStorage.js +40 -4
- package/dist/server/server/utils/websiteDataSerialization.js +85 -0
- package/package.json +12 -15
- package/src/ts/client/grapesjs/PublicationUi.ts +1 -1
- package/src/ts/client/grapesjs/index.ts +9 -11
- package/src/ts/client/grapesjs/storage.ts +6 -9
- package/src/ts/client/index.ts +0 -2
- package/src/ts/plugins/server/FtpConnector.test.ts +2 -1
- package/src/ts/plugins/server/FtpConnector.ts +61 -9
- package/src/ts/plugins/server/GitlabConnector.ts +22 -80
- package/src/ts/server/connectors/FsStorage.test.ts +6 -2
- package/src/ts/server/connectors/FsStorage.ts +60 -5
- package/src/ts/server/utils/websiteDataSerialization.test.ts +59 -0
- package/src/ts/server/utils/websiteDataSerialization.ts +143 -0
- package/src/ts/client/grapesjs/dynamic-plugins.ts +0 -63
- package/src/ts/client/grapesjs/plugin-options.ts +0 -243
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Silex website builder, free/libre no-code tool for makers.
|
|
4
|
+
* Copyright (c) 2023 lexoyo and Silex Labs foundation
|
|
5
|
+
*
|
|
6
|
+
* This program is free software: you can redistribute it and/or modify
|
|
7
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
* the Free Software Foundation, either version 3 of the License, or any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.stringify = stringify;
|
|
20
|
+
exports.split = split;
|
|
21
|
+
exports.merge = merge;
|
|
22
|
+
const page_1 = require("../../page");
|
|
23
|
+
const constants_1 = require("../../constants");
|
|
24
|
+
/**
|
|
25
|
+
* Serialize JSON with stable formatting for git
|
|
26
|
+
* - Uses 2-space indentation for readability
|
|
27
|
+
* - Sorts object keys deterministically to avoid random diffs
|
|
28
|
+
*/
|
|
29
|
+
function stringify(data) {
|
|
30
|
+
return JSON.stringify(data, sortKeys, 2);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Sort object keys for consistent output across serializations
|
|
34
|
+
*/
|
|
35
|
+
function sortKeys(key, value) {
|
|
36
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
37
|
+
const sorted = {};
|
|
38
|
+
Object.keys(value).sort().forEach(k => {
|
|
39
|
+
sorted[k] = value[k];
|
|
40
|
+
});
|
|
41
|
+
return sorted;
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Split website data into multiple files
|
|
47
|
+
* Returns array of files to create/update
|
|
48
|
+
*/
|
|
49
|
+
function split(websiteData) {
|
|
50
|
+
const files = [];
|
|
51
|
+
// Split pages into separate files
|
|
52
|
+
const pages = websiteData.pages?.map((page) => {
|
|
53
|
+
const pageName = getPageName(page);
|
|
54
|
+
const slug = (0, page_1.getPageSlug)(pageName);
|
|
55
|
+
const fileName = `${slug}-${page.id}.json`;
|
|
56
|
+
const filePath = `${constants_1.WEBSITE_PAGES_FOLDER}/${fileName}`;
|
|
57
|
+
files.push({
|
|
58
|
+
path: filePath,
|
|
59
|
+
content: stringify(page)
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
name: pageName,
|
|
63
|
+
id: page.id,
|
|
64
|
+
isFile: true,
|
|
65
|
+
};
|
|
66
|
+
}) || [];
|
|
67
|
+
// Create main website data file without pages content
|
|
68
|
+
const websiteDataWithoutPages = {
|
|
69
|
+
...websiteData,
|
|
70
|
+
fileFormatVersion: '1.0.0',
|
|
71
|
+
pages,
|
|
72
|
+
};
|
|
73
|
+
files.push({
|
|
74
|
+
path: constants_1.WEBSITE_DATA_FILE,
|
|
75
|
+
content: stringify(websiteDataWithoutPages)
|
|
76
|
+
});
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reconstruct website data from main file and page loader function
|
|
81
|
+
* The pageLoader function will be called for each page that needs to be loaded
|
|
82
|
+
*/
|
|
83
|
+
async function merge(websiteDataContent, pageLoader) {
|
|
84
|
+
const websiteDataOb = JSON.parse(websiteDataContent);
|
|
85
|
+
const { fileFormatVersion, ...websiteData } = websiteDataOb;
|
|
86
|
+
// Check file format version
|
|
87
|
+
if (fileFormatVersion && fileFormatVersion !== '1.0.0') {
|
|
88
|
+
console.warn('Website data file format version mismatch', fileFormatVersion, '!=', '1.0.0');
|
|
89
|
+
}
|
|
90
|
+
// Handle legacy format (no split pages) or empty pages
|
|
91
|
+
if (!websiteData.pages || websiteData.pages.length === 0) {
|
|
92
|
+
return websiteData;
|
|
93
|
+
}
|
|
94
|
+
// Check if pages are already embedded (legacy format)
|
|
95
|
+
if (websiteData.pages[0] && !websiteData.pages[0].isFile) {
|
|
96
|
+
return websiteData;
|
|
97
|
+
}
|
|
98
|
+
// Load pages from separate files
|
|
99
|
+
const pages = await Promise.all(websiteData.pages.map(async (pageRef) => {
|
|
100
|
+
if (pageRef.isFile) {
|
|
101
|
+
const pageName = pageRef.name;
|
|
102
|
+
const slug = (0, page_1.getPageSlug)(pageName);
|
|
103
|
+
const fileName = `${slug}-${pageRef.id}.json`;
|
|
104
|
+
const filePath = `${constants_1.WEBSITE_PAGES_FOLDER}/${fileName}`;
|
|
105
|
+
try {
|
|
106
|
+
const pageContent = await pageLoader(filePath);
|
|
107
|
+
return JSON.parse(pageContent);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.warn(`Could not load page file: ${filePath}`, error);
|
|
111
|
+
return pageRef;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return pageRef;
|
|
115
|
+
}));
|
|
116
|
+
return {
|
|
117
|
+
...websiteData,
|
|
118
|
+
pages,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get page name from page object
|
|
123
|
+
*/
|
|
124
|
+
function getPageName(page) {
|
|
125
|
+
return page.getName ? page.getName() : page.name;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=websiteDataSerialization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websiteDataSerialization.js","sourceRoot":"","sources":["../../../../../src/ts/server/utils/websiteDataSerialization.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPageSlug = getPageSlug;
|
|
4
|
+
exports.getPageLink = getPageLink;
|
|
5
|
+
function getPageSlug(pageName) {
|
|
6
|
+
return (pageName || 'index')
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/[^a-z0-9 -]/g, '')
|
|
9
|
+
.replace(/\s+/g, '-')
|
|
10
|
+
.replace(/-+/g, '-');
|
|
11
|
+
}
|
|
12
|
+
function getPageLink(pageName) {
|
|
13
|
+
return `./${getPageSlug(pageName)}.html`;
|
|
14
|
+
}
|
|
@@ -14,6 +14,7 @@ const validation_1 = require("../utils/validation");
|
|
|
14
14
|
const constants_1 = require("../../constants");
|
|
15
15
|
const stream_1 = require("stream");
|
|
16
16
|
const uuid_1 = require("uuid");
|
|
17
|
+
const websiteDataSerialization_1 = require("../utils/websiteDataSerialization");
|
|
17
18
|
if (!globalThis.__dirname) {
|
|
18
19
|
globalThis.__dirname = (0, path_1.dirname)(process.cwd() + '/src/ts/server/connectors/FsStorage.ts');
|
|
19
20
|
console.info('Redefining __dirname', globalThis.__dirname);
|
|
@@ -96,7 +97,7 @@ class FsStorage {
|
|
|
96
97
|
}
|
|
97
98
|
async setWebsiteMeta(session, id, data) {
|
|
98
99
|
const websiteId = (0, validation_1.requiredParam)(id, 'website id');
|
|
99
|
-
const content =
|
|
100
|
+
const content = (0, websiteDataSerialization_1.stringify)(data);
|
|
100
101
|
const path = (0, path_1.join)(this.options.path, id, constants_1.WEBSITE_META_DATA_FILE);
|
|
101
102
|
await promises_1.default.writeFile(path, content);
|
|
102
103
|
}
|
|
@@ -126,12 +127,47 @@ class FsStorage {
|
|
|
126
127
|
const id = (0, validation_1.requiredParam)(websiteId, 'website id');
|
|
127
128
|
const path = (0, path_1.join)(this.options.path, id, constants_1.WEBSITE_DATA_FILE);
|
|
128
129
|
const content = await promises_1.default.readFile(path);
|
|
129
|
-
|
|
130
|
+
const websiteDataContent = content.toString();
|
|
131
|
+
const parsedData = JSON.parse(websiteDataContent);
|
|
132
|
+
if (parsedData.fileFormatVersion === '1.0.0') {
|
|
133
|
+
const pageLoader = async (pagePath) => {
|
|
134
|
+
const fullPath = (0, path_1.join)(this.options.path, id, pagePath);
|
|
135
|
+
const pageContent = await promises_1.default.readFile(fullPath);
|
|
136
|
+
return pageContent.toString();
|
|
137
|
+
};
|
|
138
|
+
return await (0, websiteDataSerialization_1.merge)(websiteDataContent, pageLoader);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
return parsedData;
|
|
142
|
+
}
|
|
130
143
|
}
|
|
131
144
|
async updateWebsite(session, websiteId, data) {
|
|
132
145
|
const id = (0, validation_1.requiredParam)(websiteId, 'website id');
|
|
133
|
-
const
|
|
134
|
-
|
|
146
|
+
const websitePath = (0, path_1.join)(this.options.path, id);
|
|
147
|
+
const filesToWrite = (0, websiteDataSerialization_1.split)(data);
|
|
148
|
+
await promises_1.default.mkdir(websitePath, { recursive: true });
|
|
149
|
+
const hasPageFiles = filesToWrite.some(f => f.path.startsWith(constants_1.WEBSITE_PAGES_FOLDER));
|
|
150
|
+
if (hasPageFiles) {
|
|
151
|
+
await promises_1.default.mkdir((0, path_1.join)(websitePath, constants_1.WEBSITE_PAGES_FOLDER), { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
const pagesPath = (0, path_1.join)(websitePath, constants_1.WEBSITE_PAGES_FOLDER);
|
|
154
|
+
try {
|
|
155
|
+
const existingPageFiles = await promises_1.default.readdir(pagesPath);
|
|
156
|
+
const newPageFiles = new Set(filesToWrite
|
|
157
|
+
.filter(f => f.path.startsWith(constants_1.WEBSITE_PAGES_FOLDER))
|
|
158
|
+
.map(f => f.path.replace(`${constants_1.WEBSITE_PAGES_FOLDER}/`, '')));
|
|
159
|
+
for (const existingFile of existingPageFiles) {
|
|
160
|
+
if (existingFile.endsWith('.json') && !newPageFiles.has(existingFile)) {
|
|
161
|
+
await promises_1.default.unlink((0, path_1.join)(pagesPath, existingFile));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
}
|
|
167
|
+
for (const file of filesToWrite) {
|
|
168
|
+
const filePath = (0, path_1.join)(websitePath, file.path);
|
|
169
|
+
await promises_1.default.writeFile(filePath, file.content);
|
|
170
|
+
}
|
|
135
171
|
}
|
|
136
172
|
async deleteWebsite(session, websiteId) {
|
|
137
173
|
const id = (0, validation_1.requiredParam)(websiteId, 'website id');
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stringify = stringify;
|
|
4
|
+
exports.split = split;
|
|
5
|
+
exports.merge = merge;
|
|
6
|
+
const page_1 = require("../../page");
|
|
7
|
+
const constants_1 = require("../../constants");
|
|
8
|
+
function stringify(data) {
|
|
9
|
+
return JSON.stringify(data, sortKeys, 2);
|
|
10
|
+
}
|
|
11
|
+
function sortKeys(key, value) {
|
|
12
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
13
|
+
const sorted = {};
|
|
14
|
+
Object.keys(value).sort().forEach(k => {
|
|
15
|
+
sorted[k] = value[k];
|
|
16
|
+
});
|
|
17
|
+
return sorted;
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function split(websiteData) {
|
|
22
|
+
const files = [];
|
|
23
|
+
const pages = websiteData.pages?.map((page) => {
|
|
24
|
+
const pageName = getPageName(page);
|
|
25
|
+
const slug = (0, page_1.getPageSlug)(pageName);
|
|
26
|
+
const fileName = `${slug}-${page.id}.json`;
|
|
27
|
+
const filePath = `${constants_1.WEBSITE_PAGES_FOLDER}/${fileName}`;
|
|
28
|
+
files.push({
|
|
29
|
+
path: filePath,
|
|
30
|
+
content: stringify(page)
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
name: pageName,
|
|
34
|
+
id: page.id,
|
|
35
|
+
isFile: true,
|
|
36
|
+
};
|
|
37
|
+
}) || [];
|
|
38
|
+
const websiteDataWithoutPages = {
|
|
39
|
+
...websiteData,
|
|
40
|
+
fileFormatVersion: '1.0.0',
|
|
41
|
+
pages,
|
|
42
|
+
};
|
|
43
|
+
files.push({
|
|
44
|
+
path: constants_1.WEBSITE_DATA_FILE,
|
|
45
|
+
content: stringify(websiteDataWithoutPages)
|
|
46
|
+
});
|
|
47
|
+
return files;
|
|
48
|
+
}
|
|
49
|
+
async function merge(websiteDataContent, pageLoader) {
|
|
50
|
+
const websiteDataOb = JSON.parse(websiteDataContent);
|
|
51
|
+
const { fileFormatVersion, ...websiteData } = websiteDataOb;
|
|
52
|
+
if (fileFormatVersion && fileFormatVersion !== '1.0.0') {
|
|
53
|
+
console.warn('Website data file format version mismatch', fileFormatVersion, '!=', '1.0.0');
|
|
54
|
+
}
|
|
55
|
+
if (!websiteData.pages || websiteData.pages.length === 0) {
|
|
56
|
+
return websiteData;
|
|
57
|
+
}
|
|
58
|
+
if (websiteData.pages[0] && !websiteData.pages[0].isFile) {
|
|
59
|
+
return websiteData;
|
|
60
|
+
}
|
|
61
|
+
const pages = await Promise.all(websiteData.pages.map(async (pageRef) => {
|
|
62
|
+
if (pageRef.isFile) {
|
|
63
|
+
const pageName = pageRef.name;
|
|
64
|
+
const slug = (0, page_1.getPageSlug)(pageName);
|
|
65
|
+
const fileName = `${slug}-${pageRef.id}.json`;
|
|
66
|
+
const filePath = `${constants_1.WEBSITE_PAGES_FOLDER}/${fileName}`;
|
|
67
|
+
try {
|
|
68
|
+
const pageContent = await pageLoader(filePath);
|
|
69
|
+
return JSON.parse(pageContent);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.warn(`Could not load page file: ${filePath}`, error);
|
|
73
|
+
return pageRef;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return pageRef;
|
|
77
|
+
}));
|
|
78
|
+
return {
|
|
79
|
+
...websiteData,
|
|
80
|
+
pages,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function getPageName(page) {
|
|
84
|
+
return page.getName ? page.getName() : page.name;
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silexlabs/silex",
|
|
3
3
|
"description": "Free and easy website builder for everyone.",
|
|
4
|
-
"version": "3.3.1-
|
|
4
|
+
"version": "3.3.1-6",
|
|
5
5
|
"version:backwardcompat": "3.0.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alex Hoyau",
|
|
@@ -81,9 +81,6 @@
|
|
|
81
81
|
"start": "node dist/server/server/",
|
|
82
82
|
"start:debug": "cross-env SILEX_DEBUG=true nodemon --watch dist/server dist/server/server/",
|
|
83
83
|
"start:preview": "http-server silex/hosting -o",
|
|
84
|
-
"prepublishOnly": "$npm_execpath run lint && $npm_execpath test && $npm_execpath run build",
|
|
85
|
-
"preversion": "$npm_execpath run prepublishOnly",
|
|
86
|
-
"prepack": "$npm_execpath run prepublishOnly",
|
|
87
84
|
"build": "run-s build:css:release build:html build:client:release build:server:release build:plugins:client build:plugins:server",
|
|
88
85
|
"webpack": "webpack --config webpack.config.js",
|
|
89
86
|
"build:css:release": "sass --load-path ../../node_modules --load-path node_modules --no-source-map src/css/index.scss dist/client/css/admin.css",
|
|
@@ -105,16 +102,16 @@
|
|
|
105
102
|
"dependencies": {
|
|
106
103
|
"@fontsource/ubuntu": "^5.0.13",
|
|
107
104
|
"@fortawesome/fontawesome-free": "6.6.0",
|
|
108
|
-
"@silexlabs/grapesjs-advanced-selector": "1.2.1-
|
|
109
|
-
"@silexlabs/grapesjs-filter-styles": "1.1.
|
|
110
|
-
"@silexlabs/grapesjs-fonts": "1.1.1-
|
|
111
|
-
"@silexlabs/grapesjs-keymaps-dialog": "1.1.
|
|
112
|
-
"@silexlabs/grapesjs-loading": "1.1.1-
|
|
113
|
-
"@silexlabs/grapesjs-notifications": "0.1.
|
|
114
|
-
"@silexlabs/grapesjs-storage-rate-limit": "1.1.
|
|
115
|
-
"@silexlabs/grapesjs-symbols": "1.2.1-
|
|
116
|
-
"@silexlabs/grapesjs-ui-suggest-classes": "1.1.
|
|
117
|
-
"@silexlabs/silex-plugins": "1.1.
|
|
105
|
+
"@silexlabs/grapesjs-advanced-selector": "1.2.1-6",
|
|
106
|
+
"@silexlabs/grapesjs-filter-styles": "1.1.1-1",
|
|
107
|
+
"@silexlabs/grapesjs-fonts": "1.1.1-2",
|
|
108
|
+
"@silexlabs/grapesjs-keymaps-dialog": "1.1.1-1",
|
|
109
|
+
"@silexlabs/grapesjs-loading": "1.1.1-4",
|
|
110
|
+
"@silexlabs/grapesjs-notifications": "0.1.1-1",
|
|
111
|
+
"@silexlabs/grapesjs-storage-rate-limit": "1.1.1-1",
|
|
112
|
+
"@silexlabs/grapesjs-symbols": "1.2.1-2",
|
|
113
|
+
"@silexlabs/grapesjs-ui-suggest-classes": "1.1.1-1",
|
|
114
|
+
"@silexlabs/silex-plugins": "1.1.1-1",
|
|
118
115
|
"@types/archiver": "^6.0.2",
|
|
119
116
|
"adm-zip": "0.5.14",
|
|
120
117
|
"archiver": "^7.0.1",
|
|
@@ -142,7 +139,7 @@
|
|
|
142
139
|
"html-minifier": "^4.0.0",
|
|
143
140
|
"lit-html": "3.2.1",
|
|
144
141
|
"mkdirp": "3.0.1",
|
|
145
|
-
"node_modules-path": "2.1.0",
|
|
142
|
+
"node_modules-path": "2.1.1-0",
|
|
146
143
|
"node-fetch": "2.6.9",
|
|
147
144
|
"normalize.css": "8.0.1",
|
|
148
145
|
"npm-run-all": "^4.1.5",
|
|
@@ -129,7 +129,7 @@ export class PublicationUi {
|
|
|
129
129
|
// Create the dialog element
|
|
130
130
|
const el = document.createElement('div')
|
|
131
131
|
el.id = 'publish-dialog'
|
|
132
|
-
el.className = 'silex-dialog-inline silex-dialog gjs-two-color'
|
|
132
|
+
el.className = 'silex-dialog-inline silex-dialog gjs-two-color silex-dialog-hide'
|
|
133
133
|
document.body.append(el)
|
|
134
134
|
// Let the other buttons be added, we want to be last
|
|
135
135
|
setTimeout(() => {
|
|
@@ -49,7 +49,7 @@ import rateLimitPlugin from '@silexlabs/grapesjs-storage-rate-limit'
|
|
|
49
49
|
import borderPugin from 'grapesjs-style-border'
|
|
50
50
|
import backgroundPlugin from 'grapesjs-style-bg'
|
|
51
51
|
import resizePanelPlugin from './resize-panel'
|
|
52
|
-
import notificationsPlugin, {
|
|
52
|
+
import notificationsPlugin, { NOTIFICATION_CHANGED } from '@silexlabs/grapesjs-notifications'
|
|
53
53
|
import keymapsDialogPlugin, { cmdKeymapsDialog } from '@silexlabs/grapesjs-keymaps-dialog'
|
|
54
54
|
|
|
55
55
|
import { pagePanelPlugin, cmdTogglePages, cmdAddPage } from './page-panel'
|
|
@@ -428,16 +428,14 @@ export async function initEditor(config: EditorConfig) {
|
|
|
428
428
|
// Add the notifications container
|
|
429
429
|
document.body.querySelector('.notifications-container')?.appendChild(notificationContainer)
|
|
430
430
|
// Mark the button as dirty when there are notifications
|
|
431
|
-
|
|
432
|
-
editor.on(
|
|
433
|
-
'notifications
|
|
434
|
-
()
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
)
|
|
431
|
+
let notificationCount = 0
|
|
432
|
+
editor.on(NOTIFICATION_CHANGED, (notifications) => {
|
|
433
|
+
const notificationButton = editor.Panels.getPanel(PROJECT_BAR_PANEL_ID).view?.el.querySelector('.notifications-btn')
|
|
434
|
+
notificationCount = Array.isArray(notifications) ? notifications.length : 0
|
|
435
|
+
notificationCount > 0
|
|
436
|
+
? notificationButton?.classList.add('project-bar__dirty')
|
|
437
|
+
: notificationButton?.classList.remove('project-bar__dirty')
|
|
438
|
+
})
|
|
441
439
|
|
|
442
440
|
// GrapesJs editor is ready
|
|
443
441
|
resolve(editor)
|
|
@@ -86,13 +86,13 @@ export const storagePlugin = (editor: PublishableEditor) => {
|
|
|
86
86
|
// Let grapesjs create the pages in the frontend
|
|
87
87
|
return data
|
|
88
88
|
} else {
|
|
89
|
+
editor.runCommand(cmdPauseAutoSave)
|
|
89
90
|
const { pages, ...rest } = data
|
|
90
91
|
// Load any additional project data, e.g. symbols, but not the ones we progressive load
|
|
91
92
|
loader && render(getLoaderHtml('Loading styles, assets and symbols', 0, pages.length + 1), loader)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Load the rest of the data
|
|
93
|
+
// Add to the project, everything but pages
|
|
94
|
+
editor.loadProjectData(rest)
|
|
95
|
+
// Add the pages to the project
|
|
96
96
|
await progressiveLoadPages(editor, pages)
|
|
97
97
|
await nextFrame()
|
|
98
98
|
// Select the first page
|
|
@@ -100,18 +100,15 @@ export const storagePlugin = (editor: PublishableEditor) => {
|
|
|
100
100
|
if (firstPage) editor.Pages.select(firstPage)
|
|
101
101
|
loader && render(getLoaderHtml('Starting', 1, 1), loader)
|
|
102
102
|
await nextFrame()
|
|
103
|
-
editor.
|
|
104
|
-
|
|
105
|
-
return {}
|
|
103
|
+
editor.once('canvas:frame:load', () => editor.stopCommand(cmdPauseAutoSave))
|
|
104
|
+
return data
|
|
106
105
|
}
|
|
107
106
|
} catch (err) {
|
|
108
107
|
editor.UndoManager.clear()
|
|
109
108
|
if (err.httpStatusCode === 401) {
|
|
110
109
|
editor.once(eventLoggedIn, async () => {
|
|
111
110
|
try {
|
|
112
|
-
editor.runCommand(cmdPauseAutoSave)
|
|
113
111
|
await editor.Storage.load(options)
|
|
114
|
-
editor.once('canvas:frame:load', () => editor.stopCommand(cmdPauseAutoSave))
|
|
115
112
|
} catch (err) {
|
|
116
113
|
console.error('connectorPlugin load error', err)
|
|
117
114
|
throw err
|
package/src/ts/client/index.ts
CHANGED
|
@@ -86,8 +86,6 @@ export async function start(options = {}): Promise<void> {
|
|
|
86
86
|
|
|
87
87
|
// Load the site
|
|
88
88
|
try {
|
|
89
|
-
editor.runCommand(cmdPauseAutoSave)
|
|
90
|
-
editor.once('canvas:frame:load storage:after:load', () => editor.stopCommand(cmdPauseAutoSave))
|
|
91
89
|
await editor.load(null)
|
|
92
90
|
} catch(e) {
|
|
93
91
|
if(e.httpStatusCode === 401) {
|
|
@@ -2,6 +2,7 @@ import { ServerConfig } from '../../server/config'
|
|
|
2
2
|
import { expect, jest, beforeEach, afterEach, it, describe } from '@jest/globals'
|
|
3
3
|
import { ConnectorType, WebsiteData, WebsiteId } from '../../types'
|
|
4
4
|
import { Readable } from 'stream'
|
|
5
|
+
import { WEBSITE_DATA_FILE } from '../../constants'
|
|
5
6
|
|
|
6
7
|
const mocks = {
|
|
7
8
|
access: jest.fn(),
|
|
@@ -64,7 +65,7 @@ describe('FtpConnector website', () => {
|
|
|
64
65
|
await connector.updateWebsite(dummySession, dummyId, dummyWebsite)
|
|
65
66
|
expect(mocks.uploadFrom).toHaveBeenCalledTimes(1)
|
|
66
67
|
expect(mocks.uploadFrom.mock.calls[0][0]).toBeInstanceOf(Readable)
|
|
67
|
-
expect(mocks.uploadFrom.mock.calls[0][1]).toBe(`${storageRootPath}/${dummyId}
|
|
68
|
+
expect(mocks.uploadFrom.mock.calls[0][1]).toBe(`${storageRootPath}/${dummyId}/${WEBSITE_DATA_FILE}`)
|
|
68
69
|
})
|
|
69
70
|
})
|
|
70
71
|
|
|
@@ -19,7 +19,7 @@ import { Client } from 'basic-ftp'
|
|
|
19
19
|
import { Readable } from 'stream'
|
|
20
20
|
import { ConnectorFile, ConnectorFileContent, HostingConnector, StatusCallback, StorageConnector, contentToReadable, contentToString, toConnectorData, toConnectorEnum} from '../../server/connectors/connectors'
|
|
21
21
|
import { requiredParam } from '../../server/utils/validation'
|
|
22
|
-
import { WEBSITE_DATA_FILE, WEBSITE_META_DATA_FILE } from '../../constants'
|
|
22
|
+
import { WEBSITE_DATA_FILE, WEBSITE_META_DATA_FILE, WEBSITE_PAGES_FOLDER } from '../../constants'
|
|
23
23
|
import { ConnectorType, ConnectorUser, WebsiteMeta, FileMeta, JobData, JobStatus, WebsiteId, PublicationJobData, WebsiteMetaFileContent, defaultWebsiteData, WebsiteData, ConnectorOptions } from '../../types'
|
|
24
24
|
import { ServerConfig } from '../../server/config'
|
|
25
25
|
import { join } from 'path'
|
|
@@ -28,6 +28,7 @@ import { v4 as uuid } from 'uuid'
|
|
|
28
28
|
import { JobManager } from '../../server/jobs'
|
|
29
29
|
import { mkdtemp, rm, rmdir } from 'fs/promises'
|
|
30
30
|
import { createReadStream } from 'fs'
|
|
31
|
+
import { stringify, split, merge } from '../../server/utils/websiteDataSerialization'
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* @fileoverview FTP connector for Silex
|
|
@@ -399,7 +400,7 @@ export default class FtpConnector implements StorageConnector<FtpSession> {
|
|
|
399
400
|
const websiteId = requiredParam<WebsiteId>(id, 'website id')
|
|
400
401
|
const path = join(this.rootPath(session), websiteId, WEBSITE_META_DATA_FILE)
|
|
401
402
|
const ftp = await this.getClient(this.sessionData(session))
|
|
402
|
-
await this.write(ftp, path,
|
|
403
|
+
await this.write(ftp, path, stringify(data))
|
|
403
404
|
this.closeClient(ftp)
|
|
404
405
|
}
|
|
405
406
|
|
|
@@ -442,10 +443,10 @@ export default class FtpConnector implements StorageConnector<FtpSession> {
|
|
|
442
443
|
await this.mkdir(ftp, assetsPath)
|
|
443
444
|
// create website data file
|
|
444
445
|
const websiteDataPath = join(this.rootPath(session), id, WEBSITE_DATA_FILE)
|
|
445
|
-
await this.write(ftp, websiteDataPath,
|
|
446
|
+
await this.write(ftp, websiteDataPath, stringify(defaultWebsiteData))
|
|
446
447
|
// create website meta data file
|
|
447
448
|
const websiteMetaDataPath = join(this.rootPath(session), id, WEBSITE_META_DATA_FILE)
|
|
448
|
-
await this.write(ftp, websiteMetaDataPath,
|
|
449
|
+
await this.write(ftp, websiteMetaDataPath, stringify({}))
|
|
449
450
|
// Clean up
|
|
450
451
|
this.closeClient(ftp)
|
|
451
452
|
// All good
|
|
@@ -469,16 +470,67 @@ export default class FtpConnector implements StorageConnector<FtpSession> {
|
|
|
469
470
|
const storageRootPath = this.rootPath(session)
|
|
470
471
|
const ftp = await this.getClient(this.sessionData(session))
|
|
471
472
|
const websiteDataPath = join(storageRootPath, websiteId, WEBSITE_DATA_FILE)
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const websiteDataStream = await this.read(ftp, websiteDataPath)
|
|
476
|
+
const websiteDataContent = await contentToString(websiteDataStream)
|
|
477
|
+
|
|
478
|
+
// Check if this is the new split format by trying to parse
|
|
479
|
+
const parsedData = JSON.parse(websiteDataContent)
|
|
480
|
+
if (parsedData.fileFormatVersion === '1.0.0') {
|
|
481
|
+
// Use the merge function to reconstruct website data
|
|
482
|
+
const pageLoader = async (pagePath: string): Promise<string> => {
|
|
483
|
+
const fullPath = join(storageRootPath, websiteId, pagePath)
|
|
484
|
+
const pageStream = await this.read(ftp, fullPath)
|
|
485
|
+
return await contentToString(pageStream)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const result = await merge(websiteDataContent, pageLoader)
|
|
489
|
+
this.closeClient(ftp)
|
|
490
|
+
return result
|
|
491
|
+
} else {
|
|
492
|
+
// Legacy format, return as is
|
|
493
|
+
this.closeClient(ftp)
|
|
494
|
+
return parsedData
|
|
495
|
+
}
|
|
496
|
+
} catch (error) {
|
|
497
|
+
this.closeClient(ftp)
|
|
498
|
+
throw error
|
|
499
|
+
}
|
|
475
500
|
}
|
|
476
501
|
|
|
477
502
|
async updateWebsite(session: FtpSession, websiteId: string, data: WebsiteData): Promise<void> {
|
|
478
503
|
const storageRootPath = this.rootPath(session)
|
|
479
504
|
const ftp = await this.getClient(this.sessionData(session))
|
|
480
|
-
|
|
481
|
-
|
|
505
|
+
|
|
506
|
+
// Use the split function to create separate files for pages
|
|
507
|
+
const filesToWrite = split(data)
|
|
508
|
+
|
|
509
|
+
// **
|
|
510
|
+
// Delete pages that are not in the new website data
|
|
511
|
+
const pagesPath = join(storageRootPath, websiteId, WEBSITE_PAGES_FOLDER)
|
|
512
|
+
try {
|
|
513
|
+
const existingPageFiles = await this.readdir(ftp, pagesPath)
|
|
514
|
+
const newPageFiles = new Set(
|
|
515
|
+
filesToWrite
|
|
516
|
+
.filter(f => f.path.startsWith(WEBSITE_PAGES_FOLDER))
|
|
517
|
+
.map(f => f.path.replace(`${WEBSITE_PAGES_FOLDER}/`, ''))
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
for (const existingFile of existingPageFiles) {
|
|
521
|
+
if (existingFile.name.endsWith('.json') && !newPageFiles.has(existingFile.name)) {
|
|
522
|
+
await this.unlink(ftp, join(pagesPath, existingFile.name))
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
} catch (error) {
|
|
526
|
+
// Ignore error if pages directory doesn't exist yet
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
for (const file of filesToWrite) {
|
|
530
|
+
const filePath = join(storageRootPath, websiteId, file.path)
|
|
531
|
+
await this.write(ftp, filePath, file.content)
|
|
532
|
+
}
|
|
533
|
+
|
|
482
534
|
this.closeClient(ftp)
|
|
483
535
|
}
|
|
484
536
|
|