This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useConfig } from '../../hooks/useConfig';
|
||||
import { useUnsavedGuard } from '../../lib/unsavedGuard';
|
||||
@@ -81,6 +82,7 @@ function countDiff(a: any, b: any): number {
|
||||
}
|
||||
|
||||
export function ConfigForm({ section, isAdmin }: ConfigFormProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
// User-scoped sections use their own per-user APIs and should not load the
|
||||
// admin /api/config draft. Render them stand-alone without the global save bar.
|
||||
if (section === 'preferences') {
|
||||
@@ -93,7 +95,7 @@ export function ConfigForm({ section, isAdmin }: ConfigFormProps) {
|
||||
return <div className="max-w-2xl"><MemoryLearningForm /></div>;
|
||||
}
|
||||
if (!isAdmin) {
|
||||
return <div className="max-w-2xl text-sm text-slate-500">この設定は管理者のみ閲覧できます。</div>;
|
||||
return <div className="max-w-2xl text-sm text-slate-500">{t('configForm.adminOnly')}</div>;
|
||||
}
|
||||
// Local organizations: admin-managed via /api/admin/orgs (not config.yaml),
|
||||
// so render stand-alone without the global save bar.
|
||||
@@ -108,6 +110,7 @@ export function ConfigForm({ section, isAdmin }: ConfigFormProps) {
|
||||
}
|
||||
|
||||
function ConfigFormInner({ section }: ConfigFormProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
const { data, isLoading, error, refetch } = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -117,6 +120,7 @@ function ConfigFormInner({ section }: ConfigFormProps) {
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [toast, setToast] = useState<string | null>(null);
|
||||
const [toastIsError, setToastIsError] = useState(false);
|
||||
|
||||
// Sync fetched config into draft
|
||||
useEffect(() => {
|
||||
@@ -146,17 +150,19 @@ function ConfigFormInner({ section }: ConfigFormProps) {
|
||||
try {
|
||||
const result = await updateConfig(draft, etag);
|
||||
if (result.conflict) {
|
||||
if (confirm('設定が他で変更されました。再読み込みしますか?')) {
|
||||
if (confirm(t('configForm.conflict'))) {
|
||||
await refetch();
|
||||
}
|
||||
return;
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ['config'] });
|
||||
setIsDirty(false);
|
||||
setToast('保存しました');
|
||||
setToastIsError(false);
|
||||
setToast(t('configForm.saved'));
|
||||
setTimeout(() => setToast(null), 2000);
|
||||
} catch (e: any) {
|
||||
setToast(`エラー: ${e.message}`);
|
||||
setToastIsError(true);
|
||||
setToast(t('configForm.error', { msg: e.message }));
|
||||
setTimeout(() => setToast(null), 3000);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -169,7 +175,7 @@ function ConfigFormInner({ section }: ConfigFormProps) {
|
||||
useUnsavedGuard(dirtyCount > 0);
|
||||
|
||||
if (isLoading) return <div className="text-sm text-slate-400">Loading...</div>;
|
||||
if (error) return <div className="text-sm text-red-500">設定の読み込みに失敗しました</div>;
|
||||
if (error) return <div className="text-sm text-red-500">{t('configForm.loadError')}</div>;
|
||||
if (!draft) return null;
|
||||
|
||||
const formProps = { config: draft, onChange: handleChange, overriddenByEnv };
|
||||
@@ -259,15 +265,15 @@ function ConfigFormInner({ section }: ConfigFormProps) {
|
||||
}`}
|
||||
>
|
||||
{toast ? (
|
||||
<span className={`text-2xs mr-auto ${toast.startsWith('エラー') ? 'text-red-600' : 'text-emerald-700 dark:text-emerald-300'}`}>
|
||||
<span className={`text-2xs mr-auto ${toastIsError ? 'text-red-600' : 'text-emerald-700 dark:text-emerald-300'}`}>
|
||||
{toast}
|
||||
</span>
|
||||
) : dirty ? (
|
||||
<span className="text-xs mr-auto text-amber-800 dark:text-amber-300 flex items-center gap-1.5 font-medium min-w-0">
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse flex-shrink-0" aria-hidden />
|
||||
<span className="truncate">
|
||||
<span className="hidden sm:inline">未保存: {dirtyCount} 項目 — 「Save & Apply」を押すまで反映されません</span>
|
||||
<span className="sm:hidden">未保存 {dirtyCount}</span>
|
||||
<span className="hidden sm:inline">{t('configForm.unsaved', { count: dirtyCount })}</span>
|
||||
<span className="sm:hidden">{t('configForm.unsavedShort', { count: dirtyCount })}</span>
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user