feat: prefill agent-edit modal, Ctrl+Enter submits modals, improve README
- AgentFormModal: sync form state to current agent values when the modal opens (useEffect on open/agent), so edit always shows the agent's current name, description, website, icon URL, and max-pending value. - Modal: add optional onSubmit prop; Ctrl+Enter (or ⌘+Enter) fires it, covering AgentFormModal, DecisionModal, and both Settings modals (disable 2FA, delete account) without breaking textarea newlines or text-input behaviour. - README: substantially expanded — purpose, capabilities, architecture, quick-start with Docker Compose, dev setup, testing, agent integration (OpenClaw, generic MCP, REST), and privacy notes. - TODO.md: remove three completed items (prefill modal, Ctrl+Enter, human readme). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
> Review changes. Approve intent. Let agents proceed.
|
> Review changes. Approve intent. Let agents proceed.
|
||||||
|
|
||||||
Agents submit structured change requests containing code diffs, configuration updates, or custom values. Humans review and approve them in the browser, then agents verify the platform-signed decision through the API before applying anything.
|
Agents submit structured change requests containing code diffs, configuration updates, or custom values. Humans review and approve them in a browser UI; agents then verify the platform-signed decision through the API before applying anything. Packaged as a single `docker compose` stack — no infrastructure beyond Postgres required.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -12,110 +12,204 @@ Agents submit structured change requests containing code diffs, configuration up
|
|||||||
|
|
||||||
```
|
```
|
||||||
Agent PatchPass Human
|
Agent PatchPass Human
|
||||||
│ create_request ───────────► │ │
|
│ create_request ──────────► │ │
|
||||||
│ (diffs/config/custom) │ normalize · hash · store │
|
│ (diffs/config/custom) │ normalize · hash · store │
|
||||||
│ ◄─── request_id + URL ────────│ ──── notify (WS) ──────────► │ reviews in browser
|
│ ◄── request_id + URL ────────│ ──── notify (WebSocket) ───► │ reviews in browser
|
||||||
│ │ │ APPROVE / REJECT / REQUEST_CHANGES
|
│ │ │ APPROVE / REJECT / REQUEST_CHANGES
|
||||||
│ get_request ──────────────► │ ◄──── signed decision ──────│
|
│ get_request ─────────────► │ ◄──── signed decision ──────│
|
||||||
│ ◄─── state + receipt ─────────│ │
|
│ ◄── state + receipt ─────────│ │
|
||||||
│ consume_approval ─────────► │ mark CONSUMED (single-use) │
|
│ consume_approval ─────────► │ mark CONSUMED (single-use) │
|
||||||
│ ◄─── receipt (HMAC) ──────────│ │
|
│ ◄── receipt (HMAC-SHA256) ───│ │
|
||||||
│ apply changes │ │
|
│ apply changes │ │
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Decisions are platform-signed.** Approvals and rejections carry an HMAC-SHA256 receipt bound to the exact reviewed content (`content_hash`). If the agent changes anything after approval, the receipt no longer matches.
|
**Key guarantees:**
|
||||||
- **Approvals are single-use.** An agent must `consume` an approval before proceeding; it cannot be replayed.
|
|
||||||
- **Two ways in for agents:** a REST API (`/v1/...`) and an MCP server (`/mcp`), both authenticated with a per-agent API key and backed by the same service layer.
|
|
||||||
|
|
||||||
## Request states
|
- **Platform-signed decisions.** Every approval and rejection carries an HMAC-SHA256 receipt bound to `content_hash` — the hash of the exact content the human reviewed. If the agent changes anything after approval, the receipt no longer matches.
|
||||||
|
- **Single-use approvals.** An agent must `consume` an approval before proceeding; the same receipt cannot be replayed.
|
||||||
|
- **Two integration paths.** Agents connect via a streaming MCP server (`/mcp`) or a plain REST API (`/v1/...`), both authenticated with a per-agent API key and backed by the same service layer.
|
||||||
|
- **Changes-requested loop.** A human can return a request to the agent with reviewer notes; the agent updates and resubmits, and the updated diff is highlighted in the UI.
|
||||||
|
|
||||||
`PENDING` · `CHANGES_REQUESTED` · `APPROVED` · `REJECTED` · `EXPIRED` · `CONSUMED` · `CANCELLED`
|
---
|
||||||
|
|
||||||
No update is possible after approval, rejection, expiry, consumption, or cancellation. A `REQUEST_CHANGES` decision requires a comment and returns the request to the agent, which can `update_request` to resubmit (highlighted in the UI as **UPDATED**).
|
## Request lifecycle
|
||||||
|
|
||||||
|
| State | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `PENDING` | Submitted, awaiting human review |
|
||||||
|
| `CHANGES_REQUESTED` | Returned to agent with reviewer notes |
|
||||||
|
| `APPROVED` | Approved; agent must consume before acting |
|
||||||
|
| `REJECTED` | Hard block; agent must submit a new request |
|
||||||
|
| `EXPIRED` | Not reviewed within the expiry window |
|
||||||
|
| `CONSUMED` | Approval used; agent has proceeded |
|
||||||
|
| `CANCELLED` | Cancelled by agent or human |
|
||||||
|
|
||||||
|
No mutation is possible after `APPROVED`, `REJECTED`, `EXPIRED`, `CONSUMED`, or `CANCELLED`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- **Three change types:** `diff` (unified diffs), `config` (before/after key-value), `custom` (arbitrary structured values)
|
||||||
|
- **Real-time notifications** via WebSocket — the browser updates instantly when a new request arrives
|
||||||
|
- **TOTP 2FA** for human accounts
|
||||||
|
- **Admin panel** — global on/off switches for registration and request submission, user management, audit log for admin views of request payloads
|
||||||
|
- **Data export and account deletion** — humans can download all their data as JSON and delete their account from Settings
|
||||||
|
- **Auto-delete policy** — humans configure how long completed requests are retained
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Limits
|
## Limits
|
||||||
|
|
||||||
| Limit | Value |
|
| Limit | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Requests per agent per hour | 15 |
|
| Requests per agent per hour | 15 |
|
||||||
| Agents per human | 5 |
|
| Agents per human account | 5 |
|
||||||
| Simultaneous pending requests per agent | 1–10 (human-configured, default 5) |
|
| Simultaneous pending requests per agent | 1–10 (human-configured, default 5) |
|
||||||
| Request expiry | default 30 min, max 12 h |
|
| Request expiry window | 60 s – 12 h (default 30 min) |
|
||||||
|
| Auto-delete retention | ≥ 7 days |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Tech stack
|
## Tech stack
|
||||||
|
|
||||||
| Layer | Technology |
|
| Layer | Technology |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Backend | Node.js + TypeScript, [rjweb-server](https://server.rjweb.dev) |
|
| Backend | Node.js 24 + TypeScript (strict), [rjweb-server](https://server.rjweb.dev) |
|
||||||
| ORM | Prisma 7 (`@prisma/adapter-pg`) |
|
| ORM | Prisma 7 (`@prisma/adapter-pg`) |
|
||||||
| Database | PostgreSQL |
|
| Database | PostgreSQL 16 |
|
||||||
| Frontend | React + TypeScript + Tailwind CSS (Vite), served by the backend |
|
| Frontend | React 18 + TypeScript + Tailwind CSS, built by Vite and served by the backend |
|
||||||
| Realtime | rjweb WebSocket channels |
|
| Realtime | rjweb WebSocket channels |
|
||||||
| Tests | Vitest |
|
| Package manager | pnpm (Backend and UI are separate workspaces) |
|
||||||
|
| Tests | Vitest (integration, hits a real test database) |
|
||||||
|
| CI | Gitea Actions — lint · test · build Docker image |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
```
|
```
|
||||||
Backend/ Node + TypeScript API (rjweb-server) + Prisma
|
Backend/
|
||||||
src/
|
src/
|
||||||
index.ts server bootstrap + static UI serving
|
index.ts server bootstrap, static UI serving, SPA fallback
|
||||||
lib/ service layer, auth, signing, limits, crons, MCP tools
|
lib/
|
||||||
routes/ REST (/api, /v1), MCP (/mcp), WebSocket (/api/ws)
|
ChangeRequestService.ts request lifecycle — all business logic lives here
|
||||||
|
AgentService.ts agent CRUD
|
||||||
|
Authentication.ts session (human) and API-key (agent) resolution
|
||||||
|
Signing.ts HMAC-SHA256 approval receipts
|
||||||
|
ChangeNormalization.ts diff normalization and content hashing
|
||||||
|
Limits.ts rate, pending, and agent-count enforcement
|
||||||
|
Notifications.ts notification fan-out
|
||||||
|
WsHub.ts WebSocket connection registry
|
||||||
|
SystemCrons.ts expiry and auto-delete background jobs
|
||||||
|
DataManager.ts global (admin) settings
|
||||||
|
mcp/ MCP JSON-RPC server and tool definitions
|
||||||
|
middlewares/
|
||||||
|
RouteAuth.ts
|
||||||
|
routes/
|
||||||
|
api/ human-facing REST — auth, account, agents, requests, notifications, admin
|
||||||
|
v1/ agent-facing REST — /v1/change-requests
|
||||||
|
mcp.ts MCP endpoint — /mcp
|
||||||
|
ws.ts WebSocket — /api/ws/notifications
|
||||||
tests/ Vitest integration tests
|
tests/ Vitest integration tests
|
||||||
prisma/schema.prisma
|
prisma/
|
||||||
UI/ React + Tailwind frontend (builds to UI/build)
|
schema.prisma
|
||||||
Dockerfile multi-stage build (UI + backend)
|
migrations/
|
||||||
docker-compose.yml backend + postgres
|
UI/ React + Tailwind frontend (Vite), builds to UI/build/
|
||||||
.gitea/workflows/ CI: build · lint · test · push image
|
Dockerfile multi-stage build (UI then backend)
|
||||||
|
docker-compose.yml backend + Postgres
|
||||||
|
example.env
|
||||||
|
.gitea/workflows/deploy.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running locally
|
---
|
||||||
|
|
||||||
Prerequisites: Node 24+, pnpm, and Postgres (or Docker).
|
## Quick start (Docker Compose)
|
||||||
|
|
||||||
|
The simplest way to run PatchPass is with the pre-built image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Start Postgres (Docker)
|
# 1. Copy and edit the environment file
|
||||||
|
cp example.env .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `.env` and set at minimum:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate a strong secret: openssl rand -hex 32
|
||||||
|
INSTANCE_SECRET=<random 32-byte hex>
|
||||||
|
|
||||||
|
# Must point to the 'database' service in docker-compose.yml
|
||||||
|
DATABASE_URL=postgresql://patchpass:patchpass@db:5432/patchpass
|
||||||
|
|
||||||
|
# Public URL of your instance (used in notification links)
|
||||||
|
UI_URL=https://your-host
|
||||||
|
DOMAIN=your-host
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 2. Start
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# The backend applies migrations on boot and serves the UI.
|
||||||
|
# Open http://localhost:5000 — the first account to register becomes admin.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development setup
|
||||||
|
|
||||||
|
Prerequisites: **Node.js 24+**, **pnpm**, and PostgreSQL (or Docker for the database).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start a local Postgres with Docker
|
||||||
docker run -d --name patchpass-db \
|
docker run -d --name patchpass-db \
|
||||||
-e POSTGRES_USER=patchpass -e POSTGRES_PASSWORD=patchpass -e POSTGRES_DB=patchpass \
|
-e POSTGRES_USER=patchpass -e POSTGRES_PASSWORD=patchpass -e POSTGRES_DB=patchpass \
|
||||||
-p 5433:5432 postgres:16-alpine
|
-p 5433:5432 postgres:16-alpine
|
||||||
|
|
||||||
# 2. Backend
|
# Backend
|
||||||
cd Backend
|
cd Backend
|
||||||
cp ../example.env .env # then edit DATABASE_URL / INSTANCE_SECRET
|
cp ../example.env .env # edit DATABASE_URL to point at localhost:5433
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm generate
|
pnpm generate # generate Prisma client types
|
||||||
pnpm migrate # apply migrations
|
pnpm migrate # apply database migrations
|
||||||
pnpm dev # http://localhost:5000
|
pnpm dev # http://localhost:5000
|
||||||
|
|
||||||
# 3. UI (separate terminal, for hot-reload dev)
|
# UI — hot-reload dev server (separate terminal)
|
||||||
cd UI
|
cd UI
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm dev # http://localhost:3000 (proxies /api to :5000)
|
pnpm dev # http://localhost:3000, proxies /api and /v1 to :5000
|
||||||
```
|
```
|
||||||
|
|
||||||
For a production-style run, `cd UI && pnpm build` — the backend then serves `UI/build` at `/`.
|
For a production-style build without Docker, `cd UI && pnpm build` — the backend then serves `UI/build/` at `/`.
|
||||||
|
|
||||||
### Tests
|
### Running tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd Backend
|
cd Backend
|
||||||
pnpm test # spins up against the test database (see vitest.config.ts)
|
pnpm test # Vitest integration suite — spins up against the test database
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploying
|
The test database URL is configured in `Backend/vitest.config.ts`. Tests hit a real database; there are no mocks at the DB layer.
|
||||||
|
|
||||||
|
### Before committing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp example.env .env # set DATABASE_URL=...@db:5432/..., a strong INSTANCE_SECRET
|
cd Backend && pnpm lint # ESLint (TypeScript strict)
|
||||||
docker compose up -d --build
|
cd Backend && pnpm test # integration tests
|
||||||
|
cd UI && pnpm lint # ESLint
|
||||||
|
cd UI && pnpm build # Vite production build check
|
||||||
```
|
```
|
||||||
|
|
||||||
The backend applies migrations on boot and serves the UI. The first account to register becomes the **admin**.
|
---
|
||||||
|
|
||||||
## Connecting an agent
|
## Connecting an agent
|
||||||
|
|
||||||
Create an agent in the UI, then use its API key.
|
Create an agent in the UI (**Agents → New agent**), copy the API key shown once at creation, then configure your agent client.
|
||||||
|
|
||||||
**OpenClaw** — add to `~/.openclaw/openclaw.json`:
|
### OpenClaw
|
||||||
|
|
||||||
|
Add to `~/.openclaw/openclaw.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -131,20 +225,55 @@ Create an agent in the UI, then use its API key.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**REST**:
|
### Generic MCP client (Claude Desktop, Cursor, etc.)
|
||||||
|
|
||||||
```bash
|
```json
|
||||||
curl -X POST https://your-host/v1/change-requests \
|
{
|
||||||
-H "x-api-key: pp_agent_..." -H "Content-Type: application/json" \
|
"mcpServers": {
|
||||||
-d '{"title":"Bump timeout","changes":[{"type":"config","path":"timeout","before":30,"after":60,"content_type":"integer"}]}'
|
"patchpass": {
|
||||||
|
"type": "streamable-http",
|
||||||
|
"url": "https://your-host/mcp",
|
||||||
|
"headers": { "x-api-key": "pp_agent_..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Tell your agent: *before taking any consequential action, call `create_request`, wait for approval, then `consume_approval` before proceeding.*
|
### REST API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Submit a change request
|
||||||
|
curl -X POST https://your-host/v1/change-requests \
|
||||||
|
-H "x-api-key: pp_agent_..." \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"title": "Bump deployment timeout",
|
||||||
|
"changes": [
|
||||||
|
{ "type": "config", "path": "timeout", "before": 30, "after": 60, "content_type": "integer" }
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Poll for the decision
|
||||||
|
curl https://your-host/v1/change-requests/{request_id} -H "x-api-key: pp_agent_..."
|
||||||
|
|
||||||
|
# Consume the approval before applying the change
|
||||||
|
curl -X POST https://your-host/v1/change-requests/{request_id}/consume -H "x-api-key: pp_agent_..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agent instruction
|
||||||
|
|
||||||
|
Tell your agent:
|
||||||
|
|
||||||
|
> Before taking any consequential action (deploys, config changes, code edits), call `create_request`, wait for approval, then call `consume_approval` before proceeding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Privacy & data
|
## Privacy & data
|
||||||
|
|
||||||
Humans can export all their data as JSON and delete their account (cascading to agents, requests, and notifications) from **Settings**. Agent icon URLs are fetched once for validation and never stored. See the in-app Privacy Policy and Terms of Service.
|
Humans can export all their data as JSON and delete their account (cascading to agents, requests, and notifications) from **Settings**. Agent icon URLs are fetched once for validation and never stored. See the in-app Privacy Policy and Terms of Service.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT-0
|
MIT-0
|
||||||
|
|||||||
@@ -9,14 +9,11 @@ Thoughts:
|
|||||||
- Able to click on a agent either on home, agents list or in a request to show the agents past requests and overall actions from the list but moved over into a new page with a back button to return to the previous page.
|
- Able to click on a agent either on home, agents list or in a request to show the agents past requests and overall actions from the list but moved over into a new page with a back button to return to the previous page.
|
||||||
- Show QR for 2fa
|
- Show QR for 2fa
|
||||||
- invalid dates on admin action notifications
|
- invalid dates on admin action notifications
|
||||||
- pre fill data on agent edit modal
|
|
||||||
- overall ux improvements
|
- overall ux improvements
|
||||||
- Fix popup for passwordmanagers filling in 2fa code on settings page
|
- Fix popup for passwordmanagers filling in 2fa code on settings page
|
||||||
- ctrl enter submits modals (all)
|
|
||||||
- better repo rules for agents
|
- better repo rules for agents
|
||||||
- Nicer statistics on the dashboard
|
- Nicer statistics on the dashboard
|
||||||
- Instance Notice popup that popups up on every session (like a cookie notice), content by admins, dismissable
|
- Instance Notice popup that popups up on every session (like a cookie notice), content by admins, dismissable
|
||||||
- Instance Disabled message & setting for admins to set a message for when the instance is disabled, and a setting to allow users to see the message when the instance is disabled. (users can only delte or download their data when the instance is disabled)
|
- Instance Disabled message & setting for admins to set a message for when the instance is disabled, and a setting to allow users to see the message when the instance is disabled. (users can only delte or download their data when the instance is disabled)
|
||||||
- Data Hoster notice for footer when people host an instance for other people to use, content by admins, not dismissable footer only notice, all pages
|
- Data Hoster notice for footer when people host an instance for other people to use, content by admins, not dismissable footer only notice, all pages
|
||||||
- mobile friendly improvements
|
- mobile friendly improvements
|
||||||
- human readme
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { agents as agentsApi } from "../api/client";
|
import { agents as agentsApi } from "../api/client";
|
||||||
import type { Agent } from "../api/types";
|
import type { Agent } from "../api/types";
|
||||||
@@ -27,6 +27,15 @@ export function AgentFormModal({
|
|||||||
const [maxPending, setMaxPending] = useState(agent?.max_pending_requests ?? 5);
|
const [maxPending, setMaxPending] = useState(agent?.max_pending_requests ?? 5);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setName(agent?.name ?? "");
|
||||||
|
setDescription(agent?.description ?? "");
|
||||||
|
setWebsite(agent?.website ?? "");
|
||||||
|
setIconUrl(agent?.icon_url ?? "");
|
||||||
|
setMaxPending(agent?.max_pending_requests ?? 5);
|
||||||
|
}, [open, agent]);
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!name.trim()) return toast.error("Name is required");
|
if (!name.trim()) return toast.error("Name is required");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -55,6 +64,7 @@ export function AgentFormModal({
|
|||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
onSubmit={submit}
|
||||||
title={editing ? "Edit agent" : "New agent"}
|
title={editing ? "Edit agent" : "New agent"}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export function DecisionModal({
|
|||||||
<Modal
|
<Modal
|
||||||
open={!!decision}
|
open={!!decision}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
onSubmit={submit}
|
||||||
title={m.title}
|
title={m.title}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ReactNode, useEffect } from "react";
|
|||||||
export function Modal({
|
export function Modal({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
|
onSubmit,
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
footer,
|
footer,
|
||||||
@@ -10,6 +11,7 @@ export function Modal({
|
|||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onSubmit?: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
@@ -19,6 +21,7 @@ export function Modal({
|
|||||||
if (!open) return;
|
if (!open) return;
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") onClose();
|
if (e.key === "Escape") onClose();
|
||||||
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && onSubmit) onSubmit();
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", onKey);
|
window.addEventListener("keydown", onKey);
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = "hidden";
|
||||||
@@ -26,7 +29,7 @@ export function Modal({
|
|||||||
window.removeEventListener("keydown", onKey);
|
window.removeEventListener("keydown", onKey);
|
||||||
document.body.style.overflow = "";
|
document.body.style.overflow = "";
|
||||||
};
|
};
|
||||||
}, [open, onClose]);
|
}, [open, onClose, onSubmit]);
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ export function SettingsPage() {
|
|||||||
<Modal
|
<Modal
|
||||||
open={disable2faOpen}
|
open={disable2faOpen}
|
||||||
onClose={() => setDisable2faOpen(false)}
|
onClose={() => setDisable2faOpen(false)}
|
||||||
|
onSubmit={disable2fa}
|
||||||
title="Disable 2FA"
|
title="Disable 2FA"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
@@ -246,6 +247,7 @@ export function SettingsPage() {
|
|||||||
<Modal
|
<Modal
|
||||||
open={deleteOpen}
|
open={deleteOpen}
|
||||||
onClose={() => setDeleteOpen(false)}
|
onClose={() => setDeleteOpen(false)}
|
||||||
|
onSubmit={deleteAccount}
|
||||||
title="Delete account"
|
title="Delete account"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user