mirror of
https://github.com/filecoin-project/slate.git
synced 2024-12-28 19:42:01 +03:00
Merge pull request #944 from filecoin-project/@aminejv/new-saving-flow
Update: Saving flow
This commit is contained in:
commit
654dbb3b32
@ -44,6 +44,7 @@ const returnJSON = async (route, options) => {
|
||||
|
||||
return json;
|
||||
} catch (e) {
|
||||
if (e.name === "AbortError") return { aborted: true };
|
||||
Logging.error(e);
|
||||
}
|
||||
};
|
||||
@ -223,11 +224,12 @@ export const createFile = async (data) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const createLink = async (data) => {
|
||||
export const createLink = async (data, options) => {
|
||||
await Websockets.checkWebsocket();
|
||||
return await returnJSON(`/api/data/create-link`, {
|
||||
...DEFAULT_OPTIONS,
|
||||
body: JSON.stringify({ data }),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -91,6 +91,7 @@ export const semantic = {
|
||||
textGrayDark: system.grayDark3,
|
||||
textBlack: system.black,
|
||||
|
||||
bgWhite: system.white,
|
||||
bgLight: system.grayLight6,
|
||||
bgGrayLight: system.grayLight5,
|
||||
bgBlurWhite: "rgba(255, 255, 255, 0.7)",
|
||||
@ -137,6 +138,7 @@ export const zindex = {
|
||||
body: 2,
|
||||
sidebar: 5,
|
||||
alert: 3,
|
||||
uploadModal: 3,
|
||||
header: 4,
|
||||
modal: 6,
|
||||
tooltip: 7,
|
||||
|
@ -1,12 +1,10 @@
|
||||
import * as Actions from "~/common/actions";
|
||||
import * as Store from "~/common/store";
|
||||
import * as Credentials from "~/common/credentials";
|
||||
import * as Strings from "~/common/strings";
|
||||
import * as Validations from "~/common/validations";
|
||||
import * as Events from "~/common/custom-events";
|
||||
import * as Logging from "~/common/logging";
|
||||
import * as Environment from "~/common/environment";
|
||||
import * as Window from "~/common/window";
|
||||
|
||||
import { encode, isBlurhashValid } from "blurhash";
|
||||
import { v4 as uuid } from "uuid";
|
||||
@ -49,87 +47,15 @@ const getCookie = (name) => {
|
||||
if (match) return match[2];
|
||||
};
|
||||
|
||||
export const uploadLink = async ({ url, slate }) => {
|
||||
Events.dispatchMessage({ message: "Uploading link...", status: "INFO" });
|
||||
let createResponse = await Actions.createLink({ url, slate });
|
||||
if (Events.hasError(createResponse)) {
|
||||
return;
|
||||
}
|
||||
export const uploadLink = async ({ url, slate, uploadAbort }) => {
|
||||
const abortController = new AbortController();
|
||||
if (uploadAbort) uploadAbort.abort = abortController.abort.bind(abortController);
|
||||
|
||||
const { added, skipped } = createResponse.data;
|
||||
if (added) {
|
||||
Events.dispatchMessage({ message: "Link added", status: "INFO" });
|
||||
} else if (skipped) {
|
||||
Events.dispatchMessage({
|
||||
message: "You've already saved this link",
|
||||
});
|
||||
return;
|
||||
}
|
||||
let createResponse = await Actions.createLink({ url, slate }, { signal: abortController.signal });
|
||||
return createResponse;
|
||||
};
|
||||
|
||||
export const uploadFiles = async ({ context, files, slate, keys, numFailed = 0 }) => {
|
||||
if (!files || !files.length) {
|
||||
context._handleRegisterLoadingFinished({ keys });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedFiles = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const currentFileKey = fileKey(files[i]);
|
||||
if (Store.checkCancelled(currentFileKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE(jim): With so many failures, probably good to wait a few seconds.
|
||||
await Window.delay(3000);
|
||||
|
||||
// NOTE(jim): Sends XHR request.
|
||||
let response;
|
||||
try {
|
||||
response = await upload({
|
||||
file: files[i],
|
||||
context,
|
||||
});
|
||||
} catch (e) {
|
||||
Logging.error(e);
|
||||
}
|
||||
|
||||
if (!response || response.error) {
|
||||
continue;
|
||||
}
|
||||
resolvedFiles.push(response);
|
||||
}
|
||||
|
||||
if (!resolvedFiles.length) {
|
||||
context._handleRegisterLoadingFinished({ keys });
|
||||
return;
|
||||
}
|
||||
//NOTE(martina): this commented out portion is only for if parallel uploading
|
||||
// let responses = await Promise.allSettled(resolvedFiles);
|
||||
// let succeeded = responses
|
||||
// .filter((res) => {
|
||||
// return res.status === "fulfilled" && res.value && !res.value.error;
|
||||
// })
|
||||
// .map((res) => res.value);
|
||||
|
||||
let createResponse = await Actions.createFile({ files: resolvedFiles, slate });
|
||||
|
||||
if (Events.hasError(createResponse)) {
|
||||
context._handleRegisterLoadingFinished({ keys });
|
||||
return;
|
||||
}
|
||||
|
||||
const { added, skipped } = createResponse.data;
|
||||
|
||||
let message = Strings.formatAsUploadMessage(added, skipped + numFailed, slate);
|
||||
Events.dispatchMessage({ message, status: !added ? null : "INFO" });
|
||||
|
||||
context._handleRegisterLoadingFinished({ keys });
|
||||
};
|
||||
|
||||
//NOTE(migration): check that upload works still and file.name
|
||||
export const upload = async ({ file, context, bucketName }) => {
|
||||
const currentFileKey = fileKey(file);
|
||||
export const upload = async ({ file, onProgress, bucketName, uploadAbort }) => {
|
||||
let formData = new FormData();
|
||||
const HEIC2ANY = require("heic2any");
|
||||
|
||||
@ -155,17 +81,11 @@ export const upload = async ({ file, context, bucketName }) => {
|
||||
formData.append("data", file);
|
||||
}
|
||||
|
||||
if (Store.checkCancelled(currentFileKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _privateUploadMethod = (path, file) =>
|
||||
new Promise((resolve) => {
|
||||
const XHR = new XMLHttpRequest();
|
||||
|
||||
window.addEventListener(`cancel-${currentFileKey}`, () => {
|
||||
XHR.abort();
|
||||
});
|
||||
if (uploadAbort) uploadAbort.abort = XHR.abort.bind(XHR);
|
||||
|
||||
XHR.open("post", path, true);
|
||||
XHR.setRequestHeader("authorization", getCookie(Credentials.session.key));
|
||||
@ -178,28 +98,16 @@ export const upload = async ({ file, context, bucketName }) => {
|
||||
XHR.upload.addEventListener(
|
||||
"progress",
|
||||
(event) => {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.lengthComputable) {
|
||||
Logging.log("FILE UPLOAD PROGRESS", event);
|
||||
context.setState({
|
||||
fileLoading: {
|
||||
...context.state.fileLoading,
|
||||
[currentFileKey]: {
|
||||
name: file.name,
|
||||
loaded: event.loaded,
|
||||
total: event.total,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (onProgress) onProgress(event);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
window.removeEventListener(`cancel-${currentFileKey}`, () => XHR.abort());
|
||||
XHR.addEventListener("abort", () => {
|
||||
resolve({ aborted: true });
|
||||
});
|
||||
|
||||
XHR.onloadend = (event) => {
|
||||
Logging.log("FILE UPLOAD END", event);
|
||||
@ -236,25 +144,12 @@ export const upload = async ({ file, context, bucketName }) => {
|
||||
res = await _privateUploadMethod(`${generalRoute}${file.name}`, file);
|
||||
}
|
||||
|
||||
if (!res?.data || res.error) {
|
||||
if (context) {
|
||||
await context.setState({
|
||||
fileLoading: {
|
||||
...context.state.fileLoading,
|
||||
[`${file.lastModified}-${file.name}`]: {
|
||||
name: file.name,
|
||||
failed: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
Events.dispatchMessage({ message: "Some of your files could not be uploaded" });
|
||||
|
||||
return !res ? { decorator: "NO_RESPONSE_FROM_SERVER", error: true } : res;
|
||||
if (!res || res.error || res.aborted) {
|
||||
return res;
|
||||
}
|
||||
|
||||
let item = res.data.data;
|
||||
if (item.type.startsWith("image/")) {
|
||||
if (Validations.isPreviewableImage(item.type)) {
|
||||
let url = Strings.getURLfromCID(item.cid);
|
||||
try {
|
||||
let blurhash = await encodeImageToBlurhash(url);
|
||||
@ -271,19 +166,13 @@ export const upload = async ({ file, context, bucketName }) => {
|
||||
|
||||
export const formatPastedImages = ({ clipboardItems }) => {
|
||||
let files = [];
|
||||
let fileLoading = {};
|
||||
for (let i = 0; i < clipboardItems.length; i++) {
|
||||
// Note(Amine): skip content if it's not an image
|
||||
if (clipboardItems[i].type.indexOf("image") === -1) continue;
|
||||
const file = clipboardItems[i].getAsFile();
|
||||
files.push(file);
|
||||
fileLoading[`${file.lastModified}-${file.name}`] = {
|
||||
name: file.name,
|
||||
loaded: 0,
|
||||
total: file.size,
|
||||
};
|
||||
}
|
||||
return { fileLoading, toUpload: files };
|
||||
return { files };
|
||||
};
|
||||
|
||||
export const formatDroppedFiles = async ({ dataTransfer }) => {
|
||||
@ -294,7 +183,6 @@ export const formatDroppedFiles = async ({ dataTransfer }) => {
|
||||
}
|
||||
|
||||
const files = [];
|
||||
let fileLoading = {};
|
||||
if (dataTransfer.items && dataTransfer.items.length) {
|
||||
for (var i = 0; i < dataTransfer.items.length; i++) {
|
||||
const data = dataTransfer.items[i];
|
||||
@ -320,43 +208,21 @@ export const formatDroppedFiles = async ({ dataTransfer }) => {
|
||||
}
|
||||
|
||||
files.push(file);
|
||||
fileLoading[`${file.lastModified}-${file.name}`] = {
|
||||
name: file.name,
|
||||
loaded: 0,
|
||||
total: file.size,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!files.length) {
|
||||
Events.dispatchMessage({ message: "File type not supported. Please try a different file" });
|
||||
}
|
||||
|
||||
return { fileLoading, files, numFailed: dataTransfer.items.length - files.length };
|
||||
return { files };
|
||||
};
|
||||
|
||||
export const formatUploadedFiles = ({ files }) => {
|
||||
let toUpload = [];
|
||||
let fileLoading = {};
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
toUpload.push(file);
|
||||
fileLoading[fileKey(file)] = {
|
||||
name: file.name,
|
||||
loaded: 0,
|
||||
total: file.size,
|
||||
};
|
||||
}
|
||||
|
||||
if (!toUpload.length) {
|
||||
Events.dispatchMessage({ message: "We could not find any files to upload." });
|
||||
return false;
|
||||
}
|
||||
|
||||
return { toUpload, fileLoading, numFailed: files.length - toUpload.length };
|
||||
return { files: toUpload };
|
||||
};
|
||||
|
@ -406,6 +406,7 @@ export const useTimeout = (callback, ms, dependencies) => {
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, dependencies);
|
||||
};
|
||||
|
||||
export const useEscapeKey = (callback) => {
|
||||
const handleKeyUp = React.useCallback(
|
||||
(e) => {
|
||||
@ -416,7 +417,7 @@ export const useEscapeKey = (callback) => {
|
||||
useEventListener("keyup", handleKeyUp, [handleKeyUp]);
|
||||
};
|
||||
|
||||
export const useLockScroll = ({ lock = true }) => {
|
||||
export const useLockScroll = ({ lock = true } = {}) => {
|
||||
React.useEffect(() => {
|
||||
if (!lock) return;
|
||||
document.body.style.overflow = "hidden";
|
||||
|
@ -1,9 +0,0 @@
|
||||
const cancelledUploads = {};
|
||||
|
||||
export const checkCancelled = (val) => {
|
||||
return cancelledUploads[val];
|
||||
};
|
||||
|
||||
export const setCancelled = (val) => {
|
||||
cancelledUploads[val] = true;
|
||||
};
|
@ -2050,3 +2050,15 @@ export const Clipboard = (props) => (
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const List = (props) => (
|
||||
<svg width={16} height={16} fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M5.333 4H14M5.333 8H14M5.333 12H14M2 4h.007M2 8h.007M2 12h.007"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.25}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
222
common/upload-utilities.js
Normal file
222
common/upload-utilities.js
Normal file
@ -0,0 +1,222 @@
|
||||
import * as FileUtilities from "~/common/file-utilities";
|
||||
import * as Logging from "~/common/logging";
|
||||
import * as Actions from "~/common/actions";
|
||||
|
||||
// NOTE(amine): utilities
|
||||
export const getFileKey = ({ lastModified, name }) => `${lastModified}-${name}`;
|
||||
|
||||
const getLinkSize = (url) => new TextEncoder().encode(url).length;
|
||||
|
||||
let UploadStore = {
|
||||
queue: [],
|
||||
failedFilesCache: {},
|
||||
isUploading: false,
|
||||
uploadedFiles: {},
|
||||
};
|
||||
|
||||
let UploadAbort = {
|
||||
currentUploadingFile: null,
|
||||
abort: null,
|
||||
};
|
||||
|
||||
// NOTE(amine): queue utilities
|
||||
const getUploadQueue = () => UploadStore.queue;
|
||||
const pushToUploadQueue = ({ file, slate, bucketName }) =>
|
||||
UploadStore.queue.push({ file, slate, bucketName });
|
||||
const resetUploadQueue = () => (UploadStore.queue = []);
|
||||
const removeFromUploadQueue = ({ fileKey }) =>
|
||||
(UploadStore.queue = UploadStore.queue.filter(({ file }) => getFileKey(file) !== fileKey));
|
||||
|
||||
// NOTE(amine): failedFilesCache utilities
|
||||
const storeFileInCache = ({ file, slate, bucketName }) =>
|
||||
(UploadStore.failedFilesCache[getFileKey(file)] = { file, slate, bucketName });
|
||||
const removeFileFromCache = ({ fileKey }) => delete UploadStore.failedFilesCache[fileKey];
|
||||
const getFileFromCache = ({ fileKey }) => UploadStore.failedFilesCache[fileKey] || {};
|
||||
|
||||
// NOTE(amine): UploadAbort utilities
|
||||
const registerFileUploading = ({ fileKey }) => (UploadAbort.currentUploadingFile = fileKey);
|
||||
const resetAbortUploadState = () => (UploadAbort = { currentUploadingFile: null, abort: null });
|
||||
const abortCurrentFileUpload = () => UploadAbort.abort();
|
||||
const canCurrentFileBeAborted = () => UploadAbort.currentUploadingFile && UploadAbort.abort;
|
||||
const isFileCurrentlyUploading = ({ fileKey }) =>
|
||||
fileKey === UploadAbort.currentUploadingFile && UploadAbort.abort;
|
||||
|
||||
// NOTE(amine): upload factory function
|
||||
export function createUploadProvider({
|
||||
onStart,
|
||||
onFinish,
|
||||
onAddedToQueue,
|
||||
onProgress,
|
||||
onSuccess,
|
||||
onError,
|
||||
onCancel,
|
||||
onDuplicate,
|
||||
}) {
|
||||
const scheduleQueueUpload = async () => {
|
||||
const uploadQueue = getUploadQueue();
|
||||
if (UploadStore.isUploading || uploadQueue.length === 0) return;
|
||||
|
||||
const { file, slate, bucketName } = getUploadQueue().shift() || {};
|
||||
|
||||
const fileKey = getFileKey(file);
|
||||
|
||||
UploadStore.isUploading = true;
|
||||
registerFileUploading({ fileKey });
|
||||
|
||||
try {
|
||||
if (file.type === "link") {
|
||||
onProgress({ fileKey, loaded: getLinkSize(file.name) });
|
||||
const response = await FileUtilities.uploadLink({
|
||||
url: file.name,
|
||||
slate,
|
||||
uploadAbort: UploadAbort,
|
||||
});
|
||||
|
||||
if (!response?.aborted) {
|
||||
if (!response || response.error) throw new Error(response);
|
||||
|
||||
const isDuplicate = response.data?.duplicate;
|
||||
const fileCid = response.data?.links[0];
|
||||
|
||||
UploadStore.uploadedFiles[fileKey] = true;
|
||||
if (isDuplicate) {
|
||||
if (onDuplicate) onDuplicate({ fileKey, cid: fileCid });
|
||||
} else {
|
||||
if (onSuccess) onSuccess({ fileKey, cid: fileCid });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const response = await FileUtilities.upload({
|
||||
file,
|
||||
bucketName,
|
||||
uploadAbort: UploadAbort,
|
||||
onProgress: (e) => onProgress({ fileKey, loaded: e.loaded }),
|
||||
});
|
||||
|
||||
if (!response?.aborted) {
|
||||
if (!response || response.error) throw new Error(response);
|
||||
// TODO(amine): merge createFile and upload endpoints
|
||||
let createResponse = await Actions.createFile({ files: [response], slate });
|
||||
if (!createResponse || createResponse.error) throw new Error(response);
|
||||
|
||||
const isDuplicate = createResponse?.data?.skipped > 0;
|
||||
const fileCid = createResponse.data?.cid;
|
||||
UploadStore.uploadedFiles[fileKey] = true;
|
||||
|
||||
if (isDuplicate) {
|
||||
if (onDuplicate) onDuplicate({ fileKey, cid: fileCid });
|
||||
} else {
|
||||
if (onSuccess) onSuccess({ fileKey, cid: fileCid });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
storeFileInCache({ file, slate, bucketName });
|
||||
|
||||
if (onError) onError({ fileKey });
|
||||
Logging.error(e);
|
||||
}
|
||||
|
||||
UploadStore.isUploading = false;
|
||||
resetAbortUploadState();
|
||||
|
||||
const isQueueEmpty = getUploadQueue().length === 0;
|
||||
if (!isQueueEmpty) {
|
||||
scheduleQueueUpload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (onFinish) onFinish();
|
||||
};
|
||||
|
||||
const addToUploadQueue = ({ files, slate, bucketName }) => {
|
||||
if (!files || !files.length) return;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const fileKey = getFileKey(files[i]);
|
||||
const doesQueueIncludeFile = getUploadQueue().some(
|
||||
({ file }) => getFileKey(files[i]) === getFileKey(file)
|
||||
);
|
||||
const isUploaded = fileKey in UploadStore.uploadedFiles;
|
||||
// NOTE(amine): skip the file if already uploaded or is in queue
|
||||
if (doesQueueIncludeFile || isUploaded) continue;
|
||||
|
||||
// NOTE(amine): if the added file has failed before, remove it from failedFilesCache
|
||||
if (fileKey in UploadStore.failedFilesCache) removeFileFromCache({ fileKey });
|
||||
|
||||
if (onAddedToQueue) onAddedToQueue(files[i]);
|
||||
pushToUploadQueue({ file: files[i], slate, bucketName });
|
||||
}
|
||||
|
||||
const isQueueEmpty = getUploadQueue().length === 0;
|
||||
if (!UploadStore.isUploading && !isQueueEmpty && onStart) {
|
||||
onStart();
|
||||
scheduleQueueUpload();
|
||||
}
|
||||
};
|
||||
|
||||
const retry = ({ fileKey }) => {
|
||||
const { file, slate, bucketName } = getFileFromCache({ fileKey });
|
||||
if (file.type === "link") {
|
||||
addLinkToUploadQueue({ url: file.name, slate });
|
||||
return;
|
||||
}
|
||||
addToUploadQueue({ files: [file], slate, bucketName });
|
||||
};
|
||||
|
||||
const cancel = ({ fileKey }) => {
|
||||
if (onCancel) onCancel({ fileKeys: [fileKey] });
|
||||
|
||||
if (isFileCurrentlyUploading({ fileKey })) {
|
||||
abortCurrentFileUpload();
|
||||
return;
|
||||
}
|
||||
|
||||
removeFromUploadQueue({ fileKey });
|
||||
};
|
||||
|
||||
const cancelAll = () => {
|
||||
const fileKeys = getUploadQueue().map(({ file }) => getFileKey(file));
|
||||
if (onCancel) onCancel({ fileKeys: [UploadAbort.currentUploadingFile, ...fileKeys] });
|
||||
|
||||
if (canCurrentFileBeAborted()) abortCurrentFileUpload();
|
||||
resetUploadQueue();
|
||||
};
|
||||
|
||||
const addLinkToUploadQueue = async ({ url, slate }) => {
|
||||
const linkAsFile = {
|
||||
name: url,
|
||||
type: "link",
|
||||
size: getLinkSize(url),
|
||||
lastModified: "",
|
||||
};
|
||||
const fileKey = getFileKey(linkAsFile);
|
||||
|
||||
const doesQueueIncludeFile = getUploadQueue().some(
|
||||
({ file }) => getFileKey(linkAsFile) === getFileKey(file)
|
||||
);
|
||||
const isUploaded = fileKey in UploadStore.uploadedFiles;
|
||||
// NOTE(amine): skip the file if already uploaded or is in queue
|
||||
if (doesQueueIncludeFile || isUploaded) return;
|
||||
|
||||
// NOTE(amine): if the added file has failed before, remove it from failedFilesCache
|
||||
if (fileKey in UploadStore.failedFilesCache) removeFileFromCache({ fileKey });
|
||||
|
||||
if (onAddedToQueue) onAddedToQueue(linkAsFile);
|
||||
pushToUploadQueue({ file: linkAsFile, slate, type: "link" });
|
||||
|
||||
const isQueueEmpty = getUploadQueue().length === 0;
|
||||
if (!UploadStore.isUploading && !isQueueEmpty && onStart) {
|
||||
onStart();
|
||||
scheduleQueueUpload();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
upload: addToUploadQueue,
|
||||
uploadLink: addLinkToUploadQueue,
|
||||
retry,
|
||||
cancel,
|
||||
cancelAll,
|
||||
};
|
||||
}
|
@ -4,6 +4,8 @@ import * as Strings from "~/common/strings";
|
||||
import * as Validations from "~/common/validations";
|
||||
import * as Constants from "~/common/constants";
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
//NOTE(martina): this file is for utility functions that do not involve API calls
|
||||
//For API related utility functions, see common/user-behaviors.js
|
||||
//And for uploading related utility functions, see common/file-utilities.js
|
||||
@ -145,3 +147,25 @@ export function mapResponsiveProp(prop, mapper) {
|
||||
}
|
||||
|
||||
export const copyToClipboard = (text) => navigator.clipboard.writeText(text);
|
||||
|
||||
export function formatDateToString(date) {
|
||||
const providedDate = moment(date);
|
||||
const today = moment();
|
||||
const yesterday = moment().subtract(1, "day");
|
||||
|
||||
if (today.isSame(providedDate, "day")) {
|
||||
return "Today at " + providedDate.format("h:mm:ssA");
|
||||
}
|
||||
|
||||
if (yesterday.isSame(providedDate, "day")) {
|
||||
return "Yesterday at " + providedDate.format("h:mm:ssA");
|
||||
}
|
||||
|
||||
return providedDate.format("MMM D, YYYY") + " at " + providedDate.format("h:mm:ssA");
|
||||
}
|
||||
|
||||
export const clamp = (value, min, max) => {
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
};
|
||||
|
@ -154,40 +154,6 @@ export class Alert extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
//NOTE(martina): uploading message
|
||||
if (this.props.fileLoading && Object.keys(this.props.fileLoading).length) {
|
||||
let total = Object.values(this.props.fileLoading).filter((upload) => {
|
||||
return !upload.cancelled;
|
||||
}).length;
|
||||
let uploaded =
|
||||
Object.values(this.props.fileLoading).filter((upload) => {
|
||||
return upload.loaded === upload.total;
|
||||
}).length || 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
css={STYLES_INFO}
|
||||
style={{ cursor: "pointer", ...this.props.style }}
|
||||
onClick={() =>
|
||||
this.props.onAction({
|
||||
type: "SIDEBAR",
|
||||
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
|
||||
})
|
||||
}
|
||||
>
|
||||
<div css={STYLES_MESSAGE_BOX}>
|
||||
<div style={{ height: 16, width: 16, marginRight: 16 }}>
|
||||
<LoaderSpinner style={{ height: 16, width: 16 }} />
|
||||
</div>
|
||||
<span css={STYLES_TEXT}>
|
||||
{uploaded} / {total} file
|
||||
{total === 1 ? "" : "s"} uploading{" "}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
//NOTE(martina): don't upload sensitive info alert
|
||||
if (this.props.viewer && !this.props.noWarning) {
|
||||
return (
|
||||
|
@ -6,9 +6,7 @@ import * as Styles from "~/common/styles";
|
||||
import * as Credentials from "~/common/credentials";
|
||||
import * as Constants from "~/common/constants";
|
||||
import * as Validations from "~/common/validations";
|
||||
import * as FileUtilities from "~/common/file-utilities";
|
||||
import * as Window from "~/common/window";
|
||||
import * as Store from "~/common/store";
|
||||
import * as Websockets from "~/common/browser-websockets";
|
||||
import * as UserBehaviors from "~/common/user-behaviors";
|
||||
import * as Events from "~/common/custom-events";
|
||||
@ -38,7 +36,6 @@ import SidebarCreateSlate from "~/components/sidebars/SidebarCreateSlate";
|
||||
import SidebarCreateWalletAddress from "~/components/sidebars/SidebarCreateWalletAddress";
|
||||
import SidebarWalletSendFunds from "~/components/sidebars/SidebarWalletSendFunds";
|
||||
import SidebarFileStorageDeal from "~/components/sidebars/SidebarFileStorageDeal";
|
||||
import ModalAddFileToBucket from "~/components/sidebars/ModalAddFileToBucket";
|
||||
import SidebarAddFileToSlate from "~/components/sidebars/SidebarAddFileToSlate";
|
||||
import SidebarDragDropNotice from "~/components/sidebars/SidebarDragDropNotice";
|
||||
import SidebarSingleSlateSettings from "~/components/sidebars/SidebarSingleSlateSettings";
|
||||
@ -68,7 +65,6 @@ const SIDEBARS = {
|
||||
SIDEBAR_FILE_STORAGE_DEAL: <SidebarFileStorageDeal />,
|
||||
SIDEBAR_WALLET_SEND_FUNDS: <SidebarWalletSendFunds />,
|
||||
SIDEBAR_CREATE_WALLET_ADDRESS: <SidebarCreateWalletAddress />,
|
||||
SIDEBAR_ADD_FILE_TO_BUCKET: <ModalAddFileToBucket />,
|
||||
SIDEBAR_ADD_FILE_TO_SLATE: <SidebarAddFileToSlate />,
|
||||
SIDEBAR_CREATE_SLATE: <SidebarCreateSlate />,
|
||||
SIDEBAR_DRAG_DROP_NOTICE: <SidebarDragDropNotice />,
|
||||
@ -121,14 +117,9 @@ export default class ApplicationPage extends React.Component {
|
||||
|
||||
mounted = true;
|
||||
|
||||
window.addEventListener("dragenter", this._handleDragEnter);
|
||||
window.addEventListener("dragleave", this._handleDragLeave);
|
||||
window.addEventListener("dragover", this._handleDragOver);
|
||||
window.addEventListener("drop", this._handleDrop);
|
||||
window.addEventListener("online", this._handleOnlineStatus);
|
||||
window.addEventListener("offline", this._handleOnlineStatus);
|
||||
window.addEventListener("resize", this._handleWindowResize);
|
||||
window.addEventListener("paste", this._handleUploadFromClipboard);
|
||||
window.onpopstate = this._handleBackForward;
|
||||
|
||||
if (this.state.viewer) {
|
||||
@ -137,14 +128,9 @@ export default class ApplicationPage extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("dragenter", this._handleDragEnter);
|
||||
window.removeEventListener("dragleave", this._handleDragLeave);
|
||||
window.removeEventListener("dragover", this._handleDragOver);
|
||||
window.removeEventListener("drop", this._handleDrop);
|
||||
window.removeEventListener("online", this._handleOnlineStatus);
|
||||
window.removeEventListener("offline", this._handleOnlineStatus);
|
||||
window.removeEventListener("resize", this._handleWindowResize);
|
||||
window.removeEventListener("paste", this._handleUploadFromClipboard);
|
||||
|
||||
mounted = false;
|
||||
|
||||
@ -154,30 +140,6 @@ export default class ApplicationPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_handleUploadFromClipboard = (e) => {
|
||||
const clipboardItems = e.clipboardData.items || [];
|
||||
if (!clipboardItems) return;
|
||||
|
||||
const { fileLoading, toUpload } = FileUtilities.formatPastedImages({
|
||||
clipboardItems,
|
||||
});
|
||||
|
||||
this._handleRegisterFileLoading({ fileLoading });
|
||||
|
||||
const page = this.state.page;
|
||||
let slate = null;
|
||||
if (page?.id === "NAV_SLATE" && this.state.data?.ownerId === this.state.viewer?.id) {
|
||||
slate = this.state.data;
|
||||
}
|
||||
|
||||
FileUtilities.uploadFiles({
|
||||
files: toUpload,
|
||||
slate,
|
||||
keys: Object.keys(fileLoading),
|
||||
context: this,
|
||||
});
|
||||
};
|
||||
|
||||
_handleUpdateViewer = ({ viewer, callback }) => {
|
||||
// _handleUpdateViewer = (newViewerState, callback) => {
|
||||
// let setAsyncState = (newState) =>
|
||||
@ -290,97 +252,6 @@ export default class ApplicationPage extends React.Component {
|
||||
this.setState({ online: navigator.onLine });
|
||||
};
|
||||
|
||||
_handleDrop = async (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ sidebar: null });
|
||||
const { fileLoading, files, numFailed, error } = await FileUtilities.formatDroppedFiles({
|
||||
dataTransfer: e.dataTransfer,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let page = this.state.page;
|
||||
|
||||
let slate = null;
|
||||
if (page?.id === "NAV_SLATE" && this.state.data?.ownerId === this.state.viewer?.id) {
|
||||
slate = this.state.data;
|
||||
}
|
||||
|
||||
this._handleRegisterFileLoading({ fileLoading });
|
||||
FileUtilities.uploadFiles({
|
||||
files,
|
||||
slate,
|
||||
keys: Object.keys(fileLoading),
|
||||
numFailed,
|
||||
context: this,
|
||||
});
|
||||
};
|
||||
|
||||
_handleUploadFiles = async ({ files, slate }) => {
|
||||
const { fileLoading, toUpload, numFailed } = FileUtilities.formatUploadedFiles({ files });
|
||||
|
||||
this._handleRegisterFileLoading({ fileLoading });
|
||||
FileUtilities.uploadFiles({
|
||||
files: toUpload,
|
||||
slate,
|
||||
keys: Object.keys(fileLoading),
|
||||
numFailed,
|
||||
context: this,
|
||||
});
|
||||
};
|
||||
|
||||
_handleRegisterFileLoading = ({ fileLoading }) => {
|
||||
if (this.state.fileLoading) {
|
||||
return this.setState({
|
||||
fileLoading: { ...this.state.fileLoading, ...fileLoading },
|
||||
});
|
||||
}
|
||||
return this.setState({
|
||||
fileLoading,
|
||||
});
|
||||
};
|
||||
|
||||
_handleRegisterFileCancelled = ({ key }) => {
|
||||
let fileLoading = this.state.fileLoading;
|
||||
fileLoading[key].cancelled = true;
|
||||
this.setState({ fileLoading });
|
||||
};
|
||||
|
||||
_handleRegisterLoadingFinished = ({ keys }) => {
|
||||
let fileLoading = this.state.fileLoading;
|
||||
for (let key of keys) {
|
||||
delete fileLoading[key];
|
||||
}
|
||||
this.setState({ fileLoading });
|
||||
};
|
||||
|
||||
_handleDragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
if (this.state.sidebar) {
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE(jim): Only allow the sidebar to show with file drag and drop.
|
||||
if (e.dataTransfer?.items?.length && e.dataTransfer.items[0].kind !== "file") {
|
||||
return;
|
||||
}
|
||||
|
||||
this._handleAction({
|
||||
type: "SIDEBAR",
|
||||
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
|
||||
});
|
||||
};
|
||||
|
||||
_handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
_handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
_withAuthenticationBehavior = (authenticate) => async (state, newAccount) => {
|
||||
let response = await authenticate(state);
|
||||
if (Events.hasError(response)) {
|
||||
@ -464,10 +335,6 @@ export default class ApplicationPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (options.type === "REGISTER_FILE_CANCELLED") {
|
||||
return this._handleRegisterFileCancelled({ key: options.value });
|
||||
}
|
||||
|
||||
if (options.type === "NEW_WINDOW") {
|
||||
return window.open(options.value);
|
||||
}
|
||||
@ -566,8 +433,9 @@ export default class ApplicationPage extends React.Component {
|
||||
headerElement = (
|
||||
<ApplicationHeader
|
||||
viewer={this.state.viewer}
|
||||
navigation={NavigationData.navigation}
|
||||
data={this.state.data}
|
||||
page={page}
|
||||
navigation={NavigationData.navigation}
|
||||
onAction={this._handleAction}
|
||||
isMobile={this.state.isMobile}
|
||||
isMac={this.props.isMac}
|
||||
@ -586,7 +454,6 @@ export default class ApplicationPage extends React.Component {
|
||||
onAuthenticate: this._withAuthenticationBehavior(UserBehaviors.authenticate),
|
||||
onTwitterAuthenticate: this._withAuthenticationBehavior(UserBehaviors.authenticateViaTwitter),
|
||||
onAction: this._handleAction,
|
||||
onUpload: this._handleUploadFiles,
|
||||
isMobile: this.state.isMobile,
|
||||
isMac: this.props.isMac,
|
||||
activeUsers: this.state.activeUsers,
|
||||
@ -602,10 +469,8 @@ export default class ApplicationPage extends React.Component {
|
||||
viewer: this.state.viewer,
|
||||
data: this.state.data,
|
||||
sidebarData: this.state.sidebarData,
|
||||
fileLoading: this.state.fileLoading,
|
||||
onSelectedChange: this._handleSelectedChange,
|
||||
onCancel: this._handleDismissSidebar,
|
||||
onUpload: this._handleUploadFiles,
|
||||
onAction: this._handleAction,
|
||||
});
|
||||
}
|
||||
@ -639,7 +504,6 @@ export default class ApplicationPage extends React.Component {
|
||||
header={headerElement}
|
||||
sidebar={sidebarElement}
|
||||
onDismissSidebar={this._handleDismissSidebar}
|
||||
fileLoading={this.state.fileLoading}
|
||||
isMobile={this.state.isMobile}
|
||||
isMac={this.props.isMac}
|
||||
viewer={this.state.viewer}
|
||||
|
@ -3,6 +3,7 @@ import * as Constants from "~/common/constants";
|
||||
import * as SVG from "~/common/svg";
|
||||
import * as Events from "~/common/custom-events";
|
||||
import * as Styles from "~/common/styles";
|
||||
import * as Upload from "~/components/core/Upload";
|
||||
|
||||
import {
|
||||
ApplicationUserControls,
|
||||
@ -49,9 +50,9 @@ const STYLES_APPLICATION_HEADER_BACKGROUND = (theme) => css`
|
||||
background-color: ${theme.system.white};
|
||||
box-shadow: 0 0 0 1px ${theme.semantic.bgGrayLight};
|
||||
|
||||
@supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) {
|
||||
-webkit-backdrop-filter: blur(25px);
|
||||
backdrop-filter: blur(25px);
|
||||
@supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) {
|
||||
-webkit-backdrop-filter: blur(75px);
|
||||
backdrop-filter: blur(75px);
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
`;
|
||||
@ -107,7 +108,6 @@ const STYLES_BACKGROUND = css`
|
||||
|
||||
const STYLES_UPLOAD_BUTTON = css`
|
||||
${Styles.CONTAINER_CENTERED};
|
||||
${Styles.BUTTON_RESET};
|
||||
background-color: ${Constants.semantic.bgGrayLight};
|
||||
border-radius: 8px;
|
||||
width: 24px;
|
||||
@ -116,7 +116,7 @@ const STYLES_UPLOAD_BUTTON = css`
|
||||
pointer-events: auto;
|
||||
`;
|
||||
|
||||
export default function ApplicationHeader({ viewer, onAction }) {
|
||||
export default function ApplicationHeader({ viewer, page, data, onAction }) {
|
||||
const [state, setState] = React.useState({
|
||||
showDropdown: false,
|
||||
popup: null,
|
||||
@ -150,10 +150,6 @@ export default function ApplicationHeader({ viewer, onAction }) {
|
||||
onSubmit: handleCreateSearch,
|
||||
});
|
||||
|
||||
const handleUpload = React.useCallback(() => {
|
||||
onAction({ type: "SIDEBAR", value: "SIDEBAR_ADD_FILE_TO_BUCKET" });
|
||||
}, [onAction]);
|
||||
|
||||
const handleDismissSearch = () => setFieldValue("");
|
||||
|
||||
const { mobile } = useMediaQuery();
|
||||
@ -192,15 +188,28 @@ export default function ApplicationHeader({ viewer, onAction }) {
|
||||
{...getFieldProps()}
|
||||
/>
|
||||
</div>
|
||||
<div css={STYLES_RIGHT}>
|
||||
<Actions
|
||||
isSearching={isSearching}
|
||||
isSignedOut={isSignedOut}
|
||||
onAction={onAction}
|
||||
onUpload={handleUpload}
|
||||
onDismissSearch={handleDismissSearch}
|
||||
/>
|
||||
</div>
|
||||
<Upload.Provider page={page} data={data} viewer={viewer}>
|
||||
<Upload.Root onAction={onAction} viewer={viewer}>
|
||||
<div css={STYLES_RIGHT}>
|
||||
<Actions
|
||||
uploadAction={
|
||||
<Upload.Trigger
|
||||
enableMetrics
|
||||
viewer={viewer}
|
||||
aria-label="Upload"
|
||||
css={STYLES_UPLOAD_BUTTON}
|
||||
>
|
||||
<SVG.Plus height="16px" />
|
||||
</Upload.Trigger>
|
||||
}
|
||||
isSearching={isSearching}
|
||||
isSignedOut={isSignedOut}
|
||||
onAction={onAction}
|
||||
onDismissSearch={handleDismissSearch}
|
||||
/>
|
||||
</div>
|
||||
</Upload.Root>
|
||||
</Upload.Provider>
|
||||
</div>
|
||||
<Show when={mobile && state.popup === "profile"}>
|
||||
<ApplicationUserControlsPopup
|
||||
@ -219,7 +228,7 @@ export default function ApplicationHeader({ viewer, onAction }) {
|
||||
);
|
||||
}
|
||||
|
||||
const Actions = ({ isSignedOut, isSearching, onAction, onUpload, onDismissSearch }) => {
|
||||
const Actions = ({ uploadAction, isSignedOut, isSearching, onAction, onDismissSearch }) => {
|
||||
const authActions = React.useMemo(
|
||||
() => (
|
||||
<>
|
||||
@ -249,29 +258,18 @@ const Actions = ({ isSignedOut, isSearching, onAction, onUpload, onDismissSearch
|
||||
[onAction]
|
||||
);
|
||||
|
||||
const uploadAction = React.useMemo(
|
||||
() => (
|
||||
<button css={STYLES_UPLOAD_BUTTON} onClick={onUpload}>
|
||||
<SVG.Plus height="16px" />
|
||||
</button>
|
||||
),
|
||||
[onUpload]
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<Switch
|
||||
fallback={
|
||||
<Switch fallback={uploadAction}>
|
||||
<Match when={isSignedOut}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ y: 10, opacity: 0 }}
|
||||
>
|
||||
{uploadAction}
|
||||
{authActions}
|
||||
</motion.div>
|
||||
}
|
||||
>
|
||||
<Match when={isSignedOut}>{authActions}</Match>
|
||||
</Match>
|
||||
<Match when={isSearching}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
|
@ -8,8 +8,6 @@ import { GlobalTooltip } from "~/components/system/components/fragments/GlobalTo
|
||||
import { Boundary } from "~/components/system/components/fragments/Boundary";
|
||||
import { Alert } from "~/components/core/Alert";
|
||||
|
||||
const MODAL_MARGIN = 56;
|
||||
|
||||
const STYLES_NO_VISIBLE_SCROLL = css`
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
@ -86,27 +84,22 @@ const STYLES_MODAL = css`
|
||||
top: ${Constants.sizes.header}px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
height: calc(100vh - ${Constants.sizes.header}px);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
padding: ${MODAL_MARGIN}px;
|
||||
padding: 24px 24px 32px;
|
||||
height: calc(100vh - ${Constants.sizes.header}px);
|
||||
|
||||
background-color: ${Constants.semantic.bgBlurLight6};
|
||||
background-color: ${Constants.semantic.bgBlurWhiteOP};
|
||||
|
||||
@supports ((-webkit-backdrop-filter: blur(25px)) or (backdrop-filter: blur(25px))) {
|
||||
-webkit-backdrop-filter: blur(25px);
|
||||
backdrop-filter: blur(25px);
|
||||
@supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) {
|
||||
-webkit-backdrop-filter: blur(75px);
|
||||
backdrop-filter: blur(75px);
|
||||
}
|
||||
`;
|
||||
|
||||
const STYLES_MODAL_ELEMENTS = css`
|
||||
border-radius: 8px;
|
||||
background-color: ${Constants.system.white};
|
||||
width: 100%;
|
||||
height: calc(100vh - ${Constants.sizes.header}px - ${MODAL_MARGIN * 2}px);
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const STYLES_SIDEBAR_HEADER = css`
|
||||
@ -194,69 +187,31 @@ export default class ApplicationLayout extends React.Component {
|
||||
render() {
|
||||
let sidebarElements = null;
|
||||
if (this.props.sidebar) {
|
||||
if (this.props.sidebarName === "SIDEBAR_ADD_FILE_TO_BUCKET") {
|
||||
sidebarElements = (
|
||||
<div css={STYLES_MODAL}>
|
||||
<Boundary
|
||||
onMouseDown
|
||||
captureResize={false}
|
||||
captureScroll={false}
|
||||
enabled
|
||||
onOutsideRectEvent={this._handleDismiss}
|
||||
sidebarElements = (
|
||||
<Boundary
|
||||
onMouseDown
|
||||
captureResize={false}
|
||||
captureScroll={false}
|
||||
enabled
|
||||
onOutsideRectEvent={this._handleDismiss}
|
||||
>
|
||||
<div css={STYLES_SIDEBAR}>
|
||||
<div
|
||||
css={STYLES_SIDEBAR_ELEMENTS}
|
||||
ref={(c) => {
|
||||
this._sidebar = c;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={STYLES_MODAL_ELEMENTS}
|
||||
ref={(c) => {
|
||||
this._sidebar = c;
|
||||
}}
|
||||
>
|
||||
<div css={STYLES_SIDEBAR_HEADER} style={{ position: "absolute", right: 30 }}>
|
||||
<div css={STYLES_DISMISS} onClick={this._handleDismiss}>
|
||||
<SVG.Dismiss height="24px" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
css={STYLES_SIDEBAR_CONTENT}
|
||||
style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{this.props.sidebar}
|
||||
<div css={STYLES_SIDEBAR_HEADER}>
|
||||
<div css={STYLES_BLOCK} onClick={this._handleDismiss}>
|
||||
<SVG.Dismiss height="24px" />
|
||||
</div>
|
||||
</div>
|
||||
</Boundary>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
sidebarElements = (
|
||||
<Boundary
|
||||
onMouseDown
|
||||
captureResize={false}
|
||||
captureScroll={false}
|
||||
enabled
|
||||
onOutsideRectEvent={this._handleDismiss}
|
||||
>
|
||||
<div css={STYLES_SIDEBAR}>
|
||||
<div
|
||||
css={STYLES_SIDEBAR_ELEMENTS}
|
||||
ref={(c) => {
|
||||
this._sidebar = c;
|
||||
}}
|
||||
>
|
||||
<div css={STYLES_SIDEBAR_HEADER}>
|
||||
<div css={STYLES_BLOCK} onClick={this._handleDismiss}>
|
||||
<SVG.Dismiss height="24px" />
|
||||
</div>
|
||||
</div>
|
||||
<div css={STYLES_SIDEBAR_CONTENT}>{this.props.sidebar}</div>
|
||||
</div>
|
||||
<div css={STYLES_SIDEBAR_CONTENT}>{this.props.sidebar}</div>
|
||||
</div>
|
||||
</Boundary>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
</Boundary>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
@ -264,13 +219,8 @@ export default class ApplicationLayout extends React.Component {
|
||||
<GlobalTooltip />
|
||||
{this.props.header && (
|
||||
<>
|
||||
<div style={{ visibility: "hidden" }}>{this.props.header}</div>
|
||||
<div
|
||||
css={STYLES_HEADER}
|
||||
style={{ top: this.props.isMobile ? this.state.headerTop : null }}
|
||||
>
|
||||
{this.props.header}
|
||||
</div>
|
||||
<div style={{ height: Constants.sizes.header }} />
|
||||
<div css={STYLES_HEADER}>{this.props.header}</div>
|
||||
</>
|
||||
)}
|
||||
<Alert
|
||||
@ -278,10 +228,9 @@ export default class ApplicationLayout extends React.Component {
|
||||
this.props.page?.id === "NAV_SIGN_IN"
|
||||
? true
|
||||
: this.props.viewer
|
||||
? this.props.viewer.onboarding.hidePrivacyAlert
|
||||
? this.props.viewer?.onboarding?.hidePrivacyAlert
|
||||
: false
|
||||
}
|
||||
fileLoading={this.props.fileLoading}
|
||||
onAction={this.props.onAction}
|
||||
id={this.props.isMobile ? "slate-mobile-alert" : null}
|
||||
viewer={this.props.viewer}
|
||||
|
@ -12,6 +12,7 @@ import { Boundary } from "~/components/system/components/fragments/Boundary";
|
||||
import { H4, P3 } from "~/components/system/components/Typography";
|
||||
|
||||
import ProfilePhoto from "~/components/core/ProfilePhoto";
|
||||
import DataMeter from "~/components/core/DataMeter";
|
||||
|
||||
const STYLES_HEADER = css`
|
||||
position: relative;
|
||||
@ -80,36 +81,6 @@ const STYLES_SECTION_ITEM_HOVER = (theme) => css`
|
||||
}
|
||||
`;
|
||||
|
||||
const STYLES_DATAMETER_WRAPPER = (theme) => css`
|
||||
width: 100%;
|
||||
min-width: 240px;
|
||||
height: 8px;
|
||||
background-color: ${theme.semantic.bgBlurWhiteTRN};
|
||||
border: 1px solid ${theme.semantic.borderGrayLight4};
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const STYLES_DATAMETER = (theme) => css`
|
||||
height: 100%;
|
||||
background-color: ${theme.system.blue};
|
||||
border-radius: 2px;
|
||||
`;
|
||||
|
||||
const DataMeter = ({ bytes = 1000, maximumBytes = 4000, ...props }) => {
|
||||
const percentage = bytes / maximumBytes;
|
||||
return (
|
||||
<div css={STYLES_DATAMETER_WRAPPER} {...props}>
|
||||
<div
|
||||
style={{
|
||||
width: `calc(${percentage} * 100%)`,
|
||||
}}
|
||||
css={STYLES_DATAMETER}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export class ApplicationUserControlsPopup extends React.Component {
|
||||
state = {
|
||||
isExtensionDownloaded: false,
|
||||
@ -180,7 +151,7 @@ export class ApplicationUserControlsPopup extends React.Component {
|
||||
<DataMeter
|
||||
bytes={stats.bytes}
|
||||
maximumBytes={stats.maximumBytes}
|
||||
style={{ marginTop: 8 }}
|
||||
style={{ minWidth: "240px", marginTop: 8 }}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
@ -1,109 +1,36 @@
|
||||
import * as React from "react";
|
||||
import * as Constants from "~/common/constants";
|
||||
import * as Strings from "~/common/strings";
|
||||
import * as Utilities from "~/common/utilities";
|
||||
|
||||
import { css } from "@emotion/react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const STYLES_CONTAINER = css`
|
||||
const STYLES_DATAMETER_WRAPPER = (theme) => css`
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: ${theme.semantic.bgBlurWhiteTRN};
|
||||
border: 1px solid ${theme.semantic.borderGrayLight4};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 1px ${Constants.semantic.borderGrayLight} inset, ${Constants.shadow.lightSmall};
|
||||
padding: 32px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: ${Constants.sizes.mobile}px) {
|
||||
padding: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const STYLES_DATA = css`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
background-color: ${Constants.semantic.bgLight};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const STYLES_DATA_METER = css`
|
||||
flex-shrink: 0;
|
||||
height: 16px;
|
||||
background-color: ${Constants.system.blue};
|
||||
const STYLES_DATAMETER = (theme) => css`
|
||||
position: relative;
|
||||
left: -100%;
|
||||
height: 100%;
|
||||
background-color: ${theme.system.blue};
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
const STYLES_ROW = css`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
font-family: ${Constants.font.code};
|
||||
color: ${Constants.system.grayLight2};
|
||||
font-size: 10px;
|
||||
margin-top: 2px;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const STYLES_LEFT = css`
|
||||
min-width: 10%;
|
||||
width: 100% "";
|
||||
`;
|
||||
|
||||
const STYLES_RIGHT = css`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const STYLES_TITLE = css`
|
||||
font-family: ${Constants.font.medium};
|
||||
font-size: ${Constants.typescale.lvl1};
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
overflow-wrap: break-word;
|
||||
`;
|
||||
|
||||
const STYLES_NOTE = css`
|
||||
margin-top: 8px;
|
||||
font-family: ${Constants.font.text};
|
||||
font-size: ${Constants.typescale.lvl0};
|
||||
color: ${Constants.system.grayLight2};
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
export const DataMeterBar = (props) => {
|
||||
const percentage = props.bytes / props.maximumBytes;
|
||||
|
||||
export default function DataMeter({ bytes = 1000, maximumBytes = 4000, css, ...props }) {
|
||||
const percentage = Utilities.clamp((bytes / maximumBytes) * 100, 0, 100);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div css={STYLES_ROW}>
|
||||
<div css={STYLES_LEFT} style={{ color: props.failed ? Constants.system.red : null }}>
|
||||
{props.leftLabel}
|
||||
</div>
|
||||
<div css={STYLES_RIGHT}>{props.rightLabel}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
css={STYLES_DATA}
|
||||
style={{
|
||||
marginTop: 8,
|
||||
backgroundColor: props.failed ? Constants.system.red : null,
|
||||
}}
|
||||
>
|
||||
<div css={STYLES_DATA_METER} style={{ width: `${percentage * 100}%` }} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const DataMeter = (props) => {
|
||||
return (
|
||||
<div css={STYLES_CONTAINER} style={props.style}>
|
||||
<div css={STYLES_TITLE}>
|
||||
{Strings.bytesToSize(props.stats.bytes)} of {Strings.bytesToSize(props.stats.maximumBytes)}{" "}
|
||||
used
|
||||
</div>
|
||||
<DataMeterBar bytes={props.stats.bytes} maximumBytes={props.stats.maximumBytes} />
|
||||
<div css={[STYLES_DATAMETER_WRAPPER, css]} {...props}>
|
||||
<motion.div
|
||||
initial={false}
|
||||
css={STYLES_DATAMETER}
|
||||
animate={{ x: `${percentage}%` }}
|
||||
transition={{ type: "spring", stiffness: 170, damping: 26 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataMeter;
|
||||
}
|
||||
|
@ -1,179 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as Constants from "~/common/constants";
|
||||
import * as Strings from "~/common/strings";
|
||||
|
||||
import { css } from "@emotion/react";
|
||||
|
||||
const STYLES_CONTAINER = css`
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 0 1px ${Constants.semantic.borderGrayLight} inset, ${Constants.shadow.lightSmall};
|
||||
padding: 32px;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
${"" /* background-color: ${Constants.system.white}; */}
|
||||
|
||||
@media (max-width: ${Constants.sizes.mobile}px) {
|
||||
padding: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const STYLES_ROW = css`
|
||||
font-family: ${Constants.font.code};
|
||||
color: ${Constants.system.grayLight2};
|
||||
background-color: ${Constants.semantic.bgLight};
|
||||
display: inline-flex;
|
||||
height: 16px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const STYLES_STATS_ROW = css`
|
||||
background-color: ${Constants.semantic.bgLight};
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
height: 16px;
|
||||
`;
|
||||
|
||||
const STYLES_TITLE = css`
|
||||
font-family: ${Constants.font.medium};
|
||||
font-size: ${Constants.typescale.lvl1};
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
overflow-wrap: break-word;
|
||||
`;
|
||||
|
||||
const STYLES_NOTE = css`
|
||||
margin-top: 12px;
|
||||
font-family: ${Constants.font.text};
|
||||
font-size: ${Constants.typescale.lvl0};
|
||||
color: ${Constants.system.grayLight2};
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const STYLES_DATA_METER_KEY_WRAPPER = css`
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const STYLES_DATA_METER_KEY_SQUARE = css`
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
background: #73ad21;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
const STYLES_DATA_METER_KEY_LABEL = css`
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
vertical-align: middle;
|
||||
`;
|
||||
|
||||
const DATA_METER_METER_SEGMENT = css`
|
||||
height: 16px;
|
||||
`;
|
||||
|
||||
export const DataMeterBar = (props) => {
|
||||
const percentageUsed = props.bytes / props.maximumBytes;
|
||||
const percentageImage = props.stats.imageBytes / props.bytes;
|
||||
const percentageVideo = props.stats.videoBytes / props.bytes;
|
||||
const percentageEpub = props.stats.epubBytes / props.bytes;
|
||||
const percentagePdf = props.stats.pdfBytes / props.bytes;
|
||||
const percentageAudio = props.stats.audioBytes / props.bytes;
|
||||
const percentageFreeSpace = props.bytes - props.maximumBytes;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div css={STYLES_STATS_ROW}>
|
||||
<div
|
||||
css={STYLES_ROW}
|
||||
style={{
|
||||
width: `${percentageUsed * 100}%`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={DATA_METER_METER_SEGMENT}
|
||||
style={{
|
||||
width: `${percentageImage * 100}%`,
|
||||
backgroundColor: "#C0D8EE",
|
||||
borderRadius: "3px 0px 0px 3px",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
css={DATA_METER_METER_SEGMENT}
|
||||
style={{ width: `${percentageVideo * 100}%`, backgroundColor: "#C0DACD" }}
|
||||
/>
|
||||
<div
|
||||
css={DATA_METER_METER_SEGMENT}
|
||||
style={{ width: `${percentageEpub * 100}%`, backgroundColor: "#FEEDC4" }}
|
||||
/>
|
||||
<div
|
||||
css={DATA_METER_METER_SEGMENT}
|
||||
style={{ width: `${percentagePdf * 100}%`, backgroundColor: "#FAB413" }}
|
||||
/>
|
||||
<div
|
||||
css={DATA_METER_METER_SEGMENT}
|
||||
style={{ width: `${percentageAudio * 100}%`, backgroundColor: "#F1C4C4" }}
|
||||
/>
|
||||
<div
|
||||
css={DATA_METER_METER_SEGMENT}
|
||||
style={{
|
||||
width: `${percentageFreeSpace * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const DataMeterDetailed = (props) => {
|
||||
return (
|
||||
<div css={STYLES_CONTAINER} style={props.style}>
|
||||
<div css={STYLES_TITLE}>
|
||||
{Strings.bytesToSize(props.stats.bytes)} of {Strings.bytesToSize(props.stats.maximumBytes)}{" "}
|
||||
used
|
||||
</div>
|
||||
<DataMeterBar
|
||||
stats={props.stats}
|
||||
bytes={props.stats.bytes}
|
||||
maximumBytes={props.stats.maximumBytes}
|
||||
/>
|
||||
<div css={STYLES_NOTE} style={{ marginTop: 8 }}>
|
||||
<div css={STYLES_DATA_METER_KEY_WRAPPER}>
|
||||
<div css={STYLES_DATA_METER_KEY_SQUARE} style={{ background: `#C0D8EE` }}>
|
||||
{" "}
|
||||
</div>
|
||||
<div css={STYLES_DATA_METER_KEY_LABEL}>Images</div>
|
||||
</div>
|
||||
<div css={STYLES_DATA_METER_KEY_WRAPPER}>
|
||||
<div css={STYLES_DATA_METER_KEY_SQUARE} style={{ background: `#C0DACD` }}>
|
||||
{" "}
|
||||
</div>
|
||||
<div css={STYLES_DATA_METER_KEY_LABEL}>Videos</div>
|
||||
</div>
|
||||
<div css={STYLES_DATA_METER_KEY_WRAPPER}>
|
||||
<div css={STYLES_DATA_METER_KEY_SQUARE} style={{ background: "#FEEDC4" }}>
|
||||
{" "}
|
||||
</div>
|
||||
<div css={STYLES_DATA_METER_KEY_LABEL}>Books</div>
|
||||
</div>
|
||||
<div css={STYLES_DATA_METER_KEY_WRAPPER}>
|
||||
<div css={STYLES_DATA_METER_KEY_SQUARE} style={{ background: "#FAB413" }}>
|
||||
{" "}
|
||||
</div>
|
||||
<div css={STYLES_DATA_METER_KEY_LABEL}>PDF</div>
|
||||
</div>
|
||||
<div css={STYLES_DATA_METER_KEY_WRAPPER}>
|
||||
<div css={STYLES_DATA_METER_KEY_SQUARE} style={{ background: "#F1C4C4" }}>
|
||||
{" "}
|
||||
</div>
|
||||
<div css={STYLES_DATA_METER_KEY_LABEL}>Audio</div>
|
||||
</div>
|
||||
</div>
|
||||
{props.buttons ? <div style={{ marginTop: 24 }}>{props.buttons}</div> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataMeterDetailed;
|
14
components/core/ModalPortal.js
Normal file
14
components/core/ModalPortal.js
Normal file
@ -0,0 +1,14 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
|
||||
export const ModalPortal = ({ children }) => {
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
return () => setMounted(false);
|
||||
}, []);
|
||||
|
||||
return mounted ? ReactDOM.createPortal(children, document.getElementById("modals_portal")) : null;
|
||||
};
|
514
components/core/Upload/Modal.js
Normal file
514
components/core/Upload/Modal.js
Normal file
@ -0,0 +1,514 @@
|
||||
/* 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 { motion } from "framer-motion";
|
||||
import { Link } from "~/components/core/Link";
|
||||
|
||||
import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File";
|
||||
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;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
padding: 24px 24px 32px;
|
||||
height: calc(100vh - ${Constants.sizes.header}px);
|
||||
|
||||
background-color: ${Constants.semantic.bgBlurWhiteOP};
|
||||
|
||||
@supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) {
|
||||
-webkit-backdrop-filter: blur(75px);
|
||||
backdrop-filter: blur(75px);
|
||||
}
|
||||
`;
|
||||
|
||||
const STYLES_MODAL_ELEMENTS = css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
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_WRAPPER = css`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@keyframes global-carousel-fade-in {
|
||||
from {
|
||||
transform: translate(8px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
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" }));
|
||||
|
||||
useEscapeKey(hideUploadModal);
|
||||
|
||||
useLockScroll();
|
||||
|
||||
return (
|
||||
<div css={STYLES_MODAL}>
|
||||
<div css={STYLES_MODAL_ELEMENTS}>
|
||||
<div css={STYLES_SIDEBAR_HEADER} style={{ position: "absolute", right: 24 }}>
|
||||
{/** TODO CLOSE */}
|
||||
<button onClick={hideUploadModal} css={[Styles.BUTTON_RESET, STYLES_DISMISS]}>
|
||||
<SVG.Dismiss height="24px" />
|
||||
</button>
|
||||
</div>
|
||||
<div css={STYLES_MODAL_WRAPPER}>
|
||||
<button
|
||||
css={STYLES_SUMMARY_BUTTON}
|
||||
onClick={toggleSummaryView}
|
||||
style={{
|
||||
backgroundColor:
|
||||
state.view === "summary"
|
||||
? Constants.semantic.bgGrayLight
|
||||
: Constants.semantic.bgLight,
|
||||
}}
|
||||
>
|
||||
<SVG.List />
|
||||
<span style={{ marginLeft: 8 }}>Upload Summary</span>
|
||||
</button>
|
||||
<Show
|
||||
when={state.view === "summary"}
|
||||
fallback={<Controls showUploadSummary={showUploadSummary} />}
|
||||
>
|
||||
<Summary onAction={onAction} viewer={viewer} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Controls
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const STYLES_FILE_HIDDEN = css`
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
`;
|
||||
|
||||
function Controls({ showUploadSummary }) {
|
||||
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 });
|
||||
showUploadSummary();
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
setState((prev) => ({ ...prev, [e.target.name]: e.target.value, urlError: false }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
css={Styles.VERTICAL_CONTAINER_CENTERED}
|
||||
style={{ width: "100%", height: "100%", justifyContent: "center" }}
|
||||
>
|
||||
<input css={STYLES_FILE_HIDDEN} multiple type="file" id="file" onChange={handleUpload} />
|
||||
<div css={Styles.HORIZONTAL_CONTAINER}>
|
||||
<System.Input
|
||||
placeholder="Paste a link to save"
|
||||
value={state.url}
|
||||
style={{
|
||||
width: 392,
|
||||
backgroundColor: Constants.semantic.bgWhite,
|
||||
borderRadius: 12,
|
||||
boxShadow: state.urlError
|
||||
? `0 0 0 1px ${Constants.system.red} inset`
|
||||
: `${Constants.shadow.lightSmall}, 0 0 0 1px ${Constants.semantic.bgGrayLight} inset`,
|
||||
}}
|
||||
containerStyle={{ maxWidth: 600 }}
|
||||
name="url"
|
||||
type="url"
|
||||
onChange={handleChange}
|
||||
onSubmit={handleUploadLink}
|
||||
autoFocus
|
||||
/>
|
||||
<System.ButtonPrimary style={{ marginLeft: 8, width: 96 }} onClick={handleUploadLink}>
|
||||
Save
|
||||
</System.ButtonPrimary>
|
||||
</div>
|
||||
|
||||
<System.Divider width="64px" style={{ margin: "41px 0px" }} />
|
||||
|
||||
<System.H5 color="textGrayDark" as="p" style={{ textAlign: "center" }}>
|
||||
Drop or select files to save to Slate
|
||||
<br />
|
||||
(we recommend uploading fewer than 200 files at a time)
|
||||
</System.H5>
|
||||
<System.ButtonTertiary
|
||||
type="label"
|
||||
htmlFor="file"
|
||||
style={{
|
||||
marginTop: 23,
|
||||
maxWidth: 122,
|
||||
boxShadow: "0px 0px 40px rgba(15, 14, 18, 0.03)",
|
||||
}}
|
||||
>
|
||||
Select files
|
||||
</System.ButtonTertiary>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Summary
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const STYLES_BAR_CONTAINER = (theme) => 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;
|
||||
svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const STYLES_TABLE = (theme) => css`
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
border-radius: 12px;
|
||||
box-shadow: ${theme.shadow.lightSmall};
|
||||
border: 1px solid ${theme.semantic.borderGrayLight};
|
||||
`;
|
||||
|
||||
function Summary({ onAction }) {
|
||||
const [{ fileLoading, isUploading }, { retry, cancel }] = useUploadContext();
|
||||
|
||||
const uploadSummary = React.useMemo(() => {
|
||||
const uploadSummary = Object.entries(fileLoading).map(([, file]) => file);
|
||||
|
||||
const statusOrder = {
|
||||
failed: 1,
|
||||
saving: 2,
|
||||
duplicate: 3,
|
||||
success: 4,
|
||||
};
|
||||
return uploadSummary.sort(
|
||||
(a, b) => statusOrder[a.status] - statusOrder[b.status] || a.createdAt - b.createdAt
|
||||
);
|
||||
}, [fileLoading]);
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%", width: "100%" }} css={Styles.VERTICAL_CONTAINER}>
|
||||
<Show when={isUploading}>
|
||||
<SummaryBox />
|
||||
</Show>
|
||||
<SummaryTable
|
||||
style={{ marginTop: 24, marginBottom: 20 }}
|
||||
onAction={onAction}
|
||||
retry={retry}
|
||||
cancel={cancel}
|
||||
uploadSummary={uploadSummary}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TableButton = ({ children, as = "button", ...props }) => (
|
||||
<System.H5 css={Styles.BUTTON_RESET} color="blue" as={as} {...props}>
|
||||
{children}
|
||||
</System.H5>
|
||||
);
|
||||
|
||||
const SummaryBox = () => {
|
||||
const [
|
||||
{ totalBytesUploaded, totalBytes, totalFilesUploaded, totalFiles, uploadStartingTime },
|
||||
{ cancelAll },
|
||||
] = useUploadContext();
|
||||
|
||||
const uploadRemainingTime = useUploadRemainingTime({
|
||||
uploadStartingTime,
|
||||
totalBytes,
|
||||
totalBytesUploaded,
|
||||
});
|
||||
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0.4 }} animate={{ opacity: 1 }} css={STYLES_BAR_CONTAINER}>
|
||||
<div css={STYLES_PLACEHOLDER}>
|
||||
<FilePlaceholder />
|
||||
</div>
|
||||
<div style={{ marginLeft: 36, width: "100%" }}>
|
||||
<System.H4 color="textBlack">
|
||||
Saving {totalFiles - totalFilesUploaded} of {totalFiles} Objects...
|
||||
</System.H4>
|
||||
<DataMeter bytes={totalBytesUploaded} maximumBytes={totalBytes} style={{ marginTop: 10 }} />
|
||||
<System.H5 color="textGrayDark" style={{ marginTop: 12 }}>
|
||||
{Strings.bytesToSize(totalBytesUploaded, 0)} of {Strings.bytesToSize(totalBytes, 0)}{" "}
|
||||
<Show when={uploadRemainingTime && uploadRemainingTime !== Infinity}>
|
||||
– {Strings.getRemainingTime(uploadRemainingTime)} (Please keep this tab open during
|
||||
uploading)
|
||||
</Show>
|
||||
</System.H5>
|
||||
<System.ButtonTertiary
|
||||
onClick={cancelAll}
|
||||
style={{
|
||||
backgroundColor: Constants.semantic.bgLight,
|
||||
marginTop: 15,
|
||||
padding: "1px 12px 3px",
|
||||
minHeight: "auto",
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</System.ButtonTertiary>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const SummaryTable = ({ uploadSummary, onAction, retry, cancel, ...props }) => {
|
||||
const columns = React.useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: "status",
|
||||
name: <System.H5 color="textGrayDark">Status</System.H5>,
|
||||
width: "19%",
|
||||
contentstyle: { padding: "0px" },
|
||||
},
|
||||
{
|
||||
key: "object",
|
||||
name: <System.H5 color="textGrayDark">Objects</System.H5>,
|
||||
width: "30%",
|
||||
contentstyle: { padding: "0px" },
|
||||
},
|
||||
{
|
||||
key: "date",
|
||||
name: <System.H5 color="textGrayDark">Date saved</System.H5>,
|
||||
width: "23%",
|
||||
contentstyle: { padding: "0px" },
|
||||
},
|
||||
{
|
||||
key: "size",
|
||||
name: <System.H5 color="textGrayDark">Sizes</System.H5>,
|
||||
width: "20%",
|
||||
contentstyle: { padding: "0px" },
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
name: <System.H5 color="textGrayDark">Actions</System.H5>,
|
||||
width: "8%",
|
||||
contentstyle: { padding: "0px" },
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const rows = React.useMemo(() => {
|
||||
return uploadSummary.map((row) => ({
|
||||
status: (
|
||||
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
|
||||
<Switch fallback={<System.H5>Saved</System.H5>}>
|
||||
<Match when={row.status === "saving"}>
|
||||
<System.LoaderSpinner />
|
||||
<System.H5 color="blue" style={{ marginLeft: 8 }}>
|
||||
Saving
|
||||
</System.H5>
|
||||
</Match>
|
||||
<Match when={row.status === "failed"}>
|
||||
<System.H5 color="red">Failed</System.H5>
|
||||
</Match>
|
||||
<Match when={row.status === "duplicate"}>
|
||||
<System.H5 color="green">Already saved</System.H5>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
),
|
||||
object: (
|
||||
<div>
|
||||
{row.cid ? (
|
||||
<Link onAction={onAction} href={`/_/data?cid=${row.cid}`}>
|
||||
<System.H5 nbrOflines={1} title={row.name}>
|
||||
{row.name}
|
||||
</System.H5>
|
||||
</Link>
|
||||
) : (
|
||||
<System.H5 nbrOflines={1} title={row.name}>
|
||||
{row.name}
|
||||
</System.H5>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
date: (
|
||||
<div>
|
||||
<System.H5 nbrOflines={1} title={row.createdAt}>
|
||||
{Utilities.formatDateToString(row.createdAt)}
|
||||
</System.H5>
|
||||
</div>
|
||||
),
|
||||
size: (
|
||||
<div>
|
||||
<Show
|
||||
fallback={<System.H5>{Strings.bytesToSize(row.total)}</System.H5>}
|
||||
when={row.status === "saving"}
|
||||
>
|
||||
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
|
||||
<DataMeter bytes={row.loaded} maximumBytes={row.total} style={{ maxWidth: 84 }} />
|
||||
<System.P3 style={{ marginLeft: 8 }}>
|
||||
{Strings.bytesToSize(row.loaded, 0)} of {Strings.bytesToSize(row.total, 0)}
|
||||
</System.P3>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
),
|
||||
actions: (
|
||||
<div>
|
||||
<Switch
|
||||
fallback={
|
||||
<div css={Styles.HORIZONTAL_CONTAINER}>
|
||||
<Link onAction={onAction} href={`/_/data?cid=${row.cid}`}>
|
||||
<TableButton as="p">Edit</TableButton>
|
||||
</Link>
|
||||
<TableButton style={{ marginLeft: 15 }}>Share</TableButton>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Match when={row.status === "saving"}>
|
||||
<TableButton
|
||||
css={Styles.BUTTON_RESET}
|
||||
onClick={() => cancel({ fileKey: row.id })}
|
||||
color="blue"
|
||||
as="button"
|
||||
>
|
||||
Cancel
|
||||
</TableButton>
|
||||
</Match>
|
||||
<Match when={row.status === "failed"}>
|
||||
<TableButton onClick={() => retry({ fileKey: row.id })}>Retry</TableButton>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
}, [uploadSummary]);
|
||||
|
||||
return (
|
||||
<div css={STYLES_TABLE} {...props}>
|
||||
<Table
|
||||
noColor
|
||||
topRowStyle={{
|
||||
position: "sticky",
|
||||
zIndex: 1,
|
||||
top: "0%",
|
||||
padding: "14px 24px",
|
||||
backgroundColor: Constants.semantic.bgLight,
|
||||
}}
|
||||
rowStyle={{
|
||||
padding: "14px 24px",
|
||||
backgroundColor: Constants.system.white,
|
||||
}}
|
||||
data={{
|
||||
columns,
|
||||
rows,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
264
components/core/Upload/Provider.js
Normal file
264
components/core/Upload/Provider.js
Normal file
@ -0,0 +1,264 @@
|
||||
import * as React from "react";
|
||||
import * as UploadUtilities from "~/common/upload-utilities";
|
||||
import * as FileUtilities from "~/common/file-utilities";
|
||||
|
||||
import { useEventListener } from "~/common/hooks";
|
||||
|
||||
const UploadContext = React.createContext({});
|
||||
export const useUploadContext = () => React.useContext(UploadContext);
|
||||
|
||||
export const Provider = ({ children, page, data, viewer }) => {
|
||||
const [uploadState, uploadHandlers] = useUpload();
|
||||
|
||||
const [isUploadModalVisible, { showUploadModal, hideUploadModal }] = useUploadModal();
|
||||
|
||||
useUploadOnDrop({ upload: uploadHandlers.upload, page, data, viewer });
|
||||
|
||||
useUploadFromClipboard({ upload: uploadHandlers.upload, page, data, viewer });
|
||||
|
||||
useEventListener("upload-modal-open", showUploadModal);
|
||||
|
||||
const providerValue = React.useMemo(
|
||||
() => [
|
||||
{ isUploadModalVisible, ...uploadState },
|
||||
{ showUploadModal, hideUploadModal, ...uploadHandlers },
|
||||
],
|
||||
[isUploadModalVisible, uploadHandlers, uploadState]
|
||||
);
|
||||
|
||||
return <UploadContext.Provider value={providerValue}>{children}</UploadContext.Provider>;
|
||||
};
|
||||
|
||||
const useUploadModal = () => {
|
||||
const [isUploadModalVisible, setUploadModalState] = React.useState(false);
|
||||
const showUploadModal = () => setUploadModalState(true);
|
||||
const hideUploadModal = () => setUploadModalState(false);
|
||||
return [isUploadModalVisible, { showUploadModal, hideUploadModal }];
|
||||
};
|
||||
|
||||
const useUpload = () => {
|
||||
const DEFAULT_STATE = {
|
||||
fileLoading: {},
|
||||
isUploading: false,
|
||||
uploadStartingTime: null,
|
||||
totalBytesUploaded: 0,
|
||||
totalBytes: 0,
|
||||
totalFilesUploaded: 0,
|
||||
totalFiles: 0,
|
||||
uploadRemainingTime: 0,
|
||||
};
|
||||
|
||||
const [uploadState, setUploadState] = React.useState(DEFAULT_STATE);
|
||||
|
||||
const uploadProvider = React.useMemo(() => {
|
||||
const handleStartUploading = () => {
|
||||
setUploadState((prev) => ({ ...prev, isUploading: true, uploadStartingTime: new Date() }));
|
||||
};
|
||||
|
||||
const handleFinishUploading = () => {
|
||||
setUploadState((prev) => ({
|
||||
...DEFAULT_STATE,
|
||||
fileLoading: prev.fileLoading,
|
||||
uploadStartingTime: null,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddToQueue = (file) => {
|
||||
const fileKey = UploadUtilities.getFileKey(file);
|
||||
setUploadState((prev) => ({
|
||||
...prev,
|
||||
fileLoading: {
|
||||
...prev.fileLoading,
|
||||
[fileKey]: {
|
||||
id: fileKey,
|
||||
status: "saving",
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
createdAt: Date.now(),
|
||||
loaded: 0,
|
||||
total: file.size,
|
||||
},
|
||||
},
|
||||
totalFiles: prev.totalFiles + 1,
|
||||
totalBytes: prev.totalBytes + file.size,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSuccess = ({ fileKey, cid }) => {
|
||||
setUploadState((prev) => {
|
||||
const newFileLoading = { ...prev.fileLoading };
|
||||
newFileLoading[fileKey].status = "success";
|
||||
newFileLoading[fileKey].cid = cid;
|
||||
return {
|
||||
...prev,
|
||||
fileLoading: newFileLoading,
|
||||
totalFilesUploaded: prev.totalFilesUploaded + 1,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleDuplicate = ({ fileKey, cid }) => {
|
||||
setUploadState((prev) => {
|
||||
const newFileLoading = { ...prev.fileLoading };
|
||||
newFileLoading[fileKey].status = "duplicate";
|
||||
newFileLoading[fileKey].cid = cid;
|
||||
return {
|
||||
...prev,
|
||||
fileLoading: newFileLoading,
|
||||
totalFilesUploaded: prev.totalFilesUploaded + 1,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleProgress = ({ fileKey, loaded }) => {
|
||||
setUploadState((prev) => {
|
||||
const newFileLoading = { ...prev.fileLoading };
|
||||
const bytesLoaded = loaded - newFileLoading[fileKey].loaded;
|
||||
newFileLoading[fileKey].loaded = loaded;
|
||||
return {
|
||||
...prev,
|
||||
fileLoading: newFileLoading,
|
||||
totalBytesUploaded: prev.totalBytesUploaded + bytesLoaded,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleError = ({ fileKey }) => {
|
||||
setUploadState((prev) => {
|
||||
const newFileLoading = { ...prev.fileLoading };
|
||||
newFileLoading[fileKey].status = "failed";
|
||||
return {
|
||||
...prev,
|
||||
fileLoading: newFileLoading,
|
||||
totalFiles: prev.totalFiles - 1,
|
||||
totalBytes: prev.totalBytes - newFileLoading[fileKey].total,
|
||||
totalBytesUploaded: prev.totalBytesUploaded - newFileLoading[fileKey].total,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancelUploading = ({ fileKeys }) => {
|
||||
setUploadState((prev) => {
|
||||
const newFileLoading = { ...prev.fileLoading };
|
||||
const newTotalFiles = prev.totalFiles - fileKeys.length;
|
||||
let newTotalBytes = prev.totalBytes;
|
||||
|
||||
fileKeys.forEach((fileKey) => {
|
||||
newTotalBytes -= newFileLoading[fileKey].total;
|
||||
delete newFileLoading[fileKey];
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
fileLoading: newFileLoading,
|
||||
totalFiles: newTotalFiles,
|
||||
totalBytes: newTotalBytes,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return UploadUtilities.createUploadProvider({
|
||||
onStart: handleStartUploading,
|
||||
onFinish: handleFinishUploading,
|
||||
onAddedToQueue: handleAddToQueue,
|
||||
onSuccess: handleSuccess,
|
||||
onDuplicate: handleDuplicate,
|
||||
onProgress: handleProgress,
|
||||
onCancel: handleCancelUploading,
|
||||
onError: handleError,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return [
|
||||
uploadState,
|
||||
{
|
||||
upload: uploadProvider.upload,
|
||||
uploadLink: uploadProvider.uploadLink,
|
||||
retry: uploadProvider.retry,
|
||||
cancel: uploadProvider.cancel,
|
||||
cancelAll: uploadProvider.cancelAll,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const useUploadOnDrop = ({ upload, page, data, viewer }) => {
|
||||
const handleDragEnter = (e) => e.preventDefault();
|
||||
const handleDragLeave = (e) => e.preventDefault();
|
||||
const handleDragOver = (e) => e.preventDefault();
|
||||
const handleDrop = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { files, error } = await FileUtilities.formatDroppedFiles({
|
||||
dataTransfer: e.dataTransfer,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let slate = null;
|
||||
if (page?.id === "NAV_SLATE" && data?.ownerId === viewer?.id) {
|
||||
slate = data;
|
||||
}
|
||||
|
||||
upload({ files, slate });
|
||||
};
|
||||
|
||||
useEventListener("dragenter", handleDragEnter, []);
|
||||
useEventListener("dragleave", handleDragLeave, []);
|
||||
useEventListener("dragover", handleDragOver, []);
|
||||
useEventListener("drop", handleDrop, []);
|
||||
};
|
||||
|
||||
const useUploadFromClipboard = ({ upload, page, data, viewer }) => {
|
||||
const handlePaste = (e) => {
|
||||
const clipboardItems = e.clipboardData.items || [];
|
||||
if (!clipboardItems) return;
|
||||
|
||||
const { files } = FileUtilities.formatPastedImages({
|
||||
clipboardItems,
|
||||
});
|
||||
|
||||
let slate = null;
|
||||
if (page?.id === "NAV_SLATE" && data?.ownerId === viewer?.id) {
|
||||
slate = data;
|
||||
}
|
||||
upload({ files, slate });
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
90
components/core/Upload/index.js
Normal file
90
components/core/Upload/index.js
Normal file
@ -0,0 +1,90 @@
|
||||
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 DataMeter from "~/components/core/DataMeter";
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Root
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
const Root = ({ onAction, viewer, children }) => {
|
||||
const [{ isUploadModalVisible }] = useUploadContext();
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<Show when={isUploadModalVisible}>
|
||||
<ModalPortal>
|
||||
<UploadModal viewer={viewer} onAction={onAction} />
|
||||
</ModalPortal>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Trigger
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const Trigger = ({ enableMetrics = false, viewer, css, children, ...props }) => {
|
||||
const showUploadModal = () => {
|
||||
if (!viewer) {
|
||||
Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} });
|
||||
return;
|
||||
}
|
||||
Events.dispatchCustomEvent({ name: "upload-modal-open" });
|
||||
};
|
||||
|
||||
return (
|
||||
<div css={Styles.HORIZONTAL_CONTAINER_CENTERED}>
|
||||
<Show when={enableMetrics}>
|
||||
<UploadMetrics />
|
||||
</Show>
|
||||
<button css={[Styles.BUTTON_RESET, css]} onClick={showUploadModal} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UploadMetrics = () => {
|
||||
const [{ isUploading, totalBytesUploaded, totalBytes }, { showUploadModal }] = useUploadContext();
|
||||
const uploadProgress = Math.floor((totalBytesUploaded / totalBytes) * 100);
|
||||
|
||||
return (
|
||||
isUploading && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ y: 10, opacity: 0 }}
|
||||
css={Styles.BUTTON_RESET}
|
||||
style={{ marginRight: 14 }}
|
||||
aria-label="Upload"
|
||||
onClick={showUploadModal}
|
||||
>
|
||||
<System.P3 color="textBlack">{uploadProgress}%</System.P3>
|
||||
<DataMeter
|
||||
bytes={totalBytesUploaded}
|
||||
maximumBytes={totalBytes}
|
||||
style={{
|
||||
width: 28,
|
||||
marginTop: 4,
|
||||
backgroundColor: Constants.semantic.bgGrayLight,
|
||||
}}
|
||||
/>
|
||||
</motion.button>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export { Provider, Root, Trigger };
|
@ -1,315 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as Constants from "~/common/constants";
|
||||
import * as Strings from "~/common/strings";
|
||||
import * as System from "~/components/system";
|
||||
import * as Store from "~/common/store";
|
||||
import * as Styles from "~/common/styles";
|
||||
import * as SVG from "~/common/svg";
|
||||
import * as Actions from "~/common/actions";
|
||||
import * as Events from "~/common/custom-events";
|
||||
import * as FileUtilities from "~/common/file-utilities";
|
||||
import * as Logging from "~/common/logging";
|
||||
|
||||
import { css } from "@emotion/react";
|
||||
import { DataMeterBar } from "~/components/core/DataMeter";
|
||||
import { SidebarWarningMessage } from "~/components/core/WarningMessage";
|
||||
import { FileTypeGroup } from "~/components/core/FileTypeIcon";
|
||||
|
||||
const STYLES_FILE_HIDDEN = css`
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
`;
|
||||
|
||||
const STYLES_FILE_LIST = css`
|
||||
box-shadow: 0 0 0 1px inset ${Constants.semantic.borderGrayLight};
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
const STYLES_FILE_LINE = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid ${Constants.semantic.borderGrayLight};
|
||||
`;
|
||||
|
||||
const STYLES_FILE_NAME = css`
|
||||
min-width: 10%;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: 0.9rem;
|
||||
font-family: ${Constants.font.medium};
|
||||
`;
|
||||
|
||||
const STYLES_LEFT = css`
|
||||
width: 100%;
|
||||
min-width: 10%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const STYLES_RIGHT = css`
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const STYLES_FILE_STATUS = css`
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const STYLES_BAR_CONTAINER = css`
|
||||
${"" /* background: ${Constants.semantic.bgLight}; */}
|
||||
box-shadow: 0 0 0 1px inset ${Constants.semantic.borderGrayLight};
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
${"" /* margin-top: 48px; */}
|
||||
`;
|
||||
|
||||
const STYLES_PERFORMANCE = css`
|
||||
font-family: ${Constants.font.code};
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
margin: 0 0 8px 0;
|
||||
`;
|
||||
|
||||
export default class ModalAddFileToBucket extends React.Component {
|
||||
state = {
|
||||
url: "",
|
||||
slate: this.props.page.id === "NAV_SLATE" && this.props.data?.id ? this.props.data : null,
|
||||
urlError: false,
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
window.addEventListener("keydown", this._handleDocumentKeydown);
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
window.removeEventListener("keydown", this._handleDocumentKeydown);
|
||||
};
|
||||
|
||||
_handleDocumentKeydown = (e) => {
|
||||
if (e.keyCode === 27) {
|
||||
this.props.onCancel();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
_handleUpload = (e) => {
|
||||
this.props.onUpload({
|
||||
files: e.target.files,
|
||||
slate: this.state.slate,
|
||||
});
|
||||
this.props.onCancel();
|
||||
};
|
||||
|
||||
_handleChange = (e) => {
|
||||
this.setState({ [e.target.name]: e.target.value, urlError: false });
|
||||
};
|
||||
|
||||
_handleCancel = (e, key) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
Events.dispatchCustomEvent({ name: `cancel-${key}` }); //NOTE(martina): so that will cancel if is in the middle of uploading
|
||||
Store.setCancelled(key); //NOTE(martina): so that will cancel if hasn't started uploading yet
|
||||
this.props.onAction({ type: "REGISTER_FILE_CANCELLED", value: key }); //NOTE(martina): so that fileLoading registers it
|
||||
};
|
||||
|
||||
_handleUploadLink = () => {
|
||||
if (Strings.isEmpty(this.state.url)) {
|
||||
this.setState({ urlError: true });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = new URL(this.state.url);
|
||||
} catch (e) {
|
||||
Logging.error(e);
|
||||
this.setState({ urlError: true });
|
||||
return;
|
||||
}
|
||||
FileUtilities.uploadLink({ url: this.state.url, slate: this.state.slate });
|
||||
this.props.onCancel();
|
||||
};
|
||||
|
||||
render() {
|
||||
let loaded = 0;
|
||||
let total = 0;
|
||||
if (this.props.fileLoading) {
|
||||
for (let file of Object.values(this.props.fileLoading)) {
|
||||
if (typeof file.loaded === "number" && typeof file.total === "number") {
|
||||
total += file.total;
|
||||
loaded += file.loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.fileLoading && Object.keys(this.props.fileLoading).length) {
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%" }}>
|
||||
<System.H2 style={{ marginBottom: 36, marginTop: 12 }}>Upload Status</System.H2>
|
||||
<div css={STYLES_BAR_CONTAINER}>
|
||||
<strong css={STYLES_PERFORMANCE}>
|
||||
{Strings.bytesToSize(loaded)} / {Strings.bytesToSize(total)}
|
||||
</strong>
|
||||
<DataMeterBar bytes={loaded} maximumBytes={total} />
|
||||
</div>
|
||||
<div css={STYLES_FILE_LIST} style={{ marginTop: 36, overflow: "hidden" }}>
|
||||
{this.props.fileLoading
|
||||
? Object.entries(this.props.fileLoading).map((entry) => {
|
||||
let file = entry[1];
|
||||
return (
|
||||
<div css={STYLES_FILE_LINE} key={file.name}>
|
||||
<span css={STYLES_LEFT}>
|
||||
<div css={STYLES_FILE_STATUS}>
|
||||
{file.failed ? (
|
||||
<SVG.Alert
|
||||
height="24px"
|
||||
style={{
|
||||
color: Constants.system.red,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
) : file.cancelled ? (
|
||||
<SVG.Dismiss
|
||||
height="24px"
|
||||
style={{
|
||||
color: Constants.system.gray,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
) : file.loaded === file.total ? (
|
||||
<SVG.CheckBox
|
||||
height="24px"
|
||||
style={{ color: Constants.system.grayLight2 }}
|
||||
/>
|
||||
) : (
|
||||
<System.LoaderSpinner
|
||||
style={{
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
margin: "2px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
css={STYLES_FILE_NAME}
|
||||
style={
|
||||
file.failed
|
||||
? { color: Constants.system.red }
|
||||
: file.cancelled
|
||||
? { color: Constants.system.gray }
|
||||
: null
|
||||
}
|
||||
>
|
||||
{file.name}
|
||||
</div>
|
||||
</span>
|
||||
{file.loaded === file.total || file.failed || file.cancelled ? (
|
||||
<div css={STYLES_RIGHT} style={{ height: 24, width: 24 }} />
|
||||
) : (
|
||||
<span
|
||||
css={STYLES_RIGHT}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={(e) => this._handleCancel(e, entry[0])}
|
||||
>
|
||||
<SVG.Dismiss
|
||||
height="24px"
|
||||
className="boundary-ignore"
|
||||
style={{
|
||||
color: Constants.system.gray,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div css={Styles.VERTICAL_CONTAINER_CENTERED} style={{ width: "100%", maxWidth: 680 }}>
|
||||
<input
|
||||
css={STYLES_FILE_HIDDEN}
|
||||
multiple
|
||||
type="file"
|
||||
id="file"
|
||||
onChange={this._handleUpload}
|
||||
/>
|
||||
<div css={Styles.HORIZONTAL_CONTAINER} style={{ width: "100%" }}>
|
||||
<System.Input
|
||||
placeholder="Paste a link to save"
|
||||
value={this.state.url}
|
||||
style={{
|
||||
height: 40,
|
||||
backgroundColor: Constants.semantic.bgLight,
|
||||
boxShadow: this.state.urlError ? `0 0 0 1px ${Constants.system.red} inset` : "none",
|
||||
}}
|
||||
containerStyle={{ maxWidth: 600 }}
|
||||
name="url"
|
||||
type="url"
|
||||
onChange={this._handleChange}
|
||||
onSubmit={this._handleUploadLink}
|
||||
autoFocus
|
||||
/>
|
||||
<System.ButtonPrimary
|
||||
onClick={this._handleUploadLink}
|
||||
style={{ height: 40, marginLeft: 8 }}
|
||||
>
|
||||
Save
|
||||
</System.ButtonPrimary>
|
||||
</div>
|
||||
<System.Divider width="64px" style={{ margin: "40px 0px" }} />
|
||||
|
||||
<System.H4
|
||||
style={{
|
||||
color: Constants.semantic.textGrayDark,
|
||||
textAlign: "center",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
Drag and drop files or use the button below to save to{" "}
|
||||
{this.props.data ? Strings.getPresentationSlateName(this.props.data) : "Slate"}
|
||||
</System.H4>
|
||||
<System.P3
|
||||
style={{
|
||||
color: Constants.semantic.textGray,
|
||||
maxWidth: 456,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Please don't upload sensitive information to Slate yet. All uploaded data can currently
|
||||
be accessed by anyone with the link. Private storage is coming soon.
|
||||
</System.P3>
|
||||
|
||||
<System.ButtonPrimary
|
||||
type="label"
|
||||
full
|
||||
htmlFor="file"
|
||||
style={{ marginTop: 40, maxWidth: 210 }}
|
||||
>
|
||||
Select files
|
||||
</System.ButtonPrimary>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,280 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as Constants from "~/common/constants";
|
||||
import * as Strings from "~/common/strings";
|
||||
import * as System from "~/components/system";
|
||||
import * as Store from "~/common/store";
|
||||
import * as SVG from "~/common/svg";
|
||||
import * as Actions from "~/common/actions";
|
||||
import * as Events from "~/common/custom-events";
|
||||
import * as FileUtilities from "~/common/file-utilities";
|
||||
|
||||
import { css } from "@emotion/react";
|
||||
import { DataMeterBar } from "~/components/core/DataMeter";
|
||||
import { SidebarWarningMessage } from "~/components/core/WarningMessage";
|
||||
import { FileTypeGroup } from "~/components/core/FileTypeIcon";
|
||||
|
||||
const STYLES_FILE_HIDDEN = css`
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
`;
|
||||
|
||||
const STYLES_FILE_LINE = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background-color: ${Constants.system.white};
|
||||
border-bottom: 1px solid ${Constants.semantic.bgLight};
|
||||
`;
|
||||
|
||||
const STYLES_FILE_NAME = css`
|
||||
min-width: 10%;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: 0.9rem;
|
||||
font-family: ${Constants.font.medium};
|
||||
`;
|
||||
|
||||
const STYLES_LEFT = css`
|
||||
width: 100%;
|
||||
min-width: 10%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const STYLES_RIGHT = css`
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const STYLES_FILE_STATUS = css`
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const STYLES_BAR_CONTAINER = css`
|
||||
background: ${Constants.system.white};
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
margin-top: 48px;
|
||||
`;
|
||||
|
||||
const STYLES_PERFORMANCE = css`
|
||||
font-family: ${Constants.font.code};
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
margin: 0 0 8px 0;
|
||||
`;
|
||||
|
||||
export default class SidebarAddFileToBucket extends React.Component {
|
||||
state = {
|
||||
url: "",
|
||||
slate: this.props.page.id === "NAV_SLATE" && this.props.data?.id ? this.props.data : null,
|
||||
};
|
||||
|
||||
_handleUpload = (e) => {
|
||||
this.props.onUpload({
|
||||
files: e.target.files,
|
||||
slate: this.state.slate,
|
||||
});
|
||||
this.props.onCancel();
|
||||
};
|
||||
|
||||
_handleChange = (e) => {
|
||||
this.setState({ [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
_handleCancel = (e, key) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
Events.dispatchCustomEvent({ name: `cancel-${key}` }); //NOTE(martina): so that will cancel if is in the middle of uploading
|
||||
Store.setCancelled(key); //NOTE(martina): so that will cancel if hasn't started uploading yet
|
||||
this.props.onAction({ type: "REGISTER_FILE_CANCELLED", value: key }); //NOTE(martina): so that fileLoading registers it
|
||||
};
|
||||
|
||||
_handleUploadLink = () => {
|
||||
FileUtilities.uploadLink({ url: this.state.url, slate: this.state.slate });
|
||||
this.props.onCancel();
|
||||
};
|
||||
|
||||
render() {
|
||||
let loaded = 0;
|
||||
let total = 0;
|
||||
if (this.props.fileLoading) {
|
||||
for (let file of Object.values(this.props.fileLoading)) {
|
||||
if (typeof file.loaded === "number" && typeof file.total === "number") {
|
||||
total += file.total;
|
||||
loaded += file.loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<System.P1
|
||||
style={{
|
||||
fontFamily: Constants.font.semiBold,
|
||||
fontSize: Constants.typescale.lvl3,
|
||||
marginBottom: 36,
|
||||
}}
|
||||
>
|
||||
{this.props.fileLoading && Object.keys(this.props.fileLoading).length
|
||||
? "Upload progress"
|
||||
: "Upload data"}
|
||||
</System.P1>
|
||||
|
||||
<input
|
||||
css={STYLES_FILE_HIDDEN}
|
||||
multiple
|
||||
type="file"
|
||||
id="file"
|
||||
onChange={this._handleUpload}
|
||||
/>
|
||||
|
||||
{this.props.fileLoading && Object.keys(this.props.fileLoading).length ? null : (
|
||||
<React.Fragment>
|
||||
<FileTypeGroup style={{ margin: "64px 0px" }} />
|
||||
|
||||
<System.P1>
|
||||
Click below or drop a file anywhere on the page to upload a file
|
||||
{this.props.data?.slatename || this.props.data?.name ? (
|
||||
<span>
|
||||
{" "}
|
||||
to <strong>{Strings.getPresentationSlateName(this.props.data)}</strong>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
.
|
||||
</System.P1>
|
||||
|
||||
<SidebarWarningMessage />
|
||||
|
||||
<System.Input
|
||||
name="url"
|
||||
type="url"
|
||||
value={this.state.url}
|
||||
placeholder="URL"
|
||||
onChange={this._handleChange}
|
||||
style={{ marginTop: 48 }}
|
||||
/>
|
||||
|
||||
<System.ButtonPrimary
|
||||
full
|
||||
type="label"
|
||||
style={{ marginTop: 24 }}
|
||||
onClick={this._handleUploadLink}
|
||||
>
|
||||
Add link
|
||||
</System.ButtonPrimary>
|
||||
|
||||
<System.Divider
|
||||
color="#AEAEB2"
|
||||
width="45px"
|
||||
height="0.5px"
|
||||
style={{ margin: "0px auto", marginTop: "20px" }}
|
||||
/>
|
||||
|
||||
<System.ButtonPrimary full type="label" htmlFor="file" style={{ marginTop: 24 }}>
|
||||
Upload file
|
||||
</System.ButtonPrimary>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{this.props.fileLoading && Object.keys(this.props.fileLoading).length ? (
|
||||
<div css={STYLES_BAR_CONTAINER}>
|
||||
<strong css={STYLES_PERFORMANCE}>
|
||||
{Strings.bytesToSize(loaded)} / {Strings.bytesToSize(total)}
|
||||
</strong>
|
||||
<DataMeterBar bytes={loaded} maximumBytes={total} />
|
||||
</div>
|
||||
) : null}
|
||||
<div style={{ marginTop: 24, borderRadius: 4, overflow: "hidden" }}>
|
||||
{this.props.fileLoading
|
||||
? Object.entries(this.props.fileLoading).map((entry) => {
|
||||
let file = entry[1];
|
||||
return (
|
||||
<div css={STYLES_FILE_LINE} key={file.name}>
|
||||
<span css={STYLES_LEFT}>
|
||||
<div css={STYLES_FILE_STATUS}>
|
||||
{file.failed ? (
|
||||
<SVG.Alert
|
||||
height="24px"
|
||||
style={{
|
||||
color: Constants.system.red,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
) : file.cancelled ? (
|
||||
<SVG.Dismiss
|
||||
height="24px"
|
||||
style={{
|
||||
color: Constants.system.gray,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
) : file.loaded === file.total ? (
|
||||
<SVG.CheckBox height="24px" />
|
||||
) : (
|
||||
<System.LoaderSpinner
|
||||
style={{
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
margin: "2px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
css={STYLES_FILE_NAME}
|
||||
style={
|
||||
file.failed
|
||||
? { color: Constants.system.red }
|
||||
: file.cancelled
|
||||
? { color: Constants.system.gray }
|
||||
: null
|
||||
}
|
||||
>
|
||||
{file.name}
|
||||
</div>
|
||||
</span>
|
||||
{file.loaded === file.total || file.failed || file.cancelled ? (
|
||||
<div css={STYLES_RIGHT} style={{ height: 24, width: 24 }} />
|
||||
) : (
|
||||
<span
|
||||
css={STYLES_RIGHT}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={(e) => this._handleCancel(e, entry[0])}
|
||||
>
|
||||
<SVG.Dismiss
|
||||
height="24px"
|
||||
className="boundary-ignore"
|
||||
style={{
|
||||
color: Constants.system.gray,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ import { LoaderSpinner } from "~/components/system/components/Loaders";
|
||||
|
||||
const STYLES_BUTTON = `
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
min-height: 40px;
|
||||
|
@ -17,8 +17,6 @@ const STYLES_ROOT = css`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@ -26,9 +24,8 @@ const STYLES_ROOT = css`
|
||||
z-index: ${Constants.zindex.modal};
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
|
||||
${
|
||||
"" /* Note(Amine): we're using the blur filter to fix a weird backdrop-filter's bug in chrome */
|
||||
}
|
||||
// Note(Amine): we're using the blur filter to fix a weird backdrop-filter's bug in chrome
|
||||
|
||||
filter: blur(0px);
|
||||
@supports ((-webkit-backdrop-filter: blur(15px)) or (backdrop-filter: blur(15px))) {
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
@ -37,11 +34,11 @@ const STYLES_ROOT = css`
|
||||
|
||||
@keyframes global-carousel-fade-in {
|
||||
from {
|
||||
transform: translate(8px);
|
||||
transform: translateX(8px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: trannslateX(0px);
|
||||
transform: translateX(0px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +147,7 @@ export class Table extends React.Component {
|
||||
backgroundColor: this.props.noColor ? null : ac[c.key].color,
|
||||
flexShrink,
|
||||
}}
|
||||
contentstyle={c.contentstyle}
|
||||
tooltip={c.tooltip}
|
||||
>
|
||||
{text}
|
||||
|
@ -67,6 +67,7 @@ import {
|
||||
H2,
|
||||
H3,
|
||||
H4,
|
||||
H5,
|
||||
P1,
|
||||
P2,
|
||||
P3,
|
||||
@ -158,6 +159,7 @@ export {
|
||||
H2,
|
||||
H3,
|
||||
H4,
|
||||
H5,
|
||||
P1,
|
||||
P2,
|
||||
P3,
|
||||
|
@ -15,6 +15,7 @@ class MyDocument extends Document {
|
||||
* e.g. if the extension is installed on the user's browser, it will add 'isDownloaded' to className*/}
|
||||
<div id="browser_extension" />
|
||||
<Main />
|
||||
<div id="modals_portal" />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
|
@ -46,7 +46,13 @@ export default async (req, res) => {
|
||||
});
|
||||
|
||||
if (!filteredFiles?.length) {
|
||||
return res.status(400).send({ decorator: "SERVER_CREATE_LINK_DUPLICATE", error: true });
|
||||
return res.status(400).send({
|
||||
decorator: "SERVER_CREATE_LINK_DUPLICATE",
|
||||
data: {
|
||||
links: duplicateFiles.map((file) => file.cid),
|
||||
duplicate: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
files = [];
|
||||
@ -143,6 +149,6 @@ export default async (req, res) => {
|
||||
|
||||
return res.status(200).send({
|
||||
decorator,
|
||||
data: { added, skipped: files.length - added },
|
||||
data: { added, links: filteredFiles.map((file) => file.cid), skipped: files.length - added },
|
||||
});
|
||||
};
|
||||
|
@ -86,6 +86,11 @@ export default async (req, res) => {
|
||||
|
||||
return res.status(200).send({
|
||||
decorator,
|
||||
data: { added, skipped: files.length - added },
|
||||
data: {
|
||||
added,
|
||||
skipped: files.length - added,
|
||||
// TODO(amine): merge upload and create endpoints
|
||||
cid: added ? createdFiles[0]?.cid : duplicateFiles[0]?.cid,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -46,12 +46,12 @@ export default function SceneActivity({ page, viewer, external, onAction, ...pro
|
||||
const nbrOfCardsInRow = useNbrOfCardsPerRow(divRef);
|
||||
|
||||
const [globalCarouselState, setGlobalCarouselState] = React.useState({
|
||||
currentCarrousel: -1,
|
||||
currentCarousel: -1,
|
||||
currentObjects: [],
|
||||
});
|
||||
const handleFileClick = (fileIdx, groupFiles) =>
|
||||
setGlobalCarouselState({ currentCarrousel: fileIdx, currentObjects: groupFiles });
|
||||
console.log(globalCarouselState.currentCarrousel, globalCarouselState.currentObjects.length);
|
||||
setGlobalCarouselState({ currentCarousel: fileIdx, currentObjects: groupFiles });
|
||||
console.log(globalCarouselState.currentCarousel, globalCarouselState.currentObjects.length);
|
||||
|
||||
useIntersection({
|
||||
ref: divRef,
|
||||
@ -100,9 +100,9 @@ export default function SceneActivity({ page, viewer, external, onAction, ...pro
|
||||
carouselType="ACTIVITY"
|
||||
viewer={viewer}
|
||||
objects={globalCarouselState.currentObjects}
|
||||
index={globalCarouselState.currentCarrousel}
|
||||
index={globalCarouselState.currentCarousel}
|
||||
isMobile={props.isMobile}
|
||||
onChange={(idx) => setGlobalCarouselState((prev) => ({ ...prev, currentCarrousel: idx }))}
|
||||
onChange={(idx) => setGlobalCarouselState((prev) => ({ ...prev, currentCarousel: idx }))}
|
||||
isOwner={false}
|
||||
onAction={() => {}}
|
||||
/>
|
||||
@ -130,24 +130,29 @@ function useNbrOfCardsPerRow(ref) {
|
||||
const isMobile = window.matchMedia(`(max-width: ${Constants.sizes.mobile}px)`).matches;
|
||||
const responsiveKey = isMobile ? "mobile" : "desktop";
|
||||
|
||||
const { width: objectPreviewWidth, rowGap: objectPreviewGridRowGap } =
|
||||
Constants.grids.object[responsiveKey];
|
||||
const { width: objectPreviewWidth, rowGap: objectPreviewGridRowGap } = Constants.grids.object[
|
||||
responsiveKey
|
||||
];
|
||||
|
||||
NbrOfCardsInRow.object = calculateNbrOfCards({
|
||||
width: objectPreviewWidth,
|
||||
gap: objectPreviewGridRowGap,
|
||||
});
|
||||
|
||||
const { width: collectionPreviewWidth, rowGap: collectionPreviewGridRowGap } =
|
||||
Constants.grids.collection[responsiveKey];
|
||||
const {
|
||||
width: collectionPreviewWidth,
|
||||
rowGap: collectionPreviewGridRowGap,
|
||||
} = Constants.grids.collection[responsiveKey];
|
||||
|
||||
NbrOfCardsInRow.collection = calculateNbrOfCards({
|
||||
width: collectionPreviewWidth,
|
||||
gap: collectionPreviewGridRowGap,
|
||||
});
|
||||
|
||||
const { width: profilePreviewWidth, rowGap: profilePreviewGridRowGap } =
|
||||
Constants.grids.profile[responsiveKey];
|
||||
const {
|
||||
width: profilePreviewWidth,
|
||||
rowGap: profilePreviewGridRowGap,
|
||||
} = Constants.grids.profile[responsiveKey];
|
||||
NbrOfCardsInRow.profile = calculateNbrOfCards({
|
||||
width: profilePreviewWidth,
|
||||
gap: profilePreviewGridRowGap,
|
||||
|
@ -9,6 +9,7 @@ import * as Utilities from "~/common/utilities";
|
||||
import * as UserBehaviors from "~/common/user-behaviors";
|
||||
import * as Events from "~/common/custom-events";
|
||||
import * as Styles from "~/common/styles";
|
||||
import * as Upload from "~/components/core/Upload";
|
||||
|
||||
import { Link } from "~/components/core/Link";
|
||||
import { LoaderSpinner } from "~/components/system/components/Loaders";
|
||||
@ -356,18 +357,6 @@ class SlatePage extends React.Component {
|
||||
// detail: { index },
|
||||
// });
|
||||
|
||||
_handleAdd = async () => {
|
||||
if (!this.props.viewer) {
|
||||
Events.dispatchCustomEvent({ name: "slate-global-open-cta", detail: {} });
|
||||
return;
|
||||
}
|
||||
await this.props.onAction({
|
||||
type: "SIDEBAR",
|
||||
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
|
||||
data: this.props.data,
|
||||
});
|
||||
};
|
||||
|
||||
_handleShowSettings = () => {
|
||||
return this.props.onAction({
|
||||
type: "SIDEBAR",
|
||||
@ -406,13 +395,15 @@ class SlatePage extends React.Component {
|
||||
const isOwner = this.props.viewer ? ownerId === this.props.viewer.id : false;
|
||||
|
||||
let actions = isOwner ? (
|
||||
<span>
|
||||
<span css={Styles.HORIZONTAL_CONTAINER}>
|
||||
<SquareButtonGray onClick={this._handleDownload} style={{ marginRight: 16 }}>
|
||||
<SVG.Download height="16px" />
|
||||
</SquareButtonGray>
|
||||
<SquareButtonGray onClick={this._handleAdd} style={{ marginRight: 16 }}>
|
||||
<SVG.Plus height="16px" />
|
||||
</SquareButtonGray>
|
||||
<Upload.Trigger viewer={this.props.viewer} style={{ marginRight: 16 }}>
|
||||
<SquareButtonGray>
|
||||
<SVG.Plus height="16px" />
|
||||
</SquareButtonGray>
|
||||
</Upload.Trigger>
|
||||
<SquareButtonGray onClick={this._handleShowSettings}>
|
||||
<SVG.Settings height="16px" />
|
||||
</SquareButtonGray>
|
||||
|
Loading…
Reference in New Issue
Block a user