Merge pull request #9 from appsmithorg/release

Release
This commit is contained in:
Trisha Anand 2020-07-15 18:44:08 +05:30 committed by GitHub
commit d2de53b60d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 732 additions and 98 deletions

View File

@ -67,10 +67,7 @@ axiosInstance.interceptors.response.use(
}
}
const errorData = error.response.data.responseMeta;
if (
errorData.status === 404 &&
errorData.error.code === 4028
) {
if (errorData.status === 404 && errorData.error.code === 4028) {
history.push(PAGE_NOT_FOUND_URL);
return Promise.reject({
code: 404,

View File

@ -6,6 +6,11 @@ export interface PublishApplicationRequest {
applicationId: string;
}
export interface ChangeAppViewAccessRequest {
applicationId: string;
publicAccess: boolean;
}
export interface PublishApplicationResponse extends ApiResponse {
data: {};
}
@ -79,6 +84,8 @@ class ApplicationApi extends Api {
static baseURL = "v1/applications/";
static publishURLPath = (applicationId: string) => `publish/${applicationId}`;
static createApplicationPath = (orgId: string) => `?orgId=${orgId}`;
static changeAppViewAccessPath = (applicationId: string) =>
`${applicationId}/changeAccess`;
static setDefaultPagePath = (request: SetDefaultPageRequest) =>
`${ApplicationApi.baseURL}${request.applicationId}/page/${request.pageId}/makeDefault`;
static publishApplication(
@ -120,6 +127,17 @@ class ApplicationApi extends Api {
): AxiosPromise<ApiResponse> {
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(
request: DeleteApplicationRequest,
): AxiosPromise<ApiResponse> {

View File

@ -82,6 +82,9 @@ export const ReduxActionTypes: { [key: string]: string } = {
STORE_AS_DATASOURCE_COMPLETE: "STORE_AS_DATASOURCE_COMPLETE",
PUBLISH_APPLICATION_INIT: "PUBLISH_APPLICATION_INIT",
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_SUCCESS: "CREATE_PAGE_SUCCESS",
FETCH_PAGE_LIST_INIT: "FETCH_PAGE_LIST_INIT",
@ -382,6 +385,7 @@ export type ApplicationPayload = {
organizationId: string;
pageCount: number;
defaultPageId?: string;
isPublic?: boolean;
userPermissions?: string[];
};

View File

@ -0,0 +1,45 @@
## Appsmith EE
We use the `@appsmith` alias to switch files between CE and EE to avoid conflicts
during regular merging.
Steps to change something for the EE version of app
- For the functionality you want to change the import statement in its consumers
to include the `@appsmith` import in the *CE repo*. For eg if you want to update the ApplicationCard
component will update this file of ApplicationList component.
*FROM*
```typescript
// ApplicationList.tsx
import ApplicationCard from "./ApplicationCard";
// OR
import ApplicationCard from "pages/Applications/ApplicationCard";
```
*TO*
```typescript
// ApplicationList.tsx
import ApplicationCard from "@appsmith/pages/Applications/ApplicationCard";
```
- Create a new file inside the *EE repo* called ApplicationCard with the same path
```shell script
$ touch app/client/src/enterprise/pages/Applications/ApplicationCard
```
- EE will now use this file, so you can export a custom ApplicationCard component for EE.
The goal is to reduce conflicts and have EE extend virtually any part of CE so selecting files to
update will be crucial. You should keep the following points in mind:
- NEVER update the CE file in the EE repo
- All the consumers of the file will need it's expected exports.
- You can import from the CE file and reexport from your EE if no changes are expected.
- The types of component props if different will cause problems in the component declaration and use.
You can export that out these props into the `@appsmith` space to avoid this problem by reimplementing them in EE.

View File

@ -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 { Breadcrumbs, IBreadcrumbProps } from "@blueprintjs/core";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import {
getCurrentOrg,
getCurrentOrgId,
} from "selectors/organizationSelectors";
import { Org } from "constants/orgConstants";
import {
BASE_URL,
APPLICATIONS_URL,
@ -22,6 +30,8 @@ import {
import AnalyticsUtil from "utils/AnalyticsUtil";
import { Skin } from "constants/DefaultTheme";
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`
display: flex;
@ -48,13 +58,16 @@ const StretchedBreadCrumb = styled(Breadcrumbs)`
}
`;
const InviteButton = styled.div`
const ShareButton = styled.div`
display: flex;
flex-grow: 1;
justify-content: flex-end;
`;
type EditorHeaderProps = {
currentOrg: Org;
currentOrgId: string;
fetchCurrentOrg: (orgId: string) => void;
isSaving?: boolean;
pageSaveError?: boolean;
pageName?: string;
@ -77,6 +90,12 @@ export const EditorHeader = (props: EditorHeaderProps) => {
page => page.pageId === props.currentPageId,
)?.pageName;
const { fetchCurrentOrg, currentOrgId } = props;
useEffect(() => {
fetchCurrentOrg(currentOrgId);
}, [fetchCurrentOrg, currentOrgId]);
const pageSelectorData: CustomizedDropdownProps = {
sections: [
{
@ -146,9 +165,22 @@ export const EditorHeader = (props: EditorHeaderProps) => {
<StyledHeader>
<StretchedBreadCrumb items={navigation} minVisibleItems={3} />
<CustomizedDropdown {...pageSelectorData} />
<InviteButton>
{/* <Button text="Share" intent="primary" filled size="small" /> */}
</InviteButton>
<ShareButton>
<FormDialogComponent
trigger={
<Button
text="Share"
intent="primary"
outline
size="small"
className="t--application-publish-btn"
/>
}
Form={ShareApplicationForm}
title={props.currentOrg.name}
/>
</ShareButton>
<LoadingContainer>{saveStatusMessage}</LoadingContainer>
<PreviewPublishSection>
<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);

View 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),
);

View File

@ -10,6 +10,8 @@ import { ERROR_MESSAGE_CREATE_APPLICATION } from "constants/messages";
const initialState: ApplicationsReduxState = {
isFetchingApplications: false,
isFetchingApplication: false,
isChangingViewAccess: false,
applicationList: [],
creatingApplication: false,
deletingApplication: false,
@ -56,6 +58,22 @@ const applicationsReducer = createReducer(initialState, {
) => {
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]: (
state: ApplicationsReduxState,
) => ({ ...state, isFetchingApplications: true }),
@ -127,6 +145,8 @@ export interface ApplicationsReduxState {
applicationList: ApplicationPayload[];
searchKeyword?: string;
isFetchingApplications: boolean;
isFetchingApplication: boolean;
isChangingViewAccess: boolean;
creatingApplication: boolean;
createApplicationError?: string;
deletingApplication: boolean;

View File

@ -17,6 +17,7 @@ import ApplicationApi, {
FetchUsersApplicationsOrgsResponse,
OrganizationApplicationObject,
ApplicationObject,
ChangeAppViewAccessRequest,
} from "api/ApplicationApi";
import { getDefaultPageId } from "./SagaUtils";
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 { setDefaultApplicationPageSuccess } from "actions/applicationActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
export function* publishApplicationSaga(
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(
action: ReduxAction<{
applicationName: string;
@ -299,6 +330,10 @@ export default function* applicationSagas() {
ReduxActionTypes.FETCH_APPLICATION_LIST_INIT,
fetchApplicationListSaga,
),
takeLatest(
ReduxActionTypes.CHANGE_APPVIEW_ACCESS_INIT,
changeAppViewAccessSaga,
),
takeLatest(
ReduxActionTypes.GET_ALL_APPLICATION_INIT,
getAllApplicationSaga,

View File

@ -129,6 +129,10 @@ export function* deleteDatasourceSaga(
history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId));
}
} catch (error) {
AppToaster.show({
message: error.message,
type: ToastType.ERROR,
});
yield put({
type: ReduxActionErrorTypes.DELETE_DATASOURCE_ERROR,
payload: { error, id: actionPayload.payload.id },

View File

@ -59,7 +59,7 @@ export function getResponseErrorMessage(response: ApiResponse) {
: undefined;
}
type ErrorPayloadType = { code?: number; message?: string, show?:boolean };
type ErrorPayloadType = { code?: number; message?: string; show?: boolean };
let ActionErrorDisplayMap: {
[key: string]: (error: ErrorPayloadType) => string;
} = {};
@ -92,7 +92,7 @@ export function* errorSaga(
} = errorAction;
const message =
error && error.message ? error.message : ActionErrorDisplayMap[type](error);
if (show && error && error.show) {
// error.code !== 401 IS A HACK!
// TODO(abhinav): Figure out a generic way

View 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);
});
});
});

View File

@ -6,6 +6,10 @@ import ecma from "tern/defs/ecmascript.json";
import lodash from "constants/defs/lodash.json";
import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator";
import CodeMirror, { Hint, Pos, cmpPos } from "codemirror";
import {
getDynamicStringSegments,
isDynamicValue,
} from "utils/DynamicBindingUtils";
const DEFS = [ecma, lodash];
const bigDoc = 250;
@ -70,6 +74,79 @@ class TernServer {
this.server.addDefs(def, true);
}
requestCallback(
error: any,
data: any,
cm: CodeMirror.Editor,
resolve: Function,
) {
if (error) return this.showError(cm, error);
if (data.completions.length === 0) {
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 after = "";
const { start, end } = data;
const from = {
...start,
ch: start.ch + index,
line: cursor.line,
};
const to = {
...end,
ch: end.ch + index,
line: cursor.line,
};
if (
cm.getRange(Pos(from.line, from.ch - 2), from) === '["' &&
cm.getRange(to, Pos(to.line, to.ch + 2)) !== '"]'
) {
after = '"]';
}
for (let i = 0; i < data.completions.length; ++i) {
const completion = data.completions[i];
let className = this.typeToIcon(completion.type);
if (data.guess) className += " " + cls + "guess";
completions.push({
text: completion.name + after,
displayText: completion.displayName || completion.name,
className: className,
data: completion,
origin: completion.origin,
});
}
completions = this.sortCompletions(completions);
const obj = { from: from, to: to, list: completions };
let tooltip: HTMLElement | undefined = undefined;
CodeMirror.on(obj, "close", () => this.remove(tooltip));
CodeMirror.on(obj, "update", () => this.remove(tooltip));
CodeMirror.on(
obj,
"select",
(cur: { data: { doc: string } }, node: any) => {
this.remove(tooltip);
const content = cur.data.doc;
if (content) {
tooltip = this.makeTooltip(
node.parentNode.getBoundingClientRect().right + window.pageXOffset,
node.getBoundingClientRect().top + window.pageYOffset,
content,
);
tooltip.className += " " + cls + "hint-doc";
}
},
);
resolve(obj);
return obj;
}
getHint(cm: CodeMirror.Editor) {
return new Promise(resolve => {
this.request(
@ -82,58 +159,7 @@ class TernServer {
origins: true,
caseInsensitive: true,
},
(error, data) => {
if (error) return this.showError(cm, error);
if (data.completions.length === 0) {
return this.showError(cm, "No suggestions");
}
let completions: Completion[] = [];
let after = "";
const from = data.start;
const to = data.end;
if (
cm.getRange(Pos(from.line, from.ch - 2), from) === '["' &&
cm.getRange(to, Pos(to.line, to.ch + 2)) !== '"]'
) {
after = '"]';
}
for (let i = 0; i < data.completions.length; ++i) {
const completion = data.completions[i];
let className = this.typeToIcon(completion.type);
if (data.guess) className += " " + cls + "guess";
completions.push({
text: completion.name + after,
displayText: completion.displayName || completion.name,
className: className,
data: completion,
origin: completion.origin,
});
}
completions = this.sortCompletions(completions);
const obj = { from: from, to: to, list: completions };
let tooltip: HTMLElement | undefined = undefined;
CodeMirror.on(obj, "close", () => this.remove(tooltip));
CodeMirror.on(obj, "update", () => this.remove(tooltip));
CodeMirror.on(
obj,
"select",
(cur: { data: { doc: string } }, node: any) => {
this.remove(tooltip);
const content = cur.data.doc;
if (content) {
tooltip = this.makeTooltip(
node.parentNode.getBoundingClientRect().right +
window.pageXOffset,
node.getBoundingClientRect().top + window.pageYOffset,
content,
);
tooltip.className += " " + cls + "hint-doc";
}
},
);
resolve(obj);
},
(error, data) => this.requestCallback(error, data, cm, resolve),
);
});
}
@ -231,7 +257,7 @@ class TernServer {
addDoc(name: string, doc: CodeMirror.Doc) {
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));
return (this.docs[name] = data);
}
@ -258,7 +284,19 @@ class TernServer {
if (!allowFragments) delete query.fullDocs;
query.lineCharPositions = true;
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()) {
query.start = doc.doc.getCursor("start");
}
@ -283,7 +321,7 @@ class TernServer {
files.push({
type: "full",
name: doc.name,
text: this.docValue(doc),
text: this.getFocusedDynamicValue(doc),
});
query.file = doc.name;
doc.changed = null;
@ -297,11 +335,12 @@ class TernServer {
files.push({
type: "full",
name: cur.name,
text: this.docValue(cur),
text: this.getFocusedDynamicValue(cur),
});
cur.changed = null;
}
}
return { query: query, files: files };
}
@ -341,8 +380,17 @@ class TernServer {
sendDoc(doc: TernDoc) {
this.server.request(
// @ts-ignore
{ files: [{ type: "full", name: doc.name, text: this.docValue(doc) }] },
{
// @ts-ignore
files: [
// @ts-ignore
{
type: "full",
name: doc.name,
text: this.getFocusedDynamicValue(doc),
},
],
},
function(error: Error) {
if (error) window.console.error(error);
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) {
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(
data: TernDoc,
start: CodeMirror.Position,

View File

@ -8,4 +8,6 @@ export const MockCodemirrorEditor = {
showHint: jest.fn(),
getLine: jest.fn(),
closeHint: jest.fn(),
getRange: jest.fn(),
getDoc: jest.fn(),
};

View File

@ -2,7 +2,7 @@
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@appsmith/*": ["*"]
"@appsmith/*": ["enterprise/*"]
}
}
}

View File

@ -12651,8 +12651,9 @@ websocket-driver@>=0.5.1:
websocket-extensions ">=0.1.1"
websocket-extensions@>=0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
version "0.1.4"
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:
version "1.0.5"

View File

@ -13,4 +13,9 @@ public class EmailConfig {
@Value("${mail.enabled}")
private boolean emailEnabled = true;
@Value("${mail.from}")
private String mailFrom;
@Value("${reply.to}")
private String replyTo;
}

View File

@ -12,5 +12,6 @@ import java.util.Set;
public class ActionViewDTO {
String id;
String name;
String pageId;
Set<String> jsonPathKeys;
}

View File

@ -9,6 +9,7 @@ import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
@ -30,7 +31,9 @@ public class EmailSender {
final EmailConfig emailConfig;
private static final InternetAddress MAIL_FROM = makeFromAddress();
private final InternetAddress MAIL_FROM;
private final InternetAddress REPLY_TO;
public static final Pattern VALID_EMAIL_ADDRESS_REGEX =
Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE);
@ -38,6 +41,9 @@ public class EmailSender {
public EmailSender(JavaMailSender javaMailSender, EmailConfig emailConfig) {
this.javaMailSender = javaMailSender;
this.emailConfig = emailConfig;
MAIL_FROM = makeFromAddress();
REPLY_TO = makeReplyTo();
}
private static boolean validateEmail(String emailStr) {
@ -75,11 +81,6 @@ public class EmailSender {
return;
}
if (MAIL_FROM == null) {
log.error("MAIL_FROM is null, no From address object to send an email. Not sending email '{}'.", subject);
return;
}
// Check if the email address is valid. It's possible for certain OAuth2 providers to not return the email ID
if (to == null || !validateEmail(to)) {
log.error("The email ID: {} is not valid. Not sending an email", to);
@ -92,7 +93,12 @@ public class EmailSender {
try {
helper.setTo(to);
helper.setFrom(MAIL_FROM);
if (MAIL_FROM != null) {
helper.setFrom(MAIL_FROM);
}
if (REPLY_TO != null) {
helper.setReplyTo(REPLY_TO);
}
helper.setSubject(subject);
helper.setText(text, true);
javaMailSender.send(mimeMessage);
@ -120,11 +126,20 @@ public class EmailSender {
return stringWriter.toString();
}
private static InternetAddress makeFromAddress() {
private InternetAddress makeFromAddress() {
try {
return new InternetAddress("hello@appsmith.com", "Appsmith");
return new InternetAddress(this.emailConfig.getMailFrom(), "Appsmith");
} catch (UnsupportedEncodingException e) {
log.error("Encoding error creating Appsmith from address.", e);
log.error("Encoding error creating Appsmith mail from address.", e);
return null;
}
}
private InternetAddress makeReplyTo() {
try {
return new InternetAddress(this.emailConfig.getReplyTo(), "Appsmith");
} catch (UnsupportedEncodingException e) {
log.error("Encoding error creating Appsmith reply to address.", e);
return null;
}
}

View File

@ -537,8 +537,13 @@ public class ActionServiceImpl extends BaseService<ActionRepository, Action, Str
ActionViewDTO actionViewDTO = new ActionViewDTO();
actionViewDTO.setId(action.getId());
actionViewDTO.setName(action.getName());
actionViewDTO.setJsonPathKeys(new HashSet<>());
actionViewDTO.getJsonPathKeys().addAll(action.getJsonPathKeys());
actionViewDTO.setPageId(action.getPageId());
if (action.getJsonPathKeys() != null && !action.getJsonPathKeys().isEmpty()) {
Set<String> jsonPathKeys;
jsonPathKeys = new HashSet<>();
jsonPathKeys.addAll(action.getJsonPathKeys());
actionViewDTO.setJsonPathKeys(jsonPathKeys);
}
return actionViewDTO;
});
}

View File

@ -148,7 +148,9 @@ public class DatasourceContextServiceImpl implements DatasourceContextService {
@Override
public AuthenticationDTO decryptSensitiveFields(AuthenticationDTO authenticationDTO) {
authenticationDTO.setPassword(encryptionService.decryptString(authenticationDTO.getPassword()));
if (authenticationDTO.getPassword() != null) {
authenticationDTO.setPassword(encryptionService.decryptString(authenticationDTO.getPassword()));
}
return authenticationDTO;
}
}

View File

@ -207,7 +207,9 @@ public class DatasourceServiceImpl extends BaseService<DatasourceRepository, Dat
datasource.getDatasourceConfiguration().getAuthentication() != null) {
AuthenticationDTO authentication = datasource.getDatasourceConfiguration().getAuthentication();
// Encrypt password before saving
authentication.setPassword(encryptionService.encryptString(authentication.getPassword()));
if (authentication.getPassword() != null) {
authentication.setPassword(encryptionService.encryptString(authentication.getPassword()));
}
datasource.getDatasourceConfiguration().setAuthentication(authentication);
}

View File

@ -43,6 +43,8 @@ spring.redis.url=${APPSMITH_REDIS_URL}
# default localhost:25 SMTP server and throw an error. If false, this error won't happen because there's no attempt
# to send an email.
mail.enabled=${APPSMITH_MAIL_ENABLED:false}
mail.from=${APPSMITH_MAIL_FROM:appsmith@localhost}
reply.to=${APPSMITH_REPLY_TO:appsmith@localhost}
spring.mail.host=${APPSMITH_MAIL_HOST:}
spring.mail.port=${APPSMITH_MAIL_PORT:}
spring.mail.username=${APPSMITH_MAIL_USERNAME:}

View File

@ -44,10 +44,8 @@ import reactor.test.StepVerifier;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;
import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS;
import static com.appsmith.server.acl.AclPermission.READ_ACTIONS;
@ -472,17 +470,33 @@ public class ActionServiceTest {
action.setActionConfiguration(actionConfiguration);
action.setDatasource(datasource);
Action action1 = new Action();
action1.setName("actionInViewModeWithoutMustacheKey");
action1.setPageId(testPage.getId());
ActionConfiguration actionConfiguration1 = new ActionConfiguration();
actionConfiguration1.setHttpMethod(HttpMethod.GET);
action1.setActionConfiguration(actionConfiguration1);
action1.setDatasource(datasource);
Mono<List<ActionViewDTO>> actionsListMono = actionService.create(action)
.then(actionService.create(action1))
.then(actionService.getActionsForViewMode(testApp.getId()).collectList());
StepVerifier
.create(actionsListMono)
.assertNext(actionsList -> {
assertThat(actionsList.size()).isGreaterThan(0);
ActionViewDTO actionViewDTO = actionsList.stream().filter(action1 -> action1.getName().equals(action.getName())).findFirst().get();
ActionViewDTO actionViewDTO = actionsList.stream().filter(dto -> dto.getName().equals(action.getName())).findFirst().get();
assertThat(actionViewDTO).isNotNull();
assertThat(actionViewDTO.getJsonPathKeys()).containsAll(Set.of(key));
assertThat(actionViewDTO.getPageId()).isEqualTo(testPage.getId());
ActionViewDTO actionViewDTO1 = actionsList.stream().filter(dto -> dto.getName().equals(action1.getName())).findFirst().get();
assertThat(actionViewDTO1).isNotNull();
assertThat(actionViewDTO1.getJsonPathKeys()).isNullOrEmpty();
assertThat(actionViewDTO1.getPageId()).isEqualTo(testPage.getId());
})
.verifyComplete();
}

View File

@ -56,7 +56,7 @@ public class DatasourceContextServiceTest {
@Test
@WithUserDetails(value = "api_user")
public void checkDecryptionofAunthenticationDTOTest() {
public void checkDecryptionOfAuthenticationDTOTest() {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
Mono<Plugin> pluginMono = pluginService.findByName("Installed Plugin Name");
@ -88,4 +88,34 @@ public class DatasourceContextServiceTest {
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void checkDecryptionOfAuthenticationDTONullPassword() {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
Mono<Plugin> pluginMono = pluginService.findByName("Installed Plugin Name");
Datasource datasource = new Datasource();
datasource.setName("test datasource name for authenticated fields decryption test null password");
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
datasourceConfiguration.setUrl("http://test.com");
AuthenticationDTO authenticationDTO = new AuthenticationDTO();
datasourceConfiguration.setAuthentication(authenticationDTO);
datasource.setDatasourceConfiguration(datasourceConfiguration);
datasource.setOrganizationId(orgId);
Mono<Datasource> datasourceMono = pluginMono.map(plugin -> {
datasource.setPluginId(plugin.getId());
return datasource;
}).flatMap(datasourceService::create);
StepVerifier
.create(datasourceMono)
.assertNext(savedDatasource -> {
AuthenticationDTO authentication = savedDatasource.getDatasourceConfiguration().getAuthentication();
AuthenticationDTO decryptedAuthentication = datasourceContextService.decryptSensitiveFields(authentication);
assertThat(decryptedAuthentication.getPassword()).isNull();
})
.verifyComplete();
}
}

View File

@ -422,7 +422,7 @@ public class DatasourceServiceTest {
@Test
@WithUserDetails(value = "api_user")
public void checkEncryptionofAunthenticationDTOTest() {
public void checkEncryptionOfAuthenticationDTOTest() {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
Mono<Plugin> pluginMono = pluginService.findByName("Installed Plugin Name");
@ -453,4 +453,35 @@ public class DatasourceServiceTest {
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void checkEncryptionOfAuthenticationDTONullPassword() {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
Mono<Plugin> pluginMono = pluginService.findByName("Installed Plugin Name");
Datasource datasource = new Datasource();
datasource.setName("test datasource name for authenticated fields encryption test null password.");
DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration();
datasourceConfiguration.setUrl("http://test.com");
AuthenticationDTO authenticationDTO = new AuthenticationDTO();
authenticationDTO.setDatabaseName("admin");
datasourceConfiguration.setAuthentication(authenticationDTO);
datasource.setDatasourceConfiguration(datasourceConfiguration);
datasource.setOrganizationId(orgId);
Mono<Datasource> datasourceMono = pluginMono.map(plugin -> {
datasource.setPluginId(plugin.getId());
return datasource;
}).flatMap(datasourceService::create);
StepVerifier
.create(datasourceMono)
.assertNext(savedDatasource -> {
AuthenticationDTO authentication = savedDatasource.getDatasourceConfiguration().getAuthentication();
assertThat(authentication.getUsername()).isNull();
assertThat(authentication.getPassword()).isNull();
})
.verifyComplete();
}
}

View File

@ -130,7 +130,6 @@ if [[ "$setup_encryption" = "true" ]];then
fi
fi
echo ""
read -p 'Would you like to host appsmith on a custom domain / subdomain? [Y/n]: ' setup_domain
setup_domain=${setup_domain:-Y}
if [ $setup_domain == "Y" -o $setup_domain == "y" -o $setup_domain == "yes" -o $setup_domain == "Yes" ];then

View File

@ -8,23 +8,46 @@ if [ -f docker-compose.yml ]
fi
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_FROM=
# APPSMITH_REPLY_TO=
# APPSMITH_MAIL_HOST=
# APPSMITH_MAIL_PASSWORD=
# APPSMITH_MAIL_PORT=
# APPSMITH_MAIL_SMTP_AUTH=
# APPSMITH_MAIL_SMTP_TLS_ENABLED=
# 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=
# APPSMITH_OAUTH2_GITHUB_CLIENT_SECRET=
# ******************************
# ******** Google OAuth ********
# APPSMITH_OAUTH2_GOOGLE_CLIENT_ID=
# APPSMITH_OAUTH2_GOOGLE_CLIENT_SECRET=
# APPSMITH_RAPID_API_KEY_VALUE=
# ******************************
# ********* 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
# *******************************
# *** EE Specific Config ********
# APPSMITH_MARKETPLACE_URL=
# APPSMITH_RAPID_API_KEY_VALUE=
# APPSMITH_ROLLBAR_ACCESS_TOKEN=
# APPSMITH_ROLLBAR_ENV=
# APPSMITH_SEGMENT_KEY=
# *******************************
EOF