chore: Address misc one click binding feedbacks (#24735)

## Description
Fixes miscellaneous feedback in the one-click binding feature.

- Order of queries - show select queries on top and order by last
executed query
- Converting from JS to dropdown should be possible for the following
cases
   - {{Query.data}}
- Improve query names to be generated using the data table or collection
we use
- undefined table data value should show an error on the property pane
- Download option should be disabled when table is generated using one
click binding
-  Remove the insert binding option from the dropdown

#### PR fixes following issue(s)
Fixes https://github.com/appsmithorg/appsmith/issues/24605
> if no issue exists, please create an issue and ask the maintainers
about this first
>
>
#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
> Please delete options that are not relevant.
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- Chore (housekeeping or task changes that don't impact user perception)
- This change requires a documentation update
>
>
>
## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [x] Manual
- [x] Jest
- [x] Cypress
>
>
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
balajisoundar 2023-06-29 19:23:25 +05:30 committed by GitHub
parent cabaea58cb
commit 884c9a0bc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 253 additions and 42 deletions

View File

@ -100,7 +100,7 @@ describe("excludeForAirgap", "One click binding control", () => {
propPane.ToggleJSMode("Table data", false);
oneClickBinding.ChooseAndAssertForm(
"New from Users",
"Users",
"Users",
"public.users",
"gender",
@ -111,7 +111,7 @@ describe("excludeForAirgap", "One click binding control", () => {
propPane.MoveToTab("Content");
oneClickBinding.ChooseAndAssertForm(
"New from sample Movies",
"sample Movies",
"movies",
"movies",
"status",

View File

@ -26,7 +26,7 @@ describe("one click binding mongodb datasource", function () {
entityExplorer.SelectEntityByName("Table1", "Widgets");
oneClickBinding.ChooseAndAssertForm(
`New from ${dsName}`,
`${dsName}`,
dsName,
"netflix",
"creator",

View File

@ -25,7 +25,7 @@ describe("Table widget one click binding feature", () => {
entityExplorer.SelectEntityByName("Table1", "Widgets");
oneClickBinding.ChooseAndAssertForm(
`New from ${dsName}`,
`${dsName}`,
dsName,
"public.users",
"name",

View File

@ -66,7 +66,7 @@ describe("GSheets WidgetQueryGenerator", () => {
expect(expr).toEqual([
{
name: "Find_query",
name: "Find_someSheet",
payload: {
formData: {
command: {
@ -165,7 +165,7 @@ describe("GSheets WidgetQueryGenerator", () => {
expect(expr).toEqual([
{
name: "Update_query",
name: "Update_someSheet",
payload: {
formData: {
command: {
@ -227,7 +227,7 @@ describe("GSheets WidgetQueryGenerator", () => {
);
expect(expr).toEqual([
{
name: "Insert_query",
name: "Insert_someSheet",
payload: {
formData: {
command: {

View File

@ -7,6 +7,7 @@ import type {
GSheetsFormData,
ActionConfigurationGSheets,
} from "WidgetQueryGenerators/types";
import { removeSpecialChars } from "utils/helpers";
enum COMMAND_TYPES {
"FIND" = "FETCH_MANY",
@ -51,10 +52,10 @@ export default abstract class GSheets extends BaseQueryGenerator {
) {
const { select } = widgetConfig;
if (select) {
if (select && formConfig.sheetName) {
return {
type: QUERY_TYPE.SELECT,
name: "Find_query",
name: `Find_${removeSpecialChars(formConfig.sheetName)}`,
formData: {
where: {
data: {
@ -109,10 +110,10 @@ export default abstract class GSheets extends BaseQueryGenerator {
) {
const { select } = widgetConfig;
if (select) {
if (select && formConfig.sheetName) {
return {
type: QUERY_TYPE.TOTAL_RECORD,
name: "Total_record_query",
name: `Total_record_${removeSpecialChars(formConfig.sheetName)}`,
formData: {
where: {
data: {
@ -147,10 +148,10 @@ export default abstract class GSheets extends BaseQueryGenerator {
): Record<string, object | string> | undefined {
const { update } = widgetConfig;
if (update) {
if (update && formConfig.sheetName) {
return {
type: QUERY_TYPE.UPDATE,
name: "Update_query",
name: `Update_${removeSpecialChars(formConfig.sheetName)}`,
formData: {
rowObjects: {
data: `{{${update.value}}}`,
@ -177,10 +178,10 @@ export default abstract class GSheets extends BaseQueryGenerator {
) {
const { create } = widgetConfig;
if (create) {
if (create && formConfig.sheetName) {
return {
type: QUERY_TYPE.CREATE,
name: "Insert_query",
name: `Insert_${removeSpecialChars(formConfig.sheetName)}`,
formData: {
rowObjects: {
data: `{{${create.value}}}`,

View File

@ -44,7 +44,7 @@ describe("Mongo WidgetQueryGenerator", () => {
expect(expr).toEqual([
{
type: "select",
name: "Find_query",
name: "Find_someTable",
dynamicBindingPathList: [
{
key: "formData.find.skip.data",
@ -114,7 +114,7 @@ describe("Mongo WidgetQueryGenerator", () => {
expect(expr).toEqual([
{
name: "Update_query",
name: "Update_someTable",
type: "update",
dynamicBindingPathList: [
{
@ -170,7 +170,7 @@ describe("Mongo WidgetQueryGenerator", () => {
);
expect(expr).toEqual([
{
name: "Insert_query",
name: "Insert_someTable",
type: "create",
dynamicBindingPathList: [
{

View File

@ -7,6 +7,7 @@ import type {
ActionConfigurationMongoDB,
MongoDBFormData,
} from "WidgetQueryGenerators/types";
import { removeSpecialChars } from "utils/helpers";
enum COMMAND_TYPES {
"FIND" = "FIND",
@ -30,7 +31,7 @@ export default abstract class MongoDB extends BaseQueryGenerator {
if (select) {
return {
type: QUERY_TYPE.SELECT,
name: "Find_query",
name: `Find_${removeSpecialChars(formConfig.tableName)}`,
formData: {
find: {
skip: { data: `{{${select["offset"]}}}` },
@ -73,7 +74,7 @@ export default abstract class MongoDB extends BaseQueryGenerator {
if (select) {
return {
type: QUERY_TYPE.TOTAL_RECORD,
name: "Total_record_query",
name: `Total_record_${removeSpecialChars(formConfig.tableName)}`,
formData: {
count: {
query: {
@ -102,7 +103,7 @@ export default abstract class MongoDB extends BaseQueryGenerator {
if (update) {
return {
type: QUERY_TYPE.UPDATE,
name: "Update_query",
name: `Update_${removeSpecialChars(formConfig.tableName)}`,
formData: {
updateMany: {
query: { data: `{_id: ObjectId('{{${update.where}._id}}')}` },
@ -131,7 +132,7 @@ export default abstract class MongoDB extends BaseQueryGenerator {
if (create) {
return {
type: QUERY_TYPE.CREATE,
name: "Insert_query",
name: `Insert_${removeSpecialChars(formConfig.tableName)}`,
formData: {
insert: {
documents: { data: `{{${create.value}}}` },

View File

@ -45,7 +45,7 @@ OFFSET
expect(expr).toEqual([
{
name: "Select_query",
name: "Select_someTable",
type: "select",
dynamicBindingPathList: [
{
@ -97,7 +97,7 @@ OFFSET
expect(expr).toEqual([
{
name: "Select_query",
name: "Select_someTable",
type: "select",
dynamicBindingPathList: [
{
@ -159,7 +159,7 @@ OFFSET
expect(expr).toEqual([
{
name: "Update_query",
name: "Update_someTable",
type: "update",
dynamicBindingPathList: [
{
@ -219,7 +219,7 @@ OFFSET
);
expect(expr).toEqual([
{
name: "Insert_query",
name: "Insert_someTable",
type: "create",
dynamicBindingPathList: [
{

View File

@ -6,6 +6,7 @@ import type {
WidgetQueryGenerationFormConfig,
ActionConfigurationPostgreSQL,
} from "../types";
import { removeSpecialChars } from "utils/helpers";
export default abstract class PostgreSQL extends BaseQueryGenerator {
private static buildSelect(
widgetConfig: WidgetQueryGenerationConfig,
@ -88,7 +89,7 @@ export default abstract class PostgreSQL extends BaseQueryGenerator {
return {
type: QUERY_TYPE.SELECT,
name: "Select_query",
name: `Select_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: res,
},
@ -114,7 +115,7 @@ export default abstract class PostgreSQL extends BaseQueryGenerator {
return {
type: QUERY_TYPE.UPDATE,
name: "Update_query",
name: `Update_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `UPDATE ${formConfig.tableName} SET ${formConfig.columns
.map((column) => `"${column}"= '{{${value}.${column}}}'`)
@ -142,7 +143,7 @@ export default abstract class PostgreSQL extends BaseQueryGenerator {
return {
type: QUERY_TYPE.CREATE,
name: "Insert_query",
name: `Insert_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `INSERT INTO ${formConfig.tableName} (${formConfig.columns.map(
(a) => `"${a}"`,
@ -170,7 +171,7 @@ export default abstract class PostgreSQL extends BaseQueryGenerator {
return {
type: QUERY_TYPE.TOTAL_RECORD,
name: "Total_record_query",
name: `Total_record_${removeSpecialChars(formConfig.tableName)}`,
payload: {
body: `SELECT COUNT(*) from ${formConfig.tableName}${
formConfig.searchableColumn

View File

@ -71,6 +71,7 @@ export interface ActionApiResponseReq {
body: Record<string, unknown> | null;
httpMethod: HttpMethod | "";
url: string;
requestedAt?: number;
}
export type ActionExecutionResponse = ApiResponse<{

View File

@ -213,7 +213,7 @@ function DatasourceDropdown() {
<DropdownOption
label={
<span>
New from {option.data.isSample ? "sample " : ""}
{option.data.isSample ? "sample " : ""}
<Bold>{option.label?.replace("sample ", "")}</Bold>
</span>
}

View File

@ -35,6 +35,53 @@ import { getWidget } from "sagas/selectors";
import type { AppState } from "@appsmith/reducers";
import { DatasourceCreateEntryPoints } from "constants/Datasource";
import { getCurrentWorkspaceId } from "@appsmith/selectors/workspaceSelectors";
import type { ActionDataState } from "reducers/entityReducers/actionsReducer";
import { getDatatype } from "utils/AppsmithUtils";
enum SortingWeights {
alphabetical = 1,
execution,
datatype,
}
const SORT_INCREAMENT = 1;
function sortQueries(queries: ActionDataState, expectedDatatype: string) {
return queries.sort((A, B) => {
const score = {
A: 0,
B: 0,
};
if (A.config.name < B.config.name) {
score.A += SORT_INCREAMENT << SortingWeights.alphabetical;
} else {
score.B += SORT_INCREAMENT << SortingWeights.alphabetical;
}
if (A.data?.request?.requestedAt && B.data?.request?.requestedAt) {
if (A.data.request.requestedAt > B.data.request.requestedAt) {
score.A += SORT_INCREAMENT << SortingWeights.execution;
} else {
score.B += SORT_INCREAMENT << SortingWeights.execution;
}
} else if (A.data?.request?.requestedAt) {
score.A += SORT_INCREAMENT << SortingWeights.execution;
} else if (B.data?.request?.requestedAt) {
score.B += SORT_INCREAMENT << SortingWeights.execution;
}
if (getDatatype(A.data?.body) === expectedDatatype) {
score.A += SORT_INCREAMENT << SortingWeights.datatype;
}
if (getDatatype(B.data?.body) === expectedDatatype) {
score.B += SORT_INCREAMENT << SortingWeights.datatype;
}
return score.A > score.B ? -1 : 1;
});
}
function filterOption(option: DropdownOptionType, searchText: string) {
return (
@ -48,6 +95,7 @@ export function useDatasource(searchText: string) {
addBinding,
config,
errorMsg,
expectedType,
isSourceOpen,
onSourceClose,
propertyName,
@ -293,7 +341,7 @@ export function useDatasource(searchText: string) {
const queries = useSelector(getActionsForCurrentPage);
const queryOptions = useMemo(() => {
return queries.map((query) => ({
return sortQueries(queries, expectedType).map((query) => ({
id: query.config.id,
label: query.config.name,
value: `{{${query.config.name}.data}}`,

View File

@ -37,6 +37,7 @@ type WidgetQueryGeneratorFormContextType = {
isSourceOpen: boolean;
onSourceClose: () => void;
errorMsg: string;
expectedType: string;
};
const DEFAULT_CONFIG_VALUE = {
@ -60,6 +61,7 @@ const DEFAULT_CONTEXT_VALUE = {
onSourceClose: noop,
errorMsg: "",
propertyName: "",
expectedType: "",
};
export const WidgetQueryGeneratorFormContext =
@ -73,6 +75,7 @@ type Props = {
onUpdate: (snippet?: string, makeDynamicPropertyPath?: boolean) => void;
widgetId: string;
errorMsg: string;
expectedType: string;
};
function WidgetQueryGeneratorForm(props: Props) {
@ -80,7 +83,14 @@ function WidgetQueryGeneratorForm(props: Props) {
const [pristine, setPristine] = useState(true);
const { errorMsg, onUpdate, propertyPath, propertyValue, widgetId } = props;
const {
errorMsg,
expectedType,
onUpdate,
propertyPath,
propertyValue,
widgetId,
} = props;
const isSourceOpen = useSelector(getIsOneClickBindingOptionsVisibility);
@ -183,6 +193,7 @@ function WidgetQueryGeneratorForm(props: Props) {
onSourceClose,
errorMsg,
propertyName: propertyPath,
expectedType,
};
}, [
config,

View File

@ -6,7 +6,7 @@ export const Wrapper = styled.div``;
export const SelectWrapper = styled.div`
display: inline-block;
margin: 5px 0 2px;
margin: 0 0 2px;
max-width: ${DROPDOWN_TRIGGER_DIMENSION.WIDTH};
width: 100%;
`;
@ -63,6 +63,7 @@ export const ErrorMessage = styled.div`
font-size: 12px;
line-height: 14px;
color: var(--ads-v2-color-fg-error);
margin-top: 5px;
`;
export const Placeholder = styled.div`

View File

@ -16,10 +16,8 @@ class OneClickBindingControl extends BaseControl<OneClickBindingControlProps> {
* with default value by platform
*/
static canDisplayValueInUI(config: ControlData, value: any): boolean {
return [
/^{{[^.]*\.data}}$/gi, // {{query1.data}}
/^{{}}$/, // {{}}
].some((d) => d.test(value));
// {{query1.data}}
return /^{{[^.]*\.data}}$/gi.test(value);
}
static shouldValidateValueOnDynamicPropertyOff() {
@ -55,6 +53,7 @@ class OneClickBindingControl extends BaseControl<OneClickBindingControlProps> {
return (
<WidgetQueryGeneratorForm
errorMsg={this.getErrorMessage()}
expectedType={this.props.expected?.autocompleteDataType || ""}
onUpdate={this.onUpdatePropertyValue}
propertyPath={this.props.propertyName}
propertyValue={this.props.propertyValue}

View File

@ -1,7 +1,9 @@
import {
areArraysEqual,
createBlobUrl,
DataType,
getCamelCaseString,
getDatatype,
parseBlobUrl,
} from "utils/AppsmithUtils";
import { isURL } from "./TypeHelpers";
@ -93,3 +95,35 @@ describe("parseBlobUrl", () => {
]);
});
});
describe("getDatatype - should test the datatypes", () => {
it("1. String", () => {
expect(getDatatype("test")).toBe(DataType.STRING);
});
it("2. Number", () => {
[1, NaN].forEach((d) => {
expect(getDatatype(d)).toBe(DataType.NUMBER);
});
});
it("3. Boolean", () => {
[true, false].forEach((d) => {
expect(getDatatype(d)).toBe(DataType.BOOLEAN);
});
});
it("4. Object", () => {
expect(getDatatype({})).toBe(DataType.OBJECT);
});
it("5. Array", () => {
expect(getDatatype([])).toBe(DataType.ARRAY);
});
it("6. Rest of the types", () => {
expect(getDatatype(null)).toBe(DataType.NULL);
expect(getDatatype(undefined)).toBe(DataType.UNDEFINED);
});
});

View File

@ -5,7 +5,7 @@ import * as Sentry from "@sentry/react";
import type { Property } from "api/ActionAPI";
import type { AppIconName } from "design-system-old";
import { AppIconCollection } from "design-system-old";
import _ from "lodash";
import _, { isPlainObject } from "lodash";
import * as log from "loglevel";
import { osName } from "react-device-detect";
import type { ActionDataState } from "reducers/entityReducers/actionsReducer";
@ -457,3 +457,31 @@ export function areArraysEqual(arr1: string[], arr2: string[]) {
return false;
}
export enum DataType {
OBJECT = "OBJECT",
NUMBER = "NUMBER",
ARRAY = "ARRAY",
BOOLEAN = "BOOLEAN",
STRING = "STRING",
NULL = "NULL",
UNDEFINED = "UNDEFINED",
}
export function getDatatype(value: unknown) {
if (typeof value === "string") {
return DataType.STRING;
} else if (typeof value === "number") {
return DataType.NUMBER;
} else if (typeof value === "boolean") {
return DataType.BOOLEAN;
} else if (isPlainObject(value)) {
return DataType.OBJECT;
} else if (Array.isArray(value)) {
return DataType.ARRAY;
} else if (value === null) {
return DataType.NULL;
} else if (value === undefined) {
return DataType.UNDEFINED;
}
}

View File

@ -40,7 +40,7 @@ export const CONFIG = {
borderWidth: "1",
dynamicBindingPathList: [],
primaryColumns: {},
tableData: undefined,
tableData: "",
columnWidthMap: {},
columnOrder: [],
enableClientSideSearch: true,

View File

@ -183,6 +183,7 @@ class TableWidgetV2 extends BaseWidget<TableWidgetProps, WidgetState> {
onSort: queryConfig.select.run,
enableClientSideSearch: !formConfig.searchableColumn,
primaryColumnId: formConfig.primaryColumn,
isVisibleDownload: false,
});
}

View File

@ -10,6 +10,7 @@ import type { TableWidgetProps } from "widgets/TableWidgetV2/constants";
import { InlineEditingSaveOptions } from "widgets/TableWidgetV2/constants";
import { composePropertyUpdateHook } from "widgets/WidgetUtils";
import {
tableDataValidation,
totalRecordsCountValidation,
uniqueColumnNameValidation,
updateColumnOrderHook,
@ -35,9 +36,14 @@ export default [
isTriggerProperty: false,
isJSConvertible: true,
validation: {
type: ValidationTypes.OBJECT_ARRAY,
type: ValidationTypes.FUNCTION,
params: {
default: [],
fn: tableDataValidation,
expected: {
type: "Array",
example: `[{ "name": "John" }]`,
autocompleteDataType: AutocompleteDataType.ARRAY,
},
},
},
evaluationSubstitutionType: EvaluationSubstitutionType.SMART_SUBSTITUTE,

View File

@ -970,3 +970,82 @@ export function selectColumnOptionsValidation(
export const getColumnPath = (propPath: string) =>
propPath.split(".").slice(0, 2).join(".");
export const tableDataValidation = (
value: unknown,
props: TableWidgetProps,
_?: any,
) => {
const invalidResponse = {
isValid: false,
parsed: [],
messages: [
{
name: "TypeError",
message: `This value does not evaluate to type Array<Object>}`,
},
],
};
if (value === "") {
return {
isValid: true,
parsed: [],
};
}
if (value === undefined || value === null) {
return {
isValid: false,
parsed: [],
messages: [
{
name: "ValidationError",
message: "Data is undefined, re-run your query or fix the data",
},
],
};
}
if (!_.isString(value) && !Array.isArray(value)) {
return invalidResponse;
}
let parsed = value;
if (_.isString(value)) {
try {
parsed = JSON.parse(value as string);
} catch (e) {
return invalidResponse;
}
}
if (Array.isArray(parsed)) {
if (parsed.length === 0) {
return {
isValid: true,
parsed: [],
};
}
for (let i = 0; i < parsed.length; i++) {
if (!_.isPlainObject(parsed[i])) {
return {
isValid: false,
parsed: [],
messages: [
{
name: "ValidationError",
message: `Invalid object at index ${i}`,
},
],
};
}
}
return { isValid: true, parsed };
}
return invalidResponse;
};