diff --git a/plugins/notion/src/App.css b/plugins/notion/src/App.css index 7eaa940dd..ac65a9bc2 100644 --- a/plugins/notion/src/App.css +++ b/plugins/notion/src/App.css @@ -37,6 +37,10 @@ form { gap: 10px; } +p a { + cursor: pointer; +} + .sticky-divider { position: sticky; top: 0; @@ -249,6 +253,8 @@ select:not(:disabled) { height: 100%; padding: 0px 15px 15px 15px; gap: 15px; + user-select: none; + -webkit-user-select: none; } .login-image { @@ -296,7 +302,45 @@ select:not(:disabled) { width: 100%; } +.actions a { + display: contents; +} + .action-button { flex: 1; width: 100%; } + +/* Progress State */ + +.progress-bar-text { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + color: var(--framer-color-text-tertiary); +} + +.progress-bar-percent { + font-weight: 600; + color: var(--framer-color-text); +} + +.progress-bar { + height: 3px; + width: 100%; + flex-shrink: 0; + border-radius: 10px; + background-color: var(--framer-color-bg-tertiary); + position: relative; +} + +.progress-bar-fill { + position: absolute; + top: 0; + bottom: 0; + left: 0; + border-radius: 10px; + background-color: var(--framer-color-tint); +} diff --git a/plugins/notion/src/App.tsx b/plugins/notion/src/App.tsx index 9ed6dd418..72023ba7d 100644 --- a/plugins/notion/src/App.tsx +++ b/plugins/notion/src/App.tsx @@ -8,7 +8,7 @@ import { type DatabaseIdMap, type DataSource, getDataSource } from "./data" import { FieldMapping } from "./FieldMapping" import { NoTableAccess } from "./NoAccess" import { SelectDataSource } from "./SelectDataSource" -import { showAccessErrorUI, showFieldMappingUI, showLoginUI } from "./ui" +import { showAccessErrorUI, showFieldMappingUI, showLoginUI, showProgressUI } from "./ui" interface AppProps { collection: ManagedCollection @@ -32,6 +32,7 @@ export function App({ const [dataSource, setDataSource] = useState(null) const [isLoadingDataSource, setIsLoadingDataSource] = useState(Boolean(previousDatabaseId)) const [hasAccessError, setHasAccessError] = useState(false) + const [isSyncing, setIsSyncing] = useState(false) // Support self-referencing databases by allowing the current collection to be referenced. const databaseIdMap = useMemo(() => { @@ -46,6 +47,8 @@ export function App({ try { if (hasAccessError) { await showAccessErrorUI() + } else if (isSyncing) { + await showProgressUI() } else if (dataSource || isLoadingDataSource) { await showFieldMappingUI() } else { @@ -60,7 +63,7 @@ export function App({ } void showUI() - }, [dataSource, isLoadingDataSource, hasAccessError]) + }, [dataSource, isLoadingDataSource, hasAccessError, isSyncing]) useEffect(() => { if (!previousDatabaseId) { @@ -149,6 +152,7 @@ export function App({ previousLastSynced={previousLastSynced} previousIgnoredFieldIds={previousIgnoredFieldIds} databaseIdMap={databaseIdMap} + setIsSyncing={setIsSyncing} /> ) } diff --git a/plugins/notion/src/FieldMapping.tsx b/plugins/notion/src/FieldMapping.tsx index 4b38baf37..73ceb6f4d 100644 --- a/plugins/notion/src/FieldMapping.tsx +++ b/plugins/notion/src/FieldMapping.tsx @@ -23,6 +23,7 @@ import { type SyncProgress, syncCollection, } from "./data" +import { Progress } from "./Progress" import { assert, syncMethods } from "./utils" const labelByFieldTypeOption: Record = { @@ -144,6 +145,7 @@ interface FieldMappingProps { previousLastSynced: string | null previousIgnoredFieldIds: string | null databaseIdMap: DatabaseIdMap + setIsSyncing: (isSyncing: boolean) => void } export function FieldMapping({ @@ -153,6 +155,7 @@ export function FieldMapping({ previousLastSynced, previousIgnoredFieldIds, databaseIdMap, + setIsSyncing, }: FieldMappingProps) { const isAllowedToManage = useIsAllowedTo("ManagedCollection.setFields", ...syncMethods) @@ -250,6 +253,7 @@ export function FieldMapping({ const task = async () => { try { setStatus("syncing-collection") + setIsSyncing(true) setSyncProgress(null) const fields = fieldsInfoToCollectionFields(fieldsInfo, databaseIdMap) @@ -284,6 +288,7 @@ export function FieldMapping({ ) } finally { setStatus("mapping-fields") + setIsSyncing(false) setSyncProgress(null) } } @@ -299,7 +304,9 @@ export function FieldMapping({ ) } - const progressPercent = syncProgress ? ((syncProgress.current / syncProgress.total) * 100).toFixed(1) : null + if (isSyncing) { + return + } return (
@@ -350,15 +357,11 @@ export function FieldMapping({

diff --git a/plugins/notion/src/Progress.tsx b/plugins/notion/src/Progress.tsx new file mode 100644 index 000000000..454402f07 --- /dev/null +++ b/plugins/notion/src/Progress.tsx @@ -0,0 +1,23 @@ +export function Progress({ current, total }: { current: number; total: number }) { + const progressPercent = total > 0 ? ((current / total) * 100).toFixed(1).replace(".0", "") : "0" + const formatter = new Intl.NumberFormat("en-US") + const formattedCurrent = formatter.format(current) + const formattedTotal = formatter.format(total) + + return ( +
+
+ {progressPercent}% + + {formattedCurrent} / {formattedTotal} + +
+
+
+
+

+ {current > 0 ? "Syncing" : "Loading data"}… please keep the plugin open until the process is complete. +

+
+ ) +} diff --git a/plugins/notion/src/api.ts b/plugins/notion/src/api.ts index f3c8d3062..195567517 100644 --- a/plugins/notion/src/api.ts +++ b/plugins/notion/src/api.ts @@ -394,15 +394,28 @@ export async function getPageBlocksAsRichText(pageId: string) { return blocksToHtml(blocks) } -export async function getDatabaseItems(database: GetDatabaseResponse): Promise { +export async function getDatabaseItems( + database: GetDatabaseResponse, + onProgress?: (progress: { current: number; total: number }) => void +): Promise { const notion = getNotionClient() - const data = await collectPaginatedAPI(notion.databases.query, { + const data: PageObjectResponse[] = [] + let itemCount = 0 + + const databaseIterator = iteratePaginatedAPI(notion.databases.query, { database_id: database.id, }) - assert(data.every(isFullPage), "Response is not a full page") - return data + for await (const item of databaseIterator) { + data.push(item as PageObjectResponse) + itemCount++ + onProgress?.({ current: 0, total: itemCount }) + } + + const pages = data.filter(isFullPage) + + return pages } export function isUnchangedSinceLastSync(lastEditedTime: string, lastSyncedTime: string | null): boolean { diff --git a/plugins/notion/src/data.ts b/plugins/notion/src/data.ts index fbb782929..15697a6f6 100644 --- a/plugins/notion/src/data.ts +++ b/plugins/notion/src/data.ts @@ -127,7 +127,7 @@ export async function syncCollection( const seenItemIds = new Set() - const databaseItems = await getDatabaseItems(dataSource.database) + const databaseItems = await getDatabaseItems(dataSource.database, onProgress) const limit = pLimit(CONCURRENCY_LIMIT) // Progress tracking diff --git a/plugins/notion/src/ui.ts b/plugins/notion/src/ui.ts index 53cdd3aa0..7f75524c9 100644 --- a/plugins/notion/src/ui.ts +++ b/plugins/notion/src/ui.ts @@ -27,3 +27,13 @@ export async function showLoginUI() { resizable: false, }) } + +export async function showProgressUI() { + await framer.showUI({ + width: 260, + height: 102, + minWidth: 260, + minHeight: 102, + resizable: false, + }) +}