mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
fix: filter invalid/empty section items and experience roles
This commit is contained in:
@@ -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: [],
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user