#245: mode hidden — hide profile-violating options, mark the hiding
In hidden (and later enforced) mode, catalog-listed controls whose only purpose is enabling a violation are not rendered while their saved value is compliant (the four master switches, the Nextcloud backup block); value-listed selects keep only their compliant choices (registration mode, new-page classification, upload policy, SVG policy). Every affected section shows one accessible policy note (i18n de+en) so policy is distinguishable from missing features. A value that was already violating is surfaced exactly like in marked — never silently hidden. The API stays unchanged; enforcement is #246. e2e: hidden half of the marking pack (rows disappear, note visible, already-violating row stays marked, axe WCAG A/AA clean) — verified live locally; CI runs it against a second api (VS_NFD_MODE=hidden, same database) behind its own static server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
This commit is contained in:
parent
5fdef95f67
commit
9b1d4de426
@ -196,6 +196,12 @@ jobs:
|
|||||||
# cover the marked state (issue #244); mode off is covered by
|
# cover the marked state (issue #244); mode off is covered by
|
||||||
# local full runs and the marking pack's off-assertions there.
|
# local full runs and the marking pack's off-assertions there.
|
||||||
(cd apps/api && PORT=3001 VS_NFD_MODE=marked node dist/main.js > /tmp/api.log 2>&1 &)
|
(cd apps/api && PORT=3001 VS_NFD_MODE=marked node dist/main.js > /tmp/api.log 2>&1 &)
|
||||||
|
# Second api on the SAME database with VS_NFD_MODE=hidden: the
|
||||||
|
# marking pack's hidden half runs against it via its own static
|
||||||
|
# server (issue #245); the mode is env-only, so sharing the db is
|
||||||
|
# exactly the deploy semantics.
|
||||||
|
(cd apps/api && PORT=3006 VS_NFD_MODE=hidden MIGRATE_ON_START=false node dist/main.js > /tmp/api-hidden.log 2>&1 &)
|
||||||
|
(PORT=5176 API_TARGET=http://127.0.0.1:3006 node scripts/e2e-static-server.mjs > /tmp/web-hidden.log 2>&1 &)
|
||||||
(cd apps/collab && PORT=3002 node dist/index.js > /tmp/collab.log 2>&1 &)
|
(cd apps/collab && PORT=3002 node dist/index.js > /tmp/collab.log 2>&1 &)
|
||||||
(PORT=5173 COLLAB_TARGET=http://127.0.0.1:3002 node scripts/e2e-static-server.mjs > /tmp/web.log 2>&1 &)
|
(PORT=5173 COLLAB_TARGET=http://127.0.0.1:3002 node scripts/e2e-static-server.mjs > /tmp/web.log 2>&1 &)
|
||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
@ -620,6 +626,16 @@ jobs:
|
|||||||
E2E_BASE_URL=http://localhost:5173 E2E_VS_NFD_MODE=marked \
|
E2E_BASE_URL=http://localhost:5173 E2E_VS_NFD_MODE=marked \
|
||||||
pnpm --filter @dorfteich/web exec playwright test e2e/vs-nfd-marking.spec.ts
|
pnpm --filter @dorfteich/web exec playwright test e2e/vs-nfd-marking.spec.ts
|
||||||
|
|
||||||
|
# Ausblendung + Policy-Hinweis im Modus `hidden` (issue #245).
|
||||||
|
- name: Run VS-NfD hidden pack
|
||||||
|
run: |
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
curl -sf http://localhost:3006/api/v1/readyz >/dev/null && break
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
E2E_BASE_URL=http://localhost:5176 E2E_VS_NFD_MODE=hidden \
|
||||||
|
pnpm --filter @dorfteich/web exec playwright test e2e/vs-nfd-marking.spec.ts
|
||||||
|
|
||||||
# The marking pack's extra login on top of the six a11y logins pushes
|
# The marking pack's extra login on top of the six a11y logins pushes
|
||||||
# the theme pack over the 10/min login limit — reset again (#244).
|
# the theme pack over the 10/min login limit — reset again (#244).
|
||||||
- name: Reset login rate limit before theme pack
|
- name: Reset login rate limit before theme pack
|
||||||
|
|||||||
@ -36,10 +36,11 @@ E2E_BASE_URL=http://localhost:5173 E2E_MAILPIT_URL=http://localhost:8025 pnpm --
|
|||||||
`auth.spec.ts` skips itself when `E2E_MAILPIT_URL` is unset, so the CD
|
`auth.spec.ts` skips itself when `E2E_MAILPIT_URL` is unset, so the CD
|
||||||
smoke run never trips over it.
|
smoke run never trips over it.
|
||||||
|
|
||||||
`vs-nfd-marking.spec.ts` (issue #244) has two halves selected by
|
`vs-nfd-marking.spec.ts` (issues #244/#245) has three parts selected by
|
||||||
`E2E_VS_NFD_MODE`: set it to `marked` **and** start the api with
|
`E2E_VS_NFD_MODE` (`marked`, `hidden`, unset = `off`); each needs an api
|
||||||
`VS_NFD_MODE=marked` for the marking assertions (CI does this in the
|
started with the matching `VS_NFD_MODE`. CI runs the marked half against
|
||||||
auth-e2e job); leave both unset for the no-marking-in-`off` assertions.
|
the main e2e stack and the hidden half against a second api (port 3006)
|
||||||
|
on the same database; the off assertions run in local default stacks.
|
||||||
|
|
||||||
## Fixture matrix
|
## Fixture matrix
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import AxeBuilder from '@axe-core/playwright';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { contextForUser } from './helpers';
|
import { contextForUser } from './helpers';
|
||||||
@ -46,6 +47,39 @@ test('mode marked: card, checkbox marking, and point-of-choice marking', async (
|
|||||||
await context.close();
|
await context.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('mode hidden: rows disappear, notes mark the hiding, a11y clean', async ({ browser }) => {
|
||||||
|
test.skip(MODE !== 'hidden', 'needs an api started with VS_NFD_MODE=hidden');
|
||||||
|
const context = await contextForUser(browser, BASE_URL, 'fixture-admin');
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto('/admin');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// feeds.enabled defaults to true = ALREADY violating: never silently
|
||||||
|
// hidden — the row stays visible with its marking.
|
||||||
|
await expect(page.locator('#vs-nfd-mark-feeds\\.enabled')).toBeVisible();
|
||||||
|
|
||||||
|
// api.enabled/mcp.enabled default to false = compliant: their rows
|
||||||
|
// disappear (2 of 4 master-switch rows remain) and the section carries
|
||||||
|
// the accessible policy note.
|
||||||
|
await expect(page.locator('.api-opt-in__label')).toHaveCount(2);
|
||||||
|
await expect(page.locator('.vs-nfd-hidden-note').first()).toBeVisible();
|
||||||
|
|
||||||
|
// Value-listed control: the compliant registration mode keeps only its
|
||||||
|
// compliant choice (seed leaves it open = violating? then all options).
|
||||||
|
const regSelect = page.locator('select[name="auth.registrationMode"]');
|
||||||
|
const regField = page.locator('label.field', { has: regSelect });
|
||||||
|
const optionCount = await regSelect.locator('option').count();
|
||||||
|
const marked = await regField.locator('.vs-nfd-mark').count();
|
||||||
|
// Exactly one of the two treatments applies, never a silent third state.
|
||||||
|
expect(optionCount === 1 || marked === 1).toBe(true);
|
||||||
|
|
||||||
|
// The hidden state and the policy note pass the axe WCAG A/AA scan.
|
||||||
|
const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze();
|
||||||
|
expect(results.violations).toEqual([]);
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
test('mode off: no card, no markings', async ({ browser }) => {
|
test('mode off: no card, no markings', async ({ browser }) => {
|
||||||
test.skip(MODE !== 'off', 'covers the default stack only');
|
test.skip(MODE !== 'off', 'covers the default stack only');
|
||||||
const context = await contextForUser(browser, BASE_URL, 'fixture-admin');
|
const context = await contextForUser(browser, BASE_URL, 'fixture-admin');
|
||||||
|
|||||||
@ -22,6 +22,9 @@ export function useVsNfdMarking(): {
|
|||||||
view: VsNfdProfileView | undefined;
|
view: VsNfdProfileView | undefined;
|
||||||
/** Translated marking text for a violating value, else undefined. */
|
/** Translated marking text for a violating value, else undefined. */
|
||||||
markingFor: (key: string, value: unknown) => string | undefined;
|
markingFor: (key: string, value: unknown) => string | undefined;
|
||||||
|
/** True when the control must not be rendered (#245): mode hidden/
|
||||||
|
* enforced and the SAVED value is compliant. */
|
||||||
|
hides: (key: string, savedValue: unknown) => boolean;
|
||||||
} {
|
} {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
@ -34,16 +37,41 @@ export function useVsNfdMarking(): {
|
|||||||
mode,
|
mode,
|
||||||
view: query.data,
|
view: query.data,
|
||||||
markingFor(key, value) {
|
markingFor(key, value) {
|
||||||
// Marking is the `marked` treatment; `hidden`/`enforced` get their
|
// Marking applies in `marked`, and in `hidden`/`enforced` to values
|
||||||
// own behaviour with #245/#246.
|
// that are ALREADY violating — an existing violation is never
|
||||||
if (mode !== 'marked') return undefined;
|
// silently hidden (#245).
|
||||||
|
if (mode === 'off') return undefined;
|
||||||
const entry = VS_NFD_PROFILE.find((e) => e.scope === 'instance' && e.key === key);
|
const entry = VS_NFD_PROFILE.find((e) => e.scope === 'instance' && e.key === key);
|
||||||
if (!entry || isVsNfdCompliant(entry, value)) return undefined;
|
if (!entry || isVsNfdCompliant(entry, value)) return undefined;
|
||||||
return t('admin.vsNfd.marking', { value: describeCompliance(entry.compliance) });
|
return t('admin.vsNfd.marking', { value: describeCompliance(entry.compliance) });
|
||||||
},
|
},
|
||||||
|
hides(key, savedValue) {
|
||||||
|
// The `hidden` treatment (#245, `enforced` renders the same, #246):
|
||||||
|
// a compliant control whose only purpose would be enabling a
|
||||||
|
// violation disappears; a violating one stays visible WITH its
|
||||||
|
// marking so the deviation is surfaced, never buried.
|
||||||
|
if (mode !== 'hidden' && mode !== 'enforced') return false;
|
||||||
|
const entry = VS_NFD_PROFILE.find((e) => e.scope === 'instance' && e.key === key);
|
||||||
|
return entry !== undefined && isVsNfdCompliant(entry, savedValue);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one accessible per-section note that options are hidden by
|
||||||
|
* deployment policy (#245) — policy must be distinguishable from missing
|
||||||
|
* features for anyone reading the settings page.
|
||||||
|
*/
|
||||||
|
export function VsNfdHiddenNote(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation('settings');
|
||||||
|
return (
|
||||||
|
<p className="field__hint vs-nfd-hidden-note">
|
||||||
|
<span aria-hidden="true">🛈 </span>
|
||||||
|
{t('admin.vsNfd.hiddenNote')}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The marking element for controls not wrapped in <Field> (checkbox rows):
|
* The marking element for controls not wrapped in <Field> (checkbox rows):
|
||||||
* icon + text, colour never alone; wire `id` into the control's
|
* icon + text, colour never alone; wire `id` into the control's
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { VsNfdMark, useVsNfdMarking } from '../components/vs-nfd';
|
import { VsNfdHiddenNote, VsNfdMark, useVsNfdMarking } from '../components/vs-nfd';
|
||||||
import { formatBytes } from '../files/file-format';
|
import { formatBytes } from '../files/file-format';
|
||||||
import { ApiError, apiGet, apiPost, apiPut } from '../lib/api';
|
import { ApiError, apiGet, apiPost, apiPut } from '../lib/api';
|
||||||
|
|
||||||
@ -216,6 +216,7 @@ function BackupSettingsForm(): React.JSX.Element {
|
|||||||
if (!view) return <></>;
|
if (!view) return <></>;
|
||||||
const form = draft ?? toDraft(view);
|
const form = draft ?? toDraft(view);
|
||||||
const nextcloudMarking = vsNfd.markingFor('backup.nextcloud.enabled', form.enabled);
|
const nextcloudMarking = vsNfd.markingFor('backup.nextcloud.enabled', form.enabled);
|
||||||
|
const nextcloudHidden = vsNfd.hides('backup.nextcloud.enabled', view.nextcloud.enabled);
|
||||||
const update = (patch: Partial<SettingsDraft>): void => setDraft({ ...form, ...patch });
|
const update = (patch: Partial<SettingsDraft>): void => setDraft({ ...form, ...patch });
|
||||||
|
|
||||||
const describeError = (error: unknown): string => {
|
const describeError = (error: unknown): string => {
|
||||||
@ -307,92 +308,97 @@ function BackupSettingsForm(): React.JSX.Element {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<label className="system-backup__checkbox">
|
{nextcloudHidden && <VsNfdHiddenNote />}
|
||||||
<input
|
{!nextcloudHidden && (
|
||||||
type="checkbox"
|
<label className="system-backup__checkbox">
|
||||||
checked={form.enabled}
|
<input
|
||||||
disabled={!view.remoteTargets.allowed}
|
type="checkbox"
|
||||||
aria-describedby={nextcloudMarking ? 'vs-nfd-mark-backup-nextcloud' : undefined}
|
checked={form.enabled}
|
||||||
onChange={(e) => update({ enabled: e.target.checked })}
|
disabled={!view.remoteTargets.allowed}
|
||||||
/>
|
aria-describedby={nextcloudMarking ? 'vs-nfd-mark-backup-nextcloud' : undefined}
|
||||||
{t('backup.settings.enabled')}
|
onChange={(e) => update({ enabled: e.target.checked })}
|
||||||
</label>
|
/>
|
||||||
|
{t('backup.settings.enabled')}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
{nextcloudMarking && <VsNfdMark id="vs-nfd-mark-backup-nextcloud" text={nextcloudMarking} />}
|
{nextcloudMarking && <VsNfdMark id="vs-nfd-mark-backup-nextcloud" text={nextcloudMarking} />}
|
||||||
<fieldset
|
{!nextcloudHidden && (
|
||||||
disabled={!form.enabled || !view.remoteTargets.allowed}
|
<fieldset
|
||||||
className="system-backup__nextcloud"
|
disabled={!form.enabled || !view.remoteTargets.allowed}
|
||||||
>
|
className="system-backup__nextcloud"
|
||||||
<label>
|
|
||||||
{t('backup.settings.baseUrl')}
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
value={form.baseUrl}
|
|
||||||
placeholder="https://cloud.example.com"
|
|
||||||
onChange={(e) => update({ baseUrl: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<p className="system-backup__hint">{t('backup.settings.baseUrlHint')}</p>
|
|
||||||
<label>
|
|
||||||
{t('backup.settings.username')}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={form.username}
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={(e) => update({ username: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
{t('backup.settings.password')}
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={form.password}
|
|
||||||
autoComplete="new-password"
|
|
||||||
placeholder={view.nextcloud.passwordSet ? t('backup.settings.passwordStored') : ''}
|
|
||||||
onChange={(e) => update({ password: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<p className="system-backup__hint">{t('backup.settings.passwordHint')}</p>
|
|
||||||
<label>
|
|
||||||
{t('backup.settings.folder')}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={form.folder}
|
|
||||||
onChange={(e) => update({ folder: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
{t('backup.settings.schedule')}
|
|
||||||
<select
|
|
||||||
value={form.uploadSchedule}
|
|
||||||
onChange={(e) =>
|
|
||||||
update({ uploadSchedule: e.target.value as SettingsDraft['uploadSchedule'] })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(['off', 'daily', 'weekly'] as const).map((option) => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{t(`backup.settings.scheduleOptions.${option}`)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
{t('backup.settings.remoteRetention')}
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={form.remoteRetentionDays}
|
|
||||||
onChange={(e) => update({ remoteRetentionDays: e.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="button system-backup__test"
|
|
||||||
disabled={busy !== null}
|
|
||||||
onClick={() => void test()}
|
|
||||||
>
|
>
|
||||||
{busy === 'test' ? t('backup.settings.testPending') : t('backup.settings.test')}
|
<label>
|
||||||
</button>
|
{t('backup.settings.baseUrl')}
|
||||||
</fieldset>
|
<input
|
||||||
|
type="url"
|
||||||
|
value={form.baseUrl}
|
||||||
|
placeholder="https://cloud.example.com"
|
||||||
|
onChange={(e) => update({ baseUrl: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p className="system-backup__hint">{t('backup.settings.baseUrlHint')}</p>
|
||||||
|
<label>
|
||||||
|
{t('backup.settings.username')}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={form.username}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={(e) => update({ username: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
{t('backup.settings.password')}
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={form.password}
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder={view.nextcloud.passwordSet ? t('backup.settings.passwordStored') : ''}
|
||||||
|
onChange={(e) => update({ password: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p className="system-backup__hint">{t('backup.settings.passwordHint')}</p>
|
||||||
|
<label>
|
||||||
|
{t('backup.settings.folder')}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={form.folder}
|
||||||
|
onChange={(e) => update({ folder: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
{t('backup.settings.schedule')}
|
||||||
|
<select
|
||||||
|
value={form.uploadSchedule}
|
||||||
|
onChange={(e) =>
|
||||||
|
update({ uploadSchedule: e.target.value as SettingsDraft['uploadSchedule'] })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(['off', 'daily', 'weekly'] as const).map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{t(`backup.settings.scheduleOptions.${option}`)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
{t('backup.settings.remoteRetention')}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={form.remoteRetentionDays}
|
||||||
|
onChange={(e) => update({ remoteRetentionDays: e.target.value })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button system-backup__test"
|
||||||
|
disabled={busy !== null}
|
||||||
|
onClick={() => void test()}
|
||||||
|
>
|
||||||
|
{busy === 'test' ? t('backup.settings.testPending') : t('backup.settings.test')}
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
<button type="submit" className="button system-backup__save" disabled={busy !== null}>
|
<button type="submit" className="button system-backup__save" disabled={busy !== null}>
|
||||||
{busy === 'save' ? t('backup.settings.savePending') : t('backup.settings.save')}
|
{busy === 'save' ? t('backup.settings.savePending') : t('backup.settings.save')}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Field, FormError, FormSuccess } from '../components/forms';
|
import { Field, FormError, FormSuccess } from '../components/forms';
|
||||||
import { SettingsLayout } from '../components/SettingsLayout';
|
import { SettingsLayout } from '../components/SettingsLayout';
|
||||||
import { VsNfdMark, useVsNfdMarking } from '../components/vs-nfd';
|
import { VsNfdHiddenNote, VsNfdMark, useVsNfdMarking } from '../components/vs-nfd';
|
||||||
import { apiGet, apiPatch } from '../lib/api';
|
import { apiGet, apiPatch } from '../lib/api';
|
||||||
import { PluginManager } from './PluginManager';
|
import { PluginManager } from './PluginManager';
|
||||||
import { QuotaManager } from './QuotaManager';
|
import { QuotaManager } from './QuotaManager';
|
||||||
@ -76,6 +76,15 @@ export function AdminSettingsPage(): React.JSX.Element {
|
|||||||
<VsNfdProfileSection />
|
<VsNfdProfileSection />
|
||||||
<section className="settings-section">
|
<section className="settings-section">
|
||||||
<h2>{t('settings:admin.general')}</h2>
|
<h2>{t('settings:admin.general')}</h2>
|
||||||
|
{(vsNfd.hides('auth.registrationMode', settings.data['auth.registrationMode']) ||
|
||||||
|
vsNfd.hides(
|
||||||
|
'classification.newPageDefault',
|
||||||
|
settings.data['classification.newPageDefault'],
|
||||||
|
) ||
|
||||||
|
vsNfd.hides(
|
||||||
|
'classification.uploadPolicy',
|
||||||
|
settings.data['classification.uploadPolicy'],
|
||||||
|
)) && <VsNfdHiddenNote />}
|
||||||
<form onSubmit={onSubmit} noValidate>
|
<form onSubmit={onSubmit} noValidate>
|
||||||
<FormError error={error} />
|
<FormError error={error} />
|
||||||
<FormSuccess message={saved ? t('settings:admin.saved') : null} />
|
<FormSuccess message={saved ? t('settings:admin.saved') : null} />
|
||||||
@ -96,7 +105,9 @@ export function AdminSettingsPage(): React.JSX.Element {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<select {...form.register('auth.registrationMode')}>
|
<select {...form.register('auth.registrationMode')}>
|
||||||
<option value="open">{t('settings:admin.registrationOpen')}</option>
|
{!vsNfd.hides('auth.registrationMode', settings.data['auth.registrationMode']) && (
|
||||||
|
<option value="open">{t('settings:admin.registrationOpen')}</option>
|
||||||
|
)}
|
||||||
<option value="closed">{t('settings:admin.registrationClosed')}</option>
|
<option value="closed">{t('settings:admin.registrationClosed')}</option>
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
@ -109,9 +120,14 @@ export function AdminSettingsPage(): React.JSX.Element {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<select {...form.register('classification.newPageDefault')}>
|
<select {...form.register('classification.newPageDefault')}>
|
||||||
<option value="unclassified">
|
{!vsNfd.hides(
|
||||||
{t('settings:admin.classificationUnclassified')}
|
'classification.newPageDefault',
|
||||||
</option>
|
settings.data['classification.newPageDefault'],
|
||||||
|
) && (
|
||||||
|
<option value="unclassified">
|
||||||
|
{t('settings:admin.classificationUnclassified')}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
<option value="vs_nfd">{t('settings:admin.classificationVsNfd')}</option>
|
<option value="vs_nfd">{t('settings:admin.classificationVsNfd')}</option>
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
@ -124,7 +140,10 @@ export function AdminSettingsPage(): React.JSX.Element {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<select {...form.register('classification.uploadPolicy')}>
|
<select {...form.register('classification.uploadPolicy')}>
|
||||||
<option value="warn">{t('settings:admin.uploadPolicyWarn')}</option>
|
{!vsNfd.hides(
|
||||||
|
'classification.uploadPolicy',
|
||||||
|
settings.data['classification.uploadPolicy'],
|
||||||
|
) && <option value="warn">{t('settings:admin.uploadPolicyWarn')}</option>}
|
||||||
<option value="block">{t('settings:admin.uploadPolicyBlock')}</option>
|
<option value="block">{t('settings:admin.uploadPolicyBlock')}</option>
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
@ -266,6 +285,7 @@ function UploadSettingsForm({ settings }: { settings: InstanceSettings }): React
|
|||||||
onChange={(event) => setExtensions(event.target.value)}
|
onChange={(event) => setExtensions(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
{vsNfd.hides('upload.svgPolicy', settings['upload.svgPolicy']) && <VsNfdHiddenNote />}
|
||||||
<Field
|
<Field
|
||||||
label={t('settings.svgPolicy')}
|
label={t('settings.svgPolicy')}
|
||||||
marking={vsNfd.markingFor('upload.svgPolicy', svgPolicy)}
|
marking={vsNfd.markingFor('upload.svgPolicy', svgPolicy)}
|
||||||
@ -274,7 +294,9 @@ function UploadSettingsForm({ settings }: { settings: InstanceSettings }): React
|
|||||||
value={svgPolicy}
|
value={svgPolicy}
|
||||||
onChange={(event) => setSvgPolicy(event.target.value as 'reject' | 'sanitize')}
|
onChange={(event) => setSvgPolicy(event.target.value as 'reject' | 'sanitize')}
|
||||||
>
|
>
|
||||||
<option value="sanitize">{t('settings.svgSanitize')}</option>
|
{!vsNfd.hides('upload.svgPolicy', settings['upload.svgPolicy']) && (
|
||||||
|
<option value="sanitize">{t('settings.svgSanitize')}</option>
|
||||||
|
)}
|
||||||
<option value="reject">{t('settings.svgReject')}</option>
|
<option value="reject">{t('settings.svgReject')}</option>
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
@ -314,6 +336,9 @@ function PublicApiSettingsForm({ settings }: { settings: InstanceSettings }): Re
|
|||||||
<h2>{t('admin.title')}</h2>
|
<h2>{t('admin.title')}</h2>
|
||||||
<FormError error={error} />
|
<FormError error={error} />
|
||||||
<FormSuccess message={saved ? t('admin.saved') : null} />
|
<FormSuccess message={saved ? t('admin.saved') : null} />
|
||||||
|
{(['api.enabled', 'mcp.enabled', 'feeds.enabled', 'plugins.enabled'] as const).some((key) =>
|
||||||
|
vsNfd.hides(key, settings[key]),
|
||||||
|
) && <VsNfdHiddenNote />}
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
{ key: 'api.enabled', label: t('admin.label'), hint: t('admin.hint') },
|
{ key: 'api.enabled', label: t('admin.label'), hint: t('admin.hint') },
|
||||||
@ -322,6 +347,7 @@ function PublicApiSettingsForm({ settings }: { settings: InstanceSettings }): Re
|
|||||||
{ key: 'plugins.enabled', label: t('admin.pluginsLabel'), hint: t('admin.pluginsHint') },
|
{ key: 'plugins.enabled', label: t('admin.pluginsLabel'), hint: t('admin.pluginsHint') },
|
||||||
] as const
|
] as const
|
||||||
).map((row) => {
|
).map((row) => {
|
||||||
|
if (vsNfd.hides(row.key, settings[row.key])) return null;
|
||||||
const marking = vsNfd.markingFor(row.key, settings[row.key]);
|
const marking = vsNfd.markingFor(row.key, settings[row.key]);
|
||||||
return (
|
return (
|
||||||
<div key={row.key}>
|
<div key={row.key}>
|
||||||
|
|||||||
@ -72,7 +72,8 @@
|
|||||||
"referenceValue": "Referenzwert: {{value}}",
|
"referenceValue": "Referenzwert: {{value}}",
|
||||||
"guideRef": "Härtungsleitfaden §{{section}}",
|
"guideRef": "Härtungsleitfaden §{{section}}",
|
||||||
"marking": "Weicht von der VS-NfD-Referenzkonfiguration ab (Referenzwert: {{value}})",
|
"marking": "Weicht von der VS-NfD-Referenzkonfiguration ab (Referenzwert: {{value}})",
|
||||||
"guideNote": "Referenzkonfiguration und Begründungen:"
|
"guideNote": "Referenzkonfiguration und Begründungen:",
|
||||||
|
"hiddenNote": "Einzelne Optionen sind durch die Deployment-Policy (VS-NfD-Modus) ausgeblendet — das ist Absicht, kein fehlendes Feature."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
|
|||||||
@ -72,7 +72,8 @@
|
|||||||
"referenceValue": "Reference value: {{value}}",
|
"referenceValue": "Reference value: {{value}}",
|
||||||
"guideRef": "Hardening guide §{{section}}",
|
"guideRef": "Hardening guide §{{section}}",
|
||||||
"marking": "Deviates from the VS-NfD reference configuration (reference value: {{value}})",
|
"marking": "Deviates from the VS-NfD reference configuration (reference value: {{value}})",
|
||||||
"guideNote": "Reference configuration and rationale:"
|
"guideNote": "Reference configuration and rationale:",
|
||||||
|
"hiddenNote": "Some options are hidden by deployment policy (VS-NfD mode) — that is intentional, not a missing feature."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user