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.
@@ -36,9 +36,9 @@ settings included, belongs to Stalwart, so the container is disposable.
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Inbox & conversation (dark)**  | **Inbox & conversation (light)**  |
|
| **Inbox & conversation (dark)**  | **Inbox & conversation (light)**  |
|
||||||
| **Composer**  | **Calendar**  |
|
| **Composer**  | **Calendar**  |
|
||||||
| **Contacts**  | **Sieve filter builder**  |
|
| **Contacts**  | **Sieve filter builder**  |
|
||||||
|
|
||||||
Taken against the built-in mock with sample data. More, including the phone
|
Taken against the built-in mock with sample data. More, including the phone
|
||||||
layout, on [ihasmail.org](https://ihasmail.org/#screenshots).
|
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.
|
* images already use rather than whatever a window happens to be.
|
||||||
*
|
*
|
||||||
* npm run dev:mock # in another terminal
|
* npm run dev:mock # in another terminal
|
||||||
* node docs/screenshots.mjs docs/screenshots
|
* node scripts/screenshots.mjs screenshots
|
||||||
* node docs/screenshots-light.mjs docs/screenshots
|
* node scripts/screenshots-light.mjs screenshots
|
||||||
*
|
*
|
||||||
* Restart the mock before a run. The filters shot creates rules, so a second
|
* Restart the mock before a run. The filters shot creates rules, so a second
|
||||||
* run against the same mock shows them twice.
|
* run against the same mock shows them twice.
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
* flip --bg to #f6f8fa. The compositor simply does not repaint everything a
|
* flip --bg to #f6f8fa. The compositor simply does not repaint everything a
|
||||||
* CSS-variable change touches while metrics are overridden. Launching Chrome
|
* CSS-variable change touches while metrics are overridden. Launching Chrome
|
||||||
* at --window-size and never calling setDeviceMetricsOverride renders it
|
* 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
|
* assertTheme() stays either way: without it this script wrote a dark
|
||||||
* screenshot under a light caption and reported success, and that is how the
|
* 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 evaluate(`(() => { const c = [...document.querySelectorAll('button')].find(b => /close|discard/i.test(b.getAttribute('aria-label')||'')); if (c) c.click(); })()`);
|
||||||
await sleep(800);
|
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 ---
|
// --- calendar ---
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { describeRule as describeSieve } from "../sieve";
|
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 { setUiLanguageForFormatting } from "../datetime";
|
||||||
import { setCatalog } from "../i18n";
|
import { setCatalog } from "../i18n";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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 set = (...p: string[]) => permissionSet(p);
|
||||||
const everything = set(...ADMIN_BASELINE, "sysTenantGet", "jmapEmailGet", "impersonate");
|
const everything = set(...ADMIN_BASELINE, "sysTenantGet", "jmapEmailGet", "impersonate");
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { client, JmapMethodError } from "@/jmap/client";
|
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 });
|
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 { describe, expect, it, vi } from "vitest";
|
||||||
import { client } from "@/jmap/client";
|
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", () => {
|
describe("setting a password", () => {
|
||||||
it("writes into the existing password credential, keeping its place", () => {
|
it("writes into the existing password credential, keeping its place", () => {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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
|
* Written the way Stalwart's BIND serializer writes it (dns-update's
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { client } from "@/jmap/client";
|
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", () => {
|
describe("group membership", () => {
|
||||||
it("is a patch to each member, one pointer each, so no other membership moves", () => {
|
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 { describe, expect, it, vi } from "vitest";
|
||||||
import { client } from "@/jmap/client";
|
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", () => {
|
describe("a mailing list's recipients", () => {
|
||||||
it("are saved as what was added and removed, one pointer each", () => {
|
it("are saved as what was added and removed, one pointer each", () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { permissionSet } from "@/lib/adminAccess";
|
import { permissionSet } from "@/lib/admin/adminAccess";
|
||||||
import { canBuildOn, effectivePermissions, inherited, roleOutranks, setPatch, type DirectoryRole } from "@/lib/adminRoles";
|
import { canBuildOn, effectivePermissions, inherited, roleOutranks, setPatch, type DirectoryRole } from "@/lib/admin/adminRoles";
|
||||||
|
|
||||||
const flags = (...n: string[]) => Object.fromEntries(n.map((x) => [x, true]));
|
const flags = (...n: string[]) => Object.fromEntries(n.map((x) => [x, true]));
|
||||||
const roles = new Map<string, DirectoryRole>([
|
const roles = new Map<string, DirectoryRole>([
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { client } from "@/jmap/client";
|
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", () => {
|
describe("a tenant's limits", () => {
|
||||||
it("change one pointer each, leaving the quotas ihasmail does not offer alone", () => {
|
it("change one pointer each, leaving the quotas ihasmail does not offer alone", () => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { client } from "@/jmap/client";
|
import { client } from "@/jmap/client";
|
||||||
import { t } from "@/lib/i18n";
|
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.
|
* Stalwart 0.16's directory, over the ordinary JMAP proxy.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { client } from "@/jmap/client";
|
import { client } from "@/jmap/client";
|
||||||
import { plural, t } from "@/lib/i18n";
|
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.
|
* Stalwart 0.16's domains, over the same proxy as accounts.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { client } from "@/jmap/client";
|
import { client } from "@/jmap/client";
|
||||||
import { t } from "@/lib/i18n";
|
import { t } from "@/lib/i18n";
|
||||||
import type { PermissionsMode, UserRoles } from "@/lib/adminAccess";
|
import type { PermissionsMode, UserRoles } from "@/lib/admin/adminAccess";
|
||||||
import { DirectoryError, DISK_QUOTA, queryAccounts, type EmailAlias } from "@/lib/adminDirectory";
|
import { DirectoryError, DISK_QUOTA, queryAccounts, type EmailAlias } from "@/lib/admin/adminDirectory";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups, from Stalwart 0.16's directory.
|
* Groups, from Stalwart 0.16's directory.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { client } from "@/jmap/client";
|
import { client } from "@/jmap/client";
|
||||||
import { t } from "@/lib/i18n";
|
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.
|
* Mailing lists, from Stalwart 0.16's directory.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { apiFetch, client } from "@/jmap/client";
|
import { apiFetch, client } from "@/jmap/client";
|
||||||
import { t } from "@/lib/i18n";
|
import { t } from "@/lib/i18n";
|
||||||
import type { Permissions, RoleDef } from "@/lib/adminAccess";
|
import type { Permissions, RoleDef } from "@/lib/admin/adminAccess";
|
||||||
import { DirectoryError } from "@/lib/adminDirectory";
|
import { DirectoryError } from "@/lib/admin/adminDirectory";
|
||||||
import { DomainError } from "@/lib/adminDomains";
|
import { DomainError } from "@/lib/admin/adminDomains";
|
||||||
import type { PermissionInfo } from "@/lib/permissionLabels";
|
import type { PermissionInfo } from "@/lib/permissionLabels";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { client } from "@/jmap/client";
|
import { client } from "@/jmap/client";
|
||||||
import { t } from "@/lib/i18n";
|
import { t } from "@/lib/i18n";
|
||||||
import { DirectoryError } from "@/lib/adminDirectory";
|
import { DirectoryError } from "@/lib/admin/adminDirectory";
|
||||||
import { DomainError } from "@/lib/adminDomains";
|
import { DomainError } from "@/lib/admin/adminDomains";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenants, from Stalwart 0.16's directory.
|
* Tenants, from Stalwart 0.16's directory.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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";
|
import type { Email, EmailBodyPart } from "@/jmap/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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 at = (s: string) => new Date(s);
|
||||||
const hours = (w: { ticks: { time: Date }[] }) => w.ticks.map((t) => `${t.time.getDate()}@${t.time.getHours()}`);
|
const hours = (w: { ticks: { time: Date }[] }) => w.ticks.map((t) => `${t.time.getDate()}@${t.time.getHours()}`);
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
dayDelta,
|
dayDelta,
|
||||||
resizePatch,
|
resizePatch,
|
||||||
SNAP_MINUTES,
|
SNAP_MINUTES,
|
||||||
} from "@/lib/eventDrag";
|
} from "@/lib/calendar/eventDrag";
|
||||||
import { BIRTHDAY_ID_PREFIX } from "@/lib/birthdays";
|
import { BIRTHDAY_ID_PREFIX } from "@/lib/birthdays";
|
||||||
import type { CalendarEvent } from "@/jmap/types";
|
import type { CalendarEvent } from "@/jmap/types";
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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 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`;
|
const event = (props: string) => `BEGIN:VEVENT\r\n${props}\r\nEND:VEVENT`;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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";
|
import type { JSCalendarEvent } from "@/jmap/types";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Email, EmailAddress } from "@/jmap/types";
|
import type { Email, EmailAddress } from "@/jmap/types";
|
||||||
import { useCalendar, type EventDraft } from "@/store/calendar";
|
import { useCalendar, type EventDraft } from "@/store/calendar";
|
||||||
import { useMail } from "@/store/mail";
|
import { useMail } from "@/store/mail";
|
||||||
import { uniqueAddresses } from "./address";
|
import { uniqueAddresses } from "../address";
|
||||||
import { toLocalDateOnly } from "./dates";
|
import { toLocalDateOnly } from "../dates";
|
||||||
import { htmlToText } from "./text";
|
import { htmlToText } from "../text";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How much of a message body is copied into an event description.
|
* 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 —
|
* 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.
|
* and the caller decides whether it is allowed to save that.
|
||||||
*/
|
*/
|
||||||
import { addMinutes } from "./dates";
|
import { addMinutes } from "../dates";
|
||||||
import { isBirthdayEvent } from "./birthdays";
|
import { isBirthdayEvent } from "../birthdays";
|
||||||
import type { CalendarEvent } from "@/jmap/types";
|
import type { CalendarEvent } from "@/jmap/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { JSCalendarRecurrenceRule, JSCalendarNDay } from "@/jmap/types";
|
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";
|
import { plural, t } from "@/lib/i18n";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -5,7 +5,7 @@ import { toUTCDate, toLocalDateTime, zonedToDate, parseDuration, DAY_MS, browser
|
|||||||
import { t } from "@/lib/i18n";
|
import { t } from "@/lib/i18n";
|
||||||
import { useContacts } from "./contacts";
|
import { useContacts } from "./contacts";
|
||||||
import { BIRTHDAY_CALENDAR_ID, birthdaysInRange, isBirthdayEvent, type Birthday } from "@/lib/birthdays";
|
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 { withBase } from "@/lib/basePath";
|
||||||
import { settings, useSettings } from "./settings";
|
import { settings, useSettings } from "./settings";
|
||||||
import { useSession } from "./session";
|
import { useSession } from "./session";
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { formatSize } from "@/lib/format";
|
|||||||
import { collectShare } from "@/lib/shareTarget";
|
import { collectShare } from "@/lib/shareTarget";
|
||||||
import { TranslateBoundary } from "@/ui/TranslateBoundary";
|
import { TranslateBoundary } from "@/ui/TranslateBoundary";
|
||||||
import { t } from "@/lib/i18n";
|
import { t } from "@/lib/i18n";
|
||||||
import { hasAdministration } from "@/lib/adminAccess";
|
import { hasAdministration } from "@/lib/admin/adminAccess";
|
||||||
import { usePermissions } from "./admin/usePermissions";
|
import { usePermissions } from "./admin/usePermissions";
|
||||||
import { AdminNav } from "./admin/AdminNav";
|
import { AdminNav } from "./admin/AdminNav";
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
generatePassword,
|
generatePassword,
|
||||||
outranks,
|
outranks,
|
||||||
type UserRoles,
|
type UserRoles,
|
||||||
} from "@/lib/adminAccess";
|
} from "@/lib/admin/adminAccess";
|
||||||
import {
|
import {
|
||||||
aliasList,
|
aliasList,
|
||||||
createAccount,
|
createAccount,
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
DISK_QUOTA,
|
DISK_QUOTA,
|
||||||
type DirectoryAccount,
|
type DirectoryAccount,
|
||||||
type EmailAlias,
|
type EmailAlias,
|
||||||
} from "@/lib/adminDirectory";
|
} from "@/lib/admin/adminDirectory";
|
||||||
import { formatSize } from "@/lib/format";
|
import { formatSize } from "@/lib/format";
|
||||||
import { t, tNode } from "@/lib/i18n";
|
import { t, tNode } from "@/lib/i18n";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useLocation } from "wouter";
|
|||||||
import { ChevronLeft, ChevronRight, Search, UserPlus, Users } from "lucide-react";
|
import { ChevronLeft, ChevronRight, Search, UserPlus, Users } from "lucide-react";
|
||||||
import { useSession } from "@/store/session";
|
import { useSession } from "@/store/session";
|
||||||
import { STALWART_CAP } from "@/jmap/client";
|
import { STALWART_CAP } from "@/jmap/client";
|
||||||
import { can, type RoleDef } from "@/lib/adminAccess";
|
import { can, type RoleDef } from "@/lib/admin/adminAccess";
|
||||||
import {
|
import {
|
||||||
describeDirectoryError,
|
describeDirectoryError,
|
||||||
getAccounts,
|
getAccounts,
|
||||||
@@ -14,14 +14,14 @@ import {
|
|||||||
DISK_QUOTA,
|
DISK_QUOTA,
|
||||||
type DirectoryAccount,
|
type DirectoryAccount,
|
||||||
type DirectoryDomain,
|
type DirectoryDomain,
|
||||||
} from "@/lib/adminDirectory";
|
} from "@/lib/admin/adminDirectory";
|
||||||
import { formatSize } from "@/lib/format";
|
import { formatSize } from "@/lib/format";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
import { Avatar, Empty, Spinner } from "@/ui/misc";
|
import { Avatar, Empty, Spinner } from "@/ui/misc";
|
||||||
import { usePermissions } from "./usePermissions";
|
import { usePermissions } from "./usePermissions";
|
||||||
import { isSelf, roleName, type DirectoryContext } from "./directoryContext";
|
import { isSelf, roleName, type DirectoryContext } from "./directoryContext";
|
||||||
import { AccountSheet } from "./AccountSheet";
|
import { AccountSheet } from "./AccountSheet";
|
||||||
import { listTenantNames } from "@/lib/adminTenants";
|
import { listTenantNames } from "@/lib/admin/adminTenants";
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState, type ReactNode } from "react";
|
import { useEffect, useState, type ReactNode } from "react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { ArrowDownToLine, ArrowUpFromLine, ExternalLink, Globe, Hourglass, LayoutDashboard, MemoryStick, RefreshCw, Users } from "lucide-react";
|
import { ArrowDownToLine, ArrowUpFromLine, ExternalLink, Globe, Hourglass, LayoutDashboard, MemoryStick, RefreshCw, Users } from "lucide-react";
|
||||||
import { adminSections, dashboardCards, type DashboardCard } from "@/lib/adminAccess";
|
import { adminSections, dashboardCards, type DashboardCard } from "@/lib/admin/adminAccess";
|
||||||
import { balancedColumns, countObjects, DASHBOARD_WINDOW_MS, isRefused, loadMetrics, summarizeMetrics, type MessageStats } from "@/lib/adminDashboard";
|
import { balancedColumns, countObjects, DASHBOARD_WINDOW_MS, isRefused, loadMetrics, summarizeMetrics, type MessageStats } from "@/lib/admin/adminDashboard";
|
||||||
import { formatDayMonthTime, resolvedLocale } from "@/lib/datetime";
|
import { formatDayMonthTime, resolvedLocale } from "@/lib/datetime";
|
||||||
import { formatSize } from "@/lib/format";
|
import { formatSize } from "@/lib/format";
|
||||||
import { t } from "@/lib/i18n";
|
import { t } from "@/lib/i18n";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Link, useLocation } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { Building2, Globe, LayoutDashboard, List, ShieldCheck, User, UsersRound } from "lucide-react";
|
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 { t } from "@/lib/i18n";
|
||||||
import { usePermissions } from "./usePermissions";
|
import { usePermissions } from "./usePermissions";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Redirect } from "wouter";
|
import { Redirect } from "wouter";
|
||||||
import { adminSections, type AdminSection } from "@/lib/adminAccess";
|
import { adminSections, type AdminSection } from "@/lib/admin/adminAccess";
|
||||||
import { AccountsAdmin } from "./AccountsAdmin";
|
import { AccountsAdmin } from "./AccountsAdmin";
|
||||||
import { AdminDashboard } from "./AdminDashboard";
|
import { AdminDashboard } from "./AdminDashboard";
|
||||||
import { DomainsAdmin } from "./DomainsAdmin";
|
import { DomainsAdmin } from "./DomainsAdmin";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Copy, Globe, Plus, Trash2, X } from "lucide-react";
|
import { Copy, Globe, Plus, Trash2, X } from "lucide-react";
|
||||||
import { can } from "@/lib/adminAccess";
|
import { can } from "@/lib/admin/adminAccess";
|
||||||
import { describeDirectoryError } from "@/lib/adminDirectory";
|
import { describeDirectoryError } from "@/lib/admin/adminDirectory";
|
||||||
import {
|
import {
|
||||||
createDomain,
|
createDomain,
|
||||||
describeLinked,
|
describeLinked,
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
type DirectoryDomainFull,
|
type DirectoryDomainFull,
|
||||||
type DkimKey,
|
type DkimKey,
|
||||||
type Managed,
|
type Managed,
|
||||||
} from "@/lib/adminDomains";
|
} from "@/lib/admin/adminDomains";
|
||||||
import { formatFullDate } from "@/lib/format";
|
import { formatFullDate } from "@/lib/format";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
import { Dialog } from "@/ui/dialog";
|
import { Dialog } from "@/ui/dialog";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { ChevronLeft, ChevronRight, Globe, Plus, Search } from "lucide-react";
|
import { ChevronLeft, ChevronRight, Globe, Plus, Search } from "lucide-react";
|
||||||
import { can } from "@/lib/adminAccess";
|
import { can } from "@/lib/admin/adminAccess";
|
||||||
import { describeDirectoryError } from "@/lib/adminDirectory";
|
import { describeDirectoryError } from "@/lib/admin/adminDirectory";
|
||||||
import { countAccounts, getDomains, namesOf, queryDomains, type DirectoryDomainFull } from "@/lib/adminDomains";
|
import { countAccounts, getDomains, namesOf, queryDomains, type DirectoryDomainFull } from "@/lib/admin/adminDomains";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
import { Empty, Spinner } from "@/ui/misc";
|
import { Empty, Spinner } from "@/ui/misc";
|
||||||
import { usePermissions } from "./usePermissions";
|
import { usePermissions } from "./usePermissions";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Search, Trash2, UserMinus, UserPlus, X } from "lucide-react";
|
import { Search, Trash2, UserMinus, UserPlus, X } from "lucide-react";
|
||||||
import { can, canGrantRole } from "@/lib/adminAccess";
|
import { can, canGrantRole } from "@/lib/admin/adminAccess";
|
||||||
import { aliasList, describeDirectoryError, quotasWithDisk, updateAccount, DISK_QUOTA, type EmailAlias } from "@/lib/adminDirectory";
|
import { aliasList, describeDirectoryError, quotasWithDisk, updateAccount, DISK_QUOTA, type EmailAlias } from "@/lib/admin/adminDirectory";
|
||||||
import {
|
import {
|
||||||
createGroup,
|
createGroup,
|
||||||
destroyGroup,
|
destroyGroup,
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
setMembership,
|
setMembership,
|
||||||
type DirectoryGroup,
|
type DirectoryGroup,
|
||||||
type GroupMember,
|
type GroupMember,
|
||||||
} from "@/lib/adminGroups";
|
} from "@/lib/admin/adminGroups";
|
||||||
import { formatSize } from "@/lib/format";
|
import { formatSize } from "@/lib/format";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
import { Avatar, Spinner } from "@/ui/misc";
|
import { Avatar, Spinner } from "@/ui/misc";
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { useLocation } from "wouter";
|
|||||||
import { ChevronLeft, ChevronRight, Plus, Search, UsersRound } from "lucide-react";
|
import { ChevronLeft, ChevronRight, Plus, Search, UsersRound } from "lucide-react";
|
||||||
import { useSession } from "@/store/session";
|
import { useSession } from "@/store/session";
|
||||||
import { STALWART_CAP } from "@/jmap/client";
|
import { STALWART_CAP } from "@/jmap/client";
|
||||||
import { can, type RoleDef } from "@/lib/adminAccess";
|
import { can, type RoleDef } from "@/lib/admin/adminAccess";
|
||||||
import { describeDirectoryError, listDomains, listRoles, type DirectoryDomain } from "@/lib/adminDirectory";
|
import { describeDirectoryError, listDomains, listRoles, type DirectoryDomain } from "@/lib/admin/adminDirectory";
|
||||||
import { countMembers, getGroups, queryGroups, type DirectoryGroup } from "@/lib/adminGroups";
|
import { countMembers, getGroups, queryGroups, type DirectoryGroup } from "@/lib/admin/adminGroups";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
import { Avatar, Empty, Spinner } from "@/ui/misc";
|
import { Avatar, Empty, Spinner } from "@/ui/misc";
|
||||||
import { usePermissions } from "./usePermissions";
|
import { usePermissions } from "./usePermissions";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Plus, Search, Trash2, X } from "lucide-react";
|
import { Plus, Search, Trash2, X } from "lucide-react";
|
||||||
import { can } from "@/lib/adminAccess";
|
import { can } from "@/lib/admin/adminAccess";
|
||||||
import { aliasList, describeDirectoryError, type EmailAlias } from "@/lib/adminDirectory";
|
import { aliasList, describeDirectoryError, type EmailAlias } from "@/lib/admin/adminDirectory";
|
||||||
import { createList, destroyList, parseAddresses, recipientsPatch, updateList, type DirectoryList } from "@/lib/adminLists";
|
import { createList, destroyList, parseAddresses, recipientsPatch, updateList, type DirectoryList } from "@/lib/admin/adminLists";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
import { Dialog } from "@/ui/dialog";
|
import { Dialog } from "@/ui/dialog";
|
||||||
import { toast } from "@/ui/toast";
|
import { toast } from "@/ui/toast";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { ChevronLeft, ChevronRight, List, Plus, Search } from "lucide-react";
|
import { ChevronLeft, ChevronRight, List, Plus, Search } from "lucide-react";
|
||||||
import { can } from "@/lib/adminAccess";
|
import { can } from "@/lib/admin/adminAccess";
|
||||||
import { describeDirectoryError, listDomains, type DirectoryDomain } from "@/lib/adminDirectory";
|
import { describeDirectoryError, listDomains, type DirectoryDomain } from "@/lib/admin/adminDirectory";
|
||||||
import { getLists, queryLists, type DirectoryList } from "@/lib/adminLists";
|
import { getLists, queryLists, type DirectoryList } from "@/lib/admin/adminLists";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
import { Empty, Spinner } from "@/ui/misc";
|
import { Empty, Spinner } from "@/ui/misc";
|
||||||
import { useSession } from "@/store/session";
|
import { useSession } from "@/store/session";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Lock, Search, Trash2, X } from "lucide-react";
|
import { Lock, Search, Trash2, X } from "lucide-react";
|
||||||
import { can, canGrantRole } from "@/lib/adminAccess";
|
import { can, canGrantRole } from "@/lib/admin/adminAccess";
|
||||||
import { describeDirectoryError } from "@/lib/adminDirectory";
|
import { describeDirectoryError } from "@/lib/admin/adminDirectory";
|
||||||
import { describeLinked, DomainError } from "@/lib/adminDomains";
|
import { describeLinked, DomainError } from "@/lib/admin/adminDomains";
|
||||||
import {
|
import {
|
||||||
canBuildOn,
|
canBuildOn,
|
||||||
createRole,
|
createRole,
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
type DirectoryRole,
|
type DirectoryRole,
|
||||||
type PermissionState,
|
type PermissionState,
|
||||||
type RoleDefaults,
|
type RoleDefaults,
|
||||||
} from "@/lib/adminRoles";
|
} from "@/lib/admin/adminRoles";
|
||||||
import type { PermissionEntry } from "@/lib/permissionLabels";
|
import type { PermissionEntry } from "@/lib/permissionLabels";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
import { Dialog } from "@/ui/dialog";
|
import { Dialog } from "@/ui/dialog";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { Plus, Search, ShieldCheck } from "lucide-react";
|
import { Plus, Search, ShieldCheck } from "lucide-react";
|
||||||
import { can } from "@/lib/adminAccess";
|
import { can } from "@/lib/admin/adminAccess";
|
||||||
import { describeDirectoryError } from "@/lib/adminDirectory";
|
import { describeDirectoryError } from "@/lib/admin/adminDirectory";
|
||||||
import { effectivePermissions, listAllRoles, loadPermissionList, loadRoleDefaults, type DirectoryRole, type RoleDefaults } from "@/lib/adminRoles";
|
import { effectivePermissions, listAllRoles, loadPermissionList, loadRoleDefaults, type DirectoryRole, type RoleDefaults } from "@/lib/admin/adminRoles";
|
||||||
import { describePermissions, loadPermissionCatalog, type PermissionEntry } from "@/lib/permissionLabels";
|
import { describePermissions, loadPermissionCatalog, type PermissionEntry } from "@/lib/permissionLabels";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
import { Empty, Spinner } from "@/ui/misc";
|
import { Empty, Spinner } from "@/ui/misc";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Globe, Plus, Trash2, X } from "lucide-react";
|
import { Globe, Plus, Trash2, X } from "lucide-react";
|
||||||
import { can, canGrantRole, type RoleDef } from "@/lib/adminAccess";
|
import { can, canGrantRole, type RoleDef } from "@/lib/admin/adminAccess";
|
||||||
import { describeDirectoryError } from "@/lib/adminDirectory";
|
import { describeDirectoryError } from "@/lib/admin/adminDirectory";
|
||||||
import { describeLinked, DomainError } from "@/lib/adminDomains";
|
import { describeLinked, DomainError } from "@/lib/admin/adminDomains";
|
||||||
import {
|
import {
|
||||||
countTenantMembers,
|
countTenantMembers,
|
||||||
createTenant,
|
createTenant,
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
type TenantMemberKind,
|
type TenantMemberKind,
|
||||||
type TenantQuota,
|
type TenantQuota,
|
||||||
type TenantRoles,
|
type TenantRoles,
|
||||||
} from "@/lib/adminTenants";
|
} from "@/lib/admin/adminTenants";
|
||||||
import { formatSize } from "@/lib/format";
|
import { formatSize } from "@/lib/format";
|
||||||
import { proxiedImageUrl } from "@/lib/html";
|
import { proxiedImageUrl } from "@/lib/html";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { Building2, ChevronLeft, ChevronRight, Plus, Search } from "lucide-react";
|
import { Building2, ChevronLeft, ChevronRight, Plus, Search } from "lucide-react";
|
||||||
import { can, type RoleDef } from "@/lib/adminAccess";
|
import { can, type RoleDef } from "@/lib/admin/adminAccess";
|
||||||
import { describeDirectoryError, listRoles } from "@/lib/adminDirectory";
|
import { describeDirectoryError, listRoles } from "@/lib/admin/adminDirectory";
|
||||||
import { drawableLogo, getTenants, queryTenants, type DirectoryTenant } from "@/lib/adminTenants";
|
import { drawableLogo, getTenants, queryTenants, type DirectoryTenant } from "@/lib/admin/adminTenants";
|
||||||
import { formatSize } from "@/lib/format";
|
import { formatSize } from "@/lib/format";
|
||||||
import { proxiedImageUrl } from "@/lib/html";
|
import { proxiedImageUrl } from "@/lib/html";
|
||||||
import { plural, t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Router } from "wouter";
|
|||||||
import { memoryLocation } from "wouter/memory-location";
|
import { memoryLocation } from "wouter/memory-location";
|
||||||
import { useSession } from "@/store/session";
|
import { useSession } from "@/store/session";
|
||||||
import type { JmapSession } from "@/jmap/types";
|
import type { JmapSession } from "@/jmap/types";
|
||||||
import type { DirectoryAccount } from "@/lib/adminDirectory";
|
import type { DirectoryAccount } from "@/lib/admin/adminDirectory";
|
||||||
import { AccountSheet } from "../AccountSheet";
|
import { AccountSheet } from "../AccountSheet";
|
||||||
import type { DirectoryContext } from "../directoryContext";
|
import type { DirectoryContext } from "../directoryContext";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { memoryLocation } from "wouter/memory-location";
|
|||||||
import { JmapMethodError } from "@/jmap/client";
|
import { JmapMethodError } from "@/jmap/client";
|
||||||
import { useSession } from "@/store/session";
|
import { useSession } from "@/store/session";
|
||||||
import type { JmapSession } from "@/jmap/types";
|
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;
|
(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([]),
|
metrics: (): Promise<MetricRecord[]> => Promise.resolve([]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/adminDashboard", async (original) => ({
|
vi.mock("@/lib/admin/adminDashboard", async (original) => ({
|
||||||
...(await original<typeof import("@/lib/adminDashboard")>()),
|
...(await original<typeof import("@/lib/admin/adminDashboard")>()),
|
||||||
countObjects: vi.fn(async (object: string) => feeds.counts[object]),
|
countObjects: vi.fn(async (object: string) => feeds.counts[object]),
|
||||||
loadMetrics: vi.fn(() => feeds.metrics()),
|
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',
|
dnsZoneFile: 'example.com. IN MX 10 mail.example.com.\nexample.com. IN TXT "v=spf1 mx -all"\n',
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mock("@/lib/adminDomains", async (original) => ({
|
vi.mock("@/lib/admin/adminDomains", async (original) => ({
|
||||||
...(await original<typeof import("@/lib/adminDomains")>()),
|
...(await original<typeof import("@/lib/admin/adminDomains")>()),
|
||||||
getDomains: vi.fn(async () => [domain]),
|
getDomains: vi.fn(async () => [domain]),
|
||||||
listDkimKeys: vi.fn(async () => [{ id: "k1", "@type": "Dkim1Ed25519Sha256", selector: "v1-ed25519", stage: "active" }]),
|
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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { useSession } from "@/store/session";
|
import { useSession } from "@/store/session";
|
||||||
import type { JmapSession } from "@/jmap/types";
|
import type { JmapSession } from "@/jmap/types";
|
||||||
import type { DirectoryGroup } from "@/lib/adminGroups";
|
import type { DirectoryGroup } from "@/lib/admin/adminGroups";
|
||||||
import type { DirectoryContext } from "../directoryContext";
|
import type { DirectoryContext } from "../directoryContext";
|
||||||
|
|
||||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
@@ -17,8 +17,8 @@ const api = vi.hoisted(() => ({
|
|||||||
destroyGroup: vi.fn(async () => {}),
|
destroyGroup: vi.fn(async () => {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/adminGroups", async (original) => ({
|
vi.mock("@/lib/admin/adminGroups", async (original) => ({
|
||||||
...(await original<typeof import("@/lib/adminGroups")>()),
|
...(await original<typeof import("@/lib/admin/adminGroups")>()),
|
||||||
listMembers: vi.fn(async () => ({ members: api.members, total: api.members.length })),
|
listMembers: vi.fn(async () => ({ members: api.members, total: api.members.length })),
|
||||||
searchUsers: vi.fn(async () => []),
|
searchUsers: vi.fn(async () => []),
|
||||||
setMembership: api.setMembership,
|
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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { useSession } from "@/store/session";
|
import { useSession } from "@/store/session";
|
||||||
import type { JmapSession } from "@/jmap/types";
|
import type { JmapSession } from "@/jmap/types";
|
||||||
import type { DirectoryList } from "@/lib/adminLists";
|
import type { DirectoryList } from "@/lib/admin/adminLists";
|
||||||
import type { DirectoryContext } from "../directoryContext";
|
import type { DirectoryContext } from "../directoryContext";
|
||||||
|
|
||||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
const api = vi.hoisted(() => ({ updateList: vi.fn(async () => {}) }));
|
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");
|
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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { useSession } from "@/store/session";
|
import { useSession } from "@/store/session";
|
||||||
import type { JmapSession } from "@/jmap/types";
|
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";
|
import type { PermissionEntry } from "@/lib/permissionLabels";
|
||||||
|
|
||||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
const api = vi.hoisted(() => ({ updateRole: vi.fn(async () => {}) }));
|
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");
|
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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { useSession } from "@/store/session";
|
import { useSession } from "@/store/session";
|
||||||
import type { JmapSession } from "@/jmap/types";
|
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;
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ const api = vi.hoisted(() => ({
|
|||||||
updateTenant: vi.fn(async () => {}),
|
updateTenant: vi.fn(async () => {}),
|
||||||
setDomainTenant: vi.fn(async () => {}),
|
setDomainTenant: vi.fn(async () => {}),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/adminTenants", async (original) => ({
|
vi.mock("@/lib/admin/adminTenants", async (original) => ({
|
||||||
...(await original<typeof import("@/lib/adminTenants")>()),
|
...(await original<typeof import("@/lib/admin/adminTenants")>()),
|
||||||
countTenantMembers: vi.fn(async () => api.counts),
|
countTenantMembers: vi.fn(async () => api.counts),
|
||||||
tenantDomains: vi.fn(async () => ({ inTenant: [{ id: "d3", name: "old-brand.example" }], unassigned: [{ id: "d4", name: "spare.example" }] })),
|
tenantDomains: vi.fn(async () => ({ inTenant: [{ id: "d3", name: "old-brand.example" }], unassigned: [{ id: "d4", name: "spare.example" }] })),
|
||||||
updateTenant: api.updateTenant,
|
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;
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
const api = vi.hoisted(() => ({ queryTenants: vi.fn(async () => ({ ids: ["t1"], total: 1 })) }));
|
const api = vi.hoisted(() => ({ queryTenants: vi.fn(async () => ({ ids: ["t1"], total: 1 })) }));
|
||||||
vi.mock("@/lib/adminTenants", async (original) => ({
|
vi.mock("@/lib/admin/adminTenants", async (original) => ({
|
||||||
...(await original<typeof import("@/lib/adminTenants")>()),
|
...(await original<typeof import("@/lib/admin/adminTenants")>()),
|
||||||
queryTenants: api.queryTenants,
|
queryTenants: api.queryTenants,
|
||||||
getTenants: vi.fn(async () => [{ id: "t1", name: "Acme Corp", quotas: {}, usedDiskQuota: 0 }]),
|
getTenants: vi.fn(async () => [{ id: "t1", name: "Acme Corp", quotas: {}, usedDiskQuota: 0 }]),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { RoleDef } from "@/lib/adminAccess";
|
import type { RoleDef } from "@/lib/admin/adminAccess";
|
||||||
import type { DirectoryAccount, DirectoryDomain } from "@/lib/adminDirectory";
|
import type { DirectoryAccount, DirectoryDomain } from "@/lib/admin/adminDirectory";
|
||||||
import { t } from "@/lib/i18n";
|
import { t } from "@/lib/i18n";
|
||||||
|
|
||||||
export interface DirectoryContext {
|
export interface DirectoryContext {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useSession } from "@/store/session";
|
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.
|
* The signed-in account's permissions, as a set, stable between renders.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type { Anchor } from "@/ui/popover";
|
|||||||
import { CalendarContextMenu, eventColor, type CalendarContext } from "./CalendarContextMenu";
|
import { CalendarContextMenu, eventColor, type CalendarContext } from "./CalendarContextMenu";
|
||||||
import { toast } from "@/ui/toast";
|
import { toast } from "@/ui/toast";
|
||||||
import { askEditScope, droppedMessage, runScoped } from "./scope";
|
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";
|
import { t as translate } from "@/lib/i18n";
|
||||||
|
|
||||||
type View = "month" | "week" | "day" | "agenda";
|
type View = "month" | "week" | "day" | "agenda";
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import { RecipientInput } from "../compose/RecipientInput";
|
|||||||
import { DateField, DateTimeField } from "@/ui/datefield";
|
import { DateField, DateTimeField } from "@/ui/datefield";
|
||||||
import { browserTimeZone, dateToZonedLocal, formatDuration, fromInputDateTime, listTimeZones, parseDuration, toInputDateTime, toLocalDateOnly, zonedToDate, DAY_MS, humanDuration } from "@/lib/dates";
|
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 { 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 { newKey } from "@/lib/contacts";
|
||||||
import { availabilityWindow } from "@/lib/availabilityWindow";
|
import { availabilityWindow } from "@/lib/calendar/availabilityWindow";
|
||||||
import { askEditScope, droppedMessage, runScoped } from "./scope";
|
import { askEditScope, droppedMessage, runScoped } from "./scope";
|
||||||
import { plural, t as translate } from "@/lib/i18n";
|
import { plural, t as translate } from "@/lib/i18n";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { confirmDialog } from "@/ui/dialog";
|
|||||||
import { toast } from "@/ui/toast";
|
import { toast } from "@/ui/toast";
|
||||||
import { askDeleteScope, runScoped } from "./scope";
|
import { askDeleteScope, runScoped } from "./scope";
|
||||||
import { formatTimeRange, humanDuration, parseDuration } from "@/lib/dates";
|
import { formatTimeRange, humanDuration, parseDuration } from "@/lib/dates";
|
||||||
import { describeRule } from "@/lib/recurrence";
|
import { describeRule } from "@/lib/calendar/recurrence";
|
||||||
import { useCompose } from "@/store/compose";
|
import { useCompose } from "@/store/compose";
|
||||||
import { useSettings } from "@/store/settings";
|
import { useSettings } from "@/store/settings";
|
||||||
import { categoryOf, eventColor } from "./CalendarContextMenu";
|
import { categoryOf, eventColor } from "./CalendarContextMenu";
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { rowClick } from "@/lib/listSelection";
|
|||||||
import { MenuItem, MenuSep, MenuTitle, Popover, useMenu } from "@/ui/popover";
|
import { MenuItem, MenuSep, MenuTitle, Popover, useMenu } from "@/ui/popover";
|
||||||
import { useCompose } from "@/store/compose";
|
import { useCompose } from "@/store/compose";
|
||||||
import { useCalendar } from "@/store/calendar";
|
import { useCalendar } from "@/store/calendar";
|
||||||
import { startAppointment } from "@/lib/appointment";
|
import { startAppointment } from "@/lib/calendar/appointment";
|
||||||
import { toast } from "@/ui/toast";
|
import { toast } from "@/ui/toast";
|
||||||
import { haptic, usePullToRefresh, useTouchRow, PULL_TRIGGER } from "@/lib/touch";
|
import { haptic, usePullToRefresh, useTouchRow, PULL_TRIGGER } from "@/lib/touch";
|
||||||
import { describeSwipe, type SwipeAction, type SwipeDescriptor, type SwipeIcon } from "@/lib/swipe";
|
import { describeSwipe, type SwipeAction, type SwipeDescriptor, type SwipeIcon } from "@/lib/swipe";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useSettings } from "@/store/settings";
|
|||||||
import { draftFromMailto, useCompose } from "@/store/compose";
|
import { draftFromMailto, useCompose } from "@/store/compose";
|
||||||
import { useContacts } from "@/store/contacts";
|
import { useContacts } from "@/store/contacts";
|
||||||
import { useCalendar } from "@/store/calendar";
|
import { useCalendar } from "@/store/calendar";
|
||||||
import { startAppointment } from "@/lib/appointment";
|
import { startAppointment } from "@/lib/calendar/appointment";
|
||||||
import { client } from "@/jmap/client";
|
import { client } from "@/jmap/client";
|
||||||
import { SignatureBanner } from "./SignatureBanner";
|
import { SignatureBanner } from "./SignatureBanner";
|
||||||
import { useSignature } from "@/lib/smime/useSignature";
|
import { useSignature } from "@/lib/smime/useSignature";
|
||||||
|
|||||||