mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 21:01:37 +03:00
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:
parent
5fd0e0c30d
commit
4164277d22
@ -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
|
||||
|
||||
|
@ -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 })
|
||||
|
@ -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]
|
||||
|
@ -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>
|
||||
|
@ -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)"
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
5
app/gui2/src/providers/graphStackNavigator.ts
Normal file
5
app/gui2/src/providers/graphStackNavigator.ts
Normal 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)
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 <></>
|
||||
|
1
app/ide-desktop/lib/types/types.d.ts
vendored
1
app/ide-desktop/lib/types/types.d.ts
vendored
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user