diff --git a/marko/components/menu-link/component.js b/marko/components/menu-link/component.js index b8dd466..8a41406 100644 --- a/marko/components/menu-link/component.js +++ b/marko/components/menu-link/component.js @@ -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) diff --git a/marko/page/logs-content/components/logs-content-menu/index.marko b/marko/page/logs-content/components/logs-content-menu/index.marko index aff68bf..88e4ec1 100644 --- a/marko/page/logs-content/components/logs-content-menu/index.marko +++ b/marko/page/logs-content/components/logs-content-menu/index.marko @@ -5,7 +5,7 @@ Links - ${input.log.channel.name} + ${input.log.channel.name} ${input.log.channel.eventName} ${character.name} (${character.author}) diff --git a/marko/page/logs/components/add-filter-modal-body/component.js b/marko/page/logs/components/add-filter-modal-body/component.js new file mode 100644 index 0000000..ee3f38a --- /dev/null +++ b/marko/page/logs/components/add-filter-modal-body/component.js @@ -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 + } + +} \ No newline at end of file diff --git a/marko/page/logs/components/add-filter-modal-body/index.marko b/marko/page/logs/components/add-filter-modal-body/index.marko new file mode 100644 index 0000000..85d619f --- /dev/null +++ b/marko/page/logs/components/add-filter-modal-body/index.marko @@ -0,0 +1,12 @@ + + + + +
0) class="filter-row" > +
Search: ${state.search}
+ +
+
+
${filter.text}
+ +
\ No newline at end of file diff --git a/marko/page/logs/components/add-filter-modal/component.js b/marko/page/logs/components/add-filter-modal/component.js new file mode 100644 index 0000000..fb600a3 --- /dev/null +++ b/marko/page/logs/components/add-filter-modal/component.js @@ -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") + } +} \ No newline at end of file diff --git a/marko/page/logs/components/add-filter-modal/index.marko b/marko/page/logs/components/add-filter-modal/index.marko new file mode 100644 index 0000000..1ec5414 --- /dev/null +++ b/marko/page/logs/components/add-filter-modal/index.marko @@ -0,0 +1,7 @@ +import moment from "moment" + + +

Add Filter

+ + +
\ No newline at end of file diff --git a/marko/page/logs/components/add-filter-modal/style.less b/marko/page/logs/components/add-filter-modal/style.less new file mode 100644 index 0000000..6701f83 --- /dev/null +++ b/marko/page/logs/components/add-filter-modal/style.less @@ -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; + } +} \ No newline at end of file diff --git a/marko/page/logs/components/logs-menu/component.js b/marko/page/logs/components/logs-menu/component.js index ae6c37b..13fd610 100644 --- a/marko/page/logs/components/logs-menu/component.js +++ b/marko/page/logs/components/logs-menu/component.js @@ -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) } diff --git a/marko/page/logs/components/logs-menu/index.marko b/marko/page/logs/components/logs-menu/index.marko index 7c43e7e..af3657f 100644 --- a/marko/page/logs/components/logs-menu/index.marko +++ b/marko/page/logs/components/logs-menu/index.marko @@ -1,10 +1,12 @@ Logs - All + All - Add Log + Add Log Filters + ${filter.text} + Add Filter \ No newline at end of file diff --git a/marko/page/logs/components/page/component.js b/marko/page/logs/components/page/component.js index b16f5e3..0a6ecbb 100644 --- a/marko/page/logs/components/page/component.js +++ b/marko/page/logs/components/page/component.js @@ -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 }) } diff --git a/marko/page/logs/components/page/index.marko b/marko/page/logs/components/page/index.marko index f5fe3dd..dbf9daa 100644 --- a/marko/page/logs/components/page/index.marko +++ b/marko/page/logs/components/page/index.marko @@ -1,6 +1,7 @@ - +
- + + diff --git a/marko/page/logs/list.marko b/marko/page/logs/list.marko index e6dc5d6..99741c2 100644 --- a/marko/page/logs/list.marko +++ b/marko/page/logs/list.marko @@ -1,6 +1,6 @@ <@body> - + \ No newline at end of file diff --git a/routes/logs/index.js b/routes/logs/index.js index eff2f97..2272189 100644 --- a/routes/logs/index.js +++ b/routes/logs/index.js @@ -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) { diff --git a/rpdata/api/Character.js b/rpdata/api/Character.js index c8355de..40c8e5d 100644 --- a/rpdata/api/Character.js +++ b/rpdata/api/Character.js @@ -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} + * @returns {Promise} */ 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} + */ + 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. * diff --git a/rpdata/api/LogHeader.js b/rpdata/api/LogHeader.js index d34eb12..8856654 100644 --- a/rpdata/api/LogHeader.js +++ b/rpdata/api/LogHeader.js @@ -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} + */ + 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} @@ -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} \ No newline at end of file +module.exports = {LogHeader, LogHeaderCharacter, logHeaderApi, eventNames} \ No newline at end of file