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:
parent
cabaea58cb
commit
884c9a0bc2
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe("one click binding mongodb datasource", function () {
|
|||
entityExplorer.SelectEntityByName("Table1", "Widgets");
|
||||
|
||||
oneClickBinding.ChooseAndAssertForm(
|
||||
`New from ${dsName}`,
|
||||
`${dsName}`,
|
||||
dsName,
|
||||
"netflix",
|
||||
"creator",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}}}`,
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}}}` },
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export interface ActionApiResponseReq {
|
|||
body: Record<string, unknown> | null;
|
||||
httpMethod: HttpMethod | "";
|
||||
url: string;
|
||||
requestedAt?: number;
|
||||
}
|
||||
|
||||
export type ActionExecutionResponse = ApiResponse<{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}`,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export const CONFIG = {
|
|||
borderWidth: "1",
|
||||
dynamicBindingPathList: [],
|
||||
primaryColumns: {},
|
||||
tableData: undefined,
|
||||
tableData: "",
|
||||
columnWidthMap: {},
|
||||
columnOrder: [],
|
||||
enableClientSideSearch: true,
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ class TableWidgetV2 extends BaseWidget<TableWidgetProps, WidgetState> {
|
|||
onSort: queryConfig.select.run,
|
||||
enableClientSideSearch: !formConfig.searchableColumn,
|
||||
primaryColumnId: formConfig.primaryColumn,
|
||||
isVisibleDownload: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user