@canva/cli 1.10.0 → 1.12.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 (126) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +2 -0
  3. package/cli.js +596 -574
  4. package/lib/cjs/index.cjs +2 -2
  5. package/lib/esm/index.mjs +2 -2
  6. package/lib/index.d.ts +2 -0
  7. package/package.json +7 -2
  8. package/templates/base/package.json +9 -8
  9. package/templates/base/styles/components.css +18 -0
  10. package/templates/common/.env.template +1 -1
  11. package/templates/common/jest.config.mjs +1 -1
  12. package/templates/dam/backend/server.ts +8 -0
  13. package/templates/dam/canva-app.json +9 -0
  14. package/templates/dam/package.json +10 -8
  15. package/templates/dam/src/index.tsx +3 -21
  16. package/templates/dam/src/intents/design_editor/index.tsx +25 -0
  17. package/templates/data_connector/README.md +1 -1
  18. package/templates/data_connector/package.json +9 -8
  19. package/templates/data_connector/src/api/data_sources/designs.tsx +1 -1
  20. package/templates/data_connector/src/api/data_sources/templates.tsx +1 -1
  21. package/templates/data_connector/src/components/header.tsx +1 -1
  22. package/templates/data_connector/src/index.tsx +2 -66
  23. package/templates/data_connector/src/{app.tsx → intents/data_connector/app.tsx} +3 -3
  24. package/templates/data_connector/src/{entrypoint.tsx → intents/data_connector/entrypoint.tsx} +5 -5
  25. package/templates/data_connector/src/{home.tsx → intents/data_connector/home.tsx} +1 -1
  26. package/templates/data_connector/src/intents/data_connector/index.tsx +56 -0
  27. package/templates/data_connector/src/pages/error.tsx +1 -1
  28. package/templates/data_connector/src/pages/login.tsx +1 -1
  29. package/templates/data_connector/src/routes/protected_route.tsx +1 -1
  30. package/templates/data_connector/src/routes/routes.tsx +3 -3
  31. package/templates/data_connector/styles/components.css +18 -0
  32. package/templates/gen_ai/backend/server.ts +17 -0
  33. package/templates/gen_ai/canva-app.json +5 -0
  34. package/templates/gen_ai/package.json +10 -8
  35. package/templates/gen_ai/src/api/api.ts +4 -0
  36. package/templates/gen_ai/src/components/footer.tsx +1 -1
  37. package/templates/gen_ai/src/components/loading_results.tsx +1 -1
  38. package/templates/gen_ai/src/components/prompt_input.tsx +1 -1
  39. package/templates/gen_ai/src/index.tsx +3 -14
  40. package/templates/gen_ai/src/{app.tsx → intents/design_editor/app.tsx} +3 -3
  41. package/templates/gen_ai/src/{home.tsx → intents/design_editor/home.tsx} +1 -1
  42. package/templates/gen_ai/src/intents/design_editor/index.tsx +17 -0
  43. package/templates/gen_ai/src/pages/error.tsx +1 -1
  44. package/templates/gen_ai/src/routes/routes.tsx +2 -2
  45. package/templates/gen_ai/styles/components.css +18 -0
  46. package/templates/hello_world/canva-app.json +5 -0
  47. package/templates/hello_world/package.json +10 -8
  48. package/templates/hello_world/src/index.tsx +3 -21
  49. package/templates/hello_world/src/{app.tsx → intents/design_editor/app.tsx} +26 -3
  50. package/templates/hello_world/src/intents/design_editor/index.tsx +25 -0
  51. package/templates/hello_world/src/{tests → intents/design_editor/tests}/app.tests.tsx +19 -13
  52. package/templates/hello_world/styles/components.css +18 -0
  53. package/templates/mls/README.md +81 -0
  54. package/templates/mls/canva-app.json +25 -0
  55. package/templates/mls/declarations/declarations.d.ts +29 -0
  56. package/templates/mls/eslint.config.mjs +14 -0
  57. package/templates/mls/jest.config.mjs +36 -0
  58. package/templates/mls/jest.setup.ts +37 -0
  59. package/templates/mls/package.json +117 -0
  60. package/templates/mls/scripts/copy_env.ts +13 -0
  61. package/templates/mls/scripts/ssl/ssl.ts +131 -0
  62. package/templates/mls/scripts/start/app_runner.ts +223 -0
  63. package/templates/mls/scripts/start/context.ts +171 -0
  64. package/templates/mls/scripts/start/start.ts +46 -0
  65. package/templates/mls/src/__tests__/app.tests.tsx +11 -0
  66. package/templates/mls/src/__tests__/office_selection_page.tests.tsx +72 -0
  67. package/templates/mls/src/__tests__/utils.tsx +19 -0
  68. package/templates/mls/src/adapter.ts +126 -0
  69. package/templates/mls/src/components/agent/agent_card.tsx +57 -0
  70. package/templates/mls/src/components/agent/agent_grid.tsx +37 -0
  71. package/templates/mls/src/components/agent/agent_list.tsx +17 -0
  72. package/templates/mls/src/components/agent/agent_search_filters.tsx +88 -0
  73. package/templates/mls/src/components/breadcrumb/breadcrumb.tsx +40 -0
  74. package/templates/mls/src/components/listing/listing_card.tsx +64 -0
  75. package/templates/mls/src/components/listing/listing_grid.tsx +37 -0
  76. package/templates/mls/src/components/listing/listing_list.tsx +21 -0
  77. package/templates/mls/src/components/listing/listing_search_filters.tsx +145 -0
  78. package/templates/mls/src/components/placeholders/placeholders.tsx +65 -0
  79. package/templates/mls/src/data.ts +359 -0
  80. package/templates/mls/src/index.tsx +4 -0
  81. package/templates/mls/src/intents/design_editor/app.tsx +44 -0
  82. package/templates/mls/src/intents/design_editor/index.tsx +25 -0
  83. package/templates/mls/src/pages/agent_details_page/agent_details_page.tsx +175 -0
  84. package/templates/mls/src/pages/list_page/agent_tab_panel.tsx +126 -0
  85. package/templates/mls/src/pages/list_page/list_page.tsx +67 -0
  86. package/templates/mls/src/pages/list_page/listing_tab_panel.tsx +135 -0
  87. package/templates/mls/src/pages/listing_details_page/listing_details_page.tsx +418 -0
  88. package/templates/mls/src/pages/loading_page/loading_page.tsx +152 -0
  89. package/templates/mls/src/pages/office_selection_page/office_selection_page.tsx +144 -0
  90. package/templates/mls/src/real_estate.type.ts +44 -0
  91. package/templates/mls/src/util/use_add_element.tsx +62 -0
  92. package/templates/mls/src/util/use_drag_element.tsx +68 -0
  93. package/templates/mls/styles/components.css +56 -0
  94. package/templates/mls/tsconfig.json +55 -0
  95. package/templates/mls/webpack.config.ts +254 -0
  96. package/templates/optional/AGENTS.md +80 -2
  97. package/templates/optional/CLAUDE.md +80 -2
  98. package/templates/base/backend/routers/oauth.ts +0 -393
  99. package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +0 -99
  100. package/templates/base/utils/backend/bearer_middleware/index.ts +0 -1
  101. package/templates/base/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +0 -192
  102. package/templates/base/utils/use_add_element.ts +0 -58
  103. package/templates/base/utils/use_feature_support.ts +0 -28
  104. package/templates/common/utils/backend/base_backend/create.ts +0 -104
  105. package/templates/common/utils/table_wrapper.ts +0 -520
  106. package/templates/common/utils/use_add_element.ts +0 -58
  107. package/templates/common/utils/use_feature_support.ts +0 -28
  108. package/templates/common/utils/use_overlay_hook.ts +0 -76
  109. package/templates/common/utils/use_selection_hook.ts +0 -37
  110. package/templates/gen_ai/backend/database/database.ts +0 -42
  111. package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +0 -99
  112. package/templates/gen_ai/utils/backend/bearer_middleware/index.ts +0 -1
  113. package/templates/hello_world/utils/use_add_element.ts +0 -58
  114. package/templates/hello_world/utils/use_feature_support.ts +0 -28
  115. /package/templates/base/{utils/backend → backend}/base_backend/create.ts +0 -0
  116. /package/templates/base/{utils/backend → backend}/jwt_middleware/index.ts +0 -0
  117. /package/templates/base/{utils/backend → backend}/jwt_middleware/jwt_middleware.ts +0 -0
  118. /package/templates/dam/src/{adapter.ts → intents/design_editor/adapter.ts} +0 -0
  119. /package/templates/dam/src/{app.tsx → intents/design_editor/app.tsx} +0 -0
  120. /package/templates/dam/src/{config.ts → intents/design_editor/config.ts} +0 -0
  121. /package/templates/dam/src/{index.css → intents/design_editor/index.css} +0 -0
  122. /package/templates/data_connector/src/{paths.ts → routes/paths.ts} +0 -0
  123. /package/templates/gen_ai/src/{paths.ts → routes/paths.ts} +0 -0
  124. /package/templates/{common → gen_ai}/utils/backend/jwt_middleware/index.ts +0 -0
  125. /package/templates/{common → gen_ai}/utils/backend/jwt_middleware/jwt_middleware.ts +0 -0
  126. /package/templates/hello_world/src/{tests → intents/design_editor/tests}/__snapshots__/app.tests.tsx.snap +0 -0
@@ -1,520 +0,0 @@
1
- import type { Cell, TableElement } from "@canva/design";
2
-
3
- const MAX_CELL_COUNT = 225;
4
-
5
- // Additional information in the wrapper that are not available in the table cell element.
6
- // Currently, only merged cells, but it can later extend to other custom properties, like border, size,...
7
- type MetaCell = {
8
- // If a cell is merged into another cell, then this field should tell that other cell.
9
- mergedInto?: { row: number; column: number };
10
- };
11
-
12
- export class TableWrapper {
13
- // A shadow of the cell array, that highlight the relationship between merged cells.
14
- private readonly metaCells: MetaCell[][];
15
-
16
- private constructor(
17
- private readonly rows: {
18
- cells: (Cell | null | undefined)[];
19
- }[],
20
- ) {
21
- this.validateRowColumn();
22
- this.metaCells = [];
23
- for (const row of this.rows) {
24
- this.metaCells.push(Array.from({ length: row.cells.length }, () => ({})));
25
- }
26
- this.syncMergedCellsFromRows();
27
- }
28
-
29
- /**
30
- * Creates an empty table wrapper.
31
- * @param rowCount - The number of rows to create the table with.
32
- * @param columnCount - The number of columns to create the table with.
33
- */
34
- static create(rowCount: number, columnCount: number) {
35
- const rows = Array.from({ length: rowCount }, () => ({
36
- cells: Array.from({ length: columnCount }, () => null),
37
- }));
38
- return new TableWrapper(rows);
39
- }
40
-
41
- /**
42
- * Converts a table element into a table wrapper.
43
- * @param element - The table element to convert into a table wrapper.
44
- * @throws TableValidationError if element is not a valid {@link TableElement}.
45
- */
46
- static fromElement(element: TableElement) {
47
- if (element.type !== "table") {
48
- throw new TableValidationError(
49
- `Cannot convert element of type ${element.type} to a table wrapper.`,
50
- );
51
- }
52
- if (!Array.isArray(element.rows)) {
53
- throw new TableValidationError(
54
- `Invalid table element: expected an array of rows, got ${element.rows}`,
55
- );
56
- }
57
- const rows = element.rows.map((row) => ({
58
- cells: row.cells.map(
59
- (cell) =>
60
- cell && {
61
- ...cell,
62
- attributes: cell.attributes ? { ...cell.attributes } : undefined,
63
- },
64
- ),
65
- }));
66
- return new TableWrapper(rows);
67
- }
68
-
69
- /**
70
- * Return a table element that can be passed into the `addElementAtPoint` or `addElementAtCursor` method.
71
- * @returns A table element.
72
- */
73
- toElement(): TableElement {
74
- return {
75
- type: "table",
76
- rows: this.rows,
77
- };
78
- }
79
-
80
- /**
81
- * Adds a row to the table after the specified row.
82
- * @param afterRowPos The position of the new row. A value of `0` adds a row before the first row, a
83
- * value of `1` adds a row after the first row, etc.
84
- * @remarks
85
- * If the row above and below the new row both have the same properties, the properties will be
86
- * copied to the new row. For example, if there are two rows with the same background color and a
87
- * row is inserted between them, the new row will also have the same background color.
88
- */
89
- addRow(afterRowPos: number) {
90
- if (afterRowPos < 0 || afterRowPos > this.rows.length) {
91
- throw new TableValidationError(
92
- `New row position must be between 0 and ${this.rows.length}.`,
93
- );
94
- }
95
-
96
- this.validateRowColumn(1, 0);
97
-
98
- const columnLength = this.rows[0]?.cells.length || 0;
99
- const newRow = {
100
- cells: Array.from({ length: columnLength }, () => ({}) as Cell),
101
- };
102
- this.rows.splice(afterRowPos, 0, newRow);
103
-
104
- const newMergeCells: MetaCell[] = Array.from(
105
- { length: columnLength },
106
- () => ({}),
107
- );
108
- this.metaCells.splice(afterRowPos, 0, newMergeCells);
109
-
110
- if (0 < afterRowPos && afterRowPos < this.rows.length) {
111
- // Insert in between rows
112
- for (let i = 0; i < newRow.cells.length; i++) {
113
- this.mayCopyStyles({
114
- frontRowIdx: afterRowPos - 1,
115
- frontColumnIdx: i,
116
- currentRowIdx: afterRowPos,
117
- currentColumnIdx: i,
118
- behindRowIdx: afterRowPos + 1,
119
- behindColumnIdx: i,
120
- });
121
- }
122
- this.syncCellSpansFromMetaCells();
123
- }
124
- }
125
-
126
- /**
127
- * Adds a column to the table after the specified column.
128
- * @param afterColumnPos The position of the new column. A value of `0` adds a column before the first
129
- * column, a value of `1` adds a column after the first column, etc.
130
- * @remarks
131
- * If the column before and after the new column both have the same properties, the properties will be
132
- * copied to the new column. For example, if there are two columns with the same background color and a
133
- * column is inserted between them, the new column will also have the same background color.
134
- */
135
- addColumn(afterColumnPos: number) {
136
- const columnLength = this.rows[0]?.cells.length || 0;
137
- if (afterColumnPos < 0 || afterColumnPos > columnLength) {
138
- throw new TableValidationError(
139
- `New column position must be between 0 and ${columnLength}.`,
140
- );
141
- }
142
-
143
- this.validateRowColumn(0, 1);
144
-
145
- this.rows.forEach((row) => row.cells.splice(afterColumnPos, 0, null));
146
- const newColumnLength = this.rows[0]?.cells.length || 0;
147
-
148
- const newMergeCell: MetaCell = {};
149
- this.metaCells.forEach((row) =>
150
- row.splice(afterColumnPos, 0, newMergeCell),
151
- );
152
-
153
- if (0 < afterColumnPos && afterColumnPos < newColumnLength) {
154
- // Insert in between columns
155
- for (let i = 0; i < this.rows.length; i++) {
156
- this.mayCopyStyles({
157
- frontRowIdx: i,
158
- frontColumnIdx: afterColumnPos - 1,
159
- currentRowIdx: i,
160
- currentColumnIdx: afterColumnPos,
161
- behindRowIdx: i,
162
- behindColumnIdx: afterColumnPos + 1,
163
- });
164
- }
165
- this.syncCellSpansFromMetaCells();
166
- }
167
- }
168
-
169
- /**
170
- * Checks if the specified cell is a *ghost cell*.
171
- * @param rowPos The row number of the cell, starting from `1`.
172
- * @param columnPos The column number of the cell, starting from `1`.
173
- * @remarks
174
- * A ghost cell is a cell that can not be interacted as it is hidden by a row or column spanning
175
- * cell. For example, imagine a row where the first cell has a `colSpan` of `2`. In this case, the
176
- * second cell in the row is hidden and is therefore a ghost cell.
177
- */
178
- isGhostCell(rowPos: number, columnPos: number): boolean {
179
- this.validateCellBoundaries(rowPos, columnPos);
180
- const rowIndex = rowPos - 1;
181
- const columnIndex = columnPos - 1;
182
- const { mergedInto } = this.getMetaCell(rowIndex, columnIndex);
183
- if (!mergedInto) {
184
- // Not belongs to any merged cell
185
- return false;
186
- }
187
- // Not a ghost cell if it's merged into itself
188
- return mergedInto.row !== rowIndex || mergedInto.column !== columnIndex;
189
- }
190
-
191
- /**
192
- * Returns information about the specified cell.
193
- * @param rowPos The row number of the cell, starting from `1`.
194
- * @param columnPos The column number of the cell, starting from `1`.
195
- * @throws TableValidationError if the cell is a ghost cell. To learn more, see {@link isGhostCell}.
196
- */
197
- getCellDetails(rowPos: number, columnPos: number) {
198
- if (this.isGhostCell(rowPos, columnPos)) {
199
- throw new TableValidationError(
200
- `The cell at ${rowPos},${columnPos} is squashed into another cell`,
201
- );
202
- }
203
- return this.getCellInternal(rowPos - 1, columnPos - 1);
204
- }
205
-
206
- getCellInternal(rowIdx: number, columnIdx: number) {
207
- const row = this.rows[rowIdx];
208
- if (!row) {
209
- throw new Error("validateCellBoundaries should be called first");
210
- }
211
- return row.cells[columnIdx];
212
- }
213
-
214
- /**
215
- * Sets the details of the specified cell, including its content and appearance.
216
- * @param rowPos The row number of the cell, starting from `1`.
217
- * @param columnPos The column number of the cell, starting from `1`.
218
- * @param details The new details for the cell.
219
- * @throws TableValidationError if the cell is a ghost cell. To learn more, see {@link isGhostCell}.
220
- */
221
- setCellDetails(rowPos: number, columnPos: number, details: Cell) {
222
- const rowSpan = details.rowSpan ?? 1;
223
- const colSpan = details.colSpan ?? 1;
224
- this.validateCellBoundaries(rowPos, columnPos, rowSpan, colSpan);
225
- if (this.isGhostCell(rowPos, columnPos)) {
226
- throw new TableValidationError(
227
- `The cell at ${rowPos},${columnPos} is squashed into another cell`,
228
- );
229
- }
230
-
231
- const rowIndex = rowPos - 1;
232
- const columnIndex = columnPos - 1;
233
- const { rowSpan: oldRowSpan, colSpan: oldColSpan } =
234
- this.getCellInternal(rowIndex, columnIndex) || {};
235
-
236
- const row = this.rows[rowIndex];
237
- if (row) {
238
- row.cells[columnIndex] = details;
239
- }
240
-
241
- if (oldRowSpan !== rowSpan || oldColSpan !== colSpan) {
242
- this.syncMergedCellsFromRows();
243
- }
244
- }
245
-
246
- private validateRowColumn(toBeAddedRow = 0, toBeAddedColumn = 0) {
247
- const rowCount = this.rows.length + toBeAddedRow;
248
- const row = this.rows[0];
249
- if (rowCount === 0 || !row) {
250
- throw new TableValidationError("Table must have at least one row.");
251
- }
252
- const columnCount = row.cells.length + toBeAddedColumn;
253
- if (columnCount === 0) {
254
- throw new TableValidationError("Table must have at least one column.");
255
- }
256
- for (const row of this.rows) {
257
- if (row.cells.length + toBeAddedColumn !== columnCount) {
258
- throw new TableValidationError(
259
- "All rows must have the same number of columns.",
260
- );
261
- }
262
- }
263
- const cellCount = rowCount * columnCount;
264
- if (cellCount > MAX_CELL_COUNT) {
265
- throw new TableValidationError(
266
- `Table cannot have more than ${MAX_CELL_COUNT} cells. Actual: ${rowCount}x${columnCount} = ${cellCount}`,
267
- );
268
- }
269
- }
270
-
271
- /**
272
- * Read all cell's colSpans and rowSpans and update the merged cells accordingly.
273
- * This is opposite sync of {@link syncCellSpansFromMetaCells}.
274
- */
275
- private syncMergedCellsFromRows(): void {
276
- // First, reset metaCells to unmerged state
277
- this.metaCells.forEach((cells) =>
278
- cells.forEach((c) => (c.mergedInto = undefined)),
279
- );
280
-
281
- // Then loop through this.rows to find any merged cells
282
- for (let rowIndex = 0; rowIndex < this.rows.length; rowIndex++) {
283
- const row = this.rows[rowIndex];
284
- if (!row) {
285
- continue;
286
- }
287
- for (let columnIndex = 0; columnIndex < row.cells.length; columnIndex++) {
288
- const cell = row.cells[columnIndex] || { colSpan: 1, rowSpan: 1 };
289
- const colSpan = cell.colSpan || 1;
290
- const rowSpan = cell.rowSpan || 1;
291
- if (colSpan !== 1 || rowSpan !== 1) {
292
- this.validateCellBoundaries(
293
- rowIndex + 1,
294
- columnIndex + 1,
295
- rowSpan,
296
- colSpan,
297
- );
298
- this.setMergedCellsByBoundary(
299
- rowIndex,
300
- columnIndex,
301
- rowIndex + rowSpan - 1,
302
- columnIndex + colSpan - 1,
303
- );
304
- }
305
- }
306
- }
307
- }
308
-
309
- /**
310
- * Update mergeCells array in accordance with the span boundary
311
- * @param fromRow Top most row index
312
- * @param fromColumn Left most column index
313
- * @param toRow Bottom most row index
314
- * @param toColumn Right most column index
315
- */
316
- private setMergedCellsByBoundary(
317
- fromRow: number,
318
- fromColumn: number,
319
- toRow: number,
320
- toColumn: number,
321
- ) {
322
- for (let row = fromRow; row <= toRow; row++) {
323
- for (let column = fromColumn; column <= toColumn; column++) {
324
- const metaCell = this.getMetaCell(row, column);
325
-
326
- if (metaCell.mergedInto) {
327
- // This cell may be squashed by another merge cell
328
- const { row: originalRow, column: originalColumn } =
329
- metaCell.mergedInto;
330
- if (originalRow !== fromRow && originalColumn !== fromColumn) {
331
- // And the old origin cell is mismatched with the new origin,
332
- // this mean the current meta cell is merged into 2 different origin cells,
333
- // which is forbidden.
334
- throw new TableValidationError(
335
- `Expanding the cell at ${fromRow},${fromColumn} collides with another merged cell from ${originalRow},${originalColumn}`,
336
- );
337
- }
338
- }
339
-
340
- metaCell.mergedInto = {
341
- row: fromRow,
342
- column: fromColumn,
343
- };
344
- }
345
- }
346
- }
347
-
348
- private mayCopyStyles({
349
- frontRowIdx,
350
- frontColumnIdx,
351
- behindRowIdx,
352
- behindColumnIdx,
353
- currentRowIdx,
354
- currentColumnIdx,
355
- }: {
356
- frontRowIdx: number;
357
- frontColumnIdx: number;
358
- behindRowIdx: number;
359
- behindColumnIdx: number;
360
- currentRowIdx: number;
361
- currentColumnIdx: number;
362
- }) {
363
- // Continue span if both front and behind cells belong to a same merged cell
364
- const frontMergedCell = this.getMetaCell(
365
- frontRowIdx,
366
- frontColumnIdx,
367
- ).mergedInto;
368
- const behindMergedCell = this.getMetaCell(
369
- behindRowIdx,
370
- behindColumnIdx,
371
- ).mergedInto;
372
- if (
373
- frontMergedCell &&
374
- frontMergedCell.row === behindMergedCell?.row &&
375
- frontMergedCell.column === behindMergedCell?.column
376
- ) {
377
- this.getMetaCell(currentRowIdx, currentColumnIdx).mergedInto = {
378
- ...frontMergedCell,
379
- };
380
- }
381
-
382
- // Copy attributes if both front and behind cells are the same
383
- const frontCell = this.getCellInternal(frontRowIdx, frontColumnIdx);
384
- const behindCell = this.getCellInternal(behindRowIdx, behindColumnIdx);
385
- if (
386
- frontCell != null &&
387
- behindCell != null &&
388
- frontCell.attributes &&
389
- behindCell.attributes
390
- ) {
391
- let currentCell = this.getCellInternal(currentRowIdx, currentColumnIdx);
392
- const attrs = Object.keys(
393
- frontCell.attributes,
394
- ) as (keyof Cell["attributes"])[];
395
- for (const key of attrs) {
396
- if (frontCell.attributes[key] === behindCell.attributes[key]) {
397
- currentCell = currentCell || { type: "empty" };
398
- currentCell.attributes = currentCell.attributes || {};
399
- currentCell.attributes[key] = frontCell.attributes[key];
400
- }
401
- }
402
- const targetRow = this.rows[currentRowIdx];
403
- if (currentCell && targetRow) {
404
- targetRow.cells[currentColumnIdx] = currentCell;
405
- }
406
- }
407
- }
408
-
409
- private getMetaCell(rowIdx: number, columnIdx: number): MetaCell {
410
- const metaCell = this.metaCells[rowIdx]?.[columnIdx];
411
- if (!metaCell) {
412
- throw new Error("MetaCells does not match the table dimension");
413
- }
414
- return metaCell;
415
- }
416
-
417
- /**
418
- * Loop through meta cells and update rowSpan and colSpan of each cell accordingly.
419
- * This is opposite sync of {@link syncMergedCellsFromRows}
420
- */
421
- private syncCellSpansFromMetaCells() {
422
- const groups = new Map<string, { row: number; column: number }[]>();
423
- for (let row = 0; row < this.metaCells.length; row++) {
424
- const metaRow = this.metaCells[row];
425
- if (!metaRow) {
426
- continue;
427
- }
428
- for (let column = 0; column < metaRow.length; column++) {
429
- // Reset all rowSpans and colSpans
430
- const currentCell = this.getCellInternal(row, column);
431
- currentCell && delete currentCell.rowSpan;
432
- currentCell && delete currentCell.colSpan;
433
-
434
- const mergedCell = this.getMetaCell(row, column);
435
- if (!mergedCell.mergedInto) {
436
- continue;
437
- }
438
-
439
- const { row: actualRow, column: actualColumn } = mergedCell.mergedInto;
440
- const key = `${actualRow},${actualColumn}`;
441
- if (!groups.has(key)) {
442
- groups.set(key, []);
443
- }
444
- groups.get(key)?.push({ row, column });
445
- }
446
- }
447
-
448
- groups.forEach((cells, key) => {
449
- const { minRow, maxRow, minColumn, maxColumn } = cells.reduce(
450
- (prev, { row, column }) => {
451
- return {
452
- minRow: Math.min(prev.minRow, row),
453
- maxRow: Math.max(prev.maxRow, row),
454
- minColumn: Math.min(prev.minColumn, column),
455
- maxColumn: Math.max(prev.maxColumn, column),
456
- };
457
- },
458
- { minRow: Infinity, maxRow: -1, minColumn: Infinity, maxColumn: -1 },
459
- );
460
- if (
461
- !isFinite(minRow) ||
462
- !isFinite(minColumn) ||
463
- maxRow < 0 ||
464
- maxColumn < 0
465
- ) {
466
- throw new TableValidationError(`Invalid merged cell started at ${key}`);
467
- }
468
- const rowSpan = maxRow - minRow + 1;
469
- const columnSpan = maxColumn - minColumn + 1;
470
- if (rowSpan > 1 || columnSpan > 1) {
471
- const currentCell = this.getCellInternal(minRow, minColumn) || {
472
- type: "empty",
473
- };
474
- currentCell.rowSpan = rowSpan;
475
- currentCell.colSpan = columnSpan;
476
- const targetRow = this.rows[minRow];
477
- if (targetRow) {
478
- targetRow.cells[minColumn] = currentCell;
479
- }
480
- }
481
- });
482
- }
483
-
484
- private validateCellBoundaries(
485
- rowPos: number,
486
- columnPos: number,
487
- rowSpan = 1,
488
- columnSpan = 1,
489
- ) {
490
- if (rowPos < 1 || rowPos > this.rows.length) {
491
- throw new TableValidationError(
492
- `Row position must be between 1 and ${this.rows.length} (number of rows).`,
493
- );
494
- }
495
- const columnLength = this.rows[0]?.cells.length || 0;
496
- if (columnPos < 1 || columnPos > columnLength) {
497
- throw new TableValidationError(
498
- `Column position must be between 1 and ${columnLength} (number of columns).`,
499
- );
500
- }
501
- if (rowSpan < 1) {
502
- throw new TableValidationError(`Row span must be greater than 0.`);
503
- }
504
- if (columnSpan < 1) {
505
- throw new TableValidationError(`Column span must be greater than 0.`);
506
- }
507
- if (rowPos + rowSpan - 1 > this.rows.length) {
508
- throw new TableValidationError(
509
- `Cannot expand ${rowSpan} rows from the cell at ${rowPos},${columnPos}. Table has ${this.rows.length} rows.`,
510
- );
511
- }
512
- if (columnPos + columnSpan - 1 > columnLength) {
513
- throw new TableValidationError(
514
- `Cannot expand ${columnSpan} columns from the cell at ${rowPos},${columnPos}. Table has ${columnLength} columns.`,
515
- );
516
- }
517
- }
518
- }
519
-
520
- class TableValidationError extends Error {}
@@ -1,58 +0,0 @@
1
- import type {
2
- EmbedElement,
3
- ImageElement,
4
- RichtextElement,
5
- TableElement,
6
- TextElement,
7
- VideoElement,
8
- } from "@canva/design";
9
- import { addElementAtCursor, addElementAtPoint } from "@canva/design";
10
- import { features } from "@canva/platform";
11
- import { useEffect, useState } from "react";
12
- import { useFeatureSupport } from "./use_feature_support";
13
-
14
- type AddElementParams =
15
- | ImageElement
16
- | VideoElement
17
- | EmbedElement
18
- | TextElement
19
- | RichtextElement
20
- | TableElement;
21
-
22
- export const useAddElement = () => {
23
- const isSupported = useFeatureSupport();
24
-
25
- // Store a wrapped addElement function that checks feature support
26
- const [addElement, setAddElement] = useState(() => {
27
- return (element: AddElementParams) => {
28
- if (features.isSupported(addElementAtPoint)) {
29
- return addElementAtPoint(element);
30
- } else if (features.isSupported(addElementAtCursor)) {
31
- return addElementAtCursor(element);
32
- }
33
- // eslint-disable-next-line no-console
34
- console.warn(
35
- "Neither addElementAtPoint nor addElementAtCursor are supported",
36
- );
37
- return Promise.resolve();
38
- };
39
- });
40
-
41
- useEffect(() => {
42
- const addElement = (element: AddElementParams) => {
43
- if (isSupported(addElementAtPoint)) {
44
- return addElementAtPoint(element);
45
- } else if (isSupported(addElementAtCursor)) {
46
- return addElementAtCursor(element);
47
- }
48
- // eslint-disable-next-line no-console
49
- console.warn(
50
- "Neither addElementAtPoint nor addElementAtCursor are supported",
51
- );
52
- return Promise.resolve();
53
- };
54
- setAddElement(() => addElement);
55
- }, [isSupported]);
56
-
57
- return addElement;
58
- };
@@ -1,28 +0,0 @@
1
- import { features } from "@canva/platform";
2
- import type { Feature } from "@canva/platform";
3
- import { useEffect, useState } from "react";
4
-
5
- /**
6
- * This hook allows re-rendering of a React component whenever
7
- * the state of feature support changes in Canva.
8
- *
9
- * @returns isSupported - callback to inspect a Canva SDK method.
10
- **/
11
- export function useFeatureSupport() {
12
- // Store a wrapped function that checks feature support
13
- const [isSupported, setIsSupported] = useState(() => {
14
- return (...args: Feature[]) => features.isSupported(...args);
15
- });
16
-
17
- useEffect(() => {
18
- // create new function ref when feature support changes to trigger
19
- // re-render
20
- return features.registerOnSupportChange(() => {
21
- setIsSupported(() => {
22
- return (...args: Feature[]) => features.isSupported(...args);
23
- });
24
- });
25
- }, []);
26
-
27
- return isSupported;
28
- }
@@ -1,76 +0,0 @@
1
- import type {
2
- AppProcessId,
3
- OverlayOpenableEvent,
4
- OverlayTarget,
5
- } from "@canva/design";
6
- import { overlay as designOverlay } from "@canva/design";
7
- import type { CloseParams } from "@canva/platform";
8
- import { appProcess } from "@canva/platform";
9
- import { useEffect, useState } from "react";
10
-
11
- const initialOverlayEvent: OverlayOpenableEvent<OverlayTarget> = {
12
- canOpen: false,
13
- reason: "",
14
- };
15
-
16
- /**
17
- * Returns an object which contains the following field:
18
- * 1. canOpen - a boolean indicate whether the overlay can be opened on the specified target.
19
- * 2. isOpen - a boolean indicate whether the overlay is currently open.
20
- * 3. open - a function to open an overlay on the specified target.
21
- * 4. close - a function close the currently opened overlay.
22
- * @param target The overlay target to register for whether we can open an overlay.
23
- */
24
- export function useOverlay<
25
- T extends OverlayTarget,
26
- C extends CloseParams = CloseParams,
27
- >(
28
- target: T,
29
- ): {
30
- canOpen: boolean;
31
- isOpen: boolean;
32
- open: (opts?: {
33
- launchParameters?: unknown;
34
- }) => Promise<AppProcessId | undefined>;
35
- close: (opts: C) => Promise<void>;
36
- } {
37
- const [overlay, setOverlay] =
38
- useState<OverlayOpenableEvent<T>>(initialOverlayEvent);
39
- const [overlayId, setOverlayId] = useState<AppProcessId>();
40
- const [isOpen, setIsOpen] = useState<boolean>(false);
41
-
42
- useEffect(() => {
43
- return designOverlay.registerOnCanOpen({
44
- target,
45
- onCanOpen: setOverlay,
46
- });
47
- }, []);
48
-
49
- useEffect(() => {
50
- if (overlayId) {
51
- appProcess.registerOnStateChange(overlayId, ({ state }) =>
52
- setIsOpen(state === "open"),
53
- );
54
- }
55
- }, [overlayId]);
56
-
57
- const open = async (
58
- opts: { launchParameters?: unknown } = {},
59
- ): Promise<AppProcessId | undefined> => {
60
- if (!overlay || !overlay.canOpen) {
61
- return undefined;
62
- }
63
-
64
- const overlayId = await overlay.open(opts);
65
- setOverlayId(overlayId);
66
- return overlayId;
67
- };
68
-
69
- const close = async (opts: C) => {
70
- if (overlayId) {
71
- appProcess.requestClose<C>(overlayId, opts);
72
- }
73
- };
74
-
75
- return { canOpen: overlay.canOpen, isOpen, open, close };
76
- }