Webhookの編集をモーダル化

This commit is contained in:
CyberRex
2026-05-27 09:17:36 +09:00
parent e89f7b4cf3
commit 2a4050d442
2 changed files with 81 additions and 16 deletions

View File

@@ -1,6 +1,6 @@
# CertRemind 開発進捗 # CertRemind 開発進捗
最終更新: 2026-05-25 最終更新: 2026-05-27
## 現在の実装状況 ## 現在の実装状況
@@ -244,7 +244,7 @@ pnpm monitor:worker
- 確認ダイアログ付き履歴削除 - 確認ダイアログ付き履歴削除
- 通知方法管理画面 - 通知方法管理画面
- Webhook 登録 - Webhook 登録
- Webhook 編集 - モーダルでの Webhook 編集
- 確認ダイアログ付き Webhook 削除 - 確認ダイアログ付き Webhook 削除
- ブラウザ Push 通知の許可状態表示 - ブラウザ Push 通知の許可状態表示
- 現在のブラウザの Push 登録状態表示 - 現在のブラウザの Push 登録状態表示

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import * as Dialog from '@radix-ui/react-dialog';
import { ArrowLeft, BellOff, BellRing, Link, Pencil, Plus, Trash2 } from 'lucide-react'; import { ArrowLeft, BellOff, BellRing, Link, Pencil, Plus, Trash2 } from 'lucide-react';
import { request } from '../api/client.js'; import { request } from '../api/client.js';
import { ConfirmDialog } from '../components/ConfirmDialog.jsx'; import { ConfirmDialog } from '../components/ConfirmDialog.jsx';
@@ -56,7 +57,9 @@ export function NotificationMethodsView({ onBack }) {
const [vapidPublicKey, setVapidPublicKey] = useState(''); const [vapidPublicKey, setVapidPublicKey] = useState('');
const [currentPushStatus, setCurrentPushStatus] = useState('unchecked'); const [currentPushStatus, setCurrentPushStatus] = useState('unchecked');
const [form, setForm] = useState({ alias: '', url: '' }); const [form, setForm] = useState({ alias: '', url: '' });
const [editingId, setEditingId] = useState(''); const [editingWebhook, setEditingWebhook] = useState(null);
const [editForm, setEditForm] = useState({ alias: '', url: '' });
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [permission, setPermission] = useState( const [permission, setPermission] = useState(
typeof Notification === 'undefined' ? 'unsupported' : Notification.permission, typeof Notification === 'undefined' ? 'unsupported' : Notification.permission,
); );
@@ -121,19 +124,12 @@ export function NotificationMethodsView({ onBack }) {
setBusy(true); setBusy(true);
try { try {
validateWebhookForm(form); validateWebhookForm(form);
const endpoint = editingId await request('/api/notification-methods/webhooks', {
? `/api/notification-methods/webhooks/${editingId}` method: 'POST',
: '/api/notification-methods/webhooks';
await request(endpoint, {
method: editingId ? 'PATCH' : 'POST',
body: JSON.stringify({ alias: form.alias.trim(), url: form.url.trim() }), body: JSON.stringify({ alias: form.alias.trim(), url: form.url.trim() }),
}); });
setForm({ alias: '', url: '' }); setForm({ alias: '', url: '' });
setEditingId(''); showToast({ type: 'success', message: 'Webhookを登録しました' });
showToast({
type: 'success',
message: editingId ? 'Webhookを更新しました' : 'Webhookを登録しました',
});
await loadMethods(); await loadMethods();
} catch (err) { } catch (err) {
showToast({ type: 'error', message: err.message }); showToast({ type: 'error', message: err.message });
@@ -143,8 +139,38 @@ export function NotificationMethodsView({ onBack }) {
} }
function startEdit(webhook) { function startEdit(webhook) {
setEditingId(webhook.notificationMethodId); setEditingWebhook(webhook);
setForm({ alias: webhook.alias, url: webhook.url }); setEditForm({ alias: webhook.alias, url: webhook.url });
setEditDialogOpen(true);
}
function handleEditDialogOpenChange(open) {
setEditDialogOpen(open);
if (!open) {
setEditingWebhook(null);
setEditForm({ alias: '', url: '' });
}
}
async function submitWebhookEdit(event) {
event.preventDefault();
if (!editingWebhook) return;
setBusy(true);
try {
validateWebhookForm(editForm);
await request(`/api/notification-methods/webhooks/${editingWebhook.notificationMethodId}`, {
method: 'PATCH',
body: JSON.stringify({ alias: editForm.alias.trim(), url: editForm.url.trim() }),
});
showToast({ type: 'success', message: 'Webhookを更新しました' });
handleEditDialogOpenChange(false);
await loadMethods();
} catch (err) {
showToast({ type: 'error', message: err.message });
} finally {
setBusy(false);
}
} }
async function deleteWebhook(methodId) { async function deleteWebhook(methodId) {
@@ -273,11 +299,50 @@ export function NotificationMethodsView({ onBack }) {
</Field> </Field>
<button className="primary" disabled={busy}> <button className="primary" disabled={busy}>
<Plus aria-hidden="true" size={18} /> <Plus aria-hidden="true" size={18} />
{editingId ? '更新' : '登録'} 登録
</button> </button>
</div> </div>
</form> </form>
<Dialog.Root open={editDialogOpen} onOpenChange={handleEditDialogOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<Dialog.Title className="dialog-title">Webhookを編集</Dialog.Title>
<form className="dialog-form" onSubmit={submitWebhookEdit}>
<Field label="エイリアス名">
<input
value={editForm.alias}
onChange={(event) => setEditForm({ ...editForm, alias: event.target.value })}
placeholder="Slack 通知"
maxLength="120"
required
/>
</Field>
<Field label="URL">
<input
value={editForm.url}
onChange={(event) => setEditForm({ ...editForm, url: event.target.value })}
placeholder="https://hooks.slack.com/services/..."
maxLength="2048"
required
/>
</Field>
<div className="dialog-actions">
<Dialog.Close asChild>
<button className="secondary" type="button">
キャンセル
</button>
</Dialog.Close>
<button className="primary" disabled={busy || !editingWebhook}>
更新
</button>
</div>
</form>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<section className="panel"> <section className="panel">
<h2>登録済みWebhook</h2> <h2>登録済みWebhook</h2>
<div className="method-list"> <div className="method-list">