Electron Adventures: Episode 49: Mkdir Dialog

It's time to add our first dialog - one for creating a new directory. But wait, it's not actually our first one, command palette is a dialog too.

So before we do anything let's refactor some code so it supports both dialogs - and more we'll be adding in the future.

Rename closePalette to closeDialog

First, in src/commands.js, let's replace palette context with a single closePalette command, just renamed to dialog context with a single command closeDialog:

  dialog: [
    {
      shortcuts: [{key: "Escape"}],
      action: ["app", "closeDialog"],
    }
  ],

And let's change calls to app.closePalette() in src/CommandPalette.svelte, src/CommandPaletteEntry.svelte and src/App.svelte.

Start event chain in src/Panel.svelte

When the user presses F7, we need to bounce the event around a bit. First we need to send it to the active panel, because that's the component which knows where we're creating that directory at.

So here's another entry for src/commands.js:

    {
      name: "Create Directory",
      shortcuts: [{key: "F7"}],
      action: ["activePanel", "createDirectory"],
    },

And here's its handler in src/Panel.svelte:

  function createDirectory() {
    app.openMkdirDialog(directory)
  }

We don't need to do anything special here, just add current directory to the event and pass it along to the app.

Continue event chain in src/App.svelte

The App component used to have paletteOpen boolean flag. We need to replace it with dialog object.

Here's the relevant functions:

  let dialog = null

  $: {
    keyboardMode = "default"
    if (dialog) keyboardMode = "dialog"
    if (preview) keyboardMode = "preview"
  }
  function openPalette() {
    dialog = {type: "palette"}
  }
  function openMkdirDialog(base) {
    dialog = {type: "mkdir", base}
  }
  function closeDialog() {
    dialog = null
  }

And we also need to add it to the template:

{#if preview}
  <Preview {...preview} />
{/if}

<div class="ui">
  <header>
    File Manager
  </header>
  <Panel initialDirectory={initialDirectoryLeft} id="left" />
  <Panel initialDirectory={initialDirectoryRight} id="right" />
  <Footer />
</div>

<Keyboard mode={keyboardMode} />

{#if dialog}
  {#if dialog.type === "palette"}
    <CommandPalette />
  {:else if dialog.type === "mkdir"}
    <MkdirDialog base={dialog.base} />
  {/if}
{/if}

CommandPalette, MkdirDialog, and future dialogs we'll be adding share a lot of functionality, so perhaps there should be a Dialog component which includes them, and sets them in a proper place.

src/MkdirDialog.svelte

We just need a simple dialog with one input and the usual OK/Cancel buttons. One thing we've learned from the time when Orthodox File Managers were first created is that "OK" buttons should never actually say "OK", they should describe the actual action.

<form on:submit|preventDefault={submit}>
  <label>
    <div>Enter directory name:</div>
    <input use:focus bind:value={dir} placeholder="directory">
  </label>
  <div class="buttons">
    <button type="submit">Create directory</button>
    <button on:click={app.closeDialog}>Cancel</button>
  </div>
</form>

Styling is very close to what CommandPalette and Footer already do:

<style>
  form {
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    margin: auto;
    padding: 8px;
    max-width: 50vw;
    background: #338;
    box-shadow: 0px 0px 24px #004;
  }

  input {
    font-family: inherit;
    background-color: inherit;
    font-size: inherit;
    font-weight: inherit;
    box-sizing: border-box;
    width: 100%;
    margin: 0;
    background: #66b;
    color: inherit;
  }

  input::placeholder {
    color: inherit;
    font-style: italic;
  }

  .feedback {
    font-style: italic;
  }

  .buttons {
    display: flex;
    flex-direction: row-reverse;
    margin-top: 8px;
    gap: 8px;
  }

  button {
    font-family: inherit;
    font-size: inherit;
    background-color: #66b;
    color: inherit;
  }
</style>

The dialog does very little - beyond some boilerplate it just needs a submit handler. Then if user typed anything, we create a new directory relative to the current directory of the active panel. If user typed nothing, we just close.

<script>
  export let base

  import path from "path-browserify"
  import { getContext } from "svelte"

  let { eventBus } = getContext("app")
  let dir = ""

  let app = eventBus.target("app")

  function submit() {
    app.closeDialog()
    if (dir !== "") {
      let target = path.join(base, dir)
      window.api.createDirectory(target)
    }
  }
  function focus(el) {
    el.focus()
  }
</script>

To create new directory we need to add function to preload.js

preload.js

It used to be a huge pain to create directories in JavaScript, but node finally added {recursive: true} which makes it simple enough:

let createDirectory = (dir) => {
  fs.mkdirSync(dir, {recursive: true})
}

Result

Here's the results:

electron-adventures-49-screenshot.png

It opens a dialog, lets user type a name, and then creates the directory. So what's missing?

  • any kind of error handling - if user tries to create directory in place they have no access to, or operating system returns any other kind of error, we don't let them know in any way
  • any kind of instant feedback - and really, we can predict what would happen. If user tries to create /etc/hack we could give live feedback below the input saying that /etc is read only for them, and such things. This is a wishlist item, which we'll likely not get to in this series, but a polished program would at least give it a try to cover more common scenarios. "It didn't work" messages should be a fallback, not a regular occurrence.
  • once we create the directory, it's not actually displayed in the active panel, as it doesn't refresh unless you navigate somewhere

In the next episode, we'll try to deal with that last problem, and refresh panels when necessary, as well as add a manual refresh command.

As usual, all the code for the episode is here.