Examples
App elements
Assets and media
Fundamentals
Intents
Design interaction
Drag and drop
Design elements
Localization
Content replacement
Data connector intent
Data connector integration for external data sources.
Running this example
To run this example locally:
-
If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.
-
In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to
http://localhost:8080
. -
Clone the starter kit:
git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.gitcd canva-apps-sdk-starter-kitSHELL -
Install dependencies:
npm installSHELL -
Run the example:
npm run start data_connector_intentSHELL -
Click the Preview URL link shown in the terminal to open the example in the Canva editor.
Example app source code
import type {DataTable,DataTableImageUpload,DataTableVideoUpload,GetDataTableRequest,} from "@canva/intents/data";export const saleStageOptions: string[] = ["Initial Release Stage","Construction Stage","Final Release Stage",];// define the data source structure - the configurable parameters of the data queryexport type RealEstateDataConfig = {selectedStageFilter?: string[];};// for a given data source query, get the data from the mock API and transform it to DataTable formatexport const getRealEstateData = async (request: GetDataTableRequest,): Promise<DataTable> => {return new Promise((resolve) => {const { dataSourceRef, limit } = request;if (dataSourceRef != null) {const dataRef = JSON.parse(dataSourceRef?.source) as RealEstateDataConfig;const selectedStages = dataRef.selectedStageFilter?.length? dataRef.selectedStageFilter: saleStageOptions;// get the projects data from the mock APIconst projects = getProjects();// filter projects based on selected stages and transform to DataTableconst dataTable = transformToDataTable(projects, selectedStages);// ensure we don't exceed row limitdataTable.rows = dataTable.rows.slice(0, limit.row);resolve(dataTable);}});};// mock api response structureinterface RealEstateProject {name: string;initialReleaseStage: number;constructionStage: number;finalReleaseStage: number;media: (DataTableImageUpload | DataTableVideoUpload)[];}/*** Sample data for real estate projects.* Each project has a name, sales stage values, and media assets.*/const getProjects = (): RealEstateProject[] => [{name: "The Kensington",initialReleaseStage: getRandomSalesValue(),constructionStage: getRandomSalesValue(),finalReleaseStage: getRandomSalesValue(),media: staticMediaData,},{name: "Horizon Hurstville",initialReleaseStage: getRandomSalesValue(),constructionStage: getRandomSalesValue(),finalReleaseStage: getRandomSalesValue(),media: staticMediaData,},{name: "Sterling Lane Cove",initialReleaseStage: getRandomSalesValue(),constructionStage: getRandomSalesValue(),finalReleaseStage: getRandomSalesValue(),media: staticMediaData,},{name: "Surry Hills Village",initialReleaseStage: getRandomSalesValue(),constructionStage: getRandomSalesValue(),finalReleaseStage: getRandomSalesValue(),media: staticMediaData,},{name: "Willoughby Grounds",initialReleaseStage: getRandomSalesValue(),constructionStage: getRandomSalesValue(),finalReleaseStage: getRandomSalesValue(),media: staticMediaData,},{name: "Marque Rockdale",initialReleaseStage: getRandomSalesValue(),constructionStage: getRandomSalesValue(),finalReleaseStage: getRandomSalesValue(),media: staticMediaData,},{name: "Atrium The Retreat",initialReleaseStage: getRandomSalesValue(),constructionStage: getRandomSalesValue(),finalReleaseStage: getRandomSalesValue(),media: staticMediaData,},{name: "Aura by Aqualand",initialReleaseStage: getRandomSalesValue(),constructionStage: getRandomSalesValue(),finalReleaseStage: getRandomSalesValue(),media: staticMediaData,},];// Transform mock api data to DataTable based on selected stagesconst transformToDataTable = (projects: RealEstateProject[],selectedStages: string[],): DataTable => {const columnConfigs = [{ name: "Project", type: "string" as const },...(selectedStages.includes("Initial Release Stage")? [{ name: "Initial Release Stage", type: "number" as const }]: []),...(selectedStages.includes("Construction Stage")? [{ name: "Construction Stage", type: "number" as const }]: []),...(selectedStages.includes("Final Release Stage")? [{ name: "Final Release Stage", type: "number" as const }]: []),{ name: "Media", type: "media" as const },];const rows = projects.map((project) => ({cells: [{ type: "string" as const, value: project.name },...(selectedStages.includes("Initial Release Stage")? [{ type: "number" as const, value: project.initialReleaseStage }]: []),...(selectedStages.includes("Construction Stage")? [{ type: "number" as const, value: project.constructionStage }]: []),...(selectedStages.includes("Final Release Stage")? [{ type: "number" as const, value: project.finalReleaseStage }]: []),{ type: "media" as const, value: project.media },],}));return { columnConfigs, rows };};// static media data for the projectsconst staticMediaData: (DataTableImageUpload | DataTableVideoUpload)[] = [{type: "video_upload",mimeType: "video/mp4",url: "https://www.canva.dev/example-assets/video-import/video.mp4",thumbnailImageUrl:"https://www.canva.dev/example-assets/video-import/thumbnail-image.jpg",thumbnailVideoUrl:"https://www.canva.dev/example-assets/video-import/thumbnail-video.mp4",width: 405,height: 720,aiDisclosure: "none",},{type: "image_upload",mimeType: "image/jpeg",url: "https://www.canva.dev/example-assets/image-import/image.jpg",thumbnailUrl:"https://www.canva.dev/example-assets/image-import/thumbnail.jpg",width: 540,height: 720,aiDisclosure: "none",},];// Helper function to generate random numbers between 10 and 100function getRandomSalesValue(): number {const min = 10;const max = 100;return Math.floor(Math.random() * (max - min + 1)) + min;}
TYPESCRIPT
import type {RenderSelectionUiRequest,GetDataTableRequest,GetDataTableResponse,} from "@canva/intents/data";import { prepareDataConnector } from "@canva/intents/data";import { Alert, AppUiProvider } from "@canva/app-ui-kit";import { createRoot } from "react-dom/client";import { SelectionUI } from "./selection_ui";import "@canva/app-ui-kit/styles.css";import { getRealEstateData } from "./data";const root = createRoot(document.getElementById("root") as Element);prepareDataConnector({/*** Gets structured data from an external source.** This action is called in two scenarios:** - During data selection to preview data before import (when {@link RenderSelectionUiRequest.updateDataRef} is called).* - When refreshing previously imported data (when the user requests an update).** @param request - Parameters for the data fetching operation.* @returns A promise resolving to either a successful result with data or an error.*/getDataTable: async (request: GetDataTableRequest,): Promise<GetDataTableResponse> => {try {const dataTable = await getRealEstateData(request);return {status: "completed",dataTable,metadata: {description:"Sydney construction project sales in each release stage",providerInfo: { name: "Demo Sales API" },},};} catch {return {status: "app_error",message: "Failed to process data request",};}},/*** Renders a UI component for selecting and configuring data from external sources.* This UI should allow users to browse data sources, apply filters, and select data.* When selection is complete, the implementation must call the `updateDataRef`* callback provided in the params to preview and confirm the data selection.** @param request - parameters that provide context and configuration for the data selection UI.* Contains invocation context, size limits, and the updateDataRef callback*/renderSelectionUi: async (request: RenderSelectionUiRequest) => {root.render(<AppUiProvider><SelectionUI {...request} /></AppUiProvider>,);},});// TODO: Fallback message if you have not turned on the data connector intent.// You can remove this once your app is correctly configured.root.render(<AppUiProvider><Alert tone="critical">If you're seeing this, you need to turn on the data connector intent inthe developer portal for this app.</Alert></AppUiProvider>,);
TYPESCRIPT
import type { RenderSelectionUiRequest } from "@canva/intents/data";import {Alert,Button,Rows,Text,CheckboxGroup,FormField,Title,} from "@canva/app-ui-kit";import { useEffect, useState } from "react";import * as styles from "styles/components.css";import { saleStageOptions, type RealEstateDataConfig } from "./data";export const SelectionUI = (request: RenderSelectionUiRequest) => {const [selectedStageFilter, setSelectedStageFilter] = useState<string[]>();const [error, setError] = useState<string | null>(null);const [loading, setLoading] = useState(false);const [success, setSuccess] = useState(false);// handle the invocation context to respond to how the app was loaded// there may be an existing data source to load or an error to displayuseEffect(() => {const { reason } = request.invocationContext;switch (reason) {case "data_selection":// If there's an existing selection, pre-populate the UIif (request.invocationContext.dataSourceRef) {try {const savedParams = JSON.parse(request.invocationContext.dataSourceRef.source,) as RealEstateDataConfig;setSelectedStageFilter(savedParams.selectedStageFilter || []);} catch {setError("Failed to load saved selection");}}break;case "outdated_source_ref":// data source reference persisted in Canva is outdated. Prompt users to reselect data.setError("Your previously selected data is no longer available. Please make a new selection.",);break;case "app_error":setError(request.invocationContext.message ||"An error occurred with your data",);break;default:// this should never happenbreak;}}, [request.invocationContext]);// use updateDataRef to set the new query config// this will trigger the getDataTable callback for this connectorconst loadData = async () => {setLoading(true);setError(null);setSuccess(false);try {const result = await request.updateDataRef({source: JSON.stringify({ selectedStageFilter }),title: "Sydney construction project sales in each release stage",});if (result.status === "completed") {setSuccess(true);} else {setError(result.status === "app_error" && "message" in result? result.message || "An error occurred": `Error: ${result.status}`,);}} catch {setError("Failed to update data");} finally {setLoading(false);}};return (<div className={styles.scrollContainer}><Rows spacing="2u"><Title size="large">Project Sales</Title>{error && <Alert tone="critical" title={error} />}{success && (<Alert tone="positive" title="Data preview loaded successfully!" />)}<Text variant="bold">Sydney construction project sales in each release stage</Text><Rows spacing="1u"><FormFieldlabel="Release Stage"control={(props) => (<CheckboxGroup{...props}value={selectedStageFilter}options={saleStageOptions.map((stage) => {return {label: stage,value: stage,};})}onChange={setSelectedStageFilter}/>)}/><Buttonvariant="primary"onClick={loadData}loading={loading}stretch>Load Data</Button></Rows></Rows></div>);};
TYPESCRIPT
API Reference
Need Help?
- Join our Community Forum(opens in a new tab or window)
- Report issues with this example on GitHub(opens in a new tab or window)