Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5955e6a71e | ||
|
|
59a711f150 | ||
|
|
b75c3461f7 | ||
|
|
a10b29a0ea | ||
|
|
79c3a76300 | ||
|
|
6aef0412e8 | ||
|
|
5fe56eaae8 | ||
|
|
bb2ccf6f4b | ||
|
|
1583eb412e | ||
|
|
5c094a2471 |
+29
-21
@@ -1,25 +1,33 @@
|
||||
pipeline:
|
||||
docker-trunk:
|
||||
name: rpdata-frontend
|
||||
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: docker-release-tag
|
||||
image: plugins/docker
|
||||
repo: vmaple/rpdata-frontend
|
||||
tags: trunk
|
||||
secrets: [ docker_username, docker_password ]
|
||||
settings:
|
||||
auto_tag: true
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: r.vmaple.dev/rpdata/rpdata-frontend
|
||||
registry: r.vmaple.dev
|
||||
when:
|
||||
event: push
|
||||
branch: master
|
||||
docker-release:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: docker-release-latest
|
||||
image: plugins/docker
|
||||
repo: vmaple/rpdata-frontend
|
||||
auto_tag: true
|
||||
secrets: [ docker_username, docker_password ]
|
||||
settings:
|
||||
auto_tag: false
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: r.vmaple.dev/rpdata/rpdata-frontend
|
||||
registry: r.vmaple.dev
|
||||
when:
|
||||
event: tag
|
||||
ref: refs/tags/*
|
||||
docker-release-latest:
|
||||
image: plugins/docker
|
||||
repo: vmaple/rpdata-frontend
|
||||
tags: latest
|
||||
secrets: [ docker_username, docker_password ]
|
||||
when:
|
||||
event: tag
|
||||
ref: refs/tags/*
|
||||
event:
|
||||
- tag
|
||||
@@ -12,6 +12,7 @@ prebuild.run({
|
||||
"./marko/page/data/channels.marko",
|
||||
"./marko/page/data/characters.marko",
|
||||
"./marko/page/data/changes.marko",
|
||||
"./marko/page/data/files.marko",
|
||||
"./marko/page/story-content/view.marko",
|
||||
"./marko/page/logs-content/view.marko",
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
@@ -2,4 +2,22 @@ tr.compact-list-item {
|
||||
user-select: none;
|
||||
outline: 0.05px dotted;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tr.compact-list-item.color-highlight-primary {
|
||||
a {
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #CCC !important;
|
||||
}
|
||||
|
||||
div.label {
|
||||
color: #AA8 !important;
|
||||
}
|
||||
|
||||
div.value.color-text {
|
||||
color: #FFF !important;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
table.compact-list {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 3em;
|
||||
|
||||
width: 90%;
|
||||
max-width: 100ch;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<td class="icon color-highlight-primary">${input.icon}</td>
|
||||
<td class="content color-primary color-highlight-dark">
|
||||
<div class="title">
|
||||
<a class="color-primary" href=input.link>${input.name}</a>
|
||||
<a class="color-primary" href=input.link>${input.name || "(Untitled)"}</a>
|
||||
</div>
|
||||
<div if(input.description) class="description color-text">
|
||||
<div class="content">${input.description}</div>
|
||||
@@ -20,4 +20,4 @@
|
||||
<item-meta kind="author" value=input.author />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</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>
|
||||
@@ -65,8 +65,8 @@ module.exports = class {
|
||||
|
||||
this.state.operation += " chapter"
|
||||
this.state.links = [{
|
||||
text: chapter.title || `Untitled chapter (${chapterKey.id})`,
|
||||
href: `/story/${storyKey.id}#${chapterKey.id}`,
|
||||
text: (chapter||{}).title || `Untitled chapter (${(chapterKey||chapter).id})`,
|
||||
href: `/story/${storyKey.id}#${(chapterKey||chapter).id}`,
|
||||
}]
|
||||
|
||||
break
|
||||
@@ -99,6 +99,19 @@ module.exports = class {
|
||||
break
|
||||
}
|
||||
|
||||
case "File": {
|
||||
const key = data.keys.find(k => k.id !== "*")
|
||||
const fileObj = data.objects.find(o => o.type === "File") || {name: key.id}
|
||||
|
||||
this.state.operation += " file"
|
||||
this.state.links = [{
|
||||
text: fileObj.name,
|
||||
href: `/data/files#${key.id}`,
|
||||
}]
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "Post": {
|
||||
const logKey = data.keys.find(k => k.model === "Log")
|
||||
const postKeys = data.keys.filter(k => k.model === "Post")
|
||||
@@ -124,6 +137,7 @@ module.exports = class {
|
||||
const opText = {
|
||||
add: "added",
|
||||
remove: "removed",
|
||||
upload: "uploaded",
|
||||
move: "moved",
|
||||
edit: "edited",
|
||||
import: "imported",
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
const { changesApi } = require("../../../../../rpdata/api/Change")
|
||||
|
||||
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
characters: input.characters,
|
||||
changes: input.changes,
|
||||
shownAll: false,
|
||||
modal: null,
|
||||
}
|
||||
}
|
||||
|
||||
clearLimit(e) {
|
||||
// Disable mobile URL fallback.
|
||||
if (e && e.preventDefault) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
changesApi.list({limit: 0}).then(changes => {
|
||||
this.state.changes = changes
|
||||
this.state.shownAll = true
|
||||
})
|
||||
}
|
||||
|
||||
open(modal) {
|
||||
this.state.modal = modal
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
<div class="changes-page">
|
||||
<h1 class="color-primary">Changes</h1>
|
||||
<p>All changes to <em>listed</em> resources is stored for 90 days for transparency reasons. The current state of an object after each change exists, but is only available through the GraphQL API at this time.</p>
|
||||
<for (change in input.changes)>
|
||||
<for (change in state.changes)>
|
||||
<change data=change />
|
||||
</for>
|
||||
<a if(!state.shownAll) href="/data/changes?limit=0" class="show-more-button" on-click("clearLimit")>Show All</a>
|
||||
</div>
|
||||
</main>
|
||||
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") />
|
||||
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") />
|
||||
<add-file-modal enabled=(state.modal === "file.add") user=input.user on-close("close") />
|
||||
|
||||
@@ -33,4 +33,21 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.show-more-button {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 2em;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
display: block;
|
||||
|
||||
color: #DA1;
|
||||
}
|
||||
|
||||
.show-more-button:hover {
|
||||
cursor: pointer;
|
||||
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,4 @@
|
||||
</main>
|
||||
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-added("channelAdded") 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-close("close") />
|
||||
|
||||
@@ -31,6 +31,15 @@ module.exports = class {
|
||||
}
|
||||
|
||||
characterAdded(character) {
|
||||
const index = this.state.characters.findIndex(c => c.id === character.id)
|
||||
if (index !== -1) {
|
||||
const characters = this.state.characters.slice()
|
||||
characters[index] = character
|
||||
this.state.characters = characters
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.state.characters = this.state.characters.concat(character)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
</main>
|
||||
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") />
|
||||
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") />
|
||||
<add-file-modal enabled=(state.modal === "file.add") user=input.user on-close("close") />
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<menu-link key="characters" selected=input.selected.characters icon="C" href="/data/characters/">Characters</menu-link>
|
||||
<menu-link key="channels" selected=input.selected.channels icon="#" href="/data/channels/">Channels</menu-link>
|
||||
<menu-link key="history" selected=input.selected.changes icon="H" href="/data/changes/">History</menu-link>
|
||||
<if-permitted user=input.user permission=["member", "character.add", "channel.add"]>
|
||||
<if-permitted user=input.user permission=["member", "character.add", "channel.add", "file.add"]>
|
||||
<menu-gap />
|
||||
<menu-header>Add</menu-header>
|
||||
<if-permitted user=input.user permission=["character.add", "member"]>
|
||||
<menu-link dark key="characters_add" icon="+" on-click("emit", "open", "character.add")>Character</menu-link>
|
||||
<if-permitted key="characters_add" user=input.user permission=["character.add", "member"]>
|
||||
<menu-link dark icon="+" on-click("emit", "open", "character.add")>Character</menu-link>
|
||||
</if-permitted>
|
||||
<if-permitted user=input.user permission="channel.add">
|
||||
<menu-link dark key="channels_add" icon="+" on-click("emit", "open", "channel.add")>Channel</menu-link>
|
||||
<if-permitted key="channels_add" user=input.user permission="channel.add">
|
||||
<menu-link dark icon="+" on-click("emit", "open", "channel.add")>Channel</menu-link>
|
||||
</if-permitted>
|
||||
</if-permitted>
|
||||
</menu>
|
||||
@@ -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 if(state.privates && state.privates.length > 0) 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 if(state.publics && state.publics.length > 0) 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=`` />
|
||||
|
||||
<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>
|
||||
@@ -1,93 +1,10 @@
|
||||
const {logHeaderApi} = require("../../../../../rpdata/api/LogHeader")
|
||||
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
id: input.log.id,
|
||||
channelName: input.log.channelName,
|
||||
eventName: input.log.eventName,
|
||||
nexts: [],
|
||||
prevs: [],
|
||||
loaded: false,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
|
||||
onMount() {
|
||||
this.timeout = setTimeout(() => {
|
||||
logHeaderApi.list().then(logs => {
|
||||
const index = logs.findIndex(l => l.id === this.state.id);
|
||||
if (index === -1) {
|
||||
return Promise.reject("This log not found in list.")
|
||||
}
|
||||
const log = logs[index]
|
||||
const characterIds = log.characters.map(c => c.id);
|
||||
const nexts = []
|
||||
const prevs = []
|
||||
const foundGroups = {}
|
||||
let foundChannel = false
|
||||
|
||||
for (let i = index - 1; i >= 0; --i) {
|
||||
const log2 = logs[i]
|
||||
const hasEvent = (log.eventName ? log.eventName === log2.eventName : false)
|
||||
const hasChannel = (log.channelName === log2.channelName)
|
||||
|
||||
const characters = log2.findCharacters(...characterIds)
|
||||
const characterIdsKey = characters.map(c => c.id).sort().join(",")
|
||||
|
||||
const suggestion = {log: log2, characters, hasEvent, hasChannel}
|
||||
|
||||
if (hasChannel && !foundChannel) {
|
||||
foundChannel = true
|
||||
foundGroups[characterIdsKey] = true
|
||||
|
||||
nexts.push(suggestion)
|
||||
} else if (hasEvent) {
|
||||
foundGroups[characterIdsKey] = true
|
||||
|
||||
nexts.push(suggestion)
|
||||
} else if (nexts.length < 8 && !hasEvent && characters.length > 1 && !foundGroups[characterIdsKey]) {
|
||||
foundGroups[characterIdsKey] = true
|
||||
nexts.push(suggestion)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = index + 1; i < logs.length; ++i) {
|
||||
const log2 = logs[i]
|
||||
const hasEvent = (log.eventName ? log.eventName === log2.eventName : false)
|
||||
const hasChannel = (log.channelName === log2.channelName)
|
||||
|
||||
const characters = log2.findCharacters(...characterIds)
|
||||
const characterIdsKey = characters.map(c => c.id).sort().join(",")
|
||||
|
||||
const suggestion = {log: log2, characters, hasEvent, hasChannel}
|
||||
|
||||
if (hasChannel && !foundChannel) {
|
||||
foundChannel = true
|
||||
foundGroups[characterIdsKey] = true
|
||||
|
||||
prevs.push(suggestion)
|
||||
} else if (hasEvent) {
|
||||
foundGroups[characterIdsKey] = true
|
||||
|
||||
prevs.push(suggestion)
|
||||
} else if (prevs.length < 8 && !hasEvent && characters.length > 1 && !foundGroups[characterIdsKey]) {
|
||||
foundGroups[characterIdsKey] = true
|
||||
prevs.push(suggestion)
|
||||
}
|
||||
}
|
||||
|
||||
this.state.nexts = nexts;
|
||||
this.state.prevs = prevs;
|
||||
this.state.loaded = true;
|
||||
}).catch(err => {
|
||||
console.warn("Suggestions load error:", err)
|
||||
this.suggestionsError = "Failed to load suggestions: " + err.toString()
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
onUnmount() {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
import moment from "moment"
|
||||
|
||||
<div class="log-suggestions">
|
||||
<if (state.loaded && state.error == null)>
|
||||
<if (state.nexts.length > 0 || state.prevs.length > 0)>
|
||||
<if (state.nexts.length > 0)>
|
||||
<h2 class="color-primary">Next Logs</h2>
|
||||
<div for(s in state.nexts) class="suggestion color-primary">
|
||||
<div class="suggestion-header color-primary"><a href=`/logs/${s.log.id}`>${s.log.name || `${s.log.channelName} - ${moment(s.log.date).format("MMMM D, YYYY")}`}</a></div>
|
||||
<div for(paragraph in (s.log.description||"").split("\n").filter(p => p)) class="suggestion-description color-text">${paragraph}</div>
|
||||
<div class="suggestion-tags">
|
||||
<div if(s.hasEvent) class="suggestion-tag color-tag-event">${s.log.eventName}</div>
|
||||
<div if(s.hasChannel) class="suggestion-tag color-tag-location">${s.log.channelName}</div>
|
||||
<div for(c in s.characters) class="suggestion-tag color-tag-character">${c.name}</div>
|
||||
</div>
|
||||
<div class="log-suggestions">
|
||||
<if (input.log.nextLogs.length > 0 || input.log.prevLogs.length > 0)>
|
||||
<if (input.log.nextLogs.length > 0)>
|
||||
<h2 class="color-primary">Next Logs</h2>
|
||||
<div for(s in input.log.nextLogs) class="suggestion color-primary">
|
||||
<div class="suggestion-header color-primary"><a href=`/logs/${s.log.id}`>${s.log.title || `${s.log.channelName} - ${moment(s.log.date).format("MMMM D, YYYY")}`}</a></div>
|
||||
<div for(paragraph in (s.log.description||"").split("\n").filter(p => p)) class="suggestion-description color-text">${paragraph}</div>
|
||||
<div class="suggestion-tags">
|
||||
<div if(s.hasEvent) class="suggestion-tag color-tag-event">${input.log.eventName}</div>
|
||||
<div if(s.hasChannel) class="suggestion-tag color-tag-location">${s.log.channelName}</div>
|
||||
<div for(c in s.characters) class="suggestion-tag color-tag-character">${c.name}</div>
|
||||
</div>
|
||||
</if>
|
||||
<if (state.prevs.length > 0)>
|
||||
<h2 class="color-primary">Previous Logs</h2>
|
||||
<div for(s in state.prevs) class="suggestion color-primary">
|
||||
<div class="suggestion-header color-primary"><a href=`/logs/${s.log.id}`>${s.log.name || `${s.log.channelName} - ${moment(s.log.date).format("MMMM D, YYYY")}`}</a></div>
|
||||
<div for(paragraph in (s.log.description||"").split("\n").filter(p => p)) class="suggestion-description color-text">${paragraph}</div>
|
||||
<div class="suggestion-tags">
|
||||
<div if(s.hasEvent) class="suggestion-tag color-tag-event">${s.log.eventName}</div>
|
||||
<div if(s.hasChannel) class="suggestion-tag color-tag-location">${s.log.channelName}</div>
|
||||
<div for(c in s.characters) class="suggestion-tag color-tag-character">${c.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</if>
|
||||
<if (input.log.prevLogs.length > 0)>
|
||||
<h2 class="color-primary">Previous Logs</h2>
|
||||
<div for(s in input.log.prevLogs) class="suggestion color-primary">
|
||||
<div class="suggestion-header color-primary"><a href=`/logs/${s.log.id}`>${s.log.title || `${s.log.channelName} - ${moment(s.log.date).format("MMMM D, YYYY")}`}</a></div>
|
||||
<div for(paragraph in (s.log.description||"").split("\n").filter(p => p)) class="suggestion-description color-text">${paragraph}</div>
|
||||
<div class="suggestion-tags">
|
||||
<div if(s.hasEvent) class="suggestion-tag color-tag-event">${input.log.eventName}</div>
|
||||
<div if(s.hasChannel) class="suggestion-tag color-tag-location">${s.log.channelName}</div>
|
||||
<div for(c in s.characters) class="suggestion-tag color-tag-character">${c.name}</div>
|
||||
</div>
|
||||
</if>
|
||||
</div>
|
||||
</if>
|
||||
<else>
|
||||
<div class="empty color-menu">(No suggestions)</div>
|
||||
</else>
|
||||
</if>
|
||||
<else-if(state.error != null)>
|
||||
<div class="error color-danger">{state.error}</div>
|
||||
</else-if>
|
||||
<else>
|
||||
<div class="loading color-menu">Loading suggestions...</div>
|
||||
<div class="empty color-menu">(No suggestions)</div>
|
||||
</else>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ module.exports = class {
|
||||
channelName: "",
|
||||
date: "",
|
||||
data: "",
|
||||
sessionThresholdHours: 12,
|
||||
},
|
||||
timezones: tz.names(),
|
||||
}
|
||||
@@ -58,12 +59,24 @@ module.exports = class {
|
||||
return
|
||||
}
|
||||
} else if (input.importer === "MircLike") {
|
||||
this.state.error = "Date is required for mIRC-like logs"
|
||||
this.state.error = "Date is required for mIRC-like logs."
|
||||
return
|
||||
} else {
|
||||
input.date = null;
|
||||
}
|
||||
|
||||
if (input.importer === "IRCCloudLog") {
|
||||
const hours = parseFloat(input.sessionThresholdHours)
|
||||
if (Number.isNaN(hours)) {
|
||||
this.state.error = "The session change threshold must be a number."
|
||||
return
|
||||
}
|
||||
|
||||
input.sessionThresholdMs = Math.floor(hours * 3600000)
|
||||
}
|
||||
|
||||
delete input.sessionThresholdHours
|
||||
|
||||
if (input.channelName.length < 2 || input.channelName.includes(" ") || input.channelName.includes(" ") || input.channelName.charAt(0) !== "#") {
|
||||
this.state.error = "A valid channelName name is required"
|
||||
return
|
||||
|
||||
@@ -10,6 +10,7 @@ import moment from "moment"
|
||||
<select key="importer" autofocus class="big" placeholder="Kind" on-change("change", "importer") value=state.values.importer>
|
||||
<option value="MircLike" selected=(state.values.importer === "MircLike")>mIRC-Like log</option>
|
||||
<option value="ForumLog" selected=(state.values.importer === "ForumLog")>Forum Log</option>
|
||||
<option value="IrcCloud" selected=(state.values.importer === "IrcCloud")>IRCCloud log export</option>
|
||||
</select>
|
||||
|
||||
<label>Timezone</label>
|
||||
@@ -25,6 +26,11 @@ import moment from "moment"
|
||||
<input key="date" class="big" placeholder="(Required)" on-change("change", "date") value=state.values.date />
|
||||
</if>
|
||||
|
||||
<if(state.values.importer === "IrcCloud")>
|
||||
<label>Session Change Threshold (Hours)</label>
|
||||
<input key="sessionThreshold" class="big" placeholder="(Required)" on-change("change", "sessionThresholdHours") value=state.values.sessionThresholdHours />
|
||||
</if>
|
||||
|
||||
<label>Content</label>
|
||||
<textarea key="data" placeholder="(Required)" class="tall" on-change("change", "data") value=state.values.data />
|
||||
|
||||
|
||||
@@ -79,9 +79,14 @@ module.exports = class {
|
||||
}
|
||||
}
|
||||
|
||||
clearLimit() {
|
||||
clearLimit(e) {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {limit: 0})
|
||||
|
||||
// Disable mobile URL fallback.
|
||||
if (e && e.preventDefault) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
this.refresh()
|
||||
this.updateQuery(this.state.filter)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<main class="logs-page with-right">
|
||||
<logs-table logs=state.logs on-addfilter("addFilter") />
|
||||
<if(state.filter.limit > 0)>
|
||||
<div class="show-more-button" on-click("clearLimit")>
|
||||
<a href="/logs?limit=0" class="show-more-button" on-click("clearLimit")>
|
||||
<span>Show All</span>
|
||||
</div>
|
||||
</a>
|
||||
</if>
|
||||
</main>
|
||||
<add-log-modal enabled=(state.modal === "log.add") on-close("close") />
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
main.logs-page {
|
||||
div.show-more-button {
|
||||
.show-more-button {
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 2em;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
display: block;
|
||||
|
||||
color: #DA1;
|
||||
}
|
||||
|
||||
div.show-more-button:hover {
|
||||
.show-more-button:hover {
|
||||
cursor: pointer;
|
||||
|
||||
text-decoration: underline;
|
||||
|
||||
@@ -7,7 +7,8 @@ const strategy = new Auth0Strategy({
|
||||
domain: config.auth0.domain,
|
||||
clientID: config.auth0.clientId,
|
||||
clientSecret: config.auth0.secret, // Replace this with the client secret for your app
|
||||
callbackURL: config.root + "/auth/callback"
|
||||
callbackURL: config.root + "/auth/callback",
|
||||
state: true,
|
||||
}, (accessToken, refreshToken, extraParams, profile, done) => {
|
||||
// accessToken is the token to call Auth0 API (not needed in the most cases)
|
||||
// extraParams.id_token has the JSON Web Token
|
||||
|
||||
@@ -6,7 +6,7 @@ module.exports = session({
|
||||
secret: config.session.secret,
|
||||
store: new RedisStore(config.redis),
|
||||
cookie: {
|
||||
secure: config.session.secure
|
||||
secure: config.session.secure,
|
||||
},
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
|
||||
Generated
+7686
-62
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -30,15 +30,15 @@
|
||||
"irc-caret-notation": "^1.0.0",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"lasso": "^3.2.6",
|
||||
"lasso": "^3.3.1",
|
||||
"lasso-babel-transform": "^1.0.2",
|
||||
"lasso-less": "^3.0.1",
|
||||
"lasso-marko": "^2.4.4",
|
||||
"lasso-marko": "^2.4.7",
|
||||
"less": "^3.8.1",
|
||||
"markdown-it": "^8.4.2",
|
||||
"markdown-it-container": "^2.0.0",
|
||||
"markdown-it-regexp": "^0.4.0",
|
||||
"marko": "^4.13.8",
|
||||
"marko": "^4.18.13",
|
||||
"moment": "^2.22.2",
|
||||
"moment-timezone": "^0.5.23",
|
||||
"passport": "^0.4.0",
|
||||
|
||||
+9
-5
@@ -17,12 +17,16 @@ router.get("/login", saveReferer, passport.authenticate("auth0", {scope: "openid
|
||||
res.redirect("/")
|
||||
})
|
||||
|
||||
router.get("/callback", passport.authenticate("auth0"), (req, res) => {
|
||||
if (req.user == null) {
|
||||
throw new Error("user null");
|
||||
}
|
||||
router.get("/callback", function (req, res, next) {
|
||||
passport.authenticate("auth0", function (err, user, info) {
|
||||
if (err) { return next(err) }
|
||||
if (!user) { console.log(err, user, info); return res.redirect("/auth/login") }
|
||||
req.logIn(user, function (err) {
|
||||
if (err) { return next(err) }
|
||||
|
||||
res.redirect(req.session.loginReferrer || "/");
|
||||
res.redirect(req.session.loginReferrer || "/");
|
||||
})
|
||||
})(req, res, next);
|
||||
})
|
||||
|
||||
router.get("/user", (req, res) => {
|
||||
|
||||
@@ -6,8 +6,10 @@ const {changesApi} = require("../../rpdata/api/Change")
|
||||
const changesTemplate = require("../../marko/page/data/changes.marko")
|
||||
|
||||
router.get("/", async(req, res) => {
|
||||
const limit = parseInt(req.query.limit) || 100
|
||||
|
||||
res.markoAsync(changesTemplate, {
|
||||
changes: changesApi.list({limit: 2000}),
|
||||
changes: changesApi.list({limit}),
|
||||
selected: {changes: true},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 && res.locals.user.loggedIn) {
|
||||
privates = fileApi.list({public: false}, {token: generateToken(res.locals.user.name)})
|
||||
}
|
||||
|
||||
res.markoAsync(filesTemplate, {
|
||||
privates: privates,
|
||||
publics: fileApi.list({public: true}),
|
||||
selected: {files: true},
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
+6
-12
@@ -3,16 +3,10 @@ const express = require("express")
|
||||
const jwt = require("jsonwebtoken")
|
||||
|
||||
const config = require("../config")
|
||||
const { query } = require("../rpdata/client")
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.post("/", (req, res) => {
|
||||
if (!req.header("Content-Type").startsWith("application/json")) {
|
||||
res.status(400).json({errors: [{message: "Incorrect input type, expected application/json"}]})
|
||||
return
|
||||
}
|
||||
|
||||
const user = res.locals.user
|
||||
const permissions = (req.header("X-Permissions") || "").split(",").filter(t => t != "" && t != "undefined" && t != "null")
|
||||
let authorization = req.header("Authorization")
|
||||
@@ -20,17 +14,17 @@ router.post("/", (req, res) => {
|
||||
authorization = ""
|
||||
}
|
||||
|
||||
if (!authorization && permissions.length > 0 && user.loggedIn) {
|
||||
if (!authorization && user.loggedIn) {
|
||||
authorization = `Bearer ${generateToken(user.name, permissions)}`
|
||||
}
|
||||
|
||||
fetch(config.graphqlEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Type": req.header("Content-Type"),
|
||||
"Authorization": authorization,
|
||||
},
|
||||
body: JSON.stringify(req.body),
|
||||
body: req,
|
||||
credentials: "include",
|
||||
}).then(fetchRes => {
|
||||
res.setHeader("Content-Type", fetchRes.headers.get("Content-Type"))
|
||||
@@ -51,8 +45,8 @@ router.use("/", proxy(config.graphqlEndpoint, {ws: true}))
|
||||
* @param {string} user
|
||||
* @param {string[]} permissions
|
||||
*/
|
||||
function generateToken(user, permissions) {
|
||||
return jwt.sign({user, permissions, exp: Math.floor((Date.now() / 1000) + 1200)}, config.backend.secret, {header: {kid: config.backend.kid}})
|
||||
function generateToken(user) {
|
||||
return jwt.sign({user, exp: Math.floor((Date.now() / 1000) + 1200)}, config.backend.secret, {header: {kid: config.backend.kid}})
|
||||
}
|
||||
|
||||
module.exports = router
|
||||
module.exports = {router, generateToken}
|
||||
@@ -48,6 +48,11 @@ class ChangesAPI {
|
||||
objects {
|
||||
type: __typename
|
||||
|
||||
...on File {
|
||||
id
|
||||
name
|
||||
}
|
||||
|
||||
...on Character {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -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() }
|
||||
+19
-3
@@ -31,7 +31,11 @@ class Log {
|
||||
}
|
||||
|
||||
static fromData(data) {
|
||||
return new Log(data.id, data.shortId, data.date, data.title, data.eventName, data.description, data.open, data.channel, data.characters, data.posts)
|
||||
return new Log(
|
||||
data.id, data.shortId, data.date,
|
||||
data.title, data.eventName, data.description,
|
||||
data.open, data.channel, data.characters, data.posts
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +74,22 @@ class LogAPI {
|
||||
nick
|
||||
text
|
||||
}
|
||||
nextLogs {
|
||||
log {id title date channelName}
|
||||
hasEvent
|
||||
hasChannel
|
||||
characters {name}
|
||||
}
|
||||
prevLogs {
|
||||
log {id title date channelName}
|
||||
hasEvent
|
||||
hasChannel
|
||||
characters {name}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {id}).then(({log}) => {
|
||||
return Log.fromData(log)
|
||||
return log
|
||||
})
|
||||
}
|
||||
|
||||
@@ -219,7 +235,7 @@ class LogAPI {
|
||||
subscribeChanges(logId) {
|
||||
return new Subscription(`
|
||||
subscription Changes($keys: [ChangeKeyInput!]!) {
|
||||
changes(keys: $keys) {
|
||||
changes(filter: {keys: $keys}) {
|
||||
id
|
||||
model
|
||||
op
|
||||
|
||||
+64
-3
@@ -10,13 +10,13 @@ const compressQuery = require("graphql-query-compress")
|
||||
* @param {{[x:string]: any}} variables
|
||||
* @param {{operationName:string, token: string, permissions: string[]}} options
|
||||
*/
|
||||
function query(query, variables = {}, options = {}) {
|
||||
function query(query, variables = {}, options = {}) {
|
||||
return fetch(config.graphqlEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": options.token ? `Bearer ${options.token}` : null,
|
||||
"X-Permissions": options.permissions ? options.permissions.join(",") : null,
|
||||
//"X-Permissions": options.permissions ? options.permissions.join(",") : null,
|
||||
},
|
||||
body: JSON.stringify({query: query.length > 256 ? compressQuery(query || "") : query, variables, operationName: options.operationName}),
|
||||
credentials: "include",
|
||||
@@ -31,6 +31,67 @@ function query(query, variables = {}, options = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {string} query GQL Query
|
||||
* @param {Object} variables Variables
|
||||
* @param {{token?:string,permissions?:string[]}} options
|
||||
*/
|
||||
function queryForm(query, variables = {}, options = {}) {
|
||||
const map = {}
|
||||
const files = []
|
||||
|
||||
function findFiles(prefix, data) {
|
||||
for (const key in data) {
|
||||
if (!data.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
const value = data[key]
|
||||
|
||||
if (value instanceof File) {
|
||||
map[files.length] = [`${prefix}.${key}`]
|
||||
files.push(value)
|
||||
} else if (typeof(value) === 'object') {
|
||||
data[key] = findFiles(`${prefix}.${key}`, Object.assign({}, value))
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
const operations = {
|
||||
query: query,
|
||||
variables: findFiles("variables", Object.assign({}, variables)),
|
||||
}
|
||||
|
||||
const form = new FormData()
|
||||
form.append("operations", JSON.stringify(operations))
|
||||
form.append("map", JSON.stringify(map))
|
||||
for (let i = 0; i < files.length; ++i) {
|
||||
form.append(i.toString(), files[i])
|
||||
}
|
||||
|
||||
console.log(form)
|
||||
|
||||
return fetch(config.graphqlEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": options.token ? `Bearer ${options.token}` : null,
|
||||
"X-Permissions": options.permissions ? options.permissions.join(",") : null,
|
||||
},
|
||||
body: form,
|
||||
credentials: "include",
|
||||
}).then(res => {
|
||||
return res.json()
|
||||
}).then(json => {
|
||||
if (json.errors != null && json.errors.length > 0) {
|
||||
return Promise.reject(json.errors)
|
||||
}
|
||||
|
||||
return json.data
|
||||
})
|
||||
}
|
||||
|
||||
class Subscription extends EventEmitter {
|
||||
constructor(query, variables) {
|
||||
super()
|
||||
@@ -115,4 +176,4 @@ class Subscription extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { query, Subscription }
|
||||
module.exports = { query, queryForm, Subscription }
|
||||
@@ -69,6 +69,12 @@ app.use(session)
|
||||
app.use(passport.initialize())
|
||||
app.use(passport.session())
|
||||
app.use(require("./middleware/locals"))
|
||||
|
||||
// Proxy (does not want all middleware)
|
||||
app.use("/graphql", require("./routes/graphql").router)
|
||||
app.use("/playground", proxy(config.playgroundEndpoint))
|
||||
|
||||
// The rest of the middleware
|
||||
app.use(bodyParser.json({limit: "1mb"}))
|
||||
app.use(bodyParser.text({limit: "256kb"}))
|
||||
|
||||
@@ -79,10 +85,6 @@ app.use(lassoMiddleware.serveStatic())
|
||||
// Authentication
|
||||
app.use("/auth", require("./routes/auth"))
|
||||
|
||||
// API Proxy
|
||||
app.use("/graphql", require("./routes/graphql"))
|
||||
app.use("/playground", proxy(config.playgroundEndpoint))
|
||||
|
||||
// Global css doesn't work with prebuild.
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
app.use("/hax/global.css", require("./routes/globalcss"))
|
||||
@@ -108,6 +110,7 @@ app.use("/data/", require("./routes/data"))
|
||||
app.use("/data/characters/", require("./routes/data/characters"))
|
||||
app.use("/data/channels/", require("./routes/data/channels"))
|
||||
app.use("/data/changes/", require("./routes/data/changes"))
|
||||
app.use("/data/files/", require("./routes/data/files"))
|
||||
|
||||
// Entry point
|
||||
app.get("/", function(req, res) {
|
||||
|
||||
Reference in New Issue
Block a user