diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json
index 622aac0aae..61d619d292 100644
--- a/app/client/cypress/fixtures/testdata.json
+++ b/app/client/cypress/fixtures/testdata.json
@@ -121,5 +121,6 @@
"clientSecret": "505dac16a21681f277b5fde97445be18",
"accessTokenUrl": "https://oauth.mocklab.io/oauth/token",
"oauthResponse": "169444434892406",
- "authorizationURL": "https://oauth.mocklab.io/oauth/authorize"
+ "authorizationURL": "https://oauth.mocklab.io/oauth/authorize",
+ "basicURl": "https://envyenksqii9nf3.m.pipedream.net"
}
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js
new file mode 100644
index 0000000000..3daf51823b
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js
@@ -0,0 +1,17 @@
+const dsl = require("../../../../fixtures/buttondsl.json");
+
+describe("Widget error state", function() {
+ before(() => {
+ cy.addDsl(dsl);
+ });
+ it("Check widget error state", function() {
+ cy.openPropertyPane("buttonwidget");
+
+ cy.get(".t--property-control-visible")
+ .find(".t--js-toggle")
+ .click();
+ cy.testJsontext("visible", "Test");
+
+ cy.contains(".t--widget-error-count", 1);
+ });
+});
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceBasicProfile_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceBasicProfile_spec.js
new file mode 100644
index 0000000000..cbfdba5572
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceBasicProfile_spec.js
@@ -0,0 +1,22 @@
+const testdata = require("../../../../fixtures/testdata.json");
+describe("Create a rest datasource", function() {
+ beforeEach(() => {
+ cy.startRoutesForDatasource();
+ });
+
+ it("Create a rest datasource", function() {
+ cy.NavigateToAPI_Panel();
+ cy.CreateAPI("Testapi");
+ cy.enterDatasource(testdata.basicURl);
+ cy.get(".t--store-as-datasource").click();
+ cy.addBasicProfileDetails("test", "test@123");
+ cy.saveDatasource();
+ cy.contains(".datasource-highlight", "envyenksqii9nf3.m.pipedream.net");
+ cy.SaveAndRunAPI();
+ cy.wait(2000);
+ var encodedStringBtoA = btoa("test:test@123");
+ cy.log(encodedStringBtoA);
+ cy.ResponseStatusCheck(testdata.successStatusCode);
+ cy.ResponseTextCheck("Basic ".concat(encodedStringBtoA));
+ });
+});
diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json
index 47aa855c89..defed053bb 100644
--- a/app/client/cypress/locators/DatasourcesEditor.json
+++ b/app/client/cypress/locators/DatasourcesEditor.json
@@ -35,5 +35,8 @@
"grantType": "[data-cy='authentication.grantType']",
"authorizationURL":"[data-cy='authentication.authorizationUrl'] input",
"authorisecode": "//div[contains(@class,'option') and text()='Authorization Code']",
- "saveAndAuthorize": "button:contains('Save and Authorize')"
+ "saveAndAuthorize": "button:contains('Save and Authorize')",
+ "basic": "//div[contains(@class,'option') and text()='Basic']",
+ "basicUsername": "input[name='authentication.username']",
+ "basicPassword": "input[name='authentication.password']"
}
diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js
index 617eb1e958..bcf23bfc1a 100644
--- a/app/client/cypress/support/commands.js
+++ b/app/client/cypress/support/commands.js
@@ -286,6 +286,13 @@ Cypress.Commands.add(
},
);
+Cypress.Commands.add("addBasicProfileDetails", (username, password) => {
+ cy.get(datasource.authType).click();
+ cy.xpath(datasource.basic).click();
+ cy.get(datasource.basicUsername).type(username);
+ cy.get(datasource.basicPassword).type(password);
+});
+
Cypress.Commands.add("firestoreDatasourceForm", () => {
cy.get(datasourceEditor.datasourceConfigUrl).type(
datasourceFormData["database-url"],
@@ -474,6 +481,11 @@ Cypress.Commands.add("ResponseCheck", (textTocheck) => {
cy.get(apiwidget.responseText).should("be.visible");
});
+Cypress.Commands.add("ResponseTextCheck", (textTocheck) => {
+ cy.ResponseCheck();
+ cy.get(apiwidget.responseText).contains(textTocheck);
+});
+
Cypress.Commands.add("NavigateToAPI_Panel", () => {
cy.get(pages.addEntityAPI)
.should("be.visible")
@@ -649,21 +661,20 @@ Cypress.Commands.add("SearchEntityandOpen", (apiname1) => {
});
Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => {
- cy.get(apiwidget.resourceUrl)
- .first()
- .click({ force: true })
- .type(datasource);
- /*
- cy.xpath(apiwidget.autoSuggest)
- .first()
- .click({ force: true });
- */
+ cy.enterDatasource(datasource);
cy.get(apiwidget.editResourceUrl)
.first()
.click({ force: true })
.type(path, { parseSpecialCharSequences: false });
});
+Cypress.Commands.add("enterDatasource", (datasource) => {
+ cy.get(apiwidget.resourceUrl)
+ .first()
+ .click({ force: true })
+ .type(datasource);
+});
+
Cypress.Commands.add("changeZoomLevel", (zoomValue) => {
cy.get(commonlocators.changeZoomlevel)
.last()
@@ -1132,7 +1143,7 @@ Cypress.Commands.add("widgetText", (text, inputcss, innercss) => {
cy.get(inputcss)
.first()
.trigger("mouseover", { force: true });
- cy.get(innercss).should("have.text", text);
+ cy.contains(innercss, text);
});
Cypress.Commands.add("editColName", (text) => {
diff --git a/app/client/src/components/ads/Tabs.tsx b/app/client/src/components/ads/Tabs.tsx
index a70206d531..296a31abe4 100644
--- a/app/client/src/components/ads/Tabs.tsx
+++ b/app/client/src/components/ads/Tabs.tsx
@@ -14,7 +14,6 @@ export type TabProp = {
};
const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
- user-select: none;
border-radius: 0px;
height: 100%;
.react-tabs {
diff --git a/app/client/src/components/ads/Toast.tsx b/app/client/src/components/ads/Toast.tsx
index 8685e03e1c..34154c8fb8 100644
--- a/app/client/src/components/ads/Toast.tsx
+++ b/app/client/src/components/ads/Toast.tsx
@@ -17,6 +17,7 @@ type ToastProps = ToastOptions &
duration?: number;
onUndo?: () => void;
dispatchableAction?: { type: ReduxActionType; payload: any };
+ showDebugButton?: boolean;
hideProgressBar?: boolean;
};
@@ -131,7 +132,7 @@ function ToastComponent(props: ToastProps & { undoAction?: () => void }) {
) : null}
{props.text}
- {props.variant === Variant.danger ? (
+ {props.variant === Variant.danger && props.showDebugButton ? (
) : null}
diff --git a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx
index ca49f9b40b..43b3bf3b7b 100644
--- a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx
+++ b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx
@@ -185,7 +185,9 @@ class ChartComponent extends React.Component {
getSeriesChartData = (data: ChartDataPoint[], categories: string[]) => {
const dataMap: { [key: string]: string } = {};
- if (data.length === 0) {
+
+ // if not array or (is array and array length is zero)
+ if (!Array.isArray(data) || (Array.isArray(data) && data.length === 0)) {
return [
{
value: "",
@@ -218,7 +220,7 @@ class ChartComponent extends React.Component {
const seriesChartData: Array> = this.getSeriesChartData(item.data, categories);
+ >> = this.getSeriesChartData(get(item, "data", []), categories);
return {
seriesName: item.seriesName,
data: seriesChartData,
diff --git a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx
index f5a45770fe..3487aab4b8 100644
--- a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx
+++ b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx
@@ -337,7 +337,7 @@ class DropDownComponent extends React.Component {
};
renderTag = (option: DropdownOption) => {
- return option.label;
+ return option?.label;
};
isOptionSelected = (selectedOption: DropdownOption) => {
diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx
index 09dc38510e..145807ae65 100644
--- a/app/client/src/components/editorComponents/ApiResponseView.tsx
+++ b/app/client/src/components/editorComponents/ApiResponseView.tsx
@@ -30,6 +30,7 @@ const ResponseContainer = styled.div`
${ResizerCSS}
// Initial height of bottom tabs
height: 60%;
+ width: 100%;
// Minimum height of bottom tabs as it can be resized
min-height: 36px;
background-color: ${(props) => props.theme.colors.apiPane.responseBody.bg};
diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
index 3272c2f646..1054a84b21 100644
--- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
@@ -422,20 +422,13 @@ export const DynamicAutocompleteInputWrapper = styled.div<{
height: 100%;
flex: 1;
position: relative;
- border-color: ${(props) =>
- !props.isError && props.isActive && props.skin === Skin.DARK
- ? Colors.ALABASTER
- : "transparent"};
+ border: 1px solid ${(props) => (!props.isError ? "transparent" : "red")};
> span:first-of-type {
width: 30px;
position: absolute;
right: 0px;
}
&:hover {
- border-color: ${(props) =>
- !props.isError && props.skin === Skin.DARK
- ? Colors.ALABASTER
- : "transparent"};
.lightning-menu {
background: ${(props) => (!props.isNotHover ? "#090707" : "")};
svg {
@@ -451,7 +444,6 @@ export const DynamicAutocompleteInputWrapper = styled.div<{
}
}
}
- border: 0px;
border-radius: 0px;
.lightning-menu {
z-index: 1 !important;
diff --git a/app/client/src/components/editorComponents/Debugger/index.tsx b/app/client/src/components/editorComponents/Debugger/index.tsx
index 2e7ebb0fb7..368d73be84 100644
--- a/app/client/src/components/editorComponents/Debugger/index.tsx
+++ b/app/client/src/components/editorComponents/Debugger/index.tsx
@@ -18,7 +18,7 @@ const Container = styled.div<{ errorCount: number }>`
right: 20px;
bottom: 20px;
cursor: pointer;
- padding: 19px;
+ padding: ${(props) => props.theme.spaces[6]}px;
color: ${(props) => props.theme.colors.debugger.floatingButton.color};
border-radius: 50px;
box-shadow: ${(props) => props.theme.colors.debugger.floatingButton.shadow};
@@ -33,11 +33,9 @@ const Container = styled.div<{ errorCount: number }>`
.debugger-count {
color: ${Colors.WHITE};
- font-size: 14px;
- font-weight: 500;
${(props) => getTypographyByKey(props, "h6")}
- height: 20px;
- padding: 6px;
+ height: 16px;
+ padding: ${(props) => props.theme.spaces[1]}px;
background-color: ${(props) =>
!!props.errorCount
? props.theme.colors.debugger.floatingButton.errorCount
@@ -75,7 +73,7 @@ function Debugger() {
errorCount={errorCount}
onClick={onClick}
>
-
+
{errorCount}
);
diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx
index b33a28bd0f..6e3dfc2255 100644
--- a/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx
+++ b/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx
@@ -1,5 +1,6 @@
import React, { CSSProperties } from "react";
import { ControlIcons } from "icons/ControlIcons";
+import Icon, { IconSize } from "components/ads/Icon";
import { Colors } from "constants/Colors";
import styled from "styled-components";
import { Tooltip, Classes } from "@blueprintjs/core";
@@ -34,18 +35,41 @@ const SettingsWrapper = styled.div`
`;
const WidgetName = styled.span`
- margin-right: 5px;
+ margin-right: ${(props) => props.theme.spaces[1] + 1}px;
+ margin-left: ${(props) => props.theme.spaces[3]}px;
+`;
+
+const StyledErrorIcon = styled(Icon)`
+ &:hover {
+ svg {
+ path {
+ fill: ${Colors.WHITE};
+ }
+ }
+ }
+ margin-right: ${(props) => props.theme.spaces[1]}px;
`;
type SettingsControlProps = {
toggleSettings: (e: any) => void;
activity: Activities;
name: string;
+ errorCount: number;
};
const SettingsIcon = ControlIcons.SETTINGS_CONTROL;
-const getStyles = (activity: Activities): CSSProperties | undefined => {
+const getStyles = (
+ activity: Activities,
+ errorCount: number,
+): CSSProperties | undefined => {
+ if (errorCount > 0) {
+ return {
+ background: "red",
+ color: Colors.WHITE,
+ };
+ }
+
switch (activity) {
case Activities.ACTIVE:
return {
@@ -69,7 +93,9 @@ export function SettingsControl(props: SettingsControlProps) {
const settingsIcon = (
);
+ const errorIcon = (
+
+ );
return (
+ {!!props.errorCount && (
+ <>
+ {errorIcon}
+ {props.errorCount}
+ >
+ )}
{props.name}
{settingsIcon}
diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx
index 0e5a653bde..98d46c85d1 100644
--- a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx
+++ b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx
@@ -41,6 +41,7 @@ type WidgetNameComponentProps = {
parentId?: string;
type: WidgetType;
showControls?: boolean;
+ errorCount: number;
};
export function WidgetNameComponent(props: WidgetNameComponentProps) {
@@ -95,7 +96,8 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) {
props.showControls ||
((focusedWidget === props.widgetId || selectedWidget === props.widgetId) &&
!isDragging &&
- !isResizing);
+ !isResizing) ||
+ !!props.errorCount;
let currentActivity = Activities.NONE;
if (focusedWidget === props.widgetId) currentActivity = Activities.HOVERING;
@@ -111,6 +113,7 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) {
diff --git a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx
index 94ab53e795..1355fcb2e8 100644
--- a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx
+++ b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx
@@ -173,7 +173,7 @@ class EmbeddedDatasourcePathComponent extends React.Component {
hint: () => {
const list = datasourceList
.filter((datasource) =>
- datasource.datasourceConfiguration.url.includes(
+ (datasource.datasourceConfiguration?.url || "").includes(
parsed.datasourceUrl,
),
)
diff --git a/app/client/src/constants/HelpConstants.ts b/app/client/src/constants/HelpConstants.ts
index ecd9087c09..b6004431c8 100644
--- a/app/client/src/constants/HelpConstants.ts
+++ b/app/client/src/constants/HelpConstants.ts
@@ -41,7 +41,7 @@ export const HelpMap = {
},
DROP_DOWN_WIDGET: {
path: "/widget-reference/dropdown",
- searchKey: "Dropdown",
+ searchKey: "Select",
},
RADIO_GROUP_WIDGET: {
path: "/widget-reference/radio",
diff --git a/app/client/src/entities/Datasource/RestAPIForm.ts b/app/client/src/entities/Datasource/RestAPIForm.ts
index ea4f0239e9..9abf8719a6 100644
--- a/app/client/src/entities/Datasource/RestAPIForm.ts
+++ b/app/client/src/entities/Datasource/RestAPIForm.ts
@@ -3,6 +3,7 @@ import { Property } from "entities/Action";
export enum AuthType {
NONE = "NONE",
OAuth2 = "oAuth2",
+ basic = "basic",
}
export enum GrantType {
@@ -10,7 +11,7 @@ export enum GrantType {
AuthorizationCode = "authorization_code",
}
-export type Authentication = ClientCredentials | AuthorizationCode;
+export type Authentication = ClientCredentials | AuthorizationCode | Basic;
export interface ApiDatasourceForm {
datasourceId: string;
pluginId: string;
@@ -45,3 +46,9 @@ export interface AuthorizationCode extends Oauth2Common {
isAuthorizationHeader: boolean;
isAuthorized: boolean;
}
+
+export interface Basic {
+ authenticationType: AuthType.basic;
+ username: string;
+ password: string;
+}
diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx
index 432145bec6..1282219042 100644
--- a/app/client/src/mockResponses/WidgetConfigResponse.tsx
+++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx
@@ -269,7 +269,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
{ label: "Green", value: "GREEN" },
{ label: "Red", value: "RED" },
],
- widgetName: "Dropdown",
+ widgetName: "Select",
defaultOptionValue: "GREEN",
version: 1,
isRequired: false,
diff --git a/app/client/src/mockResponses/WidgetSidebarResponse.tsx b/app/client/src/mockResponses/WidgetSidebarResponse.tsx
index cec889206f..8a58f1ae9e 100644
--- a/app/client/src/mockResponses/WidgetSidebarResponse.tsx
+++ b/app/client/src/mockResponses/WidgetSidebarResponse.tsx
@@ -36,7 +36,7 @@ const WidgetSidebarResponse: WidgetCardProps[] = [
},
{
type: "DROP_DOWN_WIDGET",
- widgetCardName: "Dropdown",
+ widgetCardName: "Select",
key: generateReactKey(),
},
{
diff --git a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx
index af1e66bbcb..7edfb2b9e4 100644
--- a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx
+++ b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx
@@ -206,7 +206,7 @@ class DatasourceRestAPIEditor extends React.Component {
if (!this.props.formData) return;
const { authentication } = this.props.formData;
- if (!authentication || !authentication.grantType) {
+ if (!authentication || !_.get(authentication, "grantType")) {
this.props.change(
"authentication.grantType",
GrantType.ClientCredentials,
@@ -225,7 +225,7 @@ class DatasourceRestAPIEditor extends React.Component {
return false;
}
- if (authentication.grantType === GrantType.AuthorizationCode) {
+ if (_.get(authentication, "grantType") === GrantType.AuthorizationCode) {
if (_.get(authentication, "isAuthorizationHeader") === undefined) {
this.props.change("authentication.isAuthorizationHeader", true);
return false;
@@ -435,6 +435,10 @@ class DatasourceRestAPIEditor extends React.Component {
label: "None",
value: AuthType.NONE,
},
+ {
+ label: "Basic",
+ value: AuthType.basic,
+ },
{
label: "OAuth 2.0",
value: AuthType.OAuth2,
@@ -455,6 +459,8 @@ class DatasourceRestAPIEditor extends React.Component {
let content;
if (authType === AuthType.OAuth2) {
content = this.renderOauth2();
+ } else if (authType === AuthType.basic) {
+ content = this.renderBasic();
}
if (content) {
return (
@@ -465,11 +471,36 @@ class DatasourceRestAPIEditor extends React.Component {
}
};
+ renderBasic = () => {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+ };
+
renderOauth2 = () => {
const { authentication } = this.props.formData;
if (!authentication) return;
let content;
- switch (authentication?.grantType) {
+ switch (_.get(authentication, "grantType")) {
case GrantType.AuthorizationCode:
content = this.renderOauth2AuthorizationCode();
break;
@@ -525,7 +556,7 @@ class DatasourceRestAPIEditor extends React.Component {
]}
/>
- {formData.authentication?.isTokenHeader && (
+ {_.get(formData.authentication, "isTokenHeader") && (
`
background-color: white;
- width: ${(props) =>
- !props.isVisible ? "0px" : props.isActionPath ? "100%" : "75%"};
+ width: ${(props) => (!props.isVisible ? "0" : "100%")};
height: 100%;
`;
diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts
index 06148a2194..dcd97f86c4 100644
--- a/app/client/src/sagas/ActionExecutionSagas.ts
+++ b/app/client/src/sagas/ActionExecutionSagas.ts
@@ -553,6 +553,7 @@ export function* executeActionSaga(
Toaster.show({
text: createMessage(ERROR_API_EXECUTE, api.name),
variant: Variant.danger,
+ showDebugButton: true,
});
} else {
PerformanceTracker.stopAsyncTracking(
@@ -607,6 +608,7 @@ export function* executeActionSaga(
Toaster.show({
text: createMessage(ERROR_API_EXECUTE, api.name),
variant: Variant.danger,
+ showDebugButton: true,
});
if (onError) {
yield put(
@@ -836,7 +838,7 @@ function* runActionSaga(
Toaster.show({
text: createMessage(ERROR_ACTION_EXECUTE_FAIL, actionObject.name),
- variant: Variant.warning,
+ variant: Variant.danger,
});
}
} else {
diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts
index ef782826ee..a7e3fddc2d 100644
--- a/app/client/src/sagas/EvaluationsSaga.ts
+++ b/app/client/src/sagas/EvaluationsSaga.ts
@@ -58,6 +58,9 @@ const evalErrorHandler = (errors: EvalError[]) => {
text: `${error.message} Node was: ${node}`,
variant: Variant.danger,
});
+ AppsmithConsole.error({
+ text: `${error.message} Node was: ${node}`,
+ });
// Send the generic error message to sentry for better grouping
Sentry.captureException(new Error(error.message), {
tags: {
@@ -94,6 +97,10 @@ const evalErrorHandler = (errors: EvalError[]) => {
Toaster.show({
text: createMessage(ERROR_EVAL_TRIGGER, error.message),
variant: Variant.danger,
+ showDebugButton: true,
+ });
+ AppsmithConsole.error({
+ text: createMessage(ERROR_EVAL_TRIGGER, error.message),
});
break;
}
diff --git a/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts b/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts
index e6f1dbf14e..a7fe5b1453 100644
--- a/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts
+++ b/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts
@@ -8,6 +8,7 @@ import {
ClientCredentials,
GrantType,
Oauth2Common,
+ Basic,
} from "entities/Datasource/RestAPIForm";
import _ from "lodash";
@@ -103,6 +104,14 @@ const formToDatasourceAuthentication = (
};
}
}
+ if (authType === AuthType.basic) {
+ const basic: Basic = {
+ authenticationType: AuthType.basic,
+ username: authentication.username,
+ password: authentication.password,
+ };
+ return basic;
+ }
return null;
};
@@ -154,6 +163,14 @@ const datasourceToFormAuthentication = (
};
}
}
+ if (authType === AuthType.basic) {
+ const basic: Basic = {
+ authenticationType: AuthType.basic,
+ username: authentication.username || "",
+ password: authentication.password || "",
+ };
+ return basic;
+ }
};
const isClientCredentials = (
diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts
index 0f0467d9ed..865496c010 100644
--- a/app/client/src/utils/autocomplete/EntityDefinitions.ts
+++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts
@@ -76,7 +76,7 @@ export const entityDefinitions = {
},
DROP_DOWN_WIDGET: {
"!doc":
- "Dropdown is used to capture user input/s from a specified list of permitted inputs. A Dropdown can capture a single choice as well as multiple choices",
+ "Select is used to capture user input/s from a specified list of permitted inputs. A Select can capture a single choice as well as multiple choices",
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
isVisible: isVisible,
selectedOptionValue: {
diff --git a/app/client/src/utils/helpers.test.ts b/app/client/src/utils/helpers.test.ts
new file mode 100644
index 0000000000..b9a613979c
--- /dev/null
+++ b/app/client/src/utils/helpers.test.ts
@@ -0,0 +1,86 @@
+import { flattenObject } from "./helpers";
+
+describe("flattenObject test", () => {
+ it("Check if non nested object is returned correctly", () => {
+ const testObject = {
+ isVisible: true,
+ isDisabled: false,
+ tableData: false,
+ };
+
+ expect(flattenObject(testObject)).toStrictEqual(testObject);
+ });
+
+ it("Check if nested objects are returned correctly", () => {
+ const tests = [
+ {
+ input: {
+ isVisible: true,
+ isDisabled: false,
+ tableData: false,
+ settings: {
+ color: [
+ {
+ headers: {
+ left: true,
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ isVisible: true,
+ isDisabled: false,
+ tableData: false,
+ "settings.color[0].headers.left": true,
+ },
+ },
+ {
+ input: {
+ isVisible: true,
+ isDisabled: false,
+ tableData: false,
+ settings: {
+ color: true,
+ },
+ },
+ output: {
+ isVisible: true,
+ isDisabled: false,
+ tableData: false,
+ "settings.color": true,
+ },
+ },
+ {
+ input: {
+ numbers: [1, 2, 3],
+ color: { header: "red" },
+ },
+ output: {
+ "numbers[0]": 1,
+ "numbers[1]": 2,
+ "numbers[2]": 3,
+ "color.header": "red",
+ },
+ },
+ {
+ input: {
+ name: null,
+ color: { header: {} },
+ users: {
+ id: undefined,
+ },
+ },
+ output: {
+ "color.header": {},
+ name: null,
+ "users.id": undefined,
+ },
+ },
+ ];
+
+ tests.map((test) =>
+ expect(flattenObject(test.input)).toStrictEqual(test.output),
+ );
+ });
+});
diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx
index 6a9f52bf05..5534d658df 100644
--- a/app/client/src/utils/helpers.tsx
+++ b/app/client/src/utils/helpers.tsx
@@ -287,3 +287,28 @@ export const scrollbarWidth = () => {
document.body.removeChild(scrollDiv);
return scrollbarWidth;
};
+
+// Flatten object
+// From { isValid: false, settings: { color: false}}
+// To { isValid: false, settings.color: false}
+export const flattenObject = (data: Record) => {
+ const result: Record = {};
+ function recurse(cur: any, prop: any) {
+ if (Object(cur) !== cur) {
+ result[prop] = cur;
+ } else if (Array.isArray(cur)) {
+ for (let i = 0, l = cur.length; i < l; i++)
+ recurse(cur[i], prop + "[" + i + "]");
+ if (cur.length == 0) result[prop] = [];
+ } else {
+ let isEmpty = true;
+ for (const p in cur) {
+ isEmpty = false;
+ recurse(cur[p], prop ? prop + "." + p : p);
+ }
+ if (isEmpty && prop) result[prop] = {};
+ }
+ }
+ recurse(data, "");
+ return result;
+};
diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx
index baa8de6de3..0e47cd7579 100644
--- a/app/client/src/widgets/BaseWidget.tsx
+++ b/app/client/src/widgets/BaseWidget.tsx
@@ -15,6 +15,7 @@ import {
CSSUnit,
CONTAINER_GRID_PADDING,
} from "constants/WidgetConstants";
+import { memoize } from "lodash";
import DraggableComponent from "components/editorComponents/DraggableComponent";
import ResizableComponent from "components/editorComponents/ResizableComponent";
import { WidgetExecuteActionPayload } from "constants/AppsmithActionConstants/ActionConstants";
@@ -35,6 +36,7 @@ import OverlayCommentsWrapper from "comments/inlineComments/OverlayCommentsWrapp
import PreventInteractionsOverlay from "components/editorComponents/PreventInteractionsOverlay";
import AppsmithConsole from "utils/AppsmithConsole";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
+import { flattenObject } from "utils/helpers";
/***
* BaseWidget
@@ -176,6 +178,10 @@ abstract class BaseWidget<
};
}
+ getErrorCount = memoize((invalidProps) => {
+ return Object.values(flattenObject(invalidProps)).filter((e) => !!e).length;
+ }, JSON.stringify);
+
render() {
return this.getWidgetView();
}
@@ -209,6 +215,7 @@ abstract class BaseWidget<
<>
{!this.props.disablePropertyPane && (
{
label: "Date Format",
controlType: "DROP_DOWN",
isJSConvertible: true,
- optionWidth: "320px",
+ optionWidth: "340px",
options: [
{
label: moment().format("YYYY-MM-DDTHH:mm:ss.sssZ"),
@@ -62,9 +62,9 @@ class DatePickerWidget extends BaseWidget {
value: "YYYY-MM-DDTHH:mm:ss",
},
{
- label: moment().format("YYYY-MM-DD hh:mm:ss"),
- subText: "YYYY-MM-DD hh:mm:ss",
- value: "YYYY-MM-DD hh:mm:ss",
+ label: moment().format("YYYY-MM-DD hh:mm:ss A"),
+ subText: "YYYY-MM-DD hh:mm:ss A",
+ value: "YYYY-MM-DD hh:mm:ss A",
},
{
label: moment().format("DD/MM/YYYY HH:mm"),
diff --git a/app/server/README.md b/app/server/README.md
index d59286da54..ec647f1efb 100644
--- a/app/server/README.md
+++ b/app/server/README.md
@@ -1,6 +1,7 @@
# Appsmith Server
This is the server-side repository for the Appsmith framework.
+For details on setting up your development machine, please refer to the [Setup Guide](https://github.com/appsmithorg/appsmith/blob/release/contributions/ServerSetup.md)
### How to build
```bash
diff --git a/app/server/appsmith-interfaces/pom.xml b/app/server/appsmith-interfaces/pom.xml
index 745124bb92..378f1c46d2 100644
--- a/app/server/appsmith-interfaces/pom.xml
+++ b/app/server/appsmith-interfaces/pom.xml
@@ -107,7 +107,7 @@
commons-io
commons-io
- 2.6
+ 2.7
compile
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/Authentication.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/Authentication.java
index ea08952678..5fa624330f 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/Authentication.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/Authentication.java
@@ -5,6 +5,7 @@ public class Authentication {
// Auth type constants
public static final String DB_AUTH = "dbAuth";
public static final String OAUTH2 = "oAuth2";
+ public static final String BASIC = "basic";
// Request parameter names
public static final String CLIENT_ID = "client_id";
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java
index 1c002f2562..2e50af7617 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java
@@ -22,7 +22,8 @@ import java.util.Set;
defaultImpl = DBAuth.class)
@JsonSubTypes({
@JsonSubTypes.Type(value = DBAuth.class, name = Authentication.DB_AUTH),
- @JsonSubTypes.Type(value = OAuth2.class, name = Authentication.OAUTH2)
+ @JsonSubTypes.Type(value = OAuth2.class, name = Authentication.OAUTH2),
+ @JsonSubTypes.Type(value = BasicAuth.class, name = Authentication.BASIC)
})
public class AuthenticationDTO implements AppsmithDomain {
// In principle, this class should've been abstract. However, when this class is abstract, Spring's deserialization
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BasicAuth.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BasicAuth.java
new file mode 100644
index 0000000000..243ba06fcb
--- /dev/null
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BasicAuth.java
@@ -0,0 +1,26 @@
+package com.appsmith.external.models;
+
+import com.appsmith.external.annotations.documenttype.DocumentType;
+import com.appsmith.external.annotations.encryption.Encrypted;
+import com.appsmith.external.constants.Authentication;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+@Getter
+@Setter
+@ToString
+@NoArgsConstructor
+@AllArgsConstructor
+@DocumentType(Authentication.BASIC)
+public class BasicAuth extends AuthenticationDTO {
+
+ String username;
+
+ @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
+ @Encrypted
+ String password;
+}
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java
index 72b7f0faf1..96ac1c4a82 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java
+++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java
@@ -62,6 +62,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY;
@@ -89,6 +91,40 @@ public class MongoPlugin extends BasePlugin {
private static final int SMART_BSON_SUBSTITUTION_INDEX = 0;
+ /*
+ * - The regex matches the following two pattern types:
+ * - mongodb+srv://user:pass@some-url/some-db....
+ * - mongodb://user:pass@some-url:port,some-url:port,../some-db....
+ * - It has been grouped like this: (mongodb+srv://)((user):(pass))(@some-url/(some-db....))
+ */
+ private static final String MONGO_URI_REGEX = "^(mongodb(\\+srv)?:\\/\\/)((.+):(.+))(@.+\\/(.+))$";
+
+ private static final int REGEX_GROUP_HEAD = 1;
+
+ private static final int REGEX_GROUP_USERNAME = 4;
+
+ private static final int REGEX_GROUP_PASSWORD = 5;
+
+ private static final int REGEX_GROUP_TAIL = 6;
+
+ private static final int REGEX_GROUP_DBNAME = 7;
+
+ private static final String KEY_USERNAME = "username";
+
+ private static final String KEY_PASSWORD = "password";
+
+ private static final String KEY_URI_HEAD = "uriHead";
+
+ private static final String KEY_URI_TAIL = "uriTail";
+
+ private static final String KEY_URI_DBNAME = "dbName";
+
+ private static final String YES = "Yes";
+
+ private static final int DATASOURCE_CONFIG_USE_MONGO_URI_PROPERTY_INDEX = 0;
+
+ private static final int DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX = 1;
+
private static final Integer MONGO_COMMAND_EXCEPTION_UNAUTHORIZED_ERROR_CODE = 13;
public MongoPlugin(PluginWrapper wrapper) {
@@ -367,9 +403,83 @@ public class MongoPlugin extends BasePlugin {
.subscribeOn(scheduler);
}
- public static String buildClientURI(DatasourceConfiguration datasourceConfiguration) throws AppsmithPluginException {
- StringBuilder builder = new StringBuilder();
+ private boolean isUsingURI(DatasourceConfiguration datasourceConfiguration) {
+ List properties = datasourceConfiguration.getProperties();
+ if (properties != null && properties.size() > DATASOURCE_CONFIG_USE_MONGO_URI_PROPERTY_INDEX
+ && properties.get(DATASOURCE_CONFIG_USE_MONGO_URI_PROPERTY_INDEX) != null
+ && YES.equals(properties.get(DATASOURCE_CONFIG_USE_MONGO_URI_PROPERTY_INDEX).getValue())) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean hasNonEmptyURI(DatasourceConfiguration datasourceConfiguration) {
+ List properties = datasourceConfiguration.getProperties();
+ if (properties != null && properties.size() > DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX
+ && properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX) != null
+ && !StringUtils.isEmpty(properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).getValue())) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private Map extractInfoFromConnectionStringURI(String uri, String regex) {
+ if (uri.matches(regex)) {
+ Pattern pattern = Pattern.compile(regex);
+ Matcher matcher = pattern.matcher(uri);
+ if (matcher.find()) {
+ Map extractedInfoMap = new HashMap();
+ String username = matcher.group(REGEX_GROUP_USERNAME);
+ extractedInfoMap.put(KEY_USERNAME, username == null ? "" : username);
+ String password = matcher.group(REGEX_GROUP_PASSWORD);
+ extractedInfoMap.put(KEY_PASSWORD, password == null ? "" : password);
+ extractedInfoMap.put(KEY_URI_HEAD, matcher.group(REGEX_GROUP_HEAD));
+ extractedInfoMap.put(KEY_URI_TAIL, matcher.group(REGEX_GROUP_TAIL));
+ extractedInfoMap.put(KEY_URI_DBNAME, matcher.group(REGEX_GROUP_DBNAME).split("\\?")[0]);
+ return extractedInfoMap;
+ }
+ }
+
+ return null;
+ }
+
+ private String buildURIfromExtractedInfo(Map extractedInfo, String password) {
+ return extractedInfo.get(KEY_URI_HEAD) + (extractedInfo.get(KEY_USERNAME) == null ? "" :
+ extractedInfo.get(KEY_USERNAME) + ":") + (password == null ? "" : password)
+ + extractedInfo.get(KEY_URI_TAIL);
+ }
+
+ public String buildClientURI(DatasourceConfiguration datasourceConfiguration) throws AppsmithPluginException {
+ List properties = datasourceConfiguration.getProperties();
+ if (isUsingURI(datasourceConfiguration)) {
+ if (hasNonEmptyURI(datasourceConfiguration)) {
+ String uriWithHiddenPassword =
+ (String)properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).getValue();
+ Map extractedInfo = extractInfoFromConnectionStringURI(uriWithHiddenPassword, MONGO_URI_REGEX);
+ if (extractedInfo != null) {
+ String password = ((DBAuth)datasourceConfiguration.getAuthentication()).getPassword();
+ return buildURIfromExtractedInfo(extractedInfo, password);
+ }
+ else {
+ throw new AppsmithPluginException(
+ AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
+ "Appsmith server has failed to parse the Mongo connection string URI. Please check " +
+ "if the URI has the correct format."
+ );
+ }
+ }
+ else {
+ throw new AppsmithPluginException(
+ AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
+ "Could not find any Mongo connection string URI. Please edit the 'Mongo Connection String" +
+ " URI' field to provide the URI to connect to."
+ );
+ }
+ }
+
+ StringBuilder builder = new StringBuilder();
final Connection connection = datasourceConfiguration.getConnection();
final List endpoints = datasourceConfiguration.getEndpoints();
@@ -483,52 +593,89 @@ public class MongoPlugin extends BasePlugin {
@Override
public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) {
Set invalids = new HashSet<>();
+ List properties = datasourceConfiguration.getProperties();
+ if (isUsingURI(datasourceConfiguration)) {
+ if (!hasNonEmptyURI(datasourceConfiguration)) {
+ invalids.add("'Mongo Connection String URI' field is empty. Please edit the 'Mongo Connection " +
+ "URI' field to provide a connection uri to connect with.");
+ } else {
+ String mongoUri = (String)properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).getValue();
+ if (!mongoUri.matches(MONGO_URI_REGEX)) {
+ invalids.add("Mongo Connection String URI does not seem to be in the correct format. Please " +
+ "check the URI once.");
+ } else {
+ Map extractedInfo = extractInfoFromConnectionStringURI(mongoUri, MONGO_URI_REGEX);
+ if (extractedInfo == null) {
+ invalids.add("Mongo Connection String URI does not seem to be in the correct format. " +
+ "Please check the URI once.");
+ } else {
+ String mongoUriWithHiddenPassword = buildURIfromExtractedInfo(extractedInfo, "****");
+ properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).setValue(mongoUriWithHiddenPassword);
+ DBAuth authentication = datasourceConfiguration.getAuthentication() == null ?
+ new DBAuth() : (DBAuth) datasourceConfiguration.getAuthentication();
+ authentication.setUsername((String) extractedInfo.get(KEY_USERNAME));
+ authentication.setPassword((String) extractedInfo.get(KEY_PASSWORD));
+ authentication.setDatabaseName((String) extractedInfo.get(KEY_URI_DBNAME));
+ datasourceConfiguration.setAuthentication(authentication);
- List endpoints = datasourceConfiguration.getEndpoints();
- if (CollectionUtils.isEmpty(endpoints)) {
- invalids.add("Missing endpoint(s).");
+ // remove any default db set via form auto-fill via browser
+ if (datasourceConfiguration.getConnection() != null) {
+ datasourceConfiguration.getConnection().setDefaultDatabaseName(null);
+ }
+ }
+ }
+ }
+ } else {
+ List endpoints = datasourceConfiguration.getEndpoints();
+ if (CollectionUtils.isEmpty(endpoints)) {
+ invalids.add("Missing endpoint(s).");
+
+ } else if (Connection.Type.REPLICA_SET.equals(datasourceConfiguration.getConnection().getType())) {
+ if (endpoints.size() == 1 && endpoints.get(0).getPort() != null) {
+ invalids.add("REPLICA_SET connections should not be given a port." +
+ " If you are trying to specify all the shards, please add more than one.");
+ }
- } else if (Connection.Type.REPLICA_SET.equals(datasourceConfiguration.getConnection().getType())) {
- if (endpoints.size() == 1 && endpoints.get(0).getPort() != null) {
- invalids.add("REPLICA_SET connections should not be given a port." +
- " If you are trying to specify all the shards, please add more than one.");
}
- }
+ if (!CollectionUtils.isEmpty(endpoints)) {
+ boolean usingUri = endpoints
+ .stream()
+ .anyMatch(endPoint -> endPoint.getHost().matches(MONGO_URI_REGEX));
- if (!CollectionUtils.isEmpty(endpoints)) {
- boolean usingSrvUrl = endpoints
- .stream()
- .anyMatch(endPoint -> endPoint.getHost().contains("mongodb+srv"));
-
- if (usingSrvUrl) {
- invalids.add("MongoDb SRV URLs are not yet supported. Please extract the individual fields from " +
- "the SRV URL into the datasource configuration form.");
- }
- }
-
- DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
- if (authentication != null) {
- DBAuth.Type authType = authentication.getAuthType();
-
- if (authType == null || !VALID_AUTH_TYPES.contains(authType)) {
- invalids.add("Invalid authType. Must be one of " + VALID_AUTH_TYPES_STR);
+ if (usingUri) {
+ invalids.add("It seems that you are trying to use a mongo connection string URI. Please " +
+ "extract relevant fields and fill the form with extracted values. For " +
+ "details, please check out the Appsmith's documentation for Mongo database. " +
+ "Alternatively, you may use 'Import from Connection String URI' option from the " +
+ "dropdown labelled 'Use Mongo Connection String URI' to use the URI connection string" +
+ " directly.");
+ }
}
- if (StringUtils.isEmpty(authentication.getDatabaseName())) {
- invalids.add("Missing database name.");
+ DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
+ if (authentication != null) {
+ DBAuth.Type authType = authentication.getAuthType();
+
+ if (authType == null || !VALID_AUTH_TYPES.contains(authType)) {
+ invalids.add("Invalid authType. Must be one of " + VALID_AUTH_TYPES_STR);
+ }
+
+ if (StringUtils.isEmpty(authentication.getDatabaseName())) {
+ invalids.add("Missing database name.");
+ }
+
}
- }
-
- /*
- * - Ideally, it is never expected to be null because the SSL dropdown is set to a initial value.
- */
- if (datasourceConfiguration.getConnection() == null
- || datasourceConfiguration.getConnection().getSsl() == null
- || datasourceConfiguration.getConnection().getSsl().getAuthType() == null) {
- invalids.add("Appsmith server has failed to fetch SSL configuration from datasource configuration " +
- "form. Please reach out to Appsmith customer support to resolve this.");
+ /*
+ * - Ideally, it is never expected to be null because the SSL dropdown is set to a initial value.
+ */
+ if (datasourceConfiguration.getConnection() == null
+ || datasourceConfiguration.getConnection().getSsl() == null
+ || datasourceConfiguration.getConnection().getSsl().getAuthType() == null) {
+ invalids.add("Appsmith server has failed to fetch SSL configuration from datasource configuration " +
+ "form. Please reach out to Appsmith customer support to resolve this.");
+ }
}
return invalids;
@@ -581,6 +728,7 @@ public class MongoPlugin extends BasePlugin {
final DatasourceStructure structure = new DatasourceStructure();
List tables = new ArrayList<>();
structure.setTables(tables);
+
final MongoDatabase database = mongoClient.getDatabase(getDatabaseName(datasourceConfiguration));
return Flux.from(database.listCollectionNames())
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json
index 0ee963a8f7..1878ba55df 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json
+++ b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json
@@ -3,6 +3,47 @@
{
"sectionName": "Connection",
"children": [
+ {
+ "label": "Use Mongo Connection String URI Key",
+ "configProperty": "datasourceConfiguration.properties[0].key",
+ "controlType": "INPUT_TEXT",
+ "initialValue": "Use Mongo Connection String URI",
+ "hidden": true
+ },
+ {
+ "label": "Use Mongo Connection String URI",
+ "configProperty": "datasourceConfiguration.properties[0].value",
+ "controlType": "DROP_DOWN",
+ "initialValue": "No",
+ "options": [
+ {
+ "label": "Yes",
+ "value": "Yes"
+ },
+ {
+ "label": "No",
+ "value": "No"
+ }
+ ]
+ },
+ {
+ "label": "Connection String URI Key",
+ "configProperty": "datasourceConfiguration.properties[1].key",
+ "controlType": "INPUT_TEXT",
+ "initialValue": "Connection String URI",
+ "hidden": true
+ },
+ {
+ "label": "Connection String URI",
+ "placeholderText": "mongodb+srv://:@test-db.swrsq.mongodb.net/myDatabase",
+ "configProperty": "datasourceConfiguration.properties[1].value",
+ "controlType": "INPUT_TEXT",
+ "hidden": {
+ "path": "datasourceConfiguration.properties[0].value",
+ "comparison": "NOT_EQUALS",
+ "value": "Yes"
+ }
+ },
{
"label": "Connection Mode",
"configProperty": "datasourceConfiguration.connection.mode",
@@ -17,7 +58,12 @@
"label": "Read / Write",
"value": "READ_WRITE"
}
- ]
+ ],
+ "hidden": {
+ "path": "datasourceConfiguration.properties[0].value",
+ "comparison": "EQUALS",
+ "value": "Yes"
+ }
},
{
"label": "Connection Type",
@@ -33,7 +79,12 @@
"label": "Replica set",
"value": "REPLICA_SET"
}
- ]
+ ],
+ "hidden": {
+ "path": "datasourceConfiguration.properties[0].value",
+ "comparison": "EQUALS",
+ "value": "Yes"
+ }
},
{
"sectionName": null,
@@ -44,13 +95,23 @@
"controlType": "KEYVALUE_ARRAY",
"validationMessage": "Please enter a valid host",
"validationRegex": "^((?![/:]).)*$",
- "placeholderText": "myapp.abcde.mongodb.net"
+ "placeholderText": "myapp.abcde.mongodb.net",
+ "hidden": {
+ "path": "datasourceConfiguration.properties[0].value",
+ "comparison": "EQUALS",
+ "value": "Yes"
+ }
},
{
"label": "Port",
"configProperty": "datasourceConfiguration.endpoints[*].port",
"dataType": "NUMBER",
- "controlType": "KEYVALUE_ARRAY"
+ "controlType": "KEYVALUE_ARRAY",
+ "hidden": {
+ "path": "datasourceConfiguration.properties[0].value",
+ "comparison": "EQUALS",
+ "value": "Yes"
+ }
}
]
},
@@ -58,12 +119,22 @@
"label": "Default Database Name",
"placeholderText": "(Optional)",
"configProperty": "datasourceConfiguration.connection.defaultDatabaseName",
- "controlType": "INPUT_TEXT"
+ "controlType": "INPUT_TEXT",
+ "hidden": {
+ "path": "datasourceConfiguration.properties[0].value",
+ "comparison": "EQUALS",
+ "value": "Yes"
+ }
}
]
},
{
"sectionName": "Authentication",
+ "hidden": {
+ "path": "datasourceConfiguration.properties[0].value",
+ "comparison": "EQUALS",
+ "value": "Yes"
+ },
"children": [
{
"label": "Database Name",
@@ -108,13 +179,18 @@
"controlType": "INPUT_TEXT",
"placeholderText": "Password",
"encrypted": true
- }
+ }
]
}
]
},
{
"sectionName": "SSL (optional)",
+ "hidden": {
+ "path": "datasourceConfiguration.properties[0].value",
+ "comparison": "EQUALS",
+ "value": "Yes"
+ },
"children": [
{
"label": "SSL Mode",
diff --git a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java
index 7013c2f308..1709486140 100644
--- a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java
+++ b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java
@@ -473,16 +473,129 @@ public class MongoPluginTest {
}
@Test
- public void testErrorMessageOnSrvUrl() {
+ public void testErrorMessageOnSrvUriWithFormInterface() {
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
- dsConfig.getEndpoints().get(0).setHost("mongodb+srv:://url.net");
+ dsConfig.getEndpoints().get(0).setHost("mongodb+srv://user:pass@url.net/dbName");
+ dsConfig.setProperties(List.of(new Property("Import from URI", "No")));
Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig));
StepVerifier.create(invalidsMono)
.assertNext(invalids -> {
assertTrue(invalids
.stream()
- .anyMatch(error -> error.contains("MongoDb SRV URLs are not yet supported")));
+ .anyMatch(error -> error.contains("It seems that you are trying to use a mongo connection" +
+ " string URI. Please extract relevant fields and fill the form with extracted " +
+ "values. For details, please check out the Appsmith's documentation for Mongo " +
+ "database. Alternatively, you may use 'Import from Connection String URI' option " +
+ "from the dropdown labelled 'Use Mongo Connection String URI' to use the URI " +
+ "connection string directly.")));
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testErrorMessageOnNonSrvUri() {
+ DatasourceConfiguration dsConfig = createDatasourceConfiguration();
+ dsConfig.getEndpoints().get(0).setHost("mongodb://user:pass@url.net:1234,url.net:1234/dbName");
+ dsConfig.setProperties(List.of(new Property("Import from URI", "No")));
+ Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig));
+
+ StepVerifier.create(invalidsMono)
+ .assertNext(invalids -> {
+ assertTrue(invalids
+ .stream()
+ .anyMatch(error -> error.contains("It seems that you are trying to use a mongo connection" +
+ " string URI. Please extract relevant fields and fill the form with extracted " +
+ "values. For details, please check out the Appsmith's documentation for Mongo " +
+ "database. Alternatively, you may use 'Import from Connection String URI' option " +
+ "from the dropdown labelled 'Use Mongo Connection String URI' to use the URI " +
+ "connection string directly.")));
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testInvalidsOnMissingUri() {
+ DatasourceConfiguration dsConfig = createDatasourceConfiguration();
+ dsConfig.setProperties(List.of(new Property("Import from URI", "Yes")));
+ Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig));
+
+ StepVerifier.create(invalidsMono)
+ .assertNext(invalids -> {
+ assertTrue(invalids
+ .stream()
+ .anyMatch(error -> error.contains("'Mongo Connection String URI' field is empty. Please " +
+ "edit the 'Mongo Connection URI' field to provide a connection uri to connect with.")));
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testInvalidsOnBadSrvUriFormat() {
+ DatasourceConfiguration dsConfig = createDatasourceConfiguration();
+ List properties = new ArrayList<>();
+ properties.add(new Property("Import from URI", "Yes"));
+ properties.add(new Property("Srv Url", "mongodb+srv::username:password//url.net"));
+ dsConfig.setProperties(properties);
+ Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig));
+
+ StepVerifier.create(invalidsMono)
+ .assertNext(invalids -> {
+ assertTrue(invalids
+ .stream()
+ .anyMatch(error -> error.contains("Mongo Connection String URI does not seem to be in the" +
+ " correct format. Please check the URI once.")));
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testInvalidsOnBadNonSrvUriFormat() {
+ DatasourceConfiguration dsConfig = createDatasourceConfiguration();
+ List properties = new ArrayList<>();
+ properties.add(new Property("Import from URI", "Yes"));
+ properties.add(new Property("Srv Url", "mongodb::username:password//url.net"));
+ dsConfig.setProperties(properties);
+ Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig));
+
+ StepVerifier.create(invalidsMono)
+ .assertNext(invalids -> {
+ assertTrue(invalids
+ .stream()
+ .anyMatch(error -> error.contains("Mongo Connection String URI does not seem to be in the" +
+ " correct format. Please check the URI once.")));
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testInvalidsEmptyOnCorrectSrvUriFormat() {
+ DatasourceConfiguration dsConfig = createDatasourceConfiguration();
+ List properties = new ArrayList<>();
+ properties.add(new Property("Import from URI", "Yes"));
+ properties.add(new Property("Srv Url", "mongodb+srv://username:password@url.net/dbname"));
+ dsConfig.setProperties(properties);
+ Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig));
+
+ StepVerifier.create(invalidsMono)
+ .assertNext(invalids -> {
+ assertTrue(invalids.isEmpty());
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testInvalidsEmptyOnCorrectNonSrvUriFormat() {
+ DatasourceConfiguration dsConfig = createDatasourceConfiguration();
+ List properties = new ArrayList<>();
+ properties.add(new Property("Import from URI", "Yes"));
+ properties.add(new Property("Srv Url", "mongodb://username:password@url-1.net:1234,url-2:1234/dbname"));
+ dsConfig.setProperties(properties);
+ Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig));
+
+ StepVerifier.create(invalidsMono)
+ .assertNext(invalids -> {
+ assertTrue(invalids.isEmpty());
})
.verifyComplete();
}
diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java
index ccc0a8bf30..d7fc95c761 100644
--- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java
+++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java
@@ -3,6 +3,7 @@ package com.external.connections;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.models.AuthenticationDTO;
+import com.appsmith.external.models.BasicAuth;
import com.appsmith.external.models.OAuth2;
import reactor.core.publisher.Mono;
@@ -22,6 +23,8 @@ public class APIConnectionFactory {
} else {
return Mono.empty();
}
+ } else if (authenticationType instanceof BasicAuth) {
+ return Mono.from(BasicAuthentication.create((BasicAuth) authenticationType));
} else {
return Mono.empty();
}
diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/BasicAuthentication.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/BasicAuthentication.java
new file mode 100644
index 0000000000..490858e373
--- /dev/null
+++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/BasicAuthentication.java
@@ -0,0 +1,45 @@
+package com.external.connections;
+
+import com.appsmith.external.models.BasicAuth;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ClientResponse;
+import org.springframework.web.reactive.function.client.ExchangeFunction;
+import reactor.core.publisher.Mono;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+@Setter
+@Getter
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class BasicAuthentication extends APIConnection {
+
+ private String encodedAuthorizationHeader;
+ final private static String HEADER_PREFIX = "Basic ";
+
+ public static Mono create(BasicAuth basicAuth) {
+ final BasicAuthentication basicAuthentication = new BasicAuthentication();
+ final String decodedAuthorizationHeader = basicAuth.getUsername() + ":" + basicAuth.getPassword();
+
+ basicAuthentication.setEncodedAuthorizationHeader(
+ Base64.getEncoder().encodeToString(decodedAuthorizationHeader.getBytes(StandardCharsets.UTF_8)));
+
+ return Mono.just(basicAuthentication);
+ }
+
+
+ @Override
+ public Mono filter(ClientRequest request, ExchangeFunction next) {
+ return Mono.justOrEmpty(ClientRequest.from(request)
+ .headers(headers -> headers.set("Authorization", HEADER_PREFIX + this.getEncodedAuthorizationHeader()))
+ .build())
+ // Carry on to next exchange function
+ .flatMap(next::exchange)
+ // Default to next exchange function if something went wrong
+ .switchIfEmpty(next.exchange(request));
+ }
+}
diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java
index aa28d35856..d9f19f9419 100644
--- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java
+++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java
@@ -6,7 +6,9 @@ import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.AuthenticationResponse;
import com.appsmith.external.models.OAuth2;
import com.appsmith.external.models.UpdatableConnection;
+import lombok.AccessLevel;
import lombok.Getter;
+import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -31,6 +33,7 @@ import java.util.Map;
@Setter
@Getter
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class OAuth2AuthorizationCode extends APIConnection implements UpdatableConnection {
private final Clock clock = Clock.systemUTC();
@@ -42,9 +45,6 @@ public class OAuth2AuthorizationCode extends APIConnection implements UpdatableC
private Object tokenResponse;
private static final int MAX_IN_MEMORY_SIZE = 10 * 1024 * 1024; // 10 MB
- private OAuth2AuthorizationCode() {
- }
-
public static Mono create(OAuth2 oAuth2) {
if (oAuth2 == null) {
return Mono.empty();
diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java
index 33e54ebad3..ab132a4f53 100644
--- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java
+++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java
@@ -6,7 +6,9 @@ import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.AuthenticationResponse;
import com.appsmith.external.models.OAuth2;
import com.appsmith.external.models.UpdatableConnection;
+import lombok.AccessLevel;
import lombok.Getter;
+import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -31,6 +33,7 @@ import java.util.Map;
@Setter
@Getter
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class OAuth2ClientCredentials extends APIConnection implements UpdatableConnection {
private final Clock clock = Clock.systemUTC();
@@ -41,9 +44,6 @@ public class OAuth2ClientCredentials extends APIConnection implements UpdatableC
private Object tokenResponse;
private static final int MAX_IN_MEMORY_SIZE = 10 * 1024 * 1024; // 10 MB
- private OAuth2ClientCredentials() {
- }
-
public static Mono create(OAuth2 oAuth2) {
if (oAuth2 == null) {
return Mono.empty();
diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/restApiPlugin/src/main/resources/form.json
index 9945c475ca..45e5258fb0 100644
--- a/app/server/appsmith-plugins/restApiPlugin/src/main/resources/form.json
+++ b/app/server/appsmith-plugins/restApiPlugin/src/main/resources/form.json
@@ -65,6 +65,10 @@
"label": "None",
"value": "dbAuth"
},
+ {
+ "label": "Basic",
+ "value": "basic"
+ },
{
"label": "OAuth2 (Client credentials)",
"value": "oAuth2"
diff --git a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/connections/BasicAuthenticationTest.java b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/connections/BasicAuthenticationTest.java
new file mode 100644
index 0000000000..2ed3f751c2
--- /dev/null
+++ b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/connections/BasicAuthenticationTest.java
@@ -0,0 +1,26 @@
+package com.external.connections;
+
+import com.appsmith.external.models.BasicAuth;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Base64;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BasicAuthenticationTest {
+
+ @Test
+ public void testCreate_validCredentials_ReturnsWithEncodedValue() {
+ BasicAuth basicAuth = new BasicAuth();
+ basicAuth.setUsername("test");
+ basicAuth.setPassword("password");
+ BasicAuthentication connection = BasicAuthentication.create(basicAuth).block(Duration.ofMillis(100));
+ assertThat(connection).isNotNull();
+ Assert.assertEquals(
+ Base64.getEncoder().encodeToString("test:password".getBytes(StandardCharsets.UTF_8)),
+ connection.getEncodedAuthorizationHeader());
+ }
+}
\ No newline at end of file
diff --git a/app/server/appsmith-server/pom.xml b/app/server/appsmith-server/pom.xml
index 584eba9d7c..55a1bbe62c 100644
--- a/app/server/appsmith-server/pom.xml
+++ b/app/server/appsmith-server/pom.xml
@@ -66,7 +66,7 @@
org.springframework.boot
spring-boot-starter-mail
- 2.2.1.RELEASE
+ 2.4.4
org.springframework.boot
@@ -121,7 +121,7 @@
commons-io
commons-io
- 2.6
+ 2.7
commons-validator
@@ -131,7 +131,7 @@
org.springframework.boot
spring-boot-starter-actuator
- 2.3.4.RELEASE
+ 2.4.4
io.micrometer
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java
index 591bb634da..d11e22af62 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java
@@ -10,9 +10,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -60,7 +60,7 @@ public class CommentController extends BaseController new ResponseDTO<>(HttpStatus.OK.value(), threads, null));
}
- @PatchMapping("/threads/{threadId}")
+ @PutMapping("/threads/{threadId}")
public Mono> updateThread(
@Valid @RequestBody CommentThread resource,
@PathVariable String threadId
@@ -78,4 +78,11 @@ public class CommentController extends BaseController new ResponseDTO<>(HttpStatus.OK.value(), deletedResource, null));
}
+ @DeleteMapping("/threads/{threadId}")
+ public Mono> deleteThread(@PathVariable String threadId) {
+ log.debug("Going to delete thread with id: {}", threadId);
+ return service.deleteThread(threadId)
+ .map(deletedResource -> new ResponseDTO<>(HttpStatus.OK.value(), deletedResource, null));
+ }
+
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java
index b214257337..c2a6a750e9 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java
@@ -2242,4 +2242,20 @@ public class DatabaseChangelog {
NewAction.class
);
}
+
+ @ChangeSet(order = "067", id = "update-mongo-import-from-srv-field", author = "")
+ public void updateMongoImportFromSrvField(MongoTemplate mongoTemplate) {
+ Plugin mongoPlugin = mongoTemplate
+ .findOne(query(where("packageName").is("mongo-plugin")), Plugin.class);
+
+ List mongoDatasources = mongoTemplate
+ .find(query(where("pluginId").is(mongoPlugin.getId())), Datasource.class);
+
+ mongoDatasources.stream()
+ .forEach(datasource -> {
+ datasource.getDatasourceConfiguration().setProperties(List.of(new Property("Use Mongo Connection " +
+ "String URI", "No")));
+ mongoTemplate.save(datasource);
+ });
+ }
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java
index 757a7f9794..452b7ac4e8 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java
@@ -18,4 +18,6 @@ public interface CommentService extends CrudService {
Mono deleteComment(String id);
+ Mono deleteThread(String threadId);
+
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java
index cea8dc2952..c3a3b2d82f 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java
@@ -258,10 +258,17 @@ public class CommentServiceImpl extends BaseService deleteComment(String id) {
-
return repository.findById(id, AclPermission.MANAGE_COMMENT)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.COMMENT, id)))
- .flatMap(comment -> repository.archive(comment));
+ .flatMap(repository::archive)
+ .flatMap(analyticsService::sendDeleteEvent);
+ }
+
+ @Override
+ public Mono deleteThread(String threadId) {
+ return threadRepository.findById(threadId, AclPermission.MANAGE_THREAD)
+ .flatMap(threadRepository::archive)
+ .flatMap(analyticsService::sendDeleteEvent);
}
}
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java
index f87b372dc2..31c51aeb28 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java
@@ -398,19 +398,6 @@ public class UserServiceImpl extends BaseService i
return Mono.just(user)
.flatMap(this::validateObject)
.flatMap(repository::save)
- .zipWith(configService.getTemplateOrganizationId().defaultIfEmpty(""))
- .flatMap(tuple -> {
- final String templateOrganizationId = tuple.getT2();
-
- if (!StringUtils.hasText(templateOrganizationId)) {
- // Since template organization is not configured, we create an empty default organization.
- final User savedUser = tuple.getT1();
- log.debug("Creating blank default organization for user '{}'.", savedUser.getEmail());
- return organizationService.createDefault(new Organization(), savedUser);
- }
-
- return Mono.empty();
- })
.then(repository.findByEmail(user.getUsername()))
.flatMap(analyticsService::trackNewUser);
}
@@ -456,7 +443,23 @@ public class UserServiceImpl extends BaseService i
}
return Mono.error(new AppsmithException(AppsmithError.USER_ALREADY_EXISTS_SIGNUP, user.getUsername()));
})
- .switchIfEmpty(Mono.defer(() -> signupIfAllowed(user)))
+ .switchIfEmpty(Mono.defer(() -> {
+ return signupIfAllowed(user)
+ .zipWith(configService.getTemplateOrganizationId().defaultIfEmpty(""))
+ .flatMap(tuple -> {
+ final User savedUser = tuple.getT1();
+ final String templateOrganizationId = tuple.getT2();
+
+ if (!StringUtils.hasText(templateOrganizationId)) {
+ // Since template organization is not configured, we create an empty default organization.
+ log.debug("Creating blank default organization for user '{}'.", savedUser.getEmail());
+ return organizationService.createDefault(new Organization(), savedUser).thenReturn(savedUser);
+ }
+
+ return Mono.just(savedUser);
+ })
+ .flatMap(savedUser -> findByEmail(savedUser.getEmail()));
+ }))
.flatMap(savedUser ->
emailConfig.isWelcomeEmailEnabled()
? sendWelcomeEmail(savedUser, finalOriginHeader)
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java
index 74c861374e..221541a3ff 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java
@@ -80,8 +80,9 @@ public class ExamplesOrganizationCloner {
* @return Empty Mono.
*/
private Mono cloneExamplesOrganization(User user) {
- if (user.getExamplesOrganizationId() != null) {
- // This user already has an examples organization, don't have to do anything.
+ if (!CollectionUtils.isEmpty(user.getOrganizationIds())) {
+ // Don't create an examples organization if the user already has some organizations, perhaps because they
+ // were invited to some.
return Mono.empty();
}
diff --git a/app/server/appsmith-server/src/main/resources/application.properties b/app/server/appsmith-server/src/main/resources/application.properties
index 1b86904aa7..867f87d1aa 100644
--- a/app/server/appsmith-server/src/main/resources/application.properties
+++ b/app/server/appsmith-server/src/main/resources/application.properties
@@ -66,7 +66,7 @@ admin.emails = ${APPSMITH_ADMIN_EMAILS:}
emails.welcome.enabled = ${APPSMITH_EMAILS_WELCOME_ENABLED:true}
# Appsmith Cloud Services
-appsmith.cloud_services.base_url = ${APPSMITH_CLOUD_SERVICES_BASE_URL:https://cs.appsmith.com}
+appsmith.cloud_services.base_url = ${APPSMITH_CLOUD_SERVICES_BASE_URL:}
appsmith.cloud_services.username = ${APPSMITH_CLOUD_SERVICES_USERNAME:}
appsmith.cloud_services.password = ${APPSMITH_CLOUD_SERVICES_PASSWORD:}
github_repo = ${APPSMITH_GITHUB_REPO:}
diff --git a/contributions/ClientSetup.md b/contributions/ClientSetup.md
index 08b13f654a..13da4c58cf 100644
--- a/contributions/ClientSetup.md
+++ b/contributions/ClientSetup.md
@@ -10,6 +10,7 @@ On your development machine, please ensure that:
1. You have `mkcert` installed. Please visit: [https://github.com/FiloSottile/mkcert#installation](https://github.com/FiloSottile/mkcert#installation) for details. For `mkcert` to work with Firefox you may require the `nss` utility to be installed. Details are in the link above.
1. You have `envsubst` installed. use `brew install gettext` on MacOS. Linux machines usually have this installed.
1. You have cloned the repo in your local machine.
+1. You have yarn installed as a global npm package i.e. `npm install -g yarn`
### Create local HTTPS certificates:
@@ -50,7 +51,7 @@ On your development machine, please ensure that:
### Steps to build & run the code:
-1. Run `yarn`
+1. Run `yarn install`
Note:
diff --git a/office_hours.md b/office_hours.md
index d020c0d34d..d4b42be753 100644
--- a/office_hours.md
+++ b/office_hours.md
@@ -28,6 +28,15 @@ You can find the archives of the calls below with a brief summary of each sessio
## Archives
+Appsmith Live Demo #2, 6th May 2021: Building a support helpdesk using Gmail and Postgres
+
+Video Link
+
+#### Summary
+
+Nikhil shows the community how to build a ticket support dashboard to assign emails to various org members using the Gmail API and Postgres. Questions from members of our community was also discussed.
+
+------------------
29th April 2021: List widget, Release roadmap and more