From adcecf3c65c26976da9e59115f5603f0b8363e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertalan=20K=C3=B6rmendy?= Date: Mon, 30 Oct 2023 18:17:14 +0100 Subject: [PATCH] Check whether component supports children for wrapping (#4429) * wip * move todo * check exported component for children * update snapshots * bump utopia-api * clean up after merge * revert overabstraction * more lenient element checking * revert package json anc package lock * re-update snapshots * remove todo * test * re-re update snapshots * remove outdated comment * Update editor/src/core/third-party/remix-controls.ts Co-authored-by: RheeseyB <1044774+Rheeseyb@users.noreply.github.com> * move getTopLevelElementByExportsDetail --------- Co-authored-by: RheeseyB <1044774+Rheeseyb@users.noreply.github.com> --- editor/pnpm-lock.yaml | 94 ++------- .../components/canvas/canvas-globals.spec.ts | 9 + .../src/components/canvas/canvas-globals.ts | 1 + .../canvas/ui/floating-insert-menu.tsx | 1 + .../src/components/custom-code/code-file.ts | 4 +- .../editor/canvas-toolbar.spec.browser2.tsx | 68 ++++++ editor/src/components/editor/export-utils.ts | 198 +++++++++--------- .../store/store-deep-equality-instances.ts | 4 +- .../common/property-controls-hooks.spec.tsx | 2 + .../shared/project-components.spec.ts | 2 + .../components/shared/project-components.ts | 154 +++++++++----- .../text-editor/text-editor.spec.browser2.tsx | 7 +- .../src/core/model/element-metadata-utils.ts | 7 + editor/src/core/model/project-file-utils.ts | 11 + .../property-controls-local.spec.tsx | 10 +- .../property-controls-local.ts | 8 +- editor/src/core/shared/project-file-types.ts | 8 +- .../src/core/third-party/antd-components.ts | 1 + .../react-three-fiber-components.ts | 1 + editor/src/core/third-party/remix-controls.ts | 2 + .../core/third-party/utopia-api-components.ts | 19 +- utopia-api/package.json | 2 +- utopia-api/src/helpers/helper-functions.ts | 3 +- 23 files changed, 362 insertions(+), 254 deletions(-) diff --git a/editor/pnpm-lock.yaml b/editor/pnpm-lock.yaml index b3b0a33e1020..da33ec474e79 100644 --- a/editor/pnpm-lock.yaml +++ b/editor/pnpm-lock.yaml @@ -5130,10 +5130,6 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true - /@yarnpkg/lockfile/1.1.0: - resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} - dev: false - /JSONStream/1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -5726,11 +5722,6 @@ packages: resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} dev: true - /at-least-node/1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - dev: false - /atob/2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} @@ -6759,10 +6750,6 @@ packages: engines: {node: '>=6.0'} dev: true - /ci-info/2.0.0: - resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} - dev: false - /ci-info/3.2.0: resolution: {integrity: sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==} dev: true @@ -7069,7 +7056,7 @@ packages: dev: true /component-indexof/0.0.3: - resolution: {integrity: sha1-EdCRMSI5648yyPJa6csAL/6NPCQ=} + resolution: {integrity: sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==} dev: false /compressible/2.0.18: @@ -7391,6 +7378,7 @@ packages: semver: 5.7.1 shebang-command: 1.2.0 which: 1.3.1 + dev: true /cross-spawn/7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} @@ -9554,12 +9542,6 @@ packages: path-exists: 4.0.0 dev: true - /find-yarn-workspace-root/2.0.0: - resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} - dependencies: - micromatch: 4.0.4 - dev: false - /flat-cache/3.1.0: resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==} engines: {node: '>=12.0.0'} @@ -9743,16 +9725,6 @@ packages: universalify: 0.1.2 dev: true - /fs-extra/9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.8 - jsonfile: 6.1.0 - universalify: 2.0.0 - dev: false - /fs-minipass/2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -10062,9 +10034,11 @@ packages: /graceful-fs/4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true /graceful-fs/4.2.8: resolution: {integrity: sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==} + dev: true /graphemer/1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -10791,13 +10765,6 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - /is-ci/2.0.0: - resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} - hasBin: true - dependencies: - ci-info: 2.0.0 - dev: false - /is-ci/3.0.0: resolution: {integrity: sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==} hasBin: true @@ -10874,6 +10841,7 @@ packages: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true + dev: true /is-dom/1.1.0: resolution: {integrity: sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ==} @@ -11151,6 +11119,7 @@ packages: engines: {node: '>=8'} dependencies: is-docker: 2.2.1 + dev: true /isarray/0.0.1: resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} @@ -12117,6 +12086,7 @@ packages: chalk: 3.0.0 diff-match-patch: 1.0.5 dev: false + bundledDependencies: [] /jsonfile/4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -12130,6 +12100,7 @@ packages: universalify: 2.0.0 optionalDependencies: graceful-fs: 4.2.11 + dev: true /jsonify/0.0.0: resolution: {integrity: sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=} @@ -12297,12 +12268,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /klaw-sync/6.0.0: - resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==} - dependencies: - graceful-fs: 4.2.8 - dev: false - /kleur/3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -12844,6 +12809,7 @@ packages: dependencies: braces: 3.0.2 picomatch: 2.3.1 + dev: true /micromatch/4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} @@ -13174,6 +13140,7 @@ packages: /nice-try/1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + dev: true /nise/5.1.1: resolution: {integrity: sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==} @@ -13555,14 +13522,6 @@ packages: which-pm-runs: 1.0.0 dev: true - /open/7.4.2: - resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} - engines: {node: '>=8'} - dependencies: - is-docker: 2.2.1 - is-wsl: 2.2.0 - dev: false - /open/8.2.1: resolution: {integrity: sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ==} engines: {node: '>=12'} @@ -13607,6 +13566,7 @@ packages: /os-tmpdir/1.0.2: resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=} engines: {node: '>=0.10.0'} + dev: true /p-cancelable/2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} @@ -13772,27 +13732,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /patch-package/6.5.1: - resolution: {integrity: sha512-I/4Zsalfhc6bphmJTlrLoOcAF87jcxko4q0qsv4bGcurbr8IskEOtdnt9iCmsQVGL1B+iUhSQqweyTLJfCF9rA==} - engines: {node: '>=10', npm: '>5'} - hasBin: true - dependencies: - '@yarnpkg/lockfile': 1.1.0 - chalk: 4.1.2 - cross-spawn: 6.0.5 - find-yarn-workspace-root: 2.0.0 - fs-extra: 9.1.0 - is-ci: 2.0.0 - klaw-sync: 6.0.0 - minimist: 1.2.8 - open: 7.4.2 - rimraf: 2.7.1 - semver: 5.7.1 - slash: 2.0.0 - tmp: 0.0.33 - yaml: 1.10.2 - dev: false - /path-browserify/0.0.1: resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==} dev: true @@ -13817,6 +13756,7 @@ packages: /path-key/2.0.1: resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} engines: {node: '>=4'} + dev: true /path-key/3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} @@ -16044,6 +15984,7 @@ packages: hasBin: true dependencies: glob: 7.2.0 + dev: true /rimraf/3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} @@ -16465,6 +16406,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: shebang-regex: 1.0.0 + dev: true /shebang-command/2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -16475,6 +16417,7 @@ packages: /shebang-regex/1.0.0: resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} engines: {node: '>=0.10.0'} + dev: true /shebang-regex/3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} @@ -16552,6 +16495,7 @@ packages: /slash/2.0.0: resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} engines: {node: '>=6'} + dev: true /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} @@ -17407,6 +17351,7 @@ packages: engines: {node: '>=0.6.0'} dependencies: os-tmpdir: 1.0.2 + dev: true /tmp/0.2.1: resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} @@ -17850,6 +17795,7 @@ packages: /universalify/2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} + dev: true /unpipe/1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} @@ -18489,6 +18435,7 @@ packages: hasBin: true dependencies: isexe: 2.0.0 + dev: true /which/2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -18772,7 +18719,7 @@ packages: resolution: {directory: ../utopia-api, type: directory} id: file:../utopia-api name: utopia-api - version: 0.4.3 + version: 0.4.4 requiresBuild: true peerDependencies: react: 18.1.0 @@ -18782,7 +18729,6 @@ packages: '@emotion/serialize': 1.0.2 '@emotion/styled': 11.0.0_kf2s5ttaepyzuejtstis4wzmwe '@emotion/styled-base': 11.0.0 - patch-package: 6.5.1 react: 18.1.0_47cciibm4ysmleigs33s763fqu react-dom: 18.1.0_abari7w75zfr6mrhvamxwmfpxm_react@18.1.0 uuid: 3.3.2 diff --git a/editor/src/components/canvas/canvas-globals.spec.ts b/editor/src/components/canvas/canvas-globals.spec.ts index 85c1af5f1b3b..6ca2b0af3d9f 100644 --- a/editor/src/components/canvas/canvas-globals.spec.ts +++ b/editor/src/components/canvas/canvas-globals.spec.ts @@ -27,6 +27,7 @@ const cardComponentDescriptor: ComponentDescriptor = { label: 'Title', }, }, + supportsChildren: false, variants: [ { insertMenuLabel: 'Card Default', @@ -77,6 +78,7 @@ const modifiedCardComponentDescriptor: ComponentDescriptor = { label: 'Border', }, }, + supportsChildren: false, variants: [ { insertMenuLabel: 'Card Default', @@ -123,6 +125,7 @@ const selectorComponentDescriptor: ComponentDescriptor = { options: ['True', 'False', 'FileNotFound'], }, }, + supportsChildren: false, variants: [ { insertMenuLabel: 'True False Selector', @@ -213,6 +216,7 @@ describe('validateControlsToCheck', () => { "label": "Title", }, }, + "supportsChildren": false, "variants": Array [ Object { "elementToInsert": [Function], @@ -351,6 +355,7 @@ describe('validateControlsToCheck', () => { "label": "Title", }, }, + "supportsChildren": false, "variants": Array [ Object { "elementToInsert": [Function], @@ -406,6 +411,7 @@ describe('validateControlsToCheck', () => { ], }, }, + "supportsChildren": false, "variants": Array [ Object { "elementToInsert": [Function], @@ -468,6 +474,7 @@ describe('validateControlsToCheck', () => { "label": "Title", }, }, + "supportsChildren": false, "variants": Array [ Object { "elementToInsert": [Function], @@ -518,6 +525,7 @@ describe('validateControlsToCheck', () => { "label": "Title", }, }, + "supportsChildren": false, "variants": Array [ Object { "elementToInsert": [Function], @@ -544,6 +552,7 @@ describe('validateControlsToCheck', () => { "label": "Title", }, }, + "supportsChildren": false, "variants": Array [ Object { "elementToInsert": [Function], diff --git a/editor/src/components/canvas/canvas-globals.ts b/editor/src/components/canvas/canvas-globals.ts index f5ecb3dfaa59..a0cfc4d45010 100644 --- a/editor/src/components/canvas/canvas-globals.ts +++ b/editor/src/components/canvas/canvas-globals.ts @@ -86,6 +86,7 @@ export async function validateControlsToCheck( (descriptorWithName) => { return { properties: descriptorWithName.properties, + supportsChildren: descriptorWithName.supportsChildren, variants: descriptorWithName.variants, } }, diff --git a/editor/src/components/canvas/ui/floating-insert-menu.tsx b/editor/src/components/canvas/ui/floating-insert-menu.tsx index ebb046bab9d2..47c5fa4d5671 100644 --- a/editor/src/components/canvas/ui/floating-insert-menu.tsx +++ b/editor/src/components/canvas/ui/floating-insert-menu.tsx @@ -273,6 +273,7 @@ export const CustomComponentOption = (props: OptionProps)
, ): ComponentDescriptor { return { properties: properties, + supportsChildren: supportsChildren, variants: variants, } } diff --git a/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx b/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx index b6114f3618ea..1e441c6c3c49 100644 --- a/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx +++ b/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx @@ -1,3 +1,4 @@ +import { screen } from '@testing-library/react' import { BakedInStoryboardUID } from '../../core/model/scene-utils' import { createTestProjectWithMultipleFiles } from '../../sample-projects/sample-project-utils.test-utils' import { @@ -1260,6 +1261,69 @@ export var Playground = () => { }) }) + describe('wrapping', () => { + it('can only wrap into elements that support children', async () => { + const renderResult = await renderTestEditorWithCode( + `import * as React from 'react' + import { Scene, Storyboard } from 'utopia-api' + + export var App = (props) => { + return
hello
+ } + + export var Card = ({ person }) => ( +

Hello, {person ?? 'John Doe'}

+ ) + + export var MyCustomContainer = ({ children }) => ( +
{children}
+ ) + + export var storyboard = (props) => { + return ( + + + + + + ) + } + `, + 'await-first-dom-report', + ) + await selectComponentsForTest(renderResult, [EP.fromString('sb/scene/app')]) + + const element = renderResult.renderedDOM.getByText('Wrap') + await mouseClickAtPoint(element, { x: 2, y: 2 }) + + const items = getFloatingMenuItems().map((e) => e.firstChild?.textContent) + + // `Card` is not included + expect(items).toEqual([ + 'MyCustomContainer', // uses `children` + 'div', // intrinsically supports children + 'span', // intrinsically supports children + 'button', // intrinsically supports children + 'Conditional', // supports children on the code level + 'Fragment', // supports children on the code level + 'Group', // uses `children` + 'View', // uses `children` + 'FlexRow', // uses `children` + 'FlexCol', // uses `children` + 'Scene', // uses `children` + ]) + }) + }) + describe('wrap in div', () => { const entryPoints = { 'click wrap in div button': async (renderResult: EditorRenderResult): Promise => { @@ -1546,3 +1610,7 @@ async function convertViaAddElementPopup(editor: EditorRenderResult, query: stri await pressKey('c') await searchInFloatingMenu(editor, query) } + +function getFloatingMenuItems() { + return screen.queryAllByTestId(/^floating-menu-item-/gi) +} diff --git a/editor/src/components/editor/export-utils.ts b/editor/src/components/editor/export-utils.ts index 586dcc5876de..68109cd8cec5 100644 --- a/editor/src/components/editor/export-utils.ts +++ b/editor/src/components/editor/export-utils.ts @@ -1,3 +1,4 @@ +import { elementUsesProperty } from '../../core/model/element-template-utils' import { BakedInStoryboardVariableName } from '../../core/model/scene-utils' import type { UtopiaJSXComponent } from '../../core/shared/element-template' import { isUtopiaJSXComponent } from '../../core/shared/element-template' @@ -39,112 +40,115 @@ function pathLastPartWithoutExtension(path: string): string { return splitByFullStop[0] } -export function getExportedComponentImports( +export function getExportedComponentImportsFromParseSuccess( originatingPath: string, fullPath: string, - textFile: ParsedTextFile, -): ExportedComponentImports | null { - return foldParsedTextFile( - () => { - return null - }, - (success: ParseSuccess) => { - const pathLastPart = pathLastPartWithoutExtension(fullPath) - let result: ExportedComponentImports = [] + success: ParseSuccess, +): ExportedComponentImports { + const pathLastPart = pathLastPartWithoutExtension(fullPath) + let result: ExportedComponentImports = [] - function isStoryboard(component: UtopiaJSXComponent): boolean { - return fullPath === StoryboardFilePath && component.name === BakedInStoryboardVariableName - } + function isStoryboard(component: UtopiaJSXComponent): boolean { + return fullPath === StoryboardFilePath && component.name === BakedInStoryboardVariableName + } - // All the heavy lifting for what to add happens in here. - function addToResult( - elementMatchesName: string | null, - listingName: string, - importDetailsToAdd: ImportDetails, - ): void { - for (const topLevelElement of success.topLevelElements) { - if ( - isUtopiaJSXComponent(topLevelElement) && - topLevelElement.name === elementMatchesName && - !isStoryboard(topLevelElement) - ) { - // Don't add an import if this is from the same file. - const importsToAdd = - originatingPath === fullPath ? emptyImports() : { [fullPath]: importDetailsToAdd } - result.push(exportedComponentDetail(importsToAdd, listingName)) - } - } + // All the heavy lifting for what to add happens in here. + function addToResult( + elementMatchesName: string | null, + listingName: string, + importDetailsToAdd: ImportDetails, + ): void { + for (const topLevelElement of success.topLevelElements) { + if ( + isUtopiaJSXComponent(topLevelElement) && + topLevelElement.name === elementMatchesName && + !isStoryboard(topLevelElement) + ) { + // Don't add an import if this is from the same file. + const importsToAdd = + originatingPath === fullPath ? emptyImports() : { [fullPath]: importDetailsToAdd } + result.push(exportedComponentDetail(importsToAdd, listingName)) } + } + } - for (const exportDetail of success.exportsDetail) { - switch (exportDetail.type) { - case 'EXPORT_DEFAULT_FUNCTION_OR_CLASS': - addToResult( - exportDetail.name, - exportDetail.name ?? '(default)', - importDetails(exportDetail.name ?? pathLastPart, [], null), - ) - break - case 'EXPORT_CLASS': - addToResult( - exportDetail.className, - exportDetail.className, - importDetails(null, [importAlias(exportDetail.className)], null), - ) - break - case 'EXPORT_FUNCTION': - addToResult( - exportDetail.functionName, - exportDetail.functionName, - importDetails(null, [importAlias(exportDetail.functionName)], null), - ) - break - case 'EXPORT_VARIABLES': - case 'EXPORT_DESTRUCTURED_ASSIGNMENT': - case 'REEXPORT_VARIABLES': - for (const exportVar of exportDetail.variables) { - const exportName = exportVar.variableAlias ?? exportVar.variableName - addToResult( - exportVar.variableName, - exportName === 'default' ? '(default)' : exportName, - importDetails( - null, - exportName === 'default' ? [] : [importAlias(exportVar.variableName)], - null, - ), - ) - } - break - case 'EXPORT_DEFAULT_IDENTIFIER': - addToResult( - exportDetail.name, - exportDetail.name, - importDetails(null, [importAlias(exportDetail.name)], null), - ) - break - case 'REEXPORT_WILDCARD': - break - case 'EXPORT_VARIABLES_WITH_MODIFIER': - for (const exportName of exportDetail.variables) { - addToResult( - exportName, - exportName === 'default' ? '(default)' : exportName, - importDetails( - null, - exportName === 'default' ? [] : [importAlias(exportName)], - null, - ), - ) - } - break - default: - const _exhaustiveCheck: never = exportDetail - throw new Error(`Unhandled type ${JSON.stringify(exportDetail)}`) + for (const exportDetail of success.exportsDetail) { + switch (exportDetail.type) { + case 'EXPORT_DEFAULT_FUNCTION_OR_CLASS': + addToResult( + exportDetail.name, + exportDetail.name ?? '(default)', + importDetails(exportDetail.name ?? pathLastPart, [], null), + ) + break + case 'EXPORT_CLASS': + addToResult( + exportDetail.className, + exportDetail.className, + importDetails(null, [importAlias(exportDetail.className)], null), + ) + break + case 'EXPORT_FUNCTION': + addToResult( + exportDetail.functionName, + exportDetail.functionName, + importDetails(null, [importAlias(exportDetail.functionName)], null), + ) + break + case 'EXPORT_VARIABLES': + case 'EXPORT_DESTRUCTURED_ASSIGNMENT': + case 'REEXPORT_VARIABLES': + for (const exportVar of exportDetail.variables) { + const exportName = exportVar.variableAlias ?? exportVar.variableName + addToResult( + exportVar.variableName, + exportName === 'default' ? '(default)' : exportName, + importDetails( + null, + exportName === 'default' ? [] : [importAlias(exportVar.variableName)], + null, + ), + ) } - } + break + case 'EXPORT_DEFAULT_IDENTIFIER': + addToResult( + exportDetail.name, + exportDetail.name, + importDetails(null, [importAlias(exportDetail.name)], null), + ) + break + case 'REEXPORT_WILDCARD': + break + case 'EXPORT_VARIABLES_WITH_MODIFIER': + for (const exportName of exportDetail.variables) { + addToResult( + exportName, + exportName === 'default' ? '(default)' : exportName, + importDetails(null, exportName === 'default' ? [] : [importAlias(exportName)], null), + ) + } + break + default: + const _exhaustiveCheck: never = exportDetail + throw new Error(`Unhandled type ${JSON.stringify(exportDetail)}`) + } + } + + return result +} - return result +export function getExportedComponentImports( + originatingPath: string, + fullPath: string, + textFile: ParsedTextFile, +): ExportedComponentImports | null { + return foldParsedTextFile( + () => { + return null }, + (success: ParseSuccess) => + getExportedComponentImportsFromParseSuccess(originatingPath, fullPath, success), () => { return null }, diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index 390ec45a70f5..ce12eab77545 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -3063,9 +3063,11 @@ export function PropertyControlsKeepDeepEquality( } export const ComponentDescriptorKeepDeepEquality: KeepDeepEqualityCall = - combine2EqualityCalls( + combine3EqualityCalls( (descriptor) => descriptor.properties, PropertyControlsKeepDeepEquality, + (descriptor) => descriptor.supportsChildren, + BooleanKeepDeepEquality, (descriptor) => descriptor.variants, arrayDeepEquality(ComponentInfoKeepDeepEquality), componentDescriptor, diff --git a/editor/src/components/inspector/common/property-controls-hooks.spec.tsx b/editor/src/components/inspector/common/property-controls-hooks.spec.tsx index 8eeaea81e98f..6bb22a50a0c4 100644 --- a/editor/src/components/inspector/common/property-controls-hooks.spec.tsx +++ b/editor/src/components/inspector/common/property-controls-hooks.spec.tsx @@ -177,6 +177,7 @@ function callPropertyControlsHook( '/utopia/storyboard': { App: { properties: propertyControlsForApp, + supportsChildren: false, variants: [ { insertMenuLabel: 'App', @@ -187,6 +188,7 @@ function callPropertyControlsHook( }, OtherComponent: { properties: propertyControlsForOtherComponent, + supportsChildren: false, variants: [ { insertMenuLabel: 'OtherComponent', diff --git a/editor/src/components/shared/project-components.spec.ts b/editor/src/components/shared/project-components.spec.ts index 6b13e7f6de45..27812de51130 100644 --- a/editor/src/components/shared/project-components.spec.ts +++ b/editor/src/components/shared/project-components.spec.ts @@ -47,6 +47,7 @@ describe('getComponentGroups', () => { '@heroicons/react/solid': { BeakerIcon: { properties: {}, + supportsChildren: false, variants: [], }, }, @@ -70,6 +71,7 @@ describe('getDependencyStatus', () => { '@heroicons/react/solid': { BeakerIcon: { properties: {}, + supportsChildren: false, variants: [], }, }, diff --git a/editor/src/components/shared/project-components.ts b/editor/src/components/shared/project-components.ts index 4574487a33ea..c5a41ec55e8e 100644 --- a/editor/src/components/shared/project-components.ts +++ b/editor/src/components/shared/project-components.ts @@ -4,7 +4,11 @@ import { hasStyleControls, propertyControlsForComponentInFile, } from '../../core/property-controls/property-controls-utils' -import type { JSXAttributes } from '../../core/shared/element-template' +import type { + JSXAttributes, + JSXElementChild, + UtopiaJSXComponent, +} from '../../core/shared/element-template' import { emptyComments, jsExpressionValue, @@ -39,13 +43,20 @@ import type { } from '../custom-code/code-file' import { clearComponentElementToInsertUniqueIDs } from '../custom-code/code-file' import { defaultElementStyle } from '../editor/defaults' -import { getExportedComponentImports } from '../editor/export-utils' +import { + getExportedComponentImports, + getExportedComponentImportsFromParseSuccess, +} from '../editor/export-utils' import { groupJSXElement, groupJSXElementImportsToAdd, } from '../canvas/canvas-strategies/strategies/group-helpers' import type { InsertMenuMode } from '../canvas/ui/floating-insert-menu-helpers' import { insertMenuModes } from '../canvas/ui/floating-insert-menu-helpers' +import { MetadataUtils } from '../../core/model/element-metadata-utils' +import { elementUsesProperty } from '../../core/model/element-template-utils' +import { intrinsicHTMLElementNamesThatSupportChildren } from '../../core/shared/dom-utils' +import { getTopLevelElementByExportsDetail } from '../../core/model/project-file-utils' export type StylePropOption = 'do-not-add' | 'add-size' @@ -283,12 +294,14 @@ function makeHTMLDescriptor( extraPropertyControls: PropertyControls, attributes?: () => JSXAttributes, ): ComponentDescriptor { + const supportsChildren = intrinsicHTMLElementNamesThatSupportChildren.includes(tag) const propertyControls: PropertyControls = { ...stockHTMLPropertyControls, ...extraPropertyControls, } return { properties: propertyControls, + supportsChildren: supportsChildren, variants: [ { insertMenuLabel: tag, @@ -377,6 +390,7 @@ const divComponentGroup = { const conditionalElementsDescriptors: ComponentDescriptorsForFile = { conditional: { properties: {}, + supportsChildren: true, variants: [ { insertMenuLabel: 'Conditional', @@ -397,6 +411,7 @@ const conditionalElementsDescriptors: ComponentDescriptorsForFile = { const groupElementsDescriptors: ComponentDescriptorsForFile = { group: { properties: {}, + supportsChildren: true, variants: [ { insertMenuLabel: 'Group', @@ -422,6 +437,7 @@ export const fragmentComponentInfo: ComponentInfo = { const fragmentElementsDescriptors: ComponentDescriptorsForFile = { fragment: { properties: {}, + supportsChildren: true, variants: [fragmentComponentInfo], }, } @@ -429,6 +445,7 @@ const fragmentElementsDescriptors: ComponentDescriptorsForFile = { const samplesDescriptors: ComponentDescriptorsForFile = { sampleText: { properties: {}, + supportsChildren: false, variants: [ { insertMenuLabel: 'Sample text', @@ -512,6 +529,27 @@ export function moveSceneToTheBeginningAndSetDefaultSize( return groups } +function isDescriptorEligibleForMode( + insertMenuMode: InsertMenuMode, + component: ComponentDescriptor, +): boolean { + return insertMenuMode === 'wrap' ? component.supportsChildren : true +} + +function isUtopiaJSXComponentEligibleForMode( + insertMenuMode: InsertMenuMode, + element: UtopiaJSXComponent | null, +): boolean { + if (insertMenuMode === 'wrap') { + return ( + element != null && + element.param != null && + elementUsesProperty(element.rootElement, element.param, 'children') + ) + } + return true +} + export function getComponentGroups( insertMenuMode: InsertMenuMode, packageStatus: PackageStatusMap, @@ -524,33 +562,37 @@ export function getComponentGroups( // Add entries for the exported components of files within the project. walkContentsTree(projectContents, (fullPath: string, file: ProjectFile) => { if (isTextFile(file)) { - const possibleExportedComponents = getExportedComponentImports( + if (file.fileContents.parsed.type !== 'PARSE_SUCCESS') { + return + } + const parsed = file.fileContents.parsed + const possibleExportedComponents = getExportedComponentImportsFromParseSuccess( originatingPath, fullPath, file.fileContents.parsed, ) - if (possibleExportedComponents != null) { - let insertableComponents: Array = [] - fastForEach(possibleExportedComponents, (exportedComponent) => { - const pathWithoutExtension = dropFileExtension(fullPath) - const propertyControls = propertyControlsForComponentInFile( - exportedComponent.listingName, - pathWithoutExtension, - propertyControlsInfo, - ) + let insertableComponents: Array = [] + fastForEach(possibleExportedComponents, (exportedComponent) => { + const pathWithoutExtension = dropFileExtension(fullPath) + const propertyControls = propertyControlsForComponentInFile( + exportedComponent.listingName, + pathWithoutExtension, + propertyControlsInfo, + ) - // Drill down into the property controls to see if this has an appropriate style object entry. - const stylePropOptions = hasStyleControls(propertyControls) - ? addSizeAndNotStyleProp - : doNotAddStyleProp - - const propertyControlsForDependency = - propertyControlsInfo[fullPath] ?? propertyControlsInfo[pathWithoutExtension] - if ( - propertyControlsForDependency != null && - propertyControlsForDependency[exportedComponent.listingName] != null - ) { - const descriptor = propertyControlsForDependency[exportedComponent.listingName] + // Drill down into the property controls to see if this has an appropriate style object entry. + const stylePropOptions = hasStyleControls(propertyControls) + ? addSizeAndNotStyleProp + : doNotAddStyleProp + + const propertyControlsForDependency = + propertyControlsInfo[fullPath] ?? propertyControlsInfo[pathWithoutExtension] + if ( + propertyControlsForDependency != null && + propertyControlsForDependency[exportedComponent.listingName] != null + ) { + const descriptor = propertyControlsForDependency[exportedComponent.listingName] + if (isDescriptorEligibleForMode(insertMenuMode, descriptor)) { fastForEach(descriptor.variants, (insertOption) => { insertableComponents.push( insertableComponent( @@ -562,7 +604,10 @@ export function getComponentGroups( ), ) }) - } else { + } + } else { + const element = getTopLevelElementByExportsDetail(parsed, exportedComponent.listingName) + if (element == null || isUtopiaJSXComponentEligibleForMode(insertMenuMode, element)) { insertableComponents.push( insertableComponent( exportedComponent.importsToAdd, @@ -573,19 +618,18 @@ export function getComponentGroups( ), ) } - }) - result.push( - insertableComponentGroup( - insertableComponentGroupProjectComponent(fullPath), - insertableComponents, - ), - ) - } + } + }) + result.push( + insertableComponentGroup( + insertableComponentGroupProjectComponent(fullPath), + insertableComponents, + ), + ) } }) function addDependencyDescriptor( - moduleName: string | null, groupType: InsertableComponentGroupType, components: ComponentDescriptorsForFile, ): void { @@ -597,42 +641,39 @@ export function getComponentGroups( const stylePropOptions = hasStyleControls(propertyControls) ? addSizeAndNotStyleProp : doNotAddStyleProp - - fastForEach(component.variants, (insertOption) => { - insertableComponents.push( - insertableComponent( - insertOption.importsToAdd, - insertOption.elementToInsert, - insertOption.insertMenuLabel, - stylePropOptions, - null, - ), - ) - }) + if (isDescriptorEligibleForMode(insertMenuMode, component)) { + fastForEach(component.variants, (insertOption) => { + insertableComponents.push( + insertableComponent( + insertOption.importsToAdd, + insertOption.elementToInsert, + insertOption.insertMenuLabel, + stylePropOptions, + null, + ), + ) + }) + } }) result.push(insertableComponentGroup(groupType, insertableComponents)) } - addDependencyDescriptor(null, insertableComponentGroupDiv(), divComponentGroup) + addDependencyDescriptor(insertableComponentGroupDiv(), divComponentGroup) // Add HTML entries. - addDependencyDescriptor(null, insertableComponentGroupHTML(), basicHTMLElementsDescriptors) + addDependencyDescriptor(insertableComponentGroupHTML(), basicHTMLElementsDescriptors) // Add conditionals group. - addDependencyDescriptor( - null, - insertableComponentGroupConditionals(), - conditionalElementsDescriptors, - ) + addDependencyDescriptor(insertableComponentGroupConditionals(), conditionalElementsDescriptors) // Add fragment group. - addDependencyDescriptor(null, insertableComponentGroupFragment(), fragmentElementsDescriptors) + addDependencyDescriptor(insertableComponentGroupFragment(), fragmentElementsDescriptors) // Add samples group. - addDependencyDescriptor(null, { type: 'SAMPLES_GROUP' }, samplesDescriptors) + addDependencyDescriptor({ type: 'SAMPLES_GROUP' }, samplesDescriptors) // Add groups group. - addDependencyDescriptor(null, insertableComponentGroupGroups(), groupElementsDescriptors) // TODO instead of this, use createWrapInGroupActions! + addDependencyDescriptor(insertableComponentGroupGroups(), groupElementsDescriptors) // TODO instead of this, use createWrapInGroupActions! // Add entries for dependencies of the project. for (const dependency of dependencies) { @@ -647,7 +688,6 @@ export function getComponentGroups( if (infoKey.startsWith(dependency.name)) { const propertyControlsForDependency = propertyControlsInfo[infoKey] addDependencyDescriptor( - dependency.name, insertableComponentGroupProjectDependency(dependency.name, dependencyStatus), propertyControlsForDependency, ) diff --git a/editor/src/components/text-editor/text-editor.spec.browser2.tsx b/editor/src/components/text-editor/text-editor.spec.browser2.tsx index 99da66b44416..c497313fb70f 100644 --- a/editor/src/components/text-editor/text-editor.spec.browser2.tsx +++ b/editor/src/components/text-editor/text-editor.spec.browser2.tsx @@ -1,10 +1,5 @@ import * as EP from '../../core/shared/element-path' -import { - expectSingleUndo2Saves, - expectSingleUndoNSaves, - setFeatureForBrowserTestsUseInDescribeBlockOnly, - wait, -} from '../../utils/utils.test-utils' +import { expectSingleUndo2Saves, expectSingleUndoNSaves, wait } from '../../utils/utils.test-utils' import type { Modifiers } from '../../utils/modifiers' import { altCmdModifier, cmdModifier, shiftCmdModifier } from '../../utils/modifiers' import { CanvasControlsContainerID } from '../canvas/controls/new-canvas-controls' diff --git a/editor/src/core/model/element-metadata-utils.ts b/editor/src/core/model/element-metadata-utils.ts index b13fd5b56d81..46d412868fc2 100644 --- a/editor/src/core/model/element-metadata-utils.ts +++ b/editor/src/core/model/element-metadata-utils.ts @@ -1078,6 +1078,13 @@ export const MetadataUtils = { } } }, + intrinsicElementThatSupportsChildren: (element: JSXElementChild) => { + return ( + isJSXElement(element) && + isIntrinsicHTMLElement(element.name) && + intrinsicHTMLElementNamesThatSupportChildren.includes(element.name.baseVariable) + ) + }, targetTextEditable( metadata: ElementInstanceMetadataMap, pathTree: ElementPathTrees, diff --git a/editor/src/core/model/project-file-utils.ts b/editor/src/core/model/project-file-utils.ts index 20023cec3e75..ba82be67b89e 100644 --- a/editor/src/core/model/project-file-utils.ts +++ b/editor/src/core/model/project-file-utils.ts @@ -915,3 +915,14 @@ export function fileExportsFunctionWithName( ) != null ) } + +export function getTopLevelElementByExportsDetail( + file: ParseSuccess, + nameToLookFor: string, +): UtopiaJSXComponent | null { + return ( + file.topLevelElements.find( + (t): t is UtopiaJSXComponent => t.type === 'UTOPIA_JSX_COMPONENT' && t.name === nameToLookFor, + ) ?? null + ) +} diff --git a/editor/src/core/property-controls/property-controls-local.spec.tsx b/editor/src/core/property-controls/property-controls-local.spec.tsx index 46f25ded449e..30dbd4120c8c 100644 --- a/editor/src/core/property-controls/property-controls-local.spec.tsx +++ b/editor/src/core/property-controls/property-controls-local.spec.tsx @@ -19,9 +19,10 @@ describe('registered property controls', () => { } registerModule( - '/src/card', + '/src/card.js', { Card: { + supportsChildren: false, properties: { label: { control: 'string-input', @@ -68,7 +69,7 @@ describe('registered property controls', () => { await wait(10) // this is quite ugly but we want to wait for a timeout(0) in ui-jsx-canvas before calling validateControlsToCheck const editorState = renderResult.getEditorState().editor - expect(editorState.propertyControlsInfo['/src/card']).toMatchInlineSnapshot(` + expect(editorState.propertyControlsInfo['/src/card.js']).toMatchInlineSnapshot(` Object { "Card": Object { "properties": Object { @@ -82,11 +83,12 @@ describe('registered property controls', () => { "control": "checkbox", }, }, + "supportsChildren": false, "variants": Array [ Object { "elementToInsert": [Function], "importsToAdd": Object { - "/src/card": Object { + "/src/card.js": Object { "importedAs": null, "importedFromWithin": Array [ Object { @@ -102,7 +104,7 @@ describe('registered property controls', () => { Object { "elementToInsert": [Function], "importsToAdd": Object { - "/src/card": Object { + "/src/card.js": Object { "importedAs": null, "importedFromWithin": Array [ Object { diff --git a/editor/src/core/property-controls/property-controls-local.ts b/editor/src/core/property-controls/property-controls-local.ts index 60553e52bab7..a6aba8cef68d 100644 --- a/editor/src/core/property-controls/property-controls-local.ts +++ b/editor/src/core/property-controls/property-controls-local.ts @@ -19,6 +19,7 @@ import { objectKeyParser, optionalObjectKeyParser, parseArray, + parseBoolean, parseObject, parseString, } from '../../utils/value-parser-utils' @@ -101,6 +102,7 @@ async function componentDescriptorForComponentToRegister( return mapEither((variants) => { return { componentName: componentName, + supportsChildren: componentToRegister.supportsChildren, properties: componentToRegister.properties, variants: variants, } @@ -164,14 +166,16 @@ function parseComponentInsertOption(value: unknown): ParseResult { - return applicative2Either( - (properties, variants) => { + return applicative3Either( + (properties, supportsChildren, variants) => { return { properties: properties, + supportsChildren: supportsChildren, variants: variants, } }, objectKeyParser(fullyParsePropertyControls, 'properties')(value), + objectKeyParser(parseBoolean, 'supportsChildren')(value), objectKeyParser(parseArray(parseComponentInsertOption), 'variants')(value), ) } diff --git a/editor/src/core/shared/project-file-types.ts b/editor/src/core/shared/project-file-types.ts index e809b0ce0b96..c61fdfe7f971 100644 --- a/editor/src/core/shared/project-file-types.ts +++ b/editor/src/core/shared/project-file-types.ts @@ -1,6 +1,10 @@ import type { ImportType, NormalisedFrame } from 'utopia-api/core' -import type { ArbitraryJSBlock, ImportStatement, TopLevelElement } from './element-template' -import { JSExpression, JSExpressionOtherJavaScript } from './element-template' +import type { + ArbitraryJSBlock, + ImportStatement, + TopLevelElement, + UtopiaJSXComponent, +} from './element-template' import type { ErrorMessage } from './error-messages' import { arrayEqualsByValue, objectEquals } from './utils' diff --git a/editor/src/core/third-party/antd-components.ts b/editor/src/core/third-party/antd-components.ts index a4d543f9519d..910f94868f4b 100644 --- a/editor/src/core/third-party/antd-components.ts +++ b/editor/src/core/third-party/antd-components.ts @@ -22,6 +22,7 @@ function createBasicComponent( ): ComponentDescriptor { return { properties: { ...StyleObjectProps, ...propertyControls }, + supportsChildren: false, // TODO variants: [ { insertMenuLabel: [baseVariable, ...propertyPathParts].join('.'), diff --git a/editor/src/core/third-party/react-three-fiber-components.ts b/editor/src/core/third-party/react-three-fiber-components.ts index 5228f3871c2e..e51a598e964a 100644 --- a/editor/src/core/third-party/react-three-fiber-components.ts +++ b/editor/src/core/third-party/react-three-fiber-components.ts @@ -39,6 +39,7 @@ function createBasicComponent( ): ComponentDescriptor { return { properties: propertyControls, + supportsChildren: false, variants: [ { insertMenuLabel: name, diff --git a/editor/src/core/third-party/remix-controls.ts b/editor/src/core/third-party/remix-controls.ts index bb71be1d4a6f..795bbdad6b2e 100644 --- a/editor/src/core/third-party/remix-controls.ts +++ b/editor/src/core/third-party/remix-controls.ts @@ -12,6 +12,7 @@ export const RemixRunReactComponents: ComponentDescriptorsForFile = { properties: { to: { control: 'string-input' }, }, + supportsChildren: true, variants: [ { insertMenuLabel: 'Link', @@ -38,6 +39,7 @@ export const RemixRunReactComponents: ComponentDescriptorsForFile = { }, Outlet: { properties: {}, + supportsChildren: false, variants: [ { insertMenuLabel: 'Outlet', diff --git a/editor/src/core/third-party/utopia-api-components.ts b/editor/src/core/third-party/utopia-api-components.ts index 5a28f46ad860..570821972117 100644 --- a/editor/src/core/third-party/utopia-api-components.ts +++ b/editor/src/core/third-party/utopia-api-components.ts @@ -6,7 +6,6 @@ import { defaultFlexRowOrColStyle, defaultRectangleElementStyle, defaultSceneElementStyle, - defaultTextElementStyle, defaultElementStyle, } from '../../components/editor/defaults' import type { JSExpression } from '../shared/element-template' @@ -14,6 +13,7 @@ import { emptyComments, jsxAttributesEntry, jsxElementWithoutUID } from '../shar const BasicUtopiaComponentDescriptor = ( name: string, + supportsChildren: boolean, styleProp: () => JSExpression, ): ComponentDescriptor => { return { @@ -22,6 +22,7 @@ const BasicUtopiaComponentDescriptor = ( control: 'style-controls', }, }, + supportsChildren: supportsChildren, variants: [ { insertMenuLabel: name, @@ -45,11 +46,13 @@ const BasicUtopiaComponentDescriptor = ( } export const UtopiaApiComponents: ComponentDescriptorsForFile = { - Ellipse: BasicUtopiaComponentDescriptor('Ellipse', defaultElementStyle), - Rectangle: BasicUtopiaComponentDescriptor('Rectangle', defaultRectangleElementStyle), - View: BasicUtopiaComponentDescriptor('View', defaultElementStyle), - FlexRow: BasicUtopiaComponentDescriptor('FlexRow', defaultFlexRowOrColStyle), - FlexCol: BasicUtopiaComponentDescriptor('FlexCol', defaultFlexRowOrColStyle), - Scene: BasicUtopiaComponentDescriptor('Scene', () => defaultSceneElementStyle(null)), - RemixScene: BasicUtopiaComponentDescriptor('RemixScene', () => defaultSceneElementStyle(null)), + Ellipse: BasicUtopiaComponentDescriptor('Ellipse', false, defaultElementStyle), + Rectangle: BasicUtopiaComponentDescriptor('Rectangle', false, defaultRectangleElementStyle), + View: BasicUtopiaComponentDescriptor('View', true, defaultElementStyle), + FlexRow: BasicUtopiaComponentDescriptor('FlexRow', true, defaultFlexRowOrColStyle), + FlexCol: BasicUtopiaComponentDescriptor('FlexCol', true, defaultFlexRowOrColStyle), + Scene: BasicUtopiaComponentDescriptor('Scene', true, () => defaultSceneElementStyle(null)), + RemixScene: BasicUtopiaComponentDescriptor('RemixScene', false, () => + defaultSceneElementStyle(null), + ), } diff --git a/utopia-api/package.json b/utopia-api/package.json index 98be9b7d842a..272203372d4d 100644 --- a/utopia-api/package.json +++ b/utopia-api/package.json @@ -1,6 +1,6 @@ { "name": "utopia-api", - "version": "0.4.3", + "version": "0.4.4", "description": "Utopia API", "main": "src/index.ts", "module": "dist/index.js", diff --git a/utopia-api/src/helpers/helper-functions.ts b/utopia-api/src/helpers/helper-functions.ts index 1a1b87f76cee..6bb8e948fb75 100644 --- a/utopia-api/src/helpers/helper-functions.ts +++ b/utopia-api/src/helpers/helper-functions.ts @@ -1,4 +1,4 @@ -import { PropertyControls, ImportType } from '../property-controls/property-controls' +import type { PropertyControls } from '../property-controls/property-controls' export interface ComponentInsertOption { code: string @@ -8,6 +8,7 @@ export interface ComponentInsertOption { export interface ComponentToRegister { properties: PropertyControls + supportsChildren: boolean variants: Array }