feat: action redesign, UQI upgrade S3 plugin config to dual zone format & sorting field responsiveness (#36090)

## Description
Upgrade S3 plugin config to new format using SECTION_V2,
SINGLE_COLUMN_ZONE, and DOUBLE_COLUMN_ZONE.

Fixes #35484

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/10720588484>
> Commit: c66dce69902ae247b6444ff901fe2cf1595e8e34
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=10720588484&attempt=3"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Thu, 05 Sep 2024 15:04:31 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit


- **New Features**
- Enhanced sorting control with improved performance and
maintainability.
- New configuration option for specifying the expiration duration of
signed URLs in the Amazon S3 plugin.
- Updated UI layout for various actions (upload, delete, read) in the
Amazon S3 plugin to improve organization and user experience.
- Clarified labeling and structure in the Amazon S3 plugin for better
user interaction.

- **Bug Fixes**
- Streamlined logic for adding and deleting sorting fields in the
sorting component.

- **Documentation**
- Updated control types and structure in the Amazon S3 plugin
configuration for clarity and usability.

- **Style**
- Improved responsiveness of the sorting control layout and Amazon S3
plugin UI.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Alex 2024-09-09 19:00:10 +03:00 committed by GitHub
parent 1cf452fafa
commit 205ba07d53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 227 additions and 240 deletions

View File

@ -1,9 +1,12 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import FormControl from "pages/Editor/FormControl"; import FormControl from "pages/Editor/FormControl";
import { Classes } from "@appsmith/ads-old";
import styled from "styled-components"; import styled from "styled-components";
import { FieldArray, getFormValues } from "redux-form"; import {
FieldArray,
getFormValues,
type WrappedFieldArrayProps,
} from "redux-form";
import type { ControlProps } from "./BaseControl"; import type { ControlProps } from "./BaseControl";
import { getBindingOrConfigPathsForSortingControl } from "entities/Action/actionProperties"; import { getBindingOrConfigPathsForSortingControl } from "entities/Action/actionProperties";
import { SortingSubComponent } from "./utils"; import { SortingSubComponent } from "./utils";
@ -26,9 +29,6 @@ const columnFieldConfig: any = {
initialValue: "", initialValue: "",
inputType: "TEXT", inputType: "TEXT",
placeholderText: "Column name", placeholderText: "Column name",
customStyles: {
// width: "280px",
},
}; };
// Form config for the order field // Form config for the order field
@ -52,60 +52,42 @@ const orderFieldConfig: any = {
], ],
}; };
// main container for the fsorting component const SortingContainer = styled.div<{ isBreakpointSmall: boolean }>`
const SortingContainer = styled.div` display: grid;
display: flex; grid-template-columns: ${({ isBreakpointSmall }) =>
flex-direction: column; isBreakpointSmall ? "1fr 50px" : "1fr 50px"};
justify-content: space-between;
`;
// container for the two sorting dropdown
const SortingDropdownContainer = styled.div<{ size: string }>`
display: flex;
flex-direction: row;
width: min-content;
justify-content: space-between;
margin-bottom: 5px;
gap: 5px; gap: 5px;
align-items: center; align-items: center;
> div {
width: 250px;
}
${(props) =>
props.size === "small" &&
`
// Hide the dropdown labels to decrease the width
// The design system component has inline style hence the !important
.t--form-control-DROP_DOWN .${Classes.TEXT} {
display: none !important;
}
// Show the icons hidden initially
.t--form-control-DROP_DOWN .remixicon-icon {
display: initial;
}
`}
`; `;
const SortingFields = styled.div<{ isBreakpointSmall: boolean }>`
display: grid;
grid-template-columns: ${({ isBreakpointSmall }) =>
isBreakpointSmall ? "1fr" : "1fr 180px"};
grid-template-rows: ${({ isBreakpointSmall }) =>
isBreakpointSmall ? "1fr 1fr" : "1fr"};
gap: 5px;
`;
const ButtonWrapper = styled.div` const ButtonWrapper = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
`; `;
// container for the column dropdown section
const ColumnDropdownContainer = styled.div``;
// Component for the icons export type SortingControlProps = ControlProps;
const CenteredButton = styled(Button)``;
// TODO: Fix this the next time the file is edited export type SortingComponentProps = WrappedFieldArrayProps &
// eslint-disable-next-line @typescript-eslint/no-explicit-any Pick<SortingControlProps, "configProperty" | "formName">;
function SortingComponent(props: any) {
// TODO: Fix this the next time the file is edited function SortingComponent(props: SortingComponentProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const { configProperty, fields, formName } = props;
const formValues: any = useSelector((state) =>
const formValues = useSelector((state) =>
getFormValues(props.formName)(state), getFormValues(props.formName)(state),
); );
const onDeletePressed = (index: number) => { const onDeletePressed = (index: number) => {
props.fields.remove(index); fields.remove(index);
}; };
const targetRef = useRef<HTMLDivElement>(null); const targetRef = useRef<HTMLDivElement>(null);
@ -113,105 +95,100 @@ function SortingComponent(props: any) {
const isBreakpointSmall = size === "small"; const isBreakpointSmall = size === "small";
useEffect(() => { useEffect(() => {
// this path represents the path to the sortBy object, wherever the location is in the actionConfiguration object
let sortingObjectPath;
// if the path ends with .data which we expect it to. // if the path ends with .data which we expect it to.
if (props.configProperty.endsWith(".data")) { if (configProperty.endsWith(".data")) {
// we remove the .data and get the path of the sort object // we remove the .data and get the path of the sort object
// NOTE: 5 is used because (.data) = 5 // NOTE: 5 is used because (.data) = 5
sortingObjectPath = props.configProperty.substring( const sortingObjectPath = configProperty.substring(
0, 0,
props.configProperty.length - 5, configProperty.length - 5,
); );
}
// sortDataValue is the path to the value (.data included) itself in the sort object // sortDataValue is the path to the value (.data included) itself in the sort object
const sortDataValue = get(formValues, props.configProperty); const sortDataValue = get(formValues, configProperty);
// sort object value is the path to the sort object itself. // sort object value is the path to the sort object itself.
const sortObjectValue = get(formValues, sortingObjectPath); const sortObjectValue = get(formValues, sortingObjectPath);
// The reason we are making this check is to prevent new fields from being pushed when the form control is visited // The reason we are making this check is to prevent new fields from being pushed when the form control is visited
// for some reason the fields object is initially undefined in first render, before being initialized with the correct values after. // for some reason the fields object is initially undefined in first render, before being initialized with the correct values after.
// so we check to see if the sortObjectValue exist first (if the value has been initalized). // so we check to see if the sortObjectValue exist first (if the value has been initalized).
if (!sortObjectValue) { if (sortObjectValue) {
return;
}
// then we check if the redux fields have any items in it, // then we check if the redux fields have any items in it,
// and we also check if the value exists in the redux state as an array and if that value has no items in it. // and we also check if the value exists in the redux state as an array and if that value has no items in it.
// if they are both empty we want to push a new field. // if they are both empty we want to push a new field.
// We also want to check if the value is undefined, this means that the sort data value is non existent, if it is, we want to push a new field. // We also want to check if the value is undefined, this means that the sort data value is non existent, if it is, we want to push a new field.
if ( if (
(props.fields.length < 1 && (fields.length < 1 &&
isArray(sortDataValue) && isArray(sortDataValue) &&
sortDataValue.length < 1) || sortDataValue.length < 1) ||
(props.fields.length < 1 && !sortDataValue) (fields.length < 1 && !sortDataValue)
) { ) {
props.fields.push({ fields.push({
column: "", column: "",
order: OrderDropDownValues.ASCENDING, order: OrderDropDownValues.ASCENDING,
}); });
} else {
onDeletePressed(props.index);
} }
}, [props.fields.length]); }
}
}, [fields.length]);
return ( return (
<SortingContainer className={`t--${props?.configProperty}`} ref={targetRef}> <SortingContainer
{props.fields && className={`t--${props?.configProperty}`}
props.fields.length > 0 && isBreakpointSmall={isBreakpointSmall}
// TODO: Fix this the next time the file is edited ref={targetRef}
// eslint-disable-next-line @typescript-eslint/no-explicit-any >
props.fields.map((field: any, index: number) => { {fields &&
fields.length > 0 &&
fields.map((field, index: number) => {
const columnPath = getBindingOrConfigPathsForSortingControl( const columnPath = getBindingOrConfigPathsForSortingControl(
SortingSubComponent.Column, SortingSubComponent.Column,
field, field,
undefined,
); );
const OrderPath = getBindingOrConfigPathsForSortingControl(
const orderPath = getBindingOrConfigPathsForSortingControl(
SortingSubComponent.Order, SortingSubComponent.Order,
field, field,
undefined,
); );
return ( return (
<SortingDropdownContainer key={index} size={size}> <React.Fragment key={index}>
<ColumnDropdownContainer> <SortingFields isBreakpointSmall={isBreakpointSmall}>
<FormControl <FormControl
config={{ config={{
...columnFieldConfig, ...columnFieldConfig,
customStyles: { customStyles: {
width: "250px", width: "100%",
}, },
configProperty: `${columnPath}`, configProperty: columnPath,
nestedFormControl: true, nestedFormControl: true,
}} }}
formName={props.formName} formName={formName}
/> />
</ColumnDropdownContainer>
<FormControl <FormControl
config={{ config={{
...orderFieldConfig, ...orderFieldConfig,
configProperty: `${OrderPath}`,
nestedFormControl: true,
customStyles: { customStyles: {
width: isBreakpointSmall ? "65px" : "250px", maxWidth: "180px",
}, },
optionWidth: isBreakpointSmall ? "250px" : undefined, configProperty: orderPath,
nestedFormControl: true,
}} }}
formName={props.formName} formName={formName}
/> />
{/* Component to render the delete icon */} </SortingFields>
<CenteredButton <Button
data-testid={`t--sorting-delete-[${index}]`} data-testid={`t--sorting-delete-[${index}]`}
isIconButton isIconButton
kind="tertiary" kind="tertiary"
onClick={(e: React.MouseEvent) => { onClick={() => {
e.stopPropagation();
onDeletePressed(index); onDeletePressed(index);
}} }}
size="md" size="md"
startIcon="close-line" startIcon="close-line"
value={index}
/> />
</SortingDropdownContainer> </React.Fragment>
); );
})} })}
<ButtonWrapper> <ButtonWrapper>
@ -219,7 +196,7 @@ function SortingComponent(props: any) {
data-testid={`t--sorting-add-field`} data-testid={`t--sorting-add-field`}
kind="tertiary" kind="tertiary"
onClick={() => onClick={() =>
props.fields.push({ fields.push({
column: "", column: "",
order: OrderDropDownValues.ASCENDING, order: OrderDropDownValues.ASCENDING,
}) })
@ -240,18 +217,18 @@ export default function SortingControl(props: SortingControlProps) {
formName, // Name of the form, used by redux-form lib to store the data in redux store formName, // Name of the form, used by redux-form lib to store the data in redux store
} = props; } = props;
const fieldArrayProps = useMemo(
() => ({ configProperty, formName }),
[configProperty, formName],
);
return ( return (
<FieldArray <FieldArray
component={SortingComponent} component={SortingComponent}
key={`${configProperty}`} key={configProperty}
name={`${configProperty}`} name={configProperty}
props={{ props={fieldArrayProps}
configProperty,
formName,
}}
rerenderOnEveryChange={false} rerenderOnEveryChange={false}
/> />
); );
} }
export type SortingControlProps = ControlProps;

View File

@ -1,13 +1,12 @@
{ {
"identifier": "UPLOAD_FILE_FROM_BODY", "identifier": "UPLOAD_FILE_FROM_BODY",
"controlType": "SECTION", "controlType": "SECTION_V2",
"conditionals": { "conditionals": {
"show": "{{actionConfiguration.formData.command.data === 'UPLOAD_FILE_FROM_BODY'}}" "show": "{{actionConfiguration.formData.command.data === 'UPLOAD_FILE_FROM_BODY'}}"
}, },
"children": [ "children": [
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Select bucket to query",
"children": [ "children": [
{ {
"label": "Bucket name", "label": "Bucket name",
@ -16,12 +15,17 @@
"evaluationSubstitutionType": "TEMPLATE", "evaluationSubstitutionType": "TEMPLATE",
"isRequired": true, "isRequired": true,
"initialValue": "" "initialValue": ""
},
{
"label": "Expiry duration of signed URL (minutes)",
"configProperty": "actionConfiguration.formData.create.expiry.data",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"initialValue": "5"
} }
] ]
}, },
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Query",
"description": "Optional", "description": "Optional",
"children": [ "children": [
{ {
@ -45,13 +49,13 @@
"value": "NO" "value": "NO"
} }
] ]
}
]
}, },
{ {
"label": "Expiry duration of signed URL (minutes)", "controlType": "SINGLE_COLUMN_ZONE",
"configProperty": "actionConfiguration.formData.create.expiry.data", "description": "Optional",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT", "children": [
"initialValue": "5"
},
{ {
"label": "Content", "label": "Content",
"configProperty": "actionConfiguration.formData.body.data", "configProperty": "actionConfiguration.formData.body.data",

View File

@ -1,13 +1,12 @@
{ {
"identifier": "UPLOAD_MULTIPLE_FILES_FROM_BODY", "identifier": "UPLOAD_MULTIPLE_FILES_FROM_BODY",
"controlType": "SECTION", "controlType": "SECTION_V2",
"conditionals": { "conditionals": {
"show": "{{actionConfiguration.formData.command.data === 'UPLOAD_MULTIPLE_FILES_FROM_BODY'}}" "show": "{{actionConfiguration.formData.command.data === 'UPLOAD_MULTIPLE_FILES_FROM_BODY'}}"
}, },
"children": [ "children": [
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Select bucket to query",
"children": [ "children": [
{ {
"label": "Bucket name", "label": "Bucket name",
@ -16,12 +15,17 @@
"evaluationSubstitutionType": "TEMPLATE", "evaluationSubstitutionType": "TEMPLATE",
"isRequired": true, "isRequired": true,
"initialValue": "" "initialValue": ""
},
{
"label": "Expiry duration of signed URL (minutes)",
"configProperty": "actionConfiguration.formData.create.expiry.data",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"initialValue": "5"
} }
] ]
}, },
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Query",
"description": "Optional", "description": "Optional",
"children": [ "children": [
{ {
@ -45,13 +49,13 @@
"value": "NO" "value": "NO"
} }
] ]
}
]
}, },
{ {
"label": "Expiry duration of signed URL (minutes)", "controlType": "SINGLE_COLUMN_ZONE",
"configProperty": "actionConfiguration.formData.create.expiry.data", "description": "Optional",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT", "children": [
"initialValue": "5"
},
{ {
"label": "Content", "label": "Content",
"configProperty": "actionConfiguration.formData.body.data", "configProperty": "actionConfiguration.formData.body.data",

View File

@ -1,13 +1,12 @@
{ {
"identifier": "DELETE_FILE", "identifier": "DELETE_FILE",
"controlType": "SECTION", "controlType": "SECTION_V2",
"conditionals": { "conditionals": {
"show": "{{actionConfiguration.formData.command.data === 'DELETE_FILE'}}" "show": "{{actionConfiguration.formData.command.data === 'DELETE_FILE'}}"
}, },
"children": [ "children": [
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Select bucket to query",
"children": [ "children": [
{ {
"label": "Bucket name", "label": "Bucket name",
@ -20,8 +19,7 @@
] ]
}, },
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Query",
"description": "Optional", "description": "Optional",
"children": [ "children": [
{ {

View File

@ -1,13 +1,12 @@
{ {
"identifier": "DELETE_MULTIPLE_FILES", "identifier": "DELETE_MULTIPLE_FILES",
"controlType": "SECTION", "controlType": "SECTION_V2",
"conditionals": { "conditionals": {
"show": "{{actionConfiguration.formData.command.data === 'DELETE_MULTIPLE_FILES'}}" "show": "{{actionConfiguration.formData.command.data === 'DELETE_MULTIPLE_FILES'}}"
}, },
"children": [ "children": [
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Select bucket to query",
"children": [ "children": [
{ {
"label": "Bucket name", "label": "Bucket name",
@ -16,7 +15,12 @@
"evaluationSubstitutionType": "TEMPLATE", "evaluationSubstitutionType": "TEMPLATE",
"isRequired": true, "isRequired": true,
"initialValue": "" "initialValue": ""
}
]
}, },
{
"controlType": "SINGLE_COLUMN_ZONE",
"children": [
{ {
"label": "List of Files", "label": "List of Files",
"configProperty": "actionConfiguration.formData.path.data", "configProperty": "actionConfiguration.formData.path.data",

View File

@ -1,13 +1,12 @@
{ {
"identifier": "LIST", "identifier": "LIST",
"controlType": "SECTION", "controlType": "SECTION_V2",
"conditionals": { "conditionals": {
"show": "{{actionConfiguration.formData.command.data === 'LIST'}}" "show": "{{actionConfiguration.formData.command.data === 'LIST'}}"
}, },
"children": [ "children": [
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Select bucket to query",
"children": [ "children": [
{ {
"label": "Bucket name", "label": "Bucket name",
@ -16,62 +15,17 @@
"evaluationSubstitutionType": "TEMPLATE", "evaluationSubstitutionType": "TEMPLATE",
"isRequired": true, "isRequired": true,
"initialValue": "" "initialValue": ""
}
]
}, },
{
"controlType": "SECTION",
"label": "Query",
"description": "Optional",
"children": [
{ {
"label": "Prefix", "label": "Prefix",
"configProperty": "actionConfiguration.formData.list.prefix.data", "configProperty": "actionConfiguration.formData.list.prefix.data",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT", "controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"initialValue": "" "initialValue": ""
},
{
"label": "Where",
"configProperty": "actionConfiguration.formData.list.where.data",
"nestedLevels": 3,
"controlType": "WHERE_CLAUSE",
"-subtitle": "Array of Objects",
"-tooltipText": "Array of Objects",
"-alternateViewTypes": ["json"],
"logicalTypes": [
{
"label": "AND",
"value": "AND"
},
{
"label": "OR",
"value": "OR"
}
],
"comparisonTypes": [
{
"label": "==",
"value": "EQ"
},
{
"label": "!=",
"value": "NOT_EQ"
},
{
"label": "in",
"value": "IN"
},
{
"label": "not in",
"value": "NOT_IN"
}
]
} }
] ]
}, },
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Options",
"children": [ "children": [
{ {
"label": "Generate signed URL", "label": "Generate signed URL",
@ -122,9 +76,52 @@
"value": "NO" "value": "NO"
} }
] ]
}
]
}, },
{ {
"label": "Sort By", "controlType": "SINGLE_COLUMN_ZONE",
"description": "Optional",
"children": [
{
"label": "Filter data",
"configProperty": "actionConfiguration.formData.list.where.data",
"nestedLevels": 3,
"controlType": "WHERE_CLAUSE",
"-subtitle": "Array of Objects",
"-tooltipText": "Array of Objects",
"-alternateViewTypes": ["json"],
"logicalTypes": [
{
"label": "AND",
"value": "AND"
},
{
"label": "OR",
"value": "OR"
}
],
"comparisonTypes": [
{
"label": "==",
"value": "EQ"
},
{
"label": "!=",
"value": "NOT_EQ"
},
{
"label": "in",
"value": "IN"
},
{
"label": "not in",
"value": "NOT_IN"
}
]
},
{
"label": "Sort data",
"configProperty": "actionConfiguration.formData.list.sortBy.data", "configProperty": "actionConfiguration.formData.list.sortBy.data",
"controlType": "SORTING", "controlType": "SORTING",
"-subtitle": "Array of Objects", "-subtitle": "Array of Objects",
@ -132,7 +129,7 @@
"-alternateViewTypes": ["json"] "-alternateViewTypes": ["json"]
}, },
{ {
"label": "Paginate By", "label": "Paginate data",
"configProperty": "actionConfiguration.formData.list.pagination.data", "configProperty": "actionConfiguration.formData.list.pagination.data",
"controlType": "PAGINATION", "controlType": "PAGINATION",
"-subtitle": "Object", "-subtitle": "Object",

View File

@ -1,13 +1,12 @@
{ {
"identifier": "READ_FILE", "identifier": "READ_FILE",
"controlType": "SECTION", "controlType": "SECTION_V2",
"conditionals": { "conditionals": {
"show": "{{actionConfiguration.formData.command.data === 'READ_FILE'}}" "show": "{{actionConfiguration.formData.command.data === 'READ_FILE'}}"
}, },
"children": [ "children": [
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Select bucket to query",
"children": [ "children": [
{ {
"label": "Bucket name", "label": "Bucket name",
@ -20,8 +19,7 @@
] ]
}, },
{ {
"controlType": "SECTION", "controlType": "DOUBLE_COLUMN_ZONE",
"label": "Query",
"description": "Optional", "description": "Optional",
"children": [ "children": [
{ {

View File

@ -1,8 +1,11 @@
{ {
"editor": [ "editor": [
{ {
"controlType": "SECTION", "controlType": "SECTION_V2",
"identifier": "SELECTOR", "identifier": "SELECTOR",
"children": [
{
"controlType": "DOUBLE_COLUMN_ZONE",
"children": [ "children": [
{ {
"label": "Command", "label": "Command",
@ -39,6 +42,8 @@
} }
] ]
} }
]
}
], ],
"files": [ "files": [
"create.json", "create.json",