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-log = "2.0.0-rc"
|
||||||
tauri-plugin-shell = "2.2.0"
|
tauri-plugin-shell = "2.2.0"
|
||||||
tauri-plugin-os = "2.2.1"
|
tauri-plugin-os = "2.2.1"
|
||||||
|
tauri-plugin-opener = "2.2.7"
|
||||||
flate2 = "1.0"
|
flate2 = "1.0"
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|||||||
@ -2,13 +2,9 @@
|
|||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "enables the default permissions",
|
"description": "enables the default permissions",
|
||||||
"windows": [
|
"windows": ["main"],
|
||||||
"main"
|
|
||||||
],
|
|
||||||
"remote": {
|
"remote": {
|
||||||
"urls": [
|
"urls": ["http://*"]
|
||||||
"http://*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
@ -19,6 +15,7 @@
|
|||||||
"core:app:allow-set-app-theme",
|
"core:app:allow-set-app-theme",
|
||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
"os:default",
|
"os:default",
|
||||||
|
"opener:default",
|
||||||
"log:default",
|
"log:default",
|
||||||
"updater:default",
|
"updater:default",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
|
|||||||
@ -17,6 +17,7 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
|
|||||||
@ -31,6 +31,7 @@
|
|||||||
"@tanstack/react-router-devtools": "^1.116.0",
|
"@tanstack/react-router-devtools": "^1.116.0",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.2.7",
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-7 px-3 py-2 has-[>svg]:px-3 rounded-sm',
|
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',
|
lg: 'h-9 rounded-md px-4 has-[>svg]:px-4',
|
||||||
icon: 'size-8',
|
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 { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { revealItemInDir } from '@tauri-apps/plugin-opener'
|
||||||
|
import ChangeDataFolderLocation from '@/containers/dialogs/ChangeDataFolderLocation'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -32,19 +34,35 @@ import {
|
|||||||
IconExternalLink,
|
IconExternalLink,
|
||||||
IconFolder,
|
IconFolder,
|
||||||
IconLogs,
|
IconLogs,
|
||||||
|
IconCopy,
|
||||||
|
IconCopyCheck,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
import { windowKey } from '@/constants/windows'
|
import { windowKey } from '@/constants/windows'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const Route = createFileRoute(route.settings.general as any)({
|
export const Route = createFileRoute(route.settings.general as any)({
|
||||||
component: General,
|
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() {
|
function General() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting()
|
const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting()
|
||||||
const [janDataFolder, setJanDataFolder] = useState<string | undefined>()
|
const [janDataFolder, setJanDataFolder] = useState<string | undefined>()
|
||||||
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
|
const [selectedNewPath, setSelectedNewPath] = useState<string | null>(null)
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDataFolder = async () => {
|
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 (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
@ -133,42 +197,71 @@ function General() {
|
|||||||
{t('settings.dataFolder.appDataDesc', {
|
{t('settings.dataFolder.appDataDesc', {
|
||||||
ns: 'settings',
|
ns: 'settings',
|
||||||
})}
|
})}
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<span
|
<span
|
||||||
title={janDataFolder}
|
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"
|
className="bg-main-view-fg/10 text-xs px-1 py-0.5 rounded-sm text-main-view-fg/80"
|
||||||
>
|
>
|
||||||
{janDataFolder}
|
{janDataFolder}
|
||||||
</span>
|
</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={
|
actions={
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="hover:no-underline"
|
className="p-0"
|
||||||
title="App Data Folder"
|
title="App Data Folder"
|
||||||
onClick={async () => {
|
onClick={handleDataFolderChange}
|
||||||
const selectedPath = await open({
|
>
|
||||||
multiple: false,
|
<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">
|
||||||
directory: true,
|
<IconFolder
|
||||||
defaultPath: janDataFolder,
|
size={12}
|
||||||
})
|
className="text-main-view-fg/50"
|
||||||
if (selectedPath === janDataFolder) return
|
/>
|
||||||
if (selectedPath !== null) {
|
<span>Change Location</span>
|
||||||
setJanDataFolder(selectedPath)
|
</div>
|
||||||
await relocateJanDataFolder(selectedPath)
|
</Button>
|
||||||
window.core?.api?.relaunch()
|
{selectedNewPath && (
|
||||||
// TODO: we need function to move everything into new folder selectedPath
|
<ChangeDataFolderLocation
|
||||||
// eg like this
|
currentPath={janDataFolder || ''}
|
||||||
// await window.core?.api?.moveDataFolder(selectedPath)
|
newPath={selectedNewPath}
|
||||||
|
onConfirm={confirmDataFolderChange}
|
||||||
|
open={isDialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setIsDialogOpen(open)
|
||||||
|
if (!open) {
|
||||||
|
setSelectedNewPath(null)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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 />
|
||||||
<IconFolder size={18} className="text-main-view-fg/50" />
|
</ChangeDataFolderLocation>
|
||||||
</div>
|
)}
|
||||||
</Button>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CardItem
|
<CardItem
|
||||||
@ -177,17 +270,47 @@ function General() {
|
|||||||
})}
|
})}
|
||||||
description="View detailed logs of the App"
|
description="View detailed logs of the App"
|
||||||
actions={
|
actions={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="p-0"
|
||||||
onClick={handleOpenLogs}
|
onClick={handleOpenLogs}
|
||||||
title="App Logs"
|
title="App Logs"
|
||||||
>
|
>
|
||||||
{/* Open 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">
|
||||||
<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={12} className="text-main-view-fg/50" />
|
||||||
<IconLogs size={18} className="text-main-view-fg/50" />
|
<span>Open Logs</span>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</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>
|
</Card>
|
||||||
|
|||||||
@ -140,11 +140,13 @@ function LocalAPIServer() {
|
|||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="p-0"
|
||||||
onClick={handleOpenLogs}
|
onClick={handleOpenLogs}
|
||||||
title="Server Logs"
|
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" />
|
<IconLogs size={18} className="text-main-view-fg/50" />
|
||||||
|
<span>Open Logs</span>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user