diff --git a/README.md b/README.md index a87beb0504..34fe65160a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

-

The quickest way to build dashboards, workflows, forms, or any kind of internal business tool.

+

The quickest way to build dashboards, workflows, forms, or any kind of internal business tool.

@@ -22,12 +22,12 @@ ----------------- -Appsmith is a fast, easy, and secure way to build any custom internal tools. Using Appsmith is easy. -1. Create a page using pre-built UI components like table, charts, and forms. +Appsmith is a fast, easy, and secure way to build any custom internal tools. Here's how you build something: +1. Create a page using pre-built UI components like table, charts, map viewers and forms. 2. Connect the UI components to any REST API or databases like MySQL, Postgres, and MongoDB. Write any logic in JS. -3. Then deploy the interal tool to a custom URL and invite users to sign in with their Google acounts. +3. Deploy the interal tool to a custom URL and invite users to sign in with their Google acounts. -Do all this **without HTML/CSS**, and writing any custom integrations. +Do all this without depending on multiple UI libraries, coding authentication logic or writing any DB or API integrations. See the looped gif below to see how something gets built on Appsmith. It might remind you of Visual Basic. ![UI Builder Demo](https://github.com/appsmithOrg/appsmith/blob/readme/static/demo.gif) @@ -35,27 +35,27 @@ Do all this **without HTML/CSS**, and writing any custom integrations. ## Example Applications -* [Customer Support Dashboard](https://app.appsmith.com/applications/5f2aeb2580ca1f6faaed4e4a/pages/5f2d61b580ca1f6faaed4e79#utm_source=github&utm_medium=homepage) +* [Customer Support Dashboard](https://bit.ly/cs-dashboard-appsmith) ## Getting Started - -* [Cloud Hosted](https://app.appsmith.com/user/signup) -* [Deploy with Docker](https://docs.appsmith.com/quick-start#docker) +You can try our online sandbox or deploy a Docker image on a server. +* [Online sandbox](https://bit.ly/appsmith-signup-github) +* [Deploy with Docker](https://bit.ly/appsmith-docker-github) ## Why Appsmith? -If we're building an internal tool, our obvious choices are to use an admin panel or if we happen to be blessed with HTML/CSS skills, we might **"bootstrap"** a new project. While this may seem like a great idea at first, internal tools almost always require a ton of iterations and eventually we reach a point where we're either spending a lot of time customising the admin panel or the UI terrifies users so much, they prefer [holding our hand](https://giphy.com/gifs/agentm-agent-m-1gg6pvaqHBv56/fullscreen) every time they try to use it. +When we build internal tools today, we turn to admin panels frameworks or use a bootstrap theme. We took inspirations from the best admin panels, bootstrap themes, and brought back the easy UI builder of Visual Basic. -Appsmith provides a better way of building internal tools by visualising them as modular blocks (**Widgets, APIs, Queries, JS**) and giving developers a simple user interface to configure them. With appsmith updating UI, changing dataflows and modifying business logic becomes a [piece of cake](https://i.kym-cdn.com/photos/images/newsfeed/001/355/125/5ca.png) because you no longer have to trudge through large undocumented code bases or **fight with HTML/CSS**. We understand that while some configurations are static and are better controlled via UI, a lot of it is dynamic and should be configured through code. Appsmith doesn't take the fun out of coding, instead it treats every block as an object and exposes it via javascript so that you can read, transform and manipulate it. Whether it's a widget, API or query, you get to decide where you need to configure and where you need to code. +Appsmith is a quicker way of building internal tools by visualising them as modular blocks (**Widgets, APIs, Queries, JS**) and giving developers a simple user interface to configure them. New features, UI modification, changing dataflows, and modifying business logic becomes a [piece of cake](https://i.kym-cdn.com/photos/images/newsfeed/001/355/125/5ca.png) because you no longer have to trudge through large undocumented code bases or wrestle with HTML/CSS. We understand that while some configurations are static and are better controlled via UI, a lot of it is dynamic and should be configured through code. Appsmith doesn't take the fun out of coding, instead it treats every block as an object and exposes it via javascript so that you can read, transform and manipulate it. Whether it's a widget, API or query, you get to decide where you need to configure and where you need to code. ## Features * **Build custom UI**: Drag & drop, resize and style widgets **without HTML / CSS**. [Read more](https://docs.appsmith.com/core-concepts/building-the-ui) -* **Query data**: Query & update your database directly from the UI. Supports **postgres, mongo, REST & GraphQL APIs**. [Read more](https://docs.appsmith.com/core-concepts/building-the-ui/displaying-api-data) -* **JS Logic**: Write snippets of business logic using JS to transform data, manipuate UI or trigger workflows +* **Query data**: Query & update your database directly from the UI. Connect to **postgres, mongo, MySQL, REST & GraphQL APIs**. [Read more](https://docs.appsmith.com/core-concepts/building-the-ui/displaying-api-data) +* **JS Logic**: Write snippets of business logic using JS to transform data, manipuate UI or trigger workflows. Use Lodash functions * **Data Workflows**: Simple configuration to create flows when users interact with the UI. [Read more](https://docs.appsmith.com/core-concepts/building-the-ui/calling-apis-from-widgets) * **Realtime Editor**: Changes in your application reflect instantly with every edit. No need to compile! -* **Works with existing, live databases**: Connect directly to any Postgres & Mongo db +* **Works with existing, live databases**: Connect directly to any Postgres, MySQL, & Mongo db * **Fine-grained access control**: Control who can edit / view your applications from a single control panel * **App management**: Build and organise multiple applications on a single platform diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index f6916b3b44..87da3f2c72 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -290,6 +290,7 @@ Cypress.Commands.add("SearchEntity", (apiname1, apiname2) => { cy.get(commonlocators.entityExplorersearch) .clear() .type(apiname1); + cy.wait(500); cy.get( commonlocators.entitySearchResult.concat(apiname1).concat("')"), ).should("be.visible"); @@ -303,6 +304,7 @@ Cypress.Commands.add("GlobalSearchEntity", apiname1 => { cy.get(commonlocators.entityExplorersearch) .clear() .type(apiname1); + cy.wait(500); cy.get( commonlocators.entitySearchResult.concat(apiname1).concat("')"), ).should("be.visible"); @@ -451,6 +453,7 @@ Cypress.Commands.add("SearchEntityandOpen", apiname1 => { cy.get(commonlocators.entityExplorersearch) .clear() .type(apiname1); + cy.wait(500); cy.get( commonlocators.entitySearchResult.concat(apiname1).concat("')"), ).should("be.visible"); diff --git a/app/client/src/components/ads/Tabs.tsx b/app/client/src/components/ads/Tabs.tsx index cad85e0622..c5ade3092e 100644 --- a/app/client/src/components/ads/Tabs.tsx +++ b/app/client/src/components/ads/Tabs.tsx @@ -10,7 +10,7 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>` border-radius: 0px; height: 100%; .ads-icon { - margin-right: ${props => props.theme.spaces[4]}px; + margin-right: ${props => props.theme.spaces[3]}px; svg { width: ${props => props.theme.spaces[9]}px; height: ${props => props.theme.spaces[9]}px; @@ -26,7 +26,8 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>` .react-tabs__tab-list { display: flex; align-items: center; - border-bottom: 2px solid ${props => props.theme.colors.blackShades[3]}; + border-bottom: ${props => props.theme.spaces[1] - 2}px solid + ${props => props.theme.colors.blackShades[3]}; color: ${props => props.theme.colors.blackShades[6]}; path { fill: ${props => props.theme.colors.blackShades[6]}; @@ -43,12 +44,14 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>` display: flex; align-items: center; justify-content: flex-start; - padding: 0 0 ${props => props.theme.spaces[2]}px 0; + padding: 0 0 ${props => props.theme.spaces[4]}px 0; margin-right: ${props => props.theme.spaces[12] - 3}px; text-align: center; display: inline-flex; align-items: center; justify-content: center; + border-color: transparent; + position: relative; } .react-tabs__tab:hover { color: ${props => props.theme.colors.blackShades[9]}; @@ -66,13 +69,21 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>` } .react-tabs__tab--selected { color: ${props => props.theme.colors.blackShades[9]}; - border: 0px solid; - border-bottom: ${props => props.theme.colors.info.main} - ${props => props.theme.spaces[1] - 2}px solid; background-color: transparent; + path { fill: ${props => props.theme.colors.blackShades[9]}; } + + &::after { + content: ""; + position: absolute; + width: 100%; + bottom: ${props => props.theme.spaces[0] - 1}px; + left: ${props => props.theme.spaces[0]}px; + height: ${props => props.theme.spaces[1] - 2}px; + background-color: ${props => props.theme.colors.info.main}; + } } .react-tabs__tab:focus:after { content: none; diff --git a/app/client/src/components/ads/TextInput.tsx b/app/client/src/components/ads/TextInput.tsx index 18088dea06..05a531336a 100644 --- a/app/client/src/components/ads/TextInput.tsx +++ b/app/client/src/components/ads/TextInput.tsx @@ -59,7 +59,10 @@ const StyledInput = styled.input< props.isValid ? props.theme.colors.info.main : props.theme.colors.danger.main}; - box-shadow: 0px 0px 0px 4px rgba(203, 72, 16, 0.18); + box-shadow: ${props => + props.isValid + ? "0px 0px 4px 4px rgba(203, 72, 16, 0.18)" + : "0px 0px 4px 4px rgba(226, 44, 44, 0.18)"}; } &:disabled { cursor: not-allowed; diff --git a/app/client/src/components/designSystems/appsmith/CascadeFields.tsx b/app/client/src/components/designSystems/appsmith/CascadeFields.tsx index 9cc03b0158..d200b89351 100644 --- a/app/client/src/components/designSystems/appsmith/CascadeFields.tsx +++ b/app/client/src/components/designSystems/appsmith/CascadeFields.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import styled from "styled-components"; import { Icon, InputGroup } from "@blueprintjs/core"; import CustomizedDropdown from "pages/common/CustomizedDropdown"; @@ -19,6 +19,7 @@ import { DropdownOption, ReactTableFilter, } from "components/designSystems/appsmith/TableFilters"; +import { debounce } from "lodash"; const StyledRemoveIcon = styled( ControlIcons.REMOVE_CONTROL as AnyStyledComponent, @@ -221,6 +222,30 @@ const RenderOptions = (props: { return ; }; +const RenderInput = (props: { + value: string; + onChange: (value: string) => void; +}) => { + const debouncedOnChange = useCallback(debounce(props.onChange, 400), []); + const [value, setValue] = useState(props.value); + const onChange = (event: React.ChangeEvent) => { + const value = event.target.value; + setValue(value); + debouncedOnChange(value); + }; + useEffect(() => { + setValue(props.value); + }, [props.value]); + return ( + + ); +}; + type CascadeFieldProps = { columns: DropdownOption[]; column: string; @@ -402,10 +427,10 @@ const Fields = (props: CascadeFieldProps & { state: CascadeFieldState }) => { payload: condition, }); }; - const onValueChange = (event: React.ChangeEvent) => { + const onValueChange = (value: string) => { dispatch({ type: CascadeFieldActionTypes.CHANGE_VALUE, - payload: event.target.value, + payload: value, }); }; const onDateSelected = (date: string) => { @@ -436,7 +461,6 @@ const Fields = (props: CascadeFieldProps & { state: CascadeFieldState }) => { payload: props, }); }, [props]); - const { operator, column, @@ -488,12 +512,7 @@ const Fields = (props: CascadeFieldProps & { state: CascadeFieldState }) => { ) : null} {showInput ? ( - + ) : null} {showDateInput ? ( diff --git a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx index dcbad406ed..5190e0f925 100644 --- a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from "react"; import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; import Table from "components/designSystems/appsmith/Table"; -import { RenderMode, RenderModes } from "constants/WidgetConstants"; import { debounce } from "lodash"; import { getMenuOptions } from "components/designSystems/appsmith/TableUtilities"; import { @@ -37,7 +36,7 @@ interface ReactTableComponentProps { isDisabled?: boolean; isVisible?: boolean; isLoading: boolean; - renderMode: RenderMode; + editMode: boolean; width: number; height: number; pageSize: number; @@ -282,7 +281,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { hiddenColumns={props.hiddenColumns} updateHiddenColumns={props.updateHiddenColumns} data={props.tableData} - displayColumnActions={props.renderMode === RenderModes.CANVAS} + editMode={props.editMode} columnNameMap={props.columnNameMap} getColumnMenu={getColumnMenu} handleColumnNameUpdate={handleColumnNameUpdate} @@ -306,7 +305,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { enableDrag={() => { props.disableDrag(false); }} - searchTableData={debounce(props.searchTableData, 500)} + searchTableData={props.searchTableData} filters={props.filters} applyFilter={props.applyFilter} compactMode={props.compactMode} diff --git a/app/client/src/components/designSystems/appsmith/SearchComponent.tsx b/app/client/src/components/designSystems/appsmith/SearchComponent.tsx index 4ba4ee05f0..02b56c0036 100644 --- a/app/client/src/components/designSystems/appsmith/SearchComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/SearchComponent.tsx @@ -1,6 +1,7 @@ import React from "react"; import styled from "styled-components"; import { InputGroup } from "@blueprintjs/core"; +import { debounce } from "lodash"; interface SearchProps { onSearch: (value: any) => void; @@ -25,6 +26,7 @@ class SearchComponent extends React.Component< SearchProps, { localValue: string } > { + onDebouncedSearch = debounce(this.props.onSearch, 400); constructor(props: SearchProps) { super(props); this.state = { @@ -45,7 +47,7 @@ class SearchComponent extends React.Component< ) => { const search = event.target.value; this.setState({ localValue: search }); - this.props.onSearch(search); + this.onDebouncedSearch(search); }; render() { return ( diff --git a/app/client/src/components/designSystems/appsmith/Table.tsx b/app/client/src/components/designSystems/appsmith/Table.tsx index 0daaaba156..3aae6e6df9 100644 --- a/app/client/src/components/designSystems/appsmith/Table.tsx +++ b/app/client/src/components/designSystems/appsmith/Table.tsx @@ -33,7 +33,7 @@ interface TableProps { hiddenColumns?: string[]; updateHiddenColumns: (hiddenColumns?: string[]) => void; data: object[]; - displayColumnActions: boolean; + editMode: boolean; columnNameMap?: { [key: string]: string }; getColumnMenu: (columnIndex: number) => ColumnMenuOptionProps[]; handleColumnNameUpdate: (columnIndex: number, columnName: string) => void; @@ -142,7 +142,7 @@ export const Table = (props: TableProps) => { updateHiddenColumns={props.updateHiddenColumns} filters={props.filters} applyFilter={props.applyFilter} - displayColumnActions={props.displayColumnActions} + editMode={props.editMode} compactMode={props.compactMode} updateCompactMode={props.updateCompactMode} tableSizes={tableSizes} @@ -168,7 +168,7 @@ export const Table = (props: TableProps) => { } columnIndex={columnIndex} isHidden={column.isHidden} - displayColumnActions={props.displayColumnActions} + editMode={props.editMode} handleColumnNameUpdate={props.handleColumnNameUpdate} getColumnMenu={props.getColumnMenu} handleResizeColumn={props.handleResizeColumn} diff --git a/app/client/src/components/designSystems/appsmith/TableFilters.tsx b/app/client/src/components/designSystems/appsmith/TableFilters.tsx index 93a61a04b0..03d6aa8d6a 100644 --- a/app/client/src/components/designSystems/appsmith/TableFilters.tsx +++ b/app/client/src/components/designSystems/appsmith/TableFilters.tsx @@ -18,6 +18,13 @@ import { Operator, OperatorTypes, } from "widgets/TableWidget"; +import { TABLE_FILTER_COLUMN_TYPE_CALLOUT } from "constants/messages"; + +const TableFilterOuterWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; const TableFilerWrapper = styled.div` display: flex; @@ -37,6 +44,41 @@ const ButtonWrapper = styled.div` background: transparent; } `; + +const SelectedFilterWrapper = styled.div` + position: absolute; + top: 12px; + right: 12px; + background: ${Colors.GREEN}; + border: 0.5px solid ${Colors.WHITE}; + box-sizing: border-box; + border-radius: 50%; + width: 10px; + height: 10px; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + font-size: 6px; + color: ${Colors.WHITE}; +`; + +const ColumnTypeBindingMessage = styled.div` + width: 100%; + height: 41px; + line-height: 41px; + background: ${Colors.ATHENS_GRAY_DARKER}; + border: 1px dashed ${Colors.GEYSER_LIGHT}; + box-sizing: border-box; + font-size: 12px; + color: ${Colors.SLATE_GRAY}; + letter-spacing: 0.04em; + font-weight: 500; + padding: 0 16px; + min-width: 350px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +`; export interface ReactTableFilter { column: string; operator: Operator; @@ -53,6 +95,7 @@ interface TableFilterProps { columns: ReactTableColumnProps[]; filters?: ReactTableFilter[]; applyFilter: (filters: ReactTableFilter[]) => void; + editMode: boolean; } const TableFilters = (props: TableFilterProps) => { @@ -135,51 +178,63 @@ const TableFilters = (props: TableFilterProps) => { > - - e.stopPropagation()}> - {filters.map((filter: ReactTableFilter, index: number) => { - return ( - = 2 ? filters[1].operator : filter.operator - } - column={filter.column} - condition={filter.condition} - value={filter.value} - columns={columns} - applyFilter={(filter: ReactTableFilter, index: number) => { - const updatedFilters = props.filters ? [...props.filters] : []; - updatedFilters[index] = filter; - props.applyFilter(updatedFilters); - }} - removeFilter={(index: number) => { - const filters: ReactTableFilter[] = props.filters || []; - if (index === 1 && filters.length > 2) { - filters[2].operator = filters[1].operator; - } - const newFilters = [ - ...filters.slice(0, index), - ...filters.slice(index + 1), - ]; - props.applyFilter(newFilters); - }} - /> - ); - })} {showAddFilter ? ( - -