[codex] Hide empty sections from page settings (#3052)

* feat(web): hide empty layout sections

* test: add "scizor" to templates metadata test cases
This commit is contained in:
Amruth Pillai
2026-05-13 01:11:19 +02:00
committed by GitHub
parent 00dafd0c68
commit 286e165a60
4 changed files with 125 additions and 1 deletions
@@ -22,6 +22,7 @@ describe("templates metadata", () => {
"onyx",
"pikachu",
"rhyhorn",
"scizor",
].sort(),
);
});
@@ -23,6 +23,7 @@ import { cn } from "@reactive-resume/utils/style";
import { useCurrentResume, useUpdateResumeData } from "@/components/resume/builder-resume-draft";
import { templates } from "@/dialogs/resume/template/data";
import { getSectionTitle } from "@/libs/resume/section";
import { filterVisibleLayoutSectionIds } from "./visibility";
type ColumnId = "main" | "sidebar";
@@ -230,7 +231,11 @@ export function LayoutPages() {
<PageContainer
key={`page-${pageIndex}`}
pageIndex={pageIndex}
page={page}
page={{
...page,
main: filterVisibleLayoutSectionIds(page.main, resume.data),
sidebar: filterVisibleLayoutSectionIds(page.sidebar, resume.data),
}}
canDelete={layout.pages.length > 1}
sidebarPosition={templateSidebarPosition}
onDelete={handleDeletePage}
@@ -0,0 +1,47 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { describe, expect, it } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { filterVisibleLayoutSectionIds } from "./visibility";
const createResumeData = (): ResumeData => structuredClone(defaultResumeData);
describe("filterVisibleLayoutSectionIds", () => {
it("removes item-backed sections with no visible items", () => {
const data = createResumeData();
data.sections.experience.items = [
{ hidden: false, company: "Reactive Resume" },
{ hidden: true, company: "Hidden Company" },
] as never;
data.sections.references.items = [{ hidden: true, name: "Hidden Reference" }] as never;
expect(filterVisibleLayoutSectionIds(["experience", "volunteer", "references"], data)).toEqual(["experience"]);
});
it("keeps layout order for non-empty summary and custom sections", () => {
const data = createResumeData();
data.summary.content = "<p>Available for staff roles.</p>";
data.customSections = [
{
id: "custom-visible",
type: "projects",
title: "Selected Work",
columns: 1,
hidden: false,
items: [{ hidden: false, name: "Resume Builder" }],
},
{
id: "custom-empty",
type: "projects",
title: "Empty Work",
columns: 1,
hidden: false,
items: [],
},
] as never;
expect(filterVisibleLayoutSectionIds(["custom-empty", "summary", "custom-visible"], data)).toEqual([
"summary",
"custom-visible",
]);
});
});
@@ -0,0 +1,71 @@
import type { CustomSectionType, ResumeData, SectionType } from "@reactive-resume/schema/resume/data";
type HiddenItem = {
hidden: boolean;
[key: string]: unknown;
};
type ItemSectionLike = {
hidden: boolean;
items: HiddenItem[];
};
const primaryTitleFields = {
profiles: "network",
experience: "company",
education: "school",
projects: "name",
skills: "name",
languages: "language",
interests: "name",
awards: "title",
certifications: "title",
publications: "title",
volunteer: "organization",
references: "name",
} satisfies Record<SectionType, string>;
type TitleBackedSectionType = keyof typeof primaryTitleFields;
const hasText = (value: unknown): value is string => {
return typeof value === "string" && value.trim().length > 0;
};
const getPrimaryTitleField = (sectionType: CustomSectionType | SectionType | undefined): string | undefined => {
return primaryTitleFields[sectionType as TitleBackedSectionType];
};
const hasValidPrimaryTitle = (item: HiddenItem, sectionType: CustomSectionType | SectionType | undefined): boolean => {
const titleField = getPrimaryTitleField(sectionType);
if (!titleField) return true;
return hasText(item[titleField]);
};
const hasVisibleItems = (
section: ItemSectionLike,
sectionType: CustomSectionType | SectionType | undefined,
): boolean => {
return !section.hidden && section.items.some((item) => !item.hidden && hasValidPrimaryTitle(item, sectionType));
};
const getBuiltInSection = (sectionId: string, data: ResumeData): ItemSectionLike | null => {
if (!(sectionId in data.sections)) return null;
return data.sections[sectionId as SectionType] as ItemSectionLike;
};
const isVisibleLayoutSection = (sectionId: string, data: ResumeData): boolean => {
if (sectionId === "summary") return !data.summary.hidden && data.summary.content.trim().length > 0;
const builtInSection = getBuiltInSection(sectionId, data);
if (builtInSection) return hasVisibleItems(builtInSection, sectionId as SectionType);
const customSection = data.customSections.find((section) => section.id === sectionId);
if (customSection) return hasVisibleItems(customSection as ItemSectionLike, customSection.type);
return false;
};
export const filterVisibleLayoutSectionIds = (sectionIds: string[], data: ResumeData): string[] => {
return sectionIds.filter((sectionId) => isVisibleLayoutSection(sectionId, data));
};