Renaming project in GUI (#10243)

Fixes #10073

[Screencast from 2024-06-11 13-01-10.webm](https://github.com/enso-org/enso/assets/3919101/2917ad41-e080-482b-9a69-690e80087132)
This commit is contained in:
Adam Obuchowicz 2024-06-21 13:28:30 +02:00 committed by GitHub
parent 5fd0e0c30d
commit 4164277d22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 247 additions and 71 deletions

View File

@ -9,6 +9,7 @@
- [Copy-pasting multiple nodes][10194].
- The documentation editor has [formatting toolbars][10064].
- The documentation editor supports [rendering images][10205].
- [Project may be renamed in Project View][10243]
- [Fixed a bug where drop-down were not displayed for some arguments][10297].
For example, `locale` parameter of `Equal_Ignore_Case` kind in join component.
- [Node previews][10310]: Node may be previewed by hovering output port while
@ -19,6 +20,7 @@
[10194]: https://github.com/enso-org/enso/pull/10194
[10198]: https://github.com/enso-org/enso/pull/10198
[10205]: https://github.com/enso-org/enso/pull/10205
[10243]: https://github.com/enso-org/enso/pull/10243
[10297]: https://github.com/enso-org/enso/pull/10297
[10310]: https://github.com/enso-org/enso/pull/10310

View File

@ -479,6 +479,11 @@ export class LanguageServer extends ObservableV2<Notifications & TransportEvents
return this.request('refactoring/renameSymbol', { module, expressionId, newName })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#refactoringrenameproject) */
renameProject(namespace: string, oldName: string, newName: string): Promise<LsRpcResult<void>> {
return this.request('refactoring/renameProject', { namespace, oldName, newName })
}
/** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#profilingstart) */
profilingStart(memorySnapshot?: boolean): Promise<LsRpcResult<void>> {
return this.request('profiling/start', { memorySnapshot })

View File

@ -343,7 +343,11 @@ export type Notifications = {
'file/event': (param: { path: Path; kind: FileEventKind }) => void
'file/rootAdded': (param: {}) => void
'file/rootRemoved': (param: {}) => void
'refactoring/projectRenamed': (param: {}) => void
'refactoring/projectRenamed': (param: {
oldNormalizedName: string
newNormalizedName: string
newName: string
}) => void
}
export type Event<T extends keyof Notifications> = Parameters<Notifications[T]>[0]

View File

@ -25,6 +25,7 @@ const props = defineProps<{
logEvent: LogEvent
hidden: boolean
ignoreParamsRegex?: RegExp
renameProject: (newName: string) => void
}>()
const classSet = provideAppClassSet()
@ -71,7 +72,13 @@ registerAutoBlurHandler()
:unrecognizedOptions="appConfig.unrecognizedOptions"
:config="appConfig.config"
/>
<ProjectView v-else v-show="!props.hidden" class="App" :class="[...classSet.keys()]" />
<ProjectView
v-else
v-show="!props.hidden"
class="App"
:class="[...classSet.keys()]"
:renameProject="renameProject"
/>
<Teleport to="body">
<TooltipDisplayer :registry="appTooltips" />
</Teleport>

View File

@ -28,19 +28,19 @@ import { useDoubleClick } from '@/composables/doubleClick'
import { keyboardBusy, keyboardBusyExceptIn, unrefElement, useEvent } from '@/composables/events'
import { groupColorVar } from '@/composables/nodeColors'
import type { PlacementStrategy } from '@/composables/nodeCreation'
import { useStackNavigator } from '@/composables/stackNavigator'
import { useSyncLocalStorage } from '@/composables/syncLocalStorage'
import { provideGraphNavigator, type GraphNavigator } from '@/providers/graphNavigator'
import { provideNodeColors } from '@/providers/graphNodeColors'
import { provideNodeCreation } from '@/providers/graphNodeCreation'
import { provideGraphSelection } from '@/providers/graphSelection'
import { provideStackNavigator } from '@/providers/graphStackNavigator'
import { provideInteractionHandler } from '@/providers/interactionHandler'
import { provideKeyboard } from '@/providers/keyboard'
import { provideWidgetRegistry } from '@/providers/widgetRegistry'
import { provideGraphStore, type NodeId } from '@/stores/graph'
import { asNodeId } from '@/stores/graph/graphDatabase'
import type { RequiredImport } from '@/stores/graph/imports'
import { provideProjectStore } from '@/stores/project'
import { useProjectStore } from '@/stores/project'
import { provideSuggestionDbStore } from '@/stores/suggestionDatabase'
import type { Typename } from '@/stores/suggestionDatabase/entry'
import { provideVisualizationStore } from '@/stores/visualization'
@ -70,7 +70,7 @@ import {
} from 'vue'
const keyboard = provideKeyboard()
const projectStore = provideProjectStore()
const projectStore = useProjectStore()
const suggestionDb = provideSuggestionDbStore(projectStore)
const graphStore = provideGraphStore(projectStore, suggestionDb)
const widgetRegistry = provideWidgetRegistry(graphStore.db)
@ -194,7 +194,7 @@ function panToSelected() {
// == Breadcrumbs ==
const stackNavigator = useStackNavigator(projectStore, graphStore)
const stackNavigator = provideStackNavigator(projectStore, graphStore)
// === Toasts ===
@ -706,14 +706,8 @@ const groupColors = computed(() => {
v-model:showColorPicker="showColorPicker"
v-model:showCodeEditor="showCodeEditor"
v-model:showDocumentationEditor="showDocumentationEditor"
:breadcrumbs="stackNavigator.breadcrumbLabels.value"
:allowNavigationLeft="stackNavigator.allowNavigationLeft.value"
:allowNavigationRight="stackNavigator.allowNavigationRight.value"
:zoomLevel="100.0 * graphNavigator.targetScale"
:componentsSelected="nodeSelection.selected.size"
@breadcrumbClick="stackNavigator.handleBreadcrumbClick"
@back="stackNavigator.exitNode"
@forward="stackNavigator.enterNextNodeFromHistory"
@recordOnce="onRecordOnceButtonPress()"
@fitToAllClicked="zoomToSelected"
@zoomIn="graphNavigator.stepZoom(+1)"

View File

@ -1,14 +1,10 @@
<script setup lang="ts">
import NavBreadcrumbs, { type BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
import NavBreadcrumbs from '@/components/NavBreadcrumbs.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { injectStackNavigator } from '@/providers/graphStackNavigator'
import SvgButton from './SvgButton.vue'
const props = defineProps<{
breadcrumbs: BreadcrumbItem[]
allowNavigationLeft: boolean
allowNavigationRight: boolean
}>()
const emit = defineEmits<{ back: []; forward: []; breadcrumbClick: [index: number] }>()
const stackNavigator = injectStackNavigator()
</script>
<template>
@ -17,18 +13,18 @@ const emit = defineEmits<{ back: []; forward: []; breadcrumbClick: [index: numbe
<div class="breadcrumbs-controls">
<SvgButton
name="arrow_left"
:disabled="!props.allowNavigationLeft"
:disabled="!stackNavigator.allowNavigationLeft"
title="Back"
@click.stop="emit('back')"
@click.stop="stackNavigator.exitNode"
/>
<SvgButton
name="arrow_right"
:disabled="!props.allowNavigationRight"
:disabled="!stackNavigator.allowNavigationRight"
title="Forward"
@click.stop="emit('forward')"
@click.stop="stackNavigator.enterNextNodeFromHistory"
/>
</div>
<NavBreadcrumbs :breadcrumbs="props.breadcrumbs" @selected="emit('breadcrumbClick', $event)" />
<NavBreadcrumbs />
</div>
</template>

View File

@ -1,10 +1,20 @@
<script setup lang="ts">
const props = defineProps<{ text: string; active: boolean }>()
import { ref, watch, type ComponentInstance } from 'vue'
import AutoSizedInput from './widgets/AutoSizedInput.vue'
const model = defineModel<string>({ required: true })
const _props = defineProps<{ active: boolean; editing: boolean }>()
const input = ref<ComponentInstance<typeof AutoSizedInput>>()
watch(input, (input, old) => {
if (old == null && input != null) input.focus()
})
</script>
<template>
<div :class="['NavBreadcrumb', { inactive: !props.active }]">
<span v-text="props.text"></span>
<div :class="['NavBreadcrumb', { inactive: !active }]">
<AutoSizedInput v-if="editing" ref="input" v-model.lazy="model" :autoSelect="true" />
<span v-else v-text="model"></span>
</div>
</template>

View File

@ -1,31 +1,50 @@
<script setup lang="ts">
import NavBreadcrumb from '@/components/NavBreadcrumb.vue'
import SvgButton from '@/components/SvgButton.vue'
import { injectStackNavigator } from '@/providers/graphStackNavigator'
import { useProjectStore } from '@/stores/project'
import { useToast } from '@/util/toast'
import { ref } from 'vue'
export interface BreadcrumbItem {
label: string
active: boolean
}
const renameError = useToast.error()
const projectNameEdited = ref(false)
const props = defineProps<{ breadcrumbs: BreadcrumbItem[] }>()
const emit = defineEmits<{ selected: [index: number] }>()
const stackNavigator = injectStackNavigator()
const project = useProjectStore()
async function renameBreadcrumb(index: number, newName: string) {
if (index === 0) {
const result = await project.renameProject(newName)
if (!result.ok) {
renameError.reportError(result.error)
}
projectNameEdited.value = false
}
}
</script>
<template>
<div class="NavBreadcrumbs">
<template v-for="(breadcrumb, index) in props.breadcrumbs" :key="index">
<SvgButton name="edit" title="Edit Project Name" @click.stop="projectNameEdited = true" />
<template v-for="(breadcrumb, index) in stackNavigator.breadcrumbLabels.value" :key="index">
<SvgButton
v-if="index > 0"
name="arrow_right_head_only"
:disabled="breadcrumb.active"
:disabled="!breadcrumb.active"
class="arrow"
/>
<NavBreadcrumb
:text="breadcrumb.label"
:modelValue="breadcrumb.label"
:active="breadcrumb.active"
:editing="index === 0 && projectNameEdited"
:title="index === 0 ? 'Project Name' : ''"
class="clickable"
@click.stop="emit('selected', index)"
@click.stop="stackNavigator.handleBreadcrumbClick(index)"
@update:modelValue="renameBreadcrumb(index, $event)"
/>
</template>
</div>

View File

@ -1,7 +1,6 @@
<script setup lang="ts">
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 SelectionMenu from '@/components/SelectionMenu.vue'
import { injectGuiConfig } from '@/providers/guiConfig'
@ -11,18 +10,12 @@ const showColorPicker = defineModel<boolean>('showColorPicker', { required: true
const showCodeEditor = defineModel<boolean>('showCodeEditor', { required: true })
const showDocumentationEditor = defineModel<boolean>('showDocumentationEditor', { required: true })
const props = defineProps<{
breadcrumbs: BreadcrumbItem[]
recordMode: boolean
allowNavigationLeft: boolean
allowNavigationRight: boolean
zoomLevel: number
componentsSelected: number
}>()
const emit = defineEmits<{
recordOnce: []
back: []
forward: []
breadcrumbClick: [index: number]
'update:recordMode': [enabled: boolean]
fitToAllClicked: []
zoomIn: []
@ -50,14 +43,7 @@ const barStyle = computed(() => {
@update:recordMode="emit('update:recordMode', $event)"
@recordOnce="emit('recordOnce')"
/>
<NavBar
:breadcrumbs="props.breadcrumbs"
:allowNavigationLeft="props.allowNavigationLeft"
:allowNavigationRight="props.allowNavigationRight"
@back="emit('back')"
@forward="emit('forward')"
@breadcrumbClick="emit('breadcrumbClick', $event)"
/>
<NavBar />
<Transition name="selection-menu">
<SelectionMenu
v-if="componentsSelected > 1"

View File

@ -0,0 +1,5 @@
import { useStackNavigator } from '@/composables/stackNavigator'
import { createContextStore } from '@/providers'
export { injectFn as injectStackNavigator, provideFn as provideStackNavigator }
const { provideFn, injectFn } = createContextStore('graph stack navigator', useStackNavigator)

View File

@ -2,6 +2,12 @@ import { findIndexOpt } from '@/util/data/array'
import { isSome, type Opt } from '@/util/data/opt'
import { Err, Ok, type Result } from '@/util/data/result'
import { AsyncQueue, type AbortScope } from '@/util/net'
import {
qnReplaceProjectName,
tryIdentifier,
tryQualifiedName,
type Identifier,
} from '@/util/qualifiedName'
import * as array from 'lib0/array'
import * as object from 'lib0/object'
import { ObservableV2 } from 'lib0/observable'
@ -55,7 +61,7 @@ type ExecutionContextState =
visualizations: Map<Uuid, NodeVisualizationConfiguration>
stack: StackItem[]
environment?: ExecutionEnvironment
} // | { status: 'broken'} TODO[ao] think about it
}
type EntryPoint = Omit<ExplicitCall, 'type'>
@ -74,6 +80,13 @@ type ExecutionContextNotification = {
'visualizationsConfigured'(configs: Set<Uuid>): void
}
enum SyncStatus {
NOT_SYNCED,
QUEUED,
SYNCING,
SYNCED,
}
/**
* Execution Context
*
@ -86,7 +99,7 @@ type ExecutionContextNotification = {
export class ExecutionContext extends ObservableV2<ExecutionContextNotification> {
readonly id: ContextId = random.uuidv4() as ContextId
private queue: AsyncQueue<ExecutionContextState>
private syncScheduled = false
private syncStatus = SyncStatus.NOT_SYNCED
private clearScheduled = false
private _desiredStack: StackItem[] = reactive([])
private visualizationConfigs: Map<Uuid, NodeVisualizationConfiguration> = new Map()
@ -135,7 +148,7 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
// Connection closed: the created execution context is no longer available
// There is no point in any scheduled action until resynchronization
this.queue.clear()
this.syncScheduled = false
this.syncStatus = SyncStatus.NOT_SYNCED
this.queue.pushTask(() => {
this.clearScheduled = false
this.sync()
@ -143,6 +156,32 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
})
this.clearScheduled = true
})
this.lsRpc.on('refactoring/projectRenamed', ({ oldNormalizedName, newNormalizedName }) => {
const newIdent = tryIdentifier(newNormalizedName)
if (!newIdent.ok) {
console.error(
`Cannot update project name in execution stack: new name ${newNormalizedName} is not a valid identifier!`,
)
return
}
ExecutionContext.replaceProjectNameInStack(
this._desiredStack,
oldNormalizedName,
newIdent.value,
)
if (this.syncStatus === SyncStatus.SYNCED) {
this.queue.pushTask((state) => {
if (state.status !== 'created') return Promise.resolve(state)
ExecutionContext.replaceProjectNameInStack(state.stack, oldNormalizedName, newIdent.value)
return Promise.resolve(state)
})
} else {
// Engine updates project name in its execution context frames by itself. But if we are out
// of sync, we have no guarantee if the stack wasn't set with old name after project rename.
// It's safer to just re-sync the stack.
this.sync()
}
})
}
private pushItem(item: StackItem) {
@ -150,6 +189,28 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
this.sync()
}
private static replaceProjectNameInStack(
stack: StackItem[],
oldName: string,
newName: Identifier,
) {
const updatedField = (value: string) => {
const qn = tryQualifiedName(value)
if (qn.ok) {
return qnReplaceProjectName(qn.value, oldName, newName)
} else {
console.warn(`Invalid qualified name in execution context stack: ${value}`)
return value
}
}
for (const item of stack) {
if (item.type === 'ExplicitCall') {
item.methodPointer.module = updatedField(item.methodPointer.module)
item.methodPointer.definedOnType = updatedField(item.methodPointer.definedOnType)
}
}
}
get desiredStack() {
return this._desiredStack
}
@ -229,8 +290,8 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
}
private sync() {
if (this.syncScheduled || this.abort.signal.aborted) return
this.syncScheduled = true
if (this.syncStatus === SyncStatus.QUEUED || this.abort.signal.aborted) return
this.syncStatus = SyncStatus.QUEUED
this.queue.pushTask(this.syncTask())
}
@ -248,7 +309,7 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
private syncTask() {
return async (state: ExecutionContextState) => {
this.syncScheduled = false
this.syncStatus = SyncStatus.SYNCING
if (this.abort.signal.aborted) return state
let newState = { ...state }
@ -399,6 +460,9 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
this.emit('visualizationsConfigured', [
new Set(state.status === 'created' ? state.visualizations.keys() : []),
])
if (this.syncStatus === SyncStatus.SYNCING) {
this.syncStatus = SyncStatus.SYNCED
}
return newState
}
}

View File

@ -32,6 +32,7 @@ import {
markRaw,
onScopeDispose,
proxyRefs,
readonly,
ref,
shallowRef,
watch,
@ -99,7 +100,7 @@ export type ProjectStore = ReturnType<typeof useProjectStore>
*/
export const { provideFn: provideProjectStore, injectFn: useProjectStore } = createContextStore(
'project',
() => {
(renameProjectBackend: (newName: string) => void) => {
const abort = useAbortScope()
const observedFileName = ref<string>()
@ -108,9 +109,10 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
const awareness = new Awareness(doc)
const config = injectGuiConfig()
const projectName = config.value.startup?.project
if (projectName == null) throw new Error('Missing project name.')
const projectDisplayName = config.value.startup?.displayedProjectName ?? projectName
const projectNameFromCfg = config.value.startup?.project
if (projectNameFromCfg == null) throw new Error('Missing project name.')
const projectName = ref(projectNameFromCfg)
const projectDisplayName = ref(config.value.startup?.displayedProjectName ?? projectName)
const clientId = random.uuidv4() as Uuid
const lsUrls = resolveLsUrl(config.value)
@ -126,7 +128,6 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
rpcUrl.hostname === '[::1]' ||
rpcUrl.hostname === '0:0:0:0:0:0:0:1'
const name = computed(() => config.value.startup?.project)
const namespace = computed(() => config.value.engine?.namespace)
const fullName = computed(() => {
const ns = namespace.value
@ -135,14 +136,7 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
'Unknown project\'s namespace. Assuming "local", however it likely won\'t work in cloud',
)
}
const projectName = name.value
if (projectName == null) {
console.error(
"Unknown project's name. Cannot specify opened module's qualified path; many things may not work",
)
return null
}
return `${ns ?? 'local'}.${projectName}`
return `${ns ?? 'local'}.${projectName.value}`
})
const modulePath = computed(() => {
const filePath = observedFileName.value
@ -347,6 +341,24 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
},
})
function renameProject(newDisplayedName: string) {
try {
renameProjectBackend(newDisplayedName)
return Ok()
} catch (err) {
return Err(err)
}
}
lsRpcConnection.on(
'refactoring/projectRenamed',
({ oldNormalizedName, newNormalizedName, newName }) => {
if (oldNormalizedName === projectName.value) {
projectName.value = newNormalizedName
projectDisplayName.value = newName
}
},
)
const projectRootId = contentRoots.then(
(roots) => roots.find((root) => root.type === 'Project')?.id,
)
@ -370,8 +382,8 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
get observedFileName() {
return observedFileName.value
},
name: projectName,
displayName: projectDisplayName,
name: readonly(projectName),
displayName: readonly(projectDisplayName),
isOnLocalBackend,
executionContext,
firstExecution,
@ -394,6 +406,7 @@ export const { provideFn: provideProjectStore, injectFn: useProjectStore } = cre
dataflowErrors,
executeExpression,
disposeYDocsProvider,
renameProject,
})
},
)

View File

@ -5,7 +5,9 @@ import {
qnJoin,
qnLastSegment,
qnParent,
qnReplaceProjectName,
qnSplit,
tryIdentifier,
tryIdentifierOrOperatorIdentifier,
tryQualifiedName,
type IdentifierOrOperatorIdentifier,
@ -96,3 +98,15 @@ test.each([
const qn = unwrap(tryQualifiedName(name))
expect(normalizeQualifiedName(qn)).toEqual(unwrap(tryQualifiedName(expected)))
})
test.each([
['local.Project.Main', 'Project', 'NewProject', 'local.NewProject.Main'],
['local.Project.Main', 'Project2', 'NewProject', 'local.Project.Main'],
['local.Project', 'Project', 'NewProject', 'local.NewProject'],
['Project', 'Project', 'NewProject', 'Project'],
['local.Project2.Project', 'Project', 'NewProject', 'local.Project2.Project'],
])('Replacing project name in %s from %s to %s', (qname, oldName, newName, expected) => {
const qn = unwrap(tryQualifiedName(qname))
const newIdent = unwrap(tryIdentifier(newName))
expect(qnReplaceProjectName(qn, oldName, newIdent)).toBe(expected)
})

View File

@ -97,3 +97,19 @@ export function qnSlice(
export function qnIsTopElement(name: QualifiedName): boolean {
return !/[.].*?[.].*?[.]/.test(name)
}
/**
* Replace the project name in this qualified name if equal to `oldProject`, otherwise return `qn`.
*
* The namespace will be unchanged.
*/
export function qnReplaceProjectName(
qn: QualifiedName,
oldProject: string,
newProject: Identifier,
): QualifiedName {
return qn.replace(
new RegExp(`^(${identifierRegexPart}\\.)${oldProject}(?=\\.|$)`),
`$1${newProject}`,
) as QualifiedName
}

View File

@ -1,5 +1,10 @@
<script setup lang="ts">
import GraphEditor from '@/components/GraphEditor.vue'
import { provideProjectStore } from '@/stores/project'
const props = defineProps<{ renameProject: (newName: string) => void }>()
provideProjectStore(props.renameProject)
</script>
<template>

View File

@ -8,7 +8,10 @@ import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
import * as backendProvider from '#/providers/BackendProvider'
import type * as backendModule from '#/services/Backend'
import type Backend from '#/services/Backend'
import * as backendModule from '#/services/Backend'
import * as object from '#/utilities/object'
import type * as types from '../../../types/types'
@ -32,6 +35,7 @@ export default function Editor(props: EditorProps) {
const gtagEventRef = React.useRef(gtagEvent)
gtagEventRef.current = gtagEvent
const remoteBackend = backendProvider.useRemoteBackend()
const localBackend = backendProvider.useLocalBackend()
const logEvent = React.useCallback(
(message: string, projectId?: string | null, metadata?: object | null) => {
@ -42,6 +46,36 @@ export default function Editor(props: EditorProps) {
[remoteBackend]
)
const renameProject = React.useCallback(
(newName: string) => {
if (projectStartupInfo != null) {
let backend: Backend | null
switch (projectStartupInfo.backendType) {
case backendModule.BackendType.local:
backend = localBackend
break
case backendModule.BackendType.remote:
backend = remoteBackend
break
}
const { id: projectId, parentId, title } = projectStartupInfo.projectAsset
backend
?.updateProject(
projectId,
{ projectName: newName, ami: null, ideVersion: null, parentId },
title
)
.then(
() => {
projectStartupInfo.setProjectAsset?.(object.merger({ title: newName }))
},
e => toastAndLog('renameProjectError', e)
)
}
},
[remoteBackend, localBackend, projectStartupInfo, toastAndLog]
)
React.useEffect(() => {
if (hidden) {
return
@ -84,9 +118,10 @@ export default function Editor(props: EditorProps) {
hidden,
ignoreParamsRegex: new RegExp(`^${appUtils.SEARCH_PARAMS_PREFIX}(.+)$`),
logEvent,
renameProject,
}
}
}, [projectStartupInfo, toastAndLog, hidden, logEvent, ydocUrl])
}, [projectStartupInfo, toastAndLog, hidden, logEvent, ydocUrl, renameProject])
if (projectStartupInfo == null || AppRunner == null || appProps == null) {
return <></>

View File

@ -21,6 +21,7 @@ interface EditorProps {
projectId?: string | null,
metadata?: object | null
) => void
readonly renameProject: (newName: string) => void
}
/** The value passed from the entrypoint to the dashboard, which enables the dashboard to