[Feature] Grid Widget (#2389)

* Updated test

* updated assertions

* Resizing image to take full width of table cell

* updated assertion

* Stop updating dynamicBindingPathList directly from widget

* Fix selectedRow and selectedRows computations

* Fix primaryColumns computations

* Updated test for derived column

* Added tests for computed value

* Added check clear data

* Reordering of test

* updated common method

* Made image size as 100% of table cell size

* add templating logic

* Updated flow and dsl

* Clear old primary columns

* Updated testname

* updated assertion

* use evaluated values for children

* Fix primary columns update on component mount and component update

* add isArray check

* remove property pane enhancement reducer

* add property pane enhancement reducer

* disable items other than template + fix running property enchancment on drop of list widget

* disbled drag, resize, settingsControl, drag for items other than template

* add grid options

* uncomment the widget operation for add child for grid children

* handle delete scenario for child widget in list widget

* WIP: Use the new delete and update property features

* add listdsl.json for testcases

* add test cases for correct no. of items being rendered

* add test cases currentItem binding in list widget

* change dragEnabled to dragDisabled

* change resizeEnabled to resizeDisabled

* change settingsControlEnabled to settingsControlDisabled

* change dropEnabled to dropDisabled

* update settingsControlDisabled default value

* Use deleteProperties in propertyControls

* Fix unsetting of array indices when deleting widget properties

* remove old TableWidget.tsx file

* Fix derived column property update on primary column property update

* Handle undefined primary columns

* Fix filepicker immutable prop issue

* Fix object.freeze issue when adding ids to the property pane configuration

* fix widget issue in grid

* Fix column actions dynamicBindingPathList inclusion issue

* remove consoles + fix typo around batch update

* Remove redundant tests

* js binding test for date picker

* hydate enhancement map on copy list widget

* check for dynamicleaf

* fixes

* improve check

* fix getNextWidgetName

* update template in list widget when copying

* updating template copy logic when copying widget

* update dynamicBindingPathList in copied widget

* Add path parameter to hidden functions in property pane configs

* fix copy bug when copying list widget

* add computed list property control

* Remove time column type

Fix editor prompt for currentRow

Fix undefined derivedColumns scenario

Remove validations for primaryColums and derivedColumns

Fix section toggle for video, image and button column types

* Fix table widget actions and custom column migrations

* Add logs for cyclical dependency map ♻️

* Process array differences

* add property control for list widget

* Fix onClick migrations

* Property pane config parity

* binding and trigger paths from the property pane config (#2920)

* try react virtualized library

* Fix unit test

* Fix unit test 

* Fix minor issues in table widget

* Add default meta props to binding paths to ensure eval and validation

* Dummy commit 🎉

* Remove unnecessary datepicker test

Fix chart data as string issue

* Achieve table column sorting and resizing parity with release

* handle scenario where last column isn't available to access

* Fix for panel config path not existing in the widget

* Fix bindings in currentRow (default)

Add dummy property pane config for canvas widget

* Update canvas widgets with dynamicPathLists on delete of property paths

* Add all diffs to change paths and trim later

* Add back default properties 🚶🏻‍♂️

* Use object based paths instead of arrays for primaryColumns and derivedColumns

* Fix issue in reordered columns

* Fix inccorect update order

* add virtualized list

* Fix failing property pane tests

* minor change

* minor list widget change

* Remove .vscode from git

* Rename ads to alloy

Fix isVisible in list widget

* move grid component to widget folder

* fix import in widget registry

* add sticky row in virtualized list

* add sticky container

* Fix Height of grid widget items container

* fix dragging of items in children other than template children

* update list widget

* update list widget

* Fix padding in list widget

* hide scrollbar in list widget list

* fix copy bug in list widget

* regenrate enhancement map on undo delete widget

* Use enhancementmap for autocomplete in list widget

Basic styles for list widget scrollbar

* add custom control in widget config

* minor commit

* update scrollbar styles

* remove unused variable

* fix typo in custom control

* comment out test cases

* remove unused imports

* remove unused imports

* add JSON stringify in interweave

* add noPad styling in dragLayer for noPad prop

* implement grid gap

* add list item background color prop

* add white color in color picker control

* fix gap in last list item

* remove onBeforeParse in textcomponent

* remove virtualization in grid widget

* allow overflow-y

* add onListItemClick action

* add beta label

* add pagination

* fix actions in pagination in list widget

* add list widget icon

* add list background color default value

* remove extra div

* fix pagination issue

* fix list widget crashing on perpage change

* extract child operation function to widgetblueprint saga

* refactor enhancements

* add enhancement hook

* refactor propertyUpdate hook enhancment

* remove enhacement map

* revert renaming ads to alloy

* add autopagination

* Cleanup unused vars

Re-write loop using map

Fix binding with external input widget

* update default background color

* remove unnessary scrol + fix pagination per page

* remove console.log

* use grid gap in pixel instead of snap

* fix list widget tests for binding

* add tests for on click action and pagination

* remove unnecessary imports

* remove overflow hidden in list component

* Add feature to enable template actions

* update property pane help text for list widget

* disable pagination in editor view

* update property pane options

* add test case for action

* uncomment tests

* fix grid gap validation

* update test cases

* fix property pane opening issue for list tempalte

* Disable form widgets in list widget

* fix template issue for actions

* add validation tests for list data

* update starting template

* add selectedRow + enable pagination in edit mode

* remove extra padding in list widget + popper fix on settingDisabled

* add stop propagation for button click

* fix click event in edit mode

* disallow filepicker widget for list widget

* add test for list widget entity definition for selectItem

* remove unused imports

* fix test

* remove evaluated value for list child widgets

* add comment

* remove log

* fix copying bug in list widget

* add check for not allowing template to copy

* fix test

* add test for property pane actions

* remove unused import

* add draglayercomponent test

* add test for draggable component

* add test for evaluatedvalue popup

* add test for messages.ts

* add test for widgeticons

* add test for property pane selector

* add test for widget config response

* start testing widget configresponse

* add test for enhancements in widget config

* add test for codeeditor

* add test for base widget + list widget

* add test for executeWidgetBlueprintChildOperations

* remove unused import

* add test for widget operation utils

* remove unused import

* add test for handleSpecificCasesWhilePasting

* remove unused function

* remove unused import

* add empty list styling

* resolve all review comments

* fix message test

* add test for widget operation utils

* fix merge conflicts

* move validations in property config

Co-authored-by: Abhinav Jha <abhinav@appsmith.com>
Co-authored-by: nandan.anantharamu <nandan.anantharamu@thoughtspot.com>
Co-authored-by: vicky-primathon.in <vicky.bansal@primathon.in>
Co-authored-by: Pawan Kumar <pawankumar@Pawans-MacBook-Pro.local>
Co-authored-by: Piyush <piyush@codeitout.com>
Co-authored-by: hetunandu <hetu@appsmith.com>
Co-authored-by: Hetu Nandu <hetunandu@gmail.com>
Co-authored-by: root <root@DESKTOP-9GENCK0.localdomain>
This commit is contained in:
Pawan Kumar 2021-04-23 11:13:13 +05:30 committed by GitHub
parent 2eec2fba28
commit 1717b0e392
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 3677 additions and 149 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
.idea
*.iml
.env
.vscode/*
# test coverage
coverage-summary.json

View File

@ -43,3 +43,5 @@ storybook-static/*
build-storybook.log
.eslintcache
.vscode
TODO

View File

@ -2,6 +2,9 @@
"env": {
"cypress/globals": true
},
"rules": {
"cypress/no-unnecessary-waiting": 0
},
"extends": [
"plugin:cypress/recommended"
]

View File

@ -0,0 +1,161 @@
{
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 1224,
"snapColumns": 16,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
"bottomRow": 1280,
"containerStyle": "none",
"snapRows": 33,
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 9,
"minHeight": 1292,
"parentColumnSpace": 1,
"dynamicBindingPathList": [],
"leftColumn": 0,
"children": [
{
"isVisible": true,
"enhancements": true,
"backgroundColor": "",
"gridType": "vertical",
"gridGap": 0,
"items": "[\n {\n \"id\": 1,\n \"email\": \"michael.lawson@reqres.in\",\n \"first_name\": \"Michael\",\n \"last_name\": \"Lawson\",\n \"avatar\": \"https://reqres.in/img/faces/7-image.jpg\"\n },\n {\n \"id\": 2,\n \"email\": \"lindsay.ferguson@reqres.in\",\n \"first_name\": \"Lindsay\",\n \"last_name\": \"Ferguson\",\n \"avatar\": \"https://reqres.in/img/faces/8-image.jpg\"\n },\n {\n \"id\": 3,\n \"email\": \"brock.lesnar@reqres.in\",\n \"first_name\": \"Brock\",\n \"last_name\": \"Lesnar\",\n \"avatar\": \"https://reqres.in/img/faces/8-image.jpg\"\n }\n]",
"widgetName": "List1",
"children": [
{
"isVisible": true,
"widgetName": "Canvas1",
"containerStyle": "none",
"canExtend": false,
"detachFromLayout": true,
"dropDisabled": true,
"children": [
{
"isVisible": true,
"backgroundColor": "white",
"widgetName": "Container1",
"containerStyle": "card",
"children": [
{
"isVisible": true,
"widgetName": "Canvas2",
"containerStyle": "none",
"canExtend": false,
"detachFromLayout": true,
"children": [
{
"isVisible": true,
"text": "Label",
"textStyle": "LABEL",
"textAlign": "LEFT",
"widgetName": "Text1",
"type": "TEXT_WIDGET",
"isLoading": false,
"parentColumnSpace": 32,
"parentRowSpace": 40,
"leftColumn": 2,
"rightColumn": 6,
"topRow": 0,
"bottomRow": 1,
"parentId": "dinv2tsatk",
"widgetId": "k6ct7dxg4w"
},
{
"isVisible":true,
"text":"Submit",
"buttonStyle":"PRIMARY_BUTTON",
"widgetName":"Button1",
"isDisabled":false,
"isDefaultClickDisabled":true,
"version":1,
"type":"BUTTON_WIDGET",
"isLoading":false,
"parentColumnSpace":29.25,
"parentRowSpace":40,
"leftColumn":6,
"rightColumn":8,
"topRow":1,
"bottomRow":2,
"parentId":"dinv2tsatk",
"widgetId":"fuw9p7cuek"
}
],
"minHeight": null,
"type": "CANVAS_WIDGET",
"isLoading": false,
"parentColumnSpace": 1,
"parentRowSpace": 1,
"leftColumn": 0,
"rightColumn": null,
"topRow": 0,
"bottomRow": null,
"parentId": "4ruj7xl5ri",
"widgetId": "dinv2tsatk"
}
],
"dragDisabled": true,
"isDeletable": false,
"disablePropertyPane": true,
"type": "CONTAINER_WIDGET",
"isLoading": false,
"leftColumn": 0,
"rightColumn": 16,
"topRow": 0,
"bottomRow": 4,
"parentId": "0pvmmqr77m",
"widgetId": "4ruj7xl5ri"
}
],
"minHeight": 400,
"type": "CANVAS_WIDGET",
"isLoading": false,
"parentColumnSpace": 1,
"parentRowSpace": 1,
"leftColumn": 0,
"rightColumn": 592,
"topRow": 0,
"bottomRow": 400,
"parentId": "5bwz8xcvhj",
"widgetId": "0pvmmqr77m"
}
],
"type": "LIST_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 0,
"rightColumn": 8,
"topRow": 0,
"bottomRow": 10,
"parentId": "0",
"widgetId": "5bwz8xcvhj",
"dynamicBindingPathList": [],
"template": {
"Text1": {
"isVisible": true,
"text": "Label",
"textStyle": "LABEL",
"textAlign": "LEFT",
"widgetName": "Text1",
"type": "TEXT_WIDGET",
"isLoading": false,
"parentColumnSpace": 32,
"parentRowSpace": 40,
"leftColumn": 0,
"rightColumn": 4,
"topRow": 0,
"bottomRow": 1,
"parentId": "dinv2tsatk",
"widgetId": "k6ct7dxg4w"
}
}
}
]
}
}

View File

@ -0,0 +1,89 @@
const commonlocators = require("../../../locators/commonlocators.json");
const widgetsPage = require("../../../locators/Widgets.json");
const dsl = require("../../../fixtures/listdsl.json");
const publishPage = require("../../../locators/publishWidgetspage.json");
describe("Container Widget Functionality", function() {
const items = JSON.parse(dsl.dsl.children[0].items);
before(() => {
cy.addDsl(dsl);
});
it("checks if list shows correct no. of items", function() {
cy.get(commonlocators.containerWidget).then(function($lis) {
expect($lis).to.have.length(2);
});
});
it("checks currentItem binding", function() {
cy.SearchEntityandOpen("Text1");
cy.getCodeMirror().then(($cm) => {
cy.get(".CodeMirror textarea")
.first()
.type(`{{currentItem.first_name}}`, {
force: true,
parseSpecialCharSequences: false,
});
});
cy.wait(1000);
cy.closePropertyPane();
cy.get(commonlocators.TextInside).then(function($lis) {
expect($lis.eq(0)).to.contain(items[0].first_name);
expect($lis.eq(1)).to.contain(items[1].first_name);
});
});
it("checks button action", function() {
cy.SearchEntityandOpen("Button1");
cy.getCodeMirror().then(($cm) => {
cy.get(".CodeMirror textarea")
.first()
.type(`{{currentItem.first_name}}`, {
force: true,
parseSpecialCharSequences: false,
});
});
cy.addAction("{{currentItem.first_name}}");
cy.PublishtheApp();
cy.get(`${widgetsPage.widgetBtn}`)
.first()
.click();
cy.get(commonlocators.toastmsg).contains(items[0].first_name);
});
it("it checks onListItem click action", function() {
cy.get(publishPage.backToEditor).click({ force: true });
cy.SearchEntityandOpen("List1");
cy.addAction("{{currentItem.first_name}}");
cy.PublishtheApp();
cy.get(
"div[type='LIST_WIDGET'] .t--widget-containerwidget:first-child",
).click();
cy.get(commonlocators.toastmsg).contains(items[0].first_name);
});
it("it checks pagination", function() {
// clicking on second pagination button
cy.get(`${commonlocators.paginationButton}-2`).click();
// now we are on the second page which shows first the 3rd item in the list
cy.get(commonlocators.TextInside).then(function($lis) {
expect($lis.eq(0)).to.contain(items[2].first_name);
});
});
afterEach(() => {
// put your clean up code if any
});
});

View File

@ -105,6 +105,8 @@
"globalSearchInput": ".t--global-search-input",
"globalSearchTrigger": ".t--global-search-modal-trigger",
"globalSearchClearInput": ".t--global-clear-input",
"containerWidget": ".t--widget-containerwidget",
"paginationButton": ".rc-pagination-item",
"switchWidgetActive": ".t--switch-widget-active",
"switchWidgetInActive": ".t--switch-widget-inactive",
"switchWidgetLoading": ".t--switch-widget-loading"

View File

@ -12,13 +12,11 @@ describe("Table functionality ", function() {
// Navigate to add background colour and Text colour
// Ensure the row colour gets overlapped on table colour
});
it("Collapse the tabs of Property pane", function() {
// Add a table
// Click on the property pane
// Collapse the General ,Action and Tab option
});
it("Bind the column with same name", function() {
// Add a table
// Click on the property pane

View File

@ -87,6 +87,7 @@
"popper.js": "^1.15.0",
"prettier": "^1.18.2",
"prismjs": "^1.23.0",
"rc-pagination": "^3.1.3",
"re-reselect": "^3.4.0",
"react": "^16.12.0",
"react-base-table": "^1.9.1",
@ -129,7 +130,8 @@
"tinycolor2": "^1.4.1",
"toposort": "^2.0.2",
"ts-loader": "^6.0.4",
"typescript": "^3.9.2",
"tslib": "^2.1.0",
"typescript": "^4.1.3",
"unescape-js": "^1.1.4",
"url-search-params-polyfill": "^8.0.0",
"worker-loader": "^3.0.2"
@ -176,7 +178,7 @@
"@storybook/preset-create-react-app": "^3.1.4",
"@storybook/react": "^5.3.19",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.2.5",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.1",
"@types/codemirror": "^0.0.96",
"@types/deep-diff": "^1.0.0",
@ -193,8 +195,8 @@
"@types/styled-system": "^5.1.9",
"@types/tern": "0.22.0",
"@types/toposort": "^2.0.3",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"@typescript-eslint/eslint-plugin": "^4.15.0",
"@typescript-eslint/parser": "^4.15.0",
"babel-loader": "^8.1.0",
"babel-plugin-styled-components": "^1.10.7",
"craco-babel-loader": "^0.1.4",

View File

@ -22,6 +22,7 @@ export const updateWidgetPropertyRequest = (
export interface BatchPropertyUpdatePayload {
modify?: Record<string, unknown>; //Key value pairs of paths and values to update
remove?: string[]; //Array of paths to delete
triggerPaths?: string[]; // Array of paths in the modify and remove list which are trigger paths
}
export const batchUpdateWidgetProperty = (

View File

@ -1,5 +1,3 @@
import { FetchPageRequest, PageLayout, SavePageResponse } from "api/PageApi";
import { WidgetOperation } from "widgets/BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import {
EvaluationReduxAction,
@ -7,9 +5,11 @@ import {
ReduxActionTypes,
UpdateCanvasPayload,
} from "constants/ReduxActionConstants";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { WidgetOperation } from "widgets/BaseWidget";
import { FetchPageRequest, PageLayout, SavePageResponse } from "api/PageApi";
import { APP_MODE, UrlDataState } from "reducers/entityReducers/appReducer";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
export interface FetchPageListPayload {
applicationId: string;

View File

@ -0,0 +1,11 @@
import * as actions from "./propertyPaneActions";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
describe("property pane action actions", () => {
it("should create an action hide Property Pane", () => {
const expectedAction = {
type: ReduxActionTypes.HIDE_PROPERTY_PANE,
};
expect(actions.hidePropertyPane()).toEqual(expectedAction);
});
});

View File

@ -8,3 +8,9 @@ export const updateWidgetName = (widgetId: string, newName: string) => {
},
};
};
export const hidePropertyPane = () => {
return {
type: ReduxActionTypes.HIDE_PROPERTY_PANE,
};
};

View File

@ -0,0 +1,3 @@
<svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.79999 11C9.46273 11 10 11.5372 10 12.2V18.8C10 19.4628 9.46273 20 8.79999 20H1.20001C0.53727 20 0 19.4628 0 18.8V12.2C0 11.5372 0.53727 11 1.20001 11H8.79999ZM20 16V19H12V16H20ZM6.24258 15.5072C6.22756 15.5186 6.21421 15.5321 6.20297 15.5472L4.85718 17.3627C4.7914 17.4514 4.66612 17.47 4.57739 17.4042C4.5678 17.3971 4.55889 17.3891 4.55072 17.3804L3.52838 16.2915C3.45278 16.211 3.32619 16.207 3.24567 16.2826C3.23763 16.2901 3.23023 16.2984 3.22354 16.3071L1.74039 18.2513C1.67339 18.3392 1.69026 18.4646 1.77808 18.5316C1.81293 18.5582 1.85555 18.5726 1.89938 18.5726H8.42206C8.53252 18.5726 8.62204 18.4831 8.62204 18.3727C8.62204 18.3289 8.60772 18.2864 8.58124 18.2516L6.52283 15.5453C6.45596 15.4574 6.3305 15.4403 6.24258 15.5072ZM24 11V14H12V11H24ZM8.79999 0C9.46273 0 10 0.537211 10 1.19995V7.80005C10 8.46279 9.46273 9 8.79999 9H1.20001C0.53727 9 0 8.46279 0 7.80005V1.19995C0 0.537211 0.53727 0 1.20001 0H8.79999ZM20 5V8H12V5H20ZM6.24258 4.5072C6.22756 4.51862 6.21421 4.53209 6.20297 4.54724L4.85718 6.36267C4.7914 6.4514 4.66612 6.46995 4.57739 6.40417C4.5678 6.39706 4.55889 6.38907 4.55072 6.38037L3.52838 5.2915C3.45278 5.21098 3.32619 5.20699 3.24567 5.28259C3.23763 5.29014 3.23023 5.29837 3.22354 5.30713L1.74039 7.25134C1.67339 7.33917 1.69026 7.46463 1.77808 7.53162C1.81293 7.55821 1.85555 7.57263 1.89938 7.57263H8.42206C8.53252 7.57263 8.62204 7.48314 8.62204 7.37268C8.62204 7.32894 8.60772 7.2864 8.58124 7.25159L6.52283 4.54529C6.45596 4.45737 6.3305 4.44033 6.24258 4.5072ZM24 0V3H12V0H24Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -25,6 +25,8 @@ const StyledContainerComponent = styled.div<
background: ${(props) => props.backgroundColor};
${(props) => (!props.isVisible ? invisible : "")};
opacity: ${(props) => (props.resizeDisabled ? "0.5" : "1")};
pointer-events: ${(props) => (props.resizeDisabled ? "none" : "inherit")};
overflow: hidden;
${(props) => (props.shouldScrollContents ? scrollContents : "")}
}`;
@ -32,6 +34,7 @@ const StyledContainerComponent = styled.div<
const ContainerComponent = (props: ContainerComponentProps) => {
const containerStyle = props.containerStyle || "card";
const containerRef: RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!props.shouldScrollContents) {
const supportsNativeSmoothScroll =
@ -69,6 +72,7 @@ export interface ContainerComponentProps extends ComponentProps {
className?: string;
backgroundColor?: Color;
shouldScrollContents?: boolean;
resizeDisabled?: boolean;
}
export default ContainerComponent;

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from "react";
import React, { CSSProperties, ReactNode, useMemo } from "react";
import { BaseStyle } from "widgets/BaseWidget";
import { WIDGET_PADDING } from "constants/WidgetConstants";
import { generateClassName } from "utils/generators";
@ -23,27 +23,35 @@ export const PositionedContainer = (props: PositionedContainerProps) => {
const padding = WIDGET_PADDING;
const openPropertyPane = useClickOpenPropPane();
// memoized classname
const containerClassName = useMemo(() => {
return (
generateClassName(props.widgetId) +
" positioned-widget " +
`t--widget-${props.widgetType
.split("_")
.join("")
.toLowerCase()}`
);
}, [props.widgetType, props.widgetId]);
const containerStyle: CSSProperties = useMemo(() => {
return {
position: "absolute",
left: x,
top: y,
height: props.style.componentHeight + (props.style.heightUnit || "px"),
width: props.style.componentWidth + (props.style.widthUnit || "px"),
padding: padding + "px",
};
}, [props.style]);
return (
<PositionedWidget
onClickCapture={openPropertyPane}
style={{
position: "absolute",
left: x,
top: y,
height: props.style.componentHeight + (props.style.heightUnit || "px"),
width: props.style.componentWidth + (props.style.widthUnit || "px"),
padding: padding + "px",
}}
style={containerStyle}
id={props.widgetId}
//Before you remove: This is used by property pane to reference the element
className={
generateClassName(props.widgetId) +
" " +
`t--widget-${props.widgetType
.split("_")
.join("")
.toLowerCase()}`
}
className={containerClassName}
>
{props.children}
</PositionedWidget>

View File

@ -0,0 +1,77 @@
import CodeEditor from "./index";
import store from "store";
import TestRenderer from "react-test-renderer";
import React from "react";
import { Provider } from "react-redux";
import EvaluatedValuePopup from "./EvaluatedValuePopup";
import { ThemeProvider } from "styled-components";
import { theme, light } from "constants/DefaultTheme";
import {
EditorSize,
EditorTheme,
TabBehaviour,
EditorModes,
} from "./EditorConfig";
describe("CodeEditor", () => {
it("should check EvaluatedValuePopup's hideEvaluatedValue is false when hideEvaluatedValue is passed as false to codeditor", () => {
const finalTheme = { ...theme, colors: { ...theme.colors, ...light } };
const testRenderer = TestRenderer.create(
<Provider store={store}>
<ThemeProvider theme={finalTheme}>
<CodeEditor
input={{
value: "",
onChange: () => {
//
},
}}
hideEvaluatedValue={false}
additionalDynamicData={{}}
mode={EditorModes.TEXT}
theme={EditorTheme.LIGHT}
size={EditorSize.COMPACT}
tabBehaviour={TabBehaviour.INDENT}
/>
</ThemeProvider>
</Provider>,
);
const testInstance = testRenderer.root;
expect(
testInstance.findByType(EvaluatedValuePopup).props.hideEvaluatedValue,
).toBe(false);
});
it("should check EvaluatedValuePopup's hideEvaluatedValue is true when hideEvaluatedValue is passed as true to codeditor", () => {
const finalTheme = { ...theme, colors: { ...theme.colors, ...light } };
const testRenderer = TestRenderer.create(
<Provider store={store}>
<ThemeProvider theme={finalTheme}>
<CodeEditor
input={{
value: "",
onChange: () => {
//
},
}}
hideEvaluatedValue={true}
additionalDynamicData={{}}
mode={EditorModes.TEXT}
theme={EditorTheme.LIGHT}
size={EditorSize.COMPACT}
tabBehaviour={TabBehaviour.INDENT}
/>
</ThemeProvider>
</Provider>,
);
const testInstance = testRenderer.root;
expect(
testInstance.findByType(EvaluatedValuePopup).props.hideEvaluatedValue,
).toBe(true);
});
});

View File

@ -0,0 +1,50 @@
import store from "store";
import React from "react";
import { Provider } from "react-redux";
import { render, screen } from "@testing-library/react";
import EvaluatedValuePopup from "./EvaluatedValuePopup";
import { ThemeProvider, theme } from "constants/DefaultTheme";
import { EditorTheme } from "./EditorConfig";
describe("EvaluatedValuePopup", () => {
it("should render evaluated popup when hideEvaluatedValue is false", () => {
render(
<Provider store={store}>
<ThemeProvider theme={theme}>
<EvaluatedValuePopup
theme={EditorTheme.LIGHT}
isOpen={true}
hasError={false}
hideEvaluatedValue={false}
>
<div>children</div>
</EvaluatedValuePopup>
</ThemeProvider>
</Provider>,
);
const input = screen.queryByTestId("evaluated-value-popup-title");
expect(input).toBeTruthy();
});
it("should not render evaluated popup when hideEvaluatedValue is true", () => {
render(
<Provider store={store}>
<ThemeProvider theme={theme}>
<EvaluatedValuePopup
theme={EditorTheme.LIGHT}
isOpen={true}
hasError={false}
hideEvaluatedValue={true}
>
<div>children</div>
</EvaluatedValuePopup>
</ThemeProvider>
</Provider>,
);
const input = screen.queryByTestId("evaluated-value-popup-title");
expect(input).toBeNull();
});
});

View File

@ -106,6 +106,7 @@ interface Props {
children: JSX.Element;
error?: string;
useValidationMessage?: boolean;
hideEvaluatedValue?: boolean;
}
interface PopoverContentProps {
@ -117,6 +118,7 @@ interface PopoverContentProps {
theme: EditorTheme;
onMouseEnter: () => void;
onMouseLeave: () => void;
hideEvaluatedValue?: boolean;
}
export const CurrentValueViewer = (props: {
@ -164,7 +166,11 @@ export const CurrentValueViewer = (props: {
}
return (
<React.Fragment>
{!props.hideLabel && <StyledTitle>Evaluated Value</StyledTitle>}
{!props.hideLabel && (
<StyledTitle data-testid="evaluated-value-popup-title">
Evaluated Value
</StyledTitle>
)}
<CurrentValueWrapper colorTheme={props.theme}>
<>
{content}
@ -200,10 +206,12 @@ const PopoverContent = (props: PopoverContentProps) => {
</TypeText>
</React.Fragment>
)}
<CurrentValueViewer
theme={props.theme}
evaluatedValue={props.evaluatedValue}
/>
{!props.hideEvaluatedValue && (
<CurrentValueViewer
theme={props.theme}
evaluatedValue={props.evaluatedValue}
/>
)}
</ContentWrapper>
);
};
@ -242,6 +250,7 @@ const EvaluatedValuePopup = (props: Props) => {
useValidationMessage={props.useValidationMessage}
hasError={props.hasError}
theme={props.theme}
hideEvaluatedValue={props.hideEvaluatedValue}
onMouseLeave={() => {
setContentHovered(false);
}}

View File

@ -92,6 +92,7 @@ export type EditorProps = EditorStyleProps &
} & {
additionalDynamicData?: Record<string, Record<string, unknown>>;
promptMessage?: React.ReactNode | string;
hideEvaluatedValue?: boolean;
};
type Props = ReduxStateProps & EditorProps;
@ -350,6 +351,7 @@ class CodeEditor extends Component<Props, State> {
hoverInteraction,
fill,
useValidationMessage,
hideEvaluatedValue,
} = this.props;
const hasError = !!(meta && meta.error);
let evaluated = evaluatedValue;
@ -395,6 +397,7 @@ class CodeEditor extends Component<Props, State> {
hasError={hasError}
error={meta?.error}
useValidationMessage={useValidationMessage}
hideEvaluatedValue={hideEvaluatedValue}
>
<EditorWrapper
editorTheme={this.props.theme}

View File

@ -0,0 +1,54 @@
import React from "react";
import { DndProvider } from "react-dnd";
import TestRenderer from "react-test-renderer";
import TouchBackend from "react-dnd-touch-backend";
import DragLayerComponent from "./DragLayerComponent";
import { RenderModes, WidgetTypes } from "constants/WidgetConstants";
import { ThemeProvider, theme } from "constants/DefaultTheme";
describe("DragLayerComponent", () => {
it("it checks noPad prop", () => {
const dummyWidget = {
type: WidgetTypes.CANVAS_WIDGET,
widgetId: "0",
widgetName: "canvas",
parentColumnSpace: 1,
parentRowSpace: 1,
parentRowHeight: 0,
canDropTargetExtend: false,
parentColumnWidth: 0,
leftColumn: 0,
visible: true,
rightColumn: 0,
topRow: 0,
bottomRow: 0,
version: 17,
isLoading: false,
renderMode: RenderModes.CANVAS,
children: [],
noPad: true,
onBoundsUpdate: () => {
//
},
isOver: true,
parentWidgetId: "parent",
force: true,
};
const testRenderer = TestRenderer.create(
<ThemeProvider theme={theme}>
<DndProvider
backend={TouchBackend}
options={{
enableMouseEvents: true,
}}
>
<DragLayerComponent {...dummyWidget} />
</DndProvider>
</ThemeProvider>,
);
const testInstance = testRenderer.root;
expect(testInstance.findByType(DragLayerComponent).props.noPad).toBe(true);
});
});

View File

@ -12,16 +12,17 @@ import { getNearestParentCanvas } from "utils/generators";
const WrappedDragLayer = styled.div<{
columnWidth: number;
rowHeight: number;
noPad: boolean;
ref: RefObject<HTMLDivElement>;
}>`
position: absolute;
pointer-events: none;
left: 0;
top: 0;
left: ${CONTAINER_GRID_PADDING}px;
top: ${CONTAINER_GRID_PADDING}px;
height: calc(100% - ${CONTAINER_GRID_PADDING}px);
width: calc(100% - ${CONTAINER_GRID_PADDING}px);
left: ${(props) => (props.noPad ? "0" : `${CONTAINER_GRID_PADDING}px;`)};
top: ${(props) => (props.noPad ? "0" : `${CONTAINER_GRID_PADDING}px;`)};
height: ${(props) =>
props.noPad ? `100%` : `calc(100% - ${CONTAINER_GRID_PADDING}px)`};
width: ${(props) =>
props.noPad ? `100%` : `calc(100% - ${CONTAINER_GRID_PADDING}px)`};
background-image: radial-gradient(
circle,
@ -47,6 +48,7 @@ type DragLayerProps = {
isResizing?: boolean;
parentWidgetId: string;
force: boolean;
noPad: boolean;
};
const DragLayerComponent = (props: DragLayerProps) => {
@ -146,6 +148,7 @@ const DragLayerComponent = (props: DragLayerProps) => {
columnWidth={props.parentColumnWidth}
rowHeight={props.parentRowHeight}
ref={dropTargetMask}
noPad={props.noPad}
>
{props.visible &&
props.isOver &&

View File

@ -0,0 +1,10 @@
import { canDrag } from "./DraggableComponent";
describe("DraggableComponent", () => {
it("it tests draggable canDrag helper function", () => {
expect(canDrag(false, false, { dragDisabled: false })).toBe(true);
expect(canDrag(true, false, { dragDisabled: false })).toBe(false);
expect(canDrag(false, true, { dragDisabled: false })).toBe(false);
expect(canDrag(false, false, { dragDisabled: true })).toBe(false);
});
});

View File

@ -38,6 +38,22 @@ type DraggableComponentProps = WidgetProps;
/* eslint-disable react/display-name */
/**
* can drag helper function for react-dnd hook
*
* @param isResizing
* @param isDraggingDisabled
* @param props
* @returns
*/
export const canDrag = (
isResizing: boolean,
isDraggingDisabled: boolean,
props: any,
) => {
return !isResizing && !isDraggingDisabled && !props.dragDisabled;
};
const DraggableComponent = (props: DraggableComponentProps) => {
// Dispatch hook handy to toggle property pane
const showPropertyPane = useShowPropertyPane();
@ -119,7 +135,7 @@ const DraggableComponent = (props: DraggableComponentProps) => {
},
canDrag: () => {
// Dont' allow drag if we're resizing or the drag of `DraggableComponent` is disabled
return !isResizing && !isDraggingDisabled;
return canDrag(isResizing, isDraggingDisabled, props);
},
});

View File

@ -38,6 +38,7 @@ type DropTargetComponentProps = WidgetProps & {
snapColumnSpace: number;
snapRowSpace: number;
minHeight: number;
noPad?: boolean;
};
const StyledDropTarget = styled.div`
@ -65,7 +66,7 @@ export const DropTargetContext: Context<{
persistDropTargetRows?: (widgetId: string, row: number) => void;
}> = createContext({});
export const DropTargetComponent = memo((props: DropTargetComponentProps) => {
export const DropTargetComponent = (props: DropTargetComponentProps) => {
const canDropTargetExtend = props.canExtend;
const snapRows = getCanvasSnapRows(props.bottomRow, props.canExtend);
@ -244,7 +245,8 @@ export const DropTargetComponent = memo((props: DropTargetComponentProps) => {
focusWidget && focusWidget(props.parentId);
}
}
e.stopPropagation();
// commenting this out to allow propagation of click events
// e.stopPropagation();
e.preventDefault();
};
const height = canDropTargetExtend
@ -258,13 +260,15 @@ export const DropTargetComponent = memo((props: DropTargetComponentProps) => {
? "1px solid #DDDDDD"
: "1px solid transparent";
const dropRef = !props.dropDisabled ? drop : undefined;
return (
<DropTargetContext.Provider
value={{ updateDropTargetRows, persistDropTargetRows }}
>
<StyledDropTarget
onClick={handleFocus}
ref={drop}
ref={dropRef}
style={{
height,
border,
@ -287,11 +291,14 @@ export const DropTargetComponent = memo((props: DropTargetComponentProps) => {
parentRows={rows}
parentCols={props.snapColumns}
isResizing={isChildResizing}
noPad={props.noPad || false}
force={isDragging && !isOver && !props.parentId}
/>
</StyledDropTarget>
</DropTargetContext.Provider>
);
});
};
export default DropTargetComponent;
const MemoizedDropTargetComponent = memo(DropTargetComponent);
export default MemoizedDropTargetComponent;

View File

@ -265,7 +265,7 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
onStart={handleResizeStart}
onStop={updateSize}
snapGrid={{ x: props.parentColumnSpace, y: props.parentRowSpace }}
enable={!isDragging && isWidgetFocused}
enable={!isDragging && isWidgetFocused && !props.resizeDisabled}
isColliding={isColliding}
>
<VisibilityContainer

View File

@ -107,7 +107,7 @@ export const WidgetNameComponent = (props: WidgetNameComponentProps) => {
currentActivity = Activities.ACTIVE;
return showWidgetName ? (
<PositionStyle>
<PositionStyle data-testid="t--settings-controls-positioned-wrapper">
<ControlGroup>
<SettingsControl
toggleSettings={togglePropertyEditor}

View File

@ -47,6 +47,7 @@ export interface ControlFunctions {
openNextPanel: (props: any) => void;
deleteProperties: (propertyPaths: string[]) => void;
theme: EditorTheme;
hideEvaluatedValue?: boolean;
}
export default BaseControl;

View File

@ -24,7 +24,7 @@ class DropDownControl extends BaseControl<DropDownControlProps> {
options={this.props.options}
selected={defaultSelected}
onSelect={this.onItemSelect}
width="231px"
width="100%"
showLabelOnly={true}
optionWidth={
this.props.optionWidth ? this.props.optionWidth : "231px"

View File

@ -22,6 +22,7 @@ export function InputText(props: {
dataTreePath?: string;
additionalAutocomplete?: Record<string, Record<string, unknown>>;
theme?: EditorTheme;
hideEvaluatedValue?: boolean;
}) {
const {
errorMessage,
@ -32,7 +33,9 @@ export function InputText(props: {
placeholder,
dataTreePath,
evaluatedValue,
hideEvaluatedValue,
} = props;
return (
<StyledDynamicInput>
<CodeEditor
@ -53,6 +56,7 @@ export function InputText(props: {
size={EditorSize.EXTENDED}
placeholder={placeholder}
additionalDynamicData={props.additionalAutocomplete}
hideEvaluatedValue={hideEvaluatedValue}
/>
</StyledDynamicInput>
);
@ -69,7 +73,10 @@ class InputTextControl extends BaseControl<InputControlProps> {
dataTreePath,
validationMessage,
defaultValue,
additionalAutoComplete,
hideEvaluatedValue,
} = this.props;
return (
<InputText
label={label}
@ -81,6 +88,8 @@ class InputTextControl extends BaseControl<InputControlProps> {
dataTreePath={dataTreePath}
placeholder={placeholderText}
theme={this.props.theme}
additionalAutocomplete={additionalAutoComplete}
hideEvaluatedValue={hideEvaluatedValue}
/>
);
}

View File

@ -166,6 +166,11 @@ const FIELD_VALUES: Record<
shouldScroll: "boolean",
isVisible: "boolean",
},
LIST_WIDGET: {
items: "Array<Object>",
isVisible: "boolean",
gridGap: "number",
},
};
export default FIELD_VALUES;

View File

@ -103,6 +103,10 @@ export const HelpMap = {
path: "/core-concepts/connecting-to-data-sources/connecting-to-databases",
searchKey: "Connecting to databases",
},
LIST_WIDGET: {
path: "/widget-reference/list",
searchKey: "List",
},
SWITCH_WIDGET: {
path: "/widget-reference/switch",
searchKey: "Switch",

View File

@ -24,6 +24,7 @@ export enum WidgetTypes {
FILE_PICKER_WIDGET = "FILE_PICKER_WIDGET",
VIDEO_WIDGET = "VIDEO_WIDGET",
SKELETON_WIDGET = "SKELETON_WIDGET",
LIST_WIDGET = "LIST_WIDGET",
SWITCH_WIDGET = "SWITCH_WIDGET",
}

View File

@ -18,6 +18,7 @@ export enum VALIDATION_TYPES {
MAX_DATE = "MAX_DATE",
TABS_DATA = "TABS_DATA",
CHART_DATA = "CHART_DATA",
LIST_DATA = "LIST_DATA",
CUSTOM_FUSION_CHARTS_DATA = "CUSTOM_FUSION_CHARTS_DATA",
MARKERS = "MARKERS",
ACTION_SELECTOR = "ACTION_SELECTOR",

View File

@ -0,0 +1,9 @@
import { ERROR_WIDGET_COPY_NOT_ALLOWED } from "./messages";
describe("messages", () => {
it("checks for ERROR_WIDGET_COPY_NOT_ALLOWED string", () => {
expect(ERROR_WIDGET_COPY_NOT_ALLOWED()).toBe(
"This selected widget cannot be copied.",
);
});
});

View File

@ -250,6 +250,8 @@ export const WIDGET_DELETE = (widgetName: string) =>
export const WIDGET_COPY = (widgetName: string) => `Copied ${widgetName}`;
export const ERROR_WIDGET_COPY_NO_WIDGET_SELECTED = () =>
`Please select a widget to copy`;
export const ERROR_WIDGET_COPY_NOT_ALLOWED = () =>
`This selected widget cannot be copied.`;
export const WIDGET_CUT = (widgetName: string) => `Cut ${widgetName}`;
export const ERROR_WIDGET_CUT_NO_WIDGET_SELECTED = () =>
`Please select a widget to cut`;

View File

@ -0,0 +1,20 @@
import React from "react";
import { WidgetIcons } from "./WidgetIcons";
import { render, screen } from "@testing-library/react";
import { ThemeProvider, theme } from "constants/DefaultTheme";
const ListWidgetIcon = WidgetIcons["LIST_WIDGET"];
describe("WidgetIcons", () => {
it("checks widget icon for list widget", () => {
render(
<ThemeProvider theme={theme}>
<ListWidgetIcon background="red" />
</ThemeProvider>,
);
const input = screen.queryByTestId("list-widget-icon");
expect(input).toBeTruthy();
});
});

View File

@ -21,6 +21,7 @@ import { ReactComponent as ChartIcon } from "assets/icons/widget/chart.svg";
import { ReactComponent as FormIcon } from "assets/icons/widget/form.svg";
import { ReactComponent as MapIcon } from "assets/icons/widget/map.svg";
import { ReactComponent as ModalIcon } from "assets/icons/widget/modal.svg";
import { ReactComponent as ListIcon } from "assets/icons/widget/list.svg";
/* eslint-disable react/display-name */
export const WidgetIcons: {
@ -136,6 +137,11 @@ export const WidgetIcons: {
<ButtonIcon />
</IconWrapper>
),
LIST_WIDGET: (props: IconProps) => (
<IconWrapper {...props} data-testid="list-widget-icon">
<ListIcon />
</IconWrapper>
),
};
export type WidgetIcon = typeof WidgetIcons[keyof typeof WidgetIcons];

View File

@ -0,0 +1,77 @@
import WIDGET_CONFIG_RESPONSE from "./WidgetConfigResponse";
describe("WidgetConfigResponse", () => {
it("it tests autocomplete child enhancements", () => {
const mockProps = {
childAutoComplete: "child-autocomplet",
};
expect(
WIDGET_CONFIG_RESPONSE.config.LIST_WIDGET.enhancements.child.autocomplete(
mockProps,
),
).toBe(mockProps.childAutoComplete);
});
it("it tests hideEvaluatedValue child enhancements", () => {
expect(
WIDGET_CONFIG_RESPONSE.config.LIST_WIDGET.enhancements.child.hideEvaluatedValue(),
).toBe(true);
});
it("it tests propertyUpdateHook child enhancements with undefined parent widget", () => {
const mockParentWidget = {
widgetId: undefined,
};
const result = WIDGET_CONFIG_RESPONSE.config.LIST_WIDGET.enhancements.child.propertyUpdateHook(
mockParentWidget,
"child-widget-name",
"text",
"value",
false,
);
expect(result).toStrictEqual([]);
});
it("it tests propertyUpdateHook child enhancements with undefined parent widget", () => {
const mockParentWidget = {
widgetId: undefined,
};
const result = WIDGET_CONFIG_RESPONSE.config.LIST_WIDGET.enhancements.child.propertyUpdateHook(
mockParentWidget,
"child-widget-name",
"text",
"value",
false,
);
expect(result).toStrictEqual([]);
});
it("it tests propertyUpdateHook child enhancements with defined parent widget", () => {
const mockParentWidget = {
widgetId: "parent-widget-id",
widgetName: "parent-widget-name",
};
const result = WIDGET_CONFIG_RESPONSE.config.LIST_WIDGET.enhancements.child.propertyUpdateHook(
mockParentWidget,
"child-widget-name",
"text",
"value",
false,
);
expect(result).toStrictEqual([
{
widgetId: "parent-widget-id",
propertyPath: "template.child-widget-name.text",
propertyValue: "{{parent-widget-name.items.map((currentItem) => )}}",
isDynamicTrigger: false,
},
]);
});
});

View File

@ -1,10 +1,18 @@
import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigReducer";
import { WidgetProps } from "widgets/BaseWidget";
import moment from "moment-timezone";
import { cloneDeep, get, indexOf, isString } from "lodash";
import { generateReactKey } from "utils/generators";
import { WidgetTypes } from "constants/WidgetConstants";
import { BlueprintOperationTypes } from "sagas/WidgetBlueprintSagasEnums";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { getDynamicBindings } from "utils/DynamicBindingUtils";
import { Colors } from "constants/Colors";
import FileDataTypes from "widgets/FileDataTypes";
/**
* this config sets the default values of properties being used in the widget
*/
const WidgetConfigResponse: WidgetConfigReducerState = {
config: {
BUTTON_WIDGET: {
@ -225,7 +233,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
blueprint: {
operations: [
{
type: "MODIFY_PROPS",
type: BlueprintOperationTypes.MODIFY_PROPS,
fn: (widget: WidgetProps & { children?: WidgetProps[] }) => {
const tabs = [...widget.tabs];
@ -320,9 +328,10 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
],
operations: [
{
type: "MODIFY_PROPS",
type: BlueprintOperationTypes.MODIFY_PROPS,
fn: (
widget: WidgetProps & { children?: WidgetProps[] },
widgets: { [widgetId: string]: FlattenedWidgetProps },
parent?: WidgetProps & { children?: WidgetProps[] },
) => {
const iconChild =
@ -490,6 +499,323 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
widgetName: "Skeleton",
version: 1,
},
[WidgetTypes.LIST_WIDGET]: {
backgroundColor: "",
itemBackgroundColor: "white",
rows: 10,
columns: 8,
gridType: "vertical",
enhancements: {
child: {
autocomplete: (parentProps: any) => {
return parentProps.childAutoComplete;
},
hideEvaluatedValue: () => true,
propertyUpdateHook: (
parentProps: any,
widgetName: string,
propertyPath: string, // onClick
propertyValue: string,
isTriggerProperty: boolean,
) => {
let value = propertyValue;
if (!parentProps.widgetId) return [];
const { jsSnippets } = getDynamicBindings(propertyValue);
const modifiedAction = jsSnippets.reduce(
(prev: string, next: string) => {
return `${prev}${next}`;
},
"",
);
value = `{{${parentProps.widgetName}.items.map((currentItem) => ${modifiedAction})}}`;
const path = `template.${widgetName}.${propertyPath}`;
return [
{
widgetId: parentProps.widgetId,
propertyPath: path,
propertyValue: isTriggerProperty ? propertyValue : value,
isDynamicTrigger: isTriggerProperty,
},
];
},
},
},
gridGap: 0,
items: [
{
id: 1,
num: "001",
name: "Bulbasaur",
img: "http://www.serebii.net/pokemongo/pokemon/001.png",
},
{
id: 2,
num: "002",
name: "Ivysaur",
img: "http://www.serebii.net/pokemongo/pokemon/002.png",
},
{
id: 3,
num: "003",
name: "Venusaur",
img: "http://www.serebii.net/pokemongo/pokemon/003.png",
},
{
id: 4,
num: "004",
name: "Charmander",
img: "http://www.serebii.net/pokemongo/pokemon/004.png",
},
{
id: 5,
num: "005",
name: "Charmeleon",
img: "http://www.serebii.net/pokemongo/pokemon/005.png",
},
{
id: 6,
num: "006",
name: "Charizard",
img: "http://www.serebii.net/pokemongo/pokemon/006.png",
},
],
widgetName: "List",
children: [],
blueprint: {
view: [
{
type: "CANVAS_WIDGET",
position: { top: 0, left: 0 },
props: {
containerStyle: "none",
canExtend: false,
detachFromLayout: true,
dropDisabled: true,
noPad: true,
children: [],
blueprint: {
view: [
{
type: "CONTAINER_WIDGET",
size: { rows: 4, cols: 16 },
position: { top: 0, left: 0 },
props: {
backgroundColor: "white",
containerStyle: "card",
dragDisabled: true,
isDeletable: false,
disallowCopy: true,
disablePropertyPane: true,
children: [],
blueprint: {
view: [
{
type: "CANVAS_WIDGET",
position: { top: 0, left: 0 },
props: {
containerStyle: "none",
canExtend: false,
detachFromLayout: true,
children: [],
version: 1,
blueprint: {
view: [
{
type: "IMAGE_WIDGET",
size: { rows: 3, cols: 4 },
position: { top: 0, left: 0 },
props: {
defaultImage:
"https://res.cloudinary.com/drako999/image/upload/v1589196259/default.png",
imageShape: "RECTANGLE",
maxZoomLevel: 1,
image: "{{currentItem.img}}",
dynamicBindingPathList: [
{
key: "image",
},
],
dynamicTriggerPathList: [],
},
},
{
type: "TEXT_WIDGET",
size: { rows: 1, cols: 6 },
position: { top: 0, left: 4 },
props: {
text: "{{currentItem.name}}",
textStyle: "HEADING",
textAlign: "LEFT",
dynamicBindingPathList: [
{
key: "text",
},
],
dynamicTriggerPathList: [],
},
},
{
type: "TEXT_WIDGET",
size: { rows: 1, cols: 6 },
position: { top: 1, left: 4 },
props: {
text: "{{currentItem.num}}",
textStyle: "BODY",
textAlign: "LEFT",
dynamicBindingPathList: [
{
key: "text",
},
],
dynamicTriggerPathList: [],
},
},
],
},
},
},
],
},
},
},
],
},
},
},
],
operations: [
{
type: BlueprintOperationTypes.MODIFY_PROPS,
fn: (
widget: WidgetProps & { children?: WidgetProps[] },
widgets: { [widgetId: string]: FlattenedWidgetProps },
) => {
let template = {};
const container = get(
widgets,
`${get(widget, "children.0.children.0")}`,
);
const canvas = get(widgets, `${get(container, "children.0")}`);
let updatePropertyMap: any = [];
const dynamicBindingPathList: any[] = get(
widget,
"dynamicBindingPathList",
[],
);
canvas.children &&
get(canvas, "children", []).forEach((child: string) => {
const childWidget = cloneDeep(get(widgets, `${child}`));
const keys = Object.keys(childWidget);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
let value = childWidget[key];
if (isString(value) && value.indexOf("currentItem") > -1) {
const { jsSnippets } = getDynamicBindings(value);
const modifiedAction = jsSnippets.reduce(
(prev: string, next: string) => {
return prev + `${next}`;
},
"",
);
value = `{{${widget.widgetName}.items.map((currentItem) => ${modifiedAction})}}`;
childWidget[key] = value;
dynamicBindingPathList.push({
key: `template.${childWidget.widgetName}.${key}`,
});
}
}
template = {
...template,
[childWidget.widgetName]: childWidget,
};
});
updatePropertyMap = [
{
widgetId: widget.widgetId,
propertyName: "dynamicBindingPathList",
propertyValue: dynamicBindingPathList,
},
{
widgetId: widget.widgetId,
propertyName: "template",
propertyValue: template,
},
];
return updatePropertyMap;
},
},
{
type: BlueprintOperationTypes.CHILD_OPERATIONS,
fn: (
widgets: { [widgetId: string]: FlattenedWidgetProps },
widgetId: string,
parentId: string,
widgetPropertyMaps: {
defaultPropertyMap: Record<string, string>;
},
) => {
if (!parentId) return { widgets };
const widget = { ...widgets[widgetId] };
const parent = { ...widgets[parentId] };
const disallowedWidgets = [WidgetTypes.FILE_PICKER_WIDGET];
if (
Object.keys(widgetPropertyMaps.defaultPropertyMap).length > 0 ||
indexOf(disallowedWidgets, widget.type) > -1
) {
const widget = widgets[widgetId];
if (widget.children && widget.children.length > 0) {
widget.children.forEach((childId: string) => {
delete widgets[childId];
});
}
if (widget.parentId) {
const _parent = { ...widgets[widget.parentId] };
_parent.children = _parent.children?.filter(
(id) => id !== widgetId,
);
widgets[widget.parentId] = _parent;
}
delete widgets[widgetId];
return {
widgets,
message: `${
WidgetConfigResponse.config[widget.type].widgetName
} widgets cannot be used inside the list widget right now.`,
};
}
const template = {
...get(parent, "template", {}),
[widget.widgetName]: widget,
};
parent.template = template;
widgets[parentId] = parent;
return { widgets };
},
},
],
},
},
},
configVersion: 1,
};

View File

@ -49,6 +49,12 @@ const WidgetSidebarResponse: WidgetCardProps[] = [
widgetCardName: "Form",
key: generateReactKey(),
},
{
type: "LIST_WIDGET",
widgetCardName: "List",
key: generateReactKey(),
isBeta: true,
},
{
type: "IMAGE_WIDGET",
widgetCardName: "Image",

View File

@ -1,5 +1,5 @@
import React, { memo, useCallback } from "react";
import _ from "lodash";
import _, { get } from "lodash";
import {
ControlPropertyLabelContainer,
ControlWrapper,
@ -31,6 +31,11 @@ import { OnboardingStep } from "constants/OnboardingConstants";
import Indicator from "components/editorComponents/Onboarding/Indicator";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import {
useChildWidgetEnhancementFns,
useParentWithEnhancementFn,
} from "sagas/WidgetEnhancementHelpers";
type Props = PropertyPaneControlConfig & {
panel: IPanelProps;
theme: EditorTheme;
@ -39,6 +44,17 @@ type Props = PropertyPaneControlConfig & {
const PropertyControl = memo((props: Props) => {
const dispatch = useDispatch();
const widgetProperties: any = useSelector(getWidgetPropsForPropertyPane);
const parentWithEnhancement = useParentWithEnhancementFn(
widgetProperties.widgetId,
);
/** get all child enhancments functions */
const {
propertyPaneEnhancmentFn: childWidgetPropertyUpdateEnhancementFn,
autoCompleteEnhancementFn: childWidgetAutoCompleteEnhancementFn,
customJSControlEnhancementFn: childWidgetCustomJSControlEnhancementFn,
hideEvaluatedValueEnhancementFn: childWidgetHideEvaluatedValueEnhancementFn,
} = useChildWidgetEnhancementFns(widgetProperties.widgetId);
const toggleDynamicProperty = useCallback(
(propertyName: string, isDynamic: boolean) => {
@ -79,7 +95,28 @@ const PropertyControl = memo((props: Props) => {
),
[widgetProperties.widgetId, dispatch],
);
// this function updates the properties of widget passed
const onBatchUpdatePropertiesOfWidget = useCallback(
(
allUpdates: Record<string, unknown>,
widgetId: string,
triggerPaths: string[],
) => {
dispatch(
batchUpdateWidgetProperty(widgetId, {
modify: allUpdates,
triggerPaths,
}),
);
},
[dispatch],
);
/**
* this function is called whenever we change any property in the property pane
* it updates the widget property by updateWidgetPropertyRequest
* It also calls the beforeChildPropertyUpdate hook
*/
const onPropertyChange = useCallback(
(propertyName: string, propertyValue: any) => {
AnalyticsUtil.logEvent("WIDGET_PROPERTY_UPDATE", {
@ -88,7 +125,6 @@ const PropertyControl = memo((props: Props) => {
propertyName: propertyName,
updatedValue: propertyValue,
});
let propertiesToUpdate:
| Array<{
propertyPath: string;
@ -102,6 +138,39 @@ const PropertyControl = memo((props: Props) => {
propertyValue,
);
}
// if there are enhancements related to the widget, calling them here
// enhancements are basically group of functions that are called before widget propety
// is changed on propertypane. For e.g - set/update parent property
if (childWidgetPropertyUpdateEnhancementFn) {
const hookPropertiesUpdates = childWidgetPropertyUpdateEnhancementFn(
widgetProperties.widgetName,
propertyName,
propertyValue,
props.isTriggerProperty,
);
if (
Array.isArray(hookPropertiesUpdates) &&
hookPropertiesUpdates.length > 0
) {
const allUpdates: Record<string, unknown> = {};
const triggerPaths: string[] = [];
hookPropertiesUpdates.forEach(
({ propertyPath, propertyValue, isDynamicTrigger }) => {
allUpdates[propertyPath] = propertyValue;
if (isDynamicTrigger) triggerPaths.push(propertyPath);
},
);
onBatchUpdatePropertiesOfWidget(
allUpdates,
get(parentWithEnhancement, "widgetId", ""),
triggerPaths,
);
}
}
if (propertiesToUpdate) {
const allUpdates: Record<string, unknown> = {};
propertiesToUpdate.forEach(({ propertyPath, propertyValue }) => {
@ -190,6 +259,7 @@ const PropertyControl = memo((props: Props) => {
expected: FIELD_EXPECTED_VALUE[widgetProperties.type as WidgetType][
propertyName
] as any,
additionalDynamicData: {},
};
if (isPathADynamicTrigger(widgetProperties, propertyName)) {
config.isValid = true;
@ -209,6 +279,36 @@ const PropertyControl = memo((props: Props) => {
.join("")
.toLowerCase();
let additionAutocomplete = undefined;
if (additionalAutoComplete) {
additionAutocomplete = additionalAutoComplete(widgetProperties);
} else if (childWidgetAutoCompleteEnhancementFn) {
additionAutocomplete = childWidgetAutoCompleteEnhancementFn();
}
/**
* if the current widget requires a customJSControl, use that.
*/
const getCustomJSControl = () => {
if (childWidgetCustomJSControlEnhancementFn) {
return childWidgetCustomJSControlEnhancementFn();
}
return props.customJSControl;
};
/**
* should the property control hide evaluated popover
* @returns
*/
const hideEvaluatedValue = () => {
if (childWidgetHideEvaluatedValueEnhancementFn) {
return childWidgetHideEvaluatedValueEnhancementFn();
}
return false;
};
try {
return (
<ControlWrapper
@ -255,10 +355,9 @@ const PropertyControl = memo((props: Props) => {
theme: props.theme,
},
isDynamic,
props.customJSControl,
additionalAutoComplete
? additionalAutoComplete(widgetProperties)
: undefined,
getCustomJSControl(),
additionAutocomplete,
hideEvaluatedValue(),
)}
</Indicator>
</Boxed>

View File

@ -0,0 +1,66 @@
import { IPanelProps } from "@blueprintjs/core";
import {
PropertyPaneConfig,
PropertyPaneControlConfig,
PropertyPaneSectionConfig,
} from "constants/PropertyControlConstants";
import { WidgetType } from "constants/WidgetConstants";
import React from "react";
import WidgetFactory from "utils/WidgetFactory";
import PropertyControl from "./PropertyControl";
import PropertySection from "./PropertySection";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
export type PropertyControlsGeneratorProps = {
id: string;
type: WidgetType;
panel: IPanelProps;
theme: EditorTheme;
};
export const generatePropertyControl = (
propertyPaneConfig: readonly PropertyPaneConfig[],
props: PropertyControlsGeneratorProps,
) => {
if (!propertyPaneConfig) return null;
return propertyPaneConfig.map((config: PropertyPaneConfig) => {
if ((config as PropertyPaneSectionConfig).sectionName) {
const sectionConfig: PropertyPaneSectionConfig = config as PropertyPaneSectionConfig;
return (
<PropertySection
key={config.id + props.id}
id={config.id || sectionConfig.sectionName}
name={sectionConfig.sectionName}
hidden={sectionConfig.hidden}
propertyPath={sectionConfig.propertySectionPath}
isDefaultOpen
>
{config.children && generatePropertyControl(config.children, props)}
</PropertySection>
);
} else if ((config as PropertyPaneControlConfig).controlType) {
return (
<PropertyControl
key={config.id + props.id}
{...(config as PropertyPaneControlConfig)}
panel={props.panel}
theme={props.theme}
/>
);
}
throw Error("Unknown configuration provided: " + props.type);
});
};
export const PropertyControlsGenerator = (
props: PropertyControlsGeneratorProps,
) => {
const config = WidgetFactory.getWidgetPropertyPaneConfig(props.type);
return (
<>
{generatePropertyControl(config as readonly PropertyPaneConfig[], props)}
</>
);
};
export default PropertyControlsGenerator;

View File

@ -38,6 +38,7 @@ import { FormIcons } from "icons/FormIcons";
import PropertyPaneHelpButton from "pages/Editor/PropertyPaneHelpButton";
import { getProppanePreference } from "selectors/usersSelectors";
import { PropertyPanePositionConfig } from "reducers/uiReducers/usersReducer";
import { get } from "lodash";
const PropertyPaneWrapper = styled(PaneWrapper)<{
themeMode?: EditorTheme;
@ -178,6 +179,10 @@ class PropertyPane extends Component<PropertyPaneProps, PropertyPaneState> {
}
render() {
if (get(this.props, "widgetProperties.disablePropertyPane")) {
return null;
}
if (this.props.isVisible) {
log.debug("Property pane rendered");
const content = this.renderPropertyPane();
@ -212,13 +217,15 @@ class PropertyPane extends Component<PropertyPaneProps, PropertyPaneState> {
renderPropertyPane() {
const { widgetProperties } = this.props;
if (!widgetProperties)
return (
<PropertyPaneWrapper
className={"t--propertypane"}
themeMode={this.getTheme()}
/>
);
if (!widgetProperties) {
return <></>;
}
// if settings control is disabled, don't render anything
// for e.g - this will be true for list widget tempalte container widget
if (widgetProperties?.disablePropertyPane) return <></>;
return (
<PropertyPaneWrapper
className={"t--propertypane"}

View File

@ -38,20 +38,17 @@ const Wrapper = styled.div<{ iconCount: number }>`
align-items: center;
height: ${(props) => props.theme.propertyPane.titleHeight}px;
background-color: ${(props) => props.theme.colors.propertyPane.bg};
& span.${BlueprintClasses.POPOVER_TARGET} {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
&&& .${BlueprintClasses.EDITABLE_TEXT} {
height: auto;
padding: 0;
width: 100%;
}
&&&
.${BlueprintClasses.EDITABLE_TEXT_CONTENT},
&&&
@ -59,7 +56,6 @@ const Wrapper = styled.div<{ iconCount: number }>`
color: ${(props) => props.theme.colors.propertyPane.title};
font-size: ${(props) => props.theme.fontSizes[4]}px;
}
&& svg path {
fill: ${(props) => props.theme.colors.propertyPane.label};
}
@ -71,7 +67,6 @@ const NameWrapper = styled.div<{ isPanelTitle?: boolean }>`
min-width: 100%;
padding-right: 25px;
max-width: 134px;
&&&&&&& > * {
overflow: hidden;
}

View File

@ -23,6 +23,7 @@ export const Wrapper = styled.div`
padding: 10px 5px 10px 5px;
border-radius: 0px;
border: none;
position: relative;
color: ${Colors.ALTO};
height: 72px;
display: flex;
@ -54,6 +55,17 @@ export const Wrapper = styled.div`
}
`;
export const BetaLabel = styled.div`
font-size: 10px;
background: ${Colors.TUNDORA};
margin-top: 3px;
padding: 2px 4px;
border-radius: 3px;
position: absolute;
top: 0;
right: -2%;
`;
export const IconLabel = styled.h5`
text-align: center;
margin: 0;
@ -116,6 +128,7 @@ const WidgetCard = (props: CardProps) => {
<div>
<Icon />
<IconLabel>{props.details.widgetCardName}</IconLabel>
{props.details.isBeta && <BetaLabel>Beta</BetaLabel>}
</div>
</Wrapper>
</React.Fragment>

View File

@ -28,6 +28,7 @@ import { IconWidgetProps } from "widgets/IconWidget";
import { VideoWidgetProps } from "widgets/VideoWidget";
import { SkeletonWidgetProps } from "../../widgets/SkeletonWidget";
import { SwitchWidgetProps } from "widgets/SwitchWidget";
import { ListWidgetProps } from "../../widgets/ListWidget/ListWidget";
const initialState: WidgetConfigReducerState = WidgetConfigResponse;
@ -78,6 +79,7 @@ export interface WidgetConfigReducerState {
WidgetConfigProps;
ICON_WIDGET: Partial<IconWidgetProps> & WidgetConfigProps;
SKELETON_WIDGET: Partial<SkeletonWidgetProps> & WidgetConfigProps;
LIST_WIDGET: Partial<ListWidgetProps<WidgetProps>> & WidgetConfigProps;
};
configVersion: number;
}

View File

@ -56,4 +56,5 @@ const uiReducer = combineReducers({
globalSearch: globalSearchReducer,
releases: releasesReducer,
});
export default uiReducer;

View File

@ -182,6 +182,7 @@ export function* fetchPageSaga(
id,
});
const isValidResponse = yield validateResponse(fetchPageResponse);
if (isValidResponse) {
// Clear any existing caches
yield call(clearEvalCache);
@ -234,7 +235,10 @@ export function* fetchPublishedPageSaga(
const { pageId, bustCache } = pageRequestAction.payload;
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_API,
{ pageId: pageId, published: true },
{
pageId: pageId,
published: true,
},
);
const request: FetchPublishedPageRequest = {
pageId,

View File

@ -0,0 +1,51 @@
import WidgetFactory from "utils/WidgetFactory";
import {
BlueprintOperation,
executeWidgetBlueprintChildOperations,
} from "./WidgetBlueprintSagas";
import { BlueprintOperationTypes } from "./WidgetBlueprintSagasEnums";
describe("WidgetBlueprintSagas", () => {
it("should returns widgets after executing the child operation", async () => {
const mockBlueprintChildOperation: BlueprintOperation = {
type: BlueprintOperationTypes.CHILD_OPERATIONS,
fn: () => {
return { widgets: {} };
},
};
jest
.spyOn(WidgetFactory, "getWidgetDefaultPropertiesMap")
.mockReturnValue({});
const generator = executeWidgetBlueprintChildOperations(
mockBlueprintChildOperation,
{
widgetId: {
image: "",
defaultImage: "",
widgetId: "Widget1",
type: "LIST_WIDGET",
widgetName: "List1",
parentId: "parentId",
renderMode: "CANVAS",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 3,
isLoading: false,
items: [],
version: 16,
disablePropertyPane: false,
},
},
"widgetId",
"parentId",
);
expect(generator.next().value).toStrictEqual({});
});
});

View File

@ -4,6 +4,16 @@ import { WidgetProps } from "widgets/BaseWidget";
import { generateReactKey } from "utils/generators";
import { call } from "redux-saga/effects";
import { get } from "lodash";
import WidgetFactory from "utils/WidgetFactory";
import {
MAIN_CONTAINER_WIDGET_ID,
WidgetType,
} from "constants/WidgetConstants";
import WidgetConfigResponse from "mockResponses/WidgetConfigResponse";
import { Variant } from "components/ads/common";
import { Toaster } from "components/ads/Toast";
import { BlueprintOperationTypes } from "./WidgetBlueprintSagasEnums";
function buildView(view: WidgetBlueprint["view"], widgetId: string) {
const children = [];
@ -46,17 +56,28 @@ export type UpdatePropertyArgs = {
export type BlueprintOperationAddActionFn = () => void;
export type BlueprintOperationModifyPropsFn = (
widget: WidgetProps & { children?: WidgetProps[] },
widgets: { [widgetId: string]: FlattenedWidgetProps },
parent?: WidgetProps,
) => UpdatePropertyArgs[] | undefined;
export interface ChildOperationFnResponse {
widgets: Record<string, FlattenedWidgetProps>;
message?: string;
}
export type BlueprintOperationChildOperationsFn = (
widgets: { [widgetId: string]: FlattenedWidgetProps },
widgetId: string,
parentId: string,
widgetPropertyMaps: {
defaultPropertyMap: Record<string, string>;
},
) => ChildOperationFnResponse;
export type BlueprintOperationFunction =
| BlueprintOperationModifyPropsFn
| BlueprintOperationAddActionFn;
export enum BlueprintOperationTypes {
MODIFY_PROPS = "MODIFY_PROPS",
ADD_ACTION = "ADD_ACTION",
}
| BlueprintOperationAddActionFn
| BlueprintOperationChildOperationsFn;
export type BlueprintOperationType = keyof typeof BlueprintOperationTypes;
@ -71,11 +92,12 @@ export function* executeWidgetBlueprintOperations(
widgetId: string,
) {
operations.forEach((operation: BlueprintOperation) => {
const widget: WidgetProps & { children?: string[] | WidgetProps[] } = {
...widgets[widgetId],
};
switch (operation.type) {
case BlueprintOperationTypes.MODIFY_PROPS:
const widget: WidgetProps & { children?: string[] | WidgetProps[] } = {
...widgets[widgetId],
};
if (widget.children && widget.children.length > 0) {
widget.children = (widget.children as string[]).map(
(childId: string) => widgets[childId],
@ -85,6 +107,7 @@ export function* executeWidgetBlueprintOperations(
| UpdatePropertyArgs[]
| undefined = (operation.fn as BlueprintOperationModifyPropsFn)(
widget as WidgetProps & { children?: WidgetProps[] },
widgets,
get(widgets, widget.parentId || "", undefined),
);
updatePropertyPayloads &&
@ -92,7 +115,115 @@ export function* executeWidgetBlueprintOperations(
widgets[params.widgetId][params.propertyName] =
params.propertyValue;
});
break;
}
});
return yield widgets;
}
/**
* this saga executes the blueprint child operation
*
* @param parent
* @param newWidgetId
* @param widgets
*
* @returns { [widgetId: string]: FlattenedWidgetProps }
*/
export function* executeWidgetBlueprintChildOperations(
operation: BlueprintOperation,
canvasWidgets: { [widgetId: string]: FlattenedWidgetProps },
widgetId: string,
parentId: string,
) {
// TODO(abhinav): Special handling for child operaionts
// This needs to be deprecated soon
// Get the default properties map of the current widget
// The operation can handle things based on this map
// Little abstraction leak, but will be deprecated soon
const widgetPropertyMaps = {
defaultPropertyMap: WidgetFactory.getWidgetDefaultPropertiesMap(
canvasWidgets[widgetId].type as WidgetType,
),
};
const {
widgets,
message,
} = (operation.fn as BlueprintOperationChildOperationsFn)(
canvasWidgets,
widgetId,
parentId,
widgetPropertyMaps,
);
// If something odd happens show the message related to the odd scenario
if (message) {
Toaster.show({
text: message,
hideProgressBar: false,
variant: Variant.info,
});
}
// Flow returns to the usual from here.
return widgets;
}
/**
* this saga traverse the tree till we get
* to MAIN_CONTAINER_WIDGET_ID while travesring, if we find
* any widget which has CHILD_OPERATION, we will call the fn in it
*
* @param parent
* @param newWidgetId
* @param widgets
*
* @returns { [widgetId: string]: FlattenedWidgetProps }
*/
export function* traverseTreeAndExecuteBlueprintChildOperations(
parent: FlattenedWidgetProps,
newWidgetId: string,
widgets: { [widgetId: string]: FlattenedWidgetProps },
) {
let root = parent;
while (root.parentId && root.widgetId !== MAIN_CONTAINER_WIDGET_ID) {
const parentConfig = {
...(WidgetConfigResponse as any).config[root.type],
};
// find the blueprint with type CHILD_OPERATIONS
const blueprintChildOperation = get(
parentConfig,
"blueprint.operations",
[],
).find(
(operation: BlueprintOperation) =>
operation.type === BlueprintOperationTypes.CHILD_OPERATIONS,
);
// if there is blueprint operation with CHILD_OPERATION type, call the fn in it
if (blueprintChildOperation) {
const updatedWidgets:
| { [widgetId: string]: FlattenedWidgetProps }
| undefined = yield call(
executeWidgetBlueprintChildOperations,
blueprintChildOperation,
widgets,
newWidgetId,
root.widgetId,
);
if (updatedWidgets) {
widgets = updatedWidgets;
}
}
root = widgets[root.parentId];
}
return widgets;
}

View File

@ -0,0 +1,5 @@
export enum BlueprintOperationTypes {
MODIFY_PROPS = "MODIFY_PROPS",
ADD_ACTION = "ADD_ACTION",
CHILD_OPERATIONS = "CHILD_OPERATIONS",
}

View File

@ -0,0 +1,221 @@
import {
MAIN_CONTAINER_WIDGET_ID,
WidgetType,
} from "constants/WidgetConstants";
import { get, set } from "lodash";
import WidgetConfigResponse from "mockResponses/WidgetConfigResponse";
import { useSelector } from "react-redux";
import { AppState } from "reducers";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { select } from "redux-saga/effects";
import { getWidgets } from "./selectors";
/*
TODO(abhinav/pawan): Write unit tests for the following functions
Note:
Signature for enhancements in WidgetConfigResponse is as follows:
enhancements: {
child: {
autocomplete: (parentProps: any) => Record<string, Record<string, unknown>>,
customJSControl: (parentProps: any) => string,
propertyUpdateHook: (parentProps: any, widgetName: string, propertyPath: string, propertyValue: string),
action: (parentProps: any, dynamicString: string, responseData?: any[]) => { actionString: string, dataToApply?: any[]},
}
}
*/
// Enum which identifies the path in the enhancements for the
export enum WidgetEnhancementType {
WIDGET_ACTION = "child.action",
PROPERTY_UPDATE = "child.propertyUpdateHook",
CUSTOM_CONTROL = "child.customJSControl",
AUTOCOMPLETE = "child.autocomplete",
HIDE_EVALUATED_VALUE = "child.hideEvaluatedValue",
}
function getParentWithEnhancementFn(
widgetId: string,
widgets: CanvasWidgetsReduxState,
) {
let widget = get(widgets, widgetId, undefined);
// While this widget has a parent
while (widget?.parentId) {
// Get parent widget props
const parent = get(widgets, widget.parentId, undefined);
// If parent has enhancements property
// enhancements property is a new widget property which tells us that
// the property pane, properties or actions of this widget or its children
// can be enhanced
if (parent && parent.enhancements) {
return parent;
}
// If we didn't find any enhancements
// keep walking up the tree to find the parent which does
// if the parent doesn't have a parent stop walking the tree.
// also stop if the parent is the main container (Main container doesn't have enhancements)
if (parent?.parentId && parent.parentId !== MAIN_CONTAINER_WIDGET_ID) {
widget = get(widgets, widget.parentId, undefined);
continue;
}
return;
}
}
export function getWidgetEnhancementFn(
type: WidgetType,
enhancementType: WidgetEnhancementType,
) {
// Get enhancements for the widget type from the config response
// Spread the config response so that we don't pollute the original
// configs
const { enhancements = {} } = {
...(WidgetConfigResponse as any).config[type],
};
return get(enhancements, enhancementType, undefined);
}
// TODO(abhinav): Getting data from the tree may not be needed
// confirm this.
export const getPropsFromTree = (
state: AppState,
widgetName?: string,
): unknown => {
// Get the evaluated data of this widget from the evaluations tree.
if (!widgetName) return;
return get(state.evaluations.tree, widgetName, undefined);
};
export function* getChildWidgetEnhancementFn(
widgetId: string,
enhancementType: WidgetEnhancementType,
) {
// Get all widgets from the canvas
const widgets: CanvasWidgetsReduxState = yield select(getWidgets);
// Get the parent which wants to enhance this widget
const parentWithEnhancementFn = getParentWithEnhancementFn(widgetId, widgets);
// If such a parent is found
if (parentWithEnhancementFn) {
// Get the enhancement function based on the enhancementType
// from the configs
const enhancementFn = getWidgetEnhancementFn(
parentWithEnhancementFn.type,
enhancementType,
);
// Get the parent's evaluated data from the evaluatedTree
const parentDataFromDataTree: unknown = yield select(
getPropsFromTree,
parentWithEnhancementFn.widgetName,
);
if (parentDataFromDataTree) {
// Update the enhancement function by passing the widget data as the first parameter
return (...args: unknown[]) =>
enhancementFn(parentDataFromDataTree, ...args);
}
}
}
/**
* hook that returns parent with enhancments
*
* @param widgetId
* @returns
*/
export function useParentWithEnhancementFn(widgetId: string) {
const widgets: CanvasWidgetsReduxState = useSelector(getWidgets);
return getParentWithEnhancementFn(widgetId, widgets);
}
export function useChildWidgetEnhancementFn(
widgetId: string,
enhancementType: WidgetEnhancementType,
) {
// Get all widgets from the canvas
const widgets: CanvasWidgetsReduxState = useSelector(getWidgets);
// Get the parent which wants to enhance this widget
const parentWithEnhancementFn = getParentWithEnhancementFn(widgetId, widgets);
// If such a parent is found
// Get the parent's evaluated data from the evaluatedTree
const parentDataFromDataTree: unknown = useSelector((state: AppState) =>
getPropsFromTree(state, parentWithEnhancementFn?.widgetName),
);
if (parentWithEnhancementFn) {
// Get the enhancement function based on the enhancementType
// from the configs
const enhancementFn = getWidgetEnhancementFn(
parentWithEnhancementFn.type,
enhancementType,
);
if (parentDataFromDataTree && enhancementFn) {
// Update the enhancement function by passing the widget data as the first parameter
return (...args: unknown[]) =>
enhancementFn(parentDataFromDataTree, ...args);
}
}
}
type EnhancmentFns = {
propertyPaneEnhancmentFn: any;
autoCompleteEnhancementFn: any;
customJSControlEnhancementFn: any;
hideEvaluatedValueEnhancementFn: any;
};
export function useChildWidgetEnhancementFns(widgetId: string): EnhancmentFns {
const enhancmentFns = {
propertyPaneEnhancmentFn: undefined,
autoCompleteEnhancementFn: undefined,
customJSControlEnhancementFn: undefined,
hideEvaluatedValueEnhancementFn: undefined,
};
// Get all widgets from the canvas
const widgets: CanvasWidgetsReduxState = useSelector(getWidgets);
// Get the parent which wants to enhance this widget
const parentWithEnhancementFn = getParentWithEnhancementFn(widgetId, widgets);
// If such a parent is found
// Get the parent's evaluated data from the evaluatedTree
const parentDataFromDataTree: unknown = useSelector((state: AppState) =>
getPropsFromTree(state, parentWithEnhancementFn?.widgetName),
);
if (parentWithEnhancementFn) {
// Get the enhancement function based on the enhancementType
// from the configs
const widgetEnhancmentFns = {
propertyPaneEnhancmentFn: getWidgetEnhancementFn(
parentWithEnhancementFn.type,
WidgetEnhancementType.PROPERTY_UPDATE,
),
autoCompleteEnhancementFn: getWidgetEnhancementFn(
parentWithEnhancementFn.type,
WidgetEnhancementType.AUTOCOMPLETE,
),
customJSControlEnhancementFn: getWidgetEnhancementFn(
parentWithEnhancementFn.type,
WidgetEnhancementType.CUSTOM_CONTROL,
),
hideEvaluatedValueEnhancementFn: getWidgetEnhancementFn(
parentWithEnhancementFn.type,
WidgetEnhancementType.HIDE_EVALUATED_VALUE,
),
};
Object.keys(widgetEnhancmentFns).map((key: string) => {
const enhancementFn = get(widgetEnhancmentFns, `${key}`);
if (parentDataFromDataTree && enhancementFn) {
set(enhancmentFns, `${key}`, (...args: unknown[]) =>
enhancementFn(parentDataFromDataTree, ...args),
);
}
});
}
return enhancmentFns;
}

View File

@ -57,6 +57,7 @@ import WidgetFactory from "utils/WidgetFactory";
import {
buildWidgetBlueprint,
executeWidgetBlueprintOperations,
traverseTreeAndExecuteBlueprintChildOperations,
} from "sagas/WidgetBlueprintSagas";
import { resetWidgetMetaProperty } from "actions/metaActions";
import {
@ -110,7 +111,12 @@ import {
WIDGET_COPY,
WIDGET_CUT,
WIDGET_DELETE,
ERROR_WIDGET_COPY_NOT_ALLOWED,
} from "constants/messages";
import {
doesTriggerPathsContainPropertyPath,
handleSpecificCasesWhilePasting,
} from "./WidgetOperationUtils";
function* getChildWidgetProps(
parent: FlattenedWidgetProps,
@ -243,25 +249,43 @@ function* generateChildWidgets(
widget.widgetId,
);
}
// Add the parentId prop to this widget
widget.parentId = parent.widgetId;
// Remove the blueprint from the widget (if any)
// as blueprints are not useful beyond this point.
delete widget.blueprint;
// deleting propertyPaneEnchancements too as it shouldn't go in dsl because
// function can't be cloned into dsl
// instead of passing whole enhancments function in widget props, we are just setting
// enhancments as true so that we know this widget contains enhancments
if ("enhancements" in widget) {
widget.enhancements = true;
}
return { widgetId: widget.widgetId, widgets };
}
/**
* this saga is called when we drop a widget on the canvas.
*
* @param addChildAction
*/
export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
try {
const start = performance.now();
Toaster.clear();
// NOTE: widgetId here is the parentId of the dropped widget ( we should rename it to avoid confusion )
const { widgetId } = addChildAction.payload;
// Get the current parent widget whose child will be the new widget.
const stateParent: FlattenedWidgetProps = yield select(getWidget, widgetId);
// const parent = Object.assign({}, stateParent);
// Get all the widgets from the canvasWidgetsReducer
const stateWidgets = yield select(getWidgets);
const widgets = Object.assign({}, stateWidgets);
let widgets = Object.assign({}, stateWidgets);
// Generate the full WidgetProps of the widget to be added.
const childWidgetPayload: GeneratedWidgetPayload = yield generateChildWidgets(
stateParent,
@ -278,6 +302,21 @@ export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
widgets[parent.widgetId] = parent;
log.debug("add child computations took", performance.now() - start, "ms");
// some widgets need to update property of parent if the parent have CHILD_OPERATIONS
// so here we are traversing up the tree till we get to MAIN_CONTAINER_WIDGET_ID
// while travesring, if we find any widget which has CHILD_OPERATION, we will call the fn in it
const updatedWidgets: {
[widgetId: string]: FlattenedWidgetProps;
} = yield call(
traverseTreeAndExecuteBlueprintChildOperations,
parent,
addChildAction.payload.newWidgetId,
widgets,
);
widgets = updatedWidgets;
yield put({
type: ReduxActionTypes.WIDGET_CHILD_ADDED,
payload: {
@ -286,6 +325,9 @@ export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
},
});
yield put(updateAndSaveLayout(widgets));
// go up till MAIN_CONTAINER, if there is a operation CHILD_OPERATIONS IN ANY PARENT,
// call execute
} catch (error) {
yield put({
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
@ -402,6 +444,10 @@ export function* deleteSaga(deleteAction: ReduxAction<WidgetDelete>) {
if (!widgetId) {
const selectedWidget = yield select(getSelectedWidget);
if (!selectedWidget) return;
// if widget is not deletable, don't don anything
if (selectedWidget.isDeletable === false) return false;
widgetId = selectedWidget.widgetId;
parentId = selectedWidget.parentId;
}
@ -835,6 +881,7 @@ function* setWidgetDynamicPropertySaga(
function getPropertiesToUpdate(
widget: WidgetProps,
updates: Record<string, unknown>,
triggerPaths?: string[],
): {
propertyUpdates: Record<string, unknown>;
dynamicTriggerPathList: DynamicPath[];
@ -869,11 +916,17 @@ function getPropertiesToUpdate(
}
// Check if the path is a of a dynamic trigger property
const isTriggerProperty = isPropertyATriggerPath(
let isTriggerProperty = isPropertyATriggerPath(
widgetWithUpdates,
propertyPath,
);
isTriggerProperty = doesTriggerPathsContainPropertyPath(
isTriggerProperty,
propertyPath,
triggerPaths,
);
// If it is a trigger property, it will go in a different list than the general
// dynamicBindingPathList.
if (isTriggerProperty) {
@ -912,7 +965,7 @@ function* batchUpdateWidgetPropertySaga(
// Handling the case where sometimes widget id is not passed through here
return;
}
const { modify = {}, remove = [] } = updates;
const { modify = {}, remove = [], triggerPaths } = updates;
const stateWidget: WidgetProps = yield select(getWidget, widgetId);
@ -926,7 +979,7 @@ function* batchUpdateWidgetPropertySaga(
propertyUpdates,
dynamicTriggerPathList,
dynamicBindingPathList,
} = getPropertiesToUpdate(widget, modify);
} = getPropertiesToUpdate(widget, modify, triggerPaths);
// We loop over all updates
Object.entries(propertyUpdates).forEach(
@ -1091,6 +1144,13 @@ function* createWidgetCopy() {
);
}
/**
* copy here actually means saving a JSON in local storage
* so when a user hits copy on a selected widget, we save widget in localStorage
*
* @param action
* @returns
*/
function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
const selectedWidget = yield select(getSelectedWidget);
if (!selectedWidget) {
@ -1101,6 +1161,15 @@ function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
return;
}
if (selectedWidget.disallowCopy === true) {
Toaster.show({
text: createMessage(ERROR_WIDGET_COPY_NOT_ALLOWED),
variant: Variant.info,
});
return;
}
const saveResult = yield createWidgetCopy();
const eventName = action.payload.isShortcut
@ -1157,6 +1226,9 @@ function getNextWidgetName(
]);
}
/**
* this saga create a new widget from the copied one to store
*/
function* pasteWidgetSaga() {
const copiedWidgets: {
widgetId: string;
@ -1178,7 +1250,20 @@ function* pasteWidgetSaga() {
const stateWidgets = yield select(getWidgets);
let widgets = { ...stateWidgets };
const selectedWidget = yield select(getSelectedWidget);
let selectedWidget = yield select(getSelectedWidget);
// when list widget is selected, if the user is pasting, we want it to be pasted in the template
// which is first children of list widget
if (selectedWidget?.type === WidgetTypes.LIST_WIDGET) {
const childrenIds: string[] = yield call(
getWidgetChildren,
selectedWidget.children[0],
);
const firstChildId = childrenIds[0];
selectedWidget = yield select(getWidget, firstChildId);
}
let newWidgetParentId = MAIN_CONTAINER_WIDGET_ID;
let parentWidget = widgets[MAIN_CONTAINER_WIDGET_ID];
@ -1251,6 +1336,7 @@ function* pasteWidgetSaga() {
// Get a flat list of all the widgets to be updated
const widgetList = copiedWidgets.list;
const widgetIdMap: Record<string, string> = {};
const widgetNameMap: Record<string, string> = {};
const newWidgetList: FlattenedWidgetProps[] = [];
let newWidgetId: string = copiedWidget.widgetId;
// Generate new widgetIds for the flat list of all the widgets to be updated
@ -1260,12 +1346,16 @@ function* pasteWidgetSaga() {
newWidget.widgetId = generateReactKey();
// Add the new widget id so that it maps the previous widget id
widgetIdMap[widget.widgetId] = newWidget.widgetId;
// Add the new widget to the list
newWidgetList.push(newWidget);
});
// For each of the new widgets generated
newWidgetList.forEach((widget) => {
for (let i = 0; i < newWidgetList.length; i++) {
const widget = newWidgetList[i];
const oldWidgetName = widget.widgetName;
// Update the children widgetIds if it has children
if (widget.children && widget.children.length > 0) {
widget.children.forEach((childWidgetId: string, index: number) => {
@ -1327,6 +1417,8 @@ function* pasteWidgetSaga() {
widget.widgetName = getNextWidgetName(widgets, widget.type, evalTree);
}
widgetNameMap[oldWidgetName] = widget.widgetName;
// If it is the copied widget, update position properties
if (widget.widgetId === widgetIdMap[copiedWidget.widgetId]) {
newWidgetId = widget.widgetId;
@ -1385,11 +1477,27 @@ function* pasteWidgetSaga() {
widget.widgetName = getNextWidgetName(widgets, widget.type, evalTree);
// Add the new widget to the canvas widgets
widgets[widget.widgetId] = widget;
});
}
// 1. updating template in the copied widget and deleting old template associations
// 2. updating dynamicBindingPathList in the copied grid widget
for (let i = 0; i < newWidgetList.length; i++) {
const widget = newWidgetList[i];
widgets = handleSpecificCasesWhilePasting(
widget,
widgets,
widgetNameMap,
newWidgetList,
);
}
// save the new DSL
yield put(updateAndSaveLayout(widgets));
// hydrating enhancements map after save layout so that enhancement map
// for newly copied widget is hydrated
// Flash the newly pasted widget once the DSL is re-rendered
setTimeout(() => flashElementById(newWidgetId), 100);
yield put({

View File

@ -0,0 +1,207 @@
import { get } from "lodash";
import {
handleIfParentIsListWidgetWhilePasting,
handleSpecificCasesWhilePasting,
doesTriggerPathsContainPropertyPath,
} from "./WidgetOperationUtils";
describe("WidgetOperationSaga", () => {
it("should returns widgets after executing handleIfParentIsListWidgetWhilePasting", async () => {
expect(
doesTriggerPathsContainPropertyPath(false, "trigger-path-1", [
"trigger-path-1",
]),
).toBe(true);
expect(
doesTriggerPathsContainPropertyPath(false, "trigger-path-1", [
"trigger-path-2",
]),
).toBe(false);
expect(
doesTriggerPathsContainPropertyPath(true, "trigger-path-1", [
"trigger-path-2",
]),
).toBe(true);
});
it("should returns widgets after executing handleIfParentIsListWidgetWhilePasting", async () => {
const result = handleIfParentIsListWidgetWhilePasting(
{
widgetId: "text1",
type: "TEXT_WIDGET",
widgetName: "Text1",
parentId: "list1",
renderMode: "CANVAS",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 3,
isLoading: false,
items: [],
text: "{{currentItem.text}}",
version: 16,
disablePropertyPane: false,
},
{
list1: {
widgetId: "list1",
type: "LIST_WIDGET",
widgetName: "List1",
parentId: "0",
renderMode: "CANVAS",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 3,
isLoading: false,
items: [],
version: 16,
disablePropertyPane: false,
template: {},
},
0: {
image: "",
defaultImage: "",
widgetId: "0",
type: "CANVAS_WIDGET",
widgetName: "MainContainer",
renderMode: "CANVAS",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 3,
isLoading: false,
items: [],
version: 16,
disablePropertyPane: false,
template: {},
},
},
);
expect(result.list1.template["Text1"].text).toStrictEqual(
"{{List1.items.map((currentItem) => currentItem.text)}}",
);
expect(get(result, "list1.dynamicBindingPathList.0.key")).toStrictEqual(
"template.Text1.text",
);
});
it("should returns widgets after executing handleSpecificCasesWhilePasting", async () => {
const result = handleSpecificCasesWhilePasting(
{
widgetId: "text2",
type: "TEXT_WIDGET",
widgetName: "Text2",
parentId: "list2",
renderMode: "CANVAS",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 3,
isLoading: false,
items: [],
text: "{{currentItem.text}}",
version: 16,
disablePropertyPane: false,
},
{
list1: {
widgetId: "list1",
type: "LIST_WIDGET",
widgetName: "List1",
parentId: "0",
renderMode: "CANVAS",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 3,
isLoading: false,
items: [],
version: 16,
disablePropertyPane: false,
template: {},
},
0: {
image: "",
defaultImage: "",
widgetId: "0",
type: "CANVAS_WIDGET",
widgetName: "MainContainer",
renderMode: "CANVAS",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 3,
isLoading: false,
items: [],
version: 16,
disablePropertyPane: false,
template: {},
},
list2: {
widgetId: "list2",
type: "LIST_WIDGET",
widgetName: "List2",
parentId: "0",
renderMode: "CANVAS",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 3,
isLoading: false,
items: [],
version: 16,
disablePropertyPane: false,
template: {},
},
},
{
List1: "List2",
},
[
{
widgetId: "list2",
type: "LIST_WIDGET",
widgetName: "List2",
parentId: "0",
renderMode: "CANVAS",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 3,
isLoading: false,
items: [],
version: 16,
disablePropertyPane: false,
template: {},
},
],
);
expect(result.list2.template["Text2"].text).toStrictEqual(
"{{List2.items.map((currentItem) => currentItem.text)}}",
);
expect(get(result, "list2.dynamicBindingPathList.0.key")).toStrictEqual(
"template.Text2.text",
);
});
});

View File

@ -0,0 +1,177 @@
import {
MAIN_CONTAINER_WIDGET_ID,
WidgetTypes,
} from "constants/WidgetConstants";
import { cloneDeep, get, isString } from "lodash";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { getDynamicBindings } from "utils/DynamicBindingUtils";
/**
* checks if triggerpaths contains property path passed
*
* @param isTriggerProperty
* @param propertyPath
* @param triggerPaths
* @returns
*/
export const doesTriggerPathsContainPropertyPath = (
isTriggerProperty: boolean,
propertyPath: string,
triggerPaths?: string[],
) => {
if (!isTriggerProperty) {
if (
triggerPaths &&
triggerPaths.length &&
triggerPaths.includes(propertyPath)
) {
return true;
}
}
return isTriggerProperty;
};
/**
*
* check if copied widget is being pasted in list widget,
* if yes, change all keys in template of list widget and
* update dynamicBindingPathList of ListWidget
*
* updates in list widget :
* 1. `dynamicBindingPathList`
* 2. `template`
*
* @param widget
* @param widgets
*/
export const handleIfParentIsListWidgetWhilePasting = (
widget: FlattenedWidgetProps,
widgets: { [widgetId: string]: FlattenedWidgetProps },
): { [widgetId: string]: FlattenedWidgetProps } => {
let root = get(widgets, `${widget.parentId}`);
while (root.parentId && root.widgetId !== MAIN_CONTAINER_WIDGET_ID) {
if (root.type === WidgetTypes.LIST_WIDGET) {
const listWidget = root;
const currentWidget = cloneDeep(widget);
let template = get(listWidget, "template", {});
const dynamicBindingPathList: any[] = get(
listWidget,
"dynamicBindingPathList",
[],
).slice();
// iterating over each keys of the new createdWidget checking if value contains currentItem
const keys = Object.keys(currentWidget);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
let value = currentWidget[key];
if (isString(value) && value.indexOf("currentItem") > -1) {
const { jsSnippets } = getDynamicBindings(value);
const modifiedAction = jsSnippets.reduce(
(prev: string, next: string) => {
return prev + `${next}`;
},
"",
);
value = `{{${listWidget.widgetName}.items.map((currentItem) => ${modifiedAction})}}`;
currentWidget[key] = value;
dynamicBindingPathList.push({
key: `template.${currentWidget.widgetName}.${key}`,
});
}
}
template = {
...template,
[currentWidget.widgetName]: currentWidget,
};
// now we have updated `dynamicBindingPathList` and updatedTemplate
// we need to update it the list widget
widgets[listWidget.widgetId] = {
...listWidget,
template,
dynamicBindingPathList,
};
}
root = widgets[root.parentId];
}
return widgets;
};
/**
* this saga handles special cases when pasting the widget
*
* for e.g - when the list widget is being copied, we want to update template of list widget
* with new widgets name
*
* @param widget
* @param widgets
* @param widgetNameMap
* @param newWidgetList
* @returns
*/
export const handleSpecificCasesWhilePasting = (
widget: FlattenedWidgetProps,
widgets: { [widgetId: string]: FlattenedWidgetProps },
widgetNameMap: Record<string, string>,
newWidgetList: FlattenedWidgetProps[],
) => {
// this is the case when whole list widget is copied and pasted
if (widget.type === WidgetTypes.LIST_WIDGET) {
Object.keys(widget.template).map((widgetName) => {
const oldWidgetName = widgetName;
const newWidgetName = widgetNameMap[oldWidgetName];
const newWidget = newWidgetList.find(
(w: any) => w.widgetName === newWidgetName,
);
if (newWidget) {
newWidget.widgetName = newWidgetName;
if (widgetName === oldWidgetName) {
widget.template[newWidgetName] = {
...widget.template[oldWidgetName],
widgetId: newWidget.widgetId,
widgetName: newWidget.widgetName,
};
delete widget.template[oldWidgetName];
}
}
// updating dynamicBindingPath in copied widget if the copied widge thas reference to oldWidgetNames
widget.dynamicBindingPathList = (widget.dynamicBindingPathList || []).map(
(path: any) => {
if (path.key.startsWith(`template.${oldWidgetName}`)) {
return {
key: path.key.replace(
`template.${oldWidgetName}`,
`template.${newWidgetName}`,
),
};
}
return path;
},
);
});
widgets[widget.widgetId] = widget;
}
widgets = handleIfParentIsListWidgetWhilePasting(widget, widgets);
return widgets;
};

View File

@ -1,12 +1,13 @@
import { createSelector } from "reselect";
import { find, get } from "lodash";
import { AppState } from "reducers";
import { createSelector } from "reselect";
import { WidgetProps } from "widgets/BaseWidget";
import { getCanvasWidgets } from "./entitiesSelector";
import { getDataTree } from "selectors/dataTreeSelectors";
import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory";
import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { WidgetProps } from "widgets/BaseWidget";
import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory";
import { find } from "lodash";
import { getDataTree } from "selectors/dataTreeSelectors";
import { getCanvasWidgets } from "./entitiesSelector";
const getPropertyPaneState = (state: AppState): PropertyPaneReduxState =>
state.ui.propertyPane;
@ -23,7 +24,7 @@ export const getCurrentWidgetProperties = createSelector(
widgets: CanvasWidgetsReduxState,
pane: PropertyPaneReduxState,
): WidgetProps | undefined => {
return pane.widgetId && widgets ? widgets[pane.widgetId] : undefined;
return get(widgets, `${pane.widgetId}`);
},
);
@ -39,12 +40,14 @@ export const getWidgetPropsForPropertyPane = createSelector(
widgetId: widget.widgetId,
}) as DataTreeWidget;
const widgetProperties = { ...widget };
if (evaluatedWidget) {
if (evaluatedWidget.evaluatedValues) {
widgetProperties.evaluatedValues = {
...evaluatedWidget.evaluatedValues,
};
}
if (evaluatedWidget.invalidProps) {
const { invalidProps, validationMessages } = evaluatedWidget;
widgetProperties.invalidProps = invalidProps;

View File

@ -22,6 +22,7 @@ class PropertyControlFactory {
preferEditor: boolean,
customEditor?: string,
additionalAutoComplete?: Record<string, Record<string, unknown>>,
hideEvaluatedValue?: boolean,
): JSX.Element {
let controlBuilder = this.controlMap.get(controlData.controlType);
if (preferEditor) {
@ -35,7 +36,9 @@ class PropertyControlFactory {
key: controlData.id,
customJSControl: customEditor,
additionalAutoComplete,
hideEvaluatedValue,
};
const control = controlBuilder.buildPropertyControl(controlProps);
return control;
} else {

View File

@ -643,6 +643,7 @@ export const widgetOperationParams = (
columns: widget.columns,
rows: widget.rows,
};
return {
operation: WidgetOperations.ADD_CHILD,
widgetId: parentWidgetId,

View File

@ -90,6 +90,12 @@ import SkeletonWidget, {
ProfiledSkeletonWidget,
SkeletonWidgetProps,
} from "../widgets/SkeletonWidget";
import ListWidget, {
ListWidgetProps,
ProfiledListWidget,
} from "widgets/ListWidget/ListWidget";
import SwitchWidget, {
ProfiledSwitchWidget,
SwitchWidgetProps,
@ -407,7 +413,18 @@ export default class WidgetBuilderRegistry {
SkeletonWidget.getMetaPropertiesMap(),
SkeletonWidget.getPropertyPaneConfig(),
);
WidgetFactory.registerWidgetBuilder(
WidgetTypes.LIST_WIDGET,
{
buildWidget(widgetProps: ListWidgetProps<WidgetProps>): JSX.Element {
return <ProfiledListWidget {...widgetProps} />;
},
},
ListWidget.getDerivedPropertiesMap(),
ListWidget.getDefaultPropertiesMap(),
ListWidget.getMetaPropertiesMap(),
ListWidget.getPropertyPaneConfig(),
);
WidgetFactory.registerWidgetBuilder(
WidgetTypes.MODAL_WIDGET,
{

View File

@ -0,0 +1,47 @@
import { entityDefinitions } from "utils/autocomplete/EntityDefinitions";
import { WidgetTypes } from "../../constants/WidgetConstants";
describe("EntityDefinitions", () => {
it("it tests list widget selectRow", () => {
const listWidgetProps = {
widgetId: "yolo",
widgetName: "List1",
parentId: "123",
renderMode: "CANVAS",
text: "yo",
type: WidgetTypes.INPUT_WIDGET,
parentColumnSpace: 1,
parentRowSpace: 2,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 2,
isLoading: false,
version: 1,
selectedItem: {
id: 1,
name: "Some random name",
},
};
const listWidgetEntityDefinitions = entityDefinitions.LIST_WIDGET(
listWidgetProps,
);
const output = {
"!doc":
"Containers are used to group widgets together to form logical higher order widgets. Containers let you organize your page better and move all the widgets inside them together.",
"!url": "https://docs.appsmith.com/widget-reference/list",
backgroundColor: {
"!type": "string",
"!url": "https://docs.appsmith.com/widget-reference/how-to-use-widgets",
},
isVisible: {
"!type": "bool",
"!doc": "Boolean value indicating if the widget is in visible state",
},
selectedItem: { id: "number", name: "string" },
};
expect(listWidgetEntityDefinitions).toStrictEqual(output);
});
});

View File

@ -224,6 +224,17 @@ export const entityDefinitions = {
isDisabled: "bool",
uploadedFileUrls: "string",
},
LIST_WIDGET: (widget: any) => ({
"!doc":
"Containers are used to group widgets together to form logical higher order widgets. Containers let you organize your page better and move all the widgets inside them together.",
"!url": "https://docs.appsmith.com/widget-reference/list",
backgroundColor: {
"!type": "string",
"!url": "https://docs.appsmith.com/widget-reference/how-to-use-widgets",
},
isVisible: isVisible,
selectedItem: generateTypeDef(widget.selectedItem),
}),
};
export const GLOBAL_DEFS = {

View File

@ -195,6 +195,16 @@ export const isNameValid = (
);
};
/*
* Filter out empty items from an array
* for e.g - ['Pawan', undefined, 'Hetu'] --> ['Pawan', 'Hetu']
*
* @param array any[]
*/
export const removeFalsyEntries = (arr: any[]): any[] => {
return arr.filter(Boolean);
};
/**
* checks if variable passed is of type string or not
*

View File

@ -165,6 +165,12 @@ abstract class BaseWidget<
return this.getWidgetView();
}
/**
* this function is responsive for making the widget resizable.
* A widget can be made by non-resizable by passing resizeDisabled prop.
*
* @param content
*/
makeResizable(content: ReactNode) {
return (
<ResizableComponent
@ -175,21 +181,37 @@ abstract class BaseWidget<
</ResizableComponent>
);
}
/**
* this functions wraps the widget in a component that shows a setting control at the top right
* which gets shown on hover. A widget can enable/disable this by setting `disablePropertyPane` prop
*
* @param content
* @param showControls
*/
showWidgetName(content: ReactNode, showControls = false) {
return (
<React.Fragment>
<WidgetNameComponent
widgetName={this.props.widgetName}
widgetId={this.props.widgetId}
parentId={this.props.parentId}
type={this.props.type}
showControls={showControls}
/>
<>
{!this.props.disablePropertyPane && (
<WidgetNameComponent
widgetName={this.props.widgetName}
widgetId={this.props.widgetId}
parentId={this.props.parentId}
type={this.props.type}
showControls={showControls}
/>
)}
{content}
</React.Fragment>
</>
);
}
/**
* wraps the widget in a draggable component.
* Note: widget drag can be disabled by setting `dragDisabled` prop to true
*
* @param content
*/
makeDraggable(content: ReactNode) {
return <DraggableComponent {...this.props}>{content}</DraggableComponent>;
}
@ -213,11 +235,12 @@ abstract class BaseWidget<
private getWidgetView(): ReactNode {
let content: ReactNode;
switch (this.props.renderMode) {
case RenderModes.CANVAS:
content = this.getCanvasView();
if (!this.props.detachFromLayout) {
content = this.makeResizable(content);
if (!this.props.resizeDisabled) content = this.makeResizable(content);
content = this.showWidgetName(content);
content = this.makeDraggable(content);
content = this.makePositioned(content);
@ -257,17 +280,22 @@ abstract class BaseWidget<
);
}
/**
* generates styles that positions the widget
*/
private getPositionStyle(): BaseStyle {
const { componentHeight, componentWidth } = this.getComponentDimensions();
return {
positionType: PositionTypes.ABSOLUTE,
componentHeight,
componentWidth,
yPosition:
this.props.topRow * this.props.parentRowSpace + CONTAINER_GRID_PADDING,
this.props.topRow * this.props.parentRowSpace +
(this.props.noContainerOffset ? 0 : CONTAINER_GRID_PADDING),
xPosition:
this.props.leftColumn * this.props.parentColumnSpace +
CONTAINER_GRID_PADDING,
(this.props.noContainerOffset ? 0 : CONTAINER_GRID_PADDING),
xPositionUnit: CSSUnits.PIXEL,
yPositionUnit: CSSUnits.PIXEL,
};
@ -281,6 +309,11 @@ abstract class BaseWidget<
leftColumn: 0,
isLoading: false,
renderMode: RenderModes.CANVAS,
dragDisabled: false,
dropDisabled: false,
isDeletable: true,
resizeDisabled: false,
disablePropertyPane: false,
};
}
@ -328,6 +361,7 @@ export interface WidgetPositionProps extends WidgetRowCols {
// Examples: MainContainer is detached from layout,
// MODAL_WIDGET is also detached from layout.
detachFromLayout?: boolean;
noContainerOffset?: boolean; // This won't offset the child in parent
}
export const WIDGET_STATIC_PROPS = {
@ -345,6 +379,7 @@ export const WIDGET_STATIC_PROPS = {
parentId: true,
renderMode: true,
detachFromLayout: true,
noContainerOffset: false,
};
export interface WidgetDisplayProps {
@ -373,6 +408,7 @@ export interface WidgetCardProps {
type: WidgetType;
key?: string;
widgetCardName: string;
isBeta?: boolean;
}
export const WidgetOperations = {

View File

@ -113,7 +113,9 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
};
}
onButtonClick() {
onButtonClick(e: React.MouseEvent<HTMLElement>) {
e.stopPropagation();
if (this.props.onClick) {
this.setState({
isLoading: true,

View File

@ -6,6 +6,7 @@ import DropTargetComponent from "components/editorComponents/DropTargetComponent
import { getCanvasSnapRows } from "utils/WidgetPropsUtils";
import { getCanvasClassName } from "utils/generators";
import * as Sentry from "@sentry/react";
import WidgetFactory from "utils/WidgetFactory";
class CanvasWidget extends ContainerWidget {
static getPropertyPaneConfig() {
@ -39,12 +40,30 @@ class CanvasWidget extends ContainerWidget {
);
}
renderChildWidget(childWidgetData: WidgetProps): React.ReactNode {
if (!childWidgetData) return null;
// For now, isVisible prop defines whether to render a detached widget
if (childWidgetData.detachFromLayout && !childWidgetData.isVisible) {
return null;
}
const snapSpaces = this.getSnapSpaces();
childWidgetData.parentColumnSpace = snapSpaces.snapColumnSpace;
childWidgetData.parentRowSpace = snapSpaces.snapRowSpace;
if (this.props.noPad) childWidgetData.noContainerOffset = true;
childWidgetData.parentId = this.props.widgetId;
return WidgetFactory.createWidget(childWidgetData, this.props.renderMode);
}
getPageView() {
let height = 0;
const snapRows = getCanvasSnapRows(
this.props.bottomRow,
this.props.canExtend,
);
const height = snapRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
height = snapRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
const style: CSSProperties = {
width: "100%",
height: `${height}px`,
@ -61,6 +80,7 @@ class CanvasWidget extends ContainerWidget {
}
getCanvasView() {
if (this.props.dropDisabled) return this.getPageView();
return this.renderAsDropTarget();
}
}

View File

@ -1,19 +1,18 @@
import React from "react";
import _ from "lodash";
import * as Sentry from "@sentry/react";
import { map, sortBy, compact } from "lodash";
import ContainerComponent, {
ContainerStyle,
} from "components/designSystems/appsmith/ContainerComponent";
import { WidgetType, WidgetTypes } from "constants/WidgetConstants";
import WidgetFactory from "utils/WidgetFactory";
import {
GridDefaults,
CONTAINER_GRID_PADDING,
WIDGET_PADDING,
} from "constants/WidgetConstants";
import WidgetFactory from "utils/WidgetFactory";
import ContainerComponent, {
ContainerStyle,
} from "components/designSystems/appsmith/ContainerComponent";
import { WidgetType, WidgetTypes } from "constants/WidgetConstants";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import * as Sentry from "@sentry/react";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
class ContainerWidget extends BaseWidget<
@ -64,11 +63,15 @@ class ContainerWidget extends BaseWidget<
getSnapSpaces = () => {
const { componentWidth } = this.getComponentDimensions();
const padding = (CONTAINER_GRID_PADDING + WIDGET_PADDING) * 2;
let width = componentWidth;
if (!this.props.noPad) width -= padding;
else width -= WIDGET_PADDING * 2;
return {
snapRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
snapColumnSpace: componentWidth
? (componentWidth - (CONTAINER_GRID_PADDING + WIDGET_PADDING) * 2) /
GridDefaults.DEFAULT_GRID_COLUMNS
? width / GridDefaults.DEFAULT_GRID_COLUMNS
: 0,
};
};
@ -79,24 +82,16 @@ class ContainerWidget extends BaseWidget<
return null;
}
const snapSpaces = this.getSnapSpaces();
const { componentWidth, componentHeight } = this.getComponentDimensions();
if (childWidgetData.type !== WidgetTypes.CANVAS_WIDGET) {
childWidgetData.parentColumnSpace = snapSpaces.snapColumnSpace;
childWidgetData.parentRowSpace = snapSpaces.snapRowSpace;
} else {
// This is for the detached child like the default CANVAS_WIDGET child
childWidgetData.rightColumn = componentWidth;
childWidgetData.bottomRow = this.props.shouldScrollContents
? childWidgetData.bottomRow
: componentHeight;
childWidgetData.minHeight = componentHeight;
childWidgetData.isVisible = this.props.isVisible;
childWidgetData.shouldScrollContents = false;
childWidgetData.canExtend = this.props.shouldScrollContents;
}
childWidgetData.rightColumn = componentWidth;
childWidgetData.bottomRow = this.props.shouldScrollContents
? childWidgetData.bottomRow
: componentHeight;
childWidgetData.minHeight = componentHeight;
childWidgetData.isVisible = this.props.isVisible;
childWidgetData.shouldScrollContents = false;
childWidgetData.canExtend = this.props.shouldScrollContents;
childWidgetData.parentId = this.props.widgetId;
@ -104,11 +99,11 @@ class ContainerWidget extends BaseWidget<
}
renderChildren = () => {
return _.map(
return map(
// sort by row so stacking context is correct
// TODO(abhinav): This is hacky. The stacking context should increase for widgets rendered top to bottom, always.
// Figure out a way in which the stacking context is consistent.
_.sortBy(_.compact(this.props.children), (child) => child.topRow),
sortBy(compact(this.props.children), (child) => child.topRow),
this.renderChildWidget,
);
};
@ -135,6 +130,7 @@ export interface ContainerWidgetProps<T extends WidgetProps>
children?: T[];
containerStyle?: ContainerStyle;
shouldScrollContents?: boolean;
noPad?: boolean;
}
export default ContainerWidget;

View File

@ -0,0 +1,67 @@
import { Color } from "constants/Colors";
import styled from "styled-components";
import React, { RefObject, ReactNode, useMemo } from "react";
import { ListWidgetProps } from "./ListWidget";
import { WidgetProps } from "widgets/BaseWidget";
import { generateClassName, getCanvasClassName } from "utils/generators";
import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
import { getBorderCSSShorthand } from "constants/DefaultTheme";
interface GridComponentProps extends ComponentProps {
children?: ReactNode;
shouldScrollContents?: boolean;
backgroundColor?: Color;
items: Array<Record<string, unknown>>;
hasPagination?: boolean;
}
const GridContainer = styled.div<GridComponentProps>`
height: 100%;
width: 100%;
position: relative;
background: ${(props) => props.backgroundColor};
`;
const ScrollableCanvasWrapper = styled.div<
ListWidgetProps<WidgetProps> & {
ref: RefObject<HTMLDivElement>;
}
>`
width: 100%;
height: 100%;
`;
const ListComponent = (props: GridComponentProps) => {
// using memoized class name
const scrollableCanvasClassName = useMemo(() => {
return `${
props.shouldScrollContents ? `${getCanvasClassName()}` : ""
} ${generateClassName(props.widgetId)}`;
}, [props.widgetId]);
return (
<GridContainer {...props}>
<ScrollableCanvasWrapper className={scrollableCanvasClassName}>
{props.children}
</ScrollableCanvasWrapper>
</GridContainer>
);
};
export const ListComponentEmpty = styled.div`
height: 100%;
width: 100%;
position: relative;
background: white;
display: flex;
align-items: center;
justify-content: center;
font-family: Verdana, sans;
font-size: 10px;
text-anchor: middle;
color: rgb(102, 102, 102);
border: ${(props) => getBorderCSSShorthand(props.theme.borders[2])};
`;
export default ListComponent;

View File

@ -0,0 +1,326 @@
import React from "react";
import Pagination from "rc-pagination";
import styled from "styled-components";
const locale = {
// Options.jsx
items_per_page: "/ page",
jump_to: "Go to",
jump_to_confirm: "confirm",
page: "",
// Pagination.jsx
prev_page: "Previous Page",
next_page: "Next Page",
prev_5: "Previous 5 Pages",
next_5: "Next 5 Pages",
prev_3: "Previous 3 Pages",
next_3: "Next 3 Pages",
};
const StyledPagination = styled(Pagination)<{
disabled?: boolean;
}>`
margin: 0 auto;
padding: 0;
font-size: 14px;
display: flex;
justify-content: center;
position: absolute;
bottom: 4px;
left: 0;
right: 0;
pointer-events: ${(props) => (props.disabled ? "none" : "all")};
opacity: ${(props) => (props.disabled ? "0.4" : "1")};
.rc-pagination::after {
display: block;
clear: both;
height: 0;
overflow: hidden;
visibility: hidden;
content: " ";
}
.rc-pagination-total-text {
display: inline-block;
height: 28px;
margin-right: 8px;
line-height: 26px;
vertical-align: middle;
}
.rc-pagination-item {
display: inline-block;
min-width: 28px;
height: 28px;
margin-right: 8px;
font-family: Arial;
line-height: 26px;
text-align: center;
vertical-align: middle;
list-style: none;
background-color: #ffffff;
border: 1px solid #d9d9d9;
border-radius: 2px;
outline: 0;
cursor: pointer;
user-select: none;
}
.rc-pagination-item a {
display: block;
padding: 0 6px;
color: rgba(0, 0, 0, 0.85);
transition: none;
}
.rc-pagination-item a:hover {
text-decoration: none;
}
.rc-pagination-item:focus,
.rc-pagination-item:hover {
border-color: #1890ff;
transition: all 0.3s;
}
.rc-pagination-item:focus a,
.rc-pagination-item:hover a {
color: #1890ff;
}
.rc-pagination-item-active {
font-weight: 500;
background: #ffffff;
border-color: #1890ff;
}
.rc-pagination-item-active a {
color: #1890ff;
}
.rc-pagination-item-active:focus,
.rc-pagination-item-active:hover {
border-color: #40a9ff;
}
.rc-pagination-item-active:focus a,
.rc-pagination-item-active:hover a {
color: #40a9ff;
}
.rc-pagination-jump-prev,
.rc-pagination-jump-next {
outline: 0;
}
.rc-pagination-jump-prev button,
.rc-pagination-jump-next button {
background: transparent;
border: none;
cursor: pointer;
color: #666;
}
.rc-pagination-jump-prev button:after,
.rc-pagination-jump-next button:after {
display: block;
content: "•••";
}
.rc-pagination-prev,
.rc-pagination-jump-prev,
.rc-pagination-jump-next {
margin-right: 8px;
}
.rc-pagination-prev,
.rc-pagination-next,
.rc-pagination-jump-prev,
.rc-pagination-jump-next {
display: inline-block;
min-width: 28px;
height: 28px;
color: rgba(0, 0, 0, 0.85);
font-family: Arial;
line-height: 28px;
text-align: center;
vertical-align: middle;
list-style: none;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
}
.rc-pagination-prev,
.rc-pagination-next {
outline: 0;
}
.rc-pagination-prev button,
.rc-pagination-next button {
color: rgba(0, 0, 0, 0.85);
cursor: pointer;
user-select: none;
}
.rc-pagination-prev:hover button,
.rc-pagination-next:hover button {
border-color: #40a9ff;
}
.rc-pagination-prev .rc-pagination-item-link,
.rc-pagination-next .rc-pagination-item-link {
display: block;
width: 100%;
height: 100%;
font-size: 12px;
text-align: center;
background-color: #ffffff;
border: 1px solid #d9d9d9;
border-radius: 2px;
outline: none;
transition: all 0.3s;
}
.rc-pagination-prev:focus .rc-pagination-item-link,
.rc-pagination-next:focus .rc-pagination-item-link,
.rc-pagination-prev:hover .rc-pagination-item-link,
.rc-pagination-next:hover .rc-pagination-item-link {
color: #1890ff;
border-color: #1890ff;
}
.rc-pagination-prev button:after {
content: "";
display: block;
}
.rc-pagination-next button:after {
content: "";
display: block;
}
.rc-pagination-disabled,
.rc-pagination-disabled:hover,
.rc-pagination-disabled:focus {
cursor: not-allowed;
}
.rc-pagination-disabled .rc-pagination-item-link,
.rc-pagination-disabled:hover .rc-pagination-item-link,
.rc-pagination-disabled:focus .rc-pagination-item-link {
color: rgba(0, 0, 0, 0.25);
border-color: #d9d9d9;
cursor: not-allowed;
}
.rc-pagination-slash {
margin: 0 10px 0 5px;
}
.rc-pagination-options {
display: inline-block;
margin-left: 16px;
vertical-align: middle;
}
@media all and (-ms-high-contrast: none) {
.rc-pagination-options *::-ms-backdrop,
.rc-pagination-options {
vertical-align: top;
}
}
.rc-pagination-options-size-changer.rc-select {
display: inline-block;
width: auto;
margin-right: 8px;
}
.rc-pagination-options-quick-jumper {
display: inline-block;
height: 28px;
line-height: 28px;
vertical-align: top;
}
.rc-pagination-options-quick-jumper input {
width: 50px;
margin: 0 8px;
}
.rc-pagination-simple .rc-pagination-prev,
.rc-pagination-simple .rc-pagination-next {
height: 24px;
line-height: 24px;
vertical-align: top;
}
.rc-pagination-simple .rc-pagination-prev .rc-pagination-item-link,
.rc-pagination-simple .rc-pagination-next .rc-pagination-item-link {
height: 24px;
background-color: transparent;
border: 0;
}
.rc-pagination-simple .rc-pagination-prev .rc-pagination-item-link::after,
.rc-pagination-simple .rc-pagination-next .rc-pagination-item-link::after {
height: 24px;
line-height: 24px;
}
.rc-pagination-simple .rc-pagination-simple-pager {
display: inline-block;
height: 24px;
margin-right: 8px;
}
.rc-pagination-simple .rc-pagination-simple-pager input {
box-sizing: border-box;
height: 100%;
margin-right: 8px;
padding: 0 6px;
text-align: center;
background-color: #ffffff;
border: 1px solid #d9d9d9;
border-radius: 2px;
outline: none;
transition: border-color 0.3s;
}
.rc-pagination-simple .rc-pagination-simple-pager input:hover {
border-color: #1890ff;
}
.rc-pagination.rc-pagination-disabled {
cursor: not-allowed;
}
.rc-pagination.rc-pagination-disabled .rc-pagination-item {
background: #f5f5f5;
border-color: #d9d9d9;
cursor: not-allowed;
}
.rc-pagination.rc-pagination-disabled .rc-pagination-item a {
color: rgba(0, 0, 0, 0.25);
background: transparent;
border: none;
cursor: not-allowed;
}
.rc-pagination.rc-pagination-disabled .rc-pagination-item-active {
background: #dbdbdb;
border-color: transparent;
}
.rc-pagination.rc-pagination-disabled .rc-pagination-item-active a {
color: #ffffff;
}
.rc-pagination.rc-pagination-disabled .rc-pagination-item-link {
color: rgba(0, 0, 0, 0.25);
background: #f5f5f5;
border-color: #d9d9d9;
cursor: not-allowed;
}
.rc-pagination.rc-pagination-disabled .rc-pagination-item-link-icon {
opacity: 0;
}
.rc-pagination.rc-pagination-disabled .rc-pagination-item-ellipsis {
opacity: 1;
}
@media only screen and (max-width: 992px) {
.rc-pagination-item-after-jump-prev,
.rc-pagination-item-before-jump-next {
display: none;
}
}
@media only screen and (max-width: 576px) {
.rc-pagination-options {
display: none;
}
}
`;
interface ListPaginationProps {
current: number;
total: number;
perPage: number;
disabled?: boolean;
onChange: (page: number) => void;
}
const ListPagination = (props: ListPaginationProps) => {
return (
<StyledPagination
locale={locale}
total={props.total}
current={props.current}
pageSize={props.perPage}
onChange={props.onChange}
disabled={props.disabled}
/>
);
};
export default ListPagination;

View File

@ -0,0 +1,86 @@
import { get } from "lodash";
import { WidgetProps } from "widgets/BaseWidget";
import { ListWidgetProps } from "./ListWidget";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
const PropertyPaneConfig = [
{
sectionName: "General",
children: [
{
helpText: "Takes in an array of objects to display items in the list.",
propertyName: "items",
label: "Items",
controlType: "INPUT_TEXT",
placeholderText: 'Enter [{ "col1": "val1" }]',
inputType: "ARRAY",
isBindProperty: true,
isTriggerProperty: false,
validation: VALIDATION_TYPES.LIST_DATA,
},
{
propertyName: "backgroundColor",
label: "Background",
controlType: "COLOR_PICKER",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
{
propertyName: "itemBackgroundColor",
label: "Item Background",
controlType: "COLOR_PICKER",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
{
helpText: "Spacing between items in Pixels",
placeholderText: "0",
propertyName: "gridGap",
label: "Item Spacing (px)",
controlType: "INPUT_TEXT",
isBindProperty: false,
isTriggerProperty: false,
},
{
propertyName: "isVisible",
label: "Visible",
helpText: "Controls the visibility of the widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
],
},
{
sectionName: "Actions",
children: [
{
helpText: "Triggers an action when a grid list item is clicked",
propertyName: "onListItemClick",
label: "onListItemClick",
controlType: "ACTION_SELECTOR",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: true,
additionalAutoComplete: (props: ListWidgetProps<WidgetProps>) => {
return {
currentItem: Object.assign(
{},
...Object.keys(get(props, "evaluatedValues.items.0", {})).map(
(key) => ({
[key]: "",
}),
),
),
};
},
},
],
},
];
export { PropertyPaneConfig as default };

View File

@ -0,0 +1,76 @@
import React from "react";
import { WidgetProps } from "widgets/BaseWidget";
import ListWidget, { ListWidgetProps } from "./ListWidget";
import configureStore from "redux-mock-store";
import { render } from "@testing-library/react";
import { Provider } from "react-redux";
import { ThemeProvider, theme, dark } from "constants/DefaultTheme";
jest.mock("react-dnd", () => ({
useDrag: jest.fn().mockReturnValue([{ isDragging: false }, jest.fn()]),
}));
describe("<ListWidget />", () => {
const initialState = {
ui: {
widgetDragResize: {
selectedWidget: "Widget1",
},
propertyPane: {
isVisible: true,
widgetId: "Widget1",
},
},
entities: { canvasWidgets: {}, app: { mode: "canvas" } },
};
function renderListWidget(props: Partial<ListWidgetProps<WidgetProps>> = {}) {
const defaultProps: ListWidgetProps<WidgetProps> = {
image: "",
defaultImage: "",
widgetId: "Widget1",
type: "LIST_WIDGET",
widgetName: "List1",
parentId: "Container1",
renderMode: "CANVAS",
parentColumnSpace: 2,
parentRowSpace: 3,
leftColumn: 2,
rightColumn: 3,
topRow: 1,
bottomRow: 3,
isLoading: false,
items: [],
version: 16,
disablePropertyPane: false,
...props,
};
// Mock store to bypass the error of react-redux
const store = configureStore()(initialState);
return render(
<Provider store={store}>
<ThemeProvider
theme={{ ...theme, colors: { ...theme.colors, ...dark } }}
>
<ListWidget {...defaultProps} />
</ThemeProvider>
</Provider>,
);
}
test("should render settings control wrapper", async () => {
const { queryByTestId } = renderListWidget();
expect(
queryByTestId("t--settings-controls-positioned-wrapper"),
).toBeTruthy();
});
test("should not render settings control wrapper", async () => {
const { queryByTestId } = renderListWidget({ widgetId: "ListNew1" });
expect(
queryByTestId("t--settings-controls-positioned-wrapper"),
).toBeFalsy();
});
});

View File

@ -0,0 +1,569 @@
import React from "react";
import log from "loglevel";
import { compact, get, set, xor, isPlainObject, isNumber, round } from "lodash";
import * as Sentry from "@sentry/react";
import WidgetFactory from "utils/WidgetFactory";
import { removeFalsyEntries } from "utils/helpers";
import BaseWidget, { WidgetProps, WidgetState } from "../BaseWidget";
import {
RenderModes,
WidgetType,
WidgetTypes,
} from "constants/WidgetConstants";
import ListComponent, { ListComponentEmpty } from "./ListComponent";
import { ContainerStyle } from "components/designSystems/appsmith/ContainerComponent";
import { ContainerWidgetProps } from "../ContainerWidget";
import propertyPaneConfig from "./ListPropertyPaneConfig";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { getDynamicBindings } from "utils/DynamicBindingUtils";
import ListPagination from "./ListPagination";
import withMeta from "./../MetaHOC";
import { GridDefaults, WIDGET_PADDING } from "constants/WidgetConstants";
class ListWidget extends BaseWidget<ListWidgetProps<WidgetProps>, WidgetState> {
state = {
page: 1,
};
/**
* returns the property pane config of the widget
*/
static getPropertyPaneConfig() {
return propertyPaneConfig;
}
static getDerivedPropertiesMap() {
return {
selectedItem: `{{(()=>{
const selectedItemIndex =
this.selectedItemIndex === undefined ||
Number.isNaN(parseInt(this.selectedItemIndex))
? -1
: parseInt(this.selectedItemIndex);
const items = this.items || [];
if (selectedItemIndex === -1) {
const emptyRow = { ...items[0] };
Object.keys(emptyRow).forEach((key) => {
emptyRow[key] = "";
});
return emptyRow;
}
const selectedItem = { ...items[selectedItemIndex] };
return selectedItem;
})()}}`,
};
}
/**
* creates object of keys
*
* @param items
*/
getCurrentItemStructure = (items: Array<Record<string, unknown>>) => {
return Array.isArray(items) && items.length > 0
? Object.assign(
{},
...Object.keys(items[0]).map((key) => ({
[key]: "",
})),
)
: {};
};
componentDidMount() {
if (
!this.props.childAutoComplete ||
(Object.keys(this.props.childAutoComplete).length === 0 &&
this.props.items &&
Array.isArray(this.props.items))
) {
const structure = this.getCurrentItemStructure(this.props.items);
super.updateWidgetProperty("childAutoComplete", {
currentItem: structure,
});
}
}
componentDidUpdate(prevProps: ListWidgetProps<WidgetProps>) {
const oldRowStructure = this.getCurrentItemStructure(prevProps.items);
const newRowStructure = this.getCurrentItemStructure(this.props.items);
if (
xor(Object.keys(oldRowStructure), Object.keys(newRowStructure)).length > 0
) {
super.updateWidgetProperty("childAutoComplete", {
currentItem: newRowStructure,
});
}
}
static getDefaultPropertiesMap(): Record<string, string> {
return {
itemBackgroundColor: "#FFFFFF",
};
}
/**
* on click item action
*
* @param rowIndex
* @param action
* @param onComplete
*/
onItemClick = (rowIndex: number, action: string | undefined) => {
// setting selectedItemIndex on click of container
const selectedItemIndex = isNumber(this.props.selectedItemIndex)
? this.props.selectedItemIndex
: -1;
if (selectedItemIndex !== rowIndex) {
this.props.updateWidgetMetaProperty("selectedItemIndex", rowIndex, {
dynamicString: this.props.onRowSelected,
event: {
type: EventType.ON_ROW_SELECTED,
},
});
}
if (!action) return;
try {
const rowData = [this.props.items[rowIndex]];
const { jsSnippets } = getDynamicBindings(action);
const modifiedAction = jsSnippets.reduce((prev: string, next: string) => {
return prev + `{{(currentItem) => { ${next} }}} `;
}, "");
super.executeAction({
dynamicString: modifiedAction,
event: {
type: EventType.ON_CLICK,
},
responseData: rowData,
});
} catch (error) {
log.debug("Error parsing row action", error);
}
};
renderChild = (childWidgetData: WidgetProps) => {
const { componentWidth, componentHeight } = this.getComponentDimensions();
childWidgetData.parentId = this.props.widgetId;
childWidgetData.shouldScrollContents = this.props.shouldScrollContents;
childWidgetData.canExtend =
childWidgetData.virtualizedEnabled && false
? true
: this.props.shouldScrollContents;
childWidgetData.isVisible = this.props.isVisible;
childWidgetData.minHeight = componentHeight;
childWidgetData.rightColumn = componentWidth;
childWidgetData.noPad = true;
return WidgetFactory.createWidget(childWidgetData, this.props.renderMode);
};
/**
* here we are updating the position of each items and disabled resizing for
* all items except template ( first item )
*
* @param children
*/
updatePosition = (
children: ContainerWidgetProps<WidgetProps>[],
): ContainerWidgetProps<WidgetProps>[] => {
const gridGap = this.props.gridGap || 0;
return children.map((child: ContainerWidgetProps<WidgetProps>, index) => {
const gap = gridGap - 8;
return {
...child,
gap,
backgroundColor: this.props.itemBackgroundColor,
topRow:
index * children[0].bottomRow +
index * (gap / GridDefaults.DEFAULT_GRID_ROW_HEIGHT),
bottomRow:
(index + 1) * children[0].bottomRow +
index * (gap / GridDefaults.DEFAULT_GRID_ROW_HEIGHT),
resizeDisabled:
index > 0 && this.props.renderMode === RenderModes.CANVAS,
};
});
};
updateTemplateWidgetProperties = (widget: WidgetProps, itemIndex: number) => {
const {
template,
dynamicBindingPathList,
dynamicTriggerPathList,
} = this.props;
const { widgetName = "" } = widget;
// Update properties if they're dynamic
// `template` property should have an array of values
// if it is a dynamicbinding
if (
Array.isArray(dynamicBindingPathList) &&
dynamicBindingPathList.length > 0
) {
// Get all paths in the dynamicBindingPathList sans the List Widget name prefix
const dynamicPaths: string[] = compact(
dynamicBindingPathList.map((path: Record<"key", string>) =>
path.key.split(".").pop(),
),
);
// Update properties in the widget based on the paths
// By picking the correct value from the evaluated values in the template
dynamicPaths.forEach((path: string) => {
const evaluatedProperty = get(template, `${widgetName}.${path}`);
if (
Array.isArray(evaluatedProperty) &&
evaluatedProperty.length > itemIndex
) {
const evaluatedValue = evaluatedProperty[itemIndex];
if (isPlainObject(evaluatedValue))
set(widget, path, JSON.stringify(evaluatedValue));
else set(widget, path, evaluatedValue);
}
});
}
if (
Array.isArray(dynamicTriggerPathList) &&
dynamicTriggerPathList.length > 0
) {
// Get all paths in the dynamicBindingPathList sans the List Widget name prefix
const triggerPaths: string[] = compact(
dynamicTriggerPathList.map((path: Record<"key", string>) =>
path.key.indexOf(`template.${widgetName}`) === 0
? path.key.split(".").pop()
: undefined,
),
);
triggerPaths.forEach((path: string) => {
const propertyValue = get(this.props.template[widget.widgetName], path);
if (
propertyValue.indexOf("currentItem") > -1 &&
propertyValue.indexOf("{{((currentItem) => {") === -1
) {
const { jsSnippets } = getDynamicBindings(propertyValue);
const listItem = this.props.items[itemIndex];
const newPropertyValue = jsSnippets.reduce(
(prev: string, next: string) => {
if (next.indexOf("currentItem") > -1) {
return (
prev +
`{{((currentItem) => { ${next}})(JSON.parse('${JSON.stringify(
listItem,
)}'))}}`
);
}
return prev + `{{${next}}}`;
},
"",
);
set(widget, path, newPropertyValue);
}
});
}
return this.updateNonTemplateWidgetProperties(widget, itemIndex);
};
updateNonTemplateWidgetProperties = (
widget: WidgetProps,
itemIndex: number,
) => {
const { page } = this.state;
const { perPage } = this.shouldPaginate();
if (itemIndex > 0) {
const originalIndex = ((page - 1) * perPage - itemIndex) * -1;
if (this.props.renderMode === RenderModes.PAGE) {
set(
widget,
`widgetId`,
`list-widget-child-id-${itemIndex}-${widget.widgetName}`,
);
}
if (originalIndex !== 0) {
set(
widget,
`widgetId`,
`list-widget-child-id-${itemIndex}-${widget.widgetName}`,
);
if (this.props.renderMode === RenderModes.CANVAS) {
set(widget, `resizeDisabled`, true);
set(widget, `disablePropertyPane`, true);
set(widget, `dragDisabled`, true);
set(widget, `dropDisabled`, true);
}
}
}
return widget;
};
/**
* @param children
*/
useNewValues = (children: ContainerWidgetProps<WidgetProps>[]) => {
const updatedChildren = children.map(
(
listItemContainer: ContainerWidgetProps<WidgetProps>,
listItemIndex: number,
) => {
let updatedListItemContainer = listItemContainer;
// Get an array of children in the current list item
const listItemChildren = get(
updatedListItemContainer,
"children[0].children",
[],
);
// If children exist
if (listItemChildren.length > 0) {
// Update the properties of all the children
const updatedListItemChildren = listItemChildren.map(
(templateWidget: WidgetProps) => {
// This will return the updated child widget
return this.updateTemplateWidgetProperties(
templateWidget,
listItemIndex,
);
},
);
// Set the update list of children as the new children for the current list item
set(
updatedListItemContainer,
"children[0].children",
updatedListItemChildren,
);
}
// Get the item container's canvas child widget
const listItemContainerCanvas = get(
updatedListItemContainer,
"children[0]",
);
// Set properties of the container's canvas child widget
const updatedListItemContainerCanvas = this.updateNonTemplateWidgetProperties(
listItemContainerCanvas,
listItemIndex,
);
// Set the item container's canvas child widget
set(
updatedListItemContainer,
"children[0]",
updatedListItemContainerCanvas,
);
// Set properties of the item container
updatedListItemContainer = this.updateNonTemplateWidgetProperties(
listItemContainer,
listItemIndex,
);
return updatedListItemContainer;
},
);
return updatedChildren;
};
updateGridChildrenProps = (children: ContainerWidgetProps<WidgetProps>[]) => {
let updatedChildren = this.useNewValues(children);
updatedChildren = this.updateActions(updatedChildren);
updatedChildren = this.paginateItems(updatedChildren);
updatedChildren = this.updatePosition(updatedChildren);
return updatedChildren;
};
updateActions = (children: ContainerWidgetProps<WidgetProps>[]) => {
return children.map((child: ContainerWidgetProps<WidgetProps>, index) => {
return {
...child,
onClick: () => this.onItemClick(index, this.props.onListItemClick),
};
});
};
/**
* paginate items
*
* @param children
*/
paginateItems = (children: ContainerWidgetProps<WidgetProps>[]) => {
const { page } = this.state;
const { shouldPaginate, perPage } = this.shouldPaginate();
if (shouldPaginate) {
return children.slice((page - 1) * perPage, page * perPage);
}
return children;
};
// {
// list: {
// children: [ <--- children
// {
// canvas: { <--- childCanvas
// children: [ <---- canvasChildren
// {
// container: {
// children: [
// 0: {
// canvas: [
// {
// button
// image
// }
// ]
// },
// 1: {
// canvas: [
// {
// button
// image
// }
// ]
// }
// ]
// }
// }
// ]
// }
// }
// ]
// }
// }
/**
* renders children
*/
renderChildren = () => {
const numberOfItemsInGrid = this.props.items.length;
if (this.props.children && this.props.children.length > 0) {
const children = removeFalsyEntries(this.props.children);
const childCanvas = children[0];
let canvasChildren = childCanvas.children;
try {
// here we are duplicating the template for each items in the data array
// first item of the canvasChildren acts as a template
const template = canvasChildren.slice(0, 1).shift();
for (let i = 0; i < numberOfItemsInGrid; i++) {
canvasChildren[i] = JSON.parse(JSON.stringify(template));
}
// TODO(pawan): This is recalculated everytime for not much reason
// We should either use https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
// Or use memoization https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization
// In particular useNewValues can be memoized, if others can't.
canvasChildren = this.updateGridChildrenProps(canvasChildren);
childCanvas.children = canvasChildren;
} catch (e) {
console.log({ error: e });
}
return this.renderChild(childCanvas);
}
};
/**
* 400
* 200
* can data be paginated
*/
shouldPaginate = () => {
let { gridGap } = this.props;
const { items, children } = this.props;
const { componentHeight } = this.getComponentDimensions();
const templateBottomRow = get(children, "0.children.0.bottomRow");
const templateHeight = templateBottomRow * 40;
try {
gridGap = parseInt(gridGap);
if (!isNumber(gridGap) || isNaN(gridGap)) {
gridGap = 0;
}
} catch {
gridGap = 0;
}
const shouldPaginate =
templateHeight * items.length + parseInt(gridGap) * (items.length - 1) >
componentHeight;
const totalSpaceAvailable = componentHeight - (100 + WIDGET_PADDING * 2);
const spaceTakenByOneContainer =
templateHeight + (gridGap * (items.length - 1)) / items.length;
const perPage = totalSpaceAvailable / spaceTakenByOneContainer;
return { shouldPaginate, perPage: round(perPage) };
};
/**
* view that is rendered in editor
*/
getPageView() {
const children = this.renderChildren();
const { shouldPaginate, perPage } = this.shouldPaginate();
if (!isNumber(perPage) || perPage === 0) {
return (
<>Please make sure the list widget size is greater than the template</>
);
}
if (Array.isArray(this.props.items) && this.props.items.length === 0) {
return <ListComponentEmpty>No data to display</ListComponentEmpty>;
}
return (
<ListComponent {...this.props} hasPagination={shouldPaginate}>
{children}
{shouldPaginate && (
<ListPagination
total={this.props.items.length}
current={this.state.page}
perPage={perPage}
onChange={(page: number) => this.setState({ page })}
disabled={false && this.props.renderMode === RenderModes.CANVAS}
/>
)}
</ListComponent>
);
}
/**
* returns type of the widget
*/
getWidgetType(): WidgetType {
return WidgetTypes.LIST_WIDGET;
}
}
export interface ListWidgetProps<T extends WidgetProps> extends WidgetProps {
children?: T[];
containerStyle?: ContainerStyle;
shouldScrollContents?: boolean;
onListItemClick?: string;
items: Array<Record<string, unknown>>;
currentItemStructure?: Record<string, string>;
}
export default ListWidget;
export const ProfiledListWidget = Sentry.withProfiler(withMeta(ListWidget));

View File

@ -0,0 +1 @@
export { ProfiledListWidget, default } from "./ListWidget";

View File

@ -538,3 +538,66 @@ describe("validateDateString test", () => {
});
});
});
describe("List data validator", () => {
const validator = VALIDATORS.LIST_DATA;
it("correctly validates ", () => {
const cases = [
{
input: [],
output: {
isValid: true,
parsed: [],
},
},
{
input: [{ a: 1 }],
output: {
isValid: true,
parsed: [{ a: 1 }],
},
},
{
input: "sting text",
output: {
isValid: false,
message:
'Value does not match type: [{ "key1" : "val1", "key2" : "val2" }]',
parsed: [],
transformed: "sting text",
},
},
{
input: undefined,
output: {
isValid: false,
message:
'Value does not match type: [{ "key1" : "val1", "key2" : "val2" }]',
parsed: [],
transformed: undefined,
},
},
{
input: {},
output: {
isValid: false,
message:
'Value does not match type: [{ "key1" : "val1", "key2" : "val2" }]',
parsed: [],
transformed: {},
},
},
{
input: `[{ "b": 1 }]`,
output: {
isValid: true,
parsed: JSON.parse(`[{ "b": 1 }]`),
},
},
];
for (const testCase of cases) {
const response = validator(testCase.input, DUMMY_WIDGET, {});
expect(response).toStrictEqual(testCase.output);
}
});
});

View File

@ -211,7 +211,6 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
}
return { isValid: true, parsed, transformed: parsed };
} catch (e) {
console.error(e);
return {
isValid: false,
parsed: [],
@ -259,6 +258,44 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
}
return { isValid, parsed };
},
[VALIDATION_TYPES.LIST_DATA]: (
value: any,
props: WidgetProps,
dataTree?: DataTree,
): ValidationResponse => {
const { isValid, transformed, parsed } = VALIDATORS.ARRAY(
value,
props,
dataTree,
);
if (!isValid) {
return {
isValid,
parsed: [],
transformed,
message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "key1" : "val1", "key2" : "val2" }]`,
};
}
const isValidListData = every(parsed, (datum) => {
return (
isObject(datum) &&
Object.keys(datum).filter((key) => isString(key) && key.length === 0)
.length === 0
);
});
if (!isValidListData) {
return {
isValid: false,
parsed: [],
transformed,
message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "key1" : "val1", "key2" : "val2" }]`,
};
}
return { isValid, parsed };
},
[VALIDATION_TYPES.TABLE_DATA]: (
value: any,
props: WidgetProps,
@ -468,7 +505,6 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
}
return { isValid, parsed };
} catch (e) {
console.error(e);
return {
isValid: false,
parsed: [],

View File

@ -1624,7 +1624,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.5":
"@babel/runtime@^7.10.1", "@babel/runtime@^7.12.5":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
@ -3424,10 +3424,10 @@
lodash "^4.17.15"
redent "^3.0.0"
"@testing-library/react@^11.2.5":
version "11.2.5"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.5.tgz#ae1c36a66c7790ddb6662c416c27863d87818eb9"
integrity sha512-yEx7oIa/UWLe2F2dqK0FtMF9sJWNXD+2PPtp39BvE0Kh9MJ9Kl0HrZAgEuhUJR+Lx8Di6Xz+rKwSdEPY2UV8ZQ==
"@testing-library/react@^11.2.6":
version "11.2.6"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b"
integrity sha512-TXMCg0jT8xmuU8BkKMtp8l7Z50Ykew5WNX8UoIKTaLFwKkP2+1YDhOLA2Ga3wY4x29jyntk7EWfum0kjlYiSjQ==
dependencies:
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^7.28.1"
@ -4002,7 +4002,21 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^4.5.0", "@typescript-eslint/eslint-plugin@^4.6.0":
"@typescript-eslint/eslint-plugin@^4.15.0":
version "4.15.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.1.tgz#835f64aa0a403e5e9e64c10ceaf8d05c3f015180"
integrity sha512-yW2epMYZSpNJXZy22Biu+fLdTG8Mn6b22kR3TqblVk50HGNV8Zya15WAXuQCr8tKw4Qf1BL4QtI6kv6PCkLoJw==
dependencies:
"@typescript-eslint/experimental-utils" "4.15.1"
"@typescript-eslint/scope-manager" "4.15.1"
debug "^4.1.1"
functional-red-black-tree "^1.0.1"
lodash "^4.17.15"
regexpp "^3.0.0"
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/eslint-plugin@^4.5.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.6.0.tgz#210cd538bb703f883aff81d3996961f5dba31fdb"
dependencies:
@ -4014,6 +4028,18 @@
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/experimental-utils@4.15.1":
version "4.15.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.1.tgz#d744d1ac40570a84b447f7aa1b526368afd17eec"
integrity sha512-9LQRmOzBRI1iOdJorr4jEnQhadxK4c9R2aEAsm7WE/7dq8wkKD1suaV0S/JucTL8QlYUPU1y2yjqg+aGC0IQBQ==
dependencies:
"@types/json-schema" "^7.0.3"
"@typescript-eslint/scope-manager" "4.15.1"
"@typescript-eslint/types" "4.15.1"
"@typescript-eslint/typescript-estree" "4.15.1"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
"@typescript-eslint/experimental-utils@4.6.0", "@typescript-eslint/experimental-utils@^4.0.1":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.6.0.tgz#f750aef4dd8e5970b5c36084f0a5ca2f0db309a4"
@ -4035,7 +4061,17 @@
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
"@typescript-eslint/parser@^4.5.0", "@typescript-eslint/parser@^4.6.0":
"@typescript-eslint/parser@^4.15.0":
version "4.15.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.15.1.tgz#4c91a0602733db63507e1dbf13187d6c71a153c4"
integrity sha512-V8eXYxNJ9QmXi5ETDguB7O9diAXlIyS+e3xzLoP/oVE4WCAjssxLIa0mqCLsCGXulYJUfT+GV70Jv1vHsdKwtA==
dependencies:
"@typescript-eslint/scope-manager" "4.15.1"
"@typescript-eslint/types" "4.15.1"
"@typescript-eslint/typescript-estree" "4.15.1"
debug "^4.1.1"
"@typescript-eslint/parser@^4.5.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.6.0.tgz#7e9ff7df2f21d5c8f65f17add3b99eeeec33199d"
dependencies:
@ -4044,6 +4080,14 @@
"@typescript-eslint/typescript-estree" "4.6.0"
debug "^4.1.1"
"@typescript-eslint/scope-manager@4.15.1":
version "4.15.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.15.1.tgz#f6511eb38def2a8a6be600c530c243bbb56ac135"
integrity sha512-ibQrTFcAm7yG4C1iwpIYK7vDnFg+fKaZVfvyOm3sNsGAerKfwPVFtYft5EbjzByDJ4dj1WD8/34REJfw/9wdVA==
dependencies:
"@typescript-eslint/types" "4.15.1"
"@typescript-eslint/visitor-keys" "4.15.1"
"@typescript-eslint/scope-manager@4.6.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.6.0.tgz#b7d8b57fe354047a72dfb31881d9643092838662"
@ -4055,6 +4099,11 @@
version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727"
"@typescript-eslint/types@4.15.1":
version "4.15.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.15.1.tgz#da702f544ef1afae4bc98da699eaecd49cf31c8c"
integrity sha512-iGsaUyWFyLz0mHfXhX4zO6P7O3sExQpBJ2dgXB0G5g/8PRVfBBsmQIc3r83ranEQTALLR3Vko/fnCIVqmH+mPw==
"@typescript-eslint/types@4.6.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.6.0.tgz#157ca925637fd53c193c6bf226a6c02b752dde2f"
@ -4072,6 +4121,19 @@
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/typescript-estree@4.15.1":
version "4.15.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.1.tgz#fa9a9ff88b4a04d901ddbe5b248bc0a00cd610be"
integrity sha512-z8MN3CicTEumrWAEB2e2CcoZa3KP9+SMYLIA2aM49XW3cWIaiVSOAGq30ffR5XHxRirqE90fgLw3e6WmNx5uNw==
dependencies:
"@typescript-eslint/types" "4.15.1"
"@typescript-eslint/visitor-keys" "4.15.1"
debug "^4.1.1"
globby "^11.0.1"
is-glob "^4.0.1"
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/typescript-estree@4.6.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.6.0.tgz#85bd98dcc8280511cfc5b2ce7b03a9ffa1732b08"
@ -4091,6 +4153,14 @@
dependencies:
eslint-visitor-keys "^1.1.0"
"@typescript-eslint/visitor-keys@4.15.1":
version "4.15.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.1.tgz#c76abbf2a3be8a70ed760f0e5756bf62de5865dd"
integrity sha512-tYzaTP9plooRJY8eNlpAewTOqtWW/4ff/5wBjNVaJ0S0wC4Gpq/zDVRTJa5bq2v1pCNQ08xxMCndcvR+h7lMww==
dependencies:
"@typescript-eslint/types" "4.15.1"
eslint-visitor-keys "^2.0.0"
"@typescript-eslint/visitor-keys@4.6.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.6.0.tgz#fb05d6393891b0a089b243fc8f9fb8039383d5da"
@ -6006,7 +6076,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2, classnames@^2.2.5, classnames@^2.2.6:
classnames@^2.2, classnames@^2.2.1, classnames@^2.2.5, classnames@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
@ -13773,6 +13843,14 @@ raw-loader@^4.0.2:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
rc-pagination@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.1.3.tgz#afd779839fefab2cb14248d5e7b74027960bb48b"
integrity sha512-Z7CdC4xGkedfAwcUHPtfqNhYwVyDgkmhkvfsmoByCOwAd89p42t5O5T3ORar1wRmVWf3jxk/Bf4k0atenNvlFA==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.1"
re-reselect@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-3.4.0.tgz#0f2303f3c84394f57f0cd31fea08a1ca4840a7cd"
@ -16503,6 +16581,11 @@ tslib@^2.0.0, tslib@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c"
tslib@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tslib@~1.13.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
@ -16604,9 +16687,10 @@ typescript-tuple@^2.2.1:
dependencies:
typescript-compare "^0.0.2"
typescript@^3.9.2:
version "3.9.7"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
typescript@^4.1.3:
version "4.1.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72"
integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==
ua-parser-js@^0.7.18:
version "0.7.22"