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 FormControl from "pages/Editor/FormControl";
import { Classes } from "@appsmith/ads-old";
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 { getBindingOrConfigPathsForSortingControl } from "entities/Action/actionProperties";
import { SortingSubComponent } from "./utils";
@ -26,9 +29,6 @@ const columnFieldConfig: any = {
initialValue: "",
inputType: "TEXT",
placeholderText: "Column name",
customStyles: {
// width: "280px",
},
};
// Form config for the order field
@ -52,60 +52,42 @@ const orderFieldConfig: any = {
],
};
// main container for the fsorting component
const SortingContainer = styled.div`
display: flex;
flex-direction: column;
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;
const SortingContainer = styled.div<{ isBreakpointSmall: boolean }>`
display: grid;
grid-template-columns: ${({ isBreakpointSmall }) =>
isBreakpointSmall ? "1fr 50px" : "1fr 50px"};
gap: 5px;
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`
display: flex;
flex-direction: row;
`;
// container for the column dropdown section
const ColumnDropdownContainer = styled.div``;
// Component for the icons
const CenteredButton = styled(Button)``;
export type SortingControlProps = ControlProps;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function SortingComponent(props: any) {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const formValues: any = useSelector((state) =>
export type SortingComponentProps = WrappedFieldArrayProps &
Pick<SortingControlProps, "configProperty" | "formName">;
function SortingComponent(props: SortingComponentProps) {
const { configProperty, fields, formName } = props;
const formValues = useSelector((state) =>
getFormValues(props.formName)(state),
);
const onDeletePressed = (index: number) => {
props.fields.remove(index);
fields.remove(index);
};
const targetRef = useRef<HTMLDivElement>(null);
@ -113,105 +95,100 @@ function SortingComponent(props: any) {
const isBreakpointSmall = size === "small";
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 (props.configProperty.endsWith(".data")) {
if (configProperty.endsWith(".data")) {
// we remove the .data and get the path of the sort object
// NOTE: 5 is used because (.data) = 5
sortingObjectPath = props.configProperty.substring(
const sortingObjectPath = configProperty.substring(
0,
props.configProperty.length - 5,
configProperty.length - 5,
);
}
// sortDataValue is the path to the value (.data included) itself in the sort object
const sortDataValue = get(formValues, props.configProperty);
// sort object value is the path to the sort object itself.
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
// 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).
if (!sortObjectValue) {
return;
}
// sortDataValue is the path to the value (.data included) itself in the sort object
const sortDataValue = get(formValues, configProperty);
// sort object value is the path to the sort object itself.
const sortObjectValue = get(formValues, sortingObjectPath);
// 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.
// 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.
if (
(props.fields.length < 1 &&
isArray(sortDataValue) &&
sortDataValue.length < 1) ||
(props.fields.length < 1 && !sortDataValue)
) {
props.fields.push({
column: "",
order: OrderDropDownValues.ASCENDING,
});
} else {
onDeletePressed(props.index);
// 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.
// so we check to see if the sortObjectValue exist first (if the value has been initalized).
if (sortObjectValue) {
// 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.
// 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.
if (
(fields.length < 1 &&
isArray(sortDataValue) &&
sortDataValue.length < 1) ||
(fields.length < 1 && !sortDataValue)
) {
fields.push({
column: "",
order: OrderDropDownValues.ASCENDING,
});
}
}
}
}, [props.fields.length]);
}, [fields.length]);
return (
<SortingContainer className={`t--${props?.configProperty}`} ref={targetRef}>
{props.fields &&
props.fields.length > 0 &&
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props.fields.map((field: any, index: number) => {
<SortingContainer
className={`t--${props?.configProperty}`}
isBreakpointSmall={isBreakpointSmall}
ref={targetRef}
>
{fields &&
fields.length > 0 &&
fields.map((field, index: number) => {
const columnPath = getBindingOrConfigPathsForSortingControl(
SortingSubComponent.Column,
field,
undefined,
);
const OrderPath = getBindingOrConfigPathsForSortingControl(
const orderPath = getBindingOrConfigPathsForSortingControl(
SortingSubComponent.Order,
field,
undefined,
);
return (
<SortingDropdownContainer key={index} size={size}>
<ColumnDropdownContainer>
<React.Fragment key={index}>
<SortingFields isBreakpointSmall={isBreakpointSmall}>
<FormControl
config={{
...columnFieldConfig,
customStyles: {
width: "250px",
width: "100%",
},
configProperty: `${columnPath}`,
configProperty: columnPath,
nestedFormControl: true,
}}
formName={props.formName}
formName={formName}
/>
</ColumnDropdownContainer>
<FormControl
config={{
...orderFieldConfig,
configProperty: `${OrderPath}`,
nestedFormControl: true,
customStyles: {
width: isBreakpointSmall ? "65px" : "250px",
},
optionWidth: isBreakpointSmall ? "250px" : undefined,
}}
formName={props.formName}
/>
{/* Component to render the delete icon */}
<CenteredButton
<FormControl
config={{
...orderFieldConfig,
customStyles: {
maxWidth: "180px",
},
configProperty: orderPath,
nestedFormControl: true,
}}
formName={formName}
/>
</SortingFields>
<Button
data-testid={`t--sorting-delete-[${index}]`}
isIconButton
kind="tertiary"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
onClick={() => {
onDeletePressed(index);
}}
size="md"
startIcon="close-line"
value={index}
/>
</SortingDropdownContainer>
</React.Fragment>
);
})}
<ButtonWrapper>
@ -219,7 +196,7 @@ function SortingComponent(props: any) {
data-testid={`t--sorting-add-field`}
kind="tertiary"
onClick={() =>
props.fields.push({
fields.push({
column: "",
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
} = props;
const fieldArrayProps = useMemo(
() => ({ configProperty, formName }),
[configProperty, formName],
);
return (
<FieldArray
component={SortingComponent}
key={`${configProperty}`}
name={`${configProperty}`}
props={{
configProperty,
formName,
}}
key={configProperty}
name={configProperty}
props={fieldArrayProps}
rerenderOnEveryChange={false}
/>
);
}
export type SortingControlProps = ControlProps;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,12 @@
{
"identifier": "LIST",
"controlType": "SECTION",
"controlType": "SECTION_V2",
"conditionals": {
"show": "{{actionConfiguration.formData.command.data === 'LIST'}}"
},
"children": [
{
"controlType": "SECTION",
"label": "Select bucket to query",
"controlType": "DOUBLE_COLUMN_ZONE",
"children": [
{
"label": "Bucket name",
@ -16,62 +15,17 @@
"evaluationSubstitutionType": "TEMPLATE",
"isRequired": true,
"initialValue": ""
}
]
},
{
"controlType": "SECTION",
"label": "Query",
"description": "Optional",
"children": [
},
{
"label": "Prefix",
"configProperty": "actionConfiguration.formData.list.prefix.data",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"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",
"label": "Options",
"controlType": "DOUBLE_COLUMN_ZONE",
"children": [
{
"label": "Generate signed URL",
@ -122,9 +76,52 @@
"value": "NO"
}
]
}
]
},
{
"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 By",
"label": "Sort data",
"configProperty": "actionConfiguration.formData.list.sortBy.data",
"controlType": "SORTING",
"-subtitle": "Array of Objects",
@ -132,7 +129,7 @@
"-alternateViewTypes": ["json"]
},
{
"label": "Paginate By",
"label": "Paginate data",
"configProperty": "actionConfiguration.formData.list.pagination.data",
"controlType": "PAGINATION",
"-subtitle": "Object",

View File

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

View File

@ -1,39 +1,44 @@
{
"editor": [
{
"controlType": "SECTION",
"controlType": "SECTION_V2",
"identifier": "SELECTOR",
"children": [
{
"label": "Command",
"description": "Choose the method you would like to use",
"configProperty": "actionConfiguration.formData.command.data",
"controlType": "DROP_DOWN",
"initialValue": "LIST",
"options": [
"controlType": "DOUBLE_COLUMN_ZONE",
"children": [
{
"label": "List files in bucket",
"value": "LIST"
},
{
"label": "Create a new file",
"value": "UPLOAD_FILE_FROM_BODY"
},
{
"label": "Create multiple new files",
"value": "UPLOAD_MULTIPLE_FILES_FROM_BODY"
},
{
"label": "Read file",
"value": "READ_FILE"
},
{
"label": "Delete file",
"value": "DELETE_FILE"
},
{
"label": "Delete multiple files",
"value": "DELETE_MULTIPLE_FILES"
"label": "Command",
"description": "Choose the method you would like to use",
"configProperty": "actionConfiguration.formData.command.data",
"controlType": "DROP_DOWN",
"initialValue": "LIST",
"options": [
{
"label": "List files in bucket",
"value": "LIST"
},
{
"label": "Create a new file",
"value": "UPLOAD_FILE_FROM_BODY"
},
{
"label": "Create multiple new files",
"value": "UPLOAD_MULTIPLE_FILES_FROM_BODY"
},
{
"label": "Read file",
"value": "READ_FILE"
},
{
"label": "Delete file",
"value": "DELETE_FILE"
},
{
"label": "Delete multiple files",
"value": "DELETE_MULTIPLE_FILES"
}
]
}
]
}