forked from baron/baron-sso
fix(orgfront): place GPDTDC users on leaf appointments
This commit is contained in:
@@ -448,4 +448,53 @@ describe("org chart layout", () => {
|
||||
"engineering-user",
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps GPDTDC representative users to their visible leaf appointment", () => {
|
||||
const gpdtdc = tenantNode("gpdtdc", "COMPANY", "GPDTDC", "gpdtdc");
|
||||
const tdc = {
|
||||
...tenantNode("tdc", "ORGANIZATION", "기술개발센터", "tdc"),
|
||||
parentId: "gpdtdc",
|
||||
};
|
||||
const leaf = {
|
||||
...tenantNode("tdc-leaf", "USER_GROUP", "기술개발센터 1팀", "tdc-leaf"),
|
||||
parentId: "tdc",
|
||||
};
|
||||
const rootNode = {
|
||||
...gpdtdc,
|
||||
children: [{ ...tdc, children: [leaf] }],
|
||||
};
|
||||
|
||||
const usersMap = buildUsersMap(
|
||||
[
|
||||
{
|
||||
...member("gpdtdc-user"),
|
||||
companyCode: undefined,
|
||||
tenantSlug: "gpdtdc",
|
||||
metadata: {
|
||||
additionalAppointments: [
|
||||
{
|
||||
tenantSlug: "internal-planning",
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
tenantSlug: "tdc-leaf",
|
||||
isPrimary: false,
|
||||
grade: "책임",
|
||||
position: "팀장",
|
||||
},
|
||||
],
|
||||
},
|
||||
joinedTenants: undefined,
|
||||
},
|
||||
],
|
||||
[rootNode],
|
||||
{ activeOnly: true },
|
||||
);
|
||||
|
||||
expect(usersMap.get("gpdtdc")).toBeUndefined();
|
||||
expect(usersMap.get("internal-planning")).toBeUndefined();
|
||||
expect(usersMap.get("tdc-leaf")?.map((user) => user.id)).toEqual([
|
||||
"gpdtdc-user",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1121,6 +1121,11 @@ type TenantIndexes = {
|
||||
bySlug: Map<string, TenantNode>;
|
||||
};
|
||||
|
||||
type UserOrgAppointmentRef = {
|
||||
tenantId?: string;
|
||||
tenantSlug?: string;
|
||||
};
|
||||
|
||||
function buildTenantIndexes(nodes: TenantNode[]): TenantIndexes {
|
||||
const byId = new Map<string, TenantNode>();
|
||||
const bySlug = new Map<string, TenantNode>();
|
||||
@@ -1134,6 +1139,36 @@ function buildTenantIndexes(nodes: TenantNode[]): TenantIndexes {
|
||||
return { byId, bySlug };
|
||||
}
|
||||
|
||||
function normalizeOrgSlug(value: unknown) {
|
||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
function getUserOrgAppointmentRefs(user: UserSummary): UserOrgAppointmentRef[] {
|
||||
const rawAppointments = user.metadata?.additionalAppointments;
|
||||
if (!Array.isArray(rawAppointments)) return [];
|
||||
|
||||
return rawAppointments
|
||||
.filter(
|
||||
(item): item is Record<string, unknown> =>
|
||||
typeof item === "object" && item !== null,
|
||||
)
|
||||
.map((item) => ({
|
||||
tenantId: typeof item.tenantId === "string" ? item.tenantId.trim() : "",
|
||||
tenantSlug: normalizeOrgSlug(item.tenantSlug),
|
||||
}));
|
||||
}
|
||||
|
||||
function addTenantSlugCandidate(
|
||||
slugs: Set<string>,
|
||||
tenantIndexes: TenantIndexes,
|
||||
slug: string,
|
||||
) {
|
||||
const normalizedSlug = normalizeOrgSlug(slug);
|
||||
if (!normalizedSlug) return;
|
||||
if (!tenantIndexes.bySlug.has(normalizedSlug)) return;
|
||||
slugs.add(normalizedSlug);
|
||||
}
|
||||
|
||||
function isDescendantTenant(
|
||||
candidate: TenantNode,
|
||||
ancestor: TenantNode,
|
||||
@@ -1184,8 +1219,8 @@ export function buildUsersMap(
|
||||
if (!isVisibleOrgChartUser(user)) continue;
|
||||
|
||||
const slugs = new Set<string>();
|
||||
const primarySlug = user.tenantSlug?.toLowerCase() || "";
|
||||
const legacyCompanySlug = user.companyCode?.toLowerCase() || "";
|
||||
const primarySlug = normalizeOrgSlug(user.tenantSlug);
|
||||
const legacyCompanySlug = normalizeOrgSlug(user.companyCode);
|
||||
if (
|
||||
primarySlug &&
|
||||
!isSystemGlobalTenant({
|
||||
@@ -1195,7 +1230,7 @@ export function buildUsersMap(
|
||||
name: primarySlug,
|
||||
})
|
||||
) {
|
||||
slugs.add(primarySlug);
|
||||
addTenantSlugCandidate(slugs, tenantIndexes, primarySlug);
|
||||
}
|
||||
if (
|
||||
legacyCompanySlug &&
|
||||
@@ -1206,14 +1241,27 @@ export function buildUsersMap(
|
||||
name: legacyCompanySlug,
|
||||
})
|
||||
) {
|
||||
slugs.add(legacyCompanySlug);
|
||||
addTenantSlugCandidate(slugs, tenantIndexes, legacyCompanySlug);
|
||||
}
|
||||
if (user.tenant?.slug && !isSystemGlobalTenant(user.tenant)) {
|
||||
slugs.add(user.tenant.slug.toLowerCase());
|
||||
addTenantSlugCandidate(slugs, tenantIndexes, user.tenant.slug);
|
||||
}
|
||||
for (const joinedTenant of user.joinedTenants || []) {
|
||||
if (joinedTenant.slug && !isSystemGlobalTenant(joinedTenant)) {
|
||||
slugs.add(joinedTenant.slug.toLowerCase());
|
||||
addTenantSlugCandidate(slugs, tenantIndexes, joinedTenant.slug);
|
||||
}
|
||||
}
|
||||
for (const appointment of getUserOrgAppointmentRefs(user)) {
|
||||
if (appointment.tenantSlug) {
|
||||
addTenantSlugCandidate(slugs, tenantIndexes, appointment.tenantSlug);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tenantById = appointment.tenantId
|
||||
? tenantIndexes.byId.get(appointment.tenantId)
|
||||
: undefined;
|
||||
if (tenantById) {
|
||||
addTenantSlugCandidate(slugs, tenantIndexes, tenantById.slug);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -385,6 +385,71 @@ test("org chart places multi-tenant users only on leaf memberships without dupli
|
||||
await expect(svg.getByText(/^1$/)).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("org chart places GPDTDC representative users on visible leaf appointments", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/api/v1/public/orgchart**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
sharedWith: "Playwright",
|
||||
tenants: [
|
||||
tenant("group", "HMAC Group", "hmac"),
|
||||
tenant("gpdtdc", "GPDTDC", "gpdtdc", "group", "COMPANY"),
|
||||
tenant("tdc", "기술개발센터", "tdc", "gpdtdc", "ORGANIZATION"),
|
||||
tenant("tdc-leaf", "기술개발센터 1팀", "tdc-leaf", "tdc"),
|
||||
tenant(
|
||||
"internal-planning",
|
||||
"내부 구성 조직",
|
||||
"internal-planning",
|
||||
"group",
|
||||
"ORGANIZATION",
|
||||
{ visibility: "internal" },
|
||||
),
|
||||
],
|
||||
users: [
|
||||
{
|
||||
...user("u-gpdtdc-leaf", "GPDTDC Leaf User", "gpdtdc"),
|
||||
tenantSlug: "gpdtdc",
|
||||
companyCode: undefined,
|
||||
metadata: {
|
||||
additionalAppointments: [
|
||||
{
|
||||
tenantSlug: "internal-planning",
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
tenantSlug: "tdc-leaf",
|
||||
isPrimary: false,
|
||||
grade: "책임",
|
||||
position: "팀장",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/chart?token=gpdtdc-leaf");
|
||||
|
||||
await expect(page.getByText("총 1명")).toBeVisible();
|
||||
const svg = page.locator('[data-testid="orgchart-vector-svg"]');
|
||||
await expect(svg).toBeVisible();
|
||||
await expect(svg.getByText("내부 구성 조직")).toHaveCount(0);
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-testid="orgchart-node-gpdtdc"]')
|
||||
.getByText(/GPDTDC Leaf User/),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-testid="orgchart-node-tdc-leaf"]')
|
||||
.getByText("GPDTDC Leaf User 책임(팀장)"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("org chart counts multi-leaf tenant users once in ancestor totals", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user