Add useRemoteSource
- Shared logic in
@uppy/components/src/hooks - Framework specific wrappers in
@uppy/react,@uppy/vue, and@uppy/svelte - Add
dequal(only 304B) for state equality
Example
import { type PartialTreeFile, PartialTreeFolderNode } from '@uppy/core'
import { useRemoteSource } from '@uppy/react'
function File({
item,
checkbox,
}: {
item: PartialTreeFile
checkbox: (item: PartialTreeFile, checked: boolean) => void
}) {
const dtf = new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
timeStyle: 'short',
})
return (
<li key={item.id} className="flex items-center gap-2 mb-2">
<input
type="checkbox"
onChange={() => checkbox(item, false)}
checked={item.status === 'checked'}
/>
{item.data.thumbnail && (
<img src={item.data.thumbnail} alt="" className="w-5 h-5" />
)}
<div className="truncate">{item.data.name}</div>
<p className="text-gray-500 text-sm ml-auto min-w-28 text-right">
{dtf.format(new Date(item.data.modifiedDate))}
</p>
</li>
)
}
function Folder({
item,
checkbox,
open,
}: {
item: PartialTreeFolderNode
checkbox: (item: PartialTreeFolderNode, checked: boolean) => void
open: (folderId: string | null) => Promise<void>
}) {
return (
<li key={item.id} className="flex items-center gap-2 mb-2">
<input
type="checkbox"
onChange={() => checkbox(item, false)}
checked={item.status === 'checked'}
/>
<button
type="button"
className="text-blue-500"
onClick={() => open(item.id)}
>
<span aria-hidden className="w-5 h-5">
📁
</span>{' '}
{item.data.name}
</button>
</li>
)
}
export function Dropbox({ close }: { close: () => void }) {
const { state, login, logout, checkbox, open, done, cancel } =
useRemoteSource('Dropbox')
if (!state.authenticated) {
return (
<div className="p-4 pt-0 min-w-xl min-h-96">
<button
type="button"
className="block ml-auto text-blue-500"
onClick={() => login()}
>
Login
</button>
</div>
)
}
return (
<div className="w-screen h-screen max-w-3xl max-h-96 relative">
<div className="flex justify-between items-center gap-2 bg-gray-100 pb-2 px-4 py-2">
{state.breadcrumbs.map((breadcrumb, index) => (
<>
{index > 0 && <span className="text-gray-500">></span>}{' '}
{index === state.breadcrumbs.length - 1 ?
<span>
{breadcrumb.type === 'root' ? 'Dropbox' : breadcrumb.data.name}
</span>
: <button
type="button"
className="text-blue-500"
key={breadcrumb.id}
onClick={() => open(breadcrumb.id)}
>
{breadcrumb.type === 'root' ? 'Dropbox' : breadcrumb.data.name}
</button>
}
</>
))}
<div className="flex items-center gap-2 ml-auto">
<button
type="button"
className="text-blue-500"
onClick={() => {
logout()
close()
}}
>
Logout
</button>
</div>
</div>
<ul className="p-4">
{state.partialTree.map((item) => {
if (item.type === 'file') {
return <File key={item.id} item={item} checkbox={checkbox} />
}
if (item.type === 'folder') {
return (
<Folder
key={item.id}
item={item}
checkbox={checkbox}
open={open}
/>
)
}
return null
})}
</ul>
{state.selectedAmount > 0 && (
<div className="flex items-center gap-4 bg-gray-100 mt-auto py-2 px-4 absolute bottom-0 left-0 right-0">
<button
type="button"
className="text-blue-500"
onClick={() => {
done()
close()
}}
>
Done
</button>
<button
type="button"
className="text-blue-500"
onClick={() => {
cancel()
}}
>
Cancel
</button>
<p className="text-gray-500 text-sm">
Selected {state.selectedAmount} items
</p>
</div>
)}
</div>
)
}
Diff output files
diff --git a/packages/@uppy/components/lib/Thumbnail.js b/packages/@uppy/components/lib/Thumbnail.js
index 07ef352..8998e2b 100644
--- a/packages/@uppy/components/lib/Thumbnail.js
+++ b/packages/@uppy/components/lib/Thumbnail.js
@@ -14,9 +14,9 @@ export default function Thumbnail(props) {
.includes(fileTypeSpecific);
const isPDF = fileTypeGeneral === "application" && fileTypeSpecific === "pdf";
const objectUrl = useMemo(() => {
- if (!props.images) return "";
+ if (!props.images || props.file.isRemote) return "";
return URL.createObjectURL(props.file.data);
- }, [props.file.data, props.images]);
+ }, [props.file.data, props.images, props.file.isRemote]);
const showThumbnail = props.images && isImage && objectUrl;
useEffect(() => {
return () => {
diff --git a/packages/@uppy/components/lib/hooks/webcam.js b/packages/@uppy/components/lib/hooks/webcam.js
index bfc1000..acfbb30 100644
--- a/packages/@uppy/components/lib/hooks/webcam.js
+++ b/packages/@uppy/components/lib/hooks/webcam.js
@@ -1,21 +1,5 @@
+import { Subscribers } from "./utils.js";
const videoId = "uppy-webcam-video";
-class Subscribers {
- constructor() {
- this.subscribers = new Set();
- this.add = listener => {
- this.subscribers.add(listener);
- return () => this.subscribers.delete(listener);
- };
- this.emit = () => {
- for (const listener of this.subscribers) {
- listener();
- }
- };
- this.clear = () => {
- this.subscribers.clear();
- };
- }
-}
export function createWebcamController(uppy, onSubmit) {
const plugin = uppy.getPlugin("Webcam");
if (!plugin) {
diff --git a/packages/@uppy/components/lib/index.d.ts b/packages/@uppy/components/lib/index.d.ts
index e8ce317..a6e2d76 100644
--- a/packages/@uppy/components/lib/index.d.ts
+++ b/packages/@uppy/components/lib/index.d.ts
@@ -7,6 +7,7 @@ export { default as ProviderIcon, type ProviderIconProps, } from './ProviderIcon
export { createDropzone, type DropzoneReturn, type DropzoneOptions, } from './hooks/dropzone.js';
export { createFileInput, type FileInputProps, type FileInputFunctions, } from './hooks/file-input.js';
export { createWebcamController, type WebcamStore, type WebcamStatus, type WebcamSnapshot, } from './hooks/webcam.js';
+export { createRemoteSourceController, type RemoteSourceStore, type RemoteSourceSnapshot, type RemoteSourceKeys, } from './hooks/remote-source.js';
export type { UppyContext, UppyState, UploadStatus, NonNullableUppyContext, } from './types.js';
export { createUppyEventAdapter } from './uppyEventAdapter.js';
//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
diff --git a/packages/@uppy/components/lib/index.js b/packages/@uppy/components/lib/index.js
index 237584a..8b5885d 100644
--- a/packages/@uppy/components/lib/index.js
+++ b/packages/@uppy/components/lib/index.js
@@ -3,6 +3,7 @@ export { default as FilesGrid } from "./FilesGrid.js";
export { default as FilesList } from "./FilesList.js";
export { createDropzone } from "./hooks/dropzone.js";
export { createFileInput } from "./hooks/file-input.js";
+export { createRemoteSourceController } from "./hooks/remote-source.js";
export { createWebcamController } from "./hooks/webcam.js";
export { default as ProviderIcon } from "./ProviderIcon.js";
export { default as Thumbnail } from "./Thumbnail.js";
diff --git a/packages/@uppy/core/lib/Uppy.d.ts b/packages/@uppy/core/lib/Uppy.d.ts
index 13c0e79..091a584 100644
--- a/packages/@uppy/core/lib/Uppy.d.ts
+++ b/packages/@uppy/core/lib/Uppy.d.ts
@@ -99,6 +99,7 @@ export type UnknownProviderPlugin<M extends Meta, B extends Body> = UnknownPlugi
rootFolderId: string | null;
files: UppyFile<M, B>[];
provider: CompanionClientProvider;
+ view: any;
};
export type UnknownSearchProviderPluginState = {
isInputMode: boolean;
diff --git a/packages/@uppy/provider-views/lib/ProviderView/ProviderView.d.ts b/packages/@uppy/provider-views/lib/ProviderView/ProviderView.d.ts
index a7b5dca..04ffb9b 100644
--- a/packages/@uppy/provider-views/lib/ProviderView/ProviderView.d.ts
+++ b/packages/@uppy/provider-views/lib/ProviderView/ProviderView.d.ts
@@ -1,5 +1,5 @@
import { h } from 'preact';
-import type { UnknownProviderPlugin, PartialTreeFolderNode, PartialTreeFile, PartialTree, Body, Meta } from '@uppy/core';
+import type { UnknownProviderPlugin, PartialTreeFolder, PartialTreeFolderNode, PartialTreeFile, PartialTree, Body, Meta } from '@uppy/core';
import type { CompanionFile } from '@uppy/utils/lib/CompanionFile';
import type { I18n } from '@uppy/utils/lib/Translator';
export declare function defaultPickerIcon(): h.JSX.Element;
@@ -48,6 +48,8 @@ export default class ProviderView<M extends Meta, B extends Body> {
donePicking(): Promise<void>;
toggleCheckbox(ourItem: PartialTreeFolderNode | PartialTreeFile, isShiftKeyPressed: boolean): void;
getDisplayedPartialTree: () => (PartialTreeFile | PartialTreeFolderNode)[];
+ getBreadcrumbs: () => PartialTreeFolder[];
+ getSelectedAmount: () => number;
validateAggregateRestrictions: (partialTree: PartialTree) => string | null;
render(state: unknown, viewOptions?: RenderOpts<M, B>): h.JSX.Element;
}
diff --git a/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js b/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
index 7e1c6b3..7b152c1 100644
--- a/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
+++ b/packages/@uppy/provider-views/lib/ProviderView/ProviderView.js
@@ -22,6 +22,7 @@ import getClickedRange from "../utils/getClickedRange.js";
import handleError from "../utils/handleError.js";
import getBreadcrumbs from "../utils/PartialTreeUtils/getBreadcrumbs.js";
import getCheckedFilesWithPaths from "../utils/PartialTreeUtils/getCheckedFilesWithPaths.js";
+import getNumberOfSelectedFiles from "../utils/PartialTreeUtils/getNumberOfSelectedFiles.js";
import PartialTreeUtils from "../utils/PartialTreeUtils/index.js";
import shouldHandleScroll from "../utils/shouldHandleScroll.js";
export function defaultPickerIcon() {
@@ -85,6 +86,19 @@ export default class ProviderView {
});
return filtered;
};
+ this.getBreadcrumbs = () => {
+ const {
+ partialTree,
+ currentFolderId,
+ } = this.plugin.getPluginState();
+ return getBreadcrumbs(partialTree, currentFolderId);
+ };
+ this.getSelectedAmount = () => {
+ const {
+ partialTree,
+ } = this.plugin.getPluginState();
+ return getNumberOfSelectedFiles(partialTree);
+ };
this.validateAggregateRestrictions = partialTree => {
const checkedFiles = partialTree.filter(item => item.type === "file" && item.status === "checked");
const uppyFiles = checkedFiles.map(file => file.data);
@@ -338,11 +352,10 @@ export default class ProviderView {
}
const {
partialTree,
- currentFolderId,
username,
searchString,
} = this.plugin.getPluginState();
- const breadcrumbs = getBreadcrumbs(partialTree, currentFolderId);
+ const breadcrumbs = this.getBreadcrumbs();
return h(
"div",
{
diff --git a/packages/@uppy/react/lib/headless/UppyContextProvider.js b/packages/@uppy/react/lib/headless/UppyContextProvider.js
index 608b5ab..69b2bac 100644
--- a/packages/@uppy/react/lib/headless/UppyContextProvider.js
+++ b/packages/@uppy/react/lib/headless/UppyContextProvider.js
@@ -39,7 +39,7 @@ export default UppyContextProvider;
export function useUppyContext() {
const ctx = useContext(UppyContext);
if (!ctx.uppy) {
- throw new Error("useDropzone must be called within a UppyContextProvider");
+ throw new Error("Uppy hooks must be called within a UppyContextProvider");
}
return ctx;
}
diff --git a/packages/@uppy/react/lib/index.d.ts b/packages/@uppy/react/lib/index.d.ts
index c3d07f6..c8d36b9 100644
--- a/packages/@uppy/react/lib/index.d.ts
+++ b/packages/@uppy/react/lib/index.d.ts
@@ -9,6 +9,7 @@ export { default as useUppyEvent } from './useUppyEvent.js';
export { useDropzone } from './useDropzone.js';
export { useWebcam } from './useWebcam.js';
export { useFileInput } from './useFileInput.js';
+export { useRemoteSource } from './useRemoteSource.js';
export { UppyContext, UppyContextProvider, } from './headless/UppyContextProvider.js';
export * from './headless/generated/index.js';
//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
diff --git a/packages/@uppy/react/lib/index.js b/packages/@uppy/react/lib/index.js
index 54d1942..18e1325 100644
--- a/packages/@uppy/react/lib/index.js
+++ b/packages/@uppy/react/lib/index.js
@@ -8,6 +8,7 @@ export { default as ProgressBar } from "./ProgressBar.js";
export { default as StatusBar } from "./StatusBar.js";
export { useDropzone } from "./useDropzone.js";
export { useFileInput } from "./useFileInput.js";
+export { useRemoteSource } from "./useRemoteSource.js";
export { default as useUppyEvent } from "./useUppyEvent.js";
export { default as useUppyState } from "./useUppyState.js";
export { useWebcam } from "./useWebcam.js";
diff --git a/packages/@uppy/remote-sources/lib/index.d.ts b/packages/@uppy/remote-sources/lib/index.d.ts
index b1b19f3..7cc97ac 100644
--- a/packages/@uppy/remote-sources/lib/index.d.ts
+++ b/packages/@uppy/remote-sources/lib/index.d.ts
@@ -1,7 +1,28 @@
import { BasePlugin } from '@uppy/core';
import type { Uppy, DefinePluginOpts, Body, Meta } from '@uppy/core';
+import Dropbox from '@uppy/dropbox';
+import GoogleDrive from '@uppy/google-drive';
+import Instagram from '@uppy/instagram';
+import Facebook from '@uppy/facebook';
+import OneDrive from '@uppy/onedrive';
+import Box from '@uppy/box';
+import Unsplash from '@uppy/unsplash';
+import Url from '@uppy/url';
+import Zoom from '@uppy/zoom';
import type { CompanionPluginOptions } from '@uppy/companion-client';
-type AvailablePluginsKeys = 'Box' | 'Dropbox' | 'Facebook' | 'GoogleDrive' | 'Instagram' | 'OneDrive' | 'Unsplash' | 'Url' | 'Zoom';
+export declare const availablePlugins: {
+ __proto__: null;
+ Box: typeof Box;
+ Dropbox: typeof Dropbox;
+ Facebook: typeof Facebook;
+ GoogleDrive: typeof GoogleDrive;
+ Instagram: typeof Instagram;
+ OneDrive: typeof OneDrive;
+ Unsplash: typeof Unsplash;
+ Url: typeof Url;
+ Zoom: typeof Zoom;
+};
+export type AvailablePluginsKeys = 'Box' | 'Dropbox' | 'Facebook' | 'GoogleDrive' | 'Instagram' | 'OneDrive' | 'Unsplash' | 'Url' | 'Zoom';
type NestedCompanionKeysParams = {
[key in AvailablePluginsKeys]?: CompanionPluginOptions['companionKeysParams'];
};
diff --git a/packages/@uppy/remote-sources/lib/index.js b/packages/@uppy/remote-sources/lib/index.js
index 70acc91..90d4b68 100644
--- a/packages/@uppy/remote-sources/lib/index.js
+++ b/packages/@uppy/remote-sources/lib/index.js
@@ -19,7 +19,7 @@ import Zoom from "@uppy/zoom";
const packageJson = {
"version": "2.3.3",
};
-const availablePlugins = {
+export const availablePlugins = {
__proto__: null,
Box,
Dropbox,
diff --git a/packages/@uppy/vue/lib/index.d.ts b/packages/@uppy/vue/lib/index.d.ts
index 6118964..bf31efd 100644
--- a/packages/@uppy/vue/lib/index.d.ts
+++ b/packages/@uppy/vue/lib/index.d.ts
@@ -9,4 +9,5 @@ export * from './headless/generated/index.js';
export * from './useDropzone.js';
export * from './useFileInput.js';
export * from './useWebcam.js';
+export * from './useRemoteSource.js';
//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
diff --git a/packages/@uppy/vue/lib/index.js b/packages/@uppy/vue/lib/index.js
index a77b225..121c16f 100644
--- a/packages/@uppy/vue/lib/index.js
+++ b/packages/@uppy/vue/lib/index.js
@@ -8,4 +8,5 @@ export { default as ProgressBar } from "./progress-bar.js";
export { default as StatusBar } from "./status-bar.js";
export * from "./useDropzone.js";
export * from "./useFileInput.js";
+export * from "./useRemoteSource.js";
export * from "./useWebcam.js";
This is a great start! I noticed some bugs/improvements when testing:
Note that some are intended. The goal is to show a bare minimal example, creating a perfect example would massively inflate the lines of code, making it overwhelming. However, some quickfixes which require very little code change should be looked into yes
@mifi from your todo list I implemented the ones that were objectively good and left out the opinionated ones. For instance, cursor pointer is strictly for links and I don't want that here.
i feel like for most of the web, things that are clickable have cursor: pointer even though they are not links (see even here on github)
i feel like for most of the web, things that are clickable have
cursor: pointereven though they are not links (see even here on github)
Apple’s Human Interface Guidelines states that the hand cursor should be used when “the content is a URL link”.
W3C User Interface guidelines says the same thing again with “The cursor is a pointer that indicates a link”.
From "Buttons shouldn’t have a hand cursor" by Adam Silver
It's just a demo, not a shipped component. So I think it's better to stick to standards and let people decide for themselves if they want to change it. It's definitely not worth blocking this PR over.