├── front
├── .gitignore
├── .editorconfig
├── src
│ ├── components
│ │ ├── Loading
│ │ │ └── index.tsx
│ │ ├── CommentDisplay
│ │ │ └── index.tsx
│ │ ├── CreationData
│ │ │ └── index.tsx
│ │ ├── IssueStatusSelector
│ │ │ ├── components
│ │ │ │ └── IssueRadio.tsx
│ │ │ └── index.tsx
│ │ ├── HighlightedText
│ │ │ └── index.tsx
│ │ ├── IssueDisplay
│ │ │ └── index.tsx
│ │ └── IssueItem
│ │ │ └── index.tsx
│ ├── index.html
│ ├── queries
│ │ ├── issueListSearchDataQuery.ts
│ │ ├── issueDetailsQuery.ts
│ │ ├── listIssuesCommentsQuery.ts
│ │ └── listIssuesQuery.ts
│ ├── settings.ts
│ ├── utils
│ │ ├── runIterationUtilCompletion.ts
│ │ ├── getCommentsSearcher
│ │ │ ├── index.ts
│ │ │ └── searchCommentsOnIssue.ts
│ │ ├── getIssueSearcher
│ │ │ ├── searchIssuesOnRepo.ts
│ │ │ └── index.ts
│ │ ├── getClient.ts
│ │ ├── makeQueryIterator.ts
│ │ └── makeQueryIterator.test.ts
│ ├── presententionalComponents
│ │ ├── GlobalStyle.ts
│ │ └── index.ts
│ ├── index.tsx
│ ├── containers
│ │ ├── IssueDetails
│ │ │ └── index.tsx
│ │ └── IssuesList
│ │ │ └── index.tsx
│ └── types.ts
├── .prettierrc.js
├── jest.config.js
├── .babelrc
├── README.md
├── tsconfig.json
├── .eslintrc.js
├── webpack.config.js
├── tools
│ ├── queries.ts
│ ├── saveIssuesAsJson.ts
│ └── saveCommentsAsJson.ts
└── package.json
└── docker-compose.yml
/front/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | src/assets
--------------------------------------------------------------------------------
/front/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | tab_width = 2
--------------------------------------------------------------------------------
/front/src/components/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function Loading() {
4 | return
Loading...
;
5 | }
6 |
--------------------------------------------------------------------------------
/front/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: true,
3 | trailingComma: 'all',
4 | singleQuote: true,
5 | printWidth: 120,
6 | tabWidth: 2,
7 | };
8 |
--------------------------------------------------------------------------------
/front/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Issue Broswer
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/front/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | roots: ["/src"],
3 | transform: {
4 | "^.+\\.tsx?$": "ts-jest"
5 | },
6 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
7 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"]
8 | };
9 |
--------------------------------------------------------------------------------
/front/src/queries/issueListSearchDataQuery.ts:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | export const issueListSearchDataQuery = gql`
4 | query {
5 | searchTerm @client {
6 | value
7 | }
8 | searchStatus @client {
9 | value
10 | }
11 | }
12 | `;
13 |
--------------------------------------------------------------------------------
/front/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env", "@babel/typescript", "@babel/react"],
3 | "plugins": [
4 | "@babel/proposal-class-properties",
5 | "@babel/proposal-object-rest-spread",
6 | "@babel/transform-runtime",
7 | "@babel/plugin-syntax-dynamic-import"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: "3"
3 | services:
4 | front:
5 | image: node:11.10.0
6 | hostname: front
7 | ports:
8 | - "8080:8080"
9 | volumes:
10 | - ./front:/etc/front
11 | working_dir: /etc/front
12 | # command: "npm run demo"
13 | command: "sh -c 'while sleep 3600; do :; done'"
14 |
--------------------------------------------------------------------------------
/front/src/settings.ts:
--------------------------------------------------------------------------------
1 | import { Settings as SettingsType } from './types';
2 |
3 | export const settings: SettingsType = {
4 | graphqlUrl: 'https://api.github.com/graphql',
5 | limit: 10,
6 | apiSearchLimit: 100,
7 | token: process.env.GITHUB_TOKEN as string,
8 | repositoryOwner: 'facebook',
9 | repositoryName: 'react',
10 | };
11 |
--------------------------------------------------------------------------------
/front/src/utils/runIterationUtilCompletion.ts:
--------------------------------------------------------------------------------
1 | import { cursorType } from '../types';
2 |
3 | export async function runIterationUtilCompletion(
4 | iterator: IterableIterator>,
5 | ): Promise {
6 | let { done, value } = iterator.next();
7 | let resp = await value;
8 | while (!done) {
9 | ({ done, value } = iterator.next(resp));
10 | resp = await value;
11 | }
12 | return resp as T[];
13 | }
14 |
--------------------------------------------------------------------------------
/front/src/components/CommentDisplay/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CommentOnIssue } from '../../types';
3 | import { CreationData } from '../CreationData';
4 |
5 | export function CommentDisplay({ data }: { data: CommentOnIssue }) {
6 | return (
7 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/front/src/components/CreationData/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import moment from 'moment';
3 | import { IssueCreationDataProps } from '../../types';
4 | import { Text } from '../../presententionalComponents';
5 | export function CreationData({ author, createdAt }: IssueCreationDataProps) {
6 | return (
7 |
8 | By {author.login} at {moment(createdAt).format('DD/MM/YYYY HH:mm')}
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/front/README.md:
--------------------------------------------------------------------------------
1 | # React-Webpack-TypeScript-Babel
2 |
3 | > This is sample repository demonstrates how to use React, Webpack, TypeScript and Babel
4 |
5 | ## Starting the development server
6 |
7 | ```shell
8 | npm start
9 | ```
10 |
11 | ## Building the `bundle`
12 |
13 | ```shell
14 | npm run build
15 | ```
16 |
17 | ## Type-Checking the repo
18 |
19 | ```shell
20 | npm run type-check
21 | ```
22 |
23 | And to run in --watch mode:
24 |
25 | ```shell
26 | npm run type-check:watch
27 | ```
28 |
--------------------------------------------------------------------------------
/front/src/queries/issueDetailsQuery.ts:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | export const issueDetailsQuery = gql`
4 | query($repositoryOwner: String!, $repositoryName: String!, $number: Int!) {
5 | repository(owner: $repositoryOwner, name: $repositoryName) {
6 | issue(number: $number) {
7 | id
8 | closed
9 | number
10 | title
11 | bodyHTML
12 | author {
13 | login
14 | }
15 | createdAt
16 | }
17 | }
18 | }
19 | `;
20 |
--------------------------------------------------------------------------------
/front/src/components/IssueStatusSelector/components/IssueRadio.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IssueRadioProps } from '../../../types';
3 |
4 | export function IssueRadio({ children, status, selectedValue, name, onChange }: IssueRadioProps) {
5 | return (
6 | <>
7 | onChange(status)}
14 | />
15 |
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/front/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "noFallthroughCasesInSwitch": true,
5 | "noUnusedParameters": true,
6 | "noImplicitReturns": true,
7 | "moduleResolution": "node",
8 | "esModuleInterop": true,
9 | "noUnusedLocals": true,
10 | "noImplicitAny": true,
11 | "target": "es2018",
12 | "module": "esnext",
13 | "strict": true,
14 | "jsx": "react",
15 | "skipLibCheck": true,
16 | "resolveJsonModule": true
17 | },
18 | "include": ["src/**/*"],
19 | "exclude": ["node_modules", "dist"]
20 | }
21 |
--------------------------------------------------------------------------------
/front/src/components/HighlightedText/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Hilightable } from '../../presententionalComponents';
4 |
5 | export function HighlightedText({ term, children }: { term?: string | null; children: string }) {
6 | if (!term) {
7 | return <>{children}>;
8 | }
9 | const parts = children.split(new RegExp(`(${term})`, 'gi'));
10 | return (
11 |
12 | {' '}
13 | {parts.map((part, i) => (
14 |
15 | {part}
16 |
17 | ))}{' '}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/front/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: [
4 | 'plugin:@typescript-eslint/recommended',
5 | 'prettier/@typescript-eslint',
6 | 'plugin:prettier/recommended',
7 | 'plugin:react/recommended',
8 | ],
9 | parserOptions: {
10 | ecmaVersion: 2018,
11 | sourceType: 'module',
12 | },
13 | rules: {
14 | '@typescript-eslint/no-explicit-any': 0,
15 | '@typescript-eslint/explicit-function-return-type': 0,
16 | },
17 | settings: {
18 | 'import/resolver': {
19 | webpack: {
20 | config: './webpack.config.js',
21 | },
22 | },
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/front/src/queries/listIssuesCommentsQuery.ts:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | export const listIssuesCommentsQuery = gql`
4 | query($repositoryOwner: String!, $repositoryName: String!, $issueNumber: Int!, $limit: Int!, $cursor: String) {
5 | repository(owner: $repositoryOwner, name: $repositoryName) {
6 | issue(number: $issueNumber) {
7 | comments(first: $limit, after: $cursor) {
8 | edges {
9 | node {
10 | id
11 | author {
12 | login
13 | }
14 | bodyHTML
15 | body
16 | createdAt
17 | }
18 | cursor
19 | }
20 | }
21 | }
22 | }
23 | }
24 | `;
25 |
--------------------------------------------------------------------------------
/front/src/queries/listIssuesQuery.ts:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | export const listIssuesQuery = gql`
4 | query($repositoryOwner: String!, $repositoryName: String!, $cursor: String, $limit: Int!, $filterBy: IssueFilters) {
5 | repository(owner: $repositoryOwner, name: $repositoryName) {
6 | issues(last: $limit, before: $cursor, filterBy: $filterBy) {
7 | edges {
8 | node {
9 | id
10 | closed
11 | number
12 | title
13 | body
14 | bodyHTML
15 | author {
16 | login
17 | }
18 | createdAt
19 | }
20 | cursor
21 | }
22 | }
23 | }
24 | }
25 | `;
26 |
--------------------------------------------------------------------------------
/front/src/components/IssueDisplay/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IssueDisplayProps } from '../../types';
3 | import { CreationData } from '../CreationData';
4 | import { CommentDisplay } from '../CommentDisplay';
5 | import { TextHeader } from '../../presententionalComponents';
6 |
7 | export function IssueDisplay({ data, comments }: IssueDisplayProps) {
8 | return (
9 | <>
10 | {data.title}
11 |
12 |
13 | Comments
14 | {comments && comments.map(comment => )}
15 | >
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/front/src/components/IssueStatusSelector/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IssueStatus, IssueStatusSelectorProps } from '../../types';
3 | import { IssueRadio } from './components/IssueRadio';
4 |
5 | export function IssueStatusSelector({ value, onChange }: IssueStatusSelectorProps) {
6 | const commonProps = {
7 | selectedValue: value,
8 | onChange: onChange,
9 | name: 'issueStatus',
10 | };
11 | return (
12 | <>
13 |
14 | All Status
15 |
16 |
17 | Only Open
18 |
19 |
20 | Only Closed
21 |
22 | >
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/front/src/presententionalComponents/GlobalStyle.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 |
3 | export const GlobalStyle = createGlobalStyle`
4 | body {
5 | margin: 0;
6 | height: 100%;
7 | }
8 | ol, ul {
9 | list-style: none;
10 | }
11 | html {
12 | box-sizing: border-box;
13 | height: 100%;
14 | }
15 | *, *:before, *:after {
16 | box-sizing: inherit;
17 | }
18 | #root {
19 | height: 100%;
20 | }
21 | button {
22 | border: none;
23 | margin: 0;
24 | padding: 0;
25 | width: auto;
26 | overflow: visible;
27 | background: transparent;
28 | color: inherit;
29 | font: inherit;
30 | text-align: inherit;
31 | &:hover {
32 | cursor: pointer;
33 | border-bottom: 1px solid inherit;
34 | }
35 | }
36 | *:focus {
37 | outline: none;
38 | }
39 | `;
40 |
--------------------------------------------------------------------------------
/front/src/utils/getCommentsSearcher/index.ts:
--------------------------------------------------------------------------------
1 | import { CommentOnIssue, Settings, SearchCommentsOnIssueParams } from '../../types';
2 | import { ApolloClient } from 'apollo-client';
3 | import { makeQueryIterator } from '../makeQueryIterator';
4 | import { runIterationUtilCompletion } from '../runIterationUtilCompletion';
5 | import { searchCommentsOnIssue } from './searchCommentsOnIssue';
6 |
7 | export function getCommentsSearcher(client: ApolloClient, settings: Settings) {
8 | return async (issueNumber: number): Promise => {
9 | try {
10 | const searchParams = { client, settings, issueNumber };
11 | const iter = makeQueryIterator({
12 | searchParams,
13 | executeSearch: searchCommentsOnIssue,
14 | });
15 | return runIterationUtilCompletion(iter);
16 | } catch (err) {
17 | return [];
18 | }
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/front/src/components/IssueItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, Pre, IssueItemContainer, StausBadge, TextLine } from '../../presententionalComponents';
3 | import { IssueItemProps, IssueStatus } from '../../types';
4 | import { HighlightedText as HT } from '../HighlightedText';
5 | import { CreationData } from '../CreationData';
6 |
7 | export function IssueItem({ number, title, closed, body, searchTerm, author, createdAt }: IssueItemProps) {
8 | return (
9 |
10 |
11 |
12 | {closed ? 'Closed' : 'Open'}
13 |
14 |
15 | {' '}
16 | {number}{' '}
17 | {' '}
18 | - {title}
19 |
20 |
21 |
22 |
23 | {body}
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/front/src/utils/getIssueSearcher/searchIssuesOnRepo.ts:
--------------------------------------------------------------------------------
1 | import { Issue, IssueQueryResponse, IssueStatus, SearchIssuesOnRepoParams } from '../../types';
2 | import { listIssuesQuery } from '../../queries/listIssuesQuery';
3 |
4 | export async function searchIssuesOnRepo({
5 | client,
6 | settings,
7 | status,
8 | cursor,
9 | }: SearchIssuesOnRepoParams): Promise<[Issue[], string | null]> {
10 | const { repositoryOwner, repositoryName, apiSearchLimit: limit } = settings;
11 | const variables: any = {
12 | repositoryName,
13 | repositoryOwner,
14 | limit,
15 | cursor,
16 | };
17 | if (status !== IssueStatus.Both) {
18 | variables.filterBy = { states: status };
19 | }
20 | const {
21 | data: {
22 | repository: {
23 | issues: { edges },
24 | },
25 | },
26 | }: IssueQueryResponse = await client.query({
27 | query: listIssuesQuery,
28 | variables,
29 | errorPolicy: 'all',
30 | });
31 | if (!edges.length) {
32 | return [[], null];
33 | }
34 | const lastCursor = edges[0].cursor;
35 | const issues = edges.map(({ node }) => node);
36 | return [issues, lastCursor];
37 | }
38 |
--------------------------------------------------------------------------------
/front/src/utils/getCommentsSearcher/searchCommentsOnIssue.ts:
--------------------------------------------------------------------------------
1 | import { CommentOnIssue, ComentQueryResponse, SearchCommentsOnIssueParams } from '../../types';
2 | import { listIssuesCommentsQuery as query } from '../../queries/listIssuesCommentsQuery';
3 |
4 | export async function searchCommentsOnIssue({
5 | cursor,
6 | settings,
7 | issueNumber,
8 | client,
9 | }: SearchCommentsOnIssueParams): Promise<[CommentOnIssue[], string | null]> {
10 | const { repositoryOwner, repositoryName, apiSearchLimit: limit } = settings;
11 | const variables = {
12 | repositoryName,
13 | repositoryOwner,
14 | limit,
15 | issueNumber,
16 | cursor,
17 | };
18 | const {
19 | data: {
20 | repository: {
21 | issue: {
22 | comments: { edges },
23 | },
24 | },
25 | },
26 | }: ComentQueryResponse = await client.query({
27 | query,
28 | variables,
29 | errorPolicy: 'all',
30 | });
31 | if (!edges.length) {
32 | return [[], null];
33 | }
34 | let lastCursor = null;
35 | if (edges.length) {
36 | lastCursor = edges[edges.length - 1].cursor;
37 | }
38 | const comments = edges.map(({ node }) => node);
39 | return [comments, lastCursor];
40 | }
41 |
--------------------------------------------------------------------------------
/front/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const webpack = require('webpack');
3 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const path = require('path');
6 | const APP_PATH = path.resolve(__dirname, 'src');
7 |
8 | module.exports = {
9 | entry: APP_PATH,
10 |
11 | output: {
12 | filename: 'bundle.js',
13 | path: path.resolve(__dirname, 'dist'),
14 | publicPath: '/',
15 | },
16 |
17 | resolve: {
18 | extensions: ['.ts', '.tsx', '.js', '.json'],
19 | },
20 |
21 | module: {
22 | rules: [{ test: /\.(ts|js)x?$/, loader: 'babel-loader', exclude: /node_modules/ }],
23 | },
24 |
25 | plugins: [
26 | new HtmlWebpackPlugin({
27 | inject: true,
28 | template: path.join(APP_PATH, 'index.html'),
29 | favicon: false,
30 | }),
31 | new webpack.DefinePlugin({
32 | 'process.env': {
33 | NODE_ENV: JSON.stringify(process.env.NODE_ENV),
34 | GITHUB_TOKEN: JSON.stringify(process.env.GITHUB_TOKEN),
35 | },
36 | }),
37 | new ForkTsCheckerWebpackPlugin(),
38 | ],
39 | devServer: {
40 | historyApiFallback: true,
41 | hot: true,
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/front/src/utils/getClient.ts:
--------------------------------------------------------------------------------
1 | import { ApolloClient } from 'apollo-client';
2 | import { ApolloLink } from 'apollo-link';
3 | import { setContext } from 'apollo-link-context';
4 | import { createHttpLink } from 'apollo-link-http';
5 | import { InMemoryCache, NormalizedCacheObject } from 'apollo-cache-inmemory';
6 | import { persistCache } from 'apollo-cache-persist';
7 |
8 | import { Settings } from '../types';
9 | import { PersistentStorage, PersistedData } from 'apollo-cache-persist/types';
10 |
11 | let clientInstance: null | ApolloClient = null;
12 |
13 | export async function getClient({ graphqlUrl, token }: Settings) {
14 | if (clientInstance) {
15 | return clientInstance;
16 | }
17 | const httpLink = createHttpLink({
18 | uri: graphqlUrl,
19 | });
20 | const authLink = setContext((_, { headers }) => {
21 | return {
22 | headers: {
23 | ...headers,
24 | authorization: `Bearer ${token}`,
25 | },
26 | };
27 | });
28 |
29 | const cache = new InMemoryCache();
30 |
31 | clientInstance = new ApolloClient({
32 | link: ApolloLink.from([authLink, httpLink]),
33 | cache,
34 | });
35 | const storage = window.localStorage as PersistentStorage>;
36 |
37 | await persistCache({ cache, storage });
38 | return clientInstance;
39 | }
40 |
--------------------------------------------------------------------------------
/front/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { BrowserRouter as Router, Route } from 'react-router-dom';
4 | import { ApolloProvider } from 'react-apollo';
5 | import { ThemeProvider } from 'styled-components';
6 | import { getClient } from './utils/getClient';
7 | import { settings } from './settings';
8 | import { IssuesList } from './containers/IssuesList';
9 | import { IssueDetails } from './containers/IssueDetails';
10 | import { GlobalStyle, MainContainer, InnerContainer, Header, Link, theme } from './presententionalComponents';
11 |
12 | getClient(settings).then(client => {
13 | function App() {
14 | return (
15 | <>
16 |
17 |
18 |
19 |
20 |
21 |
22 | Issue browser
23 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | >
36 | );
37 | }
38 |
39 | const rootElement = document.getElementById('root');
40 | render(, rootElement);
41 | });
42 |
--------------------------------------------------------------------------------
/front/tools/queries.ts:
--------------------------------------------------------------------------------
1 | const issuesQuery = `
2 | query ($repositoryOwner: String!, $repositoryName: String!, $cursor: String, $limit: Int!, $filterBy: IssueFilters) {
3 | repository(owner: $repositoryOwner, name: $repositoryName) {
4 | issues(last: $limit, before: $cursor, filterBy: $filterBy) {
5 | edges {
6 | node {
7 | id
8 | closed
9 | number
10 | title
11 | body
12 | bodyHTML
13 | author {
14 | login
15 | __typename
16 | }
17 | createdAt
18 | __typename
19 | }
20 | cursor
21 | __typename
22 | }
23 | __typename
24 | }
25 | __typename
26 | }
27 | }
28 | `;
29 |
30 | const commentsQuery = `
31 | query ($repositoryOwner: String!, $repositoryName: String!, $issueNumber: Int!, $limit: Int!, $cursor: String) {
32 | repository(owner: $repositoryOwner, name: $repositoryName) {
33 | issue(number: $issueNumber) {
34 | comments(first: $limit, after: $cursor) {
35 | edges {
36 | node {
37 | id
38 | author {
39 | login
40 | __typename
41 | }
42 | bodyHTML
43 | body
44 | createdAt
45 | __typename
46 | }
47 | cursor
48 | __typename
49 | }
50 | __typename
51 | }
52 | __typename
53 | }
54 | __typename
55 | }
56 | }
57 |
58 | `;
59 | module.exports = {
60 | issuesQuery,
61 | commentsQuery,
62 | };
63 |
--------------------------------------------------------------------------------
/front/src/containers/IssueDetails/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Query } from 'react-apollo';
3 | import { withApollo } from 'react-apollo';
4 | import { getCommentsSearcher } from '../../utils/getCommentsSearcher';
5 |
6 | import { issueDetailsQuery as query } from '../../queries/issueDetailsQuery';
7 | import { settings } from '../../settings';
8 |
9 | import { IssueDisplay } from '../../components/IssueDisplay';
10 | import { Loading } from '../../components/Loading';
11 |
12 | import { IssueDetailsProps, IssueDetailsResponse, CommentOnIssue } from '../../types';
13 |
14 | function BaseIssueDetails({ match, client }: IssueDetailsProps) {
15 | const [issueComments, setIssueComments] = useState([]);
16 |
17 | const number = parseInt(match.params.issueNumber, 10);
18 | const searchComments = getCommentsSearcher(client, settings);
19 |
20 | const executeSearchComents = async (): Promise => {
21 | const comments = await searchComments(number);
22 | setIssueComments(comments);
23 | };
24 | useEffect((): void => {
25 | executeSearchComents();
26 | }, []);
27 | const { repositoryOwner, repositoryName } = settings;
28 | const variables = { repositoryName, repositoryOwner, number };
29 |
30 | return (
31 |
32 | {({ loading, data, error }: IssueDetailsResponse) => {
33 | if (loading) {
34 | return ;
35 | }
36 | if (error || !data) {
37 | console.log({ error });
38 | return <>{'Data not found'}>;
39 | }
40 | return ;
41 | }}
42 |
43 | );
44 | }
45 | export const IssueDetails = withApollo(BaseIssueDetails);
46 |
--------------------------------------------------------------------------------
/front/src/utils/makeQueryIterator.ts:
--------------------------------------------------------------------------------
1 | import { cursorType, WithCursor } from '../types';
2 |
3 | export type dataSearcher = (params: SearchType) => T[] | Promise<[T[], cursorType]>;
4 | export type responseFilter = (item: any) => boolean;
5 | export type completitionChecker = (filteredData: any[], currentData?: any[]) => boolean;
6 |
7 | // eslint-disable-next-line @typescript-eslint/class-name-casing
8 | export interface makeQueryIteratorParams {
9 | searchParams: SearchType;
10 | executeSearch: dataSearcher;
11 | filterData?: responseFilter;
12 | isDone?: completitionChecker;
13 | }
14 | /**
15 | * Makes a query iterator that execute the given query
16 | * until no items or when isDone says is time to stop
17 | */
18 | export function* makeQueryIterator({
19 | searchParams,
20 | executeSearch,
21 | filterData,
22 | isDone,
23 | }: makeQueryIteratorParams): IterableIterator> {
24 | let response: ItemType[] = [];
25 | let [items, cursor] = yield executeSearch(searchParams);
26 | if (filterData) {
27 | response.unshift(...items.filter(filterData));
28 | } else {
29 | response = [...items];
30 | }
31 | const mustContinue = () => {
32 | let ret = items.length > 0;
33 | if (ret && isDone) {
34 | ret = !isDone(response, items);
35 | }
36 | return ret;
37 | };
38 | while (mustContinue()) {
39 | [items, cursor] = yield executeSearch({ ...searchParams, cursor });
40 | let dataToAdd = items;
41 | if (dataToAdd && dataToAdd.length) {
42 | if (filterData) {
43 | dataToAdd = items.filter(filterData);
44 | }
45 | response.push(...dataToAdd);
46 | }
47 | }
48 | return response;
49 | }
50 |
--------------------------------------------------------------------------------
/front/tools/saveIssuesAsJson.ts:
--------------------------------------------------------------------------------
1 | export {};
2 | const fs = require('fs');
3 | const path = require('path');
4 | const { promisify } = require('util');
5 | const request = require('request');
6 | const { issuesQuery } = require('./queries');
7 | const requestPromise = promisify(request);
8 |
9 | async function saveIssues() {
10 | console.log('Saving the issues as json');
11 |
12 | const issuesFile = path.resolve(__dirname, '../src/assets/issues.json');
13 | fs.truncateSync(issuesFile);
14 | const issuesStream = fs.createWriteStream(issuesFile);
15 | issuesStream.write(Buffer.from('{"offlineIssues":['));
16 | const options = {
17 | uri: 'https://api.github.com/graphql',
18 | headers: {
19 | authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
20 | 'User-Agent':
21 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
22 | },
23 | method: 'POST',
24 | json: {
25 | query: issuesQuery,
26 | variables: { repositoryName: 'react', repositoryOwner: 'facebook', limit: 100, cursor: null },
27 | },
28 | };
29 | let edges = null;
30 | let firstIssue = true;
31 | let cursor = null;
32 | const promiseWrite = promisify(issuesStream.write.bind(issuesStream));
33 | do {
34 | if (cursor) {
35 | options.json.variables.cursor = cursor;
36 | }
37 | console.log(`Requesting with cursor ${cursor}`);
38 | const response = await requestPromise(options);
39 | if (!response.body.data.repository) continue;
40 | ({ edges } = response.body.data.repository.issues);
41 | console.log('HERE and edges ', edges.length);
42 | if (edges.length) {
43 | ({ cursor } = edges[0]);
44 | for (const edge of edges) {
45 | let data = JSON.stringify(edge.node, null, 2);
46 | if (firstIssue) {
47 | firstIssue = false;
48 | } else {
49 | data = `,${data}`;
50 | }
51 | console.log(`Starting write of ${edge.node.number}`);
52 | await promiseWrite(Buffer.from(data));
53 | }
54 | }
55 | } while (edges && edges.length);
56 | issuesStream.end(Buffer.from(']}'));
57 | }
58 | saveIssues()
59 | .then(() => {
60 | console.log('Saved issues!');
61 | })
62 | .catch(err => {
63 | console.error(err);
64 | });
65 |
--------------------------------------------------------------------------------
/front/src/utils/getIssueSearcher/index.ts:
--------------------------------------------------------------------------------
1 | import { Issue, Settings, IssueStatus, SearchIssuesOnRepoParams } from '../../types';
2 | import { ApolloClient } from 'apollo-client';
3 | import { makeQueryIterator } from '../makeQueryIterator';
4 | import { runIterationUtilCompletion } from '../runIterationUtilCompletion';
5 | import { searchIssuesOnRepo } from './searchIssuesOnRepo';
6 |
7 | export function getIssueSearcher(client: ApolloClient, settings: Settings) {
8 | return async (status = IssueStatus.Both, searchTerm?: string): Promise => {
9 | const DEBUG_OFFLINE = true;
10 | const { limit } = settings;
11 | const filterIssue = (issue: Issue) => {
12 | if (!searchTerm) {
13 | return true;
14 | }
15 | const match = searchTerm.trim().toLowerCase();
16 | return issue.title.toLowerCase().indexOf(match) > 0 || issue.body.toLowerCase().indexOf(match) > 0;
17 | };
18 | const stopIssueQuerying = (response: any[]) => {
19 | return response.length >= limit;
20 | };
21 | const searchParams = { client, settings, searchTerm, status };
22 | try {
23 | if (DEBUG_OFFLINE) {
24 | throw new Error();
25 | }
26 | const iter = makeQueryIterator({
27 | searchParams,
28 | executeSearch: searchIssuesOnRepo,
29 | filterData: filterIssue,
30 | isDone: stopIssueQuerying,
31 | });
32 | const response = await runIterationUtilCompletion(iter);
33 | return response.slice(0, limit);
34 | } catch (err) {
35 | if (err.message.startsWith('Network error:') || DEBUG_OFFLINE) {
36 | const assetModule: any = await import('../../assets/issues.json');
37 | const offlineIssues = assetModule.default.offlineIssues as Issue[];
38 | console.log(`searching offline issues ${{ status, searchTerm }}`);
39 | const response: Issue[] = [];
40 | offlineIssues.every(issue => {
41 | const isDesiredStatus =
42 | status === IssueStatus.Both ||
43 | (status === IssueStatus.Open && !issue.closed) ||
44 | (status === IssueStatus.Closed && issue.closed);
45 |
46 | if (isDesiredStatus && filterIssue(issue)) {
47 | response.push(issue);
48 | }
49 | return !stopIssueQuerying(response);
50 | });
51 | return response;
52 | }
53 | throw err;
54 | }
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/front/src/containers/IssuesList/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { ApolloClient } from 'apollo-client';
3 |
4 | import { withApollo } from 'react-apollo';
5 |
6 | import { getIssueSearcher } from '../../utils/getIssueSearcher';
7 | import { settings } from '../../settings';
8 | import { Issue, IssueStatus, IssueSearcher } from '../../types';
9 | import { IssueStatusSelector } from '../../components/IssueStatusSelector';
10 | import { Loading } from '../../components/Loading';
11 | import { IssueItem } from '../../components/IssueItem';
12 | import { Button, Box, Input, TextHeader } from '../../presententionalComponents';
13 |
14 | function BaseIssuesList({ client }: { client: ApolloClient }) {
15 | const [searchTerm, setSearchTerm] = useState('');
16 | const [isLoading, setLoading] = useState(true);
17 | const [appliedSearchTerm, setAppliedSearchTerm] = useState(null);
18 | const [issuesList, setIssuesList] = useState([]);
19 | const [issueStatus, setIssueStatus] = useState(IssueStatus.Both);
20 |
21 | const issueSearcherRef = useRef();
22 |
23 | useEffect(() => {
24 | issueSearcherRef.current = getIssueSearcher(client, settings);
25 | }, []);
26 |
27 | const onSearch = async () => {
28 | const issueSearcher = issueSearcherRef.current;
29 | if (!issueSearcher) {
30 | return;
31 | }
32 | setLoading(true);
33 | const issues = await issueSearcher(issueStatus, searchTerm);
34 | setAppliedSearchTerm(searchTerm);
35 | setIssuesList(issues);
36 | setLoading(false);
37 | };
38 | const onStatusChange = (status: IssueStatus) => {
39 | setIssueStatus(status);
40 | };
41 | useEffect(() => {
42 | onSearch();
43 | }, [issueStatus]);
44 | const handleSubmit = (e: React.FormEvent) => {
45 | e.preventDefault();
46 | onSearch();
47 | };
48 | if (isLoading) {
49 | return ;
50 | }
51 | return (
52 |
71 | );
72 | }
73 |
74 | export const IssuesList = withApollo(BaseIssuesList);
75 |
--------------------------------------------------------------------------------
/front/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "project",
3 | "version": "0.0.1",
4 | "description": "",
5 | "scripts": {
6 | "type-check": "tsc --skipLibCheck --noEmit",
7 | "type-check:watch": "npm run type-check -- --watch",
8 | "build": "webpack --progress --colors --mode=production",
9 | "start": "webpack-dev-server --progress --colors --mode=development --host 0.0.0.0 --configwebpack.config.js ",
10 | "test": "jest",
11 | "lint": "eslint src --ext js,jsx,ts,tsx",
12 | "saveIssues": "ts-node ./tools/saveIssuesAsJson",
13 | "saveComments": "ts-node ./tools/saveCommentsAsJson"
14 | },
15 | "author": {
16 | "name": "Fabio Oliveira Costa",
17 | "email": "fabiocostadev@gmail.com"
18 | },
19 | "license": "MIT",
20 | "devDependencies": {
21 | "@babel/core": "7.4.0",
22 | "@babel/plugin-proposal-class-properties": "7.4.0",
23 | "@babel/plugin-proposal-object-rest-spread": "7.4.0",
24 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
25 | "@babel/preset-env": "7.4.2",
26 | "@babel/preset-react": "7.0.0",
27 | "@babel/preset-typescript": "7.3.3",
28 | "@types/jest": "^24.0.11",
29 | "@types/request": "^2.48.1",
30 | "@types/styled-components": "^4.1.14",
31 | "@types/styled-system": "^4.1.0",
32 | "@typescript-eslint/eslint-plugin": "^1.6.0",
33 | "@typescript-eslint/parser": "^1.6.0",
34 | "babel-loader": "8.0.5",
35 | "eslint": "^5.16.0",
36 | "eslint-config-prettier": "^4.1.0",
37 | "eslint-plugin-prettier": "^3.0.1",
38 | "eslint-plugin-react": "^7.12.4",
39 | "fork-ts-checker-webpack-plugin": "1.0.0",
40 | "html-webpack-plugin": "3.2.0",
41 | "jest": "^24.7.1",
42 | "prettier": "^1.16.4",
43 | "request": "^2.88.0",
44 | "ts-jest": "^24.0.2",
45 | "ts-node": "^8.0.3",
46 | "typescript": "3.3.4000",
47 | "webpack": "4.29.6",
48 | "webpack-cli": "3.3.0",
49 | "webpack-dev-server": "3.2.1"
50 | },
51 | "dependencies": {
52 | "@types/graphql": "^14.2.0",
53 | "@types/react": "16.8.8",
54 | "@types/react-dom": "16.8.3",
55 | "@types/react-router-dom": "^4.3.1",
56 | "apollo-cache-inmemory": "^1.5.1",
57 | "apollo-cache-persist": "^0.1.1",
58 | "apollo-client": "^2.5.1",
59 | "apollo-link": "^1.2.11",
60 | "apollo-link-context": "^1.0.17",
61 | "apollo-link-http": "^1.5.14",
62 | "apollo-link-state": "^0.4.2",
63 | "graphql": "^14.2.1",
64 | "graphql-tag": "^2.10.1",
65 | "moment": "^2.24.0",
66 | "react": "^16.8.5",
67 | "react-apollo": "^2.5.4",
68 | "react-dom": "^16.8.5",
69 | "react-router-dom": "^5.0.0",
70 | "react-scripts": "^2.1.8",
71 | "styled-components": "^4.2.0",
72 | "styled-system": "^4.1.0"
73 | },
74 | "resolutions": {
75 | "terser": "3.14.1"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/front/tools/saveCommentsAsJson.ts:
--------------------------------------------------------------------------------
1 | export {};
2 | const fs = require('fs');
3 | const path = require('path');
4 | const { promisify } = require('util');
5 | const request = require('request');
6 | const { commentsQuery } = require('./queries');
7 | const requestPromise = promisify(request);
8 | const issuesData = require('../src/assets/issues.json');
9 |
10 | async function saveComents() {
11 | console.log('Saving the issues as json');
12 |
13 | const options = {
14 | uri: 'https://api.github.com/graphql',
15 | headers: {
16 | authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
17 | 'User-Agent':
18 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
19 | },
20 | method: 'POST',
21 | json: {
22 | query: commentsQuery,
23 | variables: { repositoryName: 'react', repositoryOwner: 'facebook', limit: 100, cursor: null, issueNumber: 0 },
24 | },
25 | };
26 | let edges = null;
27 | for (const { number } of issuesData.offlineIssues) {
28 | let cursor = null;
29 | options.json.variables.cursor = null;
30 | options.json.variables.issueNumber = parseInt(number, 10);
31 | const commentsFile = path.resolve(__dirname, `../src/assets/commentsFor${number}.json`);
32 | fs.openSync(commentsFile, 'w');
33 | console.log(`Should have created ${commentsFile}`);
34 | const commentStream = fs.createWriteStream(commentsFile);
35 | commentStream.write(Buffer.from('{"offlineComments":['));
36 | const promiseWrite = promisify(commentStream.write.bind(commentStream));
37 | console.log(`Getting comments for issue ${number}`);
38 | do {
39 | if (cursor) {
40 | options.json.variables.cursor = cursor;
41 | }
42 | let firstComment = true;
43 | console.log(`Requesting with variables ${JSON.stringify(options.json.variables)}`);
44 | const response = await requestPromise(options);
45 | if (!response.body.data.repository) continue;
46 | ({ edges } = response.body.data.repository.issue.comments);
47 | if (edges.length) {
48 | ({ cursor } = edges[edges.length - 1]);
49 | for (const edge of edges) {
50 | let data = JSON.stringify(edge.node, null, 2);
51 | if (firstComment) {
52 | firstComment = false;
53 | } else {
54 | data = `,${data}`;
55 | }
56 |
57 | await promiseWrite(Buffer.from(data));
58 | }
59 | } else {
60 | cursor = null;
61 | }
62 | console.log('SHOULD WRITE ', edges.length, ' on ', number);
63 | } while (edges && edges.length);
64 | commentStream.write(Buffer.from(']}'));
65 | commentStream.end();
66 | }
67 | }
68 | saveComents()
69 | .then(() => {
70 | console.log('Saved issues!');
71 | })
72 | .catch(err => {
73 | console.error(err);
74 | });
75 |
--------------------------------------------------------------------------------
/front/src/types.ts:
--------------------------------------------------------------------------------
1 | import { ApolloClient } from 'apollo-client';
2 | import { QueryProps, QueryResult } from 'react-apollo';
3 | import { match } from 'react-router-dom';
4 |
5 | export enum IssueStatus {
6 | Open = 'OPEN',
7 | Closed = 'CLOSED',
8 | Both = 'BOTH',
9 | }
10 | export type cursorType = string | null;
11 | export interface WithCursor {
12 | cursor?: cursorType;
13 | }
14 |
15 | export interface Settings {
16 | graphqlUrl: string;
17 | limit: number;
18 | apiSearchLimit: number;
19 | token: string;
20 | repositoryOwner: string;
21 | repositoryName: string;
22 | }
23 | export interface Issue {
24 | title: string;
25 | bodyHTML: string;
26 | body: string;
27 | closed: boolean;
28 | id: string;
29 | number: number;
30 | createdAt: string;
31 | author: {
32 | login: string;
33 | };
34 | }
35 |
36 | interface Edge {
37 | node: T;
38 | cursor: string;
39 | }
40 |
41 | export interface IssueQueryResponse {
42 | data: {
43 | repository: {
44 | issues: {
45 | edges: Edge[];
46 | totalCount: number;
47 | };
48 | };
49 | };
50 | }
51 |
52 | export interface CommentOnIssue {
53 | id: string;
54 | author: {
55 | login: string;
56 | };
57 | createdAt: string;
58 | bodyHTML: string;
59 | }
60 | export interface ComentQueryResponse {
61 | data: {
62 | repository: {
63 | issue: {
64 | comments: {
65 | edges: Edge[];
66 | };
67 | };
68 | };
69 | };
70 | }
71 | interface IssueDetailsData {
72 | repository: {
73 | issue: Issue;
74 | };
75 | }
76 | interface IssueDetailsVariables {
77 | repositoryOwner: string;
78 | repositoryName: string;
79 | number: number;
80 | }
81 | export type IssueDetailsResponse = QueryResult;
82 |
83 | interface IssueSearchData {
84 | searchTerm: {
85 | value: string;
86 | };
87 | searchStatus: {
88 | value: IssueStatus;
89 | };
90 | }
91 | export type IssueListSearchDataResponse = QueryResult;
92 |
93 | export interface SearchIssueParams {
94 | client: ApolloClient;
95 | settings: Settings;
96 | searchTerm?: string;
97 | status: IssueStatus;
98 | }
99 | export interface SearchIssuesOnRepoParams {
100 | settings: SearchIssueParams['settings'];
101 | client: SearchIssueParams['client'];
102 | status: SearchIssueParams['status'];
103 | cursor?: string;
104 | }
105 |
106 | export interface SearchCommentsOnIssueParams {
107 | cursor?: string;
108 | settings: Settings;
109 | issueNumber: number;
110 | client: ApolloClient;
111 | }
112 |
113 | export type HandleIssueChange = (newStatus: IssueStatus) => any;
114 | export interface IssueStatusSelectorProps {
115 | value: IssueStatus;
116 | onChange: HandleIssueChange;
117 | }
118 |
119 | export interface IssueItemProps extends Issue {
120 | searchTerm?: string | null;
121 | }
122 |
123 | export interface IssueDetailsQueryProps {
124 | children: QueryProps['children'];
125 | repositoryOwner: string;
126 | repositoryName: string;
127 | number: number;
128 | }
129 |
130 | export interface IssueDetailsProps {
131 | match: match<{ issueNumber: string }>;
132 | client: ApolloClient;
133 | }
134 |
135 | export interface IssueDisplayProps {
136 | data: Issue;
137 | comments?: CommentOnIssue[];
138 | }
139 |
140 | export interface IssueCreationDataProps {
141 | author: Issue['author'];
142 | createdAt: Issue['createdAt'];
143 | }
144 |
145 | export interface IssueRadioProps {
146 | children: React.ReactNode;
147 | status: IssueStatus;
148 | selectedValue: IssueStatus;
149 | name: string;
150 | onChange: HandleIssueChange;
151 | }
152 |
153 | export type IssueSearcher = (status?: IssueStatus, searchTerm?: string | undefined) => Promise;
154 |
--------------------------------------------------------------------------------
/front/src/utils/makeQueryIterator.test.ts:
--------------------------------------------------------------------------------
1 | import { cursorType } from '../types';
2 |
3 | import { makeQueryIterator } from './makeQueryIterator';
4 | import { runIterationUtilCompletion } from './runIterationUtilCompletion';
5 |
6 | interface MockItem {
7 | value: any;
8 | }
9 | interface MockSearchType {
10 | cursor?: cursorType;
11 | param: any;
12 | }
13 | const defaultSearchParams = { param: Symbol('MockParam') };
14 | const mockCursor = 'mockCursor';
15 |
16 | const defaultExpectedValue: MockItem[] = [...Array(30).keys()].map((value: number) => ({
17 | value,
18 | }));
19 | const iterationsResponse = [
20 | defaultExpectedValue.slice(0, 10),
21 | defaultExpectedValue.slice(10, 20),
22 | defaultExpectedValue.slice(20, 30),
23 | ];
24 | const defaultExpectedCalls = iterationsResponse.length + 1;
25 | type executSearchRepsonse = [MockItem[], cursorType];
26 | const defaultExecuteSearch = jest.fn();
27 |
28 | describe('makeQueryIterator', () => {
29 | beforeEach(() => {
30 | defaultExecuteSearch.mockReset();
31 | iterationsResponse.forEach(response => {
32 | defaultExecuteSearch.mockImplementationOnce(() =>
33 | Promise.resolve([response, mockCursor] as executSearchRepsonse),
34 | );
35 | });
36 | defaultExecuteSearch.mockImplementationOnce(() => Promise.resolve([[], null]));
37 | });
38 | it('Return all data until empty if not filtered', async () => {
39 | const iter = makeQueryIterator({
40 | searchParams: defaultSearchParams,
41 | executeSearch: defaultExecuteSearch,
42 | });
43 | const result = await runIterationUtilCompletion(iter);
44 | expect(defaultExecuteSearch).toHaveBeenCalledTimes(defaultExpectedCalls);
45 | expect(result).toEqual(defaultExpectedValue);
46 | });
47 | it('Return only filtered data if filter', async () => {
48 | const filterData = (item: MockItem) => item.value % 2 === 0;
49 | const iter = makeQueryIterator({
50 | searchParams: defaultSearchParams,
51 | executeSearch: defaultExecuteSearch,
52 | filterData,
53 | });
54 | const result = await runIterationUtilCompletion(iter);
55 | expect(defaultExecuteSearch).toHaveBeenCalledTimes(defaultExpectedCalls);
56 | expect(result).toEqual(defaultExpectedValue.filter(filterData));
57 | });
58 | it('Fails if execute search fails', async () => {
59 | const mockError = new Error('mockError');
60 | const executeSearch = jest
61 | .fn()
62 | .mockImplementationOnce(() => Promise.resolve([iterationsResponse[0], mockCursor] as executSearchRepsonse))
63 | .mockImplementationOnce(() => Promise.reject(mockError));
64 | const iter = makeQueryIterator({
65 | searchParams: defaultSearchParams,
66 | executeSearch,
67 | });
68 | let thrownError = null;
69 | try {
70 | await runIterationUtilCompletion(iter);
71 | } catch (err) {
72 | thrownError = err;
73 | }
74 | expect(executeSearch).toHaveBeenCalledTimes(2);
75 | expect(thrownError).toEqual(mockError);
76 | });
77 | it('Consider isDone function to stop iteration', async () => {
78 | const executeSearch = jest
79 | .fn()
80 | .mockImplementationOnce(() => Promise.resolve([iterationsResponse[0], mockCursor] as executSearchRepsonse))
81 | .mockImplementationOnce(() => Promise.resolve([iterationsResponse[1], mockCursor] as executSearchRepsonse));
82 | const isDone = jest
83 | .fn()
84 | .mockImplementationOnce(() => false)
85 | .mockImplementationOnce(() => true);
86 | const iter = makeQueryIterator({
87 | searchParams: defaultSearchParams,
88 | executeSearch,
89 | isDone,
90 | });
91 | const result = await runIterationUtilCompletion(iter);
92 | expect(executeSearch).toHaveBeenCalledTimes(2);
93 | expect(result).toEqual([...iterationsResponse[0], ...iterationsResponse[1]]);
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/front/src/presententionalComponents/index.ts:
--------------------------------------------------------------------------------
1 | import { IssueStatus } from '../types';
2 |
3 | import styled, { StyledComponent } from 'styled-components';
4 | import {
5 | space,
6 | color,
7 | fontSize,
8 | borders,
9 | width,
10 | WidthProps,
11 | SpaceProps,
12 | FontSizeProps,
13 | BorderProps,
14 | ColorProps,
15 | } from 'styled-system';
16 | import { Link as BaseLink } from 'react-router-dom';
17 | export { GlobalStyle } from './GlobalStyle';
18 |
19 | export const theme = {
20 | fontSizes: [12, 14, 16, 24, 32, 48, 64, 96, 128],
21 | space: [0, '0.5rem', '1rem', '1.5rem', '3rem', '5rem'],
22 | colors: {
23 | text: '#717173',
24 | white: '#fff',
25 | primary: '#E30613',
26 | secondary: '#009FE3',
27 | highlight: '#FFFF00',
28 | error: '#ff0000',
29 | black: '#000',
30 | default: 'inherit',
31 | [IssueStatus.Closed]: '#cb2431',
32 | [IssueStatus.Open]: '#2cbe4e',
33 | },
34 | borderWidths: [1, 2, '0.5em', '1em', '1.5em'],
35 | };
36 | type SpaceColorProps = SpaceProps & ColorProps;
37 | type BoxProps = WidthProps & FontSizeProps & SpaceColorProps;
38 |
39 | export const Box = styled.div`
40 | ${space}
41 | ${width}
42 | ${fontSize}
43 | ${color}
44 | `;
45 | /*
46 | is not assignable to type 'IntrinsicAttributes &
47 | Pick, HTMLInputElement>
50 | */
51 | export const Input = styled.input`
52 | ${space}
53 | ${width}
54 | ${fontSize}
55 | ${color}
56 | `;
57 | Input.defaultProps = {
58 | fontSize: 2,
59 | p: 1,
60 | };
61 | export const BaseButton: StyledComponent = styled.button`
62 | ${space}
63 | ${width}
64 | ${fontSize}
65 | ${color}
66 | &:disabled {
67 | cursor: not-allowed;
68 | }
69 | & + ${() => BaseButton} {
70 | margin-left: ${({ theme: { space } }) => space[2]};
71 | }
72 | `;
73 |
74 | export const Button = styled(BaseButton)`
75 | ${borders}
76 | &:hover:enabled {
77 | color: ${({ theme: { colors } }) => colors.white};
78 | background-color: ${({ theme: { colors } }) => colors.secondary};
79 | }
80 | &:disabled {
81 | color: ${({ theme: { colors } }) => colors.muted};
82 | border-color: ${({ theme: { colors } }) => colors.muted};
83 | }
84 | `;
85 | Button.defaultProps = {
86 | borderWidth: 1,
87 | pl: 1,
88 | pr: 1,
89 | color: 'white',
90 | borderStyle: 'solid',
91 | borderColor: 'primary',
92 | bg: 'primary',
93 | };
94 | export const Menu = styled.nav``;
95 |
96 | export const MainContainer = styled.main`
97 | ${color}
98 | ${fontSize}
99 | font-family: verdana, sans-serif;
100 | line-height: 1.5;
101 | height: 100%;
102 | width: 100%
103 | display: grid;
104 | grid-template-areas:
105 | "header header header "
106 | "left main right"
107 | "footer footer footer";
108 | grid-template-rows: auto 1fr 5vh;
109 | grid-template-columns: 1fr 8fr 1fr;
110 | `;
111 | MainContainer.defaultProps = {
112 | fontSize: 3,
113 | bg: 'white',
114 | };
115 |
116 | export const InnerContainer = styled.div`
117 | ${space}
118 | ${color}
119 | grid-area: main;
120 | `;
121 | InnerContainer.defaultProps = {
122 | pt: 3,
123 | pb: 3,
124 | pl: 5,
125 | pr: 5,
126 | color: 'text',
127 | };
128 |
129 | const getHilightProps = (props: { theme: any; highlighted: boolean }) => {
130 | if (props.highlighted) {
131 | return `
132 | font-weight: bold;
133 | background-color: ${props.theme.colors.highlight};
134 | color: ${props.theme.colors.black};
135 | `;
136 | }
137 | return '';
138 | };
139 | export const Hilightable = styled.span`
140 | ${getHilightProps}
141 | `;
142 | type TextProps = SpaceColorProps & FontSizeProps;
143 |
144 | export const Header = styled.header`
145 | ${space}
146 | ${color}
147 | grid-area: header;
148 | `;
149 | Header.defaultProps = {
150 | pt: 1,
151 | pb: 1,
152 | pl: 4,
153 | pr: 4,
154 | bg: 'secondary',
155 | color: 'white',
156 | };
157 |
158 | export const Text = styled.span`
159 | ${space}
160 | ${fontSize}
161 | ${color}
162 | `;
163 | export const TextLine = styled.p`
164 | ${space}
165 | ${fontSize}
166 | ${color}
167 | `;
168 |
169 | export const TextHeader = styled.h3`
170 | ${space}
171 | ${fontSize}
172 | ${color}
173 | `;
174 | TextHeader.defaultProps = {
175 | fontSize: 4,
176 | mt: 3,
177 | pt: 2,
178 | pb: 2,
179 | mb: 3,
180 | color: 'primary',
181 | };
182 |
183 | export const Link = styled(BaseLink)`
184 | ${color}
185 | ${fontSize}
186 | text-decoration: underline;
187 | & + & {
188 | margin-left: ${({ theme: { space } }) => space[1]};
189 | }
190 | `;
191 | Link.defaultProps = { color: 'default' };
192 |
193 | export const Pre = styled.pre`
194 | white-space: pre-wrap;
195 | word-wrap: break-word;
196 | `;
197 |
198 | export const IssueItemContainer = styled(Box)`
199 | width: 100%;
200 | height: auto;
201 | overflow: hidden;
202 | `;
203 |
204 | export const StausBadge = styled(Text)`
205 | font-weight: bold;
206 | `;
207 |
208 | StausBadge.defaultProps = {
209 | color: 'white',
210 | p: 1,
211 | };
212 |
--------------------------------------------------------------------------------