Settings › Overview offers the limits and directory guides too #49

Merged
jcoffey-dev merged 1 commits from feature/overview-cards into main 2026-09-28 22:26:21 +00:00
3 changed files with 87 additions and 28 deletions

No files matched your search

+30 -14
View File
@@ -15,6 +15,35 @@ import { cn } from '@/lib/utils';
export const DIRECTORY_WIZARD_VIEW = 'Wizard/directory';
/** "Guided or manual?" for connecting a directory. Manual opens Directories unless told otherwise. */
export function DirectoryLaunchChoice({
open,
onOpenChange,
onManual,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
onManual?: () => void;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const section = useSchemaStore((s) => s.viewToSection['x:Directory']) ?? 'Settings';
return (
<LaunchChoice
open={open}
onOpenChange={onOpenChange}
title={t('dirCard.chooseTitle', 'Connect a sign-in directory')}
guidedHint={t(
'dirCard.guidedHint',
'Pick your directory, fill in the connection, test a person, then choose domains. Nothing moves until the test passes.',
)}
manualHint={t('dirCard.manualHint', 'Create the directory and set each domain’s directory yourself.')}
onGuided={() => navigate(`/${section}/${DIRECTORY_WIZARD_VIEW}`)}
onManual={onManual ?? (() => navigate(`/${section}/x:Directory`))}
/>
);
}
/**
* inbuxa: at the top of Directories and Authentication, an invitation to
* connect a directory with the guide (settings-reorg, first wave). Manual
@@ -22,8 +51,6 @@ export const DIRECTORY_WIZARD_VIEW = 'Wizard/directory';
*/
export function DirectorySetupCard({ className }: { className?: string }) {
const { t } = useTranslation();
const navigate = useNavigate();
const section = useSchemaStore((s) => s.viewToSection['x:Directory']) ?? 'Settings';
const [open, setOpen] = useState(false);
return (
<div
@@ -45,18 +72,7 @@ export function DirectorySetupCard({ className }: { className?: string }) {
<Button type="button" onClick={() => setOpen(true)}>
{t('dirCard.start', 'Set it up')}
</Button>
<LaunchChoice
open={open}
onOpenChange={setOpen}
title={t('dirCard.chooseTitle', 'Connect a sign-in directory')}
guidedHint={t(
'dirCard.guidedHint',
'Pick your directory, fill in the connection, test a person, then choose domains. Nothing moves until the test passes.',
)}
manualHint={t('dirCard.manualHint', 'Create the directory and set each domain’s directory yourself.')}
onGuided={() => navigate(`/${section}/${DIRECTORY_WIZARD_VIEW}`)}
onManual={() => undefined}
/>
<DirectoryLaunchChoice open={open} onOpenChange={setOpen} onManual={() => undefined} />
</div>
);
}
+30 -14
View File
@@ -22,10 +22,37 @@ import { describe, type LimitObject, type RuleRecord } from './rules';
export const LIMITS_WIZARD_VIEW = 'Wizard/limits';
export function LimitsSummary({ object }: { object: LimitObject }) {
/** "Guided or manual?" for limits. Manual opens the incoming limits unless told otherwise. */
export function LimitsLaunchChoice({
open,
onOpenChange,
onManual,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
onManual?: () => void;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const section = useSchemaStore((s) => s.viewToSection[object]) ?? 'Settings';
const section = useSchemaStore((s) => s.viewToSection['x:MtaInboundThrottle']) ?? 'Settings';
return (
<LaunchChoice
open={open}
onOpenChange={onOpenChange}
title={t('limitsSummary.chooseTitle', 'Sending and receiving limits')}
guidedHint={t(
'limitsSummary.guidedHint',
'Say who uses this server and get five sensible limits, each shown as a sentence you can adjust.',
)}
manualHint={t('limitsSummary.manualHint', 'Add and edit each rule yourself, with every option.')}
onGuided={() => navigate(`/${section}/${LIMITS_WIZARD_VIEW}`)}
onManual={onManual ?? (() => navigate(`/${section}/x:MtaInboundThrottle`))}
/>
);
}
export function LimitsSummary({ object }: { object: LimitObject }) {
const { t } = useTranslation();
const [rules, setRules] = useState<RuleRecord[] | null>(null);
const [open, setOpen] = useState(false);
@@ -68,18 +95,7 @@ export function LimitsSummary({ object }: { object: LimitObject }) {
))}
</ul>
)}
<LaunchChoice
open={open}
onOpenChange={setOpen}
title={t('limitsSummary.chooseTitle', 'Sending and receiving limits')}
guidedHint={t(
'limitsSummary.guidedHint',
'Say who uses this server and get five sensible limits, each shown as a sentence you can adjust.',
)}
manualHint={t('limitsSummary.manualHint', 'Add and edit each rule yourself, with every option.')}
onGuided={() => navigate(`/${section}/${LIMITS_WIZARD_VIEW}`)}
onManual={() => undefined}
/>
<LimitsLaunchChoice open={open} onOpenChange={setOpen} onManual={() => undefined} />
</div>
);
}
@@ -23,6 +23,8 @@ import { SETTINGS_CATEGORIES, SETTINGS_LAYOUT_NAME } from '@/lib/settingsLayout'
import { resolveObject } from '@/lib/schemaResolver';
import { countChanged, singletonPages, type SettingsPageRef } from './changedFromDefault';
import { SENDING_WIZARD_VIEW, SendingLaunchChoice } from '@/features/sending/SendingSetupCard';
import { DIRECTORY_WIZARD_VIEW, DirectoryLaunchChoice } from '@/features/directory/DirectorySetupCard';
import { LIMITS_WIZARD_VIEW, LimitsLaunchChoice } from '@/features/limits/LimitsSummary';
import { CERTIFICATE_WIZARD_VIEW, CertificateLaunchChoice } from '@/features/certificates/CertificateSetupCard';
/** Calls per JMAP request; the protocol's own floor, so any server takes it. */
@@ -50,6 +52,31 @@ async function anyExist(objectName: string): Promise<boolean> {
/** Each guided setup registers here as it lands. */
const GUIDED_SETUPS: GuidedSetup[] = [
{
id: 'limits',
title: 'Sending and receiving limits',
blurb: 'Rate limits that suit who uses this server, so one stolen password can’t send without end.',
to: { choice: (props) => <LimitsLaunchChoice {...props} /> },
gate: LIMITS_WIZARD_VIEW,
isDone: async () => {
const [res] = await jmapRequest([
[
'x:MtaInboundThrottle/get',
{ accountId: getAccountId('x:MtaInboundThrottle'), ids: null, properties: ['id'] },
'0',
],
]);
return ((res?.[1]?.list as unknown[] | undefined)?.length ?? 0) > 0;
},
},
{
id: 'directory',
title: 'Connect a sign-in directory',
blurb:
'Let people sign in with Active Directory, LDAP or an OpenID Connect provider, tested before anything moves.',
to: { choice: (props) => <DirectoryLaunchChoice {...props} /> },
gate: DIRECTORY_WIZARD_VIEW,
},
{
id: 'certificates',
title: 'Certificates, automatically',