[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:
parent
2eec2fba28
commit
1717b0e392
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -2,6 +2,7 @@
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
.env
|
.env
|
||||||
|
.vscode/*
|
||||||
|
|
||||||
# test coverage
|
# test coverage
|
||||||
coverage-summary.json
|
coverage-summary.json
|
||||||
|
|
|
||||||
2
app/client/.gitignore
vendored
2
app/client/.gitignore
vendored
|
|
@ -43,3 +43,5 @@ storybook-static/*
|
||||||
build-storybook.log
|
build-storybook.log
|
||||||
|
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
.vscode
|
||||||
|
TODO
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
"env": {
|
"env": {
|
||||||
"cypress/globals": true
|
"cypress/globals": true
|
||||||
},
|
},
|
||||||
|
"rules": {
|
||||||
|
"cypress/no-unnecessary-waiting": 0
|
||||||
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:cypress/recommended"
|
"plugin:cypress/recommended"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
161
app/client/cypress/fixtures/listdsl.json
Normal file
161
app/client/cypress/fixtures/listdsl.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -105,6 +105,8 @@
|
||||||
"globalSearchInput": ".t--global-search-input",
|
"globalSearchInput": ".t--global-search-input",
|
||||||
"globalSearchTrigger": ".t--global-search-modal-trigger",
|
"globalSearchTrigger": ".t--global-search-modal-trigger",
|
||||||
"globalSearchClearInput": ".t--global-clear-input",
|
"globalSearchClearInput": ".t--global-clear-input",
|
||||||
|
"containerWidget": ".t--widget-containerwidget",
|
||||||
|
"paginationButton": ".rc-pagination-item",
|
||||||
"switchWidgetActive": ".t--switch-widget-active",
|
"switchWidgetActive": ".t--switch-widget-active",
|
||||||
"switchWidgetInActive": ".t--switch-widget-inactive",
|
"switchWidgetInActive": ".t--switch-widget-inactive",
|
||||||
"switchWidgetLoading": ".t--switch-widget-loading"
|
"switchWidgetLoading": ".t--switch-widget-loading"
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,11 @@ describe("Table functionality ", function() {
|
||||||
// Navigate to add background colour and Text colour
|
// Navigate to add background colour and Text colour
|
||||||
// Ensure the row colour gets overlapped on table colour
|
// Ensure the row colour gets overlapped on table colour
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Collapse the tabs of Property pane", function() {
|
it("Collapse the tabs of Property pane", function() {
|
||||||
// Add a table
|
// Add a table
|
||||||
// Click on the property pane
|
// Click on the property pane
|
||||||
// Collapse the General ,Action and Tab option
|
// Collapse the General ,Action and Tab option
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Bind the column with same name", function() {
|
it("Bind the column with same name", function() {
|
||||||
// Add a table
|
// Add a table
|
||||||
// Click on the property pane
|
// Click on the property pane
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@
|
||||||
"popper.js": "^1.15.0",
|
"popper.js": "^1.15.0",
|
||||||
"prettier": "^1.18.2",
|
"prettier": "^1.18.2",
|
||||||
"prismjs": "^1.23.0",
|
"prismjs": "^1.23.0",
|
||||||
|
"rc-pagination": "^3.1.3",
|
||||||
"re-reselect": "^3.4.0",
|
"re-reselect": "^3.4.0",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-base-table": "^1.9.1",
|
"react-base-table": "^1.9.1",
|
||||||
|
|
@ -129,7 +130,8 @@
|
||||||
"tinycolor2": "^1.4.1",
|
"tinycolor2": "^1.4.1",
|
||||||
"toposort": "^2.0.2",
|
"toposort": "^2.0.2",
|
||||||
"ts-loader": "^6.0.4",
|
"ts-loader": "^6.0.4",
|
||||||
"typescript": "^3.9.2",
|
"tslib": "^2.1.0",
|
||||||
|
"typescript": "^4.1.3",
|
||||||
"unescape-js": "^1.1.4",
|
"unescape-js": "^1.1.4",
|
||||||
"url-search-params-polyfill": "^8.0.0",
|
"url-search-params-polyfill": "^8.0.0",
|
||||||
"worker-loader": "^3.0.2"
|
"worker-loader": "^3.0.2"
|
||||||
|
|
@ -176,7 +178,7 @@
|
||||||
"@storybook/preset-create-react-app": "^3.1.4",
|
"@storybook/preset-create-react-app": "^3.1.4",
|
||||||
"@storybook/react": "^5.3.19",
|
"@storybook/react": "^5.3.19",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@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",
|
"@testing-library/user-event": "^13.1.1",
|
||||||
"@types/codemirror": "^0.0.96",
|
"@types/codemirror": "^0.0.96",
|
||||||
"@types/deep-diff": "^1.0.0",
|
"@types/deep-diff": "^1.0.0",
|
||||||
|
|
@ -193,8 +195,8 @@
|
||||||
"@types/styled-system": "^5.1.9",
|
"@types/styled-system": "^5.1.9",
|
||||||
"@types/tern": "0.22.0",
|
"@types/tern": "0.22.0",
|
||||||
"@types/toposort": "^2.0.3",
|
"@types/toposort": "^2.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
"@typescript-eslint/eslint-plugin": "^4.15.0",
|
||||||
"@typescript-eslint/parser": "^4.6.0",
|
"@typescript-eslint/parser": "^4.15.0",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"babel-plugin-styled-components": "^1.10.7",
|
"babel-plugin-styled-components": "^1.10.7",
|
||||||
"craco-babel-loader": "^0.1.4",
|
"craco-babel-loader": "^0.1.4",
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export const updateWidgetPropertyRequest = (
|
||||||
export interface BatchPropertyUpdatePayload {
|
export interface BatchPropertyUpdatePayload {
|
||||||
modify?: Record<string, unknown>; //Key value pairs of paths and values to update
|
modify?: Record<string, unknown>; //Key value pairs of paths and values to update
|
||||||
remove?: string[]; //Array of paths to delete
|
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 = (
|
export const batchUpdateWidgetProperty = (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { FetchPageRequest, PageLayout, SavePageResponse } from "api/PageApi";
|
|
||||||
import { WidgetOperation } from "widgets/BaseWidget";
|
|
||||||
import { WidgetType } from "constants/WidgetConstants";
|
import { WidgetType } from "constants/WidgetConstants";
|
||||||
import {
|
import {
|
||||||
EvaluationReduxAction,
|
EvaluationReduxAction,
|
||||||
|
|
@ -7,9 +5,11 @@ import {
|
||||||
ReduxActionTypes,
|
ReduxActionTypes,
|
||||||
UpdateCanvasPayload,
|
UpdateCanvasPayload,
|
||||||
} from "constants/ReduxActionConstants";
|
} from "constants/ReduxActionConstants";
|
||||||
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
|
||||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
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 { APP_MODE, UrlDataState } from "reducers/entityReducers/appReducer";
|
||||||
|
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||||
|
|
||||||
export interface FetchPageListPayload {
|
export interface FetchPageListPayload {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
|
|
||||||
11
app/client/src/actions/propertyPaneActions.test.ts
Normal file
11
app/client/src/actions/propertyPaneActions.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -8,3 +8,9 @@ export const updateWidgetName = (widgetId: string, newName: string) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hidePropertyPane = () => {
|
||||||
|
return {
|
||||||
|
type: ReduxActionTypes.HIDE_PROPERTY_PANE,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
||||||
3
app/client/src/assets/icons/widget/list.svg
Normal file
3
app/client/src/assets/icons/widget/list.svg
Normal 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 |
|
|
@ -25,6 +25,8 @@ const StyledContainerComponent = styled.div<
|
||||||
background: ${(props) => props.backgroundColor};
|
background: ${(props) => props.backgroundColor};
|
||||||
|
|
||||||
${(props) => (!props.isVisible ? invisible : "")};
|
${(props) => (!props.isVisible ? invisible : "")};
|
||||||
|
opacity: ${(props) => (props.resizeDisabled ? "0.5" : "1")};
|
||||||
|
pointer-events: ${(props) => (props.resizeDisabled ? "none" : "inherit")};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
${(props) => (props.shouldScrollContents ? scrollContents : "")}
|
${(props) => (props.shouldScrollContents ? scrollContents : "")}
|
||||||
}`;
|
}`;
|
||||||
|
|
@ -32,6 +34,7 @@ const StyledContainerComponent = styled.div<
|
||||||
const ContainerComponent = (props: ContainerComponentProps) => {
|
const ContainerComponent = (props: ContainerComponentProps) => {
|
||||||
const containerStyle = props.containerStyle || "card";
|
const containerStyle = props.containerStyle || "card";
|
||||||
const containerRef: RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
|
const containerRef: RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.shouldScrollContents) {
|
if (!props.shouldScrollContents) {
|
||||||
const supportsNativeSmoothScroll =
|
const supportsNativeSmoothScroll =
|
||||||
|
|
@ -69,6 +72,7 @@ export interface ContainerComponentProps extends ComponentProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
backgroundColor?: Color;
|
backgroundColor?: Color;
|
||||||
shouldScrollContents?: boolean;
|
shouldScrollContents?: boolean;
|
||||||
|
resizeDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContainerComponent;
|
export default ContainerComponent;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { ReactNode } from "react";
|
import React, { CSSProperties, ReactNode, useMemo } from "react";
|
||||||
import { BaseStyle } from "widgets/BaseWidget";
|
import { BaseStyle } from "widgets/BaseWidget";
|
||||||
import { WIDGET_PADDING } from "constants/WidgetConstants";
|
import { WIDGET_PADDING } from "constants/WidgetConstants";
|
||||||
import { generateClassName } from "utils/generators";
|
import { generateClassName } from "utils/generators";
|
||||||
|
|
@ -23,27 +23,35 @@ export const PositionedContainer = (props: PositionedContainerProps) => {
|
||||||
const padding = WIDGET_PADDING;
|
const padding = WIDGET_PADDING;
|
||||||
const openPropertyPane = useClickOpenPropPane();
|
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 (
|
return (
|
||||||
<PositionedWidget
|
<PositionedWidget
|
||||||
onClickCapture={openPropertyPane}
|
onClickCapture={openPropertyPane}
|
||||||
style={{
|
style={containerStyle}
|
||||||
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",
|
|
||||||
}}
|
|
||||||
id={props.widgetId}
|
id={props.widgetId}
|
||||||
//Before you remove: This is used by property pane to reference the element
|
//Before you remove: This is used by property pane to reference the element
|
||||||
className={
|
className={containerClassName}
|
||||||
generateClassName(props.widgetId) +
|
|
||||||
" " +
|
|
||||||
`t--widget-${props.widgetType
|
|
||||||
.split("_")
|
|
||||||
.join("")
|
|
||||||
.toLowerCase()}`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</PositionedWidget>
|
</PositionedWidget>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -106,6 +106,7 @@ interface Props {
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
error?: string;
|
error?: string;
|
||||||
useValidationMessage?: boolean;
|
useValidationMessage?: boolean;
|
||||||
|
hideEvaluatedValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PopoverContentProps {
|
interface PopoverContentProps {
|
||||||
|
|
@ -117,6 +118,7 @@ interface PopoverContentProps {
|
||||||
theme: EditorTheme;
|
theme: EditorTheme;
|
||||||
onMouseEnter: () => void;
|
onMouseEnter: () => void;
|
||||||
onMouseLeave: () => void;
|
onMouseLeave: () => void;
|
||||||
|
hideEvaluatedValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CurrentValueViewer = (props: {
|
export const CurrentValueViewer = (props: {
|
||||||
|
|
@ -164,7 +166,11 @@ export const CurrentValueViewer = (props: {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!props.hideLabel && <StyledTitle>Evaluated Value</StyledTitle>}
|
{!props.hideLabel && (
|
||||||
|
<StyledTitle data-testid="evaluated-value-popup-title">
|
||||||
|
Evaluated Value
|
||||||
|
</StyledTitle>
|
||||||
|
)}
|
||||||
<CurrentValueWrapper colorTheme={props.theme}>
|
<CurrentValueWrapper colorTheme={props.theme}>
|
||||||
<>
|
<>
|
||||||
{content}
|
{content}
|
||||||
|
|
@ -200,10 +206,12 @@ const PopoverContent = (props: PopoverContentProps) => {
|
||||||
</TypeText>
|
</TypeText>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
<CurrentValueViewer
|
{!props.hideEvaluatedValue && (
|
||||||
theme={props.theme}
|
<CurrentValueViewer
|
||||||
evaluatedValue={props.evaluatedValue}
|
theme={props.theme}
|
||||||
/>
|
evaluatedValue={props.evaluatedValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ContentWrapper>
|
</ContentWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -242,6 +250,7 @@ const EvaluatedValuePopup = (props: Props) => {
|
||||||
useValidationMessage={props.useValidationMessage}
|
useValidationMessage={props.useValidationMessage}
|
||||||
hasError={props.hasError}
|
hasError={props.hasError}
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
hideEvaluatedValue={props.hideEvaluatedValue}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setContentHovered(false);
|
setContentHovered(false);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ export type EditorProps = EditorStyleProps &
|
||||||
} & {
|
} & {
|
||||||
additionalDynamicData?: Record<string, Record<string, unknown>>;
|
additionalDynamicData?: Record<string, Record<string, unknown>>;
|
||||||
promptMessage?: React.ReactNode | string;
|
promptMessage?: React.ReactNode | string;
|
||||||
|
hideEvaluatedValue?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = ReduxStateProps & EditorProps;
|
type Props = ReduxStateProps & EditorProps;
|
||||||
|
|
@ -350,6 +351,7 @@ class CodeEditor extends Component<Props, State> {
|
||||||
hoverInteraction,
|
hoverInteraction,
|
||||||
fill,
|
fill,
|
||||||
useValidationMessage,
|
useValidationMessage,
|
||||||
|
hideEvaluatedValue,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const hasError = !!(meta && meta.error);
|
const hasError = !!(meta && meta.error);
|
||||||
let evaluated = evaluatedValue;
|
let evaluated = evaluatedValue;
|
||||||
|
|
@ -395,6 +397,7 @@ class CodeEditor extends Component<Props, State> {
|
||||||
hasError={hasError}
|
hasError={hasError}
|
||||||
error={meta?.error}
|
error={meta?.error}
|
||||||
useValidationMessage={useValidationMessage}
|
useValidationMessage={useValidationMessage}
|
||||||
|
hideEvaluatedValue={hideEvaluatedValue}
|
||||||
>
|
>
|
||||||
<EditorWrapper
|
<EditorWrapper
|
||||||
editorTheme={this.props.theme}
|
editorTheme={this.props.theme}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -12,16 +12,17 @@ import { getNearestParentCanvas } from "utils/generators";
|
||||||
const WrappedDragLayer = styled.div<{
|
const WrappedDragLayer = styled.div<{
|
||||||
columnWidth: number;
|
columnWidth: number;
|
||||||
rowHeight: number;
|
rowHeight: number;
|
||||||
|
noPad: boolean;
|
||||||
ref: RefObject<HTMLDivElement>;
|
ref: RefObject<HTMLDivElement>;
|
||||||
}>`
|
}>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
left: 0;
|
left: ${(props) => (props.noPad ? "0" : `${CONTAINER_GRID_PADDING}px;`)};
|
||||||
top: 0;
|
top: ${(props) => (props.noPad ? "0" : `${CONTAINER_GRID_PADDING}px;`)};
|
||||||
left: ${CONTAINER_GRID_PADDING}px;
|
height: ${(props) =>
|
||||||
top: ${CONTAINER_GRID_PADDING}px;
|
props.noPad ? `100%` : `calc(100% - ${CONTAINER_GRID_PADDING}px)`};
|
||||||
height: calc(100% - ${CONTAINER_GRID_PADDING}px);
|
width: ${(props) =>
|
||||||
width: calc(100% - ${CONTAINER_GRID_PADDING}px);
|
props.noPad ? `100%` : `calc(100% - ${CONTAINER_GRID_PADDING}px)`};
|
||||||
|
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(
|
||||||
circle,
|
circle,
|
||||||
|
|
@ -47,6 +48,7 @@ type DragLayerProps = {
|
||||||
isResizing?: boolean;
|
isResizing?: boolean;
|
||||||
parentWidgetId: string;
|
parentWidgetId: string;
|
||||||
force: boolean;
|
force: boolean;
|
||||||
|
noPad: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DragLayerComponent = (props: DragLayerProps) => {
|
const DragLayerComponent = (props: DragLayerProps) => {
|
||||||
|
|
@ -133,9 +135,9 @@ const DragLayerComponent = (props: DragLayerProps) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
When the parent offsets are not updated, we don't need to show the dropzone, as the dropzone
|
When the parent offsets are not updated, we don't need to show the dropzone, as the dropzone
|
||||||
will be rendered at an incorrect coordinates.
|
will be rendered at an incorrect coordinates.
|
||||||
We can be sure that the parent offset has been calculated
|
We can be sure that the parent offset has been calculated
|
||||||
when the coordiantes are not [0,0].
|
when the coordiantes are not [0,0].
|
||||||
*/
|
*/
|
||||||
|
|
@ -146,6 +148,7 @@ const DragLayerComponent = (props: DragLayerProps) => {
|
||||||
columnWidth={props.parentColumnWidth}
|
columnWidth={props.parentColumnWidth}
|
||||||
rowHeight={props.parentRowHeight}
|
rowHeight={props.parentRowHeight}
|
||||||
ref={dropTargetMask}
|
ref={dropTargetMask}
|
||||||
|
noPad={props.noPad}
|
||||||
>
|
>
|
||||||
{props.visible &&
|
{props.visible &&
|
||||||
props.isOver &&
|
props.isOver &&
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -38,6 +38,22 @@ type DraggableComponentProps = WidgetProps;
|
||||||
|
|
||||||
/* eslint-disable react/display-name */
|
/* 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) => {
|
const DraggableComponent = (props: DraggableComponentProps) => {
|
||||||
// Dispatch hook handy to toggle property pane
|
// Dispatch hook handy to toggle property pane
|
||||||
const showPropertyPane = useShowPropertyPane();
|
const showPropertyPane = useShowPropertyPane();
|
||||||
|
|
@ -119,7 +135,7 @@ const DraggableComponent = (props: DraggableComponentProps) => {
|
||||||
},
|
},
|
||||||
canDrag: () => {
|
canDrag: () => {
|
||||||
// Dont' allow drag if we're resizing or the drag of `DraggableComponent` is disabled
|
// Dont' allow drag if we're resizing or the drag of `DraggableComponent` is disabled
|
||||||
return !isResizing && !isDraggingDisabled;
|
return canDrag(isResizing, isDraggingDisabled, props);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ type DropTargetComponentProps = WidgetProps & {
|
||||||
snapColumnSpace: number;
|
snapColumnSpace: number;
|
||||||
snapRowSpace: number;
|
snapRowSpace: number;
|
||||||
minHeight: number;
|
minHeight: number;
|
||||||
|
noPad?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledDropTarget = styled.div`
|
const StyledDropTarget = styled.div`
|
||||||
|
|
@ -65,7 +66,7 @@ export const DropTargetContext: Context<{
|
||||||
persistDropTargetRows?: (widgetId: string, row: number) => void;
|
persistDropTargetRows?: (widgetId: string, row: number) => void;
|
||||||
}> = createContext({});
|
}> = createContext({});
|
||||||
|
|
||||||
export const DropTargetComponent = memo((props: DropTargetComponentProps) => {
|
export const DropTargetComponent = (props: DropTargetComponentProps) => {
|
||||||
const canDropTargetExtend = props.canExtend;
|
const canDropTargetExtend = props.canExtend;
|
||||||
|
|
||||||
const snapRows = getCanvasSnapRows(props.bottomRow, props.canExtend);
|
const snapRows = getCanvasSnapRows(props.bottomRow, props.canExtend);
|
||||||
|
|
@ -244,7 +245,8 @@ export const DropTargetComponent = memo((props: DropTargetComponentProps) => {
|
||||||
focusWidget && focusWidget(props.parentId);
|
focusWidget && focusWidget(props.parentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.stopPropagation();
|
// commenting this out to allow propagation of click events
|
||||||
|
// e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
const height = canDropTargetExtend
|
const height = canDropTargetExtend
|
||||||
|
|
@ -258,13 +260,15 @@ export const DropTargetComponent = memo((props: DropTargetComponentProps) => {
|
||||||
? "1px solid #DDDDDD"
|
? "1px solid #DDDDDD"
|
||||||
: "1px solid transparent";
|
: "1px solid transparent";
|
||||||
|
|
||||||
|
const dropRef = !props.dropDisabled ? drop : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropTargetContext.Provider
|
<DropTargetContext.Provider
|
||||||
value={{ updateDropTargetRows, persistDropTargetRows }}
|
value={{ updateDropTargetRows, persistDropTargetRows }}
|
||||||
>
|
>
|
||||||
<StyledDropTarget
|
<StyledDropTarget
|
||||||
onClick={handleFocus}
|
onClick={handleFocus}
|
||||||
ref={drop}
|
ref={dropRef}
|
||||||
style={{
|
style={{
|
||||||
height,
|
height,
|
||||||
border,
|
border,
|
||||||
|
|
@ -287,11 +291,14 @@ export const DropTargetComponent = memo((props: DropTargetComponentProps) => {
|
||||||
parentRows={rows}
|
parentRows={rows}
|
||||||
parentCols={props.snapColumns}
|
parentCols={props.snapColumns}
|
||||||
isResizing={isChildResizing}
|
isResizing={isChildResizing}
|
||||||
|
noPad={props.noPad || false}
|
||||||
force={isDragging && !isOver && !props.parentId}
|
force={isDragging && !isOver && !props.parentId}
|
||||||
/>
|
/>
|
||||||
</StyledDropTarget>
|
</StyledDropTarget>
|
||||||
</DropTargetContext.Provider>
|
</DropTargetContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default DropTargetComponent;
|
const MemoizedDropTargetComponent = memo(DropTargetComponent);
|
||||||
|
|
||||||
|
export default MemoizedDropTargetComponent;
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => {
|
||||||
onStart={handleResizeStart}
|
onStart={handleResizeStart}
|
||||||
onStop={updateSize}
|
onStop={updateSize}
|
||||||
snapGrid={{ x: props.parentColumnSpace, y: props.parentRowSpace }}
|
snapGrid={{ x: props.parentColumnSpace, y: props.parentRowSpace }}
|
||||||
enable={!isDragging && isWidgetFocused}
|
enable={!isDragging && isWidgetFocused && !props.resizeDisabled}
|
||||||
isColliding={isColliding}
|
isColliding={isColliding}
|
||||||
>
|
>
|
||||||
<VisibilityContainer
|
<VisibilityContainer
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ export const WidgetNameComponent = (props: WidgetNameComponentProps) => {
|
||||||
currentActivity = Activities.ACTIVE;
|
currentActivity = Activities.ACTIVE;
|
||||||
|
|
||||||
return showWidgetName ? (
|
return showWidgetName ? (
|
||||||
<PositionStyle>
|
<PositionStyle data-testid="t--settings-controls-positioned-wrapper">
|
||||||
<ControlGroup>
|
<ControlGroup>
|
||||||
<SettingsControl
|
<SettingsControl
|
||||||
toggleSettings={togglePropertyEditor}
|
toggleSettings={togglePropertyEditor}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ export interface ControlFunctions {
|
||||||
openNextPanel: (props: any) => void;
|
openNextPanel: (props: any) => void;
|
||||||
deleteProperties: (propertyPaths: string[]) => void;
|
deleteProperties: (propertyPaths: string[]) => void;
|
||||||
theme: EditorTheme;
|
theme: EditorTheme;
|
||||||
|
hideEvaluatedValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BaseControl;
|
export default BaseControl;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class DropDownControl extends BaseControl<DropDownControlProps> {
|
||||||
options={this.props.options}
|
options={this.props.options}
|
||||||
selected={defaultSelected}
|
selected={defaultSelected}
|
||||||
onSelect={this.onItemSelect}
|
onSelect={this.onItemSelect}
|
||||||
width="231px"
|
width="100%"
|
||||||
showLabelOnly={true}
|
showLabelOnly={true}
|
||||||
optionWidth={
|
optionWidth={
|
||||||
this.props.optionWidth ? this.props.optionWidth : "231px"
|
this.props.optionWidth ? this.props.optionWidth : "231px"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export function InputText(props: {
|
||||||
dataTreePath?: string;
|
dataTreePath?: string;
|
||||||
additionalAutocomplete?: Record<string, Record<string, unknown>>;
|
additionalAutocomplete?: Record<string, Record<string, unknown>>;
|
||||||
theme?: EditorTheme;
|
theme?: EditorTheme;
|
||||||
|
hideEvaluatedValue?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
|
@ -32,7 +33,9 @@ export function InputText(props: {
|
||||||
placeholder,
|
placeholder,
|
||||||
dataTreePath,
|
dataTreePath,
|
||||||
evaluatedValue,
|
evaluatedValue,
|
||||||
|
hideEvaluatedValue,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledDynamicInput>
|
<StyledDynamicInput>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
|
@ -53,6 +56,7 @@ export function InputText(props: {
|
||||||
size={EditorSize.EXTENDED}
|
size={EditorSize.EXTENDED}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
additionalDynamicData={props.additionalAutocomplete}
|
additionalDynamicData={props.additionalAutocomplete}
|
||||||
|
hideEvaluatedValue={hideEvaluatedValue}
|
||||||
/>
|
/>
|
||||||
</StyledDynamicInput>
|
</StyledDynamicInput>
|
||||||
);
|
);
|
||||||
|
|
@ -69,7 +73,10 @@ class InputTextControl extends BaseControl<InputControlProps> {
|
||||||
dataTreePath,
|
dataTreePath,
|
||||||
validationMessage,
|
validationMessage,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
additionalAutoComplete,
|
||||||
|
hideEvaluatedValue,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputText
|
<InputText
|
||||||
label={label}
|
label={label}
|
||||||
|
|
@ -81,6 +88,8 @@ class InputTextControl extends BaseControl<InputControlProps> {
|
||||||
dataTreePath={dataTreePath}
|
dataTreePath={dataTreePath}
|
||||||
placeholder={placeholderText}
|
placeholder={placeholderText}
|
||||||
theme={this.props.theme}
|
theme={this.props.theme}
|
||||||
|
additionalAutocomplete={additionalAutoComplete}
|
||||||
|
hideEvaluatedValue={hideEvaluatedValue}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,11 @@ const FIELD_VALUES: Record<
|
||||||
shouldScroll: "boolean",
|
shouldScroll: "boolean",
|
||||||
isVisible: "boolean",
|
isVisible: "boolean",
|
||||||
},
|
},
|
||||||
|
LIST_WIDGET: {
|
||||||
|
items: "Array<Object>",
|
||||||
|
isVisible: "boolean",
|
||||||
|
gridGap: "number",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FIELD_VALUES;
|
export default FIELD_VALUES;
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,10 @@ export const HelpMap = {
|
||||||
path: "/core-concepts/connecting-to-data-sources/connecting-to-databases",
|
path: "/core-concepts/connecting-to-data-sources/connecting-to-databases",
|
||||||
searchKey: "Connecting to databases",
|
searchKey: "Connecting to databases",
|
||||||
},
|
},
|
||||||
|
LIST_WIDGET: {
|
||||||
|
path: "/widget-reference/list",
|
||||||
|
searchKey: "List",
|
||||||
|
},
|
||||||
SWITCH_WIDGET: {
|
SWITCH_WIDGET: {
|
||||||
path: "/widget-reference/switch",
|
path: "/widget-reference/switch",
|
||||||
searchKey: "Switch",
|
searchKey: "Switch",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export enum WidgetTypes {
|
||||||
FILE_PICKER_WIDGET = "FILE_PICKER_WIDGET",
|
FILE_PICKER_WIDGET = "FILE_PICKER_WIDGET",
|
||||||
VIDEO_WIDGET = "VIDEO_WIDGET",
|
VIDEO_WIDGET = "VIDEO_WIDGET",
|
||||||
SKELETON_WIDGET = "SKELETON_WIDGET",
|
SKELETON_WIDGET = "SKELETON_WIDGET",
|
||||||
|
LIST_WIDGET = "LIST_WIDGET",
|
||||||
SWITCH_WIDGET = "SWITCH_WIDGET",
|
SWITCH_WIDGET = "SWITCH_WIDGET",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export enum VALIDATION_TYPES {
|
||||||
MAX_DATE = "MAX_DATE",
|
MAX_DATE = "MAX_DATE",
|
||||||
TABS_DATA = "TABS_DATA",
|
TABS_DATA = "TABS_DATA",
|
||||||
CHART_DATA = "CHART_DATA",
|
CHART_DATA = "CHART_DATA",
|
||||||
|
LIST_DATA = "LIST_DATA",
|
||||||
CUSTOM_FUSION_CHARTS_DATA = "CUSTOM_FUSION_CHARTS_DATA",
|
CUSTOM_FUSION_CHARTS_DATA = "CUSTOM_FUSION_CHARTS_DATA",
|
||||||
MARKERS = "MARKERS",
|
MARKERS = "MARKERS",
|
||||||
ACTION_SELECTOR = "ACTION_SELECTOR",
|
ACTION_SELECTOR = "ACTION_SELECTOR",
|
||||||
|
|
|
||||||
9
app/client/src/constants/messages.test.ts
Normal file
9
app/client/src/constants/messages.test.ts
Normal 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.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -250,6 +250,8 @@ export const WIDGET_DELETE = (widgetName: string) =>
|
||||||
export const WIDGET_COPY = (widgetName: string) => `Copied ${widgetName}`;
|
export const WIDGET_COPY = (widgetName: string) => `Copied ${widgetName}`;
|
||||||
export const ERROR_WIDGET_COPY_NO_WIDGET_SELECTED = () =>
|
export const ERROR_WIDGET_COPY_NO_WIDGET_SELECTED = () =>
|
||||||
`Please select a widget to copy`;
|
`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 WIDGET_CUT = (widgetName: string) => `Cut ${widgetName}`;
|
||||||
export const ERROR_WIDGET_CUT_NO_WIDGET_SELECTED = () =>
|
export const ERROR_WIDGET_CUT_NO_WIDGET_SELECTED = () =>
|
||||||
`Please select a widget to cut`;
|
`Please select a widget to cut`;
|
||||||
|
|
|
||||||
20
app/client/src/icons/WidgetIcons.test.tsx
Normal file
20
app/client/src/icons/WidgetIcons.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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 FormIcon } from "assets/icons/widget/form.svg";
|
||||||
import { ReactComponent as MapIcon } from "assets/icons/widget/map.svg";
|
import { ReactComponent as MapIcon } from "assets/icons/widget/map.svg";
|
||||||
import { ReactComponent as ModalIcon } from "assets/icons/widget/modal.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 */
|
/* eslint-disable react/display-name */
|
||||||
|
|
||||||
export const WidgetIcons: {
|
export const WidgetIcons: {
|
||||||
|
|
@ -136,6 +137,11 @@ export const WidgetIcons: {
|
||||||
<ButtonIcon />
|
<ButtonIcon />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
),
|
),
|
||||||
|
LIST_WIDGET: (props: IconProps) => (
|
||||||
|
<IconWrapper {...props} data-testid="list-widget-icon">
|
||||||
|
<ListIcon />
|
||||||
|
</IconWrapper>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WidgetIcon = typeof WidgetIcons[keyof typeof WidgetIcons];
|
export type WidgetIcon = typeof WidgetIcons[keyof typeof WidgetIcons];
|
||||||
|
|
|
||||||
77
app/client/src/mockResponses/WidgetConfigResponse.test.tsx
Normal file
77
app/client/src/mockResponses/WidgetConfigResponse.test.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigReducer";
|
import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigReducer";
|
||||||
import { WidgetProps } from "widgets/BaseWidget";
|
import { WidgetProps } from "widgets/BaseWidget";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { cloneDeep, get, indexOf, isString } from "lodash";
|
||||||
import { generateReactKey } from "utils/generators";
|
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 { Colors } from "constants/Colors";
|
||||||
import FileDataTypes from "widgets/FileDataTypes";
|
import FileDataTypes from "widgets/FileDataTypes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this config sets the default values of properties being used in the widget
|
||||||
|
*/
|
||||||
const WidgetConfigResponse: WidgetConfigReducerState = {
|
const WidgetConfigResponse: WidgetConfigReducerState = {
|
||||||
config: {
|
config: {
|
||||||
BUTTON_WIDGET: {
|
BUTTON_WIDGET: {
|
||||||
|
|
@ -225,7 +233,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
|
||||||
blueprint: {
|
blueprint: {
|
||||||
operations: [
|
operations: [
|
||||||
{
|
{
|
||||||
type: "MODIFY_PROPS",
|
type: BlueprintOperationTypes.MODIFY_PROPS,
|
||||||
fn: (widget: WidgetProps & { children?: WidgetProps[] }) => {
|
fn: (widget: WidgetProps & { children?: WidgetProps[] }) => {
|
||||||
const tabs = [...widget.tabs];
|
const tabs = [...widget.tabs];
|
||||||
|
|
||||||
|
|
@ -320,9 +328,10 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
|
||||||
],
|
],
|
||||||
operations: [
|
operations: [
|
||||||
{
|
{
|
||||||
type: "MODIFY_PROPS",
|
type: BlueprintOperationTypes.MODIFY_PROPS,
|
||||||
fn: (
|
fn: (
|
||||||
widget: WidgetProps & { children?: WidgetProps[] },
|
widget: WidgetProps & { children?: WidgetProps[] },
|
||||||
|
widgets: { [widgetId: string]: FlattenedWidgetProps },
|
||||||
parent?: WidgetProps & { children?: WidgetProps[] },
|
parent?: WidgetProps & { children?: WidgetProps[] },
|
||||||
) => {
|
) => {
|
||||||
const iconChild =
|
const iconChild =
|
||||||
|
|
@ -490,6 +499,323 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
|
||||||
widgetName: "Skeleton",
|
widgetName: "Skeleton",
|
||||||
version: 1,
|
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,
|
configVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,12 @@ const WidgetSidebarResponse: WidgetCardProps[] = [
|
||||||
widgetCardName: "Form",
|
widgetCardName: "Form",
|
||||||
key: generateReactKey(),
|
key: generateReactKey(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "LIST_WIDGET",
|
||||||
|
widgetCardName: "List",
|
||||||
|
key: generateReactKey(),
|
||||||
|
isBeta: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "IMAGE_WIDGET",
|
type: "IMAGE_WIDGET",
|
||||||
widgetCardName: "Image",
|
widgetCardName: "Image",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { memo, useCallback } from "react";
|
import React, { memo, useCallback } from "react";
|
||||||
import _ from "lodash";
|
import _, { get } from "lodash";
|
||||||
import {
|
import {
|
||||||
ControlPropertyLabelContainer,
|
ControlPropertyLabelContainer,
|
||||||
ControlWrapper,
|
ControlWrapper,
|
||||||
|
|
@ -31,6 +31,11 @@ import { OnboardingStep } from "constants/OnboardingConstants";
|
||||||
import Indicator from "components/editorComponents/Onboarding/Indicator";
|
import Indicator from "components/editorComponents/Onboarding/Indicator";
|
||||||
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useChildWidgetEnhancementFns,
|
||||||
|
useParentWithEnhancementFn,
|
||||||
|
} from "sagas/WidgetEnhancementHelpers";
|
||||||
|
|
||||||
type Props = PropertyPaneControlConfig & {
|
type Props = PropertyPaneControlConfig & {
|
||||||
panel: IPanelProps;
|
panel: IPanelProps;
|
||||||
theme: EditorTheme;
|
theme: EditorTheme;
|
||||||
|
|
@ -39,6 +44,17 @@ type Props = PropertyPaneControlConfig & {
|
||||||
const PropertyControl = memo((props: Props) => {
|
const PropertyControl = memo((props: Props) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const widgetProperties: any = useSelector(getWidgetPropsForPropertyPane);
|
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(
|
const toggleDynamicProperty = useCallback(
|
||||||
(propertyName: string, isDynamic: boolean) => {
|
(propertyName: string, isDynamic: boolean) => {
|
||||||
|
|
@ -79,7 +95,28 @@ const PropertyControl = memo((props: Props) => {
|
||||||
),
|
),
|
||||||
[widgetProperties.widgetId, dispatch],
|
[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(
|
const onPropertyChange = useCallback(
|
||||||
(propertyName: string, propertyValue: any) => {
|
(propertyName: string, propertyValue: any) => {
|
||||||
AnalyticsUtil.logEvent("WIDGET_PROPERTY_UPDATE", {
|
AnalyticsUtil.logEvent("WIDGET_PROPERTY_UPDATE", {
|
||||||
|
|
@ -88,7 +125,6 @@ const PropertyControl = memo((props: Props) => {
|
||||||
propertyName: propertyName,
|
propertyName: propertyName,
|
||||||
updatedValue: propertyValue,
|
updatedValue: propertyValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
let propertiesToUpdate:
|
let propertiesToUpdate:
|
||||||
| Array<{
|
| Array<{
|
||||||
propertyPath: string;
|
propertyPath: string;
|
||||||
|
|
@ -102,6 +138,39 @@ const PropertyControl = memo((props: Props) => {
|
||||||
propertyValue,
|
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) {
|
if (propertiesToUpdate) {
|
||||||
const allUpdates: Record<string, unknown> = {};
|
const allUpdates: Record<string, unknown> = {};
|
||||||
propertiesToUpdate.forEach(({ propertyPath, propertyValue }) => {
|
propertiesToUpdate.forEach(({ propertyPath, propertyValue }) => {
|
||||||
|
|
@ -190,6 +259,7 @@ const PropertyControl = memo((props: Props) => {
|
||||||
expected: FIELD_EXPECTED_VALUE[widgetProperties.type as WidgetType][
|
expected: FIELD_EXPECTED_VALUE[widgetProperties.type as WidgetType][
|
||||||
propertyName
|
propertyName
|
||||||
] as any,
|
] as any,
|
||||||
|
additionalDynamicData: {},
|
||||||
};
|
};
|
||||||
if (isPathADynamicTrigger(widgetProperties, propertyName)) {
|
if (isPathADynamicTrigger(widgetProperties, propertyName)) {
|
||||||
config.isValid = true;
|
config.isValid = true;
|
||||||
|
|
@ -209,6 +279,36 @@ const PropertyControl = memo((props: Props) => {
|
||||||
.join("")
|
.join("")
|
||||||
.toLowerCase();
|
.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 {
|
try {
|
||||||
return (
|
return (
|
||||||
<ControlWrapper
|
<ControlWrapper
|
||||||
|
|
@ -255,10 +355,9 @@ const PropertyControl = memo((props: Props) => {
|
||||||
theme: props.theme,
|
theme: props.theme,
|
||||||
},
|
},
|
||||||
isDynamic,
|
isDynamic,
|
||||||
props.customJSControl,
|
getCustomJSControl(),
|
||||||
additionalAutoComplete
|
additionAutocomplete,
|
||||||
? additionalAutoComplete(widgetProperties)
|
hideEvaluatedValue(),
|
||||||
: undefined,
|
|
||||||
)}
|
)}
|
||||||
</Indicator>
|
</Indicator>
|
||||||
</Boxed>
|
</Boxed>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -38,6 +38,7 @@ import { FormIcons } from "icons/FormIcons";
|
||||||
import PropertyPaneHelpButton from "pages/Editor/PropertyPaneHelpButton";
|
import PropertyPaneHelpButton from "pages/Editor/PropertyPaneHelpButton";
|
||||||
import { getProppanePreference } from "selectors/usersSelectors";
|
import { getProppanePreference } from "selectors/usersSelectors";
|
||||||
import { PropertyPanePositionConfig } from "reducers/uiReducers/usersReducer";
|
import { PropertyPanePositionConfig } from "reducers/uiReducers/usersReducer";
|
||||||
|
import { get } from "lodash";
|
||||||
|
|
||||||
const PropertyPaneWrapper = styled(PaneWrapper)<{
|
const PropertyPaneWrapper = styled(PaneWrapper)<{
|
||||||
themeMode?: EditorTheme;
|
themeMode?: EditorTheme;
|
||||||
|
|
@ -178,6 +179,10 @@ class PropertyPane extends Component<PropertyPaneProps, PropertyPaneState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (get(this.props, "widgetProperties.disablePropertyPane")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.isVisible) {
|
if (this.props.isVisible) {
|
||||||
log.debug("Property pane rendered");
|
log.debug("Property pane rendered");
|
||||||
const content = this.renderPropertyPane();
|
const content = this.renderPropertyPane();
|
||||||
|
|
@ -212,13 +217,15 @@ class PropertyPane extends Component<PropertyPaneProps, PropertyPaneState> {
|
||||||
|
|
||||||
renderPropertyPane() {
|
renderPropertyPane() {
|
||||||
const { widgetProperties } = this.props;
|
const { widgetProperties } = this.props;
|
||||||
if (!widgetProperties)
|
|
||||||
return (
|
if (!widgetProperties) {
|
||||||
<PropertyPaneWrapper
|
return <></>;
|
||||||
className={"t--propertypane"}
|
}
|
||||||
themeMode={this.getTheme()}
|
|
||||||
/>
|
// 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 (
|
return (
|
||||||
<PropertyPaneWrapper
|
<PropertyPaneWrapper
|
||||||
className={"t--propertypane"}
|
className={"t--propertypane"}
|
||||||
|
|
|
||||||
|
|
@ -38,20 +38,17 @@ const Wrapper = styled.div<{ iconCount: number }>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: ${(props) => props.theme.propertyPane.titleHeight}px;
|
height: ${(props) => props.theme.propertyPane.titleHeight}px;
|
||||||
background-color: ${(props) => props.theme.colors.propertyPane.bg};
|
background-color: ${(props) => props.theme.colors.propertyPane.bg};
|
||||||
|
|
||||||
& span.${BlueprintClasses.POPOVER_TARGET} {
|
& span.${BlueprintClasses.POPOVER_TARGET} {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&&& .${BlueprintClasses.EDITABLE_TEXT} {
|
&&& .${BlueprintClasses.EDITABLE_TEXT} {
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&&&
|
&&&
|
||||||
.${BlueprintClasses.EDITABLE_TEXT_CONTENT},
|
.${BlueprintClasses.EDITABLE_TEXT_CONTENT},
|
||||||
&&&
|
&&&
|
||||||
|
|
@ -59,7 +56,6 @@ const Wrapper = styled.div<{ iconCount: number }>`
|
||||||
color: ${(props) => props.theme.colors.propertyPane.title};
|
color: ${(props) => props.theme.colors.propertyPane.title};
|
||||||
font-size: ${(props) => props.theme.fontSizes[4]}px;
|
font-size: ${(props) => props.theme.fontSizes[4]}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&& svg path {
|
&& svg path {
|
||||||
fill: ${(props) => props.theme.colors.propertyPane.label};
|
fill: ${(props) => props.theme.colors.propertyPane.label};
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +67,6 @@ const NameWrapper = styled.div<{ isPanelTitle?: boolean }>`
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
padding-right: 25px;
|
padding-right: 25px;
|
||||||
max-width: 134px;
|
max-width: 134px;
|
||||||
|
|
||||||
&&&&&&& > * {
|
&&&&&&& > * {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export const Wrapper = styled.div`
|
||||||
padding: 10px 5px 10px 5px;
|
padding: 10px 5px 10px 5px;
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
border: none;
|
border: none;
|
||||||
|
position: relative;
|
||||||
color: ${Colors.ALTO};
|
color: ${Colors.ALTO};
|
||||||
height: 72px;
|
height: 72px;
|
||||||
display: flex;
|
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`
|
export const IconLabel = styled.h5`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -116,6 +128,7 @@ const WidgetCard = (props: CardProps) => {
|
||||||
<div>
|
<div>
|
||||||
<Icon />
|
<Icon />
|
||||||
<IconLabel>{props.details.widgetCardName}</IconLabel>
|
<IconLabel>{props.details.widgetCardName}</IconLabel>
|
||||||
|
{props.details.isBeta && <BetaLabel>Beta</BetaLabel>}
|
||||||
</div>
|
</div>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { IconWidgetProps } from "widgets/IconWidget";
|
||||||
import { VideoWidgetProps } from "widgets/VideoWidget";
|
import { VideoWidgetProps } from "widgets/VideoWidget";
|
||||||
import { SkeletonWidgetProps } from "../../widgets/SkeletonWidget";
|
import { SkeletonWidgetProps } from "../../widgets/SkeletonWidget";
|
||||||
import { SwitchWidgetProps } from "widgets/SwitchWidget";
|
import { SwitchWidgetProps } from "widgets/SwitchWidget";
|
||||||
|
import { ListWidgetProps } from "../../widgets/ListWidget/ListWidget";
|
||||||
|
|
||||||
const initialState: WidgetConfigReducerState = WidgetConfigResponse;
|
const initialState: WidgetConfigReducerState = WidgetConfigResponse;
|
||||||
|
|
||||||
|
|
@ -78,6 +79,7 @@ export interface WidgetConfigReducerState {
|
||||||
WidgetConfigProps;
|
WidgetConfigProps;
|
||||||
ICON_WIDGET: Partial<IconWidgetProps> & WidgetConfigProps;
|
ICON_WIDGET: Partial<IconWidgetProps> & WidgetConfigProps;
|
||||||
SKELETON_WIDGET: Partial<SkeletonWidgetProps> & WidgetConfigProps;
|
SKELETON_WIDGET: Partial<SkeletonWidgetProps> & WidgetConfigProps;
|
||||||
|
LIST_WIDGET: Partial<ListWidgetProps<WidgetProps>> & WidgetConfigProps;
|
||||||
};
|
};
|
||||||
configVersion: number;
|
configVersion: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,4 +56,5 @@ const uiReducer = combineReducers({
|
||||||
globalSearch: globalSearchReducer,
|
globalSearch: globalSearchReducer,
|
||||||
releases: releasesReducer,
|
releases: releasesReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default uiReducer;
|
export default uiReducer;
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@ export function* fetchPageSaga(
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
const isValidResponse = yield validateResponse(fetchPageResponse);
|
const isValidResponse = yield validateResponse(fetchPageResponse);
|
||||||
|
|
||||||
if (isValidResponse) {
|
if (isValidResponse) {
|
||||||
// Clear any existing caches
|
// Clear any existing caches
|
||||||
yield call(clearEvalCache);
|
yield call(clearEvalCache);
|
||||||
|
|
@ -234,7 +235,10 @@ export function* fetchPublishedPageSaga(
|
||||||
const { pageId, bustCache } = pageRequestAction.payload;
|
const { pageId, bustCache } = pageRequestAction.payload;
|
||||||
PerformanceTracker.startAsyncTracking(
|
PerformanceTracker.startAsyncTracking(
|
||||||
PerformanceTransactionName.FETCH_PAGE_API,
|
PerformanceTransactionName.FETCH_PAGE_API,
|
||||||
{ pageId: pageId, published: true },
|
{
|
||||||
|
pageId: pageId,
|
||||||
|
published: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const request: FetchPublishedPageRequest = {
|
const request: FetchPublishedPageRequest = {
|
||||||
pageId,
|
pageId,
|
||||||
|
|
|
||||||
51
app/client/src/sagas/WidgetBlueprintSagas.test.ts
Normal file
51
app/client/src/sagas/WidgetBlueprintSagas.test.ts
Normal 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({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,6 +4,16 @@ import { WidgetProps } from "widgets/BaseWidget";
|
||||||
import { generateReactKey } from "utils/generators";
|
import { generateReactKey } from "utils/generators";
|
||||||
import { call } from "redux-saga/effects";
|
import { call } from "redux-saga/effects";
|
||||||
import { get } from "lodash";
|
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) {
|
function buildView(view: WidgetBlueprint["view"], widgetId: string) {
|
||||||
const children = [];
|
const children = [];
|
||||||
|
|
@ -46,17 +56,28 @@ export type UpdatePropertyArgs = {
|
||||||
export type BlueprintOperationAddActionFn = () => void;
|
export type BlueprintOperationAddActionFn = () => void;
|
||||||
export type BlueprintOperationModifyPropsFn = (
|
export type BlueprintOperationModifyPropsFn = (
|
||||||
widget: WidgetProps & { children?: WidgetProps[] },
|
widget: WidgetProps & { children?: WidgetProps[] },
|
||||||
|
widgets: { [widgetId: string]: FlattenedWidgetProps },
|
||||||
parent?: WidgetProps,
|
parent?: WidgetProps,
|
||||||
) => UpdatePropertyArgs[] | undefined;
|
) => 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 =
|
export type BlueprintOperationFunction =
|
||||||
| BlueprintOperationModifyPropsFn
|
| BlueprintOperationModifyPropsFn
|
||||||
| BlueprintOperationAddActionFn;
|
| BlueprintOperationAddActionFn
|
||||||
|
| BlueprintOperationChildOperationsFn;
|
||||||
export enum BlueprintOperationTypes {
|
|
||||||
MODIFY_PROPS = "MODIFY_PROPS",
|
|
||||||
ADD_ACTION = "ADD_ACTION",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BlueprintOperationType = keyof typeof BlueprintOperationTypes;
|
export type BlueprintOperationType = keyof typeof BlueprintOperationTypes;
|
||||||
|
|
||||||
|
|
@ -71,11 +92,12 @@ export function* executeWidgetBlueprintOperations(
|
||||||
widgetId: string,
|
widgetId: string,
|
||||||
) {
|
) {
|
||||||
operations.forEach((operation: BlueprintOperation) => {
|
operations.forEach((operation: BlueprintOperation) => {
|
||||||
|
const widget: WidgetProps & { children?: string[] | WidgetProps[] } = {
|
||||||
|
...widgets[widgetId],
|
||||||
|
};
|
||||||
|
|
||||||
switch (operation.type) {
|
switch (operation.type) {
|
||||||
case BlueprintOperationTypes.MODIFY_PROPS:
|
case BlueprintOperationTypes.MODIFY_PROPS:
|
||||||
const widget: WidgetProps & { children?: string[] | WidgetProps[] } = {
|
|
||||||
...widgets[widgetId],
|
|
||||||
};
|
|
||||||
if (widget.children && widget.children.length > 0) {
|
if (widget.children && widget.children.length > 0) {
|
||||||
widget.children = (widget.children as string[]).map(
|
widget.children = (widget.children as string[]).map(
|
||||||
(childId: string) => widgets[childId],
|
(childId: string) => widgets[childId],
|
||||||
|
|
@ -85,6 +107,7 @@ export function* executeWidgetBlueprintOperations(
|
||||||
| UpdatePropertyArgs[]
|
| UpdatePropertyArgs[]
|
||||||
| undefined = (operation.fn as BlueprintOperationModifyPropsFn)(
|
| undefined = (operation.fn as BlueprintOperationModifyPropsFn)(
|
||||||
widget as WidgetProps & { children?: WidgetProps[] },
|
widget as WidgetProps & { children?: WidgetProps[] },
|
||||||
|
widgets,
|
||||||
get(widgets, widget.parentId || "", undefined),
|
get(widgets, widget.parentId || "", undefined),
|
||||||
);
|
);
|
||||||
updatePropertyPayloads &&
|
updatePropertyPayloads &&
|
||||||
|
|
@ -92,7 +115,115 @@ export function* executeWidgetBlueprintOperations(
|
||||||
widgets[params.widgetId][params.propertyName] =
|
widgets[params.widgetId][params.propertyName] =
|
||||||
params.propertyValue;
|
params.propertyValue;
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return yield widgets;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
5
app/client/src/sagas/WidgetBlueprintSagasEnums.ts
Normal file
5
app/client/src/sagas/WidgetBlueprintSagasEnums.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export enum BlueprintOperationTypes {
|
||||||
|
MODIFY_PROPS = "MODIFY_PROPS",
|
||||||
|
ADD_ACTION = "ADD_ACTION",
|
||||||
|
CHILD_OPERATIONS = "CHILD_OPERATIONS",
|
||||||
|
}
|
||||||
221
app/client/src/sagas/WidgetEnhancementHelpers.ts
Normal file
221
app/client/src/sagas/WidgetEnhancementHelpers.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,7 @@ import WidgetFactory from "utils/WidgetFactory";
|
||||||
import {
|
import {
|
||||||
buildWidgetBlueprint,
|
buildWidgetBlueprint,
|
||||||
executeWidgetBlueprintOperations,
|
executeWidgetBlueprintOperations,
|
||||||
|
traverseTreeAndExecuteBlueprintChildOperations,
|
||||||
} from "sagas/WidgetBlueprintSagas";
|
} from "sagas/WidgetBlueprintSagas";
|
||||||
import { resetWidgetMetaProperty } from "actions/metaActions";
|
import { resetWidgetMetaProperty } from "actions/metaActions";
|
||||||
import {
|
import {
|
||||||
|
|
@ -110,7 +111,12 @@ import {
|
||||||
WIDGET_COPY,
|
WIDGET_COPY,
|
||||||
WIDGET_CUT,
|
WIDGET_CUT,
|
||||||
WIDGET_DELETE,
|
WIDGET_DELETE,
|
||||||
|
ERROR_WIDGET_COPY_NOT_ALLOWED,
|
||||||
} from "constants/messages";
|
} from "constants/messages";
|
||||||
|
import {
|
||||||
|
doesTriggerPathsContainPropertyPath,
|
||||||
|
handleSpecificCasesWhilePasting,
|
||||||
|
} from "./WidgetOperationUtils";
|
||||||
|
|
||||||
function* getChildWidgetProps(
|
function* getChildWidgetProps(
|
||||||
parent: FlattenedWidgetProps,
|
parent: FlattenedWidgetProps,
|
||||||
|
|
@ -243,25 +249,43 @@ function* generateChildWidgets(
|
||||||
widget.widgetId,
|
widget.widgetId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the parentId prop to this widget
|
// Add the parentId prop to this widget
|
||||||
widget.parentId = parent.widgetId;
|
widget.parentId = parent.widgetId;
|
||||||
// Remove the blueprint from the widget (if any)
|
// Remove the blueprint from the widget (if any)
|
||||||
// as blueprints are not useful beyond this point.
|
// as blueprints are not useful beyond this point.
|
||||||
delete widget.blueprint;
|
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 };
|
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>) {
|
export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
|
||||||
try {
|
try {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
Toaster.clear();
|
Toaster.clear();
|
||||||
|
|
||||||
|
// NOTE: widgetId here is the parentId of the dropped widget ( we should rename it to avoid confusion )
|
||||||
const { widgetId } = addChildAction.payload;
|
const { widgetId } = addChildAction.payload;
|
||||||
// Get the current parent widget whose child will be the new widget.
|
// Get the current parent widget whose child will be the new widget.
|
||||||
const stateParent: FlattenedWidgetProps = yield select(getWidget, widgetId);
|
const stateParent: FlattenedWidgetProps = yield select(getWidget, widgetId);
|
||||||
// const parent = Object.assign({}, stateParent);
|
// const parent = Object.assign({}, stateParent);
|
||||||
// Get all the widgets from the canvasWidgetsReducer
|
// Get all the widgets from the canvasWidgetsReducer
|
||||||
const stateWidgets = yield select(getWidgets);
|
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.
|
// Generate the full WidgetProps of the widget to be added.
|
||||||
const childWidgetPayload: GeneratedWidgetPayload = yield generateChildWidgets(
|
const childWidgetPayload: GeneratedWidgetPayload = yield generateChildWidgets(
|
||||||
stateParent,
|
stateParent,
|
||||||
|
|
@ -278,6 +302,21 @@ export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
|
||||||
|
|
||||||
widgets[parent.widgetId] = parent;
|
widgets[parent.widgetId] = parent;
|
||||||
log.debug("add child computations took", performance.now() - start, "ms");
|
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({
|
yield put({
|
||||||
type: ReduxActionTypes.WIDGET_CHILD_ADDED,
|
type: ReduxActionTypes.WIDGET_CHILD_ADDED,
|
||||||
payload: {
|
payload: {
|
||||||
|
|
@ -286,6 +325,9 @@ export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
yield put(updateAndSaveLayout(widgets));
|
yield put(updateAndSaveLayout(widgets));
|
||||||
|
|
||||||
|
// go up till MAIN_CONTAINER, if there is a operation CHILD_OPERATIONS IN ANY PARENT,
|
||||||
|
// call execute
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
yield put({
|
yield put({
|
||||||
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
|
type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
|
||||||
|
|
@ -402,6 +444,10 @@ export function* deleteSaga(deleteAction: ReduxAction<WidgetDelete>) {
|
||||||
if (!widgetId) {
|
if (!widgetId) {
|
||||||
const selectedWidget = yield select(getSelectedWidget);
|
const selectedWidget = yield select(getSelectedWidget);
|
||||||
if (!selectedWidget) return;
|
if (!selectedWidget) return;
|
||||||
|
|
||||||
|
// if widget is not deletable, don't don anything
|
||||||
|
if (selectedWidget.isDeletable === false) return false;
|
||||||
|
|
||||||
widgetId = selectedWidget.widgetId;
|
widgetId = selectedWidget.widgetId;
|
||||||
parentId = selectedWidget.parentId;
|
parentId = selectedWidget.parentId;
|
||||||
}
|
}
|
||||||
|
|
@ -835,6 +881,7 @@ function* setWidgetDynamicPropertySaga(
|
||||||
function getPropertiesToUpdate(
|
function getPropertiesToUpdate(
|
||||||
widget: WidgetProps,
|
widget: WidgetProps,
|
||||||
updates: Record<string, unknown>,
|
updates: Record<string, unknown>,
|
||||||
|
triggerPaths?: string[],
|
||||||
): {
|
): {
|
||||||
propertyUpdates: Record<string, unknown>;
|
propertyUpdates: Record<string, unknown>;
|
||||||
dynamicTriggerPathList: DynamicPath[];
|
dynamicTriggerPathList: DynamicPath[];
|
||||||
|
|
@ -869,11 +916,17 @@ function getPropertiesToUpdate(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the path is a of a dynamic trigger property
|
// Check if the path is a of a dynamic trigger property
|
||||||
const isTriggerProperty = isPropertyATriggerPath(
|
let isTriggerProperty = isPropertyATriggerPath(
|
||||||
widgetWithUpdates,
|
widgetWithUpdates,
|
||||||
propertyPath,
|
propertyPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
isTriggerProperty = doesTriggerPathsContainPropertyPath(
|
||||||
|
isTriggerProperty,
|
||||||
|
propertyPath,
|
||||||
|
triggerPaths,
|
||||||
|
);
|
||||||
|
|
||||||
// If it is a trigger property, it will go in a different list than the general
|
// If it is a trigger property, it will go in a different list than the general
|
||||||
// dynamicBindingPathList.
|
// dynamicBindingPathList.
|
||||||
if (isTriggerProperty) {
|
if (isTriggerProperty) {
|
||||||
|
|
@ -912,7 +965,7 @@ function* batchUpdateWidgetPropertySaga(
|
||||||
// Handling the case where sometimes widget id is not passed through here
|
// Handling the case where sometimes widget id is not passed through here
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { modify = {}, remove = [] } = updates;
|
const { modify = {}, remove = [], triggerPaths } = updates;
|
||||||
|
|
||||||
const stateWidget: WidgetProps = yield select(getWidget, widgetId);
|
const stateWidget: WidgetProps = yield select(getWidget, widgetId);
|
||||||
|
|
||||||
|
|
@ -926,7 +979,7 @@ function* batchUpdateWidgetPropertySaga(
|
||||||
propertyUpdates,
|
propertyUpdates,
|
||||||
dynamicTriggerPathList,
|
dynamicTriggerPathList,
|
||||||
dynamicBindingPathList,
|
dynamicBindingPathList,
|
||||||
} = getPropertiesToUpdate(widget, modify);
|
} = getPropertiesToUpdate(widget, modify, triggerPaths);
|
||||||
|
|
||||||
// We loop over all updates
|
// We loop over all updates
|
||||||
Object.entries(propertyUpdates).forEach(
|
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 }>) {
|
function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
|
||||||
const selectedWidget = yield select(getSelectedWidget);
|
const selectedWidget = yield select(getSelectedWidget);
|
||||||
if (!selectedWidget) {
|
if (!selectedWidget) {
|
||||||
|
|
@ -1101,6 +1161,15 @@ function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedWidget.disallowCopy === true) {
|
||||||
|
Toaster.show({
|
||||||
|
text: createMessage(ERROR_WIDGET_COPY_NOT_ALLOWED),
|
||||||
|
variant: Variant.info,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const saveResult = yield createWidgetCopy();
|
const saveResult = yield createWidgetCopy();
|
||||||
|
|
||||||
const eventName = action.payload.isShortcut
|
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() {
|
function* pasteWidgetSaga() {
|
||||||
const copiedWidgets: {
|
const copiedWidgets: {
|
||||||
widgetId: string;
|
widgetId: string;
|
||||||
|
|
@ -1178,7 +1250,20 @@ function* pasteWidgetSaga() {
|
||||||
const stateWidgets = yield select(getWidgets);
|
const stateWidgets = yield select(getWidgets);
|
||||||
let widgets = { ...stateWidgets };
|
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 newWidgetParentId = MAIN_CONTAINER_WIDGET_ID;
|
||||||
let parentWidget = widgets[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
|
// Get a flat list of all the widgets to be updated
|
||||||
const widgetList = copiedWidgets.list;
|
const widgetList = copiedWidgets.list;
|
||||||
const widgetIdMap: Record<string, string> = {};
|
const widgetIdMap: Record<string, string> = {};
|
||||||
|
const widgetNameMap: Record<string, string> = {};
|
||||||
const newWidgetList: FlattenedWidgetProps[] = [];
|
const newWidgetList: FlattenedWidgetProps[] = [];
|
||||||
let newWidgetId: string = copiedWidget.widgetId;
|
let newWidgetId: string = copiedWidget.widgetId;
|
||||||
// Generate new widgetIds for the flat list of all the widgets to be updated
|
// Generate new widgetIds for the flat list of all the widgets to be updated
|
||||||
|
|
@ -1260,12 +1346,16 @@ function* pasteWidgetSaga() {
|
||||||
newWidget.widgetId = generateReactKey();
|
newWidget.widgetId = generateReactKey();
|
||||||
// Add the new widget id so that it maps the previous widget id
|
// Add the new widget id so that it maps the previous widget id
|
||||||
widgetIdMap[widget.widgetId] = newWidget.widgetId;
|
widgetIdMap[widget.widgetId] = newWidget.widgetId;
|
||||||
|
|
||||||
// Add the new widget to the list
|
// Add the new widget to the list
|
||||||
newWidgetList.push(newWidget);
|
newWidgetList.push(newWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
// For each of the new widgets generated
|
// 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
|
// Update the children widgetIds if it has children
|
||||||
if (widget.children && widget.children.length > 0) {
|
if (widget.children && widget.children.length > 0) {
|
||||||
widget.children.forEach((childWidgetId: string, index: number) => {
|
widget.children.forEach((childWidgetId: string, index: number) => {
|
||||||
|
|
@ -1327,6 +1417,8 @@ function* pasteWidgetSaga() {
|
||||||
widget.widgetName = getNextWidgetName(widgets, widget.type, evalTree);
|
widget.widgetName = getNextWidgetName(widgets, widget.type, evalTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
widgetNameMap[oldWidgetName] = widget.widgetName;
|
||||||
|
|
||||||
// If it is the copied widget, update position properties
|
// If it is the copied widget, update position properties
|
||||||
if (widget.widgetId === widgetIdMap[copiedWidget.widgetId]) {
|
if (widget.widgetId === widgetIdMap[copiedWidget.widgetId]) {
|
||||||
newWidgetId = widget.widgetId;
|
newWidgetId = widget.widgetId;
|
||||||
|
|
@ -1385,11 +1477,27 @@ function* pasteWidgetSaga() {
|
||||||
widget.widgetName = getNextWidgetName(widgets, widget.type, evalTree);
|
widget.widgetName = getNextWidgetName(widgets, widget.type, evalTree);
|
||||||
// Add the new widget to the canvas widgets
|
// Add the new widget to the canvas widgets
|
||||||
widgets[widget.widgetId] = widget;
|
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
|
// save the new DSL
|
||||||
yield put(updateAndSaveLayout(widgets));
|
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
|
// Flash the newly pasted widget once the DSL is re-rendered
|
||||||
setTimeout(() => flashElementById(newWidgetId), 100);
|
setTimeout(() => flashElementById(newWidgetId), 100);
|
||||||
yield put({
|
yield put({
|
||||||
|
|
|
||||||
207
app/client/src/sagas/WidgetOperationUtils.test.ts
Normal file
207
app/client/src/sagas/WidgetOperationUtils.test.ts
Normal 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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
177
app/client/src/sagas/WidgetOperationUtils.ts
Normal file
177
app/client/src/sagas/WidgetOperationUtils.ts
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { createSelector } from "reselect";
|
import { find, get } from "lodash";
|
||||||
import { AppState } from "reducers";
|
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 { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer";
|
||||||
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
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 =>
|
const getPropertyPaneState = (state: AppState): PropertyPaneReduxState =>
|
||||||
state.ui.propertyPane;
|
state.ui.propertyPane;
|
||||||
|
|
@ -23,7 +24,7 @@ export const getCurrentWidgetProperties = createSelector(
|
||||||
widgets: CanvasWidgetsReduxState,
|
widgets: CanvasWidgetsReduxState,
|
||||||
pane: PropertyPaneReduxState,
|
pane: PropertyPaneReduxState,
|
||||||
): WidgetProps | undefined => {
|
): 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,
|
widgetId: widget.widgetId,
|
||||||
}) as DataTreeWidget;
|
}) as DataTreeWidget;
|
||||||
const widgetProperties = { ...widget };
|
const widgetProperties = { ...widget };
|
||||||
|
|
||||||
if (evaluatedWidget) {
|
if (evaluatedWidget) {
|
||||||
if (evaluatedWidget.evaluatedValues) {
|
if (evaluatedWidget.evaluatedValues) {
|
||||||
widgetProperties.evaluatedValues = {
|
widgetProperties.evaluatedValues = {
|
||||||
...evaluatedWidget.evaluatedValues,
|
...evaluatedWidget.evaluatedValues,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evaluatedWidget.invalidProps) {
|
if (evaluatedWidget.invalidProps) {
|
||||||
const { invalidProps, validationMessages } = evaluatedWidget;
|
const { invalidProps, validationMessages } = evaluatedWidget;
|
||||||
widgetProperties.invalidProps = invalidProps;
|
widgetProperties.invalidProps = invalidProps;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ class PropertyControlFactory {
|
||||||
preferEditor: boolean,
|
preferEditor: boolean,
|
||||||
customEditor?: string,
|
customEditor?: string,
|
||||||
additionalAutoComplete?: Record<string, Record<string, unknown>>,
|
additionalAutoComplete?: Record<string, Record<string, unknown>>,
|
||||||
|
hideEvaluatedValue?: boolean,
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
let controlBuilder = this.controlMap.get(controlData.controlType);
|
let controlBuilder = this.controlMap.get(controlData.controlType);
|
||||||
if (preferEditor) {
|
if (preferEditor) {
|
||||||
|
|
@ -35,7 +36,9 @@ class PropertyControlFactory {
|
||||||
key: controlData.id,
|
key: controlData.id,
|
||||||
customJSControl: customEditor,
|
customJSControl: customEditor,
|
||||||
additionalAutoComplete,
|
additionalAutoComplete,
|
||||||
|
hideEvaluatedValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
const control = controlBuilder.buildPropertyControl(controlProps);
|
const control = controlBuilder.buildPropertyControl(controlProps);
|
||||||
return control;
|
return control;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -643,6 +643,7 @@ export const widgetOperationParams = (
|
||||||
columns: widget.columns,
|
columns: widget.columns,
|
||||||
rows: widget.rows,
|
rows: widget.rows,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
operation: WidgetOperations.ADD_CHILD,
|
operation: WidgetOperations.ADD_CHILD,
|
||||||
widgetId: parentWidgetId,
|
widgetId: parentWidgetId,
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,12 @@ import SkeletonWidget, {
|
||||||
ProfiledSkeletonWidget,
|
ProfiledSkeletonWidget,
|
||||||
SkeletonWidgetProps,
|
SkeletonWidgetProps,
|
||||||
} from "../widgets/SkeletonWidget";
|
} from "../widgets/SkeletonWidget";
|
||||||
|
|
||||||
|
import ListWidget, {
|
||||||
|
ListWidgetProps,
|
||||||
|
ProfiledListWidget,
|
||||||
|
} from "widgets/ListWidget/ListWidget";
|
||||||
|
|
||||||
import SwitchWidget, {
|
import SwitchWidget, {
|
||||||
ProfiledSwitchWidget,
|
ProfiledSwitchWidget,
|
||||||
SwitchWidgetProps,
|
SwitchWidgetProps,
|
||||||
|
|
@ -407,7 +413,18 @@ export default class WidgetBuilderRegistry {
|
||||||
SkeletonWidget.getMetaPropertiesMap(),
|
SkeletonWidget.getMetaPropertiesMap(),
|
||||||
SkeletonWidget.getPropertyPaneConfig(),
|
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(
|
WidgetFactory.registerWidgetBuilder(
|
||||||
WidgetTypes.MODAL_WIDGET,
|
WidgetTypes.MODAL_WIDGET,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
47
app/client/src/utils/autocomplete/EntityDefinitions.test.ts
Normal file
47
app/client/src/utils/autocomplete/EntityDefinitions.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -224,6 +224,17 @@ export const entityDefinitions = {
|
||||||
isDisabled: "bool",
|
isDisabled: "bool",
|
||||||
uploadedFileUrls: "string",
|
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 = {
|
export const GLOBAL_DEFS = {
|
||||||
|
|
|
||||||
|
|
@ -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
|
* checks if variable passed is of type string or not
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,12 @@ abstract class BaseWidget<
|
||||||
return this.getWidgetView();
|
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) {
|
makeResizable(content: ReactNode) {
|
||||||
return (
|
return (
|
||||||
<ResizableComponent
|
<ResizableComponent
|
||||||
|
|
@ -175,21 +181,37 @@ abstract class BaseWidget<
|
||||||
</ResizableComponent>
|
</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) {
|
showWidgetName(content: ReactNode, showControls = false) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<WidgetNameComponent
|
{!this.props.disablePropertyPane && (
|
||||||
widgetName={this.props.widgetName}
|
<WidgetNameComponent
|
||||||
widgetId={this.props.widgetId}
|
widgetName={this.props.widgetName}
|
||||||
parentId={this.props.parentId}
|
widgetId={this.props.widgetId}
|
||||||
type={this.props.type}
|
parentId={this.props.parentId}
|
||||||
showControls={showControls}
|
type={this.props.type}
|
||||||
/>
|
showControls={showControls}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{content}
|
{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) {
|
makeDraggable(content: ReactNode) {
|
||||||
return <DraggableComponent {...this.props}>{content}</DraggableComponent>;
|
return <DraggableComponent {...this.props}>{content}</DraggableComponent>;
|
||||||
}
|
}
|
||||||
|
|
@ -213,11 +235,12 @@ abstract class BaseWidget<
|
||||||
|
|
||||||
private getWidgetView(): ReactNode {
|
private getWidgetView(): ReactNode {
|
||||||
let content: ReactNode;
|
let content: ReactNode;
|
||||||
|
|
||||||
switch (this.props.renderMode) {
|
switch (this.props.renderMode) {
|
||||||
case RenderModes.CANVAS:
|
case RenderModes.CANVAS:
|
||||||
content = this.getCanvasView();
|
content = this.getCanvasView();
|
||||||
if (!this.props.detachFromLayout) {
|
if (!this.props.detachFromLayout) {
|
||||||
content = this.makeResizable(content);
|
if (!this.props.resizeDisabled) content = this.makeResizable(content);
|
||||||
content = this.showWidgetName(content);
|
content = this.showWidgetName(content);
|
||||||
content = this.makeDraggable(content);
|
content = this.makeDraggable(content);
|
||||||
content = this.makePositioned(content);
|
content = this.makePositioned(content);
|
||||||
|
|
@ -257,17 +280,22 @@ abstract class BaseWidget<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generates styles that positions the widget
|
||||||
|
*/
|
||||||
private getPositionStyle(): BaseStyle {
|
private getPositionStyle(): BaseStyle {
|
||||||
const { componentHeight, componentWidth } = this.getComponentDimensions();
|
const { componentHeight, componentWidth } = this.getComponentDimensions();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
positionType: PositionTypes.ABSOLUTE,
|
positionType: PositionTypes.ABSOLUTE,
|
||||||
componentHeight,
|
componentHeight,
|
||||||
componentWidth,
|
componentWidth,
|
||||||
yPosition:
|
yPosition:
|
||||||
this.props.topRow * this.props.parentRowSpace + CONTAINER_GRID_PADDING,
|
this.props.topRow * this.props.parentRowSpace +
|
||||||
|
(this.props.noContainerOffset ? 0 : CONTAINER_GRID_PADDING),
|
||||||
xPosition:
|
xPosition:
|
||||||
this.props.leftColumn * this.props.parentColumnSpace +
|
this.props.leftColumn * this.props.parentColumnSpace +
|
||||||
CONTAINER_GRID_PADDING,
|
(this.props.noContainerOffset ? 0 : CONTAINER_GRID_PADDING),
|
||||||
xPositionUnit: CSSUnits.PIXEL,
|
xPositionUnit: CSSUnits.PIXEL,
|
||||||
yPositionUnit: CSSUnits.PIXEL,
|
yPositionUnit: CSSUnits.PIXEL,
|
||||||
};
|
};
|
||||||
|
|
@ -281,6 +309,11 @@ abstract class BaseWidget<
|
||||||
leftColumn: 0,
|
leftColumn: 0,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
renderMode: RenderModes.CANVAS,
|
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,
|
// Examples: MainContainer is detached from layout,
|
||||||
// MODAL_WIDGET is also detached from layout.
|
// MODAL_WIDGET is also detached from layout.
|
||||||
detachFromLayout?: boolean;
|
detachFromLayout?: boolean;
|
||||||
|
noContainerOffset?: boolean; // This won't offset the child in parent
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WIDGET_STATIC_PROPS = {
|
export const WIDGET_STATIC_PROPS = {
|
||||||
|
|
@ -345,6 +379,7 @@ export const WIDGET_STATIC_PROPS = {
|
||||||
parentId: true,
|
parentId: true,
|
||||||
renderMode: true,
|
renderMode: true,
|
||||||
detachFromLayout: true,
|
detachFromLayout: true,
|
||||||
|
noContainerOffset: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface WidgetDisplayProps {
|
export interface WidgetDisplayProps {
|
||||||
|
|
@ -373,6 +408,7 @@ export interface WidgetCardProps {
|
||||||
type: WidgetType;
|
type: WidgetType;
|
||||||
key?: string;
|
key?: string;
|
||||||
widgetCardName: string;
|
widgetCardName: string;
|
||||||
|
isBeta?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WidgetOperations = {
|
export const WidgetOperations = {
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,9 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onButtonClick() {
|
onButtonClick(e: React.MouseEvent<HTMLElement>) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
if (this.props.onClick) {
|
if (this.props.onClick) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import DropTargetComponent from "components/editorComponents/DropTargetComponent
|
||||||
import { getCanvasSnapRows } from "utils/WidgetPropsUtils";
|
import { getCanvasSnapRows } from "utils/WidgetPropsUtils";
|
||||||
import { getCanvasClassName } from "utils/generators";
|
import { getCanvasClassName } from "utils/generators";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
import WidgetFactory from "utils/WidgetFactory";
|
||||||
|
|
||||||
class CanvasWidget extends ContainerWidget {
|
class CanvasWidget extends ContainerWidget {
|
||||||
static getPropertyPaneConfig() {
|
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() {
|
getPageView() {
|
||||||
|
let height = 0;
|
||||||
const snapRows = getCanvasSnapRows(
|
const snapRows = getCanvasSnapRows(
|
||||||
this.props.bottomRow,
|
this.props.bottomRow,
|
||||||
this.props.canExtend,
|
this.props.canExtend,
|
||||||
);
|
);
|
||||||
const height = snapRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
|
height = snapRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
|
||||||
|
|
||||||
const style: CSSProperties = {
|
const style: CSSProperties = {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
|
|
@ -61,6 +80,7 @@ class CanvasWidget extends ContainerWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCanvasView() {
|
getCanvasView() {
|
||||||
|
if (this.props.dropDisabled) return this.getPageView();
|
||||||
return this.renderAsDropTarget();
|
return this.renderAsDropTarget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
import React from "react";
|
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 {
|
import {
|
||||||
GridDefaults,
|
GridDefaults,
|
||||||
CONTAINER_GRID_PADDING,
|
CONTAINER_GRID_PADDING,
|
||||||
WIDGET_PADDING,
|
WIDGET_PADDING,
|
||||||
} from "constants/WidgetConstants";
|
} 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 BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import { VALIDATION_TYPES } from "constants/WidgetValidation";
|
import { VALIDATION_TYPES } from "constants/WidgetValidation";
|
||||||
|
|
||||||
class ContainerWidget extends BaseWidget<
|
class ContainerWidget extends BaseWidget<
|
||||||
|
|
@ -64,11 +63,15 @@ class ContainerWidget extends BaseWidget<
|
||||||
|
|
||||||
getSnapSpaces = () => {
|
getSnapSpaces = () => {
|
||||||
const { componentWidth } = this.getComponentDimensions();
|
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 {
|
return {
|
||||||
snapRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
|
snapRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
|
||||||
snapColumnSpace: componentWidth
|
snapColumnSpace: componentWidth
|
||||||
? (componentWidth - (CONTAINER_GRID_PADDING + WIDGET_PADDING) * 2) /
|
? width / GridDefaults.DEFAULT_GRID_COLUMNS
|
||||||
GridDefaults.DEFAULT_GRID_COLUMNS
|
|
||||||
: 0,
|
: 0,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -79,24 +82,16 @@ class ContainerWidget extends BaseWidget<
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapSpaces = this.getSnapSpaces();
|
|
||||||
const { componentWidth, componentHeight } = this.getComponentDimensions();
|
const { componentWidth, componentHeight } = this.getComponentDimensions();
|
||||||
|
|
||||||
if (childWidgetData.type !== WidgetTypes.CANVAS_WIDGET) {
|
childWidgetData.rightColumn = componentWidth;
|
||||||
childWidgetData.parentColumnSpace = snapSpaces.snapColumnSpace;
|
childWidgetData.bottomRow = this.props.shouldScrollContents
|
||||||
childWidgetData.parentRowSpace = snapSpaces.snapRowSpace;
|
? childWidgetData.bottomRow
|
||||||
} else {
|
: componentHeight;
|
||||||
// This is for the detached child like the default CANVAS_WIDGET child
|
childWidgetData.minHeight = componentHeight;
|
||||||
|
childWidgetData.isVisible = this.props.isVisible;
|
||||||
childWidgetData.rightColumn = componentWidth;
|
childWidgetData.shouldScrollContents = false;
|
||||||
childWidgetData.bottomRow = this.props.shouldScrollContents
|
childWidgetData.canExtend = 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;
|
childWidgetData.parentId = this.props.widgetId;
|
||||||
|
|
||||||
|
|
@ -104,11 +99,11 @@ class ContainerWidget extends BaseWidget<
|
||||||
}
|
}
|
||||||
|
|
||||||
renderChildren = () => {
|
renderChildren = () => {
|
||||||
return _.map(
|
return map(
|
||||||
// sort by row so stacking context is correct
|
// 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.
|
// 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.
|
// 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,
|
this.renderChildWidget,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -135,6 +130,7 @@ export interface ContainerWidgetProps<T extends WidgetProps>
|
||||||
children?: T[];
|
children?: T[];
|
||||||
containerStyle?: ContainerStyle;
|
containerStyle?: ContainerStyle;
|
||||||
shouldScrollContents?: boolean;
|
shouldScrollContents?: boolean;
|
||||||
|
noPad?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContainerWidget;
|
export default ContainerWidget;
|
||||||
|
|
|
||||||
67
app/client/src/widgets/ListWidget/ListComponent.tsx
Normal file
67
app/client/src/widgets/ListWidget/ListComponent.tsx
Normal 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;
|
||||||
326
app/client/src/widgets/ListWidget/ListPagination.tsx
Normal file
326
app/client/src/widgets/ListWidget/ListPagination.tsx
Normal 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;
|
||||||
86
app/client/src/widgets/ListWidget/ListPropertyPaneConfig.ts
Normal file
86
app/client/src/widgets/ListWidget/ListPropertyPaneConfig.ts
Normal 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 };
|
||||||
76
app/client/src/widgets/ListWidget/ListWidget.test.tsx
Normal file
76
app/client/src/widgets/ListWidget/ListWidget.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
569
app/client/src/widgets/ListWidget/ListWidget.tsx
Normal file
569
app/client/src/widgets/ListWidget/ListWidget.tsx
Normal 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));
|
||||||
1
app/client/src/widgets/ListWidget/index.tsx
Normal file
1
app/client/src/widgets/ListWidget/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { ProfiledListWidget, default } from "./ListWidget";
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,6 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
|
||||||
}
|
}
|
||||||
return { isValid: true, parsed, transformed: parsed };
|
return { isValid: true, parsed, transformed: parsed };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
parsed: [],
|
parsed: [],
|
||||||
|
|
@ -259,6 +258,44 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
|
||||||
}
|
}
|
||||||
return { isValid, parsed };
|
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]: (
|
[VALIDATION_TYPES.TABLE_DATA]: (
|
||||||
value: any,
|
value: any,
|
||||||
props: WidgetProps,
|
props: WidgetProps,
|
||||||
|
|
@ -468,7 +505,6 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
|
||||||
}
|
}
|
||||||
return { isValid, parsed };
|
return { isValid, parsed };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
parsed: [],
|
parsed: [],
|
||||||
|
|
|
||||||
|
|
@ -1624,7 +1624,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.12.5":
|
"@babel/runtime@^7.10.1", "@babel/runtime@^7.12.5":
|
||||||
version "7.13.10"
|
version "7.13.10"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
|
||||||
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
|
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
|
||||||
|
|
@ -3424,10 +3424,10 @@
|
||||||
lodash "^4.17.15"
|
lodash "^4.17.15"
|
||||||
redent "^3.0.0"
|
redent "^3.0.0"
|
||||||
|
|
||||||
"@testing-library/react@^11.2.5":
|
"@testing-library/react@^11.2.6":
|
||||||
version "11.2.5"
|
version "11.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.5.tgz#ae1c36a66c7790ddb6662c416c27863d87818eb9"
|
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b"
|
||||||
integrity sha512-yEx7oIa/UWLe2F2dqK0FtMF9sJWNXD+2PPtp39BvE0Kh9MJ9Kl0HrZAgEuhUJR+Lx8Di6Xz+rKwSdEPY2UV8ZQ==
|
integrity sha512-TXMCg0jT8xmuU8BkKMtp8l7Z50Ykew5WNX8UoIKTaLFwKkP2+1YDhOLA2Ga3wY4x29jyntk7EWfum0kjlYiSjQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@testing-library/dom" "^7.28.1"
|
"@testing-library/dom" "^7.28.1"
|
||||||
|
|
@ -4002,7 +4002,21 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/yargs-parser" "*"
|
"@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"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.6.0.tgz#210cd538bb703f883aff81d3996961f5dba31fdb"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.6.0.tgz#210cd538bb703f883aff81d3996961f5dba31fdb"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -4014,6 +4028,18 @@
|
||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
tsutils "^3.17.1"
|
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":
|
"@typescript-eslint/experimental-utils@4.6.0", "@typescript-eslint/experimental-utils@^4.0.1":
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.6.0.tgz#f750aef4dd8e5970b5c36084f0a5ca2f0db309a4"
|
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-scope "^5.0.0"
|
||||||
eslint-utils "^2.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"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.6.0.tgz#7e9ff7df2f21d5c8f65f17add3b99eeeec33199d"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.6.0.tgz#7e9ff7df2f21d5c8f65f17add3b99eeeec33199d"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -4044,6 +4080,14 @@
|
||||||
"@typescript-eslint/typescript-estree" "4.6.0"
|
"@typescript-eslint/typescript-estree" "4.6.0"
|
||||||
debug "^4.1.1"
|
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":
|
"@typescript-eslint/scope-manager@4.6.0":
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.6.0.tgz#b7d8b57fe354047a72dfb31881d9643092838662"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.6.0.tgz#b7d8b57fe354047a72dfb31881d9643092838662"
|
||||||
|
|
@ -4055,6 +4099,11 @@
|
||||||
version "3.10.1"
|
version "3.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727"
|
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":
|
"@typescript-eslint/types@4.6.0":
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.6.0.tgz#157ca925637fd53c193c6bf226a6c02b752dde2f"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.6.0.tgz#157ca925637fd53c193c6bf226a6c02b752dde2f"
|
||||||
|
|
@ -4072,6 +4121,19 @@
|
||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
tsutils "^3.17.1"
|
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":
|
"@typescript-eslint/typescript-estree@4.6.0":
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.6.0.tgz#85bd98dcc8280511cfc5b2ce7b03a9ffa1732b08"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.6.0.tgz#85bd98dcc8280511cfc5b2ce7b03a9ffa1732b08"
|
||||||
|
|
@ -4091,6 +4153,14 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint-visitor-keys "^1.1.0"
|
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":
|
"@typescript-eslint/visitor-keys@4.6.0":
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.6.0.tgz#fb05d6393891b0a089b243fc8f9fb8039383d5da"
|
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"
|
isobject "^3.0.0"
|
||||||
static-extend "^0.1.1"
|
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"
|
version "2.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
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"
|
loader-utils "^2.0.0"
|
||||||
schema-utils "^3.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:
|
re-reselect@^3.4.0:
|
||||||
version "3.4.0"
|
version "3.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/re-reselect/-/re-reselect-3.4.0.tgz#0f2303f3c84394f57f0cd31fea08a1ca4840a7cd"
|
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"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c"
|
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:
|
tslib@~1.13.0:
|
||||||
version "1.13.0"
|
version "1.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
|
||||||
|
|
@ -16604,9 +16687,10 @@ typescript-tuple@^2.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript-compare "^0.0.2"
|
typescript-compare "^0.0.2"
|
||||||
|
|
||||||
typescript@^3.9.2:
|
typescript@^4.1.3:
|
||||||
version "3.9.7"
|
version "4.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72"
|
||||||
|
integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==
|
||||||
|
|
||||||
ua-parser-js@^0.7.18:
|
ua-parser-js@^0.7.18:
|
||||||
version "0.7.22"
|
version "0.7.22"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user