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 @@