-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## Description This PR's adds the custom widget. #### PR fixes following issue(s) Fixes #28607 Fixes #28610 Fixes #28615 Fixes #28608 Fixes #28612 > if no issue exists, please create an issue and ask the maintainers about this first > > #### Media > A video or a GIF is preferred. when using Loom, don’t embed because it looks like it’s a GIF. instead, just link to the video > > #### Type of change - New feature (non-breaking change which adds functionality) > > > ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [ ] Manual - [ ] JUnit - [x] Jest - [x] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced search functionality with a wildcard option for unmatched widget searches. - Added new routes for custom widget editing. - Implemented additional editor modes for HTML and CSS. - Created new code templates and help components for custom widget builders. - Enhanced property controls with new button controls for custom widget editing. - Updated theming interfaces and constants for better theming support. - Added the CustomWidget component for embedding custom widgets in iframes. - Expanded widgets library to include the CustomWidget. - **Enhancements** - Improved `CodeEditor` with additional modes, props, and resize behavior. - Enhanced `PropertyPaneControlConfig` interface with dynamic dependencies and additional properties. - Refined custom widget scripts with communication channel and event handling. - **Bug Fixes** - Fixed visibility logic for the `ExternalWidget` card. - **Documentation** - Added new messages and documentation links for custom widget features. - **Tests** - Implemented new Cypress tests for custom widget default components and property pane interactions. - Updated workspace commands in Cypress tests. - **Refactor** - Streamlined `PropertyControl` state management and editing functions. - Refactored code editor hint helper logic. - **Style** - Added `borderLess` prop to style components without borders. - **Chores** - Updated constants and messages related to custom widget features. - Adjusted webpack configuration to ignore specific module warnings. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Loading branch information
balajisoundar
authored
Dec 28, 2023
1 parent
f8ef73b
commit 837d0cc
Showing
101 changed files
with
5,765 additions
and
151 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
...ent/cypress/e2e/Regression/ClientSide/Widgets/Custom/CustomWidgetDefaultComponent_spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { | ||
agHelper, | ||
deployMode, | ||
entityExplorer, | ||
} from "../../../../../support/Objects/ObjectsCore"; | ||
|
||
describe( | ||
"Custom widget Tests", | ||
{ tags: ["@tag.Widget", "@tag.Custom"] }, | ||
function () { | ||
before(() => { | ||
entityExplorer.DragDropWidgetNVerify("customwidget", 550, 100); | ||
cy.wait(5000); | ||
}); | ||
|
||
const getIframeBody = () => { | ||
// get the iframe > document > body | ||
// and retry until the body element is not empty | ||
return ( | ||
cy | ||
.get(".t--widget-customwidget iframe") | ||
.its("0.contentDocument.body") | ||
.should("not.be.empty") | ||
// wraps "body" DOM element to allow | ||
// chaining more Cypress commands, like ".find(...)" | ||
// https://on.cypress.io/wrap | ||
.then(cy.wrap) | ||
); | ||
}; | ||
|
||
it("should check that custom widget default component loaded and working properly", () => { | ||
getIframeBody().find(".tip-container").should("exist"); | ||
getIframeBody() | ||
.find(".tip-container p") | ||
.should( | ||
"have.text", | ||
"Pass data to this widget in the default model field", | ||
); | ||
|
||
getIframeBody().find("button.primary").trigger("click"); | ||
|
||
getIframeBody() | ||
.find(".tip-container p") | ||
.should( | ||
"have.text", | ||
"Access data in the javascript file using the appsmith.model variable", | ||
); | ||
|
||
getIframeBody().find("button.reset").trigger("click"); | ||
|
||
agHelper.ValidateToastMessage("Successfully reset!!"); | ||
}); | ||
|
||
it("should check that custom widget default component loaded and working properly in published mode", () => { | ||
deployMode.DeployApp(); | ||
|
||
getIframeBody().find(".tip-container").should("exist"); | ||
getIframeBody() | ||
.find(".tip-container p") | ||
.should( | ||
"have.text", | ||
"Pass data to this widget in the default model field", | ||
); | ||
|
||
getIframeBody().find("button.primary").trigger("click"); | ||
|
||
getIframeBody() | ||
.find(".tip-container p") | ||
.should( | ||
"have.text", | ||
"Access data in the javascript file using the appsmith.model variable", | ||
); | ||
|
||
getIframeBody().find("button.reset").trigger("click"); | ||
|
||
agHelper.ValidateToastMessage("Successfully reset!!"); | ||
}); | ||
}, | ||
); |
48 changes: 48 additions & 0 deletions
48
...t/cypress/e2e/Regression/ClientSide/Widgets/Custom/CustomWidgetEditorPropertyPane_spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { | ||
agHelper, | ||
deployMode, | ||
entityExplorer, | ||
propPane, | ||
} from "../../../../../support/Objects/ObjectsCore"; | ||
|
||
describe( | ||
"Custom widget Tests", | ||
{ tags: ["@tag.Widget", "@tag.Custom"] }, | ||
function () { | ||
before(() => { | ||
agHelper.AddDsl("customWidget"); | ||
cy.wait(5000); | ||
}); | ||
|
||
const getIframeBody = () => { | ||
// get the iframe > document > body | ||
// and retry until the body element is not empty | ||
return ( | ||
cy | ||
.get(".t--widget-customwidget iframe") | ||
.its("0.contentDocument.body") | ||
.should("not.be.empty") | ||
// wraps "body" DOM element to allow | ||
// chaining more Cypress commands, like ".find(...)" | ||
// https://on.cypress.io/wrap | ||
.then(cy.wrap) | ||
); | ||
}; | ||
|
||
it("shoud check that default model changes are converyed to custom component", () => { | ||
getIframeBody().find(".tip-container").should("exist"); | ||
|
||
agHelper.GetElement(".t--text-widget-container").should("have.text", ""); | ||
|
||
getIframeBody().find("button.primary").trigger("click"); | ||
|
||
agHelper.GetElement(".t--text-widget-container").should("have.text", "1"); | ||
|
||
getIframeBody().find("button.reset").trigger("click"); | ||
|
||
agHelper.ValidateToastMessage("Successfully reset from 1"); | ||
|
||
agHelper.GetElement(".t--text-widget-container").should("have.text", "0"); | ||
}); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
{ | ||
"dsl": { | ||
"widgetName": "MainContainer", | ||
"backgroundColor": "none", | ||
"rightColumn": 4896, | ||
"snapColumns": 64, | ||
"detachFromLayout": true, | ||
"widgetId": "0", | ||
"topRow": 0, | ||
"bottomRow": 400, | ||
"containerStyle": "none", | ||
"snapRows": 124, | ||
"parentRowSpace": 1, | ||
"type": "CANVAS_WIDGET", | ||
"canExtend": true, | ||
"version": 87, | ||
"minHeight": 1292, | ||
"dynamicTriggerPathList": [], | ||
"parentColumnSpace": 1, | ||
"dynamicBindingPathList": [], | ||
"leftColumn": 0, | ||
"children": [ | ||
{ | ||
"mobileBottomRow": 33, | ||
"widgetName": "Custom1", | ||
"srcDoc": { | ||
"html": "<!-- no need to write html, head, body tags, it is handled by the widget -->\n<div id=\"root\"></div>\n", | ||
"css": ".app {\n\theight: calc(var(--appsmith-ui-height) * 1px);\n\twidth: calc(var(--appsmith-ui-width) * 1px);\n\tjustify-content: center;\n\tborder-radius: var(--appsmith-theme-borderRadius);\n\tbox-shadow: var(--appsmith-theme-boxShadow);\n}\n\n.tip-container {\n margin-bottom: 20px;\n}\n\n.tip-container h2 {\n margin-bottom: 20px;\n\tfont-size: 16px;\n\tfont-weight: 700;\n}\n\n.tip-header {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: baseline;\n}\n\n.tip-header div {\n\tcolor: #999;\n}\n\n.button-container {\n\ttext-align: right;\t\n}\n\n.button-container button {\n margin: 0 10px;\n}\n\n.button-container button.primary {\n\tbackground: var(--appsmith-theme-primaryColor) !important;\n}\n\n.button-container button.reset {\n\tcolor: var(--appsmith-theme-primaryColor) !important;\n\tborder-color: var(--appsmith-theme-primaryColor) !important;\n}", | ||
"js": "import React from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';\nimport reactDom from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';\nimport { Button, Card } from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';\nimport Markdown from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';\nfunction App() {\n const [currentIndex, setCurrentIndex] = React.useState(0);\n const [key, setKey] = React.useState(0);\n const handleNext = () => {\n const index = (currentIndex + 1) % appsmith.model.tips.length;\n setCurrentIndex(index);\n appsmith.updateModel({\n currentIndex: index\n });\n };\n const handleReset = () => {\n setCurrentIndex(0);\n appsmith.updateModel({\n currentIndex: 0\n });\n appsmith.triggerEvent(\"onResetClick\", {\n oldIndex: currentIndex\n });\n };\n React.useEffect(() => {\n appsmith.onModelChange((model, prevModel) => {\n if (JSON.stringify(prevModel?.tips) !== JSON.stringify(model.tips)) {\n setKey(Math.random());\n }\n });\n }, []);\n return /*#__PURE__*/React.createElement(Card, {\n className: \"app\",\n key: key\n }, /*#__PURE__*/React.createElement(\"div\", {\n className: \"tip-container\"\n }, /*#__PURE__*/React.createElement(\"div\", {\n className: \"tip-header\"\n }, /*#__PURE__*/React.createElement(\"h2\", null, \"Custom Widget\"), /*#__PURE__*/React.createElement(\"div\", null, currentIndex + 1, \" / \", appsmith.model.tips.length, \" \")), /*#__PURE__*/React.createElement(Markdown, null, appsmith.model.tips[currentIndex])), /*#__PURE__*/React.createElement(\"div\", {\n className: \"button-container\"\n }, /*#__PURE__*/React.createElement(Button, {\n className: \"primary\",\n onClick: handleNext,\n type: \"primary\"\n }, \"Next Tip\"), /*#__PURE__*/React.createElement(Button, {\n className: \"reset\",\n onClick: handleReset\n }, \"Reset\")));\n}\nappsmith.onReady(() => {\n reactDom.render( /*#__PURE__*/React.createElement(App, null), document.getElementById(\"root\"));\n});" | ||
}, | ||
"isCanvas": false, | ||
"displayName": "Custom", | ||
"iconSVG": "/static/media/icon.867bcc8399fa3f897d425d72690b86e4.svg", | ||
"searchTags": [ | ||
"external" | ||
], | ||
"topRow": 3, | ||
"bottomRow": 33, | ||
"parentRowSpace": 10, | ||
"type": "CUSTOM_WIDGET", | ||
"hideCard": false, | ||
"mobileRightColumn": 41, | ||
"parentColumnSpace": 19.296875, | ||
"dynamicTriggerPathList": [ | ||
{ | ||
"key": "onResetClick" | ||
} | ||
], | ||
"dynamicBindingPathList": [ | ||
{ | ||
"key": "theme" | ||
} | ||
], | ||
"leftColumn": 21, | ||
"defaultModel": "{\n \"tips\": [\n \"Pass data to this widget in the default model field\",\n \"Access data in the javascript file using the appsmith.model variable\",\n \"Create events in the widget and trigger them in the javascript file using appsmith.triggerEvent('eventName')\",\n \"Access data in CSS as var(--appsmith-model-{property-name})\"\n ]\n}", | ||
"theme": "{{appsmith.theme}}", | ||
"onResetClick": "{{showAlert('Successfully reset from ' + oldIndex, '');}}", | ||
"events": [ | ||
"onResetClick" | ||
], | ||
"key": "nuzm5titx7", | ||
"isDeprecated": false, | ||
"rightColumn": 41, | ||
"isSearchWildcard": true, | ||
"widgetId": "giszumqrjp", | ||
"isVisible": true, | ||
"version": 1, | ||
"uncompiledSrcDoc": { | ||
"html": "<!-- no need to write html, head, body tags, it is handled by the widget -->\n<div id=\"root\"></div>\n", | ||
"css": ".app {\n\theight: calc(var(--appsmith-ui-height) * 1px);\n\twidth: calc(var(--appsmith-ui-width) * 1px);\n\tjustify-content: center;\n\tborder-radius: var(--appsmith-theme-borderRadius);\n\tbox-shadow: var(--appsmith-theme-boxShadow);\n}\n\n.tip-container {\n margin-bottom: 20px;\n}\n\n.tip-container h2 {\n margin-bottom: 20px;\n\tfont-size: 16px;\n\tfont-weight: 700;\n}\n\n.tip-header {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: baseline;\n}\n\n.tip-header div {\n\tcolor: #999;\n}\n\n.button-container {\n\ttext-align: right;\t\n}\n\n.button-container button {\n margin: 0 10px;\n}\n\n.button-container button.primary {\n\tbackground: var(--appsmith-theme-primaryColor) !important;\n}\n\n.button-container button.reset {\n\tcolor: var(--appsmith-theme-primaryColor) !important;\n\tborder-color: var(--appsmith-theme-primaryColor) !important;\n}", | ||
"js": "import React from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm'\nimport reactDom from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm'\nimport { Button, Card } from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm'\nimport Markdown from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';\n\nfunction App() {\n\tconst [currentIndex, setCurrentIndex] = React.useState(0);\n\t\n\tconst [key, setKey] = React.useState(0);\n\n\tconst handleNext = () => {\n\t\tconst index = (currentIndex + 1) % appsmith.model.tips.length\n\t\tsetCurrentIndex(index);\n\t\tappsmith.updateModel({\n\t\t\tcurrentIndex: index\n\t\t});\n\t};\n\n\tconst handleReset = () => {\n\t\tsetCurrentIndex(0);\n\t\tappsmith.updateModel({\n\t\t\tcurrentIndex: 0\n\t\t});\n\t\tappsmith.triggerEvent(\"onResetClick\", {\n\t\t\toldIndex: currentIndex\n\t\t});\n\t};\n\t\n\tReact.useEffect(() => {\t\t\n\t\tappsmith.onModelChange((model, prevModel) => {\n\t\t\tif (JSON.stringify(prevModel?.tips) !== JSON.stringify(model.tips)) {\n\t\t\t\tsetKey(Math.random());\n\t\t\t}\n\t\t});\n\t}, []);\n\t\n\n\treturn (\n\t\t<Card className=\"app\" key={key}>\n\t\t\t<div className=\"tip-container\">\n\t\t\t\t<div className=\"tip-header\">\n\t\t\t\t\t<h2>Custom Widget</h2>\n\t\t\t\t\t<div>{currentIndex + 1} / {appsmith.model.tips.length}\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Markdown>{appsmith.model.tips[currentIndex]}</Markdown>\n\t\t\t</div>\n\t\t\t<div className=\"button-container\">\n\t\t\t\t<Button className=\"primary\" onClick={handleNext} type=\"primary\">Next Tip</Button>\n\t\t\t\t<Button className=\"reset\" onClick={handleReset}>Reset</Button>\n\t\t\t</div>\n\t</Card>\n);\n}\n\nappsmith.onReady(() => {\n\treactDom.render(<App />, document.getElementById(\"root\"));\n});" | ||
}, | ||
"parentId": "0", | ||
"tags": [ | ||
"Display" | ||
], | ||
"renderMode": "CANVAS", | ||
"isLoading": false, | ||
"mobileTopRow": 3, | ||
"mobileLeftColumn": 21, | ||
"dynamicPropertyPathList": [ | ||
{ | ||
"key": "onResetClick" | ||
} | ||
] | ||
}, | ||
{ | ||
"isVisible": true, | ||
"type": "TEXT_WIDGET", | ||
"text": "{{Custom1.model.currentIndex}}", | ||
"fontSize": "1rem", | ||
"fontStyle": "BOLD", | ||
"textAlign": "LEFT", | ||
"textColor": "#231F20", | ||
"widgetName": "Text1", | ||
"shouldTruncate": false, | ||
"overflow": "NONE", | ||
"version": 1, | ||
"animateLoading": true, | ||
"responsiveBehavior": "fill", | ||
"minWidth": 450, | ||
"minDynamicHeight": 4, | ||
"maxDynamicHeight": 9000, | ||
"dynamicHeight": "AUTO_HEIGHT", | ||
"searchTags": [ | ||
"typography", | ||
"paragraph", | ||
"label" | ||
], | ||
"tags": [ | ||
"Suggested", | ||
"Content" | ||
], | ||
"hideCard": false, | ||
"isDeprecated": false, | ||
"displayName": "Text", | ||
"key": "my0l36gkcf", | ||
"iconSVG": "/static/media/icon.a47d6d5dbbb718c4dc4b2eb4f218c1b7.svg", | ||
"widgetId": "144bnot1xa", | ||
"renderMode": "CANVAS", | ||
"truncateButtonColor": "{{appsmith.theme.colors.primaryColor}}", | ||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}", | ||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", | ||
"isLoading": false, | ||
"parentColumnSpace": 19.296875, | ||
"parentRowSpace": 10, | ||
"leftColumn": 23, | ||
"rightColumn": 40, | ||
"topRow": 36, | ||
"bottomRow": 40, | ||
"mobileLeftColumn": 23, | ||
"mobileRightColumn": 39, | ||
"mobileTopRow": 38, | ||
"mobileBottomRow": 42, | ||
"parentId": "0", | ||
"dynamicBindingPathList": [ | ||
{ | ||
"key": "truncateButtonColor" | ||
}, | ||
{ | ||
"key": "fontFamily" | ||
}, | ||
{ | ||
"key": "borderRadius" | ||
}, | ||
{ | ||
"key": "text" | ||
} | ||
], | ||
"dynamicTriggerPathList": [], | ||
"originalTopRow": 38, | ||
"originalBottomRow": 42 | ||
} | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,3 @@ | ||
import Widget from "./widget"; | ||
import IconSVG from "./icon.svg"; | ||
|
||
export const CONFIG = { | ||
type: Widget.getWidgetType(), | ||
name: "{{name}}", // The display name which will be made in uppercase and show in the widgets panel ( can have spaces ) | ||
iconSVG: IconSVG, | ||
needsMeta: false, // Defines if this widget adds any meta properties | ||
isCanvas: false, // Defines if this widget has a canvas within in which we can drop other widgets | ||
features: { | ||
dynamicHeight: { | ||
sectionIndex: 0, // Index of the property pane "General" section | ||
active: false, | ||
}, | ||
}, | ||
defaults: { | ||
widgetName: "{{name}}", | ||
rows: 1, | ||
columns: 3, | ||
version: 1, | ||
}, | ||
properties: { | ||
derived: Widget.getDerivedPropertiesMap(), | ||
default: Widget.getDefaultPropertiesMap(), | ||
meta: Widget.getMetaPropertiesMap(), | ||
contentConfig: Widget.getPropertyPaneContentConfig(), | ||
styleConfig: Widget.getPropertyPaneStyleConfig(), | ||
autocompleteDefinitions: Widget.getAutocompleteDefinitions() | ||
}, | ||
}; | ||
|
||
export default Widget; |
Oops, something went wrong.