logs: Added filtering.
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
onCreate() {
|
||||
this.state = {
|
||||
classes: ["menu-link", "color-menu"],
|
||||
classes: [],
|
||||
}
|
||||
}
|
||||
|
||||
onInput(input) {
|
||||
this.state.classes = ["menu-link", "color-menu"]
|
||||
|
||||
if (input.dark) {
|
||||
this.state.classes.push(input)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<menu-gap />
|
||||
</if-permitted>
|
||||
<menu-header>Links</menu-header>
|
||||
<menu-link href=("/logs/?channels="+input.log.channel.name) icon="#">${input.log.channel.name}</menu-link>
|
||||
<menu-link href=("/logs/?channels="+encodeURIComponent(input.log.channel.name)) icon="#">${input.log.channel.name}</menu-link>
|
||||
<menu-link if(input.log.channel.eventName) href=("/logs/?channels="+input.log.channel.eventName) icon="E">${input.log.channel.eventName}</menu-link>
|
||||
<for(character in input.log.characters)>
|
||||
<menu-link href=("/logs/?characters="+character.id) icon="C">${character.name} <span class="weak">(${character.author})</span></menu-link>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
const {channelApi} = require("../../../../../rpdata/api/Channel")
|
||||
const {logHeaderApi} = require("../../../../../rpdata/api/LogHeader")
|
||||
|
||||
module.exports = class {
|
||||
onCreate() {
|
||||
this.state = {
|
||||
search: "",
|
||||
filters: [],
|
||||
channels: [],
|
||||
eventNames: [],
|
||||
}
|
||||
|
||||
this.first = true
|
||||
}
|
||||
|
||||
onInput(input) {
|
||||
if (input.filters) {
|
||||
if (this.timeout != null) {
|
||||
setTimeout(() => {
|
||||
this.searchFilters()
|
||||
}, 1)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(input.eventNames)
|
||||
}
|
||||
|
||||
onMount() {
|
||||
this.timeout = null
|
||||
|
||||
if (this.state.channels.length === 0) {
|
||||
channelApi.list().then(channels => {
|
||||
this.state.channels = channels
|
||||
this.searchFilters()
|
||||
}).catch(err => {
|
||||
console.warn("Failed to fetch channels:", err)
|
||||
})
|
||||
}
|
||||
|
||||
if (this.state.eventNames.length === 0) {
|
||||
logHeaderApi.eventNames().then(eventNames => {
|
||||
this.state.eventNames = eventNames
|
||||
this.searchFilters()
|
||||
}).catch(err => {
|
||||
console.warn("Failed to fetch eventNames:", err)
|
||||
})
|
||||
}
|
||||
|
||||
this.getEl("search").addEventListener("keydown", ev => {
|
||||
if (this.waiting) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.timeout != null) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
this.timeout = setTimeout(() => {
|
||||
this.timeout = null
|
||||
this.state.search = ev.target.value
|
||||
|
||||
this.searchFilters()
|
||||
}, 200)
|
||||
})
|
||||
}
|
||||
|
||||
onUnmount() {
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = null
|
||||
|
||||
this.state.search = ""
|
||||
this.state.filters = []
|
||||
}
|
||||
|
||||
add(type, value, filter) {
|
||||
this.emit("add", type, value)
|
||||
|
||||
this.state.filters = this.state.filters.filter(f => f != filter)
|
||||
}
|
||||
|
||||
searchFilters() {
|
||||
const filters = []
|
||||
const search = this.state.search.toLowerCase()
|
||||
|
||||
for (const channel of this.state.channels) {
|
||||
if (search == "" || channel.name.toLowerCase().includes(search)) {
|
||||
if (this.input.filter.channels && this.input.filter.channels.includes(channel.name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
filters.push({
|
||||
colorClass: "color-tag-location",
|
||||
text: channel.name,
|
||||
type: "channels",
|
||||
value: channel.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const name of this.state.eventNames) {
|
||||
if (search == "" || name.toLowerCase().includes(search)) {
|
||||
if (this.input.filter.events && this.input.filter.events.includes(name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
filters.push({
|
||||
colorClass: "color-tag-event",
|
||||
text: name,
|
||||
type: "events",
|
||||
value: name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const character of this.input.characters) {
|
||||
if (search == "" || character.name.toLowerCase().includes(search)) {
|
||||
if (this.input.filter.characters && this.input.filter.characters.includes(character.id)) {
|
||||
continue
|
||||
}
|
||||
|
||||
filters.push({
|
||||
colorClass: "color-tag-character",
|
||||
text: character.name,
|
||||
type: "characters",
|
||||
value: character.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
filters.sort((a, b) => {
|
||||
let aCompare = a.text
|
||||
let bCompare = b.text
|
||||
|
||||
if (a.text.charAt(0) === "#") {
|
||||
aCompare = a.text.slice(1)
|
||||
}
|
||||
if (b.text.charAt(0) === "#") {
|
||||
bCompare = b.text.slice(1)
|
||||
}
|
||||
|
||||
return aCompare.localeCompare(bCompare)
|
||||
})
|
||||
|
||||
this.state.filters = filters
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<label>Search</label>
|
||||
<input key="search" placeholder="" class="big" value=state.search />
|
||||
|
||||
<label>Filters</label>
|
||||
<div if(state.search.length > 0) class="filter-row" >
|
||||
<div class=["content", "color-text"]>Search: ${state.search}</div>
|
||||
<button disabled=state.loading on-click("emit", "add", "search", state.search)>${input.filter.search ? "Replace" : "Add"}</button>
|
||||
</div>
|
||||
<div for(filter in state.filters) class="filter-row" >
|
||||
<div class=["content", filter.colorClass]>${filter.text}</div>
|
||||
<button disabled=state.loading on-click("add", filter.type, filter.value, filter)>Add</button>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
const moment = require("moment")
|
||||
|
||||
const {logsApi} = require("../../../../../rpdata/api/Log")
|
||||
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
filters: [],
|
||||
search: "",
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.emit("close")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import moment from "moment"
|
||||
|
||||
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") >
|
||||
<h1 key="stuff">Add Filter</h1>
|
||||
|
||||
<add-filter-modal-body characters=input.characters filter=input.filter on-add("emit", "add") />
|
||||
</modal>
|
||||
@@ -0,0 +1,16 @@
|
||||
div.filter-row {
|
||||
padding: 0.25em;
|
||||
border-bottom: 1px solid rgba(0, 220, 255, 0.125);
|
||||
|
||||
div.content {
|
||||
width: 80%;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 20% !important;
|
||||
display: inline-block !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,68 @@
|
||||
module.exports = class {
|
||||
onCreate() {
|
||||
this.state = {
|
||||
filters: [],
|
||||
filtered: false,
|
||||
}
|
||||
}
|
||||
|
||||
onInput(input) {
|
||||
if (!input.filter) {
|
||||
this.state.filters = []
|
||||
this.state.filtered = false
|
||||
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)
|
||||
}
|
||||
|
||||
select(value) {
|
||||
this.emit("select", value)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<menu user=input.user>
|
||||
<menu-header key="logs">Logs</menu-header>
|
||||
<menu-link key="logs" selected=input.selected.index icon="L" href="/logs/">All</menu-link>
|
||||
<menu-link key="logs" selected=(input.selected.index && !state.filtered) icon="L" href="/logs/">All</menu-link>
|
||||
<if-permitted key="if-permitted" user=input.user permission="log.add">
|
||||
<menu-link key="add_log" dark on-click("select", "add") icon="+">Add Log</menu-link>
|
||||
<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-gap />
|
||||
</menu>
|
||||
@@ -3,6 +3,7 @@ const {logHeaderApi} = require("../../../../../rpdata/api/LogHeader")
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
filter: input.filter,
|
||||
logs: input.logs,
|
||||
modal: null,
|
||||
}
|
||||
@@ -16,8 +17,40 @@ module.exports = class {
|
||||
this.state.modal = null
|
||||
}
|
||||
|
||||
onMount() {
|
||||
logHeaderApi.list({limit: 0}).then(logs => {
|
||||
addFilter(type, filter) {
|
||||
if (type === "search") {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {search: filter})
|
||||
} else {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {[type]: (this.state.filter[type] || []).concat(filter)})
|
||||
}
|
||||
|
||||
console.log("FILTER:", this.state.filter)
|
||||
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
removeFilter(type, filter) {
|
||||
console.log(type, filter, this.state.filter)
|
||||
|
||||
if (type === "search") {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {search: null})
|
||||
this.refresh()
|
||||
} else if ((this.state.filter[type] || []).length === 1) {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {[type]: null})
|
||||
this.refresh()
|
||||
} else {
|
||||
const index = (this.state.filter[type] || []).indexOf(filter)
|
||||
if (index !== -1) {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {[type]: this.state.filter[type].splice(0, index).concat(this.state.filter[type].splice(index + 1))},)
|
||||
this.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
console.log("REFRESH", this.state.filter)
|
||||
|
||||
logHeaderApi.list(this.state.filter).then(logs => {
|
||||
this.state.logs = logs
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<background src="/assets/images/bg.png" opacity=0.25 />
|
||||
<logs-menu key="menu" on-select("open") selected=(input.selected || {}) user=input.user />
|
||||
<logs-menu key="menu" on-select("open") on-removefilter("removeFilter") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters />
|
||||
<main>
|
||||
<logs-list logs=state.logs />
|
||||
</main>
|
||||
<add-log-modal enabled=(state.modal === "add") on-close("close") />
|
||||
<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,6 @@
|
||||
<include("../layout", {title: "Logs", site: "logs"})>
|
||||
<@body>
|
||||
<!-- Page needed to get component.js functionality -->
|
||||
<page logs=input.logs user=input.user selected=input.selected />
|
||||
<page logs=input.logs filter=input.filter characters=input.characters user=input.user selected=input.selected />
|
||||
</@body>
|
||||
</include>
|
||||
+20
-2
@@ -1,14 +1,32 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
|
||||
const {logHeaderApi} = require("../../rpdata/api/LogHeader")
|
||||
const {logHeaderApi, eventNames} = require("../../rpdata/api/LogHeader")
|
||||
const {charactersApi} = require("../../rpdata/api/Character")
|
||||
|
||||
const listTemplate = require("../../marko/page/logs/list.marko")
|
||||
|
||||
router.get("/", async(req, res) => {
|
||||
const filter = {limit: 0}
|
||||
|
||||
if (req.query.characters) {
|
||||
filter.characters = req.query.characters.split(",")
|
||||
}
|
||||
if (req.query.channels) {
|
||||
filter.channels = req.query.channels.split(",")
|
||||
}
|
||||
if (req.query.events) {
|
||||
filter.events = req.query.events.split(",")
|
||||
}
|
||||
if (req.query.search) {
|
||||
filter.search = req.query.search
|
||||
}
|
||||
|
||||
try {
|
||||
res.markoAsync(listTemplate, {
|
||||
logs: logHeaderApi.list({limit: 0}),
|
||||
filter: filter,
|
||||
logs: logHeaderApi.list(filter),
|
||||
characters: charactersApi.listHeaders(),
|
||||
selected: {index: true},
|
||||
})
|
||||
} catch(err) {
|
||||
|
||||
+41
-8
@@ -27,21 +27,34 @@ class Character {
|
||||
}
|
||||
}
|
||||
|
||||
class CharacterHeader {
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {string} author
|
||||
* @param {string} name
|
||||
*/
|
||||
constructor(id, author, name) {
|
||||
this.id = id
|
||||
this.author = author
|
||||
this.name = name
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a character object from data.
|
||||
*/
|
||||
static fromData(data) {
|
||||
return new CharacterHeader(data.id, data.author, data.name)
|
||||
}
|
||||
}
|
||||
|
||||
class ChracterAPI {
|
||||
/**
|
||||
* Call `characters(filter)` query
|
||||
*
|
||||
* @param {{ids:string[], nicks:string[], names:string[], author:string, search:string, logged:boolean}} filter
|
||||
* @returns {Promise<Story[]>}
|
||||
* @returns {Promise<Character[]>}
|
||||
*/
|
||||
list(filter = {}) {
|
||||
if (filter.earliestFictionalDate != null && typeof(filter.earliestFictionalDate) !== "string") {
|
||||
filter.earliestFictionalDate = filter.earliestFictionalDate.toISOString()
|
||||
}
|
||||
if (filter.latestFictionalDate != null && typeof(filter.latestFictionalDate) !== "string") {
|
||||
filter.latestFictionalDate = filter.latestFictionalDate.toISOString()
|
||||
}
|
||||
|
||||
return query(`
|
||||
query ListCharacters($filter: CharactersFilter) {
|
||||
characters(filter: $filter) {
|
||||
@@ -58,6 +71,26 @@ class ChracterAPI {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `characters(filter)` query
|
||||
*
|
||||
* @param {{ids:string[], nicks:string[], names:string[], author:string, search:string, logged:boolean}} filter
|
||||
* @returns {Promise<CharacterHeader[]>}
|
||||
*/
|
||||
listHeaders(filter = {}) {
|
||||
return query(`
|
||||
query ListCharacters($filter: CharactersFilter) {
|
||||
characters(filter: $filter) {
|
||||
id
|
||||
name
|
||||
author
|
||||
}
|
||||
}
|
||||
`, {filter}).then(({characters}) => {
|
||||
return characters.map(d => CharacterHeader.fromData(d))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `addCharacter(input)` mutation, returns addable fields.
|
||||
*
|
||||
|
||||
+36
-3
@@ -1,5 +1,7 @@
|
||||
const {query} = require("../client")
|
||||
|
||||
let eventNames = []
|
||||
|
||||
class LogHeader {
|
||||
/**
|
||||
* Construct a log header. You should probably use the logHeaderApi instead of doing
|
||||
@@ -50,7 +52,38 @@ class LogHeaderCharacter {
|
||||
*/
|
||||
const logHeaderApi = {
|
||||
/**
|
||||
* Call `stories(filter)` query
|
||||
* Call `logs` query, returns event name
|
||||
*
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
eventNames() {
|
||||
return query(`
|
||||
query LogHeadersEventNames() {
|
||||
headers: logs(filter:{limit:0}) {
|
||||
eventName
|
||||
}
|
||||
}
|
||||
`, {}).then(({headers}) => {
|
||||
const eventNames = []
|
||||
const seen = {}
|
||||
|
||||
for (const header of headers) {
|
||||
if (!header.eventName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!seen[header.eventName]) {
|
||||
eventNames.push(header.eventName)
|
||||
seen[header.eventName] = true
|
||||
}
|
||||
}
|
||||
|
||||
return eventNames
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Call `logs(filter)` query
|
||||
*
|
||||
* @param {{search:string, channels:string|string[], events:string|string[], open:boolean, characters:string|string[], limit:number}} filter
|
||||
* @returns {Promise<LogHeader[]>}
|
||||
@@ -76,9 +109,9 @@ const logHeaderApi = {
|
||||
}
|
||||
}
|
||||
`, {filter}).then(({headers}) => {
|
||||
return headers.map(h => new LogHeader(h.id, h.shortId, h.date, h.channelName, h.title, h.description, h.eventName, h.open, h.characters))
|
||||
return headers.map(h => new LogHeader(h.id, h.shortId, h.date, h.channelName, h.title, h.description, h.eventName, h.open, h.characters))
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = {LogHeader, LogHeaderCharacter, logHeaderApi}
|
||||
module.exports = {LogHeader, LogHeaderCharacter, logHeaderApi, eventNames}
|
||||
Reference in New Issue
Block a user