From e0bc9e30b701cae19014c7c0a6c83fb87de93c6c Mon Sep 17 00:00:00 2001 From: John Ghatas Date: Fri, 24 Apr 2020 12:21:04 +0200 Subject: [PATCH] feat: Added detail screen * feat: added detail screen Co-authored-by: MauritsioRK Co-authored-by: Martijn Vegter --- .eslintrc | 3 +- .gitignore | 3 + .../repositories/InMemoryLogRepository.js | 34 +++++++++- lib/public/Model.js | 2 + lib/public/components/Post/index.js | 35 +++++++++++ lib/public/components/Table/index.js | 11 ++-- lib/public/view.js | 14 +++-- lib/public/views/Overview/Details/page.js | 40 ++++++++++++ lib/public/views/Overview/General/page.js | 8 +-- lib/public/views/Overview/Overview.js | 62 ++++++++++--------- test/public/overview.test.js | 46 +++++++++++--- 11 files changed, 204 insertions(+), 54 deletions(-) create mode 100644 lib/public/components/Post/index.js create mode 100644 lib/public/views/Overview/Details/page.js diff --git a/.eslintrc b/.eslintrc index 73577c3739..135e5edc55 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,7 @@ "node": true }, "parserOptions": { - "ecmaVersion": 8, + "ecmaVersion": 2018, "sourceType": "module" }, "extends": [ @@ -58,6 +58,7 @@ ], "init-declarations": "off", "key-spacing": "error", + "keyword-spacing": "error", "linebreak-style": "off", "lines-around-comment": [ "error", diff --git a/.gitignore b/.gitignore index 3eb381dee9..4bc8190f02 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,6 @@ tmp/ temp/ # End of https://www.gitignore.io/api/node + +# Ignore .vscode folder +.vscode diff --git a/lib/framework/persistence/repositories/InMemoryLogRepository.js b/lib/framework/persistence/repositories/InMemoryLogRepository.js index 653fafaca4..b807c58fc7 100644 --- a/lib/framework/persistence/repositories/InMemoryLogRepository.js +++ b/lib/framework/persistence/repositories/InMemoryLogRepository.js @@ -25,10 +25,40 @@ class InMemoryLogRepository extends LogRepository { /** * Returns all entities. * - * @returns {Promise} Promise object represents the ... + * @returns {Promise} Promise object representing the full mock data */ async findAll() { - return Promise.resolve([]); + const date = new Date().getTime(); + return Promise.resolve([ + { + entryID: 1, + authorID: 'Batman', + title: 'Run1', + creationTime: date, + tags: ['Tag1', 'Tag2'], + content: [ + { content: 'Batman wrote this...', sender: 'Batman' }, + { content: 'Nightwing wrote this...', sender: 'Nightwing' }, + { content: 'Gordon wrote this...', sender: 'Commissioner Gordon' }, + ], + }, + { + entryID: 2, + authorID: 'Joker', + title: 'Run2', + creationTime: date, + tags: ['Tag2'], + content: [{ content: 'Something about run2...', sender: 'Joker' }], + }, + { + entryID: 3, + authorID: 'Anonymous', + title: 'Run5', + creationTime: date, + tags: ['Tag3'], + content: [{ content: 'Ipem lorum...', sender: 'Anonymous' }], + }, + ]); } } diff --git a/lib/public/Model.js b/lib/public/Model.js index f9a590af5b..df659a5707 100644 --- a/lib/public/Model.js +++ b/lib/public/Model.js @@ -61,6 +61,8 @@ export default class Model extends Observable { switch (this.router.params.page) { case 'home': break; + case 'entry': + break; default: this.router.go('?page=home'); break; diff --git a/lib/public/components/Post/index.js b/lib/public/components/Post/index.js new file mode 100644 index 0000000000..388278bcf4 --- /dev/null +++ b/lib/public/components/Post/index.js @@ -0,0 +1,35 @@ +/** + * This file is part of the ALICE Electronic Logbook v2, also known as Jiskefet. + * Copyright (C) 2020 Stichting Hogeschool van Amsterdam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { h } from '/js/src/index.js'; + +/** + * A singular post which is part of a log + * @param {Object} post all data related to the post + * @param {Number} index the identification index of the post + * @return {vnode} Returns the navbar + */ +const entry = (post, index) => + h('.flex-column.p2.shadow-level1.mv2', { + id: `post${index}`, + }, [ + h('.f7.gray-darker', { style: 'align-self: flex-end;' }, `#${index}`), + h('.w-100.bg-gray-light.mv1.ph1', post.content), + h('.w-75.mv1.ph1', `Written by: ${post.sender}`), + ]); + +export default entry; diff --git a/lib/public/components/Table/index.js b/lib/public/components/Table/index.js index 946860b0a6..b3fcc8c381 100644 --- a/lib/public/components/Table/index.js +++ b/lib/public/components/Table/index.js @@ -35,14 +35,15 @@ const rowData = (data) => [h('td', data)]; * Renders the table * @param {Array} data The data array containing the objects with the data per row * @param {Array} headers The array of of the headers to be rendered in the table + * @param {Object} model Model passed for use with routing to the correct detail view * @returns {vnode} Return the total view of the table to rendered */ -const table = (data, headers) => h('table.table.shadow-level1.mh3', [ +const table = (data, headers, model) => h('table.table.shadow-level1.mh3', [ h('tr', [headers.map((header) => rowHeader(header))]), - data.map((entry, index) => h('tr', [ - rowData(index + 1), - Object.keys(entry).map((subItem) => rowData(entry[subItem])), - ])), + data.map((entry, index) => h(`tr#row${index + 1}`, { + onclick: () => model.router.go(`?page=entry&id=${entry[0]}`), + + }, [Object.keys(entry).map((subItem) => rowData(entry[subItem]) )])), ]); export { table }; diff --git a/lib/public/view.js b/lib/public/view.js index c41b534457..cafa10ff1f 100644 --- a/lib/public/view.js +++ b/lib/public/view.js @@ -19,6 +19,7 @@ import { h, switchCase } from '/js/src/index.js'; import NavBar from './components/NavBar/index.js'; import GeneralOverview from './views/Overview/General/page.js'; +import DetailsView from './views/Overview/Details/page.js'; /** * Main view layout @@ -26,14 +27,18 @@ import GeneralOverview from './views/Overview/General/page.js'; * @return {vnode} application view to be drawn according to model */ export default (model) => { - const pages = { + const navigationPages = { home: GeneralOverview, }; + const subPages = { + entry: DetailsView, + }; + return [ h('.flex-column.absolute-fill', [ - NavBar(model, pages), - content(model, pages), + NavBar(model, navigationPages), + content(model, { ...navigationPages, ...subPages }), ]), ]; }; @@ -44,4 +49,5 @@ export default (model) => { * @param {Object} pages Pass the pages to the switchcase * @returns {vnode} Returns a vnode to render the pages */ -const content = (model, pages) => h('.p4', switchCase(model.router.params.page, pages)(model)); +const content = (model, pages) => + h('.p4', switchCase(model.router.params.page, pages)(model)); diff --git a/lib/public/views/Overview/Details/page.js b/lib/public/views/Overview/Details/page.js new file mode 100644 index 0000000000..118aa17b0b --- /dev/null +++ b/lib/public/views/Overview/Details/page.js @@ -0,0 +1,40 @@ +/** + * This file is part of the ALICE Electronic Logbook v2, also known as Jiskefet. + * Copyright (C) 2020 Stichting Hogeschool van Amsterdam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { h } from '/js/src/index.js'; +import PostBox from '../../../components/Post/index.js'; + +/** + * A collection of details relating to a log + * @param {object} model Pass the model to access the defined functions + * @return {vnode} Return the view of the table with the filtering options + */ +const overviewScreen = (model) => { + const data = model.overview.getData(); + const id = parseInt(model.router.params.id); + let posts; + + data.forEach((entry) => { + if (entry.entryID === id) { + posts = entry.content; + } + }); + + return h('.w-100.flex-column', [posts.map((post, index) => h('.w-100', PostBox(post, index + 1)))]); +}; + +export default (model) => [overviewScreen(model)]; diff --git a/lib/public/views/Overview/General/page.js b/lib/public/views/Overview/General/page.js index 63c30cfe9c..864b43b3d4 100644 --- a/lib/public/views/Overview/General/page.js +++ b/lib/public/views/Overview/General/page.js @@ -19,8 +19,6 @@ import { h } from '/js/src/index.js'; import filters from '../../../components/Filters/index.js'; import { table } from '../../../components/Table/index.js'; -export default (model) => [overviewScreen(model)]; - /** * Table row header * @param {object} model Pass the model to access the defined functions @@ -28,11 +26,13 @@ export default (model) => [overviewScreen(model)]; */ const overviewScreen = (model) => { const headers = model.overview.getHeaders(); - const data = model.overview.getTableData(); + const data = model.overview.getDataWithoutTags(); const tags = model.overview.getTagCounts(); return h('.w-100.flex-row', [ filters(model, tags), - h('.w-75', [table(data, headers)]), + h('.w-75', [table(data, headers, model)]), ]); }; + +export default (model) => [overviewScreen(model)]; diff --git a/lib/public/views/Overview/Overview.js b/lib/public/views/Overview/Overview.js index ab09b5d2d2..90845d54f2 100644 --- a/lib/public/views/Overview/Overview.js +++ b/lib/public/views/Overview/Overview.js @@ -15,7 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { Observable } from '/js/src/index.js'; +import { Observable, fetchClient } from '/js/src/index.js'; /** * Model representing handlers for homePage.js @@ -29,30 +29,12 @@ export default class Overview extends Observable { constructor(model) { super(); this.model = model; - this.date = new Date().toDateString(); this.filterCriteria = []; - this.data = [ - { - authorID: 'Batman', - title: 'Run1', - creationTime: this.date, - tags: ['Tag1', 'Tag2'], - }, - { - authorID: 'Joker', - title: 'Run2', - creationTime: this.date, - tags: ['Tag2'], - }, - { - authorID: 'Anonymous', - title: 'Run5', - creationTime: this.date, - tags: ['Tag3'], - }, - ]; - this.filtered = [...this.data]; + this.data = []; + this.filtered = []; this.headers = ['ID', 'Author ID', 'Title', 'Creation Time']; + + this.fetchData(); } /** @@ -63,19 +45,43 @@ export default class Overview extends Observable { return this.headers; } + /** + * Fetch all relevant logs data from api + * @returns {undefined} Injects the data object with the response data + */ + async fetchData() { + const response = await fetchClient('/api/logs', { method: 'GET' }); + const result = await response.json(); + this.data = result.data; + this.filtered = [...result.data]; + this.notify(); + } + + /** + * Getter for all the data + * @returns {Array} Returns all of the data + */ + getData() { + return this.data; + } + /** * Get the table data * @returns {Array} The data without the tags to be rendered in a table */ - getTableData() { + getDataWithoutTags() { const subentries = this.filtered.map((entry) => { - const filter = Object.keys(entry).map((subkey) => { - if (subkey !== 'tags') { + const columnData = Object.keys(entry).map((subkey) => { + // Filter out the field not needed for the table + if (subkey !== 'tags' && subkey !== 'content') { + if (subkey === 'creationTime') { + return new Date(entry[subkey]).toLocaleString(); + } + return entry[subkey]; } }); - - return filter; + return columnData.filter((item) => item !== undefined); }); return subentries; diff --git a/test/public/overview.test.js b/test/public/overview.test.js index c43c45e3cb..e588e99703 100644 --- a/test/public/overview.test.js +++ b/test/public/overview.test.js @@ -22,12 +22,12 @@ const pti = require('puppeteer-to-istanbul'); const { server } = require('../../lib/application'); module.exports = function () { - // Configure this suite to have a default timeout of 10s - this.timeout(10000); + // Configure this suite to have a default timeout of 5s + this.timeout(5000); let page; let browser; - let PORT; + let url; before(async () => { await server.listen(); @@ -38,11 +38,11 @@ module.exports = function () { page.coverage.startCSSCoverage(), ]); - PORT = server.address().port; + const port = server.address().port; + url = `http://localhost:${port}`; }); after(async () => { - await server.close(); const [jsCoverage, cssCoverage] = await Promise.all([ page.coverage.stopJSCoverage(), page.coverage.stopCSSCoverage(), @@ -54,14 +54,18 @@ module.exports = function () { pti.write([...jsCoverage, ...cssCoverage]); await browser.close(); + await server.close(); }); it('loads the page successfully', async () => { - const response = await page.goto(`http://localhost:${PORT}`); + const response = await page.goto(url); + await page.waitFor(100); + // We expect the page to return the correct status code, making sure the server is running properly assert.equal(response.status(), 200); - const title = await page.title(); + // We expect the page to return the correct title, making sure there isn't another server running on this port + const title = await page.title(); assert.equal(title, 'AliceO2 Logbook 2020'); }); @@ -75,9 +79,31 @@ module.exports = function () { assert.equal(id, 'filtersCheckbox1'); await page.click(`#${id}`); - await page.waitFor(1000); - const newTableRows = await page.$$('table tr'); + await page.waitFor(100); + // We expect the amount of logs in this filter to match the advertised amount in the filters component - assert.equal(true, newTableRows.length - 1 === parseInt(amount.substring(1, amount.length - 1))); + const tableRows = await page.$$('table tr'); + assert.equal(true, tableRows.length - 1 === parseInt(amount.substring(1, amount.length - 1))); + + // Deselect the filter and wait for the changes to process + await page.click(`#${id}`); + await page.waitFor(100); + assert.equal(true, tableRows.length - 1 === parseInt(amount.substring(1, amount.length - 1))); + }); + + it('can navigate to a log detail page', async () => { + const firstRow = '#row1'; + const firstRowText = await page.$(firstRow + ' td'); + const id = await page.evaluate((element) => element.innerText, firstRowText); + + // We expect the entry page to have the same id as the id from the log overview + await page.click(firstRow); + await page.waitFor(100); + const redirectedUrl = await page.url(); + assert.equal(redirectedUrl, `${url}/?page=entry&id=${id}`); + + // We expect there to be at least one post in this log entry + const postExists = !!(await page.$('#post1')); + assert.equal(true, postExists); }); };