fix: Phase 2 QA — all 25 remaining HIGH/MEDIUM/LOW issues

HIGH fixes (7):
- H1: VNCdirectory admin i18n — 30+ translation keys added
- H2: handleSave try/catch with error toast
- H3: Free/busy accountId scoping
- H4: cancelEventBookings filter by eventId
- H5: Resource picker static apiFetch import
- H6: Sharing-store toast messages via lastMessage state
- H7: roleLabel for all resource types

MEDIUM fixes (11):
- M1: identitySignatureMap cleanup on delete
- M2: Now-line relative positioning
- M3: Radial menu disabled item keyboard nav
- M4: Radial menu stable event listener via refs
- M5: cancelBooking error on missing booking
- M6: PasswordRow isMasked state flag
- M7: Extract shared rights into lib/sharing-rights.ts
- M8: VNCtalk client server-side guard
- M9: Collabora configManager instead of process.env
- M10: CONFIG_ENV_MAP VNCdirectory fields
- M11: SENSITIVE_CONFIG_KEYS field name unification

LOW fixes (7):
- L1-L3: Unused imports removed
- L4: aria-labels on close, clear, search, spinner
- L5-L7: Comments for intentional patterns, null guard
This commit is contained in:
Bernd Rodler
2026-08-07 14:21:07 +02:00
parent 2e29af50d6
commit b98ab59f0d
24 changed files with 662 additions and 487 deletions
+9 -4
View File
@@ -121,7 +121,11 @@ export const useResourceStore = create<ResourceState>()((set, get) => ({
cancelBooking: async (bookingId: string) => {
const { bookings } = get();
const booking = bookings.find((b) => b.id === bookingId);
if (!booking) return;
if (!booking) {
console.error(`cancelBooking: booking with id "${bookingId}" not found`);
set({ bookingError: `Booking ${bookingId} not found` });
return;
}
try {
const res = await apiFetch(
@@ -136,9 +140,10 @@ export const useResourceStore = create<ResourceState>()((set, get) => ({
}
},
cancelEventBookings: async (_eventId: string) => {
cancelEventBookings: async (eventId: string) => {
const { bookings } = get();
for (const booking of bookings) {
const eventBookings = bookings.filter((b) => b.eventId === eventId);
for (const booking of eventBookings) {
try {
const res = await apiFetch(
`/api/resources/${booking.resourceId}/book/${booking.id}`,
@@ -149,6 +154,6 @@ export const useResourceStore = create<ResourceState>()((set, get) => ({
// silently fail
}
}
set({ bookings: [] });
set({ bookings: bookings.filter((b) => b.eventId !== eventId) });
},
}));
+25 -181
View File
@@ -7,13 +7,19 @@ import type {
FileNodeRights,
MailboxRights,
} from "@/lib/jmap/types";
import { toast } from "@/stores/toast-store";
import {
type SharedResourceKind,
MAILBOX_ROLE_LABELS,
CALENDAR_ROLE_LABELS,
ADDRESSBOOK_ROLE_LABELS,
FILE_ROLE_LABELS,
resolveRights,
detectMailboxPreset,
detectCalendarPreset,
detectAddressBookPreset,
} from "@/lib/sharing-rights";
export type SharedResourceKind =
| "mailbox"
| "calendar"
| "addressBook"
| "file";
export type { SharedResourceKind } from "@/lib/sharing-rights";
export interface SharedFolder {
id: string;
@@ -34,6 +40,7 @@ interface SharingState {
sharedWithMe: SharedFolder[];
loading: boolean;
principalsCache: Principal[];
lastMessage: { type: 'success' | 'error'; text: string } | null;
loadPrincipals: (client: IJMAPClient) => Promise<Principal[]>;
fetchShares: (client: IJMAPClient) => Promise<void>;
@@ -67,148 +74,17 @@ interface SharingState {
}
function roleLabel(kind: SharedResourceKind, role: string): string {
if (kind === "mailbox") return MAILBOX_ROLE_LABELS[role] ?? role;
return role;
}
const MAILBOX_PRESETS: Record<string, MailboxRights> = {
read: {
mayReadItems: true,
mayAddItems: false,
mayRemoveItems: false,
maySetSeen: false,
maySetKeywords: false,
mayCreateChild: false,
mayRename: false,
mayDelete: false,
maySubmit: false,
},
readWrite: {
mayReadItems: true,
mayAddItems: true,
mayRemoveItems: false,
maySetSeen: true,
maySetKeywords: true,
mayCreateChild: false,
mayRename: false,
mayDelete: false,
maySubmit: true,
},
manager: {
mayReadItems: true,
mayAddItems: true,
mayRemoveItems: true,
maySetSeen: true,
maySetKeywords: true,
mayCreateChild: true,
mayRename: true,
mayDelete: true,
maySubmit: true,
mayShare: true,
},
};
const MAILBOX_ROLE_LABELS: Record<string, string> = {
read: "Viewer",
readWrite: "Editor",
manager: "Manager",
};
const CALENDAR_PRESETS: Record<string, CalendarRights> = {
read: {
mayReadFreeBusy: true,
mayReadItems: true,
mayWriteAll: false,
mayWriteOwn: false,
mayUpdatePrivate: false,
mayRSVP: false,
mayShare: false,
mayDelete: false,
},
readWrite: {
mayReadFreeBusy: true,
mayReadItems: true,
mayWriteAll: true,
mayWriteOwn: true,
mayUpdatePrivate: true,
mayRSVP: true,
mayShare: false,
mayDelete: false,
},
manager: {
mayReadFreeBusy: true,
mayReadItems: true,
mayWriteAll: true,
mayWriteOwn: true,
mayUpdatePrivate: true,
mayRSVP: true,
mayShare: true,
mayDelete: true,
},
};
const ADDRESS_BOOK_PRESETS: Record<string, AddressBookRights> = {
read: { mayRead: true, mayWrite: false, mayShare: false, mayDelete: false },
readWrite: {
mayRead: true,
mayWrite: true,
mayShare: false,
mayDelete: false,
},
manager: {
mayRead: true,
mayWrite: true,
mayShare: true,
mayDelete: true,
},
};
const FILE_PRESETS: Record<string, FileNodeRights> = {
read: {
mayRead: true,
mayAddChildren: false,
mayRename: false,
mayDelete: false,
mayModifyContent: false,
mayShare: false,
},
readWrite: {
mayRead: true,
mayAddChildren: true,
mayRename: true,
mayDelete: true,
mayModifyContent: true,
mayShare: false,
},
manager: {
mayRead: true,
mayAddChildren: true,
mayRename: true,
mayDelete: true,
mayModifyContent: true,
mayShare: true,
},
};
function resolveRights(
kind: SharedResourceKind,
role: string,
): MailboxRights | CalendarRights | AddressBookRights | FileNodeRights {
switch (kind) {
case "mailbox":
return (
MAILBOX_PRESETS[role] ?? MAILBOX_PRESETS.read
);
return MAILBOX_ROLE_LABELS[role] ?? role;
case "calendar":
return (
CALENDAR_PRESETS[role] ?? CALENDAR_PRESETS.read
);
return CALENDAR_ROLE_LABELS[role] ?? role;
case "addressBook":
return (
ADDRESS_BOOK_PRESETS[role] ?? ADDRESS_BOOK_PRESETS.read
);
return ADDRESSBOOK_ROLE_LABELS[role] ?? role;
case "file":
return FILE_PRESETS[role] ?? FILE_PRESETS.read;
return FILE_ROLE_LABELS[role] ?? role;
default:
return role;
}
}
@@ -217,6 +93,7 @@ export const useSharingStore = create<SharingState>((set, get) => ({
sharedWithMe: [],
loading: false,
principalsCache: [],
lastMessage: null,
async loadPrincipals(client) {
const cached = get().principalsCache;
@@ -416,7 +293,7 @@ export const useSharingStore = create<SharingState>((set, get) => ({
entry,
],
}));
toast.success(`Shared "${resourceName}"`);
set({ lastMessage: { type: 'success', text: `Shared "${resourceName}"` } });
},
async revokeShare(client, resourceId, resourceKind, principalId, accountId) {
@@ -440,7 +317,7 @@ export const useSharingStore = create<SharingState>((set, get) => ({
),
),
}));
toast.success("Access revoked");
set({ lastMessage: { type: 'success', text: "Access revoked" } });
},
async changeRole(
@@ -463,7 +340,7 @@ export const useSharingStore = create<SharingState>((set, get) => ({
: f,
),
}));
toast.success("Role updated");
set({ lastMessage: { type: 'success', text: "Role updated" } });
},
async acceptShare(_client, share) {
@@ -472,14 +349,14 @@ export const useSharingStore = create<SharingState>((set, get) => ({
f.id === share.id ? { ...f, pending: false } : f,
),
}));
toast.success(`Accepted share: ${share.resourceName}`);
set({ lastMessage: { type: 'success', text: `Accepted share: ${share.resourceName}` } });
},
async declineShare(_client, share) {
set((s) => ({
sharedWithMe: s.sharedWithMe.filter((f) => f.id !== share.id),
}));
toast.success(`Declined share: ${share.resourceName}`);
set({ lastMessage: { type: 'success', text: `Declined share: ${share.resourceName}` } });
},
}));
@@ -532,37 +409,4 @@ async function applyShare(
}
}
function detectMailboxPreset(r: MailboxRights): string {
for (const [name, preset] of Object.entries(MAILBOX_PRESETS)) {
const keys = Object.keys(preset) as (keyof MailboxRights)[];
if (
keys.every(
(k) =>
(preset[k] ?? false) === (r[k as keyof MailboxRights] ?? false),
)
) {
return name;
}
}
return "custom";
}
function detectCalendarPreset(r: CalendarRights): string {
for (const [name, preset] of Object.entries(CALENDAR_PRESETS)) {
const keys = Object.keys(preset) as (keyof CalendarRights)[];
if (keys.every((k) => (preset[k] ?? false) === (r[k as keyof CalendarRights] ?? false))) {
return name;
}
}
return "custom";
}
function detectAddressBookPreset(r: AddressBookRights): string {
for (const [name, preset] of Object.entries(ADDRESS_BOOK_PRESETS)) {
const keys = Object.keys(preset) as (keyof AddressBookRights)[];
if (keys.every((k) => (preset[k] ?? false) === (r[k as keyof AddressBookRights] ?? false))) {
return name;
}
}
return "custom";
}
+22 -5
View File
@@ -61,11 +61,28 @@ export const useSignatureStore = create<SignatureState>()(
},
deleteSignature: (id) => {
set((state) => ({
signatures: state.signatures.filter((s) => s.id !== id),
defaultSignatureId: state.defaultSignatureId === id ? null : state.defaultSignatureId,
replySignatureId: state.replySignatureId === id ? null : state.replySignatureId,
}));
set((state) => {
const nextMap = { ...state.identitySignatureMap };
for (const identityId of Object.keys(nextMap)) {
const entry = nextMap[identityId];
if (entry.defaultId === id || entry.replyId === id) {
const updated = { ...entry };
if (updated.defaultId === id) delete updated.defaultId;
if (updated.replyId === id) delete updated.replyId;
if (Object.keys(updated).length === 0) {
delete nextMap[identityId];
} else {
nextMap[identityId] = updated;
}
}
}
return {
signatures: state.signatures.filter((s) => s.id !== id),
defaultSignatureId: state.defaultSignatureId === id ? null : state.defaultSignatureId,
replySignatureId: state.replySignatureId === id ? null : state.replySignatureId,
identitySignatureMap: nextMap,
};
});
},
duplicateSignature: (id) => {