mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-12 18:15:33 +00:00
Add Wiki settings for the wiki's owner
A wiki's owner gets a Wiki settings item in the source's menu. It opens a dialog with one switch, "Let API, widget and public-link users edit this wiki", which saves at once and flips back if the server refuses. Editors and viewers don't get the item; anyone without manage_settings who reaches the dialog sees the switch read-only.
This commit is contained in:
1 parent
3b44b6851d
commit
5a78fd1fa7
13 files changed
+422
-4
No files matched your search
@@ -88,6 +88,7 @@ const endpoints = {
|
||||
WIKI_PAGES: (id: string) => `/api/sources/${id}/wiki/pages`,
|
||||
WIKI_PAGE: (id: string, path: string) =>
|
||||
`/api/sources/${id}/wiki/page?path=${encodeURIComponent(path)}`,
|
||||
WIKI_SETTINGS: (id: string) => `/api/sources/${id}/wiki/settings`,
|
||||
GET_AVAILABLE_TOOLS: '/api/available_tools',
|
||||
GET_USER_TOOLS: '/api/get_tools',
|
||||
CREATE_TOOL: '/api/create_tool',
|
||||
|
||||
@@ -186,6 +186,17 @@ const userService = {
|
||||
token: string | null,
|
||||
): Promise<Response> =>
|
||||
apiClient.put(endpoints.USER.WIKI_PAGE(sourceId, data.path), data, token),
|
||||
getWikiSettings: (
|
||||
sourceId: string,
|
||||
token: string | null,
|
||||
): Promise<Response> =>
|
||||
apiClient.get(endpoints.USER.WIKI_SETTINGS(sourceId), token),
|
||||
updateWikiSettings: (
|
||||
sourceId: string,
|
||||
data: { allow_outside_edits: boolean },
|
||||
token: string | null,
|
||||
): Promise<Response> =>
|
||||
apiClient.put(endpoints.USER.WIKI_SETTINGS(sourceId), data, token),
|
||||
getAvailableTools: (token: string | null): Promise<any> =>
|
||||
apiClient.get(endpoints.USER.GET_AVAILABLE_TOOLS, token),
|
||||
getUserTools: (token: string | null): Promise<any> =>
|
||||
|
||||
@@ -179,7 +179,17 @@
|
||||
"pagesTitle": "Seiten",
|
||||
"loadFailed": "Die Wiki-Seiten konnten nicht geladen werden.",
|
||||
"pageLoadFailed": "Diese Seite konnte nicht geladen werden.",
|
||||
"editTitle": "Seite bearbeiten"
|
||||
"editTitle": "Seite bearbeiten",
|
||||
"settings": {
|
||||
"action": "Wiki-Einstellungen",
|
||||
"title": "Wiki-Einstellungen",
|
||||
"outsideEdits": {
|
||||
"label": "API-, Widget- und Public-Link-Nutzern erlauben, dieses Wiki zu bearbeiten",
|
||||
"description": "Wenn das aus ist, können Personen, die einen Agenten über seinen API-Schlüssel, sein Widget oder seinen öffentlichen Link nutzen, dieses Wiki lesen, aber nicht ändern. Du und die Bearbeiter des Wikis können es immer bearbeiten."
|
||||
},
|
||||
"loadError": "Die Wiki-Einstellungen konnten nicht geladen werden.",
|
||||
"saveError": "Die Änderung konnte nicht gespeichert werden. Bitte versuche es erneut."
|
||||
}
|
||||
},
|
||||
"graphrag": {
|
||||
"badge": "GraphRAG",
|
||||
|
||||
@@ -184,7 +184,17 @@
|
||||
"pagesTitle": "Pages",
|
||||
"loadFailed": "Could not load the wiki pages.",
|
||||
"pageLoadFailed": "Could not load this page.",
|
||||
"editTitle": "Edit page"
|
||||
"editTitle": "Edit page",
|
||||
"settings": {
|
||||
"action": "Wiki settings",
|
||||
"title": "Wiki settings",
|
||||
"outsideEdits": {
|
||||
"label": "Let API, widget and public-link users edit this wiki",
|
||||
"description": "When this is off, people who use an agent through its API key, widget or public link can read this wiki but not change it. You and the wiki's editors can always edit it."
|
||||
},
|
||||
"loadError": "Could not load the wiki settings.",
|
||||
"saveError": "Could not save the change. Please try again."
|
||||
}
|
||||
},
|
||||
"graphrag": {
|
||||
"badge": "GraphRAG",
|
||||
|
||||
@@ -179,7 +179,17 @@
|
||||
"pagesTitle": "Páginas",
|
||||
"loadFailed": "No se pudieron cargar las páginas del wiki.",
|
||||
"pageLoadFailed": "No se pudo cargar esta página.",
|
||||
"editTitle": "Editar página"
|
||||
"editTitle": "Editar página",
|
||||
"settings": {
|
||||
"action": "Configuración del wiki",
|
||||
"title": "Configuración del wiki",
|
||||
"outsideEdits": {
|
||||
"label": "Permitir que los usuarios de la API, del widget y del enlace público editen este wiki",
|
||||
"description": "Si está desactivado, quienes usan un agente mediante su clave de API, su widget o su enlace público pueden leer este wiki, pero no cambiarlo. Tú y los editores del wiki siempre pueden editarlo."
|
||||
},
|
||||
"loadError": "No se pudo cargar la configuración del wiki.",
|
||||
"saveError": "No se pudo guardar el cambio. Inténtalo de nuevo."
|
||||
}
|
||||
},
|
||||
"graphrag": {
|
||||
"badge": "GraphRAG",
|
||||
|
||||
@@ -179,6 +179,16 @@
|
||||
"loadFailed": "Wikiページを読み込めませんでした。",
|
||||
"pageLoadFailed": "このページを読み込めませんでした。",
|
||||
"editTitle": "ページを編集",
|
||||
"settings": {
|
||||
"action": "Wiki設定",
|
||||
"title": "Wiki設定",
|
||||
"outsideEdits": {
|
||||
"label": "API、ウィジェット、公開リンクの利用者にこのWikiの編集を許可",
|
||||
"description": "オフの場合、APIキー、ウィジェット、公開リンクからエージェントを利用する人はこのWikiを読めますが、変更はできません。あなたとWikiの編集者はいつでも編集できます。"
|
||||
},
|
||||
"loadError": "Wiki設定を読み込めませんでした。",
|
||||
"saveError": "変更を保存できませんでした。もう一度お試しください。"
|
||||
},
|
||||
"byline_one": "{{pages}} ページ · {{tokens}} トークン"
|
||||
},
|
||||
"graphrag": {
|
||||
|
||||
@@ -185,7 +185,17 @@
|
||||
"pagesTitle": "Страницы",
|
||||
"loadFailed": "Не удалось загрузить страницы вики.",
|
||||
"pageLoadFailed": "Не удалось загрузить эту страницу.",
|
||||
"editTitle": "Редактировать страницу"
|
||||
"editTitle": "Редактировать страницу",
|
||||
"settings": {
|
||||
"action": "Настройки вики",
|
||||
"title": "Настройки вики",
|
||||
"outsideEdits": {
|
||||
"label": "Разрешить пользователям API, виджета и публичной ссылки редактировать эту вики",
|
||||
"description": "Если выключено, те, кто пользуется агентом через его API-ключ, виджет или публичную ссылку, могут читать эту вики, но не изменять её. Вы и редакторы вики всегда могут её редактировать."
|
||||
},
|
||||
"loadError": "Не удалось загрузить настройки вики.",
|
||||
"saveError": "Не удалось сохранить изменение. Попробуйте ещё раз."
|
||||
}
|
||||
},
|
||||
"graphrag": {
|
||||
"badge": "GraphRAG",
|
||||
|
||||
@@ -179,6 +179,16 @@
|
||||
"loadFailed": "無法載入 Wiki 頁面。",
|
||||
"pageLoadFailed": "無法載入此頁面。",
|
||||
"editTitle": "編輯頁面",
|
||||
"settings": {
|
||||
"action": "Wiki 設定",
|
||||
"title": "Wiki 設定",
|
||||
"outsideEdits": {
|
||||
"label": "允許 API、小工具和公開連結使用者編輯此 Wiki",
|
||||
"description": "關閉時,透過代理的 API 金鑰、小工具或公開連結使用它的人可以閱讀此 Wiki,但無法變更。你和此 Wiki 的編輯者隨時都能編輯它。"
|
||||
},
|
||||
"loadError": "無法載入 Wiki 設定。",
|
||||
"saveError": "無法儲存變更。請再試一次。"
|
||||
},
|
||||
"byline_one": "{{pages}} 個頁面 · {{tokens}} 個 token"
|
||||
},
|
||||
"graphrag": {
|
||||
|
||||
@@ -179,6 +179,16 @@
|
||||
"loadFailed": "无法加载 Wiki 页面。",
|
||||
"pageLoadFailed": "无法加载此页面。",
|
||||
"editTitle": "编辑页面",
|
||||
"settings": {
|
||||
"action": "Wiki 设置",
|
||||
"title": "Wiki 设置",
|
||||
"outsideEdits": {
|
||||
"label": "允许 API、小组件和公开链接用户编辑此 Wiki",
|
||||
"description": "关闭时,通过智能体的 API 密钥、小组件或公开链接使用它的人可以阅读此 Wiki,但不能更改。你和此 Wiki 的编辑者始终可以编辑它。"
|
||||
},
|
||||
"loadError": "无法加载 Wiki 设置。",
|
||||
"saveError": "无法保存更改。请重试。"
|
||||
},
|
||||
"byline_one": "{{pages}} 个页面 · {{tokens}} 个 token"
|
||||
},
|
||||
"graphrag": {
|
||||
|
||||
@@ -62,6 +62,11 @@ vi.mock('../components/graph/GraphSourceView', () => ({
|
||||
vi.mock('./SourceConfigModal', () => ({ default: () => null }));
|
||||
vi.mock('./TestRetrievalModal', () => ({ default: () => null }));
|
||||
vi.mock('./ConvertToWikiModal', () => ({ default: () => null }));
|
||||
vi.mock('./WikiSettingsModal', () => ({
|
||||
default: ({ document }: { document: { name: string } }) => (
|
||||
<div data-testid="wiki-settings">{document.name}</div>
|
||||
),
|
||||
}));
|
||||
vi.mock('./EnableGraphRAGModal', () => ({ default: () => null }));
|
||||
vi.mock('../teams/ShareToTeamModal', () => ({ default: () => null }));
|
||||
vi.mock('../upload/Upload', () => ({ default: () => null }));
|
||||
@@ -231,6 +236,41 @@ describe('Sources access', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('a wiki owner gets Wiki settings; editors and viewers do not', async () => {
|
||||
const wiki = { type: 'wiki', config: { kind: 'wiki' } };
|
||||
await render(
|
||||
doc({ ...wiki, access: 'owner', allowed_actions: [...OWNER, 'use'] }),
|
||||
);
|
||||
expect(await menuItems()).toContain(
|
||||
'settings.sources.wiki.settings.action',
|
||||
);
|
||||
await clickItem('settings.sources.wiki.settings.action');
|
||||
expect(
|
||||
document.querySelector('[data-testid="wiki-settings"]')?.textContent,
|
||||
).toBe('Contracts');
|
||||
|
||||
for (const allowed of [EDITOR, VIEWER]) {
|
||||
await act(async () => root.unmount());
|
||||
root = createRoot(container);
|
||||
document.body
|
||||
.querySelectorAll('[role="menu"]')
|
||||
.forEach((m) => m.remove());
|
||||
await render(
|
||||
doc({ ...wiki, access: 'editor', allowed_actions: allowed }),
|
||||
);
|
||||
expect(await menuItems()).not.toContain(
|
||||
'settings.sources.wiki.settings.action',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('a classic source has no Wiki settings', async () => {
|
||||
await render(doc({ access: 'owner', allowed_actions: [...OWNER, 'use'] }));
|
||||
expect(await menuItems()).not.toContain(
|
||||
'settings.sources.wiki.settings.action',
|
||||
);
|
||||
});
|
||||
|
||||
it('sync and reingest are editor actions', async () => {
|
||||
const synced = { syncFrequency: 'daily', ingestStatus: 'failed' as const };
|
||||
await render(doc({ ...synced, access: 'editor', allowed_actions: EDITOR }));
|
||||
|
||||
@@ -73,6 +73,7 @@ import EnableGraphRAGModal from './EnableGraphRAGModal';
|
||||
import { clearGraphBuild, selectGraphBuilds } from './graphBuildSlice';
|
||||
import SourceConfigModal from './SourceConfigModal';
|
||||
import TestRetrievalModal from './TestRetrievalModal';
|
||||
import WikiSettingsModal from './WikiSettingsModal';
|
||||
|
||||
const formatTokens = (tokens: number): string => {
|
||||
const roundToTwoDecimals = (num: number): string => {
|
||||
@@ -129,6 +130,8 @@ export default function Sources({
|
||||
];
|
||||
const [documentToView, setDocumentToView] = useState<Doc>();
|
||||
const [documentToShare, setDocumentToShare] = useState<Doc | null>(null);
|
||||
const [documentForWikiSettings, setDocumentForWikiSettings] =
|
||||
useState<Doc | null>(null);
|
||||
const [documentToConfigure, setDocumentToConfigure] = useState<Doc | null>(
|
||||
null,
|
||||
);
|
||||
@@ -438,6 +441,18 @@ export default function Sources({
|
||||
});
|
||||
}
|
||||
|
||||
// A wiki's own settings are the owner's (manage_settings).
|
||||
if (document.id && isWiki && can(document, 'manage_settings')) {
|
||||
actions.push({
|
||||
icon: SlidersHorizontal,
|
||||
label: t('settings.sources.wiki.settings.action'),
|
||||
onClick: () => {
|
||||
setDocumentForWikiSettings(document);
|
||||
},
|
||||
variant: 'default',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.id) {
|
||||
actions.push({
|
||||
icon: Search,
|
||||
@@ -930,6 +945,13 @@ export default function Sources({
|
||||
availableModels={availableModels}
|
||||
/>
|
||||
|
||||
{documentForWikiSettings && (
|
||||
<WikiSettingsModal
|
||||
document={documentForWikiSettings}
|
||||
onClose={() => setDocumentForWikiSettings(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConvertToWikiModal
|
||||
modalState={convertModalState}
|
||||
setModalState={(state) => {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { act } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
|
||||
const { service } = vi.hoisted(() => ({
|
||||
service: { getWikiSettings: vi.fn(), updateWikiSettings: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
vi.mock('react-redux', () => ({
|
||||
useSelector: () => 'token',
|
||||
}));
|
||||
|
||||
vi.mock('../api/services/userService', () => ({ default: service }));
|
||||
|
||||
import type { Doc } from '../models/misc';
|
||||
import WikiSettingsModal from './WikiSettingsModal';
|
||||
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
||||
|
||||
const wiki: Doc = {
|
||||
id: 'wiki-1',
|
||||
name: 'Handbook',
|
||||
date: '',
|
||||
model: '',
|
||||
type: 'wiki',
|
||||
config: { kind: 'wiki' },
|
||||
};
|
||||
|
||||
const reply = (status: number, body: unknown) =>
|
||||
Promise.resolve({ ok: status < 400, status, json: async () => body });
|
||||
|
||||
const OWNER = ['use', 'edit', 'manage_settings'];
|
||||
|
||||
describe('WikiSettingsModal', () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
Object.values(service).forEach((fn) => fn.mockReset());
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount());
|
||||
container.remove();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
const render = async () => {
|
||||
await act(async () => {
|
||||
root.render(<WikiSettingsModal document={wiki} onClose={vi.fn()} />);
|
||||
});
|
||||
};
|
||||
|
||||
const theSwitch = () =>
|
||||
document.body.querySelector<HTMLButtonElement>('[role="switch"]');
|
||||
|
||||
it('shows the stored value, off by default', async () => {
|
||||
service.getWikiSettings.mockReturnValue(
|
||||
reply(200, { allow_outside_edits: false, allowed_actions: OWNER }),
|
||||
);
|
||||
await render();
|
||||
expect(service.getWikiSettings).toHaveBeenCalledWith('wiki-1', 'token');
|
||||
expect(document.body.textContent).toContain(
|
||||
'settings.sources.wiki.settings.outsideEdits.label',
|
||||
);
|
||||
expect(theSwitch()?.getAttribute('aria-checked')).toBe('false');
|
||||
expect(theSwitch()?.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('the owner turns it on', async () => {
|
||||
service.getWikiSettings.mockReturnValue(
|
||||
reply(200, { allow_outside_edits: false, allowed_actions: OWNER }),
|
||||
);
|
||||
service.updateWikiSettings.mockReturnValue(
|
||||
reply(200, { allow_outside_edits: true, allowed_actions: OWNER }),
|
||||
);
|
||||
await render();
|
||||
await act(async () => theSwitch()!.click());
|
||||
expect(service.updateWikiSettings).toHaveBeenCalledWith(
|
||||
'wiki-1',
|
||||
{ allow_outside_edits: true },
|
||||
'token',
|
||||
);
|
||||
expect(theSwitch()?.getAttribute('aria-checked')).toBe('true');
|
||||
});
|
||||
|
||||
it('flips back and says so when the save fails', async () => {
|
||||
service.getWikiSettings.mockReturnValue(
|
||||
reply(200, { allow_outside_edits: false, allowed_actions: OWNER }),
|
||||
);
|
||||
service.updateWikiSettings.mockReturnValue(reply(403, { success: false }));
|
||||
await render();
|
||||
await act(async () => theSwitch()!.click());
|
||||
expect(theSwitch()?.getAttribute('aria-checked')).toBe('false');
|
||||
expect(document.body.textContent).toContain(
|
||||
'settings.sources.wiki.settings.saveError',
|
||||
);
|
||||
});
|
||||
|
||||
it('is read-only without manage_settings', async () => {
|
||||
service.getWikiSettings.mockReturnValue(
|
||||
reply(200, { allow_outside_edits: true, allowed_actions: ['use'] }),
|
||||
);
|
||||
await render();
|
||||
expect(theSwitch()?.getAttribute('aria-checked')).toBe('true');
|
||||
expect(theSwitch()?.disabled).toBe(true);
|
||||
expect(document.body.textContent).toContain('common.viewOnlyNotice');
|
||||
});
|
||||
|
||||
it('says when the settings could not load', async () => {
|
||||
service.getWikiSettings.mockReturnValue(reply(500, { success: false }));
|
||||
await render();
|
||||
expect(theSwitch()).toBeNull();
|
||||
expect(document.body.textContent).toContain(
|
||||
'settings.sources.wiki.settings.loadError',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
import { CircleAlert } from 'lucide-react';
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import ViewOnlyNotice from '../components/ViewOnlyNotice';
|
||||
import { Alert, AlertDescription } from '../components/ui/alert';
|
||||
import { LoadingState } from '../components/ui/loading-state';
|
||||
import { Modal } from '../components/ui/modal';
|
||||
import { SettingRow, SettingRows } from '../components/ui/setting-row';
|
||||
import { Switch } from '../components/ui/switch';
|
||||
import { Doc } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { can } from '../utils/accessUtils';
|
||||
|
||||
type WikiSettings = {
|
||||
allow_outside_edits: boolean;
|
||||
allowed_actions?: string[];
|
||||
};
|
||||
|
||||
interface WikiSettingsModalProps {
|
||||
document: Doc;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wiki's owner settings. Today one switch: whether people using an agent
|
||||
* through its API key, widget or public link may edit the wiki. It saves at
|
||||
* once and flips back when the server refuses.
|
||||
*/
|
||||
export default function WikiSettingsModal({
|
||||
document,
|
||||
onClose,
|
||||
}: WikiSettingsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const token = useSelector(selectToken);
|
||||
const switchId = useId();
|
||||
|
||||
const [settings, setSettings] = useState<WikiSettings | null>(null);
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveFailed, setSaveFailed] = useState(false);
|
||||
|
||||
const sourceId = document.id ?? '';
|
||||
// Read through a ref: a token refresh is the same user and must not reload.
|
||||
const tokenRef = useRef(token);
|
||||
tokenRef.current = token;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setSettings(null);
|
||||
setLoadFailed(false);
|
||||
userService
|
||||
.getWikiSettings(sourceId, tokenRef.current)
|
||||
.then(async (response) => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = (await response.json()) as WikiSettings;
|
||||
if (!cancelled) setSettings(data);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setLoadFailed(true);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [sourceId]);
|
||||
|
||||
const canManage = can(settings, 'manage_settings');
|
||||
|
||||
const toggle = async (value: boolean) => {
|
||||
if (!settings) return;
|
||||
const previous = settings;
|
||||
setSettings({ ...previous, allow_outside_edits: value });
|
||||
setSaving(true);
|
||||
setSaveFailed(false);
|
||||
try {
|
||||
const response = await userService.updateWikiSettings(
|
||||
sourceId,
|
||||
{ allow_outside_edits: value },
|
||||
token,
|
||||
);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = (await response.json()) as WikiSettings;
|
||||
setSettings(data);
|
||||
} catch {
|
||||
setSettings(previous);
|
||||
setSaveFailed(true);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const body = () => {
|
||||
if (loadFailed) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<CircleAlert />
|
||||
<AlertDescription>
|
||||
{t('settings.sources.wiki.settings.loadError')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (!settings) return <LoadingState fill="block" />;
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{!canManage && <ViewOnlyNotice />}
|
||||
{saveFailed && (
|
||||
<Alert variant="destructive">
|
||||
<CircleAlert />
|
||||
<AlertDescription>
|
||||
{t('settings.sources.wiki.settings.saveError')}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<SettingRows>
|
||||
<SettingRow
|
||||
htmlFor={switchId}
|
||||
alignStart
|
||||
label={t('settings.sources.wiki.settings.outsideEdits.label')}
|
||||
description={t(
|
||||
'settings.sources.wiki.settings.outsideEdits.description',
|
||||
)}
|
||||
>
|
||||
<Switch
|
||||
id={switchId}
|
||||
checked={settings.allow_outside_edits}
|
||||
disabled={!canManage || saving}
|
||||
onCheckedChange={toggle}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingRows>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={true}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose();
|
||||
}}
|
||||
title={t('settings.sources.wiki.settings.title')}
|
||||
description={document.name}
|
||||
>
|
||||
{body()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user