Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .playwright/scripts/__tests__/extensionContributions.e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ test('plugin-owned preferences activate contributions, yield native content and
await expect(alpha.getByText('tests.alpha content')).toBeVisible()
await expect(nativeFooter()).toHaveCount(0)
const identity = await alpha.evaluate(() => window.fixture.instance)
await page.getByTestId('context-panel-toggle').click()
await page.getByTestId('context-panel-swap').click()
await page.getByTestId('context-panel-maximize').click()
await expect.poll(() => visible('alpha')).toBe(false)
await page.getByTestId('context-panel-maximize').click()
await expect.poll(() => visible('alpha')).toBe(true)
expect(await alpha.evaluate(() => window.fixture.instance)).toBe(identity)
await page.getByTestId('context-panel-swap').click()
await page.getByTestId('context-panel-toggle').click()
const alphaActions = await pluginFrame('alpha', 'actions')
await alphaActions.getByRole('checkbox', { name: 'Content tests.alpha' }).uncheck()
await expect(nativeFooter()).toBeVisible()
Expand Down
29 changes: 19 additions & 10 deletions apps/buddy/src/app/workbench/DesktopWorkbenchArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ import type { ComponentPublicInstance } from 'vue'
import type { DesktopShellBindings } from '../shell/desktopShellBindings'
import { NSpin } from 'naive-ui'
import { computed, watch } from 'vue'
import { RouterView } from 'vue-router'
import { RouterView, useRoute } from 'vue-router'
import { DesktopTaskIndexView, DesktopTaskResourcePanel } from '@/modules/tasks/ui'
import { useDesktopUi } from '@/shared/ui/desktopUiContext'
import WorkbenchLayout from '@/workbench/browser/layout/WorkbenchLayout.vue'
import WorkbenchMountPoint from '@/workbench/browser/mounts/WorkbenchMountPoint.vue'
import { useWorkbench } from '@/workbench/browser/workbenchContext'
import WorkbenchLayoutNode from '@/workbench/browser/WorkbenchLayoutNode.vue'
import WorkbenchResourcePanelActions from '@/workbench/browser/WorkbenchResourcePanelActions.vue'
import WorkbenchSurface from '@/workbench/browser/WorkbenchSurface.vue'
import DesktopDirectoryFileSurface from './DesktopDirectoryFileSurface.vue'

const props = defineProps<{ bindings: DesktopShellBindings, tasksVisible: boolean }>()
const { controller, layout, revision, labels } = useWorkbench()
const { controller, layout, revision, labels, panels } = useWorkbench()
const route = useRoute()
const contextVisible = computed(() => (props.tasksVisible || props.bindings.contextPanelGlobal.value) && props.bindings.resources.isOpen.value)
watch([contextVisible, () => route.path], panels.restore)
const pendingTaskIds = computed(() => {
void revision.value
return [...controller.navigation.entries.values()].filter(entry => entry.status === 'loading' && entry.view.resource.scheme === 'task').map(entry => entry.view.resource.id)
Expand Down Expand Up @@ -49,27 +53,32 @@ function focusContext() {
</script>

<template>
<WorkbenchLayout v-model:sidebar-collapsed="collapsed" v-model:sidebar-width="width" :language="language" :context-visible="(tasksVisible || bindings.contextPanelGlobal.value) && bindings.resources.isOpen.value" :sidebar-collapsible="tasksVisible" :sidebar-resizable="tasksVisible">
<WorkbenchLayout v-model:sidebar-collapsed="collapsed" v-model:sidebar-width="width" :language="language" :context-visible="contextVisible" :context-on-left="panels.contextOnLeft.value" :context-maximized="panels.contextMaximized.value" :sidebar-collapsible="tasksVisible" :sidebar-resizable="tasksVisible">
<template v-if="tasksVisible" #sidebar>
<WorkbenchMountPoint target="workbench.sidebar">
<DesktopTaskIndexView :pending-task-ids="pendingTaskIds" :index="bindings.taskIndex" :active-task-id="bindings.workbench.activeTaskId.value" @open-task="bindings.workbench.openTask" @new-task="bindings.workbench.newTask" />
</WorkbenchMountPoint>
</template>
<div v-show="tasksVisible" class="desktop-workbench-area__tasks">
<WorkbenchLayoutNode :node="layout.root" />
</div>
<RouterView v-if="!tasksVisible" />
<template #workspace>
<div v-show="tasksVisible" class="desktop-workbench-area__tasks">
<WorkbenchLayoutNode :node="layout.root" />
</div>
<RouterView v-if="!tasksVisible" />
</template>
<template #context>
<div class="desktop-workbench-area__context" data-workbench-context @focusin="focusContext" @pointerdown="focusContext">
<DesktopTaskResourcePanel :panel="bindings.resources" :context="bindings.resourceContext" :language="language" :visible="(tasksVisible || bindings.contextPanelGlobal.value) && bindings.resources.isOpen.value">
<DesktopTaskResourcePanel :panel="bindings.resources" :context="bindings.resourceContext" :language="language" :visible="contextVisible">
<template #header-actions>
<WorkbenchResourcePanelActions :language="language" :maximized="panels.contextMaximized.value" :on-left="panels.contextOnLeft.value" :can-swap="tasksVisible" @swap="panels.swap" @toggle-maximize="panels.toggleMaximize" />
</template>
<template #view="{ viewId }">
<WorkbenchSurface :view-id="viewId" :visible="(tasksVisible || bindings.contextPanelGlobal.value) && bindings.resources.isOpen.value" />
<WorkbenchSurface :view-id="viewId" :visible="contextVisible" />
</template>
<template #file-toolbar="{ tab }">
<div :ref="element => toolbarTarget(tab.id, element)" class="desktop-workbench-area__file-toolbar" />
</template>
<template #file="{ wrap, setWrap }">
<DesktopDirectoryFileSurface v-if="selectedFile" :view="selectedFile" :wrap="wrap" :visible="(tasksVisible || bindings.contextPanelGlobal.value) && bindings.resources.isOpen.value" @update-wrap="setWrap" />
<DesktopDirectoryFileSurface v-if="selectedFile" :view="selectedFile" :wrap="wrap" :visible="contextVisible" @update-wrap="setWrap" />
<div v-else class="desktop-workbench-area__file-loading">
<NSpin size="small" />{{ labels.loading }}
</div>
Expand Down
4 changes: 4 additions & 0 deletions apps/buddy/src/i18n/locales/en-US/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ export default {
'desktop.context.changeType.deleted': 'Deleted',
'desktop.context.changeType.modified': 'Modified',
'desktop.context.collapse': 'Collapse resource panel',
'desktop.context.maximize': 'Maximize',
'desktop.context.restore': 'Restore',
'desktop.context.moveToLeft': 'Move to left',
'desktop.context.moveToRight': 'Move to right',
'desktop.context.empty': 'No resources are open',
'desktop.context.editorLoadFailed': 'Could not load the difference viewer',
'desktop.context.editorLoading': 'Loading difference viewer…',
Expand Down
4 changes: 4 additions & 0 deletions apps/buddy/src/i18n/locales/zh-CN/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export default {
'desktop.context.changeType.deleted': '删除',
'desktop.context.changeType.modified': '修改',
'desktop.context.collapse': '收起资源面板',
'desktop.context.maximize': '放大',
'desktop.context.restore': '还原',
'desktop.context.moveToLeft': '移到左侧',
'desktop.context.moveToRight': '移到右侧',
'desktop.context.empty': '当前没有打开的资源',
'desktop.context.editorLoadFailed': '无法加载差异查看器',
'desktop.context.editorLoading': '正在加载差异查看器…',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const { language, appSidebarCollapsed } = useDesktopUi()
:capabilities="platformCapabilities"
/>
</template>
<RouterView />
<template #workspace>
<RouterView />
</template>
</WorkbenchLayout>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const emit = defineEmits<{
closeTab: [tabId: string]
selectTab: [tabId: string]
}>()
defineSlots<{ default?: () => unknown, toolbar?: () => unknown }>()
defineSlots<{ 'default'?: () => unknown, 'header-actions'?: () => unknown, 'toolbar'?: () => unknown }>()
const { t } = useBuddyI18n(() => props.language)
const actions = computed(() => [
...(props.canAddChanges ? [{ id: 'changes' as const, label: t('desktop.context.changes'), icon: Code16Regular }] : []),
Expand All @@ -44,6 +44,9 @@ function add(id: string) {
<template v-if="$slots.toolbar" #toolbar>
<slot name="toolbar" />
</template>
<template v-if="$slots['header-actions']" #header-actions>
<slot name="header-actions" />
</template>
<slot />
</WorkbenchResourcePanel>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ function add(kind: 'changes' | 'files' | 'browser') {
<template>
<DesktopFileSpacePicker v-model:show="fileSpacePickerOpen" :spaces="panel.fileSpaces.value" :language="language" @select="panel.openFiles" />
<DesktopTaskContextPanel :active-tab-id="activeTab?.id ?? null" :tabs="tabs" :language="language" :can-add-changes="panel.canAddChanges.value" :can-add-files="Boolean(panel.fileEntry.value)" @add="add" @close-tab="panel.closeTab" @select-tab="panel.selectTab">
<template v-if="$slots['header-actions']" #header-actions>
<slot name="header-actions" />
</template>
<DesktopFilesContextSurface :tab="activeTab?.kind === 'files' ? activeTab : null" :files="context.files" :has-tab="panel.hasTab" :language="language" @select="panel.selectFile">
<template #file-toolbar="bindings">
<slot name="file-toolbar" v-bind="bindings" />
Expand Down
18 changes: 13 additions & 5 deletions apps/buddy/src/workbench/browser/WorkbenchHost.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { WorkbenchMountTarget } from '@buddy-shared/workbench/workbenchUi'
import type { DragEndEvent } from '@dnd-kit/vue'
import type { DragEndEvent, DragStartEvent } from '@dnd-kit/vue'
import type { DropPosition, ResourceRef, WorkbenchView } from '../common/workbench'
import type { WorkbenchController } from '../services/WorkbenchController'
import type { WorkingCopyService } from '../services/WorkingCopyService'
Expand All @@ -13,6 +13,7 @@ import { computed, onMounted, onScopeDispose, provide, shallowRef, triggerRef }
import { useOptionalWorkbenchUi } from '@/shared/ui/contributions/workbenchUiContext'
import { commandLabel } from '../common/workbench'
import { workbenchLabels } from '../common/workbenchLabels'
import { useWorkbenchPanelPresentation } from './layout/useWorkbenchPanelPresentation'
import WorkbenchMountPortals from './mounts/WorkbenchMountPortals.vue'
import { useWorkbenchResize } from './useWorkbenchResize'
import { workbenchKey } from './workbenchContext'
Expand All @@ -27,6 +28,7 @@ const ui = useOptionalWorkbenchUi()
const plugins = createWorkbenchDragPlugins()
const sensors = [PointerSensor.configure({ activationConstraints: () => [new PointerActivationConstraints.Distance({ value: 6 })] }), KeyboardSensor]
const resize = useWorkbenchResize(props.controller)
const panels = useWorkbenchPanelPresentation(props.controller, () => props.active)
const layout = shallowRef(props.controller.layout)
const revision = shallowRef(0)
const labels = computed(() => workbenchLabels(props.language))
Expand Down Expand Up @@ -73,7 +75,7 @@ const commands = computed(() => {
const dropPosition = shallowRef<{ paneId: string, position: DropPosition } | null>(null)
function viewVisible(id: string): boolean {
const view = props.controller.layout.views[id]
return !!view && mounts.value.has(id) && (props.active || view.location !== 'main') && props.controller.matchesViewContext(view)
return !!view && mounts.value.has(id) && (panels.mainVisible.value || view.location !== 'main') && props.controller.matchesViewContext(view)
}
function viewTarget(id: string): HTMLElement | null {
return mounts.value.get(id) ?? null
Expand All @@ -88,13 +90,19 @@ async function execute(id: string, source: 'palette' | 'shortcut' = 'palette') {
commandFailed.value = true
}
}
function dragStart(event: DragStartEvent) {
dragging.value = true
if (event.operation.source?.type === 'workbench-task')
panels.beginDrag()
}
function dragEnd(event: DragEndEvent) {
dragging.value = false
const destination = dropPosition.value
const source = event.operation.source
panels.endDrag(!event.canceled && !!destination && !!source?.data.resource)
dropPosition.value = null
if (event.canceled || !destination)
return
const source = event.operation.source
if (source?.data.resource)
emit('dropResource', source.data.resource as ResourceRef, destination.paneId, destination.position)
}
Expand Down Expand Up @@ -138,11 +146,11 @@ onMounted(() => {
window.addEventListener('keydown', keyboard)
})
onScopeDispose(() => window.removeEventListener('keydown', keyboard))
provide(workbenchKey, { resize, controller: props.controller, copies: props.copies, layout, revision, mountPoints, registerMountPoint, labels, viewVisible, viewTarget, mountView, dropPosition })
provide(workbenchKey, { panels, resize, controller: props.controller, copies: props.copies, layout, revision, mountPoints, registerMountPoint, labels, viewVisible, viewTarget, mountView, dropPosition })
</script>

<template>
<DragDropProvider :sensors="sensors" :plugins="plugins" @drag-start="dragging = true" @drag-end="dragEnd">
<DragDropProvider :sensors="sensors" :plugins="plugins" @drag-start="dragStart" @drag-end="dragEnd">
<main class="workbench" :class="{ 'is-dragging': dragging, 'is-resizing': resize.active.value.length > 0 }" :style="{ '--workbench-resize-cursor': resize.cursor.value }" data-testid="workbench">
<div v-if="backupError" class="workbench__error" role="alert">
{{ labels.backupFailed }} <button type="button" @click="emit('retryBackup')">
Expand Down
19 changes: 16 additions & 3 deletions apps/buddy/src/workbench/browser/WorkbenchResourcePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { Component } from 'vue'
import type { BuddyLocale } from '@/i18n/buddyI18n'
import { Add20Regular, Dismiss16Regular } from '@vicons/fluent'
import { useResizeObserver } from '@vueuse/core'
import { NPopover } from 'naive-ui'
import { nextTick, shallowRef, useId, useTemplateRef, watch } from 'vue'
import { useBuddyI18n } from '@/i18n/buddyI18n'
Expand All @@ -18,7 +19,7 @@ const emit = defineEmits<{
closeTab: [tabId: string]
selectTab: [tabId: string]
}>()
defineSlots<{ default?: () => unknown, toolbar?: () => unknown, icon?: (props: { tab: Tab }) => unknown }>()
defineSlots<{ 'default'?: () => unknown, 'header-actions'?: () => unknown, 'toolbar'?: () => unknown, 'icon'?: (props: { tab: Tab }) => unknown }>()
const { t } = useBuddyI18n(() => props.language)
const panelId = `context-${useId()}`
const menuOpen = shallowRef(false)
Expand Down Expand Up @@ -52,10 +53,12 @@ function handleTabsWheel(event: WheelEvent) {
event.preventDefault()
root.scrollBy({ left: event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? root.clientWidth : 1) })
}
watch(() => [props.activeTabId, props.tabs.length], async () => {
async function revealActiveTab() {
await nextTick()
tabsScrollRoot.value?.querySelector('[aria-selected="true"]')?.closest('.desktop-task-context-panel__tab')?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
})
}
watch(() => [props.activeTabId, props.tabs.length], revealActiveTab)
useResizeObserver(tabsScrollRoot, revealActiveTab)
</script>

<template>
Expand Down Expand Up @@ -86,6 +89,9 @@ watch(() => [props.activeTabId, props.tabs.length], async () => {
</button>
</div>
</NPopover>
<div v-if="$slots['header-actions']" class="desktop-task-context-panel__actions">
<slot name="header-actions" />
</div>
</header>
<div v-if="$slots.toolbar" class="desktop-task-context-panel__toolbar">
<slot name="toolbar" />
Expand Down Expand Up @@ -131,6 +137,13 @@ watch(() => [props.activeTabId, props.tabs.length], async () => {
scrollbar-width: none;
}

.desktop-task-context-panel__actions {
display: flex;
flex: none;
align-items: center;
margin: 0 0.375rem 0 auto;
}

:deep(.desktop-task-context-panel__tabs-scrollbar) {
width: 100%;
height: 100%;
Expand Down
53 changes: 53 additions & 0 deletions apps/buddy/src/workbench/browser/WorkbenchResourcePanelActions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script setup lang="ts">
import type { BuddyLocale } from '@/i18n/buddyI18n'
import { NTooltip } from 'naive-ui'
import { nextTick } from 'vue'
import { useBuddyI18n } from '@/i18n/buddyI18n'
import DesktopIcon from '@/shared/ui/icon/DesktopIcon.vue'

const props = defineProps<{ language: BuddyLocale, maximized: boolean, onLeft: boolean, canSwap: boolean }>()
const emit = defineEmits<{ swap: [], toggleMaximize: [] }>()
const { t } = useBuddyI18n(() => props.language)

async function swap(event: MouseEvent) {
const button = event.currentTarget as HTMLButtonElement
const restoreFocus = button.matches(':focus-visible')
emit('swap')
await nextTick()
if (restoreFocus)
button.focus({ preventScroll: true })
}
</script>

<template>
<NTooltip v-if="canSwap && !maximized" :delay="350">
<template #trigger>
<button class="workbench-resource-panel-action" data-testid="context-panel-swap" type="button" :aria-label="t(onLeft ? 'desktop.context.moveToRight' : 'desktop.context.moveToLeft')" @click="swap">
<DesktopIcon :size="18">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 8h16m-4-4 4 4-4 4M20 16H4m4-4-4 4 4 4" />
</svg>
</DesktopIcon>
</button>
</template>
{{ t(onLeft ? 'desktop.context.moveToRight' : 'desktop.context.moveToLeft') }}
</NTooltip>
<NTooltip :delay="350">
<template #trigger>
<button class="workbench-resource-panel-action" data-testid="context-panel-maximize" type="button" :aria-label="t(maximized ? 'desktop.context.restore' : 'desktop.context.maximize')" :aria-pressed="maximized" @click="emit('toggleMaximize')">
<DesktopIcon :size="18">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path :d="maximized ? 'm4 20 6-6m-6 0h6v6m10-16-6 6m0-6v6h6' : 'M14 4h6v6m0-6-6 6M4 14v6h6m-6 0 6-6'" />
</svg>
</DesktopIcon>
</button>
</template>
{{ t(maximized ? 'desktop.context.restore' : 'desktop.context.maximize') }}
</NTooltip>
</template>

<style scoped>
.workbench-resource-panel-action { display: grid; width: 2rem; height: 2rem; flex: none; place-items: center; padding: 0; border: 0; border-radius: var(--buddy-icon-button-radius); background: transparent; color: var(--buddy-text-primary); cursor: pointer; }
.workbench-resource-panel-action:hover { background: var(--buddy-state-hover); color: var(--buddy-text-strong); }
.workbench-resource-panel-action:focus-visible { outline: 2px solid var(--buddy-focus-ring); outline-offset: -2px; }
</style>
Loading