Skip to content
Draft
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
13 changes: 9 additions & 4 deletions plugins/google-sheets/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export function App({ pluginContext }: AppProps) {
slugColumn,
lastSyncedTime,
sheet,
altTextAssignments,
} = context
const [headerRow] = sheet.values

Expand All @@ -224,10 +225,14 @@ export function App({ pluginContext }: AppProps) {
spreadsheetId,
sheetTitle,
fields,
// Determine if the field type is already configured, otherwise default to "string"
colFieldTypes: headerRow.map(colName => {
const field = fields.find(field => field.name === colName)
return field?.type ?? "string"
// Determine if the field type is already configured, otherwise default to "string".
// Alt text columns never become real CMS fields, so `fields` won't have them.
columnConfigs: headerRow.map(columnId => {
if (Object.hasOwn(altTextAssignments, columnId)) {
return { type: "altText" as const, imageColumnId: altTextAssignments[columnId] }
}
const field = fields.find(field => field.id === columnId)
return { type: field?.type ?? "string" }
}),
configureFields: false,
})
Expand Down
109 changes: 109 additions & 0 deletions plugins/google-sheets/src/components/FieldTypeSelectField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import cx from "classnames"
import { framer, type MenuItem } from "framer-plugin"
import { useMemo } from "react"
import { isAltTextColumn, type SheetCollectionFieldInput, type VirtualFieldType } from "../sheets"
import { IconChevron, IconChevronDown } from "./Icons"

interface FieldTypeOption {
type: VirtualFieldType
label: string
}

const fieldTypeOptions: FieldTypeOption[] = [
{ type: "string", label: "Plain Text" },
{ type: "formattedText", label: "Formatted Text" },
{ type: "date", label: "Date" },
{ type: "dateTime", label: "Date & Time" },
{ type: "link", label: "Link" },
{ type: "image", label: "Image" },
{ type: "color", label: "Color" },
{ type: "boolean", label: "Toggle" },
{ type: "number", label: "Number" },
{ type: "enum", label: "Option" },
{ type: "file", label: "File" },
]

const contextMenuOffset = 4

interface Props {
field: SheetCollectionFieldInput
fields: SheetCollectionFieldInput[]
isDisabled: boolean
disabledFieldIds: Set<string>
onFieldTypeChange: (id: string, type: VirtualFieldType, imageColumnId?: string) => void
}

export function FieldTypeSelectField({ field, fields, isDisabled, disabledFieldIds, onFieldTypeChange }: Props) {
const imageColumn = fields.find(candidate => candidate.id === field.imageColumnId)

const fieldTypeLabel = useMemo(() => {
if (isAltTextColumn(field)) {
return imageColumn ? `Alt Text → ${imageColumn.name}` : "Alt Text"
}

return fieldTypeOptions.find(option => option.type === field.type)?.label ?? field.type
}, [field, imageColumn])

const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
const availableImageFields = fields.filter(
candidate => candidate.type === "image" && candidate.id !== field.id && !disabledFieldIds.has(candidate.id)
)

const items: MenuItem[] = [
...fieldTypeOptions.map(({ type, label }) => ({
label,
checked: field.type === type,
onAction: () => {
onFieldTypeChange(field.id, type)
},
})),
{
label: "Alt Text",
checked: isAltTextColumn(field),
enabled: availableImageFields.length > 0,
submenu: availableImageFields.map(candidate => ({
label: candidate.name,
checked: isAltTextColumn(field) && field.imageColumnId === candidate.id,
onAction: () => {
onFieldTypeChange(field.id, "altText", candidate.id)
},
})),
},
]

const { left, bottom, width } = event.currentTarget.getBoundingClientRect()
void framer.showContextMenu(items, {
location: {
x: left + width - contextMenuOffset,
y: bottom + contextMenuOffset,
},
placement: "bottom-left",
width,
})
}

return (
<button
type="button"
className={cx("flex w-full min-w-0 items-center gap-1 text-left", isDisabled && "opacity-50")}
disabled={isDisabled}
title={fieldTypeLabel}
onClick={handleMenuOpen}
>
{isAltTextColumn(field) ? (
<span className="flex min-w-0 flex-1 items-center gap-1">
<span className="flex shrink-0 items-center gap-1">
Alt Text
<IconChevron />
</span>
<span className="truncate">{imageColumn?.name ?? "Image"}</span>
</span>
) : (
<span className="min-w-0 flex-1 truncate">{fieldTypeLabel}</span>
)}
<span className="shrink-0 text-content">
<IconChevronDown />
</span>
</button>
)
}
13 changes: 13 additions & 0 deletions plugins/google-sheets/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,16 @@ export const IconChevron = () => (
></path>
</svg>
)

export const IconChevronDown = () => (
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="8" height="8">
<path
fill="transparent"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
d="m1 2.75 2.293 2.293a1 1 0 0 0 1.414 0L7 2.75"
/>
</svg>
)
100 changes: 49 additions & 51 deletions plugins/google-sheets/src/pages/MapSheetFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,21 @@ import cx from "classnames"
import { framer, useIsAllowedTo } from "framer-plugin"
import { Fragment, useMemo, useState } from "react"
import { CheckboxTextfield } from "../components/CheckboxTextField"
import { FieldTypeSelectField } from "../components/FieldTypeSelectField"
import { IconChevron } from "../components/Icons"
import type {
CellValue,
HeaderRow,
PluginContext,
Row,
SheetCollectionFieldInput,
SheetColumnConfig,
SyncMutationOptions,
VirtualFieldType,
} from "../sheets"
import { isAltTextColumn } from "../sheets"
import { generateUniqueNames, isDefined, syncMethods } from "../utils"

interface FieldTypeOption {
type: VirtualFieldType
label: string
}

const fieldTypeOptions: FieldTypeOption[] = [
{ type: "string", label: "Plain Text" },
{ type: "formattedText", label: "Formatted Text" },
{ type: "date", label: "Date" },
{ type: "dateTime", label: "Date & Time" },
{ type: "link", label: "Link" },
{ type: "image", label: "Image" },
{ type: "color", label: "Color" },
{ type: "boolean", label: "Toggle" },
{ type: "number", label: "Number" },
{ type: "enum", label: "Option" },
{ type: "file", label: "File" },
]

const getInitialSlugColumn = (context: PluginContext, slugFields: SheetCollectionFieldInput[]): string => {
if (context.type === "update" && context.slugColumn) {
return context.slugColumn
Expand Down Expand Up @@ -103,15 +87,19 @@ const inferFieldType = (cellValue: CellValue): VirtualFieldType => {
return "string"
}

const getFieldType = (context: PluginContext, columnId: string, cellValue?: CellValue): VirtualFieldType => {
const getColumnConfig = (context: PluginContext, columnId: string, cellValue?: CellValue): SheetColumnConfig => {
if (context.type === "update" && Object.hasOwn(context.altTextAssignments, columnId)) {
return { type: "altText", imageColumnId: context.altTextAssignments[columnId] }
}

// Determine if the field type is already configured
if ("collectionFields" in context) {
const field = context.collectionFields.find(field => field.id === columnId)
return field?.type ?? "string"
return { type: field?.type ?? "string" }
}

// Otherwise, infer the field type from the cell value
return cellValue ? inferFieldType(cellValue) : "string"
return { type: cellValue ? inferFieldType(cellValue) : "string" }
}

const createFieldConfig = (
Expand All @@ -125,10 +113,13 @@ const createFieldConfig = (
const sanitizedName = uniqueColumnNames[columnIndex]
if (!sanitizedName) return null

const { type, imageColumnId } = getColumnConfig(context, sanitizedName, row?.[columnIndex])

return {
id: sanitizedName,
name: sanitizedName,
type: getFieldType(context, sanitizedName, row?.[columnIndex]),
type,
imageColumnId,
} as SheetCollectionFieldInput
})
.filter(isDefined)
Expand Down Expand Up @@ -200,15 +191,25 @@ export function MapSheetFieldsPage({
}))
}

const handleFieldTypeChange = (id: string, type: VirtualFieldType) => {
const handleFieldTypeChange = (id: string, type: VirtualFieldType, imageColumnId?: string) => {
setFieldConfig(current =>
current.map(field => {
if (field.id === id) {
return {
...field,
type,
} as SheetCollectionFieldInput
imageColumnId: type === "altText" ? imageColumnId : undefined,
}
}

if (type !== "image" && isAltTextColumn(field) && field.imageColumnId === id) {
return {
...field,
type: "string",
imageColumnId: undefined,
}
}

return field
})
)
Expand All @@ -220,7 +221,8 @@ export function MapSheetFieldsPage({
if (isPending) return

const allFields = fieldConfig
.filter(field => !disabledColumns.has(field.id))
// Alt text columns are virtual: they never become their own CMS field.
.filter(field => !disabledColumns.has(field.id) && !isAltTextColumn(field))
.map(field => {
const maybeOverride = fieldNameOverrides[field.id]
if (maybeOverride) {
Expand All @@ -236,7 +238,7 @@ export function MapSheetFieldsPage({
fields: allFields,
spreadsheetId,
sheetTitle,
colFieldTypes: fieldConfig.map(field => field.type),
columnConfigs: fieldConfig.map(field => ({ type: field.type, imageColumnId: field.imageColumnId })),
ignoredColumns: Array.from(disabledColumns),
slugColumn,
lastSyncedTime: getLastSyncedTime(pluginContext, slugColumn),
Expand Down Expand Up @@ -290,31 +292,27 @@ export function MapSheetFieldsPage({
<div className="flex items-center justify-center">
<IconChevron />
</div>
<select
className={cx("w-full", (isDisabled || !isAllowedToManage) && "opacity-50")}
disabled={isDisabled || !isAllowedToManage}
value={field.type}
onChange={e => {
handleFieldTypeChange(field.id, e.target.value as VirtualFieldType)
}}
title={isAllowedToManage ? undefined : "Insufficient permissions"}
>
{fieldTypeOptions.map(({ type, label }) => (
<option key={type} value={type}>
{label}
</option>
))}
</select>
<input
type="text"
className={cx("w-full", (isDisabled || !isAllowedToManage) && "opacity-50")}
disabled={isDisabled || !isAllowedToManage}
placeholder={field.name}
value={fieldNameOverrides[field.id] ?? ""}
onChange={e => {
handleFieldNameChange(field.id, e.target.value)
}}
<FieldTypeSelectField
field={field}
fields={fieldConfig}
isDisabled={isDisabled || !isAllowedToManage}
disabledFieldIds={disabledColumns}
onFieldTypeChange={handleFieldTypeChange}
/>
{!isAltTextColumn(field) ? (
<input
type="text"
className={cx("w-full", (isDisabled || !isAllowedToManage) && "opacity-50")}
disabled={isDisabled || !isAllowedToManage}
placeholder={field.name}
value={fieldNameOverrides[field.id] ?? ""}
onChange={e => {
handleFieldNameChange(field.id, e.target.value)
}}
/>
) : (
<div />
)}
</Fragment>
)
})}
Expand Down
Loading
Loading