Merge branch 'main' into feat/increase-test-coverage

# Conflicts:
#	packages/ui/src/components/input-group.test.tsx
This commit is contained in:
Amruth Pillai
2026-05-10 20:05:00 +02:00
22 changed files with 260 additions and 156 deletions
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { redactResumeForViewer } from "../helpers/resume-access-policy";
import { resumeDto } from "./resume";
describe("resume DTO output validation", () => {
it("accepts public resume responses after owner-only fields are redacted", () => {
const dbResume = {
id: "019e128d-0598-75d2-ae6a-771e2eb84614",
userId: "019bef93-a165-72cb-9c0e-d96e00000000",
name: "Armed Amaranth Catshark",
slug: "armed-amaranth-catshark",
tags: [],
data: {
...defaultResumeData,
metadata: {
...defaultResumeData.metadata,
notes: "owner-only notes",
},
},
isPublic: true,
isLocked: false,
hasPassword: false,
};
const publicResume = {
...redactResumeForViewer(dbResume, false),
hasPassword: dbResume.hasPassword,
};
expect(publicResume.name).toBe("");
expect(publicResume.data.metadata.notes).toBe("");
expect(resumeDto.getBySlug.output.safeParse(publicResume).success).toBe(true);
});
});
+2 -2
View File
@@ -127,7 +127,7 @@ const analysis = {
},
};
function toSharedResumeResponse<TPassword extends boolean>(
function toSharedResumeResponse(
resume: {
id: string;
name: string;
@@ -137,7 +137,7 @@ function toSharedResumeResponse<TPassword extends boolean>(
isPublic: boolean;
isLocked: boolean;
},
hasPassword: TPassword,
hasPassword: boolean,
) {
return {
id: resume.id,
@@ -1,5 +1,4 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type {
TemplateColorRoles,
@@ -12,6 +11,7 @@ import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -121,17 +121,9 @@ const Header = ({ styles }: { styles: AzurillStyles }) => {
<Text>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.headerContactItem}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem website={basics.website} style={styles.headerContactItem} />
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.headerContactItem}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.headerContactItem} />
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -109,17 +109,9 @@ const Header = ({ styles }: { styles: BronzorStyles }) => {
<Text>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.headerContactItem}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem website={basics.website} style={styles.headerContactItem} />
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.headerContactItem}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.headerContactItem} />
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -127,17 +127,9 @@ const Header = ({ styles }: { styles: ChikoritaStyles }) => {
</View>
<View style={styles.headerContactRow}>
{basics.website.url && (
<Link src={basics.website.url} style={styles.headerContactItem}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem website={basics.website} style={styles.headerContactItem} />
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.headerContactItem}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.headerContactItem} />
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -129,17 +129,20 @@ const Header = ({ styles, colors }: { styles: DitgarStyles; colors: TemplateColo
<Text style={styles.headerText}>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.contactItem}>
<Icon name="globe" color={colors.background} />
<Text style={styles.headerText}>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem
website={basics.website}
style={styles.contactItem}
textStyle={styles.headerText}
iconColor={colors.background}
/>
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItem}>
<Icon name={field.icon as IconName} color={colors.background} />
<Text style={styles.headerText}>{field.text}</Text>
</Link>
<CustomFieldContactItem
key={field.id}
field={field}
style={styles.contactItem}
textStyle={styles.headerText}
iconColor={colors.background}
/>
))}
</View>
</View>
+3 -11
View File
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -129,17 +129,9 @@ const Header = ({ styles }: { styles: DittoStyles }) => {
<Text>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.contactItem}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem website={basics.website} style={styles.contactItem} />
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItem}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { Fragment, useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -126,17 +126,20 @@ const Header = ({ styles, colors }: { styles: GengarStyles; colors: TemplateColo
<Text style={styles.headerText}>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.contactItem}>
<Icon name="globe" color={colors.background} />
<Text style={styles.headerText}>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem
website={basics.website}
style={styles.contactItem}
textStyle={styles.headerText}
iconColor={colors.background}
/>
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItem}>
<Icon name={field.icon as IconName} color={colors.background} />
<Text style={styles.headerText}>{field.text}</Text>
</Link>
<CustomFieldContactItem
key={field.id}
field={field}
style={styles.contactItem}
textStyle={styles.headerText}
iconColor={colors.background}
/>
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -124,17 +124,9 @@ const Header = ({ styles }: { styles: GlalieStyles }) => {
<Text>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.contactItem}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem website={basics.website} style={styles.contactItem} />
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItem}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -100,17 +100,9 @@ const Header = ({ styles }: { styles: KakunaStyles }) => {
<Text>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.contactItem}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem website={basics.website} style={styles.contactItem} />
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItem}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -99,17 +99,9 @@ const Header = ({ styles }: { styles: LaprasStyles }) => {
<Text>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.contactItem}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem website={basics.website} style={styles.contactItem} />
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItem}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { parseColorString, rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -119,17 +119,9 @@ const Header = ({ styles }: { styles: LeafishStyles }) => {
<Text>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.contactItem}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem website={basics.website} style={styles.contactItem} />
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItem}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -102,17 +102,9 @@ const Header = ({ styles }: { styles: MeowthStyles }) => {
<Text>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.contactItem}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem website={basics.website} style={styles.contactItem} />
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItem}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
))}
</View>
</View>
+3 -11
View File
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -99,17 +99,9 @@ const Header = ({ styles }: { styles: OnyxStyles }) => {
<Text>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.contactItem}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem website={basics.website} style={styles.contactItem} />
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItem}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItem} />
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateFeatures, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -126,17 +126,20 @@ const Header = ({ styles, colors }: { styles: PikachuStyles; colors: TemplateCol
<Text style={styles.headerText}>{basics.location}</Text>
</View>
)}
{basics.website.url && (
<Link src={basics.website.url} style={styles.contactItem}>
<Icon name="globe" color={colors.background} />
<Text style={styles.headerText}>{basics.website.label}</Text>
</Link>
)}
<WebsiteContactItem
website={basics.website}
style={styles.contactItem}
textStyle={styles.headerText}
iconColor={colors.background}
/>
{basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItem}>
<Icon name={field.icon as IconName} color={colors.background} />
<Text style={styles.headerText}>{field.text}</Text>
</Link>
<CustomFieldContactItem
key={field.id}
field={field}
style={styles.contactItem}
textStyle={styles.headerText}
iconColor={colors.background}
/>
))}
</View>
</View>
@@ -1,11 +1,11 @@
import type { Style } from "@react-pdf/types";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import type { TemplatePageProps } from "../../document";
import type { TemplateColorRoles, TemplateStyleContext, TemplateStyleSlots } from "../shared/types";
import { Image, Page, StyleSheet, View } from "@react-pdf/renderer";
import { useMemo } from "react";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
import { useRender } from "../../context";
import { CustomFieldContactItem, WebsiteContactItem } from "../shared/contact-item";
import { TemplateProvider } from "../shared/context";
import { filterSections } from "../shared/filtering";
import { getTemplateMetrics } from "../shared/metrics";
@@ -91,16 +91,10 @@ const Header = ({ styles }: { styles: RhyhornStyles }) => {
</View>
),
basics.website.url && (
<Link src={basics.website.url} style={styles.contactItemContent}>
<Icon name="globe" />
<Text>{basics.website.label}</Text>
</Link>
<WebsiteContactItem key="website" website={basics.website} style={styles.contactItemContent} />
),
...basics.customFields.map((field) => (
<Link key={field.id} src={field.link} style={styles.contactItemContent}>
<Icon name={field.icon as IconName} />
<Text>{field.text}</Text>
</Link>
<CustomFieldContactItem key={field.id} field={field} style={styles.contactItemContent} />
)),
].filter(Boolean);
@@ -0,0 +1,64 @@
import type { Style } from "@react-pdf/types";
import type { CustomField } from "@reactive-resume/schema/resume/data";
import type { IconName } from "phosphor-icons-react-pdf/dynamic";
import { View } from "@react-pdf/renderer";
import { getCustomFieldLinkUrl, getWebsiteDisplayText } from "./contact";
import { Icon, Link, Text } from "./primitives";
type WebsiteDisplay = {
url: string;
label?: string | undefined;
};
type ContactStyle = Style | Style[];
export const WebsiteContactItem = ({
website,
style,
textStyle,
iconColor,
}: {
website: WebsiteDisplay;
style?: ContactStyle;
textStyle?: ContactStyle;
iconColor?: string;
}) => {
if (!website.url) return null;
return (
<Link src={website.url} {...(style ? { style } : {})}>
<Icon name="globe" {...(iconColor ? { color: iconColor } : {})} />
<Text {...(textStyle ? { style: textStyle } : {})}>{getWebsiteDisplayText(website)}</Text>
</Link>
);
};
export const CustomFieldContactItem = ({
field,
style,
textStyle,
iconColor,
}: {
field: CustomField;
style?: ContactStyle;
textStyle?: ContactStyle;
iconColor?: string;
}) => {
const linkUrl = getCustomFieldLinkUrl(field);
const children = (
<>
<Icon name={field.icon as IconName} {...(iconColor ? { color: iconColor } : {})} />
<Text {...(textStyle ? { style: textStyle } : {})}>{field.text}</Text>
</>
);
if (linkUrl) {
return (
<Link src={linkUrl} {...(style ? { style } : {})}>
{children}
</Link>
);
}
return <View {...(style ? { style } : {})}>{children}</View>;
};
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { getCustomFieldLinkUrl, getWebsiteDisplayText } from "./contact";
describe("getWebsiteDisplayText", () => {
it("falls back to the full URL when the website label is blank", () => {
expect(getWebsiteDisplayText({ url: "https://davidkowalski.games", label: "" })).toBe(
"https://davidkowalski.games",
);
expect(getWebsiteDisplayText({ url: "https://davidkowalski.games", label: " " })).toBe(
"https://davidkowalski.games",
);
expect(getWebsiteDisplayText({ url: "http://example.com", label: undefined })).toBe("http://example.com");
});
it("uses the website label when one is provided", () => {
expect(getWebsiteDisplayText({ url: "https://davidkowalski.games", label: "Portfolio" })).toBe("Portfolio");
});
});
describe("getCustomFieldLinkUrl", () => {
it("returns no link URL for blank custom field links", () => {
expect(getCustomFieldLinkUrl({ link: "" })).toBeUndefined();
expect(getCustomFieldLinkUrl({ link: " " })).toBeUndefined();
expect(getCustomFieldLinkUrl({ link: undefined })).toBeUndefined();
});
it("returns the custom field link URL when one is provided", () => {
expect(getCustomFieldLinkUrl({ link: "https://example.com" })).toBe("https://example.com");
});
});
@@ -0,0 +1,20 @@
type WebsiteDisplay = {
url: string;
label?: string | undefined;
};
type CustomFieldLink = {
link?: string | undefined;
};
export const getWebsiteDisplayText = (website: WebsiteDisplay): string => {
const label = website.label?.trim();
return label || website.url;
};
export const getCustomFieldLinkUrl = (field: CustomFieldLink): string | undefined => {
const link = field.link?.trim();
return link || undefined;
};
@@ -25,6 +25,7 @@ import { match } from "ts-pattern";
import { useRender } from "../../context";
import { getResumeSectionTitle } from "../../section-title";
import { getSectionItemRows, getSectionItemsLayout, shouldUseSectionTimeline } from "./columns";
import { getWebsiteDisplayText } from "./contact";
import {
TemplatePlacementProvider,
useTemplateFeature,
@@ -225,7 +226,7 @@ const ItemTitle = ({ children, website }: { children: ReactNode; website: ItemWe
const ItemWebsiteLink = ({ website }: { website: ItemWebsite }) => {
if (!shouldRenderSeparateItemWebsite(website)) return null;
return <Link src={website.url}>{website.label || website.url}</Link>;
return <Link src={website.url}>{getWebsiteDisplayText(website)}</Link>;
};
const SummarySection = ({ showHeading = true }: { showHeading?: boolean } = {}) => {
@@ -1,5 +1,7 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, test } from "vitest";
import { createPortal } from "react-dom";
import {
InputGroup,
InputGroupAddon,
@@ -9,6 +11,10 @@ import {
InputGroupTextarea,
} from "./input-group";
function PortaledInput() {
return createPortal(<input aria-label="Portaled input" />, document.body);
}
describe("InputGroup", () => {
it("renders a fieldset with data-slot='input-group'", () => {
render(<InputGroup data-testid="g" />);
@@ -90,6 +96,29 @@ describe("InputGroupAddon", () => {
fireEvent.keyDown(addon, { key: "a" });
expect(document.activeElement).not.toBe(screen.getByTestId("i"));
});
test("does not redirect focus from controls rendered through a portal", async () => {
const user = userEvent.setup();
render(
<InputGroup>
<InputGroupAddon align="inline-end">
<span>Addon</span>
<PortaledInput />
</InputGroupAddon>
<InputGroupInput aria-label="Outer input" />
</InputGroup>,
);
const portaledInput = screen.getByLabelText("Portaled input");
const outerInput = screen.getByLabelText("Outer input");
await user.click(portaledInput);
expect(document.activeElement).toBe(portaledInput);
expect(document.activeElement).not.toBe(outerInput);
});
});
describe("InputGroupButton", () => {
@@ -48,6 +48,7 @@ function InputGroupAddon({
data-slot="input-group-addon"
className={cn(inputGroupAddonVariants({ align }), className)}
onKeyDown={(e) => {
if (!(e.target instanceof Element) || !e.currentTarget.contains(e.target)) return;
// Only respond to Space or Enter
if (e.key !== " " && e.key !== "Enter") return;
if (!(e.target as HTMLElement).closest("button")) {
@@ -56,6 +57,7 @@ function InputGroupAddon({
}
}}
onClick={(e) => {
if (!(e.target instanceof Element) || !e.currentTarget.contains(e.target)) return;
if (!(e.target as HTMLElement).closest("button")) {
e.preventDefault();
e.currentTarget.parentElement?.querySelector("input")?.focus();