mirror of
https://github.com/wekan/wekan.git
synced 2025-12-20 09:20:12 +01:00
Export Board to Zip file
* Extracts Card covers * Labels * Re-works some CSS & HTML * Produces deployable assets (minus WebFonts)
This commit is contained in:
parent
c22eaa26cd
commit
5ef83ab236
5 changed files with 253 additions and 1 deletions
|
|
@ -363,7 +363,7 @@ template(name="boardMenuPopup")
|
||||||
template(name="exportBoard")
|
template(name="exportBoard")
|
||||||
ul.pop-over-list
|
ul.pop-over-list
|
||||||
li
|
li
|
||||||
a(href="{{exportUrl}}", download="{{exportJsonFilename}}")
|
a.download-json-link(href="{{exportUrl}}", download="{{exportJsonFilename}}")
|
||||||
i.fa.fa-share-alt
|
i.fa.fa-share-alt
|
||||||
| {{_ 'export-board-json'}}
|
| {{_ 'export-board-json'}}
|
||||||
li
|
li
|
||||||
|
|
@ -374,6 +374,10 @@ template(name="exportBoard")
|
||||||
a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
|
a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
|
||||||
i.fa.fa-share-alt
|
i.fa.fa-share-alt
|
||||||
| {{_ 'export-board-tsv'}}
|
| {{_ 'export-board-tsv'}}
|
||||||
|
li
|
||||||
|
a.html-export-board
|
||||||
|
i.fa.fa-archive
|
||||||
|
| {{_ 'export-board-html'}}
|
||||||
|
|
||||||
template(name="labelsWidget")
|
template(name="labelsWidget")
|
||||||
.board-widget.board-widget-labels
|
.board-widget.board-widget-labels
|
||||||
|
|
|
||||||
|
|
@ -463,6 +463,13 @@ BlazeComponent.extendComponent({
|
||||||
},
|
},
|
||||||
}).register('exportBoardPopup');
|
}).register('exportBoardPopup');
|
||||||
|
|
||||||
|
Template.exportBoard.events({
|
||||||
|
'click .html-export-board': async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
await ExportHtml(Popup)();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Template.labelsWidget.events({
|
Template.labelsWidget.events({
|
||||||
'click .js-label': Popup.open('editLabel'),
|
'click .js-label': Popup.open('editLabel'),
|
||||||
'click .js-add-label': Popup.open('createLabel'),
|
'click .js-add-label': Popup.open('createLabel'),
|
||||||
|
|
|
||||||
206
client/lib/exportHTML.js
Normal file
206
client/lib/exportHTML.js
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
const JSZip = require('jszip');
|
||||||
|
|
||||||
|
window.ExportHtml = (Popup) => {
|
||||||
|
const saveAs = function(blob, filename) {
|
||||||
|
let dl = document.createElement('a');
|
||||||
|
dl.href = window.URL.createObjectURL(blob);
|
||||||
|
dl.onclick = event => document.body.removeChild(event.target);
|
||||||
|
dl.style.display = 'none';
|
||||||
|
dl.target = '_blank';
|
||||||
|
dl.download = filename;
|
||||||
|
document.body.appendChild(dl);
|
||||||
|
dl.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const asyncForEach = async function (array, callback) {
|
||||||
|
for (let index = 0; index < array.length; index++) {
|
||||||
|
await callback(array[index], index, array);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageHtmlString = () => {
|
||||||
|
return `<!doctype html>${
|
||||||
|
window.document.querySelector('html').outerHTML
|
||||||
|
}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAnchors = htmlString => {
|
||||||
|
const replaceOpenAnchor = htmlString.replace(new RegExp('<a ', 'gim'), '<span ');
|
||||||
|
return replaceOpenAnchor.replace(new RegExp('<\/a', 'gim'), '</span');
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureSidebarRemoved = () => {
|
||||||
|
document.querySelector('.board-sidebar.sidebar').remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
const addJsonExportToZip = async (zip, boardSlug) => {
|
||||||
|
const downloadJSONLink = document.querySelector('.download-json-link');
|
||||||
|
const downloadJSONURL = downloadJSONLink.href;
|
||||||
|
const response = await fetch(downloadJSONURL);
|
||||||
|
const responseBody = await response.text();
|
||||||
|
zip.file(`data/${boardSlug}.json`, responseBody);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeSidebar = () => {
|
||||||
|
document.querySelector('.board-header-btn.js-toggle-sidebar').click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanBoardHtml = () => {
|
||||||
|
Array.from(document.querySelectorAll('script')).forEach(elem =>
|
||||||
|
elem.remove(),
|
||||||
|
);
|
||||||
|
Array.from(
|
||||||
|
document.querySelectorAll('link:not([rel="stylesheet"])'),
|
||||||
|
).forEach(elem => elem.remove());
|
||||||
|
document.querySelector('#header-quick-access').remove();
|
||||||
|
Array.from(
|
||||||
|
document.querySelectorAll('#header-main-bar .board-header-btns'),
|
||||||
|
).forEach(elem => elem.remove());
|
||||||
|
Array.from(document.querySelectorAll('.list-composer')).forEach(elem =>
|
||||||
|
elem.remove(),
|
||||||
|
);
|
||||||
|
Array.from(
|
||||||
|
document.querySelectorAll(
|
||||||
|
'.list-composer,.js-card-composer, .js-add-card',
|
||||||
|
),
|
||||||
|
).forEach(elem => elem.remove());
|
||||||
|
Array.from(
|
||||||
|
document.querySelectorAll('.js-perfect-scrollbar > div:nth-of-type(n+2)'),
|
||||||
|
).forEach(elem => elem.remove());
|
||||||
|
Array.from(document.querySelectorAll('.js-perfect-scrollbar')).forEach(
|
||||||
|
elem => {
|
||||||
|
elem.style = 'overflow-y: auto !important;';
|
||||||
|
elem.classList.remove('js-perfect-scrollbar');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Array.from(document.querySelectorAll('[href]:not(link)')).forEach(elem =>
|
||||||
|
elem.attributes.removeNamedItem('href'),
|
||||||
|
);
|
||||||
|
Array.from(document.querySelectorAll('[href]')).forEach(elem => {
|
||||||
|
// eslint-disable-next-line no-self-assign
|
||||||
|
elem.href = elem.href;
|
||||||
|
// eslint-disable-next-line no-self-assign
|
||||||
|
elem.src = elem.src;
|
||||||
|
});
|
||||||
|
Array.from(document.querySelectorAll('.is-editable')).forEach(elem => {
|
||||||
|
elem.classList.remove('is-editable')
|
||||||
|
})
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBoardSlug = () => {
|
||||||
|
return window.location.href.split('/').pop();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStylesheetList = () => {
|
||||||
|
return Array.from(
|
||||||
|
document.querySelectorAll('link[href][rel="stylesheet"]'),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadStylesheets = async (stylesheets, zip) => {
|
||||||
|
await asyncForEach(stylesheets, async elem => {
|
||||||
|
const response = await fetch(elem.href);
|
||||||
|
const responseBody = await response.text();
|
||||||
|
|
||||||
|
const finalResponse = responseBody.replace(
|
||||||
|
new RegExp('packages\/[^\/]+\/upstream\/', 'gim'), '../'
|
||||||
|
);
|
||||||
|
|
||||||
|
const filename = elem.href
|
||||||
|
.split('/')
|
||||||
|
.pop()
|
||||||
|
.split('?')
|
||||||
|
.shift();
|
||||||
|
const fileFullPath = `style/${filename}`;
|
||||||
|
zip.file(fileFullPath, finalResponse);
|
||||||
|
elem.href = `../${fileFullPath}`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSrcAttached = () => {
|
||||||
|
return Array.from(document.querySelectorAll('[src]'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadSrcAttached = async (elements, zip, boardSlug) => {
|
||||||
|
await asyncForEach(elements, async elem => {
|
||||||
|
const response = await fetch(elem.src);
|
||||||
|
const responseBody = await response.blob();
|
||||||
|
const filename = elem.src
|
||||||
|
.split('/')
|
||||||
|
.pop()
|
||||||
|
.split('?')
|
||||||
|
.shift();
|
||||||
|
const fileFullPath = `${boardSlug}/${elem.tagName.toLowerCase()}/${filename}`;
|
||||||
|
zip.file(fileFullPath, responseBody);
|
||||||
|
elem.src = `./${elem.tagName.toLowerCase()}/${filename}`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCssUrlSurround = url => {
|
||||||
|
const working = url || "";
|
||||||
|
return working
|
||||||
|
.split("url(")
|
||||||
|
.join("")
|
||||||
|
.split("\")")
|
||||||
|
.join("")
|
||||||
|
.split("\"")
|
||||||
|
.join("")
|
||||||
|
.split("')")
|
||||||
|
.join("")
|
||||||
|
.split("'")
|
||||||
|
.join("")
|
||||||
|
.split(")")
|
||||||
|
.join("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCardCovers = () => {
|
||||||
|
return Array.from(document.querySelectorAll('.minicard-cover'))
|
||||||
|
.filter(elem => elem.style['background-image'])
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadCardCovers = async (elements, zip, boardSlug) => {
|
||||||
|
await asyncForEach(elements, async elem => {
|
||||||
|
const response = await fetch(removeCssUrlSurround(elem.style['background-image']));
|
||||||
|
const responseBody = await response.blob();
|
||||||
|
const filename = removeCssUrlSurround(elem.style['background-image'])
|
||||||
|
.split('/')
|
||||||
|
.pop()
|
||||||
|
.split('?')
|
||||||
|
.shift()
|
||||||
|
.split('#')
|
||||||
|
.shift();
|
||||||
|
const fileFullPath = `${boardSlug}/covers/${filename}`;
|
||||||
|
zip.file(fileFullPath, responseBody);
|
||||||
|
elem.style = "background-image: url('" + `covers/${filename}` + "')";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addBoardHTMLToZip = (boardSlug, zip) => {
|
||||||
|
ensureSidebarRemoved();
|
||||||
|
const htmlOutputPath = `${boardSlug}/index.html`;
|
||||||
|
zip.file(htmlOutputPath, new Blob([
|
||||||
|
removeAnchors(getPageHtmlString())
|
||||||
|
], { type: 'application/html' }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
const zip = new JSZip();
|
||||||
|
const boardSlug = getBoardSlug();
|
||||||
|
|
||||||
|
await addJsonExportToZip(zip, boardSlug);
|
||||||
|
Popup.close();
|
||||||
|
closeSidebar();
|
||||||
|
cleanBoardHtml();
|
||||||
|
|
||||||
|
await downloadStylesheets(getStylesheetList(), zip);
|
||||||
|
await downloadSrcAttached(getSrcAttached(), zip, boardSlug);
|
||||||
|
await downloadCardCovers(getCardCovers(), zip, boardSlug);
|
||||||
|
|
||||||
|
addBoardHTMLToZip(boardSlug, zip);
|
||||||
|
|
||||||
|
const content = await zip.generateAsync({ type: 'blob' });
|
||||||
|
saveAs(content, `${boardSlug}.zip`);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
34
package-lock.json
generated
34
package-lock.json
generated
|
|
@ -2039,6 +2039,11 @@
|
||||||
"minimatch": "^3.0.4"
|
"minimatch": "^3.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
|
||||||
|
},
|
||||||
"import-fresh": {
|
"import-fresh": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
|
||||||
|
|
@ -2460,6 +2465,17 @@
|
||||||
"minimist": "^1.2.5"
|
"minimist": "^1.2.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jszip": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-gZAOYuPl4EhPTXT0GjhI3o+ZAz3su6EhLrKUoAivcKqyqC7laS5JEv4XWZND9BgcDcF83vI85yGbDmDR6UhrIg==",
|
||||||
|
"requires": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"set-immediate-shim": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
|
@ -2514,6 +2530,14 @@
|
||||||
"type-check": "~0.3.2"
|
"type-check": "~0.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"requires": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-7.3.0.tgz",
|
||||||
|
|
@ -3869,6 +3893,11 @@
|
||||||
"path-to-regexp": "~1.2.1"
|
"path-to-regexp": "~1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||||
|
},
|
||||||
"papaparse": {
|
"papaparse": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.2.0.tgz",
|
||||||
|
|
@ -4330,6 +4359,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||||
},
|
},
|
||||||
|
"set-immediate-shim": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
|
||||||
|
},
|
||||||
"set-value": {
|
"set-value": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@
|
||||||
"es6-promise": "^4.2.4",
|
"es6-promise": "^4.2.4",
|
||||||
"flatted": "^2.0.1",
|
"flatted": "^2.0.1",
|
||||||
"gridfs-stream": "^0.5.3",
|
"gridfs-stream": "^0.5.3",
|
||||||
|
"jszip": "^3.4.0",
|
||||||
"ldapjs": "^1.0.2",
|
"ldapjs": "^1.0.2",
|
||||||
"meteor-node-stubs": "^0.4.1",
|
"meteor-node-stubs": "^0.4.1",
|
||||||
"mongodb": "^3.5.7",
|
"mongodb": "^3.5.7",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue