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:
parent
057accfb96
commit
b98c31b184
@ -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"
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
81
web-app/src/containers/dialogs/ChangeDataFolderLocation.tsx
Normal file
81
web-app/src/containers/dialogs/ChangeDataFolderLocation.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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',
|
||||
})}
|
||||
|
||||
</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>
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user