From de14ec74a8074b45e5783db29dde18e415f6c496 Mon Sep 17 00:00:00 2001 From: itaich Date: Tue, 19 Jan 2021 18:22:19 +0200 Subject: [PATCH 1/2] Add sort functionality to server and client --- README.md | 18 ++++++ client/src/App.scss | 23 +++++++ client/src/App.tsx | 147 +++++++++++++++++++++++++++----------------- client/src/api.ts | 21 +++++++ images/sort.png | Bin 0 -> 11854 bytes server/index.ts | 28 ++++++++- 6 files changed, 179 insertions(+), 58 deletions(-) create mode 100644 images/sort.png diff --git a/README.md b/README.md index e8f3b90..58809df 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,24 @@ Otherwise, it is mandatory. a. Add at least 3 automated browser tests using puppeteer, testing key features of your choice. b. Add component tests (using `jest`) to your work from *part 1*. +### part 5 +Add a sort component to your system. +Allow sorting by ticket title, creator email and creation date. + +#### Client +Add a client component that hightlights the current sort (if any) and states the sort direction. +Clicking on sort, sorts the collection in the server and returns the correct data sorted. +Clicking on a currently active sort changes it's direction (Ascending / Descending). +Make sure the view contains the words : title, data, email and the direction of sort. +Example UI: +![sort](images/sort.png) + +#### Server +On the `GET:Tickets` API add parameters to support sort. +Return the correct data sorted according to the request. + + + ## General notes - Test your work well. Think of edge cases. Think of how users will use it, and make sure your work is of high quality diff --git a/client/src/App.scss b/client/src/App.scss index 0e92993..d16847b 100644 --- a/client/src/App.scss +++ b/client/src/App.scss @@ -58,7 +58,30 @@ body { font-weight: 500; font-size: 12px; } + .sort { + padding-top: 5px; + button { + border: none; + padding: 5px; + box-shadow: 0 2px 6px 1px #e1e5e8; + margin:0 2px; + border-radius: 5px; + cursor: pointer; + outline: none; + background-color: white; + color: lightgray; + + &.selected { + box-shadow: 0 2px 6px 1px #3899ec; + color: #7a92a5; + } + &:active { + box-shadow: 0 2px 6px 1px #20455e; + color: #7a92a5; + } + } + } .tickets { margin: 0; diff --git a/client/src/App.tsx b/client/src/App.tsx index c9b6ab3..4293d94 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,67 +1,104 @@ import React from 'react'; import './App.scss'; -import {createApiClient, Ticket} from './api'; +import {createApiClient, Ticket, Sort, SortDirection, SortCriteria} from './api'; export type AppState = { - tickets?: Ticket[], - search: string; + tickets?: Ticket[], + search: string; + sort?: Sort } const api = createApiClient(); export class App extends React.PureComponent<{}, AppState> { - state: AppState = { - search: '' - } - - searchDebounce: any = null; - - async componentDidMount() { - this.setState({ - tickets: await api.getTickets() - }); - } - - renderTickets = (tickets: Ticket[]) => { - - const filteredTickets = tickets - .filter((t) => (t.title.toLowerCase() + t.content.toLowerCase()).includes(this.state.search.toLowerCase())); - - - return (); - } - - onSearch = async (val: string, newPage?: number) => { - - clearTimeout(this.searchDebounce); - - this.searchDebounce = setTimeout(async () => { - this.setState({ - search: val - }); - }, 300); - } - - render() { - const {tickets} = this.state; - - return (
-

Tickets List

-
- this.onSearch(e.target.value)}/> -
- {tickets ?
Showing {tickets.length} results
: null } - {tickets ? this.renderTickets(tickets) :

Loading..

} -
) - } + state: AppState = { + search: '' + } + + searchDebounce: any = null; + + async componentDidMount() { + this.setState({ + tickets: await api.getTickets() + }); + } + + renderSort = () => { + const {sort} = this.state; + const sortBy = sort && sort.by; + const direction = (sort && sort.direction) || ''; + return ( +
+ + {this.renderSortButton('Title', 'title', sortBy)} + {this.renderSortButton('Date', 'date', sortBy)} + {this.renderSortButton('Email', 'email', sortBy)} + +
+ ); + } + + renderSortButton = (text: string, criteria: SortCriteria, currentSortBy: SortCriteria | undefined) => { + return ; + } + + getSortedItems = async (sortData: Sort) => { + const {sort} = this.state; + if ((sort && sort.by) === sortData.by) { + sortData.direction = (sort && sort.direction) === 'ASC' ? 'DESC' : 'ASC'; + } else { + sortData.direction = 'ASC'; + } + const tickets = await api.getTickets(sortData); + this.setState({ + tickets, + sort:sortData + }); + } + + renderTickets = (tickets: Ticket[]) => { + + const filteredTickets = tickets + .filter((t) => (t.title.toLowerCase() + t.content.toLowerCase()).includes(this.state.search.toLowerCase())); + + + return (); + } + + onSearch = async (val: string, newPage?: number) => { + + clearTimeout(this.searchDebounce); + + this.searchDebounce = setTimeout(async () => { + this.setState({ + search: val + }); + }, 300); + } + + render() { + const {tickets} = this.state; + + return (
+

Tickets List

+
+ this.onSearch(e.target.value)}/> +
+ {this.renderSort()} + {tickets ?
Showing {tickets.length} results
: null} + {tickets ? this.renderTickets(tickets) :

Loading..

} +
) + } } -export default App; \ No newline at end of file +export default App; diff --git a/client/src/api.ts b/client/src/api.ts index 6fa32b2..64696ff 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -12,6 +12,19 @@ export type Ticket = { export type ApiClient = { getTickets: () => Promise; + getTickets: (sort?: Sort) => Promise; +} + +export type Sort = { + direction?: SortDirection, + by: SortCriteria +}; + +export type SortCriteria = 'title' | 'date' | 'email' ; +export type SortDirection = 'ASC' | 'DESC'; + +const buildQuery = (sort?: Sort) => { + return sort && sort.by ? `?sortBy=${sort.by}&sortDir=${sort.direction || `ASC`}`: ''; } export const createApiClient = (): ApiClient => { @@ -20,4 +33,12 @@ export const createApiClient = (): ApiClient => { return axios.get(APIRootPath).then((res) => res.data); } } + return { + getTickets: (sort?: Sort) => { + return axios.get(`http://localhost:3232/api/tickets${buildQuery(sort)}`).then((res) => res.data); + } + } } + + + diff --git a/images/sort.png b/images/sort.png new file mode 100644 index 0000000000000000000000000000000000000000..ebc2f5b86241d945b62ed31694a0ffd9859c45f2 GIT binary patch literal 11854 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 literal 0 HcmV?d00001 diff --git a/server/index.ts b/server/index.ts index f9c07f7..8f18387 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,9 +1,6 @@ import express from 'express'; import bodyParser = require('body-parser'); import { tempData } from './temp-data'; -import { serverAPIPort, APIPath } from '@fed-exam/config'; - -console.log('starting server', { serverAPIPort, APIPath }); const app = express(); @@ -26,6 +23,31 @@ app.get(APIPath, (req, res) => { const paginatedData = tempData.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); res.send(paginatedData); +const sort = (data: Ticket[], by: SortCriteria, dir: SortDirection) => { + interface mapper { + [key: string]: string; + } + + const mapToKey: mapper = {date: 'creationTime', email: 'userEmail', title: 'title'}; + const key = mapToKey[by]; + if(key) { + data.sort((a: Ticket, b: Ticket): number => { + // @ts-ignore + return dir === 'ASC' ? (a[key] < b[key] ? -1 : 0) : (a[key] > b[key] ? -1 : 0) + }); + } +}; + +app.get('/api/tickets', (req, res) => { + const page = req.query.page || 1; + const sortBy = req.query.sortBy || ''; + const direction = req.query.sortDir || ''; + if(sortBy){ + sort(tempData, sortBy, direction); + } + const paginatedData = tempData.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); + + res.send(paginatedData); }); app.listen(serverAPIPort); From a6eed0bde448b28b9d31e2ff245ab45f5990d8a4 Mon Sep 17 00:00:00 2001 From: Hadar Geva Date: Sat, 23 Jan 2021 19:02:58 +0200 Subject: [PATCH 2/2] fixes and added an empty test file --- README.md | 30 ++++++++----------------- client/src/App.tsx | 7 +++--- client/src/api.ts | 38 +++++++++++++------------------ package-lock.json | 2 +- server/index.ts | 55 +++++++++++++++++++++------------------------ tester/e2e.test.ts | 2 +- tester/sort.test.ts | 48 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 104 insertions(+), 78 deletions(-) create mode 100644 tester/sort.test.ts diff --git a/README.md b/README.md index 58809df..c5fcba8 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,15 @@ d. **Bonus** Step *a* wasn't enough - some tickets have long content. Add a show ### Part 2 - List functionality -a. Agents are complaining that our search functionality isn't working properly. They gave the example that when searching for "wix store", the ticket titled "Search bar for my wix store" (id `6860d043-f551-58c8-84d6-f9e6a8cb0cb2`) is not returned. Checking the data, that ticket does exist.. Find the issue and fix it. -Friendly reminder to commit and push after completing this step. +2a. +Agents desire to have ability to organize the list order. + +1.Add 3 sort buttons with the following text "sort by date", "sort by title" and "sort by email" +that allow sorting the list by ticket creation date, title and creator email respectively, +make sure to highlights the current sort button. +2.On the `GET:Tickets` API add `sortBy` parameter to support sort. +3.Connect your client side buttons to that API call +4.(Bonus) Clicking on a currently active sort changes it's direction (Ascending / Descending). b. We're showing only 20 tickets but agents can swear there are more. Solve this problem. **Keep in mind the number of tickets is planned to grow exponentially very soon so make sure to think of a proper solution.** @@ -83,25 +90,6 @@ Otherwise, it is mandatory. a. Add at least 3 automated browser tests using puppeteer, testing key features of your choice. b. Add component tests (using `jest`) to your work from *part 1*. -### part 5 -Add a sort component to your system. -Allow sorting by ticket title, creator email and creation date. - -#### Client -Add a client component that hightlights the current sort (if any) and states the sort direction. -Clicking on sort, sorts the collection in the server and returns the correct data sorted. -Clicking on a currently active sort changes it's direction (Ascending / Descending). -Make sure the view contains the words : title, data, email and the direction of sort. -Example UI: -![sort](images/sort.png) - -#### Server -On the `GET:Tickets` API add parameters to support sort. -Return the correct data sorted according to the request. - - - - ## General notes - Test your work well. Think of edge cases. Think of how users will use it, and make sure your work is of high quality - Stick to the best practices of the libraries used as much as possible diff --git a/client/src/App.tsx b/client/src/App.tsx index 4293d94..95a12c4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -30,10 +30,9 @@ export class App extends React.PureComponent<{}, AppState> { const direction = (sort && sort.direction) || ''; return (
- - {this.renderSortButton('Title', 'title', sortBy)} - {this.renderSortButton('Date', 'date', sortBy)} - {this.renderSortButton('Email', 'email', sortBy)} + {this.renderSortButton('Sort By Title', 'title', sortBy)} + {this.renderSortButton('Sort By Date', 'date', sortBy)} + {this.renderSortButton('Sort By Email', 'email', sortBy)}
); diff --git a/client/src/api.ts b/client/src/api.ts index 64696ff..d94bbf6 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -1,43 +1,37 @@ import axios from 'axios'; -import {APIRootPath} from '@fed-exam/config'; +import { APIRootPath } from '@fed-exam/config'; export type Ticket = { - id: string, - title: string; - content: string; - creationTime: number; - userEmail: string; - labels?: string[]; + id: string, + title: string; + content: string; + creationTime: number; + userEmail: string; + labels?: string[]; } export type ApiClient = { - getTickets: () => Promise; - getTickets: (sort?: Sort) => Promise; + getTickets: (sort?: Sort) => Promise; } export type Sort = { - direction?: SortDirection, - by: SortCriteria + direction?: SortDirection, + by: SortCriteria }; -export type SortCriteria = 'title' | 'date' | 'email' ; +export type SortCriteria = 'title' | 'date' | 'email'; export type SortDirection = 'ASC' | 'DESC'; const buildQuery = (sort?: Sort) => { - return sort && sort.by ? `?sortBy=${sort.by}&sortDir=${sort.direction || `ASC`}`: ''; + return sort && sort.by ? `?sortBy=${sort.by}&sortDir=${sort.direction || `ASC`}` : ''; } export const createApiClient = (): ApiClient => { - return { - getTickets: () => { - return axios.get(APIRootPath).then((res) => res.data); - } + return { + getTickets: (sort?: Sort) => { + return axios.get(`${APIRootPath}${buildQuery(sort)}`).then((res) => res.data); } - return { - getTickets: (sort?: Sort) => { - return axios.get(`http://localhost:3232/api/tickets${buildQuery(sort)}`).then((res) => res.data); - } - } + } } diff --git a/package-lock.json b/package-lock.json index 99c5edc..21baffc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "fed-entry-level-exam", + "name": "fed-entry-level-exam-root", "version": "1.0.0", "lockfileVersion": 1, "requires": true, diff --git a/server/index.ts b/server/index.ts index 8f18387..4e13ccf 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,6 +1,8 @@ import express from 'express'; import bodyParser = require('body-parser'); import { tempData } from './temp-data'; +import { SortCriteria, SortDirection, Ticket } from "../client/src/api"; +import { serverAPIPort } from "../configuration"; const app = express(); @@ -15,39 +17,34 @@ app.use((_, res, next) => { next(); }); -app.get(APIPath, (req, res) => { - - // @ts-ignore - const page: number = req.query.page || 1; - - const paginatedData = tempData.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); - - res.send(paginatedData); const sort = (data: Ticket[], by: SortCriteria, dir: SortDirection) => { - interface mapper { - [key: string]: string; - } - - const mapToKey: mapper = {date: 'creationTime', email: 'userEmail', title: 'title'}; - const key = mapToKey[by]; - if(key) { - data.sort((a: Ticket, b: Ticket): number => { - // @ts-ignore - return dir === 'ASC' ? (a[key] < b[key] ? -1 : 0) : (a[key] > b[key] ? -1 : 0) - }); - } + interface mapper { + [key: string]: string; + } + + const mapToKey: mapper = { date: 'creationTime', email: 'userEmail', title: 'title' }; + const key = mapToKey[by]; + data = [...data] + if (key) { + data.sort((a: Ticket, b: Ticket): number => { + // @ts-ignore + return dir === 'ASC' ? (a[key] < b[key] ? -1 : 0) : (a[key] > b[key] ? -1 : 0) + }); + } + return data; }; app.get('/api/tickets', (req, res) => { - const page = req.query.page || 1; - const sortBy = req.query.sortBy || ''; - const direction = req.query.sortDir || ''; - if(sortBy){ - sort(tempData, sortBy, direction); - } - const paginatedData = tempData.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); - - res.send(paginatedData); + const page = req.query.page || 1; + const sortBy = req.query.sortBy || ''; + const direction = req.query.sortDir || ''; + let data = tempData; + if (sortBy) { + data = sort(data, sortBy, direction); + } + const paginatedData = data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); + + res.send(paginatedData); }); app.listen(serverAPIPort); diff --git a/tester/e2e.test.ts b/tester/e2e.test.ts index 9f28784..054f5eb 100644 --- a/tester/e2e.test.ts +++ b/tester/e2e.test.ts @@ -1,6 +1,6 @@ const puppeteer = require('puppeteer'); const serverData = require('../server/data.json'); -import { staticsUrl } from '@fed-exa/config'; +import { staticsUrl } from '@fed-exam/config'; let browser; let page; diff --git a/tester/sort.test.ts b/tester/sort.test.ts new file mode 100644 index 0000000..18a25ec --- /dev/null +++ b/tester/sort.test.ts @@ -0,0 +1,48 @@ +import { getElementsByText } from "./getElementsByText"; + +const puppeteer = require('puppeteer'); +const serverData = require('../server/data.json'); +import { staticsUrl } from '@fed-exam/config'; + +let browser; +let page; + +beforeAll(async () => { + browser = await puppeteer.launch(); + page = await browser.newPage(); + await page.setViewport({ + width: 1280, + height: 1080, + deviceScaleFactor: 1, + }); +}) + +afterAll(async () => { + await browser.close(); +}) + +const goToMainPage = async () => { + await page.goto(staticsUrl); + //await page.screenshot({ path: 'main_page.png' }); +} + +describe("Sort list items", () => { + + test('Sort Button exist', async () => { + await goToMainPage(); + const els = await getElementsByText([ + 'sort by title', + 'Sort by title', + 'Sort By title', + 'Sort By Title', + + 'sort by date', + 'Sort by date', + 'Sort By date', + 'Sort By Date', + ], page) + + expect(els.length).toBe(2) + }); +}); +