forked from baron/baron-sso
test: raise frontend coverage baselines
This commit is contained in:
@@ -8,7 +8,6 @@ import {
|
||||
Plus,
|
||||
Save,
|
||||
Trash2,
|
||||
Mail,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../components/ui/card";
|
||||
import { Checkbox } from "../../components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -184,7 +182,7 @@ function UserCreatePage() {
|
||||
if (e.key === "Enter" || e.key === "," || e.key === " ") {
|
||||
e.preventDefault();
|
||||
const value = newSubEmail.trim().replace(/,/g, "");
|
||||
if (value && value.includes("@") && !currentSubEmails.includes(value)) {
|
||||
if (value?.includes("@") && !currentSubEmails.includes(value)) {
|
||||
setValue("metadata.sub_email", [...currentSubEmails, value], {
|
||||
shouldDirty: true,
|
||||
});
|
||||
@@ -667,8 +665,7 @@ function UserCreatePage() {
|
||||
onClick={() => {
|
||||
const value = newSubEmail.trim().replace(/,/g, "");
|
||||
if (
|
||||
value &&
|
||||
value.includes("@") &&
|
||||
value?.includes("@") &&
|
||||
!currentSubEmails.includes(value)
|
||||
) {
|
||||
setValue(
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../components/ui/card";
|
||||
import { Checkbox } from "../../components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -95,8 +94,8 @@ import { resolvePersonalTenant } from "./utils/personalTenant";
|
||||
|
||||
type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
|
||||
email: string;
|
||||
metadata: Record<string, Record<string, string | number | boolean>> & {
|
||||
sub_email?: string[];
|
||||
metadata: Record<string, unknown> & {
|
||||
sub_email?: string | string[];
|
||||
};
|
||||
};
|
||||
type UserCategory = "hanmac" | "external" | "personal";
|
||||
@@ -109,6 +108,44 @@ type AppointmentDraft = UserAppointment & {
|
||||
|
||||
const PASSWORD_RESET_MIN_LENGTH = 12;
|
||||
|
||||
function isMetadataRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cleanMetadataValue(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.filter((item): item is string => typeof item === "string")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (isMetadataRecord(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).filter(
|
||||
([_, fieldValue]) =>
|
||||
fieldValue !== undefined && fieldValue !== null && fieldValue !== "",
|
||||
),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeSubEmails(value: unknown): string[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.filter((item): item is string => typeof item === "string")
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.includes("@"));
|
||||
}
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
return value
|
||||
.split(/[;,\n\r\t]/)
|
||||
.map((email) => email.trim())
|
||||
.filter((email) => email.includes("@"));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function createDraftId() {
|
||||
return globalThis.crypto?.randomUUID?.() ?? `appointment-${Date.now()}`;
|
||||
}
|
||||
@@ -322,8 +359,8 @@ function UserDetailPage() {
|
||||
const userId = params.id ?? "";
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [successMsg, setSuccessMsg] = React.useState<string | null>(null);
|
||||
const [_error, _setError] = React.useState<string | null>(null);
|
||||
const [_successMsg, _setSuccessMsg] = React.useState<string | null>(null);
|
||||
const [isPasswordResetOpen, setIsPasswordResetOpen] = React.useState(false);
|
||||
const [generatedPassword, setGeneratedPassword] = React.useState<
|
||||
string | null
|
||||
@@ -419,7 +456,7 @@ function UserDetailPage() {
|
||||
if (e.key === "Enter" || e.key === "," || e.key === " ") {
|
||||
e.preventDefault();
|
||||
const value = newSubEmail.trim().replace(/,/g, "");
|
||||
if (value && value.includes("@") && !currentSubEmails.includes(value)) {
|
||||
if (value?.includes("@") && !currentSubEmails.includes(value)) {
|
||||
setValue("metadata.sub_email", [...currentSubEmails, value], {
|
||||
shouldDirty: true,
|
||||
});
|
||||
@@ -595,7 +632,7 @@ function UserDetailPage() {
|
||||
);
|
||||
};
|
||||
|
||||
const setPrimaryAppointment = (targetIndex: number) => {
|
||||
const _setPrimaryAppointment = (targetIndex: number) => {
|
||||
setAdditionalAppointments((current) =>
|
||||
current.map((appointment, index) => ({
|
||||
...appointment,
|
||||
@@ -774,15 +811,17 @@ function UserDetailPage() {
|
||||
});
|
||||
|
||||
const onSubmit = async (data: UserFormValues) => {
|
||||
// Filter out undefined/null/empty strings from metadata
|
||||
const cleanMetadata = Object.fromEntries(
|
||||
Object.entries(data.metadata).map(([tenantId, fields]) => {
|
||||
const cleanFields = Object.fromEntries(
|
||||
Object.entries(fields).filter(
|
||||
([_, v]) => v !== undefined && v !== null && v !== "",
|
||||
),
|
||||
);
|
||||
return [tenantId, cleanFields];
|
||||
Object.entries(data.metadata ?? {}).flatMap(([key, value]) => {
|
||||
const cleanedValue = cleanMetadataValue(value);
|
||||
if (
|
||||
cleanedValue === undefined ||
|
||||
cleanedValue === null ||
|
||||
cleanedValue === ""
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return [[key, cleanedValue]];
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -792,19 +831,11 @@ function UserDetailPage() {
|
||||
sub_email: rawSubEmail,
|
||||
...safeMetadata
|
||||
} = cleanMetadata;
|
||||
|
||||
// Parse sub_email
|
||||
let sub_email: string[] = [];
|
||||
if (typeof rawSubEmail === "string" && rawSubEmail.trim() !== "") {
|
||||
sub_email = rawSubEmail
|
||||
.split(/[;,\n\r\t]/)
|
||||
.map((e) => e.trim())
|
||||
.filter((e) => e.includes("@"));
|
||||
}
|
||||
const subEmail = normalizeSubEmails(rawSubEmail);
|
||||
|
||||
const metadata: Record<string, unknown> = {
|
||||
...safeMetadata,
|
||||
...(sub_email.length > 0 ? { sub_email } : { sub_email: [] }),
|
||||
...(subEmail.length > 0 ? { sub_email: subEmail } : { sub_email: [] }),
|
||||
};
|
||||
|
||||
const payload: UserUpdateRequest = {
|
||||
@@ -813,7 +844,7 @@ function UserDetailPage() {
|
||||
};
|
||||
// email cannot be updated directly via this API in current backend implementation,
|
||||
// so we delete it from payload if it spread
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
delete payload.email;
|
||||
payload.role = undefined;
|
||||
|
||||
@@ -989,8 +1020,7 @@ function UserDetailPage() {
|
||||
<Mail size={14} className="text-primary/70" />
|
||||
{user.email}
|
||||
</div>
|
||||
{user.metadata?.sub_email &&
|
||||
Array.isArray(user.metadata.sub_email) &&
|
||||
{Array.isArray(user.metadata?.sub_email) &&
|
||||
user.metadata.sub_email.length > 0 && (
|
||||
<div className="flex items-center gap-1.5 bg-background px-2.5 py-1 rounded-full border">
|
||||
<Mail size={14} className="text-primary/40" />
|
||||
@@ -1167,8 +1197,7 @@ function UserDetailPage() {
|
||||
onClick={() => {
|
||||
const value = newSubEmail.trim().replace(/,/g, "");
|
||||
if (
|
||||
value &&
|
||||
value.includes("@") &&
|
||||
value?.includes("@") &&
|
||||
!currentSubEmails.includes(value)
|
||||
) {
|
||||
setValue(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createI18nMock } from "../../test/i18nMock";
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Download,
|
||||
FileDown,
|
||||
FileSpreadsheet,
|
||||
LayoutDashboard,
|
||||
@@ -268,7 +267,7 @@ const UserListSearchControls = React.memo(function UserListSearchControls({
|
||||
});
|
||||
|
||||
function UserListPage() {
|
||||
const navigate = useNavigate();
|
||||
const _navigate = useNavigate();
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [selectedCompany, setSelectedCompany] = React.useState<string>("");
|
||||
@@ -563,7 +562,7 @@ function UserListPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleApplyBulkStatus = () => {
|
||||
const _handleApplyBulkStatus = () => {
|
||||
if (selectedUserIds.length === 0 || !selectedBulkStatus) return;
|
||||
bulkUpdateMutation.mutate({
|
||||
userIds: selectedUserIds,
|
||||
@@ -571,7 +570,7 @@ function UserListPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleApplyBulkPermission = () => {
|
||||
const _handleApplyBulkPermission = () => {
|
||||
if (selectedUserIds.length === 0 || !selectedBulkPermission) return;
|
||||
bulkUpdateMutation.mutate({
|
||||
userIds: selectedUserIds,
|
||||
@@ -594,7 +593,7 @@ function UserListPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (userId: string, userName: string) => {
|
||||
const _handleDelete = (userId: string, userName: string) => {
|
||||
if (
|
||||
!window.confirm(
|
||||
t(
|
||||
|
||||
@@ -19,8 +19,6 @@ import {
|
||||
bulkUpdateUsers,
|
||||
fetchAllTenants,
|
||||
fetchGroups,
|
||||
type GroupSummary,
|
||||
type TenantSummary,
|
||||
type UserSummary,
|
||||
} from "../../../lib/adminApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
@@ -49,7 +47,7 @@ export function UserBulkMoveGroupModal({
|
||||
const [searchTerm, setSearchTerm] = React.useState("");
|
||||
const [acknowledgeWarning, setAcknowledgeWarning] = React.useState(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const _queryClient = useQueryClient();
|
||||
|
||||
const { data: tenantsData } = useQuery({
|
||||
queryKey: ["tenants", "all"],
|
||||
|
||||
@@ -102,7 +102,7 @@ export function isHanmacFamilyTenant<T extends TenantFilterTarget>(
|
||||
tenants: T[],
|
||||
hanmacFamilyTenantId?: string,
|
||||
) {
|
||||
if (!tenant || !tenant.id) return false;
|
||||
if (!tenant?.id) return false;
|
||||
|
||||
const rootTenantId = resolveHanmacFamilyTenantId(
|
||||
tenants,
|
||||
|
||||
@@ -354,10 +354,12 @@ function applySecondaryEmailMetadata(
|
||||
value: string,
|
||||
) {
|
||||
const emails = splitEmailTokens(value);
|
||||
item.metadata.sub_email = uniqueEmails([
|
||||
...metadataEmailList(item.metadata.sub_email),
|
||||
const uniqueSecondaryEmails = uniqueEmails([
|
||||
...metadataEmailList(item.metadata.secondary_emails),
|
||||
...emails,
|
||||
]);
|
||||
item.metadata.sub_email = value;
|
||||
item.metadata.secondary_emails = uniqueSecondaryEmails;
|
||||
addWorksmobileAliasEmails(item, emails);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user