How to use the file upload dialog in a custom Vue component

Hello,

I am trying to figure out how to use the k-upload-dialog in a custom Vue component I am making to implement a data importing feature in the Panel.

I started by looking at the docs at Kirby Panel, however that didn’t make much sense since the code example only shows:

<k-button icon="open" variant="filled" @click="openEmpty">
    Open
</k-button>

After looking at some of the other examples, I ended up writing this in my Vue component:

<template>
  <k-field class="k-doi-field" :label="label">
    <k-button
      icon="open"
      variant="filled"
      @click="
        $panel.dialog.open({
          component: 'k-upload-dialog',
        })
      "
    >
      Open
    </k-button>
  </k-field>
</template>

However, this does not work since the upload URL is missing. I asked about this on Discord, and @distantnative replied:

Instead of implementing the component etc., you likely want to rather use Kirby Panel

You can see the options and methods (pick or open most relevant for you). URL option is e.g. the URL to the API endpoint that handles the uploaded files.

Based on this feedback I have written:

<script>
export default {
  methods: {
    onUploadClick(event) {
      let options = {
        "accept": "*",
        "attributes": {},
        "files": [],
        "max": null,
        "multiple": true,
        "replacing": null,
        "url": null
      }

      window.panel.upload.open(options);
    }
  }
}
</script>

<template>
  <k-field :label="label">
    <k-button
      icon="open"
      variant="filled"
      @click="onUploadClick"
    >
      Upload
    </k-button>
  </k-field>
</template>

However, I am still somewhat new to this so I don’t fully understand how all the pieces fit together.

In terms of creating a URL for the options object, the most promising thing I’ve found in the docs is the REST API endpoint for uploading a new file: <code>POST</code> /api/pages/:id/files | Kirby CMS, though I haven’t had a chance to test this since learning about it.

This is as far as I’ve gotten so far, so some more elaboration about the best way to use Panel components like the file upload dialog would be much appreciated!

Okay, I’ve had a chance to do a bit more testing. It took me a while to realize that the ID in the endpoint URL should be more like a slug or path rather than a Uuid. However, now I can get files to successfully upload to a specific page by using this code:

<script>
export default {
  methods: {
    onUploadClick(event) {
      let options = {
        "accept": "*",
        "attributes": {},
        "files": [],
        "max": null,
        "multiple": true,
        "replacing": null,
        "url": "/api/pages/publications+test/files"
      }

      window.panel.upload.open(options);
    }
  }
}
</script>

<template>
  <k-field :label="label">
    <k-button
      icon="open"
      variant="filled"
      @click="onUploadClick"
    >
      Upload
    </k-button>
  </k-field>
</template>

However, this is obviously not a full solution since the ID has been hard-coded into the path.

So, I think my most immediate questions are (1) what is the best way to get the ID for the current page that is open in the Panel? Do I need to figure out how to do that myself, or does Kirby have a method somewhere that does this? (2) How do I use parameters in my POST request? For example, the docs suggest that I should be able to specify the template to use when uploading a file. Does that get specified in the options object somehow?

For more general questions the distinction between using a method like window.panel.upload.open() and using a Kirby component inside the <template></template> tag isn’t fully clear to me yet. I’m assuming that Vue is parsing the custom HTML tags inside the <template></template> and converting them into valid and fully-featured HTML based on the definition for that component, and using a method like window.panel.upload.open() is relying on the Panel having already defined some methods and hoping they are available when I want to use them (I suppose I can test by just calling these methods in the console while I have the Panel open). I think what would be most helpful is to understand when it is appropriate to use one approach over the other.

Also, I’m assuming the Panel is handling a bunch of things like authentication in the REST API for me somehow, but I don’t know if there are things there that I should know about or if I can simply rely on the fact that it seems to work.

Since you are building an alternative to the files field as it seems, I recommend to have a look at the files field to see how it uses panel.upload: kirby/panel/src/components/Forms/Field/FilesField.vue at main · getkirby/kirby · GitHub

Though the files field is using its own API endpoint for the field, defined in kirby/config/fields/files.php at main · getkirby/kirby · GitHub.

For the id, you can check e.g. endpoints.model - endpoints is an object every field receives by default when defined as prop.

We also have the this.$api.pages.id() helper for changing the normal id format (notes/my-note) into the API endpoint format (notes+my-note).

The template can be passed as an entry in the attributes object passed to panel.upload.

In general, components often are more the building blocks, but not always contain the logic itself. The upload dialog is an extreme case. Using the component alone, you would have to implement all/most of the logic that kirby/panel/src/panel/upload.js at main · getkirby/kirby · GitHub contains yourself. Which one could do, but of course doesn’t make much sense.

With dialogs in general, we are trying to move more away from using the dialog components directly in your other component template tags, but rather use panel.dialog.open() here, whether for backend-defined dialogs or just opening a dialog Vue component. The benefit here is that the Panel can jump in and handle a bunch of things as well that otherwise each developer would need to implement themselves.

Overall, you are diving quite deeply into Panel development where most parts aren’t fully documented so far - unfortunately but that’s the case and you need to be ready to dive through our code files to figure things out a bit (which based on your posts you are doing very well).

Hi Nico,

Thanks for the detailed reply! This did a great job of pointing me in the right direction. I had looked the core files field a bit already but without context it didn’t make much sense.

Anyways, I have assembled something that appears to work! It took me some time to track down how to define the endpoints as a prop, but I’m guessing this is something that would be more familiar if I knew Vue.

<script>
export default {
  props: {
    endpoints: Object,
  },
  methods: {
    onUploadClick(event) {
      let options = {
        "accept": "*",
        "attributes": {},
        "files": [],
        "max": null,
        "multiple": false,
        "replacing": null,
        "url": this.$panel.urls.api + this.endpoints.model + "/fields/files/upload"
      }
      
      window.panel.upload.pick(options);

    }
  }
}
</script>

<template>
  <k-field :label="label">
    <k-button
      icon="upload"
      variant="filled"
      @click="onUploadClick"
    >
      Upload
    </k-button>
  </k-field>
</template>

Would you consider it to be best practice to use the default files endpoint by using "url": this.$panel.urls.api + this.endpoints.model + "/files" or the custom one for the files field at "url": this.$panel.urls.api + this.endpoints.model + "/fields/files/upload"? I tried both and I didn’t notice any immediate differences, but I’m guessing that the custom one would be better since it looks like it has extra methods and validation logic.

I think I understand. Because dialogs in the Panel are so reliant on the surrounding logic that is required for them to function, this is a case where it is better to use the internal methods (Kirby Panel) rather than the components. So I guess I’m sort of operating in an awkward in-between where I don’t want to handle all of the uploading process myself, but I’m also not using/extending the fully-featured and built-in files field.

On this note, there is one other thing I was hoping to do in my file field; however, I’m worried that it might not be possible without even heavier modifications. What I’m hoping to do is to have the files begin to upload automatically as soon as files are chosen but still show the progress and errors in the dialog.

I tried setting "immediate": true, in my options that I pass to window.panel.upload.pick(options); However, this makes it so that upload dialog is never shown and I loose the progress indicators and error messages.

I did find that some others on the forum were trying to do something similar, but globally: Kirby 4 - Skip Fileupload Dialog - #4 by toebu

I tried their solution shown above, and it does exactly what I hoped. However, I can’t see an easy way to replicate this behavior but keep it localized to my field without re-inventing the wheel so to speak.

I was hoping there would be a way for me to hook a callback into an event that happens right after the files are picked, so that then I could submit the dialog using window.panel.dialog.submit(). However, I couldn’t figure out a way to do that, so it seems like I would have to re-create parts of kirby/panel/src/panel/upload.js at main · getkirby/kirby · GitHub to achieve this, which probably isn’t worth it to save one click. Would you agree with my interpretation in this situation?

I have learned more!

I am now changing my mind on this point. Turns out that the custom endpoint for the files field is dynamic and changes based on the name that the user used for that field in the blueprint. I thought this.endpoints.field was about the field type, but turns out it is the field name.

So, using the custom endpoint for the files custom field was only working because I had a files field in my blueprint that I had named “files.”

Additionally, it looks to me like the files section uses the default file upload endpoint, though I don’t fully understand what this.options.upload.api is. However, if I use window.panel.upload.state() in the browser console while the upload dialog is active, I do indeed see that the url is /api/pages/:id/files

So, now I’m thinking that it is best to stick with the default files endpoint or consider making my own if I need some extra functionality. It also looks like I could piggy-back on the files custom endpoint if I wanted using something like this example from @distantnative’s CSV plugin:

'api' => fn () => [
				...(require $this->kirby()->core()->fields()['files'])['api'](),
				[
					'pattern' => '',
					'method'  => '',
					'action'  => function () {//a bunch of stuff}
				]
			]

I also figured out a very hacky way to contain the automatic upload behavior. Basically, I can add a custom string to my API endpoint, and then use that to detect whether or not to automatically trigger the uploading:

import FileUploadField from "./components/FileUploadField.vue";

window.panel.plugin("gaufde/publications", {
  fields: {
    fileupload: FileUploadField
  },
  components: {
    'k-upload-dialog': {
      extends: 'k-upload-dialog',
      created() {
        const options = window.panel.upload.state()

        if (options.url.includes("my_custom_string")) {
          options.url = options.url.replace("my_custom_string", "")
          window.panel.upload.set(options);

          this.$emit('submit');
        }
      },
    },
  },
});

I’m still a bit curious if there is less hacky way to do the above, but I’m also getting less interested in making this modification since it doesn’t fit with how Kirby’s file upload is designed to work. So, while I’m still interested in learning better techniques, I’ll probably scrap this modification for my project.

@gaufde I can’t follow all your thoughts fully but glad if you’re making progress.

Can you test if adding something like this

on: {
  open: () => {
    this.$panel.upload.submit();
  }
}

to the upload options works to start the uploading directly when the dialog opens?

That is fair! This thread has become a bit of a journal. I’m adding update posts as I learn things that I think would be useful to someone else.

<script>
export default {
  props: {
    endpoints: Object,
  },
  methods: {
    onUploadClick(event) {
      let options = {
        "accept": "*",
        "attributes": {},
        "files": [],
        "max": null,
        "multiple": false,
        "replacing": null,
        "url": this.$panel.urls.api + this.endpoints.model + "/files",
        on: {
          open: () => {
            this.$panel.upload.submit();
          }
        }

      }

      console.log(options);

      window.panel.upload.pick(options);

    }
  }
}
</script>

<template>
  <k-field :label="label">
    <k-button
      icon="upload"
      variant="filled"
      @click="onUploadClick"
    >
      Upload
    </k-button>
  </k-field>
</template>

I just tested this and it doesn’t appear to work. It would be very cool if it did though.

If you put a console log statement inside, does it not get called at all? Or just the submit call doesn’t yield the wanted result?

on: {
          open: () => {
            console.log("On Open");
            this.$panel.upload.submit();
          }
        }

The console log statement does not get called.

My bad - I think this is missing: `panel.upload`: emit `open` event by distantnative · Pull Request #6621 · getkirby/kirby · GitHub

No worries! I’ll try it again after that is merged.

Does this mean that injecting code in this way should be possible throughout different panel actions? Or is this something that you are adding specifically to panel.upload for this use-case?

It’s possible for some aspects like panel.dialog and panel.drawer. Not sure what you think of with “throughout different panel action”.