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')}
-
-
- Message
- {
- setMessage(e.target.value);
- }}
- />
-
- {`${messageMaxChars - message.length} `}
- {t('characters remaining')}
-
-
-
-
-
+ {
+ setSelectedItems({ escalation_policies: [], users: [] });
+ setMessage('');
+ toggleDisplayAddResponderModal();
+ }}
+ >
+
+
+ {t('Add Responders')}
+
+
+
+ {t('Users')}
+
+
+
+ {t('Escalation Policies')}
+
+
+
+
+ {t('Message')}
+
+
+
+
-
-
+
+
+
);
};
-const mapStateToProps = (state) => ({
- incidentActions: state.incidentActions,
- incidentTable: state.incidentTable,
- escalationPolicies: state.escalationPolicies.escalationPolicies,
- users: state.users.users,
- currentUser: state.users.currentUser,
-});
-
-const mapDispatchToProps = (dispatch) => ({
- toggleDisplayAddResponderModal: () => dispatch(toggleDisplayAddResponderModalConnected()),
- addResponder: (incidents, requesterId, responderRequestTargets, message) => {
- dispatch(addResponderConnected(incidents, requesterId, responderRequestTargets, message));
- },
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(AddResponderModalComponent);
+export default AddResponderModalComponent;
diff --git a/src/components/AddResponderModal/AddResponderModalComponent.scss b/src/components/AddResponderModal/AddResponderModalComponent.scss
deleted file mode 100644
index 2ec82f6a..00000000
--- a/src/components/AddResponderModal/AddResponderModalComponent.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.add-responder-message-remaining-chars {
- font-size: 11px;
- font-style: italic;
- color: #a7a9ac;
- margin-top: 10px;
-}
diff --git a/src/components/Common/EscalationPolicySelect.jsx b/src/components/Common/EscalationPolicySelect.jsx
new file mode 100644
index 00000000..39a3c869
--- /dev/null
+++ b/src/components/Common/EscalationPolicySelect.jsx
@@ -0,0 +1,118 @@
+import React, {
+ useState, useEffect, useCallback,
+} from 'react';
+
+import {
+ useDebouncedCallback,
+} from 'use-debounce';
+
+import {
+ Select,
+} from 'chakra-react-select';
+
+import {
+ useTranslation,
+} from 'react-i18next';
+
+import {
+ throttledPdAxiosRequest,
+} from 'src/util/pd-api-wrapper';
+
+const EscalationPolicySelect = ({
+ id = 'escalation-policy-select',
+ size = 'sm',
+ value,
+ onChange,
+ isMulti = false,
+}) => {
+ const {
+ t,
+ } = useTranslation();
+
+ const [selectOptions, setSelectOptions] = useState([]);
+ const [currentInputValue, setCurrentInputValue] = useState('');
+ const [more, setMore] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const [storedSelectEps, setStoredSelectEps] = useState([]);
+
+ // get the names for the selected epIds
+ useEffect(() => {
+ const epIds = isMulti ? value : [value];
+ const promises = epIds.map(async (epId) => {
+ const r = await throttledPdAxiosRequest('GET', `escalation_policies/${epId}`);
+ return { label: r.data.escalation_policy.name, value: r.data.escalation_policy.id };
+ });
+ Promise.all(promises).then((r) => {
+ setStoredSelectEps(r);
+ });
+ }, [value]);
+
+ const requestOptionsPage = useCallback(async (inputValue, offset) => {
+ const r = await throttledPdAxiosRequest('GET', 'escalation_policies', {
+ query: inputValue,
+ offset,
+ });
+ setMore(r.data.more);
+ const r2 = r.data.escalation_policies.map((ep) => ({ label: ep.name, value: ep.id }));
+ return r2;
+ }, []);
+
+ const loadOptions = useCallback(
+ async (inputValue) => {
+ setIsLoading(true);
+ const r = await requestOptionsPage(inputValue, 0);
+ setSelectOptions(r);
+ setIsLoading(false);
+ },
+ [currentInputValue, requestOptionsPage],
+ );
+
+ const debouncedLoadOptions = useDebouncedCallback(loadOptions, 200);
+
+ const loadMoreOptions = useCallback(async () => {
+ if (!more) {
+ return;
+ }
+ setIsLoading(true);
+ const r = await requestOptionsPage(currentInputValue, selectOptions.length);
+ setSelectOptions((prev) => [...prev, ...r]);
+ setIsLoading(false);
+ }, [currentInputValue, requestOptionsPage, more, selectOptions]);
+
+ return (
+