chore: custom widget onReady warning and template updates (#30335)

## Description
Update tempaltes and add warning message when onReady function is
missing.

> 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
> Please delete options that are not relevant.
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- Chore (housekeeping or task changes that don't impact user perception)
- This change requires a documentation update
>
>
>
## 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
- [ ] Jest
- [ ] 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
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] 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 a new Vanilla JavaScript template for custom widgets.
- Enhanced Vue.js custom widget template with new design and
functionality.
- Added a warning system to alert users when certain expected code
patterns are missing.

- **Bug Fixes**
	- Updated the `appsmithConsole` to include error handling.

- **Documentation**
	- Added new documentation URLs for custom widget development guidance.

- **Refactor**
	- Replaced the `blank` module with `vanillaJs` in code templates.
	- Removed unused styles and code comments from React template.
	- Streamlined default context values for widget development.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
balajisoundar 2024-01-17 19:44:14 +05:30 committed by GitHub
parent 1b9f3af763
commit f376d362e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 317 additions and 134 deletions

View File

@ -2356,6 +2356,7 @@ export const CUSTOM_WIDGET_FEATURE = {
},
templateKey: {
blank: () => "Blank",
vanillaJs: () => "Vanilla JS",
react: () => "React",
vue: () => "Vue",
},
@ -2411,6 +2412,8 @@ export const CUSTOM_WIDGET_FEATURE = {
helpDropdown: {
stackoverflow: () => "Search StackOverflow",
},
noOnReadyWarning: (url: string) =>
`Missing appsmith.onReady() function call. Initiate your component inside 'appsmith.onReady()' for your custom widget to work as expected. For more information - ${url}`,
},
preview: {
eventFired: () => "Event fired:",

View File

@ -432,6 +432,7 @@ export type VERSION_UPDATE_EVENTS =
| "VERSION_UPDATED_FAILED";
export type CUSTOM_WIDGET_EVENTS =
| "CUSTOM_WIDGET_LOAD_INIT"
| "CUSTOM_WIDGET_EDIT_SOURCE_CLICKED"
| "CUSTOM_WIDGET_ADD_EVENT_CLICKED"
| "CUSTOM_WIDGET_ADD_EVENT_CANCEL_CLICKED"
@ -450,4 +451,6 @@ export type CUSTOM_WIDGET_EVENTS =
| "CUSTOM_WIDGET_BUILDER_REFERENCE_VISIBILITY_CHANGED"
| "CUSTOM_WIDGET_BUILDER_REFERENCE_EVENT_OPENED"
| "CUSTOM_WIDGET_BUILDER_DEBUGGER_CLEARED"
| "CUSTOM_WIDGET_BUILDER_DEBUGGER_VISIBILITY_CHANGED";
| "CUSTOM_WIDGET_BUILDER_DEBUGGER_VISIBILITY_CHANGED"
| "CUSTOM_WIDGET_API_TRIGGER_EVENT"
| "CUSTOM_WIDGET_API_UPDATE_MODEL";

View File

@ -1,13 +0,0 @@
import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
export default {
key: createMessage(CUSTOM_WIDGET_FEATURE.templateKey.blank),
uncompiledSrcDoc: {
html: "",
css: "",
js: "",
},
};

View File

@ -1,5 +1,5 @@
import blank from "./blank";
import vanillaJs from "./vanillaJs";
import react from "./react";
import vue from "./vue";
export default [blank, react, vue];
export default [vanillaJs, react, vue];

View File

@ -2,6 +2,7 @@ import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants";
export default {
key: createMessage(CUSTOM_WIDGET_FEATURE.templateKey.react),
@ -43,6 +44,7 @@ export default {
.button-container button {
margin: 0 10px;
border-radius: var(--appsmith-theme-borderRadius);
}
.button-container button.primary {
@ -58,10 +60,6 @@ import reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm'
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm'
import Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm'
const style = {
maxWidth: "400px",
}
function App() {
const [currentIndex, setCurrentIndex] = React.useState(0);
@ -75,7 +73,7 @@ function App() {
};
return (
<Card className="app" style={style}>
<Card className="app">
<div className="tip-container">
<div className="tip-header">
<h2>Custom Widget</h2>
@ -92,92 +90,12 @@ function App() {
}
appsmith.onReady(() => {
/*
* This handler function will get called when parent application is ready.
* Initialize your component here
* more info - ${CUSTOM_WIDGET_ONREADY_DOC_URL}
*/
reactDom.render(<App />, document.getElementById("root"));
});`,
},
srcDoc: {
html: `<!-- no need to write html, head, body tags, it is handled by the widget -->
<div id="root"></div>
`,
css: `.app {
height: calc(var(--appsmith-ui-height) * 1px);
width: calc(var(--appsmith-ui-width) * 1px);
justify-content: center;
border-radius: var(--appsmith-theme-borderRadius);
box-shadow: var(--appsmith-theme-boxShadow);
}
.tip-container {
margin-bottom: 20px;
}
.tip-container h2 {
margin-bottom: 20px;
font-size: 16px;
font-weight: 700;
}
.tip-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.tip-header div {
color: #999;
}
.button-container {
text-align: right;
}
.button-container button {
margin: 0 10px;
}
.button-container button.primary {
background: var(--appsmith-theme-primaryColor) !important;
}
.button-container button.reset {
color: var(--appsmith-theme-primaryColor) !important;
border-color: var(--appsmith-theme-primaryColor) !important;
}`,
js: `import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm';
import reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm';
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm';
import Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm';
const style = {
maxWidth: "400px"
};
function App() {
const [currentIndex, setCurrentIndex] = React.useState(0);
const handleNext = () => {
setCurrentIndex(prevIndex => (prevIndex + 1) % appsmith.model.tips.length);
};
const handleReset = () => {
setCurrentIndex(0);
appsmith.triggerEvent("onReset");
};
return /*#__PURE__*/React.createElement(Card, {
className: "app",
style: style
}, /*#__PURE__*/React.createElement("div", {
className: "tip-container"
}, /*#__PURE__*/React.createElement("div", {
className: "tip-header"
}, /*#__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", {
className: "button-container"
}, /*#__PURE__*/React.createElement(Button, {
className: "primary",
onClick: handleNext,
type: "primary"
}, "Next Tip"), /*#__PURE__*/React.createElement(Button, {
onClick: handleReset
}, "Reset")));
}
appsmith.onReady(() => {
reactDom.render( /*#__PURE__*/React.createElement(App, null), document.getElementById("root"));
});`,
},
};

View File

@ -0,0 +1,129 @@
import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants";
export default {
key: createMessage(CUSTOM_WIDGET_FEATURE.templateKey.vanillaJs),
uncompiledSrcDoc: {
html: `<div class="app">
<div class="tip-container">
<div class="tip-header">
<h2>Custom Widget</h2>
<div id="index"></div>
</div>
<div id="tip"></div>
</div>
<div class="button-container">
<button id="next">Next Tip</button>
<button id="reset">Reset</button>
</div>
</div>`,
css: `.app {
height: calc(var(--appsmith-ui-height) * 1px);
width: calc(var(--appsmith-ui-width) * 1px);
justify-content: center;
border-radius: var(--appsmith-theme-borderRadius);
box-shadow: var(--appsmith-theme-boxShadow);
padding: 29px 25px;
box-sizing: border-box;
font-family: system-ui;
background: #fff;
}
.tip-container {
margin-bottom: 20px;
font-size: 14px;
line-height: 1.571429;
}
.tip-container h2 {
margin-bottom: 20px;
font-size: 16px;
font-weight: 700;
}
.tip-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 9px;
}
.tip-header div {
color: #999;
}
.button-container {
text-align: right;
padding-top: 4px;
}
.button-container button {
margin: 0 10px;
cursor: pointer;
border-radius: var(--appsmith-theme-borderRadius);
padding: 6px 16px;
background: none;
}
.button-container button#next {
background: var(--appsmith-theme-primaryColor) !important;
color: #fff;
border:1px solid var(--appsmith-theme-primaryColor) !important;
}
.button-container button#reset {
border: 1px solid #999;
color: #999;
outline: none;
box-shadow: none;
}
.button-container button#reset:hover:not(:disabled) {
color: var(--appsmith-theme-primaryColor);
border-color: var(--appsmith-theme-primaryColor);
}
.button-container button#reset:disabled {
cursor: default;
}`,
js: `function initApp() {
const index = document.getElementById("index");
const tip = document.getElementById("tip");
const next = document.getElementById("next");
const reset = document.getElementById("reset");
let currentIndex = 0;
const updateDom = () => {
tip.innerHTML = appsmith.model.tips[currentIndex];
index.innerHTML = (currentIndex + 1) + " / " + appsmith.model.tips.length;
reset.disabled = currentIndex === 0;
};
next.addEventListener("click", () => {
currentIndex = (currentIndex + 1) % appsmith.model.tips.length;
updateDom();
});
reset.addEventListener("click", () => {
currentIndex = 0;
updateDom();
appsmith.triggerEvent("onReset");
});
updateDom();
}
appsmith.onReady(() => {
/*
* This handler function will get called when parent application is ready.
* Initialize your component here
* more info - ${CUSTOM_WIDGET_ONREADY_DOC_URL}
*/
initApp();
});`,
},
};

View File

@ -2,31 +2,118 @@ import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants";
export default {
key: createMessage(CUSTOM_WIDGET_FEATURE.templateKey.vue),
uncompiledSrcDoc: {
html: `<div id="hello-world-app">
<h1>{{ msg }}</h1>
html: `<div id="app">
<div class="tip-container">
<div class="tip-header">
<h2>Custom Widget</h2>
<div id="index">{{ currentIndex + 1 }} / {{ tips.length }}</div>
</div>
<div id="tip">{{ tips[currentIndex] }}</div>
</div>
<div class="button-container">
<button @click="next" id="next">Next Tip</button>
<button @click="reset" id="reset">Reset</button>
</div>
</div>
<script
src="//cdnjs.cloudflare.com/ajax/libs/vue/2.1.6/vue.min.js">
</script>`,
css: `#hello-world-app {
font-family: monospace;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.1.6/vue.min.js"></script>`,
css: `#app {
height: calc(var(--appsmith-ui-height) * 1px);
width: calc(var(--appsmith-ui-width) * 1px);
justify-content: center;
border-radius: var(--appsmith-theme-borderRadius);
box-shadow: var(--appsmith-theme-boxShadow);
padding: 29px 25px;
box-sizing: border-box;
font-family: system-ui;
background: #fff;
}
.tip-container {
margin-bottom: 20px;
font-size: 14px;
line-height: 1.571429;
}
.tip-container h2 {
margin-bottom: 20px;
font-size: 16px;
font-weight: 700;
}
.tip-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 9px;
}
.tip-header div {
color: #999;
}
.button-container {
text-align: right;
padding-top: 4px;
}
.button-container button {
margin: 0 10px;
cursor: pointer;
border-radius: var(--appsmith-theme-borderRadius);
padding: 6px 16px;
background: none;
}
.button-container button#next {
background: var(--appsmith-theme-primaryColor) !important;
color: #fff;
border:1px solid var(--appsmith-theme-primaryColor) !important;
}
.button-container button#reset {
border: 1px solid #999;
color: #999;
outline: none;
box-shadow: none;
}
.button-container button#reset:hover:not(:disabled) {
color: var(--appsmith-theme-primaryColor);
border-color: var(--appsmith-theme-primaryColor);
}
.button-container button#reset:disabled {
cursor: default;
}`,
js: `new Vue({
el: "#hello-world-app",
data() {
return {
msg: "Hello World by Vue!"
}
}
js: `appsmith.onReady(() => {
/*
* This handler function will get called when parent application is ready.
* Initialize your component here
* more info - ${CUSTOM_WIDGET_ONREADY_DOC_URL}
*/
new Vue({
el: "#app",
data() {
return {
currentIndex: 0,
tips: appsmith.model.tips,
};
},
methods: {
next() {
this.currentIndex = (this.currentIndex + 1) % this.tips.length;
},
reset() {
this.currentIndex = 0;
appsmith.triggerEvent("onReset");
},
},
});
});`,
},
};

View File

@ -21,14 +21,14 @@ export const DEFAULT_CONTEXT_VALUE = {
name: "",
widgetId: "",
srcDoc: {
html: "<div>Hello World</div>",
js: "function test() {console.log('Hello World');}",
css: "div {color: red;}",
html: "",
js: "",
css: "",
},
uncompiledSrcDoc: {
html: "<div>Hello World</div>",
js: "function test() {console.log('Hello World');}",
css: "div {color: red;}",
html: "",
js: "",
css: "",
},
model: {},
events: {},
@ -59,3 +59,6 @@ export const CUSTOM_WIDGET_DOC_URL =
export const CUSTOM_WIDGET_DEFAULT_MODEL_DOC_URL =
"https://docs.appsmith.com/reference/widgets/custom#default-model";
export const CUSTOM_WIDGET_ONREADY_DOC_URL =
"https://docs.appsmith.com/reference/widgets/custom#onready";

View File

@ -56,14 +56,14 @@ describe("compileSrcDoc", () => {
const result = compileSrcDoc(validSrcDoc);
expect(result.code).toEqual(validSrcDoc);
expect(result.warnings).toHaveLength(0);
expect(result.warnings).toHaveLength(1);
expect(result.errors).toHaveLength(0);
});
it("should handle Babel compilation errors", () => {
const srcDocWithErrors = {
html: "<div>Hello World</div>",
js: "const a = 5 )",
js: "appsmith.onReady(() => {const a = 5 )})",
css: "div { color: red; }",
};

View File

@ -1,5 +1,10 @@
import { transform } from "@babel/standalone/";
import type { DebuggerLogItem, SrcDoc } from "./types";
import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "./constants";
interface CompiledResult {
code: SrcDoc;
@ -14,6 +19,8 @@ export const compileSrcDoc = (srcDoc: SrcDoc): CompiledResult => {
errors: [],
};
checkForWarnings(compiledResult);
try {
const result = transform(srcDoc.js, {
sourceType: "module",
@ -34,6 +41,24 @@ export const compileSrcDoc = (srcDoc: SrcDoc): CompiledResult => {
return compiledResult;
};
function checkForWarnings(compiledResult: CompiledResult) {
const code = compiledResult.code.js;
if (code?.length > 0) {
/*
* We are keeping this check as a simple string check instead of using AST
* because we want to keep the custom widget compile process as simple as possible.
*/
!code.includes("appsmith.onReady(") &&
compiledResult.warnings.push({
message: createMessage(
CUSTOM_WIDGET_FEATURE.debugger.noOnReadyWarning,
CUSTOM_WIDGET_ONREADY_DOC_URL,
),
});
}
}
export interface BabelError {
reasonCode: string;
message: string;

View File

@ -25,7 +25,7 @@
},
});
["log", "warn", "info"].forEach((method) => {
["log", "warn", "info", "error"].forEach((method) => {
nativeConsole[method] = createProxy(method);
});

View File

@ -23,6 +23,7 @@ import { combinedPreviewModeSelector } from "selectors/editorSelectors";
import { getAppMode } from "@appsmith/selectors/applicationSelectors";
import { APP_MODE } from "entities/App";
import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors";
import AnalyticsUtil from "utils/AnalyticsUtil";
const StyledIframe = styled.iframe<{ width: number; height: number }>`
width: ${(props) => props.width - 8}px;
@ -101,6 +102,16 @@ function CustomComponent(props: CustomComponentProps) {
},
"*",
);
if (
props.renderMode === "DEPLOYED" ||
props.renderMode === "EDITOR"
) {
AnalyticsUtil.logEvent("CUSTOM_WIDGET_LOAD_INIT", {
widgetId: props.widgetId,
renderMode: props.renderMode,
});
}
break;
case EVENTS.CUSTOM_WIDGET_UPDATE_MODEL:
props.update(message.data);

View File

@ -1,3 +1,5 @@
import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants";
export default {
uncompiledSrcDoc: {
html: `<!-- no need to write html, head, body tags, it is handled by the widget -->
@ -82,6 +84,11 @@ function App() {
}
appsmith.onReady(() => {
/*
* This handler function will get called when parent application is ready.
* Initialize your component here
* more info - ${CUSTOM_WIDGET_ONREADY_DOC_URL}
*/
reactDom.render(<App />, document.getElementById("root"));
});`,
},

View File

@ -31,6 +31,7 @@ import { Link } from "design-system";
import styled from "styled-components";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { Colors } from "constants/Colors";
import AnalyticsUtil from "utils/AnalyticsUtil";
const StyledLink = styled(Link)`
display: inline-block;
@ -332,6 +333,11 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
},
globalContext: contextObj,
});
AnalyticsUtil.logEvent("CUSTOM_WIDGET_API_TRIGGER_EVENT", {
widgetId: this.props.widgetId,
eventName,
});
}
};
@ -340,6 +346,10 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
...this.props.model,
...data,
});
AnalyticsUtil.logEvent("CUSTOM_WIDGET_API_UPDATE_MODEL", {
widgetId: this.props.widgetId,
});
};
getRenderMode = () => {