'use client'; // Organisation > Proofreading: whether checker plugins may check what users // write, for everyone or a scope, per checker plugin, and the writing // language. Requiring proofreading only holds while a checker plugin can't // be turned off, so the page warns when none is forced on and offers to // force one (that part is global: plugins can't be forced per scope). import { useCallback, useEffect, useState } from 'react'; import { SettingItem, SettingsSection, Select } from '@/components/settings/settings-section'; import { Button } from '@/components/ui/button'; import { AlertTriangle } from '@/components/icons'; import { apiFetch } from '@/lib/browser-navigation'; import type { SettingRestriction } from '@/lib/admin/types'; import { PLUGIN_STATES, PROOFREADING_STATES, checkersToForce, pluginProofreadingState, pluginRestrictionFor, proofreadingPolicyFor, proofreadingState, type PluginProofreadingState, type ProofreadingState, } from '@/lib/admin/proofreading-policy'; import { PROOFREADING_KEY, pluginPolicyKey } from '@/lib/proofreading/gate'; import { writingLanguageOptions } from '@/lib/proofreading/languages'; import { InheritNote } from '../org-page'; import type { OrgPolicyDraft } from '../use-org-policy-draft'; const LANGUAGE_KEY = 'writingLanguage'; const INHERIT = 'inherit'; interface CheckerPlugin { id: string; name: string; enabled: boolean; forceEnabled?: boolean; permissions?: string[]; } /** A default at the selected scope: its own, or Everyone's. */ function presetAt(draft: OrgPolicyDraft, key: string): { value: unknown; own: boolean; everyone: unknown } { const everyone = draft.policy?.defaults?.[key]; if (!draft.scope) return { value: everyone, own: true, everyone }; const defaults = draft.scope.policy.defaults ?? {}; return Object.prototype.hasOwnProperty.call(defaults, key) ? { value: defaults[key], own: true, everyone } : { value: everyone, own: false, everyone }; } function setPreset(draft: OrgPolicyDraft, key: string, value: unknown | undefined) { if (!draft.scope) { draft.update((d) => { if (value === undefined) delete d.defaults[key]; else d.defaults[key] = value as never; }); return; } draft.updateScope(draft.scope.id, (s) => { const defaults = (s.policy.defaults ??= {}); if (value === undefined) delete defaults[key]; else defaults[key] = value; if (Object.keys(defaults).length === 0) delete s.policy.defaults; }); } export function ProofreadingSection({ draft }: { draft: OrgPolicyDraft }) { const [plugins, setPlugins] = useState(null); const [forcing, setForcing] = useState(null); const [forceError, setForceError] = useState(null); const loadPlugins = useCallback(async () => { try { const res = await apiFetch('/api/admin/plugins'); const list = res.ok ? ((await res.json()) as CheckerPlugin[]) : []; setPlugins(list.filter((p) => p.permissions?.includes('compose:check'))); } catch { setPlugins([]); } }, []); useEffect(() => { void loadPlugins(); }, [loadPlugins]); const main = draft.restriction(PROOFREADING_KEY); const mainPreset = presetAt(draft, PROOFREADING_KEY); const mainState = proofreadingState(main.value, mainPreset.value); const everyoneState = proofreadingState(main.everyone, mainPreset.everyone); const mainOwn = main.own || mainPreset.own; const writeMain = (state: ProofreadingState | typeof INHERIT) => { if (state === INHERIT) { draft.setRestriction(PROOFREADING_KEY, undefined); setPreset(draft, PROOFREADING_KEY, undefined); return; } const { restriction, preset } = proofreadingPolicyFor(state); // In a scope, "no lock" must undo a lock Everyone has: an empty restriction. draft.setRestriction(PROOFREADING_KEY, restriction ?? (draft.scope ? {} : undefined)); setPreset(draft, PROOFREADING_KEY, preset); }; const pluginStates: Record = {}; for (const p of plugins ?? []) pluginStates[p.id] = pluginProofreadingState(draft.restriction(pluginPolicyKey(p.id)).value); const toForce = plugins ? checkersToForce(mainState, pluginStates, plugins) : []; const force = async (id: string) => { setForcing(id); setForceError(null); try { const res = await apiFetch('/api/admin/plugins', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, enabled: true, forceEnabled: true }), }); if (!res.ok) setForceError('Could not force the plugin on. Try it on its page under Plugins.'); await loadPlugins(); } finally { setForcing(null); } }; const language = draft.restriction(LANGUAGE_KEY); const languagePreset = presetAt(draft, LANGUAGE_KEY); const languageValue = (language.value?.locked ? language.value.value : languagePreset.value) as string | undefined; const languageLocked = language.value?.locked === true; const writeLanguage = (value: string, locked: boolean) => { if (!value) { draft.setRestriction(LANGUAGE_KEY, draft.scope ? {} : undefined); setPreset(draft, LANGUAGE_KEY, undefined); return; } draft.setRestriction(LANGUAGE_KEY, locked ? { locked: true, value } as SettingRestriction : (draft.scope ? {} : undefined)); setPreset(draft, LANGUAGE_KEY, value); }; const stateLabel = (s: ProofreadingState) => PROOFREADING_STATES.find((o) => o.value === s)?.label ?? s; return (
draft.setRestriction( pluginPolicyKey(p.id), v === INHERIT ? undefined : pluginRestrictionFor(v as PluginProofreadingState) ?? (draft.scope ? {} : undefined), )} options={[ ...(draft.scope ? [{ value: INHERIT, label: 'Same as Everyone' }] : []), ...PLUGIN_STATES.map((o) => ({ value: o.value, label: o.label })), ]} ariaLabel={`Proofreading with ${p.name}`} />
); })} {toForce.length > 0 && (

{toForce.map((id) => { const p = plugins!.find((x) => x.id === id)!; return ( ); })}
{forceError &&

{forceError}

}
)}
writeLanguage(languageValue ?? '', e.target.checked)} /> Lock

For a fully mandatory setup: force the plugin on, set Proofreading to "On, required" and, in the plugin's settings, turn off users changing its language and rules. A plugin that reaches a server on your network needs that address in PLUGIN_PROXY_PRIVATE_ORIGINS.

); }