From 7aff27dff177858b7587fd3c3e0267bfe2cc7fe8 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Mon, 27 Sep 2021 12:26:13 +0100 Subject: [PATCH 01/32] feat(UploadSummary): make objects and sizes singular --- components/core/Upload/Modal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/core/Upload/Modal.js b/components/core/Upload/Modal.js index 23d0bd00..e85e0f62 100644 --- a/components/core/Upload/Modal.js +++ b/components/core/Upload/Modal.js @@ -376,7 +376,7 @@ const SummaryTable = ({ uploadSummary, onAction, retry, cancel, ...props }) => { }, { key: "object", - name: Objects, + name: Object, width: "30%", contentstyle: { padding: "0px" }, }, @@ -388,7 +388,7 @@ const SummaryTable = ({ uploadSummary, onAction, retry, cancel, ...props }) => { }, { key: "size", - name: Sizes, + name: Size, width: "20%", contentstyle: { padding: "0px" }, }, From f0976aed8f22da73a07ed225ea3c599c68cb978d Mon Sep 17 00:00:00 2001 From: Aminejv Date: Mon, 27 Sep 2021 12:29:30 +0100 Subject: [PATCH 02/32] fix(Upload): fix the wrong number for files being uploaded by skipping the submitted file if it's currently uploading --- common/upload-utilities.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common/upload-utilities.js b/common/upload-utilities.js index 7e4b8a06..d0d641f0 100644 --- a/common/upload-utilities.js +++ b/common/upload-utilities.js @@ -133,11 +133,12 @@ export function createUploadProvider({ for (let i = 0; i < files.length; i++) { const fileKey = getFileKey(files[i]); const doesQueueIncludeFile = getUploadQueue().some( - ({ file }) => getFileKey(files[i]) === getFileKey(file) + ({ file }) => getFileKey(file) === fileKey ); const isUploaded = fileKey in UploadStore.uploadedFiles; + const isUploading = UploadAbort.currentUploadingFile === fileKey; // NOTE(amine): skip the file if already uploaded or is in queue - if (doesQueueIncludeFile || isUploaded) continue; + if (doesQueueIncludeFile || isUploaded || isUploading) continue; // NOTE(amine): if the added file has failed before, remove it from failedFilesCache if (fileKey in UploadStore.failedFilesCache) removeFileFromCache({ fileKey }); @@ -194,8 +195,9 @@ export function createUploadProvider({ ({ file }) => getFileKey(linkAsFile) === getFileKey(file) ); const isUploaded = fileKey in UploadStore.uploadedFiles; + const isUploading = UploadAbort.currentUploadingFile === fileKey; // NOTE(amine): skip the file if already uploaded or is in queue - if (doesQueueIncludeFile || isUploaded) return; + if (doesQueueIncludeFile || isUploaded || isUploading) return; // NOTE(amine): if the added file has failed before, remove it from failedFilesCache if (fileKey in UploadStore.failedFilesCache) removeFileFromCache({ fileKey }); From f0849b26041d7a718a1fda325c9dc835305053bf Mon Sep 17 00:00:00 2001 From: Aminejv Date: Mon, 27 Sep 2021 17:47:28 +0100 Subject: [PATCH 03/32] feat(Upload): - combine upload and summary in the same page - update summary's copy --- components/core/Upload/Modal.js | 285 ++++++++++++++------------ components/system/components/Input.js | 2 +- 2 files changed, 156 insertions(+), 131 deletions(-) diff --git a/components/core/Upload/Modal.js b/components/core/Upload/Modal.js index e85e0f62..c9e219a7 100644 --- a/components/core/Upload/Modal.js +++ b/components/core/Upload/Modal.js @@ -15,8 +15,8 @@ import { useEscapeKey, useLockScroll } from "~/common/hooks"; import { useUploadContext, useUploadRemainingTime } from "~/components/core/Upload/Provider"; import { Table } from "~/components/system/components/Table"; import { Match, Switch } from "~/components/utility/Switch"; -import { motion } from "framer-motion"; import { Link } from "~/components/core/Link"; +import { motion } from "framer-motion"; import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File"; import DataMeter from "~/components/core/DataMeter"; @@ -25,13 +25,6 @@ import DataMeter from "~/components/core/DataMeter"; * UploadModal * -----------------------------------------------------------------------------------------------*/ -const STYLES_SUMMARY_BUTTON = (theme) => css` - ${Styles.BUTTON_RESET}; - ${Styles.HORIZONTAL_CONTAINER_CENTERED}; - border-radius: 8px; - padding: 6px 8px; - background-color: ${theme.semantic.bgLight}; -`; const STYLES_MODAL = css` z-index: ${Constants.zindex.uploadModal}; top: ${Constants.sizes.header}px; @@ -41,6 +34,7 @@ const STYLES_MODAL = css` left: 0; padding: 24px 24px 32px; height: calc(100vh - ${Constants.sizes.header}px); + overflow-y: auto; background-color: ${Constants.semantic.bgBlurWhiteOP}; @@ -50,11 +44,6 @@ const STYLES_MODAL = css` } `; -const STYLES_MODAL_ELEMENTS = css` - width: 100%; - height: 100%; -`; - const STYLES_SIDEBAR_HEADER = css` display: flex; justify-content: flex-end; @@ -75,40 +64,17 @@ const STYLES_DISMISS = css` } `; -const STYLES_MODAL_WRAPPER = css` - height: 100%; - width: 100%; - @keyframes global-carousel-fade-in { - from { - transform: translate(8px); - opacity: 0; - } - to { - transform: translateX(0px); - opacity: 1; - } +const STYLES_MODAL_CONTROLS_MARGINS = (theme) => css` + margin-top: 200px; + margin-bottom: 200px; + @media (max-width: ${theme.sizes.mobile}px) { + margin-top: 70px; + margin-bottom: 70px; } - animation: global-carousel-fade-in 400ms ease; `; export default function UploadModal({ onAction, viewer }) { - const [{ isUploading }, { hideUploadModal }] = useUploadContext(); - - const [state, setState] = React.useState({ - url: "", - urlError: false, - // NOTE(amine): initial || summary - view: isUploading ? "summary" : "initial", - }); - - const toggleSummaryView = () => { - setState((prev) => ({ - ...prev, - view: state.view === "initial" ? "summary" : "initial", - })); - }; - - const showUploadSummary = () => setState((prev) => ({ ...prev, view: "summary" })); + const [, { hideUploadModal }] = useUploadContext(); useEscapeKey(hideUploadModal); @@ -116,34 +82,15 @@ export default function UploadModal({ onAction, viewer }) { return (
-
-
- {/** TODO CLOSE */} - -
-
- - } - > - - -
+
+ {/** TODO CLOSE */} + +
+
+ +
); @@ -163,7 +110,21 @@ const STYLES_FILE_HIDDEN = css` left: -1px; `; -function Controls({ showUploadSummary }) { +const STYLES_MODAL_CONTROLS = css` + ${Styles.VERTICAL_CONTAINER_CENTERED}; +`; + +const STYLES_LINK_INPUT = (theme) => css` + width: 392px; + border-radius: 12; + background-color: ${theme.semantic.bgWhite}; + + @media (max-width: ${theme.sizes.mobile}px) { + width: 100%; + } +`; + +function Controls({ css, ...props }) { const [, { upload, uploadLink }] = useUploadContext(); const [state, setState] = React.useState({ @@ -189,7 +150,6 @@ function Controls({ showUploadSummary }) { return; } uploadLink({ url: state.url, slate: state.slate }); - showUploadSummary(); }; const handleChange = (e) => { @@ -197,19 +157,14 @@ function Controls({ showUploadSummary }) { }; return ( -
+
css` - border-radius: 16px; - margin-top: 24px; - padding: 24px; - box-shadow: ${theme.shadow.lightSmall}; - border: 1px solid ${theme.semantic.borderGrayLight}; - background-color: ${theme.semantic.bgWhite}; - ${Styles.HORIZONTAL_CONTAINER}; -`; - const STYLES_PLACEHOLDER = css` width: 64px; height: 80px; @@ -275,16 +220,27 @@ const STYLES_PLACEHOLDER = css` const STYLES_TABLE = (theme) => css` overflow: hidden; overflow-y: auto; - border-radius: 12px; box-shadow: ${theme.shadow.lightSmall}; +`; + +const STYLES_SUMMARY_WRAPPER = (theme) => css` + ${Styles.VERTICAL_CONTAINER} + border-radius: 16px; + overflow: hidden; + margin-bottom: 160px; border: 1px solid ${theme.semantic.borderGrayLight}; `; function Summary({ onAction }) { - const [{ fileLoading, isUploading }, { retry, cancel }] = useUploadContext(); + const [{ fileLoading }, { retry, cancel }] = useUploadContext(); - const uploadSummary = React.useMemo(() => { - const uploadSummary = Object.entries(fileLoading).map(([, file]) => file); + const { uploadSummary, totalFilesSummary } = React.useMemo(() => { + let totalFilesSummary = { failed: 0, duplicate: 0, saved: 0 }; + const uploadSummary = Object.entries(fileLoading).map(([, file]) => { + if (file.status === "saving") return file; + totalFilesSummary[file.status]++; + return file; + }); const statusOrder = { failed: 1, @@ -292,24 +248,30 @@ function Summary({ onAction }) { duplicate: 3, success: 4, }; - return uploadSummary.sort( - (a, b) => statusOrder[a.status] - statusOrder[b.status] || a.createdAt - b.createdAt - ); + return { + totalFilesSummary, + uploadSummary: uploadSummary.sort( + (a, b) => statusOrder[a.status] - statusOrder[b.status] || a.createdAt - b.createdAt + ), + }; }, [fileLoading]); + if (uploadSummary.length === 0) return null; + return ( -
- - - + + -
+ ); } @@ -319,9 +281,25 @@ const TableButton = ({ children, as = "button", ...props }) => ( ); -const SummaryBox = () => { +const STYLES_SUMMARY_BOX = (theme) => css` + ${Styles.HORIZONTAL_CONTAINER}; + padding: 24px; + background-color: ${theme.semantic.bgWhite}; + min-height: 165px; + box-shadow: ${theme.shadow.lightSmall}; + border-bottom: 1px solid ${theme.semantic.borderGrayLight}; +`; + +const SummaryBox = ({ totalFilesSummary }) => { const [ - { totalBytesUploaded, totalBytes, totalFilesUploaded, totalFiles, uploadStartingTime }, + { + totalBytesUploaded, + totalBytes, + totalFilesUploaded, + totalFiles, + uploadStartingTime, + isUploading, + }, { cancelAll }, ] = useUploadContext(); @@ -331,37 +309,84 @@ const SummaryBox = () => { totalBytesUploaded, }); + const title = React.useMemo(() => { + if (isUploading) + return { + copy: `Saving ${totalFiles - totalFilesUploaded} of ${totalFiles} Objects...`, + color: "textBlack", + }; + + if (totalFilesSummary.failed === 0) return { copy: "Upload completed", color: "textBlack" }; + + return { + copy: `Upload completed, failed to upload ${totalFilesSummary.failed} ${Strings.pluralize( + "file", + totalFilesSummary.failed + )}`, + color: "red", + }; + }, [isUploading, totalFiles, totalFilesUploaded, totalFilesSummary]); + + const getTextSummary = (fileStatus, suffix) => + `${totalFilesSummary[fileStatus]} ${Strings.pluralize( + "file", + totalFilesSummary[fileStatus] + )} ${suffix}`; + return ( - +
- - Saving {totalFiles - totalFilesUploaded} of {totalFiles} Objects... - - - - {Strings.bytesToSize(totalBytesUploaded, 0)} of {Strings.bytesToSize(totalBytes, 0)}{" "} - - – {Strings.getRemainingTime(uploadRemainingTime)} (Please keep this tab open during - uploading) - - - - Cancel - + {title.copy} + {isUploading ? ( + <> + + + {Strings.bytesToSize(totalBytesUploaded, 0)} of {Strings.bytesToSize(totalBytes, 0)}{" "} + + – {Strings.getRemainingTime(uploadRemainingTime)} (Please keep this tab open during + uploading) + + + + Cancel + + + ) : ( +
+ + + {getTextSummary("saved", "saved")} +
+
+ + {getTextSummary("failed", "failed")} +
+
+ + + {getTextSummary("duplicate", "already exists")} + +
+
+ )}
- +
); }; diff --git a/components/system/components/Input.js b/components/system/components/Input.js index a4197310..ef227961 100644 --- a/components/system/components/Input.js +++ b/components/system/components/Input.js @@ -72,6 +72,7 @@ const STYLES_INPUT_CONTAINER_FULL = css` const STYLES_INPUT = css` ${"" /* ${INPUT_STYLES} */} display: flex; + width: 100%; align-items: center; justify-content: flex-start; height: 40px; @@ -235,7 +236,6 @@ export class Input extends React.Component {
Date: Tue, 28 Sep 2021 18:30:09 +0100 Subject: [PATCH 04/32] feat(Upload.Popup): add popup indicator when dropping files --- components/core/ApplicationHeader.js | 3 - components/core/Upload/Modal.js | 7 +- components/core/Upload/Popup.js | 126 +++++++++++++++++++++++++++ components/core/Upload/index.js | 5 +- 4 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 components/core/Upload/Popup.js diff --git a/components/core/ApplicationHeader.js b/components/core/ApplicationHeader.js index c5941cd6..e297bcd6 100644 --- a/components/core/ApplicationHeader.js +++ b/components/core/ApplicationHeader.js @@ -50,7 +50,6 @@ const STYLES_APPLICATION_HEADER_BACKGROUND = (theme) => css` z-index: -1; background-color: ${theme.system.white}; box-shadow: 0 0 0 1px ${theme.semantic.bgGrayLight}; - @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { -webkit-backdrop-filter: blur(75px); backdrop-filter: blur(75px); @@ -61,7 +60,6 @@ const STYLES_APPLICATION_HEADER_BACKGROUND = (theme) => css` const STYLES_APPLICATION_HEADER = css` ${Styles.HORIZONTAL_CONTAINER_CENTERED}; padding: 14px 24px; - @media (max-width: ${Constants.sizes.mobile}px) { padding: 16px 16px 12px; width: 100%; @@ -94,7 +92,6 @@ const STYLES_BACKGROUND = css` height: 100vh; background-color: ${Constants.semantic.bgBlurDark}; pointer-events: auto; - @keyframes fade-in { from { opacity: 50%; diff --git a/components/core/Upload/Modal.js b/components/core/Upload/Modal.js index c9e219a7..88fd7cd0 100644 --- a/components/core/Upload/Modal.js +++ b/components/core/Upload/Modal.js @@ -25,7 +25,7 @@ import DataMeter from "~/components/core/DataMeter"; * UploadModal * -----------------------------------------------------------------------------------------------*/ -const STYLES_MODAL = css` +const STYLES_MODAL = (theme) => css` z-index: ${Constants.zindex.uploadModal}; top: ${Constants.sizes.header}px; right: 0; @@ -33,14 +33,15 @@ const STYLES_MODAL = css` position: fixed; left: 0; padding: 24px 24px 32px; - height: calc(100vh - ${Constants.sizes.header}px); + height: calc(100vh - ${theme.sizes.header}px); overflow-y: auto; - background-color: ${Constants.semantic.bgBlurWhiteOP}; + background-color: ${theme.semantic.bgWhite}; @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { -webkit-backdrop-filter: blur(75px); backdrop-filter: blur(75px); + background-color: ${theme.semantic.bgBlurWhiteOP}; } `; diff --git a/components/core/Upload/Popup.js b/components/core/Upload/Popup.js new file mode 100644 index 00000000..1a72c640 --- /dev/null +++ b/components/core/Upload/Popup.js @@ -0,0 +1,126 @@ +import * as React from "react"; +import * as System from "~/components/system"; +import * as Styles from "~/common/styles"; +import * as Strings from "~/common/strings"; +import * as FileUtilities from "~/common/file-utilities"; + +import { css } from "@emotion/react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Show } from "~/components/utility/Show"; + +import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File"; +import { clamp } from "lodash"; +import { useEventListener } from "~/common/hooks"; + +const STYLES_POPUP = (theme) => css` + ${Styles.CONTAINER_CENTERED}; + flex-direction: column; + position: fixed; + width: 100%; + height: 100vh; + top: 0px; + left: 0; + background-color: ${theme.semantic.bgWhite}; + + @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { + -webkit-backdrop-filter: blur(75px); + backdrop-filter: blur(75px); + background-color: ${theme.semantic.bgBlurWhiteOP}; + } +`; + +const STYLES_PLACEHOLDER = css` + width: 64px; + height: 80px; + svg { + height: 100%; + width: 100%; + } +`; + +export default function Popup() { + const DEFAULT_DROPPING_STATE = { + isDroppingFiles: false, + totalFilesDropped: undefined, + }; + + const timerRef = React.useRef(); + + const [{ isDroppingFiles, totalFilesDropped }, setDroppingState] = + React.useState(DEFAULT_DROPPING_STATE); + + const handleDragOver = (e) => { + e.preventDefault(); + // NOTE(amine): Hack to hide the popup if the user drags files outside of the app + clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => { + setDroppingState(DEFAULT_DROPPING_STATE); + }, 100); + }; + + const handleDragEnter = async (e) => { + e.preventDefault(); + const { files } = await FileUtilities.formatDroppedFiles({ + dataTransfer: e.dataTransfer, + }); + setDroppingState({ totalFilesDropped: files.length || undefined, isDroppingFiles: true }); + }; + + useEventListener("dragenter", handleDragEnter, []); + useEventListener("dragover", handleDragOver, []); + + return ( + + {isDroppingFiles ? ( + + +
+ + Dropping {totalFilesDropped}{" "} + {totalFilesDropped ? Strings.pluralize("file", totalFilesDropped) : "files"} to save + to Slate + + 200}> + + (we recommend uploading 200 files at a time) + + +
+
+ ) : null} +
+ ); +} + +const DroppedFilesPlaceholder = ({ totalFilesDropped = 3 }) => { + const marginRight = clamp(totalFilesDropped - 1, 0, 2) * 8; + return ( +
+
+ +
+ = 2}> +
+ +
+
+ = 3}> +
+ +
+
+
+ ); +}; diff --git a/components/core/Upload/index.js b/components/core/Upload/index.js index 7ad3fe16..33fce40e 100644 --- a/components/core/Upload/index.js +++ b/components/core/Upload/index.js @@ -2,17 +2,16 @@ import * as React from "react"; import * as Styles from "~/common/styles"; import * as System from "~/components/system"; import * as Constants from "~/common/constants"; -import * as SVG from "~/common/svg"; import * as Events from "~/common/custom-events"; import { useUploadContext } from "~/components/core/Upload/Provider"; import { Show } from "~/components/utility/Show"; import { ModalPortal } from "../ModalPortal"; import { motion } from "framer-motion"; -import { css } from "@emotion/react"; import { Provider } from "~/components/core/Upload/Provider"; import UploadModal from "~/components/core/Upload/Modal"; +import Popup from "~/components/core/Upload/Popup"; import DataMeter from "~/components/core/DataMeter"; /* ------------------------------------------------------------------------------------------------- @@ -87,4 +86,4 @@ const UploadMetrics = () => { ); }; -export { Provider, Root, Trigger }; +export { Provider, Root, Popup, Trigger }; From def37ee54477baffd22502e602f359819922ba02 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 28 Sep 2021 18:31:24 +0100 Subject: [PATCH 05/32] fix(Typography): fix truncation in firefox --- components/system/components/Typography.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/system/components/Typography.js b/components/system/components/Typography.js index 78414824..e54c635b 100644 --- a/components/system/components/Typography.js +++ b/components/system/components/Typography.js @@ -34,7 +34,7 @@ const truncateElements = (nbrOfLines) => nbrOfLines && css` overflow: hidden; - word-break: break-word; + word-break: break-all; text-overflow: ellipsis; -webkit-line-clamp: ${nbrOfLines}; display: -webkit-box; From 8ca1fc61b6a40af5e709d8ec93cf75b32db6410e Mon Sep 17 00:00:00 2001 From: Aminejv Date: Wed, 29 Sep 2021 17:35:32 +0100 Subject: [PATCH 06/32] fix(Upload/Modal): remove seconds from date --- common/utilities.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/utilities.js b/common/utilities.js index 44a1ec78..169950e5 100644 --- a/common/utilities.js +++ b/common/utilities.js @@ -154,14 +154,14 @@ export function formatDateToString(date) { const yesterday = moment().subtract(1, "day"); if (today.isSame(providedDate, "day")) { - return "Today at " + providedDate.format("h:mm:ssA"); + return "Today at " + providedDate.format("h:mmA"); } if (yesterday.isSame(providedDate, "day")) { - return "Yesterday at " + providedDate.format("h:mm:ssA"); + return "Yesterday at " + providedDate.format("h:mmA"); } - return providedDate.format("MMM D, YYYY") + " at " + providedDate.format("h:mm:ssA"); + return providedDate.format("MMM D, YYYY") + " at " + providedDate.format("h:mmA"); } export const clamp = (value, min, max) => { From d7133ecb069392fe8c4c3b491b490fdb2889f3fa Mon Sep 17 00:00:00 2001 From: Aminejv Date: Wed, 29 Sep 2021 19:08:02 +0100 Subject: [PATCH 07/32] fix(Upload&Filter): fix zindex issue between filter navbar and upload modal --- common/constants.js | 3 +- components/core/ApplicationHeader.js | 160 +++++++++++++++------------ components/core/ApplicationLayout.js | 12 +- components/core/DataView.js | 2 +- components/core/Filter/Content.js | 3 +- components/core/Filter/Sidebar.js | 6 +- 6 files changed, 99 insertions(+), 87 deletions(-) diff --git a/common/constants.js b/common/constants.js index 172d4c35..652bea32 100644 --- a/common/constants.js +++ b/common/constants.js @@ -8,7 +8,8 @@ export const sizes = { navigation: 288, sidebar: 416, // NOTE(amine): header's height + filter navbar's height - header: 52 + 40, + header: 52, + filterNavbar: 40, tablet: 960, desktop: 1024, topOffset: 0, //NOTE(martina): Pushes UI down. 16 when there is a persistent announcement banner, 0 otherwise diff --git a/components/core/ApplicationHeader.js b/components/core/ApplicationHeader.js index e297bcd6..1a930c3b 100644 --- a/components/core/ApplicationHeader.js +++ b/components/core/ApplicationHeader.js @@ -103,6 +103,22 @@ const STYLES_BACKGROUND = css` animation: fade-in 200ms ease-out; `; +const STYLES_HEADER = css` + z-index: ${Constants.zindex.header}; + width: 100vw; + position: fixed; + right: 0; + top: 0; +`; + +const STYLES_FILTER_NAVBAR = (theme) => css` + z-index: ${theme.zindex.body}; + width: 100vw; + position: fixed; + right: 0; + top: ${theme.sizes.header}; +`; + const STYLES_UPLOAD_BUTTON = css` ${Styles.CONTAINER_CENTERED}; background-color: ${Constants.semantic.bgGrayLight}; @@ -154,79 +170,83 @@ export default function ApplicationHeader({ viewer, page, data, onAction }) { const isSearching = searchQuery.length !== 0; return ( -
-
-
-
- - - - } - > - - -
-
- {/**TODO: update Search component */} - -
- - -
- - - - } - isSearching={isSearching} - isSignedOut={isSignedOut} + <> +
+
+
+
+ + + + } + > + -
- - -
- - -
+ +
+
+ {/**TODO: update Search component */} + +
+ + +
+ + + + } + isSearching={isSearching} + isSignedOut={isSignedOut} + onAction={onAction} + onDismissSearch={handleDismissSearch} + /> +
+
+
+
+ + +
+ + {/** NOTE(amine): a fix for a backdrop-filter bug where the filter doesn't take any effects. + * It happens when we have two elements using backdrop-filter with a parent-child relationship */} +
+
+
+
+ + - {/** NOTE(amine): a fix for a backdrop-filter bug where the filter doesn't take any effects. - * It happens when we have two elements using backdrop-filter with a parent-child relationship */} -
- - - - -
+
+ ); } diff --git a/components/core/ApplicationLayout.js b/components/core/ApplicationLayout.js index 6cead2bf..f58f73f3 100644 --- a/components/core/ApplicationLayout.js +++ b/components/core/ApplicationLayout.js @@ -26,16 +26,6 @@ const STYLES_NO_VISIBLE_SCROLL = css` } `; -const STYLES_HEADER = css` - z-index: ${Constants.zindex.header}; - height: ${Constants.sizes.header}px; - width: 100vw; - position: fixed; - right: 0; - top: 0; - transition: top 0.25s; -`; - const STYLES_CONTENT = css` background: ${Constants.system.white}; width: 100%; @@ -220,7 +210,7 @@ export default class ApplicationLayout extends React.Component { {this.props.header && ( <>
-
{this.props.header}
+
{this.props.header}
)} css` width: 100%; - padding: 20px 24px 44px; + min-height: 100vh; + padding: calc(20px + ${theme.sizes.filterNavbar}px) 24px 44px; @media (max-width: ${theme.sizes.mobile}px) { padding: 31px 16px 44px; } diff --git a/components/core/Filter/Sidebar.js b/components/core/Filter/Sidebar.js index 3b5c2985..e7fb09ae 100644 --- a/components/core/Filter/Sidebar.js +++ b/components/core/Filter/Sidebar.js @@ -59,10 +59,10 @@ export function Sidebar() { const STYLES_SIDEBAR_FILTER_WRAPPER = (theme) => css` position: sticky; - top: ${theme.sizes.header}px; - min-height: 100vh; + top: ${theme.sizes.header + theme.sizes.filterNavbar}px; width: 236px; - max-height: calc(100vh - ${theme.sizes.header}px); + height: 100vh; + max-height: calc(100vh - ${theme.sizes.header + theme.sizes.filterNavbar}px); padding: 20px 24px; background-color: ${theme.semantic.bgLight}; `; From 11fe4f42a9d06e3662af8a6af65f4d6d0d446202 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:32:01 +0100 Subject: [PATCH 08/32] feat(intercom): change the widget's position and zindex --- common/constants.js | 2 +- common/styles/global.js | 6 ++++++ pages/_app.js | 27 +++++++++++++++++++++++---- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/common/constants.js b/common/constants.js index 652bea32..a590ec62 100644 --- a/common/constants.js +++ b/common/constants.js @@ -140,8 +140,8 @@ export const zindex = { body: 2, sidebar: 5, alert: 3, - uploadModal: 3, header: 4, + intercom: 4, modal: 6, tooltip: 7, cta: 8, diff --git a/common/styles/global.js b/common/styles/global.js index 5f53a7b1..19fd80dc 100644 --- a/common/styles/global.js +++ b/common/styles/global.js @@ -92,6 +92,12 @@ export const injectGlobalStyles = () => css` } `; +export const injectIntercomStyles = () => css` + .intercom-lightweight-app { + z-index: ${Constants.zindex.intercom} !important; + } +`; + /* prettier-ignore */ export const injectCodeBlockStyles = () => css` .language-javascript { diff --git a/pages/_app.js b/pages/_app.js index 461baebe..ec4dc58f 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -4,10 +4,27 @@ import ThemeProvider from "~/components/system/ThemeProvider"; import * as React from "react"; import { Global } from "@emotion/react"; -import { injectGlobalStyles, injectCodeBlockStyles } from "~/common/styles/global"; -import { IntercomProvider } from 'react-use-intercom'; +import { + injectGlobalStyles, + injectCodeBlockStyles, + injectIntercomStyles, +} from "~/common/styles/global"; +import { IntercomProvider, useIntercom } from "react-use-intercom"; -const INTERCOM_APP_ID = 'jwgbampk'; +const INTERCOM_APP_ID = "jwgbampk"; + +const CustomIntercomConfig = () => { + const { boot } = useIntercom(); + React.useLayoutEffect(() => { + boot({ + alignment: "left", + horizontalPadding: 23, + verticalPadding: 28, + }); + }, [boot]); + + return null; +}; // NOTE(wwwjim): // https://nextjs.org/docs/advanced-features/custom-app @@ -17,8 +34,10 @@ function MyApp({ Component, pageProps }) { + - + + From 84c9bcf492f497172febb74ee7e1c6f11accc1f2 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:33:06 +0100 Subject: [PATCH 09/32] feat(styles): add cursor:pointer to BUTTON_RESET --- common/styles.js | 1 + 1 file changed, 1 insertion(+) diff --git a/common/styles.js b/common/styles.js index f6ed1923..12f5aa01 100644 --- a/common/styles.js +++ b/common/styles.js @@ -251,6 +251,7 @@ export const BUTTON_RESET = css` margin: 0; background-color: unset; border: none; + cursor: pointer; ${HOVERABLE} `; From 4703961ba708dd90dd0709eed099c48fab96a0f7 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:34:07 +0100 Subject: [PATCH 10/32] feat(SVG): add ChevronUp CheckCircle XCircle AlertTriangle --- common/svg.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/common/svg.js b/common/svg.js index 9bbacb07..58252937 100644 --- a/common/svg.js +++ b/common/svg.js @@ -1160,6 +1160,20 @@ export const FilecoinLogo = (props) => ( ); +export const ChevronUp = (props) => { + return ( + + + + ); +}; + export const ChevronDown = (props) => { return ( ( xmlns="http://www.w3.org/2000/svg" {...props} > - + ( /> ); + +export const CheckCircle = (props) => ( + + + + +); + +export const XCircle = (props) => ( + + + +); + +export const AlertTriangle = (props) => ( + + + +); From 9a9221dfee8413452f27aedcc0195dbc8303a75c Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:37:57 +0100 Subject: [PATCH 11/32] feat(upload-utilities): - add function to retry all the failed uploads - add function to clear the queue's cache --- common/upload-utilities.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/upload-utilities.js b/common/upload-utilities.js index d0d641f0..2075f57e 100644 --- a/common/upload-utilities.js +++ b/common/upload-utilities.js @@ -31,6 +31,7 @@ const storeFileInCache = ({ file, slate }) => (UploadStore.failedFilesCache[getFileKey(file)] = { file, slate }); const removeFileFromCache = ({ fileKey }) => delete UploadStore.failedFilesCache[fileKey]; const getFileFromCache = ({ fileKey }) => UploadStore.failedFilesCache[fileKey] || {}; +const getFailedFilesCache = () => UploadStore.failedFilesCache; // NOTE(amine): UploadAbort utilities const registerFileUploading = ({ fileKey }) => (UploadAbort.currentUploadingFile = fileKey); @@ -163,6 +164,13 @@ export function createUploadProvider({ addToUploadQueue({ files: [file], slate }); }; + const retryAll = () => { + const failedFilesCache = getFailedFilesCache(); + Object.entries(failedFilesCache).forEach(([key]) => { + retry({ fileKey: key }); + }); + }; + const cancel = ({ fileKey }) => { if (onCancel) onCancel({ fileKeys: [fileKey] }); @@ -212,11 +220,18 @@ export function createUploadProvider({ } }; + const clearUploadCache = () => { + UploadStore.failedFilesCache = {}; + UploadStore.uploadedFiles = {}; + }; + return { upload: addToUploadQueue, uploadLink: addLinkToUploadQueue, retry, + retryAll, cancel, cancelAll, + clearUploadCache, }; } From 2d6fa04c412a97c50eb5c9a3aafcff8ec6a59526 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:38:17 +0100 Subject: [PATCH 12/32] feat(hooks): add useHover hook --- common/hooks.js | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/common/hooks.js b/common/hooks.js index 785546a6..073ced39 100644 --- a/common/hooks.js +++ b/common/hooks.js @@ -41,10 +41,12 @@ export const useForm = ({ }); const _hasError = (obj) => Object.keys(obj).some((name) => obj[name]); - const _mergeEventHandlers = (events = []) => (e) => - events.forEach((event) => { - if (event) event(e); - }); + const _mergeEventHandlers = + (events = []) => + (e) => + events.forEach((event) => { + if (event) event(e); + }); /** ---------- NOTE(amine): Input Handlers ---------- */ const handleFieldChange = (e) => @@ -164,10 +166,12 @@ export const useField = ({ touched: undefined, }); - const _mergeEventHandlers = (events = []) => (e) => - events.forEach((event) => { - if (event) event(e); - }); + const _mergeEventHandlers = + (events = []) => + (e) => + events.forEach((event) => { + if (event) event(e); + }); const setFieldValue = (value) => setState((prev) => ({ @@ -451,3 +455,12 @@ export const useWorker = ({ onStart, onMessage, onError } = {}, dependencies = [ return workerRef.current; }; + +export const useHover = () => { + const [isHovered, setHoverState] = React.useState(false); + + const handleOnMouseEnter = () => setHoverState(true); + const handleOnMouseLeave = () => setHoverState(false); + + return [isHovered, { handleOnMouseEnter, handleOnMouseLeave }]; +}; From 91dfaa6fb8fbfe96d22620a612371efa5dfb83c8 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:38:35 +0100 Subject: [PATCH 13/32] feat(Upload): remove modal --- components/core/Upload/Modal.js | 540 -------------------------------- 1 file changed, 540 deletions(-) delete mode 100644 components/core/Upload/Modal.js diff --git a/components/core/Upload/Modal.js b/components/core/Upload/Modal.js deleted file mode 100644 index 88fd7cd0..00000000 --- a/components/core/Upload/Modal.js +++ /dev/null @@ -1,540 +0,0 @@ -/* eslint-disable jsx-a11y/no-autofocus */ -import * as React from "react"; -import * as Constants from "~/common/constants"; -import * as Styles from "~/common/styles"; -import * as SVG from "~/common/svg"; -import * as Strings from "~/common/strings"; -import * as System from "~/components/system"; -import * as FileUtilities from "~/common/file-utilities"; -import * as Logging from "~/common/logging"; -import * as Utilities from "~/common/utilities"; - -import { css } from "@emotion/react"; -import { Show } from "~/components/utility/Show"; -import { useEscapeKey, useLockScroll } from "~/common/hooks"; -import { useUploadContext, useUploadRemainingTime } from "~/components/core/Upload/Provider"; -import { Table } from "~/components/system/components/Table"; -import { Match, Switch } from "~/components/utility/Switch"; -import { Link } from "~/components/core/Link"; -import { motion } from "framer-motion"; - -import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File"; -import DataMeter from "~/components/core/DataMeter"; - -/* ------------------------------------------------------------------------------------------------- - * UploadModal - * -----------------------------------------------------------------------------------------------*/ - -const STYLES_MODAL = (theme) => css` - z-index: ${Constants.zindex.uploadModal}; - top: ${Constants.sizes.header}px; - right: 0; - bottom: 0; - position: fixed; - left: 0; - padding: 24px 24px 32px; - height: calc(100vh - ${theme.sizes.header}px); - overflow-y: auto; - - background-color: ${theme.semantic.bgWhite}; - - @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { - -webkit-backdrop-filter: blur(75px); - backdrop-filter: blur(75px); - background-color: ${theme.semantic.bgBlurWhiteOP}; - } -`; - -const STYLES_SIDEBAR_HEADER = css` - display: flex; - justify-content: flex-end; - margin-bottom: 8px; -`; - -const STYLES_DISMISS = css` - ${Styles.ICON_CONTAINER} - - color: ${Constants.semantic.textGray}; - - :focus { - outline: none; - } - - :hover { - color: ${Constants.system.blue}; - } -`; - -const STYLES_MODAL_CONTROLS_MARGINS = (theme) => css` - margin-top: 200px; - margin-bottom: 200px; - @media (max-width: ${theme.sizes.mobile}px) { - margin-top: 70px; - margin-bottom: 70px; - } -`; - -export default function UploadModal({ onAction, viewer }) { - const [, { hideUploadModal }] = useUploadContext(); - - useEscapeKey(hideUploadModal); - - useLockScroll(); - - return ( -
-
- {/** TODO CLOSE */} - -
-
- - -
-
- ); -} - -/* ------------------------------------------------------------------------------------------------- - * Controls - * -----------------------------------------------------------------------------------------------*/ - -const STYLES_FILE_HIDDEN = css` - height: 1px; - width: 1px; - opacity: 0; - visibility: hidden; - position: fixed; - top: -1px; - left: -1px; -`; - -const STYLES_MODAL_CONTROLS = css` - ${Styles.VERTICAL_CONTAINER_CENTERED}; -`; - -const STYLES_LINK_INPUT = (theme) => css` - width: 392px; - border-radius: 12; - background-color: ${theme.semantic.bgWhite}; - - @media (max-width: ${theme.sizes.mobile}px) { - width: 100%; - } -`; - -function Controls({ css, ...props }) { - const [, { upload, uploadLink }] = useUploadContext(); - - const [state, setState] = React.useState({ - url: "", - urlError: false, - }); - - const handleUpload = (e) => { - const { files } = FileUtilities.formatUploadedFiles({ files: e.target.files }); - upload({ files, slate: state.slate }); - }; - - const handleUploadLink = () => { - if (Strings.isEmpty(state.url)) { - setState((prev) => ({ ...prev, urlError: true })); - return; - } - try { - new URL(state.url); - } catch (e) { - Logging.error(e); - setState((prev) => ({ ...prev, urlError: true })); - return; - } - uploadLink({ url: state.url, slate: state.slate }); - }; - - const handleChange = (e) => { - setState((prev) => ({ ...prev, [e.target.name]: e.target.value, urlError: false })); - }; - - return ( -
- -
- - - Save - -
- - - - - Drop or select files to save to Slate -
- (we recommend uploading fewer than 200 files at a time) -
- - Select files - -
-
- ); -} - -/* ------------------------------------------------------------------------------------------------- - * Summary - * -----------------------------------------------------------------------------------------------*/ - -const STYLES_PLACEHOLDER = css` - width: 64px; - height: 80px; - svg { - height: 100%; - width: 100%; - } -`; - -const STYLES_TABLE = (theme) => css` - overflow: hidden; - overflow-y: auto; - box-shadow: ${theme.shadow.lightSmall}; -`; - -const STYLES_SUMMARY_WRAPPER = (theme) => css` - ${Styles.VERTICAL_CONTAINER} - border-radius: 16px; - overflow: hidden; - margin-bottom: 160px; - border: 1px solid ${theme.semantic.borderGrayLight}; -`; - -function Summary({ onAction }) { - const [{ fileLoading }, { retry, cancel }] = useUploadContext(); - - const { uploadSummary, totalFilesSummary } = React.useMemo(() => { - let totalFilesSummary = { failed: 0, duplicate: 0, saved: 0 }; - const uploadSummary = Object.entries(fileLoading).map(([, file]) => { - if (file.status === "saving") return file; - totalFilesSummary[file.status]++; - return file; - }); - - const statusOrder = { - failed: 1, - saving: 2, - duplicate: 3, - success: 4, - }; - return { - totalFilesSummary, - uploadSummary: uploadSummary.sort( - (a, b) => statusOrder[a.status] - statusOrder[b.status] || a.createdAt - b.createdAt - ), - }; - }, [fileLoading]); - - if (uploadSummary.length === 0) return null; - - return ( - - - - - ); -} - -const TableButton = ({ children, as = "button", ...props }) => ( - - {children} - -); - -const STYLES_SUMMARY_BOX = (theme) => css` - ${Styles.HORIZONTAL_CONTAINER}; - padding: 24px; - background-color: ${theme.semantic.bgWhite}; - min-height: 165px; - box-shadow: ${theme.shadow.lightSmall}; - border-bottom: 1px solid ${theme.semantic.borderGrayLight}; -`; - -const SummaryBox = ({ totalFilesSummary }) => { - const [ - { - totalBytesUploaded, - totalBytes, - totalFilesUploaded, - totalFiles, - uploadStartingTime, - isUploading, - }, - { cancelAll }, - ] = useUploadContext(); - - const uploadRemainingTime = useUploadRemainingTime({ - uploadStartingTime, - totalBytes, - totalBytesUploaded, - }); - - const title = React.useMemo(() => { - if (isUploading) - return { - copy: `Saving ${totalFiles - totalFilesUploaded} of ${totalFiles} Objects...`, - color: "textBlack", - }; - - if (totalFilesSummary.failed === 0) return { copy: "Upload completed", color: "textBlack" }; - - return { - copy: `Upload completed, failed to upload ${totalFilesSummary.failed} ${Strings.pluralize( - "file", - totalFilesSummary.failed - )}`, - color: "red", - }; - }, [isUploading, totalFiles, totalFilesUploaded, totalFilesSummary]); - - const getTextSummary = (fileStatus, suffix) => - `${totalFilesSummary[fileStatus]} ${Strings.pluralize( - "file", - totalFilesSummary[fileStatus] - )} ${suffix}`; - - return ( -
-
- -
-
- {title.copy} - {isUploading ? ( - <> - - - {Strings.bytesToSize(totalBytesUploaded, 0)} of {Strings.bytesToSize(totalBytes, 0)}{" "} - - – {Strings.getRemainingTime(uploadRemainingTime)} (Please keep this tab open during - uploading) - - - - Cancel - - - ) : ( -
- - - {getTextSummary("saved", "saved")} -
-
- - {getTextSummary("failed", "failed")} -
-
- - - {getTextSummary("duplicate", "already exists")} - -
-
- )} -
-
- ); -}; - -const SummaryTable = ({ uploadSummary, onAction, retry, cancel, ...props }) => { - const columns = React.useMemo(() => { - return [ - { - key: "status", - name: Status, - width: "19%", - contentstyle: { padding: "0px" }, - }, - { - key: "object", - name: Object, - width: "30%", - contentstyle: { padding: "0px" }, - }, - { - key: "date", - name: Date saved, - width: "23%", - contentstyle: { padding: "0px" }, - }, - { - key: "size", - name: Size, - width: "20%", - contentstyle: { padding: "0px" }, - }, - { - key: "actions", - name: Actions, - width: "8%", - contentstyle: { padding: "0px" }, - }, - ]; - }, []); - - const rows = React.useMemo(() => { - return uploadSummary.map((row) => ({ - status: ( -
- Saved}> - - - - Saving - - - - Failed - - - Already saved - - -
- ), - object: ( -
- {row.cid ? ( - - - {row.name} - - - ) : ( - - {row.name} - - )} -
- ), - date: ( -
- - {Utilities.formatDateToString(row.createdAt)} - -
- ), - size: ( -
- {Strings.bytesToSize(row.total)}} - when={row.status === "saving"} - > -
- - - {Strings.bytesToSize(row.loaded, 0)} of {Strings.bytesToSize(row.total, 0)} - -
-
-
- ), - actions: ( -
- - - Edit - - Share -
- } - > - - cancel({ fileKey: row.id })} - color="blue" - as="button" - > - Cancel - - - - retry({ fileKey: row.id })}>Retry - - -
- ), - })); - }, [uploadSummary]); - - return ( -
- - - ); -}; From 7e412dcc8f7e7a64e46b59c1791c559a7439d867 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:39:20 +0100 Subject: [PATCH 14/32] feat(Upload): renamed the popup component to DropIndicator --- components/core/Upload/DropIndicator.js | 127 ++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 components/core/Upload/DropIndicator.js diff --git a/components/core/Upload/DropIndicator.js b/components/core/Upload/DropIndicator.js new file mode 100644 index 00000000..5d3a99f3 --- /dev/null +++ b/components/core/Upload/DropIndicator.js @@ -0,0 +1,127 @@ +import * as React from "react"; +import * as System from "~/components/system"; +import * as Styles from "~/common/styles"; +import * as Strings from "~/common/strings"; +import * as FileUtilities from "~/common/file-utilities"; + +import { css } from "@emotion/react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Show } from "~/components/utility/Show"; + +import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File"; +import { clamp } from "lodash"; +import { useEventListener } from "~/common/hooks"; + +const STYLES_INDICATOR_WRAPPER = (theme) => css` + ${Styles.CONTAINER_CENTERED}; + flex-direction: column; + position: fixed; + width: 100%; + height: 100vh; + top: 0px; + left: 0; + z-index: ${theme.zindex.cta}; + background-color: ${theme.semantic.bgWhite}; + + @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { + -webkit-backdrop-filter: blur(75px); + backdrop-filter: blur(75px); + background-color: ${theme.semantic.bgBlurWhiteOP}; + } +`; + +const STYLES_PLACEHOLDER = css` + width: 64px; + height: 80px; + svg { + height: 100%; + width: 100%; + } +`; + +export default function DropIndicator() { + const DEFAULT_DROPPING_STATE = { + isDroppingFiles: false, + totalFilesDropped: undefined, + }; + + const timerRef = React.useRef(); + + const [{ isDroppingFiles, totalFilesDropped }, setDroppingState] = + React.useState(DEFAULT_DROPPING_STATE); + + const handleDragOver = (e) => { + e.preventDefault(); + // NOTE(amine): Hack to hide the indicator if the user drags files outside of the app + clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => { + setDroppingState(DEFAULT_DROPPING_STATE); + }, 100); + }; + + const handleDragEnter = async (e) => { + e.preventDefault(); + const { files } = await FileUtilities.formatDroppedFiles({ + dataTransfer: e.dataTransfer, + }); + setDroppingState({ totalFilesDropped: files.length || undefined, isDroppingFiles: true }); + }; + + useEventListener("dragenter", handleDragEnter, []); + useEventListener("dragover", handleDragOver, []); + + return ( + + {isDroppingFiles ? ( + + +
+ + Dropping {totalFilesDropped}{" "} + {totalFilesDropped ? Strings.pluralize("file", totalFilesDropped) : "files"} to save + to Slate + + 200}> + + (we recommend uploading 200 files at a time) + + +
+
+ ) : null} +
+ ); +} + +const DroppedFilesPlaceholder = ({ totalFilesDropped = 3 }) => { + const marginRight = clamp(totalFilesDropped - 1, 0, 2) * 8; + return ( +
+
+ +
+ = 2}> +
+ +
+
+ = 3}> +
+ +
+
+
+ ); +}; From c5acec1863438fe1b75b5d8e4d818993de8f1374 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:40:24 +0100 Subject: [PATCH 15/32] feat(Jumper): add primitives for the jumper component --- components/core/Jumper/index.js | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 components/core/Jumper/index.js diff --git a/components/core/Jumper/index.js b/components/core/Jumper/index.js new file mode 100644 index 00000000..bff29222 --- /dev/null +++ b/components/core/Jumper/index.js @@ -0,0 +1,76 @@ +import * as React from "react"; +import * as System from "~/components/system"; + +import { ModalPortal } from "~/components/core/ModalPortal"; +import { css } from "@emotion/react"; +import { AnimatePresence, motion } from "framer-motion"; +import { useEscapeKey } from "~/common/hooks"; + +/* ------------------------------------------------------------------------------------------------- + * Root + * -----------------------------------------------------------------------------------------------*/ + +const JUMPER_WIDTH = 640; +const JUMPER_HEIGHT = 400; + +const STYLES_JUMPER_ROOT = (theme) => css` + position: fixed; + top: calc(50% - ${JUMPER_HEIGHT / 2}px); + left: calc(50% - ${JUMPER_WIDTH / 2}px); + width: ${JUMPER_WIDTH}px; + height: ${JUMPER_HEIGHT}px; + z-index: ${theme.zindex.modal}; + border-radius: 16px; + border: 1px solid ${theme.semantic.borderGrayLight}; + background-color: ${theme.semantic.bgWhite}; + @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { + -webkit-backdrop-filter: blur(75px); + backdrop-filter: blur(75px); + background-color: ${theme.semantic.bgBlurWhiteOP}; + } +`; + +function Root({ children, isOpen, onClose, ...props }) { + useEscapeKey(onClose); + return ( + + {isOpen ? ( + + + + {children} + + + + ) : null} + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Item + * -----------------------------------------------------------------------------------------------*/ + +function Item({ children, ...props }) { + return
{children}
; +} + +/* ------------------------------------------------------------------------------------------------- + * Divider + * -----------------------------------------------------------------------------------------------*/ +function Divider({ children, ...props }) { + return ( + + {children} + + ); +} + +export { Root, Item, Divider }; From 9c603e5cab08be1be4d5105c1497df80fd46f2e6 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:42:13 +0100 Subject: [PATCH 16/32] feat(BlobObjectPreview): add BlobObjectPreview to preview files while being uploaded --- components/core/BlobObjectPreview.js | 54 ++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 components/core/BlobObjectPreview.js diff --git a/components/core/BlobObjectPreview.js b/components/core/BlobObjectPreview.js new file mode 100644 index 00000000..44f42e2a --- /dev/null +++ b/components/core/BlobObjectPreview.js @@ -0,0 +1,54 @@ +import * as React from "react"; +import * as Validations from "~/common/validations"; + +import { css } from "@emotion/react"; + +import ObjectPlaceholder from "~/components/core/ObjectPreview/placeholders"; + +const STYLES_PLACEHOLDER_CONTAINER = css` + height: 100%; + width: 100%; + min-width: auto; +`; + +const STYLES_PREVIEW = css` + height: 100%; + width: 100%; + background-size: cover; + overflow: hidden; + img { + height: 100%; + width: 100%; + object-fit: cover; + } +`; + +export default function BlobObjectPreview({ file, css, placeholderRatio = 1, ...props }) { + const isImage = Validations.isPreviewableImage(file.type); + const [imgUrl, setImgUrl] = React.useState(); + React.useLayoutEffect(() => { + if (isImage) { + const reader = new FileReader(); + reader.addEventListener("load", () => setImgUrl(reader.result), false); + reader.readAsDataURL(file.blob); + } + }, []); + + if (isImage) { + return ( +
+ {imgUrl && File preview} +
+ ); + } + + return ( +
+ +
+ ); +} From 39336544c57ee415c503cdc5c1c6682bfb6f24ed Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:42:37 +0100 Subject: [PATCH 17/32] feat(Upload): add upload jumper --- components/core/Upload/Jumper.js | 129 +++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 components/core/Upload/Jumper.js diff --git a/components/core/Upload/Jumper.js b/components/core/Upload/Jumper.js new file mode 100644 index 00000000..796d73f9 --- /dev/null +++ b/components/core/Upload/Jumper.js @@ -0,0 +1,129 @@ +import * as React from "react"; +import * as Jumper from "~/components/core/Jumper"; +import * as System from "~/components/system"; +import * as FileUtilities from "~/common/file-utilities"; +import * as Logging from "~/common/logging"; +import * as Strings from "~/common/strings"; +import * as Styles from "~/common/styles"; +import * as Constants from "~/common/constants"; + +import { css } from "@emotion/react"; +import { useUploadContext } from "~/components/core/Upload/Provider"; + +const STYLES_JUMPER_HEADER = css` + padding: 17px 20px 15px; +`; + +const STYLES_LINK_INPUT = (theme) => css` + width: 392px; + border-radius: 12; + background-color: ${theme.semantic.bgWhite}; + + @media (max-width: ${theme.sizes.mobile}px) { + width: 100%; + } +`; + +const STYLES_FILE_HIDDEN = css` + height: 1px; + width: 1px; + opacity: 0; + visibility: hidden; + position: fixed; + top: -1px; + left: -1px; +`; + +const STYLES_LINK_UPLOAD_WRAPPER = css` + padding: 67px 72px; +`; + +const STYLES_FILES_UPLOAD_WRAPPER = css` + ${Styles.VERTICAL_CONTAINER_CENTERED}; + padding-top: 35.5px; + padding-bottom: 35.5px; +`; + +export function UploadJumper() { + const [{ isUploadJumperVisible }, { upload, uploadLink, hideUploadJumper }] = useUploadContext(); + + const [state, setState] = React.useState({ + url: "", + urlError: false, + }); + + const handleUpload = (e) => { + const { files } = FileUtilities.formatUploadedFiles({ files: e.target.files }); + upload({ files, slate: state.slate }); + }; + + const handleUploadLink = () => { + if (Strings.isEmpty(state.url)) { + setState((prev) => ({ ...prev, urlError: true })); + return; + } + try { + new URL(state.url); + } catch (e) { + Logging.error(e); + setState((prev) => ({ ...prev, urlError: true })); + return; + } + uploadLink({ url: state.url, slate: state.slate }); + }; + + const handleChange = (e) => { + setState((prev) => ({ ...prev, [e.target.name]: e.target.value, urlError: false })); + }; + + return ( + + + Upload + + + +
+ + + Save + +
+
+ + + + + Drop or select files to save to Slate +
+ (we recommend uploading fewer than 200 files at a time) +
+ + Select files + +
+
+ ); +} From 8727358958d33d714155b75c96d83d83057190cb Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:46:47 +0100 Subject: [PATCH 18/32] feat(Upload/Provider): - add is Finished state to check when an upload finishs - remove useUploadRemainingTime - add resetUploadState - change useUploadModal to useUploadJumper --- components/core/Upload/Provider.js | 77 +++++++++++------------------- 1 file changed, 27 insertions(+), 50 deletions(-) diff --git a/components/core/Upload/Provider.js b/components/core/Upload/Provider.js index 5818a067..f0d294bf 100644 --- a/components/core/Upload/Provider.js +++ b/components/core/Upload/Provider.js @@ -8,58 +8,61 @@ const UploadContext = React.createContext({}); export const useUploadContext = () => React.useContext(UploadContext); export const Provider = ({ children, page, data, viewer }) => { - const [uploadState, uploadHandlers] = useUpload(); + const [uploadState, uploadHandlers] = useUpload({}); - const [isUploadModalVisible, { showUploadModal, hideUploadModal }] = useUploadModal(); + const [isUploadJumperVisible, { showUploadJumper, hideUploadJumper }] = useUploadJumper(); useUploadOnDrop({ upload: uploadHandlers.upload, page, data, viewer }); useUploadFromClipboard({ upload: uploadHandlers.upload, page, data, viewer }); - useEventListener("upload-modal-open", showUploadModal); + useEventListener("open-upload-jumper", showUploadJumper); const providerValue = React.useMemo( () => [ - { isUploadModalVisible, ...uploadState }, - { showUploadModal, hideUploadModal, ...uploadHandlers }, + { ...uploadState, isUploadJumperVisible }, + { + ...uploadHandlers, + showUploadJumper, + hideUploadJumper, + }, ], - [isUploadModalVisible, uploadHandlers, uploadState] + [uploadHandlers, uploadState, isUploadJumperVisible] ); return {children}; }; -const useUploadModal = () => { - const [isUploadModalVisible, setUploadModalState] = React.useState(false); - const showUploadModal = () => setUploadModalState(true); - const hideUploadModal = () => setUploadModalState(false); - return [isUploadModalVisible, { showUploadModal, hideUploadModal }]; +const useUploadJumper = () => { + const [isUploadJumperVisible, setUploadJumperState] = React.useState(false); + const showUploadJumper = () => setUploadJumperState(true); + const hideUploadJumper = () => setUploadJumperState(false); + return [isUploadJumperVisible, { showUploadJumper, hideUploadJumper }]; }; const useUpload = () => { const DEFAULT_STATE = { fileLoading: {}, isUploading: false, - uploadStartingTime: null, totalBytesUploaded: 0, totalBytes: 0, totalFilesUploaded: 0, totalFiles: 0, - uploadRemainingTime: 0, + isFinished: false, }; const [uploadState, setUploadState] = React.useState(DEFAULT_STATE); const uploadProvider = React.useMemo(() => { const handleStartUploading = () => { - setUploadState((prev) => ({ ...prev, isUploading: true, uploadStartingTime: new Date() })); + setUploadState((prev) => ({ ...prev, isFinished: false, isUploading: true })); }; const handleFinishUploading = () => { setUploadState((prev) => ({ ...DEFAULT_STATE, fileLoading: prev.fileLoading, - uploadStartingTime: null, + isFinished: true, })); }; @@ -77,6 +80,7 @@ const useUpload = () => { createdAt: Date.now(), loaded: 0, total: file.size, + blob: file, }, }, totalFiles: prev.totalFiles + 1, @@ -87,7 +91,7 @@ const useUpload = () => { const handleSuccess = ({ fileKey, cid }) => { setUploadState((prev) => { const newFileLoading = { ...prev.fileLoading }; - newFileLoading[fileKey].status = "success"; + newFileLoading[fileKey].status = "saved"; newFileLoading[fileKey].cid = cid; return { ...prev, @@ -142,9 +146,11 @@ const useUpload = () => { const newFileLoading = { ...prev.fileLoading }; const newTotalFiles = prev.totalFiles - fileKeys.length; let newTotalBytes = prev.totalBytes; + let newTotalBytesUploaded = prev.totalBytesUploaded; fileKeys.forEach((fileKey) => { newTotalBytes -= newFileLoading[fileKey].total; + newTotalBytesUploaded -= newFileLoading[fileKey].loaded; delete newFileLoading[fileKey]; }); @@ -153,6 +159,7 @@ const useUpload = () => { fileLoading: newFileLoading, totalFiles: newTotalFiles, totalBytes: newTotalBytes, + totalBytesUploaded: newTotalBytesUploaded, }; }); }; @@ -169,14 +176,18 @@ const useUpload = () => { }); }, []); + const resetUploadState = () => (uploadProvider.clearUploadCache(), setUploadState(DEFAULT_STATE)); + return [ uploadState, { upload: uploadProvider.upload, uploadLink: uploadProvider.uploadLink, retry: uploadProvider.retry, + retryAll: uploadProvider.retryAll, cancel: uploadProvider.cancel, cancelAll: uploadProvider.cancelAll, + resetUploadState, }, ]; }; @@ -228,37 +239,3 @@ const useUploadFromClipboard = ({ upload, page, data, viewer }) => { useEventListener("paste", handlePaste); }; - -export const useUploadRemainingTime = ({ uploadStartingTime, totalBytes, totalBytesUploaded }) => { - const [remainingTime, setRemainingTime] = React.useState(); - - // NOTE(amine): calculate remaining time for current upload queue - const SECOND = 1000; - // NOTE(amine): hack around stale state in the useEffect callback - const uploadStartingTimeRef = React.useRef(null); - uploadStartingTimeRef.current = uploadStartingTime; - - const bytesRef = React.useRef({ - bytesLoaded: totalBytesUploaded, - bytesTotal: totalBytes, - }); - bytesRef.current = { - bytesLoaded: totalBytesUploaded, - bytesTotal: totalBytes, - }; - - React.useEffect(() => { - const intervalId = setInterval(() => { - const { bytesLoaded, bytesTotal } = bytesRef.current; - const timeElapsed = new Date() - uploadStartingTimeRef.current; - // NOTE(amine): upload speed in seconds - const uploadSpeed = bytesLoaded / (timeElapsed / SECOND); - setRemainingTime(Math.round((bytesTotal - bytesLoaded) / uploadSpeed)); - }, SECOND); - - return () => clearInterval(intervalId); - }, []); - - // NOTE(amine): delay by 1 minute - return remainingTime + 60; -}; From 1108e1aa157a0cbd51b5ae36479c3570db318358 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:47:58 +0100 Subject: [PATCH 19/32] feat(Upload/Popup): add popup component --- components/core/Upload/Popup.js | 464 +++++++++++++++++++++++++------- 1 file changed, 369 insertions(+), 95 deletions(-) diff --git a/components/core/Upload/Popup.js b/components/core/Upload/Popup.js index 1a72c640..f0fbd015 100644 --- a/components/core/Upload/Popup.js +++ b/components/core/Upload/Popup.js @@ -1,126 +1,400 @@ import * as React from "react"; -import * as System from "~/components/system"; import * as Styles from "~/common/styles"; -import * as Strings from "~/common/strings"; -import * as FileUtilities from "~/common/file-utilities"; +import * as System from "~/components/system"; +import * as Constants from "~/common/constants"; +import * as SVG from "~/common/svg"; +import { useUploadContext } from "~/components/core/Upload/Provider"; +import { motion, AnimatePresence } from "framer-motion"; import { css } from "@emotion/react"; -import { AnimatePresence, motion } from "framer-motion"; +import { Match, Switch } from "~/components/utility/Switch"; import { Show } from "~/components/utility/Show"; +import { useHover } from "~/common/hooks"; -import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File"; -import { clamp } from "lodash"; -import { useEventListener } from "~/common/hooks"; +import DataMeter from "~/components/core/DataMeter"; +import BlobObjectPreview from "~/components/core/BlobObjectPreview"; +/* ------------------------------------------------------------------------------------------------- + * Popup + * -----------------------------------------------------------------------------------------------*/ -const STYLES_POPUP = (theme) => css` - ${Styles.CONTAINER_CENTERED}; - flex-direction: column; +const STYLES_POPUP_WRAPPER = (theme) => css` position: fixed; - width: 100%; - height: 100vh; - top: 0px; - left: 0; - background-color: ${theme.semantic.bgWhite}; + bottom: 24px; + right: 24px; + z-index: ${theme.zindex.sidebar}; + @media (max-width: ${theme.sizes.mobile}px) { + right: 50%; + transform: translateX(50%); + } +`; +const STYLES_DISMISS_BUTTON = (theme) => css` + ${Styles.BUTTON_RESET}; + position: absolute; + right: -8px; + top: -8px; + border-radius: 50%; + padding: 4px; + border: 1px solid ${theme.semantic.borderGrayLight4}; + color: ${theme.semantic.textGrayDark}; + + background-color: ${theme.semantic.bgWhite}; @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { -webkit-backdrop-filter: blur(75px); backdrop-filter: blur(75px); background-color: ${theme.semantic.bgBlurWhiteOP}; } -`; -const STYLES_PLACEHOLDER = css` - width: 64px; - height: 80px; svg { - height: 100%; - width: 100%; + display: block; } `; -export default function Popup() { - const DEFAULT_DROPPING_STATE = { - isDroppingFiles: false, - totalFilesDropped: undefined, - }; +const STYLES_POPUP_CONTENT = css` + border-radius: 12px; + overflow: hidden; +`; - const timerRef = React.useRef(); +const useUploadPopup = ({ totalFilesSummary }) => { + const [{ isFinished }, { resetUploadState }] = useUploadContext(); + const [popupState, setPopupState] = React.useState({ + isVisible: false, + isSummaryExpanded: false, + }); - const [{ isDroppingFiles, totalFilesDropped }, setDroppingState] = - React.useState(DEFAULT_DROPPING_STATE); + // NOTE(amine): popup handlers + const showUploadPopup = () => setPopupState((prev) => ({ ...prev, isVisible: true })); + const hideUploadPopup = () => setPopupState((prev) => ({ ...prev, isVisible: false })); + const expandUploadSummary = () => setPopupState({ isVisible: true, isSummaryExpanded: true }); + const collapseUploadSummary = () => setPopupState({ isVisible: true, isSummaryExpanded: false }); - const handleDragOver = (e) => { - e.preventDefault(); - // NOTE(amine): Hack to hide the popup if the user drags files outside of the app - clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => { - setDroppingState(DEFAULT_DROPPING_STATE); - }, 100); - }; + const timeoutRef = React.useRef(); + //NOTE(amine): show the upload summary, then automatically collapse the upload summary after 3 seconds + const isStarted = totalFilesSummary.total > 0; + React.useEffect(() => { + if (!isStarted) return; + expandUploadSummary(); + timeoutRef.current = setTimeout(collapseUploadSummary, 3000); + }, [isStarted]); - const handleDragEnter = async (e) => { - e.preventDefault(); - const { files } = await FileUtilities.formatDroppedFiles({ - dataTransfer: e.dataTransfer, + /** + * NOTE(amine): show the upload summary when a file fails to upload, + * then automatically collapse the upload summary after 3 seconds + */ + const isSummaryExpandedRef = React.useRef(); + isSummaryExpandedRef.current = popupState.isSummaryExpanded; + React.useEffect(() => { + if (isSummaryExpandedRef.current || totalFilesSummary.failed === 0) return; + expandUploadSummary(); + clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(collapseUploadSummary, 3000); + }, [totalFilesSummary.failed]); + + // NOTE(amine): show the upload summary when upload finishes + const totalFilesSummaryRef = React.useRef(); + totalFilesSummaryRef.current = totalFilesSummary; + React.useEffect(() => { + if (!isFinished) return; + //NOTE(amine): if all the upload items have been canceled, hide the upload popup + if (totalFilesSummaryRef.current.total === 0) { + hideUploadPopup(); + resetUploadState(); + return; + } + clearTimeout(timeoutRef.current); + expandUploadSummary(); + }, [isFinished]); + + /** + * NOTE(amine): the upload summary is set to automatically collapse when the upload starts and when a file fails to upload. + * Let's cancel those effects when the user hovers over the summary + */ + const cancelAutoCollapseOnMouseEnter = () => clearTimeout(timeoutRef.current); + return [ + popupState, + { + showUploadPopup, + hideUploadPopup, + expandUploadSummary, + collapseUploadSummary, + cancelAutoCollapseOnMouseEnter, + }, + ]; +}; + +const useUploadSummary = ({ fileLoading }) => + React.useMemo(() => { + let totalFilesSummary = { failed: 0, duplicate: 0, saved: 0, total: 0 }; + const uploadSummary = Object.entries(fileLoading).map(([, file]) => { + totalFilesSummary["total"]++; + if (file.status === "saving") return { ...file, filename: file.name }; + totalFilesSummary[file.status]++; + return { ...file, filename: file.name }; }); - setDroppingState({ totalFilesDropped: files.length || undefined, isDroppingFiles: true }); - }; - useEventListener("dragenter", handleDragEnter, []); - useEventListener("dragover", handleDragOver, []); + const statusOrder = { + failed: 1, + saving: 2, + duplicate: 3, + saved: 4, + }; + + return { + totalFilesSummary, + uploadSummary: uploadSummary.sort( + (a, b) => statusOrder[a.status] - statusOrder[b.status] || a.createdAt - b.createdAt + ), + }; + }, [fileLoading]); + +export function Popup() { + const [{ isFinished, fileLoading }, { resetUploadState }] = useUploadContext(); + const { uploadSummary, totalFilesSummary } = useUploadSummary({ fileLoading }); + + const [isHovered, { handleOnMouseEnter, handleOnMouseLeave }] = useHover(); + + const [ + popupState, + { hideUploadPopup, expandUploadSummary, collapseUploadSummary, cancelAutoCollapseOnMouseEnter }, + ] = useUploadPopup({ totalFilesSummary }); + + if (!popupState.isVisible) return null; return ( - - {isDroppingFiles ? ( - - -
- - Dropping {totalFilesDropped}{" "} - {totalFilesDropped ? Strings.pluralize("file", totalFilesDropped) : "files"} to save - to Slate - - 200}> - - (we recommend uploading 200 files at a time) - - -
-
- ) : null} -
- ); -} - -const DroppedFilesPlaceholder = ({ totalFilesDropped = 3 }) => { - const marginRight = clamp(totalFilesDropped - 1, 0, 2) * 8; - return ( -
-
- +
+
+ + {popupState.isSummaryExpanded ? ( + + + + ) : null} + +
- = 2}> -
- -
-
- = 3}> -
- -
+ +
); -}; +} + +/* ------------------------------------------------------------------------------------------------- + * Popup Header + * -----------------------------------------------------------------------------------------------*/ + +const STYLES_POPUP_HEADER = (theme) => css` + color: ${theme.semantic.textGrayDark}; + width: 264px; + padding: 9px 12px 7px; + background-color: ${theme.semantic.bgWhite}; + @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { + -webkit-backdrop-filter: blur(75px); + backdrop-filter: blur(75px); + background-color: ${theme.semantic.bgBlurWhiteOP}; + } + border-radius: 12px; + transition: border-radius 0.5s ease-in-out; + border: 1px solid ${theme.semantic.bgGrayLight}; + + svg { + display: block; + } +`; + +const STYLES_RESET_BORDER_TOP = css` + border-top: none; + border-radius: 0px 0px 12px 12px; +`; + +function Header({ totalFilesSummary, popupState, expandUploadSummary, collapseUploadSummary }) { + const [{ isFinished, totalBytesUploaded, totalBytes }, { retryAll }] = useUploadContext(); + + const uploadProgress = Math.floor((totalBytesUploaded / totalBytes) * 100); + + if (isFinished && totalFilesSummary.failed > 0) { + return ( + + + {totalFilesSummary.failed} failed + + + ); + } + + if (isFinished) { + return ( +
+ + + {totalFilesSummary.saved + totalFilesSummary.duplicate} saved + +
+ ); + } + + return ( + + + + {uploadProgress}% + + + + + + + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Popup Summary + * -----------------------------------------------------------------------------------------------*/ + +const STYLES_SUMMARY = (theme) => css` + position: relative; + background-color: ${theme.system.white}; + max-height: 312px; + overflow-y: auto; + overflow-x: hidden; + + border: 1px solid ${theme.semantic.bgGrayLight}; + border-bottom: none; + border-radius: 12px 12px 0px 0px; + + // NOTE(amine): fix alignment issue caused by inline display + svg { + display: block; + } +`; + +const STYLES_PREVIEW_WRAPPER = css` + width: 24px; + height: 24px; + border-radius: 6px; + overflow: hidden; +`; + +const STYLES_SUMMARY_ACTION = css` + ${Styles.BUTTON_RESET}; + position: relative; + width: 32; + height: 32; + right: -16; +`; + +function Summary({ uploadSummary }) { + const [, { retry, cancel }] = useUploadContext(); + return ( +
+ {uploadSummary.map((file) => ( + <> +
+
+ +
+ +
+ + {file.name} + + Saved}> + + + + + failed + + + already saved + + +
+ +
+ }> + + + + + + + +
+
+ + + ))} +
+ ); +} From bc6676edbcd993ce3b3c514b47cba2ac76888cd0 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Fri, 8 Oct 2021 17:49:07 +0100 Subject: [PATCH 20/32] feat(Upload): remove upload metrics from ApplicationHeader --- components/core/ApplicationHeader.js | 1 - components/core/Upload/index.js | 63 ++++++---------------------- 2 files changed, 12 insertions(+), 52 deletions(-) diff --git a/components/core/ApplicationHeader.js b/components/core/ApplicationHeader.js index 1a930c3b..183586a7 100644 --- a/components/core/ApplicationHeader.js +++ b/components/core/ApplicationHeader.js @@ -209,7 +209,6 @@ export default function ApplicationHeader({ viewer, page, data, onAction }) { { - const [{ isUploadModalVisible }] = useUploadContext(); +const Root = ({ children }) => { return ( <> {children} - - - - - + + + + + ); }; @@ -35,20 +29,17 @@ const Root = ({ onAction, viewer, children }) => { * Trigger * -----------------------------------------------------------------------------------------------*/ -const Trigger = ({ enableMetrics = false, viewer, css, children, ...props }) => { +const Trigger = ({ viewer, css, children, ...props }) => { const showUploadModal = () => { if (!viewer) { Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} }); return; } - Events.dispatchCustomEvent({ name: "upload-modal-open" }); + Events.dispatchCustomEvent({ name: "open-upload-jumper" }); }; return (
- - - @@ -56,34 +47,4 @@ const Trigger = ({ enableMetrics = false, viewer, css, children, ...props }) => ); }; -const UploadMetrics = () => { - const [{ isUploading, totalBytesUploaded, totalBytes }, { showUploadModal }] = useUploadContext(); - const uploadProgress = Math.floor((totalBytesUploaded / totalBytes) * 100); - - return ( - isUploading && ( - - {uploadProgress}% - - - ) - ); -}; - -export { Provider, Root, Popup, Trigger }; +export { Provider, Root, Trigger }; From 4b881428c1c4d53453e6f05e4a8195946466bb49 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 12 Oct 2021 12:02:19 +0100 Subject: [PATCH 21/32] feat(Upload/Jumper): reset link input when clicking save --- components/core/Upload/Jumper.js | 1 + 1 file changed, 1 insertion(+) diff --git a/components/core/Upload/Jumper.js b/components/core/Upload/Jumper.js index 796d73f9..f5710f12 100644 --- a/components/core/Upload/Jumper.js +++ b/components/core/Upload/Jumper.js @@ -70,6 +70,7 @@ export function UploadJumper() { return; } uploadLink({ url: state.url, slate: state.slate }); + setState({ url: "", urlError: false }); }; const handleChange = (e) => { From 3216e863c9da298b9b1151307b6d6aa8a8cf5b51 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 12 Oct 2021 13:29:40 +0100 Subject: [PATCH 22/32] feat(Upload/DropIndicator): update copy --- components/core/ApplicationHeader.js | 2 +- components/core/Upload/DropIndicator.js | 9 ++++----- components/core/Upload/Jumper.js | 6 +++--- components/core/Upload/index.js | 6 +++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/components/core/ApplicationHeader.js b/components/core/ApplicationHeader.js index 183586a7..72c376af 100644 --- a/components/core/ApplicationHeader.js +++ b/components/core/ApplicationHeader.js @@ -204,7 +204,7 @@ export default function ApplicationHeader({ viewer, page, data, onAction }) { />
- +
- Dropping {totalFilesDropped}{" "} - {totalFilesDropped ? Strings.pluralize("file", totalFilesDropped) : "files"} to save - to Slate + {data?.name + ? `Drag and drop files to save them to #${data.name}` + : "Drag and drop files to save them to slate"} 200}> diff --git a/components/core/Upload/Jumper.js b/components/core/Upload/Jumper.js index f5710f12..cd4bf4d9 100644 --- a/components/core/Upload/Jumper.js +++ b/components/core/Upload/Jumper.js @@ -44,7 +44,7 @@ const STYLES_FILES_UPLOAD_WRAPPER = css` padding-bottom: 35.5px; `; -export function UploadJumper() { +export function UploadJumper({ data }) { const [{ isUploadJumperVisible }, { upload, uploadLink, hideUploadJumper }] = useUploadContext(); const [state, setState] = React.useState({ @@ -54,7 +54,7 @@ export function UploadJumper() { const handleUpload = (e) => { const { files } = FileUtilities.formatUploadedFiles({ files: e.target.files }); - upload({ files, slate: state.slate }); + upload({ files, slate: data }); }; const handleUploadLink = () => { @@ -69,7 +69,7 @@ export function UploadJumper() { setState((prev) => ({ ...prev, urlError: true })); return; } - uploadLink({ url: state.url, slate: state.slate }); + uploadLink({ url: state.url, slate: data }); setState({ url: "", urlError: false }); }; diff --git a/components/core/Upload/index.js b/components/core/Upload/index.js index 724cd279..8112f9cd 100644 --- a/components/core/Upload/index.js +++ b/components/core/Upload/index.js @@ -12,14 +12,14 @@ import DropIndicator from "~/components/core/Upload/DropIndicator"; /* ------------------------------------------------------------------------------------------------- * Root * -----------------------------------------------------------------------------------------------*/ -const Root = ({ children }) => { +const Root = ({ children, data }) => { return ( <> {children} - + - + ); From 3ddafb40ec11668ab0a73b107abeb574d22c718c Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 12 Oct 2021 13:54:52 +0100 Subject: [PATCH 23/32] feat(Upload/Popup): close the popup automatically when the upload is successful --- components/core/Upload/Popup.js | 11 ++++++++++- components/core/Upload/Provider.js | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/components/core/Upload/Popup.js b/components/core/Upload/Popup.js index f0fbd015..2614e01c 100644 --- a/components/core/Upload/Popup.js +++ b/components/core/Upload/Popup.js @@ -94,6 +94,7 @@ const useUploadPopup = ({ totalFilesSummary }) => { const totalFilesSummaryRef = React.useRef(); totalFilesSummaryRef.current = totalFilesSummary; React.useEffect(() => { + clearTimeout(timeoutRef.current); if (!isFinished) return; //NOTE(amine): if all the upload items have been canceled, hide the upload popup if (totalFilesSummaryRef.current.total === 0) { @@ -101,8 +102,16 @@ const useUploadPopup = ({ totalFilesSummary }) => { resetUploadState(); return; } - clearTimeout(timeoutRef.current); + expandUploadSummary(); + + //NOTE(amine): if the upload is successful, automatically close the popup + if (totalFilesSummaryRef.current.failed === 0) { + timeoutRef.current = setTimeout(() => { + hideUploadPopup(); + resetUploadState(); + }, 10000); + } }, [isFinished]); /** diff --git a/components/core/Upload/Provider.js b/components/core/Upload/Provider.js index f0d294bf..322b8912 100644 --- a/components/core/Upload/Provider.js +++ b/components/core/Upload/Provider.js @@ -83,6 +83,7 @@ const useUpload = () => { blob: file, }, }, + isFinished: false, totalFiles: prev.totalFiles + 1, totalBytes: prev.totalBytes + file.size, })); From be5b99f79997c26de05a46e82cb47ef8581c6c99 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Tue, 12 Oct 2021 14:06:34 +0100 Subject: [PATCH 24/32] feat(Upload/Popup): change status copy from saved to file size --- components/core/Upload/Popup.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/core/Upload/Popup.js b/components/core/Upload/Popup.js index 2614e01c..cdb6daae 100644 --- a/components/core/Upload/Popup.js +++ b/components/core/Upload/Popup.js @@ -3,6 +3,7 @@ import * as Styles from "~/common/styles"; import * as System from "~/components/system"; import * as Constants from "~/common/constants"; import * as SVG from "~/common/svg"; +import * as Strings from "~/common/strings"; import { useUploadContext } from "~/components/core/Upload/Provider"; import { motion, AnimatePresence } from "framer-motion"; @@ -361,7 +362,11 @@ function Summary({ uploadSummary }) { {file.name} - Saved}> + {Strings.bytesToSize(file.total, 0)} + } + > Date: Tue, 12 Oct 2021 19:11:55 +0100 Subject: [PATCH 25/32] feat(Upload/Jumper): - add overlay behind the jumper -m update spacing inside the jumper --- common/constants.js | 6 +- components/core/Jumper/index.js | 2 +- components/core/Upload/Jumper.js | 118 ++++++++++++++++++------------- components/core/Upload/Popup.js | 2 +- 4 files changed, 75 insertions(+), 53 deletions(-) diff --git a/common/constants.js b/common/constants.js index a590ec62..20afa98d 100644 --- a/common/constants.js +++ b/common/constants.js @@ -99,6 +99,7 @@ export const semantic = { bgBlurWhite: "rgba(255, 255, 255, 0.7)", bgBlurWhiteOP: "rgba(255, 255, 255, 0.85)", bgBlurWhiteTRN: "rgba(255, 255, 255, 0.3)", + bgBlurLightTRN: "rgba(247, 248, 249, 0.3)", bgBlurLight6: "rgba(247, 248, 249, 0.7)", bgBlurLight6OP: "rgba(247, 248, 249, 0.85)", bgBlurLight6TRN: "rgba(247, 248, 249, 0.3)", @@ -143,8 +144,9 @@ export const zindex = { header: 4, intercom: 4, modal: 6, - tooltip: 7, - cta: 8, + tooltip: 8, + jumper: 7, + cta: 9, }; export const font = { diff --git a/components/core/Jumper/index.js b/components/core/Jumper/index.js index bff29222..708116fa 100644 --- a/components/core/Jumper/index.js +++ b/components/core/Jumper/index.js @@ -19,7 +19,7 @@ const STYLES_JUMPER_ROOT = (theme) => css` left: calc(50% - ${JUMPER_WIDTH / 2}px); width: ${JUMPER_WIDTH}px; height: ${JUMPER_HEIGHT}px; - z-index: ${theme.zindex.modal}; + z-index: ${theme.zindex.jumper}; border-radius: 16px; border: 1px solid ${theme.semantic.borderGrayLight}; background-color: ${theme.semantic.bgWhite}; diff --git a/components/core/Upload/Jumper.js b/components/core/Upload/Jumper.js index cd4bf4d9..37461897 100644 --- a/components/core/Upload/Jumper.js +++ b/components/core/Upload/Jumper.js @@ -24,6 +24,21 @@ const STYLES_LINK_INPUT = (theme) => css` } `; +const STYLES_JUMPER_OVERLAY = (theme) => css` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: ${theme.zindex.jumper}; + + @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { + -webkit-backdrop-filter: blur(75px); + backdrop-filter: blur(75px); + background-color: ${theme.semantic.bgBlurLightTRN}; + } +`; + const STYLES_FILE_HIDDEN = css` height: 1px; width: 1px; @@ -35,13 +50,13 @@ const STYLES_FILE_HIDDEN = css` `; const STYLES_LINK_UPLOAD_WRAPPER = css` - padding: 67px 72px; + padding: 50px 72px; `; const STYLES_FILES_UPLOAD_WRAPPER = css` ${Styles.VERTICAL_CONTAINER_CENTERED}; - padding-top: 35.5px; - padding-bottom: 35.5px; + padding-top: 55px; + padding-bottom: 55px; `; export function UploadJumper({ data }) { @@ -78,53 +93,58 @@ export function UploadJumper({ data }) { }; return ( - - - Upload - - - -
- + {isUploadJumperVisible &&
} + + + Upload + + + +
+ + + Save + +
+
+ + + + + Drop or select files to save to Slate +
+ + (we recommend uploading fewer than 200 files at a time) + +
+ - - Save - -
- - - - - - Drop or select files to save to Slate -
- (we recommend uploading fewer than 200 files at a time) -
- - Select files - -
- + > + Select files + + + + ); } diff --git a/components/core/Upload/Popup.js b/components/core/Upload/Popup.js index cdb6daae..ef5cfe30 100644 --- a/components/core/Upload/Popup.js +++ b/components/core/Upload/Popup.js @@ -22,7 +22,7 @@ const STYLES_POPUP_WRAPPER = (theme) => css` position: fixed; bottom: 24px; right: 24px; - z-index: ${theme.zindex.sidebar}; + z-index: ${theme.zindex.tooltip}; @media (max-width: ${theme.sizes.mobile}px) { right: 50%; transform: translateX(50%); From 17a4905052f949b6ea08ad21215123a8960655ea Mon Sep 17 00:00:00 2001 From: Aminejv Date: Thu, 14 Oct 2021 18:40:53 +0100 Subject: [PATCH 26/32] fix(DataView): action bar zindex --- components/core/DataView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/core/DataView.js b/components/core/DataView.js index 21c6cd3f..d8f7049f 100644 --- a/components/core/DataView.js +++ b/components/core/DataView.js @@ -127,7 +127,7 @@ const STYLES_ACTION_BAR_CONTAINER = css` width: 100vw; display: flex; justify-content: center; - z-index: ${Constants.zindex.header + Constants.zindex.filterNavbar}; + z-index: ${Constants.zindex.header}; @media (max-width: ${Constants.sizes.mobile}px) { display: none; } From d72bf1471692cc476bdd22ce938c12fdb6c78a15 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Thu, 14 Oct 2021 18:42:24 +0100 Subject: [PATCH 27/32] update(Upload/DropIndicator): capitalize slate --- components/core/Upload/DropIndicator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/core/Upload/DropIndicator.js b/components/core/Upload/DropIndicator.js index de07e449..51c69f9a 100644 --- a/components/core/Upload/DropIndicator.js +++ b/components/core/Upload/DropIndicator.js @@ -84,7 +84,7 @@ export default function DropIndicator({ data }) { {data?.name ? `Drag and drop files to save them to #${data.name}` - : "Drag and drop files to save them to slate"} + : "Drag and drop files to save them to Slate"} 200}> From eafb38b004bb8a96ec2b8653c1d854b5f579881e Mon Sep 17 00:00:00 2001 From: Aminejv Date: Thu, 14 Oct 2021 18:44:20 +0100 Subject: [PATCH 28/32] update(Upload/Popup): clamp upload progress percentage --- components/core/Upload/Popup.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/core/Upload/Popup.js b/components/core/Upload/Popup.js index ef5cfe30..20fb4470 100644 --- a/components/core/Upload/Popup.js +++ b/components/core/Upload/Popup.js @@ -14,6 +14,7 @@ import { useHover } from "~/common/hooks"; import DataMeter from "~/components/core/DataMeter"; import BlobObjectPreview from "~/components/core/BlobObjectPreview"; +import { clamp } from "lodash"; /* ------------------------------------------------------------------------------------------------- * Popup * -----------------------------------------------------------------------------------------------*/ @@ -237,7 +238,7 @@ const STYLES_RESET_BORDER_TOP = css` function Header({ totalFilesSummary, popupState, expandUploadSummary, collapseUploadSummary }) { const [{ isFinished, totalBytesUploaded, totalBytes }, { retryAll }] = useUploadContext(); - const uploadProgress = Math.floor((totalBytesUploaded / totalBytes) * 100); + const uploadProgress = clamp(Math.floor((totalBytesUploaded / totalBytes) * 100), 0, 100); if (isFinished && totalFilesSummary.failed > 0) { return ( From f2e0eee6a159ff33f744adfb0c0ac340b201d0f7 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Thu, 14 Oct 2021 18:52:01 +0100 Subject: [PATCH 29/32] fix(Filter): fix stale state issue --- components/core/Filter/Provider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/core/Filter/Provider.js b/components/core/Filter/Provider.js index 5d91e794..be5517d5 100644 --- a/components/core/Filter/Provider.js +++ b/components/core/Filter/Provider.js @@ -85,7 +85,7 @@ const useFilterWorker = ({ filterState, setFilterObjects, library }) => { setFilterObjects(e.data); }, }, - [view, subview, type] + [view, subview, library, type] ); return workerState; From 40a960712347b1d065189fa9b37796373db64a8a Mon Sep 17 00:00:00 2001 From: Aminejv Date: Thu, 14 Oct 2021 18:53:44 +0100 Subject: [PATCH 30/32] feat(Upload/Jumper): - add dismiss button - close the jumper when submitting links/files --- components/core/Upload/Jumper.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/components/core/Upload/Jumper.js b/components/core/Upload/Jumper.js index 37461897..223cff00 100644 --- a/components/core/Upload/Jumper.js +++ b/components/core/Upload/Jumper.js @@ -6,11 +6,14 @@ import * as Logging from "~/common/logging"; import * as Strings from "~/common/strings"; import * as Styles from "~/common/styles"; import * as Constants from "~/common/constants"; +import * as SVG from "~/common/svg"; import { css } from "@emotion/react"; import { useUploadContext } from "~/components/core/Upload/Provider"; +import { AnimatePresence, motion } from "framer-motion"; const STYLES_JUMPER_HEADER = css` + ${Styles.HORIZONTAL_CONTAINER_CENTERED}; padding: 17px 20px 15px; `; @@ -59,6 +62,11 @@ const STYLES_FILES_UPLOAD_WRAPPER = css` padding-bottom: 55px; `; +const STYLES_JUMPER_DISMISS_BUTTON = (theme) => css` + ${Styles.BUTTON_RESET}; + color: ${theme.semantic.textGray}; +`; + export function UploadJumper({ data }) { const [{ isUploadJumperVisible }, { upload, uploadLink, hideUploadJumper }] = useUploadContext(); @@ -70,6 +78,7 @@ export function UploadJumper({ data }) { const handleUpload = (e) => { const { files } = FileUtilities.formatUploadedFiles({ files: e.target.files }); upload({ files, slate: data }); + hideUploadJumper(); }; const handleUploadLink = () => { @@ -84,8 +93,10 @@ export function UploadJumper({ data }) { setState((prev) => ({ ...prev, urlError: true })); return; } + uploadLink({ url: state.url, slate: data }); setState({ url: "", urlError: false }); + hideUploadJumper(); }; const handleChange = (e) => { @@ -94,10 +105,27 @@ export function UploadJumper({ data }) { return ( <> - {isUploadJumperVisible &&
} + + {isUploadJumperVisible && ( + + )} + Upload + From 8314b9ad714a81f95ff9a5dcbf8b78eab3c8b1a9 Mon Sep 17 00:00:00 2001 From: Aminejv Date: Thu, 14 Oct 2021 18:57:04 +0100 Subject: [PATCH 31/32] feat(Upload): upload a link by pasting it into the app --- components/core/Upload/Provider.js | 39 +++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/components/core/Upload/Provider.js b/components/core/Upload/Provider.js index 322b8912..20b0e8b5 100644 --- a/components/core/Upload/Provider.js +++ b/components/core/Upload/Provider.js @@ -1,6 +1,7 @@ import * as React from "react"; import * as UploadUtilities from "~/common/upload-utilities"; import * as FileUtilities from "~/common/file-utilities"; +import * as Logging from "~/common/logging"; import { useEventListener } from "~/common/hooks"; @@ -14,7 +15,13 @@ export const Provider = ({ children, page, data, viewer }) => { useUploadOnDrop({ upload: uploadHandlers.upload, page, data, viewer }); - useUploadFromClipboard({ upload: uploadHandlers.upload, page, data, viewer }); + useUploadFromClipboard({ + upload: uploadHandlers.upload, + uploadLink: uploadHandlers.uploadLink, + page, + data, + viewer, + }); useEventListener("open-upload-jumper", showUploadJumper); @@ -222,21 +229,35 @@ const useUploadOnDrop = ({ upload, page, data, viewer }) => { useEventListener("drop", handleDrop, []); }; -const useUploadFromClipboard = ({ upload, page, data, viewer }) => { +const useUploadFromClipboard = ({ upload, uploadLink, page, data, viewer }) => { const handlePaste = (e) => { - const clipboardItems = e.clipboardData.items || []; - if (!clipboardItems) return; - - const { files } = FileUtilities.formatPastedImages({ - clipboardItems, - }); + //NOTE(amine): skip when pasting into an input/textarea or an element with contentEditable set to true + const eventTargetTag = document?.activeElement.tagName.toLowerCase(); + const isEventTargetEditable = !!document?.activeElement.getAttribute("contentEditable"); + if (eventTargetTag === "input" || eventTargetTag === "textarea" || isEventTargetEditable) { + return; + } let slate = null; if (page?.id === "NAV_SLATE" && data?.ownerId === viewer?.id) { slate = data; } + + const link = e.clipboardData?.getData("text"); + try { + new URL(link); + uploadLink({ url: link, slate }); + } catch (e) { + Logging.error(e); + } + + const clipboardItems = e.clipboardData?.items || []; + if (!clipboardItems) return; + const { files } = FileUtilities.formatPastedImages({ + clipboardItems, + }); upload({ files, slate }); }; - useEventListener("paste", handlePaste); + useEventListener("paste", handlePaste, []); }; From 92d544d175543eeb9ef42dd4197c5da6152e7dcc Mon Sep 17 00:00:00 2001 From: Aminejv Date: Thu, 14 Oct 2021 19:14:09 +0100 Subject: [PATCH 32/32] feat(Upload/Popup): expand the popup when new files are added to the upload queue --- components/core/Upload/Popup.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/core/Upload/Popup.js b/components/core/Upload/Popup.js index 20fb4470..382115df 100644 --- a/components/core/Upload/Popup.js +++ b/components/core/Upload/Popup.js @@ -80,17 +80,17 @@ const useUploadPopup = ({ totalFilesSummary }) => { }, [isStarted]); /** - * NOTE(amine): show the upload summary when a file fails to upload, + * NOTE(amine): show the upload summary when a file fails to upload or is added to the queue, * then automatically collapse the upload summary after 3 seconds */ const isSummaryExpandedRef = React.useRef(); isSummaryExpandedRef.current = popupState.isSummaryExpanded; React.useEffect(() => { - if (isSummaryExpandedRef.current || totalFilesSummary.failed === 0) return; + if (isSummaryExpandedRef.current || totalFilesSummary.total === 0) return; expandUploadSummary(); clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(collapseUploadSummary, 3000); - }, [totalFilesSummary.failed]); + }, [totalFilesSummary.failed, totalFilesSummary.total]); // NOTE(amine): show the upload summary when upload finishes const totalFilesSummaryRef = React.useRef();