Merge pull request #372 from Coffey-Labs/refactor/lib-domain-folders

Group the admin and calendar modules, and stop calling screenshots docs
This commit is contained in:
jcoffey
2026-09-15 22:53:34 -07:00
committed by GitHub
72 changed files with 108 additions and 108 deletions
+3 -3
View File
@@ -36,9 +36,9 @@ settings included, belongs to Stalwart, so the container is disposable.
| | |
| --- | --- |
| **Inbox & conversation (dark)** ![Inbox, dark theme](docs/screenshots/inbox-dark.jpg) | **Inbox & conversation (light)** ![Inbox, light theme](docs/screenshots/inbox-light.jpg) |
| **Composer** ![Composer](docs/screenshots/compose.jpg) | **Calendar** ![Calendar](docs/screenshots/calendar.jpg) |
| **Contacts** ![Contacts](docs/screenshots/contacts.jpg) | **Sieve filter builder** ![Filters](docs/screenshots/filters.jpg) |
| **Inbox & conversation (dark)** ![Inbox, dark theme](screenshots/inbox-dark.jpg) | **Inbox & conversation (light)** ![Inbox, light theme](screenshots/inbox-light.jpg) |
| **Composer** ![Composer](screenshots/compose.jpg) | **Calendar** ![Calendar](screenshots/calendar.jpg) |
| **Contacts** ![Contacts](screenshots/contacts.jpg) | **Sieve filter builder** ![Filters](screenshots/filters.jpg) |
Taken against the built-in mock with sample data. More, including the phone
layout, on [ihasmail.org](https://ihasmail.org/#screenshots).

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

@@ -5,8 +5,8 @@
* images already use rather than whatever a window happens to be.
*
* npm run dev:mock # in another terminal
* node docs/screenshots.mjs docs/screenshots
* node docs/screenshots-light.mjs docs/screenshots
* node scripts/screenshots.mjs screenshots
* node scripts/screenshots-light.mjs screenshots
*
* Restart the mock before a run. The filters shot creates rules, so a second
* run against the same mock shows them twice.
@@ -30,7 +30,7 @@
* flip --bg to #f6f8fa. The compositor simply does not repaint everything a
* CSS-variable change touches while metrics are overridden. Launching Chrome
* at --window-size and never calling setDeviceMetricsOverride renders it
* correctly, which is what docs/screenshots-light.mjs does.
* correctly, which is what scripts/screenshots-light.mjs does.
*
* assertTheme() stays either way: without it this script wrote a dark
* screenshot under a light caption and reported success, and that is how the
@@ -226,7 +226,7 @@ try {
await evaluate(`(() => { const c = [...document.querySelectorAll('button')].find(b => /close|discard/i.test(b.getAttribute('aria-label')||'')); if (c) c.click(); })()`);
await sleep(800);
// (inbox-light is captured by docs/screenshots-light.mjs -- see the header)
// (inbox-light is captured by scripts/screenshots-light.mjs -- see the header)
// --- calendar ---
+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";