mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
[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:
@@ -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}
|
||||
|
||||
+47
@@ -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));
|
||||
};
|
||||
Reference in New Issue
Block a user