Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e1b50e7
Increase window size, visual improvements
madebyisaacr Jun 18, 2025
e0b914a
Remove second column of arrows
madebyisaacr Jun 18, 2025
5bf1644
Improve unsupported fields and sorting
madebyisaacr Jun 18, 2025
a9f1a43
Improve select dropdown
madebyisaacr Jun 18, 2025
7f21e04
Add link to open Airtable base
madebyisaacr Jun 18, 2025
45f2069
Make unsupported button non clickable, placeholder table name
madebyisaacr Jun 18, 2025
993cb26
Use original field name as default instead of id
madebyisaacr Jun 18, 2025
8c55d23
Fix window size
madebyisaacr Jun 18, 2025
4f43719
Add barcode and aiText fields
madebyisaacr Jun 18, 2025
0927aee
Fix field types in sync mode
madebyisaacr Jun 18, 2025
abfb51b
Make #000000 the default color
madebyisaacr Jun 18, 2025
e62b4a6
Fix warning spam for missing numbers
madebyisaacr Jun 18, 2025
840b3b4
Revert auth URI change
madebyisaacr Jun 18, 2025
eea6e71
Fix misaligned padding
madebyisaacr Jun 18, 2025
9d71fb5
Add color field support
madebyisaacr Jun 18, 2025
040951e
Primary button
madebyisaacr Jun 19, 2025
20e4ad9
Fix importing duration field
madebyisaacr Jun 19, 2025
28e13e4
Fix changing field types
madebyisaacr Jun 19, 2025
9af7802
Revert design changes
madebyisaacr Jun 19, 2025
205d92f
Remove color field type
madebyisaacr Jun 19, 2025
8608652
Add lookup fields support
madebyisaacr Jun 20, 2025
a466930
Improve lookup field code
madebyisaacr Jun 20, 2025
e220b5a
Add rollup field
madebyisaacr Jun 20, 2025
d40c3b7
Fix TypeScript errors in lookup and rollup
madebyisaacr Jun 20, 2025
1fe8d9d
Rename function
madebyisaacr Jun 20, 2025
95368d5
Add singleCollaborator field support
madebyisaacr Jun 20, 2025
492558e
Add createdBy and lastModifiedBy support
madebyisaacr Jun 20, 2025
f7589a1
Add multipleCollaborators support
madebyisaacr Jun 20, 2025
4e97668
Update styles
madebyisaacr Jun 25, 2025
1958e64
Add count field
madebyisaacr Jun 25, 2025
bcaebaa
Add color type back
madebyisaacr Jun 25, 2025
afb3503
Update type name
madebyisaacr Jun 25, 2025
7a4082b
Fix duplicate case
madebyisaacr Jun 25, 2025
0c42935
Fix eslint errors
madebyisaacr Jun 25, 2025
9d2194c
createFieldMetadata
madebyisaacr Jun 30, 2025
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
59 changes: 5 additions & 54 deletions plugins/airtable/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ form {
-webkit-user-select: none;
}

select:disabled {
opacity: 0.5;
}

select:not(:disabled) {
cursor: pointer;
}

.sticky-top {
position: sticky;
top: 0;
Expand Down Expand Up @@ -53,13 +45,6 @@ select:not(:disabled) {
border-radius: 10px;
}

.setup-container {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}

.setup label {
display: flex;
flex-direction: row;
Expand All @@ -81,7 +66,7 @@ select:not(:disabled) {

.mapping .fields {
display: grid;
grid-template-columns: 1fr 8px 1fr 1fr;
grid-template-columns: 1fr 8px 1fr 8px 1fr;
gap: 10px;
margin-bottom: auto;
padding-bottom: 10px;
Expand All @@ -99,6 +84,10 @@ select:not(:disabled) {
color: var(--framer-color-text-tertiary);
}

.mapping .field-type-select {
width: 100%;
}

.mapping .source-field {
display: flex;
flex-direction: row;
Expand Down Expand Up @@ -127,44 +116,6 @@ select:not(:disabled) {
box-shadow: none;
}

.mapping .unsupported-field {
grid-column: span 2;
background-color: var(--framer-color-bg-tertiary);
border-radius: 8px;
padding: 0px 10px;
height: 30px;
display: flex;
flex-direction: row;
align-items: center;
opacity: 0.5;
color: var(--framer-color-text-secondary);
}

.mapping .field-type-select {
width: 100%;
}

.mapping .heading-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
}

.mapping .heading-link {
width: fit-content;
max-width: 50%;
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
}

.mapping .heading-link:hover {
text-decoration: underline;
}

.mapping footer {
position: sticky;
bottom: 0;
Expand Down
4 changes: 2 additions & 2 deletions plugins/airtable/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export function App({ collection, previousBaseId, previousTableId, previousSlugF
const hasDataSourceSelected = Boolean(dataSource)

framer.showUI({
width: hasDataSourceSelected ? 425 : 320,
height: hasDataSourceSelected ? 425 : 345,
width: hasDataSourceSelected ? 360 : 320,
height: hasDataSourceSelected ? 425 : 350,
minWidth: hasDataSourceSelected ? 360 : undefined,
minHeight: hasDataSourceSelected ? 425 : undefined,
resizable: hasDataSourceSelected,
Expand Down
194 changes: 87 additions & 107 deletions plugins/airtable/src/FieldMapping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ function ChevronIcon() {
}

const fieldTypeOptions: { type: Field["type"]; label: string }[] = [
{ type: "string", label: "Plain Text" },
{ type: "formattedText", label: "Formatted Text" },
{ type: "date", label: "Date" },
{ type: "link", label: "Link" },
{ type: "image", label: "Image" },
{ type: "boolean", label: "Boolean" },
{ type: "color", label: "Color" },
{ type: "boolean", label: "Toggle" },
{ type: "number", label: "Number" },
{ type: "string", label: "String" },
{ type: "formattedText", label: "Formatted Text" },
{ type: "image", label: "Image" },
{ type: "link", label: "Link" },
{ type: "date", label: "Date" },
{ type: "enum", label: "Option" },
{ type: "file", label: "File" },
]
Expand All @@ -40,48 +40,21 @@ interface FieldMappingRowProps {
originalFieldName: string | undefined
isIgnored: boolean
disabled: boolean
unsupported?: boolean
missingCollection?: boolean
onToggleIgnored?: (fieldId: string) => void
onNameChange?: (fieldId: string, name: string) => void
onTypeChange?: (fieldId: string, type: string) => void
}

interface SelectOption {
id: string
label: string
}

const FieldMappingRow = memo(
({
field,
originalFieldName,
isIgnored,
disabled,
unsupported,
missingCollection,
onToggleIgnored,
onNameChange,
onTypeChange,
}: FieldMappingRowProps) => {
let selectOptions: SelectOption[] = []
if (!unsupported && !missingCollection) {
if (isCollectionReference(field)) {
selectOptions = field.supportedCollections.map(collection => ({
id: collection.id,
label: collection.name,
}))
} else if (Array.isArray(field.allowedTypes)) {
selectOptions = field.allowedTypes.map(type => {
const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1)
return {
id: type,
label: fieldTypeOptions.find(option => option.type === type)?.label ?? capitalizedType,
}
})
}
}

return (
<>
<button
Expand All @@ -90,55 +63,67 @@ const FieldMappingRow = memo(
aria-disabled={isIgnored}
onClick={disabled ? undefined : () => onToggleIgnored?.(field.id)}
tabIndex={0}
disabled={disabled || unsupported || missingCollection}
disabled={disabled}
>
<input type="checkbox" checked={!isIgnored} tabIndex={-1} readOnly disabled={disabled} />
<span>{originalFieldName ?? field.id}</span>
</button>
<ChevronIcon />
{unsupported || missingCollection ? (
<div className="unsupported-field">{unsupported ? "Unsupported Field" : "Missing Collection"}</div>
) : (
<>
<select
disabled={isIgnored || disabled || selectOptions.length <= 1}
value={isCollectionReference(field) ? field.collectionId : field.type}
onChange={event => onTypeChange?.(field.id, event.target.value)}
className="field-type-select"
>
{selectOptions.map(option => (
<option key={option.id} value={option.id}>
{option.label}
<input
type="text"
style={{
width: "100%",
opacity: isIgnored || disabled ? 0.5 : 1,
}}
disabled={isIgnored || disabled}
placeholder={field.id}
value={field.name}
onChange={event => onNameChange?.(field.id, event.target.value)}
onKeyDown={event => {
if (event.key === "Enter") {
event.preventDefault()
}
}}
/>
<ChevronIcon />
<select
className="field-type-select"
style={{
opacity: isIgnored || disabled ? 0.5 : 1,
}}
disabled={isIgnored || disabled}
value={isCollectionReference(field) ? field.collectionId : field.type}
onChange={event => onTypeChange?.(field.id, event.target.value)}
>
{isCollectionReference(field) && (
<>
{field.supportedCollections.length === 0 && (
<option value="unsupported">Unsupported</option>
)}
{field.supportedCollections.map(collection => (
<option key={collection.id} value={collection.id}>
{collection.name}
</option>
))}
</select>
<input
type="text"
style={{
width: "100%",
opacity: isIgnored || disabled ? 0.5 : 1,
}}
disabled={isIgnored || disabled}
placeholder={originalFieldName ?? field.id}
value={field.name}
onChange={event => onNameChange?.(field.id, event.target.value)}
onKeyDown={event => {
if (event.key === "Enter") {
event.preventDefault()
}
}}
/>
</>
)}
</>
)}
{!isCollectionReference(field) &&
field.allowedTypes?.map(type => {
const capitalizedType =
typeof type === "string" ? type.charAt(0).toUpperCase() + type.slice(1) : ""

return (
<option key={type} value={type}>
{fieldTypeOptions.find(option => option.type === type)?.label ?? capitalizedType}
</option>
)
})}
</select>
</>
)
}
)

function isFieldMissingCollection(field: PossibleField): boolean {
return isCollectionReference(field) && field.supportedCollections.length === 0
}

const initialManagedCollectionFields: PossibleField[] = []
const initialFieldIds: ReadonlySet<string> = new Set()

Expand Down Expand Up @@ -234,8 +219,16 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie
return { ...field, type: "string" } as PossibleField
case "formattedText":
return { ...field, type: "formattedText" } as PossibleField
case "number":
return { ...field, type: "number" } as PossibleField
case "boolean":
return { ...field, type: "boolean" } as PossibleField
case "color":
return { ...field, type: "color" } as PossibleField
case "date":
return { ...field, type: "date" } as PossibleField
case "enum":
return { ...field, type: "enum" } as PossibleField
default:
return field
}
Expand Down Expand Up @@ -274,10 +267,13 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie

const fieldsToSync = fields
.filter(field => !ignoredFieldIds.has(field.id))
.map(field => ({
...field,
name: field.name.trim() || field.id,
}))
.map(field => {
const originalFieldName = dataSource.fields.find(sourceField => sourceField.id === field.id)?.name
return {
...field,
name: field.name.trim() || originalFieldName || field.id,
}
})
.filter(field => field.type !== "unsupported")
.filter(
field =>
Expand Down Expand Up @@ -313,18 +309,8 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie
return (
<form className="framer-hide-scrollbar mapping" onSubmit={handleSubmit}>
<hr className="sticky-top" />

<label className="slug-field" htmlFor="slugField">
<div className="heading-row">
<span>Slug Field</span>
<a
href={`https://airtable.com/${dataSource.baseId}/${dataSource.tableId}`}
target="_blank"
className="heading-link"
>
View in Airtable
</a>
</div>
Slug Field
<select
required
name="slugField"
Expand All @@ -351,10 +337,14 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie

<div className="fields">
<span className="column-span-2">Column</span>
<span className="column-span-2">Field</span>
<span>Type</span>
<span>Name</span>
{fields
.filter(field => field.type !== "unsupported" && !isFieldMissingCollection(field))
.filter(
field =>
field.type !== "unsupported" &&
(!isCollectionReference(field) || field.supportedCollections.length > 0)
)
.map(field => (
<FieldMappingRow
key={`field-${field.id}`}
Expand All @@ -367,23 +357,19 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie
onTypeChange={changeFieldType}
/>
))}
{fields.filter(isFieldMissingCollection).map(field => (
<FieldMappingRow
key={`field-${field.id}`}
field={field}
missingCollection
originalFieldName={dataSource.fields.find(sourceField => sourceField.id === field.id)?.name}
isIgnored={true}
disabled={!isAllowedToManage}
/>
))}
{fields
.filter(field => field.type === "unsupported")
.filter(
field =>
(isCollectionReference(field) && field.supportedCollections.length === 0) ||
field.type === "unsupported"
)
.map(field => (
<FieldMappingRow
key={`field-${field.id}`}
field={field}
unsupported
field={{
...field,
name: isCollectionReference(field) ? "Missing Collection" : "Unsupported field",
}}
originalFieldName={dataSource.fields.find(sourceField => sourceField.id === field.id)?.name}
isIgnored={true}
disabled={!isAllowedToManage}
Expand All @@ -399,13 +385,7 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie
tabIndex={0}
title={isAllowedToManage ? undefined : "Insufficient permissions"}
>
{isSyncing ? (
<div className="framer-spinner" />
) : (
<span>
Import <span style={{ textTransform: "capitalize" }}>{dataSource.tableName}</span>
</span>
)}
{isSyncing ? <div className="framer-spinner" /> : <span>Import {dataSource.tableName}</span>}
</button>
</footer>
</form>
Expand Down
Loading
Loading