Add ability to disconnect Slack channel from compliance page
Users could connect a Slack channel to their compliance page but had no way to remove or change the connection afterward. This adds a disconnect button with a confirmation dialog next to connected Slack channels. - GraphQL: deleteSlackConnection mutation with resolver - Frontend: Disconnect button using useMutation with @deleteEdge Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -1,36 +1,76 @@
|
|||||||
import { sprintf } from "@probo/helpers";
|
import { sprintf } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Badge, Button, Card, Slack } from "@probo/ui";
|
import { Badge, Button, Card, Slack, useConfirm } from "@probo/ui";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment, useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
|
import type { CompliancePageSlackSectionDeleteMutation } from "#/__generated__/core/CompliancePageSlackSectionDeleteMutation.graphql";
|
||||||
import type { CompliancePageSlackSectionFragment$key } from "#/__generated__/core/CompliancePageSlackSectionFragment.graphql";
|
import type { CompliancePageSlackSectionFragment$key } from "#/__generated__/core/CompliancePageSlackSectionFragment.graphql";
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||||
|
|
||||||
const fragment = graphql`
|
const fragment = graphql`
|
||||||
fragment CompliancePageSlackSectionFragment on Organization {
|
fragment CompliancePageSlackSectionFragment on Organization {
|
||||||
compliancePage: trustCenter {
|
canConnectSlack: permission(action: "core:connector:initiate")
|
||||||
canUpdate: permission(action: "core:trust-center:update")
|
|
||||||
}
|
|
||||||
slackConnections(first: 100) {
|
slackConnections(first: 100) {
|
||||||
|
__id
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
channel
|
channel
|
||||||
createdAt
|
createdAt
|
||||||
|
canDelete: permission(action: "core:connector:delete")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const deleteMutation = graphql`
|
||||||
|
mutation CompliancePageSlackSectionDeleteMutation(
|
||||||
|
$input: DeleteSlackConnectionInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
deleteSlackConnection(input: $input) {
|
||||||
|
deletedSlackConnectionId @deleteEdge(connections: $connections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export function CompliancePageSlackSection(props: { fragmentRef: CompliancePageSlackSectionFragment$key }) {
|
export function CompliancePageSlackSection(props: { fragmentRef: CompliancePageSlackSectionFragment$key }) {
|
||||||
const { fragmentRef } = props;
|
const { fragmentRef } = props;
|
||||||
|
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
const { __, dateTimeFormat } = useTranslate();
|
const { __, dateTimeFormat } = useTranslate();
|
||||||
|
const confirm = useConfirm();
|
||||||
|
|
||||||
const organization = useFragment<CompliancePageSlackSectionFragment$key>(fragment, fragmentRef);
|
const organization = useFragment<CompliancePageSlackSectionFragment$key>(fragment, fragmentRef);
|
||||||
|
const [deleteSlackConnection] = useMutation<CompliancePageSlackSectionDeleteMutation>(deleteMutation);
|
||||||
|
|
||||||
|
const connectionId = organization.slackConnections.__id;
|
||||||
|
|
||||||
|
const handleDisconnect = (slackConnectionId: string) => {
|
||||||
|
confirm(
|
||||||
|
() =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
deleteSlackConnection({
|
||||||
|
variables: {
|
||||||
|
connections: [connectionId],
|
||||||
|
input: {
|
||||||
|
slackConnectionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: () => resolve(),
|
||||||
|
onError: error => reject(error),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
title: __("Disconnect Slack"),
|
||||||
|
message: __("Are you sure you want to disconnect this Slack channel? This action cannot be undone."),
|
||||||
|
label: __("Disconnect"),
|
||||||
|
variant: "danger",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -60,14 +100,22 @@ export function CompliancePageSlackSection(props: { fragmentRef: CompliancePageS
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="success" size="md">
|
<Badge variant="success" size="md">
|
||||||
{__("Connected")}
|
{__("Connected")}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{slackConnection.canDelete && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => handleDisconnect(slackConnection.id)}
|
||||||
|
>
|
||||||
|
{__("Disconnect")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
{organization.compliancePage?.canUpdate && organization.slackConnections.edges.length === 0 && (
|
{organization.canConnectSlack && organization.slackConnections.edges.length === 0 && (
|
||||||
<Card
|
<Card
|
||||||
padded
|
padded
|
||||||
className="flex items-center gap-3"
|
className="flex items-center gap-3"
|
||||||
@@ -100,4 +148,4 @@ function getSlackConnectionUrl(organizationId: string): string {
|
|||||||
url.searchParams.append("continue", redirectUrl);
|
url.searchParams.append("continue", redirectUrl);
|
||||||
const finalUrl = url.toString();
|
const finalUrl = url.toString();
|
||||||
return finalUrl;
|
return finalUrl;
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -2077,12 +2077,14 @@ type Organization implements Node {
|
|||||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlackConnection {
|
type SlackConnection implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
channel: String
|
channel: String
|
||||||
channelId: String
|
channelId: String
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
|
|
||||||
|
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlackConnectionConnection {
|
type SlackConnectionConnection {
|
||||||
@@ -3944,6 +3946,10 @@ type Mutation {
|
|||||||
deleteCustomDomain(
|
deleteCustomDomain(
|
||||||
input: DeleteCustomDomainInput!
|
input: DeleteCustomDomainInput!
|
||||||
): DeleteCustomDomainPayload!
|
): DeleteCustomDomainPayload!
|
||||||
|
# Slack Connection mutations
|
||||||
|
deleteSlackConnection(
|
||||||
|
input: DeleteSlackConnectionInput!
|
||||||
|
): DeleteSlackConnectionPayload!
|
||||||
}
|
}
|
||||||
|
|
||||||
# Input Types
|
# Input Types
|
||||||
@@ -6096,6 +6102,14 @@ type DeleteCustomDomainPayload {
|
|||||||
deletedCustomDomainId: ID!
|
deletedCustomDomainId: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input DeleteSlackConnectionInput {
|
||||||
|
slackConnectionId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteSlackConnectionPayload {
|
||||||
|
deletedSlackConnectionId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
# Electronic Signature
|
# Electronic Signature
|
||||||
|
|
||||||
enum ElectronicSignatureStatus
|
enum ElectronicSignatureStatus
|
||||||
|
|||||||
@@ -6811,6 +6811,25 @@ func (r *mutationResolver) DeleteCustomDomain(ctx context.Context, input types.D
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteSlackConnection is the resolver for the deleteSlackConnection field.
|
||||||
|
func (r *mutationResolver) DeleteSlackConnection(ctx context.Context, input types.DeleteSlackConnectionInput) (*types.DeleteSlackConnectionPayload, error) {
|
||||||
|
if err := r.authorize(ctx, input.SlackConnectionID, probo.ActionConnectorDelete); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, input.SlackConnectionID.TenantID())
|
||||||
|
|
||||||
|
err := prb.Connectors.Delete(ctx, input.SlackConnectionID)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot delete slack connection", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.DeleteSlackConnectionPayload{
|
||||||
|
DeletedSlackConnectionID: input.SlackConnectionID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Organization is the resolver for the organization field.
|
// Organization is the resolver for the organization field.
|
||||||
func (r *obligationResolver) Organization(ctx context.Context, obj *types.Obligation) (*types.Organization, error) {
|
func (r *obligationResolver) Organization(ctx context.Context, obj *types.Obligation) (*types.Organization, error) {
|
||||||
if err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
if err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
||||||
@@ -8665,6 +8684,10 @@ func (r *riskConnectionResolver) TotalCount(ctx context.Context, obj *types.Risk
|
|||||||
return 0, gqlutils.Internal(ctx)
|
return 0, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permission is the resolver for the permission field.
|
||||||
|
func (r *slackConnectionResolver) Permission(ctx context.Context, obj *types.SlackConnection, action string) (bool, error) {
|
||||||
|
return r.Resolver.Permission(ctx, obj, action)
|
||||||
|
}
|
||||||
// Organization is the resolver for the organization field.
|
// Organization is the resolver for the organization field.
|
||||||
func (r *snapshotResolver) Organization(ctx context.Context, obj *types.Snapshot) (*types.Organization, error) {
|
func (r *snapshotResolver) Organization(ctx context.Context, obj *types.Snapshot) (*types.Organization, error) {
|
||||||
if err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
if err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
||||||
@@ -10599,6 +10622,10 @@ func (r *Resolver) Risk() schema.RiskResolver { return &riskResolver{r} }
|
|||||||
// RiskConnection returns schema.RiskConnectionResolver implementation.
|
// RiskConnection returns schema.RiskConnectionResolver implementation.
|
||||||
func (r *Resolver) RiskConnection() schema.RiskConnectionResolver { return &riskConnectionResolver{r} }
|
func (r *Resolver) RiskConnection() schema.RiskConnectionResolver { return &riskConnectionResolver{r} }
|
||||||
|
|
||||||
|
// SlackConnection returns schema.SlackConnectionResolver implementation.
|
||||||
|
func (r *Resolver) SlackConnection() schema.SlackConnectionResolver {
|
||||||
|
return &slackConnectionResolver{r}
|
||||||
|
}
|
||||||
// Snapshot returns schema.SnapshotResolver implementation.
|
// Snapshot returns schema.SnapshotResolver implementation.
|
||||||
func (r *Resolver) Snapshot() schema.SnapshotResolver { return &snapshotResolver{r} }
|
func (r *Resolver) Snapshot() schema.SnapshotResolver { return &snapshotResolver{r} }
|
||||||
|
|
||||||
@@ -10781,6 +10808,7 @@ type rightsRequestResolver struct{ *Resolver }
|
|||||||
type rightsRequestConnectionResolver struct{ *Resolver }
|
type rightsRequestConnectionResolver struct{ *Resolver }
|
||||||
type riskResolver struct{ *Resolver }
|
type riskResolver struct{ *Resolver }
|
||||||
type riskConnectionResolver struct{ *Resolver }
|
type riskConnectionResolver struct{ *Resolver }
|
||||||
|
type slackConnectionResolver struct{ *Resolver }
|
||||||
type snapshotResolver struct{ *Resolver }
|
type snapshotResolver struct{ *Resolver }
|
||||||
type snapshotConnectionResolver struct{ *Resolver }
|
type snapshotConnectionResolver struct{ *Resolver }
|
||||||
type stateOfApplicabilityResolver struct{ *Resolver }
|
type stateOfApplicabilityResolver struct{ *Resolver }
|
||||||
|
|||||||
Reference in New Issue
Block a user