Feature/import applications (#4483)
* Added export option to app menu. TODO: call api to download app file * Added checkbox component and removed unused code * Added import app without filepicker. Opens modal * added ability to fetch the exported app * can download exported application as a json file * Updated the file picker component to accept other file formats * WIP import app * Added functionality to import application json file * minor fixes * Made the file type prop mandatory for file picker * added a test suite for export app * Test added to check if on import application click, it open a modal * added a dummy application file for cypress testing * Added end to end integration test suite to verify import app feature * added test to verify the export api status and download file. * added a linked btn to carry exporting. - according to latest BE changes * Removed old redux and saga mechanism for app export * updated cypress test to validate new flow * fixed minor linting errors * updated test case title * updated the test cases for import/export app feat * review changes * added prop to facilitate delayed upload * added new application file to fixtures. Minor fix to take care of loading state. * Removed export app modal. Added one click action, to download the file. * Updated File picker to work with all other files acc to the design. * Updated the import modal * updated the import application test * Added remove upload tooltip * updated the icons for import/export actions * removed unused logs * added hard coded feature flag to hide/show import export feature Co-authored-by: Pranav Kanade <pranav@appsmith.com>
This commit is contained in:
parent
57874b4126
commit
3a6da2d797
174
app/client/cypress/fixtures/application-file.json
Normal file
174
app/client/cypress/fixtures/application-file.json
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
{
|
||||
"exportedApplication": {
|
||||
"userPermissions": [
|
||||
"canComment:applications",
|
||||
"manage:applications",
|
||||
"read:applications",
|
||||
"publish:applications",
|
||||
"makePublic:applications"
|
||||
],
|
||||
"name": "testing app - pk",
|
||||
"isPublic": false,
|
||||
"appIsExample": false,
|
||||
"color": "#FE9F44",
|
||||
"icon": "heart",
|
||||
"new": true
|
||||
},
|
||||
"datasourceList": [],
|
||||
"pageList": [
|
||||
{
|
||||
"userPermissions": [
|
||||
"read:pages",
|
||||
"manage:pages"
|
||||
],
|
||||
"unpublishedPage": {
|
||||
"name": "Page1",
|
||||
"layouts": [
|
||||
{
|
||||
"id": "60a77186cdbfc9440388285c",
|
||||
"userPermissions": [],
|
||||
"dsl": {
|
||||
"widgetName": "MainContainer",
|
||||
"backgroundColor": "none",
|
||||
"rightColumn": 1118,
|
||||
"snapColumns": 16,
|
||||
"detachFromLayout": true,
|
||||
"widgetId": "0",
|
||||
"topRow": 0,
|
||||
"bottomRow": 1280,
|
||||
"containerStyle": "none",
|
||||
"snapRows": 33,
|
||||
"parentRowSpace": 1,
|
||||
"type": "CANVAS_WIDGET",
|
||||
"canExtend": true,
|
||||
"version": 18,
|
||||
"minHeight": 1292,
|
||||
"parentColumnSpace": 1,
|
||||
"dynamicTriggerPathList": [],
|
||||
"dynamicBindingPathList": [],
|
||||
"leftColumn": 0,
|
||||
"children": [
|
||||
{
|
||||
"widgetName": "Button1",
|
||||
"rightColumn": 4,
|
||||
"isDefaultClickDisabled": true,
|
||||
"widgetId": "g7jf8v3wkq",
|
||||
"buttonStyle": "PRIMARY_BUTTON",
|
||||
"topRow": 0,
|
||||
"bottomRow": 1,
|
||||
"parentRowSpace": 40,
|
||||
"isVisible": true,
|
||||
"type": "BUTTON_WIDGET",
|
||||
"version": 1,
|
||||
"parentId": "0",
|
||||
"isLoading": false,
|
||||
"parentColumnSpace": 67.375,
|
||||
"leftColumn": 2,
|
||||
"text": "Submit",
|
||||
"isDisabled": false
|
||||
},
|
||||
{
|
||||
"widgetName": "Chart1",
|
||||
"rightColumn": 8,
|
||||
"allowHorizontalScroll": false,
|
||||
"widgetId": "ow55pc4z0z",
|
||||
"topRow": 5,
|
||||
"bottomRow": 13,
|
||||
"parentRowSpace": 40,
|
||||
"isVisible": true,
|
||||
"type": "CHART_WIDGET",
|
||||
"version": 1,
|
||||
"parentId": "0",
|
||||
"isLoading": false,
|
||||
"chartData": {
|
||||
"pftw37090s": {
|
||||
"seriesName": "Sales",
|
||||
"data": [
|
||||
{
|
||||
"x": "Mon",
|
||||
"y": 10000
|
||||
},
|
||||
{
|
||||
"x": "Tue",
|
||||
"y": 12000
|
||||
},
|
||||
{
|
||||
"x": "Wed",
|
||||
"y": 32000
|
||||
},
|
||||
{
|
||||
"x": "Thu",
|
||||
"y": 28000
|
||||
},
|
||||
{
|
||||
"x": "Fri",
|
||||
"y": 14000
|
||||
},
|
||||
{
|
||||
"x": "Sat",
|
||||
"y": 19000
|
||||
},
|
||||
{
|
||||
"x": "Sun",
|
||||
"y": 36000
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"yAxisName": "Total Order Revenue $",
|
||||
"parentColumnSpace": 67.375,
|
||||
"chartName": "Last week's revenue",
|
||||
"leftColumn": 2,
|
||||
"xAxisName": "Last Week",
|
||||
"chartType": "LINE_CHART"
|
||||
}
|
||||
]
|
||||
},
|
||||
"layoutOnLoadActions": [],
|
||||
"new": false
|
||||
}
|
||||
],
|
||||
"userPermissions": []
|
||||
},
|
||||
"publishedPage": {
|
||||
"name": "Page1",
|
||||
"layouts": [
|
||||
{
|
||||
"id": "60a77186cdbfc9440388285c",
|
||||
"userPermissions": [],
|
||||
"dsl": {
|
||||
"widgetName": "MainContainer",
|
||||
"backgroundColor": "none",
|
||||
"rightColumn": 1224,
|
||||
"snapColumns": 16,
|
||||
"detachFromLayout": true,
|
||||
"widgetId": "0",
|
||||
"topRow": 0,
|
||||
"bottomRow": 1254,
|
||||
"containerStyle": "none",
|
||||
"snapRows": 33,
|
||||
"parentRowSpace": 1,
|
||||
"type": "CANVAS_WIDGET",
|
||||
"canExtend": true,
|
||||
"version": 4,
|
||||
"minHeight": 1292,
|
||||
"parentColumnSpace": 1,
|
||||
"dynamicBindingPathList": [],
|
||||
"leftColumn": 0,
|
||||
"children": []
|
||||
},
|
||||
"new": false
|
||||
}
|
||||
],
|
||||
"userPermissions": []
|
||||
},
|
||||
"new": true
|
||||
}
|
||||
],
|
||||
"publishedDefaultPageName": "Page1",
|
||||
"unpublishedDefaultPageName": "Page1",
|
||||
"actionList": [],
|
||||
"decryptedFields": {},
|
||||
"publishedLayoutmongoEscapedWidgets": {},
|
||||
"unpublishedLayoutmongoEscapedWidgets": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
const dsl = require("../../../../fixtures/displayWidgetDsl.json");
|
||||
const homePage = require("../../../../locators/HomePage.json");
|
||||
const commonlocators = require("../../../../locators/commonlocators.json");
|
||||
|
||||
describe("Export application as a JSON file", function() {
|
||||
before(() => {
|
||||
cy.addDsl(dsl);
|
||||
});
|
||||
|
||||
it("Check if exporting app flow works as expected", function() {
|
||||
cy.get(commonlocators.homeIcon).click({ force: true });
|
||||
const appname = localStorage.getItem("AppName");
|
||||
cy.get(homePage.searchInput).type(appname);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(2000);
|
||||
|
||||
cy.get(homePage.applicationCard)
|
||||
.first()
|
||||
.trigger("mouseover");
|
||||
cy.get(homePage.appMoreIcon)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
cy.get(homePage.exportAppFromMenu).click({ force: true });
|
||||
cy.get(homePage.toastMessage).should("contain", "Successfully exported");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
const homePage = require("../../../../locators/HomePage.json");
|
||||
|
||||
describe("Organization Import Application", function() {
|
||||
let orgid;
|
||||
let newOrganizationName;
|
||||
const fixtureDummyAppPath = "application-file.json";
|
||||
it("Can Import Application", function() {
|
||||
cy.NavigateToHome();
|
||||
cy.generateUUID().then((uid) => {
|
||||
orgid = uid;
|
||||
localStorage.setItem("OrgName", orgid);
|
||||
cy.createOrg();
|
||||
cy.wait("@createOrg").then((createOrgInterception) => {
|
||||
newOrganizationName = createOrgInterception.response.body.data.name;
|
||||
cy.renameOrg(newOrganizationName, orgid);
|
||||
cy.get(homePage.orgImportAppOption).click({ force: true });
|
||||
|
||||
cy.get(homePage.orgImportAppModal).should("be.visible");
|
||||
cy.xpath(homePage.uploadLogo).attachFile(fixtureDummyAppPath);
|
||||
|
||||
cy.get(homePage.orgImportAppButton).click({ force: true });
|
||||
cy.wait("@importNewApplication").then((interception) => {
|
||||
let appId = interception.response.body.data.id;
|
||||
let defaultPage = interception.response.body.data.pages.find(
|
||||
(eachPage) => !!eachPage.isDefault,
|
||||
);
|
||||
cy.get(homePage.toastMessage).should(
|
||||
"contain",
|
||||
"Application imported successfully",
|
||||
);
|
||||
cy.url().should(
|
||||
"include",
|
||||
`/applications/${appId}/pages/${defaultPage.id}/edit`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"appMoreIcon": ".bp3-popover-wrapper.more .bp3-popover-target",
|
||||
"duplicateApp": "[data-cy=t--duplicate]",
|
||||
"forkAppFromMenu": "[data-cy=t--fork-app]",
|
||||
"exportAppFromMenu": "[data-cy=t--export-app]",
|
||||
"forkAppOrgList": ".radio-group",
|
||||
"forkAppOrgButton": "[data-cy=t--fork-app-to-org-button]",
|
||||
"selectAction": "#Base",
|
||||
|
|
@ -59,6 +60,9 @@
|
|||
"applicationColorSelector": ".t--color-not-selected",
|
||||
"applicationBackgroundColor": ".t--application-card-background",
|
||||
"orgSettingOption": "[data-cy=t--org-setting]",
|
||||
"orgImportAppOption": "[data-cy=t--org-import-app]",
|
||||
"orgImportAppModal": ".t--import-application-modal",
|
||||
"orgImportAppButton": "[data-cy=t--org-import-app-button]",
|
||||
"orgNameInput": "[data-cy=t--org-name-input]",
|
||||
"renameOrgInput": "[data-cy=t--org-rename-input]",
|
||||
"orgEmailInput": "[data-cy=t--org-email-input]",
|
||||
|
|
|
|||
|
|
@ -2212,6 +2212,8 @@ Cypress.Commands.add("startServerAndRoutes", () => {
|
|||
cy.route("PUT", "/api/v1/actions/move").as("moveAction");
|
||||
|
||||
cy.route("POST", "/api/v1/organizations").as("createOrg");
|
||||
cy.route("POST", "api/v1/applications/import/*").as("importNewApplication");
|
||||
cy.route("GET", "api/v1/applications/export/*").as("exportApplication");
|
||||
cy.route("GET", "/api/v1/organizations/roles?organizationId=*").as(
|
||||
"getRoles",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import { APP_MODE } from "../reducers/entityReducers/appReducer";
|
||||
import { UpdateApplicationPayload } from "api/ApplicationApi";
|
||||
import {
|
||||
UpdateApplicationPayload,
|
||||
ImportApplicationRequest,
|
||||
} from "api/ApplicationApi";
|
||||
|
||||
export const setDefaultApplicationPageSuccess = (
|
||||
pageId: string,
|
||||
|
|
@ -77,6 +80,13 @@ export const duplicateApplication = (applicationId: string) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const importApplication = (appDetails: ImportApplicationRequest) => {
|
||||
return {
|
||||
type: ReduxActionTypes.IMPORT_APPLICATION_INIT,
|
||||
payload: appDetails,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAllApplications = () => {
|
||||
return {
|
||||
type: ReduxActionTypes.GET_ALL_APPLICATION_INIT,
|
||||
|
|
|
|||
|
|
@ -119,6 +119,13 @@ export interface FetchUsersApplicationsOrgsResponse extends ApiResponse {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ImportApplicationRequest {
|
||||
orgId: string;
|
||||
applicationFile?: File;
|
||||
progress?: (progressEvent: ProgressEvent) => void;
|
||||
onSuccessCallback?: () => void;
|
||||
}
|
||||
|
||||
class ApplicationApi extends Api {
|
||||
static baseURL = "v1/applications/";
|
||||
static publishURLPath = (applicationId: string) => `publish/${applicationId}`;
|
||||
|
|
@ -212,6 +219,21 @@ class ApplicationApi extends Api {
|
|||
request.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
static importApplicationToOrg(
|
||||
request: ImportApplicationRequest,
|
||||
): AxiosPromise<ApiResponse> {
|
||||
const formData = new FormData();
|
||||
if (request.applicationFile) {
|
||||
formData.append("file", request.applicationFile);
|
||||
}
|
||||
return Api.post("v1/applications/import/" + request.orgId, formData, null, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
onUploadProgress: request.progress,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationApi;
|
||||
|
|
|
|||
3
app/client/src/assets/icons/ads/download.svg
Normal file
3
app/client/src/assets/icons/ads/download.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4547 16.4284H13.117H6.88326H4.54559C2.8243 16.4284 1.42871 14.8933 1.42871 12.9999C1.42871 11.3987 2.43157 10.0641 3.7804 9.68786C3.93001 6.28329 6.47884 3.57129 9.61053 3.57129C12.7072 3.57129 15.2349 6.22243 15.4352 9.57386C17.183 9.56015 18.5716 11.1167 18.5716 12.9999C18.5716 14.8933 17.176 16.4284 15.4547 16.4284ZM12.7266 11.4286L9.99929 14.8572L7.27202 11.4286L8.83045 11.4286L8.83045 8.00004L11.1681 8.00003V11.4286L12.7266 11.4286Z" fill="#939090"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 616 B |
3
app/client/src/assets/icons/ads/upload_success.svg
Normal file
3
app/client/src/assets/icons/ads/upload_success.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.6364 16H16.3636L13.6364 16V11.7333H15.8182L12.0001 7.46667L8.18188 11.7333H10.3636V16L7.63636 16H4.36364C1.95382 16 0 14.0896 0 11.7333C0 9.7408 1.404 8.08 3.29236 7.61173C3.50182 3.37493 7.07018 0 11.4545 0C15.7898 0 19.3287 3.2992 19.6091 7.46987C22.056 7.4528 24 9.38987 24 11.7333C24 14.0896 22.0462 16 19.6364 16Z" fill="#03B365"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
|
|
@ -3,28 +3,52 @@ 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 } from "constants/messages";
|
||||
|
||||
import {
|
||||
createMessage,
|
||||
ERROR_FILE_TOO_LARGE,
|
||||
REMOVE_FILE_TOOL_TIP,
|
||||
} from "constants/messages";
|
||||
import TooltipComponent from "components/ads/Tooltip";
|
||||
import { Position } from "@blueprintjs/core/lib/esm/common/position";
|
||||
import Icon, { IconSize } from "./Icon";
|
||||
const CLOUDINARY_PRESETS_NAME = "";
|
||||
const CLOUDINARY_CLOUD_NAME = "";
|
||||
|
||||
const FileEndings = {
|
||||
IMAGE: ".jpeg,.png,.svg",
|
||||
JSON: ".json",
|
||||
TEXT: ".txt",
|
||||
ANY: "*",
|
||||
};
|
||||
|
||||
export enum FileType {
|
||||
IMAGE = "IMAGE",
|
||||
JSON = "JSON",
|
||||
TEXT = "TEXT",
|
||||
ANY = "ANY",
|
||||
}
|
||||
|
||||
type FilePickerProps = {
|
||||
onFileUploaded?: (fileUrl: string) => void;
|
||||
onFileRemoved?: () => void;
|
||||
fileUploader?: FileUploader;
|
||||
url?: string;
|
||||
logoUploadError?: string;
|
||||
fileType: FileType;
|
||||
delayedUpload?: boolean;
|
||||
};
|
||||
|
||||
const ContainerDiv = styled.div<{
|
||||
isUploaded: boolean;
|
||||
isActive: boolean;
|
||||
canDrop: boolean;
|
||||
fileType: FileType;
|
||||
}>`
|
||||
width: 320px;
|
||||
height: 190px;
|
||||
|
|
@ -41,7 +65,7 @@ const ContainerDiv = styled.div<{
|
|||
color: ${(props) => props.theme.colors.filePicker.color};
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
.upload-form-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
|
|
@ -51,15 +75,36 @@ const ContainerDiv = styled.div<{
|
|||
background-size: contain;
|
||||
}
|
||||
|
||||
.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-top: auto;
|
||||
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[2]}px;
|
||||
margin-bottom: ${(props) => props.theme.spaces[3]}px;
|
||||
span {
|
||||
margin-right: ${(props) => props.theme.spaces[4]}px;
|
||||
}
|
||||
|
|
@ -116,6 +161,11 @@ const ContainerDiv = styled.div<{
|
|||
}
|
||||
`;
|
||||
|
||||
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 = (
|
||||
|
|
@ -159,7 +209,7 @@ export function CloudinaryUploader(
|
|||
}
|
||||
|
||||
function FilePickerComponent(props: FilePickerProps) {
|
||||
const { logoUploadError } = props;
|
||||
const { fileType, logoUploadError } = props;
|
||||
const [fileInfo, setFileInfo] = useState<{ name: string; size: number }>({
|
||||
name: "",
|
||||
size: 0,
|
||||
|
|
@ -207,7 +257,7 @@ function FilePickerComponent(props: FilePickerProps) {
|
|||
}
|
||||
if (uploadPercentage === 100) {
|
||||
setIsUploaded(true);
|
||||
if (fileDescRef.current && bgRef.current) {
|
||||
if (fileDescRef.current && bgRef.current && fileType === FileType.IMAGE) {
|
||||
fileDescRef.current.style.display = "none";
|
||||
bgRef.current.style.opacity = "1";
|
||||
}
|
||||
|
|
@ -219,6 +269,35 @@ function FilePickerComponent(props: FilePickerProps) {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -253,10 +332,15 @@ function FilePickerComponent(props: FilePickerProps) {
|
|||
}
|
||||
|
||||
function removeFile() {
|
||||
if (fileContainerRef.current && bgRef.current) {
|
||||
if (fileContainerRef.current) {
|
||||
setFileUrl("");
|
||||
if (fileDescRef.current) {
|
||||
fileDescRef.current.style.display = "none";
|
||||
}
|
||||
fileContainerRef.current.style.display = "flex";
|
||||
bgRef.current.style.backgroundImage = "url('')";
|
||||
if (bgRef.current) {
|
||||
bgRef.current.style.backgroundImage = "url('')";
|
||||
}
|
||||
setIsUploaded(false);
|
||||
props.onFileRemoved && props.onFileRemoved();
|
||||
}
|
||||
|
|
@ -275,8 +359,9 @@ function FilePickerComponent(props: FilePickerProps) {
|
|||
}
|
||||
}, [props.url]);
|
||||
|
||||
// Following hook should be used only if file type is image.
|
||||
useEffect(() => {
|
||||
if (fileUrl && !isUploaded) {
|
||||
if (fileUrl && !isUploaded && fileType === FileType.IMAGE) {
|
||||
setIsUploaded(true);
|
||||
if (bgRef.current) {
|
||||
bgRef.current.style.backgroundImage = `url(${fileUrl})`;
|
||||
|
|
@ -291,42 +376,47 @@ function FilePickerComponent(props: FilePickerProps) {
|
|||
}
|
||||
}, [fileUrl, logoUploadError]);
|
||||
|
||||
return (
|
||||
<ContainerDiv
|
||||
canDrop={canDrop}
|
||||
isActive={isActive}
|
||||
isUploaded={isUploaded}
|
||||
ref={drop}
|
||||
>
|
||||
<div className="bg-image" ref={bgRef}>
|
||||
<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=".jpeg,.png,.svg"
|
||||
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>
|
||||
// <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}>
|
||||
<div className="file-spec">
|
||||
<Text type={TextType.H6}>{fileInfo.name}</Text>
|
||||
<Text type={TextType.H6}>{fileInfo.size}KB</Text>
|
||||
</div>
|
||||
{uploadStatus}
|
||||
<div className="progress-container">
|
||||
<div className="progress-inner" ref={progressRef} />
|
||||
</div>
|
||||
|
|
@ -341,6 +431,45 @@ function FilePickerComponent(props: FilePickerProps) {
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ import { ReactComponent as Pin3 } from "assets/icons/comments/pin_3.svg";
|
|||
import { ReactComponent as Unpin } from "assets/icons/comments/unpin.svg";
|
||||
import { ReactComponent as Reaction } from "assets/icons/comments/reaction.svg";
|
||||
import { ReactComponent as Reaction2 } from "assets/icons/comments/reaction-2.svg";
|
||||
import { ReactComponent as Upload } from "assets/icons/ads/upload.svg";
|
||||
import { ReactComponent as Download } from "assets/icons/ads/download.svg";
|
||||
import styled from "styled-components";
|
||||
import { CommonComponentProps, Classes } from "./common";
|
||||
import { noop } from "lodash";
|
||||
|
|
@ -117,6 +119,8 @@ export const sizeHandler = (size?: IconSize) => {
|
|||
};
|
||||
|
||||
export const IconCollection = [
|
||||
"upload",
|
||||
"download",
|
||||
"book",
|
||||
"bug",
|
||||
"cancel",
|
||||
|
|
@ -477,6 +481,14 @@ const Icon = forwardRef(
|
|||
returnIcon = <Reaction2 />;
|
||||
break;
|
||||
|
||||
case "upload":
|
||||
returnIcon = <Upload />;
|
||||
break;
|
||||
|
||||
case "download":
|
||||
returnIcon = <Download />;
|
||||
break;
|
||||
|
||||
default:
|
||||
returnIcon = null;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import FilePicker, { CloudinaryUploader } from "../ads/FilePicker";
|
||||
import FilePicker, { CloudinaryUploader, FileType } from "../ads/FilePicker";
|
||||
|
||||
export default {
|
||||
title: "FilePicker",
|
||||
|
|
@ -12,6 +12,15 @@ function ShowUploadedFile(data: any) {
|
|||
|
||||
export const withDynamicProps = () => (
|
||||
<FilePicker
|
||||
fileType={FileType.IMAGE}
|
||||
fileUploader={CloudinaryUploader}
|
||||
onFileUploaded={(data) => ShowUploadedFile(data)}
|
||||
/>
|
||||
);
|
||||
|
||||
export const withJsonInputType = () => (
|
||||
<FilePicker
|
||||
fileType={FileType.JSON}
|
||||
fileUploader={CloudinaryUploader}
|
||||
onFileUploaded={(data) => ShowUploadedFile(data)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -418,6 +418,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
|||
CURRENT_APPLICATION_LAYOUT_UPDATE: "CURRENT_APPLICATION_LAYOUT_UPDATE",
|
||||
FORK_APPLICATION_INIT: "FORK_APPLICATION_INIT",
|
||||
FORK_APPLICATION_SUCCESS: "FORK_APPLICATION_SUCCESS",
|
||||
IMPORT_APPLICATION_INIT: "IMPORT_APPLICATION_INIT",
|
||||
IMPORT_APPLICATION_SUCCESS: "IMPORT_APPLICATION_SUCCESS",
|
||||
SET_WIDGET_LOADING: "SET_WIDGET_LOADING",
|
||||
SET_GLOBAL_SEARCH_QUERY: "SET_GLOBAL_SEARCH_QUERY",
|
||||
TOGGLE_SHOW_GLOBAL_SEARCH_MODAL: "TOGGLE_SHOW_GLOBAL_SEARCH_MODAL",
|
||||
|
|
@ -525,6 +527,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
|
|||
SAVE_ACTION_NAME_ERROR: "SAVE_ACTION_NAME_ERROR",
|
||||
FETCH_USER_APPLICATIONS_ORGS_ERROR: "FETCH_USER_APPLICATIONS_ORGS_ERROR",
|
||||
FORK_APPLICATION_ERROR: "FORK_APPLICATION_ERROR",
|
||||
IMPORT_APPLICATION_ERROR: "IMPORT_APPLICATION_ERROR",
|
||||
FETCH_ALL_USERS_ERROR: "FETCH_ALL_USERS_ERROR",
|
||||
FETCH_ALL_ROLES_ERROR: "FETCH_ALL_ROLES_ERROR",
|
||||
UPDATE_USER_DETAILS_ERROR: "UPDATE_USER_DETAILS_ERROR",
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ export const GOOGLE_RECAPTCHA_DOMAIN_ERROR = () =>
|
|||
export const SERVER_API_TIMEOUT_ERROR = () =>
|
||||
`Appsmith server is taking too long to respond. Please try again after some time`;
|
||||
export const DEFAULT_ERROR_MESSAGE = () => `There was an unexpected error`;
|
||||
|
||||
export const REMOVE_FILE_TOOL_TIP = () => "Remove Upload";
|
||||
export const ERROR_FILE_TOO_LARGE = (fileSize: string) =>
|
||||
`File size should be less than ${fileSize}!`;
|
||||
export const ERROR_DATEPICKER_MIN_DATE = () =>
|
||||
|
|
@ -329,3 +329,6 @@ export const OPEN_THE_DEBUGGER = () => " to open the debugger";
|
|||
export const NO_LOGS = () => "No logs to show";
|
||||
|
||||
export const TROUBLESHOOT_ISSUE = () => "Troubleshoot issue";
|
||||
|
||||
// Import/Export Application features
|
||||
export const IMPORT_APPLICATION_MODAL_TITLE = () => "Import Application";
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ import { Classes as CsClasses } from "components/ads/common";
|
|||
import TooltipComponent from "components/ads/Tooltip";
|
||||
import { isEllipsisActive } from "utils/helpers";
|
||||
import ForkApplicationModal from "./ForkApplicationModal";
|
||||
import { Toaster } from "components/ads/Toast";
|
||||
import { Variant } from "components/ads/common";
|
||||
|
||||
type NameWrapperProps = {
|
||||
hasReadPermission: boolean;
|
||||
|
|
@ -220,6 +222,7 @@ type ApplicationCardProps = {
|
|||
share?: (applicationId: string) => void;
|
||||
delete?: (applicationId: string) => void;
|
||||
update?: (id: string, data: UpdateApplicationPayload) => void;
|
||||
enableImportExport?: boolean;
|
||||
};
|
||||
|
||||
const EditButton = styled(Button)`
|
||||
|
|
@ -297,6 +300,14 @@ export function ApplicationCard(props: ApplicationCardProps) {
|
|||
cypressSelector: "t--fork-app",
|
||||
});
|
||||
}
|
||||
if (!!props.enableImportExport) {
|
||||
moreActionItems.push({
|
||||
onSelect: exportApplicationAsJSONFile,
|
||||
text: "Export",
|
||||
icon: "download",
|
||||
cypressSelector: "t--export-app",
|
||||
});
|
||||
}
|
||||
setMoreActionItems(moreActionItems);
|
||||
addDeleteOption();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
@ -331,6 +342,25 @@ export function ApplicationCard(props: ApplicationCardProps) {
|
|||
const shareApp = () => {
|
||||
props.share && props.share(props.application.id);
|
||||
};
|
||||
const exportApplicationAsJSONFile = () => {
|
||||
// export api response comes with content-disposition header.
|
||||
// there is no straightforward way to handle it with axios/fetch
|
||||
const id = `t--export-app-link`;
|
||||
const existingLink = document.getElementById(id);
|
||||
existingLink && existingLink.remove();
|
||||
const link = document.createElement("a");
|
||||
link.href = `/api/v1/applications/export/${props.application.id}`;
|
||||
link.target = "_blank";
|
||||
link.id = id;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
setIsMenuOpen(false);
|
||||
Toaster.show({
|
||||
text: `Successfully exported ${props.application.name}`,
|
||||
variant: Variant.success,
|
||||
});
|
||||
link.remove();
|
||||
};
|
||||
const forkApplicationInitiate = () => {
|
||||
// open fork application modal
|
||||
// on click on an organisation, create app and take to app
|
||||
|
|
|
|||
|
|
@ -26,9 +26,10 @@ const StyledRadioComponent = styled(RadioComponent)`
|
|||
}
|
||||
`;
|
||||
|
||||
const ForkButton = styled(Button)`
|
||||
const ForkButton = styled(Button)<{ disabled?: boolean }>`
|
||||
height: 38px;
|
||||
width: 203px;
|
||||
pointer-events: ${(props) => (!!props.disabled ? "none" : "auto")};
|
||||
`;
|
||||
|
||||
const OrganizationList = styled.div`
|
||||
|
|
|
|||
117
app/client/src/pages/Applications/ImportApplicationModal.tsx
Normal file
117
app/client/src/pages/Applications/ImportApplicationModal.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import Button, { Size } from "components/ads/Button";
|
||||
import { StyledDialog } from "./ForkModalStyles";
|
||||
import { useSelector } from "store";
|
||||
import { AppState } from "reducers";
|
||||
import FilePicker, { SetProgress, FileType } from "components/ads/FilePicker";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { importApplication } from "actions/applicationActions";
|
||||
import { Toaster } from "components/ads/Toast";
|
||||
import { Variant } from "components/ads/common";
|
||||
import { IMPORT_APPLICATION_MODAL_TITLE } from "constants/messages";
|
||||
|
||||
const ImportButton = styled(Button)<{ disabled?: boolean }>`
|
||||
height: 30px;
|
||||
width: 81px;
|
||||
pointer-events: ${(props) => (!!props.disabled ? "none" : "auto")};
|
||||
`;
|
||||
|
||||
const ButtonWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const FilePickerWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
type ImportApplicationModalProps = {
|
||||
// import?: (file: any) => void;
|
||||
organizationId?: string;
|
||||
isModalOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
function ImportApplicationModal(props: ImportApplicationModalProps) {
|
||||
const { isModalOpen, onClose, organizationId } = props;
|
||||
const [appFileToBeUploaded, setAppFileToBeUploaded] = useState<{
|
||||
file: File;
|
||||
setProgress: SetProgress;
|
||||
} | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const importingApplication = useSelector(
|
||||
(state: AppState) => state.ui.applications.importingApplication,
|
||||
);
|
||||
|
||||
const FileUploader = useCallback(
|
||||
async (file: File, setProgress: SetProgress) => {
|
||||
if (!!file) {
|
||||
setAppFileToBeUploaded({
|
||||
file,
|
||||
setProgress,
|
||||
});
|
||||
} else {
|
||||
setAppFileToBeUploaded(null);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onImportApplication = useCallback(() => {
|
||||
if (!appFileToBeUploaded) {
|
||||
Toaster.show({
|
||||
text: "Please choose a valid application file!",
|
||||
variant: Variant.danger,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { file } = appFileToBeUploaded || {};
|
||||
|
||||
dispatch(
|
||||
importApplication({
|
||||
orgId: organizationId as string,
|
||||
applicationFile: file,
|
||||
}),
|
||||
);
|
||||
}, [appFileToBeUploaded, organizationId]);
|
||||
|
||||
const onRemoveFile = useCallback(() => setAppFileToBeUploaded(null), []);
|
||||
|
||||
return (
|
||||
<StyledDialog
|
||||
canOutsideClickClose
|
||||
className={"t--import-application-modal"}
|
||||
isOpen={isModalOpen}
|
||||
maxHeight={"540px"}
|
||||
setModalClose={onClose}
|
||||
title={IMPORT_APPLICATION_MODAL_TITLE()}
|
||||
>
|
||||
<FilePickerWrapper>
|
||||
<FilePicker
|
||||
delayedUpload
|
||||
fileType={FileType.JSON}
|
||||
fileUploader={FileUploader}
|
||||
onFileRemoved={onRemoveFile}
|
||||
/>
|
||||
</FilePickerWrapper>
|
||||
<ButtonWrapper>
|
||||
<ImportButton
|
||||
// category={ButtonCategory.tertiary}
|
||||
cypressSelector={"t--org-import-app-button"}
|
||||
disabled={!appFileToBeUploaded}
|
||||
isLoading={importingApplication}
|
||||
onClick={onImportApplication}
|
||||
size={Size.large}
|
||||
text={"IMPORT"}
|
||||
/>
|
||||
</ButtonWrapper>
|
||||
</StyledDialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportApplicationModal;
|
||||
|
|
@ -78,6 +78,7 @@ import WelcomeHelper from "components/editorComponents/Onboarding/WelcomeHelper"
|
|||
import { useIntiateOnboarding } from "components/editorComponents/Onboarding/utils";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { createOrganizationSubmitHandler } from "../organization/helpers";
|
||||
import ImportApplicationModal from "./ImportApplicationModal";
|
||||
|
||||
const OrgDropDown = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -505,6 +506,7 @@ const NoSearchResultImg = styled.img`
|
|||
`;
|
||||
|
||||
function ApplicationsSection(props: any) {
|
||||
const enableImportExport = true;
|
||||
const dispatch = useDispatch();
|
||||
const theme = useContext(ThemeContext);
|
||||
const isSavingOrgInfo = useSelector(getIsSavingOrgInfo);
|
||||
|
|
@ -534,6 +536,10 @@ function ApplicationsSection(props: any) {
|
|||
};
|
||||
|
||||
const [selectedOrgId, setSelectedOrgId] = useState<string | undefined>();
|
||||
const [
|
||||
selectedOrgIdForImportApplication,
|
||||
setSelectedOrgIdForImportApplication,
|
||||
] = useState<string | undefined>();
|
||||
const Form: any = OrgInviteUsersForm;
|
||||
|
||||
const leaveOrg = (orgId: string) => {
|
||||
|
|
@ -668,6 +674,18 @@ function ApplicationsSection(props: any) {
|
|||
}
|
||||
text="Organization Settings"
|
||||
/>
|
||||
{enableImportExport && (
|
||||
<MenuItem
|
||||
cypressSelector="t--org-import-app"
|
||||
icon="upload"
|
||||
onSelect={() =>
|
||||
setSelectedOrgIdForImportApplication(
|
||||
organization.id,
|
||||
)
|
||||
}
|
||||
text="Import Application"
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
icon="share"
|
||||
onSelect={() => setSelectedOrgId(organization.id)}
|
||||
|
|
@ -691,7 +709,15 @@ function ApplicationsSection(props: any) {
|
|||
/>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{selectedOrgIdForImportApplication && (
|
||||
<ImportApplicationModal
|
||||
isModalOpen={
|
||||
selectedOrgIdForImportApplication === organization.id
|
||||
}
|
||||
onClose={() => setSelectedOrgIdForImportApplication("")}
|
||||
organizationId={selectedOrgIdForImportApplication}
|
||||
/>
|
||||
)}
|
||||
{hasManageOrgPermissions && (
|
||||
<StyledDialog
|
||||
canEscapeKeyClose={false}
|
||||
|
|
@ -795,6 +821,7 @@ function ApplicationsSection(props: any) {
|
|||
application={application}
|
||||
delete={deleteApplication}
|
||||
duplicate={duplicateApplicationDispatch}
|
||||
enableImportExport={enableImportExport}
|
||||
key={application.id}
|
||||
update={updateApplicationDispatch}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { getOrgLoadingStates } from "selectors/organizationSelectors";
|
|||
import FilePicker, {
|
||||
SetProgress,
|
||||
UploadCallback,
|
||||
FileType,
|
||||
} from "components/ads/FilePicker";
|
||||
import { getIsFetchingApplications } from "selectors/applicationSelectors";
|
||||
|
||||
|
|
@ -171,6 +172,7 @@ export function GeneralSettings() {
|
|||
{isFetchingOrg && <FilePickerLoader className={Classes.SKELETON} />}
|
||||
{!isFetchingOrg && (
|
||||
<FilePicker
|
||||
fileType={FileType.IMAGE}
|
||||
fileUploader={FileUploader}
|
||||
logoUploadError={logoUploadError.message}
|
||||
onFileRemoved={DeleteLogo}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ const initialState: ApplicationsReduxState = {
|
|||
duplicatingApplication: false,
|
||||
userOrgs: [],
|
||||
isSavingOrgInfo: false,
|
||||
importingApplication: false,
|
||||
importedApplication: null,
|
||||
showAppInviteUsersDialog: false,
|
||||
};
|
||||
|
||||
|
|
@ -214,6 +216,28 @@ const applicationsReducer = createReducer(initialState, {
|
|||
forkingApplication: false,
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.IMPORT_APPLICATION_INIT]: (
|
||||
state: ApplicationsReduxState,
|
||||
) => ({ ...state, importingApplication: true }),
|
||||
[ReduxActionTypes.IMPORT_APPLICATION_SUCCESS]: (
|
||||
state: ApplicationsReduxState,
|
||||
action: ReduxAction<{ importedApplication: any }>,
|
||||
) => {
|
||||
const { importedApplication } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
importingApplication: false,
|
||||
importedApplication,
|
||||
};
|
||||
},
|
||||
[ReduxActionErrorTypes.IMPORT_APPLICATION_ERROR]: (
|
||||
state: ApplicationsReduxState,
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
importingApplication: false,
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.SAVING_ORG_INFO]: (state: ApplicationsReduxState) => {
|
||||
return {
|
||||
...state,
|
||||
|
|
@ -349,6 +373,8 @@ export interface ApplicationsReduxState {
|
|||
currentApplication?: ApplicationPayload;
|
||||
userOrgs: Organization[];
|
||||
isSavingOrgInfo: boolean;
|
||||
importingApplication: boolean;
|
||||
importedApplication: any;
|
||||
showAppInviteUsersDialog: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import ApplicationApi, {
|
|||
PublishApplicationResponse,
|
||||
SetDefaultPageRequest,
|
||||
UpdateApplicationRequest,
|
||||
ImportApplicationRequest,
|
||||
} from "api/ApplicationApi";
|
||||
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
||||
|
||||
|
|
@ -55,8 +56,11 @@ import {
|
|||
getCurrentPageId,
|
||||
} from "selectors/editorSelectors";
|
||||
import { showCompletionDialog } from "./OnboardingSagas";
|
||||
|
||||
import { deleteRecentAppEntities } from "utils/storage";
|
||||
import { reconnectWebsocket as reconnectWebsocketAction } from "actions/websocketActions";
|
||||
import { getCurrentOrg } from "selectors/organizationSelectors";
|
||||
import { Org } from "constants/orgConstants";
|
||||
|
||||
const getDefaultPageId = (
|
||||
pages?: ApplicationPagePayload[],
|
||||
|
|
@ -502,6 +506,53 @@ export function* forkApplicationSaga(
|
|||
}
|
||||
}
|
||||
|
||||
export function* importApplicationSaga(
|
||||
action: ReduxAction<ImportApplicationRequest>,
|
||||
) {
|
||||
try {
|
||||
const response: ApiResponse = yield call(
|
||||
ApplicationApi.importApplicationToOrg,
|
||||
action.payload,
|
||||
);
|
||||
const isValidResponse = yield validateResponse(response);
|
||||
if (isValidResponse) {
|
||||
const allOrgs = yield select(getCurrentOrg);
|
||||
const currentOrg = allOrgs.filter(
|
||||
(el: Org) => el.id === action.payload.orgId,
|
||||
);
|
||||
if (currentOrg.length > 0) {
|
||||
const {
|
||||
id: appId,
|
||||
pages,
|
||||
}: {
|
||||
id: string;
|
||||
pages: { default?: boolean; id: string; isDefault?: boolean }[];
|
||||
} = response.data;
|
||||
yield put({
|
||||
type: ReduxActionTypes.IMPORT_APPLICATION_SUCCESS,
|
||||
payload: {
|
||||
importedApplication: appId,
|
||||
},
|
||||
});
|
||||
const defaultPage = pages.filter((eachPage) => !!eachPage.isDefault);
|
||||
const pageURL = BUILDER_PAGE_URL(appId, defaultPage[0].id);
|
||||
history.push(pageURL);
|
||||
Toaster.show({
|
||||
text: "Application imported successfully",
|
||||
variant: Variant.success,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.IMPORT_APPLICATION_ERROR,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function* applicationSagas() {
|
||||
yield all([
|
||||
takeLatest(
|
||||
|
|
@ -530,5 +581,6 @@ export default function* applicationSagas() {
|
|||
ReduxActionTypes.DUPLICATE_APPLICATION_INIT,
|
||||
duplicateApplicationSaga,
|
||||
),
|
||||
takeLatest(ReduxActionTypes.IMPORT_APPLICATION_INIT, importApplicationSaga),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user