Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: requesting new integration ui to accept users preferences about the new integrations #38012

Merged
merged 9 commits into from
Dec 9, 2024
28 changes: 28 additions & 0 deletions app/client/src/ce/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2544,3 +2544,31 @@ export const CUSTOM_WIDGET_BUILDER_TAB_TITLE = {
STYLE: () => "Style",
JS: () => "Javascript",
};

export const REQUEST_NEW_INTEGRATIONS = {
UNABLE_TO_FIND: () => "Can’t find what you are looking for?",
REQUEST_NEW_BUTTON: () => "Request a new integration",
REQUEST_BUTTON: () => "Request integration",
CANCEL_BUTTON: () => "Cancel",
REQUEST_MODAL_HEADING: () => "Request a new integration",
REQUEST_MODAL_INTEGRATION: {
LABEL: () => "Integration",
PLACEHOLDER: () => "E.g. Zendesk, JIRA, Slack, others",
NAME: "integration",
ERROR: () => "Please enter integration name",
},
REQUEST_MODAL_USECASE: {
LABEL: () => "Tell us more about your case",
PLACEHOLDER: () =>
"E.g. I want to create an app to manage my customers’ account.",
NAME: "useCase",
},
REQUEST_MODAL_EMAIL: {
LABEL: () => "Email",
DESCRIPTION: () =>
"Appsmith might use this email exclusively to follow up on your integration request.",
NAME: "email",
ERROR: () => "Please enter email",
},
SUCCESS_TOAST_MESSAGE: () => "Thank you! We are looking into your request.",
};
2 changes: 2 additions & 0 deletions app/client/src/ce/entities/FeatureFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const FEATURE_FLAG = {
release_table_custom_loading_state_enabled:
"release_table_custom_loading_state_enabled",
release_custom_widget_ai_builder: "release_custom_widget_ai_builder",
ab_request_new_integration_enabled: "ab_request_new_integration_enabled",
} as const;

export type FeatureFlag = keyof typeof FEATURE_FLAG;
Expand Down Expand Up @@ -79,6 +80,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
release_ide_datasource_selector_enabled: false,
release_table_custom_loading_state_enabled: false,
release_custom_widget_ai_builder: false,
ab_request_new_integration_enabled: false,
};

export const AB_TESTING_EVENT_KEYS = {
Expand Down
4 changes: 3 additions & 1 deletion app/client/src/ce/utils/analyticsUtilTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,9 @@ export type EventName =
| BUILDING_BLOCKS_EVENTS
| "VISIT_SELF_HOST_DOCS"
| "CANVAS_HOVER"
| "MALFORMED_USAGE_PULSE";
| "MALFORMED_USAGE_PULSE"
| "REQUEST_INTEGRATION_CTA"
| "REQUEST_INTEGRATION_SUBMITTED";

type HOMEPAGE_CREATE_APP_FROM_TEMPLATE_EVENTS =
| "TEMPLATE_DROPDOWN_CLICK"
Expand Down
9 changes: 6 additions & 3 deletions app/client/src/components/utils/ReduxFormTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ const renderComponent = (
) : (
<Input
{...componentProps.input}
{...componentProps}
// type prop is omitted as textarea component doesn't support that
{...(componentProps.type === "textarea"
? omit(componentProps, "type")
: componentProps)}
errorMessage={
!componentProps.hideErrorMessage &&
showError &&
componentProps.meta.error
}
isDisabled={componentProps.disabled}
renderAs={"input"}
renderAs={componentProps.type === "textarea" ? "textarea" : "input"}
size="md"
value={value}
/>
Expand All @@ -48,7 +51,7 @@ export interface FormTextFieldProps {
name: string;
placeholder: string;
description?: string;
type?: "text" | "password" | "number" | "email" | "tel";
type?: "text" | "password" | "number" | "email" | "tel" | "textarea";
label?: React.ReactNode;
intent?: Intent;
disabled?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks";
import AIDataSources from "./AIDataSources";
import Debugger from "../DataSourceEditor/Debugger";
import { isPluginActionCreating } from "PluginActionEditor/store";
import RequestNewIntegration from "./RequestNewIntegration";

const NewIntegrationsContainer = styled.div`
${thinScrollbar};
Expand Down Expand Up @@ -250,6 +251,7 @@ interface CreateNewDatasourceScreenProps {
showDebugger: boolean;
pageId: string;
isOnboardingScreen?: boolean;
isRequestNewIntegrationEnabled: boolean;
}

interface CreateNewDatasourceScreenState {
Expand Down Expand Up @@ -281,6 +283,7 @@ class CreateNewDatasourceTab extends React.Component<
dataSources,
isCreating,
isOnboardingScreen,
isRequestNewIntegrationEnabled,
pageId,
showDebugger,
} = this.props;
Expand Down Expand Up @@ -351,6 +354,7 @@ class CreateNewDatasourceTab extends React.Component<
</>
)}
</NewIntegrationsContainer>
{isRequestNewIntegrationEnabled && <RequestNewIntegration />}
{showDebugger && <Debugger />}
</>
);
Expand Down Expand Up @@ -379,6 +383,9 @@ const mapStateToProps = (state: AppState) => {
userWorkspacePermissions,
);

const isRequestNewIntegrationEnabled =
!!featureFlags?.ab_request_new_integration_enabled;

return {
dataSources: getDatasources(state),
mockDatasources: getMockDatasources(state),
Expand All @@ -387,6 +394,7 @@ const mapStateToProps = (state: AppState) => {
canCreateDatasource,
showDebugger,
pageId,
isRequestNewIntegrationEnabled,
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Button, Flex, toast } from "@appsmith/ads";
import { Close } from "@radix-ui/react-dialog";
import { createMessage, REQUEST_NEW_INTEGRATIONS } from "ee/constants/messages";
import type { AppState } from "ee/reducers";
import React from "react";
import { connect } from "react-redux";
import {
Field,
formValueSelector,
getFormSyncErrors,
reduxForm,
type FormErrors,
type InjectedFormProps,
} from "redux-form";
import { getCurrentUser } from "selectors/usersSelectors";
import styled from "styled-components";
import { isEmail } from "utils/formhelpers";
import ReduxFormTextField from "components/utils/ReduxFormTextField";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";

const FormWrapper = styled.form`
display: flex;
flex-direction: column;
gap: var(--ads-spaces-7);
`;

const RequestIntegrationForm = (props: RequestIntegrationFormProps) => {
const onSubmit = (values: RequestIntegrationFormValues) => {
AnalyticsUtil.logEvent("REQUEST_INTEGRATION_SUBMITTED", {
integration_name: values.integration,
use_case_description: values.useCase || "",
email: values.email,
});
toast.show(createMessage(REQUEST_NEW_INTEGRATIONS.SUCCESS_TOAST_MESSAGE), {
kind: "success",
});
props.closeModal();
};

return (
<FormWrapper onSubmit={props.handleSubmit(onSubmit)}>
<Field
component={ReduxFormTextField}
label={createMessage(
REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_INTEGRATION.LABEL,
)}
name={REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_INTEGRATION.NAME}
placeholder={createMessage(
REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_INTEGRATION.PLACEHOLDER,
)}
size="md"
/>
<Field
component={ReduxFormTextField}
label={createMessage(
REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_USECASE.LABEL,
)}
name={REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_USECASE.NAME}
placeholder={createMessage(
REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_USECASE.PLACEHOLDER,
)}
size="md"
type="textarea"
/>
<Field
component={ReduxFormTextField}
description={createMessage(
REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_EMAIL.DESCRIPTION,
)}
label={createMessage(
REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_EMAIL.LABEL,
)}
name={REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_EMAIL.NAME}
size="md"
type="email"
/>
<Flex gap="spaces-7" justifyContent="flex-end" marginTop="spaces-3">
<Close>
<Button aria-label="Close" kind="secondary" size="md">
{createMessage(REQUEST_NEW_INTEGRATIONS.CANCEL_BUTTON)}
</Button>
</Close>
<Button isDisabled={props.invalid} size="md" type="submit">
{createMessage(REQUEST_NEW_INTEGRATIONS.REQUEST_BUTTON)}
</Button>
</Flex>
</FormWrapper>
);
};

const REQUEST_NEW_INTEGRATION_FORM_NAME = "REQUEST_NEW_INTEGRATION";

const selector = formValueSelector(REQUEST_NEW_INTEGRATION_FORM_NAME);

interface RequestIntegrationFormValues {
integration?: string;
email?: string;
useCase?: string;
}

type RequestIntegrationFormProps = RequestIntegrationFormValues & {
formSyncErrors?: FormErrors<string, string>;
closeModal: () => void;
} & InjectedFormProps<
RequestIntegrationFormValues,
{
formSyncErrors?: FormErrors<string, string>;
closeModal: () => void;
}
>;

const validate = (values: RequestIntegrationFormValues) => {
const errors: Partial<RequestIntegrationFormValues> = {};

if (!values.integration) {
errors.integration = createMessage(
REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_INTEGRATION.ERROR,
);
}

if (!values.email || !isEmail(values.email)) {
errors.email = createMessage(
REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_EMAIL.ERROR,
);
}
Comment on lines +121 to +125
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Email field should be optional in validation

The email field is currently mandatory in the validation logic. As per the PR objectives, it should be optional. Please update the validation to reflect this.

Apply this diff to fix the validation logic:

-  if (!values.email || !isEmail(values.email)) {
+  if (values.email && !isEmail(values.email)) {
     errors.email = createMessage(
       REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_EMAIL.ERROR,
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!values.email || !isEmail(values.email)) {
errors.email = createMessage(
REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_EMAIL.ERROR,
);
}
if (values.email && !isEmail(values.email)) {
errors.email = createMessage(
REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_EMAIL.ERROR,
);
}


return errors;
};

export default connect((state: AppState) => {
const currentUser = getCurrentUser(state);

return {
integration: selector(state, "integration"),
email: selector(state, "email"),
useCase: selector(state, "useCase"),
initialValues: {
email: currentUser?.email,
},
formSyncErrors: getFormSyncErrors(REQUEST_NEW_INTEGRATION_FORM_NAME)(state),
};
}, null)(
reduxForm<
RequestIntegrationFormValues,
{
formSyncErrors?: FormErrors<string, string>;
closeModal: () => void;
}
>({
validate,
form: REQUEST_NEW_INTEGRATION_FORM_NAME,
enableReinitialize: true,
})(RequestIntegrationForm),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
Button,
Flex,
Modal,
ModalContent,
ModalHeader,
ModalTrigger,
} from "@appsmith/ads";
import { createMessage, REQUEST_NEW_INTEGRATIONS } from "ee/constants/messages";
import React, { useState, type ReactNode } from "react";
import styled from "styled-components";
import Form from "./form";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";

const RequestNewIntegrationWrapper = styled(Flex)`
padding: var(--ads-spaces-7);
border-top: 1px solid var(--ads-v2-colors-content-surface-default-border);
position: sticky;
bottom: 0;
background: var(--ads-v2-color-bg);
`;

const ModalContentWrapper = styled(ModalContent)`
max-width: 518px;
`;

function RequestModal({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);

return (
<Modal onOpenChange={setOpen} open={open}>
<ModalTrigger>{children}</ModalTrigger>
<ModalContentWrapper>
<ModalHeader>
{createMessage(REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_HEADING)}
</ModalHeader>
<Form closeModal={() => setOpen(false)} />
</ModalContentWrapper>
</Modal>
);
}

export default function RequestNewIntegration() {
return (
<RequestNewIntegrationWrapper gap="spaces-5">
<p>{createMessage(REQUEST_NEW_INTEGRATIONS.UNABLE_TO_FIND)}</p>
<RequestModal>
<Button
kind="secondary"
onClick={() => {
AnalyticsUtil.logEvent("REQUEST_INTEGRATION_CTA");
}}
>
{createMessage(REQUEST_NEW_INTEGRATIONS.REQUEST_NEW_BUTTON)}
</Button>
</RequestModal>
</RequestNewIntegrationWrapper>
);
}
Loading