Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sort functionality to server and client #9

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down Expand Up @@ -83,7 +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*.


## 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
Expand Down
23 changes: 23 additions & 0 deletions client/src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
146 changes: 91 additions & 55 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,103 @@
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 (<ul className='tickets'>
{filteredTickets.map((ticket) => (<li key={ticket.id} className='ticket'>
<h5 className='title'>{ticket.title}</h5>
<footer>
<div className='meta-data'>By {ticket.userEmail} | { new Date(ticket.creationTime).toLocaleString()}</div>
</footer>
</li>))}
</ul>);
}

onSearch = async (val: string, newPage?: number) => {

clearTimeout(this.searchDebounce);

this.searchDebounce = setTimeout(async () => {
this.setState({
search: val
});
}, 300);
}

render() {
const {tickets} = this.state;

return (<main>
<h1>Tickets List</h1>
<header>
<input type="search" placeholder="Search..." onChange={(e) => this.onSearch(e.target.value)}/>
</header>
{tickets ? <div className='results'>Showing {tickets.length} results</div> : null }
{tickets ? this.renderTickets(tickets) : <h2>Loading..</h2>}
</main>)
}
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 (
<div className='sort'>
{this.renderSortButton('Sort By Title', 'title', sortBy)}
{this.renderSortButton('Sort By Date', 'date', sortBy)}
{this.renderSortButton('Sort By Email', 'email', sortBy)}
<label id='sort-direction'>{ direction ? (direction === 'ASC' ? ' Ascending' : ' Descending') : '' }</label>
</div>
);
}

renderSortButton = (text: string, criteria: SortCriteria, currentSortBy: SortCriteria | undefined) => {
return <button className={criteria === currentSortBy ? 'selected' : ''} id={`sort-${criteria}`}
onClick={() => {this.getSortedItems({by: criteria})} }>{text}</button>;
}

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 (<ul className='tickets'>
{filteredTickets.map((ticket) => (<li key={ticket.id} className='ticket'>
<h5 className='title'>{ticket.title}</h5>
<footer>
<div
className='meta-data'>By {ticket.userEmail} | {new Date(ticket.creationTime).toLocaleString()}</div>
</footer>
</li>))}
</ul>);
}

onSearch = async (val: string, newPage?: number) => {

clearTimeout(this.searchDebounce);

this.searchDebounce = setTimeout(async () => {
this.setState({
search: val
});
}, 300);
}

render() {
const {tickets} = this.state;

return (<main>
<h1>Tickets List</h1>
<header>
<input type="search" placeholder="Search..." onChange={(e) => this.onSearch(e.target.value)}/>
</header>
{this.renderSort()}
{tickets ? <div className='results'>Showing {tickets.length} results</div> : null}
{tickets ? this.renderTickets(tickets) : <h2>Loading..</h2>}
</main>)
}
}

export default App;
export default App;
39 changes: 27 additions & 12 deletions client/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
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<Ticket[]>;
getTickets: (sort?: Sort) => Promise<Ticket[]>;
}

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 => {
return {
getTickets: () => {
return axios.get(APIRootPath).then((res) => res.data);
}
return {
getTickets: (sort?: Sort) => {
return axios.get(`${APIRootPath}${buildQuery(sort)}`).then((res) => res.data);
}
}
}



Binary file added images/sort.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 28 additions & 9 deletions server/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
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 });
import { SortCriteria, SortDirection, Ticket } from "../client/src/api";
import { serverAPIPort } from "../configuration";

const app = express();

Expand All @@ -18,12 +17,32 @@ 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);
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];
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 || '';
let data = tempData;
if (sortBy) {
data = sort(data, sortBy, direction);
}
const paginatedData = data.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

res.send(paginatedData);
});
Expand Down
2 changes: 1 addition & 1 deletion tester/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
48 changes: 48 additions & 0 deletions tester/sort.test.ts
Original file line number Diff line number Diff line change
@@ -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)
});
});