Replace execution context controls with record mode controls (#9133)

Closes #8673. Stacked on #9132.

Before:
![image](https://github.com/enso-org/enso/assets/1047859/1b1ca9f4-eda9-45fd-bec1-cd7e255112fa)
![image](https://github.com/enso-org/enso/assets/1047859/b8cf6de1-d75c-48d0-ac0d-9e6e0c659dd4)

After:
![image](https://github.com/enso-org/enso/assets/1047859/91d13862-96e7-4175-a61e-40aecd6d62b0)
![image](https://github.com/enso-org/enso/assets/1047859/ef20511f-00f2-4042-b4d4-97728817500b)

# Important Notes
- The model has been refactored from execution-modes (live/design) to record-mode (on/off) up to the point of the project store, where it's translated (since the backend still uses the live/design concepts).
This commit is contained in:
Kaz Wesley 2024-02-23 12:10:43 -08:00 committed by GitHub
parent 6aa213757b
commit 245ec7afe8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 98 additions and 169 deletions

View File

@ -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 =

View File

@ -676,6 +676,14 @@
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 8C0 6.14915 0.641755 4.35557 1.81592 2.92485C2.99008 1.49413 4.624 0.514797 6.43928 0.153715C8.25456 -0.207366 10.1389 0.0721462 11.7712 0.944627C13.4035 1.81711 14.6827 3.22857 15.391 4.93853C16.0993 6.64848 16.1928 8.55113 15.6555 10.3223C15.1183 12.0934 13.9835 13.6235 12.4446 14.6518C10.9056 15.68 9.05779 16.1429 7.21586 15.9615C5.37394 15.7801 3.65189 14.9656 2.34315 13.6569L3.76867 12.2313C4.74761 13.2103 6.0357 13.8195 7.41347 13.9552C8.79123 14.0909 10.1734 13.7447 11.3245 12.9755C12.4756 12.2064 13.3245 11.0619 13.7263 9.73706C14.1282 8.41224 14.0583 6.98907 13.5285 5.71002C12.9987 4.43097 12.0418 3.3752 10.8208 2.72258C9.59988 2.06996 8.19041 1.86089 6.83258 2.13098C5.47475 2.40107 4.25258 3.13361 3.37431 4.20379C2.49603 5.27397 2.016 6.61557 2.016 8H4L1 12L-2 8H0ZM8 4C7.44772 4 7 4.44771 7 5V8C7 8.37712 7.20876 8.70549 7.51702 8.87584L9.77767 10.181C10.256 10.4572 10.8675 10.2933 11.1437 9.815C11.4198 9.33671 11.256 8.72512 10.7777 8.44897L9 7.42264V5C9 4.44771 8.55228 4 8 4Z" fill="currentColor"></path>
</symbol>
<symbol id="record" viewBox="0 0 16 16" width="16" height="16" fill="none">
<path d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15Z" fill="currentColor"></path>
</symbol>
<symbol id="record_once" viewBox="0 0 16 16" width="16" height="16" fill="none">
<path d="M1.33333 2H.66667C.29848 2 0 2.38376 0 2.85714V13.1429C0 13.6162.29848 14 .66667 14H1.33333C1.70152 14 2 13.6162 2 13.1429V2.85714C2 2.38376 1.70152 2 1.33333 2ZM8 13C10.7614 13 13 10.7614 13 8 13 5.23858 10.7614 3 8 3 5.23858 3 3 5.23858 3 8 3 10.7614 5.23858 13 8 13ZM15.3333 2H14.6667C14.2985 2 14 2.38376 14 2.85714V13.1429C14 13.6162 14.2985 14 14.6667 14H15.3333C15.7015 14 16 13.6162 16 13.1429V2.85714C16 2.38376 15.7015 2 15.3333 2Z" fill="currentColor"></path>
</symbol>
<symbol id="right_side_panel" viewBox="0 0 16 16" width="16" height="16" fill="none">
<path opacity="0.3" d="M0 3C0 1.34315 1.34315 0 3 0H8V16H3C1.34315 16 0 14.6569 0 13V3Z" fill="currentColor"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 0H9V16H13C14.6569 16 16 14.6569 16 13V3C16 1.34315 14.6569 0 13 0ZM14 4H11V5H14V4ZM14 6H11V7H14V6ZM11 8H14V9H11V8Z" fill="currentColor"></path>

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -1,143 +0,0 @@
<script setup lang="ts">
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<HTMLElement>()
function onDocumentClick(event: MouseEvent) {
if (
event.target instanceof Node &&
executionModeSelectorNode.value?.contains(event.target) === false
) {
isDropdownOpen.value = false
}
}
useEvent(document, 'pointerdown', onDocumentClick)
</script>
<template>
<div ref="executionModeSelectorNode" class="ExecutionModeSelector">
<div class="execution-mode-button">
<div class="execution-mode button" @click.stop="isDropdownOpen = !isDropdownOpen">
<span v-text="props.modelValue"></span>
</div>
<div class="divider"></div>
<SvgIcon
name="workflow_play"
class="play button"
draggable="false"
@click.stop="
() => {
isDropdownOpen = false
emit('execute')
}
"
/>
</div>
<Transition name="dropdown">
<div v-if="isDropdownOpen" class="execution-mode-dropdown">
<template v-for="otherMode in props.modes" :key="otherMode">
<span
v-if="modelValue !== otherMode"
class="button"
@click.stop="emit('update:modelValue', otherMode), (isDropdownOpen = false)"
v-text="otherMode"
></span>
</template>
</div>
</Transition>
</div>
</template>
<style scoped>
span {
display: inline-block;
height: 24px;
padding: 1px 0px;
vertical-align: middle;
overflow: clip;
}
.ExecutionModeSelector {
position: relative;
color: white;
}
.execution-mode-button {
display: flex;
align-items: center;
height: 24px;
border-radius: var(--radius-full);
background: #64b526;
> .execution-mode {
font-weight: 600;
padding: 0 8px;
}
> .divider {
align-self: stretch;
width: 1px;
background-color: rgba(0, 0, 0, 0.12);
}
> .play {
box-sizing: content-box;
padding: 0 8px;
color: rgba(255, 255, 255, 0.75);
}
}
.execution-mode-dropdown {
display: flex;
flex-flow: column;
position: absolute;
background: #64b526;
border-radius: 10px;
width: 100%;
top: 100%;
margin-top: 4px;
padding: 4px;
> span {
border-radius: 6px;
padding-left: 4px;
padding-right: 4px;
width: 100%;
}
*:hover {
background: var(--color-dim);
}
}
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.1s;
> span {
transition: all 0.1s;
}
}
.dropdown-enter-from,
.dropdown-leave-to {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
overflow: hidden;
> span {
padding-top: 0;
padding-bottom: 0;
max-height: 0;
}
}
</style>

View File

@ -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
@ -334,8 +333,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) {
@ -657,8 +656,7 @@ function handleEdgeDrop(source: AstId, position: Vec2) {
@canceled="onComponentBrowserCancel"
/>
<TopBar
v-model:mode="projectStore.executionMode"
:modes="EXECUTION_MODES"
v-model:recordMode="projectStore.recordMode"
:breadcrumbs="stackNavigator.breadcrumbLabels.value"
:allowNavigationLeft="stackNavigator.allowNavigationLeft.value"
:allowNavigationRight="stackNavigator.allowNavigationRight.value"
@ -666,7 +664,7 @@ function handleEdgeDrop(source: AstId, position: Vec2) {
@breadcrumbClick="stackNavigator.handleBreadcrumbClick"
@back="stackNavigator.exitNode"
@forward="stackNavigator.enterNextNodeFromHistory"
@execute="onPlayButtonPress()"
@recordOnce="onRecordOnceButtonPress()"
@fitToAllClicked="zoomToSelected"
@zoomIn="graphNavigator.scale *= 1.1"
@zoomOut="graphNavigator.scale *= 0.9"

View File

@ -0,0 +1,61 @@
<script setup lang="ts">
import SvgIcon from '@/components/SvgIcon.vue'
import ToggleIcon from '@/components/ToggleIcon.vue'
const props = defineProps<{ recordMode: boolean }>()
const emit = defineEmits<{ recordOnce: []; 'update:recordMode': [enabled: boolean] }>()
</script>
<template>
<div class="RecordControl" @pointerdown.stop @pointerup.stop @click.stop>
<div class="control left-end">
<ToggleIcon
icon="record"
class="button"
:alt="`${props.recordMode ? 'Enable' : 'Disable'} record mode`"
:modelValue="props.recordMode"
@update:modelValue="emit('update:recordMode', $event)"
/>
</div>
<div class="control right-end">
<SvgIcon
alt="Record once"
class="button"
name="record_once"
draggable="false"
@click.stop="() => emit('recordOnce')"
/>
</div>
</div>
</template>
<style scoped>
.RecordControl {
user-select: none;
display: flex;
place-items: center;
backdrop-filter: var(--blur-app-bg);
gap: 1px;
}
.control {
background: var(--color-frame-bg);
padding: 8px 8px;
}
.left-end {
border-radius: var(--radius-full) 0 0 var(--radius-full);
}
.right-end {
border-radius: 0 var(--radius-full) var(--radius-full) 0;
}
.toggledOn {
color: #ba4c40;
}
.button:active {
color: #ba4c40;
}
</style>

View File

@ -1,25 +1,24 @@
<script setup lang="ts">
import ExecutionModeSelector from '@/components/ExecutionModeSelector.vue'
import ExtendedMenu from '@/components/ExtendedMenu.vue'
import NavBar from '@/components/NavBar.vue'
import type { BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
import RecordControl from '@/components/RecordControl.vue'
import { injectGuiConfig } from '@/providers/guiConfig'
import { computed } from 'vue'
const props = defineProps<{
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: []
@ -39,11 +38,10 @@ const barStyle = computed(() => {
<template>
<div class="TopBar" :style="barStyle">
<ExecutionModeSelector
:modes="props.modes"
:modelValue="props.mode"
@update:modelValue="emit('update:mode', $event)"
@execute="emit('execute')"
<RecordControl
:recordMode="props.recordMode"
@update:recordMode="emit('update:recordMode', $event)"
@recordOnce="emit('recordOnce')"
/>
<NavBar
:breadcrumbs="props.breadcrumbs"

View File

@ -685,6 +685,15 @@ export const useProjectStore = defineStore('project', () => {
yDocsProvider = undefined
}
const recordMode = computed({
get() {
return executionMode.value === 'live'
},
set(value) {
executionMode.value = value ? 'live' : 'design'
},
})
return {
setObservedFileName(name: string) {
observedFileName.value = name
@ -708,6 +717,7 @@ export const useProjectStore = defineStore('project', () => {
isOutputContextEnabled,
stopCapturingUndo,
executionMode,
recordMode,
dataflowErrors,
executeExpression,
disposeYDocsProvider,

View File

@ -5,8 +5,7 @@ import { ref } from 'vue'
import TopBar from '@/components/TopBar.vue'
const title = ref('Mock Project')
const modes = ref(['design', 'live'])
const mode = ref('live')
const recordMode = ref(true)
const breadcrumbs = ref(['main', 'ad_analytics'])
</script>
@ -15,21 +14,19 @@ const breadcrumbs = ref(['main', 'ad_analytics'])
<div style="height: 48px">
<TopBar
:title="title"
:mode="mode"
:modes="modes"
:recordMode="recordMode"
:breadcrumbs="breadcrumbs"
@back="logEvent('back', [])"
@forward="logEvent('forward', [])"
@execute="logEvent('execute', [])"
@recordOnce="logEvent('recordOnce', [])"
@breadcrumbClick="logEvent('breadcrumbClick', [$event])"
@update:mode="logEvent('update:mode', [$event]), (mode = $event)"
@update:recordMode="logEvent('update:recordMode', [$event]), (recordMode = $event)"
/>
</div>
<template #controls>
<HstText v-model="title" title="title" />
<HstSelect v-model="mode" title="mode" :options="modes" />
<HstJson v-model="modes" title="modes" />
<HstJson v-model="recordMode" title="recordMode" />
<HstJson v-model="breadcrumbs" title="breadcrumbs" />
</template>
</Story>