Merge branch 'release' of github.com:appsmithorg/appsmith into release
This commit is contained in:
commit
0087efa6a4
28
README.md
28
README.md
|
|
@ -4,7 +4,7 @@
|
|||
</a>
|
||||
<br>
|
||||
<p>
|
||||
<h3>The quickest way to build dashboards, workflows, forms, or any kind of internal business tool. </h3>
|
||||
<h3>The quickest way to build dashboards, workflows, forms, or any kind of internal business tool.</h3>
|
||||
</p>
|
||||
<p>
|
||||
</p>
|
||||
|
|
@ -22,12 +22,12 @@
|
|||
</div>
|
||||
|
||||
-----------------
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 <CustomizedDropdown {...configs} />;
|
||||
};
|
||||
|
||||
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<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
setValue(value);
|
||||
debouncedOnChange(value);
|
||||
};
|
||||
useEffect(() => {
|
||||
setValue(props.value);
|
||||
}, [props.value]);
|
||||
return (
|
||||
<StyledInputGroup
|
||||
placeholder="Enter value"
|
||||
onChange={onChange}
|
||||
type="text"
|
||||
defaultValue={value}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type CascadeFieldProps = {
|
||||
columns: DropdownOption[];
|
||||
column: string;
|
||||
|
|
@ -402,10 +427,10 @@ const Fields = (props: CascadeFieldProps & { state: CascadeFieldState }) => {
|
|||
payload: condition,
|
||||
});
|
||||
};
|
||||
const onValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 }) => {
|
|||
</DropdownWrapper>
|
||||
) : null}
|
||||
{showInput ? (
|
||||
<StyledInputGroup
|
||||
placeholder="Enter value"
|
||||
onChange={onValueChange}
|
||||
type="text"
|
||||
defaultValue={value}
|
||||
/>
|
||||
<RenderInput onChange={onValueChange} value={value} />
|
||||
) : null}
|
||||
{showDateInput ? (
|
||||
<DatePickerWrapper>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
>
|
||||
<FilterIcon />
|
||||
</IconWrapper>
|
||||
</TableIconWrapper>
|
||||
<TableFilerWrapper onClick={e => e.stopPropagation()}>
|
||||
{filters.map((filter: ReactTableFilter, index: number) => {
|
||||
return (
|
||||
<CascadeFields
|
||||
key={index}
|
||||
index={index}
|
||||
operator={
|
||||
filters.length >= 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 ? (
|
||||
<ButtonWrapper className={Classes.POPOVER_DISMISS}>
|
||||
<Button
|
||||
intent="primary"
|
||||
text="Add Filter"
|
||||
size="small"
|
||||
onClick={addFilter}
|
||||
icon="plus"
|
||||
/>
|
||||
</ButtonWrapper>
|
||||
<SelectedFilterWrapper>{filters.length}</SelectedFilterWrapper>
|
||||
) : null}
|
||||
</TableFilerWrapper>
|
||||
</TableIconWrapper>
|
||||
<TableFilterOuterWrapper>
|
||||
<TableFilerWrapper onClick={e => e.stopPropagation()}>
|
||||
{filters.map((filter: ReactTableFilter, index: number) => {
|
||||
return (
|
||||
<CascadeFields
|
||||
key={index}
|
||||
index={index}
|
||||
operator={
|
||||
filters.length >= 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 ? (
|
||||
<ButtonWrapper className={Classes.POPOVER_DISMISS}>
|
||||
<Button
|
||||
intent="primary"
|
||||
text="Add Filter"
|
||||
size="small"
|
||||
onClick={addFilter}
|
||||
icon="plus"
|
||||
/>
|
||||
</ButtonWrapper>
|
||||
) : null}
|
||||
</TableFilerWrapper>
|
||||
{props.editMode && (
|
||||
<ColumnTypeBindingMessage>
|
||||
{TABLE_FILTER_COLUMN_TYPE_CALLOUT}
|
||||
</ColumnTypeBindingMessage>
|
||||
)}
|
||||
</TableFilterOuterWrapper>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ interface TableHeaderProps {
|
|||
serverSidePaginationEnabled: boolean;
|
||||
filters?: ReactTableFilter[];
|
||||
applyFilter: (filters: ReactTableFilter[]) => void;
|
||||
displayColumnActions: boolean;
|
||||
editMode: boolean;
|
||||
compactMode?: CompactMode;
|
||||
updateCompactMode: (compactMode: CompactMode) => void;
|
||||
width: number;
|
||||
|
|
@ -108,13 +108,14 @@ const TableHeader = (props: TableHeaderProps) => {
|
|||
columns={props.columns}
|
||||
filters={props.filters}
|
||||
applyFilter={props.applyFilter}
|
||||
editMode={props.editMode}
|
||||
/>
|
||||
<TableDataDownload
|
||||
data={props.tableData}
|
||||
columns={props.tableColumns}
|
||||
widgetName={props.widgetName}
|
||||
/>
|
||||
{props.displayColumnActions && (
|
||||
{props.editMode && (
|
||||
<TableColumnsVisibility
|
||||
columns={props.columns}
|
||||
hiddenColumns={props.hiddenColumns}
|
||||
|
|
|
|||
|
|
@ -331,6 +331,7 @@ export const TableIconWrapper = styled.div<{
|
|||
justify-content: center;
|
||||
opacity: ${props => (props.disabled ? 0.6 : 1)};
|
||||
cursor: ${props => !props.disabled && "pointer"};
|
||||
position: relative;
|
||||
&:hover {
|
||||
background: ${Colors.ATHENS_GRAY};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -576,7 +576,7 @@ export const TableHeaderCell = (props: {
|
|||
columnIndex: number;
|
||||
isHidden: boolean;
|
||||
isAscOrder?: boolean;
|
||||
displayColumnActions: boolean;
|
||||
editMode: boolean;
|
||||
handleColumnNameUpdate: (columnIndex: number, name: string) => void;
|
||||
getColumnMenu: (columnIndex: number) => ColumnMenuOptionProps[];
|
||||
sortTableColumn: (columnIndex: number, asc: boolean) => void;
|
||||
|
|
@ -635,7 +635,7 @@ export const TableHeaderCell = (props: {
|
|||
{column.render("Header")}
|
||||
</div>
|
||||
)}
|
||||
{props.displayColumnActions && (
|
||||
{props.editMode && (
|
||||
<div
|
||||
className="column-menu"
|
||||
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||
|
|
|
|||
|
|
@ -152,3 +152,6 @@ export const LIGHTNING_MENU_OPTION_HTML = "Write HTML";
|
|||
export const CHECK_REQUEST_BODY = "Check Request body to debug?";
|
||||
export const DONT_SHOW_THIS_AGAIN = "Don't show this again";
|
||||
export const SHOW_REQUEST = "Show Request";
|
||||
|
||||
export const TABLE_FILTER_COLUMN_TYPE_CALLOUT =
|
||||
"Change column datatype to see filter operators";
|
||||
|
|
|
|||
|
|
@ -397,7 +397,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
|
|||
widgetId={this.props.widgetId}
|
||||
widgetName={this.props.widgetName}
|
||||
searchKey={this.props.searchText}
|
||||
renderMode={this.props.renderMode}
|
||||
editMode={this.props.renderMode === RenderModes.CANVAS}
|
||||
hiddenColumns={hiddenColumns}
|
||||
columnActions={this.props.columnActions}
|
||||
columnNameMap={this.props.columnNameMap}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ public class FieldName {
|
|||
public static final String EMAIL = "email";
|
||||
public static final String ORGANIZATION_ID = "organizationId";
|
||||
public static final String DELETED = "deleted";
|
||||
public static final String CREATED_AT = "createdAt";
|
||||
public static final String DELETED_AT = "deletedAt";
|
||||
public static String ORGANIZATION = "organization";
|
||||
public static String ID = "id";
|
||||
|
|
|
|||
|
|
@ -29,7 +29,12 @@ import com.appsmith.server.services.EncryptionService;
|
|||
import com.appsmith.server.services.OrganizationService;
|
||||
import com.github.cloudyrock.mongock.ChangeLog;
|
||||
import com.github.cloudyrock.mongock.ChangeSet;
|
||||
import com.google.gson.Gson;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.minidev.json.JSONObject;
|
||||
import org.apache.commons.lang.ObjectUtils;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mongodb.UncategorizedMongoDbException;
|
||||
|
|
@ -38,11 +43,17 @@ import org.springframework.data.mongodb.core.index.CompoundIndexDefinition;
|
|||
import org.springframework.data.mongodb.core.index.Index;
|
||||
import org.springframework.data.mongodb.core.index.IndexOperations;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
@ -653,4 +664,131 @@ public class DatabaseChangelog {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@ChangeSet(order = "021", id = "examples-organization", author = "")
|
||||
public void examplesOrganization(MongoTemplate mongoTemplate, EncryptionService encryptionService) throws IOException {
|
||||
final Map<String, String> plugins = new HashMap<>();
|
||||
|
||||
final List<Map<String, Object>> organizationPlugins = mongoTemplate
|
||||
.find(query(where("defaultInstall").is(true)), Plugin.class)
|
||||
.stream()
|
||||
.map(plugin -> {
|
||||
assert plugin.getId() != null;
|
||||
plugins.put(plugin.getPackageName(), plugin.getId());
|
||||
return Map.of(
|
||||
"pluginId", plugin.getId(),
|
||||
"status", "FREE",
|
||||
FieldName.DELETED, false,
|
||||
"policies", Collections.emptyList()
|
||||
);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
final String jsonContent = StreamUtils.copyToString(
|
||||
new DefaultResourceLoader().getResource("examples-organization.json").getInputStream(),
|
||||
Charset.defaultCharset()
|
||||
);
|
||||
|
||||
final Map<String, Object> organization = new Gson().fromJson(jsonContent, HashMap.class);
|
||||
|
||||
final List<Map<String, Object>> datasources = (List) organization.remove("$datasources");
|
||||
final List<Map<String, Object>> applications = (List) organization.remove("$applications");
|
||||
|
||||
organization.put("plugins", organizationPlugins);
|
||||
organization.put(FieldName.CREATED_AT, Instant.now());
|
||||
final Map<String, Object> insertedOrganization = mongoTemplate.insert(organization, mongoTemplate.getCollectionName(Organization.class));
|
||||
final String organizationId = ((ObjectId) insertedOrganization.get("_id")).toHexString();
|
||||
|
||||
final Map<String, String> datasourceIdsByName = new HashMap<>();
|
||||
|
||||
for (final Map<String, Object> datasource : datasources) {
|
||||
datasource.put("pluginId", plugins.get(datasource.remove("$pluginPackageName")));
|
||||
final Map authentication = (Map) ((Map) datasource.get("datasourceConfiguration")).get("authentication");
|
||||
final String plainPassword = (String) authentication.get("password");
|
||||
authentication.put("password", encryptionService.encryptString(plainPassword));
|
||||
datasource.put(FieldName.ORGANIZATION_ID, organizationId);
|
||||
datasource.put(FieldName.CREATED_AT, Instant.now());
|
||||
final Map<String, Object> insertedDatasource = mongoTemplate.insert(datasource, mongoTemplate.getCollectionName(Datasource.class));
|
||||
datasourceIdsByName.put((String) datasource.get("name"), ((ObjectId) insertedDatasource.get("_id")).toHexString());
|
||||
}
|
||||
|
||||
for (final Map<String, Object> application : applications) {
|
||||
final List<Map<String, Object>> fullPages = (List) application.remove("$pages");
|
||||
final List<Map<String, Object>> embeddedPages = new ArrayList<>();
|
||||
|
||||
application.put(FieldName.ORGANIZATION_ID, organizationId);
|
||||
mongoTemplate.insert(application, mongoTemplate.getCollectionName(Application.class));
|
||||
final String applicationId = ((ObjectId) application.get("_id")).toHexString();
|
||||
|
||||
for (final Map<String, Object> fullPage : fullPages) {
|
||||
final boolean isDefault = (boolean) fullPage.remove("$isDefault");
|
||||
|
||||
final List<Map<String, Object>> actions = (List) ObjectUtils.defaultIfNull(
|
||||
fullPage.remove("$actions"), Collections.emptyList());
|
||||
|
||||
final List<Map<String, Object>> layouts = (List) fullPage.getOrDefault("layouts", Collections.emptyList());
|
||||
for (final Map<String, Object> layout : layouts) {
|
||||
layout.put("_id", new ObjectId());
|
||||
}
|
||||
|
||||
fullPage.put("applicationId", applicationId);
|
||||
fullPage.put(FieldName.CREATED_AT, Instant.now());
|
||||
final Map<String, Object> insertedPage = mongoTemplate.insert(fullPage, mongoTemplate.getCollectionName(Page.class));
|
||||
final String pageId = ((ObjectId) insertedPage.get("_id")).toHexString();
|
||||
embeddedPages.add(Map.of(
|
||||
"_id", pageId,
|
||||
"isDefault", isDefault
|
||||
));
|
||||
|
||||
final Map<String, String> actionIdsByName = new HashMap<>();
|
||||
for (final Map<String, Object> action : actions) {
|
||||
final Map<String, Object> datasource = (Map) action.get("datasource");
|
||||
datasource.put("pluginId", plugins.get(datasource.remove("$pluginPackageName")));
|
||||
datasource.put(FieldName.ORGANIZATION_ID, organizationId);
|
||||
if (Boolean.FALSE.equals(datasource.remove("$isEmbedded"))) {
|
||||
datasource.put("_id", new ObjectId(datasourceIdsByName.get(datasource.get("name"))));
|
||||
}
|
||||
action.put(FieldName.ORGANIZATION_ID, organizationId);
|
||||
action.put("pageId", pageId);
|
||||
action.put("pluginId", plugins.get(action.remove("$pluginPackageName")));
|
||||
action.put(FieldName.CREATED_AT, Instant.now());
|
||||
final Map<String, Object> insertedAction = mongoTemplate.insert(action, mongoTemplate.getCollectionName(Action.class));
|
||||
actionIdsByName.put((String) action.get("name"), ((ObjectId) insertedAction.get("_id")).toHexString());
|
||||
}
|
||||
|
||||
final List<Map<String, Object>> layouts1 = (List) insertedPage.get("layouts");
|
||||
for (Map<String, Object> layout : layouts1) {
|
||||
final List<List<Map<String, Object>>> onLoadActions = (List) layout.getOrDefault("layoutOnLoadActions", Collections.emptyList());
|
||||
for (final List<Map<String, Object>> actionSet : onLoadActions) {
|
||||
for (final Map<String, Object> action : actionSet) {
|
||||
action.put("_id", new ObjectId(actionIdsByName.get(action.get("name"))));
|
||||
}
|
||||
}
|
||||
final List<List<Map<String, Object>>> onLoadActions2 = (List) layout.getOrDefault("publishedLayoutOnLoadActions", Collections.emptyList());
|
||||
for (final List<Map<String, Object>> actionSet : onLoadActions2) {
|
||||
for (final Map<String, Object> action : actionSet) {
|
||||
action.put("_id", new ObjectId(actionIdsByName.get(action.get("name"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
mongoTemplate.updateFirst(
|
||||
query(where("_id").is(pageId)),
|
||||
update("layouts", layouts1),
|
||||
Page.class
|
||||
);
|
||||
}
|
||||
|
||||
application.put("pages", embeddedPages);
|
||||
mongoTemplate.updateFirst(
|
||||
query(where("_id").is(applicationId)),
|
||||
update("pages", embeddedPages),
|
||||
Application.class
|
||||
);
|
||||
}
|
||||
|
||||
Config config = new Config();
|
||||
config.setName("template-organization");
|
||||
config.setConfig(new JSONObject(Map.of("organizationId", organizationId)));
|
||||
mongoTemplate.insert(config);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user