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:
@@ -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
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user