@canva/cli 1.9.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 (88) hide show
  1. package/CHANGELOG.md +23 -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 -3
  8. package/templates/base/styles/components.css +18 -0
  9. package/templates/base/tsconfig.json +5 -3
  10. package/templates/base/utils/backend/base_backend/create.ts +3 -3
  11. package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +2 -4
  12. package/templates/base/utils/backend/jwt_middleware/jwt_middleware.ts +2 -4
  13. package/templates/base/webpack.config.ts +1 -1
  14. package/templates/common/.env.template +1 -1
  15. package/templates/common/utils/backend/base_backend/create.ts +3 -3
  16. package/templates/common/utils/backend/jwt_middleware/jwt_middleware.ts +2 -4
  17. package/templates/dam/backend/routers/dam.ts +35 -17
  18. package/templates/dam/canva-app.json +5 -0
  19. package/templates/dam/package.json +4 -3
  20. package/templates/dam/src/index.tsx +3 -21
  21. package/templates/dam/src/intents/design_editor/index.tsx +25 -0
  22. package/templates/dam/tsconfig.json +5 -3
  23. package/templates/dam/utils/backend/base_backend/create.ts +3 -3
  24. package/templates/dam/utils/backend/jwt_middleware/jwt_middleware.ts +2 -4
  25. package/templates/dam/webpack.config.ts +1 -1
  26. package/templates/data_connector/package.json +4 -3
  27. package/templates/data_connector/src/api/data_sources/designs.tsx +1 -1
  28. package/templates/data_connector/src/api/data_sources/templates.tsx +1 -1
  29. package/templates/data_connector/src/components/header.tsx +1 -1
  30. package/templates/data_connector/src/index.tsx +2 -66
  31. package/templates/data_connector/src/{app.tsx → intents/data_connector/app.tsx} +3 -3
  32. package/templates/data_connector/src/{entrypoint.tsx → intents/data_connector/entrypoint.tsx} +5 -5
  33. package/templates/data_connector/src/{home.tsx → intents/data_connector/home.tsx} +1 -1
  34. package/templates/data_connector/src/intents/data_connector/index.tsx +56 -0
  35. package/templates/data_connector/src/pages/error.tsx +1 -1
  36. package/templates/data_connector/src/pages/login.tsx +1 -1
  37. package/templates/data_connector/src/routes/protected_route.tsx +1 -1
  38. package/templates/data_connector/src/routes/routes.tsx +3 -3
  39. package/templates/data_connector/src/utils/data_table.ts +2 -1
  40. package/templates/data_connector/src/utils/tests/data_table.test.ts +2 -2
  41. package/templates/data_connector/styles/components.css +18 -0
  42. package/templates/data_connector/tsconfig.json +5 -3
  43. package/templates/data_connector/webpack.config.ts +1 -1
  44. package/templates/gen_ai/backend/routers/image.ts +4 -6
  45. package/templates/gen_ai/canva-app.json +5 -0
  46. package/templates/gen_ai/package.json +5 -3
  47. package/templates/gen_ai/src/components/footer.tsx +1 -1
  48. package/templates/gen_ai/src/components/image_grid.tsx +6 -2
  49. package/templates/gen_ai/src/components/loading_results.tsx +1 -1
  50. package/templates/gen_ai/src/components/prompt_input.tsx +5 -2
  51. package/templates/gen_ai/src/index.tsx +3 -14
  52. package/templates/gen_ai/src/{app.tsx → intents/design_editor/app.tsx} +3 -3
  53. package/templates/gen_ai/src/{home.tsx → intents/design_editor/home.tsx} +1 -1
  54. package/templates/gen_ai/src/intents/design_editor/index.tsx +17 -0
  55. package/templates/gen_ai/src/pages/error.tsx +1 -1
  56. package/templates/gen_ai/src/routes/routes.tsx +2 -2
  57. package/templates/gen_ai/styles/components.css +18 -0
  58. package/templates/gen_ai/tsconfig.json +5 -3
  59. package/templates/gen_ai/utils/backend/base_backend/create.ts +3 -3
  60. package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +2 -4
  61. package/templates/gen_ai/webpack.config.ts +1 -1
  62. package/templates/hello_world/canva-app.json +5 -0
  63. package/templates/hello_world/package.json +5 -3
  64. package/templates/hello_world/src/index.tsx +3 -21
  65. package/templates/hello_world/src/{app.tsx → intents/design_editor/app.tsx} +26 -3
  66. package/templates/hello_world/src/intents/design_editor/index.tsx +25 -0
  67. package/templates/hello_world/src/{tests → intents/design_editor/tests}/app.tests.tsx +20 -14
  68. package/templates/hello_world/styles/components.css +18 -0
  69. package/templates/hello_world/tsconfig.json +5 -3
  70. package/templates/hello_world/webpack.config.ts +1 -1
  71. package/templates/optional/AGENTS.md +80 -2
  72. package/templates/optional/CLAUDE.md +80 -2
  73. package/templates/base/utils/use_add_element.ts +0 -48
  74. package/templates/base/utils/use_feature_support.ts +0 -28
  75. package/templates/common/utils/table_wrapper.ts +0 -477
  76. package/templates/common/utils/use_add_element.ts +0 -48
  77. package/templates/common/utils/use_feature_support.ts +0 -28
  78. package/templates/common/utils/use_overlay_hook.ts +0 -74
  79. package/templates/common/utils/use_selection_hook.ts +0 -37
  80. package/templates/hello_world/utils/use_add_element.ts +0 -48
  81. package/templates/hello_world/utils/use_feature_support.ts +0 -28
  82. /package/templates/dam/src/{adapter.ts → intents/design_editor/adapter.ts} +0 -0
  83. /package/templates/dam/src/{app.tsx → intents/design_editor/app.tsx} +0 -0
  84. /package/templates/dam/src/{config.ts → intents/design_editor/config.ts} +0 -0
  85. /package/templates/dam/src/{index.css → intents/design_editor/index.css} +0 -0
  86. /package/templates/data_connector/src/{paths.ts → routes/paths.ts} +0 -0
  87. /package/templates/gen_ai/src/{paths.ts → routes/paths.ts} +0 -0
  88. /package/templates/hello_world/src/{tests → intents/design_editor/tests}/__snapshots__/app.tests.tsx.snap +0 -0
@@ -28,7 +28,10 @@
28
28
  "sourceMap": true,
29
29
  "inlineSources": true,
30
30
  "module": "ESNext",
31
- "noImplicitAny": false,
31
+ "noImplicitAny": true,
32
+ "noImplicitReturns": true,
33
+ "noFallthroughCasesInSwitch": true,
34
+ "noUncheckedIndexedAccess": true,
32
35
  "removeComments": true,
33
36
  "preserveConstEnums": true,
34
37
  "allowSyntheticDefaultImports": true,
@@ -44,8 +47,7 @@
44
47
  "./utils/**/*",
45
48
  "./scripts/**/*",
46
49
  "./declarations/declarations.d.ts",
47
- "./styles/**/*",
48
- "./node_modules/@types/**/*"
50
+ "./styles/**/*"
49
51
  ],
50
52
  "ts-node": {
51
53
  "compilerOptions": {
@@ -196,7 +196,7 @@ function buildDevConfig(options?: DevConfig): {
196
196
  return {};
197
197
  }
198
198
 
199
- const { port, enableHmr, appOrigin, appId, enableHttps, certFile, keyFile } =
199
+ const { port, enableHmr, appOrigin, enableHttps, certFile, keyFile } =
200
200
  options;
201
201
  const host = "localhost";
202
202
 
@@ -151,7 +151,7 @@ export const createImageRouter = () => {
151
151
  // Add the job to the jobQueue along with the timeoutId
152
152
  jobQueue.push({ jobId, prompt, timeoutId });
153
153
 
154
- res.status(200).send({
154
+ return res.status(200).send({
155
155
  jobId,
156
156
  });
157
157
  });
@@ -208,15 +208,13 @@ export const createImageRouter = () => {
208
208
  }
209
209
 
210
210
  const index = jobQueue.findIndex((job) => job.jobId === jobId);
211
- if (index !== -1) {
211
+ const job = jobQueue[index];
212
+ if (index !== -1 && job) {
212
213
  cancelledJobs.push({ jobId });
213
214
  // If the job is found, remove it from the jobQueue
214
- const { timeoutId } = jobQueue[index];
215
215
  jobQueue.splice(index, 1);
216
216
  // Also clear the timeout associated with this job if it exists
217
- if (timeoutId) {
218
- clearTimeout(timeoutId);
219
- }
217
+ clearTimeout(job.timeoutId);
220
218
  return res.status(200).send("Job successfully cancelled.");
221
219
  }
222
220
 
@@ -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",
@@ -44,9 +46,9 @@
44
46
  "@pmmmwh/react-refresh-webpack-plugin": "0.6.1",
45
47
  "@svgr/webpack": "8.1.0",
46
48
  "@testing-library/react": "16.3.0",
49
+ "@types/cors": "2.8.19",
47
50
  "@types/debug": "4.1.12",
48
51
  "@types/express": "4.17.21",
49
- "@types/express-serve-static-core": "5.0.7",
50
52
  "@types/jest": "29.5.14",
51
53
  "@types/jsonwebtoken": "9.0.10",
52
54
  "@types/node": "20.19.2",
@@ -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
 
@@ -34,7 +34,9 @@ export const ImageGrid = () => {
34
34
  ) => {
35
35
  const parentNode = event.currentTarget.parentElement;
36
36
  try {
37
- parentNode?.classList.add(styles.hidden);
37
+ if (styles.hidden) {
38
+ parentNode?.classList.add(styles.hidden);
39
+ }
38
40
 
39
41
  await ui.startDragToPoint(event, {
40
42
  type: "image",
@@ -50,7 +52,9 @@ export const ImageGrid = () => {
50
52
  },
51
53
  });
52
54
  } finally {
53
- parentNode?.classList.remove(styles.hidden);
55
+ if (styles.hidden) {
56
+ parentNode?.classList.remove(styles.hidden);
57
+ }
54
58
  }
55
59
  };
56
60
 
@@ -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.
@@ -59,8 +59,11 @@ const generateExamplePrompt = (currentPrompt: string): string => {
59
59
  const MAX_ATTEMPTS = 3;
60
60
 
61
61
  while (currentPrompt === newPrompt && attempts < MAX_ATTEMPTS) {
62
- newPrompt =
62
+ const randomPrompt =
63
63
  examplePrompts[Math.floor(Math.random() * examplePrompts.length)];
64
+ if (randomPrompt) {
65
+ newPrompt = randomPrompt;
66
+ }
64
67
  attempts++;
65
68
  }
66
69
 
@@ -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
+ }
@@ -28,7 +28,10 @@
28
28
  "sourceMap": true,
29
29
  "inlineSources": true,
30
30
  "module": "ESNext",
31
- "noImplicitAny": false,
31
+ "noImplicitAny": true,
32
+ "noImplicitReturns": true,
33
+ "noFallthroughCasesInSwitch": true,
34
+ "noUncheckedIndexedAccess": true,
32
35
  "removeComments": true,
33
36
  "preserveConstEnums": true,
34
37
  "allowSyntheticDefaultImports": true,
@@ -44,8 +47,7 @@
44
47
  "./utils/**/*",
45
48
  "./scripts/**/*",
46
49
  "./declarations/declarations.d.ts",
47
- "./styles/**/*",
48
- "./node_modules/@types/**/*"
50
+ "./styles/**/*"
49
51
  ],
50
52
  "ts-node": {
51
53
  "compilerOptions": {
@@ -41,12 +41,12 @@ export function createBaseServer(router: express.Router): BaseServer {
41
41
  app.disable("x-powered-by");
42
42
 
43
43
  // Health check endpoint
44
- app.get("/healthz", (req, res: Response) => {
44
+ app.get("/healthz", (req, res) => {
45
45
  res.sendStatus(200);
46
46
  });
47
47
 
48
48
  // logging middleware
49
- app.use((req: Request, res: Response, next: NextFunction) => {
49
+ app.use((req, _res, next) => {
50
50
  serverDebug(`${new Date().toISOString()}: ${req.method} ${req.url}`);
51
51
  next();
52
52
  });
@@ -62,7 +62,7 @@ export function createBaseServer(router: express.Router): BaseServer {
62
62
  });
63
63
 
64
64
  // default error handler
65
- app.use((err, req, res, next) => {
65
+ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
66
66
  console.error(err.stack);
67
67
  res.status(500).send({
68
68
  error: "something went wrong",
@@ -1,8 +1,6 @@
1
1
  /* eslint-disable no-console */
2
2
  import debug from "debug";
3
3
  import type { NextFunction, Request, Response } from "express";
4
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
- import Express from "express-serve-static-core";
6
4
 
7
5
  /**
8
6
  * Prefix your start command with `DEBUG=express:middleware:bearer` to enable debug logging
@@ -47,13 +45,13 @@ export function createBearerMiddleware(
47
45
 
48
46
  req["user_id"] = user;
49
47
 
50
- next();
48
+ return next();
51
49
  } catch (e) {
52
50
  if (e instanceof AuthorizationError) {
53
51
  return sendUnauthorizedResponse(res, e.message);
54
52
  }
55
53
 
56
- next(e);
54
+ return next(e);
57
55
  }
58
56
  };
59
57
  }
@@ -196,7 +196,7 @@ function buildDevConfig(options?: DevConfig): {
196
196
  return {};
197
197
  }
198
198
 
199
- const { port, enableHmr, appOrigin, appId, enableHttps, certFile, keyFile } =
199
+ const { port, enableHmr, appOrigin, enableHttps, certFile, keyFile } =
200
200
  options;
201
201
  const host = "localhost";
202
202
 
@@ -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",
@@ -39,8 +41,8 @@
39
41
  "@pmmmwh/react-refresh-webpack-plugin": "0.6.1",
40
42
  "@svgr/webpack": "8.1.0",
41
43
  "@testing-library/react": "16.3.0",
44
+ "@types/debug": "4.1.12",
42
45
  "@types/express": "4.17.21",
43
- "@types/express-serve-static-core": "5.0.7",
44
46
  "@types/jest": "29.5.14",
45
47
  "@types/jsonwebtoken": "9.0.10",
46
48
  "@types/node": "20.19.2",
@@ -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
@@ -73,7 +79,7 @@ describe("Hello World Tests", () => {
73
79
  expect(mockRequestOpenExternalUrl).toHaveBeenCalled();
74
80
 
75
81
  // assert that the requestOpenExternalUrl function was called with the expected arguments
76
- expect(mockRequestOpenExternalUrl.mock.calls[0][0]).toEqual({
82
+ expect(mockRequestOpenExternalUrl.mock.calls[0]?.[0]).toEqual({
77
83
  url: DOCS_URL,
78
84
  });
79
85
  });
@@ -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
+ }
@@ -28,7 +28,10 @@
28
28
  "sourceMap": true,
29
29
  "inlineSources": true,
30
30
  "module": "ESNext",
31
- "noImplicitAny": false,
31
+ "noImplicitAny": true,
32
+ "noImplicitReturns": true,
33
+ "noFallthroughCasesInSwitch": true,
34
+ "noUncheckedIndexedAccess": true,
32
35
  "removeComments": true,
33
36
  "preserveConstEnums": true,
34
37
  "allowSyntheticDefaultImports": true,
@@ -44,8 +47,7 @@
44
47
  "./utils/**/*",
45
48
  "./scripts/**/*",
46
49
  "./declarations/declarations.d.ts",
47
- "./styles/**/*",
48
- "./node_modules/@types/**/*"
50
+ "./styles/**/*"
49
51
  ],
50
52
  "ts-node": {
51
53
  "compilerOptions": {