PromucFlow_constructor/app/client/src/components/ads/FilePicker.tsx
haojin111 9cfca0518f
feat: 9754 import work flow (#10453)
* updated import application modal design as v2

* updated import flow

* added title, description, uploadIcon on filepicker ads component for custom file picker

* adding modal of add credential for git import

* added "Git Import" modal

* added generating ssh key for importing flow

* fixed issue of merging

* chore: fix import

* chore: show old import modal based on feature flag

* seperated import api from connect

* added datasource list on reconnect credential modal

* chore: minor changes

* chore: move ssh keys to git sync reducer from applications reducer

* chore: minor fixes

* chore: fetch datasource config for import

* for pulling

* for review of displaying of datasource

* added reconnect datasources after git import

* fix: initialize datasource with default values

* fix: initialise redux for after updating datasource with default values

* fixed issue of git connection init when importing

* if there is a datasource config missing in import, reconnect modal should be opened

* updated logic for unconfigured datasources

* commented unnecessary code

* fixed issue of successful import

* updated import app error logic

* Add un-configured datasources to Import via file response

* Add test

* fix

* chore: refactors

* change per review

* fix: reset ssh keys / url

* Fix issue with newly created datasources not sent

* fix

* chore: minor updates

* chore: minor fix

* WIP

* added saas and rest api datasource form

* feat: fixes and updates for file import flow

* chore: close on upload

* Refactor logic ofr finding unconfigured datasources

* fix: minor fixes

* Fix issue with IsPartialImport

* fix

* Add PartialImport flag for ImportExport service

* refactoring of datasource editor form for both of importing app and editing app

* fixed collapse config

* Fix tests

* Handle redirection back to the /applications for oAuth type

* Show reconnect button on the datasources pages if the datasource configuration is skipped

* added analytic events for reconnecting datasource modal

* Fix the repo limit check for git import

* updated test of importing app from json as new work flow

* updated exported app json while testing automatically

* Add isImport flag for handling OAuth redirection in import flow

* WIP

* updated card UI for import from git title and message in import app modal

* chore: cleanup

* chore: lint

* fix: add is import query param to get token for oauth

* fix

* When the user imports the application there should not be any uncommitted changes displayed on the commit icon

* Add flag to identify OAuth redirection for git import

* Update the variable name

* refactoring reconnect datasource modal

* close git import modal when repo limit error responded

* fixed issue of restoring draft data of datasource form without save on reconnect datasource modal

* chore: update query

* updated query name of oauth redirection url

* Fix duplicate name issue in git import

* fixed rest api reconnect issue on reconnect modal

* init datasources and plugins after imported app, updated reconnect modal as new design

* added unconfigured datasource list logic when importing and updated rest api form delete button visible

* removed put default config of datasource and fixed issue on it

* Add logic to check isCOnfugred in datasource API

* Expose API to get un configured datasources for git import

* added fetch unconfigured datasource list api when redirecting form OAuth

* Remove sensitive fields from application json during export

* update put call response to check for datasourceConfig

* chore: use @appsmith for constants/messages

* chore: use download icon and Import for Importing application label

* chore: move import application text up a bit

* Fix bad merge

* chore: update skip to application tooltip text

* fixed tooltip content of skip to application CTA

* init values of datasource when importing

* updated ui of git import modal as figma design

* fixing padding issue of reconnect datasource modal

* fixed cursor issue on import app modal

* Fix issue with datasource config

* chore: make code compile

* chore: sort lines

* fixed save button issue of dbform on reconnecting modal

* fixed style of import application modal

* Fix iisue with wrong value updated to flag

* reverted from reconnection form style

* fix: update design as per slack discussions on 2022.02.23

* fix: move modal close button to the left

* Remove check for the flag and use the one from db

* Set siCOnfigured as true for mockdata sets

* updated creating datasource with isConfigured as false

* Fix NPE while importing

* fixed scrollbar issue and text alignment on reconnect datasource modal

* fixed style of form container in reconnect datasource and redirecting to app if all are configured

* remove unwanted fields from application json

* FIx NPE for file import

* fix: move close button up in import modal

* remove delete button on reconnect datasource modal

* Add isConfigured false while creating datasources

* fix: add a gap and update color

gap between git import dialog title and subtitle
update color of subtext to GREY_800

* fix: use git import feature flag

* fix: do not use older modal

* updated selecting logic of unconfigured datasource in reconnect modal

* cleanup: auto format

* cleanup: refactor react component

* cleanup: refactor some more

* cleanup: autoformat

* Fix reconnect flag for mockdatasource

* During git import set the isConfigured to false for datasources

* Remove decrypted field from the applicationJson file

* Remove decrypted field from the applicationJson file

* Add app slug to remote repo

* fixed cypress test related with git

* updated json while testing

* Changes per review

* Update the method name

* fixed cypress test related with git

* fixed migration cypress test

* set is configured field as true on tour app

* Fix issue with datasource creation for welcome tour

* fixed issue of replay_editor cypress test

Co-authored-by: Rishabh Saxena <rishabh@appsmith.com>
Co-authored-by: Anagh Hegde <anagh@appsmith.com>
Co-authored-by: Anubhav <anubhav@appsmith.com>
Co-authored-by: f0c1s <iamanubhavsaini+git@gmail.com>
2022-03-17 15:58:54 +05:30

494 lines
13 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import styled from "styled-components";
import Button, { Category, Size } from "./Button";
import axios from "axios";
import { ReactComponent as UploadIcon } from "../../assets/icons/ads/upload.svg";
import { ReactComponent as UploadSuccessIcon } from "../../assets/icons/ads/upload_success.svg";
import { DndProvider, useDrop, DropTargetMonitor } from "react-dnd";
import HTML5Backend, { NativeTypes } from "react-dnd-html5-backend";
import Text, { TextType } from "./Text";
import { Classes, Variant } from "./common";
import { Toaster } from "./Toast";
import {
createMessage,
ERROR_FILE_TOO_LARGE,
REMOVE_FILE_TOOL_TIP,
} from "@appsmith/constants/messages";
import TooltipComponent from "components/ads/Tooltip";
import { Position } from "@blueprintjs/core/lib/esm/common/position";
import Icon, { IconName, IconSize } from "./Icon";
import { error as logError } from "loglevel";
const CLOUDINARY_PRESETS_NAME = "";
const CLOUDINARY_CLOUD_NAME = "";
export const FileEndings = {
IMAGE: ".jpeg,.png,.svg",
JSON: ".json",
TEXT: ".txt",
ANY: "*",
};
export enum FileType {
IMAGE = "IMAGE",
JSON = "JSON",
TEXT = "TEXT",
ANY = "ANY",
}
export type FilePickerProps = {
onFileUploaded?: (fileUrl: string) => void;
onFileRemoved?: () => void;
fileUploader?: FileUploader;
url?: string;
logoUploadError?: string;
fileType: FileType;
delayedUpload?: boolean;
uploadIcon?: IconName;
title?: string;
description?: string;
containerClickable?: boolean; // when cotainer is clicked, it'll be work as button
iconFillColor?: string;
};
export const ContainerDiv = styled.div<{
isUploaded: boolean;
isActive: boolean;
canDrop: boolean;
fileType: FileType;
}>`
width: 320px;
height: 190px;
background-color: ${(props) => props.theme.colors.filePicker.bg};
position: relative;
#fileInput {
display: none;
}
.drag-drop-text {
margin: ${(props) => props.theme.spaces[6]}px 0
${(props) => props.theme.spaces[6]}px 0;
color: ${(props) => props.theme.colors.filePicker.color};
}
.upload-form-container {
width: 100%;
height: 100%;
display: grid;
place-items: center;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
background-origin: content-box;
padding: 8px;
}
.centered {
justify-content: center;
flex-direction: column;
align-items: center;
.success-container {
display: flex;
align-items: center;
.success-icon {
margin-right: ${(props) => props.theme.spaces[4]}px;
}
.success-text {
color: #03b365;
margin-right: ${(props) => props.theme.spaces[4]}px;
}
}
}
.file-description {
width: 95%;
margin: 0 auto;
margin-top: ${(props) =>
props.fileType === FileType.IMAGE ? "auto" : "0px"};
margin-bottom: ${(props) => props.theme.spaces[6] + 1}px;
display: none;
}
.file-spec {
margin-bottom: ${(props) => props.theme.spaces[3]}px;
span {
margin-right: ${(props) => props.theme.spaces[4]}px;
}
}
.progress-container {
width: 100%;
background: ${(props) => props.theme.colors.filePicker.progress};
transition: height 0.2s;
}
.progress-inner {
background-color: ${(props) => props.theme.colors.success.light};
transition: width 0.4s ease;
height: ${(props) => props.theme.spaces[1]}px;
border-radius: ${(props) => props.theme.spaces[1] - 1}px;
width: 0%;
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.remove-button {
display: none;
position: absolute;
bottom: 0;
right: 0;
background: linear-gradient(
180deg,
${(props) => props.theme.colors.filePicker.shadow.from},
${(props) => props.theme.colors.filePicker.shadow.to}
);
opacity: 0.6;
width: 100%;
a {
width: 110px;
margin: ${(props) => props.theme.spaces[13]}px
${(props) => props.theme.spaces[3]}px
${(props) => props.theme.spaces[3]}px auto;
.${Classes.ICON} {
margin-right: ${(props) => props.theme.spaces[2] - 1}px;
}
}
}
&:hover {
.remove-button {
display: ${(props) => (props.isUploaded ? "block" : "none")};
}
}
`;
const IconWrapper = styled.div`
width: ${(props) => props.theme.spaces[9]}px;
padding-left: ${(props) => props.theme.spaces[2]}px;
`;
export type SetProgress = (percentage: number) => void;
export type UploadCallback = (url: string) => void;
export type FileUploader = (
file: any,
setProgress: SetProgress,
onUpload: UploadCallback,
) => void;
export function CloudinaryUploader(
file: any,
setProgress: SetProgress,
onUpload: UploadCallback,
) {
const formData = new FormData();
formData.append("upload_preset", CLOUDINARY_PRESETS_NAME);
if (file) {
formData.append("file", file);
}
axios
.post(
`https://api.cloudinary.com/v1_1/${CLOUDINARY_CLOUD_NAME}/image/upload`,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress: function(progressEvent: ProgressEvent) {
const uploadPercentage = Math.round(
(progressEvent.loaded / progressEvent.total) * 100,
);
setProgress(uploadPercentage);
},
},
)
.then((data) => {
onUpload(data.data.url);
})
.catch((error) => {
logError("error in file uploading", error);
});
}
function FilePickerComponent(props: FilePickerProps) {
const { fileType, logoUploadError } = props;
const [fileInfo, setFileInfo] = useState<{ name: string; size: number }>({
name: "",
size: 0,
});
const [isUploaded, setIsUploaded] = useState<boolean>(false);
const [fileUrl, setFileUrl] = useState("");
const [{ canDrop, isOver }, drop] = useDrop({
accept: [NativeTypes.FILE],
drop(item, monitor) {
onDrop(monitor);
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const inputRef = useRef<HTMLInputElement>(null);
const bgRef = useRef<HTMLDivElement>(null);
const progressRef = useRef<HTMLDivElement>(null);
const fileDescRef = useRef<HTMLDivElement>(null);
const fileContainerRef = useRef<HTMLDivElement>(null);
function ButtonClick(event: React.MouseEvent<HTMLElement>) {
event.preventDefault();
if (inputRef.current) {
inputRef.current.click();
}
}
function onDrop(monitor: DropTargetMonitor) {
if (monitor) {
const files = monitor.getItem().files;
if (!files) {
return;
}
handleFileUpload(files);
}
}
function setProgress(uploadPercentage: number) {
if (progressRef.current) {
progressRef.current.style.width = `${uploadPercentage}%`;
}
if (uploadPercentage === 100) {
setIsUploaded(true);
if (fileDescRef.current && bgRef.current && fileType === FileType.IMAGE) {
fileDescRef.current.style.display = "none";
bgRef.current.style.opacity = "1";
}
}
}
function onUpload(url: string) {
props.onFileUploaded && props.onFileUploaded(url);
}
function handleFileUpload(files: FileList | null) {
if (fileType === FileType.IMAGE) {
handleImageFileUpload(files);
} else {
handleOtherFileUpload(files);
}
}
function handleOtherFileUpload(files: FileList | null) {
const file = files && files[0];
let fileSize = 0;
if (!file) {
return;
}
fileSize = Math.floor(file.size / 1024);
setFileInfo({ name: file.name, size: fileSize });
if (props.delayedUpload) {
setIsUploaded(true);
setProgress(100);
}
if (fileDescRef.current) {
fileDescRef.current.style.display = "flex";
}
if (fileContainerRef.current) {
fileContainerRef.current.style.display = "none";
}
props.fileUploader && props.fileUploader(file, setProgress, onUpload);
}
function handleImageFileUpload(files: FileList | null) {
const file = files && files[0];
let fileSize = 0;
if (!file) {
return;
}
fileSize = Math.floor(file.size / 1024);
setFileInfo({ name: file.name, size: fileSize });
if (fileSize < 250) {
if (bgRef.current) {
bgRef.current.style.backgroundImage = `url(${URL.createObjectURL(
file,
)})`;
bgRef.current.style.opacity = "0.5";
}
if (fileDescRef.current) {
fileDescRef.current.style.display = "block";
}
if (fileContainerRef.current) {
fileContainerRef.current.style.display = "none";
}
/* set form data and send api request */
props.fileUploader && props.fileUploader(file, setProgress, onUpload);
} else {
Toaster.show({
text: createMessage(ERROR_FILE_TOO_LARGE, "250 KB"),
variant: Variant.warning,
});
}
}
function removeFile() {
if (fileContainerRef.current) {
setFileUrl("");
if (fileDescRef.current) {
fileDescRef.current.style.display = "none";
}
fileContainerRef.current.style.display = "flex";
if (bgRef.current) {
bgRef.current.style.backgroundImage = "url('')";
}
setIsUploaded(false);
props.onFileRemoved && props.onFileRemoved();
}
}
const isActive = canDrop && isOver;
useEffect(() => {
if (props.url) {
const urlKeys = props.url.split("/");
if (urlKeys[urlKeys.length - 1] !== "null") {
setFileUrl(props.url);
} else {
setFileUrl("");
}
}
}, [props.url]);
// Following hook should be used only if file type is image.
useEffect(() => {
if (fileUrl && !isUploaded && fileType === FileType.IMAGE) {
setIsUploaded(true);
if (bgRef.current) {
bgRef.current.style.backgroundImage = `url(${fileUrl})`;
bgRef.current.style.opacity = "1";
}
if (fileDescRef.current) {
fileDescRef.current.style.display = "none";
}
if (fileContainerRef.current) {
fileContainerRef.current.style.display = "none";
}
}
}, [fileUrl, logoUploadError]);
// <UploadSuccessIcon />
const uploadFileForm = (
<div className="button-wrapper" ref={fileContainerRef}>
<UploadIcon />
<Text className="drag-drop-text" type={TextType.P2}>
Drag & Drop files to upload or
</Text>
<form>
<input
accept={FileEndings[fileType]}
id="fileInput"
multiple={false}
onChange={(el) => handleFileUpload(el.target.files)}
ref={inputRef}
type="file"
value={""}
/>
<Button
category={Category.tertiary}
onClick={(el) => ButtonClick(el)}
size={Size.medium}
text="Browse"
/>
</form>
</div>
);
const uploadStatus = (
<div className="file-spec">
<Text type={TextType.H6}>{fileInfo.name}</Text>
<Text type={TextType.H6}>{fileInfo.size}KB</Text>
</div>
);
const imageUploadComponent = (
<>
<div className="upload-form-container" ref={bgRef}>
{uploadFileForm}
<div className="file-description" id="fileDesc" ref={fileDescRef}>
{uploadStatus}
<div className="progress-container">
<div className="progress-inner" ref={progressRef} />
</div>
</div>
</div>
<div className="remove-button">
<Button
category={Category.tertiary}
icon="delete"
onClick={() => removeFile()}
size={Size.medium}
text="remove"
/>
</div>
</>
);
const uploadComponent = (
<div className="upload-form-container">
{uploadFileForm}
<div
className="file-description centered"
id="fileDesc"
ref={fileDescRef}
>
{uploadStatus}
<div className="success-container">
<UploadSuccessIcon className="success-icon" />
<Text className="success-text" type={TextType.H4}>
Successfully Uploaded!
</Text>
<TooltipComponent
content={REMOVE_FILE_TOOL_TIP()}
position={Position.TOP}
>
<IconWrapper className="icon-wrapper" onClick={() => removeFile()}>
<Icon name="close" size={IconSize.XL} />
</IconWrapper>
</TooltipComponent>
</div>
</div>
</div>
);
return (
<ContainerDiv
canDrop={canDrop}
fileType={fileType}
isActive={isActive}
isUploaded={isUploaded}
ref={drop}
>
{fileType === FileType.IMAGE ? imageUploadComponent : uploadComponent}
</ContainerDiv>
);
}
function FilePicker(props: FilePickerProps) {
return (
<DndProvider backend={HTML5Backend}>
<FilePickerComponent {...props} />
</DndProvider>
);
}
export default FilePicker;