diff --git a/editor/src/components/canvas/design-panel-root.tsx b/editor/src/components/canvas/design-panel-root.tsx index 68d027d4950d..41526aecc865 100644 --- a/editor/src/components/canvas/design-panel-root.tsx +++ b/editor/src/components/canvas/design-panel-root.tsx @@ -23,6 +23,7 @@ import { CanvasStrategyInspector } from './canvas-strategies/canvas-strategy-ins import { getQueryParam } from '../../common/env-vars' import { when } from '../../utils/react-conditionals' import { InsertMenuPane } from '../navigator/insert-menu-pane' +import { VariablesMenuPane } from '../navigator/variables-menu-pane' import { useDispatch } from '../editor/store/dispatch-context' import { GridPanelsContainer } from './grid-panels-container' import { TitleBarCode, TitleBarUserProfile } from '../titlebar/title-bar' @@ -212,6 +213,10 @@ export const RightPane = React.memo((props) => { onClickTab(RightMenuTab.Insert) }, [onClickTab]) + const onClickVariablesTab = React.useCallback(() => { + onClickTab(RightMenuTab.Variables) + }, [onClickTab]) + const onClickCommentsTab = React.useCallback(() => { onClickTab(RightMenuTab.Comments) }, [onClickTab]) @@ -253,6 +258,11 @@ export const RightPane = React.memo((props) => { selected={selectedTab === RightMenuTab.Insert} onClick={onClickInsertTab} /> + {when( isFeatureEnabled('Commenting'), ((props) => { }} > {when(selectedTab === RightMenuTab.Insert, )} + {when(selectedTab === RightMenuTab.Variables, )} {when(selectedTab === RightMenuTab.Inspector, )} {when(selectedTab === RightMenuTab.Settings, )} {when(selectedTab === RightMenuTab.Comments, )} diff --git a/editor/src/components/common/actions/index.ts b/editor/src/components/common/actions/index.ts index b58160a0e349..ab21faae31d6 100644 --- a/editor/src/components/common/actions/index.ts +++ b/editor/src/components/common/actions/index.ts @@ -6,6 +6,7 @@ export type LeftMenuPanel = | 'genericExternalResources' | 'googleFontsResources' | 'insertmenu' + | 'variablesmenu' | 'projectsettings' | 'githuboptions' @@ -43,6 +44,8 @@ export function paneForPanel(panel: EditorPanel | null): EditorPane | null { return 'leftmenu' case 'insertmenu': return 'rightmenu' + case 'variablesmenu': + return 'rightmenu' case 'projectsettings': return 'leftmenu' case 'navigator': diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index a2f259367338..4adf961d68b0 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -2653,6 +2653,7 @@ export const UPDATE_FNS = { case 'canvas': case 'center': case 'insertmenu': + case 'variablesmenu': case 'projectsettings': case 'githuboptions': return editor @@ -2759,6 +2760,7 @@ export const UPDATE_FNS = { case 'misccodeeditor': case 'center': case 'insertmenu': + case 'variablesmenu': case 'githuboptions': return editor default: diff --git a/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx b/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx index b57dc2ba778c..8ec5f866e313 100644 --- a/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx +++ b/editor/src/components/editor/canvas-toolbar.spec.browser2.tsx @@ -549,7 +549,7 @@ describe('canvas toolbar', () => { FOR_TESTS_setNextGeneratedUids(['reserved', 'sample-text']) - await insertViaAddElementPopup(editor, 'myObj') + await insertViaAddElementPopup(editor, 'myObj.test') expect(getPrintedUiJsCode(editor.getEditorState())).toEqual( makeTestProjectCodeWithComponentInnards(` @@ -567,7 +567,7 @@ describe('canvas toolbar', () => { data-uid='container' >
- {JSON.stringify(myObj)} + {myObj.test}
)`), ) diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index c8e4a93c83ab..c6cab6fe2591 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -203,6 +203,7 @@ export enum RightMenuTab { Inspector = 'inspector', Settings = 'settings', Comments = 'comments', + Variables = 'variables', } // TODO: this should just contain an NpmDependency and a status diff --git a/editor/src/components/editor/variablesmenu.spec.browser2.tsx b/editor/src/components/editor/variablesmenu.spec.browser2.tsx new file mode 100644 index 000000000000..e34c20e61cf9 --- /dev/null +++ b/editor/src/components/editor/variablesmenu.spec.browser2.tsx @@ -0,0 +1,227 @@ +import { act, fireEvent, screen } from '@testing-library/react' +import type { EditorRenderResult } from '../canvas/ui-jsx.test-utils' +import { + TestAppUID, + TestSceneUID, + getPrintedUiJsCode, + makeTestProjectCodeWithComponentInnards, + makeTestProjectCodeWithSnippet, + renderTestEditorWithCode, +} from '../canvas/ui-jsx.test-utils' +import * as EP from '../../core/shared/element-path' +import { setPanelVisibility, setRightMenuTab } from './actions/action-creators' +import { RightMenuTab } from './store/editor-state' +import { selectComponentsForTest } from '../../utils/utils.test-utils' +import { BakedInStoryboardUID } from '../../core/model/scene-utils' +import { forceNotNull } from '../../core/shared/optional-utils' +import { InsertMenuFilterTestId } from './insertmenu' + +function getInsertItems() { + return screen.queryAllByTestId(/^insert-item-/gi) +} + +function openVariablesMenu(renderResult: EditorRenderResult) { + return renderResult.dispatch( + [setPanelVisibility('rightmenu', true), setRightMenuTab(RightMenuTab.Variables)], + true, + ) +} + +describe('variables menu', () => { + describe('filter search', () => { + it('can filter by variable name', async () => { + const editor = await renderTestEditorWithCode( + makeTestProjectCodeWithComponentInnards(` + const myObj = { test: 'test', num: 5, image: 'img.png' } + return ( +
+
+
+ )`), + 'await-first-dom-report', + ) + + await selectComponentsForTest(editor, [ + EP.fromString(`${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:container`), + ]) + + await openVariablesMenu(editor) + + expect(getInsertItems().length).toEqual(3) + + document.execCommand('insertText', false, 'myObj.im') + + expect(getInsertItems().length).toEqual(1) + expect(getInsertItems()[0].innerText).toEqual('myObj.image') + }) + + describe('no match', () => { + it('shows all elements', async () => { + const renderResult = await renderTestEditorWithCode( + makeTestProjectCodeWithSnippet(`
`), + 'await-first-dom-report', + ) + + await openVariablesMenu(renderResult) + + expect(getInsertItems().length).toEqual(0) + }) + }) + + it('does not show insertables that cannot be inserted', async () => { + const editor = await renderTestEditorWithCode( + makeTestProjectCodeWithComponentInnards(` + const myObj = { test: 'test', num: 5, image: 'img.png' } + return ( +
+
+
+ )`), + 'await-first-dom-report', + ) + + await openVariablesMenu(editor) + + expect(getInsertItems().length).toEqual(0) + }) + }) + + describe('insertion', () => { + it('inserts an image from within an object', async () => { + const editor = await renderTestEditorWithCode( + makeTestProjectCodeWithComponentInnards(` + const myObj = { test: 'test', num: 5, image: 'img.png' } + return ( +
+
+
+ )`), + 'await-first-dom-report', + ) + + await selectComponentsForTest(editor, [ + EP.fromString(`${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:container`), + ]) + + await openVariablesMenu(editor) + + document.execCommand('insertText', false, 'myObj.image') + + const filterBox = await screen.findByTestId(InsertMenuFilterTestId) + forceNotNull('the filter box must not be null', filterBox) + + await act(async () => { + fireEvent.keyDown(filterBox, { key: 'Enter', keycode: 13 }) + }) + + expect(getPrintedUiJsCode(editor.getEditorState())).toEqual( + makeTestProjectCodeWithComponentInnards(` + const myObj = { test: 'test', num: 5, image: 'img.png' } + return ( +
+
+ +
+ )`), + ) + }) + it('inserts a text', async () => { + const editor = await renderTestEditorWithCode( + makeTestProjectCodeWithComponentInnards(` + const myText = '' + return ( +
+
+
+ )`), + 'await-first-dom-report', + ) + + await selectComponentsForTest(editor, [ + EP.fromString(`${BakedInStoryboardUID}/${TestSceneUID}/${TestAppUID}:container`), + ]) + + await openVariablesMenu(editor) + + document.execCommand('insertText', false, 'myText') + + const filterBox = await screen.findByTestId(InsertMenuFilterTestId) + forceNotNull('the filter box must not be null', filterBox) + + await act(async () => { + fireEvent.keyDown(filterBox, { key: 'Enter', keycode: 13 }) + }) + + expect(getPrintedUiJsCode(editor.getEditorState())).toEqual( + makeTestProjectCodeWithComponentInnards(` + const myText = '' + return ( +
+
+ {myText} +
+ )`), + ) + }) + }) +}) diff --git a/editor/src/components/editor/variablesmenu.tsx b/editor/src/components/editor/variablesmenu.tsx new file mode 100644 index 000000000000..e04c42ab0be8 --- /dev/null +++ b/editor/src/components/editor/variablesmenu.tsx @@ -0,0 +1,400 @@ +/** @jsxRuntime classic */ +/** @jsx jsx */ +import type { CSSObject } from '@emotion/react' +import { jsx } from '@emotion/react' +import React, { useState } from 'react' +import type { + InputActionMeta, + InputProps, + MenuListComponentProps, + OptionProps, + StylesConfig, +} from 'react-windowed-select' +import WindowedSelect, { components, createFilter } from 'react-windowed-select' +import { RightMenuTab } from './store/editor-state' +import { Icn, UIRow, UtopiaTheme, useColorTheme } from '../../uuiui' +import { getControlStyles } from '../../uuiui-deps' +import { InspectorInputEmotionStyle } from '../../uuiui/inputs/base-input' +import type { ProjectContentTreeRoot } from '../assets' +import type { InsertableComponent, InsertableComponentGroup } from '../shared/project-components' +import { getInsertableGroupLabel } from '../shared/project-components' +import { setRightMenuTab } from './actions/action-creators' +import type { Mode } from './editor-modes' +import { useDispatch } from './store/dispatch-context' +import { Substores, useEditorState } from './store/store-hook' +import type { AllVariablesInScope } from '../shared/scoped-variables' +import { convertVariablesToElements, getVariablesInScope } from '../shared/scoped-variables' +import { useToInsert } from './insert-callbacks' +import type { InsertMenuItem, InsertableComponentFlatList } from '../canvas/ui/floating-insert-menu' +import { optionalMap } from '../../core/shared/optional-utils' + +export const VariablesMenuFilterTestId = 'insert-menu-filter' + +interface VariablesMenuProps { + mode: Mode + currentlyOpenFilename: string | null + projectContents: ProjectContentTreeRoot + scopedVariables: AllVariablesInScope[] +} + +export const VariablesMenu = React.memo(() => { + const restOfEditorProps = useEditorState( + Substores.restOfEditor, + (store) => { + return { + mode: store.editor.mode, + } + }, + 'VariablesMenu restOfEditorProps', + ) + + const selectedViews = useEditorState( + Substores.selectedViews, + (store) => store.editor.selectedViews, + 'VariablesMenu selectedViews', + ) + + const canvasProps = useEditorState( + Substores.canvas, + (store) => { + return { + currentlyOpenFilename: store.editor.canvas.openFile?.filename ?? null, + canvasScale: store.editor.canvas.scale, + } + }, + 'VariablesMenu canvasProps', + ) + + const projectContents = useEditorState( + Substores.projectContents, + (store) => store.editor.projectContents, + 'VariablesMenu projectContents', + ) + + const scopedVariables = useEditorState( + Substores.variablesInScope, + (store) => + getVariablesInScope(selectedViews[0], projectContents, store.editor.variablesInScope), + 'VariablesMenu scopedVariables', + ) + + const propsWithDependencies: VariablesMenuProps = { + ...restOfEditorProps, + ...canvasProps, + projectContents: projectContents, + scopedVariables: scopedVariables, + } + + return +}) + +const Input = (props: InputProps) => { + return +} + +const iconByType = (insertMenuItem: InsertMenuItem): string => { + const variableType = insertMenuItem.value.metadata?.variableType as string + + const iconsByType: Record = { + string: 'text', + number: 'text', + boolean: 'conditional', + object: 'text-generated', + array: 'lists', + image: 'image', + } + return iconsByType[variableType] ?? 'component' +} + +const Option = React.memo((props: OptionProps) => { + const colorTheme = useColorTheme() + const [isHovered, setIsHovered] = useState(false) + const setIsHoveredTrue = React.useCallback(() => { + setIsHovered(true) + }, []) + + const setIsHoveredFalse = React.useCallback(() => { + setIsHovered(false) + }, []) + + return ( +
+ + + {props.label} + +
+ ) +}) + +type GroupOptionItem = { + label: string + options: ComponentOptionItem[] +} + +type ComponentOptionItem = { + label: string + source: string + value: InsertableComponent +} + +function useSelectStyles( + hasResults: boolean, + hasOptions: boolean, +): StylesConfig { + const colorTheme = useColorTheme() + return React.useMemo( + () => ({ + container: (styles): CSSObject => ({ + height: '100%', + }), + control: (styles): CSSObject => ({ + background: 'transparent', + outline: 'none', + ':focus-within': { + outline: 'none', + border: 'none', + }, + padding: 8, + }), + valueContainer: (styles): CSSObject => ({ + display: 'flex', + position: 'relative', + flexGrow: 1, + flexShrink: 0, + alignItems: 'center', + gap: 4, + }), + indicatorsContainer: (styles): CSSObject => ({ + display: 'none', + }), + menu: (styles): CSSObject => { + return { + marginTop: 8, + paddingLeft: 8, + paddingRight: 8, + height: '100%', + } + }, + menuList: (styles): CSSObject => { + return { + height: '100%', + overflow: 'scroll', + paddingBottom: 100, + display: 'flex', + flexDirection: 'column', + gap: 2, + paddingLeft: 8, + paddingRight: 8, + } + }, + input: (styles): CSSObject => { + return { + ...(InspectorInputEmotionStyle({ + hasLabel: false, + controlStyles: getControlStyles(hasOptions ? 'simple' : 'disabled'), + }) as CSSObject), + paddingLeft: 4, + backgroundColor: colorTheme.bg2.value, + flexGrow: 1, + display: 'flex', + alignItems: 'center', + cursor: hasOptions ? 'text' : 'default', + border: `1px solid ${hasOptions && !hasResults ? colorTheme.error.value : 'transparent'}`, + opacity: hasOptions ? 1 : 0.5, + } + }, + placeholder: (styles): CSSObject => { + return { + ...styles, + position: 'absolute', + marginLeft: 5, + } + }, + group: (): CSSObject => { + return { + display: 'flex', + flexDirection: 'column', + gap: 2, + } + }, + groupHeading: (styles): CSSObject => { + return { + display: 'flex', + alignItems: 'center', + height: UtopiaTheme.layout.rowHeight.smaller, + fontWeight: 700, + } + }, + }), + [colorTheme.bg2.value, colorTheme.error.value, hasOptions, hasResults], + ) +} + +const MenuList = React.memo((menuListProps: MenuListComponentProps) => { + return +}) + +function convertInsertableComponentsToFlatList( + insertableComponents: InsertableComponentGroup[], +): InsertableComponentFlatList { + return insertableComponents.flatMap((componentGroup) => { + return { + label: getInsertableGroupLabel(componentGroup.source), + options: componentGroup.insertableComponents.map( + (componentToBeInserted, index): InsertMenuItem => { + const source = index === 0 ? componentGroup.source : null + return { + label: componentToBeInserted.name, + source: optionalMap(getInsertableGroupLabel, source), + value: { + ...componentToBeInserted, + key: `${getInsertableGroupLabel(componentGroup.source)}-${ + componentToBeInserted.name + }`, + source: source, + }, + } + }, + ), + } + }) +} + +const VariablesMenuInner = React.memo((props: VariablesMenuProps) => { + const toInsertCallback = useToInsert() + + const toInsertAndClose = React.useCallback( + (toInsert: InsertMenuItem | null) => { + toInsertCallback(toInsert) + }, + [toInsertCallback], + ) + + const dispatch = useDispatch() + const [filter, setFilter] = React.useState('') + + const insertableVariables = React.useMemo(() => { + if (props.currentlyOpenFilename == null) { + return [] + } else { + return convertInsertableComponentsToFlatList( + convertVariablesToElements(props.scopedVariables), + ) + } + }, [props.currentlyOpenFilename, props.scopedVariables]) + + const filterOption = createFilter({ + ignoreAccents: true, + stringify: (c) => c.data.source + c.data.label, + ignoreCase: true, + trim: true, + matchFrom: 'any', + }) + + const { hasResults } = React.useMemo(() => { + const filteredOptions = insertableVariables + .flatMap((g) => g.options) + .filter((o) => filterOption({ data: o } as any, filter)) + return { + hasResults: filteredOptions.length > 0, + } + }, [insertableVariables, filterOption, filter]) + + const { hasOptions } = React.useMemo(() => { + return { + hasOptions: insertableVariables.some((g) => g.options.length > 0), + } + }, [insertableVariables]) + + function onFilterChange(newValue: string, actionMeta: InputActionMeta) { + if (actionMeta.action !== 'input-blur' && actionMeta.action !== 'menu-close') { + setFilter(newValue.trim()) + } + } + + const onKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + setIsActive(true) + if (e.key === 'Escape') { + dispatch([setRightMenuTab(RightMenuTab.Inspector)]) + } + }, + [dispatch], + ) + + const onChange = React.useCallback( + (e: InsertMenuItem) => { + onFilterChange(e.label, { action: 'input-change' }) + toInsertAndClose(e) + }, + [toInsertAndClose], + ) + + function alwaysTrue() { + return true + } + + function noVariablesMessage() { + return hasOptions ? 'No results' : 'No variables in scope' + } + + const styles = useSelectStyles(hasResults, hasOptions) + + const [isActive, setIsActive] = React.useState(true) + function onMouseLeave() { + setIsActive(false) + } + function onMouseEnter() { + setIsActive(true) + } + + return ( +
+ +
+ ) +}) diff --git a/editor/src/components/navigator/variables-menu-pane.tsx b/editor/src/components/navigator/variables-menu-pane.tsx new file mode 100644 index 000000000000..4a7053e8168c --- /dev/null +++ b/editor/src/components/navigator/variables-menu-pane.tsx @@ -0,0 +1,60 @@ +/** @jsxRuntime classic */ +/** @jsx jsx */ +/** @jsxFrag React.Fragment */ +import { jsx } from '@emotion/react' +import React from 'react' +import { Section, SectionBodyArea } from '../../uuiui' +import { setFocus } from '../common/actions' +import type { EditorDispatch, LoginState } from '../editor/action-types' +import { VariablesMenu } from '../editor/variablesmenu' +import { useDispatch } from '../editor/store/dispatch-context' +import type { DerivedState, EditorState } from '../editor/store/editor-state' +import { RightMenuTab } from '../editor/store/editor-state' +import { Substores, useEditorState } from '../editor/store/store-hook' +import { setRightMenuTab } from '../editor/actions/action-creators' + +export interface LeftPaneProps { + editorState: EditorState + derivedState: DerivedState + editorDispatch: EditorDispatch + loginState: LoginState +} + +export const VariablesMenuPane = React.memo(() => { + const dispatch = useDispatch() + const { focusedPanel } = useEditorState( + Substores.restOfEditor, + (store) => { + return { + focusedPanel: store.editor.focusedPanel, + } + }, + 'VariablesMenuPane', + ) + + const onFocus = React.useCallback( + (event: React.FocusEvent) => { + if (focusedPanel !== 'variablesmenu') { + dispatch([setFocus('variablesmenu')], 'everyone') + } + }, + [dispatch, focusedPanel], + ) + + const onClickClose = React.useCallback(() => { + dispatch([setRightMenuTab(RightMenuTab.Inspector)]) + }, [dispatch]) + + return ( +
+ + + +
+ ) +}) diff --git a/editor/src/components/shared/project-components.ts b/editor/src/components/shared/project-components.ts index c5a41ec55e8e..4d0405f13a67 100644 --- a/editor/src/components/shared/project-components.ts +++ b/editor/src/components/shared/project-components.ts @@ -66,6 +66,7 @@ export interface InsertableComponent { name: string stylePropOptions: Array defaultSize: Size | null + metadata?: Record } export function insertableComponent( @@ -74,14 +75,22 @@ export function insertableComponent( name: string, stylePropOptions: Array, defaultSize: Size | null, + metadata?: Record, ): InsertableComponent { - return { + const component = { importsToAdd: importsToAdd, element: element, name: name, stylePropOptions: stylePropOptions, defaultSize: defaultSize, } + if (metadata != null) { + return { + ...component, + metadata: metadata, + } + } + return component } export function clearInsertableComponentUniqueIDs( diff --git a/editor/src/components/shared/scoped-variables.ts b/editor/src/components/shared/scoped-variables.ts index ac60d3628877..d38f506e3450 100644 --- a/editor/src/components/shared/scoped-variables.ts +++ b/editor/src/components/shared/scoped-variables.ts @@ -83,11 +83,23 @@ function getVariablesFromComponent( const jsxComponentVariables = variablesInScopeFromEditorState[elementPathString] ?? {} return { filePath: jsxComponent?.name ?? 'Component', - variables: Object.entries(jsxComponentVariables).map(([name, value]) => { - return { + variables: Object.entries(jsxComponentVariables).flatMap(([name, value]) => { + const type = getTypeByValue(value) + const variable = { name, value, - type: getTypeByValue(value), + type, + } + if (type === 'object' && value != null) { + // iterate only first-level keys of object + return Object.entries(value).map(([key, innerValue]) => ({ + name: `${name}.${key}`, + value: innerValue, + type: getTypeByValue(innerValue), + parent: variable, + })) + } else { + return variable } }), } @@ -106,6 +118,7 @@ export function convertVariablesToElements( variable.name, [], null, + { variableType: variable.type }, ) }), } @@ -117,15 +130,16 @@ function getMatchingElementForVariable(variable: Variable): ComponentElementToIn } function getMatchingElementForVariableInner(variable: Variable): InsertableComponentAndJSX { + const originalVariableName = variable.parent != null ? variable.parent.name : variable.name switch (variable.type) { case 'string': - return simpleInsertableComponentAndJsx('span', variable.name) + return simpleInsertableComponentAndJsx('span', variable.name, originalVariableName) case 'number': - return simpleInsertableComponentAndJsx('span', variable.name) + return simpleInsertableComponentAndJsx('span', variable.name, originalVariableName) case 'boolean': return insertableComponentAndJSX( jsxConditionalExpressionWithoutUID( - jsIdentifierName(variable.name), + jsExpressionOtherJavaScriptSimple(variable.name, [originalVariableName]), variable.name, jsExpressionValue(null, emptyComments), jsExpressionValue(null, emptyComments), @@ -137,7 +151,7 @@ function getMatchingElementForVariableInner(variable: Variable): InsertableCompo return simpleInsertableComponentAndJsx( 'span', `JSON.stringify(${variable.name})`, - variable.name, + originalVariableName, ) case 'array': return arrayInsertableComponentAndJsx(variable) @@ -146,7 +160,7 @@ function getMatchingElementForVariableInner(variable: Variable): InsertableCompo jsxElementWithoutUID( 'img', jsxAttributesFromMap({ - src: jsIdentifierName(variable.name), + src: jsExpressionOtherJavaScriptSimple(variable.name, [originalVariableName]), }), [], ), @@ -156,7 +170,7 @@ function getMatchingElementForVariableInner(variable: Variable): InsertableCompo return simpleInsertableComponentAndJsx( 'span', `JSON.stringify(${variable.name})`, - variable.name, + originalVariableName, ) } } @@ -182,10 +196,6 @@ function arrayInsertableComponentAndJsx(variable: Variable): InsertableComponent return insertableComponentAndJSX(arrayElement, arrayElementString) } -function jsIdentifierName(name: string): JSExpressionOtherJavaScript { - return jsExpressionOtherJavaScriptSimple(name, [name]) -} - function getArrayType(arrayVariable: Variable): InsertableType { const arr = arrayVariable.value as unknown[] const types = new Set(arr.map((item) => getTypeByValue(item))) @@ -220,7 +230,7 @@ interface InsertableComponentAndJSX { jsx: string } -type AllVariablesInScope = { +export type AllVariablesInScope = { filePath: string variables: Variable[] } @@ -241,4 +251,5 @@ interface Variable { name: string type: InsertableType value?: unknown + parent?: Variable }