feat: Adds option to fork an app within the app editor (#23158)
## Description
* Fork within application, this needs 2 things:
* Load the workspaceList even when we have setModalClose variable set
* When fork is successful, on the next page it should close the forking
modal
* Adds forking model to EditorAppName menu
* Adds FETCH_APPLICATION_INIT to forkApplicationSaga
* This makes sure that when we fork an app from within another app,
it will reinitialize the new app properly.
* Corrects workspaceId variable for forkApplicationSaga
#### PR fixes following issue(s)
Fixes # (issue number)
#21470
#### Media

#### Type of change
- New feature (non-breaking change which adds functionality)
## Testing
#### How Has This Been Tested?
- [x] Cypress
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag
#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Test-plan-implementation#speedbreaker-features-to-consider-for-every-change)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans/_edit#areas-of-interest)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
parent
198084f93a
commit
53ea8e208b
|
|
@ -0,0 +1,47 @@
|
|||
import * as _ from "../../../../support/Objects/ObjectsCore";
|
||||
|
||||
const dsl = require("../../../../fixtures/basicDsl.json");
|
||||
|
||||
let forkedApplicationDsl;
|
||||
let parentApplicationDsl: any;
|
||||
|
||||
describe("Fork application across workspaces", function () {
|
||||
before(() => {
|
||||
_.agHelper.AddDsl(dsl);
|
||||
});
|
||||
|
||||
it("1. Signed user should be able to fork a public forkable app & Check if the forked application has the same dsl as the original", function () {
|
||||
const appname: string = localStorage.getItem("AppName") || "randomApp";
|
||||
_.entityExplorer.SelectEntityByName("Input1");
|
||||
|
||||
cy.intercept("PUT", "/api/v1/layouts/*/pages/*").as("inputUpdate");
|
||||
_.propPane.TypeTextIntoField("defaultvalue", "A");
|
||||
cy.wait("@inputUpdate").then((response) => {
|
||||
response.response &&
|
||||
(parentApplicationDsl = response.response.body.data.dsl);
|
||||
});
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(2000);
|
||||
_.homePage.NavigateToHome();
|
||||
_.homePage.FilterApplication(appname);
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.get(_.homePage._applicationCard).first().trigger("mouseover");
|
||||
cy.get(_.homePage._appEditIcon).first().click({ force: true });
|
||||
|
||||
cy.get(_.homePage._applicationName).click({ force: true });
|
||||
cy.contains("Fork Application").click({ force: true });
|
||||
|
||||
cy.get(_.locators._forkAppToWorkspaceBtn).click({ force: true });
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait("@postForkAppWorkspace").its("status").should("equal", 200);
|
||||
// check that forked application has same dsl
|
||||
cy.get("@getPage")
|
||||
.its("response.body.data")
|
||||
.then((data) => {
|
||||
forkedApplicationDsl = data.layouts[0].dsl;
|
||||
expect(JSON.stringify(forkedApplicationDsl)).to.contain(
|
||||
JSON.stringify(parentApplicationDsl),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -213,4 +213,5 @@ export class CommonLocators {
|
|||
`//*[text()='${popupname}']/following-sibling::button`;
|
||||
_selectByValue = (value: string) =>
|
||||
`//button[contains(@class, 't--open-dropdown-${value}')]`;
|
||||
_forkAppToWorkspaceBtn = ".t--fork-app-to-workspace-button";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,13 +45,14 @@ export class HomePage {
|
|||
public _closeBtn = ".ads-v2-modal__content-header-close-button";
|
||||
private _appHome = "//a[@href='/applications']";
|
||||
_applicationCard = ".t--application-card";
|
||||
_appEditIcon = ".t--application-edit-link";
|
||||
_homeIcon = ".t--appsmith-logo";
|
||||
private _homeAppsmithImage = "a.t--appsmith-logo";
|
||||
private _appContainer = ".t--applications-container";
|
||||
_homePageAppCreateBtn = this._appContainer + " .createnew";
|
||||
private _existingWorkspaceCreateNewApp = (existingWorkspaceName: string) =>
|
||||
`//span[text()='${existingWorkspaceName}']/ancestor::div[contains(@class, 't--workspace-section')]//button[contains(@class, 't--new-button')]`;
|
||||
private _applicationName = ".t--application-name";
|
||||
_applicationName = ".t--application-name";
|
||||
private _editAppName = "bp3-editable-text-editing";
|
||||
private _appMenu = ".ads-v2-menu__menu-item-children";
|
||||
_buildFromDataTableActionCard = "[data-testid='generate-app']";
|
||||
|
|
@ -323,10 +324,10 @@ export class HomePage {
|
|||
.should("be.enabled");
|
||||
}
|
||||
|
||||
public FilterApplication(appName: string, workspaceId: string) {
|
||||
public FilterApplication(appName: string, workspaceId?: string) {
|
||||
cy.get(this._searchInput).type(appName, { force: true });
|
||||
this.agHelper.Sleep(2000);
|
||||
cy.get(this._appContainer).contains(workspaceId);
|
||||
workspaceId && cy.get(this._appContainer).contains(workspaceId);
|
||||
cy.xpath(this.locator._spanButton("Share")).first().should("be.visible");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export interface DeleteApplicationRequest {
|
|||
export interface ForkApplicationRequest {
|
||||
applicationId: string;
|
||||
workspaceId: string;
|
||||
editMode?: boolean;
|
||||
}
|
||||
|
||||
export type GetAllApplicationResponse = ApiResponse<ApplicationPagePayload[]>;
|
||||
|
|
|
|||
|
|
@ -625,7 +625,10 @@ export function* forkApplicationSaga(
|
|||
application: ApplicationResponsePayload;
|
||||
isPartialImport: boolean;
|
||||
unConfiguredDatasourceList: Datasource[];
|
||||
}> = yield call(ApplicationApi.forkApplication, action.payload);
|
||||
}> = yield call(ApplicationApi.forkApplication, {
|
||||
applicationId: action.payload.applicationId,
|
||||
workspaceId: action.payload.workspaceId,
|
||||
});
|
||||
const isValidResponse: boolean = yield validateResponse(response);
|
||||
if (isValidResponse) {
|
||||
yield put(resetCurrentApplication());
|
||||
|
|
@ -644,12 +647,24 @@ export function* forkApplicationSaga(
|
|||
yield put({
|
||||
type: ReduxActionTypes.SET_CURRENT_WORKSPACE_ID,
|
||||
payload: {
|
||||
id: action.payload.workspaceId,
|
||||
workspaceId: action.payload.workspaceId,
|
||||
},
|
||||
});
|
||||
const pageURL = builderURL({
|
||||
pageId: application.defaultPageId as string,
|
||||
});
|
||||
|
||||
if (action.payload.editMode) {
|
||||
const appId = application.id;
|
||||
const pageId = application.defaultPageId;
|
||||
yield put({
|
||||
type: ReduxActionTypes.FETCH_APPLICATION_INIT,
|
||||
payload: {
|
||||
applicationId: appId,
|
||||
pageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
history.push(pageURL);
|
||||
|
||||
const isEditorInitialized: boolean = yield select(getIsEditorInitialized);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type ForkApplicationModalProps = {
|
|||
trigger?: React.ReactNode;
|
||||
isModalOpen?: boolean;
|
||||
setModalClose?: (isOpen: boolean) => void;
|
||||
isInEditMode?: boolean;
|
||||
};
|
||||
|
||||
function ForkApplicationModal(props: ForkApplicationModalProps) {
|
||||
|
|
@ -55,17 +56,35 @@ function ForkApplicationModal(props: ForkApplicationModalProps) {
|
|||
const queryParams = new URLSearchParams(location.search);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryParams.get("fork") === "true" || isModalOpen) {
|
||||
if (queryParams.get("fork") === "true") {
|
||||
handleOpen();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// This effect makes sure that no if <ForkApplicationModel />
|
||||
// is getting controlled from outside, then we always load workspaces
|
||||
if (isModalOpen) {
|
||||
handleOpen();
|
||||
return;
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
// when we fork from within the appeditor, fork modal remains open
|
||||
// even on the landing page of "Forked" app, this closes it
|
||||
const shouldCloseForcibly =
|
||||
!forkingApplication && isModalOpen && setModalClose;
|
||||
shouldCloseForcibly && setModalClose(false);
|
||||
}, [forkingApplication]);
|
||||
|
||||
const forkApplication = () => {
|
||||
dispatch({
|
||||
type: ReduxActionTypes.FORK_APPLICATION_INIT,
|
||||
payload: {
|
||||
applicationId: props.applicationId,
|
||||
workspaceId: workspace?.value,
|
||||
editMode: props.isInEditMode,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -110,13 +129,14 @@ function ForkApplicationModal(props: ForkApplicationModalProps) {
|
|||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
// TODO: removed if condition here. Ensure it will affect something or not.
|
||||
const url = new URL(window.location.href);
|
||||
if (!url.searchParams.has("fork")) {
|
||||
url.searchParams.append("fork", "true");
|
||||
history.push(url.toString().slice(url.origin.length));
|
||||
if (!props.setModalClose) {
|
||||
const url = new URL(window.location.href);
|
||||
if (!url.searchParams.has("fork")) {
|
||||
url.searchParams.append("fork", "true");
|
||||
history.push(url.toString().slice(url.origin.length));
|
||||
}
|
||||
}
|
||||
dispatch(getAllApplications());
|
||||
!workspaceList.length && dispatch(getAllApplications());
|
||||
};
|
||||
|
||||
const handleOnOpenChange = (isOpen: boolean) => {
|
||||
|
|
|
|||
|
|
@ -26,10 +26,12 @@ import type { ThemeProp } from "widgets/constants";
|
|||
|
||||
type NavigationMenuDataProps = ThemeProp & {
|
||||
editMode: typeof noop;
|
||||
setForkApplicationModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const GetNavigationMenuData = ({
|
||||
editMode,
|
||||
setForkApplicationModalOpen,
|
||||
}: NavigationMenuDataProps): MenuItemData[] => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
|
@ -43,6 +45,10 @@ export const GetNavigationMenuData = ({
|
|||
currentApplication?.userPermissions ?? [],
|
||||
PERMISSION_TYPE.EXPORT_APPLICATION,
|
||||
);
|
||||
const hasEditPermission = isPermitted(
|
||||
currentApplication?.userPermissions ?? [],
|
||||
PERMISSION_TYPE.MANAGE_APPLICATION,
|
||||
);
|
||||
const openExternalLink = useCallback((link: string) => {
|
||||
if (link) {
|
||||
window.open(link, "_blank");
|
||||
|
|
@ -148,6 +154,12 @@ export const GetNavigationMenuData = ({
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Fork Application",
|
||||
onClick: () => setForkApplicationModalOpen(true),
|
||||
type: MenuTypes.MENU,
|
||||
isVisible: isApplicationIdPresent && hasEditPermission,
|
||||
},
|
||||
{
|
||||
text: "Export application",
|
||||
onClick: () =>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { GetNavigationMenuData } from "./NavigationMenuData";
|
|||
import { NavigationMenu } from "./NavigationMenu";
|
||||
import type { Theme } from "constants/DefaultTheme";
|
||||
import { Icon, Menu, toast, MenuTrigger } from "design-system";
|
||||
import ForkApplicationModal from "pages/Applications/ForkApplicationModal";
|
||||
|
||||
type EditorAppNameProps = CommonComponentProps & {
|
||||
applicationId: string | undefined;
|
||||
|
|
@ -80,6 +81,8 @@ export function EditorAppName(props: EditorAppNameProps) {
|
|||
const [savingState, setSavingState] = useState<SavingState>(
|
||||
SavingState.NOT_STARTED,
|
||||
);
|
||||
const [isForkApplicationModalopen, setForkApplicationModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const onBlur = (value: string) => {
|
||||
if (props.onBlur) props.onBlur(value);
|
||||
|
|
@ -123,48 +126,57 @@ export function EditorAppName(props: EditorAppNameProps) {
|
|||
const NavigationMenuData = GetNavigationMenuData({
|
||||
editMode,
|
||||
theme,
|
||||
setForkApplicationModalOpen,
|
||||
});
|
||||
|
||||
return defaultValue !== "" ? (
|
||||
<Menu
|
||||
className="t--application-edit-menu"
|
||||
onOpenChange={handleOnInteraction}
|
||||
open={isPopoverOpen}
|
||||
>
|
||||
<MenuTrigger disabled={isEditing}>
|
||||
<Container onClick={handleAppNameClick}>
|
||||
<EditableAppName
|
||||
className={props.className}
|
||||
defaultSavingState={defaultSavingState}
|
||||
defaultValue={defaultValue}
|
||||
editInteractionKind={props.editInteractionKind}
|
||||
fill={props.fill}
|
||||
hideEditIcon
|
||||
inputValidation={inputValidation}
|
||||
isEditing={isEditing}
|
||||
isEditingDefault={isEditingDefault}
|
||||
isError={props.isError}
|
||||
isInvalid={isInvalid}
|
||||
onBlur={onBlur}
|
||||
placeholder={props.placeholder}
|
||||
savingState={savingState}
|
||||
setIsEditing={setIsEditing}
|
||||
setIsInvalid={setIsInvalid}
|
||||
setSavingState={setSavingState}
|
||||
/>
|
||||
{!isEditing && (
|
||||
<StyledIcon
|
||||
name={isPopoverOpen ? "expand-less" : "down-arrow"}
|
||||
size="md"
|
||||
<>
|
||||
<Menu
|
||||
className="t--application-edit-menu"
|
||||
onOpenChange={handleOnInteraction}
|
||||
open={isPopoverOpen}
|
||||
>
|
||||
<MenuTrigger disabled={isEditing}>
|
||||
<Container onClick={handleAppNameClick}>
|
||||
<EditableAppName
|
||||
className={props.className}
|
||||
defaultSavingState={defaultSavingState}
|
||||
defaultValue={defaultValue}
|
||||
editInteractionKind={props.editInteractionKind}
|
||||
fill={props.fill}
|
||||
hideEditIcon
|
||||
inputValidation={inputValidation}
|
||||
isEditing={isEditing}
|
||||
isEditingDefault={isEditingDefault}
|
||||
isError={props.isError}
|
||||
isInvalid={isInvalid}
|
||||
onBlur={onBlur}
|
||||
placeholder={props.placeholder}
|
||||
savingState={savingState}
|
||||
setIsEditing={setIsEditing}
|
||||
setIsInvalid={setIsInvalid}
|
||||
setSavingState={setSavingState}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</MenuTrigger>
|
||||
<NavigationMenu
|
||||
menuItems={NavigationMenuData}
|
||||
setIsPopoverOpen={setIsPopoverOpen}
|
||||
{!isEditing && (
|
||||
<StyledIcon
|
||||
name={isPopoverOpen ? "expand-less" : "down-arrow"}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</MenuTrigger>
|
||||
<NavigationMenu
|
||||
menuItems={NavigationMenuData}
|
||||
setIsPopoverOpen={setIsPopoverOpen}
|
||||
/>
|
||||
</Menu>
|
||||
<ForkApplicationModal
|
||||
applicationId={props.applicationId || ""}
|
||||
isInEditMode
|
||||
isModalOpen={isForkApplicationModalopen}
|
||||
setModalClose={setForkApplicationModalOpen}
|
||||
/>
|
||||
</Menu>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user