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",
|
"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>;
|
bySlug: Map<string, TenantNode>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UserOrgAppointmentRef = {
|
||||||
|
tenantId?: string;
|
||||||
|
tenantSlug?: string;
|
||||||
|
};
|
||||||
|
|
||||||
function buildTenantIndexes(nodes: TenantNode[]): TenantIndexes {
|
function buildTenantIndexes(nodes: TenantNode[]): TenantIndexes {
|
||||||
const byId = new Map<string, TenantNode>();
|
const byId = new Map<string, TenantNode>();
|
||||||
const bySlug = new Map<string, TenantNode>();
|
const bySlug = new Map<string, TenantNode>();
|
||||||
@@ -1134,6 +1139,36 @@ function buildTenantIndexes(nodes: TenantNode[]): TenantIndexes {
|
|||||||
return { byId, bySlug };
|
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(
|
function isDescendantTenant(
|
||||||
candidate: TenantNode,
|
candidate: TenantNode,
|
||||||
ancestor: TenantNode,
|
ancestor: TenantNode,
|
||||||
@@ -1184,8 +1219,8 @@ export function buildUsersMap(
|
|||||||
if (!isVisibleOrgChartUser(user)) continue;
|
if (!isVisibleOrgChartUser(user)) continue;
|
||||||
|
|
||||||
const slugs = new Set<string>();
|
const slugs = new Set<string>();
|
||||||
const primarySlug = user.tenantSlug?.toLowerCase() || "";
|
const primarySlug = normalizeOrgSlug(user.tenantSlug);
|
||||||
const legacyCompanySlug = user.companyCode?.toLowerCase() || "";
|
const legacyCompanySlug = normalizeOrgSlug(user.companyCode);
|
||||||
if (
|
if (
|
||||||
primarySlug &&
|
primarySlug &&
|
||||||
!isSystemGlobalTenant({
|
!isSystemGlobalTenant({
|
||||||
@@ -1195,7 +1230,7 @@ export function buildUsersMap(
|
|||||||
name: primarySlug,
|
name: primarySlug,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
slugs.add(primarySlug);
|
addTenantSlugCandidate(slugs, tenantIndexes, primarySlug);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
legacyCompanySlug &&
|
legacyCompanySlug &&
|
||||||
@@ -1206,14 +1241,27 @@ export function buildUsersMap(
|
|||||||
name: legacyCompanySlug,
|
name: legacyCompanySlug,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
slugs.add(legacyCompanySlug);
|
addTenantSlugCandidate(slugs, tenantIndexes, legacyCompanySlug);
|
||||||
}
|
}
|
||||||
if (user.tenant?.slug && !isSystemGlobalTenant(user.tenant)) {
|
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 || []) {
|
for (const joinedTenant of user.joinedTenants || []) {
|
||||||
if (joinedTenant.slug && !isSystemGlobalTenant(joinedTenant)) {
|
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);
|
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 ({
|
test("org chart counts multi-leaf tenant users once in ancestor totals", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user