refactor(app): move storage workspace navigation into File menu

- Replace the full-width editor header action with a shared browser and native menu command
- Localize the destination and cover menu schema and browser navigation
This commit is contained in:
Danila Poyarkov 2026-08-03 17:28:39 +03:00
parent d85e2ec5e1
commit 79aa6ba5dd
18 changed files with 46 additions and 21 deletions

View file

@ -12,6 +12,10 @@
"label": "Open…",
"accelerator": "CmdOrCtrl+O"
},
{
"id": "open-storage-workspace",
"label": "Open Storage Workspace…"
},
{
"type": "separator"
},

View file

@ -7,6 +7,7 @@
"text": "Text",
"new": "Neu",
"open": "Öffnen…",
"openStorageWorkspace": "Speicher-Arbeitsbereich öffnen…",
"save": "Speichern",
"saveAs": "Speichern unter…",
"exportSelection": "Auswahl exportieren…",

View file

@ -7,6 +7,7 @@
"text": "Texto",
"new": "Nuevo",
"open": "Abrir…",
"openStorageWorkspace": "Abrir espacio de almacenamiento…",
"save": "Guardar",
"saveAs": "Guardar como…",
"exportSelection": "Exportar selección…",

View file

@ -7,6 +7,7 @@
"text": "Texte",
"new": "Nouveau",
"open": "Ouvrir…",
"openStorageWorkspace": "Ouvrir lespace de stockage…",
"save": "Enregistrer",
"saveAs": "Enregistrer sous…",
"exportSelection": "Exporter la sélection…",

View file

@ -7,6 +7,7 @@
"text": "Testo",
"new": "Nuovo",
"open": "Apri…",
"openStorageWorkspace": "Apri area di archiviazione…",
"save": "Salva",
"saveAs": "Salva con nome…",
"exportSelection": "Esporta selezione…",

View file

@ -7,6 +7,7 @@
"text": "テキスト",
"new": "新規作成",
"open": "開く…",
"openStorageWorkspace": "ストレージワークスペースを開く…",
"save": "保存",
"saveAs": "名前を付けて保存…",
"exportSelection": "選択範囲をエクスポート…",

View file

@ -7,6 +7,7 @@
"text": "Tekst",
"new": "Nowy",
"open": "Otwórz…",
"openStorageWorkspace": "Otwórz obszar przechowywania…",
"save": "Zapisz",
"saveAs": "Zapisz jako…",
"exportSelection": "Eksportuj zaznaczenie…",

View file

@ -7,6 +7,7 @@
"text": "Текст",
"new": "Создать",
"open": "Открыть…",
"openStorageWorkspace": "Открыть область хранилища…",
"save": "Сохранить",
"saveAs": "Сохранить как…",
"exportSelection": "Экспорт выделения…",

View file

@ -7,6 +7,7 @@
"text": "文本",
"new": "新建",
"open": "打开…",
"openStorageWorkspace": "打开存储工作区…",
"save": "保存",
"saveAs": "另存为…",
"exportSelection": "导出所选内容…",

View file

@ -165,7 +165,6 @@ export const dialogMessageDefaults = {
settingsStorage: 'Cloud storage',
storageWorkspace: 'Storage workspace',
openStorageWorkspace: 'Open workspace',
backToStorageWorkspace: 'Back to workspace',
newStoredDocument: 'New document',
emptyStorageWorkspace: 'No stored documents yet.',
loadingDocuments: 'Loading documents…',

View file

@ -10,6 +10,7 @@ export const menuMessageDefaults = {
new: 'New',
open: 'Open…',
openStorageWorkspace: 'Open storage workspace…',
save: 'Save',
saveAs: 'Save as…',
exportSelection: 'Export selection…',

View file

@ -1,4 +1,5 @@
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import type { MenuEntry } from '@open-pencil/vue'
import { useEditorCommands, useI18n } from '@open-pencil/vue'
@ -6,6 +7,7 @@ import { useEditorCommands, useI18n } from '@open-pencil/vue'
import { useEditorStore } from '@/app/editor/active-store'
import { openSettingsDialog } from '@/app/settings/dialog'
import { createSharedEditorMenuActions } from '@/app/shell/menu/editor-actions'
import { openStorageWorkspace } from '@/app/shell/menu/navigation'
import type { AppMenuActionItem, AppMenuEntry, AppMenuGroupSchema } from '@/app/shell/menu/schema'
import { APP_MENU_SCHEMA } from '@/app/shell/menu/schema'
import { createSelectionMenuActions } from '@/app/shell/menu/selection-actions'
@ -29,6 +31,7 @@ function isSeparator(entry: AppMenuEntry): entry is Extract<AppMenuEntry, { type
export function useAppMenu() {
const store = useEditorStore()
const router = useRouter()
const {
commands,
menuItem: commandMenuItem,
@ -41,6 +44,7 @@ export function useAppMenu() {
const translatedMenuItemLabels: Partial<Record<string, keyof typeof menu.value>> = {
new: 'new',
open: 'open',
'open-storage-workspace': 'openStorageWorkspace',
save: 'save',
'save-as': 'saveAs',
'export-selection': 'exportSelection',
@ -94,6 +98,7 @@ export function useAppMenu() {
void import('@/app/tabs').then((m) => m.createTab())
},
open: () => void openFileDialog(),
'open-storage-workspace': () => openStorageWorkspace(router),
save: () => void store.saveFigFile(),
'save-as': () => void store.saveFigFileAs(),
'export-selection': () => exportSelection('png'),

View file

@ -0,0 +1,5 @@
import type { Router } from 'vue-router'
export function openStorageWorkspace(router: Router): void {
void router.push('/storage')
}

View file

@ -33,6 +33,7 @@ export const APP_MENU_SCHEMA = [
items: [
{ id: 'new', label: 'New', shortcut: 'MOD+N' },
{ id: 'open', label: 'Open…', shortcut: 'MOD+O' },
{ id: 'open-storage-workspace', label: 'Open Storage Workspace…' },
{ type: 'separator' },
{ id: 'save', label: 'Save', shortcut: 'MOD+S' },
{ id: 'save-as', label: 'Save As…', shortcut: 'MOD+SHIFT+S' },

View file

@ -7,6 +7,7 @@ import { useEditorStore } from '@/app/editor/active-store'
import { openSettingsDialog } from '@/app/settings/dialog'
import { createSharedEditorMenuActions } from '@/app/shell/menu/editor-actions'
import { importFileDialog, openFileDialog } from '@/app/shell/menu/files'
import { openStorageWorkspace } from '@/app/shell/menu/navigation'
import { APP_MENU_SCHEMA, type AppMenuEntry } from '@/app/shell/menu/schema'
import { createSelectionMenuActions } from '@/app/shell/menu/selection-actions'
import { useAppTheme } from '@/app/shell/theme'
@ -40,6 +41,9 @@ export function useMenu() {
const actions: Partial<Record<string, () => void>> = {
new: () => createTab(),
open: () => void openFileDialog(),
'open-storage-workspace': () => {
void import('@/router').then(({ default: router }) => openStorageWorkspace(router))
},
close: () => {
if (activeTab.value) closeTab(activeTab.value.id)
},

View file

@ -1,6 +1,5 @@
<script setup lang="ts">
import { watch } from 'vue'
import { useRouter } from 'vue-router'
import { templateRef } from '@vueuse/core'
import {
@ -18,7 +17,6 @@ import {
MenubarTrigger
} from 'reka-ui'
import IconArrowLeft from '~icons/lucide/arrow-left'
import IconChevronRight from '~icons/lucide/chevron-right'
import { vTestId, useI18n } from '@open-pencil/vue'
@ -44,15 +42,6 @@ import { useEditorStore } from '@/app/editor/active-store'
import { openSettingsDialog } from '@/app/settings/dialog'
const store = useEditorStore()
const router = useRouter()
// The workspace is the app's top level. Browser back is history-dependent (and
// absent in the desktop shell), so the way back has to be an explicit
// destination, present on both platforms. Shown unconditionally: StorageView
// prompts for setup itself when storage isn't configured yet.
async function backToWorkspace(): Promise<void> {
await router.push('/storage')
}
const { rename, editingName, startRename, commitRename } = useDocumentNameRename(store)
const nameInput = templateRef<HTMLInputElement>('nameInput')
@ -71,15 +60,6 @@ const subMenuCls = useMenuUI({ content: 'min-w-44' })
<template>
<div class="shrink-0 border-b border-border">
<button
type="button"
data-test-id="app-back-to-workspace"
class="flex w-full cursor-pointer items-center gap-1.5 border-b border-border px-2 py-1.5 text-xs text-muted transition-colors hover:bg-hover hover:text-surface"
@click="backToWorkspace"
>
<IconArrowLeft class="size-3.5 shrink-0" />
<span class="truncate">{{ dialogs.backToStorageWorkspace }}</span>
</button>
<div class="flex items-center gap-2 px-2 py-1.5">
<img data-test-id="app-logo" src="/favicon-32.png" class="size-4" alt="OpenPencil" />
<input

View file

@ -20,6 +20,7 @@ test('File menu opens and shows items', async () => {
const items = await menu.locator('[role="menuitem"]').allTextContents()
expect(items.some((t) => t.includes('Open'))).toBe(true)
expect(items.some((t) => t.includes('Open storage workspace'))).toBe(true)
expect(items.some((t) => t.includes('Save'))).toBe(true)
expect(items.some((t) => t.includes('Save as'))).toBe(true)
@ -155,3 +156,11 @@ test('Zoom to fit via View menu works', async () => {
const zoomAfter = await getStoreStateNumber('zoom')
expect(zoomAfter).not.toBe(zoomBefore)
})
test('Open storage workspace navigates from the File menu', async () => {
await editor.page.getByRole('menuitem', { name: 'File', exact: true }).click()
await editor.page.getByRole('menuitem', { name: 'Open storage workspace…' }).click()
await expect(editor.page).toHaveURL(/\/storage$/)
await expect(editor.page.getByRole('heading', { name: 'Storage workspace' })).toBeVisible()
})

View file

@ -43,6 +43,15 @@ describe('APP_MENU_SCHEMA', () => {
expect(commandIds).toContain('selection.moveToPage')
})
test('includes storage workspace navigation in the shared File menu', () => {
const fileMenu = APP_MENU_SCHEMA.find((group) => group.label === 'File')
const entries = fileMenu ? actionItems(fileMenu.items) : []
expect(entries).toContainEqual(
expect.objectContaining({ id: 'open-storage-workspace', label: 'Open Storage Workspace…' })
)
})
test('keeps move-to-page destination selection in the browser menu', () => {
const objectMenu = APP_MENU_SCHEMA.find((group) => group.label === 'Object')
const entries = objectMenu ? actionItems(objectMenu.items) : []