Skip to content
Open
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
5 changes: 4 additions & 1 deletion plugins/google-sheets/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ export function App({ pluginContext }: AppProps) {
const mode = framer.mode

// Not a hook because we don't want to re-run the effect
const isAllowedToSync = framer.isAllowedTo(...syncMethods)
const hasEnumField = context.type === "update" && context.collectionFields.some(field => field.type === "enum")
const isAllowedToSync =
framer.isAllowedTo(...syncMethods) && (!hasEnumField || framer.isAllowedTo("ManagedCollection.setFields"))
const shouldSyncOnly = mode === "syncManagedCollection" && shouldSyncImmediately(context) && isAllowedToSync

useLayoutEffect(() => {
Expand Down Expand Up @@ -222,6 +224,7 @@ export function App({ pluginContext }: AppProps) {
spreadsheetId,
sheetTitle,
fields,
configureFields: false,
// Determine if the field type is already configured, otherwise default to "string"
colFieldTypes: headerRow.map(colName => {
const field = fields.find(field => field.name === colName)
Expand Down
3 changes: 2 additions & 1 deletion plugins/google-sheets/src/pages/MapSheetFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const fieldTypeOptions: FieldTypeOption[] = [
{ type: "color", label: "Color" },
{ type: "boolean", label: "Toggle" },
{ type: "number", label: "Number" },
{ type: "enum", label: "Option" },
{ type: "file", label: "File" },
]

Expand Down Expand Up @@ -322,7 +323,7 @@ export function MapSheetFieldsPage({
<button
disabled={!isAllowedToManage}
title={isAllowedToManage ? undefined : "Insufficient permissions"}
className="whitespace-nowrap inline-block"
className={cx("whitespace-nowrap", !isPending && "inline-block")}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fix for the loading spinner being left aligned instead of centered. Not related to the other changes in this PR.

>
{isPending ? <div className="framer-spinner" /> : `Import from ${sheetTitle}`}
</button>
Expand Down
90 changes: 82 additions & 8 deletions plugins/google-sheets/src/sheets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ import * as v from "valibot"
import auth from "./auth"
import { logSyncResult } from "./debug.ts"
import { queryClient } from "./main.tsx"
import { assert, columnToLetter, generateHashId, generateUniqueNames, isDefined, listFormatter, slugify } from "./utils"
import {
assert,
columnToLetter,
generateHashId,
generateUniqueNames,
hashStringToEnumCaseId,
isDefined,
listFormatter,
slugify,
} from "./utils"

const USER_INFO_API_URL = "https://www.googleapis.com/oauth2/v1"
const SHEETS_API_URL = "https://sheets.googleapis.com/v4"
Expand Down Expand Up @@ -306,6 +315,11 @@ export interface SyncMutationOptions {
ignoredColumns: string[]
colFieldTypes: VirtualFieldType[]
lastSyncedTime: string | null
/**
* When false (e.g. sync-only mode), schema is only applied when enum fields need refreshed cases.
* When true (default), collection fields are always updated from the mapping.
*/
configureFields?: boolean
}

const BASE_DATE_1900 = new Date(Date.UTC(1899, 11, 30))
Expand Down Expand Up @@ -348,6 +362,54 @@ function isValidISODate(dateString: string): boolean {
}
}

function isEnumCellValueEmpty(cell: CellValue | undefined): boolean {
if (!isDefined(cell)) return true
if (typeof cell === "string" && cell.trim() === "") return true
return false
}

function buildEnumCasesForColumn(rows: Row[], colIndex: number): { id: string; name: string }[] {
const unique = new Set<string>()
let hasEmptyCell = false

for (const row of rows) {
const cell = row[colIndex]
if (isEnumCellValueEmpty(cell)) {
hasEmptyCell = true
continue
}
unique.add(String(cell))
}

const sorted = Array.from(unique).sort((a, b) => a.localeCompare(b))
const valueCases = sorted.map(text => ({
id: hashStringToEnumCaseId(text),
name: text,
}))

if (hasEmptyCell) {
return [{ id: "null", name: "None" }, ...valueCases]
}

return valueCases
}

function enrichFieldsWithEnumCases(
fields: SheetCollectionFieldInput[],
uniqueHeaderRowNames: string[],
rows: Row[]
): SheetCollectionFieldInput[] {
return fields.map(field => {
if (field.type !== "enum") return field

const colIndex = uniqueHeaderRowNames.indexOf(field.id)
if (colIndex === -1) return field

const cases = buildEnumCasesForColumn(rows, colIndex)
return { ...field, type: "enum", cases }
})
}

function getFieldDataEntryInput(type: VirtualFieldType, cellValue: CellValue): FieldDataEntryInput | null {
switch (type) {
case "number": {
Expand Down Expand Up @@ -389,6 +451,10 @@ function getFieldDataEntryInput(type: VirtualFieldType, cellValue: CellValue): F
if (!isDefined(cellValue)) return null
return { type, value: String(cellValue) }
}
case "enum": {
if (isEnumCellValueEmpty(cellValue)) return null
return { type: "enum", value: hashStringToEnumCaseId(String(cellValue)) }
}
default:
return null
}
Expand Down Expand Up @@ -460,6 +526,12 @@ function processSheetRow({
type: "date",
}
break
case "enum":
fieldDataEntryInput = {
value: "null",
type: "enum",
}
break
}
}

Expand Down Expand Up @@ -565,6 +637,7 @@ export async function syncSheet({
ignoredColumns,
slugColumn,
colFieldTypes,
configureFields,
}: SyncMutationOptions) {
if (fields.length === 0) {
throw new Error("Expected to have at least one field selected to sync.")
Expand All @@ -576,6 +649,13 @@ export async function syncSheet({
const [headerRow, ...rows] = sheet.values

const uniqueHeaderRowNames = generateUniqueNames(headerRow)
const enrichedFields = enrichFieldsWithEnumCases(fields, uniqueHeaderRowNames, rows)
const needsFieldSchemaUpdate = !configureFields || enrichedFields.some(field => field.type === "enum")

if (needsFieldSchemaUpdate) {
await collection.setFields(enrichedFields.map(mapFieldToFramer))
}

const headerRowHash = generateHeaderRowHash(headerRow, ignoredColumns)

// Find the longest row length to check if any sheet rows are longer than the header row
Expand Down Expand Up @@ -798,13 +878,7 @@ export const useSyncSheetMutation = ({
}) => {
return useMutation({
mutationFn: async (args: SyncMutationOptions) => {
const collection = await framer.getActiveManagedCollection()
const fields = args.fields.map(mapFieldToFramer)
await collection.setFields(fields)
return await syncSheet({
...args,
fields,
})
return await syncSheet(args)
},
onSuccess,
onError,
Expand Down
23 changes: 23 additions & 0 deletions plugins/google-sheets/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ export function generateHashId(text: string): string {
return unsignedHash.toString(16).padStart(8, "0")
}

/**
* Stable 32-character lowercase hex id derived from text (enum case ids from cell values).
*/
export function hashStringToEnumCaseId(text: string): string {
let h0 = 5381
let h1 = 52711
let h2 = 0
let h3 = 0
for (let i = 0; i < text.length; i++) {
const c = text.charCodeAt(i)
h0 = (Math.imul(h0, 33) ^ c) >>> 0
h1 = (Math.imul(h1, 33) ^ (c + i)) >>> 0
h2 = (Math.imul(h2, 33) + c) >>> 0
h3 = (h3 + Math.imul(c, i + 1)) >>> 0
}
return [
h0.toString(16).padStart(8, "0"),
h1.toString(16).padStart(8, "0"),
h2.toString(16).padStart(8, "0"),
h3.toString(16).padStart(8, "0"),
].join("")
}

/**
* Generates unique names by appending a suffix if the name is already used.
*/
Expand Down
Loading