Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stage main post-election-2020 stable #1830

Merged
merged 15 commits into from
Sep 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 2 additions & 30 deletions __test__/TexterStats.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,8 @@ describe("TexterStats (Non-dynamic campaign)", () => {
<TexterStats campaign={campaign} organizationId="1" />
);
expect(stats.text()).toEqual(
"<Link />19%<LinearProgress /><Link />99%<LinearProgress />"
"Test Tester <Link />19%<LinearProgress /><Link />  |  <Link />Someone Else (Suspended) <Link />99%<LinearProgress /><Link />  |  <Link />"
);
expect(
stats
.find("Link")
.at(0)
.children()
.text()
).toBe("Test Tester");
expect(
stats
.find("Link")
.at(1)
.children()
.text()
).toBe("Someone Else (Suspended)");
});

it("creates linear progress correctly", () => {
Expand All @@ -100,21 +86,7 @@ describe("TexterStats (Dynamic campaign)", () => {
it("contains the right text", () => {
const stats = shallow(<TexterStats campaign={campaignDynamic} />);
expect(stats.text()).toEqual(
"<Link />45 initial messages sent. <Link /><Link />541 initial messages sent. <Link />"
"Test Tester <Link />45 initial messages sent. <Link />  |  <Link />Someone Else (Suspended) <Link />541 initial messages sent. <Link />  |  <Link />"
);
expect(
stats
.find("Link")
.at(0)
.children()
.text()
).toBe("Test Tester");
expect(
stats
.find("Link")
.at(2)
.children()
.text()
).toBe("Someone Else (Suspended)");
});
});
5 changes: 5 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@
"description": "ensures that the DB is connecting over SSL which is a requirement for Heroku DBs",
"required": true,
"value": "true"
},

"TWILIO_VALIDATION": {
"description": "Validate twilio message report links as well",
"value": "1"
}
},
"addons": [
Expand Down
2 changes: 1 addition & 1 deletion docs/HOWTO_INTEGRATE_TWILIO.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ If you follow the instructions above, every organization and campaign in your in
- For security, Twilio Auth Tokens are encrypted using the `SESSION_SECRET` environment variable before being stored in the database.
- You can still set instance-wide credentials in the .env file (as described above). If you do, those credentials will be used as fallback if credentials aren't configured for an organization.
- It is not required to configure all settings for all organizations. For example, to use a single site-wide Twilio account but with separate phone number pools for some organizations, follow the instuctions above and then set the Default Message Service SID (leaving the other fields blank) in the organizations settings for the orgs you want to override.
- When using multiple Twilio accounts you will need to change the Inbound Request URL for your messaging service in the Twilio console [step 7 above]. It should look like `https://<YOUR_APP_URL>/twilio/<ORG_ID>`. The correct URL to use will be displayed on the settings page after you save the Twilio credentials.
- When using multiple Twilio accounts you will need to change the Inbound Request URL for your messaging service in the Twilio console [step 7 above]. It should look like `https://<YOUR_APP_URL>/twilio/<ORG_ID>` and `https://<YOUR_APP_URL>/twilio-message-report/<ORG_ID>`. The correct URL to use will be displayed on the settings page after you save the Twilio credentials.
2 changes: 2 additions & 0 deletions docs/REFERENCE-environment_variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
| ROLLBAR_ACCESS_TOKEN | Access token for Rollbar error tracking. |
| ROLLBAR_ENDPOINT | Endpoint URL for Rollbar error tracking. |
| SESSION_SECRET | Unique key used to encrypt sessions. _Required_. |
| SHOW_SERVER_ERROR | Best practice is to hide errors in production for security purposes which can reveal internal database/system state (even in an open-source project where the code paths are known) |
| SLACK_NOTIFY_URL | If set, then on post-install (often from deploying) a message will be posted to a slack channel's `#spoke` channel |
| SUPPRESS_SELF_INVITE | Boolean value to prevent self-invitations. Recommend setting before making sites available to public. _Default_: false. |
| SUPPRESS_DATABASE_AUTOCREATE | Suppress database auto-creation on first start. Mostly just used for test context |
Expand All @@ -103,6 +104,7 @@
| TWILIO_MULTI_ORG | Boolean value to indicate if organizations can override Twilio credentials in the organization settings. _Default_: false. |
| TWILIO_STATUS_CALLBACK_URL | URL for Twilio status callbacks. Should end with `/twilio-message-report`, e.g. `https://example.org/twilio-message-report`. Required if using Twilio. |
| TWILIO_SQS_QUEUE_URL | AWS SQS URL to handle incoming messages when app isn't connected to twilio |
| TWILIO_VALIDATION | Validate message report links as well -- you should enable this. It's only non-default for backwards compatibility reasons. |
| TWILIO_VOICE_URL | Global Twilio voice url for phone numbers provisioned through Spoke. If not set, the default Twilio voicemail will be used. |
| WAREHOUSE_DB_*X*<br>{TYPE,HOST,PORT,NAME,USER,PASSWORD,SCHEMA,USE_SSL} | Enables ability to load contacts directly from a SQL query from a separate data-warehouse db -- only is_superadmin-marked users will see the interface |
| WAREHOUSE_DB_LAMBDA_ITERATION | If the WAREHOUSE*DB* connection/feature is enabled, then on AWS Lambda, queries that take longer than 5min can expire. This will enable incrementing through queries on new lambda invocations to avoid timeouts. |
Expand Down
34 changes: 25 additions & 9 deletions src/components/IncomingMessageList/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type from "prop-types";
import FlatButton from "material-ui/FlatButton";
import ActionOpenInNew from "material-ui/svg-icons/action/open-in-new";
import loadData from "../../containers/hoc/load-data";
import { withRouter } from "react-router";
import { Link, withRouter } from "react-router";
import gql from "graphql-tag";
import { getHighestRole } from "../../lib/permissions";
import LoadingIndicator from "../../components/LoadingIndicator";
Expand All @@ -17,13 +17,7 @@ import { MESSAGE_STATUSES } from "../../components/IncomingMessageFilter";
export const prepareDataTableData = conversations =>
conversations.map(conversation => ({
campaignTitle: conversation.campaign.title,
texter:
conversation.texter.id !== null
? conversation.texter.displayName +
(getHighestRole(conversation.texter.roles) === "SUSPENDED"
? " (Suspended)"
: "")
: "unassigned",
texter: conversation.texter,
to:
conversation.contact.firstName +
" " +
Expand Down Expand Up @@ -128,7 +122,29 @@ export class IncomingMessageList extends Component {
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "pre-line"
}
},
render: (columnKey, row) => (
<span>
{row.texter.id !== null ? (
<span>
{row.texter.displayName +
(getHighestRole(row.texter.roles) === "SUSPENDED"
? " (Suspended)"
: "")}{" "}
<Link
target="_blank"
to={`/app/${this.props.organizationId}/todos/other/${row.texter.id}`}
>
<ActionOpenInNew
style={{ width: 14, height: 14, color: theme.colors.green }}
/>
</Link>
</span>
) : (
"unassigned"
)}
</span>
)
},
{
key: "to",
Expand Down
74 changes: 50 additions & 24 deletions src/components/TexterStats.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import PropTypes from "prop-types";
import React from "react";
import { Link } from "react-router";
import ActionOpenInNew from "material-ui/svg-icons/action/open-in-new";
import LinearProgress from "material-ui/LinearProgress";
import { getHighestRole } from "../lib/permissions";
import theme from "../styles/theme";

class TexterStats extends React.Component {
renderAssignment(assignment, campaign) {
Expand All @@ -28,41 +30,65 @@ class TexterStats extends React.Component {
if (getHighestRole(texter.roles) === "SUSPENDED") {
displayName += " (Suspended)";
}
const bottomLinks = [
<Link
key={id}
to={`/admin/${this.props.organizationId}/incoming?campaigns=${campaign.id}&texterId=${texter.id}`}
>
Review conversations
</Link>
];

return (
<div key={id}>
if (unmessagedCount) {
bottomLinks.push(
<Link
to={`/admin/${this.props.organizationId}/incoming?campaigns=${campaign.id}&texterId=${texter.id}`}
to={`/admin/${this.props.organizationId}/incoming?campaigns=${campaign.id}&texterId=${texter.id}&messageStatus=needsMessage`}
>
{displayName}
Unmessaged: {unmessagedCount}
</Link>
);
}
if (unrepliedCount) {
bottomLinks.push(
<span>
{unrepliedCount} contacts{" "}
<Link
to={`/admin/${this.props.organizationId}/incoming?campaigns=${campaign.id}&texterId=${texter.id}&messageStatus=needsResponse`}
>
awaiting a reply
</Link>
</span>
);
}
return (
<div key={id}>
<h3>
{displayName}{" "}
<Link
target="_blank"
to={`/app/${this.props.organizationId}/todos/other/${texter.id}`}
>
<ActionOpenInNew
style={{ width: 14, height: 14, color: theme.colors.green }}
/>
</Link>
</h3>
{percentComplete ? (
<div>
<div>{percentComplete}%</div>
<LinearProgress mode="determinate" value={percentComplete} />
</div>
) : (
<div>
{contactsCount - unmessagedCount} initial messages sent.{" "}
{unmessagedCount ? (
<Link
to={`/admin/${this.props.organizationId}/incoming?campaigns=${campaign.id}&texterId=${texter.id}&messageStatus=needsMessage`}
>
Unmessaged: {unmessagedCount}
</Link>
) : null}
</div>
<div>{contactsCount - unmessagedCount} initial messages sent. </div>
)}
{unrepliedCount ? (
<div>
{unrepliedCount} contacts{" "}
<Link
to={`/admin/${this.props.organizationId}/incoming?campaigns=${campaign.id}&texterId=${texter.id}&messageStatus=needsResponse`}
>
awaiting a reply
</Link>
</div>
) : null}
<div>
{bottomLinks.map((link, i) => (
<span key={`${texter.id}_${i}`}>
{i ? <span>&nbsp;&nbsp;|&nbsp;&nbsp;</span> : null}
{link}
</span>
))}
</div>
</div>
);
}
Expand Down
20 changes: 18 additions & 2 deletions src/containers/PeopleList.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React, { Component } from "react";
import { Link } from "react-router";
import type from "prop-types";
import FlatButton from "material-ui/FlatButton";
import loadData from "./hoc/load-data";
import gql from "graphql-tag";
import LoadingIndicator from "../components/LoadingIndicator";
import ActionOpenInNew from "material-ui/svg-icons/action/open-in-new";
import DataTables from "material-ui-datatables";
import UserEditDialog from "../components/PeopleList/UserEditDialog";
import ResetPasswordDialog from "../components/PeopleList/ResetPasswordDialog";
import RolesDropdown from "../components/PeopleList/RolesDropdown";
import { dataTest } from "../lib/attributes";

import theme from "../styles/theme";
import PeopleIcon from "material-ui/svg-icons/social/people";
import Empty from "../components/Empty";

Expand Down Expand Up @@ -43,6 +45,7 @@ export class PeopleList extends Component {
}

prepareTableColumns = () => {
const { organizationId } = this.props;
const columns = [
{
key: "texter",
Expand All @@ -51,7 +54,20 @@ export class PeopleList extends Component {
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "pre-line"
}
},
render: (columnKey, row) => (
<h3>
{row.texter}{" "}
<Link
target="_blank"
to={`/app/${organizationId}/todos/other/${row.texterId}`}
>
<ActionOpenInNew
style={{ width: 14, height: 14, color: theme.colors.green }}
/>
</Link>
</h3>
)
},
{
key: "email",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const showSidebox = ({
// Return anything Truth-y to show
// Return 'popup' to force a popup on mobile screens (instead of letting it hide behind a button)
return (
assignment.allContactsCount &&
(assignment.hasContacts || assignment.allContactsCount) &&
!finished &&
(campaign.useDynamicAssignment ||
settingsData.releaseContactsNonDynamicToo) &&
Expand All @@ -55,10 +55,13 @@ export class TexterSideboxClass extends React.Component {
const { settingsData, messageStatusFilter, assignment } = this.props;
const showReleaseConvos =
settingsData.releaseContactsReleaseConvos &&
(messageStatusFilter !== "needsMessage" || assignment.unrepliedCount);
((messageStatusFilter && messageStatusFilter !== "needsMessage") ||
assignment.unrepliedCount ||
assignment.hasUnreplied);
return (
<div style={{}}>
{assignment.unmessagedCount ? (
{assignment.unmessagedCount ||
(messageStatusFilter === "needsMessage" && assignment.hasUnmessaged) ? (
<div>
<div>
{settingsData.releaseContactsBatchTitle ? (
Expand Down Expand Up @@ -122,6 +125,7 @@ export const mutations = {
mutation releaseContacts(
$assignmentId: String!
$contactsFilter: ContactsFilter!
$needsResponseFilter: ContactsFilter!
$releaseConversations: Boolean
) {
releaseContacts(
Expand All @@ -132,15 +136,24 @@ export const mutations = {
contacts(contactsFilter: $contactsFilter) {
id
}
allContactsCount: contactsCount
unmessagedCount: contactsCount(contactsFilter: $contactsFilter)
hasUnmessaged: contactsCount(contactsFilter: $contactsFilter)
maybeUnrepliedCount: contactsCount(
contactsFilter: $needsResponseFilter
)
}
}
`,
variables: {
assignmentId: ownProps.assignment.id,
releaseConversations,
contactsFilter: {
messageStatus: ownProps.messageStatusFilter,
messageStatus: "needsMessage",
isOptedOut: false,
validTimezone: true
},
needsResponseFilter: {
messageStatus: releaseConversations ? "needsResponse" : "needsMessage",
isOptedOut: false,
validTimezone: true
}
Expand Down
6 changes: 5 additions & 1 deletion src/server/api/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ export async function assignmentRequiredOrAdminRole(
roleRequired
);
if (!hasPermission) {
throw new GraphQLError("You are not authorized to access that resource.");
const error = new GraphQLError(
"You are not authorized to access that resource."
);
error.code = "UNAUTHORIZED";
throw error;
}
return userHasAssignment || true;
}
Loading