* 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>
494 lines
13 KiB
TypeScript
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;
|