///
import {
Cell,
computed,
Default,
handler,
ifElse,
NAME,
navigateTo,
pattern,
UI,
} from "commontools";
// Import Google Auth utility
import {
createGoogleAuth,
type ScopeKey,
} from "../core/util/google-auth-manager.tsx";
// Import markdown conversion utilities
import {
convertDocToMarkdown,
extractDocTitle,
} from "../core/util/google-docs-markdown.ts";
// Import Google Docs client
import {
extractFileId,
type GoogleComment,
GoogleDocsClient,
} from "../core/util/google-docs-client.ts";
// Import Note pattern for "Save as Note" feature
// NOTE: This requires deploying with --root packages/patterns to resolve the import
import Note from "../../notes/note.tsx";
// =============================================================================
// SETUP REQUIREMENTS
// =============================================================================
//
// This pattern requires Google OAuth with specific scopes and APIs enabled:
//
// 1. GOOGLE AUTH CHARM
// - Create and favorite a Google Auth piece with these scopes enabled:
// - Drive (read/write files & comments) - for fetching comments
// - Docs (read document content) - for fetching document content
//
// 2. GOOGLE CLOUD CONSOLE
// The OAuth project must have these APIs enabled:
// - Google Drive API (usually enabled by default)
// - Google Docs API (must be explicitly enabled)
//
// =============================================================================
// =============================================================================
// Types
// =============================================================================
interface Input {
docUrl?: Cell>;
markdown?: Cell>;
docTitle?: Cell>;
isFetching?: Cell>;
lastError?: Cell>;
includeComments?: Cell>;
embedImages?: Cell>;
}
/** Google Docs Markdown Importer. Import Google Docs as Markdown with comments. #googleDocsImporter */
interface Output {
docUrl: string;
markdown: string;
docTitle: string;
}
// =============================================================================
// Handlers
// =============================================================================
// Fetch document and convert to markdown
const importDocument = handler<
unknown,
{
docUrl: Cell;
auth: Cell;
markdown: Cell;
docTitle: Cell;
isFetching: Cell;
lastError: Cell;
includeComments: Cell;
embedImages: Cell;
}
>(
async (
_,
{
docUrl,
auth,
markdown,
docTitle,
isFetching,
lastError,
includeComments,
embedImages,
},
) => {
const url = docUrl.get();
if (!url) {
lastError.set("Please enter a Google Doc URL");
return;
}
const fileId = extractFileId(url);
if (!fileId) {
lastError.set("Could not extract file ID from URL");
return;
}
const authData = auth.get() as { token?: string } | null;
const token = authData?.token;
if (!token) {
lastError.set("Please authenticate with Google first");
return;
}
isFetching.set(true);
lastError.set(null);
try {
// Create client with auth Cell for automatic token refresh
const client = new GoogleDocsClient(auth as Cell, {
debugMode: true,
});
// Fetch document content
const doc = await client.getDocument(fileId);
const title = extractDocTitle(doc);
docTitle.set(title);
// Fetch comments if enabled (client handles pagination and filtering)
let comments: GoogleComment[] = [];
if (includeComments.get()) {
try {
comments = await client.listComments(
fileId,
false, /* includeResolved */
);
} catch (e) {
console.warn("[importDocument] Could not fetch comments:", e);
// Non-fatal - we can still convert without comments
}
}
// Convert to markdown
const md = await convertDocToMarkdown(doc, comments, {
includeComments: includeComments.get(),
embedImages: embedImages.get(),
token,
});
markdown.set(md);
} catch (e: unknown) {
console.error("[importDocument] Error:", e);
const errorMessage = e instanceof Error
? e.message
: "Failed to import document";
lastError.set(errorMessage);
} finally {
isFetching.set(false);
}
},
);
// Save as Note piece
const saveAsNote = handler<
unknown,
{ markdown: Cell; docTitle: Cell }
>((_, { markdown, docTitle }) => {
const md = markdown.get();
const title = docTitle.get() || "Imported Document";
if (!md) {
return;
}
// Create and navigate to a new Note piece with the imported content
return navigateTo(Note({ title, content: md }));
});
// Toggle include comments
const toggleComments = handler }>(
(_, { includeComments }) => {
includeComments.set(!includeComments.get());
},
);
// Toggle embed images
const toggleEmbedImages = handler }>(
(_, { embedImages }) => {
embedImages.set(!embedImages.get());
},
);
// =============================================================================
// Pattern
// =============================================================================
export default pattern(
(
{
docUrl,
markdown,
docTitle,
isFetching,
lastError,
includeComments,
embedImages,
},
) => {
// Save cell references
const docUrlCell = docUrl;
const markdownCell = markdown;
const docTitleCell = docTitle;
const isFetchingCell = isFetching;
const lastErrorCell = lastError;
const includeCommentsCell = includeComments;
const embedImagesCell = embedImages;
// Auth via createGoogleAuth utility (requires Drive and Docs scopes)
const {
auth,
authInfo,
fullUI: authFullUI,
isReady: isAuthenticated,
} = createGoogleAuth({
requiredScopes: ["drive", "docs"] as ScopeKey[],
});
// Has markdown content
const hasMarkdown = computed(() => {
const md = markdownCell.get();
return md && md.trim().length > 0;
});
// Has error
const hasError = computed(() => !!lastErrorCell.get());
// Computed name based on doc title
const pieceName = computed(() => {
const title = docTitleCell.get();
return title ? `Import: ${title}` : "Google Docs Importer";
});
return {
[NAME]: pieceName,
[UI]: (
{/* Header */}
Google Docs Markdown Importer
{authInfo.statusText}
{/* Main content */}
{/* Auth UI */}
{authFullUI}
{/* Document URL input */}
{ifElse(
isAuthenticated,
{ifElse(
isFetchingCell,
Importing...,
"Import",
)}
,
null,
)}
{/* Options */}
{/* Error display */}
{ifElse(
hasError,
{lastErrorCell}
,
null,
)}
{/* Markdown preview */}
{ifElse(
hasMarkdown,
Preview: {docTitleCell}
Copy to Clipboard
Save as Note
{markdownCell}
,
{ifElse(
isAuthenticated,
"Enter a Google Doc URL and click Import to convert it to Markdown",
"Please authenticate with Google to import documents",
)}