Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions plugins/ashby/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"check-eslint": "run g:check-eslint",
"preview": "run g:preview",
"pack": "npx framer-plugin-tools@latest pack",
"check-typescript": "run g:check-typescript"
"check-typescript": "run g:check-typescript",
"check-vitest": "run g:check-vitest"
},
"dependencies": {
"framer-plugin": "^3.6.0",
Expand All @@ -20,6 +21,7 @@
},
"devDependencies": {
"@types/react": "^18.3.24",
"@types/react-dom": "^18.3.7"
"@types/react-dom": "^18.3.7",
"vitest": "^3.2.4"
}
}
3 changes: 2 additions & 1 deletion plugins/ashby/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ form {
pointer-events: none;
}

.setup .field {
.setup .field,
.setup label {
display: flex;
flex-direction: row;
align-items: center;
Expand Down
2 changes: 1 addition & 1 deletion plugins/ashby/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function App({ collection, previousDataSourceId, previousSlugFieldId, pre

void framer.showUI({
width: hasDataSourceSelected ? 400 : 320,
height: hasDataSourceSelected ? 427 : 285,
height: hasDataSourceSelected ? 427 : 325,
minHeight: hasDataSourceSelected ? 427 : undefined,
minWidth: hasDataSourceSelected ? 400 : undefined,
resizable: hasDataSourceSelected,
Expand Down
21 changes: 20 additions & 1 deletion plugins/ashby/src/api-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import * as v from "valibot"

/** Base interface for all data items that can be synced */
export interface DataItem {
id: string
[key: string]: unknown
}

const AddressSchema = v.object({
addressLocality: v.optional(v.string()),
addressRegion: v.optional(v.string()),
Expand Down Expand Up @@ -28,7 +34,7 @@ export const JobAddressSchema = v.object({
postalAddress: AddressSchema,
})

const SecondaryLocationSchema = v.object({
export const SecondaryLocationSchema = v.object({
location: v.string(),
address: JobAddressSchema,
})
Expand Down Expand Up @@ -65,6 +71,19 @@ export type Job = v.InferOutput<typeof JobSchema>
export type Address = v.InferOutput<typeof AddressSchema>
export type CompensationComponent = v.InferOutput<typeof CompensationComponentSchema>
export type CompensationTiers = v.InferOutput<typeof CompensationTiersSchema>
export type SecondaryLocation = v.InferOutput<typeof SecondaryLocationSchema>
export type JobAddress = v.InferOutput<typeof JobAddressSchema>

export const LocationSchema = v.object({
id: v.string(),
name: v.string(),
locality: v.string(),
region: v.string(),
country: v.string(),
fullAddress: v.string(),
})

export type Location = v.InferOutput<typeof LocationSchema>

export function hasOwnProperty<T extends object, Key extends PropertyKey>(
object: T,
Expand Down
31 changes: 27 additions & 4 deletions plugins/ashby/src/components/SelectDataSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export function SelectDataSource({
onSelectDataSource,
}: SelectDataSourceProps) {
const [jobBoardName, setJobBoardName] = useState<string>(previousJobBoardName ?? "")
const [selectedDataSourceId] = useState<string>(previousDataSourceId ?? dataSources[0]?.id ?? "")
const [selectedDataSourceId, setSelectedDataSourceId] = useState<string>(
previousDataSourceId ?? dataSources[0]?.id ?? ""
)
const [isLoading, setIsLoading] = useState(false)

const isAllowedToManage = useIsAllowedTo("ManagedCollection.setFields", ...syncMethods)
Expand Down Expand Up @@ -72,19 +74,40 @@ export function SelectDataSource({
<img src={hero} alt="Ashby Hero" />

<form onSubmit={handleSubmit}>
<div className="field">
<label>
<p>Job Board Name</p>
<input
id="jobBoardName"
type="text"
required
placeholder="jobBoardName"
placeholder="Enter Job Board Name…"
value={jobBoardName}
onChange={event => {
setJobBoardName(event.target.value)
}}
/>
</div>
</label>
<label>
<p>Collection</p>
<select
id="collection"
required
onChange={event => {
setSelectedDataSourceId(event.target.value)
}}
value={selectedDataSourceId}
disabled={!jobBoardName}
>
<option value="" disabled>
Choose Source…
</option>
{dataSources.map(({ id, name }) => (
<option key={id} value={id}>
{name}
</option>
))}
</select>
</label>
<button disabled={isButtonDisabled}>{isLoading ? <div className="framer-spinner" /> : "Next"}</button>
</form>
</main>
Expand Down
47 changes: 26 additions & 21 deletions plugins/ashby/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ export function mergeFieldsWithExistingFields(
}

const StringifiableSchema = v.union([v.string(), v.number(), v.boolean()])
const ArrayWithIdsSchema = v.array(v.object({ id: v.number() }))

async function getItems(
dataSource: AshbyDataSource,
Expand Down Expand Up @@ -159,16 +158,20 @@ async function getItems(
}

const fieldData: FieldDataInput = {}
for (const [fieldName, rawValue] of Object.entries(item) as [string, unknown][]) {
const isFieldIgnored = !fieldsToSync.find(field => field.id === fieldName)
for (const [fieldName, rawValue] of Object.entries(item)) {
const fields = fieldLookup.get(fieldName) ?? []

if (fields.length === 0 || isFieldIgnored) {
if (fields.length === 0) {
continue
}

for (const field of fields) {
const value = field.getValue ? field.getValue(rawValue) : rawValue
const isFieldIgnored = !fieldsToSync.find(f => f.id === field.id)
if (isFieldIgnored) {
continue
}

const value = !isCollectionReference(field) && field.getValue ? field.getValue(rawValue) : rawValue

switch (field.type) {
case "string":
Expand Down Expand Up @@ -198,26 +201,29 @@ async function getItems(
}
break
case "multiCollectionReference": {
const ids: string[] = []
if (v.is(ArrayWithIdsSchema, value)) {
ids.push(...value.map(item => String(item.id)))
const targetDataSource = dataSources.find(ds => ds.id === field.dataSourceId)
if (!targetDataSource?.getItemId || !Array.isArray(rawValue)) {
fieldData[field.id] = { value: [], type: "multiCollectionReference" }
break
}

fieldData[field.id] = {
value: ids,
type: "multiCollectionReference",
const ids: string[] = []
for (const entry of rawValue) {
const id = targetDataSource.getItemId(entry)
if (id) ids.push(id)
}

fieldData[field.id] = { value: ids, type: "multiCollectionReference" }
break
}
case "collectionReference": {
if (typeof value !== "object" || value == null || !("id" in value)) {
continue
}
const targetDataSource = dataSources.find(ds => ds.id === field.dataSourceId)
if (!targetDataSource?.getItemId) continue

fieldData[field.id] = {
value: String(value.id),
type: "collectionReference",
}
const id = targetDataSource.getItemId(rawValue)
if (!id) continue

fieldData[field.id] = { value: id, type: "collectionReference" }
break
}
case "image":
Expand Down Expand Up @@ -292,7 +298,6 @@ export async function syncExistingCollection(
framer.closePlugin("You are not allowed to sync this collection.", {
variant: "error",
})
return { didSync: false }
}

try {
Expand All @@ -301,7 +306,7 @@ export async function syncExistingCollection(

const slugField = dataSource.fields.find(field => field.id === previousSlugFieldId)
if (!slugField) {
framer.notify(`No field matches the slug field id ${previousSlugFieldId}. Sync will not be performed.`, {
framer.notify(`No field matches the slug field id "${previousSlugFieldId}". Sync will not be performed.`, {
variant: "error",
})
return { didSync: false }
Expand All @@ -311,7 +316,7 @@ export async function syncExistingCollection(
return { didSync: true }
} catch (error) {
console.error(error)
framer.notify(`Failed to sync collection ${previousDataSourceId}. Check browser console for more details.`, {
framer.notify(`Failed to sync collection "${previousDataSourceId}". Check browser console for more details.`, {
variant: "error",
})
return { didSync: false }
Expand Down
90 changes: 90 additions & 0 deletions plugins/ashby/src/dataSources.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, it } from "vitest"
import { extractLocation } from "./dataSources"

describe("extractLocation", () => {
it("extracts location with full address", () => {
const result = extractLocation("San Francisco", {
postalAddress: {
addressLocality: "San Francisco",
addressRegion: "CA",
addressCountry: "USA",
},
})

expect(result).toEqual({
id: "san-francisco",
name: "San Francisco",
locality: "San Francisco",
region: "CA",
country: "USA",
fullAddress: "San Francisco, CA, USA",
})
})

it("handles null address", () => {
const result = extractLocation("Remote", null)

expect(result).toEqual({
id: "remote",
name: "Remote",
locality: "",
region: "",
country: "",
fullAddress: "",
})
})

it("handles partial address", () => {
const result = extractLocation("Berlin", {
postalAddress: {
addressCountry: "Germany",
},
})

expect(result).toEqual({
id: "berlin",
name: "Berlin",
locality: "",
region: "",
country: "Germany",
fullAddress: "Germany",
})
})

it("deduplicates address parts", () => {
const result = extractLocation("California", {
postalAddress: {
addressLocality: "CA",
addressRegion: "CA",
addressCountry: "USA",
},
})

expect(result.fullAddress).toBe("CA, USA")
})

it("trims whitespace from address parts", () => {
const result = extractLocation("New York", {
postalAddress: {
addressLocality: " New York ",
addressRegion: " NY ",
addressCountry: " USA ",
},
})

expect(result.locality).toBe("New York")
expect(result.region).toBe("NY")
expect(result.country).toBe("USA")
})

it("handles non-ASCII location names", () => {
const result = extractLocation("東京", {
postalAddress: {
addressCountry: "Japan",
},
})

expect(result.id).toBe("東京")
expect(result.name).toBe("東京")
})
})
Loading
Loading