diff --git a/__tests__/taskRequests/taskRequest.test.js b/__tests__/taskRequests/taskRequest.test.js new file mode 100644 index 00000000..0a083e2b --- /dev/null +++ b/__tests__/taskRequests/taskRequest.test.js @@ -0,0 +1,289 @@ +const puppeteer = require('puppeteer'); + +const { fetchedTaskRequests } = require('../../mock-data/taskRequests'); + +const SITE_URL = 'http://localhost:8000'; +// helper/loadEnv.js file causes API_BASE_URL to be stagin-api on local env url in taskRequest/index.html +const API_BASE_URL = 'https://staging-api.realdevsquad.com'; + +describe('Task Requests', () => { + let browser; + let page; + + jest.setTimeout(60000); + + beforeEach(async () => { + browser = await puppeteer.launch({ + headless: 'new', + ignoreHTTPSErrors: true, + args: ['--incognito', '--disable-web-security'], + devtools: false, + }); + }); + beforeEach(async () => { + page = await browser.newPage(); + + await page.setRequestInterception(true); + + page.on('request', (request) => { + if ( + request.url() === `${API_BASE_URL}/taskRequests` || + request.url() === `${API_BASE_URL}/taskRequests?dev=true` || + request.url() === + `${API_BASE_URL}/taskRequests?size=20&q=status%3Apending+sort%3Acreated-asc&dev=true` + ) { + request.respond({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ data: fetchedTaskRequests }), + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } else if ( + request.url() === + `${API_BASE_URL}/taskRequests?size=20&q=status%3Aapproved++sort%3Acreated-asc&dev=true` + ) { + const list = []; + for (let i = 0; i < 20; i++) { + list.push(fetchedTaskRequests[0]); + } + request.respond({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + data: list, + next: '/taskRequests?size=20&q=status%3Aapproved++sort%3Acreated-asc&dev=true', + }), + }); + } else { + request.continue(); + } + }); + await page.goto(`${SITE_URL}/taskRequests`); + await page.waitForNetworkIdle(); + }); + + afterEach(async () => { + await page.close(); + }); + + afterAll(async () => { + await browser.close(); + }); + describe('When the user is super user', () => { + it('should display the task requests card', async () => { + const url = await page.evaluate(() => API_BASE_URL); + const taskCards = await page.$$('.taskRequest__card'); + const title = await taskCards[0].evaluate( + (el) => el.children[0].textContent, + ); + const purpose = await taskCards[0].evaluate( + (el) => el.children[1].textContent, + ); + + expect(taskCards).toHaveLength(1); + expect(title).toMatch(/test title/i); + expect(purpose).toMatch(/test purpose/i); + }); + describe('Filter Modal', () => { + it('should be hidden initially', async () => { + const modal = await page.$('.filter-modal'); + expect( + await modal.evaluate((el) => el.classList.contains('hidden')), + ).toBe(true); + }); + + it('should be displayed after clicking the filter button and hidden on outside click', async () => { + const modal = await page.$('.filter-modal'); + const filterHead = await page.$('.filter-head'); + const filterContainer = await page.$('.filters-container'); + expect(filterHead).toBeTruthy(); + expect(filterContainer).toBeTruthy(); + await page.click('#filter-button'); + expect(modal).not.toBeNull(); + expect( + await modal.evaluate((el) => el.classList.contains('hidden')), + ).toBe(false); + await page.mouse.click(200, 200); + expect( + await modal.evaluate((el) => el.classList.contains('hidden')), + ).toBe(true); + }); + + it('checks if PENDING is checked by default', async () => { + const filterButton = await page.$('#filter-button'); + await filterButton.click(); + await page.waitForSelector('.filter-modal'); + const activeFilter = await page.$('input[value="PENDING"]'); + const currentState = await activeFilter.getProperty('checked'); + const isChecked = await currentState.jsonValue(); + expect(isChecked).toBe(true); + }); + + it('Selecting filters and clicking on apply should filter task request list', async () => { + let cardsList = await page.$$('.taskRequest__card'); + expect(cardsList).not.toBeNull(); + const initialLength = cardsList.length; + await page.click('#filter-button'); + await page.click('input[value="PENDING"]'); + await page.click('input[value="APPROVED"]'); + await page.click('#apply-filter-button'); + await page.waitForNetworkIdle(); + cardsList = await page.$$('.taskRequest__card'); + expect(cardsList).not.toBeNull(); + expect(cardsList.length).toBeGreaterThanOrEqual(0); + expect(cardsList.length).not.toBe(initialLength); + }); + + it('clears the filter when the Clear button is clicked', async () => { + const filterButton = await page.$('#filter-button'); + await filterButton.click(); + await page.waitForSelector('.filter-modal'); + const activeFilter = await page.$('input[value="APPROVED"]'); + await activeFilter.click(); + const clearButton = await page.$('.filter-modal #clear-button'); + await clearButton.click(); + await page.waitForSelector('.filter-modal', { hidden: true }); + const currentState = await activeFilter.getProperty('checked'); + const isChecked = await currentState.jsonValue(); + expect(isChecked).toBe(false); + }); + }); + + describe('Sort Modal', () => { + it('should be hidden initially', async () => { + const sortModal = await page.$('.sort-modal'); + const assigneButton = await page.$('#REQUESTORS_COUNT_ASC'); + expect( + await sortModal.evaluate((el) => el.classList.contains('hidden')), + ).toBe(true); + expect(assigneButton).toBeTruthy(); + }); + + it('should toggle visibility sort modal by clicking the sort button and selecting an option', async () => { + const sortModal = await page.$('.sort-modal'); + const assigneButton = await page.$('#REQUESTORS_COUNT_ASC'); + const sortHead = await page.$('.sort-head'); + const sortContainer = await page.$('.sorts-container'); + + expect(sortHead).toBeTruthy(); + expect(sortContainer).toBeTruthy(); + + await page.click('.sort-button'); + await page.click('#REQUESTORS_COUNT_ASC'); + expect( + await assigneButton.evaluate((el) => + el.classList.contains('selected'), + ), + ).toBe(true); + expect( + await sortModal.evaluate((el) => el.classList.contains('hidden')), + ).toBe(true); + await page.click('.sort-button'); + await page.click('#REQUESTORS_COUNT_ASC'); + expect( + await assigneButton.evaluate((el) => + el.classList.contains('selected'), + ), + ).toBe(false); + expect( + await sortModal.evaluate((el) => el.classList.contains('hidden')), + ).toBe(true); + }); + }); + + it('Checks that new items are loaded when scrolled to the bottom', async () => { + await page.click('#filter-button'); + await page.click('input[value="PENDING"]'); + await page.click('input[value="APPROVED"]'); + await page.click('#apply-filter-button'); + await page.waitForNetworkIdle(); + let taskRequestList = await page.$$('.taskRequest__card'); + expect(taskRequestList.length).toBe(20); + await page.evaluate(() => { + const element = document.querySelector('.virtual'); + if (element) { + element.scrollIntoView({ behavior: 'auto' }); + } + }); + await page.waitForNetworkIdle(); + taskRequestList = await page.$$('.taskRequest__card'); + expect(taskRequestList.length).toBe(40); + }); + }); +}); + +describe('createCustomElement', () => { + let browser; + let page; + + beforeAll(async () => { + browser = await puppeteer.launch({ + headless: 'new', + }); + + page = await browser.newPage(); + + await page.goto(`${SITE_URL}/taskRequests`); + await page.waitForNetworkIdle(); + }); + + afterAll(async () => { + await browser.close(); + }); + + describe('tagName', () => { + it('should create tag with provided tagName', async () => { + const tag = await page.evaluate( + () => createCustomElement({ tagName: 'p' }).tagName, + ); + expect(tag).toMatch(/p/i); + }); + + it('should not add tagName attribute', async () => { + const tagNameAttr = await page.evaluate(() => + createCustomElement({ tagName: 'p' }).getAttribute('tagName'), + ); + + expect(tagNameAttr).toBeNull(); + }); + }); + + describe('className', () => { + it('should add the class when class key is provided using string', async () => { + const classes = await page.evaluate(() => [ + ...createCustomElement({ tagName: 'p', class: 'test-class' }).classList, + ]); + + expect(classes).toHaveLength(1); + expect(classes).toContain('test-class'); + }); + + it('should add multiple classes when class key has array as value', async () => { + const classes = await page.evaluate(() => [ + ...createCustomElement({ + tagName: 'p', + class: ['test-class-1', 'test-class-2'], + }).classList, + ]); + + expect(classes).toHaveLength(2); + expect(classes).toStrictEqual(['test-class-1', 'test-class-2']); + }); + }); + + describe('textContent', () => { + it('should add textContent key when specified', async () => { + const textContent = await page.evaluate( + () => + createCustomElement({ tagName: 'p', textContent: 'test content' }) + .textContent, + ); + + expect(textContent).toBe('test content'); + }); + }); +}); diff --git a/__tests__/taskRequests/taskRequestDetails.test.js b/__tests__/taskRequests/taskRequestDetails.test.js new file mode 100644 index 00000000..b29ce8c0 --- /dev/null +++ b/__tests__/taskRequests/taskRequestDetails.test.js @@ -0,0 +1,179 @@ +const puppeteer = require('puppeteer'); +const { + urlMappings, + defaultMockResponseHeaders, +} = require('../../mock-data/taskRequests'); + +describe('Task request details page', () => { + let browser; + let page; + jest.setTimeout(60000); + + beforeAll(async () => { + browser = await puppeteer.launch({ + headless: 'new', + ignoreHTTPSErrors: true, + args: ['--incognito', '--disable-web-security'], + devtools: false, + }); + page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (interceptedRequest) => { + const url = interceptedRequest.url(); + if (urlMappings.hasOwnProperty(url)) { + interceptedRequest.respond({ + ...defaultMockResponseHeaders, + body: JSON.stringify(urlMappings[url]), + }); + } else { + interceptedRequest.continue(); + } + }); + await page.goto( + 'http://localhost:8000/taskRequests/details/?id=dM5wwD9QsiTzi7eG7Oq5', + ); + }); + + afterAll(async () => { + await browser.close(); + }); + + it('Checks the Modal working as expected', async () => { + await page.waitForNetworkIdle(); + await page.click('.info__more'); + await page.waitForSelector('#requestor_details_modal_content', { + visible: true, + }); + const modalHeading = await page.$eval( + '[data-modal-header="requestor-details-header"]', + (element) => element.textContent, + ); + expect(modalHeading).toBe('Requestor Details'); + + const proposedStartDateHeading = await page.$eval( + '[data-modal-start-date-text="proposed-start-date-text"]', + (element) => element.textContent, + ); + expect(proposedStartDateHeading).toBe('Proposed Start Date:'); + + const proposedStartDateValue = await page.$eval( + '[data-modal-start-date-value="proposed-start-date-value"]', + (element) => element.textContent, + ); + expect(proposedStartDateValue).toBe('30-10-2023'); + + const proposedEndDateHeading = await page.$eval( + '[data-modal-end-date-text="proposed-end-date-text"]', + (element) => element.textContent, + ); + expect(proposedEndDateHeading).toBe('Proposed Deadline:'); + + const proposedEndDateValue = await page.$eval( + '[data-modal-end-date-value="proposed-end-date-value"]', + (element) => element.textContent, + ); + expect(proposedEndDateValue).toBe('5-11-2023'); + + const descriptionTextHeading = await page.$eval( + '[data-modal-description-text="proposed-description-text"]', + (element) => element.textContent, + ); + expect(descriptionTextHeading).toBe('Description:'); + + const descriptionTextValue = await page.$eval( + '[data-modal-description-value="proposed-description-value"]', + (element) => element.textContent, + ); + expect(descriptionTextValue).toBe( + 'code change 3 days , testing - 2 days. total - 5 days', + ); + }); + + it('Should contain Approve and Reject buttons', async function () { + const approveButton = await page.$('.requestors__conatainer__list__button'); + const rejectButton = await page.$('.request-details__reject__button'); + expect(approveButton).toBeTruthy(); + expect(rejectButton).toBeTruthy(); + }); +}); + +describe('Task request details page with status creation', () => { + let browser; + let page; + jest.setTimeout(60000); + + beforeAll(async () => { + browser = await puppeteer.launch({ + headless: 'new', + ignoreHTTPSErrors: true, + args: ['--incognito', '--disable-web-security'], + devtools: false, + }); + page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', (interceptedRequest) => { + const url = interceptedRequest.url(); + if (urlMappings.hasOwnProperty(url)) { + interceptedRequest.respond({ + ...defaultMockResponseHeaders, + body: JSON.stringify(urlMappings[url]), + }); + } else { + interceptedRequest.continue(); + } + }); + await page.goto( + 'http://localhost:8000/taskRequests/details/?id=uC0IUpkFMx393XjnKx4w', + ); + }); + + afterAll(async () => { + await browser.close(); + }); + + it('Should render github issue', async () => { + await page.waitForNetworkIdle(); + + const issue = await page.$('#task-details'); + const testId = await issue.evaluate((el) => el.innerHTML); + + expect(testId).toContain( + 'When super_user try to update skills of new users the data of', + ); + }); + it('Should render title of the issue', async () => { + await page.waitForNetworkIdle(); + const issueTitle = await page.$('#issue_title'); + const title = await issueTitle.evaluate((el) => el.innerHTML); + + expect(title).toBe( + 'Fix: user data is not showing up in memberSkillsUpdateModal', + ); + }); + it('Should render author and time of the issue', async () => { + await page.waitForNetworkIdle(); + const issueTimeAndAuthor = await page.$('#issue_time_author'); + const timeAndAuthor = await issueTimeAndAuthor.evaluate( + (el) => el.innerHTML, + ); + + expect(timeAndAuthor).toContain('Wed Sep 06 2023'); + expect(timeAndAuthor).toContain('anishpawaskar'); + }); + it('Should render assignee of the issue', async () => { + await page.waitForNetworkIdle(); + const issueAssignee = await page.$('#issue_assignee'); + const assignee = await issueAssignee.evaluate((el) => el.innerHTML); + + expect(assignee).toContain('anishpawaskar'); + }); + it('Should render link of the issue', async () => { + await page.waitForNetworkIdle(); + const issueLink = await page.$('#issue_link'); + const link = await issueLink.evaluate((el) => el.innerHTML); + + expect(link).toContain( + 'https://github.com/Real-Dev-Squad/members-site/issues/92', + ); + }); +}); diff --git a/taskRequests/assets/RDSLogo.png b/taskRequests/assets/RDSLogo.png new file mode 100644 index 00000000..7f10e48f Binary files /dev/null and b/taskRequests/assets/RDSLogo.png differ diff --git a/taskRequests/assets/funnel.svg b/taskRequests/assets/funnel.svg new file mode 100644 index 00000000..4af43b94 --- /dev/null +++ b/taskRequests/assets/funnel.svg @@ -0,0 +1,3 @@ + + + diff --git a/taskRequests/assets/sort-down.svg b/taskRequests/assets/sort-down.svg new file mode 100644 index 00000000..9f51c3ba --- /dev/null +++ b/taskRequests/assets/sort-down.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/taskRequests/assets/sort-menu.svg b/taskRequests/assets/sort-menu.svg new file mode 100644 index 00000000..8fa429b1 --- /dev/null +++ b/taskRequests/assets/sort-menu.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/taskRequests/assets/sort-up.svg b/taskRequests/assets/sort-up.svg new file mode 100644 index 00000000..e991c745 --- /dev/null +++ b/taskRequests/assets/sort-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/taskRequests/constants.js b/taskRequests/constants.js new file mode 100644 index 00000000..ca84f255 --- /dev/null +++ b/taskRequests/constants.js @@ -0,0 +1,50 @@ +const taskRequestStatus = { + WAITING: 'WAITING', + APPROVED: 'APPROVED', +}; +const DEV_FEATURE_FLAG = 'dev'; +const DEFAULT_PAGE_SIZE = 20; +const Status = { + APPROVED: 'approved', + PENDING: 'pending', + DENIED: 'denied', +}; + +const Order = { + REQUESTORS_COUNT_ASC: { requestors: 'asc' }, + REQUESTORS_COUNT_DESC: { requestors: 'desc' }, + CREATED_TIME_DESC: { created: 'desc' }, + CREATED_TIME_ASC: { created: 'asc' }, +}; + +const FILTER_MODAL = 'filter-modal'; +const FILTER_BUTTON = 'filter-button'; +const APPLY_FILTER_BUTTON = 'apply-filter-button'; +const SEARCH_ELEMENT = 'assignee-search'; +const SORT_BUTTON = '.sort-button'; +const CLEAR_BUTTON = 'clear-button'; +const FILTER_CONTAINER = '.sort-filters'; +const SORT_MODAL = 'sort-modal'; +const ASSIGNEE_COUNT = 'REQUESTORS_COUNT_ASC'; +const ASSIGNEE_DESC = 'REQUESTORS_COUNT_DESC'; +const CREATED_TIME = 'CREATED_TIME_ASC'; +const CREATED_TIME_DESC = 'CREATED_TIME_DESC'; +const BACKDROP = '.backdrop'; +const LAST_ELEMENT_CONTAINER = '.virtual'; + +const MessageStatus = { + SUCCESS: 'SUCCESS', + ERROR: 'ERROR', +}; + +const TaskRequestAction = { + APPROVE: 'approve', + REJECT: 'reject', +}; +const ErrorMessages = { + UNAUTHENTICATED: + 'You are unauthenticated to view this section, please login!', + UNAUTHORIZED: 'You are unauthrozed to view this section', + NOT_FOUND: 'Task Requests not found', + SERVER_ERROR: 'Unexpected error occurred', +}; diff --git a/taskRequests/details/index.html b/taskRequests/details/index.html new file mode 100644 index 00000000..24f3c236 --- /dev/null +++ b/taskRequests/details/index.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + Task Requests | Real Dev Squad + + + + + + + + +
+
+ RDS logo + Home +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+

Requestors

+
    +
      +
    • +
    • +
    • +
    • +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + × +
    +
    +
    + + diff --git a/taskRequests/details/script.js b/taskRequests/details/script.js new file mode 100644 index 00000000..88879116 --- /dev/null +++ b/taskRequests/details/script.js @@ -0,0 +1,583 @@ +const API_BASE_URL = window.API_BASE_URL; + +let taskRequest; + +const taskRequestSkeleton = document.querySelector('.taskRequest__skeleton'); +const container = document.querySelector('.container'); +const taskSkeleton = document.querySelector('.task__skeleton'); +const requestorSkeleton = document.querySelector( + '.requestors__container__list__skeleton', +); + +const taskRequestContainer = document.getElementById('task-request-details'); +const taskContainer = document.getElementById('task-details'); +const toast = document.getElementById('toast_task_details'); +const rejectButton = document.getElementById('reject-button'); +const requestorsContainer = document.getElementById('requestors-details'); +const taskRequestId = new URLSearchParams(window.location.search).get('id'); +history.pushState({}, '', window.location.href); +const errorMessage = + 'The requested operation could not be completed. Please try again later.'; +let taskId; + +function renderTaskRequestDetails(taskRequest) { + taskRequestContainer.append( + createCustomElement({ + tagName: 'h1', + textContent: `Task Request `, + class: 'taskRequest__title', + child: [ + createCustomElement({ + tagName: 'span', + class: 'taskRequest__title__subtitle', + textContent: `#${taskRequest?.id}`, + }), + ], + }), + createCustomElement({ + tagName: 'p', + textContent: 'Status: ', + class: 'taskRequest__status', + child: [ + createCustomElement({ + tagName: 'span', + textContent: taskRequest?.status, + id: 'taskRequest__status_text', + class: [ + 'taskRequest__status__chip', + `taskRequest__status__chip--${taskRequest?.status?.toLowerCase()}`, + ], + }), + ], + }), + createCustomElement({ + tagName: 'p', + textContent: 'Request Type: ', + class: 'taskRequest__status', + child: [ + createCustomElement({ + tagName: 'span', + textContent: taskRequest?.requestType || 'ASSIGNMENT', + class: [ + 'taskRequest__status__chip', + `taskRequest__status__chip--tag`, + ], + }), + ], + }), + ); +} + +function updateStatus(status) { + const statusText = document.getElementById('taskRequest__status_text'); + statusText.classList = []; + statusText.classList.add('taskRequest__status__chip'); + statusText.classList.add( + `taskRequest__status__chip--${status?.toLowerCase()}`, + ); + statusText.textContent = status; +} + +async function renderTaskDetails(taskRequest) { + const { taskId, taskTitle } = taskRequest; + try { + requestorsContainer.classList.add('requester-border'); + const res = await fetch(`${API_BASE_URL}/tasks/${taskId}/details`); + taskSkeleton.classList.add('hidden'); + const data = await res.json(); + let taskReqAssigneeName = await getAssigneeName(); + + const { taskData } = data ?? {}; + + taskContainer.append( + createCustomElement({ + tagName: 'h2', + class: 'task__title', + textContent: taskData?.title || taskTitle || 'N/A', + }), + createCustomElement({ + tagName: 'p', + class: 'task_type', + textContent: 'Type: ', + child: [ + taskData?.type + ? createCustomElement({ + tagName: 'span', + class: [ + 'task__type__chip', + `task__type__chip--${taskData?.type}`, + ], + textContent: taskData?.type, + }) + : '', + taskData?.isNoteworthy + ? createCustomElement({ + tagName: 'span', + class: ['task__type__chip', `task__type__chip--noteworthy`], + textContent: 'Note worthy', + }) + : '', + ], + }), + createCustomElement({ + tagName: 'p', + class: 'task__createdBy', + textContent: `Created By: `, + child: [ + createCustomElement({ + tagName: 'a', + href: `https://members.realdevsquad.com/${taskData?.createdBy}`, + textContent: taskData?.createdBy || 'N/A', + }), + ], + }), + createCustomElement({ + tagName: 'p', + class: 'task__createdBy', + textContent: `Purpose : ${taskData?.purpose ?? 'N/A'}`, + }), + ); + renderAssignedTo(taskReqAssigneeName); + } catch (e) { + console.error(e); + } +} + +function getAvatar(user) { + if (user?.user?.picture?.url) { + return createCustomElement({ + tagName: 'img', + src: user?.user?.picture?.url, + alt: user?.user?.first_name, + title: user?.user?.first_name, + className: 'circular-image', + }); + } + return createCustomElement({ + tagName: 'span', + title: user?.user?.first_name, + textContent: user?.user?.first_name[0], + }); +} + +async function updateTaskRequest(action, userId) { + const removeSpinner = addSpinner(container); + container.classList.add('container-disabled'); + try { + const queryParams = new URLSearchParams({ action: action }); + const res = await fetch(`${API_BASE_URL}/taskRequests?${queryParams}`, { + credentials: 'include', + method: 'PATCH', + body: JSON.stringify({ + taskRequestId, + userId, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (res.ok) { + showToast('Task updated Successfully', 'success'); + taskRequest = await fetchTaskRequest(); + requestorsContainer.innerHTML = ''; + updateStatus(taskRequest.status); + renderRequestors(taskRequest); + renderRejectButton(taskRequest); + return res; + } else { + showToast(errorMessage, 'failure'); + } + } catch (e) { + showToast(errorMessage, 'failure'); + console.error(e); + } finally { + removeSpinner(); + container.classList.remove('container-disabled'); + } +} + +function getActionButton(requestor) { + if (taskRequest?.status === taskRequestStatus.APPROVED) { + if (taskRequest.approvedTo === requestor?.user?.id) { + return createCustomElement({ + tagName: 'p', + textContent: 'Approved', + class: ['requestors__container__list__approved'], + }); + } else { + return ''; + } + } + return createCustomElement({ + tagName: 'button', + textContent: 'Approve', + class: 'requestors__conatainer__list__button', + eventListeners: [ + { + event: 'click', + func: () => + updateTaskRequest(TaskRequestAction.APPROVE, requestor.user?.id), + }, + ], + }); +} + +async function renderRequestors(taskRequest) { + const requestors = taskRequest?.users; + requestorSkeleton.classList.remove('hidden'); + const data = await Promise.all( + requestors.map((requestor) => { + return fetch(`${API_BASE_URL}/users/userId/${requestor.userId}`).then( + (res) => res.json(), + ); + }), + ); + + requestorSkeleton.classList.add('hidden'); + + data.forEach((requestor, index) => { + const userDetailsDiv = createCustomElement({ + tagName: 'li', + child: [ + createCustomElement({ + tagName: 'div', + class: 'requestors__container__list__userDetails', + child: [ + createCustomElement({ + tagName: 'div', + class: 'requestors__container__list__userDetails__avatar', + child: [getAvatar(requestor)], + }), + createCustomElement({ + tagName: 'div', + class: 'requestors__container__list__userDetails__info', + child: [ + createCustomElement({ + tagName: 'p', + class: 'info__name', + textContent: requestor.user?.first_name, + }), + createCustomElement({ + tagName: 'a', + textContent: 'details>', + class: 'info__more', + eventListeners: [ + { + event: 'click', + func: () => populateModalContent(index), + }, + ], + }), + ], + }), + ], + }), + createCustomElement({ + tagName: 'div', + child: [ + taskRequest.status !== 'DENIED' ? getActionButton(requestor) : '', + ], + }), + ], + }); + const avatarDiv = userDetailsDiv.querySelector( + '.requestors__container__list__userDetails__avatar', + ); + requestorsContainer.append(userDetailsDiv); + }); +} + +async function fetchTaskRequest() { + const res = await fetch(`${API_BASE_URL}/taskRequests/${taskRequestId}`, { + credentials: 'include', + }); + + const { data } = await res.json(); + const approvedTo = data.users + .filter((user) => user.status === 'APPROVED') + ?.map((user) => user.userId)?.[0]; + data.approvedTo = approvedTo; + return data; +} + +const renderGithubIssue = async () => { + converter = new showdown.Converter({ + tables: true, + simplifiedAutoLink: true, + tasklists: true, + simplifiedAutoLink: true, + ghCodeBlocks: true, + openLinksInNewWindow: true, + }); + let res = await fetch(taskRequest?.externalIssueUrl); + res = await res.json(); + taskSkeleton.classList.add('hidden'); + taskContainer.classList.add('task__issue__container'); + taskContainer.append( + createCustomElement({ + tagName: 'h2', + innerHTML: res?.title, + id: 'issue_title', + }), + ); + taskContainer.appendChild( + createCustomElement({ + tagName: 'p', + id: 'issue_time_author', + child: [ + createCustomElement({ + tagName: 'span', + textContent: + 'Opened on ' + new Date(res?.created_at).toDateString() + ' by ', + }), + createCustomElement({ + tagName: 'a', + href: res?.user?.html_url, + textContent: res?.user?.login, + }), + ], + }), + ); + html = converter.makeHtml(res?.body); + taskContainer.appendChild( + createCustomElement({ + tagName: 'div', + innerHTML: html, + }), + ); + + if (res?.assignee) { + taskContainer.appendChild( + createCustomElement({ + tagName: 'p', + id: 'issue_assignee', + child: [ + createCustomElement({ + tagName: 'span', + child: [ + createCustomElement({ + tagName: 'span', + textContent: 'Assigned to: ', + }), + createCustomElement({ + tagName: 'a', + class: 'card__link', + textContent: res?.assignee?.login, + href: res?.assignee?.html_url, + }), + ], + }), + ], + }), + ); + } + taskContainer.appendChild( + createCustomElement({ + tagName: 'p', + id: 'issue_link', + class: 'card__link_issue', + child: [ + createCustomElement({ + tagName: 'span', + textContent: 'Issue link: ', + }), + createCustomElement({ + tagName: 'a', + class: 'card__link', + textContent: res?.html_url, + href: res?.html_url || '#', + }), + ], + }), + ); + taskContainer.appendChild( + createCustomElement({ + tagName: 'div', + child: res?.labels.map((label) => + createCustomElement({ + tagName: 'button', + textContent: label?.name, + class: 'card__tag', + }), + ), + }), + ); +}; +const renderRejectButton = (taskRequest) => { + if (taskRequest?.status !== 'PENDING') { + rejectButton.disabled = true; + } + + rejectButton.addEventListener('click', async () => { + const res = await updateTaskRequest(TaskRequestAction.REJECT); + if (res?.ok) { + rejectButton.disabled = true; + } + }); +}; +const renderTaskRequest = async () => { + taskRequestSkeleton.classList.remove('hidden'); + taskContainer.classList.remove('hidden'); + try { + taskRequest = await fetchTaskRequest(); + taskRequestSkeleton.classList.add('hidden'); + renderRejectButton(taskRequest); + renderTaskRequestDetails(taskRequest); + + if (taskRequest?.requestType === 'CREATION') { + renderGithubIssue(); + } else if (taskRequest?.requestType === 'ASSIGNMENT') { + renderTaskDetails(taskRequest); + } + renderRequestors(taskRequest); + } catch (e) { + console.error(e); + } +}; + +function showToast(message, type) { + toast.innerHTML = `
    ${message}
    `; + toast.classList.remove('hidden'); + + if (type === 'success') { + toast.classList.add('success'); + toast.classList.remove('failure'); + } else if (type === 'failure') { + toast.classList.add('failure'); + toast.classList.remove('success'); + } + + setTimeout(() => { + toast.classList.add('hidden'); + toast.innerHTML = ''; + }, 5000); +} + +async function getAssigneeName() { + let userName = ''; + let res; + if (taskRequest.approvedTo) { + try { + res = await fetch( + `${API_BASE_URL}/users/userId/${taskRequest.approvedTo}`, + ); + } catch (error) { + console.error(error); + } + if (res.ok) { + const userData = await res.json(); + userName = userData.user.first_name; + } + } + return userName; +} + +async function renderAssignedTo(userName) { + const assignedToText = 'Assigned To: '; + const linkOrText = userName.length + ? `${userName}` + : 'N/A'; + + taskContainer.append( + createCustomElement({ + tagName: 'p', + class: 'task__createdBy', + id: 'task__createdBy', + innerHTML: assignedToText + linkOrText, + }), + ); +} + +const openModalBtn = document.getElementById('requestor_details_modal_open'); +const closeModal = document.getElementById('requestor_details_modal_close'); + +const modalOverlay = document.getElementById('overlay'); + +closeModal.addEventListener('click', function () { + modalOverlay.style.display = 'none'; +}); +modalOverlay.addEventListener('click', function (event) { + if (event.target == modalOverlay) { + modalOverlay.style.display = 'none'; + } +}); + +function populateModalContent(index) { + if ( + !Array.isArray(taskRequest.users) || + index < 0 || + index >= taskRequest.users.length + ) { + showToast('No Data Available for this requestor', 'failure'); + return; + } + const modal = document.getElementById('requestor_details_modal_content'); + const userData = taskRequest.users[index]; + + const modalContent = modal.querySelector('.requestor_details_modal_info'); + + const proposedStartDateText = document.createElement('p'); + proposedStartDateText.setAttribute( + 'data-modal-start-date-text', + 'proposed-start-date-text', + ); + proposedStartDateText.innerHTML = 'Proposed Start Date:'; + + const proposedStartDateValue = document.createElement('p'); + proposedStartDateValue.setAttribute( + 'data-modal-start-date-value', + 'proposed-start-date-value', + ); + proposedStartDateValue.textContent = getHumanReadableDate( + userData.proposedStartDate, + ); + + const proposedDeadlineText = document.createElement('p'); + proposedDeadlineText.setAttribute( + 'data-modal-end-date-text', + 'proposed-end-date-text', + ); + proposedDeadlineText.innerHTML = 'Proposed Deadline:'; + + const proposedDeadlineValue = document.createElement('p'); + proposedDeadlineValue.setAttribute( + 'data-modal-end-date-value', + 'proposed-end-date-value', + ); + proposedDeadlineValue.textContent = getHumanReadableDate( + userData.proposedDeadline, + ); + + const descriptionText = document.createElement('p'); + descriptionText.setAttribute( + 'data-modal-description-text', + 'proposed-description-text', + ); + descriptionText.innerHTML = 'Description:'; + + const descriptionValue = document.createElement('p'); + descriptionValue.setAttribute( + 'data-modal-description-value', + 'proposed-description-value', + ); + descriptionValue.textContent = userData.description; + + const header = document.createElement('h2'); + header.setAttribute('data-modal-header', 'requestor-details-header'); + header.className = 'requestor_details_modal_heading'; + header.textContent = 'Requestor Details'; + + modalContent.innerHTML = ''; + + modalContent.appendChild(header); + modalContent.appendChild(proposedStartDateText); + modalContent.appendChild(proposedStartDateValue); + modalContent.appendChild(proposedDeadlineText); + modalContent.appendChild(proposedDeadlineValue); + modalContent.appendChild(descriptionText); + modalContent.appendChild(descriptionValue); + modalOverlay.style.display = 'block'; +} + +renderTaskRequest(); diff --git a/taskRequests/details/style.css b/taskRequests/details/style.css new file mode 100644 index 00000000..9d35d32a --- /dev/null +++ b/taskRequests/details/style.css @@ -0,0 +1,522 @@ +:root { + font-family: 'Inter', sans-serif; + --color-success: rgba(20, 102, 75, 0.6); + --color-gray-light: #eee; + --color-gray: #666; + --color-green: green; + --color-warn: rgba(199, 129, 18, 0.4); + --color-warn-background: #fcf1e0; + --color-white: white; + --color-rds-blue: #1d1283; + --color-blue-light: #1d1283af; + --color-black: #000; + --color-gray-shade: #aaa; + --color-red-variant1: #f43030; + --color-black-shade-70percent: rgba(0, 0, 0, 0.7); + --color-light-gray: #f4f4f4; + --color-gray-variant2: #888; + --color-red-light: #fadee0; + --color-red-variant2: #ae1820; +} + +body { + padding: 0; + margin: 0; +} + +.hidden { + display: none !important; +} + +.skeleton { + animation: skeleton 2s linear infinite; + border-radius: 0.5rem; + min-height: 0.5rem; + margin: 0.5rem 0; +} + +.header { + background: #1d1283; + padding: 1rem; +} +.header__contents { + max-width: 1440px; + padding: 0.5rem 1rem; + margin: 0 auto; + color: var(--color-white); + display: flex; + align-items: center; + gap: 0.5rem; +} +.header__contents__navlink { + color: var(--color-white); + text-decoration: none; +} +.header__contents__navlink:hover { + text-decoration: underline; +} + +.container { + max-width: 1440px; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(12, 1fr); +} +.container-disabled { + opacity: 50%; + pointer-events: none; +} + +.request-details { + grid-column: auto / span 4; +} + +.taskRequest { + padding: 1rem; + grid-column: 1 / span 12; +} +.taskRequest__skeleton__title { + height: 1.5rem; + width: 50ch; + margin: 0.5rem 0; +} +.taskRequest__skeleton__subtitle { + height: 1rem; + max-width: 30ch; + animation: skeleton 2s linear infinite; +} +.taskRequest__title { + font-weight: 400; + font-size: 2rem; + line-height: 2.5rem; +} +.taskRequest__title__subtitle { + font-size: 1rem; + font-weight: 700; + color: var(--color-gray-variant2); + font-size: 0.875rem; +} +.taskRequest__status__chip { + padding: 0.5rem; + line-height: 1.5rem; + border-radius: 1rem; + font-weight: 700; +} +.taskRequest__status__chip--approved { + background: #e1f9f1; + color: #19805e; +} +.taskRequest__status__chip--waiting { + background: #fcf1e0; + color: #c78112; +} +.taskRequest__status__chip--pending { + background: var(--color-warn-background); + color: var(--color-warn); +} +.taskRequest__status__chip--denied { + background: var(--color-red-light); + color: var(--color-red-variant2); +} +.taskRequest__status__chip--tag { + background: var(--color-gray-light); + color: var(--color-gray); +} +.task__skeleton__title { + height: 1.25rem; + max-width: 45ch; +} +.task__skeleton__details { + height: 0.75rem; + max-width: 20ch; +} +.task__skeleton__description { + height: 0.75rem; + max-width: 75ch; +} + +.task { + grid-column: 1 / span 8; + padding: 1rem; +} +.task__title { + font-size: 1.5rem; + line-height: 2rem; + color: #1d1283; + margin: 0; +} +.task__purpose { + font-size: 0.875rem; + line-height: 1.25rem; + margin-top: 1rem; + max-width: 80ch; +} +.task__type__chip { + padding: 0.5rem; + line-height: 1.5rem; + border-radius: 1rem; + font-weight: 700; + margin: 0 0.25rem; + white-space: nowrap; +} +.task__type__chip--feature { + background: #dfe4ff; + border: solid 1px #9eadfe; + color: #0224df; +} +.task__type__chip--refactor { + background: #fadee0; + border: solid 1px #f19ca1; + color: #ae1820; +} +.task__type__chip--bug { + background: #e1f9f1; + border: solid 1px #7fe6c4; + color: #14664b; +} +.task__type__chip--noteworthy { + background: #14664b; + color: var(--color-white); +} + +.requestors { + padding: 1rem; + align-self: flex-start; +} +.requestors__container__title { + font-size: 1.375rem; + line-height: 1.75rem; + font-weight: 400; + margin: 0; +} +.requestors__container__list { + list-style-type: none; + padding: 0; +} +.requestors__container__list li { + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; +} +.requestors__container__list__userDetails { + display: flex; + gap: 1rem; + align-items: center; +} +.requestors__container__list__userDetails__avatar { + height: 2rem; + width: 2rem; + display: grid; + background-color: #e2e2e2; + place-items: center; + border-radius: 50%; +} +.requestors__container__list li:nth-child(even) { + background: #eee; +} +.info__name { + margin: 0; +} +.info__more { + margin: 0; + margin-top: 2px; + display: block; + width: 100%; + text-align: end; + font-size: 0.7rem; + color: var(--color-rds-blue); + cursor: pointer; +} +.info__more:hover { + color: var(--color-blue-light); +} +.requestors__conatainer__list__button { + padding: 0.375rem 0.5rem; + background: #fff; + border: solid 1px #19805e; + font-weight: 700; + font-size: 1rem; + line-height: 1.5rem; + color: #19805e; + border-radius: 0.25rem; + cursor: pointer; + margin-left: 1rem; +} +.requestors__conatainer__list__button:hover { + color: var(--color-white); + background: #19805e; + transition: 0.3s ease-in-out; +} +.requestors__container__list__approved { + background: transparent; + border: none; + color: #c3c3c3; + font-weight: 600; + margin-left: 1rem; +} +.reject__container { + display: flex; + justify-content: end; + margin-top: 2.5rem; + padding: 1rem; + min-width: 8rem; +} +.request-details__reject__button { + padding: 0.375rem 0.5rem; + width: 80%; + max-width: 10rem; + background: var(--color-white); + border: solid 1px var(--color-red-variant1); + font-weight: 700; + font-size: 1rem; + line-height: 1.5rem; + color: var(--color-red-variant1); + border-radius: 0.25rem; + cursor: pointer; +} +.request-details__reject__button:hover { + color: var(--color-white); + background: var(--color-red-variant1); + transition: 0.3s ease-in-out; +} +.request-details__reject__button:disabled { + color: var(--color-gray-variant2); + background: transparent; + border: solid 1px var(--color-gray-variant2); + pointer-events: none; +} + +.circular-image { + border-radius: 50%; + max-width: 100%; + max-height: 100%; +} + +.success { + color: var(--color-white); + background: var(--color-green); + display: flex; + align-items: center; + flex-direction: column; +} + +.failure { + color: var(--color-white); + background: var(--color-red-variant1); + display: flex; + align-items: center; + flex-direction: column; +} +.spinner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2.5rem; + height: 2.5rem; + border: 9px solid transparent; + border-top: 9px solid var(--color-rds-blue); + border-radius: 50%; + z-index: 100; + animation: spin 1s linear infinite; +} +#toast_task_details { + position: absolute; + top: 90%; + right: 10%; + padding: 10px; + border-radius: 5px; + font-weight: bold; + border: 1px solid; + display: flex; + align-items: center; + flex-direction: column; +} + +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + + 100% { + -webkit-transform: rotate(360deg); + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes skeleton { + 0% { + background: hsl(0, 0%, 75%); + } + 50% { + background: hsl(0, 0%, 95%); + } + 100% { + background: hsl(0, 0%, 75%); + } +} +#task-details img { + max-width: 100%; + border: 1px solid #78716c; + border-radius: 0.5rem; +} +#task-details p a { + word-wrap: break-word; +} +#task-details table { + background: #fff; + border: solid 1px #ddd; + margin-bottom: 1.25rem; + table-layout: auto; + border-spacing: 2px; +} +#task-details table thead { + background: #f5f5f5; +} +#task-details table thead tr th, +#task-details table thead tr td { + color: #222; + font-size: 0.875rem; + font-weight: bold; + padding: 0.5rem 0.625rem 0.625rem; +} +#task-details table thead tr th, +#task-details table tfoot tr th, +#task-details table tfoot tr td, +#task-details table tbody tr th, +#task-details table tbody tr td, +#task-details table tr td { + display: table-cell; + line-height: 1.125rem; +} +#task-details table tr th, +table tr td { + color: #222; + font-size: 0.875rem; + padding: 0.5625rem 0.625rem; + text-align: left; +} +.card__link { + color: #2563eb; + text-decoration: none; +} +.card__link_issue { + color: #78716c; +} + +.card__tag { + display: block; + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + border: none; + border-radius: 1.25rem; + background: #dbeafe; + font-weight: 500; + font-family: Inter; + margin-right: 0.5rem; +} +.task__issue__container { + margin: 0 1rem 1.5rem 1rem; + border-right: solid 1px rgba(0, 0, 0, 0.1); + border: solid 1px rgba(0, 0, 0, 0.1); + border-radius: 0.5rem; +} + +.requester-border { + border-left: solid 1px rgba(0, 0, 0, 0.1); +} + +@media (max-width: 599px) { + .taskRequest__title { + font-size: 1.5rem; + line-height: 1.75rem; + } + .taskRequest__title__subtitle { + font-size: 0.875rem; + line-height: 1rem; + } + .taskRequest__status { + font-size: 0.75rem; + } +} + +@media (max-width: 904px) { + .task { + grid-column: 1 / span 12; + } + + .requestors { + grid-column: 1 / span 12; + border: none; + } + + .taskRequest__skeleton__title { + max-width: 80%; + height: 1rem; + } + .taskRequest__skeleton__subtitle { + max-width: 40%; + } + .request-details { + grid-column: auto; + } +} + +.requestor_details_modal_content { + background-color: var(--color-light-gray); + margin: 10% auto; + padding: 2rem; + border: 1px solid var(--color-gray-variant2); + width: 25%; + text-align: center; + right: 10%; + border-radius: 1rem; +} + +.requestor_details_modal_close { + color: var(--color-gray-shade); + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +.requestor_details_modal_close:hover { + color: var(--color-black); +} + +.requestor_details_modal_textarea { + width: 80%; + min-width: 50%; + max-width: 100%; +} + +.requestor_details_modal_heading { + color: var(--color-rds-blue); +} + +.requestors__container__list__userDetails__avatar, +.requestors__container__list__userDetails__avatar:hover, +p:hover { + cursor: pointer; +} + +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: none; +} diff --git a/taskRequests/index.html b/taskRequests/index.html new file mode 100644 index 00000000..ba7a233a --- /dev/null +++ b/taskRequests/index.html @@ -0,0 +1,95 @@ + + + + + + + + + + + + Task Requests | Real Dev Squad + + + + + + + + + +
    +
    + RDS logo + Home +
    +
    +
    +
    + +
    + + + +
    +
    +
    + + +
    +
    +
    + + diff --git a/taskRequests/script.js b/taskRequests/script.js new file mode 100644 index 00000000..c8e2c26c --- /dev/null +++ b/taskRequests/script.js @@ -0,0 +1,420 @@ +const API_BASE_URL = window.API_BASE_URL; +const taskRequestContainer = document.getElementById('task-request-container'); +const containerBody = document.querySelector('.container__body'); +const filterModal = document.getElementsByClassName(FILTER_MODAL)[0]; +const applyFilterButton = document.getElementById(APPLY_FILTER_BUTTON); +const clearButton = document.getElementById(CLEAR_BUTTON); +const filterButton = document.getElementById(FILTER_BUTTON); +const sortModal = document.getElementsByClassName(SORT_MODAL)[0]; +const containerFilter = document.querySelector(FILTER_CONTAINER); +const lastElementContainer = document.querySelector(LAST_ELEMENT_CONTAINER); +const sortButton = document.querySelector(SORT_BUTTON); +const backDrop = document.querySelector(BACKDROP); +const params = new URLSearchParams(window.location.search); +const isDev = params.get(DEV_FEATURE_FLAG) === 'true'; +const loader = document.querySelector('.container__body__loader'); +const startLoading = () => loader.classList.remove('hidden'); +const stopLoading = () => loader.classList.add('hidden'); +let pageVersion = 0; +let nextLink = ''; +let isDataLoading = false; +let selectedSortButton = null; + +const filterStates = { + dev: true, + status: Status.PENDING, + order: CREATED_TIME, + size: DEFAULT_PAGE_SIZE, +}; + +const updateFilterStates = (key, value) => { + filterStates[key] = value; +}; + +async function getTaskRequests(query = {}, nextLink) { + let finalUrl = + API_BASE_URL + (nextLink || '/taskRequests' + getQueryParamsString(query)); + try { + const res = await fetch(finalUrl, { + credentials: 'include', + }); + + if (res.ok) { + const data = await res.json(); + return data; + } + + if (res.status === 401) { + showMessage('ERROR', ErrorMessages.UNAUTHENTICATED); + return; + } + + if (res.status === 403) { + showMessage('ERROR', ErrorMessages.UNAUTHORIZED); + return; + } + + if (res.status === 404) { + showMessage('ERROR', ErrorMessages.NOT_FOUND); + return; + } + + showMessage('ERROR', ErrorMessages.SERVER_ERROR); + } catch (e) { + console.error(e); + } +} + +function showMessage(type, message) { + const p = document.createElement('p'); + const classes = ['taskRequest__message']; + if (type === 'ERROR') { + classes.push('taskRequest__message--error'); + } + p.classList.add(...classes); + p.textContent = message; + taskRequestContainer.innerHTML = ''; + taskRequestContainer.appendChild(p); +} + +function getAvatar(user) { + if (user?.picture?.url) { + return createCustomElement({ + tagName: 'img', + src: user?.picture?.url, + }); + } + return createCustomElement({ + tagName: 'span', + textContent: user?.first_name?.[0] || '?', + }); +} +function getRemainingCount(requestors) { + if (requestors.length > 3) { + return createCustomElement({ + tagName: 'span', + textContent: `+${requestors.length - 3}`, + }); + } +} +function openTaskDetails(id) { + const url = new URL(`/taskRequests/details`, window.location.href); + + url.searchParams.append('id', id); + window.location.href = url; +} +const changeFilter = () => { + nextLink = ''; + taskRequestContainer.innerHTML = ''; +}; + +sortButton.addEventListener('click', async (event) => { + event.stopPropagation(); + sortModal.classList.toggle('hidden'); + backDrop.style.display = 'flex'; +}); + +backDrop.addEventListener('click', () => { + sortModal.classList.add('hidden'); + filterModal.classList.add('hidden'); + backDrop.style.display = 'none'; +}); + +function toggleStatusCheckbox(statusValue) { + const element = document.querySelector( + `#status-filter input[value=${statusValue}]`, + ); + element.checked = !element.checked; +} +function clearCheckboxes(groupName) { + const checkboxes = document.querySelectorAll(`input[name="${groupName}"]`); + checkboxes.forEach((cb) => { + cb.checked = false; + }); +} +function getCheckedValues(groupName) { + const checkboxes = document.querySelectorAll( + `input[name="${groupName}"]:checked`, + ); + return Array.from(checkboxes).map((cb) => cb.value.toLowerCase()); +} + +filterButton.addEventListener('click', (event) => { + filterModal.classList.toggle('hidden'); + backDrop.style.display = 'flex'; +}); + +applyFilterButton.addEventListener('click', async () => { + filterModal.classList.toggle('hidden'); + const checkedValuesStatus = getCheckedValues('status-filter'); + const checkedValuesRequestType = getCheckedValues('request-type-filter'); + changeFilter(); + if (checkedValuesStatus) { + updateFilterStates('status', checkedValuesStatus); + } + if (checkedValuesRequestType) { + updateFilterStates('requestType', checkedValuesRequestType); + } + await renderTaskRequestCards(filterStates); +}); +clearButton.addEventListener('click', async function () { + clearCheckboxes('status-filter'); + filterModal.classList.toggle('hidden'); + changeFilter(); + updateFilterStates('status', ''); + await renderTaskRequestCards(filterStates); +}); + +function addCheckbox(labelText, value, groupName) { + const group = document.getElementById(groupName); + const label = document.createElement('label'); + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.name = groupName; + checkbox.value = value; + label.innerHTML = checkbox.outerHTML + ' ' + labelText; + label.classList.add('checkbox-label'); + label.appendChild(document.createElement('br')); + group.appendChild(label); +} +function addSortByIcon(name, id, groupName, order) { + const group = document.getElementById(groupName); + + const containerAsc = createSortContainer(id, name, order); + group.appendChild(containerAsc); +} + +function sortModalButtons() { + const assigneeAsc = document.getElementById(ASSIGNEE_COUNT); + const assigneeDesc = document.getElementById(ASSIGNEE_DESC); + const createTimeAsc = document.getElementById(CREATED_TIME); + const createTimeDesc = document.getElementById(CREATED_TIME_DESC); + + const sortModalButtons = [ + assigneeAsc, + assigneeDesc, + createTimeAsc, + createTimeDesc, + ]; + + function toggleSortModal() { + sortModal.classList.toggle('hidden'); + backDrop.style.display = 'none'; + } + + function selectButton(button) { + if (selectedSortButton === button) { + selectedSortButton.classList.remove('selected'); + selectedSortButton = null; + toggleSortModal(); + } else { + if (selectedSortButton) { + selectedSortButton.classList.remove('selected'); + } + selectedSortButton = button; + selectedSortButton.classList.add('selected'); + toggleSortModal(); + } + } + + sortModalButtons.forEach((button) => { + if (button) { + button.addEventListener('click', async () => { + selectButton(button); + changeFilter(); + updateFilterStates('order', button.id); + await renderTaskRequestCards(filterStates); + }); + } + }); + selectButton(createTimeAsc); + toggleSortModal(); +} + +function createSortContainer(id, name, sortOrder) { + const container = document.createElement('div'); + container.classList.add('sort-container', sortOrder); + + container.id = id; + + const nameSpan = document.createElement('span'); + nameSpan.classList.add('sort__button__text'); + nameSpan.textContent = name; + const label = document.createElement('label'); + label.appendChild(nameSpan); + + label.classList.add('sort-label'); + + container.appendChild(label); + + return container; +} + +function populateStatus() { + const statusList = [ + { name: 'Approved', id: 'APPROVED' }, + { name: 'Pending', id: 'PENDING' }, + { name: 'Denied', id: 'DENIED' }, + ]; + const requestList = [ + { name: 'Assignment', id: 'assignment' }, + { name: 'Creation', id: 'creation' }, + ]; + + statusList.map(({ name, id }) => addCheckbox(name, id, 'status-filter')); + + requestList.map(({ name, id }) => + addCheckbox(name, id, 'request-type-filter'), + ); + + const sortByList = [ + { + name: 'Least Requested', + id: 'REQUESTORS_COUNT_ASC', + order: 'asc', + }, + { + name: 'Most Requested', + id: 'REQUESTORS_COUNT_DESC', + order: 'desc', + }, + { + name: 'Newest First', + id: 'CREATED_TIME_DESC', + order: 'desc', + }, + { + name: 'Oldest First', + id: 'CREATED_TIME_ASC', + order: 'asc', + }, + ]; + + sortByList.forEach(({ name, id, order }) => + addSortByIcon(name, id, 'sort_by-filter', order), + ); +} + +populateStatus(); +sortModalButtons(); + +function createTaskRequestCard(taskRequest) { + let { id, task, status, taskTitle, users } = taskRequest; + const card = createCustomElement({ + tagName: 'div', + class: 'taskRequest__card', + eventListeners: [{ event: 'click', func: (e) => openTaskDetails(id, e) }], + child: [ + createCustomElement({ + tagName: 'div', + class: 'taskRequest__card__header', + child: [ + createCustomElement({ + tagName: 'h3', + class: 'taskRequest__card__header__title', + textContent: task?.title || taskTitle, + }), + createCustomElement({ + tagName: 'div', + class: [ + 'taskRequest__card__header__status', + `taskRequest__card__header__status--${status.toLowerCase()}`, + ], + title: status.toLowerCase(), + }), + ], + }), + createCustomElement({ + tagName: 'div', + class: 'taskRequest__card__body', + child: [ + createCustomElement({ + tagName: 'p', + textContent: task?.purpose, + }), + ], + }), + createCustomElement({ + tagName: 'div', + class: 'taskRequest__card__footer', + child: [ + createCustomElement({ + tagName: 'p', + textContent: 'Requested By', + }), + createCustomElement({ + tagName: 'div', + class: 'taskRequest__card__footer__requestor', + child: [ + ...users.map((user, index) => { + if (index < 3) { + return createCustomElement({ + tagName: 'div', + class: 'taskRequest__card__footer__requestor__avatar', + title: user?.first_name, + child: [getAvatar(user)], + }); + } + }), + getRemainingCount(users) || '', + ], + }), + ], + }), + ], + }); + return card; +} + +const intersectionObserver = new IntersectionObserver(async (entries) => { + if (!nextLink) { + return; + } + if (entries[0].isIntersecting && !isDataLoading) { + await renderTaskRequestCards({}, nextLink); + } +}); + +const addIntersectionObserver = () => { + intersectionObserver.observe(lastElementContainer); +}; +const removeIntersectionObserver = () => { + intersectionObserver.unobserve(lastElementContainer); +}; + +async function renderTaskRequestCards(queries = {}, newLink = '') { + pageVersion++; + const currentVersion = pageVersion; + try { + isDataLoading = true; + startLoading(); + const taskRequestResponse = await getTaskRequests(queries, newLink); + const taskRequestsList = taskRequestResponse.data; + nextLink = taskRequestResponse.next; + if (currentVersion !== pageVersion) { + return; + } + taskRequestsList.forEach((taskRequest) => { + taskRequestContainer.appendChild(createTaskRequestCard(taskRequest)); + }); + } catch (error) { + console.error(error); + showMessage('ERROR', ErrorMessages.SERVER_ERROR); + } finally { + if (currentVersion !== pageVersion) return; + stopLoading(); + isDataLoading = false; + if (taskRequestContainer.innerHTML === '') { + showMessage('INFO', 'No task requests found!'); + } + } +} + +async function render() { + toggleStatusCheckbox(Status.PENDING.toUpperCase()); + + await renderTaskRequestCards(filterStates); + addIntersectionObserver(); +} + +render(); diff --git a/taskRequests/style.css b/taskRequests/style.css new file mode 100644 index 00000000..8d5ae76c --- /dev/null +++ b/taskRequests/style.css @@ -0,0 +1,428 @@ +:root { + font-family: 'Inter', sans-serif; + --color-primary: #1d1283; + --color-success: rgba(20, 102, 75, 0.6); + --color-error: #da1e28; + --color-warn: rgba(199, 129, 18, 0.4); + --color-gray-light: #eee; + --color-gray: #666; + --red-color: red; + --light-gray-color: lightgray; + --dark-gray-color: rgb(199, 195, 195); + --blue-hover-color: #11085c; + --black-color: black; + --blue-color: #1d1283; + --white: #ffffff; + --color-text-light: rgba(0, 0, 0, 0.6); + --elevation-1: 0 1px 3px 1px rgba(0, 0, 0, 0.1), + 0 1px 2px 0 rgba(0, 0, 0, 0.1); + --elevation-3: 0px 1px 3px 0px rgba(0, 0, 0, 0.3), + 0px 4px 8px 3px rgba(0, 0, 0, 0.15); + --black-transparent: #000000a8; + --light-gray-color: lightgray; +} + +body { + padding: 0; + margin: 0; +} + +.hidden { + display: none; +} + +.header { + background: var(--color-primary); + padding: 1rem; +} +.header__contents { + max-width: 1440px; + padding: 0.5rem 1rem; + margin: 0 auto; + color: white; + display: flex; + align-items: center; + gap: 0.5rem; +} +.header__contents__navlink { + color: white; + text-decoration: none; +} +.header__contents__navlink:hover { + text-decoration: underline; +} + +.container { + max-width: 1440px; + margin: 0 auto; + padding: 2.5rem; + position: relative; +} +.container__filters { + margin: 1rem 0; + display: none; + gap: 0.5rem; +} +.container__filters__status { + border-radius: 0.5rem; + min-width: 8rem; + padding: 0.5rem 0.25rem; + background: white; + font-size: 1rem; + line-height: 1.25rem; +} +.container__title { + font-weight: 400; + font-size: 2rem; + line-height: 2.5rem; +} +.container__body__loader { + font-weight: 600; + text-align: center; + font-size: 1.5rem; + margin: 2rem; +} +.container__body { + margin-top: 2.5rem; +} +.sort-filters { + display: flex; + justify-content: end; + align-items: center; + gap: 1rem; +} + +.funnel-icon { + width: 1.2rem; + height: 1.5rem; + margin-left: 0.5rem; +} + +.filter-button:hover { + background-color: var(--blue-hover-color); +} +.filter-button { + background-color: var(--blue-color); + color: var(--white); + border: none; + border-radius: 0.4rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 9rem; + height: 2.5rem; + padding: 0.7rem; +} + +.sort-button { + display: flex; + background-color: var(--light-gray-color); + border-radius: 0.4rem; + border: 2.5px solid var(--black-color); + padding: 12px; + height: 2.5rem; + cursor: pointer; +} + +.sort__button__text { + cursor: pointer; +} +.sort-button:hover { + background-color: var(--medium-gray); +} + +.sort-modal { + width: 20%; + min-width: 14rem; + max-width: 18rem; + border: 1px solid var(--light-gray-color); + box-shadow: 0 0 10px var(--black-transparent); + border-radius: 0.31rem; + flex-direction: column; + align-items: center; + padding: 1rem; + padding-bottom: 1.5rem; + z-index: 2; + position: absolute; + top: 5.5rem; + background-color: var(--white); +} + +.sort-container { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 6px; + padding: 0.5rem; + border: 1px solid var(--light-gray-color); + border-radius: 0.31rem; + cursor: pointer; + font-size: 0.9rem; +} +.selected { + background-color: var(--dark-gray-color); +} + +.sort-container:hover { + background-color: var(--color-gray-light); +} + +.sort-label { + display: contents; +} + +/* Filter modal */ +.filter-modal { + width: 20%; + min-width: 14rem; + max-width: 18rem; + border: 1px solid var(--light-gray-color); + box-shadow: 0 0 10px var(--black-transparent); + border-radius: 0.31rem; + flex-direction: column; + align-items: center; + padding: 1rem; + padding-bottom: 1.5rem; + position: absolute; + top: 5.5rem; + background-color: var(--white); +} + +.filter-head, +.sort-head { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1rem 0 1rem; +} + +.filters-container, +.sorts-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.modal-form { + text-align: initial; + padding: 0.5rem; + width: 80%; +} + +.checkbox-label { + display: block; + margin-bottom: 0.5rem; +} + +.clear-btn { + background-color: var(--white); + border: 1px solid var(--light-gray-color); + border-radius: 0.31rem; + padding: 0.31rem 0.62rem; + cursor: pointer; +} + +.clear-btn:hover { + background-color: var(--red-color); + color: var(--white); +} + +.filters { + width: 100%; + padding: 0.62rem; + border: 1px solid var(--light-gray-color); + border-radius: 0.31rem; + margin: 0.31rem 0.62rem; + cursor: pointer; +} + +.filters:hover { + background-color: var(--light-gray-color); + border: 1px solid var(--black-color); +} + +.apply-filter-button { + border: 1px solid var(--light-gray-color); + border-radius: 0.31rem; + padding: 0.62rem; + cursor: pointer; + width: 100%; + background-color: var(--blue-color); + color: var(--white); +} + +.apply-filter-button:hover { + background-color: var(--blue-hover-color); +} + +/* Filter modal end */ + +.backdrop { + display: none; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; +} + +.taskRequest { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; +} +.taskRequest__message { + line-height: 1.25rem; + font-weight: 600; + text-align: center; + font-size: 1.5rem; + width: 100%; + position: absolute; +} +.taskRequest__message--error { + color: var(--color-error); + font-weight: 600; + text-align: center; + font-size: 1.5rem; +} +.taskRequest__card { + cursor: pointer; + padding: 1rem; + box-shadow: var(--elevation-1); + border-radius: 0.5rem; + min-width: 16rem; + display: flex; + flex-flow: column; + gap: 0.5rem; +} +.taskRequest__card:hover { + box-shadow: var(--elevation-3); + transition: 300ms ease-in; +} +.taskRequest__card:active { + box-shadow: var(--elevation-1); + transition: 100ms ease-in; +} +.taskRequest__card__header { + display: flex; + justify-content: space-between; +} +.taskRequest__card__header__title { + margin: 0; + font-weight: 400; + font-size: 1.5rem; + line-height: 2rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 90%; + color: var(--color-primary); +} +.taskRequest__card__header__status { + height: 0.75rem; + width: 0.75rem; + border-radius: 50%; +} +.taskRequest__card__header__status--approved { + background-color: var(--color-success); +} +.taskRequest__card__header__status--waiting { + background-color: var(--color-warn); +} +.taskRequest__card__header__status--pending { + background-color: var(--color-warn); +} +.taskRequest__card__header__status--denied { + background-color: var(--red-color); +} +.taskRequest__card__body p { + font-size: 0.875rem; + line-height: 1.25rem; + margin: 0; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + color: var(--color-text-light); +} +.taskRequest__card__footer { + display: flex; + justify-content: space-between; + align-items: center; +} +.taskRequest__card__footer__requestor { + display: flex; + margin-left: 0.75rem; +} +.taskRequest__card__footer__requestor__avatar { + display: grid; + place-items: center; + height: 2rem; + width: 2rem; + margin-left: -0.75rem; + border: solid 2px white; + overflow: hidden; + border-radius: 50%; + background-color: var(--color-gray-light); +} +.taskRequest__card__footer__requestor__avatar img { + height: 100%; + width: 100%; +} +.taskRequest__card__footer__requestor__avatar span { + font-size: 0.875rem; + color: var(--color-gray); +} + +@media screen and (max-width: 440px) { + .container { + padding: 1.5rem; + } + + .filter-button { + width: min-content; + } + + .filter-text { + display: none; + } + + .filter-modal, + .sort-modal { + top: 4.5rem; + } + .funnel-icon { + margin: auto; + } +} + +@media screen and (max-width: 320px) { + .container { + padding: 1rem; + } + + .filter-modal, + .sort-modal { + top: 4rem; + } +} +@media (max-width: 1439px) { + .taskRequest { + grid-template-columns: repeat(3, 1fr); + } +} +@media (max-width: 905px) { + .taskRequest { + grid-template-columns: 1fr 1fr; + } +} +@media (max-width: 599px) { + .container__filters { + justify-content: space-between; + } + .taskRequest { + grid-template-columns: 1fr; + } +} diff --git a/taskRequests/util.js b/taskRequests/util.js new file mode 100644 index 00000000..d3e55bb2 --- /dev/null +++ b/taskRequests/util.js @@ -0,0 +1,69 @@ +function createCustomElement(domObjectMap) { + const el = document.createElement(domObjectMap.tagName); + for (const [key, value] of Object.entries(domObjectMap)) { + if (key === 'tagName') { + continue; + } + if (key === 'eventListeners') { + value.forEach((obj) => { + el.addEventListener(obj.event, obj.func); + }); + } + if (key === 'class') { + if (Array.isArray(value)) { + el.classList.add(...value); + } else { + el.classList.add(value); + } + } else if (key === 'child') { + el.append(...value); + } else { + el[key] = value; + } + } + return el; +} + +function getQueryParamsString(taskRequestStates) { + let filterQueries = {}; + let sortQueries = {}; + + if (taskRequestStates.status) { + filterQueries.status = taskRequestStates.status; + } + if (taskRequestStates.requestType) { + filterQueries['request-type'] = taskRequestStates.requestType; + } + if (taskRequestStates.order) { + sortQueries = Order[taskRequestStates.order]; + } + + const queryString = generateRqlQuery(filterQueries, sortQueries); + + const urlParams = new URLSearchParams(); + if (taskRequestStates.size) { + urlParams.append('size', taskRequestStates.size); + } + if (queryString) { + urlParams.append('q', queryString); + } + if (taskRequestStates.dev) { + urlParams.append('dev', true); + } + return '?' + urlParams.toString(); +} + +const addSpinner = (container) => { + const spinner = createCustomElement({ + tagName: 'div', + className: 'spinner', + }); + + container.append(spinner); + + function removeSpinner() { + spinner.remove(); + } + + return removeSpinner; +};