Skip to content
Open
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
1 change: 1 addition & 0 deletions libs/gql-schema/organization-membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const schema = `
organization: Organization!
role: UserRole!
requestAutoApprove: RequestAutoApprove!
assignmentRequestNotifications: Boolean!
}

type OrganizationMembershipEdge {
Expand Down
2 changes: 1 addition & 1 deletion libs/gql-schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ const rootSchema = `
createOrganization(name: String!, userId: String!, inviteId: String!): Organization
editOrganization(id: String! input: EditOrganizationInput!): Organization!
joinOrganization(organizationUuid: String!): Organization!
editOrganizationMembership(id: String!, level: RequestAutoApprove, role: String): OrganizationMembership!
editOrganizationMembership(id: String!, level: RequestAutoApprove, role: String, assignmentRequestNotifications: Boolean): OrganizationMembership!
editOrganizationSettings(id: String!, input: OrganizationSettingsInput!): OrganizationSettings!
purgeOrganizationUsers(organizationId: String!): Int!
editUser(organizationId: String!, userId: String!, userData:UserInput): User
Expand Down
14 changes: 14 additions & 0 deletions migrations/20260902090000_add_assignment_request_notifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
exports.up = function up(knex) {
return knex.schema.alterTable("user_organization", (table) => {
table
.boolean("assignment_request_notifications")
.notNullable()
.defaultTo(false);
});
};

exports.down = function down(knex) {
return knex.schema.alterTable("user_organization", (table) => {
table.dropColumn("assignment_request_notifications");
});
};
1 change: 1 addition & 0 deletions schema-dump.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3370,6 +3370,7 @@ CREATE TABLE public.user_organization (
role text NOT NULL,
updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
request_status public.texter_status DEFAULT 'approval_required'::public.texter_status NOT NULL,
assignment_request_notifications boolean DEFAULT false NOT NULL,
CONSTRAINT user_organization_role_check CHECK ((role = ANY (ARRAY['OWNER'::text, 'ADMIN'::text, 'SUPERVOLUNTEER'::text, 'TEXTER'::text, 'SUSPENDED'::text])))
);

Expand Down
49 changes: 47 additions & 2 deletions src/containers/AdminAssignmentRequest/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import isEqual from "lodash/isEqual";
import PropTypes from "prop-types";
import React, { Component } from "react";
Expand Down Expand Up @@ -84,6 +86,13 @@ class AdminAssignmentRequest extends Component {
handleResolveRequest = (approved) => (requestId) =>
this.resolveRequest(requestId, approved);

handleNotificationSubscription = ({ target: { checked } }) => {
const {
node
} = this.props.pendingAssignmentRequests.currentUser.memberships.edges[0];
this.props.mutations.editOrganizationMembership(node.id, checked);
};

resolveRequest = async (requestId, approved, autoApprove = false) => {
const { resolveAssignmentRequest } = this.props.mutations;
this.setRequestStatus(requestId, RowWorkStatus.Working);
Expand All @@ -110,6 +119,8 @@ class AdminAssignmentRequest extends Component {
render() {
const { currentUser } = this.props.pendingAssignmentRequests;
const { assignmentRequests, autoApproveReqId } = this.state;
const isAdmin = hasRole("ADMIN", currentUser.roles);
const membership = currentUser.memberships.edges[0].node;
const autoApproveRequest =
autoApproveReqId &&
assignmentRequests.find(({ id }) => id === autoApproveReqId);
Expand All @@ -129,8 +140,20 @@ class AdminAssignmentRequest extends Component {

return (
<div>
{isAdmin && (
<FormControlLabel
control={
<Switch
checked={membership.assignmentRequestNotifications}
color="primary"
onChange={this.handleNotificationSubscription}
/>
}
label="Email me when assignment requests need approval"
/>
)}
<AssignmentRequestTable
isAdmin={hasRole("ADMIN", currentUser.roles)}
isAdmin={isAdmin}
assignmentRequests={assignmentRequests}
onAutoApproveRequest={this.handleAutoApproveRequest}
onApproveRequest={this.handleResolveRequest(true)}
Expand Down Expand Up @@ -174,7 +197,15 @@ const queries = {
) {
currentUser {
id
roles(organizationId: "1")
roles(organizationId: $organizationId)
memberships(organizationId: $organizationId, first: 1) {
edges {
node {
id
assignmentRequestNotifications
}
}
}
}
assignmentRequests(organizationId: $organizationId, status: $status) {
id
Expand All @@ -201,6 +232,20 @@ const queries = {
};

const mutations = {
editOrganizationMembership: () => (id, enabled) => ({
mutation: gql`
mutation editOrganizationMembership($id: String!, $enabled: Boolean!) {
editOrganizationMembership(
id: $id
assignmentRequestNotifications: $enabled
) {
id
assignmentRequestNotifications
}
}
`,
variables: { id, enabled }
}),
resolveAssignmentRequest: () => (
assignmentRequestId,
approved,
Expand Down
3 changes: 2 additions & 1 deletion src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ type RootMutation {
createOrganization(name: String!, userId: String!, inviteId: String!): Organization
editOrganization(id: String! input: EditOrganizationInput!): Organization!
joinOrganization(organizationUuid: String!): Organization!
editOrganizationMembership(id: String!, level: RequestAutoApprove, role: String): OrganizationMembership!
editOrganizationMembership(id: String!, level: RequestAutoApprove, role: String, assignmentRequestNotifications: Boolean): OrganizationMembership!
editOrganizationSettings(id: String!, input: OrganizationSettingsInput!): OrganizationSettings!
purgeOrganizationUsers(organizationId: String!): Int!
editUser(organizationId: String!, userId: String!, userData:UserInput): User
Expand Down Expand Up @@ -552,6 +552,7 @@ type OrganizationMembership {
organization: Organization!
role: UserRole!
requestAutoApprove: RequestAutoApprove!
assignmentRequestNotifications: Boolean!
}

type OrganizationMembershipEdge {
Expand Down
2 changes: 1 addition & 1 deletion src/server/api/organization-membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface IOrganizationMembership {

export const resolvers = {
OrganizationMembership: {
...sqlResolvers(["id"]),
...sqlResolvers(["id", "assignmentRequestNotifications"]),
user: async (membership: IOrganizationMembership) =>
membership.user
? membership.user
Expand Down
35 changes: 34 additions & 1 deletion src/server/api/root-mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,14 +353,22 @@ const rootMutations = {

editOrganizationMembership: async (
_root,
{ id, level, role },
{ id, level, role, assignmentRequestNotifications },
{ user: authUser }
) => {
const membership = await r
.knex("user_organization")
.where({ id: parseInt(id, 10) })
.first();
if (!membership) throw new Error("No such org membership");
if (
_.isBoolean(assignmentRequestNotifications) &&
membership.user_id !== authUser.id
) {
throw new ForbiddenError(
"You can only edit your own notification settings."
);
}

let roleRequired = UserRoleType.ADMIN;
if (
Expand Down Expand Up @@ -396,6 +404,11 @@ const rootMutations = {
.returning("*");

if (level) updateQuery.update({ request_status: level.toLowerCase() });
if (_.isBoolean(assignmentRequestNotifications)) {
updateQuery.update({
assignment_request_notifications: assignmentRequestNotifications
});
}
if (role) {
// update both tables if role change
userUpdateQuery.update(
Expand Down Expand Up @@ -2164,6 +2177,26 @@ const rootMutations = {
throw new Error(`Could not submit external requst: ${message}`);
}
});

const subscribers = await trx("user_organization")
.join("user", "user_organization.user_id", "user.id")
.where({
organization_id: organizationId,
assignment_request_notifications: true
})
.whereIn("role", ["ADMIN", "OWNER"])
.pluck("email");
await Promise.all(
subscribers.map((to) =>
sendEmail({
to,
subject: "Assignment request needs approval",
html: `A request for ${count} texts needs approval. <a href="${config.BASE_URL}/admin/${organizationId}/assignment-requests">Review request</a>.`
})
)
).catch((err) =>
logger.error("Error sending assignment request email: ", err)
);
}

if (config.AUTO_HANDLE_REQUESTS) {
Expand Down
Loading