Skip to content

Commit 2f0b052

Browse files
authored
Merge pull request framer#364 from framer/feature/airtable/strip-link-protocol
Airtable: ensure there is only one protocol when sync email/phone fields
2 parents a8bbcf1 + 1c872a2 commit 2f0b052

11 files changed

Lines changed: 161 additions & 12 deletions

File tree

.github/workflows/ci.yml

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,50 @@ jobs:
8181

8282
steps:
8383
- uses: actions/checkout@v4
84+
with:
85+
fetch-depth: 0
8486

8587
- uses: actions/setup-node@v4
8688
with:
8789
node-version-file: .tool-versions
8890

89-
- run: yarn
90-
91-
- run: yarn turbo test
91+
- name: Get changed files
92+
id: changed-files
93+
uses: tj-actions/changed-files@v45
94+
with:
95+
files: |
96+
plugins/*/src/**
97+
plugins/*/test/**
98+
plugins/*/*.test.ts
99+
plugins/*/*.test.tsx
100+
plugins/*/vitest.config.ts
101+
plugins/*/test-setup.ts
102+
103+
- name: Install dependencies
104+
if: steps.changed-files.outputs.any_changed == 'true'
105+
run: yarn
106+
107+
- name: Get changed plugins
108+
if: steps.changed-files.outputs.any_changed == 'true'
109+
id: changed-plugins
110+
run: |
111+
# Extract unique plugin names from changed files
112+
PLUGINS=$(echo "${{ steps.changed-files.outputs.all_changed_files }}" | \
113+
grep -E '^plugins/[^/]+/' | \
114+
cut -d'/' -f2 | \
115+
sort -u | \
116+
tr '\n' ' ')
117+
echo "plugins=$PLUGINS" >> $GITHUB_OUTPUT
118+
echo "Changed plugins: $PLUGINS"
119+
120+
- name: Run tests for changed plugins
121+
if: steps.changed-files.outputs.any_changed == 'true' && steps.changed-plugins.outputs.plugins != ''
122+
run: |
123+
for plugin in ${{ steps.changed-plugins.outputs.plugins }}; do
124+
echo "Checking tests for plugin: $plugin"
125+
if [ -f "plugins/$plugin/package.json" ] && grep -q '"check-vitest"' "plugins/$plugin/package.json"; then
126+
yarn workspace $plugin check-vitest
127+
else
128+
echo "No check-vitest script found for $plugin, skipping..."
129+
fi
130+
done

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"plugins/*"
1010
],
1111
"scripts": {
12-
"check": "turbo run --continue check-biome check-eslint check-prettier check-svelte check-typescript test",
12+
"check": "turbo run --continue check-biome check-eslint check-prettier check-svelte check-typescript check-vitest",
1313
"dev": "turbo run dev --concurrency=40",
1414
"fix-biome": "turbo run --continue check-biome -- --write",
1515
"fix-eslint": "turbo run --continue check-eslint -- --fix",
@@ -18,6 +18,7 @@
1818
"g:check-biome": "biome check $INIT_CWD",
1919
"g:check-eslint": "cd $INIT_CWD && DEBUG='eslint:eslint' eslint --report-unused-disable-directives-severity error .",
2020
"g:check-typescript": "tsc --project $INIT_CWD",
21+
"g:check-vitest": "cd $INIT_CWD && vitest run",
2122
"g:dev": "cd $INIT_CWD && run g:vite",
2223
"g:preview": "cd $INIT_CWD && run g:vite preview",
2324
"g:vite": "cd $INIT_CWD && NODE_OPTIONS='--no-warnings=ExperimentalWarning' vite --config $PROJECT_CWD/packages/vite-config/src/index.ts",

plugins/airtable/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"check-eslint": "run g:check-eslint",
1111
"preview": "run g:preview",
1212
"pack": "npx framer-plugin-tools@latest pack",
13-
"check-typescript": "run g:check-typescript"
13+
"check-typescript": "run g:check-typescript",
14+
"check-vitest": "vitest run"
1415
},
1516
"dependencies": {
1617
"framer-plugin": "^3.5.2",
@@ -21,6 +22,8 @@
2122
},
2223
"devDependencies": {
2324
"@types/react": "^18.3.23",
24-
"@types/react-dom": "^18.3.7"
25+
"@types/react-dom": "^18.3.7",
26+
"happy-dom": "^18.0.1",
27+
"vitest": "^3.2.4"
2528
}
2629
}

plugins/airtable/src/data.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 = () =>
11+
({
12+
id: "email_field",
13+
name: "Email",
14+
type: "link",
15+
airtableType: "email",
16+
}) as const satisfies PossibleField
17+
18+
const createPhoneField = () =>
19+
({
20+
id: "phone_field",
21+
name: "Phone",
22+
type: "link",
23+
airtableType: "phoneNumber",
24+
}) as const satisfies PossibleField
25+
26+
describe("Email field processing", () => {
27+
it.each<[PossibleField, string]>([
28+
[createEmailField(), "user@example.com"],
29+
[createEmailField(), "mailto:user@example.com"],
30+
[createEmailField(), "mailto:mailto:user@example.com"],
31+
[createEmailField(), "MAILTO:user@example.com"],
32+
[
33+
{
34+
id: "link_field",
35+
name: "Link",
36+
type: "link",
37+
airtableType: "url", // Not an email field type
38+
},
39+
"user@example.com",
40+
],
41+
])("%s -> %s", (field, input) => {
42+
const result = getFieldDataEntryForFieldSchema(field, input)
43+
44+
expect(result).toEqual({
45+
value: "mailto:user@example.com",
46+
type: "link",
47+
})
48+
})
49+
})
50+
51+
describe("Phone field processing", () => {
52+
it.each<[PossibleField, string]>([
53+
[createPhoneField(), "+1234567890"],
54+
[createPhoneField(), "tel:+1234567890"],
55+
[createPhoneField(), "tel:tel:+1234567890"],
56+
[createPhoneField(), "TEL:+1234567890"],
57+
[
58+
{
59+
id: "link_field",
60+
name: "Link",
61+
type: "link",
62+
airtableType: "url", // Not a phone field type
63+
},
64+
"+1234567890",
65+
],
66+
])("%s -> %s", (field, input) => {
67+
const result = getFieldDataEntryForFieldSchema(field, input)
68+
69+
expect(result).toEqual({
70+
value: "tel:+1234567890",
71+
type: "link",
72+
})
73+
})
74+
})
75+
})

plugins/airtable/src/data.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ 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+
/**
69+
* Helper function to ensure a value has the specified prefix,
70+
* removing any existing instances of the prefix first
71+
*/
72+
function ensurePrefix(prefix: string, value: string): string {
73+
const regex = new RegExp(prefix, "gi")
74+
const result = value.replace(regex, "")
75+
return `${prefix}${result}`
76+
}
77+
6878
const NonEmptyArrayOfAttachmentsSchema = v.pipe(
6979
v.array(v.object({ type: v.optional(v.string()), id: v.string(), url: v.string() })),
7080
v.minLength(1)
@@ -73,7 +83,10 @@ const NonEmptyArrayOfAttachmentsSchema = v.pipe(
7383
const ArrayOfStringsSchema = v.array(v.string())
7484
const NonEmptyArrayOfStringsSchema = v.pipe(ArrayOfStringsSchema, v.minLength(1))
7585

76-
function getFieldDataEntryForFieldSchema(fieldSchema: PossibleField, value: unknown): FieldDataEntryInput | null {
86+
export function getFieldDataEntryForFieldSchema(
87+
fieldSchema: PossibleField,
88+
value: unknown
89+
): FieldDataEntryInput | null {
7790
// If the field is a lookup field, only use the first value from the array.
7891
if (fieldSchema.originalAirtableType === "multipleLookupValues") {
7992
if (!Array.isArray(value)) return null
@@ -94,14 +107,14 @@ function getFieldDataEntryForFieldSchema(fieldSchema: PossibleField, value: unkn
94107
if (typeof value === "string") {
95108
if (fieldSchema.airtableType === "email" || EMAIL_REGEX.test(value)) {
96109
return {
97-
value: `mailto:${value}`,
110+
value: ensurePrefix("mailto:", value),
98111
type: "link",
99112
}
100113
}
101114

102115
if (fieldSchema.airtableType === "phoneNumber" || PHONE_REGEX.test(value)) {
103116
return {
104-
value: `tel:${value}`,
117+
value: ensurePrefix("tel:", value),
105118
type: "link",
106119
}
107120
}

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+
})

plugins/code-versions/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"preview": "run g:preview",
1212
"pack": "npx framer-plugin-tools@latest pack",
1313
"check-typescript": "run g:check-typescript",
14-
"test": "vitest run",
14+
"check-vitest": "run g:check-vitest",
1515
"test-watch": "vitest"
1616
},
1717
"dependencies": {

plugins/global-search/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"preview": "run g:preview",
1212
"pack": "npx framer-plugin-tools@latest pack",
1313
"check-typescript": "run g:check-typescript",
14-
"test": "vitest"
14+
"check-vitest": "run g:check-vitest"
1515
},
1616
"dependencies": {
1717
"clsx": "^2.1.1",

turbo.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@
3131
"cache": false,
3232
"persistent": true
3333
},
34-
"test": {}
34+
"check-vitest": {}
3535
}
3636
}

0 commit comments

Comments
 (0)