diff --git a/app/gui2/e2e/collapsingAndEntering.spec.ts b/app/gui2/e2e/collapsingAndEntering.spec.ts index aba9ac26e297..052cd3e1d5a0 100644 --- a/app/gui2/e2e/collapsingAndEntering.spec.ts +++ b/app/gui2/e2e/collapsingAndEntering.spec.ts @@ -11,16 +11,16 @@ test('Entering nodes', async ({ page }) => { await actions.goToGraph(page) await mockCollapsedFunctionInfo(page, 'final', 'func1') await expectInsideMain(page) - await expect(locate.navBreadcrumb(page)).toHaveText(['main']) + await expect(locate.navBreadcrumb(page)).toHaveText(['Mock Project']) await locate.graphNodeByBinding(page, 'final').dblclick() await mockCollapsedFunctionInfo(page, 'f2', 'func2') await expectInsideFunc1(page) - await expect(locate.navBreadcrumb(page)).toHaveText(['main', 'func1']) + await expect(locate.navBreadcrumb(page)).toHaveText(['Mock Project', 'func1']) await locate.graphNodeByBinding(page, 'f2').dblclick() await expectInsideFunc2(page) - await expect(locate.navBreadcrumb(page)).toHaveText(['main', 'func1', 'func2']) + await expect(locate.navBreadcrumb(page)).toHaveText(['Mock Project', 'func1', 'func2']) }) test('Leaving entered nodes', async ({ page }) => { @@ -42,7 +42,7 @@ test('Using breadcrumbs to navigate', async ({ page }) => { await page.mouse.dblclick(100, 100) await expectInsideMain(page) // Breadcrumbs still have all the crumbs, but the last two are dimmed. - await expect(locate.navBreadcrumb(page)).toHaveText(['main', 'func1', 'func2']) + await expect(locate.navBreadcrumb(page)).toHaveText(['Mock Project', 'func1', 'func2']) await expect(locate.navBreadcrumb(page, (f) => f.class('inactive'))).toHaveText([ 'func1', 'func2', @@ -51,7 +51,7 @@ test('Using breadcrumbs to navigate', async ({ page }) => { await locate.navBreadcrumb(page).filter({ hasText: 'func2' }).click() await expectInsideFunc2(page) - await locate.navBreadcrumb(page).filter({ hasText: 'main' }).click() + await locate.navBreadcrumb(page).filter({ hasText: 'Mock Project' }).click() await expectInsideMain(page) await locate.navBreadcrumb(page).filter({ hasText: 'func1' }).click() diff --git a/app/gui2/e2e/componentBrowser.spec.ts b/app/gui2/e2e/componentBrowser.spec.ts index ccebe783e672..5e7239fcd34d 100644 --- a/app/gui2/e2e/componentBrowser.spec.ts +++ b/app/gui2/e2e/componentBrowser.spec.ts @@ -5,7 +5,8 @@ import * as actions from './actions' import * as customExpect from './customExpect' import * as locate from './locate' -const ACCEPT_SUGGESTION_SHORTCUT = os.platform() === 'darwin' ? 'Meta+Enter' : 'Control+Enter' +const CONTROL_KEY = os.platform() === 'darwin' ? 'Meta' : 'Control' +const ACCEPT_SUGGESTION_SHORTCUT = `${CONTROL_KEY}+Enter` async function deselectAllNodes(page: Page) { await page.keyboard.press('Escape') @@ -148,3 +149,36 @@ test('Filtering list', async ({ page }) => { const highlighted = locate.componentBrowserEntry(page).locator('.component-label-segment.match') await expect(highlighted).toHaveText(['re', '_te']) }) + +test('Editing existing nodes', async ({ page }) => { + await actions.goToGraph(page) + const node = locate.graphNodeByBinding(page, 'data') + const ADDED_PATH = '"/home/enso/Input.txt"' + + // Start node editing + await locate.graphNodeIcon(node).click({ modifiers: [CONTROL_KEY] }) + await expect(locate.componentBrowser(page)).toBeVisible() + const input = locate.componentBrowserInput(page).locator('input') + await expect(input).toHaveValue('Data.read') + + // Add argument and accept + await page.keyboard.press('End') + await input.pressSequentially(` ${ADDED_PATH}`) + await expect(input).toHaveValue(`Data.read ${ADDED_PATH}`) + await page.keyboard.press('Enter') + await expect(locate.componentBrowser(page)).not.toBeVisible() + await expect(node.locator('.WidgetToken')).toHaveText(['Data', '.', 'read']) + await expect(node.locator('.WidgetText input')).toHaveValue(ADDED_PATH) + + // Edit again, using "edit" button + await locate.graphNodeIcon(node).click() + await node.getByTestId('edit-button').click() + await expect(locate.componentBrowser(page)).toBeVisible() + await expect(input).toHaveValue(`Data.read ${ADDED_PATH}`) + for (let i = 0; i < ADDED_PATH.length; ++i) await page.keyboard.press('Backspace') + await expect(input).toHaveValue('Data.read ') + await page.keyboard.press('Enter') + await expect(locate.componentBrowser(page)).not.toBeVisible() + await expect(node.locator('.WidgetToken')).toHaveText(['Data', '.', 'read']) + await expect(node.locator('.WidgetText')).not.toBeVisible() +}) diff --git a/app/gui2/e2e/locate.ts b/app/gui2/e2e/locate.ts index 190c0303e8c6..1dee7549ab66 100644 --- a/app/gui2/e2e/locate.ts +++ b/app/gui2/e2e/locate.ts @@ -113,6 +113,23 @@ export function exitFullscreenButton(page: Locator | Page) { export const toggleFullscreenButton = or(enterFullscreenButton, exitFullscreenButton) +// === Nodes === + +declare const nodeLocatorBrand: unique symbol +type Node = Locator & { [nodeLocatorBrand]: never } + +export function graphNode(page: Page | Locator): Node { + return page.locator('.GraphNode') as Node +} +export function graphNodeByBinding(page: Locator | Page, binding: string): Node { + return graphNode(page).filter({ + has: page.locator('.binding').and(page.getByText(binding)), + }) as Node +} +export function graphNodeIcon(node: Node) { + return node.locator('.icon') +} + // === Data locators === type SanitizeClassName = T extends `${infer A}.${infer B}` @@ -128,10 +145,6 @@ function componentLocator(className: SanitizeClassName) { } export const graphEditor = componentLocator('GraphEditor') -export const graphNode = componentLocator('GraphNode') -export function graphNodeByBinding(page: Locator | Page, binding: string) { - return graphNode(page).filter({ has: page.locator('.binding').and(page.getByText(binding)) }) -} // @ts-expect-error export const anyVisualization = componentLocator('GraphVisualization > *') export const circularMenu = componentLocator('CircularMenu') diff --git a/app/gui2/scripts/generateIconMetadata.js b/app/gui2/scripts/generateIconMetadata.js index 06e5f056e679..ab755ef1c2cb 100644 --- a/app/gui2/scripts/generateIconMetadata.js +++ b/app/gui2/scripts/generateIconMetadata.js @@ -11,8 +11,8 @@ console.info('Writing icon name type to "./src/util/iconName.ts"...') await fs.writeFile( './src/util/iconName.ts', `\ -// Generated by \`scripts/generateIcons.js\`. -// Please run \`npm run generate\` to regenerate this file whenever \`icons.svg\` is changed. +// Generated by \`scripts/generateIconMetadata.js\`. +// Please run \`npm run generate-metadata\` to regenerate this file whenever \`icons.svg\` is changed. import iconNames from '@/util/iconList.json' export type Icon = diff --git a/app/gui2/src/assets/icons.svg b/app/gui2/src/assets/icons.svg index 8bbd0d7d56f9..848cee9ef98b 100644 --- a/app/gui2/src/assets/icons.svg +++ b/app/gui2/src/assets/icons.svg @@ -152,6 +152,10 @@ + + + + @@ -248,6 +252,10 @@ + + + + @@ -367,6 +375,24 @@ + + + + + + + + + + + + + + + + + + @@ -650,6 +676,14 @@ + + + + + + + + @@ -893,6 +927,10 @@ + + + + diff --git a/app/gui2/src/components/CircularMenu.vue b/app/gui2/src/components/CircularMenu.vue index b5dcb5f9124a..b34d3ae9d781 100644 --- a/app/gui2/src/components/CircularMenu.vue +++ b/app/gui2/src/components/CircularMenu.vue @@ -31,7 +31,12 @@ const emit = defineEmits<{ :modelValue="props.isVisualizationVisible" @update:modelValue="emit('update:isVisualizationVisible', $event)" /> - + - + - + -import SvgIcon from '@/components/SvgIcon.vue' -import { useEvent } from '@/composables/events' -import { ref } from 'vue' - -const props = defineProps<{ modes: string[]; modelValue: string }>() -const emit = defineEmits<{ execute: []; 'update:modelValue': [mode: string] }>() - -const isDropdownOpen = ref(false) - -const executionModeSelectorNode = ref() - -function onDocumentClick(event: MouseEvent) { - if ( - event.target instanceof Node && - executionModeSelectorNode.value?.contains(event.target) === false - ) { - isDropdownOpen.value = false - } -} - -useEvent(document, 'pointerdown', onDocumentClick) - - - - - diff --git a/app/gui2/src/components/GraphEditor.vue b/app/gui2/src/components/GraphEditor.vue index 5cc5aa8b8c16..18712f90c3b7 100644 --- a/app/gui2/src/components/GraphEditor.vue +++ b/app/gui2/src/components/GraphEditor.vue @@ -38,7 +38,6 @@ import { computed, onMounted, onScopeDispose, onUnmounted, ref, watch } from 'vu import { ProjectManagerEvents } from '../../../ide-desktop/lib/dashboard/src/utilities/ProjectManager' import { type Usage } from './ComponentBrowser/input' -const EXECUTION_MODES = ['design', 'live'] // Assumed size of a newly created node. This is used to place the component browser. const DEFAULT_NODE_SIZE = new Vec2(0, 24) const gapBetweenNodes = 48.0 @@ -344,8 +343,8 @@ const codeEditorHandler = codeEditorBindings.handler({ }, }) -/** Track play button presses. */ -function onPlayButtonPress() { +/** Handle record-once button presses. */ +function onRecordOnceButtonPress() { projectStore.lsRpcConnection.then(async () => { const modeValue = projectStore.executionMode if (modeValue == undefined) { @@ -667,9 +666,7 @@ function handleEdgeDrop(source: AstId, position: Vec2) { @canceled="onComponentBrowserCancel" /> { startEvent != null && pos.absolute.distanceSquared(startPos) <= MAXIMUM_CLICK_DISTANCE_SQ ) { - nodeSelection?.handleSelectionOf(startEvent, new Set([nodeId.value])) + nodeSelection?.handleSelectionOf(event, new Set([nodeId.value])) handleNodeClick(event) menuVisible.value = MenuState.Partial } @@ -261,7 +261,7 @@ const nodeEditHandler = nodeEditBindings.handler({ }) function startEditingNode(position: Vec2 | undefined) { - let sourceOffset = 0 + let sourceOffset = props.node.rootSpan.code().length if (position != null) { let domNode, domOffset if ((document as any).caretPositionFromPoint) { @@ -588,7 +588,7 @@ function openFullMenu() { caret-shape: bar; height: var(--node-height); border-radius: var(--node-border-radius); - display: flex; + display: inline-flex; flex-direction: row; align-items: center; white-space: nowrap; diff --git a/app/gui2/src/components/GraphEditor/widgets/WidgetArgumentName.vue b/app/gui2/src/components/GraphEditor/widgets/WidgetArgumentName.vue index 86170bee1412..d9c4146dcff2 100644 --- a/app/gui2/src/components/GraphEditor/widgets/WidgetArgumentName.vue +++ b/app/gui2/src/components/GraphEditor/widgets/WidgetArgumentName.vue @@ -21,6 +21,11 @@ const showArgumentValue = computed(() => { const placeholder = computed(() => props.input instanceof ArgumentPlaceholder) const primary = computed(() => props.nesting < 2) + +const innerInput = computed(() => ({ + ...props.input, + [ArgumentNameShownKey]: true, +})) diff --git a/app/gui2/src/components/ProjectTitle.vue b/app/gui2/src/components/ProjectTitle.vue deleted file mode 100644 index 4f0c8bb9a55a..000000000000 --- a/app/gui2/src/components/ProjectTitle.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - - diff --git a/app/gui2/src/components/RecordControl.vue b/app/gui2/src/components/RecordControl.vue new file mode 100644 index 000000000000..b8322bb8c4ee --- /dev/null +++ b/app/gui2/src/components/RecordControl.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/app/gui2/src/components/TopBar.vue b/app/gui2/src/components/TopBar.vue index 64583e29872e..772f6dd090f0 100644 --- a/app/gui2/src/components/TopBar.vue +++ b/app/gui2/src/components/TopBar.vue @@ -2,25 +2,23 @@ import ExtendedMenu from '@/components/ExtendedMenu.vue' import NavBar from '@/components/NavBar.vue' import type { BreadcrumbItem } from '@/components/NavBreadcrumbs.vue' -import ProjectTitle from '@/components/ProjectTitle.vue' +import RecordControl from '@/components/RecordControl.vue' import { injectGuiConfig } from '@/providers/guiConfig' import { computed } from 'vue' const props = defineProps<{ - title: string breadcrumbs: BreadcrumbItem[] - modes: string[] - mode: string + recordMode: boolean allowNavigationLeft: boolean allowNavigationRight: boolean zoomLevel: number }>() const emit = defineEmits<{ - execute: [] + recordOnce: [] back: [] forward: [] breadcrumbClick: [index: number] - 'update:mode': [mode: string] + 'update:recordMode': [enabled: boolean] fitToAllClicked: [] zoomIn: [] zoomOut: [] @@ -40,12 +38,10 @@ const barStyle = computed(() => {