Group the admin and calendar modules, and stop calling screenshots docs

web/src/lib had grown to 85 flat modules -- 42% of the web source, about
12,800 lines -- with one subdirectory (smime/) to its name. The tell was
that a naming prefix had taken over a directory's job: eight adminX.ts
files sat adjacent because alphabetical order put them there, not because
anything said they belonged together.

  lib/admin/     adminAccess, adminDashboard, adminDirectory, adminDomains,
                 adminGroups, adminLists, adminRoles, adminTenants
  lib/calendar/  appointment, availabilityWindow, eventDrag, ics, recurrence

Tests move with their modules into lib/admin/__tests__ and
lib/calendar/__tests__, which is what views/ already does. describeRules
stays in lib/__tests__: it checks that sieve's describeRule and
recurrence's agree, so it belongs to neither.

recurrence.ts joins the calendar group and archiveDate.ts does not, which
is the opposite of the first guess from the filenames. archiveDate picks
the Archive/2026/09 mailbox for a message -- mail, not calendar --
while recurrence reads JSCalendarRecurrenceRule. schedule.ts is scheduled
*send*, so it stays put too. birthdays.ts is left alone deliberately: it
is read off the contact cards and only rendered by the calendar, so it
belongs to whichever of the two you ask.

docs/ held no documentation. It held ten JPEGs and the two scripts that
capture them, while the actual documentation is a separate site in the
ihasmail.org repository -- so anyone opening docs/ expecting prose found
a headless-Chrome driver. The images are now screenshots/, and the two
capture scripts join the other .mjs tooling in scripts/, which is where a
generator belongs. Renaming docs/ to screenshots/ wholesale would have
produced screenshots/screenshots/inbox-dark.jpg.

No behavior changes: every import was already on the @/ alias, so this is
path rewrites and nothing else.
This commit is contained in:
2026-09-15 22:44:53 -07:00
parent 0bde2df69d
commit f7712b1c1e
72 changed files with 108 additions and 108 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
*/
import { describe, expect, it } from "vitest";
import { describeRule as describeSieve } from "../sieve";
import { describeRule as describeRecurrence, weekdayOptions } from "../recurrence";
import { describeRule as describeRecurrence, weekdayOptions } from "../calendar/recurrence";
import { setUiLanguageForFormatting } from "../datetime";
import { setCatalog } from "../i18n";
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { ADMIN_BASELINE, adminSections, can, dashboardCards, canGrantRole, generatePassword, hasAdministration, outranks, permissionSet, resolveRoles, type RoleDef } from "@/lib/adminAccess";
import { ADMIN_BASELINE, adminSections, can, dashboardCards, canGrantRole, generatePassword, hasAdministration, outranks, permissionSet, resolveRoles, type RoleDef } from "@/lib/admin/adminAccess";
const set = (...p: string[]) => permissionSet(p);
const everything = set(...ADMIN_BASELINE, "sysTenantGet", "jmapEmailGet", "impersonate");
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { client, JmapMethodError } from "@/jmap/client";
import { balancedColumns, countObjects, isRefused, loadMetrics, summarizeMetrics, type MetricRecord } from "@/lib/adminDashboard";
import { balancedColumns, countObjects, isRefused, loadMetrics, summarizeMetrics, type MetricRecord } from "@/lib/admin/adminDashboard";
const counter = (metric: string, count: number, timestamp = "2026-09-15T14:00:00Z"): MetricRecord => ({ "@type": "Counter", metric, count, timestamp });
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { client } from "@/jmap/client";
import { aliasList, describeDirectoryError, DirectoryError, hasPassword, passwordPatch, queryAccounts, quotasWithDisk } from "@/lib/adminDirectory";
import { aliasList, describeDirectoryError, DirectoryError, hasPassword, passwordPatch, queryAccounts, quotasWithDisk } from "@/lib/admin/adminDirectory";
describe("setting a password", () => {
it("writes into the existing password credential, keeping its place", () => {
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { describeLinked, dkimAlgorithm, looksLikeDomain, normalizeDomain, parseZoneFile } from "@/lib/adminDomains";
import { describeLinked, dkimAlgorithm, looksLikeDomain, normalizeDomain, parseZoneFile } from "@/lib/admin/adminDomains";
/**
* Written the way Stalwart's BIND serializer writes it (dns-update's
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { client } from "@/jmap/client";
import { countMembers, createGroup, destroyGroup, groupRoleKey, groupRolesFromKey, membershipPatch } from "@/lib/adminGroups";
import { countMembers, createGroup, destroyGroup, groupRoleKey, groupRolesFromKey, membershipPatch } from "@/lib/admin/adminGroups";
describe("group membership", () => {
it("is a patch to each member, one pointer each, so no other membership moves", () => {
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { client } from "@/jmap/client";
import { createList, parseAddresses, queryLists, recipientsPatch } from "@/lib/adminLists";
import { createList, parseAddresses, queryLists, recipientsPatch } from "@/lib/admin/adminLists";
describe("a mailing list's recipients", () => {
it("are saved as what was added and removed, one pointer each", () => {
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { permissionSet } from "@/lib/adminAccess";
import { canBuildOn, effectivePermissions, inherited, roleOutranks, setPatch, type DirectoryRole } from "@/lib/adminRoles";
import { permissionSet } from "@/lib/admin/adminAccess";
import { canBuildOn, effectivePermissions, inherited, roleOutranks, setPatch, type DirectoryRole } from "@/lib/admin/adminRoles";
const flags = (...n: string[]) => Object.fromEntries(n.map((x) => [x, true]));
const roles = new Map<string, DirectoryRole>([
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { client } from "@/jmap/client";
import { countTenantMembers, drawableLogo, quotasPatch, setDomainTenant } from "@/lib/adminTenants";
import { countTenantMembers, drawableLogo, quotasPatch, setDomainTenant } from "@/lib/admin/adminTenants";
describe("a tenant's limits", () => {
it("change one pointer each, leaving the quotas ihasmail does not offer alone", () => {
@@ -1,6 +1,6 @@
import { client } from "@/jmap/client";
import { t } from "@/lib/i18n";
import type { PermissionsMode, RoleDef, UserRoles } from "@/lib/adminAccess";
import type { PermissionsMode, RoleDef, UserRoles } from "@/lib/admin/adminAccess";
/**
* Stalwart 0.16's directory, over the ordinary JMAP proxy.
@@ -1,6 +1,6 @@
import { client } from "@/jmap/client";
import { plural, t } from "@/lib/i18n";
import { DirectoryError } from "@/lib/adminDirectory";
import { DirectoryError } from "@/lib/admin/adminDirectory";
/**
* Stalwart 0.16's domains, over the same proxy as accounts.
@@ -1,7 +1,7 @@
import { client } from "@/jmap/client";
import { t } from "@/lib/i18n";
import type { PermissionsMode, UserRoles } from "@/lib/adminAccess";
import { DirectoryError, DISK_QUOTA, queryAccounts, type EmailAlias } from "@/lib/adminDirectory";
import type { PermissionsMode, UserRoles } from "@/lib/admin/adminAccess";
import { DirectoryError, DISK_QUOTA, queryAccounts, type EmailAlias } from "@/lib/admin/adminDirectory";
/**
* Groups, from Stalwart 0.16's directory.
@@ -1,6 +1,6 @@
import { client } from "@/jmap/client";
import { t } from "@/lib/i18n";
import { DirectoryError, type EmailAlias } from "@/lib/adminDirectory";
import { DirectoryError, type EmailAlias } from "@/lib/admin/adminDirectory";
/**
* Mailing lists, from Stalwart 0.16's directory.
@@ -1,8 +1,8 @@
import { apiFetch, client } from "@/jmap/client";
import { t } from "@/lib/i18n";
import type { Permissions, RoleDef } from "@/lib/adminAccess";
import { DirectoryError } from "@/lib/adminDirectory";
import { DomainError } from "@/lib/adminDomains";
import type { Permissions, RoleDef } from "@/lib/admin/adminAccess";
import { DirectoryError } from "@/lib/admin/adminDirectory";
import { DomainError } from "@/lib/admin/adminDomains";
import type { PermissionInfo } from "@/lib/permissionLabels";
/**
@@ -1,7 +1,7 @@
import { client } from "@/jmap/client";
import { t } from "@/lib/i18n";
import { DirectoryError } from "@/lib/adminDirectory";
import { DomainError } from "@/lib/adminDomains";
import { DirectoryError } from "@/lib/admin/adminDirectory";
import { DomainError } from "@/lib/admin/adminDomains";
/**
* Tenants, from Stalwart 0.16's directory.
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { appointmentDraft, nextHalfHour } from "@/lib/appointment";
import { appointmentDraft, nextHalfHour } from "@/lib/calendar/appointment";
import type { Email, EmailBodyPart } from "@/jmap/types";
/**
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { availabilityWindow } from "@/lib/availabilityWindow";
import { availabilityWindow } from "@/lib/calendar/availabilityWindow";
const at = (s: string) => new Date(s);
const hours = (w: { ticks: { time: Date }[] }) => w.ticks.map((t) => `${t.time.getDate()}@${t.time.getHours()}`);
@@ -13,7 +13,7 @@ import {
dayDelta,
resizePatch,
SNAP_MINUTES,
} from "@/lib/eventDrag";
} from "@/lib/calendar/eventDrag";
import { BIRTHDAY_ID_PREFIX } from "@/lib/birthdays";
import type { CalendarEvent } from "@/jmap/types";
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { looksLikeCalendar, parseIcs, parseIcsDuration, parseDateValue, parseLine, unescapeText, unfold } from "@/lib/ics";
import { looksLikeCalendar, parseIcs, parseIcsDuration, parseDateValue, parseLine, unescapeText, unfold } from "@/lib/calendar/ics";
const cal = (body: string) => `BEGIN:VCALENDAR\r\nVERSION:2.0\r\n${body}\r\nEND:VCALENDAR\r\n`;
const event = (props: string) => `BEGIN:VEVENT\r\n${props}\r\nEND:VEVENT`;
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { toIcs, parseIcs } from "@/lib/ics";
import { toIcs, parseIcs } from "@/lib/calendar/ics";
import type { JSCalendarEvent } from "@/jmap/types";
/*
@@ -1,9 +1,9 @@
import type { Email, EmailAddress } from "@/jmap/types";
import { useCalendar, type EventDraft } from "@/store/calendar";
import { useMail } from "@/store/mail";
import { uniqueAddresses } from "./address";
import { toLocalDateOnly } from "./dates";
import { htmlToText } from "./text";
import { uniqueAddresses } from "../address";
import { toLocalDateOnly } from "../dates";
import { htmlToText } from "../text";
/**
* How much of a message body is copied into an event description.
@@ -10,8 +10,8 @@
* question given an event and a gesture, what are the new start and end
* and the caller decides whether it is allowed to save that.
*/
import { addMinutes } from "./dates";
import { isBirthdayEvent } from "./birthdays";
import { addMinutes } from "../dates";
import { isBirthdayEvent } from "../birthdays";
import type { CalendarEvent } from "@/jmap/types";
/**
@@ -1,5 +1,5 @@
import type { JSCalendarRecurrenceRule, JSCalendarNDay } from "@/jmap/types";
import { formatList, weekdayName, weekdayNames } from "./datetime";
import { formatList, weekdayName, weekdayNames } from "../datetime";
import { plural, t } from "@/lib/i18n";
/**
+1 -1
View File
@@ -5,7 +5,7 @@ import { toUTCDate, toLocalDateTime, zonedToDate, parseDuration, DAY_MS, browser
import { t } from "@/lib/i18n";
import { useContacts } from "./contacts";
import { BIRTHDAY_CALENDAR_ID, birthdaysInRange, isBirthdayEvent, type Birthday } from "@/lib/birthdays";
import { looksLikeCalendar, parseIcs, toIcs, type IcsEvent } from "@/lib/ics";
import { looksLikeCalendar, parseIcs, toIcs, type IcsEvent } from "@/lib/calendar/ics";
import { withBase } from "@/lib/basePath";
import { settings, useSettings } from "./settings";
import { useSession } from "./session";
+1 -1
View File
@@ -22,7 +22,7 @@ import { formatSize } from "@/lib/format";
import { collectShare } from "@/lib/shareTarget";
import { TranslateBoundary } from "@/ui/TranslateBoundary";
import { t } from "@/lib/i18n";
import { hasAdministration } from "@/lib/adminAccess";
import { hasAdministration } from "@/lib/admin/adminAccess";
import { usePermissions } from "./admin/usePermissions";
import { AdminNav } from "./admin/AdminNav";
+2 -2
View File
@@ -7,7 +7,7 @@ import {
generatePassword,
outranks,
type UserRoles,
} from "@/lib/adminAccess";
} from "@/lib/admin/adminAccess";
import {
aliasList,
createAccount,
@@ -20,7 +20,7 @@ import {
DISK_QUOTA,
type DirectoryAccount,
type EmailAlias,
} from "@/lib/adminDirectory";
} from "@/lib/admin/adminDirectory";
import { formatSize } from "@/lib/format";
import { t, tNode } from "@/lib/i18n";
import { Link } from "wouter";
+3 -3
View File
@@ -3,7 +3,7 @@ import { useLocation } from "wouter";
import { ChevronLeft, ChevronRight, Search, UserPlus, Users } from "lucide-react";
import { useSession } from "@/store/session";
import { STALWART_CAP } from "@/jmap/client";
import { can, type RoleDef } from "@/lib/adminAccess";
import { can, type RoleDef } from "@/lib/admin/adminAccess";
import {
describeDirectoryError,
getAccounts,
@@ -14,14 +14,14 @@ import {
DISK_QUOTA,
type DirectoryAccount,
type DirectoryDomain,
} from "@/lib/adminDirectory";
} from "@/lib/admin/adminDirectory";
import { formatSize } from "@/lib/format";
import { plural, t } from "@/lib/i18n";
import { Avatar, Empty, Spinner } from "@/ui/misc";
import { usePermissions } from "./usePermissions";
import { isSelf, roleName, type DirectoryContext } from "./directoryContext";
import { AccountSheet } from "./AccountSheet";
import { listTenantNames } from "@/lib/adminTenants";
import { listTenantNames } from "@/lib/admin/adminTenants";
const PAGE_SIZE = 50;
+2 -2
View File
@@ -1,8 +1,8 @@
import { useEffect, useState, type ReactNode } from "react";
import { Link } from "wouter";
import { ArrowDownToLine, ArrowUpFromLine, ExternalLink, Globe, Hourglass, LayoutDashboard, MemoryStick, RefreshCw, Users } from "lucide-react";
import { adminSections, dashboardCards, type DashboardCard } from "@/lib/adminAccess";
import { balancedColumns, countObjects, DASHBOARD_WINDOW_MS, isRefused, loadMetrics, summarizeMetrics, type MessageStats } from "@/lib/adminDashboard";
import { adminSections, dashboardCards, type DashboardCard } from "@/lib/admin/adminAccess";
import { balancedColumns, countObjects, DASHBOARD_WINDOW_MS, isRefused, loadMetrics, summarizeMetrics, type MessageStats } from "@/lib/admin/adminDashboard";
import { formatDayMonthTime, resolvedLocale } from "@/lib/datetime";
import { formatSize } from "@/lib/format";
import { t } from "@/lib/i18n";
+1 -1
View File
@@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import { Link, useLocation } from "wouter";
import { Building2, Globe, LayoutDashboard, List, ShieldCheck, User, UsersRound } from "lucide-react";
import { adminSections, type AdminSection } from "@/lib/adminAccess";
import { adminSections, type AdminSection } from "@/lib/admin/adminAccess";
import { t } from "@/lib/i18n";
import { usePermissions } from "./usePermissions";
+1 -1
View File
@@ -1,6 +1,6 @@
import type { ReactNode } from "react";
import { Redirect } from "wouter";
import { adminSections, type AdminSection } from "@/lib/adminAccess";
import { adminSections, type AdminSection } from "@/lib/admin/adminAccess";
import { AccountsAdmin } from "./AccountsAdmin";
import { AdminDashboard } from "./AdminDashboard";
import { DomainsAdmin } from "./DomainsAdmin";
+3 -3
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { Copy, Globe, Plus, Trash2, X } from "lucide-react";
import { can } from "@/lib/adminAccess";
import { describeDirectoryError } from "@/lib/adminDirectory";
import { can } from "@/lib/admin/adminAccess";
import { describeDirectoryError } from "@/lib/admin/adminDirectory";
import {
createDomain,
describeLinked,
@@ -18,7 +18,7 @@ import {
type DirectoryDomainFull,
type DkimKey,
type Managed,
} from "@/lib/adminDomains";
} from "@/lib/admin/adminDomains";
import { formatFullDate } from "@/lib/format";
import { plural, t } from "@/lib/i18n";
import { Dialog } from "@/ui/dialog";
+3 -3
View File
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { useLocation } from "wouter";
import { ChevronLeft, ChevronRight, Globe, Plus, Search } from "lucide-react";
import { can } from "@/lib/adminAccess";
import { describeDirectoryError } from "@/lib/adminDirectory";
import { countAccounts, getDomains, namesOf, queryDomains, type DirectoryDomainFull } from "@/lib/adminDomains";
import { can } from "@/lib/admin/adminAccess";
import { describeDirectoryError } from "@/lib/admin/adminDirectory";
import { countAccounts, getDomains, namesOf, queryDomains, type DirectoryDomainFull } from "@/lib/admin/adminDomains";
import { plural, t } from "@/lib/i18n";
import { Empty, Spinner } from "@/ui/misc";
import { usePermissions } from "./usePermissions";
+3 -3
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { Search, Trash2, UserMinus, UserPlus, X } from "lucide-react";
import { can, canGrantRole } from "@/lib/adminAccess";
import { aliasList, describeDirectoryError, quotasWithDisk, updateAccount, DISK_QUOTA, type EmailAlias } from "@/lib/adminDirectory";
import { can, canGrantRole } from "@/lib/admin/adminAccess";
import { aliasList, describeDirectoryError, quotasWithDisk, updateAccount, DISK_QUOTA, type EmailAlias } from "@/lib/admin/adminDirectory";
import {
createGroup,
destroyGroup,
@@ -12,7 +12,7 @@ import {
setMembership,
type DirectoryGroup,
type GroupMember,
} from "@/lib/adminGroups";
} from "@/lib/admin/adminGroups";
import { formatSize } from "@/lib/format";
import { plural, t } from "@/lib/i18n";
import { Avatar, Spinner } from "@/ui/misc";
+3 -3
View File
@@ -3,9 +3,9 @@ import { useLocation } from "wouter";
import { ChevronLeft, ChevronRight, Plus, Search, UsersRound } from "lucide-react";
import { useSession } from "@/store/session";
import { STALWART_CAP } from "@/jmap/client";
import { can, type RoleDef } from "@/lib/adminAccess";
import { describeDirectoryError, listDomains, listRoles, type DirectoryDomain } from "@/lib/adminDirectory";
import { countMembers, getGroups, queryGroups, type DirectoryGroup } from "@/lib/adminGroups";
import { can, type RoleDef } from "@/lib/admin/adminAccess";
import { describeDirectoryError, listDomains, listRoles, type DirectoryDomain } from "@/lib/admin/adminDirectory";
import { countMembers, getGroups, queryGroups, type DirectoryGroup } from "@/lib/admin/adminGroups";
import { plural, t } from "@/lib/i18n";
import { Avatar, Empty, Spinner } from "@/ui/misc";
import { usePermissions } from "./usePermissions";
+3 -3
View File
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useState } from "react";
import { Plus, Search, Trash2, X } from "lucide-react";
import { can } from "@/lib/adminAccess";
import { aliasList, describeDirectoryError, type EmailAlias } from "@/lib/adminDirectory";
import { createList, destroyList, parseAddresses, recipientsPatch, updateList, type DirectoryList } from "@/lib/adminLists";
import { can } from "@/lib/admin/adminAccess";
import { aliasList, describeDirectoryError, type EmailAlias } from "@/lib/admin/adminDirectory";
import { createList, destroyList, parseAddresses, recipientsPatch, updateList, type DirectoryList } from "@/lib/admin/adminLists";
import { plural, t } from "@/lib/i18n";
import { Dialog } from "@/ui/dialog";
import { toast } from "@/ui/toast";
+3 -3
View File
@@ -1,9 +1,9 @@
import { useEffect, useMemo, useState } from "react";
import { useLocation } from "wouter";
import { ChevronLeft, ChevronRight, List, Plus, Search } from "lucide-react";
import { can } from "@/lib/adminAccess";
import { describeDirectoryError, listDomains, type DirectoryDomain } from "@/lib/adminDirectory";
import { getLists, queryLists, type DirectoryList } from "@/lib/adminLists";
import { can } from "@/lib/admin/adminAccess";
import { describeDirectoryError, listDomains, type DirectoryDomain } from "@/lib/admin/adminDirectory";
import { getLists, queryLists, type DirectoryList } from "@/lib/admin/adminLists";
import { plural, t } from "@/lib/i18n";
import { Empty, Spinner } from "@/ui/misc";
import { useSession } from "@/store/session";
+4 -4
View File
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useState } from "react";
import { Lock, Search, Trash2, X } from "lucide-react";
import { can, canGrantRole } from "@/lib/adminAccess";
import { describeDirectoryError } from "@/lib/adminDirectory";
import { describeLinked, DomainError } from "@/lib/adminDomains";
import { can, canGrantRole } from "@/lib/admin/adminAccess";
import { describeDirectoryError } from "@/lib/admin/adminDirectory";
import { describeLinked, DomainError } from "@/lib/admin/adminDomains";
import {
canBuildOn,
createRole,
@@ -15,7 +15,7 @@ import {
type DirectoryRole,
type PermissionState,
type RoleDefaults,
} from "@/lib/adminRoles";
} from "@/lib/admin/adminRoles";
import type { PermissionEntry } from "@/lib/permissionLabels";
import { plural, t } from "@/lib/i18n";
import { Dialog } from "@/ui/dialog";
+3 -3
View File
@@ -1,9 +1,9 @@
import { useEffect, useMemo, useState } from "react";
import { useLocation } from "wouter";
import { Plus, Search, ShieldCheck } from "lucide-react";
import { can } from "@/lib/adminAccess";
import { describeDirectoryError } from "@/lib/adminDirectory";
import { effectivePermissions, listAllRoles, loadPermissionList, loadRoleDefaults, type DirectoryRole, type RoleDefaults } from "@/lib/adminRoles";
import { can } from "@/lib/admin/adminAccess";
import { describeDirectoryError } from "@/lib/admin/adminDirectory";
import { effectivePermissions, listAllRoles, loadPermissionList, loadRoleDefaults, type DirectoryRole, type RoleDefaults } from "@/lib/admin/adminRoles";
import { describePermissions, loadPermissionCatalog, type PermissionEntry } from "@/lib/permissionLabels";
import { plural, t } from "@/lib/i18n";
import { Empty, Spinner } from "@/ui/misc";
+4 -4
View File
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useState } from "react";
import { Globe, Plus, Trash2, X } from "lucide-react";
import { can, canGrantRole, type RoleDef } from "@/lib/adminAccess";
import { describeDirectoryError } from "@/lib/adminDirectory";
import { describeLinked, DomainError } from "@/lib/adminDomains";
import { can, canGrantRole, type RoleDef } from "@/lib/admin/adminAccess";
import { describeDirectoryError } from "@/lib/admin/adminDirectory";
import { describeLinked, DomainError } from "@/lib/admin/adminDomains";
import {
countTenantMembers,
createTenant,
@@ -19,7 +19,7 @@ import {
type TenantMemberKind,
type TenantQuota,
type TenantRoles,
} from "@/lib/adminTenants";
} from "@/lib/admin/adminTenants";
import { formatSize } from "@/lib/format";
import { proxiedImageUrl } from "@/lib/html";
import { plural, t } from "@/lib/i18n";
+3 -3
View File
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { useLocation } from "wouter";
import { Building2, ChevronLeft, ChevronRight, Plus, Search } from "lucide-react";
import { can, type RoleDef } from "@/lib/adminAccess";
import { describeDirectoryError, listRoles } from "@/lib/adminDirectory";
import { drawableLogo, getTenants, queryTenants, type DirectoryTenant } from "@/lib/adminTenants";
import { can, type RoleDef } from "@/lib/admin/adminAccess";
import { describeDirectoryError, listRoles } from "@/lib/admin/adminDirectory";
import { drawableLogo, getTenants, queryTenants, type DirectoryTenant } from "@/lib/admin/adminTenants";
import { formatSize } from "@/lib/format";
import { proxiedImageUrl } from "@/lib/html";
import { plural, t } from "@/lib/i18n";
@@ -5,7 +5,7 @@ import { Router } from "wouter";
import { memoryLocation } from "wouter/memory-location";
import { useSession } from "@/store/session";
import type { JmapSession } from "@/jmap/types";
import type { DirectoryAccount } from "@/lib/adminDirectory";
import type { DirectoryAccount } from "@/lib/admin/adminDirectory";
import { AccountSheet } from "../AccountSheet";
import type { DirectoryContext } from "../directoryContext";
@@ -6,7 +6,7 @@ import { memoryLocation } from "wouter/memory-location";
import { JmapMethodError } from "@/jmap/client";
import { useSession } from "@/store/session";
import type { JmapSession } from "@/jmap/types";
import type { MetricRecord } from "@/lib/adminDashboard";
import type { MetricRecord } from "@/lib/admin/adminDashboard";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
@@ -15,8 +15,8 @@ const feeds = vi.hoisted(() => ({
metrics: (): Promise<MetricRecord[]> => Promise.resolve([]),
}));
vi.mock("@/lib/adminDashboard", async (original) => ({
...(await original<typeof import("@/lib/adminDashboard")>()),
vi.mock("@/lib/admin/adminDashboard", async (original) => ({
...(await original<typeof import("@/lib/admin/adminDashboard")>()),
countObjects: vi.fn(async (object: string) => feeds.counts[object]),
loadMetrics: vi.fn(() => feeds.metrics()),
}));
@@ -17,8 +17,8 @@ const domain = {
dnsZoneFile: 'example.com. IN MX 10 mail.example.com.\nexample.com. IN TXT "v=spf1 mx -all"\n',
};
vi.mock("@/lib/adminDomains", async (original) => ({
...(await original<typeof import("@/lib/adminDomains")>()),
vi.mock("@/lib/admin/adminDomains", async (original) => ({
...(await original<typeof import("@/lib/admin/adminDomains")>()),
getDomains: vi.fn(async () => [domain]),
listDkimKeys: vi.fn(async () => [{ id: "k1", "@type": "Dkim1Ed25519Sha256", selector: "v1-ed25519", stage: "active" }]),
}));
@@ -3,7 +3,7 @@ import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useSession } from "@/store/session";
import type { JmapSession } from "@/jmap/types";
import type { DirectoryGroup } from "@/lib/adminGroups";
import type { DirectoryGroup } from "@/lib/admin/adminGroups";
import type { DirectoryContext } from "../directoryContext";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
@@ -17,8 +17,8 @@ const api = vi.hoisted(() => ({
destroyGroup: vi.fn(async () => {}),
}));
vi.mock("@/lib/adminGroups", async (original) => ({
...(await original<typeof import("@/lib/adminGroups")>()),
vi.mock("@/lib/admin/adminGroups", async (original) => ({
...(await original<typeof import("@/lib/admin/adminGroups")>()),
listMembers: vi.fn(async () => ({ members: api.members, total: api.members.length })),
searchUsers: vi.fn(async () => []),
setMembership: api.setMembership,
@@ -3,13 +3,13 @@ import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useSession } from "@/store/session";
import type { JmapSession } from "@/jmap/types";
import type { DirectoryList } from "@/lib/adminLists";
import type { DirectoryList } from "@/lib/admin/adminLists";
import type { DirectoryContext } from "../directoryContext";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const api = vi.hoisted(() => ({ updateList: vi.fn(async () => {}) }));
vi.mock("@/lib/adminLists", async (original) => ({ ...(await original<typeof import("@/lib/adminLists")>()), updateList: api.updateList }));
vi.mock("@/lib/admin/adminLists", async (original) => ({ ...(await original<typeof import("@/lib/admin/adminLists")>()), updateList: api.updateList }));
const { ListSheet } = await import("../ListSheet");
@@ -3,13 +3,13 @@ import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useSession } from "@/store/session";
import type { JmapSession } from "@/jmap/types";
import type { DirectoryRole } from "@/lib/adminRoles";
import type { DirectoryRole } from "@/lib/admin/adminRoles";
import type { PermissionEntry } from "@/lib/permissionLabels";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const api = vi.hoisted(() => ({ updateRole: vi.fn(async () => {}) }));
vi.mock("@/lib/adminRoles", async (original) => ({ ...(await original<typeof import("@/lib/adminRoles")>()), updateRole: api.updateRole }));
vi.mock("@/lib/admin/adminRoles", async (original) => ({ ...(await original<typeof import("@/lib/admin/adminRoles")>()), updateRole: api.updateRole }));
const { RoleSheet } = await import("../RoleSheet");
@@ -3,7 +3,7 @@ import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useSession } from "@/store/session";
import type { JmapSession } from "@/jmap/types";
import type { DirectoryTenant } from "@/lib/adminTenants";
import type { DirectoryTenant } from "@/lib/admin/adminTenants";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
@@ -13,8 +13,8 @@ const api = vi.hoisted(() => ({
updateTenant: vi.fn(async () => {}),
setDomainTenant: vi.fn(async () => {}),
}));
vi.mock("@/lib/adminTenants", async (original) => ({
...(await original<typeof import("@/lib/adminTenants")>()),
vi.mock("@/lib/admin/adminTenants", async (original) => ({
...(await original<typeof import("@/lib/admin/adminTenants")>()),
countTenantMembers: vi.fn(async () => api.counts),
tenantDomains: vi.fn(async () => ({ inTenant: [{ id: "d3", name: "old-brand.example" }], unassigned: [{ id: "d4", name: "spare.example" }] })),
updateTenant: api.updateTenant,
@@ -9,8 +9,8 @@ import type { JmapSession } from "@/jmap/types";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const api = vi.hoisted(() => ({ queryTenants: vi.fn(async () => ({ ids: ["t1"], total: 1 })) }));
vi.mock("@/lib/adminTenants", async (original) => ({
...(await original<typeof import("@/lib/adminTenants")>()),
vi.mock("@/lib/admin/adminTenants", async (original) => ({
...(await original<typeof import("@/lib/admin/adminTenants")>()),
queryTenants: api.queryTenants,
getTenants: vi.fn(async () => [{ id: "t1", name: "Acme Corp", quotas: {}, usedDiskQuota: 0 }]),
}));
+2 -2
View File
@@ -1,5 +1,5 @@
import type { RoleDef } from "@/lib/adminAccess";
import type { DirectoryAccount, DirectoryDomain } from "@/lib/adminDirectory";
import type { RoleDef } from "@/lib/admin/adminAccess";
import type { DirectoryAccount, DirectoryDomain } from "@/lib/admin/adminDirectory";
import { t } from "@/lib/i18n";
export interface DirectoryContext {
+1 -1
View File
@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { useSession } from "@/store/session";
import { permissionSet, type Permissions } from "@/lib/adminAccess";
import { permissionSet, type Permissions } from "@/lib/admin/adminAccess";
/**
* The signed-in account's permissions, as a set, stable between renders.
+1 -1
View File
@@ -15,7 +15,7 @@ import type { Anchor } from "@/ui/popover";
import { CalendarContextMenu, eventColor, type CalendarContext } from "./CalendarContextMenu";
import { toast } from "@/ui/toast";
import { askEditScope, droppedMessage, runScoped } from "./scope";
import { canDragEvent, dayDelta, moveByDaysPatch, movePatch, pixelsToMinutes, resizePatch, snap, type DragPatch } from "@/lib/eventDrag";
import { canDragEvent, dayDelta, moveByDaysPatch, movePatch, pixelsToMinutes, resizePatch, snap, type DragPatch } from "@/lib/calendar/eventDrag";
import { t as translate } from "@/lib/i18n";
type View = "month" | "week" | "day" | "agenda";
+2 -2
View File
@@ -13,9 +13,9 @@ import { RecipientInput } from "../compose/RecipientInput";
import { DateField, DateTimeField } from "@/ui/datefield";
import { browserTimeZone, dateToZonedLocal, formatDuration, fromInputDateTime, listTimeZones, parseDuration, toInputDateTime, toLocalDateOnly, zonedToDate, DAY_MS, humanDuration } from "@/lib/dates";
import { formatClock, formatNumericDate, formatWeekday, formatWeekdayDate } from "@/lib/datetime";
import { WEEKDAY_KEYS, weekdayOptions, describeRule, presetFor, ruleFromPreset, type RecurrencePreset } from "@/lib/recurrence";
import { WEEKDAY_KEYS, weekdayOptions, describeRule, presetFor, ruleFromPreset, type RecurrencePreset } from "@/lib/calendar/recurrence";
import { newKey } from "@/lib/contacts";
import { availabilityWindow } from "@/lib/availabilityWindow";
import { availabilityWindow } from "@/lib/calendar/availabilityWindow";
import { askEditScope, droppedMessage, runScoped } from "./scope";
import { plural, t as translate } from "@/lib/i18n";
+1 -1
View File
@@ -6,7 +6,7 @@ import { confirmDialog } from "@/ui/dialog";
import { toast } from "@/ui/toast";
import { askDeleteScope, runScoped } from "./scope";
import { formatTimeRange, humanDuration, parseDuration } from "@/lib/dates";
import { describeRule } from "@/lib/recurrence";
import { describeRule } from "@/lib/calendar/recurrence";
import { useCompose } from "@/store/compose";
import { useSettings } from "@/store/settings";
import { categoryOf, eventColor } from "./CalendarContextMenu";
+1 -1
View File
@@ -16,7 +16,7 @@ import { rowClick } from "@/lib/listSelection";
import { MenuItem, MenuSep, MenuTitle, Popover, useMenu } from "@/ui/popover";
import { useCompose } from "@/store/compose";
import { useCalendar } from "@/store/calendar";
import { startAppointment } from "@/lib/appointment";
import { startAppointment } from "@/lib/calendar/appointment";
import { toast } from "@/ui/toast";
import { haptic, usePullToRefresh, useTouchRow, PULL_TRIGGER } from "@/lib/touch";
import { describeSwipe, type SwipeAction, type SwipeDescriptor, type SwipeIcon } from "@/lib/swipe";
+1 -1
View File
@@ -8,7 +8,7 @@ import { useSettings } from "@/store/settings";
import { draftFromMailto, useCompose } from "@/store/compose";
import { useContacts } from "@/store/contacts";
import { useCalendar } from "@/store/calendar";
import { startAppointment } from "@/lib/appointment";
import { startAppointment } from "@/lib/calendar/appointment";
import { client } from "@/jmap/client";
import { SignatureBanner } from "./SignatureBanner";
import { useSignature } from "@/lib/smime/useSignature";