Remove orphaned apps/trust frontend
Compliance portal already embeds and serves the public SPA, so the old @probo/trust app was unused. Drop its Makefile/CI stubs, Relay project, CORS port, and the helper that only it imported. Signed-off-by: Émile Ré <emile@probo.com>
@@ -18,7 +18,7 @@
|
||||
# ── Base URL & ports ──────────────────────────────────────────────────
|
||||
# PROBOD_BASE_URL=http://localhost:8080
|
||||
# PROBOD_API_ADDR=:8080
|
||||
# PROBOD_API_CORS_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5173,http://localhost:5174,http://localhost:5175
|
||||
# PROBOD_API_CORS_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5173,http://localhost:5174
|
||||
# PROBOD_TRUST_CENTER_HTTP_ADDR=:10080
|
||||
# PROBOD_TRUST_CENTER_HTTPS_ADDR=:443
|
||||
# PROBOD_TRUST_CENTER_BASE_DOMAIN=probopage.localhost
|
||||
|
||||
11
.github/workflows/make.yaml
vendored
@@ -245,10 +245,9 @@ jobs:
|
||||
node: "false"
|
||||
- name: "Create placeholder dist files"
|
||||
run: |
|
||||
mkdir -p apps/console/dist apps/compliance-portal/dist apps/trust/dist packages/emails/dist
|
||||
mkdir -p apps/console/dist apps/compliance-portal/dist packages/emails/dist
|
||||
echo dev-server > apps/console/dist/index.html
|
||||
echo dev-server > apps/compliance-portal/dist/index.html
|
||||
echo dev-server > apps/trust/dist/index.html
|
||||
echo dev-server > packages/emails/dist/placeholder
|
||||
- name: "Generate Go code"
|
||||
run: |
|
||||
@@ -310,10 +309,9 @@ jobs:
|
||||
- uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0
|
||||
- name: "Create placeholder dist files"
|
||||
run: |
|
||||
mkdir -p apps/console/dist apps/compliance-portal/dist apps/trust/dist packages/emails/dist
|
||||
mkdir -p apps/console/dist apps/compliance-portal/dist packages/emails/dist
|
||||
echo dev-server > apps/console/dist/index.html
|
||||
echo dev-server > apps/compliance-portal/dist/index.html
|
||||
echo dev-server > apps/trust/dist/index.html
|
||||
echo dev-server > packages/emails/dist/placeholder
|
||||
- name: "Generate Go code"
|
||||
run: |
|
||||
@@ -371,7 +369,7 @@ jobs:
|
||||
env:
|
||||
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
for dir in apps/console apps/trust apps/compliance-portal packages/ui packages/eslint-config; do
|
||||
for dir in apps/console apps/compliance-portal packages/ui packages/eslint-config; do
|
||||
(cd "$dir" && npx eslint . --concurrency 4 --format stylish 2>/dev/null) | \
|
||||
reviewdog -f=eslint -reporter=github-pr-review -filter-mode=nofilter -name="eslint ($dir)" || true
|
||||
done
|
||||
@@ -389,10 +387,9 @@ jobs:
|
||||
- uses: "./.github/actions/setup"
|
||||
- name: "Create placeholder dist files"
|
||||
run: |
|
||||
mkdir -p apps/console/dist apps/compliance-portal/dist apps/trust/dist
|
||||
mkdir -p apps/console/dist apps/compliance-portal/dist
|
||||
echo dev-server > apps/console/dist/index.html
|
||||
echo dev-server > apps/compliance-portal/dist/index.html
|
||||
echo dev-server > apps/trust/dist/index.html
|
||||
- run: "npm --workspace @probo/emails run build"
|
||||
- run: "make test"
|
||||
env:
|
||||
|
||||
@@ -72,7 +72,6 @@ GENERATED= pkg/server/api/connect/v1/schema/schema.go \
|
||||
|
||||
EMBEDDED= apps/console/dist/index.html \
|
||||
apps/compliance-portal/dist/index.html \
|
||||
apps/trust/dist/index.html \
|
||||
@probo/emails
|
||||
|
||||
PROBOD_BIN_EXTRA_DEPS=
|
||||
@@ -138,7 +137,7 @@ go-lint: generate
|
||||
$(GOLINTCMD) run ./...
|
||||
|
||||
.PHONY: test
|
||||
test: generate apps/trust/dist/index.html
|
||||
test: generate
|
||||
test: CGO_ENABLED=1
|
||||
test: ## Run tests with race detection and coverage (usage: make test [MODULE=./pkg/some/module])
|
||||
$(GO_TEST) $(if $(MODULE),$(MODULE),$(shell $(GO) list ./... | grep -v /e2e/))
|
||||
@@ -211,7 +210,7 @@ cfg/dev.yaml: bin/probod-bootstrap $(CFG_DEV_OAUTH2_KEY) compose/step-ca/certs/r
|
||||
PROBOD_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes"; \
|
||||
PROBOD_AUTH_COOKIE_SECURE=false; \
|
||||
PROBOD_OAUTH2_SERVER_SIGNING_KEY="$$($(CAT) $(CFG_DEV_OAUTH2_KEY))"; \
|
||||
PROBOD_API_CORS_ALLOWED_ORIGINS="http://localhost:8080,http://localhost:5173,http://localhost:5174,http://localhost:5175"; \
|
||||
PROBOD_API_CORS_ALLOWED_ORIGINS="http://localhost:8080,http://localhost:5173,http://localhost:5174"; \
|
||||
PROBOD_PG_ADDR=localhost:5432; \
|
||||
PROBOD_PG_USERNAME=postgres; \
|
||||
PROBOD_PG_PASSWORD=postgres; \
|
||||
@@ -387,7 +386,7 @@ fmt-go: ## Format Go code
|
||||
clean: ## Clean the project (node_modules and build artifacts)
|
||||
$(RM) -rf bin/*
|
||||
$(RM) -rf node_modules
|
||||
$(RM) -rf apps/{console,compliance-portal,trust}/{dist,node_modules}
|
||||
$(RM) -rf apps/{console,compliance-portal}/{dist,node_modules}
|
||||
$(RM) -rf packages/emails/{dist,node_modules}
|
||||
$(RM) -rf sbom-docker.json sbom.json
|
||||
$(RM) -rf coverage.out coverage.html coverage-e2e.out coverage-e2e.html coverage-combined.out coverage-combined.html
|
||||
@@ -443,7 +442,7 @@ compose/keycloak/probo-realm.json: compose/keycloak/probo-realm.json.tmpl compos
|
||||
-e "s|PRIVATE_KEY_PLACEHOLDER|$$(awk 'NR==1 {printf "%s", $$0; next} {printf "\\\\n%s", $$0}' compose/keycloak/certs/private-key.pem)|g" \
|
||||
$@.tmpl > $@
|
||||
|
||||
apps/console/dist/index.html apps/compliance-portal/dist/index.html apps/trust/dist/index.html:
|
||||
apps/console/dist/index.html apps/compliance-portal/dist/index.html:
|
||||
$(MKDIR) $(dir $@)
|
||||
$(ECHO) dev-server > $@
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
VITE_API_URL=http://localhost:8080
|
||||
2
apps/trust/.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
__generated__/*.graphql linguist-generated
|
||||
__generated__/*.js linguist-generated
|
||||
26
apps/trust/.gitignore
vendored
@@ -1,26 +0,0 @@
|
||||
public/pdfjs
|
||||
# Logs
|
||||
old
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,2 +0,0 @@
|
||||
# Trust app (compliance portal)
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
(function () {
|
||||
var m = location.pathname.match(/^\/trust\/[^/]+/);
|
||||
var b = document.createElement("base");
|
||||
b.href = m ? m[0] + "/" : "/";
|
||||
document.head.appendChild(b);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- <link rel="apple-touch-icon-precomposed" sizes="57x57" href="/favicons/apple-touch-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/favicons/apple-touch-icon-114x114.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/favicons/apple-touch-icon-72x72.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/favicons/apple-touch-icon-144x144.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="/favicons/apple-touch-icon-60x60.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="/favicons/apple-touch-icon-120x120.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="/favicons/apple-touch-icon-76x76.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/favicons/apple-touch-icon-152x152.png" />
|
||||
<link rel="icon" type="image/png" href="/favicons/favicon-196x196.png" sizes="196x196" />
|
||||
<link rel="icon" type="image/png" href="/favicons/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/png" href="/favicons/favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="/favicons/favicon-16x16.png" sizes="16x16" />
|
||||
<link rel="icon" type="image/png" href="/favicons/favicon-128.png" sizes="128x128" /> -->
|
||||
<link rel="icon" id="favicon" href="{{if .FaviconURL}}{{.FaviconURL}}{{else}}/favicons/favicon.ico{{end}}">
|
||||
|
||||
<meta name="application-name" content=" " />
|
||||
<meta name="msapplication-TileColor" content="#FFFFFF" />
|
||||
<meta name="msapplication-TileImage" content="/favicons/mstile-144x144.png" />
|
||||
<meta name="msapplication-square70x70logo" content="/favicons/mstile-70x70.png" />
|
||||
<meta name="msapplication-square150x150logo" content="/favicons/mstile-150x150.png" />
|
||||
<meta name="msapplication-wide310x150logo" content="/favicons/mstile-310x150.png" />
|
||||
<meta name="msapplication-square310x310logo" content="/favicons/mstile-310x310.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{.Title}}</title>
|
||||
<meta name="description" content="{{.Description}}">
|
||||
<meta property="og:title" content="{{.Title}}">
|
||||
<meta property="og:description" content="{{.Description}}">
|
||||
<meta property="og:url" content="{{.OGURL}}">
|
||||
<meta property="og:type" content="website">
|
||||
</head>
|
||||
|
||||
<body class="text-txt-primary bg-level-0">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"name": "@probo/trust",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5175",
|
||||
"build": "tsc -b && vite build",
|
||||
"check": "tsc --noEmit -p tsconfig.app.json",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@probo/helpers": "1.0.0",
|
||||
"@probo/hooks": "1.0.0",
|
||||
"@probo/i18n": "1.0.0",
|
||||
"@probo/routes": "^1.0.0",
|
||||
"@probo/ui": "1.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-pdf": "^10.3.0",
|
||||
"react-relay": "^21.0.1",
|
||||
"react-router": "^8.1.0",
|
||||
"relay-runtime": "^21.0.1",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^8.0.1",
|
||||
"@rolldown/plugin-babel": "^0.2.3",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/node": "^26",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"babel-plugin-relay": "^21.0.1",
|
||||
"graphql": "^17.0.1",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "^8.1.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.4.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 625 B |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Toasts } from "@probo/ui";
|
||||
import { RouterProvider } from "react-router";
|
||||
|
||||
import { router } from "./routes";
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<>
|
||||
<RouterProvider router={router} />
|
||||
<Toasts />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { UnAuthenticatedError } from "@probo/relay";
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
FrameworkLogo,
|
||||
IconArrowLink,
|
||||
IconLock,
|
||||
IconMedal,
|
||||
Table,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { type PropsWithChildren } from "react";
|
||||
import { useFragment, useMutation } from "react-relay";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { AuditRow_requestAccessMutation } from "./__generated__/AuditRow_requestAccessMutation.graphql";
|
||||
import type { AuditRowFragment$key } from "./__generated__/AuditRowFragment.graphql";
|
||||
|
||||
const requestAccessMutation = graphql`
|
||||
mutation AuditRow_requestAccessMutation($input: RequestReportAccessInput!) {
|
||||
requestReportAccess(input: $input) {
|
||||
audit {
|
||||
reportFile {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const auditRowFragment = graphql`
|
||||
fragment AuditRowFragment on Audit {
|
||||
name
|
||||
reportFile {
|
||||
id
|
||||
alias
|
||||
isUserAuthorized
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
framework {
|
||||
id
|
||||
name
|
||||
lightLogo {
|
||||
downloadUrl
|
||||
}
|
||||
darkLogo {
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function AuditRow(props: { audit: AuditRowFragment$key }) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const [searchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const audit = useFragment(auditRowFragment, props.audit);
|
||||
const reportPath = audit.reportFile?.alias ?? audit.reportFile?.id;
|
||||
const hasRequested = audit.reportFile?.access?.status === "REQUESTED";
|
||||
|
||||
const [requestAccess, isRequestingAccess]
|
||||
= useMutation<AuditRow_requestAccessMutation>(requestAccessMutation);
|
||||
|
||||
const handleRequestAccess = () => {
|
||||
requestAccess({
|
||||
variables: {
|
||||
input: {
|
||||
reportId: audit.reportFile?.id ?? "",
|
||||
},
|
||||
},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(__("Cannot request access"), errors),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Access request submitted successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
searchParams.set("request-report-id", audit.reportFile?.id ?? "");
|
||||
const urlSearchParams = new URLSearchParams([[
|
||||
"continue",
|
||||
window.location.origin + location.pathname + "?" + searchParams.toString(),
|
||||
]]);
|
||||
void navigate(`/connect?${urlSearchParams.toString()}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message ?? __("Cannot request access"),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconMedal size={16} className="flex-none text-txt-tertiary" />
|
||||
{audit.name ?? audit.framework.name}
|
||||
</div>
|
||||
{audit.reportFile && (
|
||||
audit.reportFile.isUserAuthorized
|
||||
? (
|
||||
<Button
|
||||
className="w-full md:w-max"
|
||||
variant="secondary"
|
||||
icon={IconArrowLink}
|
||||
to={reportPath ? `/documents/${reportPath}` : undefined}
|
||||
>
|
||||
{__("View")}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
disabled={hasRequested || isRequestingAccess}
|
||||
className="w-full md:w-max"
|
||||
variant="secondary"
|
||||
icon={IconLock}
|
||||
onClick={handleRequestAccess}
|
||||
>
|
||||
{hasRequested ? __("Access requested") : __("Request access")}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AuditRowAvatar(props: { audit: AuditRowFragment$key }) {
|
||||
const audit = useFragment(auditRowFragment, props.audit);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuditDialog audit={props.audit}>
|
||||
<button
|
||||
className="block cursor-pointer aspect-square"
|
||||
title={`Logo ${audit.framework.name}`}
|
||||
>
|
||||
<div className="flex flex-col gap-2 items-center w-19">
|
||||
<FrameworkLogo
|
||||
className="size-19"
|
||||
lightLogoURL={audit.framework.lightLogo?.downloadUrl}
|
||||
darkLogoURL={audit.framework.darkLogo?.downloadUrl}
|
||||
name={audit.framework.name}
|
||||
/>
|
||||
<div className="txt-primary text-sm max-w-19 overflow-hidden min-w-0 whitespace-nowrap text-ellipsis">
|
||||
{audit.framework.name}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</AuditDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AuditDialog(
|
||||
props: PropsWithChildren<{ audit: AuditRowFragment$key; logo?: string }>,
|
||||
) {
|
||||
const audit = useFragment(auditRowFragment, props.audit);
|
||||
const location = useLocation();
|
||||
const { __ } = useTranslate();
|
||||
const items = [
|
||||
{
|
||||
label: __("Certifications"),
|
||||
to: location.pathname,
|
||||
},
|
||||
{
|
||||
label: audit.framework.name,
|
||||
to: location.pathname,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Dialog
|
||||
trigger={props.children}
|
||||
className="max-w-[500px]"
|
||||
title={<Breadcrumb items={items} />}
|
||||
>
|
||||
<DialogContent className="p-4 lg:p-8 space-y-6">
|
||||
<FrameworkLogo
|
||||
className="size-24 mx-auto"
|
||||
lightLogoURL={audit.framework.lightLogo?.downloadUrl}
|
||||
darkLogoURL={audit.framework.darkLogo?.downloadUrl}
|
||||
name={audit.framework.name}
|
||||
/>
|
||||
<h2 className="text-xl font-semibold mb-1">{audit.framework.name}</h2>
|
||||
<Table>
|
||||
<AuditRow audit={props.audit} />
|
||||
</Table>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { UnAuthenticatedError } from "@probo/relay";
|
||||
import {
|
||||
Button,
|
||||
IconArrowLink,
|
||||
IconLock,
|
||||
IconPageTextLine,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useFragment, useMutation } from "react-relay";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { CompliancePortalFileRow_requestAccessMutation } from "./__generated__/CompliancePortalFileRow_requestAccessMutation.graphql";
|
||||
import type { CompliancePortalFileRowFragment$key } from "./__generated__/CompliancePortalFileRowFragment.graphql";
|
||||
|
||||
const requestAccessMutation = graphql`
|
||||
mutation CompliancePortalFileRow_requestAccessMutation(
|
||||
$input: RequestCompliancePortalFileAccessInput!
|
||||
) {
|
||||
requestCompliancePortalFileAccess(input: $input) {
|
||||
file {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const compliancePortalFileRowFragment = graphql`
|
||||
fragment CompliancePortalFileRowFragment on CompliancePortalFile {
|
||||
id
|
||||
alias
|
||||
name
|
||||
isUserAuthorized
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function CompliancePortalFileRow(props: {
|
||||
file: CompliancePortalFileRowFragment$key;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const file = useFragment(compliancePortalFileRowFragment, props.file);
|
||||
const filePath = file.alias ?? file.id;
|
||||
const hasRequested = file.access?.status === "REQUESTED";
|
||||
|
||||
const [requestAccess, isRequestingAccess]
|
||||
= useMutation<CompliancePortalFileRow_requestAccessMutation>(
|
||||
requestAccessMutation,
|
||||
);
|
||||
|
||||
const handleRequestAccess = () => {
|
||||
requestAccess({
|
||||
variables: {
|
||||
input: {
|
||||
compliancePortalFileId: file.id,
|
||||
},
|
||||
},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(__("Cannot request access"), errors),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Access request submitted successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
searchParams.set("request-file-id", file.id);
|
||||
const urlSearchParams = new URLSearchParams([[
|
||||
"continue",
|
||||
window.location.origin + location.pathname + "?" + searchParams.toString(),
|
||||
]]);
|
||||
void navigate(`/connect?${urlSearchParams.toString()}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message ?? __("Cannot request access"),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconPageTextLine size={16} className=" flex-none text-txt-tertiary" />
|
||||
{file.name}
|
||||
</div>
|
||||
{file.isUserAuthorized
|
||||
? (
|
||||
<Button
|
||||
className="w-full md:w-max"
|
||||
variant="secondary"
|
||||
icon={IconArrowLink}
|
||||
onClick={() => void navigate(`/documents/${filePath}`)}
|
||||
>
|
||||
{__("View")}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
disabled={hasRequested || isRequestingAccess}
|
||||
className="w-full md:w-max"
|
||||
variant="secondary"
|
||||
icon={IconLock}
|
||||
onClick={handleRequestAccess}
|
||||
>
|
||||
{hasRequested ? __("Access requested") : __("Request access")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
FullNameRequiredError,
|
||||
NDASignatureRequiredError,
|
||||
UnAuthenticatedError,
|
||||
} from "@probo/relay";
|
||||
import { Button, IconChevronLeft, IconPageCross } from "@probo/ui";
|
||||
import { Link, Navigate, useLocation, useRouteError } from "react-router";
|
||||
|
||||
export function DocumentPageErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
const location = useLocation();
|
||||
const { __ } = useTranslate();
|
||||
|
||||
const search = new URLSearchParams();
|
||||
|
||||
if (location.pathname !== "/" || location.search !== "") {
|
||||
search.set("continue", window.location.href);
|
||||
}
|
||||
|
||||
const queryString = search.toString();
|
||||
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={{
|
||||
pathname: "/connect",
|
||||
search: queryString ? "?" + queryString : "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof FullNameRequiredError) {
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={{
|
||||
pathname: "/full-name",
|
||||
search: queryString ? "?" + queryString : "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof NDASignatureRequiredError) {
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={{
|
||||
pathname: "/nda",
|
||||
search: queryString ? "?" + queryString : "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-level-2">
|
||||
<header className="flex items-center h-12 gap-3 border-b border-border-solid px-4 flex-none bg-level-1">
|
||||
<Link
|
||||
to="/documents"
|
||||
className="size-8 grid place-items-center hover:bg-secondary-hover rounded-sm transition-all"
|
||||
>
|
||||
<IconChevronLeft size={16} />
|
||||
</Link>
|
||||
</header>
|
||||
<main className="flex-1 min-h-0 flex items-center justify-center">
|
||||
<div className="text-center max-w-sm">
|
||||
<IconPageCross size={32} className="mx-auto text-txt-tertiary mb-4" />
|
||||
<h2 className="text-lg font-medium mb-2">
|
||||
{__("Document not found")}
|
||||
</h2>
|
||||
<p className="text-sm text-txt-secondary mb-6">
|
||||
{__("The document you are looking for does not exist or has been removed.")}
|
||||
</p>
|
||||
<Button variant="secondary" asChild className="inline-flex">
|
||||
<Link to="/documents">
|
||||
{__("Back to documents")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { UnAuthenticatedError } from "@probo/relay";
|
||||
import {
|
||||
Button,
|
||||
IconArrowLink,
|
||||
IconLock,
|
||||
IconPageTextLine,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useFragment, useMutation } from "react-relay";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { DocumentRow_requestAccessMutation } from "./__generated__/DocumentRow_requestAccessMutation.graphql";
|
||||
import type { DocumentRowFragment$key } from "./__generated__/DocumentRowFragment.graphql";
|
||||
|
||||
const requestAccessMutation = graphql`
|
||||
mutation DocumentRow_requestAccessMutation(
|
||||
$input: RequestDocumentAccessInput!
|
||||
) {
|
||||
requestDocumentAccess(input: $input) {
|
||||
document {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const documentRowFragment = graphql`
|
||||
fragment DocumentRowFragment on Document {
|
||||
id
|
||||
alias
|
||||
title
|
||||
isUserAuthorized
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function DocumentRow(props: { document: DocumentRowFragment$key }) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const document = useFragment(documentRowFragment, props.document);
|
||||
const documentPath = document.alias ?? document.id;
|
||||
const hasRequested = document.access?.status === "REQUESTED";
|
||||
|
||||
const [requestAccess, isRequestingAccess]
|
||||
= useMutation<DocumentRow_requestAccessMutation>(requestAccessMutation);
|
||||
|
||||
const handleRequestAccess = () => {
|
||||
requestAccess({
|
||||
variables: {
|
||||
input: {
|
||||
documentId: document.id,
|
||||
},
|
||||
},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(__("Cannot request access"), errors),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Access request submitted successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
searchParams.set("request-document-id", document.id);
|
||||
const urlSearchParams = new URLSearchParams([[
|
||||
"continue",
|
||||
window.location.origin + location.pathname + "?" + searchParams.toString(),
|
||||
]]);
|
||||
void navigate(`/connect?${urlSearchParams.toString()}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message ?? __("Cannot request access"),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconPageTextLine size={16} className=" flex-none text-txt-tertiary" />
|
||||
{document.title}
|
||||
</div>
|
||||
{document.isUserAuthorized
|
||||
? (
|
||||
<Button
|
||||
className="w-full md:w-max"
|
||||
variant="secondary"
|
||||
icon={IconArrowLink}
|
||||
onClick={() => void navigate(`/documents/${documentPath}`)}
|
||||
>
|
||||
{__("View")}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
disabled={hasRequested || isRequestingAccess}
|
||||
className="w-full md:w-max"
|
||||
variant="secondary"
|
||||
icon={IconLock}
|
||||
onClick={handleRequestAccess}
|
||||
>
|
||||
{hasRequested ? __("Access requested") : __("Request access")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { FrameworkLogo } from "@probo/ui";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { FrameworkBadgeFragment$key } from "./__generated__/FrameworkBadgeFragment.graphql";
|
||||
|
||||
const frameworkFragment = graphql`
|
||||
fragment FrameworkBadgeFragment on Framework {
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
id
|
||||
name
|
||||
lightLogo {
|
||||
downloadUrl
|
||||
}
|
||||
darkLogo {
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function FrameworkBadge(props: { framework: FrameworkBadgeFragment$key }) {
|
||||
const framework = useFragment(frameworkFragment, props.framework);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 items-center w-19">
|
||||
<FrameworkLogo
|
||||
className="size-19"
|
||||
lightLogoURL={framework.lightLogo?.downloadUrl}
|
||||
darkLogoURL={framework.darkLogo?.downloadUrl}
|
||||
name={framework.name}
|
||||
/>
|
||||
<div className="txt-primary text-xs max-w-19 min-w-0 text-center">
|
||||
{framework.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { detectSocialName, externalLinkProps, formatError } from "@probo/helpers";
|
||||
import { useSystemTheme } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { UnAuthenticatedError } from "@probo/relay";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
IconBell2,
|
||||
IconBlock,
|
||||
IconLock,
|
||||
IconMedal,
|
||||
SocialIcon,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { type PropsWithChildren } from "react";
|
||||
import { useMutation } from "react-relay";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { CompliancePortalGraphCurrentQuery$data } from "#/queries/__generated__/CompliancePortalGraphCurrentQuery.graphql";
|
||||
|
||||
import type { OrganizationSidebar_requestAllAccessesMutation } from "./__generated__/OrganizationSidebar_requestAllAccessesMutation.graphql";
|
||||
import type { OrganizationSidebar_subscribeToMailingListMutation } from "./__generated__/OrganizationSidebar_subscribeToMailingListMutation.graphql";
|
||||
import type { OrganizationSidebar_unsubscribeFromMailingListMutation } from "./__generated__/OrganizationSidebar_unsubscribeFromMailingListMutation.graphql";
|
||||
import { FrameworkBadge } from "./FrameworkBadge";
|
||||
|
||||
const requestAllAccessesMutation = graphql`
|
||||
mutation OrganizationSidebar_requestAllAccessesMutation {
|
||||
requestAllAccesses {
|
||||
compliancePortalAccess {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const subscribeToMailingListMutation = graphql`
|
||||
mutation OrganizationSidebar_subscribeToMailingListMutation {
|
||||
subscribeToMailingList {
|
||||
subscription {
|
||||
id
|
||||
email
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const unsubscribeFromMailingListMutation = graphql`
|
||||
mutation OrganizationSidebar_unsubscribeFromMailingListMutation {
|
||||
unsubscribeFromMailingList {
|
||||
deletedMailingListSubscriberId @deleteRecord
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function OrganizationSidebar({
|
||||
compliancePortal,
|
||||
isAuthenticated,
|
||||
}: {
|
||||
compliancePortal: CompliancePortalGraphCurrentQuery$data["currentCompliancePortal"];
|
||||
isAuthenticated: boolean;
|
||||
}) {
|
||||
const compliancePortalId = compliancePortal?.id;
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const theme = useSystemTheme();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const logoFileUrl = theme === "dark"
|
||||
? (compliancePortal?.darkLogo?.downloadUrl ?? compliancePortal?.logo?.downloadUrl)
|
||||
: compliancePortal?.logo?.downloadUrl;
|
||||
|
||||
const [requestAllAccesses, isRequestingAccess]
|
||||
= useMutation<OrganizationSidebar_requestAllAccessesMutation>(
|
||||
requestAllAccessesMutation,
|
||||
);
|
||||
|
||||
const [subscribeToMailingList, isSubscribing]
|
||||
= useMutation<OrganizationSidebar_subscribeToMailingListMutation>(
|
||||
subscribeToMailingListMutation,
|
||||
);
|
||||
|
||||
const [unsubscribeFromMailingList, isUnsubscribing]
|
||||
= useMutation<OrganizationSidebar_unsubscribeFromMailingListMutation>(
|
||||
unsubscribeFromMailingListMutation,
|
||||
);
|
||||
|
||||
const handleRequestAllAccesses = () => {
|
||||
requestAllAccesses({
|
||||
variables: {},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(__("Cannot request access"), errors),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Access request submitted successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
|
||||
window.location.href = location.pathname;
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
searchParams.set("request-all", "true");
|
||||
const urlSearchParams = new URLSearchParams([[
|
||||
"continue",
|
||||
window.location.origin + location.pathname + "?" + searchParams.toString(),
|
||||
]]);
|
||||
void navigate(`/connect?${urlSearchParams.toString()}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message ?? __("Cannot request access"),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubscribe = () => {
|
||||
subscribeToMailingList({
|
||||
variables: {},
|
||||
updater: (store, data) => {
|
||||
const subscription = data?.subscribeToMailingList?.subscription;
|
||||
if (!subscription?.id || !compliancePortalId) return;
|
||||
const compliancePortalRecord = store.get(compliancePortalId);
|
||||
if (!compliancePortalRecord) return;
|
||||
const subscriptionRecord = store.get(subscription.id);
|
||||
if (!subscriptionRecord) return;
|
||||
compliancePortalRecord.setLinkedRecord(subscriptionRecord, "viewerSubscription");
|
||||
},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(__("Cannot subscribe"), errors),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Subscribed"),
|
||||
description: __("You will be notified of security updates."),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message ?? __("Cannot subscribe"),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleUnsubscribe = () => {
|
||||
unsubscribeFromMailingList({
|
||||
variables: {},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(__("Cannot unsubscribe"), errors),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Unsubscribed"),
|
||||
description: __("You will no longer receive security updates."),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message ?? __("Cannot unsubscribe"),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!compliancePortal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6 relative overflow-hidden border-b border-border-low isolate">
|
||||
<div className="h-21 bg-[#044E4114] absolute top-0 left-0 right-0 -z-1"></div>
|
||||
{logoFileUrl
|
||||
? (
|
||||
<img
|
||||
alt=""
|
||||
src={logoFileUrl}
|
||||
className="size-24 rounded-2xl border border-border-mid shadow-mid bg-level-1"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className="size-24 rounded-2xl border border-border-mid shadow-mid bg-level-1" />
|
||||
)}
|
||||
<h1 className="text-2xl mt-6">{compliancePortal.entityName}</h1>
|
||||
<p className="text-sm text-txt-secondary mt-1">
|
||||
{compliancePortal.description}
|
||||
</p>
|
||||
|
||||
<hr className="my-6 -mx-6 h-px bg-border-low border-none" />
|
||||
|
||||
{/* Business information */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xs text-txt-secondary flex gap-1 items-center">
|
||||
<IconBlock size={16} />
|
||||
{__("Business information")}
|
||||
</h2>
|
||||
{compliancePortal.websiteUrl && (
|
||||
<BusinessInfo label={__("Website")}>
|
||||
<a {...externalLinkProps(compliancePortal.websiteUrl)}>
|
||||
<span className="text-txt-info hover:underline ">
|
||||
{new URL(compliancePortal.websiteUrl).host}
|
||||
</span>
|
||||
</a>
|
||||
</BusinessInfo>
|
||||
)}
|
||||
{compliancePortal.email && (
|
||||
<BusinessInfo label={__("Contact")}>
|
||||
<a href={`mailto:${compliancePortal.email}`}>
|
||||
<span className="text-txt-info hover:underline ">
|
||||
{compliancePortal.email}
|
||||
</span>
|
||||
</a>
|
||||
</BusinessInfo>
|
||||
)}
|
||||
{compliancePortal.headquarterAddress && (
|
||||
<BusinessInfo label={__("HQ address")}>
|
||||
{compliancePortal.headquarterAddress}
|
||||
</BusinessInfo>
|
||||
)}
|
||||
{compliancePortal.customLinks.edges.length > 0 && (
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2">
|
||||
{compliancePortal.customLinks.edges.map(({ node }) => (
|
||||
<a
|
||||
key={node.id}
|
||||
{...externalLinkProps(node.url)}
|
||||
className="flex items-center gap-1.5 text-sm text-txt-secondary hover:text-txt-primary transition-colors"
|
||||
>
|
||||
<SocialIcon socialName={detectSocialName(node.url)} size={14} className="shrink-0" />
|
||||
<span>{node.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr className="my-6 -mx-6 h-px bg-border-low border-none" />
|
||||
|
||||
{/* Certifications */}
|
||||
{compliancePortal.complianceFrameworks.edges.length > 0 && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xs text-txt-secondary flex gap-1 items-center">
|
||||
<IconMedal size={16} />
|
||||
{__("Frameworks")}
|
||||
</h2>
|
||||
<div
|
||||
className="grid grid-cols-4 gap-4"
|
||||
style={{
|
||||
gridTemplateColumns: "repeat(auto-fit, 75px",
|
||||
}}
|
||||
>
|
||||
{compliancePortal.complianceFrameworks.edges.map(edge => (
|
||||
<FrameworkBadge key={edge.node.id} framework={edge.node.framework} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="my-6 -mx-6 h-px bg-border-low border-none" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
disabled={isRequestingAccess}
|
||||
variant="primary"
|
||||
icon={IconLock}
|
||||
className="w-full h-10"
|
||||
onClick={handleRequestAllAccesses}
|
||||
>
|
||||
{__("Request access")}
|
||||
</Button>
|
||||
{isAuthenticated && (
|
||||
compliancePortal.viewerSubscription
|
||||
? (
|
||||
<Button
|
||||
disabled={isUnsubscribing}
|
||||
variant="secondary"
|
||||
icon={IconBell2}
|
||||
className="w-full h-10"
|
||||
onClick={handleUnsubscribe}
|
||||
>
|
||||
{__("Unsubscribe from updates")}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
disabled={isSubscribing}
|
||||
variant="secondary"
|
||||
icon={IconBell2}
|
||||
className="w-full h-10"
|
||||
onClick={handleSubscribe}
|
||||
>
|
||||
{__("Subscribe to updates")}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function BusinessInfo({
|
||||
children,
|
||||
label,
|
||||
}: PropsWithChildren<{ label: string }>) {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-xs text-txt-secondary">{label}</div>
|
||||
<div className="text-sm">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import "react-pdf/dist/Page/TextLayer.css";
|
||||
import "react-pdf/dist/Page/AnnotationLayer.css";
|
||||
|
||||
import { times } from "@probo/helpers";
|
||||
import {
|
||||
IconArrowInbox,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconPlusLarge,
|
||||
Spinner,
|
||||
} from "@probo/ui";
|
||||
import { IconMinusLarge } from "@probo/ui/src/Atoms/Icons/IconMinusLarge";
|
||||
import { type ComponentProps, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Document, Page, pdfjs } from "react-pdf";
|
||||
|
||||
// Worker for PDF.js
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
|
||||
|
||||
const btnClass
|
||||
= "size-8 grid place-items-center hover:bg-secondary-hover cursor-pointer rounded-sm disabled:opacity-30 transition-all";
|
||||
|
||||
export function PDFPreview({ src, name }: { src: string; name?: string }) {
|
||||
const [numPages, setNumPages] = useState(0);
|
||||
const [scale, setScale] = useState(1.0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const documentRef: ComponentProps<typeof Document>["ref"] = useRef(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onDocumentLoadSuccess: ComponentProps<
|
||||
typeof Document
|
||||
>["onLoadSuccess"] = (document) => {
|
||||
setNumPages(document.numPages);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const zoomFactor = (factor: number) => () => {
|
||||
setScale(scale * factor);
|
||||
};
|
||||
|
||||
const currentPageRef = useRef(currentPage);
|
||||
useEffect(() => {
|
||||
currentPageRef.current = currentPage;
|
||||
}, [currentPage]);
|
||||
|
||||
const movePage = useCallback((direction: 1 | -1) => {
|
||||
const prev = currentPageRef.current;
|
||||
if (prev === 1 && direction === -1) {
|
||||
return;
|
||||
}
|
||||
const newPage = prev + direction;
|
||||
const page = documentRef.current?.pages.current[newPage - 1];
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
page.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
inline: "center",
|
||||
});
|
||||
setCurrentPage(newPage);
|
||||
}, []);
|
||||
|
||||
const resolveCurrentPage = useCallback(() => {
|
||||
if (!wrapperRef.current) {
|
||||
return;
|
||||
}
|
||||
const pages = documentRef.current?.pages.current;
|
||||
if (!pages?.length) {
|
||||
return;
|
||||
}
|
||||
const parentRect = wrapperRef.current.getBoundingClientRect();
|
||||
const parentMiddleY = parentRect.top + parentRect.height / 2;
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const childRect = pages[i].getBoundingClientRect();
|
||||
if (childRect.top <= parentMiddleY && childRect.bottom >= parentMiddleY) {
|
||||
setCurrentPage(i + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDownload = () => {
|
||||
const link = document.createElement("a");
|
||||
link.href = src;
|
||||
link.download = name || "document.pdf";
|
||||
link.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[max-content_1fr] h-full bg-subtle">
|
||||
{/* Custom Zoom Controls */}
|
||||
<nav className="flex-none flex items-center gap-2 bg-level-1 py-3 text-sm pl-4 pr-3 text-txt-primary">
|
||||
<div>{name}</div>
|
||||
<div className="mx-auto flex gap-1 items-center">
|
||||
<button
|
||||
onClick={() => movePage(-1)}
|
||||
className={btnClass}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<IconChevronLeft size={16} />
|
||||
</button>
|
||||
<div>
|
||||
{currentPage}
|
||||
{" "}
|
||||
/
|
||||
{numPages}
|
||||
</div>
|
||||
<button onClick={() => movePage(1)} className={btnClass}>
|
||||
<IconChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={zoomFactor(0.8)} className={btnClass}>
|
||||
<IconMinusLarge size={16} />
|
||||
</button>
|
||||
<button onClick={zoomFactor(1.2)} className={btnClass}>
|
||||
<IconPlusLarge size={16} />
|
||||
</button>
|
||||
<button onClick={handleDownload} className={btnClass}>
|
||||
<IconArrowInbox size={16} />
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* PDF Document */}
|
||||
<div
|
||||
className="overflow-auto scroll-p-6"
|
||||
onScrollEnd={resolveCurrentPage}
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<Document
|
||||
file={src}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
className="flex flex-col gap-4 py-10"
|
||||
ref={documentRef}
|
||||
>
|
||||
{numPages === 0 && <Spinner className="mx-auto" />}
|
||||
{times(numPages, index => (
|
||||
<Page
|
||||
className="w-max h-max mx-auto shadow-mid"
|
||||
key={index.toString()}
|
||||
pageNumber={index + 1}
|
||||
scale={scale} // Apply zoom via scale prop
|
||||
/>
|
||||
))}
|
||||
</Document>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
ErrorDetailMessage,
|
||||
ErrorDetails,
|
||||
ErrorLayout,
|
||||
} from "@probo/ui";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Link, useLocation, useRouteError } from "react-router";
|
||||
|
||||
type Props = {
|
||||
resetErrorBoundary?: () => void;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||
const routeError = useRouteError();
|
||||
const error = routeError ?? propsError;
|
||||
const { __ } = useTranslate();
|
||||
const location = useLocation();
|
||||
const baseLocation = useRef(location);
|
||||
|
||||
const isFullPage = Boolean(routeError ?? propsError);
|
||||
|
||||
// Reset error boundary on page change
|
||||
useEffect(() => {
|
||||
if (
|
||||
location.pathname !== baseLocation.current.pathname
|
||||
&& resetErrorBoundary
|
||||
) {
|
||||
resetErrorBoundary();
|
||||
}
|
||||
}, [location, resetErrorBoundary]);
|
||||
|
||||
const actions = (
|
||||
<Button asChild>
|
||||
<Link to="/">{__("Go home")}</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const layoutProps = {
|
||||
fullPage: isFullPage,
|
||||
showLogo: isFullPage,
|
||||
actions,
|
||||
};
|
||||
|
||||
if (!error) {
|
||||
return (
|
||||
<ErrorLayout
|
||||
{...layoutProps}
|
||||
title={__("Page not found")}
|
||||
description={__("The page you are looking for does not exist.")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error
|
||||
&& error.message
|
||||
.toLowerCase()
|
||||
.match(/(token|expired|invalid|401|unauthorized)/)
|
||||
) {
|
||||
const isExpiredToken = error.message.toLowerCase().includes("expired");
|
||||
const title = isExpiredToken
|
||||
? __("Expired token")
|
||||
: __("Invalid Access Link");
|
||||
const description = isExpiredToken
|
||||
? __(
|
||||
"This access link has expired. Compliance page access links are valid for 7 days for security reasons.",
|
||||
)
|
||||
: __(
|
||||
"This access link is not valid. It may have been revoked or the link might be incorrect.",
|
||||
);
|
||||
return (
|
||||
<ErrorLayout
|
||||
{...layoutProps}
|
||||
title={title}
|
||||
description={description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorLayout
|
||||
{...layoutProps}
|
||||
title={__("Something went wrong")}
|
||||
description={__("We hit an unexpected error. Head back home to continue.")}
|
||||
>
|
||||
{error instanceof Error && (
|
||||
<ErrorDetails summary={__("Technical details")}>
|
||||
<ErrorDetailMessage>{error.message}</ErrorDetailMessage>
|
||||
</ErrorDetails>
|
||||
)}
|
||||
</ErrorLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { FullNameRequiredError, NDASignatureRequiredError, UnAuthenticatedError } from "@probo/relay";
|
||||
import { Navigate, useLocation, useRouteError } from "react-router";
|
||||
|
||||
import { PageError } from "./PageError";
|
||||
|
||||
export function RootErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
const location = useLocation();
|
||||
|
||||
const search = new URLSearchParams();
|
||||
|
||||
if (location.pathname !== "/" || location.search !== "") {
|
||||
search.set("continue", window.location.href);
|
||||
}
|
||||
|
||||
const queryString = search.toString();
|
||||
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={{
|
||||
pathname: "/connect",
|
||||
search: queryString ? "?" + queryString : "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof FullNameRequiredError) {
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={{
|
||||
pathname: "/full-name",
|
||||
search: queryString ? "?" + queryString : "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof NDASignatureRequiredError) {
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={{
|
||||
pathname: "/nda",
|
||||
search: queryString ? "?" + queryString : "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <PageError error={error instanceof Error ? error : new Error("unknown error")} />;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export function RowHeader({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div className="bg-subtle text-xs font-medium text-txt-tertiary uppercase ">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export function Rows({
|
||||
children,
|
||||
className,
|
||||
}: PropsWithChildren<{ className?: string }>) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"*:first:rounded-t-lg *:last:rounded-b-lg *:px-6 *:py-3 *:-mt-[1px] *:border *:border-border-solid",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { getCompliancePortalUrl } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Skeleton, TabLink, Tabs } from "@probo/ui";
|
||||
|
||||
import { TabSkeleton } from "./TabSkeleton";
|
||||
|
||||
export function MainSkeleton() {
|
||||
const { __ } = useTranslate();
|
||||
return (
|
||||
<div className="grid grid-cols-1 max-w-[1280px] mx-4 pt-6 gap-4 lg:mx-auto lg:gap-10 lg:pt-20 lg:grid-cols-[400px_1fr] ">
|
||||
<Skeleton className="w-full h-300" />
|
||||
<main>
|
||||
<Tabs className="mb-8">
|
||||
<TabLink to={getCompliancePortalUrl("overview")}>{__("Overview")}</TabLink>
|
||||
<TabLink to={getCompliancePortalUrl("documents")}>{__("Documents")}</TabLink>
|
||||
<TabLink to={getCompliancePortalUrl("subprocessors")}>
|
||||
{__("Subprocessors")}
|
||||
</TabLink>
|
||||
<TabLink to={getCompliancePortalUrl("updates")}>{__("Updates")}</TabLink>
|
||||
</Tabs>
|
||||
<TabSkeleton />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Skeleton } from "@probo/ui";
|
||||
|
||||
export function TabSkeleton() {
|
||||
return (
|
||||
<div className="h-64">
|
||||
<Skeleton className="h-6 w-[110px] mb-1" />
|
||||
<Skeleton className="h-6 w-[250px] mb-4" />
|
||||
<Skeleton className="w-full h-[180px]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { faviconUrl, getCountryName } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { IconPin } from "@probo/ui";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { SubprocessorRowFragment$key } from "./__generated__/SubprocessorRowFragment.graphql";
|
||||
|
||||
const subprocessorRowFragment = graphql`
|
||||
fragment SubprocessorRowFragment on Subprocessor {
|
||||
name
|
||||
description
|
||||
websiteUrl
|
||||
countries
|
||||
}
|
||||
`;
|
||||
|
||||
export function SubprocessorRow(props: { subprocessor: SubprocessorRowFragment$key; hasAnyCountries?: boolean }) {
|
||||
const subprocessor = useFragment(subprocessorRowFragment, props.subprocessor);
|
||||
const logo = faviconUrl(subprocessor.websiteUrl);
|
||||
const { __ } = useTranslate();
|
||||
|
||||
return (
|
||||
<div className="flex text-sm leading-tight gap-6 items-center">
|
||||
{logo
|
||||
? (
|
||||
<img
|
||||
src={logo}
|
||||
className="size-8 flex-none rounded-lg"
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className="size-8 flex-none rounded-lg" />
|
||||
)}
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<span className="text-sm">{subprocessor.name}</span>
|
||||
<div className="text-xs text-txt-secondary w-full">{subprocessor.description}</div>
|
||||
{props.hasAnyCountries
|
||||
&& (
|
||||
<div className="text-xs flex gap-1 items-start text-txt-quaternary">
|
||||
{subprocessor.countries.length > 0 && (
|
||||
<>
|
||||
<IconPin size={16} className="flex-none" />
|
||||
<span>
|
||||
{subprocessor.countries
|
||||
.map(country => getCountryName(__, country))
|
||||
.join(", ")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
export function documentTypeLabel(type: string, __: (s: string) => string) {
|
||||
switch (type) {
|
||||
case "POLICY":
|
||||
return __("Policy");
|
||||
case "GOVERNANCE":
|
||||
return __("Governance");
|
||||
case "PROCEDURE":
|
||||
return __("Procedure");
|
||||
case "PLAN":
|
||||
return __("Plan");
|
||||
case "REGISTER":
|
||||
return __("Register");
|
||||
case "RECORD":
|
||||
return __("Record");
|
||||
case "REPORT":
|
||||
return __("Report");
|
||||
case "TEMPLATE":
|
||||
return __("Template");
|
||||
default:
|
||||
return __("Other");
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useContext } from "react";
|
||||
|
||||
import { CompliancePortalContext } from "#/providers/CompliancePortalProvider";
|
||||
|
||||
export function useCompliancePortal(): {
|
||||
id: string;
|
||||
entityName: string;
|
||||
} {
|
||||
const context = useContext(CompliancePortalContext);
|
||||
if (!context) {
|
||||
throw new Error("useCompliancePortal must be used within a CompliancePortalProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { type FieldValues, useForm, type UseFormReturn } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
|
||||
export function useFormWithSchema<T extends z.ZodType<FieldValues, FieldValues>>(
|
||||
schema: T,
|
||||
options: Parameters<typeof useForm<z.input<T>, unknown, z.output<T>>>[0],
|
||||
): UseFormReturn<z.input<T>, unknown, z.output<T>> {
|
||||
return useForm<z.input<T>, unknown, z.output<T>>({
|
||||
...options,
|
||||
resolver: standardSchemaResolver(schema),
|
||||
});
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useToast } from "@probo/ui";
|
||||
import { useCallback } from "react";
|
||||
import { useMutation, type UseMutationConfig } from "react-relay";
|
||||
import type { GraphQLTaggedNode, MutationParameters } from "relay-runtime";
|
||||
|
||||
/**
|
||||
* A decorated useMutation hook that emits toast notifications on success or error.
|
||||
*/
|
||||
export function useMutationWithToasts<T extends MutationParameters>(
|
||||
query: GraphQLTaggedNode,
|
||||
baseOptions?: {
|
||||
onSuccess?: (response: T["response"]) => void;
|
||||
errorMessage?: string;
|
||||
},
|
||||
) {
|
||||
const [mutate, isLoading] = useMutation<T>(query);
|
||||
const { toast } = useToast();
|
||||
const { __ } = useTranslate();
|
||||
const mutateWithToast = useCallback(
|
||||
(
|
||||
queryOptions: UseMutationConfig<T> & {
|
||||
onSuccess?: (response: T["response"]) => void;
|
||||
errorMessage?: string;
|
||||
},
|
||||
) => {
|
||||
const options = { ...baseOptions, ...queryOptions };
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
mutate({
|
||||
...queryOptions,
|
||||
onCompleted: (response, error) => {
|
||||
options.onCompleted?.(response, error);
|
||||
if (error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description:
|
||||
options.errorMessage
|
||||
?? __("Failed to commit this operation."),
|
||||
variant: "error",
|
||||
});
|
||||
reject(error instanceof Error ? error : new Error(__("Failed to commit this operation.")));
|
||||
return;
|
||||
}
|
||||
options.onSuccess?.(response);
|
||||
resolve();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description:
|
||||
options.errorMessage ?? __("Failed to commit this operation."),
|
||||
variant: "error",
|
||||
});
|
||||
reject(error);
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
[mutate, toast, __, baseOptions],
|
||||
);
|
||||
|
||||
return [mutateWithToast, isLoading] as const;
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useToast } from "@probo/ui";
|
||||
import { useEffect } from "react";
|
||||
import { useMutation } from "react-relay";
|
||||
import { useLocation, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { useRequestAccessCallback_allMutation } from "./__generated__/useRequestAccessCallback_allMutation.graphql";
|
||||
import type { useRequestAccessCallback_documentMutation } from "./__generated__/useRequestAccessCallback_documentMutation.graphql";
|
||||
import type { useRequestAccessCallback_fileMutation } from "./__generated__/useRequestAccessCallback_fileMutation.graphql";
|
||||
import type { useRequestAccessCallback_reportMutation } from "./__generated__/useRequestAccessCallback_reportMutation.graphql";
|
||||
|
||||
const documentMutation = graphql`
|
||||
mutation useRequestAccessCallback_documentMutation(
|
||||
$input: RequestDocumentAccessInput!
|
||||
) {
|
||||
requestDocumentAccess(input: $input) {
|
||||
document {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const reportMutation = graphql`
|
||||
mutation useRequestAccessCallback_reportMutation(
|
||||
$input: RequestReportAccessInput!
|
||||
) {
|
||||
requestReportAccess(input: $input) {
|
||||
audit {
|
||||
reportFile {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const fileMutation = graphql`
|
||||
mutation useRequestAccessCallback_fileMutation(
|
||||
$input: RequestCompliancePortalFileAccessInput!
|
||||
) {
|
||||
requestCompliancePortalFileAccess(input: $input) {
|
||||
file {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const allMutation = graphql`
|
||||
mutation useRequestAccessCallback_allMutation {
|
||||
requestAllAccesses {
|
||||
compliancePortalAccess {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function errorToastArgs(__: (s: string) => string, error: GraphQLError | GraphQLError[]) {
|
||||
return {
|
||||
title: __("Error"),
|
||||
description: formatError(__("Cannot request access"), error),
|
||||
variant: "error" as const,
|
||||
};
|
||||
}
|
||||
|
||||
function successToastArgs(__: (s: string) => string) {
|
||||
return {
|
||||
title: __("Success"),
|
||||
description: __("Access request submitted successfully."),
|
||||
variant: "success" as const,
|
||||
};
|
||||
}
|
||||
|
||||
export function useRequestAccessCallback() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
|
||||
const documentId = searchParams.get("request-document-id");
|
||||
const reportId = searchParams.get("request-report-id");
|
||||
const fileId = searchParams.get("request-file-id");
|
||||
const all = searchParams.get("request-all");
|
||||
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [requestDocumentAccess] = useMutation<useRequestAccessCallback_documentMutation>(documentMutation);
|
||||
const [requestReportAccess] = useMutation<useRequestAccessCallback_reportMutation>(reportMutation);
|
||||
const [requestFileAccess] = useMutation<useRequestAccessCallback_fileMutation>(fileMutation);
|
||||
const [requestAll] = useMutation<useRequestAccessCallback_allMutation>(allMutation);
|
||||
|
||||
useEffect(() => {
|
||||
if (documentId) {
|
||||
searchParams.delete("request-document-id");
|
||||
void requestDocumentAccess({
|
||||
variables: {
|
||||
input: { documentId },
|
||||
},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast(errorToastArgs(__, errors));
|
||||
return;
|
||||
}
|
||||
|
||||
toast(successToastArgs(__));
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(errorToastArgs(__, error));
|
||||
},
|
||||
});
|
||||
setSearchParams(searchParams);
|
||||
} else if (reportId) {
|
||||
searchParams.delete("request-report-id");
|
||||
void requestReportAccess({
|
||||
variables: {
|
||||
input: { reportId },
|
||||
},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast(errorToastArgs(__, errors));
|
||||
return;
|
||||
}
|
||||
|
||||
toast(successToastArgs(__));
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(errorToastArgs(__, error));
|
||||
},
|
||||
});
|
||||
setSearchParams(searchParams);
|
||||
} else if (fileId) {
|
||||
searchParams.delete("request-file-id");
|
||||
void requestFileAccess({
|
||||
variables: {
|
||||
input: { compliancePortalFileId: fileId },
|
||||
},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast(errorToastArgs(__, errors));
|
||||
return;
|
||||
}
|
||||
|
||||
toast(successToastArgs(__));
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(errorToastArgs(__, error));
|
||||
},
|
||||
});
|
||||
setSearchParams(searchParams);
|
||||
} else if (all) {
|
||||
searchParams.delete("request-all");
|
||||
void requestAll({
|
||||
variables: {},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast(errorToastArgs(__, errors));
|
||||
return;
|
||||
}
|
||||
|
||||
toast(successToastArgs(__));
|
||||
window.location.href = window.location.origin + location.pathname;
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(errorToastArgs(__, error));
|
||||
},
|
||||
});
|
||||
setSearchParams(searchParams);
|
||||
}
|
||||
}, [
|
||||
documentId,
|
||||
reportId,
|
||||
fileId,
|
||||
all,
|
||||
__,
|
||||
requestDocumentAccess,
|
||||
requestReportAccess,
|
||||
requestFileAccess,
|
||||
requestAll,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
toast,
|
||||
location,
|
||||
]);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
export function useSafeContinueUrl(): URL {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const continueUrlParam = searchParams.get("continue");
|
||||
const fallback = window.location.origin + "/";
|
||||
|
||||
const safeContinueUrl = useMemo(() => {
|
||||
if (continueUrlParam) {
|
||||
let continueUrl: URL;
|
||||
try {
|
||||
continueUrl = new URL(continueUrlParam, window.location.origin);
|
||||
} catch {
|
||||
return new URL(fallback, window.location.origin);
|
||||
}
|
||||
if (
|
||||
continueUrl.origin === window.location.origin
|
||||
&& continueUrl.pathname.startsWith("/")
|
||||
) {
|
||||
return new URL(
|
||||
continueUrl.pathname + continueUrl.search,
|
||||
window.location.origin,
|
||||
);
|
||||
}
|
||||
return new URL(fallback, window.location.origin);
|
||||
}
|
||||
return new URL(fallback, window.location.origin);
|
||||
}, [continueUrlParam, fallback]);
|
||||
|
||||
return safeContinueUrl;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap");
|
||||
@import "tailwindcss";
|
||||
@import "@probo/ui/src/theme.css";
|
||||
@import "tw-animate-css";
|
||||
@source "../../../packages/ui/src";
|
||||
@source "../../../packages/helpers/src";
|
||||
|
||||
.react-pdf__Page__annotations.annotationLayer {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useFavicon, useSystemTheme } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Logo, TabLink, Tabs } from "@probo/ui";
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
import { OrganizationSidebar } from "#/components/OrganizationSidebar";
|
||||
import { useRequestAccessCallback } from "#/hooks/useRequestAccessCallback";
|
||||
import { CompliancePortalProvider } from "#/providers/CompliancePortalProvider";
|
||||
import type { CompliancePortalGraphCurrentQuery } from "#/queries/__generated__/CompliancePortalGraphCurrentQuery.graphql";
|
||||
import { currentCompliancePortalGraphQuery } from "#/queries/CompliancePortalGraph";
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<CompliancePortalGraphCurrentQuery>;
|
||||
};
|
||||
|
||||
export function MainLayout(props: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const data = usePreloadedQuery<CompliancePortalGraphCurrentQuery>(currentCompliancePortalGraphQuery, props.queryRef);
|
||||
const compliancePortal = data.currentCompliancePortal;
|
||||
const isAuthenticated = data.viewer != null;
|
||||
|
||||
const theme = useSystemTheme();
|
||||
|
||||
useFavicon(
|
||||
theme === "dark"
|
||||
? (compliancePortal?.darkLogo?.downloadUrl ?? compliancePortal?.logo?.downloadUrl)
|
||||
: compliancePortal?.logo?.downloadUrl,
|
||||
);
|
||||
useRequestAccessCallback();
|
||||
|
||||
return (
|
||||
<CompliancePortalProvider compliancePortal={compliancePortal}>
|
||||
<div className="grid grid-cols-1 max-w-[1280px] mx-4 pt-6 gap-4 lg:mx-auto lg:gap-10 lg:pt-20 lg:grid-cols-[400px_1fr] lg:items-start ">
|
||||
<OrganizationSidebar compliancePortal={compliancePortal} isAuthenticated={isAuthenticated} />
|
||||
<main>
|
||||
<Tabs className="mb-8">
|
||||
<TabLink to="/overview">{__("Overview")}</TabLink>
|
||||
<TabLink to="/documents">{__("Documents")}</TabLink>
|
||||
{compliancePortal.subprocessorInfo.totalCount > 0
|
||||
&& <TabLink to="/subprocessors">{__("Subprocessors")}</TabLink>}
|
||||
<TabLink to="/updates">{__("Updates")}</TabLink>
|
||||
</Tabs>
|
||||
<Outlet context={{ compliancePortal }} />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://www.probo.com/"
|
||||
className="flex gap-2 text-sm font-medium text-txt-tertiary items-center w-max mx-auto my-10"
|
||||
>
|
||||
{__("Powered by")}
|
||||
{" "}
|
||||
<Logo withPicto className="h-6" />
|
||||
</a>
|
||||
</CompliancePortalProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
import { App } from "./App";
|
||||
import { RelayProvider } from "./providers/RelayProviders";
|
||||
import { TranslatorProvider } from "./providers/TranslatorProvider";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RelayProvider>
|
||||
<TranslatorProvider>
|
||||
<App />
|
||||
</TranslatorProvider>
|
||||
</RelayProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
23
apps/trust/src/module.d.ts
vendored
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
declare module "https://cdn.jsdelivr.net/npm/pdfjs-dist@5.4.149/+esm" {
|
||||
export * from "pdfjs-dist";
|
||||
}
|
||||
@@ -1,502 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useSystemTheme } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { UnAuthenticatedError } from "@probo/relay";
|
||||
import {
|
||||
Button,
|
||||
IconArrowDown,
|
||||
IconChevronLeft,
|
||||
IconLock,
|
||||
Spinner,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
type PreloadedQuery,
|
||||
useMutation,
|
||||
usePreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import { PDFPreview } from "#/components/PDFPreview";
|
||||
|
||||
import type { DocumentPageExportCompliancePortalFileMutation } from "./__generated__/DocumentPageExportCompliancePortalFileMutation.graphql";
|
||||
import type { DocumentPageExportDocumentMutation } from "./__generated__/DocumentPageExportDocumentMutation.graphql";
|
||||
import type { DocumentPageExportReportMutation } from "./__generated__/DocumentPageExportReportMutation.graphql";
|
||||
import type { DocumentPageQuery as DocumentPageQueryType } from "./__generated__/DocumentPageQuery.graphql";
|
||||
import type { DocumentPageRequestCompliancePortalFileAccessMutation } from "./__generated__/DocumentPageRequestCompliancePortalFileAccessMutation.graphql";
|
||||
import type { DocumentPageRequestDocumentAccessMutation } from "./__generated__/DocumentPageRequestDocumentAccessMutation.graphql";
|
||||
import type { DocumentPageRequestReportAccessMutation } from "./__generated__/DocumentPageRequestReportAccessMutation.graphql";
|
||||
|
||||
export const documentPageQuery = graphql`
|
||||
query DocumentPageQuery($alias: String!) {
|
||||
currentCompliancePortal {
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
darkLogo {
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
aliasedNode(alias: $alias) @required(action: THROW) {
|
||||
__typename
|
||||
... on Document {
|
||||
id
|
||||
title
|
||||
isUserAuthorized
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
... on CompliancePortalFile {
|
||||
id
|
||||
name
|
||||
isUserAuthorized
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
... on AuditReport {
|
||||
id
|
||||
fileName
|
||||
isUserAuthorized
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportDocumentMutation = graphql`
|
||||
mutation DocumentPageExportDocumentMutation(
|
||||
$input: ExportDocumentPDFInput!
|
||||
) {
|
||||
exportDocumentPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportCompliancePortalFileMutation = graphql`
|
||||
mutation DocumentPageExportCompliancePortalFileMutation(
|
||||
$input: ExportCompliancePortalFileInput!
|
||||
) {
|
||||
exportCompliancePortalFile(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportReportMutation = graphql`
|
||||
mutation DocumentPageExportReportMutation(
|
||||
$input: ExportReportPDFInput!
|
||||
) {
|
||||
exportReportPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const requestDocumentAccessMutation = graphql`
|
||||
mutation DocumentPageRequestDocumentAccessMutation(
|
||||
$input: RequestDocumentAccessInput!
|
||||
) {
|
||||
requestDocumentAccess(input: $input) {
|
||||
document {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const requestCompliancePortalFileAccessMutation = graphql`
|
||||
mutation DocumentPageRequestCompliancePortalFileAccessMutation(
|
||||
$input: RequestCompliancePortalFileAccessInput!
|
||||
) {
|
||||
requestCompliancePortalFileAccess(input: $input) {
|
||||
file {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const requestReportAccessMutation = graphql`
|
||||
mutation DocumentPageRequestReportAccessMutation(
|
||||
$input: RequestReportAccessInput!
|
||||
) {
|
||||
requestReportAccess(input: $input) {
|
||||
audit {
|
||||
reportFile {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<DocumentPageQueryType>;
|
||||
};
|
||||
|
||||
function isPdfDataUri(dataUri: string): boolean {
|
||||
return dataUri.startsWith("data:application/pdf;");
|
||||
}
|
||||
|
||||
function extractBase64Data(dataUri: string): string {
|
||||
const commaIndex = dataUri.indexOf(",");
|
||||
if (commaIndex === -1) return dataUri;
|
||||
return dataUri.substring(commaIndex + 1);
|
||||
}
|
||||
|
||||
function extractMimeType(dataUri: string): string {
|
||||
const match = dataUri.match(/^data:([^;]+);/);
|
||||
return match?.[1] ?? "application/octet-stream";
|
||||
}
|
||||
|
||||
function getNodeTitle(node: DocumentPageQueryType["response"]["aliasedNode"]): string | undefined {
|
||||
switch (node.__typename) {
|
||||
case "Document":
|
||||
return node.title;
|
||||
case "CompliancePortalFile":
|
||||
return node.name;
|
||||
case "AuditReport":
|
||||
return node.fileName;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeId(node: DocumentPageQueryType["response"]["aliasedNode"]): string | undefined {
|
||||
switch (node.__typename) {
|
||||
case "Document":
|
||||
case "CompliancePortalFile":
|
||||
case "AuditReport":
|
||||
return node.id;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function DocumentPage({ queryRef }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const theme = useSystemTheme();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [pdfData, setPdfData] = useState<string | null>(null);
|
||||
const [fileData, setFileData] = useState<string | null>(null);
|
||||
const [exportError, setExportError] = useState<string | null>(null);
|
||||
|
||||
const data = usePreloadedQuery<DocumentPageQueryType>(documentPageQuery, queryRef);
|
||||
const compliancePortal = data.currentCompliancePortal;
|
||||
const node = data.aliasedNode;
|
||||
|
||||
if (
|
||||
node.__typename !== "Document"
|
||||
&& node.__typename !== "CompliancePortalFile"
|
||||
&& node.__typename !== "AuditReport"
|
||||
) {
|
||||
throw new Error(`Unexpected node type: ${node.__typename}`);
|
||||
}
|
||||
|
||||
const nodeTitle = getNodeTitle(node);
|
||||
const nodeId = getNodeId(node);
|
||||
|
||||
const logoFileUrl = theme === "dark"
|
||||
? (compliancePortal?.darkLogo?.downloadUrl ?? compliancePortal?.logo?.downloadUrl)
|
||||
: compliancePortal?.logo?.downloadUrl;
|
||||
|
||||
const [exportDocument, isExportingDocument]
|
||||
= useMutation<DocumentPageExportDocumentMutation>(exportDocumentMutation);
|
||||
const [exportFile, isExportingFile]
|
||||
= useMutation<DocumentPageExportCompliancePortalFileMutation>(exportCompliancePortalFileMutation);
|
||||
const [exportReport, isExportingReport]
|
||||
= useMutation<DocumentPageExportReportMutation>(exportReportMutation);
|
||||
const [requestAccess, isRequestingAccess]
|
||||
= useMutation<DocumentPageRequestDocumentAccessMutation>(requestDocumentAccessMutation);
|
||||
const [requestFileAccess, isRequestingFileAccess]
|
||||
= useMutation<DocumentPageRequestCompliancePortalFileAccessMutation>(requestCompliancePortalFileAccessMutation);
|
||||
const [requestReportAccess, isRequestingReportAccess]
|
||||
= useMutation<DocumentPageRequestReportAccessMutation>(requestReportAccessMutation);
|
||||
|
||||
const isExporting = isExportingDocument || isExportingFile || isExportingReport;
|
||||
|
||||
const [prevNodeId, setPrevNodeId] = useState(nodeId);
|
||||
if (prevNodeId !== nodeId) {
|
||||
setPrevNodeId(nodeId);
|
||||
setPdfData(null);
|
||||
setFileData(null);
|
||||
setExportError(null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!node.isUserAuthorized || pdfData || fileData || exportError) return;
|
||||
|
||||
const onError = (error: Error) => {
|
||||
setExportError(error.message ?? __("Cannot export document"));
|
||||
};
|
||||
|
||||
const onCompletedErrors = (errors: readonly { message: string }[] | null | undefined) => {
|
||||
if (errors?.length) {
|
||||
setExportError(formatError(__("Cannot export document"), [...errors]));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
switch (node.__typename) {
|
||||
case "Document":
|
||||
exportDocument({
|
||||
variables: { input: { documentId: node.id } },
|
||||
onCompleted: (response, errors) => {
|
||||
if (onCompletedErrors(errors)) return;
|
||||
setPdfData(response.exportDocumentPDF.data);
|
||||
},
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
case "CompliancePortalFile":
|
||||
exportFile({
|
||||
variables: { input: { compliancePortalFileId: node.id } },
|
||||
onCompleted: (response, errors) => {
|
||||
if (onCompletedErrors(errors)) return;
|
||||
if (isPdfDataUri(response.exportCompliancePortalFile.data)) {
|
||||
setPdfData(response.exportCompliancePortalFile.data);
|
||||
} else {
|
||||
setFileData(response.exportCompliancePortalFile.data);
|
||||
}
|
||||
},
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
case "AuditReport":
|
||||
exportReport({
|
||||
variables: { input: { reportId: node.id } },
|
||||
onCompleted: (response, errors) => {
|
||||
if (onCompletedErrors(errors)) return;
|
||||
setPdfData(response.exportReportPDF.data);
|
||||
},
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [node, pdfData, fileData, exportError, exportDocument, exportFile, exportReport, __]);
|
||||
|
||||
const handleRequestAccess = () => {
|
||||
const onError = (error: Error) => {
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
const urlSearchParams = new URLSearchParams([[
|
||||
"continue",
|
||||
window.location.origin + location.pathname + "?" + searchParams.toString(),
|
||||
]]);
|
||||
void navigate(`/connect?${urlSearchParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message ?? __("Cannot request access"),
|
||||
variant: "error",
|
||||
});
|
||||
};
|
||||
|
||||
const onCompleted = (_: unknown, errors: readonly { message: string }[] | null | undefined) => {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(__("Cannot request access"), [...errors]),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Access request submitted successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
};
|
||||
|
||||
switch (node.__typename) {
|
||||
case "Document":
|
||||
requestAccess({
|
||||
variables: { input: { documentId: node.id } },
|
||||
onCompleted,
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
case "CompliancePortalFile":
|
||||
requestFileAccess({
|
||||
variables: { input: { compliancePortalFileId: node.id } },
|
||||
onCompleted,
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
case "AuditReport":
|
||||
requestReportAccess({
|
||||
variables: { input: { reportId: node.id } },
|
||||
onCompleted,
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const isRequesting = isRequestingAccess || isRequestingFileAccess || isRequestingReportAccess;
|
||||
const hasRequested = node.access?.status === "REQUESTED";
|
||||
const isPdf = node.__typename === "Document" || node.__typename === "AuditReport" || (node.__typename === "CompliancePortalFile" && pdfData !== null);
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!fileData || !nodeTitle) return;
|
||||
const base64 = extractBase64Data(fileData);
|
||||
const mimeType = extractMimeType(fileData);
|
||||
const byteCharacters = atob(base64);
|
||||
const byteNumbers = new Uint8Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([byteNumbers], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = nodeTitle;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-level-2">
|
||||
<header className="flex items-center h-12 gap-3 border-b border-border-solid px-4 flex-none bg-level-1">
|
||||
<Link
|
||||
to="/documents"
|
||||
className="size-8 grid place-items-center hover:bg-secondary-hover rounded-sm transition-all"
|
||||
>
|
||||
<IconChevronLeft size={16} />
|
||||
</Link>
|
||||
{logoFileUrl && (
|
||||
<img
|
||||
alt=""
|
||||
src={logoFileUrl}
|
||||
className="h-6 w-auto"
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm font-medium truncate">
|
||||
{nodeTitle}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 min-h-0">
|
||||
{exportError
|
||||
? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center max-w-sm">
|
||||
<h2 className="text-lg font-medium mb-2">
|
||||
{__("Failed to load document")}
|
||||
</h2>
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{exportError}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: node.isUserAuthorized
|
||||
? (
|
||||
isPdf
|
||||
? (
|
||||
isExporting || !pdfData
|
||||
? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<PDFPreview src={pdfData} name={nodeTitle ?? ""} />
|
||||
)
|
||||
)
|
||||
: (
|
||||
isExporting || !fileData
|
||||
? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center max-w-sm">
|
||||
<h2 className="text-lg font-medium mb-2">
|
||||
{nodeTitle}
|
||||
</h2>
|
||||
<p className="text-sm text-txt-secondary mb-6">
|
||||
{__("This file cannot be previewed in the browser.")}
|
||||
</p>
|
||||
<Button
|
||||
icon={IconArrowDown}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{__("Download file")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
)
|
||||
: (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center max-w-sm">
|
||||
<IconLock size={32} className="mx-auto text-txt-tertiary mb-4" />
|
||||
<h2 className="text-lg font-medium mb-2">
|
||||
{nodeTitle}
|
||||
</h2>
|
||||
<p className="text-sm text-txt-secondary mb-6">
|
||||
{__("This document requires access approval before viewing.")}
|
||||
</p>
|
||||
<Button
|
||||
disabled={hasRequested || isRequesting}
|
||||
icon={IconLock}
|
||||
onClick={handleRequestAccess}
|
||||
>
|
||||
{hasRequested
|
||||
? __("Access requested")
|
||||
: __("Request access")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useQueryLoader } from "react-relay";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
import { RelayProvider } from "#/providers/RelayProviders";
|
||||
|
||||
import type { DocumentPageQuery } from "./__generated__/DocumentPageQuery.graphql";
|
||||
import { DocumentPage, documentPageQuery } from "./DocumentPage";
|
||||
|
||||
function DocumentPageQueryLoader() {
|
||||
const { documentId } = useParams<{ documentId: string }>();
|
||||
const [queryRef, loadQuery] = useQueryLoader<DocumentPageQuery>(documentPageQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (documentId) {
|
||||
loadQuery({ alias: documentId });
|
||||
}
|
||||
}, [documentId, loadQuery]);
|
||||
|
||||
if (!queryRef) return null;
|
||||
|
||||
return <DocumentPage queryRef={queryRef} />;
|
||||
}
|
||||
|
||||
export default function DocumentPageLoader() {
|
||||
return (
|
||||
<RelayProvider>
|
||||
<DocumentPageQueryLoader />
|
||||
</RelayProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { groupBy, objectEntries } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Fragment } from "react";
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
|
||||
import { CompliancePortalFileRow } from "#/components/CompliancePortalFileRow";
|
||||
import { DocumentRow } from "#/components/DocumentRow";
|
||||
import { RowHeader } from "#/components/RowHeader";
|
||||
import { Rows } from "#/components/Rows";
|
||||
import { documentTypeLabel } from "#/helpers/documents";
|
||||
import type { CompliancePortalGraphCurrentDocumentsQuery } from "#/queries/__generated__/CompliancePortalGraphCurrentDocumentsQuery.graphql";
|
||||
import { currentTrustDocumentsQuery } from "#/queries/CompliancePortalGraph";
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<CompliancePortalGraphCurrentDocumentsQuery>;
|
||||
};
|
||||
|
||||
export function DocumentsPage({ queryRef }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const data = usePreloadedQuery<CompliancePortalGraphCurrentDocumentsQuery>(
|
||||
currentTrustDocumentsQuery,
|
||||
queryRef,
|
||||
);
|
||||
const documents
|
||||
= data.currentCompliancePortal?.documents.edges.map(edge => edge.node) ?? [];
|
||||
const files
|
||||
= data.currentCompliancePortal?.compliancePortalFiles.edges.map(edge => edge.node) ?? [];
|
||||
const documentsPerType = groupBy(documents, document =>
|
||||
documentTypeLabel(document.documentType, __),
|
||||
);
|
||||
const filesPerCategory = groupBy(files, file => file.category);
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-medium mb-1">{__("Documents")}</h2>
|
||||
<p className="text-sm text-txt-secondary mb-4">
|
||||
{__("Security and compliance documentation:")}
|
||||
</p>
|
||||
<Rows className="mb-8">
|
||||
{objectEntries(documentsPerType).map(([label, documents]) => (
|
||||
<Fragment key={label}>
|
||||
<RowHeader>{label}</RowHeader>
|
||||
{documents.map(document => (
|
||||
<DocumentRow key={document.id} document={document} />
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
{objectEntries(filesPerCategory).map(([category, files]) => (
|
||||
<Fragment key={category}>
|
||||
<RowHeader>{category}</RowHeader>
|
||||
{files.map(file => (
|
||||
<CompliancePortalFileRow key={file.id} file={file} />
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</Rows>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Card, IconCircleX, Logo, Spinner } from "@probo/ui";
|
||||
import { startTransition, useEffect, useRef } from "react";
|
||||
import {
|
||||
type PreloadedQuery,
|
||||
useMutation,
|
||||
usePreloadedQuery,
|
||||
useRefetchableFragment,
|
||||
} from "react-relay";
|
||||
import { Navigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { useWindowSize } from "usehooks-ts";
|
||||
|
||||
import { PDFPreview } from "#/components/PDFPreview";
|
||||
|
||||
import type { NDAPageAcceptElectronicSignatureMutation } from "./__generated__/NDAPageAcceptElectronicSignatureMutation.graphql";
|
||||
import type { NDAPageFragment$key } from "./__generated__/NDAPageFragment.graphql";
|
||||
import type { NDAPageQuery as NDAPageQueryType } from "./__generated__/NDAPageQuery.graphql";
|
||||
import type { NDAPageRecordSigningEventMutation } from "./__generated__/NDAPageRecordSigningEventMutation.graphql";
|
||||
import type { NDAPageRefetchQuery } from "./__generated__/NDAPageRefetchQuery.graphql";
|
||||
|
||||
export const ndaPageQuery = graphql`
|
||||
query NDAPageQuery {
|
||||
viewer {
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
id
|
||||
}
|
||||
currentCompliancePortal @required(action: THROW) {
|
||||
entityName
|
||||
nonDisclosureAgreement {
|
||||
fileName
|
||||
fileUrl
|
||||
viewerSignature {
|
||||
status
|
||||
}
|
||||
}
|
||||
...NDAPageFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ndaPageFragment = graphql`
|
||||
fragment NDAPageFragment on CompliancePortal
|
||||
@refetchable(queryName: "NDAPageRefetchQuery") {
|
||||
nonDisclosureAgreement @required(action: THROW) {
|
||||
viewerSignature {
|
||||
id
|
||||
status
|
||||
consentText
|
||||
lastError
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const acceptElectronicSignatureMutation = graphql`
|
||||
mutation NDAPageAcceptElectronicSignatureMutation(
|
||||
$input: AcceptElectronicSignatureInput!
|
||||
) {
|
||||
acceptElectronicSignature(input: $input) {
|
||||
signature {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const recordSigningEventMutation = graphql`
|
||||
mutation NDAPageRecordSigningEventMutation(
|
||||
$input: RecordSigningEventInput!
|
||||
) {
|
||||
recordSigningEvent(input: $input) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function NDAPage(props: {
|
||||
queryRef: PreloadedQuery<NDAPageQueryType>;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const documentViewedRef = useRef(false);
|
||||
const { width } = useWindowSize();
|
||||
const isMobile = width < 1100;
|
||||
const isDesktop = !isMobile;
|
||||
|
||||
const queryData = usePreloadedQuery<NDAPageQueryType>(ndaPageQuery, props.queryRef);
|
||||
const compliancePortal = queryData.currentCompliancePortal;
|
||||
const viewer = queryData.viewer;
|
||||
|
||||
const [data, refetch] = useRefetchableFragment<NDAPageRefetchQuery, NDAPageFragment$key>(
|
||||
ndaPageFragment,
|
||||
compliancePortal,
|
||||
);
|
||||
const ndaSignature = data.nonDisclosureAgreement.viewerSignature;
|
||||
|
||||
const continueUrlParam = searchParams.get("continue");
|
||||
let safeContinueUrl: string;
|
||||
if (continueUrlParam) {
|
||||
try {
|
||||
const continueUrl = new URL(continueUrlParam, window.location.origin);
|
||||
if (continueUrl.origin === window.location.origin && continueUrl.pathname.startsWith("/")) {
|
||||
safeContinueUrl = window.location.origin + continueUrl.pathname + continueUrl.search;
|
||||
} else {
|
||||
safeContinueUrl = window.location.origin;
|
||||
}
|
||||
} catch {
|
||||
safeContinueUrl = window.location.origin;
|
||||
}
|
||||
} else {
|
||||
safeContinueUrl = window.location.origin;
|
||||
}
|
||||
|
||||
const [acceptSignature, isAccepting] = useMutation<NDAPageAcceptElectronicSignatureMutation>(
|
||||
acceptElectronicSignatureMutation,
|
||||
);
|
||||
|
||||
const [recordSigningEvent] = useMutation<NDAPageRecordSigningEventMutation>(
|
||||
recordSigningEventMutation,
|
||||
);
|
||||
|
||||
const isProcessing
|
||||
= ndaSignature?.status === "ACCEPTED"
|
||||
|| ndaSignature?.status === "PROCESSING";
|
||||
|
||||
const isFailed = ndaSignature?.status === "FAILED";
|
||||
const isCompleted = ndaSignature?.status === "COMPLETED";
|
||||
|
||||
useEffect(() => {
|
||||
if (isCompleted) {
|
||||
window.location.href = safeContinueUrl;
|
||||
}
|
||||
}, [isCompleted, safeContinueUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isProcessing) return;
|
||||
|
||||
const poll = () => startTransition(() => {
|
||||
refetch({}, { fetchPolicy: "network-only" });
|
||||
});
|
||||
const interval = setInterval(poll, 1500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isProcessing, refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
ndaSignature
|
||||
&& ndaSignature.status === "PENDING"
|
||||
&& !documentViewedRef.current
|
||||
) {
|
||||
documentViewedRef.current = true;
|
||||
recordSigningEvent({
|
||||
variables: {
|
||||
input: {
|
||||
signatureId: ndaSignature.id,
|
||||
eventType: "DOCUMENT_VIEWED",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [ndaSignature, recordSigningEvent]);
|
||||
|
||||
const handleAccept = () => {
|
||||
if (!ndaSignature) return;
|
||||
|
||||
if (ndaSignature.status === "PENDING") {
|
||||
recordSigningEvent({
|
||||
variables: {
|
||||
input: {
|
||||
signatureId: ndaSignature.id,
|
||||
eventType: "FULL_NAME_TYPED",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
recordSigningEvent({
|
||||
variables: {
|
||||
input: {
|
||||
signatureId: ndaSignature.id,
|
||||
eventType: "CONSENT_GIVEN",
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
acceptSignature({
|
||||
variables: {
|
||||
input: {
|
||||
signatureId: ndaSignature.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const nda = compliancePortal.nonDisclosureAgreement;
|
||||
if (!viewer) {
|
||||
return <Navigate to="/connect" replace />;
|
||||
}
|
||||
|
||||
if (!nda || !nda.viewerSignature || nda.viewerSignature.status === "COMPLETED") {
|
||||
return <Navigate to="/overview" replace />;
|
||||
}
|
||||
|
||||
const consentText = ndaSignature?.consentText;
|
||||
|
||||
return (
|
||||
<div className="bg-level-2 flex flex-col min-h-screen lg:h-screen">
|
||||
<header className="flex items-center h-12 justify-between border-b border-border-solid px-4 flex-none">
|
||||
<Logo />
|
||||
</header>
|
||||
<div className="grid lg:grid-cols-2 min-h-0 flex-1">
|
||||
<div className="flex flex-col items-center overflow-y-auto">
|
||||
<div className="max-w-[440px] w-full mx-auto px-4 py-12 lg:py-20 flex-1">
|
||||
<h1 className="text-2xl font-semibold">
|
||||
{__("Non-Disclosure Agreement")}
|
||||
</h1>
|
||||
<p className="text-txt-secondary mt-2">
|
||||
{sprintf(
|
||||
__(
|
||||
"%s requires you to sign an NDA before accessing compliance documents.",
|
||||
),
|
||||
compliancePortal.entityName,
|
||||
)}
|
||||
</p>
|
||||
{isMobile && nda?.fileUrl && (
|
||||
<Card className="flex justify-between py-3 px-4 text-sm items-center mt-6">
|
||||
{nda?.fileName}
|
||||
<Button variant="secondary" asChild>
|
||||
<a target="_blank" rel="noopener noreferrer" href={nda.fileUrl}>
|
||||
{__("View document")}
|
||||
</a>
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
<div className="mt-8">
|
||||
<p className="text-xs text-txt-tertiary mt-6">
|
||||
{consentText}
|
||||
</p>
|
||||
{isFailed && (
|
||||
<div className="flex items-start gap-2 mt-4 rounded-md bg-red-50 border border-red-200 p-3 text-sm text-red-800">
|
||||
<IconCircleX size={16} className="mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{__("Signature processing failed")}
|
||||
</p>
|
||||
<p className="mt-0.5 text-red-600">
|
||||
{ndaSignature?.lastError
|
||||
?? __("We encountered an issue processing your signature. Please try again.")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isProcessing
|
||||
? (
|
||||
<Button
|
||||
type="button"
|
||||
className="h-10 w-full mt-4"
|
||||
disabled
|
||||
icon={Spinner}
|
||||
>
|
||||
{__("Sealing your signature...")}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleAccept}
|
||||
className="h-10 w-full mt-4"
|
||||
disabled={isProcessing}
|
||||
icon={isAccepting ? Spinner : undefined}
|
||||
>
|
||||
{isFailed
|
||||
? __("Try again")
|
||||
: __("Review and sign")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="https://www.probo.com/"
|
||||
className="flex gap-1 text-sm font-medium text-txt-tertiary items-center py-6"
|
||||
>
|
||||
Powered by
|
||||
{" "}
|
||||
<Logo withPicto className="h-6" />
|
||||
</a>
|
||||
</div>
|
||||
{isDesktop && (
|
||||
<div className="bg-subtle h-full border-l border-border-solid min-h-0">
|
||||
{nda?.fileUrl && <PDFPreview src={nda.fileUrl} name={nda.fileName ?? ""} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useQueryLoader } from "react-relay";
|
||||
|
||||
import { RelayProvider } from "#/providers/RelayProviders";
|
||||
|
||||
import type { NDAPageQuery } from "./__generated__/NDAPageQuery.graphql";
|
||||
import { NDAPage, ndaPageQuery } from "./NDAPage";
|
||||
|
||||
function NDAPageQueryLoader() {
|
||||
const [queryRef, loadQuery]
|
||||
= useQueryLoader<NDAPageQuery>(ndaPageQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (!queryRef) {
|
||||
loadQuery({});
|
||||
}
|
||||
});
|
||||
|
||||
if (!queryRef) return null;
|
||||
|
||||
return <NDAPage queryRef={queryRef} />;
|
||||
}
|
||||
|
||||
export default function NDAPageLoader() {
|
||||
return (
|
||||
<RelayProvider>
|
||||
<NDAPageQueryLoader />
|
||||
</RelayProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import {
|
||||
getCompliancePortalUrl,
|
||||
groupBy,
|
||||
objectEntries,
|
||||
sprintf,
|
||||
} from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Card, IconChevronRight } from "@probo/ui";
|
||||
import { Fragment } from "react";
|
||||
import { useFragment } from "react-relay";
|
||||
import { Link, useOutletContext } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import { AuditRow } from "#/components/AuditRow";
|
||||
import { CompliancePortalFileRow } from "#/components/CompliancePortalFileRow";
|
||||
import { DocumentRow } from "#/components/DocumentRow";
|
||||
import { RowHeader } from "#/components/RowHeader";
|
||||
import { Rows } from "#/components/Rows";
|
||||
import { SubprocessorRow } from "#/components/SubprocessorRow";
|
||||
import { documentTypeLabel } from "#/helpers/documents";
|
||||
import type { CompliancePortalGraphCurrentQuery$data } from "#/queries/__generated__/CompliancePortalGraphCurrentQuery.graphql";
|
||||
|
||||
import type {
|
||||
OverviewPageFragment$data,
|
||||
OverviewPageFragment$key,
|
||||
} from "./__generated__/OverviewPageFragment.graphql";
|
||||
|
||||
const overviewFragment = graphql`
|
||||
fragment OverviewPageFragment on CompliancePortal {
|
||||
references(first: 14) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
websiteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
subprocessors(first: 3) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
countries
|
||||
...SubprocessorRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
documents(first: 5) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
documentType
|
||||
...DocumentRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
compliancePortalFiles(first: 5) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
category
|
||||
...CompliancePortalFileRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function OverviewPage() {
|
||||
const { compliancePortal } = useOutletContext<{
|
||||
compliancePortal: OverviewPageFragment$key
|
||||
& CompliancePortalGraphCurrentQuery$data["currentCompliancePortal"];
|
||||
}>();
|
||||
const fragment = useFragment(overviewFragment, compliancePortal);
|
||||
return (
|
||||
<div>
|
||||
<References
|
||||
references={fragment.references.edges.map(edge => edge.node)}
|
||||
/>
|
||||
<Documents
|
||||
audits={compliancePortal.audits.edges}
|
||||
documents={fragment.documents.edges}
|
||||
files={fragment.compliancePortalFiles.edges}
|
||||
url={getCompliancePortalUrl("documents")}
|
||||
/>
|
||||
<Subprocessors
|
||||
title={compliancePortal.entityName}
|
||||
subprocessors={fragment.subprocessors.edges}
|
||||
url={getCompliancePortalUrl("subprocessors")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Documents({
|
||||
documents,
|
||||
files,
|
||||
audits,
|
||||
url,
|
||||
}: {
|
||||
documents: OverviewPageFragment$data["documents"]["edges"];
|
||||
files: OverviewPageFragment$data["compliancePortalFiles"]["edges"];
|
||||
audits: NonNullable<
|
||||
CompliancePortalGraphCurrentQuery$data["currentCompliancePortal"]
|
||||
>["audits"]["edges"];
|
||||
url: string;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const documentsPerType = groupBy(
|
||||
documents.map(edge => edge.node),
|
||||
node => documentTypeLabel(node.documentType, __),
|
||||
);
|
||||
const filesPerCategory = groupBy(
|
||||
files.map(edge => edge.node),
|
||||
node => node.category,
|
||||
);
|
||||
const hasAudits = audits.length > 0;
|
||||
const hasDocuments = hasAudits || documents.length > 0 || files.length > 0;
|
||||
|
||||
if (!hasDocuments) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-medium mb-1">{__("Documents")}</h2>
|
||||
<p className="text-sm text-txt-secondary mb-4">
|
||||
{__("Security and compliance documentation:")}
|
||||
</p>
|
||||
<Rows className="mb-8">
|
||||
{audits.length > 0 && (
|
||||
<>
|
||||
<RowHeader>{__("Compliance")}</RowHeader>
|
||||
{audits.map(audit => (
|
||||
<AuditRow key={audit.node.id} audit={audit.node} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{objectEntries(documentsPerType).map(([label, documents]) => (
|
||||
<Fragment key={label}>
|
||||
<RowHeader>{label}</RowHeader>
|
||||
{documents.map(document => (
|
||||
<DocumentRow key={document.id} document={document} />
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
{objectEntries(filesPerCategory).map(([category, files]) => (
|
||||
<Fragment key={category}>
|
||||
<RowHeader>{category}</RowHeader>
|
||||
{files.map(file => (
|
||||
<CompliancePortalFileRow key={file.id} file={file} />
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
<Link to={url} className="text-sm font-medium flex gap-2 items-center">
|
||||
{__("See all documents")}
|
||||
<IconChevronRight size={16} />
|
||||
</Link>
|
||||
</Rows>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Subprocessors({
|
||||
subprocessors,
|
||||
url,
|
||||
title,
|
||||
}: {
|
||||
subprocessors: OverviewPageFragment$data["subprocessors"]["edges"];
|
||||
url: string;
|
||||
title: string;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
if (subprocessors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasAnyCountries = subprocessors.some((subprocessor) => {
|
||||
const subprocessorData = subprocessor.node;
|
||||
return subprocessorData.countries && subprocessorData.countries.length > 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-medium mb-1">{__("Subprocessors")}</h2>
|
||||
<p className="text-sm text-txt-secondary mb-4">
|
||||
{sprintf(
|
||||
__("Third-party subprocessors %s work with:"),
|
||||
title,
|
||||
)}
|
||||
</p>
|
||||
<Rows className="mb-8 *:py-5">
|
||||
{subprocessors.map(subprocessor => (
|
||||
<SubprocessorRow
|
||||
key={subprocessor.node.id}
|
||||
subprocessor={subprocessor.node}
|
||||
hasAnyCountries={hasAnyCountries}
|
||||
/>
|
||||
))}
|
||||
<Link to={url} className="text-sm font-medium flex gap-2 items-center">
|
||||
{__("See all subprocessors")}
|
||||
<IconChevronRight size={16} />
|
||||
</Link>
|
||||
</Rows>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Reference = {
|
||||
name: string;
|
||||
logo: { downloadUrl: string } | null;
|
||||
websiteUrl: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
function References({ references }: { references: Reference[] }) {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
if (references.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<h2 className="font-medium mb-4">{__("Trusted by")}</h2>
|
||||
<Card className="grid grid-cols-2 flex-wrap p-6 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-7">
|
||||
{references.map(reference => (
|
||||
<a
|
||||
key={reference.id}
|
||||
href={reference.websiteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex flex-col justify-center items-center gap-2"
|
||||
>
|
||||
<img
|
||||
src={reference.logo?.downloadUrl}
|
||||
alt={reference.name}
|
||||
className="rounded-2xl size-12 block"
|
||||
/>
|
||||
<span className="text-xs text-txt-secondary text-center">{reference.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
|
||||
import { Rows } from "#/components/Rows";
|
||||
import { SubprocessorRow } from "#/components/SubprocessorRow";
|
||||
import type { CompliancePortalGraphCurrentSubprocessorsQuery } from "#/queries/__generated__/CompliancePortalGraphCurrentSubprocessorsQuery.graphql";
|
||||
import { currentTrustSubprocessorsQuery } from "#/queries/CompliancePortalGraph";
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<CompliancePortalGraphCurrentSubprocessorsQuery>;
|
||||
};
|
||||
|
||||
export function SubprocessorsPage({ queryRef }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const data = usePreloadedQuery<CompliancePortalGraphCurrentSubprocessorsQuery>(currentTrustSubprocessorsQuery, queryRef);
|
||||
const subprocessors
|
||||
= data.currentCompliancePortal?.subprocessors.edges.map(edge => edge.node) ?? [];
|
||||
|
||||
const hasAnyCountries = subprocessors.some(subprocessor => subprocessor.countries.length > 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-medium mb-1">{__("Subprocessors")}</h2>
|
||||
<p className="text-sm text-txt-secondary mb-4">
|
||||
{sprintf(
|
||||
__("Third-party subprocessors %s work with:"),
|
||||
data.currentCompliancePortal?.entityName ?? "",
|
||||
)}
|
||||
</p>
|
||||
<Rows>
|
||||
{subprocessors.map(subprocessor => (
|
||||
<SubprocessorRow key={subprocessor.id} subprocessor={subprocessor} hasAnyCountries={hasAnyCountries} />
|
||||
))}
|
||||
</Rows>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
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 { Rows } from "#/components/Rows";
|
||||
import type { UpdatesPageQuery } from "#/pages/__generated__/UpdatesPageQuery.graphql";
|
||||
|
||||
export const currentTrustUpdatesQuery = graphql`
|
||||
query UpdatesPageQuery {
|
||||
currentCompliancePortal {
|
||||
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.currentCompliancePortal?.updates.edges.map(e => e.node) ?? [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-medium mb-1">{__("Updates")}</h2>
|
||||
{items.length === 0
|
||||
? (
|
||||
<Rows>
|
||||
<div className="text-sm text-txt-tertiary text-center py-5">
|
||||
{__("No updates have been published yet.")}
|
||||
</div>
|
||||
</Rows>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useSystemTheme } from "@probo/hooks";
|
||||
import { Card, Logo } from "@probo/ui";
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
import { Outlet } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { AuthLayoutQuery } from "./__generated__/AuthLayoutQuery.graphql";
|
||||
|
||||
export const authLayoutQuery = graphql`
|
||||
query AuthLayoutQuery {
|
||||
currentCompliancePortal @required(action: THROW) {
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
darkLogo {
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function AuthLayout(props: { queryRef: PreloadedQuery<AuthLayoutQuery> }) {
|
||||
const { queryRef } = props;
|
||||
|
||||
const { currentCompliancePortal: compliancePage } = usePreloadedQuery<AuthLayoutQuery>(authLayoutQuery, queryRef);
|
||||
const theme = useSystemTheme();
|
||||
|
||||
const logoFileUrl = theme === "dark"
|
||||
? compliancePage.darkLogo?.downloadUrl ?? compliancePage.logo?.downloadUrl
|
||||
: compliancePage.logo?.downloadUrl;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen text-txt-primary bg-level-0 flex flex-col items-center justify-center">
|
||||
<Card className="w-full max-w-lg px-12 py-8 flex flex-col items-center justify-center">
|
||||
<div className="w-full flex flex-col items-center justify-center gap-8">
|
||||
{logoFileUrl
|
||||
? (
|
||||
<img
|
||||
alt=""
|
||||
src={logoFileUrl}
|
||||
className="h-20 rounded-2xl"
|
||||
/>
|
||||
)
|
||||
: <Logo withPicto className="w-[110px]" />}
|
||||
<div className="w-full border-t border-t-border-mid" />
|
||||
</div>
|
||||
|
||||
<Outlet />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useQueryLoader } from "react-relay";
|
||||
|
||||
import { RelayProvider } from "#/providers/RelayProviders";
|
||||
|
||||
import type { AuthLayoutQuery } from "./__generated__/AuthLayoutQuery.graphql";
|
||||
import { AuthLayout, authLayoutQuery } from "./AuthLayout";
|
||||
|
||||
function AuthLayoutQueryLoader() {
|
||||
const [queryRef, loadQuery] = useQueryLoader<AuthLayoutQuery>(authLayoutQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (!queryRef) {
|
||||
return loadQuery({});
|
||||
}
|
||||
});
|
||||
|
||||
if (!queryRef) return null;
|
||||
|
||||
return <AuthLayout queryRef={queryRef} />;
|
||||
}
|
||||
|
||||
export default function AuthLayoutLoader() {
|
||||
return (
|
||||
<RelayProvider>
|
||||
<AuthLayoutQueryLoader />
|
||||
</RelayProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button } from "@probo/ui";
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import { useSafeContinueUrl } from "#/hooks/useSafeContinueUrl";
|
||||
|
||||
import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql";
|
||||
|
||||
export const connectPageQuery = graphql`
|
||||
query ConnectPageQuery {
|
||||
currentCompliancePortal @required(action: THROW) {
|
||||
entityName
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function ConnectPage(props: {
|
||||
queryRef: PreloadedQuery<ConnectPageQuery>;
|
||||
}) {
|
||||
const { queryRef } = props;
|
||||
|
||||
const { __ } = useTranslate();
|
||||
const safeContinueUrl = useSafeContinueUrl();
|
||||
|
||||
const {
|
||||
currentCompliancePortal: { entityName },
|
||||
} = usePreloadedQuery<ConnectPageQuery>(connectPageQuery, queryRef);
|
||||
|
||||
usePageTitle(__(`Connect to ${entityName}'s Compliance Page`));
|
||||
|
||||
const initiateURL = new URL("/initiate", window.location.origin);
|
||||
initiateURL.searchParams.set("continue", safeContinueUrl.toString());
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-3xl font-bold">
|
||||
{__(`Connect to ${entityName}'s Compliance Page`)}
|
||||
</h1>
|
||||
<p className="text-txt-tertiary">
|
||||
{__(
|
||||
"Sign in to start requesting access to documents",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full h-10"
|
||||
onClick={() => {
|
||||
window.location.href = initiateURL.toString();
|
||||
}}
|
||||
>
|
||||
{__("Get Access")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { useQueryLoader } from "react-relay";
|
||||
|
||||
import { RelayProvider } from "#/providers/RelayProviders";
|
||||
|
||||
import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql";
|
||||
import { ConnectPage, connectPageQuery } from "./ConnectPage";
|
||||
|
||||
function ConnectPageQueryLoader() {
|
||||
const [queryRef, loadQuery]
|
||||
= useQueryLoader<ConnectPageQuery>(connectPageQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (!queryRef) {
|
||||
loadQuery({});
|
||||
}
|
||||
});
|
||||
|
||||
if (!queryRef) return null;
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<ConnectPage queryRef={queryRef} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ConnectPageLoader() {
|
||||
return (
|
||||
<RelayProvider>
|
||||
<ConnectPageQueryLoader />
|
||||
</RelayProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Field, useToast } from "@probo/ui";
|
||||
import {
|
||||
useMutation,
|
||||
} from "react-relay";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
|
||||
import type { FullNamePageMutation } from "./__generated__/FullNamePageMutation.graphql";
|
||||
|
||||
const updateMutation = graphql`
|
||||
mutation FullNamePageMutation($input: UpdateFullNameInput!) {
|
||||
updateFullName(input: $input) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const schema = z.object({
|
||||
fullName: z.string().min(2),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
export default function FullNamePage() {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const continueUrlParam = searchParams.get("continue");
|
||||
let safeContinueUrl: string;
|
||||
if (continueUrlParam) {
|
||||
try {
|
||||
const continueUrl = new URL(continueUrlParam, window.location.origin);
|
||||
if (continueUrl.origin === window.location.origin && continueUrl.pathname.startsWith("/")) {
|
||||
safeContinueUrl = window.location.origin + continueUrl.pathname + continueUrl.search;
|
||||
} else {
|
||||
safeContinueUrl = window.location.origin;
|
||||
}
|
||||
} catch {
|
||||
safeContinueUrl = window.location.origin;
|
||||
}
|
||||
} else {
|
||||
safeContinueUrl = window.location.origin;
|
||||
}
|
||||
|
||||
const {
|
||||
handleSubmit: handleSubmitWrapper,
|
||||
register,
|
||||
formState,
|
||||
} = useFormWithSchema(schema, {
|
||||
defaultValues: {
|
||||
fullName: "",
|
||||
},
|
||||
});
|
||||
|
||||
const [update] = useMutation<FullNamePageMutation>(
|
||||
updateMutation,
|
||||
);
|
||||
|
||||
const handleSubmit = handleSubmitWrapper(({ fullName }: FormData) => {
|
||||
update({
|
||||
variables: {
|
||||
input: {
|
||||
fullName,
|
||||
},
|
||||
},
|
||||
onCompleted: (_, errors: GraphQLError[] | null) => {
|
||||
if (errors) {
|
||||
for (const err of errors) {
|
||||
if (err.extensions?.code === "ALREADY_AUTHENTICATED") {
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
}
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: __("Cannot send magic link"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Full name updated!"),
|
||||
variant: "success",
|
||||
});
|
||||
|
||||
window.location.href = safeContinueUrl;
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message,
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-3xl font-bold">
|
||||
{__("Please set your profile's full name")}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={e => void handleSubmit(e)} className="space-y-6">
|
||||
<Field
|
||||
label={__("Full Name")}
|
||||
placeholder="John Doe"
|
||||
{...register("fullName")}
|
||||
type="text"
|
||||
required
|
||||
error={formState.errors.fullName?.message}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-xs h-10 mx-auto"
|
||||
disabled={formState.isSubmitting}
|
||||
>
|
||||
{__("Continue")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button } from "@probo/ui";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export default function MagicLinkAlreadyUsedPage() {
|
||||
const { __ } = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
usePageTitle(__("Link Already Used"));
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-3xl font-bold">{__("Link Already Used")}</h1>
|
||||
<p className="text-txt-tertiary">
|
||||
{__(
|
||||
"This magic link has already been used. Magic links can only be used once. Please request a new one if you need to sign in again.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
className="w-xs h-10"
|
||||
onClick={() => void navigate("/connect")}
|
||||
>
|
||||
{__("Request a new link")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button } from "@probo/ui";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export default function MagicLinkExpiredPage() {
|
||||
const { __ } = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
usePageTitle(__("Link Expired"));
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-3xl font-bold">{__("Link Expired")}</h1>
|
||||
<p className="text-txt-tertiary">
|
||||
{__(
|
||||
"This magic link has expired. Magic links are only valid for 15 minutes. Please request a new one.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
className="w-xs h-10"
|
||||
onClick={() => void navigate("/connect")}
|
||||
>
|
||||
{__("Request a new link")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
export function Divider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="relative my-6 w-full">
|
||||
<div className="border-t border-border-mid" />
|
||||
<span className="px-4 text-xs uppercase text-txt-secondary bg-level-0 absolute top-0 left-1/2 -translate-1/2">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Google, Microsoft } from "@probo/ui";
|
||||
import type { ComponentProps } from "react";
|
||||
import { useFragment } from "react-relay";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import { useSafeContinueUrl } from "#/hooks/useSafeContinueUrl";
|
||||
|
||||
import type { OIDCButtonFragment$key } from "./__generated__/OIDCButtonFragment.graphql";
|
||||
|
||||
const fragment = graphql`
|
||||
fragment OIDCButtonFragment on OIDCProviderInfo {
|
||||
name
|
||||
loginURL
|
||||
}
|
||||
`;
|
||||
|
||||
const providerIcons: Record<
|
||||
string,
|
||||
(props: ComponentProps<"svg">) => React.ReactNode
|
||||
> = {
|
||||
google: Google,
|
||||
microsoft: Microsoft,
|
||||
};
|
||||
|
||||
export function OIDCButton({
|
||||
providerRef,
|
||||
}: {
|
||||
providerRef: OIDCButtonFragment$key;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const safeContinueUrl = useSafeContinueUrl();
|
||||
const provider = useFragment(fragment, providerRef);
|
||||
const Icon = providerIcons[provider.name];
|
||||
const organizationId = searchParams.get("organization-id");
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full h-10"
|
||||
onClick={() => {
|
||||
const loginURL = new URL(provider.loginURL, window.location.origin);
|
||||
loginURL.searchParams.set("continue", safeContinueUrl.toString());
|
||||
if (organizationId) {
|
||||
loginURL.searchParams.set("organization_id", organizationId);
|
||||
}
|
||||
|
||||
window.location.href = loginURL.toString();
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{Icon && <Icon width={18} height={18} />}
|
||||
{__(`Sign in with ${provider.name.charAt(0).toUpperCase() + provider.name.slice(1)}`)}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { createContext, useMemo } from "react";
|
||||
|
||||
export const AuthContext = createContext({ isAuthenticated: false });
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
isAuthenticated: boolean;
|
||||
};
|
||||
|
||||
export function AuthProvider({ children, isAuthenticated }: Props) {
|
||||
const value = useMemo(() => ({ isAuthenticated }), [isAuthenticated]);
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { createContext, type ReactNode } from "react";
|
||||
|
||||
import type { CompliancePortalGraphCurrentQuery$data } from "#/queries/__generated__/CompliancePortalGraphCurrentQuery.graphql";
|
||||
|
||||
export const CompliancePortalContext = createContext<
|
||||
CompliancePortalGraphCurrentQuery$data["currentCompliancePortal"] | null
|
||||
>(null);
|
||||
|
||||
export const CompliancePortalProvider = ({
|
||||
children,
|
||||
compliancePortal,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
compliancePortal: CompliancePortalGraphCurrentQuery$data["currentCompliancePortal"];
|
||||
}) => {
|
||||
return (
|
||||
<CompliancePortalContext.Provider value={compliancePortal}>
|
||||
{children}
|
||||
</CompliancePortalContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { makeFetchQuery } from "@probo/relay";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { RelayEnvironmentProvider } from "react-relay";
|
||||
import {
|
||||
Environment,
|
||||
Network,
|
||||
RecordSource,
|
||||
Store,
|
||||
} from "relay-runtime";
|
||||
|
||||
export function buildEndpoint(): string {
|
||||
let host = import.meta.env.VITE_API_URL;
|
||||
|
||||
if (!host) {
|
||||
host = window.location.origin;
|
||||
}
|
||||
|
||||
const formattedHost
|
||||
= host.startsWith("http://") || host.startsWith("https://")
|
||||
? host
|
||||
: `https://${host}`;
|
||||
|
||||
const url = new URL(formattedHost);
|
||||
|
||||
// Compliance pages are always served at the root of a dedicated host.
|
||||
url.pathname = "/graphql";
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const source = new RecordSource();
|
||||
const store = new Store(source, {
|
||||
queryCacheExpirationTime: 1 * 60 * 1000,
|
||||
gcReleaseBufferSize: 20,
|
||||
});
|
||||
|
||||
export const consoleEnvironment = new Environment({
|
||||
configName: "compliance-page",
|
||||
network: Network.create(makeFetchQuery(buildEndpoint())),
|
||||
store,
|
||||
});
|
||||
|
||||
/**
|
||||
* Provider for relay with the probo environment
|
||||
*/
|
||||
export function RelayProvider({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<RelayEnvironmentProvider environment={consoleEnvironment}>
|
||||
{children}
|
||||
</RelayEnvironmentProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import { TranslatorProvider as ProboTranslatorProvider } from "../../../../packages/i18n/TranslatorProvider";
|
||||
|
||||
// TODO : implement a way to retrieve translations strings
|
||||
const loader = () => {
|
||||
return Promise.resolve({} as Record<string, string>);
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider for the translator
|
||||
*/
|
||||
export function TranslatorProvider({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<ProboTranslatorProvider lang="en" loader={loader}>
|
||||
{children}
|
||||
</ProboTranslatorProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */
|
||||
|
||||
// Queries for custom domain (subdomain) approach
|
||||
export const currentCompliancePortalGraphQuery = graphql`
|
||||
query CompliancePortalGraphCurrentQuery {
|
||||
viewer {
|
||||
id
|
||||
}
|
||||
currentCompliancePortal @required(action: THROW) {
|
||||
id
|
||||
slug
|
||||
entityName
|
||||
description
|
||||
websiteUrl
|
||||
email
|
||||
headquarterAddress
|
||||
viewerSubscription {
|
||||
id
|
||||
email
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
logo {
|
||||
downloadUrl
|
||||
}
|
||||
darkLogo {
|
||||
downloadUrl
|
||||
}
|
||||
nonDisclosureAgreement {
|
||||
fileName
|
||||
fileUrl
|
||||
viewerSignature {
|
||||
status
|
||||
}
|
||||
}
|
||||
customLinks(first: 20) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
...OverviewPageFragment
|
||||
subprocessorInfo: subprocessors(first: 0) {
|
||||
totalCount
|
||||
}
|
||||
audits(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...AuditRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
complianceFrameworks(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
framework {
|
||||
...FrameworkBadgeFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const currentTrustDocumentsQuery = graphql`
|
||||
query CompliancePortalGraphCurrentDocumentsQuery {
|
||||
currentCompliancePortal {
|
||||
id
|
||||
documents(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
documentType
|
||||
...DocumentRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
compliancePortalFiles(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
category
|
||||
...CompliancePortalFileRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const currentTrustSubprocessorsQuery = graphql`
|
||||
query CompliancePortalGraphCurrentSubprocessorsQuery {
|
||||
currentCompliancePortal {
|
||||
id
|
||||
entityName
|
||||
subprocessors(first: 50) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
countries
|
||||
...SubprocessorRowFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,164 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { lazy } from "@probo/react-lazy";
|
||||
import {
|
||||
type AppRoute,
|
||||
loaderFromQueryLoader,
|
||||
routeFromAppRoute,
|
||||
withQueryRef,
|
||||
} from "@probo/routes";
|
||||
import { Fragment } from "react";
|
||||
import { loadQuery } from "react-relay";
|
||||
import { createBrowserRouter, redirect } from "react-router";
|
||||
|
||||
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 {
|
||||
currentCompliancePortalGraphQuery,
|
||||
currentTrustDocumentsQuery,
|
||||
currentTrustSubprocessorsQuery,
|
||||
} from "#/queries/CompliancePortalGraph";
|
||||
|
||||
import { DocumentPageErrorBoundary } from "./components/DocumentPageErrorBoundary";
|
||||
import { PageError } from "./components/PageError";
|
||||
import { RootErrorBoundary } from "./components/RootErrorBoundary";
|
||||
import { MainSkeleton } from "./components/Skeletons/MainSkeleton";
|
||||
import { TabSkeleton } from "./components/Skeletons/TabSkeleton";
|
||||
import { consoleEnvironment } from "./providers/RelayProviders";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
Component: lazy(() => import("#/pages/auth/AuthLayoutLoader")),
|
||||
children: [
|
||||
{
|
||||
path: "/connect",
|
||||
Component: lazy(() => import("#/pages/auth/ConnectPageLoader")),
|
||||
},
|
||||
{
|
||||
path: "/full-name",
|
||||
Component: lazy(() => import("#/pages/auth/FullNamePage")),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
loader: () => {
|
||||
// eslint-disable-next-line
|
||||
throw redirect("/overview");
|
||||
},
|
||||
Component: Fragment,
|
||||
ErrorBoundary: RootErrorBoundary,
|
||||
},
|
||||
{
|
||||
path: "/nda",
|
||||
Component: lazy(() => import("#/pages/NDAPageLoader")),
|
||||
ErrorBoundary: RootErrorBoundary,
|
||||
},
|
||||
// Custom domain routes (subdomain-based)
|
||||
{
|
||||
path: "/overview",
|
||||
loader: loaderFromQueryLoader(() =>
|
||||
loadQuery(consoleEnvironment, currentCompliancePortalGraphQuery, {}),
|
||||
),
|
||||
Component: withQueryRef(MainLayout),
|
||||
Fallback: MainSkeleton,
|
||||
ErrorBoundary: RootErrorBoundary,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
Fallback: TabSkeleton,
|
||||
Component: OverviewPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/documents/:documentId",
|
||||
Component: lazy(() => import("#/pages/DocumentPageLoader")),
|
||||
ErrorBoundary: DocumentPageErrorBoundary,
|
||||
},
|
||||
{
|
||||
path: "/documents",
|
||||
loader: loaderFromQueryLoader(() =>
|
||||
loadQuery(consoleEnvironment, currentCompliancePortalGraphQuery, {}),
|
||||
),
|
||||
Component: withQueryRef(MainLayout),
|
||||
Fallback: MainSkeleton,
|
||||
ErrorBoundary: RootErrorBoundary,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
loader: loaderFromQueryLoader(() =>
|
||||
loadQuery(consoleEnvironment, currentTrustDocumentsQuery, {}),
|
||||
),
|
||||
Fallback: TabSkeleton,
|
||||
Component: withQueryRef(DocumentsPage),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/subprocessors",
|
||||
loader: loaderFromQueryLoader(() =>
|
||||
loadQuery(consoleEnvironment, currentCompliancePortalGraphQuery, {}),
|
||||
),
|
||||
Component: withQueryRef(MainLayout),
|
||||
Fallback: MainSkeleton,
|
||||
ErrorBoundary: RootErrorBoundary,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
loader: loaderFromQueryLoader(() =>
|
||||
loadQuery(consoleEnvironment, currentTrustSubprocessorsQuery, {}),
|
||||
),
|
||||
Fallback: TabSkeleton,
|
||||
Component: withQueryRef(SubprocessorsPage),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/updates",
|
||||
loader: loaderFromQueryLoader(() =>
|
||||
loadQuery(consoleEnvironment, currentCompliancePortalGraphQuery, {}),
|
||||
),
|
||||
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: "*",
|
||||
Component: PageError,
|
||||
},
|
||||
] satisfies AppRoute[];
|
||||
|
||||
export const router = createBrowserRouter(routes.map(routeFromAppRoute), {});
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
export type NodeOf<T>
|
||||
= NonNullable<T> extends
|
||||
| { readonly edges: ReadonlyArray<{ readonly node: infer U }> }
|
||||
| undefined
|
||||
? U
|
||||
: never;
|
||||
|
||||
export type ItemOf<T> = T extends (infer U)[] ? U : never;
|
||||
29
apps/trust/src/vite-env.d.ts
vendored
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package truststatics
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed dist
|
||||
var StaticFiles embed.FS
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"#/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"allowJs": true,
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
import babel from "@rolldown/plugin-babel";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
// @vitejs/plugin-react@6 (Vite 8) no longer runs Babel, so the Relay tagged
|
||||
// template transform is applied via @rolldown/plugin-babel instead.
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
babel({ plugins: ["relay"] }),
|
||||
tailwindcss(),
|
||||
],
|
||||
build: {
|
||||
assetsDir: "assets",
|
||||
},
|
||||
base: "./",
|
||||
server: {
|
||||
port: 5175,
|
||||
proxy: {
|
||||
"^/trust/[^/]+/api": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"#": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
Template files use the extension pattern `<name>.<output-ext>.tmpl`:
|
||||
|
||||
```
|
||||
pkg/trust/sitemap.xml.tmpl
|
||||
pkg/complianceportal/visitor/sitemap.xml.tmpl
|
||||
pkg/server/mailactions/templates/page.html.tmpl
|
||||
pkg/cookiebanner/prompts/tracker_identification.txt.tmpl
|
||||
pkg/probo/templates/risk_list.json.tmpl
|
||||
|
||||
@@ -17,7 +17,7 @@ Configured in `apps/console/src/environments.ts`. Each has its own store with 1-
|
||||
|
||||
## Relay compiler
|
||||
|
||||
Config lives in `relay.config.json` at the repo root with three projects (`core`, `iam`, `trust`) mapped to different source directories and schemas. Each project uses `schema` pointing to `base.graphql` and `schemaExtensions` pointing to the `graphql/` directory containing the per-entity schema files. Generated files go into `__generated__/` directories.
|
||||
Config lives in `relay.config.json` at the repo root with three projects (`core`, `iam`, `complianceportal`) mapped to different source directories and schemas. Each project uses `schema` pointing to `base.graphql` and `schemaExtensions` pointing to the `graphql/` directory containing the per-entity schema files. Generated files go into `__generated__/` directories.
|
||||
|
||||
```sh
|
||||
make relay # merge split schemas + clean + compile
|
||||
|
||||
@@ -120,7 +120,7 @@ PROBOD_AUTH_COOKIE_SECRET="this-is-a-secure-secret-for-cookie-signing-at-least-3
|
||||
PROBOD_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes" \
|
||||
PROBOD_ENCRYPTION_KEY="thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \
|
||||
PROBOD_OAUTH2_SERVER_SIGNING_KEY="$(cat "${OAUTH2_SIGNING_KEY_PATH}")" \
|
||||
PROBOD_API_CORS_ALLOWED_ORIGINS="http://${VM_IP}:8080,http://${VM_IP}:5173,http://${VM_IP}:5174,http://${VM_IP}:5175" \
|
||||
PROBOD_API_CORS_ALLOWED_ORIGINS="http://${VM_IP}:8080,http://${VM_IP}:5173,http://${VM_IP}:5174" \
|
||||
PROBOD_AWS_ENDPOINT="http://127.0.0.1:8333" \
|
||||
PROBOD_AWS_ACCESS_KEY_ID="probod" \
|
||||
PROBOD_AWS_SECRET_ACCESS_KEY="thisisnotasecret" \
|
||||
|
||||
@@ -4,7 +4,7 @@ import { defineConfig, globalIgnores } from "eslint/config";
|
||||
// Workspaces that are linted by this root config. Each gets the shared rule
|
||||
// sets below; everything else is ignored so a bare `eslint .` keeps the same
|
||||
// scope as the previous per-workspace configs.
|
||||
const appDirs = ["apps/console/**", "apps/trust/**", "apps/compliance-portal/**"];
|
||||
const appDirs = ["apps/console/**", "apps/compliance-portal/**"];
|
||||
const reactDirs = [...appDirs, "packages/ui/**", "packages/relay/**", "packages/routes/**"];
|
||||
const lintedDirs = [...reactDirs, "packages/eslint-config/**"];
|
||||
|
||||
@@ -50,8 +50,8 @@ export default defineConfig([
|
||||
{
|
||||
// compliance-portal mutates through the awaitable useMutation bound in
|
||||
// #/lib/relay/useMutation (over @probo/relay's createUseMutation), never
|
||||
// react-relay's useMutation directly. Scoped to this app only: console and
|
||||
// trust still use react-relay's useMutation.
|
||||
// react-relay's useMutation directly. Scoped to this app only: console
|
||||
// still uses react-relay's useMutation.
|
||||
files: ["apps/compliance-portal/**"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
|
||||
290
package-lock.json
generated
@@ -594,292 +594,6 @@
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"apps/trust": {
|
||||
"name": "@probo/trust",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@probo/helpers": "1.0.0",
|
||||
"@probo/hooks": "1.0.0",
|
||||
"@probo/i18n": "1.0.0",
|
||||
"@probo/routes": "^1.0.0",
|
||||
"@probo/ui": "1.0.0",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-pdf": "^10.3.0",
|
||||
"react-relay": "^21.0.1",
|
||||
"react-router": "^8.1.0",
|
||||
"relay-runtime": "^21.0.1",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^8.0.1",
|
||||
"@rolldown/plugin-babel": "^0.2.3",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/node": "^26",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"babel-plugin-relay": "^21.0.1",
|
||||
"graphql": "^17.0.1",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"typescript": "~6.0.3",
|
||||
"vite": "^8.1.2"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/code-frame": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz",
|
||||
"integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^8.0.0",
|
||||
"js-tokens": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/compat-data": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.0.tgz",
|
||||
"integrity": "sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/core": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.1.tgz",
|
||||
"integrity": "sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^8.0.0",
|
||||
"@babel/generator": "^8.0.0",
|
||||
"@babel/helper-compilation-targets": "^8.0.0",
|
||||
"@babel/helpers": "^8.0.0",
|
||||
"@babel/parser": "^8.0.0",
|
||||
"@babel/template": "^8.0.0",
|
||||
"@babel/traverse": "^8.0.0",
|
||||
"@babel/types": "^8.0.0",
|
||||
"@types/gensync": "^1.0.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"empathic": "^2.0.1",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
"import-meta-resolve": "^4.2.0",
|
||||
"json5": "^2.2.3",
|
||||
"obug": "^2.1.1",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/babel"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/generator": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz",
|
||||
"integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^8.0.0",
|
||||
"@babel/types": "^8.0.0",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"@types/jsesc": "^2.5.0",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.0.tgz",
|
||||
"integrity": "sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^8.0.0",
|
||||
"@babel/helper-validator-option": "^8.0.0",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^11.0.0",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/helper-globals": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.0.tgz",
|
||||
"integrity": "sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/helper-string-parser": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
|
||||
"integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.2.tgz",
|
||||
"integrity": "sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/helper-validator-option": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz",
|
||||
"integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/helpers": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.0.tgz",
|
||||
"integrity": "sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^8.0.0",
|
||||
"@babel/types": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/parser": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0.tgz",
|
||||
"integrity": "sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^8.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/template": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz",
|
||||
"integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^8.0.0",
|
||||
"@babel/parser": "^8.0.0",
|
||||
"@babel/types": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/traverse": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.0.tgz",
|
||||
"integrity": "sha512-bxTj/W2VclGE6CctlfQOpxg8MPDzXArRqkOBePw8EHfebcjF7fETWSS3BriEECo+UiU/Yblq+xUtSImFu7cTbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^8.0.0",
|
||||
"@babel/generator": "^8.0.0",
|
||||
"@babel/helper-globals": "^8.0.0",
|
||||
"@babel/parser": "^8.0.0",
|
||||
"@babel/template": "^8.0.0",
|
||||
"@babel/types": "^8.0.0",
|
||||
"obug": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/@babel/types": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0.tgz",
|
||||
"integrity": "sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^8.0.0",
|
||||
"@babel/helper-validator-identifier": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.11.0"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/js-tokens": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"apps/trust/node_modules/lru-cache": {
|
||||
"version": "11.5.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
|
||||
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"apps/trust/node_modules/zod": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"examples/cookie-banner-react": {
|
||||
"name": "@probo/example-cookie-banner-react",
|
||||
"version": "0.0.0",
|
||||
@@ -4826,10 +4540,6 @@
|
||||
"resolved": "packages/skills",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@probo/trust": {
|
||||
"resolved": "apps/trust",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@probo/tsconfig": {
|
||||
"resolved": "packages/tsconfig",
|
||||
"link": true
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
export function getCompliancePortalUrl(path: string): string {
|
||||
const currentPath = window.location.pathname;
|
||||
const trustMatch = currentPath.match(/^\/trust\/([^/]+)/);
|
||||
|
||||
if (!trustMatch) {
|
||||
return `/${path}`;
|
||||
}
|
||||
|
||||
return `../${path}`;
|
||||
}
|
||||
@@ -122,7 +122,6 @@ export {
|
||||
fromMaxAgeSeconds,
|
||||
} from "./duration";
|
||||
export { getTrackerTypeBadge, getTrackerSourceBadge } from "./tracker";
|
||||
export { getCompliancePortalUrl } from "./compliancePortalUrl";
|
||||
export { detectSocialName } from "./socialUrl";
|
||||
export { formatError, type GraphQLError } from "./error";
|
||||
export { Role, roles, getAssignableRoles } from "./roles";
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"sources": {
|
||||
"apps/console/src/pages/iam": "iam",
|
||||
"apps/console/src": "core",
|
||||
"apps/trust/src": "trust",
|
||||
"apps/compliance-portal/src": "complianceportal"
|
||||
},
|
||||
"projects": {
|
||||
@@ -43,16 +42,6 @@
|
||||
"Map": "Record<string, string>"
|
||||
}
|
||||
},
|
||||
"trust": {
|
||||
"schema": "pkg/server/api/complianceportal/v1/schema.graphql",
|
||||
"language": "typescript",
|
||||
"noFutureProofEnums": true,
|
||||
"customScalarTypes": {
|
||||
"Datetime": "string",
|
||||
"CursorKey": "string",
|
||||
"EmailAddr": "string"
|
||||
}
|
||||
},
|
||||
"complianceportal": {
|
||||
"schema": "pkg/server/api/complianceportal/v1/schema.graphql",
|
||||
"language": "typescript",
|
||||
|
||||