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 ──────────────────────────────────────────────────
|
# ── Base URL & ports ──────────────────────────────────────────────────
|
||||||
# PROBOD_BASE_URL=http://localhost:8080
|
# PROBOD_BASE_URL=http://localhost:8080
|
||||||
# PROBOD_API_ADDR=: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_HTTP_ADDR=:10080
|
||||||
# PROBOD_TRUST_CENTER_HTTPS_ADDR=:443
|
# PROBOD_TRUST_CENTER_HTTPS_ADDR=:443
|
||||||
# PROBOD_TRUST_CENTER_BASE_DOMAIN=probopage.localhost
|
# PROBOD_TRUST_CENTER_BASE_DOMAIN=probopage.localhost
|
||||||
|
|||||||
11
.github/workflows/make.yaml
vendored
@@ -245,10 +245,9 @@ jobs:
|
|||||||
node: "false"
|
node: "false"
|
||||||
- name: "Create placeholder dist files"
|
- name: "Create placeholder dist files"
|
||||||
run: |
|
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/console/dist/index.html
|
||||||
echo dev-server > apps/compliance-portal/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
|
echo dev-server > packages/emails/dist/placeholder
|
||||||
- name: "Generate Go code"
|
- name: "Generate Go code"
|
||||||
run: |
|
run: |
|
||||||
@@ -310,10 +309,9 @@ jobs:
|
|||||||
- uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0
|
- uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0
|
||||||
- name: "Create placeholder dist files"
|
- name: "Create placeholder dist files"
|
||||||
run: |
|
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/console/dist/index.html
|
||||||
echo dev-server > apps/compliance-portal/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
|
echo dev-server > packages/emails/dist/placeholder
|
||||||
- name: "Generate Go code"
|
- name: "Generate Go code"
|
||||||
run: |
|
run: |
|
||||||
@@ -371,7 +369,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
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) | \
|
(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
|
reviewdog -f=eslint -reporter=github-pr-review -filter-mode=nofilter -name="eslint ($dir)" || true
|
||||||
done
|
done
|
||||||
@@ -389,10 +387,9 @@ jobs:
|
|||||||
- uses: "./.github/actions/setup"
|
- uses: "./.github/actions/setup"
|
||||||
- name: "Create placeholder dist files"
|
- name: "Create placeholder dist files"
|
||||||
run: |
|
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/console/dist/index.html
|
||||||
echo dev-server > apps/compliance-portal/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: "npm --workspace @probo/emails run build"
|
||||||
- run: "make test"
|
- run: "make test"
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ GENERATED= pkg/server/api/connect/v1/schema/schema.go \
|
|||||||
|
|
||||||
EMBEDDED= apps/console/dist/index.html \
|
EMBEDDED= apps/console/dist/index.html \
|
||||||
apps/compliance-portal/dist/index.html \
|
apps/compliance-portal/dist/index.html \
|
||||||
apps/trust/dist/index.html \
|
|
||||||
@probo/emails
|
@probo/emails
|
||||||
|
|
||||||
PROBOD_BIN_EXTRA_DEPS=
|
PROBOD_BIN_EXTRA_DEPS=
|
||||||
@@ -138,7 +137,7 @@ go-lint: generate
|
|||||||
$(GOLINTCMD) run ./...
|
$(GOLINTCMD) run ./...
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: generate apps/trust/dist/index.html
|
test: generate
|
||||||
test: CGO_ENABLED=1
|
test: CGO_ENABLED=1
|
||||||
test: ## Run tests with race detection and coverage (usage: make test [MODULE=./pkg/some/module])
|
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/))
|
$(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_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes"; \
|
||||||
PROBOD_AUTH_COOKIE_SECURE=false; \
|
PROBOD_AUTH_COOKIE_SECURE=false; \
|
||||||
PROBOD_OAUTH2_SERVER_SIGNING_KEY="$$($(CAT) $(CFG_DEV_OAUTH2_KEY))"; \
|
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_ADDR=localhost:5432; \
|
||||||
PROBOD_PG_USERNAME=postgres; \
|
PROBOD_PG_USERNAME=postgres; \
|
||||||
PROBOD_PG_PASSWORD=postgres; \
|
PROBOD_PG_PASSWORD=postgres; \
|
||||||
@@ -387,7 +386,7 @@ fmt-go: ## Format Go code
|
|||||||
clean: ## Clean the project (node_modules and build artifacts)
|
clean: ## Clean the project (node_modules and build artifacts)
|
||||||
$(RM) -rf bin/*
|
$(RM) -rf bin/*
|
||||||
$(RM) -rf node_modules
|
$(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 packages/emails/{dist,node_modules}
|
||||||
$(RM) -rf sbom-docker.json sbom.json
|
$(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
|
$(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" \
|
-e "s|PRIVATE_KEY_PLACEHOLDER|$$(awk 'NR==1 {printf "%s", $$0; next} {printf "\\\\n%s", $$0}' compose/keycloak/certs/private-key.pem)|g" \
|
||||||
$@.tmpl > $@
|
$@.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 $@)
|
$(MKDIR) $(dir $@)
|
||||||
$(ECHO) dev-server > $@
|
$(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`:
|
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/server/mailactions/templates/page.html.tmpl
|
||||||
pkg/cookiebanner/prompts/tracker_identification.txt.tmpl
|
pkg/cookiebanner/prompts/tracker_identification.txt.tmpl
|
||||||
pkg/probo/templates/risk_list.json.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
|
## 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
|
```sh
|
||||||
make relay # merge split schemas + clean + compile
|
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_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes" \
|
||||||
PROBOD_ENCRYPTION_KEY="thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \
|
PROBOD_ENCRYPTION_KEY="thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \
|
||||||
PROBOD_OAUTH2_SERVER_SIGNING_KEY="$(cat "${OAUTH2_SIGNING_KEY_PATH}")" \
|
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_ENDPOINT="http://127.0.0.1:8333" \
|
||||||
PROBOD_AWS_ACCESS_KEY_ID="probod" \
|
PROBOD_AWS_ACCESS_KEY_ID="probod" \
|
||||||
PROBOD_AWS_SECRET_ACCESS_KEY="thisisnotasecret" \
|
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
|
// 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
|
// sets below; everything else is ignored so a bare `eslint .` keeps the same
|
||||||
// scope as the previous per-workspace configs.
|
// 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 reactDirs = [...appDirs, "packages/ui/**", "packages/relay/**", "packages/routes/**"];
|
||||||
const lintedDirs = [...reactDirs, "packages/eslint-config/**"];
|
const lintedDirs = [...reactDirs, "packages/eslint-config/**"];
|
||||||
|
|
||||||
@@ -50,8 +50,8 @@ export default defineConfig([
|
|||||||
{
|
{
|
||||||
// compliance-portal mutates through the awaitable useMutation bound in
|
// compliance-portal mutates through the awaitable useMutation bound in
|
||||||
// #/lib/relay/useMutation (over @probo/relay's createUseMutation), never
|
// #/lib/relay/useMutation (over @probo/relay's createUseMutation), never
|
||||||
// react-relay's useMutation directly. Scoped to this app only: console and
|
// react-relay's useMutation directly. Scoped to this app only: console
|
||||||
// trust still use react-relay's useMutation.
|
// still uses react-relay's useMutation.
|
||||||
files: ["apps/compliance-portal/**"],
|
files: ["apps/compliance-portal/**"],
|
||||||
rules: {
|
rules: {
|
||||||
"no-restricted-imports": [
|
"no-restricted-imports": [
|
||||||
|
|||||||
290
package-lock.json
generated
@@ -594,292 +594,6 @@
|
|||||||
"url": "https://github.com/sponsors/colinhacks"
|
"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": {
|
"examples/cookie-banner-react": {
|
||||||
"name": "@probo/example-cookie-banner-react",
|
"name": "@probo/example-cookie-banner-react",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -4826,10 +4540,6 @@
|
|||||||
"resolved": "packages/skills",
|
"resolved": "packages/skills",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@probo/trust": {
|
|
||||||
"resolved": "apps/trust",
|
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"node_modules/@probo/tsconfig": {
|
"node_modules/@probo/tsconfig": {
|
||||||
"resolved": "packages/tsconfig",
|
"resolved": "packages/tsconfig",
|
||||||
"link": true
|
"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,
|
fromMaxAgeSeconds,
|
||||||
} from "./duration";
|
} from "./duration";
|
||||||
export { getTrackerTypeBadge, getTrackerSourceBadge } from "./tracker";
|
export { getTrackerTypeBadge, getTrackerSourceBadge } from "./tracker";
|
||||||
export { getCompliancePortalUrl } from "./compliancePortalUrl";
|
|
||||||
export { detectSocialName } from "./socialUrl";
|
export { detectSocialName } from "./socialUrl";
|
||||||
export { formatError, type GraphQLError } from "./error";
|
export { formatError, type GraphQLError } from "./error";
|
||||||
export { Role, roles, getAssignableRoles } from "./roles";
|
export { Role, roles, getAssignableRoles } from "./roles";
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"sources": {
|
"sources": {
|
||||||
"apps/console/src/pages/iam": "iam",
|
"apps/console/src/pages/iam": "iam",
|
||||||
"apps/console/src": "core",
|
"apps/console/src": "core",
|
||||||
"apps/trust/src": "trust",
|
|
||||||
"apps/compliance-portal/src": "complianceportal"
|
"apps/compliance-portal/src": "complianceportal"
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
@@ -43,16 +42,6 @@
|
|||||||
"Map": "Record<string, string>"
|
"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": {
|
"complianceportal": {
|
||||||
"schema": "pkg/server/api/complianceportal/v1/schema.graphql",
|
"schema": "pkg/server/api/complianceportal/v1/schema.graphql",
|
||||||
"language": "typescript",
|
"language": "typescript",
|
||||||
|
|||||||