@canva/cli 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/cli.js +472 -463
  3. package/lib/cjs/index.cjs +2 -2
  4. package/lib/esm/index.mjs +2 -2
  5. package/lib/index.d.ts +2 -0
  6. package/package.json +1 -1
  7. package/templates/base/package.json +3 -2
  8. package/templates/base/styles/components.css +18 -0
  9. package/templates/common/.env.template +1 -1
  10. package/templates/dam/canva-app.json +5 -0
  11. package/templates/dam/package.json +4 -2
  12. package/templates/dam/src/index.tsx +3 -21
  13. package/templates/dam/src/intents/design_editor/index.tsx +25 -0
  14. package/templates/data_connector/package.json +3 -2
  15. package/templates/data_connector/src/api/data_sources/designs.tsx +1 -1
  16. package/templates/data_connector/src/api/data_sources/templates.tsx +1 -1
  17. package/templates/data_connector/src/components/header.tsx +1 -1
  18. package/templates/data_connector/src/index.tsx +2 -66
  19. package/templates/data_connector/src/{app.tsx → intents/data_connector/app.tsx} +3 -3
  20. package/templates/data_connector/src/{entrypoint.tsx → intents/data_connector/entrypoint.tsx} +5 -5
  21. package/templates/data_connector/src/{home.tsx → intents/data_connector/home.tsx} +1 -1
  22. package/templates/data_connector/src/intents/data_connector/index.tsx +56 -0
  23. package/templates/data_connector/src/pages/error.tsx +1 -1
  24. package/templates/data_connector/src/pages/login.tsx +1 -1
  25. package/templates/data_connector/src/routes/protected_route.tsx +1 -1
  26. package/templates/data_connector/src/routes/routes.tsx +3 -3
  27. package/templates/data_connector/styles/components.css +18 -0
  28. package/templates/gen_ai/canva-app.json +5 -0
  29. package/templates/gen_ai/package.json +4 -2
  30. package/templates/gen_ai/src/components/footer.tsx +1 -1
  31. package/templates/gen_ai/src/components/loading_results.tsx +1 -1
  32. package/templates/gen_ai/src/components/prompt_input.tsx +1 -1
  33. package/templates/gen_ai/src/index.tsx +3 -14
  34. package/templates/gen_ai/src/{app.tsx → intents/design_editor/app.tsx} +3 -3
  35. package/templates/gen_ai/src/{home.tsx → intents/design_editor/home.tsx} +1 -1
  36. package/templates/gen_ai/src/intents/design_editor/index.tsx +17 -0
  37. package/templates/gen_ai/src/pages/error.tsx +1 -1
  38. package/templates/gen_ai/src/routes/routes.tsx +2 -2
  39. package/templates/gen_ai/styles/components.css +18 -0
  40. package/templates/hello_world/canva-app.json +5 -0
  41. package/templates/hello_world/package.json +4 -2
  42. package/templates/hello_world/src/index.tsx +3 -21
  43. package/templates/hello_world/src/{app.tsx → intents/design_editor/app.tsx} +26 -3
  44. package/templates/hello_world/src/intents/design_editor/index.tsx +25 -0
  45. package/templates/hello_world/src/{tests → intents/design_editor/tests}/app.tests.tsx +19 -13
  46. package/templates/hello_world/styles/components.css +18 -0
  47. package/templates/optional/AGENTS.md +80 -2
  48. package/templates/optional/CLAUDE.md +80 -2
  49. package/templates/base/utils/use_add_element.ts +0 -58
  50. package/templates/base/utils/use_feature_support.ts +0 -28
  51. package/templates/common/utils/table_wrapper.ts +0 -520
  52. package/templates/common/utils/use_add_element.ts +0 -58
  53. package/templates/common/utils/use_feature_support.ts +0 -28
  54. package/templates/common/utils/use_overlay_hook.ts +0 -76
  55. package/templates/common/utils/use_selection_hook.ts +0 -37
  56. package/templates/hello_world/utils/use_add_element.ts +0 -58
  57. package/templates/hello_world/utils/use_feature_support.ts +0 -28
  58. /package/templates/dam/src/{adapter.ts → intents/design_editor/adapter.ts} +0 -0
  59. /package/templates/dam/src/{app.tsx → intents/design_editor/app.tsx} +0 -0
  60. /package/templates/dam/src/{config.ts → intents/design_editor/config.ts} +0 -0
  61. /package/templates/dam/src/{index.css → intents/design_editor/index.css} +0 -0
  62. /package/templates/data_connector/src/{paths.ts → routes/paths.ts} +0 -0
  63. /package/templates/gen_ai/src/{paths.ts → routes/paths.ts} +0 -0
  64. /package/templates/hello_world/src/{tests → intents/design_editor/tests}/__snapshots__/app.tests.tsx.snap +0 -0
@@ -7,7 +7,7 @@ import {
7
7
  Title,
8
8
  } from "@canva/app-ui-kit";
9
9
  import { useNavigate } from "react-router-dom";
10
- import { Paths } from "src/paths";
10
+ import { Paths } from "src/routes/paths";
11
11
 
12
12
  export const Header = ({
13
13
  title,
@@ -1,68 +1,4 @@
1
- import { Alert, AppUiProvider } from "@canva/app-ui-kit";
2
- import type {
3
- GetDataTableRequest,
4
- GetDataTableResponse,
5
- RenderSelectionUiRequest,
6
- } from "@canva/intents/data";
7
1
  import { prepareDataConnector } from "@canva/intents/data";
8
- import { auth } from "@canva/user";
9
- import { createRoot } from "react-dom/client";
10
- import { buildDataTableResult, scope } from "./api";
11
- import { App } from "./app";
12
- import "@canva/app-ui-kit/styles.css";
2
+ import dataConnector from "./intents/data_connector";
13
3
 
14
- const root = createRoot(document.getElementById("root") as Element);
15
- prepareDataConnector({
16
- /**
17
- * Fetches structured data from an external source.
18
- *
19
- * This action is called in two scenarios:
20
- *
21
- * - During data selection to preview data before import (when {@link RenderSelectionUiRequest.updateDataRef} is called).
22
- * - When refreshing previously imported data (when the user requests an update).
23
- *
24
- * @param params - Parameters for the data fetching operation.
25
- * @returns A promise resolving to either a successful result with data or an error.
26
- */
27
- getDataTable: async (
28
- params: GetDataTableRequest,
29
- ): Promise<GetDataTableResponse> => {
30
- const oauth = auth.initOauth();
31
- const token = await oauth.getAccessToken({ scope });
32
- return buildDataTableResult(params, token?.token);
33
- },
34
-
35
- /**
36
- * Renders a UI component for selecting and configuring data from external sources.
37
- * This UI should allow users to browse data sources, apply filters, and select data.
38
- * When selection is complete, the implementation must call the `updateDataRef`
39
- * callback provided in the params to preview and confirm the data selection.
40
- *
41
- * @param request - parameters that provide context and configuration for the data selection UI.
42
- * Contains invocation context, size limits, and the updateDataRef callback
43
- */
44
- renderSelectionUi: async (request: RenderSelectionUiRequest) => {
45
- function render() {
46
- root.render(<App request={request} />);
47
- }
48
-
49
- render();
50
-
51
- if (module.hot) {
52
- module.hot.accept("./app", render);
53
- module.hot.accept("./api", render);
54
- }
55
- },
56
- });
57
-
58
- // TODO: Fallback message if you have not turned on the data connector intent.
59
- // You can remove this once your app is correctly configured.
60
- root.render(
61
- <AppUiProvider>
62
- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
63
- <Alert tone="critical">
64
- If you're seeing this, you need to turn on the data connector intent in
65
- the Developer Portal for this app.
66
- </Alert>
67
- </AppUiProvider>,
68
- );
4
+ prepareDataConnector(dataConnector);
@@ -3,9 +3,9 @@ import { AppUiProvider } from "@canva/app-ui-kit";
3
3
  import type { RenderSelectionUiRequest } from "@canva/intents/data";
4
4
  import { ErrorBoundary } from "react-error-boundary";
5
5
  import { createHashRouter, RouterProvider } from "react-router-dom";
6
- import { ContextProvider } from "./context";
7
- import { ErrorPage } from "./pages";
8
- import { routes } from "./routes";
6
+ import { ContextProvider } from "../../context";
7
+ import { ErrorPage } from "../../pages";
8
+ import { routes } from "../../routes";
9
9
 
10
10
  export const App = ({ request }: { request: RenderSelectionUiRequest }) => (
11
11
  <AppI18nProvider>
@@ -5,15 +5,15 @@ import type {
5
5
  APIResponseItem,
6
6
  DataSourceConfig,
7
7
  DataSourceHandler,
8
- } from "./api";
9
- import { DATA_SOURCES } from "./api/data_sources";
10
- import { useAppContext } from "./context";
11
- import { Paths } from "./paths";
8
+ } from "../../api";
9
+ import { DATA_SOURCES } from "../../api/data_sources";
10
+ import { useAppContext } from "../../context";
11
+ import { Paths } from "../../routes/paths";
12
12
  import {
13
13
  isDataRefEmpty,
14
14
  isLaunchedWithError,
15
15
  isOutdatedSource,
16
- } from "./utils/data_params";
16
+ } from "../../utils/data_params";
17
17
 
18
18
  const parseDataSource = (source: string) => {
19
19
  try {
@@ -1,7 +1,7 @@
1
1
  import { Box, Rows } from "@canva/app-ui-kit";
2
2
  import { Outlet } from "react-router-dom";
3
3
  import * as styles from "styles/components.css";
4
- import { AppError } from "./components";
4
+ import { AppError } from "../../components";
5
5
 
6
6
  export const Home = () => (
7
7
  <div className={styles.scrollContainer}>
@@ -0,0 +1,56 @@
1
+ import "@canva/app-ui-kit/styles.css";
2
+ import type {
3
+ DataConnectorIntent,
4
+ GetDataTableRequest,
5
+ GetDataTableResponse,
6
+ RenderSelectionUiRequest,
7
+ } from "@canva/intents/data";
8
+ import { auth } from "@canva/user";
9
+ import { createRoot } from "react-dom/client";
10
+ import { buildDataTableResult, scope } from "../../api";
11
+ import { App } from "./app";
12
+
13
+ const dataConnector: DataConnectorIntent = {
14
+ /**
15
+ * Fetches structured data from an external source.
16
+ *
17
+ * This action is called in two scenarios:
18
+ *
19
+ * - During data selection to preview data before import (when {@link RenderSelectionUiRequest.updateDataRef} is called).
20
+ * - When refreshing previously imported data (when the user requests an update).
21
+ *
22
+ * @param params - Parameters for the data fetching operation.
23
+ * @returns A promise resolving to either a successful result with data or an error.
24
+ */
25
+ getDataTable: async (
26
+ params: GetDataTableRequest,
27
+ ): Promise<GetDataTableResponse> => {
28
+ const oauth = auth.initOauth();
29
+ const token = await oauth.getAccessToken({ scope });
30
+ return buildDataTableResult(params, token?.token);
31
+ },
32
+
33
+ /**
34
+ * Renders a UI component for selecting and configuring data from external sources.
35
+ * This UI should allow users to browse data sources, apply filters, and select data.
36
+ * When selection is complete, the implementation must call the `updateDataRef`
37
+ * callback provided in the params to preview and confirm the data selection.
38
+ *
39
+ * @param request - parameters that provide context and configuration for the data selection UI.
40
+ * Contains invocation context, size limits, and the updateDataRef callback
41
+ */
42
+ renderSelectionUi: async (request: RenderSelectionUiRequest) => {
43
+ function render() {
44
+ const root = createRoot(document.getElementById("root") as Element);
45
+ root.render(<App request={request} />);
46
+ }
47
+
48
+ render();
49
+
50
+ if (module.hot) {
51
+ module.hot.accept("./app", render);
52
+ module.hot.accept("../../api", render);
53
+ }
54
+ },
55
+ };
56
+ export default dataConnector;
@@ -1,7 +1,7 @@
1
1
  import { Button, Rows, Text } from "@canva/app-ui-kit";
2
2
  import { FormattedMessage, useIntl } from "react-intl";
3
3
  import { useNavigate } from "react-router-dom";
4
- import { Paths } from "src/paths";
4
+ import { Paths } from "src/routes/paths";
5
5
  import * as styles from "styles/components.css";
6
6
 
7
7
  /**
@@ -11,7 +11,7 @@ import { defineMessages, FormattedMessage, useIntl } from "react-intl";
11
11
  import { useNavigate } from "react-router-dom";
12
12
  import { scope } from "src/api";
13
13
  import { Header } from "src/components";
14
- import { Paths } from "src/paths";
14
+ import { Paths } from "src/routes/paths";
15
15
  import * as styles from "styles/components.css";
16
16
  import { useAppContext } from "../context";
17
17
 
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { useEffect } from "react";
3
3
  import { useNavigate } from "react-router-dom";
4
- import { Paths } from "src/paths";
4
+ import { Paths } from "src/routes/paths";
5
5
  import { useAppContext } from "../context";
6
6
 
7
7
  interface ProtectedRouteProps {
@@ -1,10 +1,10 @@
1
- import { Entrypoint } from "src/entrypoint";
2
- import { Home } from "src/home";
1
+ import { Entrypoint } from "src/intents/data_connector/entrypoint";
2
+ import { Home } from "src/intents/data_connector/home";
3
3
  import { DataSourceConfig } from "src/pages/data_source_config";
4
4
  import { ErrorPage } from "src/pages/error";
5
5
  import { Login } from "src/pages/login";
6
6
  import { SelectSource } from "src/pages/select_source";
7
- import { Paths } from "src/paths";
7
+ import { Paths } from "src/routes/paths";
8
8
  import { ProtectedRoute } from "./protected_route";
9
9
 
10
10
  export const routes = [
@@ -36,3 +36,21 @@
36
36
  .scrollContainer:focus-within::-webkit-scrollbar-thumb {
37
37
  visibility: visible;
38
38
  }
39
+
40
+ /* Main container for the content publisher preview UI */
41
+ .previewContainer {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ flex-direction: column;
46
+ width: 100%;
47
+ height: 100%;
48
+ }
49
+
50
+ /* Wrapper for the content publisher post preview */
51
+ .previewWrapper {
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ width: calc(400px + 32px + 2px); /* Image width + padding + border */
56
+ }
@@ -16,5 +16,10 @@
16
16
  "type": "mandatory"
17
17
  }
18
18
  ]
19
+ },
20
+ "intent": {
21
+ "design_editor": {
22
+ "enrolled": true
23
+ }
19
24
  }
20
25
  }
@@ -18,11 +18,13 @@
18
18
  "postinstall": "ts-node ./scripts/copy_env.ts"
19
19
  },
20
20
  "dependencies": {
21
+ "@canva/app-hooks": "^0.0.0-beta.4",
21
22
  "@canva/app-i18n-kit": "^1.1.1",
22
23
  "@canva/app-ui-kit": "^5.2.1",
23
24
  "@canva/asset": "^2.2.1",
24
25
  "@canva/design": "^2.7.3",
25
26
  "@canva/error": "^2.1.0",
27
+ "@canva/intents": "^2.0.0",
26
28
  "@canva/platform": "^2.2.0",
27
29
  "@canva/user": "^2.1.1",
28
30
  "cookie-parser": "1.4.7",
@@ -66,7 +68,7 @@
66
68
  "debug": "4.4.1",
67
69
  "dotenv": "16.6.0",
68
70
  "exponential-backoff": "3.1.2",
69
- "express": "4.21.2",
71
+ "express": "4.22.1",
70
72
  "express-basic-auth": "1.2.1",
71
73
  "jest": "29.7.0",
72
74
  "jest-css-modules-transform": "4.4.2",
@@ -75,7 +77,7 @@
75
77
  "jwks-rsa": "3.2.0",
76
78
  "mini-css-extract-plugin": "2.9.4",
77
79
  "node-fetch": "3.3.2",
78
- "node-forge": "1.3.1",
80
+ "node-forge": "1.3.2",
79
81
  "nodemon": "3.0.1",
80
82
  "open": "8.4.2",
81
83
  "postcss-loader": "8.1.1",
@@ -6,7 +6,7 @@ import { purchaseCredits, queueImageGeneration } from "src/api";
6
6
  import { RemainingCredits } from "src/components";
7
7
  import { NUMBER_OF_IMAGES_TO_GENERATE } from "src/config";
8
8
  import { useAppContext } from "src/context";
9
- import { Paths } from "src/paths";
9
+ import { Paths } from "src/routes/paths";
10
10
  import { getObsceneWords } from "src/utils";
11
11
  import { FooterMessages as Messages } from "./footer.messages";
12
12
 
@@ -12,7 +12,7 @@ import { FormattedMessage, useIntl } from "react-intl";
12
12
  import { useNavigate } from "react-router-dom";
13
13
  import { cancelImageGenerationJob, getImageGenerationJobStatus } from "src/api";
14
14
  import { useAppContext } from "src/context";
15
- import { Paths } from "src/paths";
15
+ import { Paths } from "src/routes/paths";
16
16
 
17
17
  const INTERVAL_DURATION_IN_MS = 100;
18
18
  const TOTAL_PROGRESS_PERCENTAGE = 100;
@@ -9,7 +9,7 @@ import { useState } from "react";
9
9
  import { useIntl } from "react-intl";
10
10
  import { useLocation } from "react-router-dom";
11
11
  import { useAppContext } from "src/context";
12
- import { Paths } from "src/paths";
12
+ import { Paths } from "src/routes/paths";
13
13
  import { PromptInputMessages as Messages } from "./prompt_input.messages";
14
14
 
15
15
  // @TODO: Adjust according to your specific requirements.
@@ -1,15 +1,4 @@
1
- import { createRoot } from "react-dom/client";
2
- import "@canva/app-ui-kit/styles.css";
3
- import { App } from "./app";
1
+ import { prepareDesignEditor } from "@canva/intents/design";
2
+ import designEditor from "./intents/design_editor";
4
3
 
5
- const root = createRoot(document.getElementById("root") as Element);
6
-
7
- function render() {
8
- root.render(<App />);
9
- }
10
-
11
- render();
12
-
13
- if (module.hot) {
14
- module.hot.accept("./app", render);
15
- }
4
+ prepareDesignEditor(designEditor);
@@ -2,9 +2,9 @@ import { AppI18nProvider } from "@canva/app-i18n-kit";
2
2
  import { AppUiProvider } from "@canva/app-ui-kit";
3
3
  import { ErrorBoundary } from "react-error-boundary";
4
4
  import { createHashRouter, RouterProvider } from "react-router-dom";
5
- import { ContextProvider } from "./context";
6
- import { ErrorPage } from "./pages";
7
- import { routes } from "./routes";
5
+ import { ContextProvider } from "../../context";
6
+ import { ErrorPage } from "../../pages";
7
+ import { routes } from "../../routes";
8
8
 
9
9
  export const App = () => (
10
10
  <AppI18nProvider>
@@ -1,7 +1,7 @@
1
1
  import { Rows } from "@canva/app-ui-kit";
2
2
  import { Outlet } from "react-router-dom";
3
3
  import * as styles from "styles/components.css";
4
- import { Footer } from "./components";
4
+ import { Footer } from "../../components";
5
5
 
6
6
  export const Home = () => (
7
7
  <div className={styles.scrollContainer}>
@@ -0,0 +1,17 @@
1
+ import "@canva/app-ui-kit/styles.css";
2
+ import type { DesignEditorIntent } from "@canva/intents/design";
3
+ import { createRoot } from "react-dom/client";
4
+ import { App } from "./app";
5
+
6
+ async function render() {
7
+ const root = createRoot(document.getElementById("root") as Element);
8
+
9
+ root.render(<App />);
10
+ }
11
+
12
+ const designEditor: DesignEditorIntent = { render };
13
+ export default designEditor;
14
+
15
+ if (module.hot) {
16
+ module.hot.accept("./app", render);
17
+ }
@@ -2,7 +2,7 @@ import { Button, Rows, Text } from "@canva/app-ui-kit";
2
2
  import { FormattedMessage, useIntl } from "react-intl";
3
3
  import { useNavigate } from "react-router-dom";
4
4
  import { useAppContext } from "src/context";
5
- import { Paths } from "src/paths";
5
+ import { Paths } from "src/routes/paths";
6
6
  import * as styles from "styles/components.css";
7
7
 
8
8
  /**
@@ -1,8 +1,8 @@
1
- import { Home } from "src/home";
1
+ import { Home } from "src/intents/design_editor/home";
2
2
  import { ErrorPage } from "src/pages/error";
3
3
  import { GeneratePage } from "src/pages/generate";
4
4
  import { ResultsPage } from "src/pages/results";
5
- import { Paths } from "src/paths";
5
+ import { Paths } from "src/routes/paths";
6
6
 
7
7
  export const routes = [
8
8
  {
@@ -36,3 +36,21 @@
36
36
  .scrollContainer:focus-within::-webkit-scrollbar-thumb {
37
37
  visibility: visible;
38
38
  }
39
+
40
+ /* Main container for the content publisher preview UI */
41
+ .previewContainer {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ flex-direction: column;
46
+ width: 100%;
47
+ height: 100%;
48
+ }
49
+
50
+ /* Wrapper for the content publisher post preview */
51
+ .previewWrapper {
52
+ display: flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ width: calc(400px + 32px + 2px); /* Image width + padding + border */
56
+ }
@@ -12,5 +12,10 @@
12
12
  "type": "mandatory"
13
13
  }
14
14
  ]
15
+ },
16
+ "intent": {
17
+ "design_editor": {
18
+ "enrolled": true
19
+ }
15
20
  }
16
21
  }
@@ -19,11 +19,13 @@
19
19
  "postinstall": "ts-node ./scripts/copy_env.ts"
20
20
  },
21
21
  "dependencies": {
22
+ "@canva/app-hooks": "^0.0.0-beta.4",
22
23
  "@canva/app-i18n-kit": "^1.1.1",
23
24
  "@canva/app-ui-kit": "^5.2.1",
24
25
  "@canva/asset": "^2.2.1",
25
26
  "@canva/design": "^2.7.3",
26
27
  "@canva/error": "^2.1.0",
28
+ "@canva/intents": "^2.0.0",
27
29
  "@canva/platform": "^2.2.0",
28
30
  "@canva/user": "^2.1.1",
29
31
  "react": "^19.2.0",
@@ -57,7 +59,7 @@
57
59
  "cssnano": "7.1.1",
58
60
  "debug": "4.4.1",
59
61
  "dotenv": "16.6.0",
60
- "express": "4.21.2",
62
+ "express": "4.22.1",
61
63
  "express-basic-auth": "1.2.1",
62
64
  "jest": "29.7.0",
63
65
  "jest-css-modules-transform": "4.4.2",
@@ -66,7 +68,7 @@
66
68
  "jwks-rsa": "3.2.0",
67
69
  "mini-css-extract-plugin": "2.9.4",
68
70
  "node-fetch": "3.3.2",
69
- "node-forge": "1.3.1",
71
+ "node-forge": "1.3.2",
70
72
  "nodemon": "3.0.1",
71
73
  "open": "8.4.2",
72
74
  "postcss-loader": "8.1.1",
@@ -1,22 +1,4 @@
1
- import { AppI18nProvider } from "@canva/app-i18n-kit";
2
- import { AppUiProvider } from "@canva/app-ui-kit";
3
- import { createRoot } from "react-dom/client";
4
- import { App } from "./app";
5
- import "@canva/app-ui-kit/styles.css";
1
+ import { prepareDesignEditor } from "@canva/intents/design";
2
+ import designEditor from "./intents/design_editor";
6
3
 
7
- const root = createRoot(document.getElementById("root") as Element);
8
- function render() {
9
- root.render(
10
- <AppI18nProvider>
11
- <AppUiProvider>
12
- <App />
13
- </AppUiProvider>
14
- </AppI18nProvider>,
15
- );
16
- }
17
-
18
- render();
19
-
20
- if (module.hot) {
21
- module.hot.accept("./app", render);
22
- }
4
+ prepareDesignEditor(designEditor);
@@ -1,15 +1,23 @@
1
+ import { useFeatureSupport } from "@canva/app-hooks";
1
2
  import { Button, Rows, Text } from "@canva/app-ui-kit";
3
+ import { addElementAtCursor, addElementAtPoint } from "@canva/design";
2
4
  import { requestOpenExternalUrl } from "@canva/platform";
3
5
  import { FormattedMessage, useIntl } from "react-intl";
4
- import { useAddElement } from "utils/use_add_element";
5
6
  import * as styles from "styles/components.css";
6
7
 
7
8
  export const DOCS_URL = "https://www.canva.dev/docs/apps/";
8
9
 
9
10
  export const App = () => {
10
- const addElement = useAddElement();
11
+ const isSupported = useFeatureSupport();
12
+ const addElement = [addElementAtPoint, addElementAtCursor].find((fn) =>
13
+ isSupported(fn),
14
+ );
11
15
 
12
16
  const onClick = () => {
17
+ if (!addElement) {
18
+ return;
19
+ }
20
+
13
21
  addElement({
14
22
  type: "text",
15
23
  children: ["Hello world!"],
@@ -43,7 +51,22 @@ export const App = () => {
43
51
  }}
44
52
  />
45
53
  </Text>
46
- <Button variant="primary" onClick={onClick} stretch>
54
+ <Button
55
+ variant="primary"
56
+ onClick={onClick}
57
+ disabled={!addElement}
58
+ tooltipLabel={
59
+ !addElement
60
+ ? intl.formatMessage({
61
+ defaultMessage:
62
+ "This feature is not supported in the current page",
63
+ description:
64
+ "Tooltip label for when a feature is not supported in the current design",
65
+ })
66
+ : undefined
67
+ }
68
+ stretch
69
+ >
47
70
  {intl.formatMessage({
48
71
  defaultMessage: "Do something cool",
49
72
  description:
@@ -0,0 +1,25 @@
1
+ import "@canva/app-ui-kit/styles.css";
2
+ import { AppI18nProvider } from "@canva/app-i18n-kit";
3
+ import { AppUiProvider } from "@canva/app-ui-kit";
4
+ import type { DesignEditorIntent } from "@canva/intents/design";
5
+ import { createRoot } from "react-dom/client";
6
+ import { App } from "./app";
7
+
8
+ async function render() {
9
+ const root = createRoot(document.getElementById("root") as Element);
10
+
11
+ root.render(
12
+ <AppI18nProvider>
13
+ <AppUiProvider>
14
+ <App />
15
+ </AppUiProvider>
16
+ </AppI18nProvider>,
17
+ );
18
+ }
19
+
20
+ const designEditor: DesignEditorIntent = { render };
21
+ export default designEditor;
22
+
23
+ if (module.hot) {
24
+ module.hot.accept("./app", render);
25
+ }
@@ -1,11 +1,13 @@
1
1
  /* eslint-disable formatjs/no-literal-string-in-jsx */
2
+ import { useFeatureSupport } from "@canva/app-hooks";
2
3
  import { TestAppI18nProvider } from "@canva/app-i18n-kit";
3
4
  import { TestAppUiProvider } from "@canva/app-ui-kit";
5
+ import { addElementAtCursor, addElementAtPoint } from "@canva/design";
6
+ import type { Feature } from "@canva/platform";
4
7
  import { requestOpenExternalUrl } from "@canva/platform";
5
8
  import { fireEvent, render } from "@testing-library/react";
6
9
  import type { RenderResult } from "@testing-library/react";
7
10
  import type { ReactNode } from "react";
8
- import { useAddElement } from "utils/use_add_element";
9
11
  import { App, DOCS_URL } from "../app";
10
12
 
11
13
  function renderInTestProvider(node: ReactNode): RenderResult {
@@ -17,33 +19,35 @@ function renderInTestProvider(node: ReactNode): RenderResult {
17
19
  );
18
20
  }
19
21
 
20
- jest.mock("utils/use_add_element");
22
+ jest.mock("@canva/app-hooks");
21
23
 
22
24
  // This test demonstrates how to test code that uses functions from the Canva Apps SDK
23
25
  // For more information on testing with the Canva Apps SDK, see https://www.canva.dev/docs/apps/testing/
24
26
  describe("Hello World Tests", () => {
25
- // Mocking the useAddElement hook
26
- const mockAddElement = jest.fn();
27
- const mockAddUseElement = jest.mocked(useAddElement);
27
+ const mockIsSupported = jest.fn();
28
+ const mockUseFeatureSupport = jest.mocked(useFeatureSupport);
28
29
  const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);
29
30
 
30
31
  beforeEach(() => {
31
32
  jest.resetAllMocks();
32
- mockAddUseElement.mockReturnValue(mockAddElement);
33
+ mockIsSupported.mockImplementation(
34
+ (fn: Feature) => fn === addElementAtPoint,
35
+ );
36
+ mockUseFeatureSupport.mockReturnValue(mockIsSupported);
33
37
  mockRequestOpenExternalUrl.mockResolvedValue({ status: "completed" });
34
38
  });
35
39
 
36
- // this test uses a mock in place of the useAddElement hook
40
+ // this test uses a mock in place of the useFeatureSupport hook
37
41
  it("should add a text element when the button is clicked", () => {
38
42
  // assert that the mocks are in the expected clean state
39
- expect(mockAddUseElement).not.toHaveBeenCalled();
40
- expect(mockAddElement).not.toHaveBeenCalled();
43
+ expect(mockUseFeatureSupport).not.toHaveBeenCalled();
44
+ expect(addElementAtPoint).not.toHaveBeenCalled();
41
45
 
42
46
  const result = renderInTestProvider(<App />);
43
47
 
44
48
  // the hook should have been called in the render process but not the callback
45
- expect(mockAddUseElement).toHaveBeenCalled();
46
- expect(mockAddElement).not.toHaveBeenCalled();
49
+ expect(mockUseFeatureSupport).toHaveBeenCalled();
50
+ expect(addElementAtPoint).not.toHaveBeenCalled();
47
51
 
48
52
  // get a reference to the do something cool button element
49
53
  const doSomethingCoolBtn = result.getByRole("button", {
@@ -53,8 +57,10 @@ describe("Hello World Tests", () => {
53
57
  // programmatically simulate clicking the button
54
58
  fireEvent.click(doSomethingCoolBtn);
55
59
 
56
- // we expect that addElement has been called by the button's click handler
57
- expect(mockAddElement).toHaveBeenCalled();
60
+ // we expect that addElementAtPoint has been called by the button's click handler
61
+ expect(mockIsSupported).toHaveBeenCalledWith(addElementAtPoint);
62
+ expect(mockIsSupported).not.toHaveBeenCalledWith(addElementAtCursor);
63
+ expect(addElementAtPoint).toHaveBeenCalled();
58
64
  });
59
65
 
60
66
  // this test uses a mock in place of the @canva/platform requestOpenExternalUrl function