From e89f7b4cf3ac39db023f1d1334fe3ee3d3aab17f Mon Sep 17 00:00:00 2001 From: CyberRex <26585194+CyberRex0@users.noreply.github.com> Date: Wed, 27 May 2026 09:06:00 +0900 Subject: [PATCH] =?UTF-8?q?=E9=80=9A=E7=9F=A5=E3=82=BF=E3=82=A4=E3=83=9F?= =?UTF-8?q?=E3=83=B3=E3=82=B0=E3=81=AE=E8=BF=BD=E5=8A=A0=E3=83=BB=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E3=82=92=E3=83=A2=E3=83=BC=E3=83=80=E3=83=AB=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- development_status.md | 3 + src/client/routes/SiteSettingsPanel.jsx | 234 ++++++++++++++++++------ src/client/styles/app.css | 44 ++++- 3 files changed, 220 insertions(+), 61 deletions(-) diff --git a/development_status.md b/development_status.md index 9eb3270..1f2fb65 100644 --- a/development_status.md +++ b/development_status.md @@ -229,7 +229,10 @@ pnpm monitor:worker - 証明書情報として発行元、発行日時、失効日時を表示 - 通知タイミング設定 - 時間 / 日 / 週間の単位指定 + - 通知タイミング未設定時の空状態表示 + - モーダルでの通知タイミング追加・編集 - 複数タイミングの追加・確認ダイアログ付き削除 + - 通知タイミング0件での設定保存 - アプリ内アラート必須表示 - Webhook 選択 - プッシュ通知フラグ設定 diff --git a/src/client/routes/SiteSettingsPanel.jsx b/src/client/routes/SiteSettingsPanel.jsx index 20317ff..1e9e72e 100644 --- a/src/client/routes/SiteSettingsPanel.jsx +++ b/src/client/routes/SiteSettingsPanel.jsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; -import { ArrowLeft, Bell, Plus, Save, Trash2 } from 'lucide-react'; +import * as Dialog from '@radix-ui/react-dialog'; +import { ArrowLeft, Bell, Pencil, Plus, Save, Trash2 } from 'lucide-react'; import { request } from '../api/client.js'; import { ConfirmDialog } from '../components/ConfirmDialog.jsx'; import { Field } from '../components/Field.jsx'; @@ -22,7 +23,9 @@ function toDisplayThreshold(thresholdHours) { } function toThresholdHours(condition) { - return Number.parseInt(condition.value, 10) * unitToHours[condition.unit]; + const value = String(condition.value).trim(); + if (!/^\d+$/.test(value)) return Number.NaN; + return Number.parseInt(value, 10) * unitToHours[condition.unit]; } function formatCertificateValue(value) { @@ -32,10 +35,22 @@ function formatCertificateValue(value) { return date.toLocaleString(); } +function formatCondition(condition) { + const unitLabels = { + hours: '時間前', + days: '日前', + weeks: '週間前', + }; + return `${condition.value}${unitLabels[condition.unit]}`; +} + export function SiteSettingsPanel({ site, onBack }) { const [alias, setAlias] = useState(site.alias); const [savedAlias, setSavedAlias] = useState(site.alias); - const [conditions, setConditions] = useState([{ value: 7, unit: 'days' }]); + const [conditions, setConditions] = useState([]); + const [timingDialogOpen, setTimingDialogOpen] = useState(false); + const [editingConditionIndex, setEditingConditionIndex] = useState(null); + const [timingForm, setTimingForm] = useState({ value: '', unit: 'days' }); const [availableWebhooks, setAvailableWebhooks] = useState([]); const [webhookMethodIds, setWebhookMethodIds] = useState([]); const [pushEnabled, setPushEnabled] = useState(false); @@ -66,21 +81,65 @@ export function SiteSettingsPanel({ site, onBack }) { ); setWebhookMethodIds(data.settings.conditions[0].webhookMethodIds); setPushEnabled(data.settings.conditions[0].pushEnabled); + } else { + setConditions([]); + setWebhookMethodIds([]); + setPushEnabled(false); } } loadSettings().catch((err) => showToast({ type: 'error', message: err.message })); }, [showToast, site.siteId]); - function updateCondition(index, patch) { - setConditions((current) => - current.map((condition, currentIndex) => - currentIndex === index ? { ...condition, ...patch } : condition, - ), - ); + function openAddConditionDialog() { + setEditingConditionIndex(null); + setTimingForm({ value: '', unit: 'days' }); + setTimingDialogOpen(true); } - function addCondition() { - setConditions((current) => [...current, { value: 7, unit: 'days' }]); + function openEditConditionDialog(index) { + setEditingConditionIndex(index); + setTimingForm({ + value: String(conditions[index].value), + unit: conditions[index].unit, + }); + setTimingDialogOpen(true); + } + + function saveTimingCondition(event) { + event.preventDefault(); + event.stopPropagation(); + + const thresholdHours = toThresholdHours(timingForm); + if (!Number.isInteger(thresholdHours) || thresholdHours <= 0 || thresholdHours > 17520) { + showToast({ + type: 'error', + message: '通知タイミングは 1 時間以上、2 年以内で指定してください', + }); + return; + } + + const duplicated = conditions.some((condition, index) => { + if (index === editingConditionIndex) return false; + return toThresholdHours(condition) === thresholdHours; + }); + if (duplicated) { + showToast({ type: 'error', message: '同じ通知タイミングは重複して登録できません' }); + return; + } + + setConditions((current) => { + const nextCondition = { + value: Number.parseInt(String(timingForm.value).trim(), 10), + unit: timingForm.unit, + }; + if (editingConditionIndex === null) { + return [...current, nextCondition]; + } + return current.map((condition, index) => + index === editingConditionIndex ? nextCondition : condition, + ); + }); + setTimingDialogOpen(false); } function removeCondition(index) { @@ -108,6 +167,9 @@ export function SiteSettingsPanel({ site, onBack }) { if (thresholdHours.some((value) => !Number.isInteger(value) || value <= 0 || value > 17520)) { throw new Error('通知タイミングは 1 時間以上、2 年以内で指定してください'); } + if (new Set(thresholdHours).size !== thresholdHours.length) { + throw new Error('同じ通知タイミングは重複して登録できません'); + } if (nextAlias !== savedAlias) { await request(`/api/sites/${site.siteId}`, { method: 'PATCH', @@ -116,14 +178,20 @@ export function SiteSettingsPanel({ site, onBack }) { setAlias(nextAlias); setSavedAlias(nextAlias); } - await request(`/api/sites/${site.siteId}/settings`, { - method: 'PUT', - body: JSON.stringify({ - conditions: thresholdHours.map((value) => ({ thresholdHours: value })), - webhookMethodIds, - pushEnabled, - }), - }); + if (thresholdHours.length === 0) { + await request(`/api/sites/${site.siteId}/settings`, { method: 'DELETE' }); + setWebhookMethodIds([]); + setPushEnabled(false); + } else { + await request(`/api/sites/${site.siteId}/settings`, { + method: 'PUT', + body: JSON.stringify({ + conditions: thresholdHours.map((value) => ({ thresholdHours: value })), + webhookMethodIds, + pushEnabled, + }), + }); + } showToast({ type: 'success', message: '設定を保存しました' }); } catch (err) { showToast({ type: 'error', message: err.message }); @@ -136,7 +204,7 @@ export function SiteSettingsPanel({ site, onBack }) { setBusy(true); try { await request(`/api/sites/${site.siteId}/settings`, { method: 'DELETE' }); - setConditions([{ value: 7, unit: 'days' }]); + setConditions([]); setWebhookMethodIds([]); setPushEnabled(false); showToast({ type: 'success', message: '設定を削除しました' }); @@ -203,53 +271,103 @@ export function SiteSettingsPanel({ site, onBack }) {
通知タイミングはまだ設定されていません。
+ ) : ( + conditions.map((condition, index) => ( +