Add clone page option (#365)

Adding the option to clone a page in the overflow menu of the entity explorer
This commit is contained in:
akash-codemonk 2020-08-22 08:40:22 +05:30 committed by GitHub
parent 27b7f6c5e2
commit 3deb9ace43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 144 additions and 0 deletions

View File

@ -0,0 +1,18 @@
const pages = require("../../../locators/Pages.json");
describe("Pages", function() {
it("Clone page", function() {
cy.xpath(pages.popover)
.last()
.click({ force: true });
cy.get(pages.clonePage).click({ force: true });
cy.wait("@clonePage").should(
"have.nested.property",
"response.body.responseMeta.status",
201,
);
cy.get(".t--entity-name:contains(Page1 Copy)");
});
});

View File

@ -16,6 +16,7 @@
"entityExplorer": ".t--nav-link-entity-explorer",
"popover": "//div[contains(@class,'t--entity page')]//*[local-name()='g' and @id='Icon/Outline/more-vertical']",
"editName": ".single-select >div:contains('Edit Name')",
"clonePage": ".single-select >div:contains('Clone')",
"deletePage": ".single-select >div:contains('Delete')",
"entityQuery": ".t--entity-name:contains('Queries')"
}

View File

@ -1478,6 +1478,7 @@ Cypress.Commands.add("startServerAndRoutes", () => {
);
cy.route("GET", "/api/v1/users/me").as("getUser");
cy.route("POST", "/api/v1/pages").as("createPage");
cy.route("POST", "/api/v1/pages/clone/*").as("clonePage");
});
Cypress.Commands.add("alertValidate", text => {

View File

@ -105,6 +105,30 @@ export const createPage = (applicationId: string, pageName: string) => {
};
};
export const clonePageInit = (pageId: string) => {
return {
type: ReduxActionTypes.CLONE_PAGE_INIT,
payload: {
id: pageId,
},
};
};
export const clonePageSuccess = (
pageId: string,
pageName: string,
layoutId: string,
) => {
return {
type: ReduxActionTypes.CLONE_PAGE_SUCCESS,
payload: {
pageId,
pageName,
layoutId,
},
};
};
export const updatePage = (id: string, name: string) => {
return {
type: ReduxActionTypes.UPDATE_PAGE_INIT,

View File

@ -78,6 +78,10 @@ export interface DeletePageRequest {
id: string;
}
export interface ClonePageRequest {
id: string;
}
export interface UpdateWidgetNameRequest {
pageId: string;
layoutId: string;
@ -150,6 +154,10 @@ class PageApi extends Api {
return Api.delete(PageApi.url + "/" + request.id);
}
static clonePage(request: ClonePageRequest): AxiosPromise<ApiResponse> {
return Api.post(PageApi.url + "/clone/" + request.id);
}
static updateWidgetName(
request: UpdateWidgetNameRequest,
): AxiosPromise<UpdateWidgetNameResponse> {

View File

@ -161,6 +161,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
DELETE_APPLICATION_SUCCESS: "DELETE_APPLICATION_SUCCESS",
DELETE_PAGE_INIT: "DELETE_PAGE_INIT",
DELETE_PAGE_SUCCESS: "DELETE_PAGE_SUCCESS",
CLONE_PAGE_INIT: "CLONE_PAGE_INIT",
CLONE_PAGE_SUCCESS: "CLONE_PAGE_SUCCESS",
SET_DEFAULT_APPLICATION_PAGE_INIT: "SET_DEFAULT_APPLICATION_PAGE_INIT",
SET_DEFAULT_APPLICATION_PAGE_SUCCESS: "SET_DEFAULT_APPLICATION_PAGE_SUCCESS",
CREATE_ORGANIZATION_INIT: "CREATE_ORGANIZATION_INIT",
@ -312,6 +314,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
MOVE_ACTION_ERROR: "MOVE_ACTION_ERROR",
COPY_ACTION_ERROR: "COPY_ACTION_ERROR",
DELETE_PAGE_ERROR: "DELETE_PAGE_ERROR",
CLONE_PAGE_ERROR: "CLONE_PAGE_ERROR",
DELETE_APPLICATION_ERROR: "DELETE_APPLICATION_ERROR",
SET_DEFAULT_APPLICATION_PAGE_ERROR: "SET_DEFAULT_APPLICATION_PAGE_ERROR",
CREATE_ORGANIZATION_ERROR: "CREATE_ORGANIZATION_ERROR",
@ -394,6 +397,13 @@ export interface Page {
latest?: boolean;
}
export interface ClonePageSuccessPayload {
pageName: string;
pageId: string;
layoutId: string;
isDefault: boolean;
}
export type PageListPayload = Array<Page>;
export type ApplicationPayload = {

View File

@ -9,6 +9,7 @@ import { ReduxActionTypes } from "constants/ReduxActionConstants";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { ContextMenuPopoverModifiers } from "../helpers";
import { initExplorerEntityNameEdit } from "actions/explorerActions";
import { clonePageInit } from "actions/pageActions";
export const PageContextMenu = (props: {
pageId: string;
@ -52,12 +53,22 @@ export const PageContextMenu = (props: {
[dispatch, props.pageId],
);
const clonePage = useCallback(() => dispatch(clonePageInit(props.pageId)), [
dispatch,
props.pageId,
]);
const optionTree: TreeDropdownOption[] = [
{
value: "rename",
onSelect: editPageName,
label: "Edit Name",
},
{
value: "clone",
onSelect: clonePage,
label: "Clone",
},
];
if (!props.isDefaultPage) {
optionTree.push({

View File

@ -3,6 +3,7 @@ import {
ReduxAction,
ReduxActionTypes,
PageListPayload,
ClonePageSuccessPayload,
} from "constants/ReduxActionConstants";
const initialState: PageListReduxState = {
@ -51,6 +52,17 @@ const pageListReducer = createReducer(initialState, {
_state.pages.push({ ...action.payload, latest: true });
return { ..._state };
},
[ReduxActionTypes.CLONE_PAGE_SUCCESS]: (
state: PageListReduxState,
action: ReduxAction<ClonePageSuccessPayload>,
): PageListReduxState => {
return {
...state,
pages: state.pages
.map(page => ({ ...page, latest: false }))
.concat([{ ...action.payload, latest: true }]),
};
},
[ReduxActionTypes.SET_DEFAULT_APPLICATION_PAGE_SUCCESS]: (
state: PageListReduxState,
action: ReduxAction<{ pageId: string; applicationId: string }>,

View File

@ -22,6 +22,8 @@ const initialState: EditorReduxState = {
isPageSwitching: false,
creatingPage: false,
creatingPageError: false,
cloningPage: false,
cloningPageError: false,
updatingWidgetName: false,
updateWidgetNameError: false,
},
@ -111,6 +113,20 @@ const editorReducer = createReducer(initialState, {
currentApplicationId,
};
},
[ReduxActionTypes.CLONE_PAGE_INIT]: (state: EditorReduxState) => {
state.loadingStates.cloningPage = true;
state.loadingStates.cloningPageError = false;
return { ...state };
},
[ReduxActionTypes.CLONE_PAGE_ERROR]: (state: EditorReduxState) => {
state.loadingStates.cloningPageError = true;
state.loadingStates.cloningPage = false;
return { ...state };
},
[ReduxActionTypes.CLONE_PAGE_SUCCESS]: (state: EditorReduxState) => {
state.loadingStates.cloningPage = false;
return { ...state };
},
[ReduxActionTypes.CREATE_PAGE_INIT]: (state: EditorReduxState) => {
state.loadingStates.creatingPage = true;
state.loadingStates.creatingPageError = false;
@ -162,6 +178,8 @@ export interface EditorReduxState {
pageSwitchingError: boolean;
creatingPage: boolean;
creatingPageError: boolean;
cloningPage: boolean;
cloningPageError: boolean;
updatingWidgetName: boolean;
updateWidgetNameError: boolean;
};

View File

@ -32,6 +32,10 @@ const explorerReducer = createReducer(initialState, {
[ReduxActionTypes.FETCH_PAGE_ERROR]: setEntityUpdateError,
[ReduxActionTypes.FETCH_PAGE_SUCCESS]: setEntityUpdateSuccess,
[ReduxActionTypes.CLONE_PAGE_INIT]: setUpdatingEntity,
[ReduxActionTypes.CLONE_PAGE_ERROR]: setEntityUpdateError,
[ReduxActionTypes.CLONE_PAGE_SUCCESS]: setEntityUpdateSuccess,
[ReduxActionTypes.MOVE_ACTION_INIT]: setUpdatingEntity,
[ReduxActionErrorTypes.MOVE_ACTION_ERROR]: setEntityUpdateError,
[ReduxActionTypes.MOVE_ACTION_SUCCESS]: setEntityUpdateSuccess,

View File

@ -10,6 +10,7 @@ import {
} from "constants/ReduxActionConstants";
import {
deletePageSuccess,
clonePageSuccess,
fetchPageSuccess,
fetchPublishedPageSuccess,
savePageSuccess,
@ -31,6 +32,7 @@ import PageApi, {
UpdatePageRequest,
UpdateWidgetNameRequest,
UpdateWidgetNameResponse,
ClonePageRequest,
} from "api/PageApi";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import {
@ -382,6 +384,40 @@ export function* deletePageSaga(action: ReduxAction<DeletePageRequest>) {
}
}
export function* clonePageSaga(clonePageAction: ReduxAction<ClonePageRequest>) {
try {
const request: ClonePageRequest = clonePageAction.payload;
const response: FetchPageResponse = yield call(PageApi.clonePage, request);
const applicationId = yield select(
(state: AppState) => state.entities.pageList.applicationId,
);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put(
clonePageSuccess(
response.data.id,
response.data.name,
response.data.layouts[0].id,
),
);
yield put({
type: ReduxActionTypes.FETCH_PAGE_DSL_INIT,
payload: {
pageId: response.data.id,
},
});
history.push(BUILDER_PAGE_URL(applicationId, response.data.id));
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.CLONE_PAGE_ERROR,
payload: {
error,
},
});
}
}
export function* updateWidgetNameSaga(
action: ReduxAction<{ id: string; newName: string }>,
) {
@ -477,6 +513,7 @@ export default function* pageSagas() {
),
takeLatest(ReduxActionTypes.UPDATE_LAYOUT, saveLayoutSaga),
takeLeading(ReduxActionTypes.CREATE_PAGE_INIT, createPageSaga),
takeLeading(ReduxActionTypes.CLONE_PAGE_INIT, clonePageSaga),
takeLatest(ReduxActionTypes.FETCH_PAGE_LIST_INIT, fetchPageListSaga),
takeLatest(ReduxActionTypes.UPDATE_PAGE_INIT, updatePageSaga),
takeLatest(ReduxActionTypes.DELETE_PAGE_INIT, deletePageSaga),