From 85ec106cd617888455ffeede70094c4bd2cecf1e Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Thu, 5 Mar 2026 18:05:41 +0100 Subject: [PATCH] Send mailing list emails Signed-off-by: Sacha Al Himdani --- ...pliancePageMailingListPageQuery.graphql.ts | 209 +- ...cePageUpdatesListDeleteMutation.graphql.ts | 132 ++ ...mpliancePageUpdatesListFragment.graphql.ts | 226 ++ .../CompliancePageUpdatesListQuery.graphql.ts | 270 +++ ...ancePageUpdatesListSendMutation.graphql.ts | 138 ++ ...tComplianceUpdateDialogMutation.graphql.ts | 140 ++ ...wComplianceUpdateDialogMutation.graphql.ts | 193 ++ .../CompliancePageMailingListPage.tsx | 85 +- .../_components/CompliancePageUpdatesList.tsx | 209 ++ .../EditComplianceUpdateDialog.tsx | 118 ++ .../NewCompliancePageSubscriberDialog.tsx | 3 +- .../_components/NewComplianceUpdateDialog.tsx | 102 + .../CompliancePageFrameworkList.tsx | 3 +- apps/trust/src/layouts/MainLayout.tsx | 2 + apps/trust/src/pages/UpdatesPage.tsx | 108 + .../__generated__/UpdatesPageQuery.graphql.ts | 146 ++ apps/trust/src/routes.tsx | 20 + packages/emails/emails.go | 87 + packages/emails/scripts/build.ts | 15 + .../emails/src/MailingListSubscription.tsx | 46 + .../emails/src/MailingListUnsubscription.tsx | 26 + packages/emails/src/MailingListUpdates.tsx | 48 + .../templates/mailing-list-subscription.txt | 16 + .../templates/mailing-list-unsubscription.txt | 8 + .../emails/templates/mailing-list-updates.txt | 22 + pkg/coredata/email.go | 116 +- pkg/coredata/entity_type_reg.go | 3 + pkg/coredata/mailing_list_subscriber.go | 100 +- pkg/coredata/mailing_list_update.go | 385 ++++ .../mailing_list_update_order_field.go | 39 + pkg/coredata/mailing_list_update_status.go | 64 + pkg/coredata/migrations/20260305T120000Z.sql | 18 + pkg/coredata/trust_center.go | 51 + pkg/esign/completion_certificate_worker.go | 1 + pkg/iam/account_service.go | 1 + pkg/iam/auth_service.go | 3 + pkg/iam/organization_service.go | 1 + pkg/mailer/mailer.go | 10 + pkg/mailman/compliance_mailing_list.go | 161 ++ pkg/mailman/errors.go | 8 +- pkg/mailman/mailing_list_worker.go | 230 +++ pkg/mailman/service.go | 546 ++++- pkg/mailman/token.go | 86 + pkg/probo/actions.go | 7 + pkg/probo/document_service.go | 2 + pkg/probo/framework_service.go | 1 + pkg/probo/trust_center_access_service.go | 1 + pkg/probod/probod.go | 15 +- pkg/server/api/console/v1/schema.graphql | 101 + pkg/server/api/console/v1/schema/schema.go | 1822 +++++++++++++++++ .../console/v1/types/mailing_list_update.go | 66 + pkg/server/api/console/v1/types/types.go | 54 + pkg/server/api/console/v1/v1_resolver.go | 139 +- pkg/server/api/trust/v1/resolver.go | 2 - pkg/server/api/trust/v1/schema.graphql | 25 + pkg/server/api/trust/v1/schema/schema.go | 758 +++++++ .../trust/v1/types/mailing_list_subscriber.go | 2 + .../api/trust/v1/types/mailing_list_update.go | 50 + pkg/server/api/trust/v1/types/types.go | 21 + pkg/server/api/trust/v1/v1_resolver.go | 52 +- pkg/server/mailactions/confirm.go | 124 ++ pkg/server/mailactions/mux.go | 40 + pkg/server/mailactions/render.go | 45 + .../mailactions/templates/page.html.tmpl | 99 + pkg/server/mailactions/unsubscribe.go | 116 ++ pkg/server/server.go | 36 +- pkg/trust/trust_center_access_service.go | 2 + 67 files changed, 7615 insertions(+), 160 deletions(-) create mode 100644 apps/console/src/__generated__/core/CompliancePageUpdatesListDeleteMutation.graphql.ts create mode 100644 apps/console/src/__generated__/core/CompliancePageUpdatesListFragment.graphql.ts create mode 100644 apps/console/src/__generated__/core/CompliancePageUpdatesListQuery.graphql.ts create mode 100644 apps/console/src/__generated__/core/CompliancePageUpdatesListSendMutation.graphql.ts create mode 100644 apps/console/src/__generated__/core/EditComplianceUpdateDialogMutation.graphql.ts create mode 100644 apps/console/src/__generated__/core/NewComplianceUpdateDialogMutation.graphql.ts create mode 100644 apps/console/src/pages/organizations/compliance-page/mailing-list/_components/CompliancePageUpdatesList.tsx create mode 100644 apps/console/src/pages/organizations/compliance-page/mailing-list/_components/EditComplianceUpdateDialog.tsx create mode 100644 apps/console/src/pages/organizations/compliance-page/mailing-list/_components/NewComplianceUpdateDialog.tsx create mode 100644 apps/trust/src/pages/UpdatesPage.tsx create mode 100644 apps/trust/src/pages/__generated__/UpdatesPageQuery.graphql.ts create mode 100644 packages/emails/src/MailingListSubscription.tsx create mode 100644 packages/emails/src/MailingListUnsubscription.tsx create mode 100644 packages/emails/src/MailingListUpdates.tsx create mode 100644 packages/emails/templates/mailing-list-subscription.txt create mode 100644 packages/emails/templates/mailing-list-unsubscription.txt create mode 100644 packages/emails/templates/mailing-list-updates.txt create mode 100644 pkg/coredata/mailing_list_update.go create mode 100644 pkg/coredata/mailing_list_update_order_field.go create mode 100644 pkg/coredata/mailing_list_update_status.go create mode 100644 pkg/coredata/migrations/20260305T120000Z.sql create mode 100644 pkg/mailman/compliance_mailing_list.go create mode 100644 pkg/mailman/mailing_list_worker.go create mode 100644 pkg/mailman/token.go create mode 100644 pkg/server/api/console/v1/types/mailing_list_update.go create mode 100644 pkg/server/api/trust/v1/types/mailing_list_update.go create mode 100644 pkg/server/mailactions/confirm.go create mode 100644 pkg/server/mailactions/mux.go create mode 100644 pkg/server/mailactions/render.go create mode 100644 pkg/server/mailactions/templates/page.html.tmpl create mode 100644 pkg/server/mailactions/unsubscribe.go diff --git a/apps/console/src/__generated__/core/CompliancePageMailingListPageQuery.graphql.ts b/apps/console/src/__generated__/core/CompliancePageMailingListPageQuery.graphql.ts index 898112fd0..a557f1b2c 100644 --- a/apps/console/src/__generated__/core/CompliancePageMailingListPageQuery.graphql.ts +++ b/apps/console/src/__generated__/core/CompliancePageMailingListPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<7e30e59b53799ccae49199d7b46f0320>> + * @generated SignedSource<<1ddc7eb54e71de0d95fb5af865fc0348>> * @lightSyntaxTransform * @nogrep */ @@ -21,6 +21,7 @@ export type CompliancePageMailingListPageQuery$data = { readonly mailingList: { readonly id: string; readonly replyTo: string | null | undefined; + readonly " $fragmentSpreads": FragmentRefs<"CompliancePageUpdatesListFragment">; } | null | undefined; readonly " $fragmentSpreads": FragmentRefs<"CompliancePageMailingListFragment">; }; @@ -77,7 +78,65 @@ v5 = [ "name": "first", "value": 20 } -]; +], +v6 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + } + ], + "storageKey": null +}, +v7 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null +}, +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null +}, +v9 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v10 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}; return { "fragment": { "argumentDefinitions": (v0/*: any*/), @@ -117,7 +176,12 @@ return { "plural": false, "selections": [ (v3/*: any*/), - (v4/*: any*/) + (v4/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "CompliancePageUpdatesListFragment" + } ], "storageKey": null }, @@ -179,6 +243,76 @@ return { "selections": [ (v3/*: any*/), (v4/*: any*/), + { + "alias": null, + "args": (v5/*: any*/), + "concreteType": "MailingListUpdateConnection", + "kind": "LinkedField", + "name": "updates", + "plural": false, + "selections": [ + (v6/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "MailingListUpdateEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MailingListUpdate", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "body", + "storageKey": null + }, + (v7/*: any*/), + (v8/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + }, + (v2/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/) + ], + "storageKey": null + }, + (v10/*: any*/) + ], + "storageKey": "updates(first:20)" + }, + { + "alias": null, + "args": (v5/*: any*/), + "filters": null, + "handle": "connection", + "key": "CompliancePageUpdatesList_updates", + "kind": "LinkedHandle", + "name": "updates" + }, { "alias": null, "args": (v5/*: any*/), @@ -187,31 +321,7 @@ return { "name": "subscribers", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasNextPage", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "endCursor", - "storageKey": null - } - ], - "storageKey": null - }, + (v6/*: any*/), { "alias": null, "args": null, @@ -243,46 +353,17 @@ return { "name": "email", "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "status", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null - }, + (v7/*: any*/), + (v8/*: any*/), (v2/*: any*/) ], "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } + (v9/*: any*/) ], "storageKey": null }, - { - "kind": "ClientExtension", - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__id", - "storageKey": null - } - ] - } + (v10/*: any*/) ], "storageKey": "subscribers(first:20)" }, @@ -312,16 +393,16 @@ return { ] }, "params": { - "cacheID": "8eb57a5cf8f8b1294da6aee3228176f3", + "cacheID": "c8ba2eeecdfac4115f0ab1493ecf02ce", "id": null, "metadata": {}, "name": "CompliancePageMailingListPageQuery", "operationKind": "query", - "text": "query CompliancePageMailingListPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n compliancePage: trustCenter {\n id\n mailingList {\n id\n replyTo\n }\n ...CompliancePageMailingListFragment\n }\n }\n id\n }\n}\n\nfragment CompliancePageMailingListFragment on TrustCenter {\n mailingList {\n id\n subscribers(first: 20) {\n pageInfo {\n hasNextPage\n endCursor\n }\n edges {\n node {\n id\n fullName\n email\n status\n createdAt\n __typename\n }\n cursor\n }\n }\n }\n id\n}\n" + "text": "query CompliancePageMailingListPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n compliancePage: trustCenter {\n id\n mailingList {\n id\n replyTo\n ...CompliancePageUpdatesListFragment\n }\n ...CompliancePageMailingListFragment\n }\n }\n id\n }\n}\n\nfragment CompliancePageMailingListFragment on TrustCenter {\n mailingList {\n id\n subscribers(first: 20) {\n pageInfo {\n hasNextPage\n endCursor\n }\n edges {\n node {\n id\n fullName\n email\n status\n createdAt\n __typename\n }\n cursor\n }\n }\n }\n id\n}\n\nfragment CompliancePageUpdatesListFragment on MailingList {\n updates(first: 20) {\n pageInfo {\n hasNextPage\n endCursor\n }\n edges {\n node {\n id\n title\n body\n status\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n }\n id\n}\n" } }; })(); -(node as any).hash = "0df7b2a8b9f0d2f421f354bd56773e43"; +(node as any).hash = "bda0ad6e5ff15a324d4c2ad76d0c94cd"; export default node; diff --git a/apps/console/src/__generated__/core/CompliancePageUpdatesListDeleteMutation.graphql.ts b/apps/console/src/__generated__/core/CompliancePageUpdatesListDeleteMutation.graphql.ts new file mode 100644 index 000000000..8fa20effa --- /dev/null +++ b/apps/console/src/__generated__/core/CompliancePageUpdatesListDeleteMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<<8fc725e69722e66e680df0ed52206c77>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteMailingListUpdateInput = { + id: string; +}; +export type CompliancePageUpdatesListDeleteMutation$variables = { + connections: ReadonlyArray; + input: DeleteMailingListUpdateInput; +}; +export type CompliancePageUpdatesListDeleteMutation$data = { + readonly deleteMailingListUpdate: { + readonly deletedMailingListUpdateId: string; + }; +}; +export type CompliancePageUpdatesListDeleteMutation = { + response: CompliancePageUpdatesListDeleteMutation$data; + variables: CompliancePageUpdatesListDeleteMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedMailingListUpdateId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "CompliancePageUpdatesListDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteMailingListUpdatePayload", + "kind": "LinkedField", + "name": "deleteMailingListUpdate", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "CompliancePageUpdatesListDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteMailingListUpdatePayload", + "kind": "LinkedField", + "name": "deleteMailingListUpdate", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedMailingListUpdateId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "892a75687f4d8560945e5a30915aecbd", + "id": null, + "metadata": {}, + "name": "CompliancePageUpdatesListDeleteMutation", + "operationKind": "mutation", + "text": "mutation CompliancePageUpdatesListDeleteMutation(\n $input: DeleteMailingListUpdateInput!\n) {\n deleteMailingListUpdate(input: $input) {\n deletedMailingListUpdateId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "29a3142fa30bc64beb74d21f5162a00c"; + +export default node; diff --git a/apps/console/src/__generated__/core/CompliancePageUpdatesListFragment.graphql.ts b/apps/console/src/__generated__/core/CompliancePageUpdatesListFragment.graphql.ts new file mode 100644 index 000000000..7ef0f85c1 --- /dev/null +++ b/apps/console/src/__generated__/core/CompliancePageUpdatesListFragment.graphql.ts @@ -0,0 +1,226 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type MailingListUpdateStatus = "DRAFT" | "ENQUEUED" | "PROCESSING" | "SENT"; +import { FragmentRefs } from "relay-runtime"; +export type CompliancePageUpdatesListFragment$data = { + readonly id: string; + readonly updates: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly body: string; + readonly createdAt: string; + readonly id: string; + readonly status: MailingListUpdateStatus; + readonly title: string; + readonly updatedAt: string; + }; + }>; + readonly pageInfo: { + readonly endCursor: string | null | undefined; + readonly hasNextPage: boolean; + }; + }; + readonly " $fragmentType": "CompliancePageUpdatesListFragment"; +}; +export type CompliancePageUpdatesListFragment$key = { + readonly " $data"?: CompliancePageUpdatesListFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"CompliancePageUpdatesListFragment">; +}; + +import CompliancePageUpdatesListQuery_graphql from './CompliancePageUpdatesListQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "updates" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": "first", + "cursor": "after", + "direction": "forward", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": null, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": CompliancePageUpdatesListQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "CompliancePageUpdatesListFragment", + "selections": [ + { + "alias": "updates", + "args": null, + "concreteType": "MailingListUpdateConnection", + "kind": "LinkedField", + "name": "__CompliancePageUpdatesList_updates_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "MailingListUpdateEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MailingListUpdate", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "body", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + (v1/*: any*/) + ], + "type": "MailingList", + "abstractKey": null +}; +})(); + +(node as any).hash = "97bcf2215cc4fa04d1d4b84b2da4c33a"; + +export default node; diff --git a/apps/console/src/__generated__/core/CompliancePageUpdatesListQuery.graphql.ts b/apps/console/src/__generated__/core/CompliancePageUpdatesListQuery.graphql.ts new file mode 100644 index 000000000..6cdfff1bd --- /dev/null +++ b/apps/console/src/__generated__/core/CompliancePageUpdatesListQuery.graphql.ts @@ -0,0 +1,270 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type CompliancePageUpdatesListQuery$variables = { + after?: string | null | undefined; + first?: number | null | undefined; + id: string; +}; +export type CompliancePageUpdatesListQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"CompliancePageUpdatesListFragment">; + }; +}; +export type CompliancePageUpdatesListQuery = { + response: CompliancePageUpdatesListQuery$data; + variables: CompliancePageUpdatesListQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v2 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "after" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "first" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "CompliancePageUpdatesListQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": (v2/*: any*/), + "kind": "FragmentSpread", + "name": "CompliancePageUpdatesListFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "CompliancePageUpdatesListQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "MailingListUpdateConnection", + "kind": "LinkedField", + "name": "updates", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "MailingListUpdateEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MailingListUpdate", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "body", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + }, + (v3/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v2/*: any*/), + "filters": null, + "handle": "connection", + "key": "CompliancePageUpdatesList_updates", + "kind": "LinkedHandle", + "name": "updates" + } + ], + "type": "MailingList", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "e1a6160f157715c1c52adca86d02b9c6", + "id": null, + "metadata": {}, + "name": "CompliancePageUpdatesListQuery", + "operationKind": "query", + "text": "query CompliancePageUpdatesListQuery(\n $after: CursorKey = null\n $first: Int = 20\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...CompliancePageUpdatesListFragment_2HEEH6\n id\n }\n}\n\nfragment CompliancePageUpdatesListFragment_2HEEH6 on MailingList {\n updates(first: $first, after: $after) {\n pageInfo {\n hasNextPage\n endCursor\n }\n edges {\n node {\n id\n title\n body\n status\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "97bcf2215cc4fa04d1d4b84b2da4c33a"; + +export default node; diff --git a/apps/console/src/__generated__/core/CompliancePageUpdatesListSendMutation.graphql.ts b/apps/console/src/__generated__/core/CompliancePageUpdatesListSendMutation.graphql.ts new file mode 100644 index 000000000..334e30aa1 --- /dev/null +++ b/apps/console/src/__generated__/core/CompliancePageUpdatesListSendMutation.graphql.ts @@ -0,0 +1,138 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type MailingListUpdateStatus = "DRAFT" | "ENQUEUED" | "PROCESSING" | "SENT"; +export type SendMailingListUpdateInput = { + id: string; +}; +export type CompliancePageUpdatesListSendMutation$variables = { + input: SendMailingListUpdateInput; +}; +export type CompliancePageUpdatesListSendMutation$data = { + readonly sendMailingListUpdate: { + readonly mailingListUpdate: { + readonly body: string; + readonly id: string; + readonly status: MailingListUpdateStatus; + readonly title: string; + readonly updatedAt: string; + }; + }; +}; +export type CompliancePageUpdatesListSendMutation = { + response: CompliancePageUpdatesListSendMutation$data; + variables: CompliancePageUpdatesListSendMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "SendMailingListUpdatePayload", + "kind": "LinkedField", + "name": "sendMailingListUpdate", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MailingListUpdate", + "kind": "LinkedField", + "name": "mailingListUpdate", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "body", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "CompliancePageUpdatesListSendMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "CompliancePageUpdatesListSendMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "23fee99a4999d2c9cbeecd6fa9863b67", + "id": null, + "metadata": {}, + "name": "CompliancePageUpdatesListSendMutation", + "operationKind": "mutation", + "text": "mutation CompliancePageUpdatesListSendMutation(\n $input: SendMailingListUpdateInput!\n) {\n sendMailingListUpdate(input: $input) {\n mailingListUpdate {\n id\n title\n body\n status\n updatedAt\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "37c68b46d761280e2ff16f5bff08b249"; + +export default node; diff --git a/apps/console/src/__generated__/core/EditComplianceUpdateDialogMutation.graphql.ts b/apps/console/src/__generated__/core/EditComplianceUpdateDialogMutation.graphql.ts new file mode 100644 index 000000000..137a3b9ab --- /dev/null +++ b/apps/console/src/__generated__/core/EditComplianceUpdateDialogMutation.graphql.ts @@ -0,0 +1,140 @@ +/** + * @generated SignedSource<<622eccc63036573a8e38ffc8dd36e7d7>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type MailingListUpdateStatus = "DRAFT" | "ENQUEUED" | "PROCESSING" | "SENT"; +export type UpdateMailingListUpdateInput = { + body: string; + id: string; + title: string; +}; +export type EditComplianceUpdateDialogMutation$variables = { + input: UpdateMailingListUpdateInput; +}; +export type EditComplianceUpdateDialogMutation$data = { + readonly updateMailingListUpdate: { + readonly mailingListUpdate: { + readonly body: string; + readonly id: string; + readonly status: MailingListUpdateStatus; + readonly title: string; + readonly updatedAt: string; + }; + }; +}; +export type EditComplianceUpdateDialogMutation = { + response: EditComplianceUpdateDialogMutation$data; + variables: EditComplianceUpdateDialogMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateMailingListUpdatePayload", + "kind": "LinkedField", + "name": "updateMailingListUpdate", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MailingListUpdate", + "kind": "LinkedField", + "name": "mailingListUpdate", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "body", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "EditComplianceUpdateDialogMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "EditComplianceUpdateDialogMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "49b4c9f194a0bf6ebe01408ae6387f2e", + "id": null, + "metadata": {}, + "name": "EditComplianceUpdateDialogMutation", + "operationKind": "mutation", + "text": "mutation EditComplianceUpdateDialogMutation(\n $input: UpdateMailingListUpdateInput!\n) {\n updateMailingListUpdate(input: $input) {\n mailingListUpdate {\n id\n title\n body\n status\n updatedAt\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "8b2246ab3bddba658cc488610afcd80f"; + +export default node; diff --git a/apps/console/src/__generated__/core/NewComplianceUpdateDialogMutation.graphql.ts b/apps/console/src/__generated__/core/NewComplianceUpdateDialogMutation.graphql.ts new file mode 100644 index 000000000..6ed0c94ee --- /dev/null +++ b/apps/console/src/__generated__/core/NewComplianceUpdateDialogMutation.graphql.ts @@ -0,0 +1,193 @@ +/** + * @generated SignedSource<<458f30b721ecd9df210621a8853008c3>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type MailingListUpdateStatus = "DRAFT" | "ENQUEUED" | "PROCESSING" | "SENT"; +export type CreateMailingListUpdateInput = { + body: string; + mailingListId: string; + title: string; +}; +export type NewComplianceUpdateDialogMutation$variables = { + connections: ReadonlyArray; + input: CreateMailingListUpdateInput; +}; +export type NewComplianceUpdateDialogMutation$data = { + readonly createMailingListUpdate: { + readonly mailingListUpdate: { + readonly body: string; + readonly createdAt: string; + readonly id: string; + readonly status: MailingListUpdateStatus; + readonly title: string; + readonly updatedAt: string; + }; + }; +}; +export type NewComplianceUpdateDialogMutation = { + response: NewComplianceUpdateDialogMutation$data; + variables: NewComplianceUpdateDialogMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "concreteType": "MailingListUpdate", + "kind": "LinkedField", + "name": "mailingListUpdate", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "body", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + } + ], + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "NewComplianceUpdateDialogMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateMailingListUpdatePayload", + "kind": "LinkedField", + "name": "createMailingListUpdate", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "NewComplianceUpdateDialogMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateMailingListUpdatePayload", + "kind": "LinkedField", + "name": "createMailingListUpdate", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependNode", + "key": "", + "kind": "LinkedHandle", + "name": "mailingListUpdate", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + }, + { + "kind": "Literal", + "name": "edgeTypeName", + "value": "MailingListUpdateEdge" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "33fc392100d93803de3a189e75961a75", + "id": null, + "metadata": {}, + "name": "NewComplianceUpdateDialogMutation", + "operationKind": "mutation", + "text": "mutation NewComplianceUpdateDialogMutation(\n $input: CreateMailingListUpdateInput!\n) {\n createMailingListUpdate(input: $input) {\n mailingListUpdate {\n id\n title\n body\n status\n createdAt\n updatedAt\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "bf02b571efaf38d4f4a8fde50b311832"; + +export default node; diff --git a/apps/console/src/pages/organizations/compliance-page/mailing-list/CompliancePageMailingListPage.tsx b/apps/console/src/pages/organizations/compliance-page/mailing-list/CompliancePageMailingListPage.tsx index 7fd30cc9d..27d00afd3 100644 --- a/apps/console/src/pages/organizations/compliance-page/mailing-list/CompliancePageMailingListPage.tsx +++ b/apps/console/src/pages/organizations/compliance-page/mailing-list/CompliancePageMailingListPage.tsx @@ -1,5 +1,5 @@ import { useTranslate } from "@probo/i18n"; -import { Button, Card, Field, IconPlusLarge, Spinner, useDialogRef } from "@probo/ui"; +import { Button, Card, Field, IconPlusLarge, Spinner, TabItem, Tabs, useDialogRef } from "@probo/ui"; import { useState } from "react"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { ConnectionHandler, graphql } from "relay-runtime"; @@ -9,7 +9,10 @@ import type { CompliancePageMailingListPageQuery } from "#/__generated__/core/Co import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { CompliancePageMailingList } from "./_components/CompliancePageMailingList"; +import { CompliancePageUpdatesList, type UpdateNode } from "./_components/CompliancePageUpdatesList"; +import { EditComplianceUpdateDialog } from "./_components/EditComplianceUpdateDialog"; import { NewCompliancePageSubscriberDialog } from "./_components/NewCompliancePageSubscriberDialog"; +import { NewComplianceUpdateDialog } from "./_components/NewComplianceUpdateDialog"; export const compliancePageMailingListPageQuery = graphql` query CompliancePageMailingListPageQuery($organizationId: ID!) { @@ -21,6 +24,7 @@ export const compliancePageMailingListPageQuery = graphql` mailingList { id replyTo + ...CompliancePageUpdatesListFragment } ...CompliancePageMailingListFragment } @@ -40,12 +44,19 @@ const updateMailingListMutation = graphql` } `; +type Tab = "updates" | "subscribers"; + export function CompliancePageMailingListPage(props: { queryRef: PreloadedQuery; }) { const { queryRef } = props; const { __ } = useTranslate(); - const dialogRef = useDialogRef(); + const subscriberDialogRef = useDialogRef(); + const newUpdateDialogRef = useDialogRef(); + const editUpdateDialogRef = useDialogRef(); + + const [activeTab, setActiveTab] = useState("updates"); + const [selectedUpdate, setSelectedUpdate] = useState(null); const { organization } = usePreloadedQuery( compliancePageMailingListPageQuery, @@ -56,13 +67,18 @@ export function CompliancePageMailingListPage(props: { throw new Error("invalid type for node"); } - const mailingList = organization.compliancePage.mailingList; + const { compliancePage } = organization; + const mailingList = compliancePage.mailingList; const mailingListId = mailingList?.id; - const connectionId = mailingListId + const subscriberConnectionId = mailingListId ? ConnectionHandler.getConnectionID(mailingListId, "CompliancePageMailingList_subscribers") : null; + const updatesConnectionId = mailingListId + ? ConnectionHandler.getConnectionID(mailingListId, "CompliancePageUpdatesList_updates") + : null; + const [replyTo, setReplyTo] = useState(mailingList?.replyTo ?? ""); const [updateMailingList, isUpdating] = useMutationWithToasts( @@ -85,6 +101,11 @@ export function CompliancePageMailingListPage(props: { }); }; + const handleEditUpdate = (update: UpdateNode) => { + setSelectedUpdate({ ...update }); + editUpdateDialogRef.current?.open(); + }; + return (
{mailingListId && ( @@ -119,27 +140,57 @@ export function CompliancePageMailingListPage(props: {
-
-

{__("Subscribers")}

-

- {__("People subscribed to receive security and compliance updates")} -

-
- {mailingListId && ( - + )} + {activeTab === "subscribers" && mailingListId && ( + )}
- + {activeTab === "updates" && mailingList && ( + + )} + + {activeTab === "subscribers" && ( + + )}
- {mailingListId && connectionId && ( - + )} + + + + {mailingListId && subscriberConnectionId && ( + )}
diff --git a/apps/console/src/pages/organizations/compliance-page/mailing-list/_components/CompliancePageUpdatesList.tsx b/apps/console/src/pages/organizations/compliance-page/mailing-list/_components/CompliancePageUpdatesList.tsx new file mode 100644 index 000000000..83be541de --- /dev/null +++ b/apps/console/src/pages/organizations/compliance-page/mailing-list/_components/CompliancePageUpdatesList.tsx @@ -0,0 +1,209 @@ +import { useTranslate } from "@probo/i18n"; +import { Badge, Button, IconChevronDown, IconPencil, IconSend, IconTrashCan, Spinner, Table, Tbody, Td, Th, Thead, Tr, useConfirm } from "@probo/ui"; +import { usePaginationFragment } from "react-relay"; +import { graphql } from "relay-runtime"; + +import type { CompliancePageUpdatesListDeleteMutation } from "#/__generated__/core/CompliancePageUpdatesListDeleteMutation.graphql"; +import type { CompliancePageUpdatesListFragment$data, CompliancePageUpdatesListFragment$key } from "#/__generated__/core/CompliancePageUpdatesListFragment.graphql"; +import type { CompliancePageUpdatesListQuery } from "#/__generated__/core/CompliancePageUpdatesListQuery.graphql"; +import type { CompliancePageUpdatesListSendMutation } from "#/__generated__/core/CompliancePageUpdatesListSendMutation.graphql"; +import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; + +const deleteMutation = graphql` + mutation CompliancePageUpdatesListDeleteMutation( + $input: DeleteMailingListUpdateInput! + $connections: [ID!]! + ) { + deleteMailingListUpdate(input: $input) { + deletedMailingListUpdateId @deleteEdge(connections: $connections) + } + } +`; + +const sendMutation = graphql` + mutation CompliancePageUpdatesListSendMutation($input: SendMailingListUpdateInput!) { + sendMailingListUpdate(input: $input) { + mailingListUpdate { + id + title + body + status + updatedAt + } + } + } +`; + +const fragment = graphql` + fragment CompliancePageUpdatesListFragment on MailingList + @argumentDefinitions( + first: { type: Int, defaultValue: 20 } + after: { type: CursorKey, defaultValue: null } + ) + @refetchable(queryName: "CompliancePageUpdatesListQuery") { + updates( + first: $first + after: $after + ) @connection(key: "CompliancePageUpdatesList_updates") { + __id + pageInfo { + hasNextPage + endCursor + } + edges { + node { + id + title + body + status + createdAt + updatedAt + } + } + } + } +`; + +export type UpdateNode = CompliancePageUpdatesListFragment$data["updates"]["edges"][number]["node"]; + +export function CompliancePageUpdatesList(props: { + fragmentRef: CompliancePageUpdatesListFragment$key; + onEdit: (update: UpdateNode) => void; +}) { + const { fragmentRef, onEdit } = props; + const { __ } = useTranslate(); + const confirm = useConfirm(); + + const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment< + CompliancePageUpdatesListQuery, + CompliancePageUpdatesListFragment$key + >(fragment, fragmentRef); + + const connection = data.updates; + + const [deleteUpdate, isDeleting] + = useMutationWithToasts(deleteMutation, { + successMessage: __("Update deleted successfully"), + errorMessage: __("Failed to delete update"), + }); + + const [sendUpdate, isSending] + = useMutationWithToasts(sendMutation, { + successMessage: __("Update enqueued for delivery"), + errorMessage: __("Failed to enqueue update for delivery"), + }); + + const handleDelete = (id: string) => { + void deleteUpdate({ + variables: { + input: { id }, + connections: [connection.__id], + }, + }); + }; + + const handleSend = (node: UpdateNode) => { + confirm( + () => + sendUpdate({ + variables: { + input: { + id: node.id, + }, + }, + }), + { + title: __("Send this update to all subscribers?"), + message: `"${node.title}" — ${node.body.length > 120 ? node.body.slice(0, 120) + "…" : node.body}`, + label: __("Send"), + variant: "primary", + }, + ); + }; + + return ( + <> + {connection.edges.length === 0 + ? ( + + + + + + +
+ {__("No updates yet")} +
+ ) + : ( + <> + + + + + + + + + + {connection.edges.map(({ node }) => ( + + + + + + + ))} + +
{__("Title")}{__("Status")}{__("Created")} +
{node.title} + + {node.status === "SENT" ? __("Sent") : node.status === "ENQUEUED" ? __("Queued") : node.status === "PROCESSING" ? __("Processing…") : __("Draft")} + + + {new Date(node.createdAt).toLocaleDateString()} + +
+ {node.status === "DRAFT" && ( + + )} +
+
+ {hasNext && ( + + )} + + )} + + ); +} diff --git a/apps/console/src/pages/organizations/compliance-page/mailing-list/_components/EditComplianceUpdateDialog.tsx b/apps/console/src/pages/organizations/compliance-page/mailing-list/_components/EditComplianceUpdateDialog.tsx new file mode 100644 index 000000000..1eb443216 --- /dev/null +++ b/apps/console/src/pages/organizations/compliance-page/mailing-list/_components/EditComplianceUpdateDialog.tsx @@ -0,0 +1,118 @@ +import { useTranslate } from "@probo/i18n"; +import { Button, Dialog, DialogContent, DialogFooter, type DialogRef, Field, IconCircleInfo, Spinner, Textarea } from "@probo/ui"; +import { useEffect } from "react"; +import { graphql } from "relay-runtime"; +import { z } from "zod"; + +import type { EditComplianceUpdateDialogMutation } from "#/__generated__/core/EditComplianceUpdateDialogMutation.graphql"; +import { useFormWithSchema } from "#/hooks/useFormWithSchema"; +import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; + +import type { UpdateNode } from "./CompliancePageUpdatesList"; + +const updateMutation = graphql` + mutation EditComplianceUpdateDialogMutation($input: UpdateMailingListUpdateInput!) { + updateMailingListUpdate(input: $input) { + mailingListUpdate { + id + title + body + status + updatedAt + } + } + } +`; + +export function EditComplianceUpdateDialog(props: { + update: UpdateNode | null; + ref: DialogRef; +}) { + const { update, ref } = props; + const { __ } = useTranslate(); + + const isSent = update?.status !== "DRAFT"; + + const schema = z.object({ + title: z.string().trim().min(1, __("Title is required")), + body: z.string().trim().min(1, __("Body is required")), + }); + + const form = useFormWithSchema(schema, { + defaultValues: { title: "", body: "" }, + }); + + useEffect(() => { + if (update) { + form.reset({ title: update.title, body: update.body }); + } + }, [update, form]); + + const [saveUpdate, isSaving] = useMutationWithToasts( + updateMutation, + { + successMessage: __("Update saved successfully"), + errorMessage: __("Failed to save update"), + }, + ); + + const handleSubmit = async (data: { title: string; body: string }) => { + if (!update) return; + await saveUpdate({ + variables: { + input: { + id: update.id, + title: data.title.trim(), + body: data.body.trim(), + }, + }, + onCompleted: (_, errors) => { + if (!errors?.length) { + ref.current?.close(); + } + }, + }); + }; + + return ( + +
void form.handleSubmit(handleSubmit)(e)}> + +
+ +

+ {__("Do not include confidential or sensitive information in this update. Any content that requires protection should be placed behind your NDA-gated documents instead.")} +

+
+ + +