enhancement: open folder log and change data folder dialog confirm (#5159)

* enhancement: ux change data folder with confirmation and reveal in finder logs

* chore: update button open logs local api server

* Update web-app/src/components/ui/button.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* chore: handle error when change location data folder failed

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Faisal Amir 2025-06-02 08:54:16 +07:00 committed by GitHub
parent 057accfb96
commit b98c31b184
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 258 additions and 52 deletions

View File

@ -25,6 +25,7 @@ tauri = { version = "2.4.0", features = [ "protocol-asset", "macos-private-api",
tauri-plugin-log = "2.0.0-rc"
tauri-plugin-shell = "2.2.0"
tauri-plugin-os = "2.2.1"
tauri-plugin-opener = "2.2.7"
flate2 = "1.0"
tar = "0.4"
rand = "0.8"

View File

@ -2,13 +2,9 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"windows": ["main"],
"remote": {
"urls": [
"http://*"
]
"urls": ["http://*"]
},
"permissions": [
"core:default",
@ -19,6 +15,7 @@
"core:app:allow-set-app-theme",
"core:window:allow-set-focus",
"os:default",
"opener:default",
"log:default",
"updater:default",
"dialog:default",
@ -77,4 +74,4 @@
},
"store:default"
]
}
}

View File

@ -17,6 +17,7 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_updater::Builder::new().build())

View File

@ -31,6 +31,7 @@
"@tanstack/react-router-devtools": "^1.116.0",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.7",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-updater": "^2.7.1",
"@types/react-syntax-highlighter": "^15.5.13",

View File

@ -16,7 +16,7 @@ const buttonVariants = cva(
},
size: {
default: 'h-7 px-3 py-2 has-[>svg]:px-3 rounded-sm',
sm: 'h-6 rounded gap-1.5 px-2 has-[>svg]:px-2.5',
sm: 'h-6 gap-1.5 px-2 has-[>svg]:px-2.5 rounded-sm',
lg: 'h-9 rounded-md px-4 has-[>svg]:px-4',
icon: 'size-8',
},

View File

@ -0,0 +1,81 @@
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { IconFolder } from '@tabler/icons-react'
interface ChangeDataFolderLocationProps {
children: React.ReactNode
currentPath: string
newPath: string
onConfirm: () => void
open: boolean
onOpenChange: (open: boolean) => void
}
export default function ChangeDataFolderLocation({
children,
currentPath,
newPath,
onConfirm,
open,
onOpenChange,
}: ChangeDataFolderLocationProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<IconFolder size={20} />
Change Data Folder Location
</DialogTitle>
<DialogDescription>
Are you sure you want to change the data folder location? This will
move all your data to the new location and restart the application.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-main-view-fg/80 mb-2">
Current Location:
</h4>
<div className="bg-main-view-fg/5 border border-main-view-fg/10 rounded">
<code className="text-xs text-main-view-fg/70 break-all">
{currentPath}
</code>
</div>
</div>
<div>
<h4 className="text-sm font-medium text-main-view-fg/80 mb-2">
New Location:
</h4>
<div className="bg-accent/10 border border-accent/20 rounded">
<code className="text-xs text-accent break-all">{newPath}</code>
</div>
</div>
</div>
<DialogFooter className="flex items-center gap-2">
<DialogClose asChild>
<Button variant="link" size="sm">
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={onConfirm}>Change Location</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@ -10,6 +10,8 @@ import { useTranslation } from 'react-i18next'
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
import { useEffect, useState } from 'react'
import { open } from '@tauri-apps/plugin-dialog'
import { revealItemInDir } from '@tauri-apps/plugin-opener'
import ChangeDataFolderLocation from '@/containers/dialogs/ChangeDataFolderLocation'
import {
Dialog,
@ -32,19 +34,35 @@ import {
IconExternalLink,
IconFolder,
IconLogs,
IconCopy,
IconCopyCheck,
} from '@tabler/icons-react'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { windowKey } from '@/constants/windows'
import { toast } from 'sonner'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.settings.general as any)({
component: General,
})
const openFileTitle = (): string => {
if (IS_MACOS) {
return 'Show in Finder'
} else if (IS_WINDOWS) {
return 'Show in File Explorer'
} else {
return 'Open Containing Folder'
}
}
function General() {
const { t } = useTranslation()
const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting()
const [janDataFolder, setJanDataFolder] = useState<string | undefined>()
const [isCopied, setIsCopied] = useState(false)
const [selectedNewPath, setSelectedNewPath] = useState<string | null>(null)
const [isDialogOpen, setIsDialogOpen] = useState(false)
useEffect(() => {
const fetchDataFolder = async () => {
@ -97,6 +115,52 @@ function General() {
}
}
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 2000) // Reset after 2 seconds
} catch (error) {
console.error('Failed to copy to clipboard:', error)
}
}
const handleDataFolderChange = async () => {
const selectedPath = await open({
multiple: false,
directory: true,
defaultPath: janDataFolder,
})
if (selectedPath === janDataFolder) return
if (selectedPath !== null) {
setSelectedNewPath(selectedPath)
setIsDialogOpen(true)
}
}
const confirmDataFolderChange = async () => {
if (selectedNewPath) {
try {
setJanDataFolder(selectedNewPath)
await relocateJanDataFolder(selectedNewPath)
// Only relaunch if relocation was successful
window.core?.api?.relaunch()
setSelectedNewPath(null)
setIsDialogOpen(false)
} catch (error) {
console.error('Failed to relocate data folder:', error)
// Revert the data folder path on error
const originalPath = await getJanDataFolder()
setJanDataFolder(originalPath)
toast.error(
'Failed to relocate data folder. Please try again or choose a different location.'
)
}
}
}
return (
<div className="flex flex-col h-full">
<HeaderPage>
@ -133,42 +197,71 @@ function General() {
{t('settings.dataFolder.appDataDesc', {
ns: 'settings',
})}
&nbsp;
</span>
<span
title={janDataFolder}
className="bg-main-view-fg/10 text-xs mt-1 px-1 py-0.5 rounded-sm text-main-view-fg/80 line-clamp-1"
>
{janDataFolder}
</span>
<div className="flex items-center gap-2 mt-1">
<span
title={janDataFolder}
className="bg-main-view-fg/10 text-xs px-1 py-0.5 rounded-sm text-main-view-fg/80"
>
{janDataFolder}
</span>
<button
onClick={() =>
janDataFolder && copyToClipboard(janDataFolder)
}
className="cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out p-1"
title={isCopied ? 'Copied!' : 'Copy path'}
>
{isCopied ? (
<div className="flex items-center gap-1">
<IconCopyCheck size={12} className="text-accent" />
<span className="text-xs leading-0">Copied</span>
</div>
) : (
<IconCopy
size={12}
className="text-main-view-fg/50"
/>
)}
</button>
</div>
</>
}
actions={
<Button
variant="link"
size="sm"
className="hover:no-underline"
title="App Data Folder"
onClick={async () => {
const selectedPath = await open({
multiple: false,
directory: true,
defaultPath: janDataFolder,
})
if (selectedPath === janDataFolder) return
if (selectedPath !== null) {
setJanDataFolder(selectedPath)
await relocateJanDataFolder(selectedPath)
window.core?.api?.relaunch()
// TODO: we need function to move everything into new folder selectedPath
// eg like this
// await window.core?.api?.moveDataFolder(selectedPath)
}
}}
>
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out">
<IconFolder size={18} className="text-main-view-fg/50" />
</div>
</Button>
<>
<Button
variant="link"
size="sm"
className="p-0"
title="App Data Folder"
onClick={handleDataFolderChange}
>
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
<IconFolder
size={12}
className="text-main-view-fg/50"
/>
<span>Change Location</span>
</div>
</Button>
{selectedNewPath && (
<ChangeDataFolderLocation
currentPath={janDataFolder || ''}
newPath={selectedNewPath}
onConfirm={confirmDataFolderChange}
open={isDialogOpen}
onOpenChange={(open) => {
setIsDialogOpen(open)
if (!open) {
setSelectedNewPath(null)
}
}}
>
<div />
</ChangeDataFolderLocation>
)}
</>
}
/>
<CardItem
@ -177,17 +270,47 @@ function General() {
})}
description="View detailed logs of the App"
actions={
<Button
variant="link"
size="sm"
onClick={handleOpenLogs}
title="App Logs"
>
{/* Open Logs */}
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out">
<IconLogs size={18} className="text-main-view-fg/50" />
</div>
</Button>
<div className="flex items-center gap-2">
<Button
variant="link"
size="sm"
className="p-0"
onClick={handleOpenLogs}
title="App Logs"
>
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
<IconLogs size={12} className="text-main-view-fg/50" />
<span>Open Logs</span>
</div>
</Button>
<Button
variant="link"
size="sm"
className="p-0"
onClick={async () => {
if (janDataFolder) {
try {
const logsPath = `${janDataFolder}/logs`
await revealItemInDir(logsPath)
} catch (error) {
console.error(
'Failed to reveal logs folder:',
error
)
}
}
}}
title="Reveal logs folder in file explorer"
>
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
<IconFolder
size={12}
className="text-main-view-fg/50"
/>
<span>{openFileTitle()}</span>
</div>
</Button>
</div>
}
/>
</Card>

View File

@ -140,11 +140,13 @@ function LocalAPIServer() {
<Button
variant="link"
size="sm"
className="p-0"
onClick={handleOpenLogs}
title="Server Logs"
>
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out">
<div className="cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out px-2 py-1 gap-1">
<IconLogs size={18} className="text-main-view-fg/50" />
<span>Open Logs</span>
</div>
</Button>
}