diff --git a/.github/workflows/cd-workflow.yml b/.github/workflows/cd-workflow.yml index 8b8505a2..8a4f995f 100644 --- a/.github/workflows/cd-workflow.yml +++ b/.github/workflows/cd-workflow.yml @@ -49,7 +49,7 @@ jobs: - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 4e751977..0d4a0136 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,4 +17,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@v4 - name: 'Dependency Review' - uses: actions/dependency-review-action@v3 + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/lint-workflow.yml b/.github/workflows/lint-workflow.yml index ecb812a9..82bf5ce8 100644 --- a/.github/workflows/lint-workflow.yml +++ b/.github/workflows/lint-workflow.yml @@ -15,7 +15,7 @@ jobs: - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/.github/workflows/test-workflow.yml b/.github/workflows/test-workflow.yml index f523102d..1cac379c 100644 --- a/.github/workflows/test-workflow.yml +++ b/.github/workflows/test-workflow.yml @@ -25,7 +25,7 @@ jobs: - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -48,7 +48,7 @@ jobs: - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/cypress/e2e/Incidents/incidents.spec.js b/cypress/e2e/Incidents/incidents.spec.js index 14507255..18aa181a 100644 --- a/cypress/e2e/Incidents/incidents.spec.js +++ b/cypress/e2e/Incidents/incidents.spec.js @@ -156,17 +156,17 @@ describe('Manage Open Incidents', { failFast: { enabled: true } }, () => { cy.get('.query-urgency-low-button').check({ force: true }); let assignment = 'User A1'; selectIncident(0); - reassign(assignment); + reassign(assignment, 'user'); checkActionAlertsModalContent(`have been reassigned to ${assignment}`); assignment = 'Team A'; selectIncident(1); - reassign(assignment); + reassign(assignment, 'ep'); checkActionAlertsModalContent(`have been reassigned to ${assignment}`); }); it('Add User and Team responders to singular incident', () => { - let responders = ['User A1']; + let responders = [{ assignment: 'User A1', type: 'user' }]; const message = 'Need help with this incident'; let incidentIdx = 0; selectIncident(incidentIdx); @@ -178,7 +178,7 @@ describe('Manage Open Incidents', { failFast: { enabled: true } }, () => { checkIncidentCellContent(incidentId, 'Latest Log Entry Type', 'responder_request'); }); - responders = ['Team A']; + responders = [{ assignment: 'Team A', type: 'ep' }]; incidentIdx = 1; selectIncident(incidentIdx); addResponders(responders, message); @@ -191,7 +191,7 @@ describe('Manage Open Incidents', { failFast: { enabled: true } }, () => { }); it('Add multiple responders (Team A + Team B) to singular incident', () => { - const responders = ['Team A', 'Team B']; + const responders = ['Team A', 'Team B'].map((assignment) => ({ assignment, type: 'ep' })); const message = "Need everyone's help with this incident"; const incidentIdx = 0; selectIncident(incidentIdx); @@ -316,6 +316,10 @@ describe('Manage Alerts', { failFast: { enabled: true } }, () => { cy.get( `[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${incidentIdx}"]`, ).within(() => { + cy.get('[aria-haspopup="dialog"]').realHover(); + // wait for async alert fetch to complete + cy.get('[data-popper-placement="bottom"]').should('be.visible'); + cy.get('[data-popper-placement="bottom"]').should('contain', 'Created At'); cy.get('[aria-haspopup="dialog"]').click(); }); @@ -348,6 +352,9 @@ describe('Manage Alerts', { failFast: { enabled: true } }, () => { cy.get( `[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${incidentIdx}"]`, ).within(() => { + cy.get('[aria-haspopup="dialog"]').should('be.visible').should('have.text', '2').realHover(); + cy.get('[data-popper-placement="bottom"]').should('be.visible'); + cy.get('[data-popper-placement="bottom"]').should('contain', 'Created At'); cy.get('[aria-haspopup="dialog"]').should('be.visible').should('have.text', '2').click(); }); @@ -384,6 +391,9 @@ describe('Manage Alerts', { failFast: { enabled: true } }, () => { cy.get( `[data-incident-header="Num Alerts"][data-incident-row-cell-idx="${sourceIncidentIdx}"]`, ).within(() => { + cy.get('[aria-haspopup="dialog"]').should('be.visible').should('have.text', '1').realHover(); + cy.get('[data-popper-placement="bottom"]').should('be.visible'); + cy.get('[data-popper-placement="bottom"]').should('contain', 'Created At'); cy.get('[aria-haspopup="dialog"]').should('be.visible').should('have.text', '1').click(); }); diff --git a/cypress/e2e/Query/query.spec.js b/cypress/e2e/Query/query.spec.js index 40da603b..5a24c729 100644 --- a/cypress/e2e/Query/query.spec.js +++ b/cypress/e2e/Query/query.spec.js @@ -115,7 +115,8 @@ describe('Query Incidents', { failFast: { enabled: true } }, () => { const teams = ['Team A', 'Team B']; teams.forEach((team) => { it(`Query for incidents on ${team} only`, () => { - cy.get('#query-team-select').click().type(`${team}{enter}`); + cy.get('#query-team-select').click().type(`${team}`); + cy.get('div[role="button"]').contains(team).should('exist').click(); waitForIncidentTable(); checkIncidentCellContentAllRows('Teams', team); cy.get('#query-team-select').click().type('{del}'); @@ -125,7 +126,8 @@ describe('Query Incidents', { failFast: { enabled: true } }, () => { const escalationPolicies = ['Team A (EP)', 'Team B (EP)']; escalationPolicies.forEach((escalationPolicy) => { it(`Query for incidents on ${escalationPolicy} only`, () => { - cy.get('#query-escalation-policy-select').click().type(`${escalationPolicy}{enter}`); + cy.get('#query-escalation-policy-select').click().type(`${escalationPolicy}`); + cy.get('div[role="button"]').contains(escalationPolicy).should('exist').click(); waitForIncidentTable(); checkIncidentCellContentAllRows('Escalation Policy', escalationPolicy); cy.get('#query-escalation-policy-select').click().type('{del}'); @@ -135,7 +137,8 @@ describe('Query Incidents', { failFast: { enabled: true } }, () => { const services = ['Service A1', 'Service B2']; services.forEach((service) => { it(`Query for incidents on ${service} only`, () => { - cy.get('#query-service-select').click().type(`${service}{enter}`); + cy.get('#query-service-select').click().type(`${service}`); + cy.get('div[role="button"]').contains(service).should('exist').click(); waitForIncidentTable(); checkIncidentCellContentAllRows('Service', service); cy.get('#query-service-select').click().type('{del}'); @@ -143,8 +146,10 @@ describe('Query Incidents', { failFast: { enabled: true } }, () => { }); it('Query for incidents on Team A and Service A1 only', () => { - cy.get('#query-team-select').click().type('Team A{enter}'); - cy.get('#query-service-select').click().type('Service A1{enter}'); + cy.get('#query-team-select').click().type('Team A'); + cy.get('div[role="button"]').contains('Team A').should('exist').click(); + cy.get('#query-service-select').click().type('Service A1'); + cy.get('div[role="button"]').contains('Service A1').should('exist').click(); waitForIncidentTable(); checkIncidentCellContentAllRows('Service', 'Service A1'); @@ -155,8 +160,10 @@ describe('Query Incidents', { failFast: { enabled: true } }, () => { }); it('Query for incidents on Team A (EP) and Service A1 only', () => { - cy.get('#query-escalation-policy-select').click().type('Team A (EP){enter}'); - cy.get('#query-service-select').click().type('Service A1{enter}'); + cy.get('#query-escalation-policy-select').click().type('Team A (EP)'); + cy.get('div[role="button"]').contains('Team A (EP)').should('exist').click(); + cy.get('#query-service-select').click().type('Service A1'); + cy.get('div[role="button"]').contains('Service A1').should('exist').click(); waitForIncidentTable(); checkIncidentCellContentAllRows('Service', 'Service A1'); @@ -189,11 +196,10 @@ describe('Query Incidents', { failFast: { enabled: true } }, () => { }); it('Query for incidents assigned to User A1, A2, or A3', () => { - cy.get('#query-user-select') - .click() - .type('User A1{enter}') - .type('User A2{enter}') - .type('User A3{enter}'); + ['User A1', 'User A2', 'User A3'].forEach((user) => { + cy.get('#query-user-select').click().type(user); + cy.get('div[role="button"]').contains(user).should('exist').click(); + }); waitForIncidentTable(); checkIncidentCellContentAllRows('Assignees', 'UA'); diff --git a/cypress/e2e/app.spec.js b/cypress/e2e/app.spec.js index f0f5ca8f..92063b85 100644 --- a/cypress/e2e/app.spec.js +++ b/cypress/e2e/app.spec.js @@ -87,7 +87,7 @@ describe('PagerDuty Live', { failFast: { enabled: true } }, () => { 'GET', [ 'https://api.pagerduty.com/incidents', - '?limit=100&total=true&offset=0', + '*limit=100*offset=0*', `&since=${since}&until=${until}*`, ].join(''), ).as('getUrl'); diff --git a/cypress/support/util/common.js b/cypress/support/util/common.js index f8ca5d7d..61ab0e23 100644 --- a/cypress/support/util/common.js +++ b/cypress/support/util/common.js @@ -11,7 +11,7 @@ export const pd = api({ token: Cypress.env('PD_USER_TOKEN') }); */ export const acceptDisclaimer = () => { cy.visit('/'); - cy.get('.modal-title', { timeout: 30000 }).contains('Disclaimer & License'); + cy.get('.modal-title').contains('Disclaimer & License').should('be.visible'); cy.get('#disclaimer-agree-checkbox').click({ force: true }); cy.get('#disclaimer-accept-button').click({ force: true }); }; @@ -38,7 +38,7 @@ export const waitForAlerts = () => { export const selectIncident = (incidentIdx = 0, shiftKey = false) => { const selector = `[data-incident-row-idx="${incidentIdx}"]`; cy.get(selector).invoke('attr', 'data-incident-id').as(`selectedIncidentId_${incidentIdx}`); - cy.get(selector).click({ shiftKey }); + cy.get(selector).click({ shiftKey, force: true }); }; export const selectAlert = (alertIdx = 0) => { @@ -142,25 +142,34 @@ export const escalate = (escalationLevel) => { cy.get(`.escalation-level-${escalationLevel}-button`).click(); }; -export const reassign = (assignment) => { +export const reassign = (assignment, type = 'ep') => { + // fail if type is not 'ep' or 'user' + if (type !== 'ep' && type !== 'user') { + throw new Error('Invalid type'); + } + const tabId = type === 'ep' ? 'reassign-ep-tab' : 'reassign-user-tab'; + const tabSelector = `[data-tab-id="${tabId}"]`; cy.get('#incident-action-reassign-button').click(); + cy.get(tabSelector).click(); cy.get('#reassign-select').click(); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(200); - cy.contains('.react-select__option', assignment).click({ force: true }); - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(200); - cy.get('#reassign-button').click({ force: true }); + cy.get('#reassign-select input').first().type(assignment); + cy.get('div[role="button"]').contains(assignment).should('exist').click(); + cy.get('#reassign-button').click(); }; export const addResponders = (responders = [], message = null) => { cy.get('#incident-action-add-responders-button').click(); responders.forEach((responder) => { - cy.get('#add-responders-select').click(); - cy.contains('.react-select__option', responder).click({ force: true }); + if (responder.type !== 'user' && responder.type !== 'ep') { + throw new Error(`Invalid responder type: ${JSON.stringify(responder)}`); + } + const selectId = responder.type === 'user' ? 'add-responders-select-users' : 'add-responders-select-eps'; + cy.get(`#${selectId}`).click(); + cy.get(`#${selectId} input`).first().type(responder.assignment); + cy.get('div[role="button"]').contains(responder.assignment).should('exist').click(); }); if (message) cy.get('#add-responders-textarea').type(message); - cy.get('#add-responders-button').click({ force: true }); + cy.get('#add-responders-button').click(); }; export const snooze = (duration) => { diff --git a/package.json b/package.json index a7aa4000..9071ac31 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pd-live-react", "homepage": "https://pagerduty.github.io/pd-live-react", - "version": "0.11.1-beta.0", + "version": "0.12.0-beta.0", "private": true, "dependencies": { "@chakra-ui/icons": "^2.1.1", @@ -11,13 +11,13 @@ "@emotion/styled": "^11.11.0", "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-brands-svg-icons": "^6.4.2", - "@fortawesome/free-regular-svg-icons": "^6.4.2", + "@fortawesome/free-regular-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", "@pagerduty/pdjs": "^2.2.3", "@types/jest": "^29.5.4", "@types/node": "^20.10.8", - "@types/react": "^18.2.21", + "@types/react": "^18.2.55", "@types/react-dom": "^18.2.17", "axios": "^1.6.2", "bootstrap": "^4.6.2", @@ -45,7 +45,6 @@ "react-dom": "^18", "react-i18next": "^13.2.0", "react-icons": "^4.9.0", - "react-intersection-observer": "^9.5.3", "react-minimal-pie-chart": "^8.4.0", "react-redux": "^8.1.2", "react-select": "^5.7.7", @@ -102,7 +101,7 @@ "@babel/preset-react": "^7.22.5", "@cypress/react": "^8.0.0", "@faker-js/faker": "^8.0.2", - "@testing-library/dom": "^9.3.3", + "@testing-library/dom": "^9.3.4", "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^14.1.2", "@testing-library/react-hooks": "^8.0.1", @@ -122,14 +121,14 @@ "eslint-plugin-cypress": "^2.15.1", "eslint-plugin-import": "^2.29.0", "eslint-plugin-jsx": "^0.1.0", - "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "eslint-plugin-styled-components-a11y": "^2.1.31", "genversion": "^3.1.1", - "gh-pages": "^6.0.0", + "gh-pages": "^6.1.1", "i18next-parser": "^8.9.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.6.3", @@ -145,7 +144,7 @@ "redux-saga-test-plan": "^4.0.6", "sass": "^1.66.1", "string.prototype.replaceall": "^1.0.6", - "vite": "^4.4.12", + "vite": "^4.5.2", "vite-plugin-environment": "^1.1.3", "vite-plugin-eslint": "^1.8.1", "vite-plugin-svgr": "^3.2.0", diff --git a/src/App.jsx b/src/App.jsx index 92abc5d4..f6f911e4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -35,6 +35,9 @@ import AddResponderModalComponent from 'src/components/AddResponderModal/AddResp import MergeModalComponent from 'src/components/MergeModal/MergeModalComponent'; import IncidentAlertsModal from 'src/components/IncidentTable/subcomponents/IncidentAlertsModal'; +import { + getExtensionsAsync as getExtensionsAsyncConnected, +} from 'src/redux/extensions/actions'; import { getIncidentsAsync as getIncidentsAsyncConnected, // refreshIncidentsAsync as refreshIncidentsAsyncConnected, @@ -43,22 +46,12 @@ import { getLogEntriesAsync as getLogEntriesAsyncConnected, cleanRecentLogEntriesAsync as cleanRecentLogEntriesAsyncConnected, } from 'src/redux/log_entries/actions'; -import { - getServicesAsync as getServicesAsyncConnected, -} from 'src/redux/services/actions'; -import { - getTeamsAsync as getTeamsAsyncConnected, -} from 'src/redux/teams/actions'; import { getPrioritiesAsync as getPrioritiesAsyncConnected, } from 'src/redux/priorities/actions'; import { userAuthorize as userAuthorizeConnected, - getUsersAsync as getUsersAsyncConnected, } from 'src/redux/users/actions'; -import { - getEscalationPoliciesAsync as getEscalationPoliciesAsyncConnected, -} from 'src/redux/escalation_policies/actions'; import { getResponsePlaysAsync as getResponsePlaysAsyncConnected, } from 'src/redux/response_plays/actions'; @@ -98,11 +91,8 @@ const App = () => { const checkAbilities = () => dispatch(checkAbilitiesConnected()); const checkConnectionStatus = () => dispatch(checkConnectionStatusConnected()); const updateQueueStats = (queueStats) => dispatch(updateQueueStatsConnected(queueStats)); - const getServicesAsync = (teamIds) => dispatch(getServicesAsyncConnected(teamIds)); - const getTeamsAsync = () => dispatch(getTeamsAsyncConnected()); + const getExtensionsAsync = () => dispatch(getExtensionsAsyncConnected()); const getPrioritiesAsync = () => dispatch(getPrioritiesAsyncConnected()); - const getUsersAsync = (teamIds) => dispatch(getUsersAsyncConnected(teamIds)); - const getEscalationPoliciesAsync = () => dispatch(getEscalationPoliciesAsyncConnected()); const getResponsePlaysAsync = () => dispatch(getResponsePlaysAsyncConnected()); const getLogEntriesAsync = (since) => dispatch(getLogEntriesAsyncConnected(since)); const cleanRecentLogEntriesAsync = () => dispatch(cleanRecentLogEntriesAsyncConnected()); @@ -134,15 +124,10 @@ const App = () => { if (token && userAuthorized) { startMonitoring(); checkAbilities(); - getUsersAsync(); - getServicesAsync(); - getTeamsAsync(); - getEscalationPoliciesAsync(); - getResponsePlaysAsync(); getPrioritiesAsync(); - // NB: Get incidents, notes, and alerts are implicitly done from query now - // not anymore getIncidentsAsync(); + getExtensionsAsync(); + getResponsePlaysAsync(); checkConnectionStatus(); } }, [userAuthorized]); diff --git a/src/components/AddResponderModal/AddResponderModalComponent.jsx b/src/components/AddResponderModal/AddResponderModalComponent.jsx index 5dff07b9..538ac22c 100644 --- a/src/components/AddResponderModal/AddResponderModalComponent.jsx +++ b/src/components/AddResponderModal/AddResponderModalComponent.jsx @@ -1,15 +1,32 @@ import React, { - useState, + useCallback, useState, useRef, } from 'react'; + import { - connect, + useSelector, useDispatch, } from 'react-redux'; import { - Modal, Form, Button, -} from 'react-bootstrap'; -import Select from 'react-select'; -import makeAnimated from 'react-select/animated'; + useDebouncedCallback, +} from 'use-debounce'; + +import { + Button, + FormControl, + FormLabel, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Textarea, +} from '@chakra-ui/react'; + +import { + Select, +} from 'chakra-react-select'; import { useTranslation, @@ -20,108 +37,192 @@ import { addResponder as addResponderConnected, } from 'src/redux/incident_actions/actions'; -import './AddResponderModalComponent.scss'; +import { + throttledPdAxiosRequest, +} from 'src/util/pd-api-wrapper'; -const animatedComponents = makeAnimated(); +import { + addUserToUsersMap as addUserToUsersMapConnected, +} from 'src/redux/users/actions'; + +const AddResponderModalComponent = () => { + const messageMaxChars = 110; -const AddResponderModalComponent = ({ - incidentActions, - incidentTable, - escalationPolicies, - users, - currentUser, - toggleDisplayAddResponderModal, - addResponder, -}) => { const { t, } = useTranslation(); + const { displayAddResponderModal, - } = incidentActions; + } = useSelector((state) => state.incidentActions); const { selectedRows, - } = incidentTable; + } = useSelector((state) => state.incidentTable); const { id: currentUserId, - } = currentUser; + } = useSelector((state) => state.users.currentUser); - const messageMaxChars = 110; + const usersMap = useSelector((state) => state.users.usersMap); + const dispatch = useDispatch(); + const addUserToUsersMap = (user) => { + dispatch(addUserToUsersMapConnected(user)); + }; + const addResponder = (incidents, requesterId, responderRequestTargets, message) => { + dispatch(addResponderConnected(incidents, requesterId, responderRequestTargets, message)); + }; + const toggleDisplayAddResponderModal = () => { + dispatch(toggleDisplayAddResponderModalConnected()); + }; - const [responderRequestTargets, setResponderRequestTargets] = useState([]); + const [selectOptions, setSelectOptions] = useState({ escalation_policies: [], users: [] }); + const [currentInputValue, setCurrentInputValue] = useState({ + escalation_policies: '', + users: '', + }); + const [more, setMore] = useState({ escalation_policies: false, users: false }); + const [isLoading, setIsLoading] = useState({ escalation_policies: false, users: false }); + const [selectedItems, setSelectedItems] = useState({ escalation_policies: [], users: [] }); const [message, setMessage] = useState(''); - // Generate lists/data from store - const selectListResponderRequestTargets = escalationPolicies - .map((escalationPolicy) => ({ - label: `EP: ${escalationPolicy.name}`, - name: escalationPolicy.name, - value: escalationPolicy.id, - type: 'escalation_policy', - })) - .concat( - users.map((user) => ({ - label: `User: ${user.name}`, - name: user.name, - value: user.id, - type: 'user', - })), - ); + const epSelectRef = useRef(null); + const userSelectRef = useRef(null); + + const requestOptionsPage = useCallback(async (inputValue, offset, epsOrUsers) => { + const epOrUser = epsOrUsers === 'escalation_policies' ? 'escalation_policy' : 'user'; + const r = await throttledPdAxiosRequest('GET', epsOrUsers, { query: inputValue, offset }); + setMore((prev) => ({ ...prev, [epsOrUsers]: r.data.more })); + const r2 = r.data[epsOrUsers].map((obj) => { + // take the opportunity to add the object to the map + if (epOrUser === 'user') { + if (!usersMap[obj.id]) { + addUserToUsersMap(obj); + } + } + return { label: obj.name, name: obj.name, value: obj.id, type: epOrUser }; + }); + return r2; + }, []); + + const loadOptions = useCallback( + async (epsOrUsers, inputValue) => { + setIsLoading((prev) => ({ ...prev, [epsOrUsers]: true })); + const r = await requestOptionsPage(inputValue, 0, epsOrUsers); + setSelectOptions((prev) => ({ ...prev, [epsOrUsers]: r })); + setIsLoading((prev) => ({ ...prev, [epsOrUsers]: false })); + }, + [currentInputValue, requestOptionsPage], + ); + + const debouncedLoadOptions = useDebouncedCallback(loadOptions, 200); + + const loadMoreOptions = useCallback( + async (epsOrUsers) => { + if (!more[epsOrUsers]) { + return; + } + setIsLoading((prev) => ({ ...prev, [epsOrUsers]: true })); + const r = await requestOptionsPage( + currentInputValue[epsOrUsers], + selectOptions[epsOrUsers].length, + epsOrUsers, + ); + setSelectOptions((prev) => ({ ...prev, [epsOrUsers]: [...prev[epsOrUsers], ...r] })); + setIsLoading((prev) => ({ ...prev, [epsOrUsers]: false })); + }, + [currentInputValue, requestOptionsPage, more, selectOptions], + ); return ( -
- { - setResponderRequestTargets([]); - toggleDisplayAddResponderModal(); - }} - > - - {t('Add Responders')} - - -
- - {t('Responders')} - { + setSelectedItems((prev) => ({ ...prev, users: selectedUsers })); + }} + onInputChange={(inputValue) => { + setCurrentInputValue((prev) => ({ ...prev, users: inputValue })); + debouncedLoadOptions('users', inputValue); + }} + onMenuScrollToBottom={() => loadMoreOptions('users')} + onFocus={() => loadOptions('users', currentInputValue.users)} + options={selectOptions.users} + placeholder={`${t('Select dotdotdot')}`} + /> + + + + {t('Escalation Policies')} + +