feat: update app via import from within application (#22793)
This commit is contained in:
parent
68f8d97e19
commit
51e44e6605
|
|
@ -179,6 +179,7 @@ export interface ImportApplicationRequest {
|
|||
applicationFile?: File;
|
||||
progress?: (progressEvent: ProgressEvent) => void;
|
||||
onSuccessCallback?: () => void;
|
||||
appId?: string;
|
||||
}
|
||||
|
||||
export interface AppEmbedSetting {
|
||||
|
|
@ -341,7 +342,9 @@ export class ApplicationApi extends Api {
|
|||
formData.append("file", request.applicationFile);
|
||||
}
|
||||
return Api.post(
|
||||
ApplicationApi.baseURL + "/import/" + request.workspaceId,
|
||||
`${ApplicationApi.baseURL}/import/${request.workspaceId}${
|
||||
request.appId ? `?applicationId=${request.appId}` : ""
|
||||
}`,
|
||||
formData,
|
||||
null,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -560,6 +560,8 @@ export const NO_JS_FUNCTION_RETURN_VALUE = (JSFunctionName: string) =>
|
|||
`${JSFunctionName} did not return any data. Did you add a return statement?`;
|
||||
|
||||
// Import/Export Application features
|
||||
export const ERROR_IMPORTING_APPLICATION_TO_WORKSPACE = () =>
|
||||
"Error importing application. No workspace found";
|
||||
export const IMPORT_APPLICATION_MODAL_TITLE = () => "Import application";
|
||||
export const IMPORT_APPLICATION_MODAL_LABEL = () =>
|
||||
"Where would you like to import your application from?";
|
||||
|
|
@ -1476,8 +1478,7 @@ export const APP_SETTINGS_CLOSE_TOOLTIP = () => "Close settings panel";
|
|||
|
||||
export const GENERAL_SETTINGS_SECTION_HEADER = () => "General";
|
||||
export const GENERAL_SETTINGS_SECTION_CONTENT_HEADER = () => "General Settings";
|
||||
export const GENERAL_SETTINGS_SECTION_HEADER_DESC = () =>
|
||||
"App name, icon and share";
|
||||
export const GENERAL_SETTINGS_SECTION_HEADER_DESC = () => "App name and icon";
|
||||
export const GENERAL_SETTINGS_APP_NAME_LABEL = () => "App Name";
|
||||
export const GENERAL_SETTINGS_NAME_EMPTY_MESSAGE = () =>
|
||||
"App name cannot be empty";
|
||||
|
|
@ -1513,6 +1514,17 @@ export const PAGE_SETTINGS_SET_AS_HOMEPAGE_TOOLTIP_NON_HOME_PAGE = () =>
|
|||
export const PAGE_SETTINGS_ACTION_NAME_CONFLICT_ERROR = (name: string) =>
|
||||
`${name} is already being used.`;
|
||||
|
||||
export const UPDATE_VIA_IMPORT_SETTING = {
|
||||
settingHeader: () => "Update through file import",
|
||||
settingDesc: () => "Update app by importing file",
|
||||
settingLabel: () => "Import",
|
||||
settingContent: () =>
|
||||
"This action will override your existing application. Please exercise caution while selecting the file to import.",
|
||||
settingActionButtonTxt: () => "Import",
|
||||
disabledForGit: () =>
|
||||
"This feature is not supported for apps connected to Git version control. Please use Git Pull to update and sync your app.",
|
||||
};
|
||||
|
||||
export const IN_APP_EMBED_SETTING = {
|
||||
applicationUrl: () => "application url",
|
||||
allowEmbeddingLabel: () => "Embedding enabled",
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import {
|
|||
DELETING_APPLICATION,
|
||||
DISCARD_SUCCESS,
|
||||
DUPLICATING_APPLICATION,
|
||||
ERROR_IMPORTING_APPLICATION_TO_WORKSPACE,
|
||||
} from "@appsmith/constants/messages";
|
||||
import type { AppIconName } from "design-system-old";
|
||||
import { Toaster, Variant } from "design-system-old";
|
||||
|
|
@ -81,7 +82,10 @@ import {
|
|||
reconnectAppLevelWebsocket,
|
||||
reconnectPageLevelWebsocket,
|
||||
} from "actions/websocketActions";
|
||||
import { getCurrentWorkspace } from "@appsmith/selectors/workspaceSelectors";
|
||||
import {
|
||||
getCurrentWorkspace,
|
||||
getCurrentWorkspaceId,
|
||||
} from "@appsmith/selectors/workspaceSelectors";
|
||||
|
||||
import {
|
||||
getCurrentStep,
|
||||
|
|
@ -727,20 +731,22 @@ export function* importApplicationSaga(
|
|||
ApplicationApi.importApplicationToWorkspace,
|
||||
action.payload,
|
||||
);
|
||||
const urlObject = new URL(window.location.href);
|
||||
const isApplicationUrl = urlObject.pathname.includes("/app/");
|
||||
const isValidResponse: boolean = yield validateResponse(response);
|
||||
if (isValidResponse) {
|
||||
const currentWorkspaceId: string = yield select(getCurrentWorkspaceId);
|
||||
const allWorkspaces: Workspace[] = yield select(getCurrentWorkspace);
|
||||
const currentWorkspace = allWorkspaces.filter(
|
||||
(el: Workspace) => el.id === action.payload.workspaceId,
|
||||
);
|
||||
if (currentWorkspace.length > 0) {
|
||||
if (currentWorkspaceId || currentWorkspace.length > 0) {
|
||||
const {
|
||||
// @ts-expect-error: response is of type unknown
|
||||
application: { pages },
|
||||
// @ts-expect-error: response is of type unknown
|
||||
isPartialImport,
|
||||
} = response.data;
|
||||
|
||||
// @ts-expect-error: response is of type unknown
|
||||
yield put(importApplicationSuccess(response.data?.application));
|
||||
|
||||
|
|
@ -758,10 +764,24 @@ export function* importApplicationSaga(
|
|||
} else {
|
||||
// @ts-expect-error: pages is of type any
|
||||
// TODO: Update route params here
|
||||
const defaultPage = pages.filter((eachPage) => !!eachPage.isDefault);
|
||||
const { application } = response.data;
|
||||
const defaultPage = pages.filter(
|
||||
(eachPage: any) => !!eachPage.isDefault,
|
||||
);
|
||||
const pageURL = builderURL({
|
||||
pageId: defaultPage[0].id,
|
||||
});
|
||||
if (isApplicationUrl) {
|
||||
const appId = application.id;
|
||||
const pageId = application.defaultPageId;
|
||||
yield put({
|
||||
type: ReduxActionTypes.FETCH_APPLICATION_INIT,
|
||||
payload: {
|
||||
applicationId: appId,
|
||||
pageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
history.push(pageURL);
|
||||
const guidedTour: boolean = yield select(inGuidedTour);
|
||||
|
||||
|
|
@ -772,6 +792,13 @@ export function* importApplicationSaga(
|
|||
variant: Variant.success,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.IMPORT_APPLICATION_ERROR,
|
||||
payload: {
|
||||
error: createMessage(ERROR_IMPORTING_APPLICATION_TO_WORKSPACE),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@ const Row = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const FileImportCard = styled.div`
|
||||
width: 320px;
|
||||
const FileImportCard = styled.div<{ fillCardWidth: boolean }>`
|
||||
width: ${(props) => (props.fillCardWidth ? "100%" : "320px")};
|
||||
height: 200px;
|
||||
border: 1px solid ${Colors.GREY_4};
|
||||
display: flex;
|
||||
|
|
@ -234,10 +234,12 @@ type ImportApplicationModalProps = {
|
|||
workspaceId?: string;
|
||||
isModalOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
appId?: string;
|
||||
toApp?: boolean;
|
||||
};
|
||||
|
||||
function ImportApplicationModal(props: ImportApplicationModalProps) {
|
||||
const { isModalOpen, onClose, workspaceId } = props;
|
||||
const { appId, isModalOpen, onClose, toApp = false, workspaceId } = props;
|
||||
const [appFileToBeUploaded, setAppFileToBeUploaded] = useState<{
|
||||
file: File;
|
||||
setProgress: SetProgress;
|
||||
|
|
@ -272,6 +274,7 @@ function ImportApplicationModal(props: ImportApplicationModalProps) {
|
|||
});
|
||||
dispatch(
|
||||
importApplication({
|
||||
appId: appId as string,
|
||||
workspaceId: workspaceId as string,
|
||||
applicationFile: file,
|
||||
}),
|
||||
|
|
@ -310,16 +313,18 @@ function ImportApplicationModal(props: ImportApplicationModalProps) {
|
|||
>
|
||||
<TextWrapper>
|
||||
<Text color={Colors.COD_GRAY} type={TextType.P1}>
|
||||
{createMessage(
|
||||
importingApplication
|
||||
? UPLOADING_JSON
|
||||
: IMPORT_APPLICATION_MODAL_LABEL,
|
||||
)}
|
||||
{toApp
|
||||
? null
|
||||
: createMessage(
|
||||
importingApplication
|
||||
? UPLOADING_JSON
|
||||
: IMPORT_APPLICATION_MODAL_LABEL,
|
||||
)}
|
||||
</Text>
|
||||
</TextWrapper>
|
||||
{!importingApplication && (
|
||||
<Row>
|
||||
<FileImportCard className="t--import-json-card">
|
||||
<FileImportCard className="t--import-json-card" fillCardWidth={toApp}>
|
||||
<FilePickerV2
|
||||
containerClickable
|
||||
description={createMessage(IMPORT_APP_FROM_FILE_MESSAGE)}
|
||||
|
|
@ -331,7 +336,7 @@ function ImportApplicationModal(props: ImportApplicationModalProps) {
|
|||
uploadIcon="file-line"
|
||||
/>
|
||||
</FileImportCard>
|
||||
<GitImportCard handler={onGitImport} />
|
||||
{!toApp && <GitImportCard handler={onGitImport} />}
|
||||
</Row>
|
||||
)}
|
||||
{importingApplication && (
|
||||
|
|
|
|||
|
|
@ -39,10 +39,11 @@ type ImportApplicationModalProps = {
|
|||
workspaceId?: string;
|
||||
isModalOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
appId?: string;
|
||||
};
|
||||
|
||||
function ImportApplicationModal(props: ImportApplicationModalProps) {
|
||||
const { isModalOpen, onClose, workspaceId } = props;
|
||||
const { appId, isModalOpen, onClose, workspaceId } = props;
|
||||
const [appFileToBeUploaded, setAppFileToBeUploaded] = useState<{
|
||||
file: File;
|
||||
setProgress: SetProgress;
|
||||
|
|
@ -77,6 +78,7 @@ function ImportApplicationModal(props: ImportApplicationModalProps) {
|
|||
|
||||
dispatch(
|
||||
importApplication({
|
||||
appId: appId as string,
|
||||
workspaceId: workspaceId as string,
|
||||
applicationFile: file,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import {
|
||||
UPDATE_VIA_IMPORT_SETTING,
|
||||
createMessage,
|
||||
} from "@appsmith/constants/messages";
|
||||
import { getCurrentAppWorkspace } from "@appsmith/selectors/workspaceSelectors";
|
||||
import { Button, Text, TextType } from "design-system-old";
|
||||
import ImportApplicationModal from "pages/Applications/ImportApplicationModal";
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { getCurrentApplicationId } from "selectors/editorSelectors";
|
||||
import { getIsGitConnected } from "selectors/gitSyncSelectors";
|
||||
import styled from "styled-components";
|
||||
|
||||
const SettingWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
color: var(--appsmith-color-black-800);
|
||||
|
||||
.import-btn {
|
||||
width: 40%;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledText = styled(Text)`
|
||||
color: var(--appsmith-color-black-800);
|
||||
|
||||
&.setting-header {
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
export function ImportAppSettings() {
|
||||
const appId = useSelector(getCurrentApplicationId);
|
||||
const workspace = useSelector(getCurrentAppWorkspace);
|
||||
const isGitConnected = useSelector(getIsGitConnected);
|
||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||
|
||||
function handleClose() {
|
||||
setIsModalOpen(false);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SettingWrapper>
|
||||
<StyledText className="setting-header" type={TextType.P1}>
|
||||
{createMessage(UPDATE_VIA_IMPORT_SETTING.settingHeader)}
|
||||
</StyledText>
|
||||
<StyledText type={TextType.P3}>
|
||||
{isGitConnected
|
||||
? createMessage(UPDATE_VIA_IMPORT_SETTING.disabledForGit)
|
||||
: createMessage(UPDATE_VIA_IMPORT_SETTING.settingContent)}
|
||||
</StyledText>
|
||||
<Button
|
||||
className="import-btn"
|
||||
cypressSelector="t--app-setting-import-btn"
|
||||
disabled={isGitConnected}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
size="small"
|
||||
tag="button"
|
||||
text={createMessage(
|
||||
UPDATE_VIA_IMPORT_SETTING.settingActionButtonTxt,
|
||||
).toLocaleUpperCase()}
|
||||
/>
|
||||
</SettingWrapper>
|
||||
<ImportApplicationModal
|
||||
appId={appId}
|
||||
isModalOpen={isModalOpen}
|
||||
onClose={handleClose}
|
||||
toApp
|
||||
workspaceId={workspace?.id}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -22,12 +22,14 @@ import {
|
|||
THEME_SETTINGS_SECTION_CONTENT_HEADER,
|
||||
THEME_SETTINGS_SECTION_HEADER,
|
||||
THEME_SETTINGS_SECTION_HEADER_DESC,
|
||||
UPDATE_VIA_IMPORT_SETTING,
|
||||
} from "@appsmith/constants/messages";
|
||||
import { Colors } from "constants/Colors";
|
||||
import EmbedSettings from "./EmbedSettings";
|
||||
import NavigationSettings from "./NavigationSettings";
|
||||
import { updateAppSettingsPaneSelectedTabAction } from "actions/appSettingsPaneActions";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { ImportAppSettings } from "./ImportAppSettings";
|
||||
|
||||
export enum AppSettingsTabs {
|
||||
General,
|
||||
|
|
@ -35,6 +37,7 @@ export enum AppSettingsTabs {
|
|||
Theme,
|
||||
Navigation,
|
||||
Page,
|
||||
Import,
|
||||
}
|
||||
|
||||
export interface SelectedTab {
|
||||
|
|
@ -147,6 +150,19 @@ function AppSettings() {
|
|||
},
|
||||
subText: createMessage(APP_NAVIGATION_SETTING.sectionHeaderDesc),
|
||||
},
|
||||
{
|
||||
id: "t--update-via-import",
|
||||
icon: "download-line",
|
||||
isSelected: selectedTab.type === AppSettingsTabs.Import,
|
||||
name: createMessage(UPDATE_VIA_IMPORT_SETTING.settingLabel),
|
||||
onClick: () => {
|
||||
setSelectedTab({ type: AppSettingsTabs.Import });
|
||||
AnalyticsUtil.logEvent("APP_SETTINGS_SECTION_CLICK", {
|
||||
section: "Import",
|
||||
});
|
||||
},
|
||||
subText: createMessage(UPDATE_VIA_IMPORT_SETTING.settingDesc),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -214,6 +230,8 @@ function AppSettings() {
|
|||
return <EmbedSettings />;
|
||||
case AppSettingsTabs.Navigation:
|
||||
return <NavigationSettings />;
|
||||
case AppSettingsTabs.Import:
|
||||
return <ImportAppSettings />;
|
||||
}
|
||||
})()}
|
||||
</SectionContent>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@ function AppSettingsPane() {
|
|||
const dispatch = useDispatch();
|
||||
const paneRef = useRef(null);
|
||||
const portalRef = useRef(null);
|
||||
|
||||
useOnClickOutside([paneRef, portalRef], () => {
|
||||
if (document.getElementById("save-theme-modal")) return;
|
||||
if (document.getElementById("delete-theme-modal")) return;
|
||||
if (document.getElementById("manual-upgrades-modal")) return;
|
||||
// No id property for `Dialog` component, so using class name
|
||||
if (document.querySelector(".t--import-application-modal")) {
|
||||
return;
|
||||
}
|
||||
dispatch(closeAppSettingsPaneAction());
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user