uppy icon indicating copy to clipboard operation
uppy copied to clipboard

RFC: deprecate Robodog in favor of presets

Open Murderlon opened this issue 2 years ago • 18 comments

Problem

We want to market Uppy as an easy to integrate product. When we look at competitors of Uppy, they showcase a couple of lines for the integration to get a UI and upload experience. When people visit Uppy, one of the things they will likely see first is the dashboard example. The integration code shown below the dashboard is extensive.

Integration code from examples/dashboard
const Uppy = require('@uppy/core')
const Dashboard = require('@uppy/dashboard')
const GoogleDrive = require('@uppy/google-drive')
const Dropbox = require('@uppy/dropbox')
const Box = require('@uppy/box')
const Instagram = require('@uppy/instagram')
const Facebook = require('@uppy/facebook')
const OneDrive = require('@uppy/onedrive')
const Webcam = require('@uppy/webcam')
const ScreenCapture = require('@uppy/screen-capture')
const ImageEditor = require('@uppy/image-editor')
const Tus = require('@uppy/tus')
const Url = require('@uppy/url')
const DropTarget = require('@uppy/drop-target')
const GoldenRetriever = require('@uppy/golden-retriever')

const uppy = new Uppy({
  debug: true,
  autoProceed: false,
  restrictions: {
    maxFileSize: 1000000,
    maxNumberOfFiles: 3,
    minNumberOfFiles: 2,
    allowedFileTypes: ['image/*', 'video/*'],
    requiredMetaFields: ['caption'],
  }
})
.use(Dashboard, {
  trigger: '.UppyModalOpenerBtn',
  inline: true,
  target: '.DashboardContainer',
  showProgressDetails: true,
  note: 'Images and video only, 2–3 files, up to 1 MB',
  height: 470,
  metaFields: [
    { id: 'name', name: 'Name', placeholder: 'file name' },
    { id: 'caption', name: 'Caption', placeholder: 'describe what the image is about' }
  ],
  browserBackButtonClose: false
})
.use(GoogleDrive, { target: Dashboard, companionUrl: 'https://companion.uppy.io' })
.use(Dropbox, { target: Dashboard, companionUrl: 'https://companion.uppy.io' })
.use(Box, { target: Dashboard, companionUrl: 'https://companion.uppy.io' })
.use(Instagram, { target: Dashboard, companionUrl: 'https://companion.uppy.io' })
.use(Facebook, { target: Dashboard, companionUrl: 'https://companion.uppy.io' })
.use(OneDrive, { target: Dashboard, companionUrl: 'https://companion.uppy.io' })
.use(Webcam, { target: Dashboard })
.use(ScreenCapture, { target: Dashboard })
.use(ImageEditor, { target: Dashboard })
.use(Tus, { endpoint: 'https://tusd.tusdemo.net/files/' })
.use(DropTarget, {target: document.body })
.use(GoldenRetriever)

uppy.on('complete', result => {
  console.log('successful files:', result.successful)
  console.log('failed files:', result.failed)
})

This might be daunting for potential users, and the pattern of repeating target andcompanionUrl might seem off to some. (Note that this is partly a documentation problem, but we'll come back to that). This is what brought Robodog to life. An alternative with the same features, but with a more ergonomic and minimal API.

However, it didn't come with its own set of new problems.

  • It tries to do the exact same, but it looks like an entirely different product
  • It's confusing for users whether they want to use Robodog or Uppy directly.
  • Documentation is scarce, and the tradeoffs are unclear
  • It's not marketed, you need to stumble on it in the docs.
  • Extra maintenance burden
  • Extra hosting effort and costs

I never really understood Robodog, and I have been working on Uppy for half a year, so what will it be like for new users?

It is understandable how Robodog came to be, but we are not the first one to fall for the fallacy of uncoordinated good ideas. This reminded me of some lessons from the The Mythical Man-Month, a book with essays about software engineering and what people succeeded or failed to do after many years of experience:

  • Simplicity and straight-forward-ness proceed from conceptual integrity
  • It is better to have one good idea and carry it through the project than having several uncoordinated good ideas
  • It is important that all the aspects of the system follow the same philosophy and the same conceptual integrity throughout the system. Thus, by doing so, the user or the customer only needs to be aware of or know one concept

Case in point: part of the problem is the lack of a shared definition of what Uppy does and wants to be. Is it the most easy to integrate product? Does it have the most features? Does it have the most flexibility? It can't be all at once.

Solution

I think Uppy is the option with a lot of great tightly-coupled features and flexible APIs to let the users build the rest they want. It will not be the integration with the least lines of code. But that doesn't mean we can't do a bit better. So let's deprecate Robodog, take the confusion away, and own the strengths of Uppy and tweak its integration pain point a bit. The latter is what brings us to 'presets'.

Presets

Let's look at the difference at a glance.

const Uppy = require("@uppy/core");
const Dashboard = require("@uppy/dashboard");
const GoogleDrive = require("@uppy/google-drive");
const Dropbox = require("@uppy/dropbox");
const Box = require("@uppy/box");
const Instagram = require("@uppy/instagram");
const Facebook = require("@uppy/facebook");
const OneDrive = require("@uppy/onedrive");
const ImageEditor = require("@uppy/image-editor");

const uppy = new Uppy()
  .use(Dashboard, {
    inline: true,
    target: ".DashboardContainer",
  })
  .use(ImageEditor, { target: Dashboard })
  .use(GoogleDrive, { target: Dashboard, companionUrl: "https://companion.uppy.io" })
  .use(Dropbox, { target: Dashboard, companionUrl: "https://companion.uppy.io" })
  .use(Box, { target: Dashboard, companionUrl: "https://companion.uppy.io" })
  .use(Instagram, { target: Dashboard, companionUrl: "https://companion.uppy.io" })
  .use(Facebook, { target: Dashboard, companionUrl: "https://companion.uppy.io" })
  .use(OneDrive, { target: Dashboard, companionUrl: "https://companion.uppy.io" });

const Uppy = require("@uppy/core");
const Uppy = require("@uppy/preset-providers"); // @uppy/preset-dashboard-providers?

const uppy = new Uppy().use(PresetProviders, {
  target: ".DashboardContainer",
  inline: true,
  companionUrl: "https://companion.uppy.io",
  providers: ["drive", "dropbox", "box", "instagram", "facebook", "onedrive"],
});

Another example of a preset could be @uppy/preset-transloadit (less impressive, but would align with what Robodog currently offers):

const Uppy = require("@uppy/core");
const Dashboard = require("@uppy/dashboard");
const Transloadit = require("@uppy/transloadit");
const ImageEditor = require("@uppy/image-editor");

const uppy = new Uppy()
  .use(Dashboard, {
    inline: true,
    target: ".DashboardContainer",
  })
  .use(ImageEditor, { target: Dashboard })
  .use(Transloadit, { params: { auth: { key: "" }, template_id: "" })
const Uppy = require("@uppy/core");
const Uppy = require("@uppy/preset-transloadit");

const uppy = new Uppy().use(PresetTransloadit, {
  target: ".DashboardContainer",
  inline: true,
  params: { auth: { key: "" }, template_id: "" },
});

Presets

  • ...are new plugins which compose existing plugins
  • ...are familiar as it's just a plugin as you're used to
  • ...are for people who want a lot of functionality at once
  • ...are not for people who want to exactly compose what they want
  • ...share settings like companionUrl, but only pass unique settings to the right plugin
  • ...have the dashboard (and the image editor) build-in. A lot of plugins are tightly coupled to the dashboard anyway. But you could of course also create presets with @uppy/drag-drop
  • ...are technically easy to implement

Note that this probably has to be combined with a small redo of the documentation. We currently have a lot of 'documentation islands' per plugin, but if the examples/dashboard integration code is still the first thing people see, the problem partly remains. There should be a build up, from this is all you get in a couple of lines, to this is all the custom things you could do.

Alternatives

  • Keep Robodog, but drastically improve the documentation
  • Deprecate Robodog with no alternative

cc @kvz @goto-bus-stop @arturi @aduh95 @mifi

Murderlon avatar Nov 16 '21 16:11 Murderlon

Some more simplifications that come to mind:

  • inline could default to true (or other sensible default), so that the users don't have to specify in the basic simple get-started example
  • all the plugins have { target: Dashboard }, could this be the default so we don't need to specify it? Can there even be multiple Dashboards in one one uppy? if not, then maybe no need to specify target at all?
  • maybe companionUrl can be set globally, then it would be simpler
  • maybe further simplify with a declarative api

With these improvements the main api could look a lot more clean, concise and easy to read for someone who is just new to the project:

const Uppy = require("@uppy/core");
const Dashboard = require("@uppy/dashboard");
const GoogleDrive = require("@uppy/google-drive");
const Dropbox = require("@uppy/dropbox");
const Box = require("@uppy/box");
const Instagram = require("@uppy/instagram");
const Facebook = require("@uppy/facebook");
const OneDrive = require("@uppy/onedrive");
const ImageEditor = require("@uppy/image-editor");

const uppy = new Uppy({
  companionUrl: "https://companion.uppy.io",
  plugins: [
    Dashboard('.DashboardContainer'),
    ImageEditor,
    GoogleDrive,
    Dropbox,
    Box
    Instagram,
    Facebook
    OneDrive,
  ],
})

mifi avatar Nov 29 '21 07:11 mifi

inline could default to true (or other sensible default), so that the users don't have to specify in the basic simple get-started example

That would be a breaking change and unrelated to this issue. I would refrain from changing default settings.

all the plugins have { target: Dashboard }, could this be the default so we don't need to specify it? Can there even be multiple Dashboards in one one uppy? if not, then maybe no need to specify target at all?

That's already the case in this proposal. Plugins in a preset are automatically hooked to the dashboard, but the dashboard itself still needs a target, so we can't get rid of that. If you add another dashboard, or another preset which also has the dashboard, then it won't be mounted as the plugin already exists. First config wins.

maybe companionUrl can be set globally, then it would be simpler

I think in general we want to refrain from uppy core having any options that aren't considered 'core'. Like plugin options. In this case companion could be an exception but not sure.

With these improvements the main api could look a lot more clean, concise and easy to read for someone who is just new to the project:

I think this would once again fall for having uncoordinated good ideas. This could confuse users again on what the tradeoffs are for these approaches. Unless you propose to deprecate the current .use approach but that seems excessive. I deliberately went with an API proposal that is literally just a composition of plugins, in the same style, with the same expectations. It's not about achieving the most concise API, as I said, I think we need to own Uppy's strengths and weaknesses. The weakness being it will not be the most concise/easy to integrate option. But we can make it a bit better.

Murderlon avatar Nov 29 '21 10:11 Murderlon

I was actually thinking about possibly redesigning the whole API to something much simpler, which would indeed yield breaking changes, and a new major version without keeping any of the old API. IMO it's a good idea to have just one way of doing things, and not a lot of different supported apis (new/old/deprecated) due to maintainence of the code. But if that is out of the question, and we want to keep our API backwards compatible, then your suggestion sounds like the way to go.

mifi avatar Nov 29 '21 10:11 mifi

I don't know if this has been suggested, here's a syntax that could let us define plugin options in Uppy constructor.

const uppy = new Uppy({
    defaultPluginOptions: { target: Dashboard, companionUrl: "https://companion.uppy.io" },
  })
  .use(Dashboard, {
    inline: true,
    target: ".DashboardContainer",
  })
  .use(ImageEditor)
  .use(GoogleDrive)
  .use(Dropbox)
  .use(Box)
  .use(Instagram)
  .use(Facebook)
  .use(OneDrive);

aduh95 avatar Nov 29 '21 14:11 aduh95

what I talked about in the call, by default it could just a standard implicit list of plugins, so the API could be even simpler:

new Uppy({ companionUrl: "https://companion.uppy.io", dashboardTarget: '.target' }).bind()

mifi avatar Nov 29 '21 14:11 mifi

Below is Merlijn's initial idea with the addition of being able to adjust what the Preset sets up for you. Because how broadly a Preset/default config can be applied quickly diminishes with each little thing that cannot be changed afterwards. The 'majority use case' does not exist when you factor in all the little details that folks want differently.

I think we learned from Robodog that we also should not abstract and wrap all of Uppy and require this abstraction to have intimate knowledge of each Plugin's parameters, hence overly tightly coupling their evolutionary paths. We have had so many requests: "I just want to change this little thing" and we had to say: "sorry Robodog does not support that Dashboard parameter yet so please use these 200 lines to reimplement Robodog and allow for changing that one small thing that was crucial to your use case".

const Uppy = require('@uppy/core')
const PresetTransloadit = require('@uppy/preset-transloadit')

const uppy = new Uppy()
  .use(PresetTransloadit, {
    params: { auth: { key: '' }, template_id: '' },
  })
  .modify('ImageEditor', { quality: 0.9 })
  .remove('OneDrive')

kvz avatar Nov 29 '21 14:11 kvz

Thanks for all the input. I'll go through them and show my final outcome.

I was actually thinking about possibly redesigning the whole API to something much simpler, which would indeed yield breaking changes, and a new major version without keeping any of the old API.

I tried the think of how that would look like, how big the difference would be, and what that would mean for existing integrations and framework implementations. But I didn't quickly come up with an API design that makes it worth it, or simply requires to many changes in all the other references.

I don't know if this has been suggested, here's a syntax that could let us define plugin options in Uppy constructor.

const uppy = new Uppy({
    defaultPluginOptions: { target: Dashboard, companionUrl: "https://companion.uppy.io" },
  })
  .use(Dashboard, {
    inline: true,
    target: ".DashboardContainer",
  })
  .use(ImageEditor)
  .use(GoogleDrive)
  .use(Dropbox)
  .use(Box)
  .use(Instagram)
  .use(Facebook)
  .use(OneDrive);

I think companionUrl belongs in @uppy/core, but I think defaultPluginOptions is too much. This would mean any plugin, also user custom plugins, suddenly forcibly get the default options. But I'll come back to this in my proposal below.

Below is Merlijn's initial idea with the addition of being able to adjust what the Preset sets up for you. Because how broadly a Preset/default config can be applied quickly diminishes with each little thing that cannot be changed afterwards. The 'majority use case' does not exist when you factor in all the little details that folks want differently.

I think we learned from Robodog that we also should not abstract and wrap all of Uppy and require this abstraction to have intimate knowledge of each Plugin's parameters, hence overly tightly coupling their evolutionary paths. We have had so many requests: "I just want to change this little thing" and we had to say: "sorry Robodog does not support that Dashboard parameter yet so please use these 200 lines to reimplement Robodog and allow for changing that one small thing that was crucial to your use case".

const Uppy = require('@uppy/core')
const PresetTransloadit = require('@uppy/preset-transloadit')

const uppy = new Uppy()
  .use(PresetTransloadit, {
    params: { auth: { key: '' }, template_id: '' },
  })
  .modify('ImageEditor', { quality: 0.9 })
  .remove('OneDrive')

This is problematic because you are introducing 'global' methods on core that are in fact only meant and sensible for presets. I'm afraid it would also be repetitive and eventually defeat the purpose of decreasing lines of integration code. But, I did take inspiration from it.


Updated proposal

const Uppy = require("@uppy/core");
const PresetProviders = require("@uppy/preset-providers");

new Uppy({ companionUrl: "https://companion.uppy.io" }).use(PresetProviders, {
  options: { Dashboard: { target: '.uppy-dashboard' }, // keys match the default plugin IDs
  exclude: ['instagram'],
});

Changes:

  • companionUrl is now a @uppy/core setting which plugins leverage. This is beneficial for presets but also removes the repetition for non-preset users.
  • presets only have two opts, options to modify settings in underlying plugins, and exclude to remove plugins. This is more explicit than magically passing the right settings to the right plugin as in the original proposal.

Murderlon avatar Nov 29 '21 17:11 Murderlon

This is problematic because you are introducing 'global' methods on core that are in fact only meant and sensible for presets.

There seems to be a misunderstanding here maybe. They operate on plugins and it isn’t any different than what .use() does? These methods are not Preset specific. I think if you can add plugins with .use() (and we may as well call it add), it is not outrageous if core can also remove or modify them. And as Artur indicated this is already supported via the current but slightly less wieldy core.getPlugins().setOptions(). So functionally just deleting a plug-in from core would be new, tho I guess could maybe also already be done

kvz avatar Nov 29 '21 17:11 kvz

exclude: ['instagram']

I’d go with: nothing specified — include all, otherwise explicitly list what to include:

plugins: ['instagram', 'dropbox', 'facebook', 'webcam', 'audio'] — control the order of icons/buttons too, in one go.

companionUrl is now a @uppy/core setting which plugins leverage. This is beneficial for presets but also removes the repetition for non-preset users.

+1000

arturi avatar Nov 29 '21 17:11 arturi

There seems to be a misunderstanding here maybe. They operate on plugins and it isn’t any different than what .use() does? These methods are not Preset specific. I think if you can add plugins with .use() (and we may as well call it add), it is not outrageous if core can also remove or modify them. And as Artur indicated this is already supported via the current but slightly less wieldy core.getPlugins().setOptions(). So functionally just deleting a plug-in from core would be new, tho I guess could maybe also already be done

I don't understand the need for it.

  • If I want to change or remove a plugin during setup, you simply remove the .use or change the setting inside the options.
  • If I want to remove a plugin programmatically, I do uppy.getPlugin('Webcam').uninstall()
  • if I want to modify a plugin programmatically, I do uppy.getPlugin('Webcam').setOptions()
  • if I want to remove a plugin from a preset, I use exclude (or the inverse with include)
  • If I want to modify a plugin from a preset, I use options.

The code example you provide is modify/remove during setup, which would never require programmatically removing/modifying a plugin, you just change the setup code. If you want to remove/modify plugins from within plugins, I think the current 'less wieldy' approach via getPlugin will suffice. If it's used inside a plugin and not top level at setup, I don't think we have to do a change to provide a slightly more concise alternative, which would still use getPlugin under the hood regardless.

Would you agree or do you see it differently?

I’d go with: nothing specified — include all, otherwise explicitly list what to include:

plugins: ['instagram', 'dropbox', 'facebook', 'webcam', 'audio'] — control the order of icons/buttons too, in one go.

Good point on the ordering! I'm more a fan of exclude because people tend to go for the provider preset if they want at least a couple, so exclude tends to be shorter. You can't use ordering in there because one could also include or exclude for instance image editor. So I'd say we order alphabetically until users want sorting, when we could add something for it.

Murderlon avatar Nov 30 '21 10:11 Murderlon

sorry i still disagree

you just change the setup code

If you use a preset, the preset runs its own setup code. These methods would allow you to tweak the default that we cast with the preset, making it more widely usable and causing less duplication.

  • If I want to remove a plugin programmatically, I do uppy.getPlugin('Webcam').uninstall()

so i guess all i'm proposing is to have these aliases:

  • uppy.add(Webcam) -> uppy.use(OneDrive) (optional)
  • uppy.remove('Webcam') -> uppy.getPlugin('Webcam').uninstall()
  • uppy.change('Webcam') -> uppy.getPlugin('Webcam').setOptions()

as a more ergonomic way of tweaking plugins (that a preset may have set up for you). Just so that:

new Uppy()
  .use(PresetTransloadit, {
    params: { auth: { key: '' }, template_id: '' },
  })
  .getPlugin('Webcam').setOptions({ quality: 0.9 })
  .getPlugin('OneDrive').uninstall()

can become:

new Uppy()
  .add(PresetTransloadit, {
    params: { auth: { key: '' }, template_id: '' },
  })
  .change('ImageEditor', { quality: 0.9 })
  .remove(['OneDrive', 'Box'])

which would be more appealing to me as a new developer onboarding, even though functionally it seems much of this is already possible, and the new thing to build is then mainly the preset that can configure uppy (?)

To me that is a clearer way of interfacing with plugins than:

new Uppy()
  .use(PresetTransloadit, {
    params: { auth: { key: '' }, template_id: '' },
    options: { ImageEditor: { quality: 0.9 },
    exclude: ['OneDrive', 'Box'],
  }
)

kvz avatar Nov 30 '21 11:11 kvz

Note that this is inconsistent as I thought we opted to move away from magically assigning opts to underlying plugins, which is what params is currently doing.

new Uppy()
  .use(PresetTransloadit, {
    params: { auth: { key: '' }, template_id: '' },
  })
  .change('ImageEditor', { quality: 0.9 })
  .remove(['OneDrive', 'Box'])

So it would be this

new Uppy()
  .use(PresetTransloadit)
  .change('Transloadit', { params: { auth: { key: '' }, template_id: '' }})
  .change('ImageEditor', { quality: 0.9 })
  .remove(['OneDrive', 'Box'])

Because we need to think about the presets API in general, therefor passing transloadit opts in to a transloadit preset may seem logical because of the name but to what plugin would the opts go in @uppy/preset-providers? or any other preset? Seem reason why sorting in include doesn't make sense, because it's tied to the idea of icon order in the dashboard, but presets are more generic than that.

I can't help but feel it's a bit weird to always having to do a .change in order to even get your preset working. It's not a change if it's essential setup code.

Murderlon avatar Nov 30 '21 12:11 Murderlon

That's why i think a Preset should have the liberty to take options if it so desires. If I were to develop the TransloaditPreset, I would want to take some options, like encoding params, and set them on the Transloadit Plugin that I am creating on behalf of my consuming dev. All for ergonomics.

I don't think we should have to force people to always change in such a case (where we know we are always going to need configuration)--just for consistency reasons. If consistency were the only law, we should shoot down other things in Uppy too, like the plan to support companionUrl on core vs Provider plugins. Which i by the way think is a separate proposal, which I am personally +-0 on.

kvz avatar Nov 30 '21 12:11 kvz

I'm more a fan of exclude because people tend to go for the provider preset if they want at least a couple, so exclude tends to be shorter.

Yes, but this way it’s implicit — you see from code what’s included for sure + ordering.

You can't use ordering in there because one could also include or exclude for instance image editor.

My thinking was sources / “providers” go on a special list in options. Image editor, maybe compressor and others are included by default:

const uppy = new Uppy()
  .use(PresetTransloadit, {
    params: { auth: { key: 'FOO' }, template_id: 'BAR' },
    sources: 'local', 'webcam', 'facebook', 'google-drive', 'box', 'instagram', 'audio',
  }
)

Then what Kevin is suggesting, for those who want to modify:

uppy.remove('ImageEditor')
uppy.change('Webcam', { quality: high })

Shortcuts are maybe a separate discussion / PR, but I agree it looks nicer without the .getPlugin. We have shortcuts in Core already: uppy.setFileState does uppy.setState that finds a specific file.

One problem here is that if you want to pass metaFields to Dashboard, as well as note: 'Images and video only, 2–3 files, up to 1 MB', you’ll have to do .change. Basically some important things will have to be passed this way, rather than optional rare modifications.

arturi avatar Nov 30 '21 13:11 arturi

One problem here is that if you want to pass metaFields to Dashboard, as well as note: 'Images and video only, 2–3 files, up to 1 MB', you’ll have to do .change, like basically some important things will have to be passed this way, not just modifing something you don’t like rarely.

Not if Presets are allowed to take options, and handle them themselves. So your sources could be handled by PresetTransloadit, and it could also take params and do something with it, as it could with a note. It's all up to the preset this way and Uppy doesn't care(?)

kvz avatar Nov 30 '21 13:11 kvz

Not if Presets are allowed to take options

Yes, but if we don’t limit preset’s options to params (absolute must for Transloadit key) and sources — we end up with Robodog problem — passing options like note and metaFields to Dashboard, then imageQuality to compressor and so on.

arturi avatar Nov 30 '21 14:11 arturi

We would do it for things that are semi-required, and have .change() for other cases so we do not end up in Robodogland 🤔

kvz avatar Nov 30 '21 14:11 kvz

The fact that so far every solution feels like it has some quirks to it, or is met with concerns or disagreements, may be telling that the proposed solution is not there yet. I think this is because the definition of a preset ("adjusted or applied in advance") is partly at odds with still being required to configure some things (can't use it as-is), and the need to not loose anything on flexibility. This comes paired with some other untackled problems, such as the fact that things like dashboard plugins are presented as loosely coupled, as if you can just easily add them to a different UI, but that's not the case it all. Or repetition of companion url.

I've let this rest for a day, and I keep coming back to defining what the product philosophy is, and how to present that. Rather than just forcibly getting lines of integration code down. So I think the answer may lie in taking presets into account with a bigger change.

With that in mind, and feedback from every single one of you, I think there is potentially another way to look at this problem — by having a shared definition of what Uppy is and designing an API around that philosophy. Combined with the new upcoming website, I think Uppy should be presented as an advanced uploader combined with three ways to integrate it into a user interface.

Let's look at it from a glance and break down the changes afterwards

1) Dashboard: everything you need

const Uppy = require("@uppy/core");
const { Dashboard, PresetProvides } = require("@uppy/dashboard");
const ImageEditor = require("@uppy/image-editor");
const Transloadit = require("@uppy/transloadit");

const dashboard = new Dashboard({ note: "Hi" })
  .add(PresetProviders)
  .add(ImageEditor)
  .remove(["Facebook", "OneDrive"]);

const uppy = new Uppy()
  .interface(dashboard, "#dashboard")
  .uploader(Transloadit, { companionUrl: "", params: { auth: { key: "" } } });

2) Drag & Drop: minimally and fancy

Note this would mean a new major version of @uppy/drag-drop, which would make it sort of similar to filepond

const Uppy = require("@uppy/core");
const { DragDrop, ImagePreview } = require("@uppy/drag-drop");
const Tus = require("@uppy/tus");

const dragdrop = new DragDrop().add(ImagePreview)

const uppy = new Uppy()
  .interface(dragdrop, "#drag-drop")
  .uploader(Tus, { endpoint: "https://tusd.tusdemo.net/files/" });

3) 'Bring your own UI'

const Uppy = require("@uppy/core");
const Tus = require("@uppy/tus"

const uppy = new Uppy().uploader(Tus, { endpoint: "https://tusd.tusdemo.net/files/" });
const input = document.querySelector(".file-input");

input.addEventListener("change", doSomethingWithUppy);

function doSomethingWithUppy() {
  const files = input.files;
  // uppy.addFile() + custom things
}

Breakdown

  • Focus marketing/presentation and docs on these three levels of control. This would mean getting rid of the distractions that aren't aligned with this vision: deprecating Robodog, but also file input (it ships with a styled button, which will never align with anyone's custom design. 'Bring your own UI' supersedes this).
  • Embrace being tightly coupled, mostly. Let's call it what it is, provider plugins aren't standalone plugins you can simply add in any UI, they were build specifically for dashboard. No need to target anything other than the dashboard.
  • Uppy is conceptually an uploader and an interface, the API should reflect that. .interface() takes an interface and a target and the composition of an interface is done separately. This ties into the previous argument, the plugins for a specific interface (such as dashboard) are added there, and can be designed to do that easily, without thinking of potential other usages. .uploader() should have everything regarding uploading.
    • companionUrl belongs in .uploader(). At the moment all the providers are designed for the dashboard, therefor conceptually I think of them as UI components to the dashboard, not the place to add companionUrl, repeatedly. .uploader() is everything regarding uploading, thus it should be passed there to be referenced by other plugins.
  • Presets are designed for a specific interface and should be able to be used without any additional config. This means PresetProviders only contains the providers. Most of the discussions in this thread have been about how to pass options to the right underlying plugins, without it being confusing. If we are trying to design a solution to make options not confusing with a big set of plugins, perhaps the answers is not trying to be smart about it, but getting rid of the plugins in there that make it hard to design it in the first place.

Bonus points (optional)

I think uppy core has a lot of options, the class has become very big, and I also think restrictions are something powerful. With the above proposed changes, it wouldn't be too weird of a thought to also design a .restricter() which lets you use pre-designed restrictions but also has an API to build one yourself.

const uppy = new Uppy()
  .interface(dragdrop, "#drag-drop")
  .uploader(Tus, { endpoint: "https://tusd.tusdemo.net/files/" });
+ .restricter()

Murderlon avatar Dec 01 '21 12:12 Murderlon