* Add more options to action settings (#473)

* Update install.sh

* Add few ternjs defs for moment (#508)

* default icon selection issue fixed in stories (#495)

* Feature: Toggle component (#490)

* Toggle component implemented with story

* props fixed

* lighten and darken colors implemented

* removed the unused css properties

* removed the used import in story

* toggle hover state fixed

* storywrapper import updated

* Drag And drop widget (#503)

* Drag And drop widget

* Updated cordinates

* Feature: Tooltip component with story (#498)

* tooltip component implemented with story

* default props fixed

* used constant classes names

* blueprint classname corrected

* Feature: Checkbox component (#475)

* checkbox disabled state added and checked UI fixed

* checkbox imported corrected in story

* checkbox implemented using input and label elements.

* align and isChecked props removed

* used styled component for span

* Feature: Radio group component (#481)

* Radio group component with story implemented

* radio group implemented using native input and label.

* radio component height fixed in story

* disabled name in theme corrected for radio

* align prop removed

* removed duplicate user object (#513)

removed id field which is never populated
added help modal in applications page and intercom to load in help modal
shutdown intercom on signout to clear existing chats

Co-authored-by: Nikhil Nandagopal <nikhil@appsmith.com>

* Feature: Table Widget - multi row selection (#507)

* Fix typo (#514)

* Update install.sh

* selectedRowIndexes changed to selectedRowIndices (#516)

Data type of selectedRowIndices changed to array of numbers

* Fixing storybook build (#517)

* Fixes (#522)

- Remove cache fields from settings
- Call the event callback method with success false to fix button widget loading issue

* Add migration to fix incorrect action IDs for onLoad (#519)

* Add migration to fix incorrect action IDs for onLoad

* Remove redundant comments

* Fix migration to work with published onLoad actions as well

* Only update the action ID when correcting onLoad action IDs

* Fix migration name for correcting action IDs

Co-authored-by: Trisha Anand <trisha@appsmith.com>

* Fix migration id for correcting action IDs

* Reformat code

Co-authored-by: Trisha Anand <trisha@appsmith.com>

* deprecated selectedOptionValueArr

* Feature/intercom (#521)

* removed duplicate user object
removed id field which is never populated
added help modal in applications page and intercom to load in help modal
shutdown intercom on signout to clear existing chats

* added analytics to help modal and invite user
unescaped bindings before evaluation to prune newlines

* updated action confirmation text

* added a check to ignore old properties which are no longer dynamic

* fixed test case

* fix tests

Co-authored-by: Nikhil Nandagopal <nikhil@appsmith.com>
Co-authored-by: Hetu Nandu <hetunandu@gmail.com>

* Remove run on page load in api pane (#524)

* Remove run on page load in api pane

* Remove run on page load setting in query pane

* Revert removal of run query on page load

Co-authored-by: akash-codemonk <67054171+akash-codemonk@users.noreply.github.com>
Co-authored-by: Nikhil Nandagopal <nikhil.nandagopal@gmail.com>
Co-authored-by: devrk96 <rohit.kumawat@primathon.in>
Co-authored-by: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com>
Co-authored-by: Nikhil Nandagopal <nikhil@appsmith.com>
Co-authored-by: vicky-primathon <67091118+vicky-primathon@users.noreply.github.com>
Co-authored-by: satbir121 <39981226+satbir121@users.noreply.github.com>
Co-authored-by: Shrikant Sharat Kandula <shrikant@appsmith.com>
Co-authored-by: Trisha Anand <trisha@appsmith.com>
This commit is contained in:
Hetu Nandu 2020-09-10 13:13:17 +05:30 committed by GitHub
parent f7931701d8
commit 86b2f2b017
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1254 additions and 222 deletions

View File

@ -0,0 +1,59 @@
const testdata = require("../../../fixtures/testdata.json");
const apiwidget = require("../../../locators/apiWidgetslocator.json");
const explorer = require("../../../locators/explorerlocators.json");
const commonlocators = require("../../../locators/commonlocators.json");
const formWidgetsPage = require("../../../locators/FormWidgets.json");
const publish = require("../../../locators/publishWidgetspage.json");
const pageid = "MyPage";
describe("Entity explorer Drag and Drop widgets testcases", function() {
it("Drag and drop form widget and validate", function() {
cy.log("Login Successful");
cy.get(explorer.addWidget).click();
cy.get(commonlocators.entityExplorersearch).should("be.visible");
cy.get(commonlocators.entityExplorersearch)
.clear()
.type("form");
cy.dragAndDropToCanvas("formwidget");
/**
* @param{Text} Random Text
* @param{FormWidget}Mouseover
* @param{FormPre Css} Assertion
*/
cy.widgetText(
"FormTest",
formWidgetsPage.formWidget,
formWidgetsPage.formInner,
);
/**
* @param{Text} Random Colour
*/
cy.testCodeMirror(this.data.colour);
cy.get(formWidgetsPage.formD)
.should("have.css", "background-color")
.and("eq", this.data.rgbValue);
/**
* @param{toggleButton Css} Assert to be checked
*/
cy.togglebar(commonlocators.scrollView);
cy.get(formWidgetsPage.formD)
.scrollTo("bottom")
.should("be.visible");
cy.get(commonlocators.editPropCrossButton).click();
cy.get(explorer.closeWidgets).click();
cy.PublishtheApp();
cy.get(publish.backToEditor)
.first()
.click();
cy.SearchEntityandOpen("FormTest");
cy.get(explorer.property)
.last()
.click({ force: true });
cy.get(apiwidget.propertyList).then(function($lis) {
expect($lis).to.have.length(2);
expect($lis.eq(0)).to.contain("{{FormTest.isVisible}}");
expect($lis.eq(1)).to.contain("{{FormTest.data}}");
});
});
});

View File

@ -0,0 +1,49 @@
const queryLocators = require("../../../locators/QueryEditor.json");
const queryEditor = require("../../../locators/QueryEditor.json");
let datasourceName;
describe("Confirm run action", function() {
beforeEach(() => {
cy.createPostgresDatasource();
cy.get("@createDatasource").then(httpResponse => {
datasourceName = httpResponse.response.body.data.name;
});
});
it("Confirm run action", () => {
cy.NavigateToQueryEditor();
cy.get(".t--datasource-name")
.contains(datasourceName)
.click();
cy.get(queryLocators.templateMenu).click();
cy.get(".CodeMirror textarea")
.first()
.focus()
.type("select * from configs");
cy.get("li:contains('Settings')").click({ force: true });
cy.get("[data-cy=confirmBeforeExecute]")
.find(".bp3-switch")
.click();
cy.get(queryEditor.runQuery).click();
cy.get(".bp3-dialog")
.find(".bp3-button")
.contains("Confirm")
.click();
cy.wait("@postExecute").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
cy.get(queryEditor.deleteQuery).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
cy.deletePostgresDatasource(datasourceName);
});
});

View File

@ -16,5 +16,8 @@
"property": ".language-appsmith-binding",
"editNameField": ".editing input",
"editEntityField": ".bp3-editable-text-input",
"entity":".t--entity-name"
"entity":".t--entity-name",
"addWidget":".widgets .t--entity-add-btn",
"dropHere":".appsmith_widget_0",
"closeWidgets":".t--close-widgets-sidebar"
}

View File

@ -34,6 +34,15 @@ Cypress.Commands.add("createOrg", orgName => {
);
});
Cypress.Commands.add(
"dragTo",
{ prevSubject: "element" },
(subject, targetEl) => {
cy.wrap(subject).trigger("dragstart");
cy.get(targetEl).trigger("drop");
},
);
Cypress.Commands.add("navigateToOrgSettings", orgName => {
cy.get(homePage.orgList.concat(orgName).concat(")"))
.scrollIntoView()
@ -1307,6 +1316,30 @@ Cypress.Commands.add("fillPostgresDatasourceForm", () => {
);
});
Cypress.Commands.add("createPostgresDatasource", () => {
cy.NavigateToDatasourceEditor();
cy.get(datasourceEditor.PostgreSQL).click();
cy.getPluginFormsAndCreateDatasource();
cy.fillPostgresDatasourceForm();
cy.testSaveDatasource();
});
Cypress.Commands.add("deletePostgresDatasource", datasourceName => {
cy.NavigateToDatasourceEditor();
cy.get(".t--entity-name:contains(PostgreSQL)").click();
cy.get(`.t--entity-name:contains(${datasourceName})`).click();
cy.get(".t--delete-datasource").click();
cy.wait("@deleteDatasource").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
});
Cypress.Commands.add("runQuery", () => {
cy.get(queryEditor.runQuery).click();
cy.wait("@postExecute").should(
@ -1363,6 +1396,16 @@ Cypress.Commands.add("runAndDeleteQuery", () => {
);
});
Cypress.Commands.add("dragAndDropToCanvas", widgetType => {
const selector = `.t--widget-card-draggable-${widgetType}`;
cy.get(selector)
.trigger("mousedown", { button: 0 }, { force: true })
.trigger("mousemove", 300, -300, { force: true });
cy.get(explorer.dropHere)
.click()
.trigger("mouseup", { force: true });
});
Cypress.Commands.add("openPropertyPane", widgetType => {
const selector = `.t--draggable-${widgetType}`;
cy.get(selector)

View File

@ -16,8 +16,8 @@
"@craco/craco": "^5.6.1",
"@manaflair/redux-batch": "^1.0.0",
"@optimizely/optimizely-sdk": "^4.0.0",
"@sentry/react": "^5.22.2",
"@sentry/tracing": "^5.22.2",
"@sentry/react": "^5.22.3",
"@sentry/tracing": "^5.22.3",
"@sentry/webpack-plugin": "^1.12.1",
"@types/chance": "^1.0.7",
"@types/lodash": "^4.14.120",

View File

@ -1,14 +1,100 @@
import { CommonComponentProps } from "./common";
import React, { useState } from "react";
import styled from "styled-components";
type CheckboxProps = CommonComponentProps & {
label: string;
isChecked: boolean;
onCheckChange: (isChecked: boolean) => void;
isLoading: boolean;
align: "left" | "right";
cypressSelector?: string;
onCheckChange?: (isChecked: boolean) => void;
};
export default function Checkbox(props: CheckboxProps) {
return null;
}
const Checkmark = styled.span<{
disabled?: boolean;
isChecked?: boolean;
}>`
position: absolute;
top: 1px;
left: 0;
width: ${props => props.theme.spaces[8]}px;
height: ${props => props.theme.spaces[8]}px;
background-color: ${props =>
props.isChecked
? props.disabled
? props.theme.colors.blackShades[3]
: props.theme.colors.info.main
: "transparent"};
border: 2px solid
${props =>
props.isChecked
? props.disabled
? props.theme.colors.blackShades[3]
: props.theme.colors.info.main
: props.theme.colors.blackShades[4]};
&::after {
content: "";
position: absolute;
display: none;
top: 0px;
left: 4px;
width: 6px;
height: 11px;
border: solid
${props =>
props.disabled ? "#565656" : props.theme.colors.blackShades[9]};
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
`;
const StyledCheckbox = styled.label<{
disabled?: boolean;
}>`
position: relative;
display: block;
width: 100%;
cursor: ${props => (props.disabled ? "not-allowed" : "pointer")};
font-weight: ${props => props.theme.typography.p1.fontWeight};
font-size: ${props => props.theme.typography.p1.fontSize}px;
line-height: ${props => props.theme.typography.p1.lineHeight}px;
letter-spacing: ${props => props.theme.typography.p1.letterSpacing}px;
color: ${props => props.theme.colors.blackShades[7]};
padding-left: ${props => props.theme.spaces[12] - 2}px;
input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
input:checked ~ ${Checkmark}:after {
display: block;
}
`;
const Checkbox = (props: CheckboxProps) => {
const [checked, setChecked] = useState<boolean>(false);
const onChangeHandler = (checked: boolean) => {
setChecked(checked);
props.onCheckChange && props.onCheckChange(checked);
};
return (
<StyledCheckbox disabled={props.disabled}>
{props.label}
<input
type="checkbox"
disabled={props.disabled}
checked={checked}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChangeHandler(e.target.checked)
}
/>
<Checkmark disabled={props.disabled} isChecked={checked} />
</StyledCheckbox>
);
};
export default Checkbox;

View File

@ -1,12 +1,154 @@
import { CommonComponentProps } from "./common";
import React, { useState, useEffect } from "react";
import styled from "styled-components";
type RadioProps = CommonComponentProps & {
align?: "horizontal" | "vertical" | "column" | "row";
columns?: number;
rows?: number;
value?: string;
type OptionProps = {
label: string;
value: string;
disabled?: boolean;
onSelect?: (value: string) => void;
};
export default function Radio(props: RadioProps) {
return null;
type RadioProps = CommonComponentProps & {
columns?: number;
rows?: number;
defaultValue: string;
onSelect?: (value: string) => void;
options: OptionProps[];
};
const RadioGroup = styled.div<{
rows?: number;
}>`
display: flex;
flex-wrap: wrap;
${props =>
props.rows && props.rows > 0
? `
flex-direction: column;
height: 100%;
`
: null};
`;
const Radio = styled.label<{
disabled?: boolean;
columns?: number;
rows?: number;
}>`
display: block;
position: relative;
padding-left: ${props => props.theme.spaces[12] - 2}px;
cursor: ${props => (props.disabled ? "not-allowed" : "pointer")};
font-size: ${props => props.theme.typography.p1.fontSize}px;
font-weight: ${props => props.theme.typography.p1.fontWeight};
line-height: ${props => props.theme.typography.p1.lineHeight}px;
letter-spacing: ${props => props.theme.typography.p1.letterSpacing}px;
color: ${props => props.theme.colors.blackShades[9]};
${props =>
props.rows && props.rows > 0
? `flex-basis: calc(100% / ${props.rows})`
: null};
${props =>
props.columns && props.columns > 0
? `
flex-basis: calc(100% / ${props.columns});
margin-bottom: ${props.theme.spaces[11] + 1}px;
`
: null};
input {
position: absolute;
opacity: 0;
cursor: pointer;
}
.checkbox {
position: absolute;
top: 0;
left: 0;
width: ${props => props.theme.spaces[8]}px;
height: ${props => props.theme.spaces[8]}px;
background-color: transparent;
border: ${props => props.theme.spaces[1] - 2}px solid
${props => props.theme.colors.blackShades[4]};
border-radius: 50%;
margin-top: ${props => props.theme.spaces[0]}px;
}
.checkbox:after {
content: "";
position: absolute;
display: none;
}
input:checked ~ .checkbox:after {
display: block;
}
input:disabled ~ .checkbox:after {
background-color: ${props => props.theme.colors.radio.disabled};
}
.checkbox:after {
content: "";
position: absolute;
width: ${props => props.theme.spaces[4]}px;
height: ${props => props.theme.spaces[4]}px;
${props =>
props.disabled
? `background-color: ${props.theme.colors.radio.disabled}`
: `background-color: ${props.theme.colors.info.main};`};
top: ${props => props.theme.spaces[1] - 2}px;
left: ${props => props.theme.spaces[1] - 2}px;
border-radius: 50%;
}
`;
export default function RadioComponent(props: RadioProps) {
const [selected, setSelected] = useState(props.defaultValue);
useEffect(() => {
if (props.rows && props.columns && props.rows > 0 && props.columns > 0) {
console.error(
"Please pass either rows prop or column prop but not both.",
);
}
}, [props]);
useEffect(() => {
setSelected(props.defaultValue);
}, [props.defaultValue]);
const onChangeHandler = (value: string) => {
setSelected(value);
props.onSelect && props.onSelect(value);
};
return (
<RadioGroup
rows={props.rows}
onChange={(e: any) => onChangeHandler(e.target.value)}
>
{props.options.map((option: OptionProps, index: number) => (
<Radio
key={index}
columns={props.columns}
rows={props.rows}
disabled={props.disabled || option.disabled}
>
{option.label}
<input
type="radio"
value={option.value}
disabled={props.disabled || option.disabled}
onChange={e => option.onSelect && option.onSelect(e.target.value)}
checked={selected === option.value}
name="radio"
/>
<span className="checkbox"></span>
</Radio>
))}
</RadioGroup>
);
}

View File

@ -1,6 +1,7 @@
import React from "react";
import styled, { keyframes } from "styled-components";
import { sizeHandler, IconSize } from "./Icon";
import { Classes } from "./common";
const rotate = keyframes`
100% {
@ -23,14 +24,14 @@ const dash = keyframes`
}
`;
const SvgContainer = styled("svg")<SpinnerProp>`
const SvgContainer = styled.svg<SpinnerProp>`
animation: ${rotate} 2s linear infinite;
width: ${props => sizeHandler(props.size)}px;
height: ${props => sizeHandler(props.size)}px;
`;
const SvgCircle = styled("circle")`
stroke: white;
const SvgCircle = styled.circle`
stroke: ${props => props.theme.colors.blackShades[9]};
stroke-linecap: round;
animation: ${dash} 1.5s ease-in-out infinite;
stroke-width: ${props => props.theme.spaces[1]}px;
@ -46,7 +47,11 @@ Spinner.defaultProp = {
export default function Spinner(props: SpinnerProp) {
return (
<SvgContainer viewBox="0 0 50 50" className="new-spinner" size={props.size}>
<SvgContainer
viewBox="0 0 50 50"
className={Classes.SPINNER}
size={props.size}
>
<SvgCircle cx="25" cy="25" r="20" fill="none"></SvgCircle>
</SvgContainer>
);

View File

@ -1,10 +1,149 @@
import { CommonComponentProps } from "./common";
import { CommonComponentProps, Classes, lighten, darken } from "./common";
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import Spinner from "./Spinner";
type ToggleProps = CommonComponentProps & {
onToggle: (value: boolean) => void;
value: boolean;
};
const StyledToggle = styled.label<{
isLoading?: boolean;
disabled?: boolean;
value: boolean;
}>`
position: relative;
display: block;
input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
background-color: ${props =>
props.isLoading
? props.theme.colors.blackShades[3]
: props.theme.colors.blackShades[4]};
transition: 0.4s;
width: 46px;
height: 23px;
border-radius: 92px;
}
${props =>
props.isLoading
? `.toggle-spinner {
position: absolute;
top: 3px;
left: 17px;
}
.slider:before {
display: none;
}`
: `.slider:before {
position: absolute;
content: "";
height: 19px;
width: 19px;
top: 2px;
left: 2px;
background-color: ${
props.disabled && !props.value
? lighten(props.theme.colors.tertiary.dark, 16)
: props.theme.colors.blackShades[9]
};
box-shadow: ${
props.value
? "1px 0px 3px rgba(0, 0, 0, 0.16)"
: "-1px 0px 3px rgba(0, 0, 0, 0.16)"
};
opacity: ${props.value ? 1 : 0.9};
transition: .4s;
border-radius: 50%;
}`}
&& input:hover + .slider:before {
opacity: 1;
}
input:focus + .slider:before {
${props => (props.value ? "opacity: 0.6" : "opacity: 0.7")};
}
input:disabled + .slider:before {
${props => (props.value ? "opacity: 0.24" : "opacity: 1")};
}
input:checked + .slider:before {
transform: translateX(23px);
}
input:checked + .slider {
background-color: ${props => props.theme.colors.info.main};
}
input:hover + .slider,
input:focus + .slider {
background-color: ${props =>
props.value
? lighten(props.theme.colors.info.main, 12)
: lighten(props.theme.colors.blackShades[3], 16)};
}
input:disabled + .slider {
cursor: not-allowed;
background-color: ${props =>
props.value && !props.isLoading
? darken(props.theme.colors.info.darker, 20)
: props.theme.colors.tertiary.dark};
}
.${Classes.SPINNER} {
circle {
stroke: ${props => props.theme.colors.blackShades[6]};
}
}
`;
export default function Toggle(props: ToggleProps) {
return null;
const [value, setValue] = useState(false);
useEffect(() => {
setValue(props.value);
}, [props.value]);
const onChangeHandler = (value: boolean) => {
setValue(value);
props.onToggle && props.onToggle(value);
};
return (
<StyledToggle
isLoading={props.isLoading}
disabled={props.disabled}
value={value}
>
<input
type="checkbox"
checked={value}
disabled={props.disabled || props.isLoading}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChangeHandler(e.target.checked)
}
/>
<span className="slider"></span>
{props.isLoading ? (
<div className="toggle-spinner">
<Spinner />
</div>
) : null}
</StyledToggle>
);
}

View File

@ -0,0 +1,59 @@
import React from "react";
import { CommonComponentProps } from "./common";
import styled from "styled-components";
import { Position, Tooltip, Classes } from "@blueprintjs/core";
import { Classes as CsClasses } from "./common";
type Variant = "dark" | "light";
type TooltipProps = CommonComponentProps & {
content: JSX.Element | string;
position?: Position;
children: JSX.Element;
variant?: Variant;
};
const TooltipWrapper = styled.div<{ variant?: Variant }>`
.${Classes.TOOLTIP} .${Classes.POPOVER_CONTENT} {
padding: 10px 12px;
border-radius: 0px;
background-color: ${props =>
props.variant === "dark"
? props.theme.colors.blackShades[0]
: props.theme.colors.blackShades[8]};
}
div.${Classes.POPOVER_ARROW} {
display: block;
}
.${Classes.TOOLTIP} {
box-shadow: 0px 12px 20px rgba(0, 0, 0, 0.35);a
}
.${Classes.TOOLTIP} .${CsClasses.BP3_POPOVER_ARROW_BORDER},
&&&& .${Classes.TOOLTIP} .${CsClasses.BP3_POPOVER_ARROW_FILL} {
fill: ${props =>
props.variant === "dark"
? props.theme.colors.blackShades[0]
: props.theme.colors.blackShades[8]};
}
`;
const TooltipComponent = (props: TooltipProps) => {
return (
<TooltipWrapper variant={props.variant}>
<Tooltip
content={props.content}
position={props.position}
usePortal={false}
>
{props.children}
</Tooltip>
</TooltipWrapper>
);
};
TooltipComponent.defaultProps = {
position: Position.TOP,
variant: "dark",
};
export default TooltipComponent;

View File

@ -1,4 +1,5 @@
import { Theme } from "constants/DefaultTheme";
import tinycolor from "tinycolor2";
import styled from "styled-components";
export interface CommonComponentProps {
@ -14,6 +15,9 @@ export type ThemeProp = {
export enum Classes {
ICON = "cs-icon",
TEXT = "cs-text",
BP3_POPOVER_ARROW_BORDER = "bp3-popover-arrow-border",
BP3_POPOVER_ARROW_FILL = "bp3-popover-arrow-fill",
SPINNER = "cs-spinner",
}
export const hexToRgb = (
@ -42,6 +46,17 @@ export const hexToRgba = (color: string, alpha: number) => {
return `rgba(${value.r}, ${value.g}, ${value.b}, ${alpha});`;
};
export const lighten = (color: string, amount: number) => {
return tinycolor(color)
.lighten(amount)
.toString();
};
export const darken = (color: string, amount: number) => {
return tinycolor(color)
.darken(amount)
.toString();
};
export const StoryWrapper = styled.div`
background: #1a191c;
height: 700px;

View File

@ -54,7 +54,7 @@ interface ReactTableComponentProps {
serverSidePaginationEnabled: boolean;
columnActions?: ColumnAction[];
selectedRowIndex: number;
selectedRowIndexes: number[];
selectedRowIndices: number[];
multiRowSelection?: boolean;
hiddenColumns?: string[];
columnNameMap?: { [key: string]: string };
@ -301,7 +301,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => {
}}
serverSidePaginationEnabled={props.serverSidePaginationEnabled}
selectedRowIndex={props.selectedRowIndex}
selectedRowIndexes={props.selectedRowIndexes}
selectedRowIndices={props.selectedRowIndices}
disableDrag={() => {
props.disableDrag(true);
}}

View File

@ -49,7 +49,7 @@ interface TableProps {
prevPageClick: () => void;
serverSidePaginationEnabled: boolean;
selectedRowIndex: number;
selectedRowIndexes: number[];
selectedRowIndices: number[];
disableDrag: () => void;
enableDrag: () => void;
searchTableData: (searchKey: any) => void;
@ -111,7 +111,7 @@ export const Table = (props: TableProps) => {
}
const subPage = page.slice(startIndex, endIndex);
const selectedRowIndex = props.selectedRowIndex;
const selectedRowIndexes = props.selectedRowIndexes;
const selectedRowIndices = props.selectedRowIndices;
const tableSizes = TABLE_SIZES[props.compactMode || CompactModeTypes.DEFAULT];
/* Subtracting 9px to handling widget padding */
return (
@ -205,7 +205,7 @@ export const Table = (props: TableProps) => {
"tr" +
`${
row.index === selectedRowIndex ||
selectedRowIndexes.includes(row.index)
selectedRowIndices.includes(row.index)
? " selected-row"
: ""
}`
@ -215,7 +215,7 @@ export const Table = (props: TableProps) => {
props.selectTableRow(
row,
row.index === selectedRowIndex ||
selectedRowIndexes.includes(row.index),
selectedRowIndices.includes(row.index),
);
}}
key={rowIndex}

View File

@ -13,8 +13,11 @@ import { getAppsmithConfigs } from "configs";
import { LayersContext } from "constants/Layers";
import { connect } from "react-redux";
import { AppState } from "reducers";
import { getCurrentUser } from "selectors/usersSelectors";
import { User } from "constants/userConstants";
import AnalyticsUtil from "utils/AnalyticsUtil";
const { algolia } = getAppsmithConfigs();
const { algolia, cloudHosting, intercomAppID } = getAppsmithConfigs();
const HelpButton = styled.button<{
highlight: boolean;
layer: number;
@ -54,10 +57,29 @@ const HelpIcon = HelpIcons.HELP_ICON;
type Props = {
isHelpModalOpen: boolean;
dispatch: any;
user?: User;
page: string;
};
class HelpModal extends React.Component<Props> {
static contextType = LayersContext;
componentDidMount() {
const { user } = this.props;
if (cloudHosting && intercomAppID && window.Intercom) {
window.Intercom("boot", {
// eslint-disable-next-line @typescript-eslint/camelcase
app_id: intercomAppID,
// eslint-disable-next-line @typescript-eslint/camelcase
user_id: user?.username,
// eslint-disable-next-line @typescript-eslint/camelcase
custom_launcher_selector: "#intercom-trigger",
name: user?.name,
email: user?.email,
});
}
}
render() {
const { dispatch, isHelpModalOpen } = this.props;
const layers = this.context;
@ -91,6 +113,7 @@ class HelpModal extends React.Component<Props> {
highlight={!isHelpModalOpen}
layer={layers.help}
onClick={() => {
AnalyticsUtil.logEvent("OPEN_HELP", { page: this.props.page });
dispatch(setHelpModalVisibility(!isHelpModalOpen));
}}
>
@ -104,6 +127,7 @@ class HelpModal extends React.Component<Props> {
const mapStateToProps = (state: AppState) => ({
isHelpModalOpen: getHelpModalOpen(state),
user: getCurrentUser(state),
});
export default connect(mapStateToProps)(HelpModal);

View File

@ -33,7 +33,7 @@ export class SwitchField extends React.Component<Props> {
return (
<div>
<SwitchWrapped>
<SwitchWrapped data-cy={this.props.configProperty}>
<StyledFormLabel>
{label} {isRequired && "*"}
</StyledFormLabel>

View File

@ -3,7 +3,7 @@ import Button, { Size, Category, Variant } from "components/ads/Button";
import { withKnobs, select, boolean, text } from "@storybook/addon-knobs";
import { withDesign } from "storybook-addon-designs";
import { StoryWrapper } from "components/ads/common";
import { IconCollection } from "components/ads/Icon";
import { IconCollection, IconName } from "components/ads/Icon";
export default {
title: "Button",
@ -17,7 +17,11 @@ export const withDynamicProps = () => (
size={select("size", Object.values(Size), Size.large)}
category={select("category", Object.values(Category), Category.primary)}
variant={select("variant", Object.values(Variant), Variant.info)}
icon={select("Icon name", IconCollection, undefined)}
icon={select(
"Icon name",
["Select icon" as IconName, ...IconCollection],
"Select icon" as IconName,
)}
isLoading={boolean("Loading", false)}
disabled={boolean("Disabled", false)}
text={text("text", "Get")}

View File

@ -0,0 +1,22 @@
import React from "react";
import { withKnobs, boolean, text } from "@storybook/addon-knobs";
import { withDesign } from "storybook-addon-designs";
import { action } from "@storybook/addon-actions";
import Checkbox from "components/ads/Checkbox";
import { StoryWrapper } from "components/ads/common";
export default {
title: "Checkbox",
component: Checkbox,
decorators: [withKnobs, withDesign],
};
export const CustomCheckbox = () => (
<StoryWrapper>
<Checkbox
label={text("label", "Checked")}
disabled={boolean("disabled", false)}
onCheckChange={action("check-change")}
/>
</StoryWrapper>
);

View File

@ -28,18 +28,7 @@ export const ButtonIcon = () => (
export const BordelessIcon = () => (
<StoryWrapper>
<Icon
size={select(
"Icon size",
[
IconSize.SMALL,
IconSize.MEDIUM,
IconSize.LARGE,
IconSize.XL,
IconSize.XXL,
IconSize.XXXL,
],
IconSize.LARGE,
)}
size={select("Icon size", Object.values(IconSize), IconSize.LARGE)}
name={select("Icon name", IconCollection, "delete")}
/>
</StoryWrapper>
@ -48,11 +37,7 @@ export const BordelessIcon = () => (
export const AppIconVariant = () => (
<StoryWrapper>
<AppIcon
size={select(
"Icon size",
[Size.small, Size.medium, Size.large],
Size.small,
)}
size={select("Icon size", Object.values(Size), Size.small)}
color={select(
"Icon color",
[

View File

@ -14,7 +14,7 @@ import EditableText, {
EditInteractionKind,
SavingStateHandler,
} from "components/ads/EditableText";
import { IconCollection } from "components/ads/Icon";
import { IconCollection, IconName } from "components/ads/Icon";
export default {
title: "Menu",
@ -104,7 +104,11 @@ export const MenuStory = () => {
<MenuDivider />
<MenuItem
text={text("First option", "Invite user")}
icon={select("First Icon", IconCollection, undefined)}
icon={select(
"First Icon",
["Select icon" as IconName, ...IconCollection],
"Select icon" as IconName,
)}
onSelect={action("clicked-first-option")}
label={<span>W</span>}
disabled={boolean("First option disabled", false)}
@ -112,7 +116,11 @@ export const MenuStory = () => {
{boolean("First menu item divider", false) ? <MenuDivider /> : null}
<MenuItem
text={text("Second option", "Are you sure")}
icon={select("Second Icon", IconCollection, undefined)}
icon={select(
"Second Icon",
["Select icon" as IconName, ...IconCollection],
"Select icon" as IconName,
)}
onSelect={action("clicked-second-option")}
label={<span>W</span>}
/>

View File

@ -0,0 +1,56 @@
import React from "react";
import {
withKnobs,
select,
boolean,
text,
number,
} from "@storybook/addon-knobs";
import { withDesign } from "storybook-addon-designs";
import { StoryWrapper } from "components/ads/common";
import RadioComponent from "components/ads/Radio";
import { action } from "@storybook/addon-actions";
export default {
title: "Radio",
component: RadioComponent,
decorators: [withKnobs, withDesign],
};
export const Radio = () => (
<StoryWrapper>
<div style={{ height: "133px" }}>
<RadioComponent
defaultValue={select(
"defaultValue",
["React", "Angular", "Vue"],
"React",
)}
columns={number("Column number", 2)}
rows={number("Row number", 2)}
disabled={boolean("Radio group disabled", false)}
onSelect={action("selected-radio-option")}
options={[
{
label: "React",
value: "React",
onSelect: action("first-radio-option"),
disabled: boolean("Option-1-disabled", false),
},
{
label: "Angular",
value: "Angular",
onSelect: action("second-radio-option"),
disabled: boolean("Option-2-disabled", false),
},
{
label: "Vue",
value: "Vue",
onSelect: action("third-radio-option"),
disabled: boolean("Option-3-disabled", false),
},
]}
/>
</div>
</StoryWrapper>
);

View File

@ -2,7 +2,7 @@ import React from "react";
import { TabComponent, TabProp } from "components/ads/Tabs";
import { select, text, withKnobs } from "@storybook/addon-knobs";
import { withDesign } from "storybook-addon-designs";
import { IconCollection } from "components/ads/Icon";
import { IconCollection, IconName } from "components/ads/Icon";
import { StoryWrapper } from "components/ads/common";
export default {
@ -87,13 +87,29 @@ const TabStory = (props: any) => {
export const Tabs = () => (
<TabStory
icon1={select("Icon 1", IconCollection, "general")}
icon1={select(
"Icon 1",
["Select icon" as IconName, ...IconCollection],
"Select icon" as IconName,
)}
title1={text("Title 1", "General")}
icon2={select("Icon 2", IconCollection, "user")}
icon2={select(
"Icon 2",
["Select icon" as IconName, ...IconCollection],
"Select icon" as IconName,
)}
title2={text("Title 2", "User")}
icon3={select("Icon 3", IconCollection, "billing")}
icon3={select(
"Icon 3",
["Select icon" as IconName, ...IconCollection],
"Select icon" as IconName,
)}
title3={text("Title 3", "Billing")}
icon4={select("Icon 4", IconCollection, undefined)}
icon4={select(
"Icon 4",
["Select icon" as IconName, ...IconCollection],
"Select icon" as IconName,
)}
title4={text("Title 4", "")}
/>
);

View File

@ -0,0 +1,23 @@
import React from "react";
import { withKnobs, boolean } from "@storybook/addon-knobs";
import { withDesign } from "storybook-addon-designs";
import { action } from "@storybook/addon-actions";
import Toggle from "components/ads/Toggle";
import { StoryWrapper } from "components/ads/common";
export default {
title: "Toggle",
component: Toggle,
decorators: [withKnobs, withDesign],
};
export const CustomToggle = () => (
<StoryWrapper>
<Toggle
value={boolean("switchOn", false)}
disabled={boolean("disabled", false)}
isLoading={boolean("isLoading", false)}
onToggle={action("toggle-on")}
/>
</StoryWrapper>
);

View File

@ -0,0 +1,34 @@
import React from "react";
import { select, withKnobs } from "@storybook/addon-knobs";
import { withDesign } from "storybook-addon-designs";
import { Position } from "@blueprintjs/core";
import TooltipComponent from "components/ads/Tooltip";
import { StoryWrapper } from "components/ads/common";
import Text, { TextType } from "components/ads/Text";
import Button from "components/ads/Button";
export default {
title: "Tooltip",
component: TooltipComponent,
decorators: [withKnobs, withDesign],
};
export const MenuStory = () => (
<StoryWrapper>
<div style={{ paddingTop: "50px", paddingLeft: "50px", width: "200px" }}>
<TooltipComponent
position={select("Position", Object.values(Position), Position.RIGHT)}
content={
<Text type={TextType.P1} highlight>
This is a tooltip
</Text>
}
variant={select("variant", ["dark", "light"], "dark")}
>
<Text type={TextType.P1} highlight>
Hover to show tooltip
</Text>
</TooltipComponent>
</div>
</StoryWrapper>
);

View File

@ -572,6 +572,9 @@ export const theme: Theme = {
darker: "#2B1A1D",
darkest: "#462F32",
},
radio: {
disabled: "#565656",
},
primaryOld: Colors.GREEN,
primaryDarker: Colors.JUNGLE_GREEN,
primaryDarkest: Colors.JUNGLE_GREEN_DARKER,

View File

@ -0,0 +1,109 @@
{
"!name": "moment",
"moment": {
"!type": "fn(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean) -> Moment",
"!url": "https://momentjs.com/docs/#/parsing/",
"!doc": "Returns a wrapper for the Date object",
"now": {
"!type": "fn() -> number",
"!doc": "Returns unix time in milliseconds."
}
},
"Moment": {
"isValid": {
"!type": "fn() -> bool",
"!url": "https://momentjs.com/docs/#/parsing/is-valid/",
"!doc": "Check whether the Moment considers the date invalid"
},
"add": {
"!type": "fn(amount?: DurationInputArg1, unit?: DurationInputArg2)",
"!url": "https://momentjs.com/docs/#/manipulating/add/",
"!doc": "Mutates the original moment by adding time."
},
"format": {
"!type": "fn(format?: string) -> string",
"!url": "https://momentjs.com/docs/#/displaying/format/",
"!doc": "Takes a string of tokens and replaces them with their corresponding values"
},
"isAfter": {
"!type": "fn(inp?: MomentInput, granularity?: ?) -> bool",
"!url": "https://momentjs.com/docs/#/query/is-after/",
"!doc": "Check if a moment is after another moment."
},
"isSame": {
"!type": "fn(inp?: MomentInput, granularity?: ?) -> bool",
"!url": "https://momentjs.com/docs/#/query/is-same/",
"!doc": "Check if a moment is the same as another moment."
},
"isBefore": {
"!type": "fn(inp?: MomentInput, granularity?: ?) -> bool",
"!url": "https://momentjs.com/docs/#/query/is-before/",
"!doc": "Check if a moment is before another moment"
},
"fromNow": {
"!type": "fn(withoutSuffix?: bool) -> string",
"!url": "https://momentjs.com/docs/#/displaying/fromnow/",
"!doc": "Get relative time"
},
"clone": {
"!type": "fn() -> Moment",
"!url": "https://momentjs.com/docs/#/parsing/moment-clone/",
"!doc": "Returns the clone of a moment,"
},
"year": {
"!type": "fn(y: number) -> Moment",
"!url": "https://momentjs.com/docs/#/get-set/year/",
"!doc": "Gets or sets the year"
},
"month": {
"!type": "fn() -> number",
"!url": "https://momentjs.com/docs/#/get-set/month/",
"!doc": "Gets or sets the month"
},
"day": {
"!type": "fn() -> number",
"!url": "https://momentjs.com/docs/#/get-set/day/",
"!doc": "Gets or sets the day of the week."
},
"date": {
"!type": "fn() -> number",
"!url": "https://momentjs.com/docs/#/get-set/date/",
"!doc": "Gets or sets the day of the month."
},
"hour": {
"!type": "fn() -> number",
"!url": "https://momentjs.com/docs/#/get-set/hour/",
"!doc": "Gets or sets the hour."
},
"minute": {
"!type": "fn() -> number",
"!url": "https://momentjs.com/docs/#/get-set/minute/",
"!doc": "Gets or sets the minutes."
},
"second": {
"!type": "fn([s])",
"!url": "https://momentjs.com/docs/#/get-set/second/",
"!doc": "Gets or sets the seconds."
},
"millisecond": {
"!type": "fn([ms])",
"!url": "https://momentjs.com/docs/#/get-set/millisecond/",
"!doc": "Gets or sets the milliseconds."
},
"get": {
"!type": "fn(unit: ?) -> number",
"!url": "https://momentjs.com/docs/#/get-set/get/",
"!doc": "String getter"
},
"set": {
"!type": "fn(unit: ?, value: number) -> Moment",
"!url": "https://momentjs.com/docs/#/get-set/set/",
"!doc": "Generic setter, accepting unit as first argument, and value as second"
},
"toDate": {
"!type": "fn()",
"!url": "https://momentjs.com/docs/#/displaying/as-javascript-date/",
"!doc": "Get a copy of the native Date object that Moment.js wraps"
}
}
}

View File

@ -1,12 +1,15 @@
export const ANONYMOUS_USERNAME = "anonymousUser";
type Gender = "MALE" | "FEMALE";
export type User = {
id: string;
email: string;
currentOrganizationId: string;
organizationIds: string[];
applications: UserApplication[];
username: string;
name: string;
gender: Gender;
};
export interface UserApplication {

View File

@ -71,6 +71,7 @@ export interface Action {
providerId?: string;
provider?: ActionProvider;
documentation?: { text: string };
confirmBeforeExecute?: boolean;
}
export interface RestAction extends Action {

View File

@ -9,12 +9,29 @@ export const queryActionSettingsConfig = [
controlType: "SWITCH",
info: "Will refresh data everytime page is reloaded",
},
{
label: "Request confirmation before running query",
configProperty: "confirmBeforeExecute",
controlType: "SWITCH",
info: "Ask confirmation from the user everytime before refreshing data",
},
// {
// label: "Request confirmation before running query",
// configProperty: "requestConfirmation",
// label: "Cache response",
// configProperty: "shouldCacheResponse",
// controlType: "SWITCH",
// info: "Ask confirmation from the user everytime before refreshing data",
// },
// {
// label: "Cache timeout (in milliseconds)",
// configProperty: "cacheTimeout",
// controlType: "INPUT_TEXT",
// dataType: "NUMBER",
// },
{
label: "Query Timeout (in milliseconds)",
configProperty: "actionConfiguration.timeoutInMillisecond",
controlType: "INPUT_TEXT",
dataType: "NUMBER",
},
],
},
];
@ -24,18 +41,35 @@ export const apiActionSettingsConfig = [
sectionName: "",
id: 1,
children: [
// {
// label: "Run api on Page load",
// configProperty: "executeOnLoad",
// controlType: "SWITCH",
// info: "Will refresh data everytime page is reloaded",
// },
{
label: "Run api on Page load",
configProperty: "executeOnLoad",
label: "Request confirmation before running api",
configProperty: "confirmBeforeExecute",
controlType: "SWITCH",
info: "Will refresh data everytime page is reloaded",
info: "Ask confirmation from the user everytime before refreshing data",
},
// {
// label: "Request confirmation before running api",
// configProperty: "requestConfirmation",
// label: "Cache response",
// configProperty: "shouldCacheResponse",
// controlType: "SWITCH",
// info: "Ask confirmation from the user everytime before refreshing data",
// },
// {
// label: "Cache timeout (in milliseconds)",
// configProperty: "cacheTimeout",
// controlType: "INPUT_TEXT",
// dataType: "NUMBER",
// },
{
label: "Api Timeout (in milliseconds)",
configProperty: "actionConfiguration.timeoutInMillisecond",
controlType: "INPUT_TEXT",
dataType: "NUMBER",
},
],
},
];

View File

@ -39,6 +39,7 @@ import {
import { Directions } from "utils/helpers";
import { HeaderIcons } from "icons/HeaderIcons";
import { duplicateApplication } from "actions/applicationActions";
import HelpModal from "components/designSystems/appsmith/help/HelpModal";
const OrgDropDown = styled.div`
display: flex;
@ -337,6 +338,7 @@ class Applications extends Component<
</OrgSection>
);
})}
<HelpModal page={"Applications"} />
</PageWrapper>
);
}

View File

@ -19,12 +19,14 @@ class ConfirmRunModal extends React.Component<Props> {
const { dispatch, isModalOpen } = this.props;
const handleClose = () => {
dispatch(showRunActionConfirmModal(false));
dispatch(cancelRunActionConfirmModal());
};
return (
<Dialog title="Confirm run" isOpen={isModalOpen} onClose={handleClose}>
<Dialog title="Confirm Action" isOpen={isModalOpen} onClose={handleClose}>
<div className={Classes.DIALOG_BODY}>
Are you sure you want to refresh your current data
Are you sure you want to perform this action?
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
@ -39,7 +41,7 @@ class ConfirmRunModal extends React.Component<Props> {
/>
<Button
filled
text="Confirm and run"
text="Confirm"
intent="primary"
onClick={() => {
dispatch(acceptRunActionConfirmModal());

View File

@ -257,7 +257,7 @@ export const EditorHeader = (props: EditorHeaderProps) => {
/>
</DeploySection>
</HeaderSection>
<HelpModal />
<HelpModal page={"Editor"} />
</HeaderWrapper>
);
};

View File

@ -227,6 +227,7 @@ const TabContainerView = styled.div`
const SettingsWrapper = styled.div`
padding-left: 15px;
padding-top: 8px;
padding-bottom: 8px;
`;
type QueryFormProps = {

View File

@ -34,14 +34,11 @@ import {
WIDGETS_SEARCH_ID,
} from "constants/Explorer";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import { getAppsmithConfigs } from "configs";
import { getCurrentUser } from "selectors/usersSelectors";
import { User } from "constants/userConstants";
import ConfirmRunModal from "pages/Editor/ConfirmRunModal";
import * as Sentry from "@sentry/react";
const { cloudHosting, intercomAppID } = getAppsmithConfigs();
type EditorProps = {
currentApplicationId?: string;
currentPageId?: string;
@ -85,7 +82,6 @@ class Editor extends Component<Props> {
};
componentDidMount() {
const { user } = this.props;
editorInitializer().then(() => {
this.setState({ registered: true });
});
@ -93,21 +89,8 @@ class Editor extends Component<Props> {
if (applicationId && pageId) {
this.props.initEditor(applicationId, pageId);
}
if (cloudHosting && intercomAppID && window.Intercom) {
window.Intercom("boot", {
// eslint-disable-next-line @typescript-eslint/camelcase
app_id: intercomAppID,
// eslint-disable-next-line @typescript-eslint/camelcase
custom_launcher_selector: "#intercom-trigger",
name: user?.username,
email: user?.email,
});
}
}
componentDidUpdate(previously: Props) {
if (cloudHosting && intercomAppID && window.Intercom) {
window.Intercom("update");
}
if (
previously.isPublishing &&
!(this.props.isPublishing || this.props.errorPublishing)

View File

@ -34,6 +34,7 @@ import {
} from "../Applications/permissionHelpers";
import { getAppsmithConfigs } from "configs";
import { ReactComponent as NoEmailConfigImage } from "assets/images/email-not-configured.svg";
import AnalyticsUtil from "utils/AnalyticsUtil";
const OrgInviteTitle = styled.div`
font-weight: bold;
@ -228,6 +229,7 @@ const OrgInviteUsersForm = (props: any) => {
<StyledForm
onSubmit={handleSubmit((values: any, dispatch: any) => {
validateFormValues(values);
AnalyticsUtil.logEvent("INVITE_USER", values);
return inviteUsersToOrg({ ...values, orgId: props.orgId }, dispatch);
})}
>

View File

@ -33,7 +33,7 @@ const usersReducer = createReducer(initialState, {
action: ReduxAction<User>,
) => {
const users = [...state.users];
const userIndex = _.findIndex(users, { id: action.payload.id });
const userIndex = _.findIndex(users, { username: action.payload.username });
if (userIndex > -1) {
users[userIndex] = action.payload;
} else {
@ -54,7 +54,7 @@ const usersReducer = createReducer(initialState, {
action: ReduxAction<User>,
) => {
const users = [...state.list];
const userIndex = _.findIndex(users, { id: action.payload.id });
const userIndex = _.findIndex(users, { username: action.payload.username });
if (userIndex > -1) {
users[userIndex] = action.payload;
} else {

View File

@ -284,8 +284,19 @@ export function* executeActionSaga(
) {
const { actionId, onSuccess, onError, params } = apiAction;
try {
yield put(executeApiActionRequest({ id: apiAction.actionId }));
const api: RestAction = yield select(getAction, actionId);
if (api.confirmBeforeExecute) {
const confirmed = yield call(confirmRunActionSaga);
if (!confirmed) {
if (event.callback) {
event.callback({ success: false });
}
return;
}
}
yield put(executeApiActionRequest({ id: apiAction.actionId }));
const actionParams: Property[] = yield call(
getActionParams,
api.jsonPathKeys,
@ -454,6 +465,25 @@ function* executeAppAction(action: ReduxAction<ExecuteActionPayload>) {
}
}
function* runActionInitSaga(
reduxAction: ReduxAction<{
id: string;
paginationField: PaginationField;
}>,
) {
const action = yield select(getAction, reduxAction.payload.id);
if (action.confirmBeforeExecute) {
const confirmed = yield call(confirmRunActionSaga);
if (!confirmed) return;
}
yield put({
type: ReduxActionTypes.RUN_ACTION_REQUEST,
payload: reduxAction.payload,
});
}
function* runActionSaga(
reduxAction: ReduxAction<{
id: string;
@ -537,33 +567,15 @@ function* runActionSaga(
}
}
function* confirmRunActionSaga(
reduxAction: ReduxAction<{
id: string;
paginationField: PaginationField;
}>,
) {
const action = yield select(getAction, reduxAction.payload.id);
if (action.requestConfirmation) {
yield put(showRunActionConfirmModal(true));
function* confirmRunActionSaga() {
yield put(showRunActionConfirmModal(true));
const { accept } = yield race({
cancel: take(ReduxActionTypes.CANCEL_RUN_ACTION_CONFIRM_MODAL),
accept: take(ReduxActionTypes.ACCEPT_RUN_ACTION_CONFIRM_MODAL),
});
const { accept } = yield race({
cancel: take(ReduxActionTypes.CANCEL_RUN_ACTION_CONFIRM_MODAL),
accept: take(ReduxActionTypes.ACCEPT_RUN_ACTION_CONFIRM_MODAL),
});
if (accept) {
yield put({
type: ReduxActionTypes.RUN_ACTION_REQUEST,
payload: reduxAction.payload,
});
}
} else {
yield put({
type: ReduxActionTypes.RUN_ACTION_REQUEST,
payload: reduxAction.payload,
});
}
return !!accept;
}
function* executePageLoadAction(pageAction: PageAction) {
@ -611,7 +623,7 @@ export function* watchActionExecutionSagas() {
yield all([
takeEvery(ReduxActionTypes.EXECUTE_ACTION, executeAppAction),
takeLatest(ReduxActionTypes.RUN_ACTION_REQUEST, runActionSaga),
takeLatest(ReduxActionTypes.RUN_ACTION_INIT, confirmRunActionSaga),
takeLatest(ReduxActionTypes.RUN_ACTION_INIT, runActionInitSaga),
takeLatest(
ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS,
executePageLoadActionsSaga,

View File

@ -4,6 +4,7 @@ import FeatureFlag from "./featureFlags";
import smartlookClient from "smartlook-client";
import { getAppsmithConfigs } from "configs";
import * as Sentry from "@sentry/react";
import { User } from "../constants/userConstants";
export type EventName =
| "LOGIN_CLICK"
@ -63,18 +64,10 @@ export type EventName =
| "PROPERTY_PANE_OPEN"
| "PROPERTY_PANE_CLOSE"
| "PROPERTY_PANE_OPEN_CLICK"
| "OPEN_HELP"
| "INVITE_USER"
| "PROPERTY_PANE_CLOSE_CLICK";
export type Gender = "MALE" | "FEMALE";
export interface User {
username: string;
name: string;
email: string;
gender: Gender;
currentOrganizationId?: string;
applications: any[];
}
function getApplicationId(location: Location) {
const pathSplit = location.pathname.split("/");
const applicationsIndex = pathSplit.findIndex(
@ -204,6 +197,9 @@ class AnalyticsUtil {
static reset() {
const windowDoc: any = window;
if (windowDoc.Intercom) {
windowDoc.Intercom("shutdown");
}
windowDoc.analytics && windowDoc.analytics.reset();
windowDoc.mixpanel && windowDoc.mixpanel.reset();
}

View File

@ -123,12 +123,7 @@ export const evaluateDynamicBoundValue = (
path: string,
callbackData?: any,
): JSExecutorResult => {
const unescapedInput = unescapeJS(path);
return JSExecutionManagerSingleton.evaluateSync(
unescapedInput,
data,
callbackData,
);
return JSExecutionManagerSingleton.evaluateSync(path, data, callbackData);
};
// For creating a final value where bindings could be in a template format
@ -265,6 +260,8 @@ export function getEvaluatedDataTree(dataTree: DataTree): DataTree {
// Create Dependencies DAG
const createDepsStart = performance.now();
const dataTreeString = JSON.stringify(dataTree);
// Stringify before doing a fast equals because the data tree has functions and fast equal will always treat those as changed values
// Better solve will be to prune functions
if (!equal(dataTreeString, cachedDataTreeString)) {
cachedDataTreeString = dataTreeString;
dependencyTreeCache = createDependencyTree(dataTree);
@ -333,10 +330,20 @@ export const createDependencyTree = (
];
});
if (entity.dynamicBindings) {
Object.keys(entity.dynamicBindings).forEach(prop => {
const { jsSnippets } = getDynamicBindings(_.get(entity, prop));
const existingDeps = dependencyMap[`${entityKey}.${prop}`] || [];
dependencyMap[`${entityKey}.${prop}`] = existingDeps.concat(
Object.keys(entity.dynamicBindings).forEach(propertyName => {
// using unescape to remove new lines from bindings which interfere with our regex extraction
let unevalPropValue = _.get(entity, propertyName);
if (
_.isString(unevalPropValue) &&
isDynamicValue(unevalPropValue)
) {
unevalPropValue = unescapeJS(unevalPropValue);
}
_.set(entity, propertyName, unevalPropValue);
const { jsSnippets } = getDynamicBindings(unevalPropValue);
const existingDeps =
dependencyMap[`${entityKey}.${propertyName}`] || [];
dependencyMap[`${entityKey}.${propertyName}`] = existingDeps.concat(
jsSnippets.filter(jsSnippet => !!jsSnippet),
);
});
@ -350,7 +357,16 @@ export const createDependencyTree = (
if (entity.ENTITY_TYPE === ENTITY_TYPE.ACTION) {
if (entity.dynamicBindingPathList.length) {
entity.dynamicBindingPathList.forEach(prop => {
const { jsSnippets } = getDynamicBindings(_.get(entity, prop.key));
// using unescape to remove new lines from bindings which interfere with our regex extraction
let unevalPropValue = _.get(entity, prop.key);
if (
_.isString(unevalPropValue) &&
isDynamicValue(unevalPropValue)
) {
unevalPropValue = unescapeJS(unevalPropValue);
}
_.set(entity, prop.key, unevalPropValue);
const { jsSnippets } = getDynamicBindings(unevalPropValue);
const existingDeps =
dependencyMap[`${entityKey}.${prop.key}`] || [];
dependencyMap[`${entityKey}.${prop.key}`] = existingDeps.concat(

View File

@ -77,7 +77,7 @@ export const entityDefinitions = {
"!doc": "The value selected in a single select dropdown",
"!url": "https://docs.appsmith.com/widget-reference/dropdown",
},
selectedOptionValueArr: {
selectedOptionValues: {
"!type": "[string]",
"!doc": "The array of values selected in a multi select dropdown",
"!url": "https://docs.appsmith.com/widget-reference/dropdown",

View File

@ -5,6 +5,7 @@ import tern, { Server, Def } from "tern";
import ecma from "tern/defs/ecmascript.json";
import lodash from "constants/defs/lodash.json";
import base64 from "constants/defs/base64-js.json";
import moment from "constants/defs/moment.json";
import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator";
import CodeMirror, { Hint, Pos, cmpPos } from "codemirror";
import {
@ -12,7 +13,7 @@ import {
isDynamicValue,
} from "utils/DynamicBindingUtils";
const DEFS = [ecma, lodash, base64];
const DEFS = [ecma, lodash, base64, moment];
const bigDoc = 250;
const cls = "CodeMirror-Tern-";
const hintDelay = 1700;

View File

@ -26,6 +26,7 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
isRequired: VALIDATION_TYPES.BOOLEAN,
// onOptionChange: VALIDATION_TYPES.ACTION_SELECTOR,
selectedOptionValueArr: VALIDATION_TYPES.ARRAY,
selectedOptionValues: VALIDATION_TYPES.ARRAY,
defaultOptionValue: (
value: string | string[],
props: WidgetProps,
@ -73,6 +74,7 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
selectedIndex: `{{ _.findIndex(this.options, { value: this.selectedOption.value } ) }}`,
selectedIndexArr: `{{ this.selectedOptionValueArr.map(o => _.findIndex(this.options, { value: o })) }}`,
value: `{{ this.selectionType === 'SINGLE_SELECT' ? this.selectedOptionValue : this.selectedOptionValueArr }}`,
selectedOptionValues: `{{ this.selectedOptionValueArr }}`,
};
}
@ -93,6 +95,7 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
return {
selectedOptionValue: undefined,
selectedOptionValueArr: undefined,
selectedOptionValues: undefined,
};
}

View File

@ -93,7 +93,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
pageNo: 1,
pageSize: undefined,
selectedRowIndex: -1,
selectedRowIndexes: "",
selectedRowIndices: [],
searchText: undefined,
selectedRow: {},
selectedRows: [],
@ -351,32 +351,29 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
this.getSelectedRow(filteredTableData),
);
} else {
const selectedRowIndexes = this.getSelectedRowIndexes(
this.props.selectedRowIndexes,
);
super.updateWidgetMetaProperty(
"selectedRows",
filteredTableData.filter((item: object, i: number) => {
return selectedRowIndexes.includes(i);
return this.props.selectedRowIndices.includes(i);
}),
);
}
}
if (this.props.multiRowSelection !== prevProps.multiRowSelection) {
if (this.props.multiRowSelection) {
const selectedRowIndexes = this.props.selectedRowIndex
const selectedRowIndices = this.props.selectedRowIndex
? [this.props.selectedRowIndex]
: [];
super.updateWidgetMetaProperty(
"selectedRowIndexes",
selectedRowIndexes.join(","),
"selectedRowIndices",
selectedRowIndices,
);
super.updateWidgetMetaProperty("selectedRowIndex", -1);
const filteredTableData = this.filterTableData();
super.updateWidgetMetaProperty(
"selectedRows",
filteredTableData.filter((item: object, i: number) => {
return selectedRowIndexes.includes(i);
return selectedRowIndices.includes(i);
}),
);
super.updateWidgetMetaProperty(
@ -385,7 +382,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
);
} else {
const filteredTableData = this.filterTableData();
super.updateWidgetMetaProperty("selectedRowIndexes", "");
super.updateWidgetMetaProperty("selectedRowIndices", []);
super.updateWidgetMetaProperty("selectedRows", []);
super.updateWidgetMetaProperty(
"selectedRow",
@ -406,7 +403,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
tableData,
hiddenColumns,
filteredTableData,
selectedRowIndexes,
selectedRowIndices,
} = this.props;
const tableColumns = this.getTableColumns(tableData);
@ -469,7 +466,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
: this.props.selectedRowIndex
}
multiRowSelection={this.props.multiRowSelection}
selectedRowIndexes={this.getSelectedRowIndexes(selectedRowIndexes)}
selectedRowIndices={selectedRowIndices}
serverSidePaginationEnabled={serverSidePaginationEnabled}
onRowClick={this.handleRowClick}
pageNo={pageNo}
@ -561,25 +558,19 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
};
handleRowClick = (rowData: object, index: number) => {
const { onRowSelected } = this.props;
const { onRowSelected, selectedRowIndices } = this.props;
if (this.props.multiRowSelection) {
const selectedRowIndexes = this.getSelectedRowIndexes(
this.props.selectedRowIndexes,
);
if (selectedRowIndexes.includes(index)) {
const rowIndex = selectedRowIndexes.indexOf(index);
selectedRowIndexes.splice(rowIndex, 1);
if (selectedRowIndices.includes(index)) {
const rowIndex = selectedRowIndices.indexOf(index);
selectedRowIndices.splice(rowIndex, 1);
} else {
selectedRowIndexes.push(index);
selectedRowIndices.push(index);
}
super.updateWidgetMetaProperty(
"selectedRowIndexes",
selectedRowIndexes.join(","),
);
super.updateWidgetMetaProperty("selectedRowIndices", selectedRowIndices);
super.updateWidgetMetaProperty(
"selectedRows",
this.props.filteredTableData.filter((item: object, i: number) => {
return selectedRowIndexes.includes(i);
return selectedRowIndices.includes(i);
}),
);
} else {
@ -616,7 +607,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
resetSelectedRowIndex = () => {
super.updateWidgetMetaProperty("selectedRowIndex", -1);
super.updateWidgetMetaProperty("selectedRowIndexes", "");
super.updateWidgetMetaProperty("selectedRowIndices", []);
};
handlePrevPageClick = () => {
@ -679,7 +670,7 @@ export interface TableWidgetProps extends WidgetProps {
onRowSelected?: string;
onSearchTextChanged: string;
selectedRowIndex?: number;
selectedRowIndexes: string;
selectedRowIndices: number[];
columnActions?: ColumnAction[];
serverSidePaginationEnabled?: boolean;
multiRowSelection?: boolean;

View File

@ -2719,14 +2719,14 @@
dependencies:
any-observable "^0.3.0"
"@sentry/browser@5.22.2":
version "5.22.2"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.22.2.tgz#c37c61c8612a169059ddcd8c08cd09441da7776c"
integrity sha512-kkNRFMcNErtWvz9WI0bG5Va2W+mRWhk5CxaJKWUMdMcGR2rIrl3D+kcMdpYDi9tNYPHUdUzTCb3vJQfO8o6TbA==
"@sentry/browser@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.22.3.tgz#7a64bd1cf01bf393741a3e4bf35f82aa927f5b4e"
integrity sha512-2TzE/CoBa5ZkvxJizDdi1Iz1ldmXSJpFQ1mL07PIXBjCt0Wxf+WOuFSj5IP4L40XHfJE5gU8wEvSH0VDR8nXtA==
dependencies:
"@sentry/core" "5.22.2"
"@sentry/types" "5.22.2"
"@sentry/utils" "5.22.2"
"@sentry/core" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/cli@^1.55.0":
@ -2740,69 +2740,69 @@
progress "^2.0.3"
proxy-from-env "^1.1.0"
"@sentry/core@5.22.2":
version "5.22.2"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.22.2.tgz#3b57300d92c13163c26174311ad82482a08b9266"
integrity sha512-Tj3FlHiqK8uveKh56QP3PhNNrH13LTWqN1TwRwE2B2FLiqwIHGmJsCQNfyslQdBAkNeGRnnQrQxqH53KeuJGGA==
"@sentry/core@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.22.3.tgz#030f435f2b518f282ba8bd954dac90cd70888bd7"
integrity sha512-eGL5uUarw3o4i9QUb9JoFHnhriPpWCaqeaIBB06HUpdcvhrjoowcKZj1+WPec5lFg5XusE35vez7z/FPzmJUDw==
dependencies:
"@sentry/hub" "5.22.2"
"@sentry/minimal" "5.22.2"
"@sentry/types" "5.22.2"
"@sentry/utils" "5.22.2"
"@sentry/hub" "5.22.3"
"@sentry/minimal" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/hub@5.22.2":
version "5.22.2"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.22.2.tgz#812c8250970e44c63ee2bf1f779777d32dd88f54"
integrity sha512-6McBsonfpOY5hzlowzDfdLZklFQ1wWTGtiA0eByKxS/H1GePJc+UUSsu6D3bZJG0bIjFoq5vxLQFSZq6C7BPlQ==
"@sentry/hub@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.22.3.tgz#08309a70d2ea8d5e313d05840c1711f34f2fffe5"
integrity sha512-INo47m6N5HFEs/7GMP9cqxOIt7rmRxdERunA3H2L37owjcr77MwHVeeJ9yawRS6FMtbWXplgWTyTIWIYOuqVbw==
dependencies:
"@sentry/types" "5.22.2"
"@sentry/utils" "5.22.2"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/minimal@5.22.2":
version "5.22.2"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.22.2.tgz#bb050a49158c48596094184cff2806ce8cb63c9d"
integrity sha512-jfth6bY19FXE/kQc6hLBCKg5CjfX1MG+weyEXnPFstCb5JFMvSt6YPRI3OsY1hG3rQLxTX0mVSbe2YrBJE5kXA==
"@sentry/minimal@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.22.3.tgz#706e4029ae5494123d3875c658ba8911aa5cc440"
integrity sha512-HoINpYnVYCpNjn2XIPIlqH5o4BAITpTljXjtAftOx6Hzj+Opjg8tR8PWliyKDvkXPpc4kXK9D6TpEDw8MO0wZA==
dependencies:
"@sentry/hub" "5.22.2"
"@sentry/types" "5.22.2"
"@sentry/hub" "5.22.3"
"@sentry/types" "5.22.3"
tslib "^1.9.3"
"@sentry/react@^5.22.2":
version "5.22.2"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-5.22.2.tgz#8a0f9ee475659faf780080569ca7c7c5cb92b218"
integrity sha512-DgpLWZV5htumjXaE13E/KMODwKyr56bNtk1gaTOPbm368b6zdcM/bwutdNQSTuNlhkJ4bhnhpVs7OzErJkcDLw==
"@sentry/react@^5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-5.22.3.tgz#ed692f9e2aff718da6cd15d2941ddda4f1d63385"
integrity sha512-Or/tLayuxpOJhIWOXiDKdaJQZ981uRS9NT0QcPvU+Si1qTElSqtH1zB94GlwhgpglkbmLPiYq6VPrG2HOiZ79Q==
dependencies:
"@sentry/browser" "5.22.2"
"@sentry/minimal" "5.22.2"
"@sentry/types" "5.22.2"
"@sentry/utils" "5.22.2"
"@sentry/browser" "5.22.3"
"@sentry/minimal" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
hoist-non-react-statics "^3.3.2"
tslib "^1.9.3"
"@sentry/tracing@^5.22.2":
version "5.22.2"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.22.2.tgz#6a326bf43b3491c3e462330f8e7d6cd52a975a2d"
integrity sha512-M4F4CN85luWoHBuBiQMk2/dtpn5L2CriBTcLsKQNkA/fk6lv8CbE1WZ/SkCMWzK4hxo5I2vSyzLlb2OwcMHmdQ==
"@sentry/tracing@^5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.22.3.tgz#9b5a376e3164c007a22e8642ec094104468cac0c"
integrity sha512-Zp59kMCk5v56ZAyErqjv/QvGOWOQ5fRltzeVQVp8unIDTk6gEFXfhwPsYHOokJe1mfkmrgPDV6xAkYgtL3KCDQ==
dependencies:
"@sentry/hub" "5.22.2"
"@sentry/minimal" "5.22.2"
"@sentry/types" "5.22.2"
"@sentry/utils" "5.22.2"
"@sentry/hub" "5.22.3"
"@sentry/minimal" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/types@5.22.2":
version "5.22.2"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.22.2.tgz#32d7f82537afe7712971fd6222c7744f0d8a27fe"
integrity sha512-Ko9pri0D0TNaSHocLVLQQZlnTtMXrBhP5AZYjB193aYqc1x52dFchQlhiKLEgyFCKjTEIlD/J9ZD7QkQoeYT+A==
"@sentry/types@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.22.3.tgz#d1d547b30ee8bd7771fa893af74c4f3d71f0fd18"
integrity sha512-cv+VWK0YFgCVDvD1/HrrBWOWYG3MLuCUJRBTkV/Opdy7nkdNjhCAJQrEyMM9zX0sac8FKWKOHT0sykNh8KgmYw==
"@sentry/utils@5.22.2":
version "5.22.2"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.22.2.tgz#7296cd9036e8d34638743a27fe8c6fe0e70db902"
integrity sha512-HGpPohNgwRhR+7bf2OlziX84JVdwQAauesqcL4Y78e+U09+E06cQAEQkRIQJfn2Ai2NvVmMVhdj51v+AqmaAWQ==
"@sentry/utils@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.22.3.tgz#e3bda3e789239eb16d436f768daa12829f33d18f"
integrity sha512-AHNryXMBvIkIE+GQxTlmhBXD0Ksh+5w1SwM5qi6AttH+1qjWLvV6WB4+4pvVvEoS8t5F+WaVUZPQLmCCWp6zKw==
dependencies:
"@sentry/types" "5.22.2"
"@sentry/types" "5.22.3"
tslib "^1.9.3"
"@sentry/webpack-plugin@^1.12.1":

View File

@ -1,6 +1,7 @@
package com.appsmith.server.migrations;
import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AppsmithRole;
import com.appsmith.server.constants.FieldName;
@ -11,6 +12,7 @@ import com.appsmith.server.domains.Config;
import com.appsmith.server.domains.Datasource;
import com.appsmith.server.domains.Group;
import com.appsmith.server.domains.InviteUser;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.OrganizationPlugin;
import com.appsmith.server.domains.Page;
@ -24,6 +26,7 @@ import com.appsmith.server.domains.Role;
import com.appsmith.server.domains.Sequence;
import com.appsmith.server.domains.Setting;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.DslActionDTO;
import com.appsmith.server.dtos.OrganizationPluginStatus;
import com.appsmith.server.services.EncryptionService;
import com.appsmith.server.services.OrganizationService;
@ -42,6 +45,7 @@ import org.springframework.data.mongodb.core.MongoTemplate;
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.data.mongodb.core.query.Criteria;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StreamUtils;
@ -827,4 +831,89 @@ public class DatabaseChangelog {
);
}
@ChangeSet(order = "024", id = "update-erroneous-action-ids", author = "")
public void updateErroneousActionIdsInPage(MongoTemplate mongoTemplate) {
final org.springframework.data.mongodb.core.query.Query configQuery = query(where("name").is("template-organization"));
final Config config = mongoTemplate.findOne(
configQuery,
Config.class
);
if (config == null) {
// No template organization configured. Nothing to migrate.
return;
}
final String organizationId = config.getConfig().getAsString("organizationId");
final org.springframework.data.mongodb.core.query.Query query = query(where(FieldName.ORGANIZATION_ID).is(organizationId));
query.fields().include("_id");
// Get IDs of applications in the template org.
final List<String> applicationIds = mongoTemplate
.find(query, Application.class)
.stream()
.map(BaseDomain::getId)
.collect(Collectors.toList());
// Get IDs of actions in the template org.
final List<String> actionIds = mongoTemplate
.find(query, Action.class)
.stream()
.map(BaseDomain::getId)
.collect(Collectors.toList());
// Get pages that are not in applications in the template org, and have template org's action IDs in their
// layoutOnload lists.
final Criteria incorrectActionIdCriteria = new Criteria().orOperator(
where("layouts.layoutOnLoadActions").elemMatch(new Criteria().elemMatch(where("_id").in(actionIds))),
where("layouts.publishedLayoutOnLoadActions").elemMatch(new Criteria().elemMatch(where("_id").in(actionIds)))
);
final List<Page> pagesToFix = mongoTemplate.find(
query(where(FieldName.APPLICATION_ID).not().in(applicationIds))
.addCriteria(incorrectActionIdCriteria),
Page.class
);
for (Page page : pagesToFix) {
for (Layout layout : page.getLayouts()) {
final ArrayList<HashSet<DslActionDTO>> layoutOnLoadActions = new ArrayList<>();
if (layout.getLayoutOnLoadActions() != null) {
layoutOnLoadActions.addAll(layout.getLayoutOnLoadActions());
}
if (layout.getPublishedLayoutOnLoadActions() != null) {
layoutOnLoadActions.addAll(layout.getPublishedLayoutOnLoadActions());
}
for (HashSet<DslActionDTO> actionSet : layoutOnLoadActions) {
for (DslActionDTO actionDTO : actionSet) {
final String actionName = actionDTO.getName();
final Action action = mongoTemplate.findOne(
query(where(FieldName.PAGE_ID).is(page.getId()))
.addCriteria(where(FieldName.NAME).is(actionName)),
Action.class
);
if (action != null) {
// Update the erroneous action id (template action id) to the cloned action id
actionDTO.setId(action.getId());
}
}
}
}
mongoTemplate.save(page);
}
final long unfixablePagesCount = mongoTemplate.count(
query(where(FieldName.APPLICATION_ID).not().in(applicationIds))
.addCriteria(where("layouts.layoutOnLoadActions").elemMatch(new Criteria().elemMatch(where("_id").in(actionIds)))),
Page.class
);
if (unfixablePagesCount > 0) {
log.info("Not all pages' onLoad actions could be fixed. Some old applications might not auto-run actions.");
}
}
}

View File

@ -303,6 +303,18 @@ echo_contact_support() {
}
bye() { # Prints a friendly good bye message and exits the script.
echo "Please share your email to receive support with the installation"
read -rp 'Email: ' email
curl -s -O --location --request POST 'https://api.segment.io/v1/track' \
--header 'Authorization: Basic QjJaM3hXRThXdDRwYnZOWDRORnJPNWZ3VXdnYWtFbk06' \
--header 'Content-Type: application/json' \
--data-raw '{
"userId": "'"$email"'",
"event": "Installation Support",
"properties": {
"osEnum": '$desired_os'
}
}'
echo -e "\nExiting for now. Bye! \U1F44B\n"
exit 1
}
@ -544,7 +556,7 @@ if [[ $status_code -ne 401 ]]; then
--header 'Content-Type: application/json' \
--data-raw '{
"userId": "'"$email"'",
"event": "Installation Failed",
"event": "Installation Support",
"properties": {
"osEnum": '$desired_os'
}