Gisle Aune
5 years ago
35 changed files with 875 additions and 68 deletions
-
1build.js
-
2marko/components/compact-list-item/index.marko
-
18marko/components/compact-list-item/style.less
-
1marko/components/compact-list/style.less
-
61marko/page/data/components/add-file-modal/component.js
-
19marko/page/data/components/add-file-modal/index.marko
-
14marko/page/data/components/change/component.js
-
1marko/page/data/components/channels-page/index.marko
-
9marko/page/data/components/characters-page/component.js
-
1marko/page/data/components/characters-page/index.marko
-
14marko/page/data/components/data-menu/index.marko
-
59marko/page/data/components/edit-file-modal/component.js
-
16marko/page/data/components/edit-file-modal/index.marko
-
7marko/page/data/components/file-list/component.js
-
9marko/page/data/components/file-list/index.marko
-
38marko/page/data/components/file/component.js
-
22marko/page/data/components/file/index.marko
-
47marko/page/data/components/files-page/component.js
-
11marko/page/data/components/files-page/index.marko
-
10marko/page/data/components/files-page/style.less
-
19marko/page/data/components/link-file-modal/component.js
-
36marko/page/data/components/link-file-modal/index.marko
-
19marko/page/data/components/link-file-modal/style.less
-
51marko/page/data/components/remove-file-modal/component.js
-
11marko/page/data/components/remove-file-modal/index.marko
-
6marko/page/data/files.marko
-
200package-lock.json
-
6package.json
-
2routes/data/changes.js
-
22routes/data/files.js
-
18routes/graphql.js
-
5rpdata/api/Change.js
-
110rpdata/api/File.js
-
67rpdata/client.js
-
11server.js
@ -1,3 +1,3 @@ |
|||
<tr class="compact-list-item color-highlight-dark"> |
|||
<tr class=["compact-list-item", input.highlight ? "color-highlight-primary" : "color-highlight-dark"]> |
|||
<include(input.renderBody)/> |
|||
</tr> |
@ -0,0 +1,61 @@ |
|||
const moment = require("moment") |
|||
|
|||
const {fileApi} = require("../../../../../rpdata/api/File") |
|||
|
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
error: null, |
|||
loading: false, |
|||
values: { |
|||
file: null, |
|||
name: "", |
|||
public: false, |
|||
|
|||
realFile: null, |
|||
} |
|||
} |
|||
} |
|||
|
|||
change(key, ev) { |
|||
if (key === "file") { |
|||
this.state.values = Object.assign({}, this.state.values, { |
|||
[key]: ev.target.value, |
|||
realFile: ev.target.files[0], |
|||
name: ev.target.files[0].name, |
|||
}) |
|||
} else { |
|||
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value}) |
|||
} |
|||
} |
|||
|
|||
open() { |
|||
|
|||
} |
|||
|
|||
close() { |
|||
this.emit("close") |
|||
} |
|||
|
|||
save() { |
|||
if (this.state.loading) { |
|||
return |
|||
} |
|||
|
|||
const input = Object.assign({}, this.state.values) |
|||
input.file = input.realFile |
|||
delete input.realFile |
|||
|
|||
this.state.loading = true |
|||
fileApi.uploadFile(input).then((res) => { |
|||
this.emit("added", res) |
|||
this.emit("close") |
|||
}).catch(errs => { |
|||
console.warn("Failed to add:", errs) |
|||
|
|||
this.state.error = "Failed to add: " + JSON.stringify(errs) |
|||
}).then(() => { |
|||
this.state.loading = false |
|||
}) |
|||
} |
|||
} |
@ -0,0 +1,19 @@ |
|||
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") > |
|||
<h1>Upload File</h1> |
|||
|
|||
<p class="color-error">${state.error}</p> |
|||
|
|||
<label>File</label> |
|||
<input key="eventName" type="file" placeholder="(None)" on-change("change", "file") value=state.values.file /> |
|||
|
|||
<label>Name</label> |
|||
<input key="name" autofocus placeholder="(Required)" class="big" on-change("change", "name") value=state.values.name /> |
|||
|
|||
<label>Public</label> |
|||
<toggle value=state.values.public on="On" off="Off" |
|||
onDesc="Other members may see this file on this page." |
|||
offDesc="Only you may see this file on this page." |
|||
on-change("change", "public") /> |
|||
|
|||
<button disabled=state.loading on-click("save")>Save</button> |
|||
</modal> |
@ -0,0 +1,59 @@ |
|||
const moment = require("moment") |
|||
|
|||
const {fileApi} = require("../../../../../rpdata/api/File") |
|||
|
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
error: null, |
|||
loading: false, |
|||
values: { |
|||
id: "", |
|||
name: "", |
|||
public: false, |
|||
} |
|||
} |
|||
} |
|||
|
|||
onInput(input) { |
|||
this.state.values = { |
|||
id: input.file.id, |
|||
name: input.file.name, |
|||
public: input.file.public, |
|||
} |
|||
} |
|||
|
|||
change(key, ev) { |
|||
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value}) |
|||
} |
|||
|
|||
open() { |
|||
|
|||
} |
|||
|
|||
close() { |
|||
this.emit("close") |
|||
} |
|||
|
|||
save() { |
|||
if (this.state.loading) { |
|||
return |
|||
} |
|||
|
|||
const input = Object.assign({}, this.state.values) |
|||
input.file = input.realFile |
|||
delete input.realFile |
|||
|
|||
this.state.loading = true |
|||
fileApi.editFile(input).then((res) => { |
|||
this.emit("edited", res) |
|||
this.emit("close") |
|||
}).catch(errs => { |
|||
console.warn("Failed to add:", errs) |
|||
|
|||
this.state.error = "Failed to add: " + JSON.stringify(errs) |
|||
}).then(() => { |
|||
this.state.loading = false |
|||
}) |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") > |
|||
<h1>Upload File</h1> |
|||
|
|||
<p class="color-error">${state.error}</p> |
|||
|
|||
<label>Name</label> |
|||
<input key="name" autofocus placeholder="(Required)" class="big" on-change("change", "name") value=state.values.name /> |
|||
|
|||
<label>Public</label> |
|||
<toggle value=state.values.public on="On" off="Off" |
|||
onDesc="Other members may see this file on this page." |
|||
offDesc="Only you may see this file on this page." |
|||
on-change("change", "public") /> |
|||
|
|||
<button disabled=state.loading on-click("save")>Save</button> |
|||
</modal> |
@ -0,0 +1,7 @@ |
|||
module.exports = class { |
|||
isHighlighted(id) { |
|||
if (typeof(window) !== "undefined") { |
|||
return window.location.hash.endsWith(id) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
<compact-list> |
|||
<compact-list-item for(file in input.files) key=file.id id=file.id highlight=component.isHighlighted(file.id)> |
|||
<file data=file |
|||
user=input.user |
|||
on-removed("emit", "removed", file) |
|||
on-edited("emit", "edited", file) |
|||
/> |
|||
</compact-list-item> |
|||
</compact-list> |
@ -0,0 +1,38 @@ |
|||
const moment = require("moment") |
|||
|
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
modal: null, |
|||
deleted: false, |
|||
date: moment(input.data.time).format("MMM D, YYYY"), |
|||
} |
|||
} |
|||
|
|||
onInput(input) { |
|||
if (input && input.data) { |
|||
this.state.date = moment(input.data.time).format("MMM D, YYYY") |
|||
} |
|||
} |
|||
|
|||
open(modal) { |
|||
this.state.modal = modal |
|||
} |
|||
|
|||
close() { |
|||
this.state.modal = null |
|||
} |
|||
|
|||
removed() { |
|||
this.state.deleted = true |
|||
this.emit("removed") |
|||
} |
|||
|
|||
sizeString(bytes) { |
|||
const order = Math.floor(Math.log2(bytes) / 10) |
|||
const unit = ["B", "KB", "MB", "GB", "TB", "PB"][order] |
|||
const number = bytes / Math.pow(2, (order)*10) |
|||
|
|||
return `${number.toFixed(2)} ${unit}` |
|||
} |
|||
} |
@ -0,0 +1,22 @@ |
|||
<if (!state.deleted)> |
|||
<a class="anchor" id=input.data.id /> |
|||
<compact-list-property primary long label="Name" value=input.data.name /> |
|||
<compact-list-property short label="Date" value=state.date /> |
|||
<compact-list-property short label="Type" value=input.data.mimeType /> |
|||
<compact-list-property short label="Author" value=input.data.author /> |
|||
<compact-list-property short label="Size" value=component.sizeString(input.data.size) /> |
|||
<compact-list-options long> |
|||
<a key="link" class="color-menu" on-click("open", "link")>Link</a> |
|||
<if(input.data.kind === "upload")> |
|||
<if-permitted user=input.user author=input.data.author permission="file.edit"> |
|||
<a key="edit" on-click("open", "edit", input.data)>Edit</a> |
|||
</if-permitted> |
|||
<if-permitted user=input.user author=input.data.author permission="file.remove"> |
|||
<a key="remove" on-click("open", "remove", input.data)>Remove</a> |
|||
</if-permitted> |
|||
</if> |
|||
</compact-list-options> |
|||
<link-file-modal enabled=(state.modal === "link") file=input.data on-close("close") /> |
|||
<edit-file-modal enabled=(state.modal === "edit") file=input.data on-edited("emit", "edited") on-close("close") /> |
|||
<remove-file-modal enabled=(state.modal === "remove") file=input.data on-removed("removed") on-close("close") /> |
|||
</if> |
@ -0,0 +1,47 @@ |
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
publics: input.publics.sort(byDate), |
|||
privates: input.privates.sort(byDate), |
|||
modal: null, |
|||
} |
|||
} |
|||
|
|||
onMount() { |
|||
} |
|||
|
|||
open(modal) { |
|||
this.state.modal = modal |
|||
} |
|||
|
|||
close() { |
|||
this.state.modal = null |
|||
} |
|||
|
|||
fileRemoved({id}) { |
|||
this.state.publics = this.state.publics.filter(f => f.id !== id) |
|||
this.state.privates = this.state.privates.filter(f => f.id !== id) |
|||
} |
|||
|
|||
fileEdited(file, patch) { |
|||
this.fileRemoved({id: file.id}) |
|||
this.fileAdded(Object.assign({}, file, patch)) |
|||
|
|||
console.log(Object.assign({}, file, patch)) |
|||
} |
|||
|
|||
fileAdded(file) { |
|||
if (file.public) { |
|||
this.state.publics = [].concat(...this.state.publics).concat(file).sort(byDate) |
|||
} else { |
|||
this.state.privates = [].concat(...this.state.privates).concat(file).sort(byDate) |
|||
} |
|||
} |
|||
} |
|||
|
|||
function byDate(a, b) { |
|||
const ad = new Date(a.time) |
|||
const bd = new Date(b.time) |
|||
|
|||
return ad.getTime() - bd.getTime() |
|||
} |
@ -0,0 +1,11 @@ |
|||
<data-menu categories=input.categories selected=(input.selected || {}) user=input.user on-open("open") /> |
|||
<main class="data-file-page"> |
|||
<h1 class="file-list-headers color-primary">Your Private Files</h1> |
|||
<file-list user=input.user files=state.privates on-removed("fileRemoved") on-edited("fileEdited") on-nicks("fileNicksChanged") /> |
|||
|
|||
<h1 class="file-list-headers color-primary">Public Files</h1> |
|||
<file-list user=input.user files=state.publics on-removed("fileRemoved") on-edited("fileEdited") on-nicks("fileNicksChanged") /> |
|||
</main> |
|||
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") /> |
|||
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-close("close") /> |
|||
<add-file-modal enabled=(state.modal === "file.add") user=input.user on-added("fileAdded") on-close("close") /> |
@ -0,0 +1,10 @@ |
|||
h1.file-list-headers { |
|||
margin: 0; |
|||
font-size: 1.5em; |
|||
margin-top: 0.5em; |
|||
text-align: center; |
|||
} |
|||
|
|||
main.data-file-page { |
|||
margin-bottom: 4em; |
|||
} |
@ -0,0 +1,19 @@ |
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
isImage: (input.file && input.file.mimeType.startsWith("image/")) |
|||
} |
|||
} |
|||
|
|||
onInput(input) { |
|||
this.state.isImage = (input.file && input.file.mimeType.startsWith("image/")) |
|||
} |
|||
|
|||
open() { |
|||
|
|||
} |
|||
|
|||
close() { |
|||
this.emit("close") |
|||
} |
|||
} |
@ -0,0 +1,36 @@ |
|||
<modal class="modal color-text nolabel link-file-modal" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") > |
|||
<h1>Links for ${input.file.name}</h1> |
|||
|
|||
<p> |
|||
Below are links and copy-pastable syntax for sharing the file. They can be used for linking to it |
|||
in your stories and wiki pages. |
|||
</p> |
|||
|
|||
<p class="color-danger"> |
|||
This is the link to the actual file on the CDN, not a share of it. You must delete the file if you want |
|||
to un-share it. |
|||
</p> |
|||
|
|||
<if(state.isImage)> |
|||
<p class="color-danger"> |
|||
Images uploaded here cannot be inlined on the wiki. For that you have to upload it there. |
|||
</p> |
|||
</if> |
|||
|
|||
<label>URL</label> |
|||
<input key="url" disabled placeholder="(None)" on-change("change", "eventName") value=input.file.url /> |
|||
|
|||
<label>Markdown link</label> |
|||
<input key="mdLink" disabled placeholder="(None)" on-change("change", "locationName") value=`[Link text](${input.file.url})` /> |
|||
|
|||
<label>Wiki link</label> |
|||
<input key="wikiLink" disabled placeholder="(None)" on-change("change", "locationName") value=`[${input.file.url} Link text]` /> |
|||
|
|||
<if(state.isImage)> |
|||
<label>Markdown image</label> |
|||
<input key="mdImage" disabled placeholder="(None)" on-change("change", "locationName") value=`![Alt. text](${input.file.url})` /> |
|||
|
|||
<label>Image Preview</label> |
|||
<img src=input.file.url alt="File preview"/> |
|||
</if> |
|||
</modal> |
@ -0,0 +1,19 @@ |
|||
div.link-file-modal { |
|||
user-select: text; |
|||
|
|||
p { |
|||
user-select: text; |
|||
cursor: text; |
|||
} |
|||
|
|||
img { |
|||
margin: 0; |
|||
padding: 0; |
|||
margin-top: 0.25em; |
|||
width: 100%; |
|||
} |
|||
|
|||
input { |
|||
user-select: all !important; |
|||
} |
|||
} |
@ -0,0 +1,51 @@ |
|||
const {fileApi} = require("../../../../../rpdata/api/File") |
|||
|
|||
module.exports = class { |
|||
onCreate(input) { |
|||
this.state = { |
|||
error: null, |
|||
loading: false, |
|||
values: { |
|||
id: "", |
|||
} |
|||
} |
|||
} |
|||
|
|||
onInput(input) { |
|||
this.state.values = { |
|||
id: input.file.id, |
|||
} |
|||
} |
|||
|
|||
change(key, ev) { |
|||
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value}) |
|||
} |
|||
|
|||
open() { |
|||
|
|||
} |
|||
|
|||
close() { |
|||
this.emit("close") |
|||
} |
|||
|
|||
doIt() { |
|||
if (this.state.loading) { |
|||
return |
|||
} |
|||
|
|||
const input = Object.assign({}, this.state.values) |
|||
|
|||
this.state.loading = true |
|||
fileApi.removeFile(input).then((res) => { |
|||
this.emit("removed", res) |
|||
this.emit("close") |
|||
}).catch(errs => { |
|||
console.warn("Failed to add:", errs) |
|||
|
|||
this.state.error = "Failed to add: " + JSON.stringify(errs) |
|||
}).then(() => { |
|||
this.state.loading = false |
|||
}) |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") > |
|||
<h1>Delete File ${input.file.name}</h1> |
|||
|
|||
<p class="color-error">${state.error}</p> |
|||
|
|||
<p class="color-danger"> |
|||
It might take up to an hour for it to be removed from CDN caches if it's been recently loaded. |
|||
</p> |
|||
|
|||
<button disabled=state.loading on-click("doIt")>Remove</button> |
|||
</modal> |
@ -0,0 +1,6 @@ |
|||
<include("../layout", {title: "Data", site: "data"})> |
|||
<@body> |
|||
<background src="/assets/images/bg.png" opacity=0.25 /> |
|||
<files-page user=input.user privates=input.privates publics=input.publics selected=input.selected /> |
|||
</@body> |
|||
</include> |
@ -0,0 +1,22 @@ |
|||
const express = require("express") |
|||
const router = express.Router() |
|||
|
|||
const {generateToken} = require("../graphql") |
|||
const {fileApi} = require("../../rpdata/api/File") |
|||
|
|||
const filesTemplate = require("../../marko/page/data/files.marko") |
|||
|
|||
router.get("/", async(req, res) => { |
|||
let privates = [] |
|||
if (res.locals.user) { |
|||
privates = fileApi.list({public: false}, {token: generateToken(res.locals.user.name)}) |
|||
} |
|||
|
|||
res.markoAsync(filesTemplate, { |
|||
privates: privates, |
|||
publics: fileApi.list(), |
|||
selected: {files: true}, |
|||
}) |
|||
}) |
|||
|
|||
module.exports = router |
@ -0,0 +1,110 @@ |
|||
const {query, queryForm} = require("../client") |
|||
|
|||
class File { |
|||
/** |
|||
* @param {{id:string,kind:string,time:Date|string|number,public:boolean,name:string,mimeType:string,size:number,author:string,url:string}} data |
|||
*/ |
|||
constructor(data) { |
|||
this.id = data.id |
|||
this.kind = data.kind |
|||
this.time = new Date(data.time) |
|||
this.public = data.public |
|||
this.name = data.name |
|||
this.mimeType = data.mimeType |
|||
this.size = data.size |
|||
this.author = data.author |
|||
this.url = data.url |
|||
} |
|||
} |
|||
|
|||
|
|||
class FileAPI { |
|||
/** |
|||
* Call `characters(filter)` query |
|||
* |
|||
* @param {{public:boolean,mimeTypes:string[]}} filter |
|||
* @returns {Promise<File[]>} |
|||
*/ |
|||
list(filter = {public: true}, opts = {}) { |
|||
return query(`
|
|||
query ListFiles($filter: FilesFilter!) { |
|||
files(filter:$filter) { |
|||
id |
|||
kind |
|||
time |
|||
public |
|||
name |
|||
mimeType |
|||
size |
|||
author |
|||
url |
|||
} |
|||
} |
|||
`, {filter}, opts).then(({files}) => {
|
|||
return files.map(d => new File(d)) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Upload a file |
|||
* |
|||
* @param {{public:boolean, file:File, name:string}} input |
|||
*/ |
|||
uploadFile(input) { |
|||
return queryForm(`
|
|||
mutation UploadFile($input: FileUploadInput!) { |
|||
uploadFile(input:$input) { |
|||
id |
|||
kind |
|||
time |
|||
public |
|||
name |
|||
mimeType |
|||
size |
|||
author |
|||
url |
|||
} |
|||
} |
|||
`, {input}).then(({uploadFile}) => {
|
|||
return new File(uploadFile) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Edit a file |
|||
* |
|||
* @param {{id: string, public:boolean, name:string}} input |
|||
*/ |
|||
editFile(input) { |
|||
return queryForm(`
|
|||
mutation EditFile($input: FileEditInput!) { |
|||
editFile(input:$input) { |
|||
id |
|||
public |
|||
name |
|||
} |
|||
} |
|||
`, {input}).then(({editFile}) => {
|
|||
return editFile |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Edit a file |
|||
* |
|||
* @param {{id: string, public:boolean, name:string}} input |
|||
*/ |
|||
removeFile(input) { |
|||
return queryForm(`
|
|||
mutation RemoveFIle($input: FileRemoveInput!) { |
|||
removeFile(input:$input) { |
|||
id |
|||
} |
|||
} |
|||
`, {input}).then(({removeFile}) => {
|
|||
return removeFile |
|||
}) |
|||
} |
|||
} |
|||
|
|||
module.exports = { File, fileApi: new FileAPI() } |
Write
Preview
Loading…
Cancel
Save
Reference in new issue