forked from baron/baron-sso
chore: fix formatting and lint errors across Makefile, backend, and adminfront
This commit addresses several linting and formatting issues that caused CI checks to fail: - Makefile: Removed obsolete '--organize-imports-enabled' from Biome and switched to '@biomejs/biome'. - backend: Fixed spacing and alignment issues according to gofmt. - adminfront: Fixed multiple unused variables and imports, and configured unsafe fixes in the Biome config to remove dead code.
This commit is contained in:
12
Makefile
12
Makefile
@@ -276,18 +276,18 @@ code-check-front-lint:
|
||||
@echo "==> adminfront biome lint/format check"
|
||||
rm -rf adminfront/playwright-report adminfront/test-results
|
||||
cd adminfront && CI=true npx pnpm install --frozen-lockfile --ignore-scripts
|
||||
cd adminfront && npx biome check . --formatter-enabled=false --organize-imports-enabled=false
|
||||
cd adminfront && npx biome check . --linter-enabled=false --organize-imports-enabled=false
|
||||
cd adminfront && npx @biomejs/biome check . --formatter-enabled=false --assist-enabled=false
|
||||
cd adminfront && npx @biomejs/biome check . --linter-enabled=false --assist-enabled=false
|
||||
@echo "==> devfront biome lint/format check"
|
||||
rm -rf devfront/playwright-report devfront/test-results
|
||||
cd devfront && npm ci --ignore-scripts
|
||||
cd devfront && npx biome check . --formatter-enabled=false --organize-imports-enabled=false
|
||||
cd devfront && npx biome check . --linter-enabled=false --organize-imports-enabled=false
|
||||
cd devfront && npx @biomejs/biome check . --formatter-enabled=false --assist-enabled=false
|
||||
cd devfront && npx @biomejs/biome check . --linter-enabled=false --assist-enabled=false
|
||||
@echo "==> orgfront biome lint/format check"
|
||||
rm -rf orgfront/playwright-report orgfront/test-results
|
||||
cd orgfront && npm ci --ignore-scripts
|
||||
cd orgfront && npx biome check . --formatter-enabled=false --organize-imports-enabled=false
|
||||
cd orgfront && npx biome check . --linter-enabled=false --organize-imports-enabled=false
|
||||
cd orgfront && npx @biomejs/biome check . --formatter-enabled=false --assist-enabled=false
|
||||
cd orgfront && npx @biomejs/biome check . --linter-enabled=false --assist-enabled=false
|
||||
|
||||
code-check-backend-tests:
|
||||
@echo "==> backend tests"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createServer } from "node:http";
|
||||
import { extname, join, normalize, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const rootDir = fileURLToPath(new URL("..", import.meta.url));
|
||||
const _rootDir = fileURLToPath(new URL("..", import.meta.url));
|
||||
const distDir = resolve(
|
||||
process.env.ADMINFRONT_BUILD_OUT_DIR ?? "/tmp/baron-sso-adminfront-dist",
|
||||
);
|
||||
|
||||
@@ -19,7 +19,6 @@ import { TenantProfilePage } from "../features/tenants/routes/TenantProfilePage"
|
||||
import { TenantSchemaPage } from "../features/tenants/routes/TenantSchemaPage";
|
||||
import { TenantWorksmobilePage } from "../features/tenants/routes/TenantWorksmobilePage";
|
||||
import TenantUserGroupsTab from "../features/user-groups/routes/TenantUserGroupsTab";
|
||||
import { UserGroupDetailPage } from "../features/user-groups/routes/UserGroupDetailPage";
|
||||
import UserCreatePage from "../features/users/UserCreatePage";
|
||||
import UserDetailPage from "../features/users/UserDetailPage";
|
||||
import UserListPage from "../features/users/UserListPage";
|
||||
|
||||
@@ -150,8 +150,8 @@ function AppLayout() {
|
||||
);
|
||||
const {
|
||||
data: profile,
|
||||
isLoading: isProfileLoading,
|
||||
error: profileError,
|
||||
isLoading: _isProfileLoading,
|
||||
error: _profileError,
|
||||
} = useQuery({
|
||||
queryKey: ["me"],
|
||||
queryFn: async () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { AxiosError } from "axios";
|
||||
import { Download, NotebookTabs, RefreshCw, Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
formatAuditValue,
|
||||
parseAuditDetails,
|
||||
resolveAuditAction,
|
||||
resolveAuditActor,
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
fetchDataIntegrityReport,
|
||||
type RPUsageDailyMetric,
|
||||
type RPUsagePeriod,
|
||||
type TenantSummary,
|
||||
} from "../../lib/adminApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Plus,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
UserPlus,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../../components/ui/dialog";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import {
|
||||
@@ -68,7 +66,7 @@ function mergePendingMembers(
|
||||
export function TenantAdminsAndOwnersTab() {
|
||||
const auth = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const currentUserId = auth.user?.profile.sub;
|
||||
const _currentUserId = auth.user?.profile.sub;
|
||||
const { tenantId: tenantIdParam } = useParams<{ tenantId: string }>();
|
||||
const tenantId = tenantIdParam ?? "";
|
||||
const queryClient = useQueryClient();
|
||||
@@ -187,7 +185,7 @@ export function TenantAdminsAndOwnersTab() {
|
||||
),
|
||||
);
|
||||
},
|
||||
onError: (err: AxiosError<{ error?: string }>, userId, context) => {
|
||||
onError: (err: AxiosError<{ error?: string }>, _userId, context) => {
|
||||
if (context?.previousOwners) {
|
||||
queryClient.setQueryData(
|
||||
["tenant-owners", tenantId],
|
||||
@@ -288,7 +286,7 @@ export function TenantAdminsAndOwnersTab() {
|
||||
t("msg.admin.tenants.admins.remove_success", "권한이 회수되었습니다."),
|
||||
);
|
||||
},
|
||||
onError: (err: AxiosError<{ error?: string }>, userId, context) => {
|
||||
onError: (err: AxiosError<{ error?: string }>, _userId, context) => {
|
||||
if (context?.previousAdmins) {
|
||||
queryClient.setQueryData(
|
||||
["tenant-admins", tenantId],
|
||||
@@ -310,7 +308,7 @@ export function TenantAdminsAndOwnersTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveOwner = (userId: string, userName: string) => {
|
||||
const _handleRemoveOwner = (userId: string, userName: string) => {
|
||||
if (
|
||||
window.confirm(
|
||||
t(
|
||||
@@ -324,7 +322,7 @@ export function TenantAdminsAndOwnersTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAdmin = (userId: string, userName: string) => {
|
||||
const _handleRemoveAdmin = (userId: string, userName: string) => {
|
||||
if (
|
||||
window.confirm(
|
||||
t(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Copy } from "lucide-react";
|
||||
import { Link, Outlet, useLocation, useParams } from "react-router-dom";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { fetchMe, fetchTenant } from "../../../lib/adminApi";
|
||||
import { t } from "../../../lib/i18n";
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../../components/ui/dialog";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import { Label } from "../../../components/ui/label";
|
||||
@@ -239,7 +238,7 @@ const UserGroupTreeNode: React.FC<UserGroupTreeNodeProps> = ({
|
||||
function TenantGroupsPage() {
|
||||
const params = useParams<{ tenantId: string }>();
|
||||
const tenantId = params.tenantId ?? "";
|
||||
const queryClient = useQueryClient();
|
||||
const _queryClient = useQueryClient();
|
||||
|
||||
const [newGroupName, setNewGroupName] = useState("");
|
||||
const [newGroupDesc, setNewGroupNameDesc] = useState("");
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import type { AxiosError } from "axios";
|
||||
import {
|
||||
ArrowDown,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Download,
|
||||
ExternalLink,
|
||||
FileSpreadsheet,
|
||||
LayoutDashboard,
|
||||
List,
|
||||
@@ -28,22 +26,13 @@ import {
|
||||
import * as React from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { PageHeader } from "../../../../../common/core/components/page";
|
||||
import {
|
||||
SortableTableHead,
|
||||
sortableTableHeadBaseClassName,
|
||||
sortableTableHeaderClassName,
|
||||
} from "../../../../../common/core/components/sort";
|
||||
import {
|
||||
type SortConfig,
|
||||
type SortResolverMap,
|
||||
sortItems,
|
||||
toggleSort,
|
||||
} from "../../../../../common/core/utils";
|
||||
import {
|
||||
commonStickyTableHeaderClass,
|
||||
commonTableShellClass,
|
||||
commonTableViewportClass,
|
||||
} from "../../../../../common/ui/table";
|
||||
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
||||
import { RoleGuard } from "../../../components/auth/RoleGuard";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
@@ -71,7 +60,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "../../../components/ui/dropdown-menu";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import { ScrollArea } from "../../../components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -79,7 +67,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../../components/ui/select";
|
||||
import { Separator } from "../../../components/ui/separator";
|
||||
import { Switch } from "../../../components/ui/switch";
|
||||
import {
|
||||
Table,
|
||||
@@ -132,13 +119,12 @@ import {
|
||||
const tenantCSVTemplate =
|
||||
"name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type\n";
|
||||
const tenantPageSize = 500;
|
||||
const tenantVirtualizationThreshold = 250;
|
||||
const tenantEstimatedRowHeight = 73;
|
||||
const tenantLoadAheadPx = 360;
|
||||
const tenantLoadAheadRows = 30;
|
||||
const _tenantVirtualizationThreshold = 250;
|
||||
const _tenantEstimatedRowHeight = 73;
|
||||
const _tenantLoadAheadPx = 360;
|
||||
const _tenantLoadAheadRows = 30;
|
||||
|
||||
type TenantSortKey = keyof TenantSummary | "recursiveMemberCount";
|
||||
type TenantListRow = TenantSummary & { recursiveMemberCount: number };
|
||||
|
||||
const getTenantIcon = (type?: string) => {
|
||||
switch (type?.toUpperCase()) {
|
||||
@@ -301,7 +287,7 @@ function TenantListPage() {
|
||||
>({});
|
||||
const [previewOpen, setPreviewOpen] = React.useState(false);
|
||||
const [selectedBulkStatus, setSelectedBulkStatus] = React.useState("");
|
||||
const tenantTableScrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const _tenantTableScrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { data: profile } = useQuery({
|
||||
queryKey: ["me"],
|
||||
@@ -1368,8 +1354,8 @@ const TenantHierarchyView: React.FC<{
|
||||
selectedIds,
|
||||
onSelect,
|
||||
onSelectAll,
|
||||
onDelete,
|
||||
isDeletePending,
|
||||
onDelete: _onDelete,
|
||||
isDeletePending: _isDeletePending,
|
||||
search,
|
||||
deletableTenants,
|
||||
statusMutation,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowRight, Building2, Plus } from "lucide-react";
|
||||
import { Building2, Plus } from "lucide-react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
commonStickyTableHeaderClass,
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import {
|
||||
Loader2,
|
||||
Mail,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
User,
|
||||
UserMinus,
|
||||
UserPlus,
|
||||
} from "lucide-react";
|
||||
import { Loader2, Mail, Plus, User, UserPlus } from "lucide-react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
@@ -19,12 +11,6 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../../../components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../../../components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -80,7 +66,7 @@ function TenantUsersPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleRemoveMember = (userId: string, userName: string) => {
|
||||
const _handleRemoveMember = (userId: string, userName: string) => {
|
||||
if (!tenantSlug) return;
|
||||
if (
|
||||
window.confirm(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Building2, Plus, Users } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Building2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CornerDownRight,
|
||||
Download,
|
||||
ExternalLink,
|
||||
FolderOpen,
|
||||
@@ -41,7 +40,6 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../../components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -52,7 +50,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "../../../components/ui/dropdown-menu";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import { Label } from "../../../components/ui/label";
|
||||
import { ScrollArea } from "../../../components/ui/scroll-area";
|
||||
import {
|
||||
Table,
|
||||
@@ -62,15 +59,8 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../components/ui/table";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "../../../components/ui/tabs";
|
||||
import { toast } from "../../../components/ui/use-toast";
|
||||
import {
|
||||
createUser,
|
||||
exportTenantsCSV,
|
||||
fetchAllTenants,
|
||||
fetchUsers,
|
||||
@@ -413,7 +403,7 @@ const MemberTable: React.FC<{
|
||||
|
||||
function TenantUserGroupsTab() {
|
||||
const { tenantId } = useParams<{ tenantId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const _navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string>(tenantId || "");
|
||||
@@ -452,7 +442,7 @@ function TenantUserGroupsTab() {
|
||||
queryFn: () => fetchAllTenants(),
|
||||
});
|
||||
|
||||
const { currentBase, subTree } = useMemo(() => {
|
||||
const { currentBase, subTree: _subTree } = useMemo(() => {
|
||||
const allItems = allTenantsData?.items ?? [];
|
||||
return buildTenantFullTree(allItems, tenantId);
|
||||
}, [allTenantsData, tenantId]);
|
||||
@@ -855,7 +845,7 @@ const UserAddDialog: React.FC<{
|
||||
try {
|
||||
const res = await fetchUsers(20, 0, userSearch);
|
||||
setSearchResults(res.items);
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
toast.error(t("msg.admin.users.list.fetch_error", "사용자 검색 실패"));
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
|
||||
@@ -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,
|
||||
@@ -322,8 +321,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 +418,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 +594,7 @@ function UserDetailPage() {
|
||||
);
|
||||
};
|
||||
|
||||
const setPrimaryAppointment = (targetIndex: number) => {
|
||||
const _setPrimaryAppointment = (targetIndex: number) => {
|
||||
setAdditionalAppointments((current) =>
|
||||
current.map((appointment, index) => ({
|
||||
...appointment,
|
||||
@@ -662,14 +661,14 @@ function UserDetailPage() {
|
||||
string,
|
||||
Record<string, string | number | boolean>
|
||||
>) || {}),
|
||||
sub_email: Array.isArray(user.metadata?.sub_email)
|
||||
sub_email: (Array.isArray(user.metadata?.sub_email)
|
||||
? user.metadata.sub_email
|
||||
: typeof user.metadata?.sub_email === "string"
|
||||
? user.metadata.sub_email
|
||||
.split(/[;,\n\r\t]/)
|
||||
.map((e) => e.trim())
|
||||
.filter((e) => e.includes("@"))
|
||||
: [],
|
||||
: []) as unknown[],
|
||||
},
|
||||
});
|
||||
const isUserHanmacFamily = isHanmacFamilyUser(
|
||||
@@ -792,14 +791,17 @@ function UserDetailPage() {
|
||||
sub_email: rawSubEmail,
|
||||
...safeMetadata
|
||||
} = cleanMetadata;
|
||||
|
||||
|
||||
// Parse sub_email
|
||||
let sub_email: string[] = [];
|
||||
if (typeof rawSubEmail === "string" && rawSubEmail.trim() !== "") {
|
||||
sub_email = rawSubEmail
|
||||
if (
|
||||
typeof rawSubEmail === "string" &&
|
||||
(rawSubEmail as string).trim() !== ""
|
||||
) {
|
||||
sub_email = (rawSubEmail as string)
|
||||
.split(/[;,\n\r\t]/)
|
||||
.map((e) => e.trim())
|
||||
.filter((e) => e.includes("@"));
|
||||
.map((e: string) => e.trim())
|
||||
.filter((e: string) => e.includes("@"));
|
||||
}
|
||||
|
||||
const metadata: Record<string, unknown> = {
|
||||
@@ -813,7 +815,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,13 +991,13 @@ function UserDetailPage() {
|
||||
<Mail size={14} className="text-primary/70" />
|
||||
{user.email}
|
||||
</div>
|
||||
{user.metadata?.sub_email &&
|
||||
{!!user.metadata?.sub_email &&
|
||||
Array.isArray(user.metadata.sub_email) &&
|
||||
user.metadata.sub_email.length > 0 && (
|
||||
(user.metadata.sub_email as unknown[]).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" />
|
||||
<span className="text-[10px] font-bold">
|
||||
+{user.metadata.sub_email.length}
|
||||
+{(user.metadata.sub_email as unknown[]).length}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -1167,8 +1169,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,
|
||||
|
||||
@@ -141,8 +141,7 @@ primary@samaneng.com,Primary User,rnd-saman,EMP001,secondary@hanmaceng.co.kr`;
|
||||
tenantSlug: "rnd-saman",
|
||||
metadata: {
|
||||
employee_id: "EMP001",
|
||||
sub_email: "secondary@hanmaceng.co.kr",
|
||||
secondary_emails: ["secondary@hanmaceng.co.kr"],
|
||||
sub_email: ["secondary@hanmaceng.co.kr"],
|
||||
aliasEmails: ["secondary@hanmaceng.co.kr"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export function buildTenantFullTree(
|
||||
});
|
||||
}
|
||||
|
||||
const visitedDuringBuild = new Set<string>();
|
||||
const _visitedDuringBuild = new Set<string>();
|
||||
// Build initial children relations and prevent simple cycles
|
||||
for (const t of allTenants) {
|
||||
if (t.parentId && t.parentId !== t.id) {
|
||||
|
||||
@@ -32,7 +32,7 @@ test.describe("Audit Logs Management", () => {
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
@@ -67,19 +67,21 @@ test.describe("Audit Logs Management", () => {
|
||||
});
|
||||
|
||||
await page.goto("/audit-logs");
|
||||
|
||||
|
||||
// Check header
|
||||
await expect(page.getByText(/감사 로그|Audit Logs/i).first()).toBeVisible();
|
||||
|
||||
|
||||
// Check if table rows are rendered
|
||||
await expect(page.locator("tbody tr")).toHaveCount(20);
|
||||
|
||||
|
||||
// Check specific data visible in the row
|
||||
await expect(page.locator("tbody")).toContainText("user-even");
|
||||
await expect(page.locator("tbody")).toContainText("CREATE_TENANT");
|
||||
});
|
||||
|
||||
test("should load more logs on scroll (infinite scroll)", async ({ page }) => {
|
||||
test("should load more logs on scroll (infinite scroll)", async ({
|
||||
page,
|
||||
}) => {
|
||||
let callCount = 0;
|
||||
await page.route("**/api/v1/audit?limit=50*", async (route) => {
|
||||
const logs = generateMockLogs(20, callCount * 20);
|
||||
@@ -93,10 +95,12 @@ test.describe("Audit Logs Management", () => {
|
||||
await page.goto("/audit-logs");
|
||||
await expect(page.locator("tbody tr")).toHaveCount(20);
|
||||
|
||||
const loadMoreBtn = page.getByRole('button', { name: /더 보기|Load more/i });
|
||||
const loadMoreBtn = page.getByRole("button", {
|
||||
name: /더 보기|Load more/i,
|
||||
});
|
||||
await expect(loadMoreBtn).toBeVisible();
|
||||
await loadMoreBtn.click();
|
||||
|
||||
|
||||
// Wait for the next page to load
|
||||
await expect(page.locator("tbody tr")).toHaveCount(40);
|
||||
// user-even and CREATE_TENANT should still be visible in the newly loaded rows
|
||||
@@ -118,10 +122,10 @@ test.describe("Audit Logs Management", () => {
|
||||
// Search by User ID
|
||||
const userIdInput = page.getByTestId("audit-search-user-id");
|
||||
await userIdInput.fill("user-even");
|
||||
|
||||
|
||||
// Wait for deferred value to apply
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
|
||||
// Half of 20 logs are user-even
|
||||
await expect(page.locator("tbody tr")).toHaveCount(10);
|
||||
await expect(page.locator("tbody")).not.toContainText("user-odd");
|
||||
@@ -135,7 +139,7 @@ test.describe("Audit Logs Management", () => {
|
||||
const actionInput = page.getByTestId("audit-search-action");
|
||||
await actionInput.fill("ROTATE_SECRET");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
|
||||
// Check that we only see ROTATE_SECRET (20 - 7 = 13)
|
||||
await expect(page.locator("tbody tr")).toHaveCount(13);
|
||||
await expect(page.locator("tbody")).not.toContainText("CREATE_TENANT");
|
||||
@@ -158,11 +162,11 @@ test.describe("Audit Logs Management", () => {
|
||||
// ID % 5 === 0 are status "failure"
|
||||
// IDs: 0, 5, 10, 15 => 4 items
|
||||
await expect(page.locator("tbody tr")).toHaveCount(4);
|
||||
|
||||
|
||||
// Select "Success" status
|
||||
await page.getByTestId("audit-filter-status").selectOption("success");
|
||||
|
||||
|
||||
// IDs: 1,2,3,4, 6,7,8,9, 11,12,13,14, 16,17,18,19 => 16 items
|
||||
await expect(page.locator("tbody tr")).toHaveCount(16);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -258,7 +258,7 @@ test.describe("Tenants Management", () => {
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 900, height: 700 });
|
||||
let requestCount = 0;
|
||||
let _requestCount = 0;
|
||||
|
||||
await page.route("**/api/v1/admin/tenants**", async (route) => {
|
||||
if (route.request().method() !== "GET") {
|
||||
@@ -266,7 +266,7 @@ test.describe("Tenants Management", () => {
|
||||
}
|
||||
const url = new URL(route.request().url());
|
||||
const cursor = url.searchParams.get("cursor");
|
||||
requestCount += 1;
|
||||
_requestCount += 1;
|
||||
|
||||
if (!cursor) {
|
||||
return route.fulfill({
|
||||
|
||||
@@ -5,7 +5,9 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })._IS_TEST_MODE = true;
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authData = {
|
||||
access_token: "fake-token",
|
||||
@@ -13,12 +15,20 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
|
||||
profile: { sub: "admin-user", name: "Admin", role: "super_admin" },
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem("oidc.user:http://localhost:5000/oidc:adminfront", JSON.stringify(authData));
|
||||
window.localStorage.setItem(
|
||||
"oidc.user:http://localhost:5000/oidc:adminfront",
|
||||
JSON.stringify(authData),
|
||||
);
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/user/me", async (route) => {
|
||||
return route.fulfill({
|
||||
json: { id: "admin-user", name: "Admin", role: "super_admin", manageableTenants: [] },
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
@@ -31,7 +41,7 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/users*", async (route) => {
|
||||
if(route.request().url().includes("/bulk")) {
|
||||
if (route.request().url().includes("/bulk")) {
|
||||
return route.continue();
|
||||
}
|
||||
return route.fulfill({
|
||||
@@ -45,14 +55,20 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("should parse secondary_emails and send to backend", async ({ page }) => {
|
||||
let bulkPayload: any = null;
|
||||
test("should parse secondary_emails and send to backend", async ({
|
||||
page,
|
||||
}) => {
|
||||
let bulkPayload: Record<string, unknown> | null = null;
|
||||
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
bulkPayload = route.request().postDataJSON();
|
||||
bulkPayload = route.request().postDataJSON() as Record<string, unknown>;
|
||||
return route.fulfill({
|
||||
json: { results: [{ email: "test@example.com", success: true, userId: "u-1" }] },
|
||||
json: {
|
||||
results: [
|
||||
{ email: "test@example.com", success: true, userId: "u-1" },
|
||||
],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
@@ -60,21 +76,26 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page.getByTestId("page-title")).toContainText(/사용자|Users/i, { timeout: 20000 });
|
||||
await expect(page.getByTestId("page-title")).toContainText(
|
||||
/사용자|Users/i,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
|
||||
await page.getByTestId("user-data-mgmt-btn").click();
|
||||
await page.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i }).click();
|
||||
await page
|
||||
.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
|
||||
.click();
|
||||
|
||||
// Create a mock CSV with secondary_emails
|
||||
const csvContent = `email,sub_email,name,phone,role,tenant_slug\ntest@example.com,sub1@test.com;sub2@test.com,홍길동,010-1234-5678,user,tenant-slug`;
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||
await page.getByText(/파일 선택|Change file|Select file/i).click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
|
||||
await fileChooser.setFiles({
|
||||
name: 'users_with_secondary.csv',
|
||||
mimeType: 'text/csv',
|
||||
name: "users_with_secondary.csv",
|
||||
mimeType: "text/csv",
|
||||
buffer: Buffer.from(csvContent),
|
||||
});
|
||||
|
||||
@@ -86,10 +107,14 @@ test.describe("Users Bulk Upload Secondary Emails", () => {
|
||||
await expect(page.getByText(/성공|Success/i)).toBeVisible();
|
||||
|
||||
expect(bulkPayload).not.toBeNull();
|
||||
expect(bulkPayload.users).toHaveLength(1);
|
||||
|
||||
|
||||
const payloadUsers = bulkPayload?.users as Array<{
|
||||
metadata: { sub_email: string[] };
|
||||
}>;
|
||||
expect(payloadUsers).toHaveLength(1);
|
||||
|
||||
// The most important check - does it parse to the metadata
|
||||
expect(bulkPayload.users[0].metadata.sub_email).toContain("sub1@test.com");
|
||||
expect(bulkPayload.users[0].metadata.sub_email).toContain("sub2@test.com");
|
||||
expect(payloadUsers[0].metadata.sub_email).toContain("sub1@test.com");
|
||||
expect(payloadUsers[0].metadata.sub_email).toContain("sub2@test.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2050,7 +2050,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
// Validate all collected LoginIDs
|
||||
userEmail := extractTraitString(traits, "email")
|
||||
userPhone := extractTraitString(traits, "phone_number")
|
||||
|
||||
|
||||
allEmails := []string{userEmail}
|
||||
if secondaryRaw, exists := traits["sub_email"]; exists {
|
||||
if secondaryEmails, ok := secondaryRaw.([]interface{}); ok {
|
||||
|
||||
@@ -251,8 +251,8 @@ func TestBuildWorksmobileUserPayloadKeepsFirstAffiliationPrimaryWhenBaronReprese
|
||||
user,
|
||||
gpdtdcTenant,
|
||||
map[string]domain.Tenant{
|
||||
gpdtdcID: gpdtdcTenant,
|
||||
firstTenantID: firstTenant,
|
||||
gpdtdcID: gpdtdcTenant,
|
||||
firstTenantID: firstTenant,
|
||||
secondTenantID: secondTenant,
|
||||
},
|
||||
nil,
|
||||
|
||||
Reference in New Issue
Block a user