diff --git a/__tests__/extension-requests/extension-requests.test.js b/__tests__/extension-requests/extension-requests.test.js index 2966e157..392c0de1 100644 --- a/__tests__/extension-requests/extension-requests.test.js +++ b/__tests__/extension-requests/extension-requests.test.js @@ -281,7 +281,7 @@ describe('Tests the Extension Requests Screen', () => { }, body: JSON.stringify(extensionRequestListForAuditLogs), }); - } else if (url === `${STAGING_API_URL}/users/self`) { + } else if (url === `${STAGING_API_URL}/users?profile=true`) { interceptedRequest.respond({ status: 200, contentType: 'application/json', @@ -654,6 +654,165 @@ describe('Tests the Extension Requests Screen', () => { expect(hasSkeletonClassAfter).toBe(false); }); + it('shows error messages for empty title and reason inputs on update under dev feature flag', async () => { + await page.goto(`${LOCAL_TEST_PAGE_URL}/extension-requests/?dev=true`); + const editButtonSelector = '[data-testid="edit-button"]'; + const editButton = await page.$(editButtonSelector); + if (!editButton) { + return; + } + await page.click(editButtonSelector); + const updateButtonSelector = '[data-testid="update-button"]'; + const titleInputSelector = '[data-testid="title-text-input"]'; + const reasonInputSelector = '[data-testid="reason-input-text-area"]'; + const titleErrorSelector = '[data-testid="title-input-error"]'; + const reasonErrorSelector = '[data-testid="reason-input-error"]'; + + await page.evaluate((selector) => { + const element = document.querySelector(selector); + if (element) element.value = ''; + }, titleInputSelector); + + await page.evaluate((selector) => { + const element = document.querySelector(selector); + if (element) element.value = ''; + }, reasonInputSelector); + + await page.click(updateButtonSelector); + + const isTitleErrorVisible = await page + .$eval(titleErrorSelector, (el) => el && !el.classList.contains('hidden')) + .catch(() => false); + + const isReasonErrorVisible = await page + .$eval( + reasonErrorSelector, + (el) => el && !el.classList.contains('hidden'), + ) + .catch(() => false); + expect(isTitleErrorVisible).toBe(true); + expect(isReasonErrorVisible).toBe(true); + }); + + it('shows error message if deadline is set to past date under dev feature flag', async () => { + await page.goto(`${LOCAL_TEST_PAGE_URL}/extension-requests/?dev=true`); + const editButtonSelector = '[data-testid="edit-button"]'; + const editButton = await page.$(editButtonSelector); + if (!editButton) { + return; + } + + await page.click(editButtonSelector); + + const extensionInputSelector = '[data-testid="extension-input"]'; + const extensionErrorSelector = '[data-testid="extension-input-error"]'; + await page.$eval(extensionInputSelector, (input) => { + input.value = '2020-01-01'; + }); + await page.click('[data-testid="update-button"]'); + const isExtensionErrorVisible = await page.$eval( + extensionErrorSelector, + (el) => + !el.classList.contains('hidden') && + el.innerText.includes("Past date can't be the new deadline"), + ); + expect(isExtensionErrorVisible).toBe(true); + }); + + it('hides edit button and displays update wrapper on successful update under dev feature flag', async () => { + await page.goto(`${LOCAL_TEST_PAGE_URL}/extension-requests/?dev=true`); + const editButtonSelector = '[data-testid="edit-button"]'; + const editButton = await page.$(editButtonSelector); + if (!editButton) { + return; + } + + await page.click(editButtonSelector); + + const updateButtonSelector = '[data-testid="update-button"]'; + const updateWrapperSelector = '[data-testid="update-wrapper"]'; + + await page.type('[data-testid="title-text-input"]', 'Valid Title'); + await page.type('[data-testid="reason-input-text-area"]', 'Valid Reason'); + await page.type('[data-testid="extension-input"]', '2050-01-01'); + + await page.click(updateButtonSelector); + + const isEditButtonHidden = await page.$eval(editButtonSelector, (el) => + el.classList.contains('hidden'), + ); + const isUpdateWrapperVisible = await page.$eval( + updateWrapperSelector, + (el) => !el.classList.contains('hidden'), + ); + expect(isEditButtonHidden).toBe(true); + expect(isUpdateWrapperVisible).toBe(true); + }); + + it('handles long title and long reason properly under dev feature flag', async () => { + await page.goto(`${LOCAL_TEST_PAGE_URL}/extension-requests/?dev=true`); + + const editButtonSelector = '[data-testid="edit-button"]'; + const titleInputSelector = '[data-testid="title-text-input"]'; + const reasonInputSelector = '[data-testid="reason-input-text-area"]'; + const titleDisplaySelector = '.title-text'; + const reasonDisplaySelector = '.reason-text'; + + const longTitle = 'A'.repeat(300); + const longReason = 'This is a very long reason '.repeat(50); + + const editButton = await page.$(editButtonSelector); + if (!editButton) { + return; + } + await page.click(editButtonSelector); + + await page.type(titleInputSelector, longTitle); + await page.type(reasonInputSelector, longReason); + + const isTitleTruncated = await page.$eval( + titleDisplaySelector, + (el) => window.getComputedStyle(el).textOverflow === 'ellipsis', + ); + + const isReasonWrapped = await page.$eval( + reasonDisplaySelector, + (el) => window.getComputedStyle(el).whiteSpace === 'normal', + ); + + expect(isTitleTruncated).toBe(true); + expect(isReasonWrapped).toBe(true); + }); + + it('displays an error message for invalid date format under dev feature flag', async () => { + await page.goto(`${LOCAL_TEST_PAGE_URL}/extension-requests/?dev=true`); + + const editButtonSelector = '[data-testid="edit-button"]'; + const editButton = await page.$(editButtonSelector); + if (!editButton) { + return; + } + + await page.click(editButtonSelector); + + const extensionInputSelector = '[data-testid="extension-input"]'; + const extensionErrorSelector = '[data-testid="extension-input-error"]'; + + await page.$eval(extensionInputSelector, (input) => { + input.value = 'invalid-date'; + }); + await page.click('[data-testid="update-button"]'); + + const isExtensionErrorVisible = await page.$eval( + extensionErrorSelector, + (el) => + !el.classList.contains('hidden') && + el.innerText.includes('Invalid date format.'), + ); + + expect(isExtensionErrorVisible).toBe(true); + }); + it('Checks whether the card is not removed from display when api call is unsuccessful', async () => { const extensionCards = await page.$$('.extension-card'); diff --git a/__tests__/groups/group.test.js b/__tests__/groups/group.test.js index 0a31114f..be21648d 100644 --- a/__tests__/groups/group.test.js +++ b/__tests__/groups/group.test.js @@ -313,7 +313,7 @@ describe('Discord Groups Page', () => { test('Should display delete button for super users', async () => { setSuperUserPermission(); - await page.goto(`${LOCAL_TEST_PAGE_URL}/groups?dev=true`); + await page.goto(`${LOCAL_TEST_PAGE_URL}/groups`); await page.waitForNetworkIdle(); await page.waitForTimeout(1000); @@ -325,15 +325,6 @@ describe('Discord Groups Page', () => { test('Should not display delete button when user is normal user', async () => { resetUserPermission(); - await page.goto(`${LOCAL_TEST_PAGE_URL}/groups?dev=true`); - await page.waitForNetworkIdle(); - - const deleteButtons = await page.$$('.delete-group'); - expect(deleteButtons.length).toBe(0); - }); - - test('Should not display delete button when dev=false', async () => { - setSuperUserPermission(); await page.goto(`${LOCAL_TEST_PAGE_URL}/groups`); await page.waitForNetworkIdle(); @@ -343,7 +334,7 @@ describe('Discord Groups Page', () => { test('Should display delete confirmation modal on click of delete button', async () => { setSuperUserPermission(); - await page.goto(`${LOCAL_TEST_PAGE_URL}/groups?dev=true`); + await page.goto(`${LOCAL_TEST_PAGE_URL}/groups`); await page.waitForNetworkIdle(); await page.waitForTimeout(1000); @@ -359,7 +350,7 @@ describe('Discord Groups Page', () => { test('Should close delete confirmation modal when cancel button is clicked', async () => { setSuperUserPermission(); - await page.goto(`${LOCAL_TEST_PAGE_URL}/groups?dev=true`); + await page.goto(`${LOCAL_TEST_PAGE_URL}/groups`); await page.waitForNetworkIdle(); await page.waitForTimeout(1000); @@ -372,4 +363,25 @@ describe('Discord Groups Page', () => { const modalClosed = await page.$('.delete-confirmation-modal'); expect(modalClosed).toBeFalsy(); }); + + test('Should render loader when deleting a group', async () => { + setSuperUserPermission(); + await page.goto(`${LOCAL_TEST_PAGE_URL}/groups`); + await page.waitForNetworkIdle(); + await page.waitForTimeout(1000); + + const deleteButton = await page.$('.delete-group'); + await deleteButton.click(); + + const confirmButton = await page.waitForSelector('#confirm-delete'); + confirmButton.click(); + + const loader = await page.waitForSelector('.loader'); + expect(loader).toBeTruthy(); + + await page.waitForTimeout(1000); + + const loaderAfter = await page.$('.loader'); + expect(loaderAfter).toBeFalsy(); + }); }); diff --git a/__tests__/tasks/task-dependency.test.js b/__tests__/tasks/task-dependency.test.js index 26e2035e..3043e26d 100644 --- a/__tests__/tasks/task-dependency.test.js +++ b/__tests__/tasks/task-dependency.test.js @@ -16,49 +16,37 @@ describe('Input box', () => { args: ['--incognito', '--disable-web-security'], devtools: false, }); - page = await browser.newPage(); - await page.setRequestInterception(true); - - page.on('request', (interceptedRequest) => { - const url = interceptedRequest.url(); - if (url === `${STAGING_API_URL}/levels`) { - interceptedRequest.respond({ - status: 200, - contentType: 'application/json', - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - }, - body: JSON.stringify(levels), - }); - } else if (url === `${STAGING_API_URL}/users`) { - interceptedRequest.respond({ - status: 200, - contentType: 'application/json', - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - }, - body: JSON.stringify(users), - }); - } else if (url === `${STAGING_API_URL}/tags`) { - interceptedRequest.respond({ - status: 200, - contentType: 'application/json', - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - }, - body: JSON.stringify(tags), - }); - } else { - interceptedRequest.continue(); - } - }); + // Mock API response setup + const interceptAPI = async (page) => { + await page.setRequestInterception(true); + page.on('request', (interceptedRequest) => { + const url = interceptedRequest.url(); + + const mockResponses = { + [`${STAGING_API_URL}/levels`]: levels, + [`${STAGING_API_URL}/users`]: users, + [`${STAGING_API_URL}/tags`]: tags, + }; + + if (mockResponses[url]) { + interceptedRequest.respond({ + status: 200, + contentType: 'application/json', + headers: { + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify(mockResponses[url]), + }); + } else { + interceptedRequest.continue(); + } + }); + }; + + // Open a shared page instance and intercept API for all tests + page = await browser.newPage(); + await interceptAPI(page); await page.goto('http://localhost:8000/task'); await page.waitForNetworkIdle(); }); @@ -66,29 +54,123 @@ describe('Input box', () => { afterAll(async () => { await browser.close(); }); - it('DependsOn input box should exist', async () => { - const inputBox = await page.evaluate(() => - document.querySelector('.inputBox'), - ); - const linksDisplay = await page.evaluate(() => - document.querySelector('#linksDisplay'), - ); + + // Form Presence Tests + describe('Form Field Presence', () => { + it('should display the title field', async () => { + const titleField = await page.$('[data-testid="title"]'); + expect(titleField).toBeTruthy(); + }); + + it('should display the status field', async () => { + const statusField = await page.$('[data-testid="status"]'); + expect(statusField).toBeTruthy(); + }); + + it('should display the priority field', async () => { + const priorityField = await page.$('[data-testid="priority"]'); + expect(priorityField).toBeTruthy(); + }); + + it('should display the isNoteworthy checkbox', async () => { + const noteworthyField = await page.$('[data-testid="isNoteworthy"]'); + expect(noteworthyField).toBeTruthy(); + }); + + it('should display the purpose field', async () => { + const purposeField = await page.$('[data-testid="purpose"]'); + expect(purposeField).toBeTruthy(); + }); + + it('should display the dependsOn field', async () => { + const dependsOnField = await page.$('[data-testid="dependsOn"]'); + expect(dependsOnField).toBeTruthy(); + }); }); - it('DependsOn input should have correct attributes', async () => { - const input = await page.$('#dependsOn'); - const type = await input.evaluate((el) => el.getAttribute('type')); - const name = await input.evaluate((el) => el.getAttribute('name')); - const id = await input.evaluate((el) => el.getAttribute('id')); - const placeholder = await input.evaluate((el) => - el.getAttribute('placeholder'), - ); - const classList = await input.evaluate((el) => Array.from(el.classList)); - - expect(type).toBe('text'); - expect(name).toBe('dependsOn'); - expect(id).toBe('dependsOn'); - expect(placeholder).toBe('Task ID separated with comma '); - expect(classList.includes('notEditing')).toBeTruthy(); + // Status Field Behavior Tests + describe('Status Field Behavior', () => { + beforeEach(async () => { + await page.goto('http://localhost:8000/task'); + await page.waitForNetworkIdle(); + }); + + it('should have default status as "available"', async () => { + const defaultStatus = await page.$eval( + '[data-testid="status"] select', + (el) => el.value, + ); + expect(defaultStatus).toBe('AVAILABLE'); + }); + + it('should show/hide fields based on "status" selection', async () => { + // Change status to "assigned" + await page.select('[data-testid="status"] select', 'ASSIGNED'); + + const assigneeVisible = await page.$eval( + '[data-testid="assignee"]', + (el) => window.getComputedStyle(el).display !== 'none', + ); + const endsOnVisible = await page.$eval( + '[data-testid="endsOn"]', + (el) => window.getComputedStyle(el).display !== 'none', + ); + expect(assigneeVisible).toBeTruthy(); + expect(endsOnVisible).toBeTruthy(); + + // Change status back to "available" + await page.select('[data-testid="status"] select', 'available'); + + const assigneeHidden = await page.$eval( + '[data-testid="assignee"]', + (el) => window.getComputedStyle(el).display === 'none', + ); + const endsOnHidden = await page.$eval( + '[data-testid="endsOn"]', + (el) => window.getComputedStyle(el).display === 'none', + ); + expect(assigneeHidden).toBeTruthy(); + expect(endsOnHidden).toBeTruthy(); + }); + }); + + // Dev Mode Tests + describe('Dev Mode Behavior', () => { + beforeAll(async () => { + await page.goto('http://localhost:8000/task?dev=true'); + await page.waitForNetworkIdle(); + }); + + it('should hide feature URL field in dev mode', async () => { + const featureUrlField = await page.$('[data-testid="featureUrl"]'); + const display = await page.$eval( + '[data-testid="featureUrl"]', + (el) => window.getComputedStyle(el).display, + ); + expect(display).toBe('none'); + }); + + it('should hide task level field in dev mode', async () => { + const taskLevelField = await page.$('[data-testid="taskLevel"]'); + const display = await page.$eval( + '[data-testid="taskLevel"]', + (el) => window.getComputedStyle(el).display, + ); + expect(display).toBe('none'); + }); + + it('should hide feature/group radio buttons in dev mode', async () => { + const radioButtons = await page.$('[data-testid="radioButtons"]'); + const display = await page.$eval( + '[data-testid="radioButtons"]', + (el) => window.getComputedStyle(el).display, + ); + expect(display).toBe('none'); + }); + + it('should display the dependsOn field in dev mode', async () => { + const dependsOnField = await page.$('[data-testid="dependsOn"]'); + expect(dependsOnField).toBeTruthy(); + }); }); }); diff --git a/extension-requests/constants.js b/extension-requests/constants.js index d3575b10..f74146a3 100644 --- a/extension-requests/constants.js +++ b/extension-requests/constants.js @@ -41,3 +41,5 @@ const SORT_ASC_ICON = 'asc-sort-icon'; const SORT_DESC_ICON = 'desc-sort-icon'; const OLDEST_FIRST = 'Oldest first'; const NEWEST_FIRST = 'Newest first'; + +const UPDATE_TOAST_TIMING = 3000; diff --git a/extension-requests/local-utils.js b/extension-requests/local-utils.js index 78769c11..200d8dc3 100644 --- a/extension-requests/local-utils.js +++ b/extension-requests/local-utils.js @@ -10,7 +10,7 @@ const Order = { }; async function getSelfUser() { try { - const res = await fetch(`${API_BASE_URL}/users/self`, { + const res = await fetch(`${API_BASE_URL}/users?profile=true`, { method: 'GET', credentials: 'include', headers: { @@ -103,8 +103,10 @@ const parseExtensionRequestParams = (uri, nextPageParamsObject) => { return nextPageParamsObject; }; -async function updateExtensionRequest({ id, body }) { - const url = `${API_BASE_URL}/extension-requests/${id}`; +async function updateExtensionRequest({ id, body, underDevFeatureFlag }) { + const url = underDevFeatureFlag + ? `${API_BASE_URL}/extension-requests/${id}?dev=true` + : `${API_BASE_URL}/extension-requests/${id}`; const res = await fetch(url, { credentials: 'include', method: 'PATCH', diff --git a/extension-requests/script.js b/extension-requests/script.js index 19d574c8..d139a6f4 100644 --- a/extension-requests/script.js +++ b/extension-requests/script.js @@ -227,7 +227,21 @@ const getExtensionColor = (deadline, createdTime) => { return 'orange-text'; }; +const currentUserDetailsPromise = getSelfUser() + .then((response) => { + currentUserDetails = response; + }) + .catch((error) => { + currentUserDetails = null; + if (isDev) { + showToast(error?.message || "Couldn't fetch user details.", 'error'); + } + }); + async function populateExtensionRequests(query = {}, newLink) { + if (query.dev && !currentUserDetails) { + await currentUserDetailsPromise; + } extensionPageVersion++; const currentVersion = extensionPageVersion; try { @@ -538,8 +552,25 @@ async function createExtensionCard(data, dev) { id: 'title', name: 'title', value: data.title, + 'data-testid': 'title-text-input', }, }); + const titleInputWrapper = createElement({ + type: 'div', + attributes: { class: 'title-input-wrapper hidden' }, + }); + const titleInputError = createElement({ + type: 'div', + attributes: { + class: 'title-input-error hidden', + 'data-testid': 'title-input-error', + }, + innerText: 'Title is required', + }); + if (dev) { + titleInputWrapper.appendChild(titleInput); + titleInputWrapper.appendChild(titleInputError); + } const commitedHoursHoverCard = createElement({ type: 'div', attributes: { class: 'comitted-hours hidden' }, @@ -562,7 +593,11 @@ async function createExtensionCard(data, dev) { }); commitedHoursHoverCard.appendChild(CommitedHourslabel); commitedHoursHoverCard.appendChild(CommitedHoursContent); - extensionCardHeaderWrapper.appendChild(titleInput); + if (dev) { + extensionCardHeaderWrapper.appendChild(titleInputWrapper); + } else { + extensionCardHeaderWrapper.appendChild(titleInput); + } extensionCardHeaderWrapper.appendChild(titleText); extensionCardHeaderWrapper.appendChild(commitedHoursHoverTrigger); extensionCardHeaderWrapper.appendChild(commitedHoursHoverCard); @@ -770,9 +805,21 @@ async function createExtensionCard(data, dev) { id: 'newEndsOn', oninput: 'this.blur()', value: dateString(secondsToMilliSeconds(data.newEndsOn)), + 'data-testid': 'extension-input', + }, + }); + const extensionInputError = createElement({ + type: 'div', + attributes: { + class: 'extension-input-error hidden', + 'data-testid': 'extension-input-error', }, + innerText: "Past date can't be the new deadline", }); newDeadlineContainer.appendChild(extensionInput); + if (dev) { + newDeadlineContainer.appendChild(extensionInputError); + } extensionForContainer.appendChild(extensionForValue); const extensionRequestNumberContainer = createElement({ type: 'div' }); @@ -866,9 +913,15 @@ async function createExtensionCard(data, dev) { } else { const editButton = createElement({ type: 'button', - attributes: { class: 'edit-button' }, + attributes: { class: 'edit-button', 'data-testid': 'edit-button' }, }); - extensionCardButtons.appendChild(editButton); + if (dev) { + if (shouldDisplayEditButton(data.assigneeId, currentUserDetails)) { + extensionCardButtons.appendChild(editButton); + } + } else { + extensionCardButtons.appendChild(editButton); + } const editIcon = createElement({ type: 'img', attributes: { src: EDIT_ICON, alt: 'edit-icon' }, @@ -876,12 +929,15 @@ async function createExtensionCard(data, dev) { editButton.appendChild(editIcon); const updateWrapper = createElement({ type: 'div', - attributes: { class: 'update-wrapper hidden' }, + attributes: { + class: 'update-wrapper hidden', + 'data-testid': 'update-wrapper', + }, }); extensionCardButtons.appendChild(updateWrapper); const updateButton = createElement({ type: 'button', - attributes: { class: 'update-button' }, + attributes: { class: 'update-button', 'data-testid': 'update-button' }, innerText: 'SAVE', }); @@ -930,10 +986,46 @@ async function createExtensionCard(data, dev) { updateAccordionHeight(panel); }); updateButton.addEventListener('click', (event) => { - toggleInputs(); - toggleActionButtonVisibility(); - editButton.classList.toggle('hidden'); - updateWrapper.classList.toggle('hidden'); + if (dev) { + const isTitleMissing = !titleInput.value; + const isReasonMissing = !reasonInput.value; + const todayDate = Math.floor(new Date().getTime() / 1000); + const newDeadline = new Date(extensionInput.value).getTime() / 1000; + const isDeadlineInPast = newDeadline < todayDate; + const isInvalidDateFormat = isNaN(newDeadline); + + if (isInvalidDateFormat) { + extensionInputError.innerText = + 'Invalid date format. Please provide a valid date.'; + } else if (isDeadlineInPast) { + extensionInputError.innerText = + "Past date can't be the new deadline."; + } + + titleInputError.classList.toggle('hidden', !isTitleMissing); + reasonInputError.classList.toggle('hidden', !isReasonMissing); + extensionInputError.classList.toggle( + 'hidden', + !(isDeadlineInPast || isInvalidDateFormat), + ); + + if ( + !isTitleMissing && + !isReasonMissing && + !(isDeadlineInPast || isInvalidDateFormat) + ) { + toggleInputs(); + toggleActionButtonVisibility(); + editButton.classList.toggle('hidden'); + updateWrapper.classList.toggle('hidden'); + titleInputWrapper.classList.add('hidden'); + } + } else { + toggleInputs(); + toggleActionButtonVisibility(); + editButton.classList.toggle('hidden'); + updateWrapper.classList.toggle('hidden'); + } }); cancelButton.addEventListener('click', (event) => { titleInput.value = data.title; @@ -944,6 +1036,11 @@ async function createExtensionCard(data, dev) { toggleActionButtonVisibility(); editButton.classList.toggle('hidden'); updateWrapper.classList.toggle('hidden'); + if (dev) { + titleInputError.classList.add('hidden'); + reasonInputError.classList.add('hidden'); + extensionInputError.classList.add('hidden'); + } }); const payloadForLog = { body: {}, @@ -1071,10 +1168,22 @@ async function createExtensionCard(data, dev) { class: 'input-text-area hidden', id: 'reason', name: 'reason', + 'data-testid': 'reason-input-text-area', }, innerText: data.reason, }); + const reasonInputError = createElement({ + type: 'span', + attributes: { + class: 'reason-input-error red-text hidden', + 'data-testid': 'reason-input-error', + }, + innerText: 'Reason is required', + }); reasonContainer.appendChild(reasonInput); + if (dev) { + reasonContainer.appendChild(reasonInputError); + } reasonContainer.appendChild(reasonParagraph); const renderExtensionCreatedLog = () => { @@ -1130,6 +1239,17 @@ async function createExtensionCard(data, dev) { e.preventDefault(); let formData = formDataToObject(new FormData(e.target)); formData['newEndsOn'] = new Date(formData['newEndsOn']).getTime() / 1000; + if (dev) { + const todayDate = Math.floor(new Date().getTime() / 1000); + if ( + !formData.title || + !formData.reason || + isNaN(formData['newEndsOn']) || + formData['newEndsOn'] < todayDate + ) { + return; + } + } const removeSpinner = addSpinner(rootElement); rootElement.classList.add('disabled'); const revertDataChange = updateCardData(formData); @@ -1160,17 +1280,29 @@ async function createExtensionCard(data, dev) { updateExtensionRequest({ id: data.id, body: formData, + underDevFeatureFlag: dev, }) .then(() => { data.reason = formData.reason; data.tile = formData.title; data.newEndsOn = data.newEndsOn; handleSuccess(rootElement); + if (dev) { + const successMessage = 'Extension request successfully updated.'; + showToast(successMessage, 'success'); + } appendLogs(payloadForLog, data.id); }) - .catch(() => { + .catch((error) => { revertDataChange(); handleFailure(rootElement); + if (dev) { + const errorMessage = + error?.response?.data?.message || + error?.message || + 'An error occurred. Please try again.'; + showToast(errorMessage, 'error'); + } }) .finally(() => { rootElement.classList.remove('disabled'); @@ -1216,6 +1348,9 @@ async function createExtensionCard(data, dev) { return revertDataChange; } function toggleInputs() { + if (dev) { + titleInputWrapper.classList.toggle('hidden'); + } titleInput.classList.toggle('hidden'); titleText.classList.toggle('hidden'); reasonInput.classList.toggle('hidden'); @@ -1320,6 +1455,34 @@ async function createExtensionCard(data, dev) { } } +function shouldDisplayEditButton(assigneeId, currentUserData) { + return ( + currentUserData && + (assigneeId === currentUserData.id || currentUserData.roles.super_user) + ); +} + +function showToast(message, type) { + const existingToast = document.querySelector( + '.extension-request-update-toast', + ); + if (existingToast) { + existingToast.remove(); + } + const toast = document.createElement('div'); + toast.className = `extension-request-update-toast toast-${type}`; + toast.textContent = message; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.classList.add('fade-out'); + toast.addEventListener('transitionend', () => { + toast.remove(); + }); + }, UPDATE_TOAST_TIMING); +} + function generateSentence(response, parentClassName, id) { let arraySentence = []; let sentence = ''; diff --git a/extension-requests/style.css b/extension-requests/style.css index bfcafebe..f3166f3a 100644 --- a/extension-requests/style.css +++ b/extension-requests/style.css @@ -773,8 +773,43 @@ body { align-items: center; justify-content: space-between; position: relative; - height: 1.6rem; + height: 2rem; +} + +.title-input-wrapper { + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.title-input-error, +.reason-input-error, +.extension-input-error { + font-size: 12px; + color: var(--red-color); } + +.extension-request-update-toast { + position: fixed; + bottom: 24px; + right: 24px; + padding: 16px 24px; + background-color: var(--red-color); + color: #fff; + font-size: 16px; + border-radius: 4px; + opacity: 1; + transition: opacity 0.3s ease; + z-index: 1000; +} + +.extension-request-update-toast.fade-out { + opacity: 0; +} + +.extension-request-update-toast.toast-success { + background-color: var(--green-color); +} + .disabled { opacity: 0.5; pointer-events: none; diff --git a/groups/createElements.js b/groups/createElements.js index 3a7c0270..18c7c8e6 100644 --- a/groups/createElements.js +++ b/groups/createElements.js @@ -56,7 +56,7 @@ const createCard = ( .querySelector('.delete-group') .addEventListener('click', (e) => { e.stopPropagation(); - onDelete(rawGroup.id, rawGroup.roleId); + onDelete(rawGroup.id); }); } diff --git a/groups/render.js b/groups/render.js index 9647b07d..8077f48c 100644 --- a/groups/render.js +++ b/groups/render.js @@ -87,6 +87,27 @@ const removeLoadingCards = () => { loadingCards.forEach((card) => mainContainer.removeChild(card)); }; +const renderLoader = () => { + const loaderContainer = document.querySelector('.loader'); + + if (!loaderContainer) { + const loaderContainer = document.createElement('div'); + loaderContainer.className = 'loader'; + loaderContainer.innerHTML = ` +
+ `; + + document.body.appendChild(loaderContainer); + } +}; + +const removeLoader = () => { + const loader = document.querySelector('.loader'); + if (loader) { + document.body.removeChild(loader); + } +}; + const renderGroupById = ({ group, cardOnClick = () => {}, @@ -148,4 +169,6 @@ export { renderNoGroupFound, renderDeleteConfirmationModal, removeDeleteConfirmationModal, + renderLoader, + removeLoader, }; diff --git a/groups/script.js b/groups/script.js index 7b42e819..b5569b3e 100644 --- a/groups/script.js +++ b/groups/script.js @@ -13,6 +13,8 @@ import { renderNotAuthenticatedPage, renderDeleteConfirmationModal, removeDeleteConfirmationModal, + renderLoader, + removeLoader, } from './render.js'; import { @@ -32,7 +34,6 @@ const QUERY_PARAM_KEY = { DEV_FEATURE_FLAG: 'dev', GROUP_SEARCH: 'name', }; -const isDev = getParamValueFromURL(QUERY_PARAM_KEY.DEV_FEATURE_FLAG) === 'true'; const handler = { set: (obj, prop, value) => { @@ -287,23 +288,23 @@ function renderAllGroups({ cardOnClick }) { renderGroupById({ group: group, cardOnClick: () => cardOnClick(id), - onDelete: isDev ? showDeleteModal : undefined, - isSuperUser: dataStore.isSuperUser && isDev, + onDelete: showDeleteModal, + isSuperUser: dataStore.isSuperUser, }); } }); } } -function showDeleteModal(groupId, roleId) { - if (!isDev) return; +function showDeleteModal(groupId) { renderDeleteConfirmationModal({ onClose: () => { removeDeleteConfirmationModal(); }, onConfirm: async () => { + renderLoader(); try { - await deleteDiscordGroupRole(groupId, roleId); + await deleteDiscordGroupRole(groupId); showToaster('Group deleted successfully'); updateGroup(groupId, { isDeleted: true }); @@ -318,6 +319,7 @@ function showDeleteModal(groupId, roleId) { showToaster(error.message || 'Failed to delete group'); } finally { removeDeleteConfirmationModal(); + removeLoader(); } }, }); diff --git a/groups/style.css b/groups/style.css index b9b966f8..08c9a89e 100644 --- a/groups/style.css +++ b/groups/style.css @@ -699,3 +699,39 @@ For Delete Confirmation Modal .button--danger:hover { background-color: #c82333; } + +/* +For Spin Loader +*/ + +.loader { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.loader-spin { + border: 8px solid #f3f3f3; + border-top: 8px solid #345bdb; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/groups/utils.js b/groups/utils.js index e6437a0e..3ad47aaf 100644 --- a/groups/utils.js +++ b/groups/utils.js @@ -116,19 +116,15 @@ async function removeRoleFromMember(roleId, discordId) { } } -async function deleteDiscordGroupRole(groupId, roleId) { +async function deleteDiscordGroupRole(groupId) { try { - const res = await fetch( - `${BASE_URL}/discord-actions/groups/${groupId}?dev=true`, - { - method: 'DELETE', - credentials: 'include', - headers: { - 'Content-type': 'application/json', - }, - body: JSON.stringify({ roleid: roleId }), + const res = await fetch(`${BASE_URL}/discord-actions/groups/${groupId}`, { + method: 'DELETE', + credentials: 'include', + headers: { + 'Content-type': 'application/json', }, - ); + }); if (!res.ok) { const errorResponse = await res.json(); diff --git a/task-requests/details/style.css b/task-requests/details/style.css index f5ddd3a2..ebd865f4 100644 --- a/task-requests/details/style.css +++ b/task-requests/details/style.css @@ -432,6 +432,10 @@ table tr td { border-radius: 0.5rem; } +.task__issue__container code { + white-space: pre-wrap; +} + .requester-border { border-left: solid 1px rgba(0, 0, 0, 0.1); } diff --git a/task/constants.js b/task/constants.js new file mode 100644 index 00000000..b27eb797 --- /dev/null +++ b/task/constants.js @@ -0,0 +1,4 @@ +const StatusType = { + AVAILABLE: 'AVAILABLE', + ASSIGNED: 'ASSIGNED', +}; diff --git a/task/index.html b/task/index.html index d086ac56..b0dc59df 100644 --- a/task/index.html +++ b/task/index.html @@ -16,7 +16,7 @@
-
+
-
+
-
+
-
+
-
+
@@ -86,7 +90,7 @@ *
-
+
-
+
@@ -112,11 +116,11 @@
-
+

-
-
- - - -
-
+
+ + + +
+
@@ -167,7 +171,7 @@

Suggested users

-
+
Suggested users class="notEditing" />
-
+
-
+
@@ -207,7 +211,7 @@

Suggested users

-
+
@@ -221,7 +225,7 @@

Suggested users

class="notEditing" />
-
+

@@ -249,7 +253,7 @@

Suggested users

/>
-
+

@@ -278,7 +282,7 @@

Suggested users

-
+
Suggested users
+
diff --git a/task/script.js b/task/script.js index 7101fca9..a231a7f5 100644 --- a/task/script.js +++ b/task/script.js @@ -1,9 +1,35 @@ const API_BASE_URL = window.API_BASE_URL; const suggestedUsers = []; const allUser = []; + +const params = new URLSearchParams(window.location.search); +const isDev = params.get('dev') === 'true'; + +// hide fields under isDev feature flag +const containers = [ + 'featureUrlContainer', + 'featureGroupContainer', + 'taskLevelContainer', +]; + +function hideElements(isDev, elementIds) { + if (isDev) { + elementIds.forEach((id) => { + const element = document.getElementById(id); + if (element) { + element.style.display = 'none'; + } + }); + } +} +// hide fields if dev=true +hideElements(isDev, containers); + const category = document.getElementById('category'); category.addEventListener('change', async () => { + if (isDev) return; + try { showSubmitLoader(); const categoryValue = category.value; @@ -54,7 +80,7 @@ const getDaysInEpoch = (remainingDays) => { }; const setDefaultDates = () => { - if (document.getElementById('status').value === 'ASSIGNED') { + if (document.getElementById('status').value === StatusType.ASSIGNED) { const endsOn = document.getElementById('endsOn'); endsOn.value = getFutureDateString(14); endsOn.min = getFutureDateString(1); @@ -71,7 +97,7 @@ function getObjectOfFormData(formId) { const object = {}; const data = new FormData(formId); const isStatusAssigned = - document.getElementById('status').value === 'ASSIGNED'; + document.getElementById('status').value === StatusType.ASSIGNED; data.forEach((value, key) => { if (!Reflect.has(object, key)) { @@ -155,7 +181,7 @@ taskForm.onsubmit = async (e) => { isNoteworthy, } = getObjectOfFormData(taskForm); - if (status === 'ASSIGNED' && !assignee.trim()) { + if (status === StatusType.ASSIGNED && !assignee.trim()) { alert('Assignee can not be empty'); showSubmitLoader(false); document.getElementById('assignee').focus(); @@ -184,7 +210,7 @@ taskForm.onsubmit = async (e) => { isNoteworthy: isNoteworthy == 'on', }; - if (status === 'ASSIGNED') { + if (status === StatusType.ASSIGNED) { dataToBeSent.startedOn = new Date() / 1000; } @@ -192,28 +218,34 @@ taskForm.onsubmit = async (e) => { delete dataToBeSent.endsOn; } - if (status === 'AVIALABLE') { + if (status === StatusType.AVAILABLE) { delete dataToBeSent.endsOn; } - if (dataToBeSent.type == 'feature') { - dataToBeSent.assignee = assignee.trim() ? assignee : ' '; - } + if (isDev) { + delete dataToBeSent.featureUrl; + delete dataToBeSent.type; + delete dataToBeSent.participants; - if (dataToBeSent.type == 'group') { - dataToBeSent.participants = participants.trim() - ? participants.split(',') - : []; + dataToBeSent.assignee = assignee.trim() ? assignee : ' '; + } else { + if (dataToBeSent.featureUrl.trim() === '') { + delete dataToBeSent.featureUrl; + } + if (dataToBeSent.type == 'feature') { + dataToBeSent.assignee = assignee.trim() ? assignee : ' '; + } + if (dataToBeSent.type == 'group') { + dataToBeSent.participants = participants.trim() + ? participants.split(',') + : []; + } } if (dataToBeSent.purpose.trim() === '') { delete dataToBeSent.purpose; } - if (dataToBeSent.featureUrl.trim() === '') { - delete dataToBeSent.featureUrl; - } - dataToBeSent.links = dataToBeSent.links.filter((link) => link); if (dataToBeSent.links.length !== 0) { @@ -248,19 +280,22 @@ taskForm.onsubmit = async (e) => { const result = await response.json(); if (response.ok) { - const body = { - itemId: result.id, - itemType: 'task', - tagPayload: [{ tagId: category, levelId: level }], - }; - await fetch(`${API_BASE_URL}/items`, { - method: 'POST', - credentials: 'include', - body: JSON.stringify(body), - headers: { - 'Content-type': 'application/json', - }, - }); + if (!isDev) { + const body = { + itemId: result.id, + itemType: 'task', + tagPayload: [{ tagId: category, levelId: level }], + }; + + await fetch(`${API_BASE_URL}/items`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(body), + headers: { + 'Content-type': 'application/json', + }, + }); + } alert(result.message); window.location.reload(true); } @@ -289,7 +324,7 @@ let stateHandle = () => { ) { return true; } else if ( - item.value === 'ASSIGNED' && + item.value === StatusType.ASSIGNED && (wasAssigneeSet === false || assigneeEl.value === '') ) { return true; @@ -308,6 +343,8 @@ let stateHandle = () => { }; let hideUnusedField = (radio) => { + if (isDev) return; + const assigneeInput = document.getElementById('assigneeInput'); const participantsInput = document.getElementById('participantsInput'); if ( @@ -364,12 +401,14 @@ const handleDateChange = (event) => { previewDate.innerHTML = !!input.value ? getRemainingDays(input.value) : 14; }; -function handleStatusChange(event = { target: { value: 'AVAILABLE' } }) { +function handleStatusChange( + event = { target: { value: StatusType.AVAILABLE } }, +) { const assignee = document.getElementById('assigneeInput'); const assigneeEl = document.getElementById('assignee'); const endsOnWrapper = document.getElementById('endsOnWrapper'); const featureRadio = document.getElementById('feature'); - if (event.target.value === 'ASSIGNED') { + if (event.target.value === StatusType.ASSIGNED) { setDefaultDates(); assignee.classList.add('show-assignee-field'); assignee.style.display = 'none'; @@ -381,7 +420,7 @@ function handleStatusChange(event = { target: { value: 'AVAILABLE' } }) { document.getElementById('endsOn').value = ''; assigneeEl.value = ''; } - if (event.target.value === 'ASSIGNED' && featureRadio.checked) { + if (event.target.value === StatusType.ASSIGNED && featureRadio.checked) { assignee.style.display = 'flex'; } } @@ -404,6 +443,8 @@ function debounce(func, delay) { } async function fetchTags() { + if (isDev) return; + const response = await fetch(`${API_BASE_URL}/tags`); const data = await response.json(); const { tags } = data; @@ -419,6 +460,8 @@ async function fetchTags() { } async function fetchLevel() { + if (isDev) return; + const response = await fetch(`${API_BASE_URL}/levels`); const data = await response.json(); const { levels } = data;