Rewrite HTML export to support all node types (#3)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details

master
Garrett Mills 4 years ago
parent 1825c11569
commit b5b2a7cd85
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246

@ -4,6 +4,28 @@
* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-simple-sidebar/blob/master/LICENSE)
*/
@font-face {
font-family: 'Raleway';
src: url('../Raleway-Regular.ttf');
font-weight: normal;
}
@font-face {
font-family: 'Raleway';
src: url('../Raleway-Light.ttf');
font-weight: 100;
}
@font-face {
font-family: 'Raleway';
src: url('../Raleway-Bold.ttf');
font-weight: bold;
}
body {
font-family: 'Raleway', sans-serif;
}
#wrapper {
overflow-x: hidden;
}
@ -53,10 +75,13 @@
margin-top: 20px;
margin-bottom: 20px;
height: 600px;
padding: 3px;
border: 2px solid darkgrey;
border-radius: 5px;
}
.file-ref {
border: 1px solid darkgray;
border: 2px solid darkgray;
margin: 20px 0;
border-radius: 5px;
padding: 10px;
@ -84,3 +109,16 @@
border-radius: 4px;
text-decoration: none !important;
}
.database-container {
width: 100%;
border: 2px solid darkgrey;
border-radius: 4px;
margin: 15px 5px;
padding: 5px;
}
.database-container .database-ref {
height: 600px;
width: 100%;
}

@ -0,0 +1,19 @@
(function() {
// specify the columns
const columnDefs = {{ COLUMN_DEFS }};
// specify the data
var rowData = {{ ROW_DATA }};
// let the grid know which columns and what data to use
var gridOptions = {
columnDefs: columnDefs,
rowData: rowData
};
// lookup the container we want the Grid to use
const eGridDiv = document.querySelector('#{{ GRID_ID }}');
// create the grid passing in the div to use together with the columns & data we want to use
new agGrid.Grid(eGridDiv, gridOptions);
})();

@ -0,0 +1,17 @@
@font-face {
font-family: 'Raleway';
src: url('./Raleway-Regular.ttf');
font-weight: normal;
}
@font-face {
font-family: 'Raleway';
src: url('./Raleway-Light.ttf');
font-weight: 100;
}
@font-face {
font-family: 'Raleway';
src: url('./Raleway-Bold.ttf');
font-weight: bold;
}

@ -8,8 +8,12 @@
<title>{{ PAGE_TITLE }} | {{ GROUP_TITLE }}</title>
<script src="vendor/aggrid/ag-grid-community.min.noStyle.js"></script>
<!-- Bootstrap core CSS -->
<link href="vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="vendor/aggrid/ag-grid.css">
<link rel="stylesheet" href="vendor/aggrid/ag-theme-balham.css">
<!-- Custom styles for this template -->
<link href="css/simple-sidebar.css" rel="stylesheet">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -2,10 +2,13 @@ const { Controller } = require('libflitter')
const ncp = require('ncp').ncp
const fs = require('fs').promises
const path = require('path')
const md = require('markdown').markdown
const rimraf = require('rimraf')
const uuid = require('uuid/v4')
class ExportController extends Controller {
static get services() {
return [...super.services, 'models', 'utility']
return [...super.services, 'models', 'utility', 'upload']
}
async get_export_list(req, res, next) {
@ -17,13 +20,54 @@ class ExportController extends Controller {
return res.api(exports)
}
async download_export(req, res, next) {
const Export = this.models.get('api:Export')
const exported = await Export.findOne({ UUID: req.params.ExportId, user_id: req.user.id })
if ( !exported ) {
return res.status(404)
.message('No export with that UUID found.')
.api()
}
const File = this.models.get('upload::File')
const file = await File.findOne({_id: File.ObjectId(exported.file_id)})
if ( !file ) return res.status(404).message('This export file has been deleted.').api({})
return file.send(res)
}
async export_subtree(req, res, next) {
const Export = this.models.get('api:Export')
const format = req.form.format
const page = req.form.page
if ( format === 'html' ) {
const generated_export = await this.export_subtree_as_html(page, req.user)
// Store the generated archive
const uploader = this.upload.provider()
const file = await uploader.store({
temp_path: generated_export,
original_name: `export-${uuid()}.tar.gz`,
mime_type: 'application/gzip',
tag: 'generated_export',
})
const exp = new Export({
user_id: req.user.id,
format,
subtree: true,
file_id: file.id,
PageId: page.UUID,
})
await exp.save()
return res.api(exp)
}
return res.status(400)
.message('Invalid export format!')
.api()
}
async export_subtree_as_html(page, user) {
@ -75,7 +119,30 @@ class ExportController extends Controller {
await fs.writeFile(path.resolve(work_dir, item.file_name), item_template)
}
return work_dir
// Write the main page redirect
const redir_html = `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; url=${flat_tree[0].file_name}">
</head>
</html>
`
await fs.writeFile(path.resolve(work_dir, 'index.html'), redir_html)
// Create the archive
const tar = require('tar')
const archive_path = path.resolve(work_dir, '..', `export-${uuid()}.tar.gz`)
await tar.c({
gzip: true,
file: archive_path,
cwd: path.resolve(work_dir, '..'),
}, [path.basename(work_dir)])
await new Promise(res => {
rimraf(work_dir, res)
})
return archive_path
}
async copy_template(from, to) {
@ -89,28 +156,43 @@ class ExportController extends Controller {
async scratch_dir() {
const tmp = require('tmp')
const gen_id = uuid()
return new Promise((res, rej) => {
tmp.dir((err, path) => {
tmp.dir((err, tmp_path) => {
if ( err ) rej(err)
else res(path)
else {
fs.mkdir(path.resolve(tmp_path, `export`)).then(() => {
res(path.resolve(tmp_path, `export`))
})
}
})
})
}
/**
* Given a page and a template working directory, render that page as HTML.
* @param {Page} page
* @param {string} work_dir
* @return {Promise<string>}
*/
async page_as_html(page, work_dir) {
const Codium = this.models.get('api:Codium')
const FileGroup = this.models.get('api:FileGroup')
const File = this.models.get('upload::File')
const Database = this.models.get('api:db:Database')
const DBEntry = this.models.get('api:db:DBEntry')
let html = ''
const nodes = await page.nodes
for ( const node of nodes ) {
// ATM, there are 4 node types: norm, database_ref, files_ref, and code_ref
// ATM, there are 5 node types: norm, markdown, database_ref, files_ref, and code_ref
if ( node.Value.Mode === 'norm' ) {
html += node.Value.Value
} else if ( node.Value.Mode === 'markdown' ) {
html += md.toHTML(node.Value.Value)
} else if ( node.Type === 'code_ref' ) {
const code = await Codium.findOne({ UUID: node.Value.Value })
if ( code ) {
@ -132,12 +214,13 @@ class ExportController extends Controller {
const file = await File.findById(file_id)
if ( file ) {
const store_path = file.provider().filepath(file.store_id)
await this.copy_template(store_path, path.resolve(work_dir, `file-${file.upload_name}`))
const ext = file.original_name.split('.').reverse()[0]
await this.copy_template(store_path, path.resolve(work_dir, `file-${file.upload_name}.${ext}`))
file_htmls.push(`
<div class="file">
<div class="file-name">${file.original_name}</div>
<a href="file-${file.upload_name}" class="dl-link" target="_blank"></a>
<a href="file-${file.upload_name}.${ext}" class="dl-link" target="_blank"></a>
</div>
`)
}
@ -148,6 +231,38 @@ class ExportController extends Controller {
${file_htmls.join('\n')}
</div>`
}
} else if ( node.Type === 'database_ref' ) {
const grid_id = `dbase-${node.Value.Value}`
const db = await Database.findOne({ UUID: node.Value.Value })
if ( db ) {
html += `
<div id="${grid_id}-container" class="database-container">
<h4>${db.Name}</h4>
<div id="${grid_id}" class="ag-theme-balham database-ref"></div>
</div>
<script src="${grid_id}.js"></script>
`
// generate the column defs
const cols = await db.get_columns()
const col_defs = cols.map(col => {
return {
field: col.field,
headerName: col.headerName,
}
})
// generate the data rows
const entries = await DBEntry.find({ DatabaseId: db.UUID })
const rows = entries.map(entry => entry.RowData)
let dbTemplateContents = await fs.readFile(path.resolve(work_dir, 'database.js'), 'utf-8')
dbTemplateContents = dbTemplateContents.replace(/{{\s?COLUMN_DEFS\s?}}/g, JSON.stringify(col_defs))
dbTemplateContents = dbTemplateContents.replace(/{{\s?ROW_DATA\s?}}/g, JSON.stringify(rows))
dbTemplateContents = dbTemplateContents.replace(/{{\s?GRID_ID\s?}}/g, grid_id)
await fs.writeFile(path.resolve(work_dir, `${grid_id}.js`), dbTemplateContents)
}
}
}

@ -1,4 +1,5 @@
const { Model } = require('flitter-orm')
const uuid = require('uuid/v4')
class ExportModel extends Model {
static get schema() {
@ -10,6 +11,7 @@ class ExportModel extends Model {
file_id: String,
PageId: String,
Active: { type: Boolean, default: true },
UUID: { type: String, default: uuid },
}
}
}

@ -13,6 +13,7 @@ const index = {
get: {
'/': ['controller::api:v1:Export.get_export_list'],
'/:ExportId/download': ['controller::api:v1:Export.download_export'],
// '/page/:PageId/info': [
// ['middleware::api:RequiredFields', { form: 'sharing.page' }],
// ['middleware::api:PageRoute', {level: 'manage'}],

@ -26,7 +26,10 @@
"flitter-upload": "^0.8.1",
"jsonwebtoken": "^8.5.1",
"libflitter": "^0.54.1",
"markdown": "^0.5.0",
"ncp": "^2.0.0",
"rimraf": "^3.0.2",
"tar": "^6.0.5",
"tmp": "^0.2.1"
}
}

@ -880,6 +880,11 @@ chownr@^1.1.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
clean-css@^4.1.11:
version "4.2.1"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17"
@ -1724,6 +1729,13 @@ fs-minipass@^1.2.5:
dependencies:
minipass "^2.2.1"
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
dependencies:
minipass "^3.0.0"
fs-readdir-recursive@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-0.1.2.tgz#315b4fb8c1ca5b8c47defef319d073dad3568059"
@ -2606,6 +2618,13 @@ make-dir@^3.0.0, make-dir@^3.0.2:
dependencies:
semver "^6.0.0"
markdown@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/markdown/-/markdown-0.5.0.tgz#28205b565a8ae7592de207463d6637dc182722b2"
integrity sha1-KCBbVlqK51kt4gdGPWY33BgnIrI=
dependencies:
nopt "~2.1.1"
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -2697,6 +2716,13 @@ minipass@^2.2.1, minipass@^2.3.4:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minipass@^3.0.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
dependencies:
yallist "^4.0.0"
minizlib@^1.1.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614"
@ -2704,6 +2730,14 @@ minizlib@^1.1.1:
dependencies:
minipass "^2.2.1"
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"
mkdirp@0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
@ -2945,6 +2979,13 @@ nopt@~1.0.10:
dependencies:
abbrev "1"
nopt@~2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.1.2.tgz#6cccd977b80132a07731d6e8ce58c2c8303cf9af"
integrity sha1-bMzZd7gBMqB3MdbozljCyDA8+a8=
dependencies:
abbrev "1"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@ -3681,7 +3722,7 @@ rimraf@^2.2.8, rimraf@^2.6.1, rimraf@^2.6.3:
dependencies:
glob "^7.1.3"
rimraf@^3.0.0:
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
@ -4107,6 +4148,18 @@ tar@^4:
safe-buffer "^5.1.2"
yallist "^3.0.2"
tar@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f"
integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^3.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
test-exclude@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
@ -4412,6 +4465,11 @@ yallist@^3.0.0, yallist@^3.0.2:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yargs-parser@13.1.2, yargs-parser@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"

Loading…
Cancel
Save