fix: filter invalid/empty section items and experience roles

This commit is contained in:
Amruth Pillai
2026-05-11 09:27:14 +02:00
parent 334ea48bc7
commit 0713cf20d4
4 changed files with 152 additions and 26 deletions
@@ -104,8 +104,6 @@ export function RichInput({ value, onChange, style, className, editorClassName,
const textDirection = isRTL(i18n.locale) ? "rtl" : undefined;
const [isFullscreen, setIsFullscreen] = useState(false);
console.log(value);
const editor = useEditor({
...options,
extensions,
@@ -27,6 +27,38 @@ describe("filterItems", () => {
const items = [{ hidden: false, name: "Alice", level: 4 }];
expect(filterItems(items)).toEqual([{ hidden: false, name: "Alice", level: 4 }]);
});
it("filters items with invalid primary titles when a section type is provided", () => {
const items = [
{ hidden: false, company: " ", position: "Engineer" },
{ hidden: false, company: "Acme", position: "Engineer" },
{ hidden: false, company: "\n\t", position: "Manager" },
];
expect(filterItems(items, "experience")).toEqual([{ hidden: false, company: "Acme", position: "Engineer" }]);
});
it("filters invalid experience roles by position", () => {
const items = [
{
hidden: false,
company: "Acme",
roles: [
{ id: "role-1", position: " ", period: "2020", description: "" },
{ id: "role-2", position: "Lead Engineer", period: "2021", description: "" },
{ id: "role-3", position: "\n", period: "2022", description: "" },
],
},
];
expect(filterItems(items, "experience")).toEqual([
{
hidden: false,
company: "Acme",
roles: [{ id: "role-2", position: "Lead Engineer", period: "2021", description: "" }],
},
]);
});
});
describe("hasVisibleItems", () => {
@@ -45,6 +77,10 @@ describe("hasVisibleItems", () => {
it("returns false for empty items", () => {
expect(hasVisibleItems({ hidden: false, items: [] })).toBe(false);
});
it("returns false when all items have invalid primary titles", () => {
expect(hasVisibleItems({ hidden: false, items: [{ hidden: false, school: " " }] }, "education")).toBe(false);
});
});
describe("isVisibleSummary", () => {
@@ -69,7 +105,7 @@ describe("isSectionVisible", () => {
const data = {
summary: { hidden: false, content: "<p>Hi</p>" },
sections: {
experience: { hidden: false, items: [{ hidden: false }] },
experience: { hidden: false, items: [{ hidden: false, company: "Acme" }] },
skills: { hidden: false, items: [] },
education: { hidden: true, items: [{ hidden: false }] },
},
@@ -88,6 +124,15 @@ describe("isSectionVisible", () => {
expect(isSectionVisible("experience", data)).toBe(true);
});
it("returns false for built-in section when all items have invalid primary titles", () => {
expect(
isSectionVisible("experience", {
...data,
sections: { experience: { hidden: false, items: [{ hidden: false, company: " " }] } },
}),
).toBe(false);
});
it("returns false for built-in section with no items", () => {
expect(isSectionVisible("skills", data)).toBe(false);
});
@@ -100,6 +145,17 @@ describe("isSectionVisible", () => {
expect(isSectionVisible("ext-1", data)).toBe(true);
});
it("uses custom section type to validate item primary titles", () => {
expect(
isSectionVisible("ext-education", {
...data,
customSections: [
{ id: "ext-education", type: "education", hidden: false, items: [{ hidden: false, school: "" }] },
],
}),
).toBe(false);
});
it("returns false for unknown section id", () => {
expect(isSectionVisible("does-not-exist", data)).toBe(false);
});
@@ -109,7 +165,7 @@ describe("filterSections", () => {
const data = {
summary: { hidden: false, content: "<p>Hi</p>" },
sections: {
experience: { hidden: false, items: [{ hidden: false }] },
experience: { hidden: false, items: [{ hidden: false, company: "Acme" }] },
skills: { hidden: false, items: [] },
},
customSections: [],
+79 -7
View File
@@ -1,20 +1,43 @@
import type { Summary } from "@reactive-resume/schema/resume/data";
import type { CustomSectionType, Summary } from "@reactive-resume/schema/resume/data";
type HiddenItem = {
hidden: boolean;
[key: string]: unknown;
};
type TitleBackedSectionType = Exclude<CustomSectionType, "summary" | "cover-letter">;
type ItemSectionLike<T extends HiddenItem = HiddenItem> = {
hidden: boolean;
items: T[];
};
type CustomSectionLike = ItemSectionLike & {
id: string;
type?: string;
};
type FilterableData = {
summary: Pick<Summary, "hidden" | "content">;
sections: Partial<Record<string, ItemSectionLike>>;
customSections: Array<ItemSectionLike & { id: string }>;
customSections: CustomSectionLike[];
};
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<TitleBackedSectionType, string>;
const isItemSection = (section: unknown): section is ItemSectionLike => {
return typeof section === "object" && section !== null && "items" in section;
};
@@ -23,12 +46,48 @@ const isSummarySection = (section: unknown): section is Summary => {
return typeof section === "object" && section !== null && "content" in section;
};
export const filterItems = <T extends HiddenItem>(items: T[]): T[] => {
return items.filter((item) => !item.hidden);
const hasText = (value: unknown): value is string => {
return typeof value === "string" && value.trim().length > 0;
};
export const hasVisibleItems = (section: ItemSectionLike): boolean => {
return !section.hidden && filterItems(section.items).length > 0;
const getPrimaryTitleField = (sectionType?: string): string | undefined => {
return primaryTitleFields[sectionType as TitleBackedSectionType];
};
const hasValidPrimaryTitle = (item: HiddenItem, sectionType?: string): boolean => {
const titleField = getPrimaryTitleField(sectionType);
if (!titleField) return true;
return hasText((item as Record<string, unknown>)[titleField]);
};
const hasVisibleExperienceRole = (role: unknown): boolean => {
return typeof role === "object" && role !== null && hasText((role as { position?: unknown }).position);
};
const filterExperienceRoles = <T extends HiddenItem>(item: T, sectionType?: string): T => {
if (sectionType !== "experience") return item;
const roles = (item as { roles?: unknown }).roles;
if (!Array.isArray(roles)) return item;
const visibleRoles = roles.filter(hasVisibleExperienceRole);
if (visibleRoles.length === roles.length) return item;
return { ...item, roles: visibleRoles } as T;
};
export const filterItems = <T extends HiddenItem>(items: T[], sectionType?: string): T[] => {
return items
.filter((item) => !item.hidden && hasValidPrimaryTitle(item, sectionType))
.map((item) => filterExperienceRoles(item, sectionType));
};
export const hasVisibleItems = (section: ItemSectionLike, sectionType?: string): boolean => {
return !section.hidden && filterItems(section.items, sectionType).length > 0;
};
export const isVisibleSummary = (summary: Pick<Summary, "hidden" | "content">): boolean => {
@@ -41,12 +100,25 @@ const getSectionForFiltering = (sectionId: string, data: FilterableData) => {
return data.sections[sectionId] ?? data.customSections.find((section) => section.id === sectionId);
};
const getSectionTypeForFiltering = (sectionId: string, section: unknown): string | undefined => {
if (sectionId === "summary") return "summary";
if (typeof section === "object" && section !== null && "type" in section) {
const type = (section as { type?: unknown }).type;
if (typeof type === "string") return type;
}
return sectionId;
};
export const isSectionVisible = (sectionId: string, data: FilterableData): boolean => {
const section = getSectionForFiltering(sectionId, data);
const sectionType = getSectionTypeForFiltering(sectionId, section);
if (!section) return false;
if (isSummarySection(section)) return isVisibleSummary(section);
if (isItemSection(section)) return hasVisibleItems(section);
if (isItemSection(section)) return hasVisibleItems(section, sectionType);
return false;
};
+15 -15
View File
@@ -51,10 +51,10 @@ const SectionItemsContext = createContext<SectionItemsContextValue>({ itemStyle:
const useSectionItemsContext = () => useContext(SectionItemsContext);
const getVisibleItems = <T extends { hidden: boolean }>(section: ItemSection<T>): T[] => {
if (!hasVisibleItems(section)) return [];
const getVisibleItems = <T extends { hidden: boolean }>(section: ItemSection<T>, sectionType?: string): T[] => {
if (!hasVisibleItems(section, sectionType)) return [];
return filterItems(section.items);
return filterItems(section.items, sectionType);
};
const SectionShell = ({
@@ -285,7 +285,7 @@ const ProfileSection = ({
} = {}) => {
const data = useRender();
const profiles = sectionData ?? data.sections.profiles;
const items = getVisibleItems(profiles);
const items = getVisibleItems(profiles, "profiles");
const inlineStyle = useTemplateStyle("inline");
if (items.length === 0) return null;
@@ -318,7 +318,7 @@ const ExperienceSection = ({
} = {}) => {
const data = useRender();
const experience = sectionData ?? data.sections.experience;
const items = getVisibleItems(experience);
const items = getVisibleItems(experience, "experience");
const splitRowStyle = useSectionSplitRowStyle();
const alignRightStyle = useTemplateStyle("alignRight");
const inlineItemHeader = useTemplateFeature("inlineItemHeader");
@@ -400,7 +400,7 @@ const EducationSection = ({
} = {}) => {
const data = useRender();
const education = sectionData ?? data.sections.education;
const items = getVisibleItems(education);
const items = getVisibleItems(education, "education");
const splitRowStyle = useSectionSplitRowStyle();
const alignRightStyle = useTemplateStyle("alignRight");
const inlineItemHeader = useTemplateFeature("inlineItemHeader");
@@ -474,7 +474,7 @@ const ProjectsSection = ({
} = {}) => {
const data = useRender();
const projects = sectionData ?? data.sections.projects;
const items = getVisibleItems(projects);
const items = getVisibleItems(projects, "projects");
const splitRowStyle = useSectionSplitRowStyle();
const alignRightStyle = useTemplateStyle("alignRight");
@@ -511,7 +511,7 @@ const SkillsSection = ({
} = {}) => {
const data = useRender();
const skills = sectionData ?? data.sections.skills;
const items = getVisibleItems(skills);
const items = getVisibleItems(skills, "skills");
const inlineStyle = useTemplateStyle("inline");
const metrics = getTemplateMetrics(data.metadata.page);
@@ -551,7 +551,7 @@ const LanguagesSection = ({
} = {}) => {
const data = useRender();
const languages = sectionData ?? data.sections.languages;
const items = getVisibleItems(languages);
const items = getVisibleItems(languages, "languages");
if (items.length === 0) return null;
@@ -581,7 +581,7 @@ const InterestsSection = ({
} = {}) => {
const data = useRender();
const interests = sectionData ?? data.sections.interests;
const items = getVisibleItems(interests);
const items = getVisibleItems(interests, "interests");
const inlineStyle = useTemplateStyle("inline");
if (items.length === 0) return null;
@@ -615,7 +615,7 @@ const AwardsSection = ({
} = {}) => {
const data = useRender();
const awards = sectionData ?? data.sections.awards;
const items = getVisibleItems(awards);
const items = getVisibleItems(awards, "awards");
const splitRowStyle = useTemplateStyle("splitRow");
const alignRightStyle = useTemplateStyle("alignRight");
@@ -652,7 +652,7 @@ const CertificationsSection = ({
} = {}) => {
const data = useRender();
const certifications = sectionData ?? data.sections.certifications;
const items = getVisibleItems(certifications);
const items = getVisibleItems(certifications, "certifications");
if (items.length === 0) return null;
@@ -685,7 +685,7 @@ const PublicationsSection = ({
} = {}) => {
const data = useRender();
const publications = sectionData ?? data.sections.publications;
const items = getVisibleItems(publications);
const items = getVisibleItems(publications, "publications");
if (items.length === 0) return null;
@@ -718,7 +718,7 @@ const VolunteerSection = ({
} = {}) => {
const data = useRender();
const volunteer = sectionData ?? data.sections.volunteer;
const items = getVisibleItems(volunteer);
const items = getVisibleItems(volunteer, "volunteer");
const splitRowStyle = useSectionSplitRowStyle();
const alignRightStyle = useTemplateStyle("alignRight");
const inlineItemHeader = useTemplateFeature("inlineItemHeader");
@@ -767,7 +767,7 @@ const ReferencesSection = ({
} = {}) => {
const data = useRender();
const references = sectionData ?? data.sections.references;
const items = getVisibleItems(references);
const items = getVisibleItems(references, "references");
if (items.length === 0) return null;