diff --git a/__tests__/extension-requests/extension-requests.test.js b/__tests__/extension-requests/extension-requests.test.js index b8d50c5e..f758b151 100644 --- a/__tests__/extension-requests/extension-requests.test.js +++ b/__tests__/extension-requests/extension-requests.test.js @@ -667,79 +667,6 @@ describe('Tests the Extension Requests Screen', () => { expect(hasSkeletonClassAfter).toBe(false); }); - it('checks whether the shimmer effect is working for deadlineValue element under feature flag', async () => { - await page.goto(`${baseUrl}/?dev=true`); - const deadlineValueSelector = await page.$$( - '[data-testid="skeleton-span"]', - ); - expect(deadlineValueSelector).toBeTruthy(); - await page.waitForTimeout(5000); - const hasSkeletonClassAfter = await page.$eval('.tooltip-container', (el) => - el.classList.contains('skeleton-span'), - ); - expect(hasSkeletonClassAfter).toBe(false); - }); - - it('checks whether the shimmer effect is working for requestedValue element under feature flag', async () => { - await page.goto(`${baseUrl}/?dev=true`); - const requestedValueSelector = await page.$$( - '[data-testid="skeleton-text"]', - ); - expect(requestedValueSelector).toBeTruthy(); - await page.waitForTimeout(5000); - const hasSkeletonClassAfter = await page.$eval('.requested-day', (el) => - el.classList.contains('skeleton-text'), - ); - expect(hasSkeletonClassAfter).toBe(false); - }); - it('checks whether the shimmer effect is working for newDeadlineValue element under feature flag', async () => { - await page.goto(`${baseUrl}/?dev=true`); - const newDeadlineValueSelector = await page.$$( - '[data-testid="skeleton-span"]', - ); - expect(newDeadlineValueSelector).toBeTruthy(); - await page.waitForTimeout(5000); - const hasSkeletonClassAfter = await page.$eval('.requested-day', (el) => - el.classList.contains('skeleton-span'), - ); - expect(hasSkeletonClassAfter).toBe(false); - }); - - it('checks whether the shimmer effect is working for extensionRequestNumberValue element under feature flag', async () => { - await page.goto(`${baseUrl}/?dev=true`); - const extensionRequestNumberValueSelector = await page.$$( - '[data-testid="skeleton-span"]', - ); - expect(extensionRequestNumberValueSelector).toBeTruthy(); - await page.waitForTimeout(5000); - const hasSkeletonClassAfter = await page.$eval( - '.extension-request-number', - (el) => el.classList.contains('skeleton-span'), - ); - expect(hasSkeletonClassAfter).toBe(false); - }); - - it('checks whether the shimmer effect is visible under dev flag only for the statusSiteLink element', async () => { - await page.goto(`${baseUrl}/?dev=true`); - const statusSiteLinkSelector = await page.$$( - '[data-testid="external-link skeleton-link"]', - ); - expect(statusSiteLinkSelector).toBeTruthy(); - await page.waitForTimeout(5000); - const hasSkeletonClassAfter = await page.$eval('.external-link', (el) => - el.classList.contains('skeleton-link'), - ); - expect(hasSkeletonClassAfter).toBe(false); - }); - - it('checks whether the shimmer effect is visible under dev flag only for the taskStatusValue element', async () => { - await page.goto(`${baseUrl}/?dev=true`); - const taskStatusValueElement = await page.$$( - '[data-testid="skeleton-span"]', - ); - expect(taskStatusValueElement).toBeTruthy(); - }); - 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 9830f377..828758d5 100644 --- a/__tests__/groups/group.test.js +++ b/__tests__/groups/group.test.js @@ -1,10 +1,21 @@ const puppeteer = require('puppeteer'); -const { allUsersData } = require('../../mock-data/users'); +const { allUsersData, superUserData } = require('../../mock-data/users'); const { discordGroups, GroupRoleData } = require('../../mock-data/groups'); const BASE_URL = 'https://api.realdevsquad.com'; const PAGE_URL = 'http://localhost:8000'; +function setSuperUserPermission() { + allUsersData.users[0] = superUserData; +} + +function resetUserPermission() { + allUsersData.users[0] = { + ...allUsersData.users[0], + roles: { archived: false }, + }; +} + describe('Discord Groups Page', () => { let browser; let page; @@ -297,4 +308,66 @@ describe('Discord Groups Page', () => { const repoLinkStyle = await page.evaluate((el) => el.style, repoLink); expect(repoLinkStyle).toBeTruthy(); }); + + test('Should display delete button for super users', async () => { + setSuperUserPermission(); + await page.goto(`${PAGE_URL}/groups?dev=true`); + await page.waitForNetworkIdle(); + await page.waitForTimeout(1000); + + const deleteButtons = await page.$$('.delete-group'); + const cards = await page.$$('.card'); + expect(deleteButtons.length).toBe(cards.length); + expect(deleteButtons.length).toBeGreaterThan(0); + }); + + test('Should not display delete button when user is normal user', async () => { + resetUserPermission(); + await page.goto(`${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(`${PAGE_URL}/groups`); + await page.waitForNetworkIdle(); + + const deleteButtons = await page.$$('.delete-group'); + expect(deleteButtons.length).toBe(0); + }); + + test('Should display delete confirmation modal on click of delete button', async () => { + setSuperUserPermission(); + await page.goto(`${PAGE_URL}/groups?dev=true`); + await page.waitForNetworkIdle(); + await page.waitForTimeout(1000); + + const deleteButton = await page.$('.delete-group'); + await deleteButton.click(); + + const deleteConfirmationModal = await page.waitForSelector( + '.delete-confirmation-modal', + ); + + expect(deleteConfirmationModal).toBeTruthy(); + }); + + test('Should close delete confirmation modal when cancel button is clicked', async () => { + setSuperUserPermission(); + await page.goto(`${PAGE_URL}/groups?dev=true`); + await page.waitForNetworkIdle(); + await page.waitForTimeout(1000); + + const deleteButton = await page.$('.delete-group'); + await deleteButton.click(); + + const cancelButton = await page.waitForSelector('#cancel-delete'); + await cancelButton.click(); + + const modalClosed = await page.$('.delete-confirmation-modal'); + expect(modalClosed).toBeFalsy(); + }); }); diff --git a/__tests__/task-requests/task-requestDetails.test.js b/__tests__/task-requests/task-requestDetails.test.js index 88be744e..0c827846 100644 --- a/__tests__/task-requests/task-requestDetails.test.js +++ b/__tests__/task-requests/task-requestDetails.test.js @@ -3,6 +3,63 @@ const { urlMappings, defaultMockResponseHeaders, } = require('../../mock-data/taskRequests'); +const { user } = require('../../mock-data/users/index.js'); + +describe('Request container for non-super users', () => { + 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 (url == 'https://staging-api.realdevsquad.com/users/self') { + interceptedRequest.respond({ + ...defaultMockResponseHeaders, + body: JSON.stringify(user), + }); + } else if (urlMappings.hasOwnProperty(url)) { + interceptedRequest.respond({ + ...defaultMockResponseHeaders, + body: JSON.stringify(urlMappings[url]), + }); + } else { + interceptedRequest.continue(); + } + }); + await page.goto( + 'http://localhost:8000/task-requests/details/?id=dM5wwD9QsiTzi7eG7Oq5&dev=true', + ); + }); + + afterAll(async () => { + await browser.close(); + }); + + it('Approve and Reject buttons should not render for non-super users', async function () { + await page.waitForNetworkIdle(); + const approveButton = await page.$('[data-testid="task-approve-button"]'); + const rejectButton = await page.$('[data-testid="task-reject-button"]'); + expect(approveButton).toBeNull(); + expect(rejectButton).toBeNull(); + }); + + it('Should render task status for non-super users', async function () { + await page.waitForNetworkIdle(); + const taskRequestStatus = await page.$( + '[data-testid="requestors-task-status"]', + ); + expect(taskRequestStatus).toBeTruthy(); + }); +}); describe('Task request details page', () => { let browser; @@ -89,9 +146,13 @@ describe('Task request details page', () => { ); }); - 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'); + it('Should render Approve and Reject buttons for super users', async function () { + await page.goto( + 'http://localhost:8000/task-requests/details/?id=dM5wwD9QsiTzi7eG7Oq5&dev=true', + ); + await page.waitForNetworkIdle(); + const approveButton = await page.$('[data-testid="task-approve-button"]'); + const rejectButton = await page.$('[data-testid="task-reject-button"]'); expect(approveButton).toBeTruthy(); expect(rejectButton).toBeTruthy(); }); @@ -180,9 +241,13 @@ describe('Task request details page with markdown support in description', () => expect(descriptionHtmlValue).toContain('

Heading

'); }); - 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'); + it('Should render Approve and Reject buttons for super users', async function () { + await page.goto( + 'http://localhost:8000/task-requests/details/?id=dM5wwD9QsiTzi7eG7Oq6&dev=true', + ); + await page.waitForNetworkIdle(); + const approveButton = await page.$('[data-testid="task-approve-button"]'); + const rejectButton = await page.$('[data-testid="task-reject-button"]'); expect(approveButton).toBeTruthy(); expect(rejectButton).toBeTruthy(); }); diff --git a/__tests__/users/App.test.js b/__tests__/users/App.test.js index 05b36b53..f501677c 100644 --- a/__tests__/users/App.test.js +++ b/__tests__/users/App.test.js @@ -37,9 +37,13 @@ describe('App Component', () => { headers, body: JSON.stringify({ ...filteredUsersData, - users: filteredUsersData.users.filter( - (user) => user.roles.in_discord, - ), + ...mockUserData, + users: [ + ...filteredUsersData.users.filter( + (user) => user.roles.in_discord, + ), + ...mockUserData.users.filter((user) => user.roles.in_discord), + ], }), }); } else if (url === `${API_BASE_URL}/users/search/?verified=true`) { @@ -49,9 +53,11 @@ describe('App Component', () => { headers, body: JSON.stringify({ ...filteredUsersData, - users: filteredUsersData.users.filter((user) => user.discordId), ...mockUserData, - users: mockUserData.users.filter((user) => user.discordId), + users: [ + ...filteredUsersData.users.filter((user) => user.discordId), + ...mockUserData.users.filter((user) => user.discordId), + ], }), }); } else { @@ -87,13 +93,55 @@ describe('App Component', () => { }); it('should update the URL query string and re-render the app', async () => { - await page.waitForSelector('[data_key="verified"]'); - - // Click on the "Linked Accounts" tab - await page.click('[data_key="verified"]'); + await page.waitForSelector('[data-testid="tabs-section-select"]'); + await page.select('[data-testid="tabs-section-select"]', 'verified'); // Get the current URL and make sure the query string has been updated const url = await page.url(); expect(url).toContain('?tab=verified'); }); + + it('should handle user card clicks and apply active_tab class to clicked card only in discord tab', async () => { + await page.goto(`${BASE_URL}/users/discord/?tab=in_discord`); + await page.waitForNetworkIdle(); + await page.waitForSelector('.user_card[data-key]'); + const userCardTestIds = await page.$$eval( + '[data-testid^="user-card-"]', + (cards) => cards.map((card) => card.getAttribute('data-testid')), + ); + for (let i = 0; i < userCardTestIds.length; i++) { + const userCardSelector = `[data-testid="${userCardTestIds[i]}"]`; + const userCardElement = await page.$(userCardSelector); + await userCardElement.click(); + await page.waitForTimeout(1000); + const isActive = await page.evaluate((selector) => { + return document + .querySelector(selector) + ?.classList.contains('active_tab'); + }, userCardSelector); + expect(isActive).toBe(true); + } + }); + + it('should handle user card clicks and apply active_tab class to clicked card only verified tab', async () => { + await page.goto(`${BASE_URL}/users/discord/?tab=verified`); + await page.waitForNetworkIdle(); + await page.waitForSelector('.user_card[data-key]'); + const userCardTestIds = await page.$$eval( + '[data-testid^="user-card-"]', + (cards) => cards.map((card) => card.getAttribute('data-testid')), + ); + for (let i = 0; i < userCardTestIds.length; i++) { + const userCardSelector = `[data-testid="${userCardTestIds[i]}"]`; + const userCardElement = await page.$(userCardSelector); + await userCardElement.click(); + await page.waitForTimeout(1000); + const isActive = await page.evaluate((selector) => { + return document + .querySelector(selector) + ?.classList.contains('active_tab'); + }, userCardSelector); + expect(isActive).toBe(true); + } + }); }); diff --git a/__tests__/users/applyFilterPagination.test.js b/__tests__/users/applyFilterPagination.test.js index a987fa99..e9561f0e 100644 --- a/__tests__/users/applyFilterPagination.test.js +++ b/__tests__/users/applyFilterPagination.test.js @@ -78,8 +78,8 @@ describe('Apply Filter and Pagination Functionality', () => { }); it('should update the URL query string when applying filters', async () => { - // click on the "Verified" tab - await page.click('[data_key="verified"]'); + await page.waitForSelector('[data-testid="tabs-section-select"]'); + await page.select('[data-testid="tabs-section-select"]', 'verified'); // get the current URL const url = await page.url(); diff --git a/__tests__/users/user-management-home-screen.test.js b/__tests__/users/user-management-home-screen.test.js index 66f7b60c..f50242eb 100644 --- a/__tests__/users/user-management-home-screen.test.js +++ b/__tests__/users/user-management-home-screen.test.js @@ -9,9 +9,6 @@ describe('Tests the User Management User Listing Screen', () => { let tileViewBtn; let tableViewBtn; let userSearchElement; - let paginationElement; - let prevBtn; - let nextBtn; jest.setTimeout(60000); beforeAll(async () => { @@ -71,9 +68,6 @@ describe('Tests the User Management User Listing Screen', () => { tileViewBtn = await page.$('#tile-view-btn'); tableViewBtn = await page.$('#table-view-btn'); userSearchElement = await page.$('#user-search'); - paginationElement = await page.$('#pagination'); - prevBtn = await page.$('#prevButton'); - nextBtn = await page.$('#nextButton'); }); afterAll(async () => { @@ -85,9 +79,6 @@ describe('Tests the User Management User Listing Screen', () => { expect(tileViewBtn).toBeTruthy(); expect(tableViewBtn).toBeTruthy(); expect(userSearchElement).toBeTruthy(); - expect(paginationElement).toBeTruthy(); - expect(prevBtn).toBeTruthy(); - expect(nextBtn).toBeTruthy(); }); it('Check the UI interactions of tile view and table view button.', async () => { diff --git a/extension-requests/script.js b/extension-requests/script.js index dfe1818b..19d574c8 100644 --- a/extension-requests/script.js +++ b/extension-requests/script.js @@ -623,26 +623,16 @@ async function createExtensionCard(data, dev) { }); deadlineContainer.appendChild(deadlineText); - let deadlineValue; - if (dev) { - deadlineValue = createElement({ - type: 'span', - attributes: { - class: 'skeleton-span', - 'data-testid': 'skeleton-span', - }, - }); - } else { - deadlineValue = createElement({ - type: 'span', - innerText: `${deadlineDays}`, - attributes: { - class: `tooltip-container ${ - isDeadLineCrossed && isStatusPending ? 'red-text' : '' - }`, - }, - }); - } + const deadlineValue = createElement({ + type: 'span', + innerText: `${deadlineDays}`, + attributes: { + class: `tooltip-container ${ + isDeadLineCrossed && isStatusPending ? 'red-text' : '' + }`, + }, + }); + deadlineContainer.appendChild(deadlineValue); const deadlineTooltip = createElement({ type: 'span', @@ -662,24 +652,14 @@ async function createExtensionCard(data, dev) { }); requestedContainer.appendChild(requestedText); - let requestedValue; - if (dev) { - requestedValue = createElement({ - type: 'span', - attributes: { - class: 'skeleton-text', - 'data-testid': 'skeleton-text', - }, - }); - } else { - requestedValue = createElement({ - type: 'span', - attributes: { - class: `requested-day tooltip-container ${requestedDaysTextColor}`, - }, - innerText: `${requestedDaysAgo}`, - }); - } + const requestedValue = createElement({ + type: 'span', + attributes: { + class: `requested-day tooltip-container ${requestedDaysTextColor}`, + }, + innerText: ` ${requestedDaysAgo}`, + }); + const requestedToolTip = createElement({ type: 'span', attributes: { class: 'tooltip' }, @@ -744,19 +724,12 @@ async function createExtensionCard(data, dev) { }); newDeadlineContainer.appendChild(newDeadlineText); - let newDeadlineValue; - if (dev) { - newDeadlineValue = createElement({ - type: 'span', - attributes: { class: 'skeleton-span', 'data-testid': 'skeleton-span' }, - }); - } else { - newDeadlineValue = createElement({ - type: 'span', - attributes: { class: 'requested-day tooltip-container' }, - innerText: ` ${newDeadlineDays}`, - }); - } + const newDeadlineValue = createElement({ + type: 'span', + attributes: { class: 'requested-day tooltip-container' }, + innerText: ` ${newDeadlineDays}`, + }); + const newDeadlineToolTip = createElement({ type: 'span', attributes: { class: 'tooltip' }, @@ -776,19 +749,12 @@ async function createExtensionCard(data, dev) { }); extensionForContainer.appendChild(extensionForText); - let extensionForValue; - if (dev) { - extensionForValue = createElement({ - type: 'span', - attributes: { class: 'skeleton-span' }, - }); - } else { - extensionForValue = createElement({ - type: 'span', - attributes: { class: 'tooltip-container' }, - innerText: ` +${extensionDays}`, - }); - } + const extensionForValue = createElement({ + type: 'span', + attributes: { class: 'tooltip-container' }, + innerText: ` +${extensionDays}`, + }); + const extensionToolTip = createElement({ type: 'span', attributes: { class: 'tooltip' }, @@ -821,19 +787,12 @@ async function createExtensionCard(data, dev) { const requestNumber = data.requestNumber || 1; - let extensionRequestNumberValue; - if (dev) { - extensionRequestNumberValue = createElement({ - type: 'span', - attributes: { class: 'skeleton-span', 'data-testid': 'skeleton-span' }, - }); - } else { - extensionRequestNumberValue = createElement({ - type: 'span', - attributes: { class: 'extension-request-number' }, - innerText: `#${requestNumber}`, - }); - } + const extensionRequestNumberValue = createElement({ + type: 'span', + attributes: { class: 'extension-request-number' }, + innerText: `#${requestNumber}`, + }); + extensionRequestNumberContainer.appendChild(extensionRequestNumberValue); const cardAssigneeButtonContainer = createElement({ type: 'div', @@ -1330,44 +1289,7 @@ async function createExtensionCard(data, dev) { CommitedHoursContent.innerText = 'Missing'; CommitedHoursContent.classList.add('label-content-missing'); } - if (dev) { - deadlineValue.classList.remove('skeleton-span'); - deadlineValue.innerText = `${deadlineDays}`; - - deadlineValue.classList.add('tooltip-container'); - if (isDeadLineCrossed && isStatusPending) { - deadlineValue.classList.add('red-text'); - } - } - - if (dev) { - requestedValue.classList.remove('skeleton-text'); - requestedValue.innerText = `${requestedDaysAgo}`; - - requestedValue.classList.add( - 'requested-day', - 'tooltip-container', - requestedDaysTextColor, - ); - } - - if (dev) { - newDeadlineValue.classList.remove('skeleton-span'); - newDeadlineValue.innerText = ` ${newDeadlineDays}`; - newDeadlineValue.classList.add('requested-day', 'tooltip-container'); - } - if (dev) { - extensionForValue.classList.remove('skeleton-span'); - extensionForValue.innerText = ` +${extensionDays}`; - extensionForValue.classList.add('tooltip-container'); - } - - if (dev) { - extensionRequestNumberValue.classList.remove('skeleton-span'); - extensionRequestNumberValue.innerText = `#${requestNumber}`; - extensionRequestNumberValue.classList.add('extension-request-number'); - } if (!dev) { removeSpinner(); renderExtensionCreatedLog(); diff --git a/groups/assets/delete.svg b/groups/assets/delete.svg new file mode 100644 index 00000000..1ca5a7a1 --- /dev/null +++ b/groups/assets/delete.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/groups/createElements.js b/groups/createElements.js index 44254e8a..3a7c0270 100644 --- a/groups/createElements.js +++ b/groups/createElements.js @@ -1,4 +1,9 @@ -const createCard = (rawGroup, onClick = () => {}) => { +const createCard = ( + rawGroup, + onClick = () => {}, + onDelete = () => {}, + isSuperUser = false, +) => { const group = { ...rawGroup, description: @@ -10,7 +15,17 @@ const createCard = (rawGroup, onClick = () => {}) => { cardElement.className = 'card'; cardElement.id = `group-${group.id}`; cardElement.innerHTML = ` -
+
+
+ ${ + isSuperUser + ? ` + ` + : '' + } +

@@ -36,6 +51,15 @@ const createCard = (rawGroup, onClick = () => {}) => { .querySelector('.card__btn') .addEventListener('click', () => group.isUpdating || onClick()); + if (isSuperUser) { + cardElement + .querySelector('.delete-group') + .addEventListener('click', (e) => { + e.stopPropagation(); + onDelete(rawGroup.id, rawGroup.roleId); + }); + } + return cardElement; }; @@ -214,6 +238,44 @@ const createGroupCreationModal = (onClose = () => {}, onSubmit = () => {}) => { return backdropElement; }; +const createDeleteConfirmationModal = ( + onClose = () => {}, + onConfirm = () => {}, +) => { + const backdropElement = document.createElement('div'); + backdropElement.className = 'backdrop'; + + const modalElement = document.createElement('div'); + modalElement.className = 'delete-confirmation-modal'; + modalElement.innerHTML = ` +
+

Confirm Delete

+ +
+
+

Are you sure you want to delete this group?

+
+ +
+ + +
+ `; + + modalElement.querySelector('#close-button').onclick = onClose; + modalElement.querySelector('#cancel-delete').onclick = onClose; + modalElement.querySelector('#confirm-delete').onclick = onConfirm; + + backdropElement.appendChild(modalElement); + backdropElement.onclick = (e) => { + if (e.target === backdropElement) onClose(); + }; + + return backdropElement; +}; + export { createCard, createLoadingCard, @@ -222,4 +284,5 @@ export { createNavbarProfileLoading, createNavbarProfileSignin, createGroupCreationModal, + createDeleteConfirmationModal, }; diff --git a/groups/index.html b/groups/index.html index b58fcdcf..a2e188ab 100644 --- a/groups/index.html +++ b/groups/index.html @@ -10,6 +10,7 @@ + diff --git a/groups/render.js b/groups/render.js index c8513b2a..9647b07d 100644 --- a/groups/render.js +++ b/groups/render.js @@ -6,6 +6,7 @@ import { createNavbarProfile, createNavbarProfileLoading, createNavbarProfileSignin, + createDeleteConfirmationModal, } from './createElements.js'; const renderNotAuthenticatedPage = () => { @@ -86,8 +87,13 @@ const removeLoadingCards = () => { loadingCards.forEach((card) => mainContainer.removeChild(card)); }; -const renderGroupById = ({ group, cardOnClick = () => {} }) => { - const card = createCard(group, cardOnClick); +const renderGroupById = ({ + group, + cardOnClick = () => {}, + onDelete = () => {}, + isSuperUser = false, +}) => { + const card = createCard(group, cardOnClick, onDelete, isSuperUser); const mainContainer = document.querySelector('.group-container'); const groupElement = document.getElementById(`group-${group.id}`); if (groupElement) { @@ -105,6 +111,29 @@ const renderNoGroupFound = () => { mainContainer.append(noGroupContainer); }; +const renderDeleteConfirmationModal = ({ + onClose = () => {}, + onConfirm = () => {}, +}) => { + const container = document.querySelector('body'); + const existingBackdrop = document.querySelector('.backdrop'); + + if (existingBackdrop) { + container.removeChild(existingBackdrop); + } + + const modal = createDeleteConfirmationModal(onClose, onConfirm); + container.appendChild(modal); +}; + +const removeDeleteConfirmationModal = () => { + const container = document.querySelector('body'); + const backdrop = document.querySelector('.backdrop'); + if (backdrop) { + container.removeChild(backdrop); + } +}; + export { renderNotAuthenticatedPage, renderGroupCreationModal, @@ -117,4 +146,6 @@ export { removeLoadingCards, renderGroupById, renderNoGroupFound, + renderDeleteConfirmationModal, + removeDeleteConfirmationModal, }; diff --git a/groups/script.js b/groups/script.js index 9ac12657..7b42e819 100644 --- a/groups/script.js +++ b/groups/script.js @@ -11,7 +11,10 @@ import { renderNavbarProfile, renderNavbarProfileSignin, renderNotAuthenticatedPage, + renderDeleteConfirmationModal, + removeDeleteConfirmationModal, } from './render.js'; + import { addGroupRoleToMember, createDiscordGroupRole, @@ -22,11 +25,14 @@ import { getDiscordGroupIdsFromSearch, getParamValueFromURL, setParamValueInURL, + deleteDiscordGroupRole, } from './utils.js'; 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) => { @@ -42,11 +48,16 @@ const handler = { .filter( (ng) => JSON.stringify(oldGroups?.[ng.id]) !== JSON.stringify(ng), ) - .filter((ng) => dataStore.filteredGroupsIds.includes(ng.id)); + .filter((ng) => dataStore.filteredGroupsIds.includes(ng.id)) + .filter((ng) => !ng.isDeleted); diffGroups.forEach((group) => renderGroupById({ - group, + group: { + ...dataStore.groups[group.id], + roleId: dataStore.groups[group.id].roleid, + }, cardOnClick: () => groupCardOnAction(group.id), + isSuperUser: dataStore.isSuperUser, }), ); break; @@ -99,6 +110,9 @@ const handler = { case 'discordId': obj[prop] = value; break; + case 'isSuperUser': + obj[prop] = value; + break; default: throw new Error('Invalid property set'); } @@ -114,6 +128,7 @@ const dataStore = new Proxy( search: getParamValueFromURL(QUERY_PARAM_KEY.GROUP_SEARCH), discordId: null, isCreateGroupModalOpen: false, + isSuperUser: false, }, handler, ); @@ -155,10 +170,13 @@ const onCreate = () => { }; const afterAuthentication = async () => { renderNavbarProfile({ profile: dataStore.userSelf }); + dataStore.isSuperUser = await checkUserIsSuperUser(); + await Promise.all([getDiscordGroups(), getUserGroupRoles()]).then( ([groups, roleData]) => { - dataStore.filteredGroupsIds = groups.map((group) => group.id); - dataStore.groups = groups.reduce((acc, group) => { + const nonDeletedGroups = groups.filter((group) => !group.isDeleted); + dataStore.filteredGroupsIds = nonDeletedGroups.map((group) => group.id); + dataStore.groups = nonDeletedGroups.reduce((acc, group) => { let title = group.rolename .replace('group-', '') .split('-') @@ -180,6 +198,9 @@ const afterAuthentication = async () => { dataStore.search, ); dataStore.discordId = roleData.userId; + renderAllGroups({ + cardOnClick: groupCardOnAction, + }); }, ); }; @@ -254,12 +275,52 @@ function groupCardOnAction(id) { function renderAllGroups({ cardOnClick }) { const mainContainer = document.querySelector('.group-container'); mainContainer.innerHTML = ''; - dataStore.filteredGroupsIds.forEach((id) => - renderGroupById({ - group: dataStore.groups[id], - cardOnClick: () => cardOnClick(id), - }), + const nonDeletedGroups = dataStore.filteredGroupsIds.filter( + (id) => !dataStore.groups[id].isDeleted, ); + if (nonDeletedGroups.length === 0) { + renderNoGroupFound(); + } else { + nonDeletedGroups.forEach((id) => { + const group = dataStore.groups[id]; + if (!group.isDeleted) { + renderGroupById({ + group: group, + cardOnClick: () => cardOnClick(id), + onDelete: isDev ? showDeleteModal : undefined, + isSuperUser: dataStore.isSuperUser && isDev, + }); + } + }); + } +} + +function showDeleteModal(groupId, roleId) { + if (!isDev) return; + renderDeleteConfirmationModal({ + onClose: () => { + removeDeleteConfirmationModal(); + }, + onConfirm: async () => { + try { + await deleteDiscordGroupRole(groupId, roleId); + showToaster('Group deleted successfully'); + + updateGroup(groupId, { isDeleted: true }); + + dataStore.filteredGroupsIds = dataStore.filteredGroupsIds.filter( + (id) => id !== groupId, + ); + renderAllGroups({ + cardOnClick: groupCardOnAction, + }); + } catch (error) { + showToaster(error.message || 'Failed to delete group'); + } finally { + removeDeleteConfirmationModal(); + } + }, + }); } onCreate(); diff --git a/groups/style.css b/groups/style.css index 054e47b4..b9b966f8 100644 --- a/groups/style.css +++ b/groups/style.css @@ -127,6 +127,12 @@ body { grid-template-columns: repeat(4, 1fr); } +@media screen and (max-width: 400px) { + .group-header { + gap: 0.8rem; + } +} + .spacer { flex-grow: 1; } @@ -345,6 +351,32 @@ body { width: 100%; } +.card__header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.delete-group { + background-color: transparent; + border: none; + padding: 0; + cursor: pointer; +} + +.delete-group__icon { + height: 1.2em; + opacity: 0.5; + transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out, + scale 0.2s ease-in-out; +} + +.delete-group:hover .delete-group__icon { + transform: rotate(10deg); + opacity: 1; + scale: 1.2; +} + .card__title { font-size: 1.1rem; font-weight: 600; @@ -592,3 +624,78 @@ body { gap: 0; } } + +/* +For Delete Confirmation Modal +*/ + +.delete-confirmation-modal { + background-color: var(--color-white); + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + max-width: 400px; + width: 90%; + padding: 24px; + position: relative; +} + +.delete-modal__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.delete-modal__title { + font-size: 24px; + font-weight: bold; + color: #333; + margin: 0; +} + +.delete-modal__close { + background: none; + border: none; + cursor: pointer; + padding: 0.8rem 1rem; + margin: -1rem -0.8rem 0.8rem 0.4rem; +} + +.delete-modal__content { + margin-bottom: 24px; +} + +.delete-modal__msg { + font-size: 16px; + color: #666; + line-height: 1.5; +} + +.delete-modal__buttons { + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.delete-modal-button { + padding: 10px 20px; + border-radius: 4px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.button--secondary:hover { + background-color: var(--color-group-btn-background); +} + +.button--danger { + background-color: #dc3545; + color: #fff; + border: none; +} + +.button--danger:hover { + background-color: #c82333; +} diff --git a/groups/utils.js b/groups/utils.js index 4dfeff83..e6437a0e 100644 --- a/groups/utils.js +++ b/groups/utils.js @@ -1,5 +1,4 @@ const BASE_URL = window.API_BASE_URL; // REPLACE WITH YOUR LOCALHOST URL FOR TESTING LOCAL BACKEND - async function getMembers() { try { const res = await fetch(`${BASE_URL}/users/`, { @@ -117,6 +116,33 @@ async function removeRoleFromMember(roleId, discordId) { } } +async function deleteDiscordGroupRole(groupId, roleId) { + 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 }), + }, + ); + + if (!res.ok) { + const errorResponse = await res.json(); + throw new Error( + `Failed to delete group role: ${JSON.stringify(errorResponse.error)}`, + ); + } + + return await res.json(); + } catch (err) { + throw err; + } +} + function removeGroupKeywordFromDiscordRoleName(groupName) { if (/^group.*/.test(groupName)) { const splitNames = groupName.split('-'); @@ -164,6 +190,7 @@ export { createDiscordGroupRole, addGroupRoleToMember, removeRoleFromMember, + deleteDiscordGroupRole, removeGroupKeywordFromDiscordRoleName, getDiscordGroupIdsFromSearch, getParamValueFromURL, diff --git a/mock-data/taskRequests/index.js b/mock-data/taskRequests/index.js index 3b5d184d..c17e92ca 100644 --- a/mock-data/taskRequests/index.js +++ b/mock-data/taskRequests/index.js @@ -1,3 +1,5 @@ +const { superUserDetails } = require('../users/mockdata.js'); + const fetchedTaskRequests = [ { id: '123CCXSDF123', @@ -260,7 +262,6 @@ const githubIssue = { performed_via_github_app: null, state_reason: 'completed', }; - const individualTaskDetail = { message: 'task returned successfully', taskData: { @@ -306,7 +307,6 @@ const userInformationTaskCreation = { }, }, }; - const userInformation = { message: 'User returned successfully!', user: { @@ -351,6 +351,7 @@ const defaultMockResponseHeaders = { }; const urlMappings = { + 'https://staging-api.realdevsquad.com/users/self': superUserDetails.user, 'https://api.realdevsquad.com/taskRequests/dM5wwD9QsiTzi7eG7Oq5': individualTaskReqDetail, 'https://api.realdevsquad.com/taskRequests/dM5wwD9QsiTzi7eG7Oq6': diff --git a/task-requests/details/index.html b/task-requests/details/index.html index 5dc02fbb..7543e635 100644 --- a/task-requests/details/index.html +++ b/task-requests/details/index.html @@ -24,13 +24,13 @@ crossorigin="anonymous" referrerpolicy="no-referrer" > - +
@@ -85,11 +85,6 @@

Requestors

-
- -
-