Send mailing list emails
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
108
apps/trust/src/pages/UpdatesPage.tsx
Normal file
108
apps/trust/src/pages/UpdatesPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
146
apps/trust/src/pages/__generated__/UpdatesPageQuery.graphql.ts
generated
Normal file
146
apps/trust/src/pages/__generated__/UpdatesPageQuery.graphql.ts
generated
Normal 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;
|
||||
@@ -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: "*",
|
||||
|
||||
Reference in New Issue
Block a user