Merge pull request #372 from Coffey-Labs/refactor/lib-domain-folders
Group the admin and calendar modules, and stop calling screenshots docs
@@ -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";
|
||||||
|
|||||||