Merge branch 'release' of https://github.com/appsmithorg/appsmith into release
This commit is contained in:
commit
f2b10d5759
|
|
@ -67,10 +67,7 @@ axiosInstance.interceptors.response.use(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const errorData = error.response.data.responseMeta;
|
const errorData = error.response.data.responseMeta;
|
||||||
if (
|
if (errorData.status === 404 && errorData.error.code === 4028) {
|
||||||
errorData.status === 404 &&
|
|
||||||
errorData.error.code === 4028
|
|
||||||
) {
|
|
||||||
history.push(PAGE_NOT_FOUND_URL);
|
history.push(PAGE_NOT_FOUND_URL);
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
code: 404,
|
code: 404,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ export interface PublishApplicationRequest {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChangeAppViewAccessRequest {
|
||||||
|
applicationId: string;
|
||||||
|
publicAccess: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PublishApplicationResponse extends ApiResponse {
|
export interface PublishApplicationResponse extends ApiResponse {
|
||||||
data: {};
|
data: {};
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +84,8 @@ class ApplicationApi extends Api {
|
||||||
static baseURL = "v1/applications/";
|
static baseURL = "v1/applications/";
|
||||||
static publishURLPath = (applicationId: string) => `publish/${applicationId}`;
|
static publishURLPath = (applicationId: string) => `publish/${applicationId}`;
|
||||||
static createApplicationPath = (orgId: string) => `?orgId=${orgId}`;
|
static createApplicationPath = (orgId: string) => `?orgId=${orgId}`;
|
||||||
|
static changeAppViewAccessPath = (applicationId: string) =>
|
||||||
|
`${applicationId}/changeAccess`;
|
||||||
static setDefaultPagePath = (request: SetDefaultPageRequest) =>
|
static setDefaultPagePath = (request: SetDefaultPageRequest) =>
|
||||||
`${ApplicationApi.baseURL}${request.applicationId}/page/${request.pageId}/makeDefault`;
|
`${ApplicationApi.baseURL}${request.applicationId}/page/${request.pageId}/makeDefault`;
|
||||||
static publishApplication(
|
static publishApplication(
|
||||||
|
|
@ -120,6 +127,17 @@ class ApplicationApi extends Api {
|
||||||
): AxiosPromise<ApiResponse> {
|
): AxiosPromise<ApiResponse> {
|
||||||
return Api.put(ApplicationApi.setDefaultPagePath(request));
|
return Api.put(ApplicationApi.setDefaultPagePath(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static changeAppViewAccess(
|
||||||
|
request: ChangeAppViewAccessRequest,
|
||||||
|
): AxiosPromise<ApiResponse> {
|
||||||
|
return Api.put(
|
||||||
|
ApplicationApi.baseURL +
|
||||||
|
ApplicationApi.changeAppViewAccessPath(request.applicationId),
|
||||||
|
{ publicAccess: request.publicAccess },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static deleteApplication(
|
static deleteApplication(
|
||||||
request: DeleteApplicationRequest,
|
request: DeleteApplicationRequest,
|
||||||
): AxiosPromise<ApiResponse> {
|
): AxiosPromise<ApiResponse> {
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,9 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
||||||
STORE_AS_DATASOURCE_COMPLETE: "STORE_AS_DATASOURCE_COMPLETE",
|
STORE_AS_DATASOURCE_COMPLETE: "STORE_AS_DATASOURCE_COMPLETE",
|
||||||
PUBLISH_APPLICATION_INIT: "PUBLISH_APPLICATION_INIT",
|
PUBLISH_APPLICATION_INIT: "PUBLISH_APPLICATION_INIT",
|
||||||
PUBLISH_APPLICATION_SUCCESS: "PUBLISH_APPLICATION_SUCCESS",
|
PUBLISH_APPLICATION_SUCCESS: "PUBLISH_APPLICATION_SUCCESS",
|
||||||
|
CHANGE_APPVIEW_ACCESS_INIT: "CHANGE_APPVIEW_ACCESS_INIT",
|
||||||
|
CHANGE_APPVIEW_ACCESS_SUCCESS: "CHANGE_APPVIEW_ACCESS_SUCCESS",
|
||||||
|
CHANGE_APPVIEW_ACCESS_ERROR: "CHANGE_APPVIEW_ACCESS_ERROR",
|
||||||
CREATE_PAGE_INIT: "CREATE_PAGE_INIT",
|
CREATE_PAGE_INIT: "CREATE_PAGE_INIT",
|
||||||
CREATE_PAGE_SUCCESS: "CREATE_PAGE_SUCCESS",
|
CREATE_PAGE_SUCCESS: "CREATE_PAGE_SUCCESS",
|
||||||
FETCH_PAGE_LIST_INIT: "FETCH_PAGE_LIST_INIT",
|
FETCH_PAGE_LIST_INIT: "FETCH_PAGE_LIST_INIT",
|
||||||
|
|
@ -382,6 +385,7 @@ export type ApplicationPayload = {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
defaultPageId?: string;
|
defaultPageId?: string;
|
||||||
|
isPublic?: boolean;
|
||||||
userPermissions?: string[];
|
userPermissions?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { AppState } from "reducers";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Breadcrumbs, IBreadcrumbProps } from "@blueprintjs/core";
|
import { Breadcrumbs, IBreadcrumbProps } from "@blueprintjs/core";
|
||||||
|
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||||
|
import {
|
||||||
|
getCurrentOrg,
|
||||||
|
getCurrentOrgId,
|
||||||
|
} from "selectors/organizationSelectors";
|
||||||
|
import { Org } from "constants/orgConstants";
|
||||||
import {
|
import {
|
||||||
BASE_URL,
|
BASE_URL,
|
||||||
APPLICATIONS_URL,
|
APPLICATIONS_URL,
|
||||||
|
|
@ -22,6 +30,8 @@ import {
|
||||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||||
import { Skin } from "constants/DefaultTheme";
|
import { Skin } from "constants/DefaultTheme";
|
||||||
import { HelpModal } from "components/designSystems/appsmith/help/HelpModal";
|
import { HelpModal } from "components/designSystems/appsmith/help/HelpModal";
|
||||||
|
import { FormDialogComponent } from "components/editorComponents/form/FormDialogComponent";
|
||||||
|
import ShareApplicationForm from "pages/Editor/ShareApplicationForm";
|
||||||
|
|
||||||
const LoadingContainer = styled.div`
|
const LoadingContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -48,13 +58,16 @@ const StretchedBreadCrumb = styled(Breadcrumbs)`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const InviteButton = styled.div`
|
const ShareButton = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type EditorHeaderProps = {
|
type EditorHeaderProps = {
|
||||||
|
currentOrg: Org;
|
||||||
|
currentOrgId: string;
|
||||||
|
fetchCurrentOrg: (orgId: string) => void;
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
pageSaveError?: boolean;
|
pageSaveError?: boolean;
|
||||||
pageName?: string;
|
pageName?: string;
|
||||||
|
|
@ -77,6 +90,12 @@ export const EditorHeader = (props: EditorHeaderProps) => {
|
||||||
page => page.pageId === props.currentPageId,
|
page => page.pageId === props.currentPageId,
|
||||||
)?.pageName;
|
)?.pageName;
|
||||||
|
|
||||||
|
const { fetchCurrentOrg, currentOrgId } = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCurrentOrg(currentOrgId);
|
||||||
|
}, [fetchCurrentOrg, currentOrgId]);
|
||||||
|
|
||||||
const pageSelectorData: CustomizedDropdownProps = {
|
const pageSelectorData: CustomizedDropdownProps = {
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
|
|
@ -146,9 +165,22 @@ export const EditorHeader = (props: EditorHeaderProps) => {
|
||||||
<StyledHeader>
|
<StyledHeader>
|
||||||
<StretchedBreadCrumb items={navigation} minVisibleItems={3} />
|
<StretchedBreadCrumb items={navigation} minVisibleItems={3} />
|
||||||
<CustomizedDropdown {...pageSelectorData} />
|
<CustomizedDropdown {...pageSelectorData} />
|
||||||
<InviteButton>
|
<ShareButton>
|
||||||
{/* <Button text="Share" intent="primary" filled size="small" /> */}
|
<FormDialogComponent
|
||||||
</InviteButton>
|
trigger={
|
||||||
|
<Button
|
||||||
|
text="Share"
|
||||||
|
intent="primary"
|
||||||
|
outline
|
||||||
|
size="small"
|
||||||
|
className="t--application-publish-btn"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
Form={ShareApplicationForm}
|
||||||
|
title={props.currentOrg.name}
|
||||||
|
/>
|
||||||
|
</ShareButton>
|
||||||
|
|
||||||
<LoadingContainer>{saveStatusMessage}</LoadingContainer>
|
<LoadingContainer>{saveStatusMessage}</LoadingContainer>
|
||||||
<PreviewPublishSection>
|
<PreviewPublishSection>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -166,4 +198,19 @@ export const EditorHeader = (props: EditorHeaderProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditorHeader;
|
const mapStateToProps = (state: AppState) => ({
|
||||||
|
currentOrg: getCurrentOrg(state),
|
||||||
|
currentOrgId: getCurrentOrgId(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => ({
|
||||||
|
fetchCurrentOrg: (orgId: string) =>
|
||||||
|
dispatch({
|
||||||
|
type: ReduxActionTypes.FETCH_CURRENT_ORG,
|
||||||
|
payload: {
|
||||||
|
orgId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(EditorHeader);
|
||||||
|
|
|
||||||
94
app/client/src/pages/Editor/ShareApplicationForm.tsx
Normal file
94
app/client/src/pages/Editor/ShareApplicationForm.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { withRouter } from "react-router";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { AppState } from "reducers";
|
||||||
|
import { StyledSwitch } from "components/propertyControls/StyledControls";
|
||||||
|
import { fetchApplication } from "actions/applicationActions";
|
||||||
|
import Spinner from "components/editorComponents/Spinner";
|
||||||
|
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||||
|
|
||||||
|
const ShareWithPublicOption = styled.div`
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
padding-top: 20px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ShareToggle = styled.div`
|
||||||
|
{
|
||||||
|
&&& label {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
&&& div {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ShareApplicationForm = (props: any) => {
|
||||||
|
const {
|
||||||
|
match: {
|
||||||
|
params: { applicationId },
|
||||||
|
},
|
||||||
|
fetchApplication,
|
||||||
|
isFetchingApplication,
|
||||||
|
isChangingViewAccess,
|
||||||
|
currentApplicationDetails,
|
||||||
|
changeAppViewAccess,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchApplication(applicationId);
|
||||||
|
}, [fetchApplication, applicationId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShareWithPublicOption>
|
||||||
|
Share the application with anyone
|
||||||
|
<ShareToggle>
|
||||||
|
{(isChangingViewAccess || isFetchingApplication) && (
|
||||||
|
<Spinner size={20} />
|
||||||
|
)}
|
||||||
|
{currentApplicationDetails && (
|
||||||
|
<StyledSwitch
|
||||||
|
onChange={() => {
|
||||||
|
changeAppViewAccess(
|
||||||
|
applicationId,
|
||||||
|
!currentApplicationDetails.isPublic,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
disabled={isChangingViewAccess || isFetchingApplication}
|
||||||
|
checked={currentApplicationDetails.isPublic}
|
||||||
|
large
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ShareToggle>
|
||||||
|
</ShareWithPublicOption>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState) => ({
|
||||||
|
currentApplicationDetails: state.ui.applications.currentApplication,
|
||||||
|
isFetchingApplication: state.ui.applications.isFetchingApplication,
|
||||||
|
isChangingViewAccess: state.ui.applications.isChangingViewAccess,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => ({
|
||||||
|
fetchApplication: (applicationId: string) => {
|
||||||
|
return dispatch(fetchApplication(applicationId));
|
||||||
|
},
|
||||||
|
changeAppViewAccess: (applicationId: string, publicAccess: boolean) =>
|
||||||
|
dispatch({
|
||||||
|
type: ReduxActionTypes.CHANGE_APPVIEW_ACCESS_INIT,
|
||||||
|
payload: {
|
||||||
|
applicationId,
|
||||||
|
publicAccess,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withRouter(
|
||||||
|
connect(mapStateToProps, mapDispatchToProps)(ShareApplicationForm),
|
||||||
|
);
|
||||||
|
|
@ -10,6 +10,8 @@ import { ERROR_MESSAGE_CREATE_APPLICATION } from "constants/messages";
|
||||||
|
|
||||||
const initialState: ApplicationsReduxState = {
|
const initialState: ApplicationsReduxState = {
|
||||||
isFetchingApplications: false,
|
isFetchingApplications: false,
|
||||||
|
isFetchingApplication: false,
|
||||||
|
isChangingViewAccess: false,
|
||||||
applicationList: [],
|
applicationList: [],
|
||||||
creatingApplication: false,
|
creatingApplication: false,
|
||||||
deletingApplication: false,
|
deletingApplication: false,
|
||||||
|
|
@ -56,6 +58,22 @@ const applicationsReducer = createReducer(initialState, {
|
||||||
) => {
|
) => {
|
||||||
return { ...state, deletingApplication: false };
|
return { ...state, deletingApplication: false };
|
||||||
},
|
},
|
||||||
|
[ReduxActionTypes.CHANGE_APPVIEW_ACCESS_INIT]: (
|
||||||
|
state: ApplicationsReduxState,
|
||||||
|
) => ({ ...state, isChangingViewAccess: true }),
|
||||||
|
[ReduxActionTypes.CHANGE_APPVIEW_ACCESS_SUCCESS]: (
|
||||||
|
state: ApplicationsReduxState,
|
||||||
|
action: ReduxAction<{ id: string; isPublic: boolean }>,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isChangingViewAccess: false,
|
||||||
|
currentApplication: {
|
||||||
|
...state.currentApplication,
|
||||||
|
isPublic: action.payload.isPublic,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
[ReduxActionTypes.FETCH_APPLICATION_LIST_INIT]: (
|
[ReduxActionTypes.FETCH_APPLICATION_LIST_INIT]: (
|
||||||
state: ApplicationsReduxState,
|
state: ApplicationsReduxState,
|
||||||
) => ({ ...state, isFetchingApplications: true }),
|
) => ({ ...state, isFetchingApplications: true }),
|
||||||
|
|
@ -127,6 +145,8 @@ export interface ApplicationsReduxState {
|
||||||
applicationList: ApplicationPayload[];
|
applicationList: ApplicationPayload[];
|
||||||
searchKeyword?: string;
|
searchKeyword?: string;
|
||||||
isFetchingApplications: boolean;
|
isFetchingApplications: boolean;
|
||||||
|
isFetchingApplication: boolean;
|
||||||
|
isChangingViewAccess: boolean;
|
||||||
creatingApplication: boolean;
|
creatingApplication: boolean;
|
||||||
createApplicationError?: string;
|
createApplicationError?: string;
|
||||||
deletingApplication: boolean;
|
deletingApplication: boolean;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import ApplicationApi, {
|
||||||
FetchUsersApplicationsOrgsResponse,
|
FetchUsersApplicationsOrgsResponse,
|
||||||
OrganizationApplicationObject,
|
OrganizationApplicationObject,
|
||||||
ApplicationObject,
|
ApplicationObject,
|
||||||
|
ChangeAppViewAccessRequest,
|
||||||
} from "api/ApplicationApi";
|
} from "api/ApplicationApi";
|
||||||
import { getDefaultPageId } from "./SagaUtils";
|
import { getDefaultPageId } from "./SagaUtils";
|
||||||
import { call, put, takeLatest, all, select } from "redux-saga/effects";
|
import { call, put, takeLatest, all, select } from "redux-saga/effects";
|
||||||
|
|
@ -29,6 +30,7 @@ import { BUILDER_PAGE_URL } from "constants/routes";
|
||||||
import { AppState } from "reducers";
|
import { AppState } from "reducers";
|
||||||
import { setDefaultApplicationPageSuccess } from "actions/applicationActions";
|
import { setDefaultApplicationPageSuccess } from "actions/applicationActions";
|
||||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||||
|
|
||||||
export function* publishApplicationSaga(
|
export function* publishApplicationSaga(
|
||||||
requestAction: ReduxAction<PublishApplicationRequest>,
|
requestAction: ReduxAction<PublishApplicationRequest>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -214,6 +216,35 @@ export function* deleteApplicationSaga(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* changeAppViewAccessSaga(
|
||||||
|
requestAction: ReduxAction<ChangeAppViewAccessRequest>,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const request = requestAction.payload;
|
||||||
|
const response: ApiResponse = yield call(
|
||||||
|
ApplicationApi.changeAppViewAccess,
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
const isValidResponse = yield validateResponse(response);
|
||||||
|
if (isValidResponse) {
|
||||||
|
yield put({
|
||||||
|
type: ReduxActionTypes.CHANGE_APPVIEW_ACCESS_SUCCESS,
|
||||||
|
payload: {
|
||||||
|
id: response.data.id,
|
||||||
|
isPublic: response.data.isPublic,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
yield put({
|
||||||
|
type: ReduxActionErrorTypes.CHANGE_APPVIEW_ACCESS_ERROR,
|
||||||
|
payload: {
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function* createApplicationSaga(
|
export function* createApplicationSaga(
|
||||||
action: ReduxAction<{
|
action: ReduxAction<{
|
||||||
applicationName: string;
|
applicationName: string;
|
||||||
|
|
@ -299,6 +330,10 @@ export default function* applicationSagas() {
|
||||||
ReduxActionTypes.FETCH_APPLICATION_LIST_INIT,
|
ReduxActionTypes.FETCH_APPLICATION_LIST_INIT,
|
||||||
fetchApplicationListSaga,
|
fetchApplicationListSaga,
|
||||||
),
|
),
|
||||||
|
takeLatest(
|
||||||
|
ReduxActionTypes.CHANGE_APPVIEW_ACCESS_INIT,
|
||||||
|
changeAppViewAccessSaga,
|
||||||
|
),
|
||||||
takeLatest(
|
takeLatest(
|
||||||
ReduxActionTypes.GET_ALL_APPLICATION_INIT,
|
ReduxActionTypes.GET_ALL_APPLICATION_INIT,
|
||||||
getAllApplicationSaga,
|
getAllApplicationSaga,
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,10 @@ export function* deleteDatasourceSaga(
|
||||||
history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId));
|
history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: error.message,
|
||||||
|
type: ToastType.ERROR,
|
||||||
|
});
|
||||||
yield put({
|
yield put({
|
||||||
type: ReduxActionErrorTypes.DELETE_DATASOURCE_ERROR,
|
type: ReduxActionErrorTypes.DELETE_DATASOURCE_ERROR,
|
||||||
payload: { error, id: actionPayload.payload.id },
|
payload: { error, id: actionPayload.payload.id },
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export function getResponseErrorMessage(response: ApiResponse) {
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorPayloadType = { code?: number; message?: string, show?:boolean };
|
type ErrorPayloadType = { code?: number; message?: string; show?: boolean };
|
||||||
let ActionErrorDisplayMap: {
|
let ActionErrorDisplayMap: {
|
||||||
[key: string]: (error: ErrorPayloadType) => string;
|
[key: string]: (error: ErrorPayloadType) => string;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
|
||||||
165
app/client/src/utils/autocomplete/TernServer.test.ts
Normal file
165
app/client/src/utils/autocomplete/TernServer.test.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import TernServer from "./TernServer";
|
||||||
|
import { MockCodemirrorEditor } from "../../../test/__mocks__/CodeMirrorEditorMock";
|
||||||
|
jest.mock("jsExecution/RealmExecutor");
|
||||||
|
|
||||||
|
describe("Tern server", () => {
|
||||||
|
it("Check whether the correct value is being sent to tern", () => {
|
||||||
|
const ternServer = new TernServer({});
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
name: "test",
|
||||||
|
doc: ({
|
||||||
|
getCursor: () => ({ ch: 0, line: 0 }),
|
||||||
|
getLine: () => "{{Api.}}",
|
||||||
|
} as unknown) as CodeMirror.Doc,
|
||||||
|
changed: null,
|
||||||
|
},
|
||||||
|
expectedOutput: "{{Api.}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
name: "test",
|
||||||
|
doc: ({
|
||||||
|
getCursor: () => ({ ch: 0, line: 0 }),
|
||||||
|
getLine: () => "a{{Api.}}",
|
||||||
|
} as unknown) as CodeMirror.Doc,
|
||||||
|
changed: null,
|
||||||
|
},
|
||||||
|
expectedOutput: "a{{Api.}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
name: "test",
|
||||||
|
doc: ({
|
||||||
|
getCursor: () => ({ ch: 2, line: 0 }),
|
||||||
|
getLine: () => "a{{Api.}}",
|
||||||
|
} as unknown) as CodeMirror.Doc,
|
||||||
|
changed: null,
|
||||||
|
},
|
||||||
|
expectedOutput: "{{Api.}}",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(testCase => {
|
||||||
|
const value = ternServer.getFocusedDynamicValue(testCase.input);
|
||||||
|
expect(value).toBe(testCase.expectedOutput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Check whether the correct position is sent for querying autocomplete", () => {
|
||||||
|
const ternServer = new TernServer({});
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
name: "test",
|
||||||
|
doc: ({
|
||||||
|
getCursor: () => ({ ch: 0, line: 0 }),
|
||||||
|
getLine: () => "{{Api.}}",
|
||||||
|
somethingSelected: () => false,
|
||||||
|
} as unknown) as CodeMirror.Doc,
|
||||||
|
changed: null,
|
||||||
|
},
|
||||||
|
expectedOutput: { ch: 0, line: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
name: "test",
|
||||||
|
doc: ({
|
||||||
|
getCursor: () => ({ ch: 0, line: 1 }),
|
||||||
|
getLine: () => "{{Api.}}",
|
||||||
|
somethingSelected: () => false,
|
||||||
|
} as unknown) as CodeMirror.Doc,
|
||||||
|
changed: null,
|
||||||
|
},
|
||||||
|
expectedOutput: { ch: 0, line: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
name: "test",
|
||||||
|
doc: ({
|
||||||
|
getCursor: () => ({ ch: 3, line: 1 }),
|
||||||
|
getLine: () => "g {{Api.}}",
|
||||||
|
somethingSelected: () => false,
|
||||||
|
} as unknown) as CodeMirror.Doc,
|
||||||
|
changed: null,
|
||||||
|
},
|
||||||
|
expectedOutput: { ch: 1, line: 0 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((testCase, index) => {
|
||||||
|
const request = ternServer.buildRequest(testCase.input, {});
|
||||||
|
|
||||||
|
expect(request.query.end).toEqual(testCase.expectedOutput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`Check whether the position is evaluated correctly for placing the selected
|
||||||
|
autocomplete value`, () => {
|
||||||
|
const ternServer = new TernServer({});
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
codeEditor: {
|
||||||
|
value: "{{}}",
|
||||||
|
cursor: { ch: 2, line: 0 },
|
||||||
|
doc: ({
|
||||||
|
getCursor: () => ({ ch: 2, line: 0 }),
|
||||||
|
getLine: () => "{{}}",
|
||||||
|
somethingSelected: () => false,
|
||||||
|
} as unknown) as CodeMirror.Doc,
|
||||||
|
},
|
||||||
|
requestCallbackData: {
|
||||||
|
completions: [{ name: "Api1" }],
|
||||||
|
start: { ch: 2, line: 0 },
|
||||||
|
end: { ch: 6, line: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedOutput: { ch: 2, line: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: {
|
||||||
|
codeEditor: {
|
||||||
|
value: "\n {{}}",
|
||||||
|
cursor: { ch: 3, line: 1 },
|
||||||
|
doc: ({
|
||||||
|
getCursor: () => ({ ch: 3, line: 1 }),
|
||||||
|
getLine: () => " {{}}",
|
||||||
|
somethingSelected: () => false,
|
||||||
|
} as unknown) as CodeMirror.Doc,
|
||||||
|
},
|
||||||
|
requestCallbackData: {
|
||||||
|
completions: [{ name: "Api1" }],
|
||||||
|
start: { ch: 2, line: 1 },
|
||||||
|
end: { ch: 6, line: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedOutput: { ch: 3, line: 1 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach((testCase, index) => {
|
||||||
|
MockCodemirrorEditor.getValue.mockReturnValueOnce(
|
||||||
|
testCase.input.codeEditor.value,
|
||||||
|
);
|
||||||
|
MockCodemirrorEditor.getCursor.mockReturnValueOnce(
|
||||||
|
testCase.input.codeEditor.cursor,
|
||||||
|
);
|
||||||
|
MockCodemirrorEditor.getDoc.mockReturnValueOnce(
|
||||||
|
testCase.input.codeEditor.doc,
|
||||||
|
);
|
||||||
|
|
||||||
|
const value: any = ternServer.requestCallback(
|
||||||
|
null,
|
||||||
|
testCase.input.requestCallbackData,
|
||||||
|
(MockCodemirrorEditor as unknown) as CodeMirror.Editor,
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(value.from).toEqual(testCase.expectedOutput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -6,6 +6,10 @@ import ecma from "tern/defs/ecmascript.json";
|
||||||
import lodash from "constants/defs/lodash.json";
|
import lodash from "constants/defs/lodash.json";
|
||||||
import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator";
|
import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator";
|
||||||
import CodeMirror, { Hint, Pos, cmpPos } from "codemirror";
|
import CodeMirror, { Hint, Pos, cmpPos } from "codemirror";
|
||||||
|
import {
|
||||||
|
getDynamicStringSegments,
|
||||||
|
isDynamicValue,
|
||||||
|
} from "utils/DynamicBindingUtils";
|
||||||
|
|
||||||
const DEFS = [ecma, lodash];
|
const DEFS = [ecma, lodash];
|
||||||
const bigDoc = 250;
|
const bigDoc = 250;
|
||||||
|
|
@ -70,27 +74,34 @@ class TernServer {
|
||||||
this.server.addDefs(def, true);
|
this.server.addDefs(def, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getHint(cm: CodeMirror.Editor) {
|
requestCallback(
|
||||||
return new Promise(resolve => {
|
error: any,
|
||||||
this.request(
|
data: any,
|
||||||
cm,
|
cm: CodeMirror.Editor,
|
||||||
{
|
resolve: Function,
|
||||||
type: "completions",
|
) {
|
||||||
types: true,
|
|
||||||
docs: true,
|
|
||||||
urls: true,
|
|
||||||
origins: true,
|
|
||||||
caseInsensitive: true,
|
|
||||||
},
|
|
||||||
(error, data) => {
|
|
||||||
if (error) return this.showError(cm, error);
|
if (error) return this.showError(cm, error);
|
||||||
if (data.completions.length === 0) {
|
if (data.completions.length === 0) {
|
||||||
return this.showError(cm, "No suggestions");
|
return this.showError(cm, "No suggestions");
|
||||||
}
|
}
|
||||||
|
const doc = this.findDoc(cm.getDoc());
|
||||||
|
const cursor = cm.getCursor();
|
||||||
|
const lineValue = this.lineValue(doc);
|
||||||
|
const focusedValue = this.getFocusedDynamicValue(doc);
|
||||||
|
const index = lineValue.indexOf(focusedValue);
|
||||||
let completions: Completion[] = [];
|
let completions: Completion[] = [];
|
||||||
let after = "";
|
let after = "";
|
||||||
const from = data.start;
|
const { start, end } = data;
|
||||||
const to = data.end;
|
const from = {
|
||||||
|
...start,
|
||||||
|
ch: start.ch + index,
|
||||||
|
line: cursor.line,
|
||||||
|
};
|
||||||
|
const to = {
|
||||||
|
...end,
|
||||||
|
ch: end.ch + index,
|
||||||
|
line: cursor.line,
|
||||||
|
};
|
||||||
if (
|
if (
|
||||||
cm.getRange(Pos(from.line, from.ch - 2), from) === '["' &&
|
cm.getRange(Pos(from.line, from.ch - 2), from) === '["' &&
|
||||||
cm.getRange(to, Pos(to.line, to.ch + 2)) !== '"]'
|
cm.getRange(to, Pos(to.line, to.ch + 2)) !== '"]'
|
||||||
|
|
@ -123,8 +134,7 @@ class TernServer {
|
||||||
const content = cur.data.doc;
|
const content = cur.data.doc;
|
||||||
if (content) {
|
if (content) {
|
||||||
tooltip = this.makeTooltip(
|
tooltip = this.makeTooltip(
|
||||||
node.parentNode.getBoundingClientRect().right +
|
node.parentNode.getBoundingClientRect().right + window.pageXOffset,
|
||||||
window.pageXOffset,
|
|
||||||
node.getBoundingClientRect().top + window.pageYOffset,
|
node.getBoundingClientRect().top + window.pageYOffset,
|
||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
|
|
@ -133,7 +143,23 @@ class TernServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
resolve(obj);
|
resolve(obj);
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHint(cm: CodeMirror.Editor) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.request(
|
||||||
|
cm,
|
||||||
|
{
|
||||||
|
type: "completions",
|
||||||
|
types: true,
|
||||||
|
docs: true,
|
||||||
|
urls: true,
|
||||||
|
origins: true,
|
||||||
|
caseInsensitive: true,
|
||||||
},
|
},
|
||||||
|
(error, data) => this.requestCallback(error, data, cm, resolve),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +257,7 @@ class TernServer {
|
||||||
|
|
||||||
addDoc(name: string, doc: CodeMirror.Doc) {
|
addDoc(name: string, doc: CodeMirror.Doc) {
|
||||||
const data = { doc: doc, name: name, changed: null };
|
const data = { doc: doc, name: name, changed: null };
|
||||||
this.server.addFile(name, this.docValue(data));
|
this.server.addFile(name, this.getFocusedDynamicValue(data));
|
||||||
CodeMirror.on(doc, "change", this.trackChange.bind(this));
|
CodeMirror.on(doc, "change", this.trackChange.bind(this));
|
||||||
return (this.docs[name] = data);
|
return (this.docs[name] = data);
|
||||||
}
|
}
|
||||||
|
|
@ -258,7 +284,19 @@ class TernServer {
|
||||||
if (!allowFragments) delete query.fullDocs;
|
if (!allowFragments) delete query.fullDocs;
|
||||||
query.lineCharPositions = true;
|
query.lineCharPositions = true;
|
||||||
if (!query.end) {
|
if (!query.end) {
|
||||||
query.end = pos || doc.doc.getCursor("end");
|
const lineValue = this.lineValue(doc);
|
||||||
|
const focusedValue = this.getFocusedDynamicValue(doc);
|
||||||
|
const index = lineValue.indexOf(focusedValue);
|
||||||
|
|
||||||
|
const positions = pos || doc.doc.getCursor("end");
|
||||||
|
const queryChPosition = positions.ch - index;
|
||||||
|
|
||||||
|
query.end = {
|
||||||
|
...positions,
|
||||||
|
line: 0,
|
||||||
|
ch: queryChPosition,
|
||||||
|
};
|
||||||
|
|
||||||
if (doc.doc.somethingSelected()) {
|
if (doc.doc.somethingSelected()) {
|
||||||
query.start = doc.doc.getCursor("start");
|
query.start = doc.doc.getCursor("start");
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +321,7 @@ class TernServer {
|
||||||
files.push({
|
files.push({
|
||||||
type: "full",
|
type: "full",
|
||||||
name: doc.name,
|
name: doc.name,
|
||||||
text: this.docValue(doc),
|
text: this.getFocusedDynamicValue(doc),
|
||||||
});
|
});
|
||||||
query.file = doc.name;
|
query.file = doc.name;
|
||||||
doc.changed = null;
|
doc.changed = null;
|
||||||
|
|
@ -297,11 +335,12 @@ class TernServer {
|
||||||
files.push({
|
files.push({
|
||||||
type: "full",
|
type: "full",
|
||||||
name: cur.name,
|
name: cur.name,
|
||||||
text: this.docValue(cur),
|
text: this.getFocusedDynamicValue(cur),
|
||||||
});
|
});
|
||||||
cur.changed = null;
|
cur.changed = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { query: query, files: files };
|
return { query: query, files: files };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,8 +380,17 @@ class TernServer {
|
||||||
|
|
||||||
sendDoc(doc: TernDoc) {
|
sendDoc(doc: TernDoc) {
|
||||||
this.server.request(
|
this.server.request(
|
||||||
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
{ files: [{ type: "full", name: doc.name, text: this.docValue(doc) }] },
|
files: [
|
||||||
|
// @ts-ignore
|
||||||
|
{
|
||||||
|
type: "full",
|
||||||
|
name: doc.name,
|
||||||
|
text: this.getFocusedDynamicValue(doc),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
function(error: Error) {
|
function(error: Error) {
|
||||||
if (error) window.console.error(error);
|
if (error) window.console.error(error);
|
||||||
else doc.changed = null;
|
else doc.changed = null;
|
||||||
|
|
@ -350,10 +398,35 @@ class TernServer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lineValue(doc: TernDoc) {
|
||||||
|
const cursor = doc.doc.getCursor();
|
||||||
|
|
||||||
|
return doc.doc.getLine(cursor.line);
|
||||||
|
}
|
||||||
|
|
||||||
docValue(doc: TernDoc) {
|
docValue(doc: TernDoc) {
|
||||||
return doc.doc.getValue();
|
return doc.doc.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFocusedDynamicValue(doc: TernDoc) {
|
||||||
|
const cursor = doc.doc.getCursor();
|
||||||
|
const value = this.lineValue(doc);
|
||||||
|
const stringSegments = getDynamicStringSegments(value);
|
||||||
|
const dynamicStrings = stringSegments.filter(segment => {
|
||||||
|
if (isDynamicValue(segment)) {
|
||||||
|
const index = value.indexOf(segment);
|
||||||
|
|
||||||
|
if (cursor.ch >= index && cursor.ch <= index + segment.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return dynamicStrings.length ? dynamicStrings[0] : value;
|
||||||
|
}
|
||||||
|
|
||||||
getFragmentAround(
|
getFragmentAround(
|
||||||
data: TernDoc,
|
data: TernDoc,
|
||||||
start: CodeMirror.Position,
|
start: CodeMirror.Position,
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,6 @@ export const MockCodemirrorEditor = {
|
||||||
showHint: jest.fn(),
|
showHint: jest.fn(),
|
||||||
getLine: jest.fn(),
|
getLine: jest.fn(),
|
||||||
closeHint: jest.fn(),
|
closeHint: jest.fn(),
|
||||||
|
getRange: jest.fn(),
|
||||||
|
getDoc: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12651,8 +12651,9 @@ websocket-driver@>=0.5.1:
|
||||||
websocket-extensions ">=0.1.1"
|
websocket-extensions ">=0.1.1"
|
||||||
|
|
||||||
websocket-extensions@>=0.1.1:
|
websocket-extensions@>=0.1.1:
|
||||||
version "0.1.3"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
|
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
|
||||||
|
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
|
||||||
|
|
||||||
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
|
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,6 @@ if [[ "$setup_encryption" = "true" ]];then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
read -p 'Would you like to host appsmith on a custom domain / subdomain? [Y/n]: ' setup_domain
|
read -p 'Would you like to host appsmith on a custom domain / subdomain? [Y/n]: ' setup_domain
|
||||||
setup_domain=${setup_domain:-Y}
|
setup_domain=${setup_domain:-Y}
|
||||||
if [ $setup_domain == "Y" -o $setup_domain == "y" -o $setup_domain == "yes" -o $setup_domain == "Yes" ];then
|
if [ $setup_domain == "Y" -o $setup_domain == "y" -o $setup_domain == "yes" -o $setup_domain == "Yes" ];then
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ if [ -f docker-compose.yml ]
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat > docker.env << EOF
|
cat > docker.env << EOF
|
||||||
|
# Read our documentation on how to configure these features
|
||||||
|
# https://docs.appsmith.com/v/v1.1/enabling-3p-services
|
||||||
|
|
||||||
|
# ***** Email **********
|
||||||
APPSMITH_MAIL_ENABLED=false
|
APPSMITH_MAIL_ENABLED=false
|
||||||
# APPSMITH_MAIL_HOST=
|
# APPSMITH_MAIL_HOST=
|
||||||
# APPSMITH_MAIL_PASSWORD=
|
# APPSMITH_MAIL_PASSWORD=
|
||||||
|
|
@ -15,16 +19,24 @@ APPSMITH_MAIL_ENABLED=false
|
||||||
# APPSMITH_MAIL_SMTP_AUTH=
|
# APPSMITH_MAIL_SMTP_AUTH=
|
||||||
# APPSMITH_MAIL_SMTP_TLS_ENABLED=
|
# APPSMITH_MAIL_SMTP_TLS_ENABLED=
|
||||||
# APPSMITH_MAIL_USERNAME=
|
# APPSMITH_MAIL_USERNAME=
|
||||||
# APPSMITH_MARKETPLACE_URL=
|
# ******************************
|
||||||
APPSMITH_MONGODB_URI=mongodb://$mongo_root_user:$mongo_root_password@$mongo_host/appsmith?retryWrites=true
|
|
||||||
# APPSMITH_OAUTH2_GITHUB_CLIENT_ID=
|
# ******** Google OAuth ********
|
||||||
# APPSMITH_OAUTH2_GITHUB_CLIENT_SECRET=
|
|
||||||
# APPSMITH_OAUTH2_GOOGLE_CLIENT_ID=
|
# APPSMITH_OAUTH2_GOOGLE_CLIENT_ID=
|
||||||
# APPSMITH_OAUTH2_GOOGLE_CLIENT_SECRET=
|
# APPSMITH_OAUTH2_GOOGLE_CLIENT_SECRET=
|
||||||
# APPSMITH_RAPID_API_KEY_VALUE=
|
# ******************************
|
||||||
APPSMITH_REDIS_URL=redis://redis:6379
|
|
||||||
# APPSMITH_ROLLBAR_ACCESS_TOKEN=
|
|
||||||
# APPSMITH_ROLLBAR_ENV=
|
|
||||||
# APPSMITH_SEGMENT_KEY=
|
|
||||||
|
|
||||||
|
# ********* Github OAUth **********
|
||||||
|
# APPSMITH_OAUTH2_GITHUB_CLIENT_ID=
|
||||||
|
# APPSMITH_OAUTH2_GITHUB_CLIENT_SECRET=
|
||||||
|
# *********************************
|
||||||
|
|
||||||
|
# ******** Google Maps ***********
|
||||||
|
# APPSMITH_GOOGLE_MAPS_API_KEY=
|
||||||
|
# ********************************
|
||||||
|
|
||||||
|
# ******** Database *************
|
||||||
|
APPSMITH_REDIS_URL=redis://redis:6379
|
||||||
|
APPSMITH_MONGODB_URI=mongodb://$mongo_root_user:$mongo_root_password@$mongo_host/appsmith?retryWrites=true
|
||||||
|
# *******************************
|
||||||
EOF
|
EOF
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user