Skip to content

Commit 3cc03e4

Browse files
committed
Add strip mailto/tel prefix from Airtable value
1 parent 28cf203 commit 3cc03e4

6 files changed

Lines changed: 168 additions & 4 deletions

File tree

plugins/airtable/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
},
2222
"devDependencies": {
2323
"@types/react": "^18.3.23",
24-
"@types/react-dom": "^18.3.7"
24+
"@types/react-dom": "^18.3.7",
25+
"happy-dom": "^18.0.1",
26+
"vitest": "^3.2.4"
2527
}
2628
}

plugins/airtable/src/data.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest"
2+
import { getFieldDataEntryForFieldSchema } from "./data"
3+
import type { PossibleField } from "./fields"
4+
5+
describe("getFieldDataEntryForFieldSchema", () => {
6+
beforeEach(() => {
7+
vi.clearAllMocks()
8+
})
9+
10+
const createEmailField = (): PossibleField => ({
11+
id: "email_field",
12+
name: "Email",
13+
type: "link",
14+
userEditable: true,
15+
airtableType: "email",
16+
})
17+
18+
const createPhoneField = (): PossibleField => ({
19+
id: "phone_field",
20+
name: "Phone",
21+
type: "link",
22+
userEditable: true,
23+
airtableType: "phoneNumber",
24+
})
25+
26+
describe("Email field processing", () => {
27+
it("should add mailto: prefix to email without prefix", () => {
28+
const result = getFieldDataEntryForFieldSchema(createEmailField(), "user@example.com")
29+
30+
expect(result).toEqual({
31+
value: "mailto:user@example.com",
32+
type: "link",
33+
})
34+
})
35+
36+
it("should preserve single mailto: prefix", () => {
37+
const result = getFieldDataEntryForFieldSchema(createEmailField(), "mailto:user@example.com")
38+
39+
expect(result).toEqual({
40+
value: "mailto:user@example.com",
41+
type: "link",
42+
})
43+
})
44+
45+
it("should normalize multiple mailto: prefixes to one", () => {
46+
const result = getFieldDataEntryForFieldSchema(createEmailField(), "mailto:mailto:user@example.com")
47+
48+
expect(result).toEqual({
49+
value: "mailto:user@example.com",
50+
type: "link",
51+
})
52+
})
53+
54+
it("should handle case insensitive mailto: prefix", () => {
55+
const result = getFieldDataEntryForFieldSchema(createEmailField(), "MAILTO:user@example.com")
56+
57+
expect(result).toEqual({
58+
value: "mailto:user@example.com",
59+
type: "link",
60+
})
61+
})
62+
63+
it("should detect email by regex when field type is not email", () => {
64+
const linkField: PossibleField = {
65+
id: "link_field",
66+
name: "Link",
67+
type: "link",
68+
userEditable: true,
69+
airtableType: "url", // Not an email field type
70+
}
71+
72+
const result = getFieldDataEntryForFieldSchema(linkField, "test@domain.co.uk")
73+
74+
expect(result).toEqual({
75+
value: "mailto:test@domain.co.uk",
76+
type: "link",
77+
})
78+
})
79+
})
80+
81+
describe("Phone field processing", () => {
82+
it("should add tel: prefix to phone without prefix", () => {
83+
const result = getFieldDataEntryForFieldSchema(createPhoneField(), "+1234567890")
84+
85+
expect(result).toEqual({
86+
value: "tel:+1234567890",
87+
type: "link",
88+
})
89+
})
90+
91+
it("should preserve single tel: prefix", () => {
92+
const result = getFieldDataEntryForFieldSchema(createPhoneField(), "tel:+1234567890")
93+
94+
expect(result).toEqual({
95+
value: "tel:+1234567890",
96+
type: "link",
97+
})
98+
})
99+
100+
it("should normalize multiple tel: prefixes to one", () => {
101+
const result = getFieldDataEntryForFieldSchema(createPhoneField(), "tel:tel:+1234567890")
102+
103+
expect(result).toEqual({
104+
value: "tel:+1234567890",
105+
type: "link",
106+
})
107+
})
108+
109+
it("should handle case insensitive tel: prefix", () => {
110+
const result = getFieldDataEntryForFieldSchema(createPhoneField(), "TEL:+1234567890")
111+
112+
expect(result).toEqual({
113+
value: "tel:+1234567890",
114+
type: "link",
115+
})
116+
})
117+
118+
it("should detect phone by regex when field type is not phoneNumber", () => {
119+
const linkField: PossibleField = {
120+
id: "link_field",
121+
name: "Link",
122+
type: "link",
123+
userEditable: true,
124+
airtableType: "url", // Not a phone field type
125+
}
126+
127+
const result = getFieldDataEntryForFieldSchema(linkField, "1234567890")
128+
129+
expect(result).toEqual({
130+
value: "tel:1234567890",
131+
type: "link",
132+
})
133+
})
134+
})
135+
})

plugins/airtable/src/data.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ export async function getTables(baseId: string, signal: AbortSignal): Promise<Ai
6565
const EMAIL_REGEX = /\S[^\s@]*@\S+\.\S+/
6666
const PHONE_REGEX = /^(\+?[0-9])[0-9]{7,14}$/
6767

68+
const MAILTO_PREFIX = "mailto:"
69+
const TEL_PREFIX = "tel:"
70+
71+
const MAILTO_REGEX = new RegExp(MAILTO_PREFIX, "gi")
72+
const TEL_REGEX = new RegExp(TEL_PREFIX, "gi")
73+
6874
const NonEmptyArrayOfAttachmentsSchema = v.pipe(
6975
v.array(v.object({ type: v.optional(v.string()), id: v.string(), url: v.string() })),
7076
v.minLength(1)
@@ -73,7 +79,10 @@ const NonEmptyArrayOfAttachmentsSchema = v.pipe(
7379
const ArrayOfStringsSchema = v.array(v.string())
7480
const NonEmptyArrayOfStringsSchema = v.pipe(ArrayOfStringsSchema, v.minLength(1))
7581

76-
function getFieldDataEntryForFieldSchema(fieldSchema: PossibleField, value: unknown): FieldDataEntryInput | null {
82+
export function getFieldDataEntryForFieldSchema(
83+
fieldSchema: PossibleField,
84+
value: unknown
85+
): FieldDataEntryInput | null {
7786
// If the field is a lookup field, only use the first value from the array.
7887
if (fieldSchema.originalAirtableType === "multipleLookupValues") {
7988
if (!Array.isArray(value)) return null
@@ -94,14 +103,14 @@ function getFieldDataEntryForFieldSchema(fieldSchema: PossibleField, value: unkn
94103
if (typeof value === "string") {
95104
if (fieldSchema.airtableType === "email" || EMAIL_REGEX.test(value)) {
96105
return {
97-
value: `mailto:${value}`,
106+
value: `${MAILTO_PREFIX}${value.replace(MAILTO_REGEX, "")}`,
98107
type: "link",
99108
}
100109
}
101110

102111
if (fieldSchema.airtableType === "phoneNumber" || PHONE_REGEX.test(value)) {
103112
return {
104-
value: `tel:${value}`,
113+
value: `${TEL_PREFIX}${value.replace(TEL_REGEX, "")}`,
105114
type: "link",
106115
}
107116
}

plugins/airtable/test-setup.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { vi } from "vitest"
2+
3+
// Mock framer-plugin for tests
4+
vi.mock("framer-plugin", () => ({
5+
framer: {
6+
showUI: vi.fn(),
7+
},
8+
}))

plugins/airtable/vitest.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineConfig } from "vitest/config"
2+
3+
export default defineConfig({
4+
test: {
5+
environment: "happy-dom",
6+
setupFiles: ["./test-setup.ts"],
7+
},
8+
})

yarn.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3187,10 +3187,12 @@ __metadata:
31873187
"@types/react": "npm:^18.3.23"
31883188
"@types/react-dom": "npm:^18.3.7"
31893189
framer-plugin: "npm:^3.5.2"
3190+
happy-dom: "npm:^18.0.1"
31903191
marked: "npm:^16.1.1"
31913192
react: "npm:^18.3.1"
31923193
react-dom: "npm:^18.3.1"
31933194
valibot: "npm:^1.1.0"
3195+
vitest: "npm:^3.2.4"
31943196
languageName: unknown
31953197
linkType: soft
31963198

0 commit comments

Comments
 (0)