diff --git a/__mocks__/handlers/members.handler.js b/__mocks__/handlers/members.handler.js index 0e9998f27..9dcd406ad 100644 --- a/__mocks__/handlers/members.handler.js +++ b/__mocks__/handlers/members.handler.js @@ -2,52 +2,66 @@ import { rest } from 'msw'; const URL = process.env.NEXT_PUBLIC_BASE_URL; const membersHandlers = [ - rest.get(`${URL}/members/idle`, (_, res, ctx) => { - return res( - ctx.status(200), - ctx.json({ - message: 'Idle members returned successfully!', - idleMemberUserNames: [ - 'rohan-rajgupta', - 'sumit', - 'swaraj', - 'rohit', - 'tanya', - 'akshay', - 'shubham', - 'devashish', - 'lakshay', - 'rucha', - 'swebert', - 'nikhil', - 'ishika', - 'rajakvk', - 'moses', - 'prem', - 'bhavesh', - 'ankush', - 'prakash', - 'deipayan', - 'mehul', - 'ashwini', - 'amanA', - 'sagar', - 'shankar', - 'aman-saxena', - 'harshith', - 'pranav', - 'ankur', - 'pujarini', - 'sanyogita', - 'pavan', - 'ankita', - 'shashwat', - 'vividh', - 'rahil', - ], - }) - ); - }), + rest.get(`${URL}/members/idle`, (_, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + message: 'Idle members returned successfully!', + idleMemberUserNames: [ + 'rohan-rajgupta', + 'sumit', + 'swaraj', + 'rohit', + 'tanya', + 'akshay', + 'shubham', + 'devashish', + 'lakshay', + 'rucha', + 'swebert', + 'nikhil', + 'ishika', + 'rajakvk', + 'moses', + 'prem', + 'bhavesh', + 'ankush', + 'prakash', + 'deipayan', + 'mehul', + 'ashwini', + 'amanA', + 'sagar', + 'shankar', + 'aman-saxena', + 'harshith', + 'pranav', + 'ankur', + 'pujarini', + 'sanyogita', + 'pavan', + 'ankita', + 'shashwat', + 'vividh', + 'rahil', + ], + }) + ); + }), ]; +export const failedIdleMembersHandler = rest.get( + `${URL}/members/idle`, + (_, res, ctx) => { + return res( + ctx.status(500), + ctx.json({ + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + }) + ); + } +); + export default membersHandlers; diff --git a/__tests__/Unit/Components/Header/Header.test.tsx b/__tests__/Unit/Components/Header/Header.test.tsx new file mode 100644 index 000000000..5e656ea6c --- /dev/null +++ b/__tests__/Unit/Components/Header/Header.test.tsx @@ -0,0 +1,441 @@ +import { Header } from '@/components/Header'; +import { render, screen } from '@testing-library/react'; + +/* eslint-disable */ +const useRouter = jest.spyOn(require('next/router'), 'useRouter'); + +describe('Header without dev mode', () => { + it('should have Tasks category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/', + query: {}, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.queryByText('Standup'); + const availabilityElement = screen.queryByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeTruthy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement).not.toBeInTheDocument(); + expect(availabilityElement).not.toBeInTheDocument(); + }); + + it('should have Issues category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/issues', + query: {}, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.queryByText('Standup'); + const availabilityElement = screen.queryByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeTruthy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement).not.toBeInTheDocument(); + expect(availabilityElement).not.toBeInTheDocument(); + }); + + it('should have Mine category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/mine', + query: {}, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.queryByText('Standup'); + const availabilityElement = screen.queryByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeTruthy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement).not.toBeInTheDocument(); + expect(availabilityElement).not.toBeInTheDocument(); + }); + + it('should have Open PRs category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/openPRs', + query: {}, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.queryByText('Standup'); + const availabilityElement = screen.queryByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeTruthy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement).not.toBeInTheDocument(); + expect(availabilityElement).not.toBeInTheDocument(); + }); + + it('should have Stale PRs category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/stale-pr', + query: {}, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.queryByText('Standup'); + const availabilityElement = screen.queryByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeTruthy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement).not.toBeInTheDocument(); + expect(availabilityElement).not.toBeInTheDocument(); + }); + + it('should have Idle Users category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/idle-users', + query: {}, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.queryByText('Standup'); + const availabilityElement = screen.queryByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeTruthy(); + expect(standupElement).not.toBeInTheDocument(); + expect(availabilityElement).not.toBeInTheDocument(); + }); + + it('should have Standup category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/standup', + query: {}, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.queryByText('Standup'); + const availabilityElement = screen.queryByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement).not.toBeInTheDocument(); + expect(availabilityElement).not.toBeInTheDocument(); + }); + + it('should have Availability Panel category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/availability-panel', + query: {}, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.queryByText('Standup'); + const availabilityElement = screen.queryByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement).not.toBeInTheDocument(); + expect(availabilityElement).not.toBeInTheDocument(); + }); +}); + +describe('Header with dev mode', () => { + it('should have Tasks category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/', + query: { dev: true }, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.getByText('Standup'); + const availabilityElement = screen.getByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeTruthy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement.classList.contains('active')).toBeFalsy(); + expect(availabilityElement.classList.contains('active')).toBeFalsy(); + }); + + it('should have Issues category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/issues', + query: { dev: true }, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.getByText('Standup'); + const availabilityElement = screen.getByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeTruthy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement.classList.contains('active')).toBeFalsy(); + expect(availabilityElement.classList.contains('active')).toBeFalsy(); + }); + + it('should have Mine category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/mine', + query: { dev: true }, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.getByText('Standup'); + const availabilityElement = screen.getByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeTruthy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement.classList.contains('active')).toBeFalsy(); + expect(availabilityElement.classList.contains('active')).toBeFalsy(); + }); + + it('should have Open PRs category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/openPRs', + query: { dev: true }, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.getByText('Standup'); + const availabilityElement = screen.getByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeTruthy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement.classList.contains('active')).toBeFalsy(); + expect(availabilityElement.classList.contains('active')).toBeFalsy(); + }); + + it('should have Stale PRs category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/stale-pr', + query: { dev: true }, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.getByText('Standup'); + const availabilityElement = screen.getByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeTruthy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement.classList.contains('active')).toBeFalsy(); + expect(availabilityElement.classList.contains('active')).toBeFalsy(); + }); + + it('should have Idle Users category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/idle-users', + query: { dev: true }, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.getByText('Standup'); + const availabilityElement = screen.getByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeTruthy(); + expect(standupElement.classList.contains('active')).toBeFalsy(); + expect(availabilityElement.classList.contains('active')).toBeFalsy(); + }); + + it('should have Standup category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/standup', + query: { dev: true }, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.getByText('Standup'); + const availabilityElement = screen.getByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement.classList.contains('active')).toBeTruthy(); + expect(availabilityElement.classList.contains('active')).toBeFalsy(); + }); + + it('should have Availability Panel category', () => { + useRouter.mockImplementation(() => { + return { + pathname: '/availability-panel', + query: { dev: true }, + }; + }); + render(
); + const taskElement = screen.getByText('Tasks'); + const issueElement = screen.getByText('Issues'); + const mineElement = screen.getByText('Mine'); + const openPRElement = screen.getByText('Open PRs'); + const stalePRElement = screen.getByText('Stale PRs'); + const idleUsersElement = screen.getByText('Idle Users'); + const standupElement = screen.getByText('Standup'); + const availabilityElement = screen.getByText('Availability Panel'); + + expect(taskElement.classList.contains('active')).toBeFalsy(); + expect(issueElement.classList.contains('active')).toBeFalsy(); + expect(mineElement.classList.contains('active')).toBeFalsy(); + expect(openPRElement.classList.contains('active')).toBeFalsy(); + expect(stalePRElement.classList.contains('active')).toBeFalsy(); + expect(idleUsersElement.classList.contains('active')).toBeFalsy(); + expect(standupElement.classList.contains('active')).toBeFalsy(); + expect(availabilityElement.classList.contains('active')).toBeTruthy(); + }); +}); diff --git a/__tests__/Unit/Components/Searchbar/searchbar.test.tsx b/__tests__/Unit/Components/Searchbar/searchbar.test.tsx new file mode 100644 index 000000000..97aa599c2 --- /dev/null +++ b/__tests__/Unit/Components/Searchbar/searchbar.test.tsx @@ -0,0 +1,78 @@ +import Searchbar from '@/components/Dashboard/Searchbar'; +import * as util from '@/utils/splitNSearch'; +import { fireEvent, render, screen } from '@testing-library/react'; + +describe('test searchbar component', function () { + beforeEach(() => { + jest.spyOn(util, 'splitNSearch'); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('renders searchbar input and a search button', function () { + render(); + + const input = screen.getByRole('textbox') as HTMLInputElement; + const searchBtn = screen.getByRole('button', { + name: 'Search', + }) as HTMLButtonElement; + + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute('placeholder', 'test-label:'); + expect(searchBtn).toBeInTheDocument(); + expect(searchBtn).toHaveTextContent('Search'); + }); + + it('checks if we can type into the searchbar', function () { + render(); + + const input = screen.getByPlaceholderText( + 'test-label:' + ) as HTMLInputElement; + + fireEvent.change(input, { target: { value: '123,456' } }); + expect(input.value).toBe('123,456'); + }); + + it('tests if the click handler is called', function () { + render(); + + const input = screen.getByPlaceholderText( + 'test-label:' + ) as HTMLInputElement; + + const searchBtn = screen.getByRole('button', { + name: 'Search', + }) as HTMLButtonElement; + + fireEvent.change(input, { target: { value: '123' } }); + fireEvent.click(searchBtn); + + expect(util.splitNSearch).toBeCalledTimes(1); + }); + + it('tests if enter key calls search', function () { + render(); + + const input = screen.getByPlaceholderText( + 'test-label:' + ) as HTMLInputElement; + + fireEvent.change(input, { target: { value: '123' } }); + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 }); + + expect(util.splitNSearch).toBeCalledTimes(1); + }); + + it('tests other key down events do not call search', function () { + render(); + + const input = screen.getByPlaceholderText( + 'test-label:' + ) as HTMLInputElement; + + fireEvent.keyDown(input, { key: 'A', code: 'KeyA' }); + + expect(util.splitNSearch).toBeCalledTimes(0); + }); +}); diff --git a/__tests__/Unit/Components/Tasks/TaskStatusEditMode.test.tsx b/__tests__/Unit/Components/Tasks/TaskStatusEditMode.test.tsx new file mode 100644 index 000000000..28b751859 --- /dev/null +++ b/__tests__/Unit/Components/Tasks/TaskStatusEditMode.test.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { + TaskStatusEditMode, + beautifyStatus, +} from '@/components/tasks/card/TaskStatusEditMode'; +import { TASK } from '../../../../__mocks__/db/tasks'; + +import { renderWithProviders } from '@/test-utils/renderWithProvider'; +import { BACKEND_TASK_STATUS } from '@/constants/task-status'; + +const BLOCKED_TASK = { + ...TASK, + id: '1-BLOCKED', + status: 'BLOCKED', +}; + +describe('TaskStatusEditMode', () => { + it('should correctly set default option', () => { + const mockUpdateTask = jest.fn(); + renderWithProviders( + + ); + const statusSelect = screen.getByLabelText('Status:'); + + expect(statusSelect).toHaveValue('BLOCKED'); + }); + + it('change task status from BLOCKED to AVAILABLE', () => { + const mockUpdateTask = jest.fn(); + renderWithProviders( + + ); + const statusSelect = screen.getByLabelText('Status:'); + + expect(statusSelect).toHaveValue('BLOCKED'); + + fireEvent.change(statusSelect, { target: { value: 'AVAILABLE' } }); + + expect(statusSelect).toHaveValue('AVAILABLE'); + + expect(mockUpdateTask).toHaveBeenCalledWith('1-BLOCKED', { + status: 'AVAILABLE', + }); + }); + + it('task status UN_ASSIGNED is mapped to AVAILABLE', () => { + const mockUpdateTask = jest.fn(); + const UN_ASSIGNED_TASK = { ...BLOCKED_TASK, status: 'UN_ASSIGNED' }; + renderWithProviders( + + ); + + const statusSelect = screen.getByLabelText('Status:'); + + expect(statusSelect).toHaveValue('AVAILABLE'); + }); + + it('renders a list of task ', () => { + const mockUpdateTask = jest.fn(); + renderWithProviders( + + ); + + const statusSelect = screen.getByLabelText('Status:'); + + const allOptions = Array.from( + statusSelect.querySelectorAll('option') + ).map((option) => [option.value, option.textContent]); + + const allTaskStatus = Object.entries(BACKEND_TASK_STATUS).map( + ([name, status]) => [status, beautifyStatus(name)] + ); + + expect(allOptions).toEqual(allTaskStatus); + }); +}); + +describe('test beautifyStatus function', () => { + it('test usage', () => { + const output = beautifyStatus('I_N'); + expect(output).toEqual('I N'); + }); +}); diff --git a/__tests__/Unit/hooks/membersApi.test.tsx b/__tests__/Unit/hooks/membersApi.test.tsx new file mode 100644 index 000000000..e8a50320d --- /dev/null +++ b/__tests__/Unit/hooks/membersApi.test.tsx @@ -0,0 +1,73 @@ +import { setupServer } from 'msw/node'; +import handlers, { + failedIdleMembersHandler, +} from '../../../__mocks__/handlers/members.handler.js'; + +import { PropsWithChildren } from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { Provider } from 'react-redux'; +import { store } from '@/app/store'; +import { useGetIdleMembersQuery } from '@/app/services/membersApi'; + +const server = setupServer(...handlers); + +beforeAll(() => { + server.listen(); +}); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +function Wrapper({ + children, +}: PropsWithChildren>): JSX.Element { + return {children}; +} + +describe('useGetIdleMembersQuery', () => { + test('returns idle members', async () => { + const { result, waitForNextUpdate } = renderHook( + () => useGetIdleMembersQuery(), + { + wrapper: Wrapper, + } + ); + + const initialResponse = result.current; + expect(initialResponse.data).toBeUndefined(); + expect(initialResponse.isLoading).toBe(true); + + await act(() => waitForNextUpdate()); + + const nextResponse = result.current; + expect(nextResponse.data).not.toBeUndefined(); + expect(nextResponse.isLoading).toBe(false); + expect(nextResponse.isSuccess).toBe(true); + expect(nextResponse.data).toBeDefined(); + }); + + test('checks for error response', async () => { + server.use(failedIdleMembersHandler); + const { result, waitForNextUpdate } = renderHook( + () => useGetIdleMembersQuery(), + { + wrapper: Wrapper, + } + ); + + const initialResponse = result.current; + expect(initialResponse.data).toBeUndefined(); + expect(initialResponse.isLoading).toBe(true); + + await act(() => waitForNextUpdate()); + + const nextResponse = result.current; + expect(nextResponse.isError).toBe(true); + expect(nextResponse.error).toHaveProperty('status', 500); + + expect(nextResponse.error).toHaveProperty('data', { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + }); + }); +}); diff --git a/__tests__/Unit/pages/Dashboard/dashboardTest.test.tsx b/__tests__/Unit/pages/Dashboard/dashboardTest.test.tsx new file mode 100644 index 000000000..8392a0434 --- /dev/null +++ b/__tests__/Unit/pages/Dashboard/dashboardTest.test.tsx @@ -0,0 +1,58 @@ +import DashboardPage from '@/pages/dashboard'; +import { renderWithRouter } from '@/test_utils/createMockRouter'; +import { Provider } from 'react-redux'; +import { store } from '@/app/store'; +import { fireEvent, screen } from '@testing-library/react'; + +describe('dashboard page test', function () { + it('checks if the page is rendered with exact components', function () { + renderWithRouter( + + + , + { query: { dev: 'true' } } + ); + + const searchBar = screen.getByRole('textbox'); + expect(searchBar).toBeInTheDocument(); + + const searchBtn = screen.getByRole('button', { + name: 'Search', + }) as HTMLButtonElement; + expect(searchBtn).toBeInTheDocument(); + }); + + it('renders 404 without passing the feature flag', function () { + renderWithRouter( + + + + ); + + const headings = screen.getAllByRole('heading'); + expect(headings).toHaveLength(1); + expect(headings[0].innerHTML).toBe('404 - Page Not Found'); + }); + + it('console logs the value', function () { + console.log = jest.fn(); + + renderWithRouter( + + + , + { query: { dev: 'true' } } + ); + + const input = screen.getByRole('textbox') as HTMLInputElement; + fireEvent.change(input, { target: { value: 'jhon, doe' } }); + + const searchBtn = screen.getByRole('button', { + name: 'Search', + }) as HTMLButtonElement; + fireEvent.click(searchBtn); + + const value = input.value.split(','); + expect(console.log).toBeCalledWith('Searching', value); + }); +}); diff --git a/package.json b/package.json index 0c88318f2..69a2385bf 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build": "next build", "start": "next start -p $PORT", "test": "jest --coverage", + "test-watch": "jest --watch ", "lint": "next lint", "lint-fix": "eslint . --fix --ext js,jsx,ts,tsx", "postinstall": "husky install", diff --git a/src/app/services/api.ts b/src/app/services/api.ts index 6124bf25f..3238cb5e5 100644 --- a/src/app/services/api.ts +++ b/src/app/services/api.ts @@ -22,6 +22,8 @@ export const api = createApi({ 'User', 'Tags', 'Levels', + 'Idle_Members', + 'User_Standup', ], /** * This api has endpoints injected in adjacent files, diff --git a/src/app/services/membersApi.ts b/src/app/services/membersApi.ts new file mode 100644 index 000000000..594c2610d --- /dev/null +++ b/src/app/services/membersApi.ts @@ -0,0 +1,22 @@ +import { MEMBERS_IDLE } from '@/constants/url'; +import { api } from './api'; + +type IdleMembersResponse = { idleMemberUserNames: string[] }; +export const membersApi = api.injectEndpoints({ + endpoints: (build) => ({ + getIdleMembers: build.query({ + query: () => MEMBERS_IDLE, + providesTags: ['Idle_Members'], + transformResponse: (response: IdleMembersResponse) => { + const filterMembers = response.idleMemberUserNames.filter( + (username: string) => username + ); + const sortedIdleMembers = filterMembers.sort(); + + return sortedIdleMembers; + }, + }), + }), +}); + +export const { useGetIdleMembersQuery } = membersApi; diff --git a/src/app/services/progressesApi.ts b/src/app/services/progressesApi.ts index c20a532d7..7a4e0e76f 100644 --- a/src/app/services/progressesApi.ts +++ b/src/app/services/progressesApi.ts @@ -1,4 +1,5 @@ import { api } from './api'; +import { userDetails } from '@/types/standup.type'; export const progressesApi = api.injectEndpoints({ endpoints: (builder) => ({ @@ -13,7 +14,12 @@ export const progressesApi = api.injectEndpoints({ }, }), }), + userProgressDetails: builder.query({ + query: (id): string => `/progresses?userId=${id}`, + providesTags: ['User_Standup'], + }), }), }); -export const { useSaveProgressMutation } = progressesApi; +export const { useSaveProgressMutation, useUserProgressDetailsQuery } = + progressesApi; diff --git a/src/components/Dashboard/Dashboard.module.scss b/src/components/Dashboard/Dashboard.module.scss index 504b314dc..9727ab0be 100644 --- a/src/components/Dashboard/Dashboard.module.scss +++ b/src/components/Dashboard/Dashboard.module.scss @@ -1,42 +1,52 @@ -$base-unit: 4px; -$base-font-size: 0.3rem; $color-blue: #041187; $color-red: #e30062; $color-green: #85da6b; -$color-disabled: #e5e7eb; +$color-disabled: #7c7e82; $color-white: #fff; $color-black: #000; -$diff-margin: $base-unit * 15; -$similar-margin: $base-unit * 8; -$section-padding-top-bottom: $base-unit * 30; -$section-padding-left-right: $base-unit * 25; .container { - margin: $diff-margin auto; - width: 85%; -} - -.searchLabel { - font-size: $base-font-size * 5; - padding-right: $base-unit * 8; - color: $color-blue; + margin: 40px auto; + width: 95%; + display: flex; + justify-content: center; + line-height: 2rem; + align-items: center; } .searchBar { - background-color: $color-disabled; - border-radius: $base-unit * 2; - border: none; - width: 80%; - height: $base-unit * 15; - font-size: $base-font-size * 5; + width: 600px; + font-family: sans-serif; + margin-right: 8px; + border-radius: 4px; + font-size: 1.2rem; + padding: 0.5rem; } -.searchBtn { - margin-left: $base-unit * 8; +.btn { + background-color: $color-blue; + border-radius: 4px; + color: white; background-color: $color-blue; - color: $color-white; - height: $base-unit * 15; - border-radius: $base-unit * 2; border: none; - width: $base-unit * 40; + padding: 12px 60px; + cursor: pointer; +} + +.btn:disabled { + color: white; + background-color: $color-disabled; + cursor: not-allowed; +} + +@media only screen and (max-width: 520px) { + .container { + display: flex; + flex-direction: column; + align-items: center; + } + .searchBar { + width: 95%; + margin: 4px; + } } diff --git a/src/components/Dashboard/Searchbar.tsx b/src/components/Dashboard/Searchbar.tsx index 94114a758..e4e1abbd8 100644 --- a/src/components/Dashboard/Searchbar.tsx +++ b/src/components/Dashboard/Searchbar.tsx @@ -6,16 +6,13 @@ function Searchbar({ label }: searchProps) { const [query, setQuery] = useState(''); const handleKeyPress = (event: KeyboardEvent) => { - if (event.key === 'Enter') { + if (event.key === 'Enter' && query !== '') { splitNSearch(query); } }; return (
- setQuery(e.target.value)} onKeyDown={(e) => handleKeyPress(e)} className={styles.searchBar} + placeholder={label + ':'} /> diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss new file mode 100644 index 000000000..acaa0a832 --- /dev/null +++ b/src/components/Header/Header.module.scss @@ -0,0 +1,39 @@ +.header { + padding-top: 0.6rem; + display: flex; + justify-content: center; + flex-wrap: wrap; + align-items: center; + border-bottom: 1px solid var(--toastify-text-color-light); +} + +.link { + padding: 0.6rem; + text-decoration: none; + font-weight: 700; + color: #041484; + cursor: pointer; + background: none; + border: none; + position: relative; + + &::after { + content: ''; + position: absolute; + height: 1rem; + width: 2px; + right: 0; + background-color: #00000077; + } +} + +// Hide | for the last elements +.header a:last-child { + .link::after { + display: none; + } +} + +.active { + color: #e30464; +} diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx new file mode 100644 index 000000000..6d843e36c --- /dev/null +++ b/src/components/Header/index.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import styles from './Header.module.scss'; +import Link from 'next/link'; +import { HeaderLinkProps } from '@/interfaces/HeaderItem.type'; +import { useRouter } from 'next/router'; +import { + devHeaderCategories, + headerCategories, +} from '@/constants/header-categories'; + +export const HeaderLink: FC = ({ title, link, isActive }) => { + const linkClasses = `${styles.link} ${isActive ? styles.active : ''}`; + + return ( + + + + ); +}; + +export const Header = () => { + const router = useRouter(); + + // Dev feature toggle + const { query } = router; + const dev = !!query.dev; + + return ( +
+ {headerCategories.map(({ title, refURL, pathName }, index) => ( + + ))} + + {dev && + devHeaderCategories.map( + ({ title, refURL, pathName }, index) => ( + + ) + )} +
+ ); +}; diff --git a/src/components/Layout/Layout.module.scss b/src/components/Layout/Layout.module.scss index 2374e854b..a97f88a9c 100644 --- a/src/components/Layout/Layout.module.scss +++ b/src/components/Layout/Layout.module.scss @@ -6,32 +6,10 @@ min-height: 100%; } -.header { - display: flex; - justify-content: center; - align-items: center; - flex-wrap: wrap; - padding-top: 10px; -} - .wrapper { margin: 0 2em; } -.link { - padding: 10px; - text-decoration: none; - font-weight: bold; - color: #041484; - cursor: pointer; - background: none; - border: none; -} - -.active { - color: #e30464; -} - @media (max-width: 970px) { .navbar { display: flex; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 748ce1eee..13efc45a6 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,89 +1,25 @@ import { FC, ReactNode } from 'react'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; import Footer from '@/components/footer'; import styles from '@/components/Layout/Layout.module.scss'; import NavBar from '@/components/navBar'; import { useGetUserQuery } from '@/app/services/userApi'; import { Loader } from '../tasks/card/Loader'; +import { Header } from '@/components/Header'; interface Props { children?: ReactNode; + hideHeader?: boolean; } -const navBarContent = (title: string, refUrl: string, isActive = false) => { - const linkClasses = `${styles.link} ${isActive ? styles.active : ''}`; - - return ( - - - - ); -}; - -const Layout: FC = ({ children }) => { - const router = useRouter(); +const Layout: FC = ({ hideHeader, children }) => { const { isLoading } = useGetUserQuery(); - // Dev feature toggle - const { query } = router; - const dev = !!query.dev; - return (
{isLoading && }
-
- {navBarContent('Tasks', '/', router.pathname === '/')}| - {navBarContent( - 'Issues', - '/issues', - router.pathname === '/issues' - )} - | - {navBarContent( - 'Mine', - '/mine', - router.pathname === '/mine' - )} - {/* TODO: Uncomment when DS(Chanllenges) is ready */} - {/* | - {navBarContent( - 'DS', - '/challenges', - router.pathname === '/challenges' - )} */} - | - {navBarContent( - 'Open PRs', - '/openPRs', - router.pathname === '/openPRs' - )} - | - {navBarContent( - 'Stale PRs', - '/stale-pr', - router.pathname === '/stale-pr' - )} - | - {navBarContent( - 'Idle Users', - '/idle-users', - router.pathname === '/idle-users' - )} - {dev && ( - <> - | - {navBarContent( - 'Availability Panel', - '/availability-panel' - )} - - )} -
+ {!hideHeader ?
: null} {children}
diff --git a/src/components/ProgressForm/ProgressForm.module.scss b/src/components/ProgressForm/ProgressForm.module.scss index 6d1ec96df..429b238e0 100644 --- a/src/components/ProgressForm/ProgressForm.module.scss +++ b/src/components/ProgressForm/ProgressForm.module.scss @@ -11,18 +11,6 @@ $similar-margin: $base-unit * 8; $section-padding-top-bottom: $base-unit * 30; $section-padding-left-right: $base-unit * 25; -.banner { - width: 60%; - margin: $diff-margin auto; - background-color: $color-blue; - color: $color-white; - border: 1px solid $color-red; - font-size: $base-unit * 4; - display: flex; - flex-direction: column; - align-items: center; -} - .mark { color: $color-red; text-decoration: underline; diff --git a/src/components/ProgressForm/ProgressHeader.tsx b/src/components/ProgressForm/ProgressHeader.tsx index 84d24ce56..92e3866f7 100644 --- a/src/components/ProgressForm/ProgressHeader.tsx +++ b/src/components/ProgressForm/ProgressHeader.tsx @@ -1,15 +1,27 @@ -import styles from '@/components/ProgressForm/ProgressForm.module.scss'; +import { FC } from 'react'; +import styles from '@/components/standup/standupContainer.module.scss'; +import { progressHeaderProps } from '@/types/ProgressUpdates'; -function ProgressHeader() { +const ProgressHeader: FC = ({ + totalMissedUpdates, + updateType, +}) => { return ( -
-

- You have 2 missed Progress - Updates -

-

Lets try to avoid missing updates

-
+
+
+

+ You have + + {totalMissedUpdates} missed + + {updateType} updates +

+

+ Let's try to avoid having zero days +

+
+
); -} +}; export default ProgressHeader; diff --git a/src/components/ProgressForm/ProgressLayout.tsx b/src/components/ProgressForm/ProgressLayout.tsx index d4fa10739..7aeee00bf 100644 --- a/src/components/ProgressForm/ProgressLayout.tsx +++ b/src/components/ProgressForm/ProgressLayout.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { FC } from 'react'; import getCurrentDate from '@/utils/getLatestDate'; @@ -8,12 +8,22 @@ import ProgressForm from './ProgressForm'; import styles from '@/components/ProgressForm/ProgressForm.module.scss'; import { questions } from '@/constants/ProgressUpdates'; +import { getTotalMissedUpdates } from '@/utils/getTotalMissedUpdate'; +import { useUserProgressDetailsQuery } from '@/app/services/progressesApi'; +import { useGetUserQuery } from '@/app/services/userApi'; -function ProgressLayout() { +const ProgressLayout: FC = () => { + const { data: user } = useGetUserQuery(); + const { data: userStandupdata } = useUserProgressDetailsQuery(user?.id); + const standupDates = userStandupdata?.data?.map((element) => element.date); + const totalMissedUpdates = getTotalMissedUpdates(standupDates || []); return ( <> - +

Task Updates

On {getCurrentDate()}

@@ -21,6 +31,6 @@ function ProgressLayout() {
); -} +}; export default ProgressLayout; diff --git a/src/components/Tabs/Tabs.module.scss b/src/components/Tabs/Tabs.module.scss index 93badc756..862b8e886 100644 --- a/src/components/Tabs/Tabs.module.scss +++ b/src/components/Tabs/Tabs.module.scss @@ -1,18 +1,31 @@ $theme-grey: #d4d4d5; +$theme-grey-light: #f1f1f5; +$gray-shade-color: #808080; +$gray-shade-color2: #5c5b5b; .tabButton { - background: white; border: none; - padding: 12px 8px 8px; - font-size: 0.8em; - border-radius: 5px 5px 0 0; - border-bottom: 1px solid $theme-grey; + border-bottom: 1.8px solid $theme-grey; + font-size: 0.85rem; + font-weight: bold; + color: $gray-shade-color; + background: var(--toastify-color-light); + padding: 0.45rem 0.8rem; + border-radius: 0.2rem 0.2rem 0 0; cursor: pointer; } +.tabButton:hover { + color: $gray-shade-color2; +} + +.tabButton:active { + background-color: $theme-grey-light; +} + .active { - font-weight: bold; - border: 1px solid $theme-grey; + color: var(--toastify-color-dark); + border: 1.8px solid $theme-grey; border-bottom: none; - background: white; + background: var(--toastify-color-light); } diff --git a/src/components/standup/FormInputComponent.tsx b/src/components/standup/FormInputComponent.tsx new file mode 100644 index 000000000..84074932d --- /dev/null +++ b/src/components/standup/FormInputComponent.tsx @@ -0,0 +1,38 @@ +import { FC } from 'react'; + +import styles from '@/components/standup/standupContainer.module.scss'; +import { InputProps } from '@/types/standup.type'; + +const FormInputComponent: FC = ({ + placeholder, + name, + value, + dataTestId, + labelValue, + htmlFor, + inputId, + handleChange, +}) => { + return ( + <> + + ) => + handleChange(e) + } + /> + + ); +}; + +export default FormInputComponent; diff --git a/src/components/standup/index.tsx b/src/components/standup/index.tsx new file mode 100644 index 000000000..160207471 --- /dev/null +++ b/src/components/standup/index.tsx @@ -0,0 +1,134 @@ +import { FC, useState, useEffect } from 'react'; +import moment from 'moment'; +import styles from '@/components/standup/standupContainer.module.scss'; + +import { + useSaveProgressMutation, + useUserProgressDetailsQuery, +} from '@/app/services/progressesApi'; +import { useGetUserQuery } from '@/app/services/userApi'; + +import FormInputComponent from './FormInputComponent'; +import { standupUpdateType } from '@/types/standup.type'; +import { getTotalMissedUpdates } from '@/utils/getTotalMissedUpdate'; +import { toast, ToastTypes } from '@/helperFunctions/toast'; +import { + ERROR_MESSAGE, + STANDUP_SUBMISSION_SUCCESS, +} from '@/constants/constants'; +import ProgressHeader from '../ProgressForm/ProgressHeader'; + +const defaultState = { + type: 'user', + completed: '', + planned: '', + blockers: '', +}; + +const StandUpContainer: FC = () => { + const [standupUpdate, setStandupUpdate] = + useState(defaultState); + + const [addStandup] = useSaveProgressMutation(); + const { data: user } = useGetUserQuery(); + const { data: userStandupdata } = useUserProgressDetailsQuery(user?.id); + + const { SUCCESS, ERROR } = ToastTypes; + const standupDates = userStandupdata?.data?.map((element) => element.date); + const totalMissedUpdates = getTotalMissedUpdates(standupDates || []); + const yesterdayDate = moment().subtract(1, 'days').format('MMMM DD, YYYY'); + + const handleChange = (event: React.ChangeEvent) => { + setStandupUpdate((prevStandupUpdate) => ({ + ...prevStandupUpdate, + [event.target.name]: event.target.value, + })); + }; + const isValidate = () => { + return ( + standupUpdate.completed !== '' && + standupUpdate.planned !== '' && + standupUpdate.blockers !== '' + ); + }; + + const buttonStyleClass = !isValidate() + ? `${styles.nonActiveButton}` + : `${styles.activeButton}`; + + const handleFormSubmission = async ( + event: React.FormEvent + ) => { + event.preventDefault(); + try { + await addStandup(standupUpdate); + toast(SUCCESS, STANDUP_SUBMISSION_SUCCESS); + setStandupUpdate(defaultState); + } catch (error) { + console.error(error); + toast(ERROR, ERROR_MESSAGE); + } + }; + + return ( + <> +
+ +
+
+

Standup Update

+
+
+ + + +
+ +
+
+
+
+ + ); +}; + +export default StandUpContainer; diff --git a/src/components/standup/standupContainer.module.scss b/src/components/standup/standupContainer.module.scss new file mode 100644 index 000000000..d72e589f8 --- /dev/null +++ b/src/components/standup/standupContainer.module.scss @@ -0,0 +1,160 @@ +$theme-focusHighlight: #1c64f2; +$theme-primaryBackground: #1c325e; +$theme-whiteColor: #ffffff; +$theme-bannerFontColor: #f1416c; +$theme-secondaryBackground: #1e429f; +$theme-ceriseRed: #d61f69; +$theme-headingColor: #6b7280; +$theme-inputFieldBorder: #d1d5db; +$theme-nonActiveButtonBackground: #e5e7eb; +$radius: 0.5rem; + +.standupContainer { + display: flex; + flex-direction: column; + width: 100%; + align-items: center; + justify-content: center; +} + +.progressBanner { + background-color: $theme-primaryBackground; + width: 84%; + height: 12%; + color: $theme-whiteColor; + padding: 2rem; + border-radius: $radius; +} + +.bannerContainer { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + width: 100%; + margin-top: 2rem; + margin-left: 8.2rem; +} + +.bannerPara { + font-family: 'Poppins', sans-serif; + font-size: 1.2rem; + font-weight: 600; +} + +.totalMissedUpdates { + color: $theme-bannerFontColor; + text-decoration: underline; + margin: 0 0.4rem; +} + +.standupUpdateContainer { + width: 50%; + margin-top: 2rem; +} + +.standupForm { + margin-top: 2.2rem; +} + +.standupTitle { + color: $theme-secondaryBackground; + font-family: 'Inter', sans-serif; + font-size: 2.2rem; + padding-left: 0.8rem; +} + +.formFields { + margin-bottom: 1.25rem; + border: none; +} + +.updateHeading { + color: $theme-headingColor; + font-size: 1.4rem; + font-family: 'Inter', sans-serif; +} + +.inputField { + height: 4rem; + width: 100%; + border-radius: $radius; + border: 1px solid $theme-inputFieldBorder; + background: $theme-whiteColor; + padding: 1rem; + font-weight: 400; + font-size: 0.8rem; + line-height: 150%; + font-family: 'Inter', sans-serif; + margin: 0.8rem 0 2.4rem 0; +} + +.inputField:focus { + border: 1px solid $theme-focusHighlight; + outline: none; +} + +.submitButton { + width: 14rem; + height: 4rem; + border-radius: $radius; + font-weight: 400; + font-size: 0.8rem; + line-height: 150%; + font-family: 'Inter', sans-serif; + border: none; + cursor: pointer; + letter-spacing: 0.8px; + margin-left: 1rem; +} + +.submitButton:active { + background: $theme-secondaryBackground; + color: $theme-whiteColor; +} + +.activeButton { + background: $theme-secondaryBackground; + color: $theme-whiteColor; +} + +.nonActiveButton { + background: $theme-nonActiveButtonBackground; +} + +@media (max-width: 946px) { + .standupUpdateContainer { + width: 76%; + } +} + +@media (max-width: 600px) { + .standupBanner { + width: 100%; + padding: 2rem; + text-align: center; + } + + .bannerPara { + font-size: 1rem; + word-wrap: break-word; + } + + .standupUpdateContainer { + width: 100%; + margin-top: 0.24rem; + } + + .standupTitle { + font-size: 1.6rem; + } + + .updateHeading { + font-size: 1rem; + } + + .submitButton { + width: 10rem; + height: 2.6rem; + } +} diff --git a/src/components/taskDetails/index.tsx b/src/components/taskDetails/index.tsx index 7b3b26b40..4a2e2143f 100644 --- a/src/components/taskDetails/index.tsx +++ b/src/components/taskDetails/index.tsx @@ -5,9 +5,7 @@ import React, { useContext, useRef, useState, - ChangeEventHandler, } from 'react'; -import NavBar from '@/components/navBar/index'; import TaskContainer from './TaskContainer'; import Details from './Details'; import { isUserAuthorizedContext } from '@/context/isUserAuthorized'; @@ -22,18 +20,13 @@ import { useGetTaskDetailsQuery, useUpdateTaskDetailsMutation, } from '@/app/services/taskDetailsApi'; -import { taskDetailsDataType } from '@/interfaces/taskDetails.type'; +import { + ButtonProps, + TextAreaProps, + taskDetailsDataType, +} from '@/interfaces/taskDetails.type'; +import Layout from '@/components/Layout'; -type ButtonProps = { - buttonName: string; - clickHandler: (value: any) => void; - value?: boolean; -}; -type TextAreaProps = { - name: string; - value: string; - onChange: ChangeEventHandler; -}; function Button(props: ButtonProps) { const { buttonName, clickHandler, value } = props; return ( @@ -165,8 +158,7 @@ const TaskDetails: FC = ({ taskID }) => { }; const shouldRenderParentContainer = () => !isLoading && !isError && data; return ( - <> - + {renderLoadingComponent()} {shouldRenderParentContainer() && (
@@ -333,7 +325,7 @@ const TaskDetails: FC = ({ taskID }) => {
)} - + ); }; diff --git a/src/components/tasks/TaskList/TaskList.tsx b/src/components/tasks/TaskList/TaskList.tsx index 810e88534..6f8574dff 100644 --- a/src/components/tasks/TaskList/TaskList.tsx +++ b/src/components/tasks/TaskList/TaskList.tsx @@ -53,7 +53,7 @@ export default function TaskList({ } return ( - <> +
{filteredTasks.map((item: task) => ( )} - +
); } diff --git a/src/components/tasks/card/TaskStatusEditMode.tsx b/src/components/tasks/card/TaskStatusEditMode.tsx new file mode 100644 index 000000000..c90191ed7 --- /dev/null +++ b/src/components/tasks/card/TaskStatusEditMode.tsx @@ -0,0 +1,43 @@ +import { BACKEND_TASK_STATUS } from '@/constants/task-status'; +import task from '@/interfaces/task.type'; + +type Props = { + task: task; + updateTask: (changeId: string, changeObject: { status: string }) => void; +}; + +// TODO: remove this after fixing the card beautify status +const beautifyStatus = (status: string) => status.split('_').join(' '); +const taskStatus = Object.entries(BACKEND_TASK_STATUS); + +const TaskStatusEditMode = ({ task, updateTask }: Props) => { + const onChangeUpdateTaskStatus = ({ + target: { value }, + }: React.ChangeEvent) => { + updateTask(task.id, { + status: value, + }); + }; + + // TODO: remove this after fixing the card beautify status + const defaultStatus = task.status.toUpperCase().split(' ').join('_'); + + return ( + + ); +}; + +export { TaskStatusEditMode, beautifyStatus }; diff --git a/src/components/tasks/card/card.module.scss b/src/components/tasks/card/card.module.scss index b721017ae..78ffd3347 100644 --- a/src/components/tasks/card/card.module.scss +++ b/src/components/tasks/card/card.module.scss @@ -27,6 +27,7 @@ $seeMoreTasksShadow: #595959cc; } .card { + text-align: left; display: flex; flex-direction: column; padding: 1rem; @@ -35,7 +36,7 @@ $seeMoreTasksShadow: #595959cc; justify-content: space-between; border: 1px solid #d9d9d9; position: relative; - max-width: 720px; + width: 50rem; @media (max-width: 48rem) { width: 100%; @@ -303,3 +304,10 @@ $seeMoreTasksShadow: #595959cc; transform: rotate(360deg); } } + +.taskCardsContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} diff --git a/src/components/tasks/card/index.tsx b/src/components/tasks/card/index.tsx index 05497bf2e..019ece98b 100644 --- a/src/components/tasks/card/index.tsx +++ b/src/components/tasks/card/index.tsx @@ -10,10 +10,10 @@ import { ALT_KEY } from '@/constants/key'; import { toast, ToastTypes } from '@/helperFunctions/toast'; import { useRouter } from 'next/router'; import TaskLevelEdit from './TaskTagEdit'; +import { TaskStatusEditMode } from './TaskStatusEditMode'; import { updateTaskDetails } from '@/interfaces/taskItem.type'; import fetch from '@/helperFunctions/fetch'; import { TASKS_URL } from '@/constants/url'; -import { BACKEND_TASK_STATUS } from '@/constants/task-status'; import { DUMMY_NAME, @@ -194,43 +194,6 @@ const Card: FC = ({ return percentageOfDaysLeft; } - const TaskStatusEditMode: FC = () => { - const updateTaskStatus = ({ - target: { value }, - }: React.ChangeEvent) => { - onContentChange(cardDetails.id, { - status: value, - }); - }; - - // TODO: remove this after fixing the card beautify status - const defaultStatus = cardDetails.status - .toUpperCase() - .split(' ') - .join('_'); - - const beautifyStatus = (status: string) => status.split('_').join(' '); - - return ( - - ); - }; - function handleProgressColor( percentCompleted: number, startedOn: string, @@ -536,7 +499,12 @@ const Card: FC = ({ {/* EDIT task status */}
- {shouldEdit ? : ''} + {shouldEdit && ( + + )}
{showAssignButton() ? ( diff --git a/src/constants/constants.ts b/src/constants/constants.ts index d47dc520f..05b4a4dbe 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -1,4 +1,6 @@ export const MAX_SEARCH_RESULTS = 1; +export const ERROR_MESSAGE = 'Something went wrong!'; +export const STANDUP_SUBMISSION_SUCCESS = 'Standup submitted successfully'; export const COMPLETED = 'COMPLETED'; export const DONE = 'DONE'; export const AVAILABLE = 'AVAILABLE'; diff --git a/src/constants/header-categories.ts b/src/constants/header-categories.ts new file mode 100644 index 000000000..db83555b4 --- /dev/null +++ b/src/constants/header-categories.ts @@ -0,0 +1,45 @@ +export const headerCategories = [ + { + title: 'Tasks', + refURL: '/', + pathName: '/', + }, + { + title: 'Issues', + refURL: '/issues', + pathName: '/issues', + }, + { + title: 'Mine', + refURL: '/mine', + pathName: '/mine', + }, + { + title: 'Open PRs', + refURL: '/openPRs', + pathName: '/openPRs', + }, + { + title: 'Stale PRs', + refURL: '/stale-pr', + pathName: '/stale-pr', + }, + { + title: 'Idle Users', + refURL: '/idle-users', + pathName: '/idle-users', + }, +]; + +export const devHeaderCategories = [ + { + title: 'Standup', + refURL: '/standup/?dev=true', + pathName: '/standup', + }, + { + title: 'Availability Panel', + refURL: '/availability-panel', + pathName: '/availability-panel', + }, +]; diff --git a/src/constants/url.ts b/src/constants/url.ts index c04b98f2a..4fd053f5c 100644 --- a/src/constants/url.ts +++ b/src/constants/url.ts @@ -11,6 +11,7 @@ export const LOGIN_URL = `https://github.com/login/oauth/authorize?client_id=23c export const MEMBERS_URL = 'https://members.realdevsquad.com'; export const CHALLENGES_URL = `${BASE_URL}/challenges`; export const USER_SELF = `${BASE_URL}/users/self`; +export const MEMBERS_IDLE = `${BASE_URL}/members/idle`; export const DEFAULT_AVATAR = '/Avatar.png'; export const RDS_LOGO = '/RDSLogo.png'; export const GITHUB_LOGO = '/github-white.png'; diff --git a/src/interfaces/HeaderItem.type.ts b/src/interfaces/HeaderItem.type.ts new file mode 100644 index 000000000..50f92f866 --- /dev/null +++ b/src/interfaces/HeaderItem.type.ts @@ -0,0 +1,5 @@ +export interface HeaderLinkProps { + title: string; + link: string; + isActive: boolean; +} diff --git a/src/interfaces/taskDetails.type.ts b/src/interfaces/taskDetails.type.ts index cdc772635..2c1454103 100644 --- a/src/interfaces/taskDetails.type.ts +++ b/src/interfaces/taskDetails.type.ts @@ -1,3 +1,4 @@ +import { ChangeEventHandler } from 'react'; export type taskDetailsDataType = { message?: string; taskData?: { @@ -16,3 +17,14 @@ export type taskDetailsDataType = { type: string; }; }; + +export type ButtonProps = { + buttonName: string; + clickHandler: (value: any) => void; + value?: boolean; +}; +export type TextAreaProps = { + name: string; + value: string; + onChange: ChangeEventHandler; +}; diff --git a/src/pages/availability-panel/index.tsx b/src/pages/availability-panel/index.tsx index 054efaa56..580f94583 100644 --- a/src/pages/availability-panel/index.tsx +++ b/src/pages/availability-panel/index.tsx @@ -9,14 +9,19 @@ import updateTasksStatus from '@/helperFunctions/updateTasksStatus'; import { AVAILABLE } from '@/constants/task-status'; import { FEATURE } from '@/constants/task-type'; import { BASE_URL } from '@/constants/url'; +import { useGetIdleMembersQuery } from '@/app/services/membersApi'; const AvailabilityPanel: FC = () => { - const [idleMembersList, setIdleMembersList] = useState([]); const [unAssignedTasks, setUnAssignedTasks] = useState([]); const [error, setError] = useState(false); const [isTaskLoading, setIsTaskLoading] = useState(true); - const [isMemberLoading, setIsMemberLoading] = useState(true); const [refreshData, setRefreshData] = useState(false); + const { + data: idleMembersList = [], + isError: isIdleMembersError, + isLoading: isIdleMemberLoading, + refetch: refreshMemberList, + } = useGetIdleMembersQuery(); useEffect(() => { const fetchTasks = async () => { @@ -36,42 +41,24 @@ const AvailabilityPanel: FC = () => { setIsTaskLoading(false); } }; - const fetchIdleUsers = async () => { - try { - const url = `${process.env.NEXT_PUBLIC_BASE_URL}/members/idle`; - const { requestPromise } = fetch({ url }); - const fetchPromise = await requestPromise; - const { idleMemberUserNames } = fetchPromise.data; - const filterMembers = idleMemberUserNames.filter( - (username: string) => username - ); - const sortedIdleMembers = filterMembers.sort(); - setIdleMembersList(sortedIdleMembers); - setError(false); - } catch (Error) { - setError(true); - } finally { - setIsMemberLoading(false); - } - }; fetchTasks(); - fetchIdleUsers(); }, [refreshData]); let isErrorOrIsLoading; - if (error) { + if (error || isIdleMembersError) { isErrorOrIsLoading = ( Something went wrong, please contact admin! ); - } else if (isTaskLoading || isMemberLoading) { + } else if (isTaskLoading || isIdleMemberLoading) { isErrorOrIsLoading = ( Loading... ); } const getData = () => { + refreshMemberList(); setRefreshData(!refreshData); }; diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index b7df43bed..a9249f803 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -4,11 +4,6 @@ import PageNotFound from '../404'; import NavBar from '@/components/navBar'; import Searchbar from '@/components/Dashboard/Searchbar'; -const search = (query: string) => { - const searchValues = query.split(','); - console.log('Searching', searchValues); -}; - const DashboardPage = () => { const router = useRouter(); diff --git a/src/pages/issues/index.tsx b/src/pages/issues/index.tsx index 0608c6b30..5672a4119 100644 --- a/src/pages/issues/index.tsx +++ b/src/pages/issues/index.tsx @@ -12,24 +12,22 @@ import { IssueItem } from '@/interfaces/issueItem.type'; import { PullRequestAndIssueItem } from '@/interfaces/pullRequestIssueItem'; type SearchFieldProps = { - searchText: string; - onSearchTextChanged: (event: ChangeEvent) => void; - onSearchTextSubmitted: () => void; + onSearchTextSubmitted: (searchString: string) => void; loading: boolean; }; -const SearchField = ({ - searchText, - onSearchTextChanged, - onSearchTextSubmitted, - loading, -}: SearchFieldProps) => { +const SearchField = ({ onSearchTextSubmitted, loading }: SearchFieldProps) => { + const [searchText, setSearchText] = useState(''); + const onSearchTextChanged = (e: ChangeEvent) => { + setSearchText(e.target.value); + }; + return (
{ e.preventDefault(); - onSearchTextSubmitted(); + onSearchTextSubmitted(searchText); }} >