chore: release v1.1.2

Add a Changelog page that renders CHANGELOG.md from the header menu,
switch the default color theme to the brand-accurate Stalwart theme, and
document all changes since v1.1.1.
This commit is contained in:
Steven RYDELL
2026-08-01 23:25:02 +02:00
parent f5524eb45a
commit 41b3824894
9 changed files with 125 additions and 11 deletions
+19
View File
@@ -2,6 +2,25 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
## [1.1.2] - 2026-08-01
### Added
- Brand-accurate "Stalwart" color theme (red/pink accent from the official site tokens). The previous theme named "Stalwart" was only the neutral black/white look and is now labeled "Default".
- Changelog page in the header user dropdown, rendering this repository's `CHANGELOG.md` so release notes stay in sync with every release.
- Usage/Quota column on the Groups list (same synthetic column as Accounts); unlimited quota renders as ∞ instead of the word "Unlimited".
- Aliases count column on Accounts, Groups, Mailing Lists, and Domains (resolved from each object's real `aliases` property).
- Enabled Permissions and Disabled Permissions count columns on the Roles list.
- Client-side column sorting on Accounts, Groups, Mailing Lists, Roles, and Domains for the most useful columns (Email/Name/Description, Usage/Quota, Aliases, permission counts, Domain Name, Enabled). The backend rejects sort on these properties (`unsupportedSort`), so lists fetch then sort/paginate locally when a sortable header is clicked — same approach as mailbox hierarchy sort.
- `scripts/dev-seed` (`.sh` / `.ps1`) to populate the local test server with sample Users, Groups, Mailing Lists, and Roles after `dev-server-init` (idempotent; documented in DEVELOPMENT.md).
### Changed
- Default color theme is now the new Stalwart theme (previously Ocean since 1.0.9); existing saved preferences are unaffected.
- Client-side sort is driven by a shared `clientSortable` column flag instead of hardcoded view/column maps, so any list can opt in without touching `DynamicList` branching.
### Fixed
- Sidebar: expanding a collapsible group (e.g. Directory) and then navigating to an unrelated top-level link (e.g. Cluster) now collapses that group instead of leaving it open.
- Vite respects the `$PORT` environment variable so preview/dev tooling that assigns a free port can bind the same port Vite actually listens on.
## [1.1.1] - 2026-08-01
### Fixed
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "stalwart-webui-fork",
"version": "1.1.1",
"version": "1.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "stalwart-webui-fork",
"version": "1.1.1",
"version": "1.1.2",
"dependencies": {
"@daypicker/react": "^10.0.1",
"@radix-ui/react-alert-dialog": "^1.1.23",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "stalwart-webui-fork",
"private": true,
"version": "1.1.1",
"version": "1.1.2",
"description": "Stalwart WebUI Fork",
"type": "module",
"scripts": {
+6 -1
View File
@@ -7,7 +7,7 @@
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import * as LucideIcons from 'lucide-react';
const { User, LogOut, Check, Menu, Sparkles, Search, Palette } = LucideIcons;
const { User, LogOut, Check, Menu, Sparkles, Search, Palette, ScrollText } = LucideIcons;
import { Button } from '@/components/ui/button';
import { CommandPalette } from '@/components/common/CommandPalette';
import {
@@ -203,6 +203,11 @@ export function TopBar() {
{t('appearance.label', 'Appearance')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigate('/Changelog')}>
<ScrollText className="mr-2 h-4 w-4" />
{t('changelog.label', 'Changelog')}
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onClick={() => {
+78
View File
@@ -0,0 +1,78 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
import { ExternalLink } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import type { Components } from 'react-markdown';
import { Card, CardContent } from '@/components/ui/card';
import changelogSource from '../../../CHANGELOG.md?raw';
// Renders this fork's actual CHANGELOG.md — the single source of truth for
// release notes, kept up to date after every release — instead of a
// separately maintained copy that could drift from it.
const components: Components = {
h1: () => null,
h2: ({ children }) => (
<h2 className="mt-10 border-b pb-2 text-lg font-semibold tracking-tight first:mt-0">{children}</h2>
),
h3: ({ children }) => (
<h3 className="mt-5 mb-2 text-xs font-semibold tracking-wide text-muted-foreground uppercase">{children}</h3>
),
p: ({ children }) => <p className="text-sm text-muted-foreground">{children}</p>,
ul: ({ children }) => <ul className="mt-1 list-disc space-y-1.5 pl-5 text-sm">{children}</ul>,
li: ({ children }) => <li className="leading-relaxed">{children}</li>,
a: ({ children, href }) => (
<a
href={href}
target="_blank"
rel="noreferrer"
className="text-primary underline underline-offset-2 hover:no-underline"
>
{children}
</a>
),
code: ({ children }) => <code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">{children}</code>,
};
export function ChangelogPage() {
const { t } = useTranslation();
return (
<div className="mx-auto w-full max-w-3xl space-y-6">
<Card>
<CardContent className="flex items-start gap-3 p-4 sm:p-6">
<ExternalLink className="mt-0.5 h-4 w-4 shrink-0 text-primary" aria-hidden />
<p className="text-sm leading-relaxed">
{t(
'changelog.officialNotice',
'The official WebUI is maintained by the Stalwart team and is available at',
)}{' '}
<a
href="https://github.com/stalwartlabs/webui"
target="_blank"
rel="noreferrer"
className="font-medium text-primary underline underline-offset-2 hover:no-underline"
>
https://github.com/stalwartlabs/webui
</a>
.
</p>
</CardContent>
</Card>
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">{t('changelog.label', 'Changelog')}</h1>
<p className="text-sm text-muted-foreground">
{t('changelog.description', "What's new in this fork, release by release.")}
</p>
</div>
<ReactMarkdown components={components}>{changelogSource}</ReactMarkdown>
</div>
);
}
+5
View File
@@ -56,6 +56,11 @@
"welcome": "Welcome to Stalwart",
"welcomeSubtitle": "Let's get your server set up."
},
"changelog": {
"description": "What's new in this fork, release by release.",
"label": "Changelog",
"officialNotice": "The official WebUI is maintained by the Stalwart team and is available at"
},
"common": {
"apply": "Apply",
"back": "Back",
+5 -2
View File
@@ -24,8 +24,11 @@ import { getBasePath } from './lib/basePath';
const theme = state?.theme;
const dark = theme === 'dark' || (theme !== 'light' && !!window.matchMedia?.('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', dark);
const colorTheme = state?.colorTheme;
// Match uiStore defaults (stalwart + square) so the first paint is correct
// before zustand rehydrates — including brand-new sessions with no persisted state.
const colorTheme = state?.colorTheme ?? 'stalwart';
if (
colorTheme === 'stalwart' ||
colorTheme === 'ocean' ||
colorTheme === 'forest' ||
colorTheme === 'violet' ||
@@ -35,7 +38,7 @@ import { getBasePath } from './lib/basePath';
) {
document.documentElement.dataset.theme = colorTheme;
}
if (state?.radius === 'square') {
if ((state?.radius ?? 'square') === 'square') {
document.documentElement.dataset.radius = 'square';
}
// eslint-disable-next-line no-empty
+7 -3
View File
@@ -27,6 +27,7 @@ import { LoadingFallback } from '@/components/common/LoadingFallback';
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
import { friendlyName } from '@/hooks/useGlobalSearch';
import { AppearancePage } from '@/features/appearance/AppearancePage';
import { ChangelogPage } from '@/features/changelog/ChangelogPage';
import {
findFirstAccessibleLinkInLayout,
findFirstVisibleLinkInLayout,
@@ -101,6 +102,7 @@ export default function AdminPanel() {
// what the user sees in the navigation, e.g. "MTA-STS · Settings".
const pageTitle = useMemo(() => {
if (section === 'Appearance') return t('appearance.label', 'Appearance');
if (section === 'Changelog') return t('changelog.label', 'Changelog');
if (!section) return t('dashboard.title', 'Dashboard');
if (!viewName) return section;
const entry =
@@ -220,9 +222,9 @@ export default function AdminPanel() {
}
}
if (section === 'Appearance') {
// Appearance is a client-side page outside the schema layouts: keep the
// sidebar bound to a real section while it is open.
if (section === 'Appearance' || section === 'Changelog') {
// Appearance and Changelog are client-side pages outside the schema
// layouts: keep the sidebar bound to a real section while open.
if (!layouts.some((l) => l.name === activeSection)) {
setActiveSection(layouts[0]?.name ?? '');
}
@@ -337,6 +339,8 @@ export default function AdminPanel() {
<ErrorBoundary key={activeAccountId ?? 'none'}>
{section === 'Appearance' ? (
<AppearancePage />
) : section === 'Changelog' ? (
<ChangelogPage />
) : (
<MainContent viewName={viewName} id={id} section={section} />
)}
+2 -2
View File
@@ -73,7 +73,7 @@ export const useUIStore = create<UIState>()(
(set, get) => ({
theme:
typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
colorTheme: 'ocean',
colorTheme: 'stalwart',
radius: 'square',
sidebarOpen: typeof window !== 'undefined' ? (window.matchMedia?.('(min-width: 768px)').matches ?? true) : true,
activeSection: '',
@@ -116,7 +116,7 @@ export const useUIStore = create<UIState>()(
return (state) => {
if (state) {
applyThemeClass(state.theme);
applyColorThemeAttribute(state.colorTheme ?? 'ocean');
applyColorThemeAttribute(state.colorTheme ?? 'stalwart');
applyRadiusAttribute(state.radius ?? 'square');
}
};