Skip to content
4 changes: 3 additions & 1 deletion __test__/cypress/integration/phone-inventory.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ describe("Phone number management screen in the Admin interface", () => {
// Waits until job run completes
cy.waitUntil(
() =>
cy.get(`tr:contains(${testAreaCode}) td:nth-child(4)`).contains("1"),
cy
.get(`tr:contains(${testAreaCode}) td:nth-child(4) div`)
.contains("1"),
{ timeout: 1000 }
);
});
Expand Down
92 changes: 48 additions & 44 deletions src/components/AssignmentTexter/Controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import { dataTest } from "../../lib/attributes";
import ContactToolbar from "./ContactToolbar";
import { getCookie, setCookie } from "../../lib/cookie";
import { deepCopy } from "../utils";

export class AssignmentTexterContactControls extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -124,7 +125,10 @@ export class AssignmentTexterContactControls extends React.Component {
let currentInteractionStep = null;

if (availableSteps.length > 0) {
currentInteractionStep = availableSteps[availableSteps.length - 1];
const currentInteractionStep = deepCopy(
availableSteps[availableSteps.length - 1]
);

currentInteractionStep.question.filteredAnswerOptions =
currentInteractionStep.question.answerOptions;
}
Expand Down Expand Up @@ -508,8 +512,8 @@ export class AssignmentTexterContactControls extends React.Component {

const otherResponsesLink =
currentInteractionStep &&
currentInteractionStep.question.filteredAnswerOptions.length > 6 &&
filteredCannedResponses.length ? (
currentInteractionStep.question.filteredAnswerOptions.length > 6 &&
filteredCannedResponses.length ? (
<div className={css(flexStyles.popoverLink)} key={"otherresponses"}>
<a
href="#otherresponses"
Expand All @@ -522,8 +526,8 @@ export class AssignmentTexterContactControls extends React.Component {

const searchBar = currentInteractionStep &&
currentInteractionStep.question.answerOptions.length +
campaign.cannedResponses.length >
5 && (
campaign.cannedResponses.length >
5 && (
<SearchBar
onRequestSearch={this.handleSearchChange}
onChange={this.handleSearchChange}
Expand Down Expand Up @@ -661,12 +665,12 @@ export class AssignmentTexterContactControls extends React.Component {
margin: "9px",
color:
this.state.optOutMessageText ===
this.props.campaign.organization.optOutMessage
this.props.campaign.organization.optOutMessage
? "white"
: "#494949",
backgroundColor:
this.state.optOutMessageText ===
this.props.campaign.organization.optOutMessage
this.props.campaign.organization.optOutMessage
? "#727272"
: "white"
}}
Expand Down Expand Up @@ -879,9 +883,9 @@ export class AssignmentTexterContactControls extends React.Component {
shortCannedResponses = shortCannedResponses.filter(script => {
var textLength = global.HIDE_BRANCHED_SCRIPTS
? this.getShortButtonText(
script.title,
cannedResponseScript ? 40 : 13
).length
script.title,
cannedResponseScript ? 40 : 13
).length
: script.title.length;

if (joinedLength + 1 + textLength < 80) {
Expand Down Expand Up @@ -965,7 +969,7 @@ export class AssignmentTexterContactControls extends React.Component {
<div className={css(flexStyles.subButtonsExitButtons)}>
<Button
onClick={
!disabled ? this.handleOpenAnswerResponsePopover : noAction => { }
!disabled ? this.handleOpenAnswerResponsePopover : noAction => {}
}
style={{
backgroundColor: this.props.muiTheme.palette.background.default,
Expand Down Expand Up @@ -1197,40 +1201,40 @@ export class AssignmentTexterContactControls extends React.Component {
const content = firstMessage
? this.renderFirstMessage(enabledSideboxes)
: [
this.renderToolbar(enabledSideboxes),
<div
key="superSectionMessagePage"
className={css(flexStyles.superSectionMessagePage)}
>
{this.state.contactListOpen &&
this.renderAssignmentContactsList(
this.props.assignment.contacts,
this.props.contact,
this.props.updateCurrentContactById
)}
<div className={css(flexStyles.superSectionMessageListAndControls)}>
<ContactToolbar
campaignContact={this.props.contact}
campaign={this.props.campaign}
navigationToolbarChildren={this.props.navigationToolbarChildren}
toggleContactList={this.toggleContactList}
/>
{this.renderMessageBox(
<MessageList
contact={this.props.contact}
currentUser={this.props.currentUser}
messages={this.props.contact.messages}
organizationId={this.props.organizationId}
review={this.props.review}
styles={messageListStyles}
hideMedia={this.state.hideMedia}
/>,
enabledSideboxes
)}
{this.renderMessageControls(enabledSideboxes)}
this.renderToolbar(enabledSideboxes),
<div
key="superSectionMessagePage"
className={css(flexStyles.superSectionMessagePage)}
>
{this.state.contactListOpen &&
this.renderAssignmentContactsList(
this.props.assignment.contacts,
this.props.contact,
this.props.updateCurrentContactById
)}
<div className={css(flexStyles.superSectionMessageListAndControls)}>
<ContactToolbar
campaignContact={this.props.contact}
campaign={this.props.campaign}
navigationToolbarChildren={this.props.navigationToolbarChildren}
toggleContactList={this.toggleContactList}
/>
{this.renderMessageBox(
<MessageList
contact={this.props.contact}
currentUser={this.props.currentUser}
messages={this.props.contact.messages}
organizationId={this.props.organizationId}
review={this.props.review}
styles={messageListStyles}
hideMedia={this.state.hideMedia}
/>,
enabledSideboxes
)}
{this.renderMessageControls(enabledSideboxes)}
</div>
</div>
</div>
];
];
return (
<div
className={css(flexStyles.topContainer)}
Expand Down
13 changes: 13 additions & 0 deletions src/components/utils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,16 @@ export function getButtonProps(props) {
];
return pick(props, validProps);
}

// Create a deep copy of an object so nested properties are also mutable
export function deepCopy(obj) {
if (Array.isArray(obj)) {
return obj.map(item => deepCopy(item));
} else if (typeof obj === "object" && obj !== null) {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, deepCopy(value)])
);
} else {
return obj;
}
}
1 change: 0 additions & 1 deletion src/containers/AdminIncomingMessageList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export class AdminIncomingMessageList extends Component {
constructor(props) {
super(props);

const query = props.location.query;
const filters = getConversationFiltersFromQuery(
props.location.query,
props.organization.organization.tags
Expand Down
2 changes: 1 addition & 1 deletion src/containers/AdminPhoneNumberInventory.js
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ const mutations = {
variables: {
organizationId: ownProps.params.organizationId,
areaCode,
limit
limit: parseInt(limit)
},
refetchQueries: () => ["getOrganizationData"]
}),
Expand Down
27 changes: 18 additions & 9 deletions src/containers/PaginatedUsersRetriever.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,25 @@ export class PaginatedUsersRetriever extends Component {
let offset = 0;
let total = undefined;
let users = [];
let results;
do {
const results = await fetchPeople(
offset,
pageSize,
organizationId,
campaignsFilter,
sortBy || "FIRST_NAME",
roleFilter
);
const { pageInfo, users: newUsers } = results.data.people;
try {
results = await fetchPeople(
offset,
pageSize,
organizationId,
campaignsFilter,
sortBy || "FIRST_NAME",
roleFilter
);
} catch (error) {
console.error("Error fetching people:", error);
}

const { pageInfo, users: newUsers } = results?.data?.people || {
pageInfo: {},
users: []
};
users = users.concat(newUsers);
offset += pageSize;
total = pageInfo.total;
Expand Down
7 changes: 4 additions & 3 deletions src/containers/TexterTodoList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ class TexterTodoList extends React.Component {
}

renderTodoList(assignments) {
return assignments
const sortedAssignments = [...assignments];
return sortedAssignments
.sort((x, y) => {
// Sort with feedback at the top, and then based on Text assignment size
const xHasFeedback =
Expand Down Expand Up @@ -266,7 +267,7 @@ const queries = {
query: dataQuery,
options: ownProps => ({
variables: {
userId: ownProps.params.userId || null,
userId: parseInt(ownProps.params.userId) || null,
organizationId: ownProps.params.organizationId,
todosOrg:
ownProps.location.query["org"] == "all" ||
Expand Down Expand Up @@ -301,7 +302,7 @@ const queries = {
);
return {
variables: {
userId: ownProps.params.userId || null,
userId: parseInt(ownProps.params.userId) || null,
organizationId: ownProps.params.organizationId
},
fetchPolicy: "network-only",
Expand Down
46 changes: 25 additions & 21 deletions src/containers/UserEdit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,23 +62,29 @@ const styles = StyleSheet.create({
}
});

const fetchUser = async (organizationId, userId) =>
apolloClient.query({
query: gql`
query getEditedUser($organizationId: String!, $userId: Int!) {
user(organizationId: $organizationId, userId: $userId) {
id
firstName
email
lastName
alias
cell
extra
const fetchUser = async (organizationId, userId) => {
try {
const response = await apolloClient.query({
query: gql`
query getEditedUser($organizationId: String!, $userId: Int!) {
user(organizationId: $organizationId, userId: $userId) {
id
firstName
email
lastName
alias
cell
extra
}
}
}
`,
variables: { organizationId, userId }
});
`,
variables: { organizationId, userId }
});
return response;
} catch (error) {
console.error("Error fetching user:", error);
}
};

const fetchOrg = async organizationId =>
apolloClient.query({
Expand Down Expand Up @@ -130,7 +136,7 @@ export class UserEditBase extends React.Component {
if (!this.props.authType && this.props.userId) {
const response = await fetchUser(
this.props.organizationId,
this.props.userId
parseInt(this.props.userId)
);
this.setState({
editedUser: response.data
Expand Down Expand Up @@ -483,7 +489,7 @@ const mutations = {
editUser: ownProps => userData => ({
mutation: editUserMutation,
variables: {
userId: ownProps.userId,
userId: parseInt(ownProps.userId),
organizationId: ownProps.organizationId,
userData
}
Expand All @@ -507,9 +513,7 @@ const mutations = {
};

const UserEdit = withMuiTheme(
withRouter(
loadData({ queries, mutations})(UserEditBase)
)
withRouter(loadData({ queries, mutations })(UserEditBase))
);

export default UserEdit;
23 changes: 22 additions & 1 deletion src/network/apollo-client-singleton.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,32 @@ const cache = new InMemoryCache({
}
return null;
},
// FUTURE: Apollo Client 3.0 allows this much more easily:
typePolicies: {
ContactTag: {
// key is just the tag id and the value is contact-specific
keyFields: false
},
// Define custom merge functions for multiple fields
// https://go.apollo.dev/c/merging-non-normalized-objects
// https://www.apollographql.com/docs/react/caching/cache-field-behavior/#merging-arrays
Query: {
fields: {
organization: {
merge: true
}
}
},
Campaign: {
fields: {
ingestMethod: {
merge: true
},
pendingJobs: {
merge(existing = [], incoming) {
return incoming;
}
}
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this resolves the apollo inmemorycache errors similar to:
Screenshot 2024-03-29 at 3 04 21 AM

}
}
});
Expand Down