Send mailing list emails

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-03-05 18:05:41 +01:00
parent aa01c40184
commit 85ec106cd6
67 changed files with 7615 additions and 160 deletions

View File

@@ -39,6 +39,8 @@ export function MainLayout(props: Props) {
<TabLink to="/documents">{__("Documents")}</TabLink>
{trustCenter.vendorInfo.totalCount > 0
&& <TabLink to="/subprocessors">{__("Subprocessors")}</TabLink>}
{isAuthenticated
&& <TabLink to="/updates">{__("Updates")}</TabLink>}
</Tabs>
<Outlet context={{ trustCenter }} />
</main>

View File

@@ -0,0 +1,108 @@
import { useTranslate } from "@probo/i18n";
import { IconChevronDown } from "@probo/ui";
import { useState } from "react";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import type { UpdatesPageQuery } from "#/pages/__generated__/UpdatesPageQuery.graphql";
export const currentTrustUpdatesQuery = graphql`
query UpdatesPageQuery {
currentTrustCenter {
id
updates(first: 50) {
edges {
node {
id
title
body
updatedAt
}
}
}
}
}
`;
type Props = {
queryRef: PreloadedQuery<UpdatesPageQuery>;
};
export function UpdatesPage({ queryRef }: Props) {
const { __ } = useTranslate();
const data = usePreloadedQuery<UpdatesPageQuery>(
currentTrustUpdatesQuery,
queryRef,
);
const items
= data.currentTrustCenter?.updates.edges.map(e => e.node) ?? [];
return (
<div>
<h2 className="font-medium mb-1">{__("Updates")}</h2>
<p className="text-sm text-txt-secondary mb-4">
{__("Latest compliance and security updates:")}
</p>
<div className="space-y-0">
{items.map(item => (
<UpdateItem key={item.id} item={item} />
))}
</div>
</div>
);
}
type UpdateItemType = {
id: string;
title: string;
body: string;
updatedAt: string;
};
function UpdateItem({ item }: { item: UpdateItemType }) {
const [open, setOpen] = useState(false);
return (
<div className="border border-border-solid -mt-px first:rounded-t-lg last:rounded-b-lg overflow-hidden">
<button
type="button"
onClick={() => setOpen(o => !o)}
className="w-full flex items-center justify-between px-6 py-4 text-left hover:bg-highlight transition-colors group"
>
<div className="flex items-center gap-3 min-w-0">
<span className="text-sm font-medium text-txt-primary truncate">
{item.title}
</span>
</div>
<div className="flex items-center gap-4 flex-none ml-4">
<span className="text-xs text-txt-tertiary hidden sm:block">
{new Date(item.updatedAt).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
<IconChevronDown
size={16}
className={`flex-none text-txt-tertiary transition-transform duration-200 ${open ? "rotate-180" : ""}`}
/>
</div>
</button>
{open && (
<div className="px-6 pb-5 pt-0 border-t border-border-solid bg-highlight/40">
<p className="text-sm text-txt-secondary whitespace-pre-wrap leading-relaxed pt-4">
{item.body}
</p>
<p className="text-xs text-txt-tertiary mt-3 sm:hidden">
{new Date(item.updatedAt).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</p>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,146 @@
/**
* @generated SignedSource<<4560049874109fe98528d64249673d23>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type UpdatesPageQuery$variables = Record<PropertyKey, never>;
export type UpdatesPageQuery$data = {
readonly currentTrustCenter: {
readonly id: string;
readonly updates: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly body: string;
readonly id: string;
readonly title: string;
readonly updatedAt: any;
};
}>;
};
} | null | undefined;
};
export type UpdatesPageQuery = {
response: UpdatesPageQuery$data;
variables: UpdatesPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v1 = [
{
"alias": null,
"args": null,
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "currentTrustCenter",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 50
}
],
"concreteType": "MailingListUpdateConnection",
"kind": "LinkedField",
"name": "updates",
"plural": false,
"selections": [
{
"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": [
(v0/*: 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": "updatedAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "updates(first:50)"
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "UpdatesPageQuery",
"selections": (v1/*: any*/),
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "UpdatesPageQuery",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "5d323cbbdf9bfec294bb22dd6a6be735",
"id": null,
"metadata": {},
"name": "UpdatesPageQuery",
"operationKind": "query",
"text": "query UpdatesPageQuery {\n currentTrustCenter {\n id\n updates(first: 50) {\n edges {\n node {\n id\n title\n body\n updatedAt\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "5c0e074ce562f24bd841cc750f492988";
export default node;

View File

@@ -13,6 +13,7 @@ import { MainLayout } from "#/layouts/MainLayout";
import { DocumentsPage } from "#/pages/DocumentsPage";
import { OverviewPage } from "#/pages/OverviewPage";
import { SubprocessorsPage } from "#/pages/SubprocessorsPage";
import { currentTrustUpdatesQuery, UpdatesPage } from "#/pages/UpdatesPage";
import {
currentTrustDocumentsQuery,
currentTrustGraphQuery,
@@ -112,6 +113,25 @@ const routes = [
},
],
},
{
path: "/updates",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustGraphQuery, {}),
),
Component: withQueryRef(MainLayout),
Fallback: MainSkeleton,
ErrorBoundary: RootErrorBoundary,
children: [
{
path: "",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustUpdatesQuery, {}),
),
Fallback: TabSkeleton,
Component: withQueryRef(UpdatesPage),
},
],
},
// Fallback URL to the NotFound Page
{
path: "*",