1
0
forked from baron/baron-sso

Update dev workflow and org chart settings

This commit is contained in:
2026-05-20 18:15:54 +09:00
parent 5496735e2f
commit 2c3cab78b1
21 changed files with 288 additions and 76 deletions

View File

@@ -106,10 +106,20 @@ describe("buildOrgPickerTree", () => {
]);
});
it("excludes private tenants and their descendants from picker choices", () => {
it("excludes internal and private tenants from picker choices by default", () => {
const tenants = [
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
{
...tenant(
"internal-id",
"ORGANIZATION",
"내부 조직",
"internal",
"saman-id",
),
config: { visibility: "internal" },
},
{
...tenant(
"secret-id",
@@ -138,4 +148,34 @@ describe("buildOrgPickerTree", () => {
expect(tree.roots[0]?.children.map((node) => node.id)).toEqual(["open-id"]);
});
it("includes internal tenants when explicitly requested", () => {
const tenants = [
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
{
...tenant(
"internal-id",
"ORGANIZATION",
"내부 조직",
"internal",
"saman-id",
),
config: { visibility: "internal" },
},
tenant("open-id", "ORGANIZATION", "공개 조직", "open", "saman-id"),
];
const tree = buildOrgPickerTree({
includeInternal: true,
tenants,
users: [] satisfies UserSummary[],
tenantId: "saman",
});
expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([
"internal-id",
"open-id",
]);
});
});

View File

@@ -108,11 +108,13 @@ function findTenantNode(
}
export function buildOrgPickerTree({
includeInternal = false,
tenants,
users,
rootTenantId,
tenantId,
}: {
includeInternal?: boolean;
tenants: TenantSummary[];
users: UserSummary[];
rootTenantId?: string;
@@ -120,7 +122,7 @@ export function buildOrgPickerTree({
}) {
const visibleTenants = filterTenantsByVisibility(
tenants.filter(isOrgFrontTenantType),
"internal",
includeInternal ? "internal" : "public",
);
const usersBySlug = new Map<string, UserSummary[]>();
for (const user of users) {

View File

@@ -8,6 +8,7 @@ describe("org picker embed options", () => {
it("builds slug-based tenant scope urls", () => {
expect(
buildOrgPickerEmbedSrc({
includeInternal: false,
mode: "single",
select: "tenant",
includeDescendants: true,
@@ -21,6 +22,27 @@ describe("org picker embed options", () => {
);
});
it("builds and parses internal visibility flag only when requested", () => {
const src = buildOrgPickerEmbedSrc({
includeInternal: true,
mode: "single",
select: "tenant",
includeDescendants: true,
showDescendantToggle: true,
tenantId: "",
width: 400,
height: 600,
});
expect(src).toBe(
"/embed/picker?mode=single&select=tenant&width=400&height=600&includeInternal=true",
);
expect(parseOrgPickerEmbedOptions(src).includeInternal).toBe(true);
expect(parseOrgPickerEmbedOptions("?mode=single").includeInternal).toBe(
false,
);
});
it("parses tenantSlug first and keeps legacy tenantId compatibility", () => {
expect(
parseOrgPickerEmbedOptions(

View File

@@ -16,6 +16,7 @@ export type OrgPickerResult = {
};
export type OrgPickerEmbedOptions = {
includeInternal: boolean;
mode: OrgPickerMode;
select: OrgPickerSelectableType;
includeDescendants: boolean;
@@ -68,6 +69,7 @@ export function parseOrgPickerEmbedOptions(search: string) {
? ("single" as const)
: ("multiple" as const),
select: parseOrgPickerSelectableType(params.get("select")),
includeInternal: params.get("includeInternal") === "true",
includeDescendants: params.get("includeDescendants") !== "false",
showDescendantToggle: params.get("showDescendantToggle") !== "false",
tenantId:
@@ -87,6 +89,9 @@ export function buildOrgPickerEmbedSrc(options: OrgPickerEmbedOptions) {
width: String(options.width),
height: String(options.height),
});
if (options.includeInternal) {
params.set("includeInternal", "true");
}
const tenantSlug = options.tenantId.trim();
if (tenantSlug) {

View File

@@ -1200,6 +1200,8 @@ export function TenantOrgChartPage() {
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const shareToken = searchParams.get("token");
const visibilityMode =
searchParams.get("includeInternal") === "true" ? "internal" : "public";
const [selectedTenantFilter, setSelectedTenantFilter] =
React.useState(FAMILY_FILTER_ID);
const [collapsedIds, setCollapsedIds] = React.useState<Set<string>>(
@@ -1270,7 +1272,7 @@ export function TenantOrgChartPage() {
}
const rootNodes = buildTenantFullTree(
filterSystemGlobalTenants(tenantsQuery.data.items, "internal"),
filterSystemGlobalTenants(tenantsQuery.data.items, visibilityMode),
).subTree;
return {
@@ -1280,7 +1282,13 @@ export function TenantOrgChartPage() {
}),
sharedWith: "",
};
}, [publicQuery.data, shareToken, tenantsQuery.data, usersQuery.data]);
}, [
publicQuery.data,
shareToken,
tenantsQuery.data,
usersQuery.data,
visibilityMode,
]);
const familyRoot = React.useMemo(() => {
return (

View File

@@ -22,7 +22,7 @@ function PickerScenarioControls({
onChange: (options: OrgPickerEmbedOptions) => void;
}) {
return (
<div className="grid gap-3 rounded-md border border-border bg-card p-4 md:grid-cols-2 lg:grid-cols-[1fr,1fr,1fr,auto,auto,auto,auto] lg:items-end">
<div className="grid gap-3 rounded-md border border-border bg-card p-4 md:grid-cols-2 lg:grid-cols-[1fr,1fr,1fr,auto,auto,auto,auto,auto] lg:items-end">
<label className="space-y-1 text-sm font-medium">
<span className="block text-muted-foreground"> </span>
<select
@@ -104,6 +104,20 @@ function PickerScenarioControls({
<span> </span>
</label>
<label className="flex h-10 items-center gap-2 rounded-md border border-border bg-background px-3 text-sm">
<input
checked={options.includeInternal}
onChange={(event) =>
onChange({
...options,
includeInternal: event.target.checked,
})
}
type="checkbox"
/>
<span>internal </span>
</label>
<label className="space-y-1 text-sm font-medium">
<span className="block text-muted-foreground"> </span>
<input

View File

@@ -333,6 +333,7 @@ export function OrgPickerEmbedPage() {
const mode = parseOrgPickerMode(searchParams.get("mode"));
const select = parseOrgPickerSelectableType(searchParams.get("select"));
const rootTenantId = searchParams.get("rootTenantId") || undefined;
const includeInternal = searchParams.get("includeInternal") === "true";
const tenantId =
searchParams.get("tenantSlug") ||
searchParams.get("tenantId") ||
@@ -363,12 +364,20 @@ export function OrgPickerEmbedPage() {
const tree = React.useMemo(() => {
return buildOrgPickerTree({
includeInternal,
tenants: tenantsQuery.data?.items ?? [],
users: select === "tenant" ? [] : (usersQuery.data?.items ?? []),
rootTenantId,
tenantId,
});
}, [rootTenantId, select, tenantId, tenantsQuery.data, usersQuery.data]);
}, [
includeInternal,
rootTenantId,
select,
tenantId,
tenantsQuery.data,
usersQuery.data,
]);
const selectedItems = React.useMemo(
() =>
@@ -579,7 +588,7 @@ export function OrgPickerPage() {
</div>
</header>
<section className="grid gap-3 rounded-md border border-border bg-card p-4 md:grid-cols-2 lg:grid-cols-[1fr,1fr,1fr,auto,auto,auto,auto] lg:items-end">
<section className="grid gap-3 rounded-md border border-border bg-card p-4 md:grid-cols-2 lg:grid-cols-[1fr,1fr,1fr,auto,auto,auto,auto,auto] lg:items-end">
<label className="space-y-1 text-sm font-medium">
<span className="block text-muted-foreground"> </span>
<select
@@ -661,6 +670,20 @@ export function OrgPickerPage() {
<span> </span>
</label>
<label className="flex h-10 items-center gap-2 rounded-md border border-border bg-background px-3 text-sm">
<input
checked={options.includeInternal}
onChange={(event) =>
setOptions((current) => ({
...current,
includeInternal: event.target.checked,
}))
}
type="checkbox"
/>
<span>internal </span>
</label>
<label className="space-y-1 text-sm font-medium">
<span className="block text-muted-foreground"> </span>
<input