mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
This commit is contained in:
@@ -14,6 +14,9 @@ const mockSync = vi.fn();
|
||||
const mockSyncStatus = vi.fn();
|
||||
const mockRemove = vi.fn();
|
||||
|
||||
const mockOpenUrl = vi.fn();
|
||||
const mockRevealPath = vi.fn();
|
||||
|
||||
vi.mock('../../utils/tauriCommands/vault', () => ({
|
||||
openhumanVaultList: (...args: unknown[]) => mockList(...args),
|
||||
openhumanVaultCreate: (...args: unknown[]) => mockCreate(...args),
|
||||
@@ -22,6 +25,11 @@ vi.mock('../../utils/tauriCommands/vault', () => ({
|
||||
openhumanVaultRemove: (...args: unknown[]) => mockRemove(...args),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/openUrl', () => ({
|
||||
openUrl: (...args: unknown[]) => mockOpenUrl(...args),
|
||||
revealPath: (...args: unknown[]) => mockRevealPath(...args),
|
||||
}));
|
||||
|
||||
function vault(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'v-1',
|
||||
@@ -67,6 +75,8 @@ describe('<VaultPanel />', () => {
|
||||
mockSync.mockReset();
|
||||
mockSyncStatus.mockReset();
|
||||
mockRemove.mockReset();
|
||||
mockOpenUrl.mockReset();
|
||||
mockRevealPath.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -403,4 +413,57 @@ describe('<VaultPanel />', () => {
|
||||
);
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('open vault fires obsidian deep link and shows success toast', async () => {
|
||||
mockList.mockResolvedValueOnce({ result: [vault()], logs: [] });
|
||||
mockOpenUrl.mockResolvedValueOnce(undefined);
|
||||
const onToast = vi.fn();
|
||||
render(<VaultPanel onToast={onToast} />);
|
||||
await waitFor(() => screen.getByTestId('vault-list'));
|
||||
|
||||
fireEvent.click(screen.getByTestId('vault-open'));
|
||||
await waitFor(() =>
|
||||
expect(mockOpenUrl).toHaveBeenCalledWith(
|
||||
'obsidian://open?path=' + encodeURIComponent('/Users/me/notes')
|
||||
)
|
||||
);
|
||||
expect(onToast).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'info', title: 'Opened in Obsidian' })
|
||||
);
|
||||
});
|
||||
|
||||
it('open vault falls back to revealPath when obsidian deep link fails', async () => {
|
||||
mockList.mockResolvedValueOnce({ result: [vault()], logs: [] });
|
||||
mockOpenUrl.mockRejectedValueOnce(new Error('scheme not handled'));
|
||||
mockRevealPath.mockResolvedValueOnce(undefined);
|
||||
const onToast = vi.fn();
|
||||
render(<VaultPanel onToast={onToast} />);
|
||||
await waitFor(() => screen.getByTestId('vault-list'));
|
||||
|
||||
fireEvent.click(screen.getByTestId('vault-open'));
|
||||
await waitFor(() => expect(mockOpenUrl).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockRevealPath).toHaveBeenCalledWith('/Users/me/notes'));
|
||||
expect(onToast).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'info',
|
||||
title: 'Obsidian not found — opened in file manager',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('open vault shows error toast when both obsidian and reveal fail', async () => {
|
||||
mockList.mockResolvedValueOnce({ result: [vault()], logs: [] });
|
||||
mockOpenUrl.mockRejectedValueOnce(new Error('scheme not handled'));
|
||||
mockRevealPath.mockRejectedValueOnce(new Error('permission denied'));
|
||||
const onToast = vi.fn();
|
||||
render(<VaultPanel onToast={onToast} />);
|
||||
await waitFor(() => screen.getByTestId('vault-list'));
|
||||
|
||||
fireEvent.click(screen.getByTestId('vault-open'));
|
||||
await waitFor(() => expect(mockOpenUrl).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockRevealPath).toHaveBeenCalled());
|
||||
expect(onToast).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error', title: "Couldn't open vault" })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useT } from '../../lib/i18n/I18nContext';
|
||||
import type { ToastNotification } from '../../types/intelligence';
|
||||
import { openUrl, revealPath } from '../../utils/openUrl';
|
||||
import {
|
||||
type CoreVault,
|
||||
type CoreVaultSyncState,
|
||||
@@ -240,6 +241,26 @@ export function VaultPanel({ onToast }: VaultPanelProps) {
|
||||
[onToast, reload, t]
|
||||
);
|
||||
|
||||
const handleOpenVault = useCallback(
|
||||
async (rootPath: string) => {
|
||||
try {
|
||||
await openUrl(`obsidian://open?path=${encodeURIComponent(rootPath)}`);
|
||||
onToast?.({ type: 'info', title: t('vault.openSuccess'), message: rootPath });
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error('[ui-flow][vault-panel] obsidian deep link failed', err);
|
||||
}
|
||||
try {
|
||||
await revealPath(rootPath);
|
||||
onToast?.({ type: 'info', title: t('vault.openFallback'), message: rootPath });
|
||||
} catch (err) {
|
||||
console.error('[ui-flow][vault-panel] reveal vault failed', err);
|
||||
onToast?.({ type: 'error', title: t('vault.openError'), message: String(err) });
|
||||
}
|
||||
},
|
||||
[onToast, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4 shadow-sm"
|
||||
@@ -360,6 +381,16 @@ export function VaultPanel({ onToast }: VaultPanelProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleOpenVault(v.root_path)}
|
||||
disabled={state === 'sync' || state === 'remove'}
|
||||
className="rounded-md border border-stone-300 bg-white dark:bg-neutral-900 px-3 py-1.5 text-xs
|
||||
font-semibold text-stone-700 dark:text-neutral-200 shadow-sm transition-colors
|
||||
hover:bg-stone-50 dark:hover:bg-neutral-800 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
data-testid="vault-open">
|
||||
{t('vault.openButton')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleSync(v)}
|
||||
|
||||
@@ -3988,6 +3988,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': 'قبل {count}m',
|
||||
'vault.relative.hr': 'قبل {count} قبل',
|
||||
'vault.relative.day': 'قبل {count}d',
|
||||
'vault.openButton': 'فتح',
|
||||
'vault.openSuccess': 'تم الفتح في Obsidian',
|
||||
'vault.openFallback': 'لم يتم العثور على Obsidian — تم الفتح في مدير الملفات',
|
||||
'vault.openError': 'تعذر فتح المخزن',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 دقائق',
|
||||
'subconscious.interval.tenMinutes': '10 دقائق',
|
||||
|
||||
@@ -4053,6 +4053,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': '{count}মি আগে',
|
||||
'vault.relative.hr': '{count}সেকেন্ড আগে',
|
||||
'vault.relative.day': '{count}দিন আগে',
|
||||
'vault.openButton': 'Open',
|
||||
'vault.openSuccess': 'Opened in Obsidian',
|
||||
'vault.openFallback': 'Obsidian not found — opened in file manager',
|
||||
'vault.openError': "Couldn't open vault",
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 মিনিট',
|
||||
'subconscious.interval.tenMinutes': '10 মিনিট',
|
||||
|
||||
@@ -4164,6 +4164,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': 'vor {count}m',
|
||||
'vault.relative.hr': 'vor {count}h',
|
||||
'vault.relative.day': 'vor {count}d',
|
||||
'vault.openButton': 'Open',
|
||||
'vault.openSuccess': 'Opened in Obsidian',
|
||||
'vault.openFallback': 'Obsidian not found — opened in file manager',
|
||||
'vault.openError': "Couldn't open vault",
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 Min.',
|
||||
'subconscious.interval.tenMinutes': '10 Min.',
|
||||
|
||||
@@ -4269,6 +4269,10 @@ const en: TranslationMap = {
|
||||
'vault.relative.min': '{count}m ago',
|
||||
'vault.relative.hr': '{count}h ago',
|
||||
'vault.relative.day': '{count}d ago',
|
||||
'vault.openButton': 'Open',
|
||||
'vault.openSuccess': 'Opened in Obsidian',
|
||||
'vault.openFallback': 'Obsidian not found — opened in file manager',
|
||||
'vault.openError': "Couldn't open vault",
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 min',
|
||||
'subconscious.interval.tenMinutes': '10 min',
|
||||
|
||||
@@ -4129,6 +4129,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': 'Hace {count}m',
|
||||
'vault.relative.hr': 'Hace {count}h',
|
||||
'vault.relative.day': 'Hace {count}d',
|
||||
'vault.openButton': 'Open',
|
||||
'vault.openSuccess': 'Opened in Obsidian',
|
||||
'vault.openFallback': 'Obsidian not found — opened in file manager',
|
||||
'vault.openError': "Couldn't open vault",
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 minutos',
|
||||
'subconscious.interval.tenMinutes': '10 minutos',
|
||||
|
||||
@@ -4146,6 +4146,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': 'il y a {count}m',
|
||||
'vault.relative.hr': 'il y a {count}h',
|
||||
'vault.relative.day': '{count}d',
|
||||
'vault.openButton': 'Ouvrir',
|
||||
'vault.openSuccess': 'Ouvert dans Obsidian',
|
||||
'vault.openFallback': 'Obsidian introuvable — ouvert dans le gestionnaire de fichiers',
|
||||
'vault.openError': 'Impossible d\u0027ouvrir le coffre',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 min',
|
||||
'subconscious.interval.tenMinutes': '10 min',
|
||||
|
||||
@@ -4062,6 +4062,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': '{count}m पहले',
|
||||
'vault.relative.hr': '{count}h पहले',
|
||||
'vault.relative.day': '{count}d पहले',
|
||||
'vault.openButton': 'Open',
|
||||
'vault.openSuccess': 'Opened in Obsidian',
|
||||
'vault.openFallback': 'Obsidian not found — opened in file manager',
|
||||
'vault.openError': "Couldn't open vault",
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 मिनट',
|
||||
'subconscious.interval.tenMinutes': '10 मिनट',
|
||||
|
||||
@@ -4072,6 +4072,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': '{count}m yang lalu',
|
||||
'vault.relative.hr': '{count}h yang lalu',
|
||||
'vault.relative.day': '{count}d yang lalu',
|
||||
'vault.openButton': 'Buka',
|
||||
'vault.openSuccess': 'Dibuka di Obsidian',
|
||||
'vault.openFallback': 'Obsidian tidak ditemukan — dibuka di pengelola file',
|
||||
'vault.openError': 'Tidak dapat membuka vault',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 menit',
|
||||
'subconscious.interval.tenMinutes': '10 menit',
|
||||
|
||||
@@ -4123,6 +4123,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': '{count}m fa',
|
||||
'vault.relative.hr': '{count}h fa',
|
||||
'vault.relative.day': '{count}d fa',
|
||||
'vault.openButton': 'Open',
|
||||
'vault.openSuccess': 'Opened in Obsidian',
|
||||
'vault.openFallback': 'Obsidian not found — opened in file manager',
|
||||
'vault.openError': "Couldn't open vault",
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 minuti',
|
||||
'subconscious.interval.tenMinutes': '10 minuti',
|
||||
|
||||
@@ -4023,6 +4023,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': '{count}분 전',
|
||||
'vault.relative.hr': '{count}시간 전',
|
||||
'vault.relative.day': '{count}일 전',
|
||||
'vault.openButton': '열기',
|
||||
'vault.openSuccess': 'Obsidian에서 열림',
|
||||
'vault.openFallback': 'Obsidian을 찾을 수 없음 — 파일 관리자에서 열림',
|
||||
'vault.openError': '볼트를 열 수 없습니다',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5분',
|
||||
'subconscious.interval.tenMinutes': '10분',
|
||||
|
||||
@@ -4125,6 +4125,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': '{count} min temu',
|
||||
'vault.relative.hr': '{count} godz. temu',
|
||||
'vault.relative.day': '{count} dni temu',
|
||||
'vault.openButton': 'Open',
|
||||
'vault.openSuccess': 'Opened in Obsidian',
|
||||
'vault.openFallback': 'Obsidian not found — opened in file manager',
|
||||
'vault.openError': "Couldn't open vault",
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 min',
|
||||
'subconscious.interval.tenMinutes': '10 min',
|
||||
|
||||
@@ -4124,6 +4124,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': '{count}m atrás',
|
||||
'vault.relative.hr': '{count}h atrás',
|
||||
'vault.relative.day': '{count}d atrás',
|
||||
'vault.openButton': 'Open',
|
||||
'vault.openSuccess': 'Opened in Obsidian',
|
||||
'vault.openFallback': 'Obsidian not found — opened in file manager',
|
||||
'vault.openError': "Couldn't open vault",
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 minutos',
|
||||
'subconscious.interval.tenMinutes': '10 minutos',
|
||||
|
||||
@@ -4092,6 +4092,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': '{count}m назад',
|
||||
'vault.relative.hr': '{count}h назад',
|
||||
'vault.relative.day': '{count}d назад',
|
||||
'vault.openButton': 'Открыть',
|
||||
'vault.openSuccess': 'Открыто в Obsidian',
|
||||
'vault.openFallback': 'Obsidian не найден — открыто в файловом менеджере',
|
||||
'vault.openError': 'Не удалось открыть хранилище',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5 минут',
|
||||
'subconscious.interval.tenMinutes': '10 минут',
|
||||
|
||||
@@ -3865,6 +3865,10 @@ const messages: TranslationMap = {
|
||||
'vault.relative.min': '{count}分钟前',
|
||||
'vault.relative.hr': '{count} 小时前',
|
||||
'vault.relative.day': '{count} 天前',
|
||||
'vault.openButton': '打开',
|
||||
'vault.openSuccess': '已在 Obsidian 中打开',
|
||||
'vault.openFallback': '未找到 Obsidian — 已在文件管理器中打开',
|
||||
'vault.openError': '无法打开保管库',
|
||||
'whatsapp.title': 'WhatsApp',
|
||||
'subconscious.interval.fiveMinutes': '5分钟',
|
||||
'subconscious.interval.tenMinutes': '10分钟',
|
||||
|
||||
Reference in New Issue
Block a user