As we manage files there will be errors, and we need to handle them somehow.
Panel unable to fetch directory listing
The first error condition we should deal with is when the panel is unable to fetch the directory listing. This could happen because user tries to enter directory belonging to another user. Or because the directory got deleted and panel is now trying to refresh.
There is a correct solution to this, and the correct solution is to do absolutely nothing. If user tries to navigate to directory they have no access to, just stay where they are. If directory is gone, just keep going up a level until file manager reaches directory that is accessible. This behaviour is much more understandable to the user than error popups, as it should be completely obvious to the user where they still are.
Even more optimally we could display some sort of feedback, as long as it didn't stop the user. Right now we don't have any such mechanism.
Here are relevant changes to src/Panel.svelte
:
$: fetchFiles(directory)
async function fetchFiles() {
try {
files = await window.api.directoryContents(directory)
setInitialSelected()
setInitialFocus()
} catch (err) {
console.log(err)
if (directory === "/") {
files = []
} else {
initialFocus = path.basename(directory)
directory = path.join(directory, "..")
}
}
}
Unfortunately current implementation resets selection on error. Keeping selection on failed navigation would require a bit more bookkeeping.
Errors we want to display
If creating new directories and deleting files fails, error should be displayed, as it's not obvious what is the fallback.
Taking a step back
As I was writing this, I noticed that the fancy dialog system I setup in the previous episode wasn't actually doing what I needed. So we'll have to go through a lot of files again and I'll try to explain what I had to change and why.
src/Dialog.svelte
The fancy metaprogramming I setup wasn't actually working very well when I tried to transition from one open dialog (mkdir or delete) directly to another open dialog (error). Svelte supports $$props
for all props, but doesn't automatically react to new unknown props being added or removed from it while component is mounted, so we'd need to write a bit of extra code.
So instead I changed it to use two props - type
and data
. That's a bit extra verbosity upstream, but it would get a bit difficult to understand otherwise.
Also because error dialog needs to be of a different color, some of the styling got moved into individual dialogs.
<script>
import CommandPalette from "./CommandPalette.svelte"
import DeleteDialog from "./DeleteDialog.svelte"
import MkdirDialog from "./MkdirDialog.svelte"
import ErrorDialog from "./ErrorDialog.svelte"
let component = {CommandPalette, MkdirDialog, DeleteDialog, ErrorDialog}
export let type
export let data = {}
</script>
<div>
<svelte:component this={component[type]} {...data}/>
</div>
<style>
div {
position: fixed;
left: 0;
top: 0;
right: 0;
margin: auto;
max-width: 50vw;
}
</style>
src/App.svelte
Instead of having event handler per dialog, App
component just has a single openDialog
method.
The exception is openPalette
which stayed separate, because this one goes directly from a keyboard shortcut, so we need some target that gets invoked without any arguments. It could be defined as openDialog("CommandPalette")
too.
function openPalette() {
dialog = {type: "CommandPalette"}
}
function openDialog(type, data) {
dialog = {type, data}
}
src/Panel.svelte
The F7 and F8 handlers changed to use the new API.
function createDirectory() {
app.openDialog("MkdirDialog", {base: directory})
}
function deleteFiles() {
let filesTodo
if (selected.length) {
filesTodo = selected.map(idx => files[idx].name)
} else if (focused && focused.name !== "..") {
filesTodo = [focused.name]
} else {
return
}
app.openDialog("DeleteDialog", {base: directory, files: filesTodo})
}
src/MkdirDialog.svelte
We need to add a try/catch
block. The catch
section logs the error both to console and to the error
dialog. We still need to call refresh
even if an error happened.
function submit() {
app.closeDialog()
if (dir !== "") {
let target = path.join(base, dir)
try {
window.api.createDirectory(target)
} catch (err) {
console.log(`Error creating directory ${target}`, err)
app.openDialog("ErrorDialog", {error: `Error creating directory ${target}: ${err.message}`})
}
bothPanels.refresh()
}
}
Styling also got a section on how to color this dialog:
form {
padding: 8px;
background: #338;
box-shadow: 0px 0px 24px #004;
}
src/DeleteDialog.svelte
We need a try/catch
block here as well. We actually need to do refresh
and return
in the loop in case of error, as normally we close the dialog once we finish, but if we just break
from the loop we'd be closing error dialog e just opened.
Because this error comes from running external program to move things to trash, it's honestly quite terrible. I don't know if there's any better JavaScript packages for moving files to trash. If you know of any, let me know in the comments.
async function submit() {
for (let file of files) {
let fullPath = path.join(base, file)
try {
await window.api.moveFileToTrash(fullPath)
} catch(err) {
console.log(`Error deleting file ${fullPath}`, err)
app.openDialog("ErrorDialog", {error: `Error deleting file ${fullPath}: ${err.message}`})
bothPanels.refresh()
return
}
}
app.closeDialog()
bothPanels.refresh()
}
It also got the same styling as MkdirDialog
.
src/ErrorDialog.svelte
ErrorDialog
only has OK button, which is totally fine, as it's purely informational, and that OK does not represent any action. Using OK buttons to confirm an action is terrible design I complained about many times before, but that's not what we're doing here - we're just informing the user.
<script>
export let error
import { getContext } from "svelte"
let { eventBus } = getContext("app")
let app = eventBus.target("app")
function submit() {
app.closeDialog()
}
function focus(el) {
el.focus()
}
</script>
<form on:submit|preventDefault={submit}>
<div>{error}</div>
<div class="buttons">
<button type="submit" use:focus>OK</button>
</div>
</form>
<style>
form {
padding: 8px;
background: #833;
box-shadow: 0px 0px 24px #400;
}
.buttons {
display: flex;
flex-direction: row-reverse;
margin-top: 8px;
gap: 8px;
}
button {
font-family: inherit;
font-size: inherit;
background-color: #b66;
color: inherit;
}
</style>
I feel like there's too much boilerplate here for something so simple, and maybe we should move some of those things out.
Also I don't love this shade of red.
Result
Here's the results:
In the next episode, we'll take a break from the file manager for a while, and see what other interesting things we can do in Electron. This series turned a bit too much into File Manager Development series, and while the file manager keeps bringing new interesting issues to talk about, that's not quite what I had in mind.
As usual, all the code for the episode is here.
Top comments (0)