Action
Craft Imagine Smart Add
Posted by @FlohGro,
Last update
about 9 hours ago
created by @FlohGro / more on my Blog
This Action connects Drafts with Craft Imagine.
Seamlessly send drafts to any Craft document via the Multi-Document API. Intelligently detects collections and dynamically prompts for properties based on the collection schema.
Features:
- Lists all accessible Craft documents and lets you choose the destination
- Automatically detects collections in the selected document
- For collections: uses draft title as item title and generates dynamic prompts for all properties (text, dates, single/multi-select, etc.); you can also choose to append it as text
- For documents without collections: appends content as formatted blocks
- Draft body is added as content inside collection items
Requirements: Craft account with API access via Craft Imagine
Setup:
- Create an API Connection via the Imagine tab in Craft (and add the documents you want to use with the API)
- Copy the API base URL (I also recommend to set a token)
- Run the action and paste the base URL (and token) into the “Confiure Action” pop-up;
- Start using the Action with real content š
If you find this useful and want to support me you can donate or buy me a coffee
Steps
-
configurationKey
name Craft API Base Url
key apiBaseUrl
-
configurationKey
name Craft API Token
key apiToken
-
script
// created by @FlohGro@social.lol //const apiBaseUrl = "https://connect.craft.do/links/ChW7CJdljaU/api/v1"; const apiBaseUrl = action.configuredValues["apiBaseUrl"] //const apiToken = "pdk_e3051c73-d61f-3982-48d8-cdf2de01f4b8"; const apiToken = action.configuredValues["apiToken"] // Add draft body as content inside collection items? const includeBodyAsContent = true; // Validate configuration if (apiBaseUrl === "YOUR_CRAFT_MULTI_DOC_API_BASE_URL" || !apiBaseUrl.startsWith("https://")) { app.displayErrorMessage("Please configure your Craft API base URL in the action script."); context.fail(); } // Get draft content const draftContent = draft.content.trim(); const draftTitle = draft.displayTitle; const draftBody = draft.processTemplate("[[body]]").trim(); if (!draftContent) { app.displayErrorMessage("Draft is empty."); context.fail(); } // API Helper Functions const http = HTTP.create(); function apiRequest(method, endpoint, data = null) { const options = { "url": `${apiBaseUrl}${endpoint}`, "method": method, "headers": { "Authorization": `Bearer ${apiToken}`, "Accept": "application/json", "Content-Type": "application/json" } }; if (data) { options.encoding = "json"; options.data = data; } const response = http.request(options); let parsedData = null; if (response.responseText) { try { parsedData = JSON.parse(response.responseText); } catch (e) { parsedData = { raw: response.responseText }; } } return { success: response.success && response.statusCode >= 200 && response.statusCode < 300, statusCode: response.statusCode, data: parsedData, error: response.success ? null : response.responseText }; } function formatDateForAPI(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function showCancelMessage(text) { const message = text + " cancelled." app.displayInfoMessage(message) } // Fetch and Select Document function f() { const docsResult = apiRequest("GET", "/documents"); if (!docsResult.success) { app.displayErrorMessage(`Failed to fetch documents: ${docsResult.error || docsResult.statusCode}`); context.fail(); } const documents = (docsResult.data.items || []).filter(doc => !doc.isDeleted); if (documents.length === 0) { app.displayErrorMessage("No documents found in your Craft connection."); context.fail(); } // Show document selection const docPrompt = Prompt.create(); docPrompt.title = "Select Document"; docPrompt.message = `Choose a Craft document:\n\nDraft: "${draftTitle}"`; for (const doc of documents) { docPrompt.addButton(doc.title, doc.id) } if (!docPrompt.show()) { context.cancel(); showCancelMessage("Selecting Document") return } const selectedDocId = docPrompt.buttonPressed const docTitle = documents.filter(doc => doc.id == selectedDocId)[0].title // Check for Collections in Document const collectionsResult = apiRequest("GET", `/collections?documentIds[]=${selectedDocId}`); let collections = []; if (collectionsResult.success && collectionsResult.data.items) { collections = collectionsResult.data.items; } // Handle Based on Collections if (collections.length === 0) { // No collections - just append content as blocks const insertResult = apiRequest("POST", "/blocks", { "markdown": draftContent, "position": { "position": "end", "pageId": selectedDocId } }); if (insertResult.success) { app.displaySuccessMessage("ā Added to " + docTitle); } else { app.displayErrorMessage(`Failed to add content: ${insertResult.error || insertResult.statusCode}`); context.fail(); return } } else { // Collections found - ask user what to do const modePrompt = Prompt.create(); modePrompt.title = "Document has Collections"; modePrompt.message = `"${docTitle}" contains ${collections.length} collection(s):\n${collections.map(col => "\"" + col.name + "\"" || "Untitled")}\n\nWhat would you like to do?`; modePrompt.addButton("Add to Collection"); modePrompt.addButton("Append as Text"); if (!modePrompt.show()) { context.cancel(); showCancelMessage("Selecting Append Type") return } if (modePrompt.buttonPressed === "Append as Text") { let paragraphs = draft.content.split(/\n\n+/).filter(p => p.trim()); paragraphs.unshift(draftTitle) const blocks = paragraphs.map(p => ({ "type": "text", "markdown": p.trim() })); const insertResult = apiRequest("POST", "/blocks", { "blocks": blocks, "position": { "position": "end", "pageId": selectedDocId } }); if (insertResult.success) { app.displaySuccessMessage(`ā Added to "${docTitle}"`); } else { app.displayErrorMessage(`Failed to add content: ${insertResult.error || insertResult.statusCode}`); context.fail(); return } } else { // Add to collection let selectedCollection; if (collections.length === 1) { selectedCollection = collections[0]; } else { const colPrompt = Prompt.create(); colPrompt.title = "Select Collection"; colPrompt.message = "Choose which collection to add to:"; for (const col of collections) { colPrompt.addButton(col.name || "Untitled Collection", col) } if (!colPrompt.show()) { context.cancel(); showCancelMessage("Selecting Collection") return } selectedCollection = colPrompt.buttonPressed } // Fetch Collection Schema const schemaResult = apiRequest("GET", `/collections/${selectedCollection.id}/schema?format=schema`); if (!schemaResult.success) { app.displayErrorMessage(`Failed to fetch schema: ${schemaResult.error || schemaResult.statusCode}`); context.fail(); return } const schema = schemaResult.data; const properties = schema.properties || []; // Build Dynamic Prompt for Properties const itemTitle = draftTitle || draftContent.split('\n')[0]; const propPrompt = Prompt.create(); propPrompt.title = `New ${schema.name || 'Collection'} Item`; let promptMessage = `Title: "${itemTitle}"`; if (draftBody && includeBodyAsContent) { promptMessage += `\n\nš Body will be added as content inside the item.`; } promptMessage += `\n\nSet properties (leave empty to skip):`; propPrompt.message = promptMessage; const fieldMap = []; for (const prop of properties) { const fieldKey = `prop_${prop.key || prop.name}`; const label = prop.name; switch (prop.type) { case "text": propPrompt.addTextField(fieldKey, label, ""); fieldMap.push({ key: prop.key || prop.name, fieldKey, type: "text", name: prop.name }); break; case "number": propPrompt.addTextField(fieldKey, `${label} (number)`, 0, true); fieldMap.push({ key: prop.key || prop.name, fieldKey, type: "number", name: prop.name, config: prop.config }); break; case "date": propPrompt.addDatePicker(fieldKey, label, new Date(), { "mode": "date" }); fieldMap.push({ key: prop.key || prop.name, fieldKey, type: "date", name: prop.name }); break; case "boolean": propPrompt.addSwitch(fieldKey, label, false); fieldMap.push({ key: prop.key || prop.name, fieldKey, type: "boolean", name: prop.name }); break; case "url": propPrompt.addTextField(fieldKey, `${label} (URL)`, ""); fieldMap.push({ key: prop.key || prop.name, fieldKey, type: "url", name: prop.name }); break; case "email": propPrompt.addTextField(fieldKey, `${label} (Email)`, ""); fieldMap.push({ key: prop.key || prop.name, fieldKey, type: "email", name: prop.name }); break; case "phone": propPrompt.addTextField(fieldKey, `${label} (Phone)`, ""); fieldMap.push({ key: prop.key || prop.name, fieldKey, type: "phone", name: prop.name }); break; case "singleSelect": if (prop.options && prop.options.length > 0) { const optionNames = prop.options.map(opt => opt.name); const optionsWithEmpty = ["ā Select ā", ...optionNames]; propPrompt.addPicker(fieldKey, label, [optionsWithEmpty], [0]); fieldMap.push({ key: prop.key || prop.name, fieldKey, type: "singleSelect", name: prop.name, options: optionsWithEmpty, originalOptions: optionNames }); } break; case "multiSelect": if (prop.options && prop.options.length > 0) { const optionNames = prop.options.map(opt => opt.name); propPrompt.addSelect(fieldKey, `${label} (multi)`, optionNames, [], true); fieldMap.push({ key: prop.key || prop.name, fieldKey, type: "multiSelect", name: prop.name, options: optionNames }); } break; case "relation": // Skip relations - too complex for basic prompt break; case "blockLink": // Skip block links break; default: propPrompt.addTextField(fieldKey, label, ""); fieldMap.push({ key: prop.key || prop.name, fieldKey, type: "text", name: prop.name }); } } propPrompt.addButton("Create Item"); if (!propPrompt.show()) { context.cancel(); showCancelMessage("Creating Item") return } // Build Item Data from Prompt Values const itemProperties = {}; for (const field of fieldMap) { const value = propPrompt.fieldValues[field.fieldKey]; if (value === undefined || value === null) continue; switch (field.type) { case "text": case "url": case "email": case "phone": if (value && String(value).trim()) { itemProperties[field.key] = String(value).trim(); } break; case "number": if (value && String(value).trim()) { const num = parseFloat(String(value).trim()); if (!isNaN(num)) { itemProperties[field.key] = num; } } break; case "date": if (value instanceof Date) { itemProperties[field.key] = formatDateForAPI(value); } break; case "boolean": // Always include boolean values itemProperties[field.key] = Boolean(value); break; case "singleSelect": const selectedIdx = value[0]; if (selectedIdx > 0) { itemProperties[field.key] = field.originalOptions[selectedIdx - 1]; } break; case "multiSelect": if (Array.isArray(value) && value.length > 0) { itemProperties[field.key] = value; } break; } } // Create Collection Item const newItem = { "title": itemTitle, "properties": itemProperties }; const createResult = apiRequest("POST", `/collections/${selectedCollection.id}/items`, { "items": [newItem] }); if (!createResult.success) { let errorMsg = `Failed to create item: ${createResult.statusCode}`; if (createResult.data && createResult.data.message) { errorMsg = createResult.data.message; } app.displayErrorMessage(errorMsg); console.log("Create item error:", JSON.stringify(createResult.data || createResult.error)); context.fail(); return } // Step 8: Add Body Content to Item (if enabled) let contentAdded = false; if (includeBodyAsContent && draftBody && createResult.data && createResult.data.items && createResult.data.items.length > 0) { const createdItem = createResult.data.items[0]; const itemId = createdItem.id; if (itemId) { // Split body into paragraphs and create separate blocks const paragraphs = draftBody.split(/\n\n+/).filter(p => p.trim()); const blocks = paragraphs.map(p => ({ "type": "text", "markdown": p.trim() })); const contentResult = apiRequest("POST", "/blocks", { "blocks": blocks, "position": { "position": "end", "pageId": itemId } }); contentAdded = contentResult.success; if (!contentAdded) { console.log("failed to add body content:", json.stringify(contentresult.data || contentresult.error)); } } } let message = `ā created "${itemTitle}"`; app.displaySuccessMessage(message); } } } f();
Options
-
After Success Default Notification None Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.