@@ -0,0 +1,5 @@
|
||||
<div class=["right-menu", input.class]>
|
||||
<div class="menu-body">
|
||||
<include(input.renderBody) />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
.right-menu {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 28ch;
|
||||
|
||||
padding: 0.333em;
|
||||
padding-bottom: 1em;
|
||||
|
||||
text-align: left;
|
||||
|
||||
user-select: none;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
.showhide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.right-menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,11 @@ body {
|
||||
width: calc(100% - 32ch);
|
||||
margin-right: 0;
|
||||
}
|
||||
@media screen and (min-width: 700px) {
|
||||
main.with-right {
|
||||
width: calc(100% - 60ch)
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
characters: [],
|
||||
channels: [],
|
||||
eventNames: [],
|
||||
filters: [],
|
||||
search: "",
|
||||
activeMap: {
|
||||
events: {},
|
||||
characters: {},
|
||||
channels: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
onInput(input) {
|
||||
this.update(input)
|
||||
}
|
||||
|
||||
onMount(input) {
|
||||
this.getEl("search").addEventListener("keydown", ev => {
|
||||
if (this.waiting) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.timeout != null) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
if (ev.key === "Enter") {
|
||||
this.emit("add", "search", ev.target.value)
|
||||
this.getEl("search").value = ""
|
||||
}
|
||||
|
||||
this.timeout = setTimeout(() => {
|
||||
this.timeout = null
|
||||
this.state.search = ev.target.value
|
||||
|
||||
this.update(this.input)
|
||||
}, 200)
|
||||
})
|
||||
}
|
||||
|
||||
update(input) {
|
||||
if (input.filter) {
|
||||
this.updateActive(input)
|
||||
}
|
||||
|
||||
const search = this.state.search.toLocaleLowerCase()
|
||||
|
||||
this.state.characters = input.characters.filter(c => !this.state.activeMap.characters[c.id]).filter(c => this.matches(c.name, search)).sort((a,b) => a.name.localeCompare(b.name))
|
||||
this.state.channels = input.channels.filter(c => !this.state.activeMap.channels[c.name]).filter(c => this.matches(c.name, search)).sort((a,b) => a.name.localeCompare(b.name))
|
||||
this.state.eventNames = input.eventNames.filter(e => !this.state.activeMap.events[e]).filter(e => this.matches(e, search)).sort((a,b) => a.localeCompare(b))
|
||||
}
|
||||
|
||||
updateActive({filter, characters, channels, eventNames}) {
|
||||
const filters = []
|
||||
const activeMap = {
|
||||
events: {},
|
||||
characters: {},
|
||||
channels: {},
|
||||
}
|
||||
|
||||
if (filter.search != null) {
|
||||
filters.push({
|
||||
type: "search",
|
||||
key: "search",
|
||||
icon: "T",
|
||||
color: "color-text",
|
||||
text: filter.search,
|
||||
})
|
||||
}
|
||||
|
||||
for (const character of (filter.characters || [])) {
|
||||
activeMap.characters[character] = true
|
||||
|
||||
filters.push({
|
||||
type: "characters",
|
||||
key: character,
|
||||
icon: "C",
|
||||
color: "color-tag-character",
|
||||
id: character,
|
||||
text: (characters.find(c => c.id === character) || {name: "Unknown ("+character+")"}).name,
|
||||
})
|
||||
}
|
||||
|
||||
for (const channel of (filter.channels || [])) {
|
||||
activeMap.channels[channel] = true
|
||||
|
||||
filters.push({
|
||||
type: "channels",
|
||||
key: channel.replace(/'/g, "__"),
|
||||
icon: "#",
|
||||
color: "color-tag-location",
|
||||
id: channel,
|
||||
text: channel,
|
||||
})
|
||||
}
|
||||
|
||||
for (const event of (filter.events || [])) {
|
||||
activeMap.events[event] = true
|
||||
|
||||
filters.push({
|
||||
type: "events",
|
||||
key: event.replace(/['\s\.]/g, "__"),
|
||||
icon: "E",
|
||||
color: "color-tag-event",
|
||||
id: event,
|
||||
text: event,
|
||||
})
|
||||
}
|
||||
|
||||
this.state.filters = filters
|
||||
this.state.activeMap = activeMap
|
||||
}
|
||||
|
||||
matches(str, search) {
|
||||
if (search == "") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (search.length > str.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return str.toLowerCase().indexOf(search) !== -1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<right-menu class="filter-menu">
|
||||
<input key="search" />
|
||||
<menu-gap />
|
||||
<menu-header if(state.filters.length > 0)>Active Filters</menu-header>
|
||||
<div class="filter-section">
|
||||
<menu-link for(filter in state.filters)
|
||||
on-click("emit", "remove", filter.type, filter.id)
|
||||
textClass=filter.color
|
||||
unselectable
|
||||
icon=filter.icon >${filter.text}</menu-link>
|
||||
</div>
|
||||
<menu-gap if(state.filters.length > 0) />
|
||||
<menu-header>Characters</menu-header>
|
||||
<div class="filter-section">
|
||||
<menu-link for(character in state.characters)
|
||||
on-click("emit", "add", "characters", character.id)
|
||||
icon=character.name.charAt(0).toUpperCase()
|
||||
textClass="color-tag-character">${character.name}</menu-link>
|
||||
</div>
|
||||
<menu-gap />
|
||||
<menu-header>Channels</menu-header>
|
||||
<div class="filter-section">
|
||||
<menu-link for(channel in state.channels)
|
||||
on-click("emit", "add", "channels", channel.name)
|
||||
icon=channel.name.charAt(1).toUpperCase()
|
||||
textClass="color-tag-location">${channel.name}</menu-link>
|
||||
</div>
|
||||
<menu-gap />
|
||||
<menu-header>Events</menu-header>
|
||||
<div class="filter-section">
|
||||
<menu-link for(eventName in state.eventNames)
|
||||
on-click("emit", "add", "events", eventName)
|
||||
icon=eventName.charAt(0).toUpperCase()
|
||||
textClass="color-tag-event">${eventName}</menu-link>
|
||||
</div>
|
||||
</right-menu>
|
||||
@@ -0,0 +1,24 @@
|
||||
div.right-menu.filter-menu {
|
||||
input {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
width: 95%;
|
||||
margin-top: 1em;
|
||||
margin-left: 2.5%;
|
||||
border: 1px solid rgba(119, 119, 119, 0.25);
|
||||
font-size: 1.1em;
|
||||
box-sizing: border-box;
|
||||
|
||||
color: #999;
|
||||
}
|
||||
input:focus {
|
||||
color: #FC1;
|
||||
}
|
||||
|
||||
div.filter-section {
|
||||
max-height: calc(25vh - 3em);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
@@ -13,54 +13,7 @@ module.exports = class {
|
||||
return
|
||||
}
|
||||
|
||||
const filters = []
|
||||
const {characters, channels, events, search} = input.filter
|
||||
|
||||
if (search != null) {
|
||||
filters.push({
|
||||
type: "search",
|
||||
key: "search",
|
||||
icon: "T",
|
||||
color: "color-text",
|
||||
text: search,
|
||||
})
|
||||
}
|
||||
|
||||
for (const character of (characters || [])) {
|
||||
filters.push({
|
||||
type: "characters",
|
||||
key: character,
|
||||
icon: "C",
|
||||
color: "color-tag-character",
|
||||
id: character,
|
||||
text: (input.characters.find(c => c.id === character) || {name: "Unknown ("+character+")"}).name,
|
||||
})
|
||||
}
|
||||
|
||||
for (const channel of (channels || [])) {
|
||||
filters.push({
|
||||
type: "channels",
|
||||
key: channel.replace(/'/g, "__"),
|
||||
icon: "#",
|
||||
color: "color-tag-location",
|
||||
id: channel,
|
||||
text: channel,
|
||||
})
|
||||
}
|
||||
|
||||
for (const event of (events || [])) {
|
||||
filters.push({
|
||||
type: "events",
|
||||
key: event.replace(/['\s\.]/g, "__"),
|
||||
icon: "E",
|
||||
color: "color-tag-event",
|
||||
id: event,
|
||||
text: event,
|
||||
})
|
||||
}
|
||||
|
||||
this.state.filters = filters
|
||||
this.state.filtered = (filters.length > 0)
|
||||
this.state.filtered = input.filter.search || input.filter.channels || input.filter.characters || input.filter.events
|
||||
}
|
||||
|
||||
select(value) {
|
||||
|
||||
@@ -4,8 +4,4 @@
|
||||
<if-permitted key="if-permitted" user=input.user permission="log.add">
|
||||
<menu-link key="add_log" dark on-click("select", "log.add") icon="+">Add Log</menu-link>
|
||||
</if-permitted>
|
||||
<menu-gap />
|
||||
<menu-header key="filters">Filters</menu-header>
|
||||
<menu-link for(filter in state.filters) textClass=filter.color unselectable icon=filter.icon on-click("emit", "removefilter", filter.type, filter.id)>${filter.text}</menu-link>
|
||||
<menu-link key="add_filter" dark on-click("select", "filter.add") icon="+">Add Filter</menu-link>
|
||||
</menu>
|
||||
@@ -10,11 +10,17 @@ tr.logs-table-row {
|
||||
}
|
||||
|
||||
td.expand {
|
||||
width: 2%;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
td.channel, td.date, td.event {
|
||||
width: 12%;
|
||||
td.date {
|
||||
width: 16%;
|
||||
}
|
||||
td.channel {
|
||||
width: 14%;
|
||||
}
|
||||
td.event {
|
||||
width: 18%;
|
||||
}
|
||||
|
||||
td.character {
|
||||
|
||||
@@ -9,7 +9,7 @@ table.logs-table {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
@media screen and (max-width: 1200px) {
|
||||
td.event, th.event {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -19,11 +19,18 @@ module.exports = class {
|
||||
|
||||
addFilter(type, filter) {
|
||||
if (type === "search") {
|
||||
if (filter.trim().length > 0) {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {search: filter})
|
||||
} else {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {search: null})
|
||||
}
|
||||
|
||||
} else {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {[type]: (this.state.filter[type] || []).filter(f => f !== filter).concat(filter)})
|
||||
}
|
||||
|
||||
this.updateQuery(this.state.filter)
|
||||
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
@@ -41,6 +48,31 @@ module.exports = class {
|
||||
this.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
this.updateQuery(this.state.filter)
|
||||
}
|
||||
|
||||
updateQuery(filter) {
|
||||
const queries = []
|
||||
|
||||
if (filter.characters) {
|
||||
queries.push("characters=" + filter.characters.join(","))
|
||||
}
|
||||
if (filter.channels) {
|
||||
queries.push("channels=" + filter.channels.map(c => encodeURIComponent(c)).join(","))
|
||||
}
|
||||
if (filter.events) {
|
||||
queries.push("events=" + filter.events.map(e => encodeURIComponent(e)).join(","))
|
||||
}
|
||||
if (filter.search) {
|
||||
queries.push("search=" + encodeURIComponent(filter.search))
|
||||
}
|
||||
|
||||
if (queries.length > 0) {
|
||||
history.replaceState("", "", `/logs/?${queries.join("&")}`)
|
||||
} else {
|
||||
history.replaceState("", "", "/logs/")
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<background src="/assets/images/bg.png" opacity=0.25 />
|
||||
<logs-menu key="menu" on-select("open") on-removefilter("removeFilter") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters />
|
||||
<main>
|
||||
<logs-menu key="menu" on-select("open") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters />
|
||||
<filter-menu on-add("addFilter") on-remove("removeFilter") filter=state.filter characters=input.characters channels=input.channels eventNames=input.eventNames />
|
||||
<main class="with-right">
|
||||
<logs-table logs=state.logs on-addfilter("addFilter") />
|
||||
</main>
|
||||
<add-log-modal enabled=(state.modal === "log.add") on-close("close") />
|
||||
<add-filter-modal enabled=(state.modal === "filter.add") characters=input.characters filter=state.filter on-add("addFilter") on-close("close") />
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<include("../layout", {title: "Logs", site: "logs"})>
|
||||
<@body>
|
||||
<!-- Page needed to get component.js functionality -->
|
||||
<page logs=input.logs filter=input.filter characters=input.characters user=input.user selected=input.selected />
|
||||
<page logs=input.logs
|
||||
filter=input.filter
|
||||
characters=input.characters
|
||||
channels=input.channels
|
||||
eventNames=input.eventNames
|
||||
user=input.user
|
||||
selected=input.selected />
|
||||
</@body>
|
||||
</include>
|
||||
@@ -3,6 +3,7 @@ const router = express.Router()
|
||||
|
||||
const {logHeaderApi, eventNames} = require("../../rpdata/api/LogHeader")
|
||||
const {charactersApi} = require("../../rpdata/api/Character")
|
||||
const {channelApi} = require("../../rpdata/api/Channel")
|
||||
|
||||
const listTemplate = require("../../marko/page/logs/list.marko")
|
||||
|
||||
@@ -27,6 +28,8 @@ router.get("/", async(req, res) => {
|
||||
filter: filter,
|
||||
logs: logHeaderApi.list(filter),
|
||||
characters: charactersApi.listHeaders(),
|
||||
channels: channelApi.list(),
|
||||
eventNames: logHeaderApi.eventNames(),
|
||||
selected: {index: true},
|
||||
})
|
||||
} catch(err) {
|
||||
|
||||
Reference in New Issue
Block a user