chore: misc updates to custom widget (#30114)

#### PR fixes following issue(s)
Fixes https://github.com/appsmithorg/appsmith/issues/29991
Fixes https://github.com/appsmithorg/appsmith/issues/30154
Fixes https://github.com/appsmithorg/appsmith/issues/30020
Fixes https://github.com/appsmithorg/appsmith/issues/30019
Fixes https://github.com/appsmithorg/appsmith/issues/30130
Fixes https://github.com/appsmithorg/appsmith/issues/30159
Fixes https://github.com/appsmithorg/appsmith/issues/30223

#### 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**
- Custom widgets now support analytics events, enhancing visibility into
user interactions.
- Template selection, layout controls, and reference triggers in the
Custom Widget Builder are now integrated with analytics.
- Added new style options for custom widgets, including `primaryColor`,
`backgroundColor`, `borderRadius`, and `boxShadow`.

- **Bug Fixes**
  - Corrected a typo in the constant title for better clarity.
- Updated help text for the Container Widget to accurately describe the
widget's border edge.

- **Enhancements**
- Improved user interface with additional styling for reference names in
the Custom Widget Builder.
- Enhanced debugger functionality with `useCallback` optimization and
new analytics logging.

- **Refactor**
- Streamlined the property pane by introducing a new `LabelContainer`
styled component.
- Refined the handling of dynamic binding paths to ignore certain
properties efficiently.

- **Documentation**
- Added a new constant for the default model documentation URL in the
Custom Widget Builder.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
balajisoundar 2024-01-16 10:52:17 +05:30 committed by GitHub
parent 8bb61d996a
commit 5d44d4f2cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 453 additions and 95 deletions

View File

@ -11,22 +11,18 @@ describe(
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)
);
return cy
.get(".t--widget-customwidget iframe")
.its("0.contentDocument")
.should("exist")
.its("body")
.should("not.be.undefined")
.then(cy.wrap);
};
it("shoud check that default model changes are converyed to custom component", () => {

View File

@ -2375,7 +2375,7 @@ export const CUSTOM_WIDGET_FEATURE = {
split: () => "Splits",
},
referrences: {
title: () => "Referrences",
title: () => "References",
tooltip: {
open: () => "Open references",
close: () => "Close references",

View File

@ -350,7 +350,8 @@ export type EventName =
| "START_FROM_TEMPLATES_CLICK_SKIP_BUTTON"
| "SUPPORT_REQUEST_INITIATED"
| ONBOARDING_FLOW_EVENTS
| CANVAS_STARTER_BUILDING_BLOCK_EVENTS;
| CANVAS_STARTER_BUILDING_BLOCK_EVENTS
| CUSTOM_WIDGET_EVENTS;
export type CANVAS_STARTER_BUILDING_BLOCK_EVENTS =
| "STARTER_BUILDING_BLOCK_HOVER"
@ -429,3 +430,24 @@ export type VERSION_UPDATE_EVENTS =
| "VERSION_UPDATE_REQUESTED"
| "VERSION_UPDATE_SUCCESS"
| "VERSION_UPDATED_FAILED";
export type CUSTOM_WIDGET_EVENTS =
| "CUSTOM_WIDGET_EDIT_SOURCE_CLICKED"
| "CUSTOM_WIDGET_ADD_EVENT_CLICKED"
| "CUSTOM_WIDGET_ADD_EVENT_CANCEL_CLICKED"
| "CUSTOM_WIDGET_ADD_EVENT_SAVE_CLICKED"
| "CUSTOM_WIDGET_EDIT_EVENT_CLICKED"
| "CUSTOM_WIDGET_DELETE_EVENT_CLICKED"
| "CUSTOM_WIDGET_EDIT_EVENT_SAVE_CLICKED"
| "CUSTOM_WIDGET_EDIT_EVENT_CANCEL_CLICKED"
| "CUSTOM_WIDGET_BUILDER_SRCDOC_UPDATE"
| "CUSTOM_WIDGET_BUILDER_TEMPLATE_OPENED"
| "CUSTOM_WIDGET_BUILDER_TEMPLATE_REVERT_TO_ORIGINAL_CLICKED"
| "CUSTOM_WIDGET_BUILDER_TEMPLATE_SELECT"
| "CUSTOM_WIDGET_BUILDER_TEMPLATE_SELECT_CANCELED"
| "CUSTOM_WIDGET_BUILDER_TEMPLATE_SELECT_CONFIRMED"
| "CUSTOM_WIDGET_BUILDER_LAYOUT_CHANGED"
| "CUSTOM_WIDGET_BUILDER_REFERENCE_VISIBILITY_CHANGED"
| "CUSTOM_WIDGET_BUILDER_REFERENCE_EVENT_OPENED"
| "CUSTOM_WIDGET_BUILDER_DEBUGGER_CLEARED"
| "CUSTOM_WIDGET_BUILDER_DEBUGGER_VISIBILITY_CHANGED";

View File

@ -9,6 +9,7 @@ import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import AnalyticsUtil from "utils/AnalyticsUtil";
interface ButtonControlState {
showInput: boolean;
@ -72,8 +73,12 @@ class ButtonControl extends BaseControl<
reset = () => {
this.setState({ showInput: false, eventName: "", pristine: true });
};
onCancel = () => {
this.reset();
AnalyticsUtil.logEvent("CUSTOM_WIDGET_ADD_EVENT_CANCEL_CLICKED", {
widgetId: this.props.widgetProperties.widgetId,
});
};
onSave = () => {
@ -83,6 +88,9 @@ class ButtonControl extends BaseControl<
);
this.batchUpdateProperties(updates);
this.reset();
AnalyticsUtil.logEvent("CUSTOM_WIDGET_ADD_EVENT_SAVE_CLICKED", {
widgetId: this.props.widgetProperties.widgetId,
});
};
hasError = () => {
@ -172,7 +180,12 @@ class ButtonControl extends BaseControl<
) : (
<Button
kind="tertiary"
onClick={() => this.setState({ showInput: true })}
onClick={() => {
this.setState({ showInput: true });
AnalyticsUtil.logEvent("CUSTOM_WIDGET_ADD_EVENT_CLICKED", {
widgetId: this.props.widgetProperties.widgetId,
});
}}
size="sm"
startIcon="plus"
>

View File

@ -10,6 +10,7 @@ import {
} from "@appsmith/constants/messages";
import CustomWidgetBuilderService from "utils/CustomWidgetBuilderService";
import styled from "styled-components";
import AnalyticsUtil from "utils/AnalyticsUtil";
interface ButtonControlState {
isSourceEditorOpen: boolean;
@ -24,7 +25,32 @@ class ButtonControl extends BaseControl<ControlProps, ButtonControlState> {
isSourceEditorOpen: false,
};
getPayload = () => {
return {
name: this.props.widgetProperties.widgetName,
widgetId: this.props.widgetProperties.widgetId,
srcDoc: this.props.widgetProperties.srcDoc,
uncompiledSrcDoc: this.props.widgetProperties.uncompiledSrcDoc,
model:
this.props.widgetProperties.__evaluation__?.evaluatedValues
?.defaultModel,
events: this.props.widgetProperties.events.reduce(
(prev: Record<string, string>, curr: string) => {
prev[curr] = this.props.widgetProperties[curr];
return prev;
},
{},
),
theme: this.props.widgetProperties.__evaluation__?.evaluatedValues?.theme,
};
};
onCTAClick = () => {
AnalyticsUtil.logEvent("CUSTOM_WIDGET_EDIT_SOURCE_CLICKED", {
widgetId: this.props.widgetProperties.widgetId,
});
if (
CustomWidgetBuilderService.isConnected(
this.props.widgetProperties.widgetId,
@ -40,22 +66,7 @@ class ButtonControl extends BaseControl<ControlProps, ButtonControlState> {
onMessage(CUSTOM_WIDGET_BUILDER_EVENTS.READY, () => {
postMessage({
type: CUSTOM_WIDGET_BUILDER_EVENTS.READY_ACK,
name: this.props.widgetProperties.widgetName,
srcDoc: this.props.widgetProperties.srcDoc,
uncompiledSrcDoc: this.props.widgetProperties.uncompiledSrcDoc,
model:
this.props.widgetProperties.__evaluation__?.evaluatedValues
?.defaultModel,
events: this.props.widgetProperties.events.reduce(
(prev: Record<string, string>, curr: string) => {
prev[curr] = this.props.widgetProperties[curr];
return prev;
},
{},
),
theme:
this.props.widgetProperties.__evaluation__?.evaluatedValues?.theme,
...this.getPayload(),
});
});
@ -74,6 +85,7 @@ class ButtonControl extends BaseControl<ControlProps, ButtonControlState> {
onMessage(CUSTOM_WIDGET_BUILDER_EVENTS.DISCONNECTED, () => {
CustomWidgetBuilderService.closeConnection(
this.props.widgetProperties.widgetId,
true,
);
this.setState({
@ -105,6 +117,7 @@ class ButtonControl extends BaseControl<ControlProps, ButtonControlState> {
this.props.widgetProperties.widgetId,
)?.postMessage({
type: CUSTOM_WIDGET_BUILDER_EVENTS.RESUME,
...this.getPayload(),
});
}
}

View File

@ -22,6 +22,7 @@ import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import AnalyticsUtil from "utils/AnalyticsUtil";
const StyledButton = styled(Button)`
height: 32px !important;
@ -70,13 +71,14 @@ function ConfirmationModal(props: {
}
export function CodeTemplates() {
const { bulkUpdate, initialSrcDoc, lastSaved } = useContext(
const { bulkUpdate, initialSrcDoc, lastSaved, widgetId } = useContext(
CustomWidgetBuilderContext,
);
const [open, setOpen] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<SrcDoc | null>(null);
const [selectedTemplateName, setSelectedTemplateName] = useState("");
return (
<div className={styles.templateMenu}>
@ -85,9 +87,13 @@ export function CodeTemplates() {
<StyledButton
className="t--custom-widget-template-trigger"
kind="secondary"
onClick={() => {
AnalyticsUtil.logEvent("CUSTOM_WIDGET_BUILDER_TEMPLATE_OPENED", {
widgetId: widgetId,
});
}}
size="sm"
startIcon="query"
style={{}}
>
{createMessage(CUSTOM_WIDGET_FEATURE.template.buttonCTA)}
</StyledButton>
@ -98,7 +104,17 @@ export function CodeTemplates() {
<MenuItem
onClick={() => {
setSelectedTemplate(initialSrcDoc);
setSelectedTemplateName(
CUSTOM_WIDGET_FEATURE.template.revert,
);
setOpen(true);
AnalyticsUtil.logEvent(
"CUSTOM_WIDGET_BUILDER_TEMPLATE_SELECT",
{
widgetId: widgetId,
templateName: CUSTOM_WIDGET_FEATURE.template.revert,
},
);
}}
>
{createMessage(CUSTOM_WIDGET_FEATURE.template.revert)}
@ -111,7 +127,15 @@ export function CodeTemplates() {
key={template.key}
onClick={() => {
setSelectedTemplate(template.uncompiledSrcDoc);
setSelectedTemplateName(template.key);
setOpen(true);
AnalyticsUtil.logEvent(
"CUSTOM_WIDGET_BUILDER_TEMPLATE_SELECT",
{
widgetId: widgetId,
templateName: template.key,
},
);
}}
>
{template.key}
@ -122,18 +146,42 @@ export function CodeTemplates() {
<ConfirmationModal
onCancel={() => {
setSelectedTemplate(null);
setSelectedTemplateName("");
setOpen(false);
AnalyticsUtil.logEvent(
"CUSTOM_WIDGET_BUILDER_TEMPLATE_SELECT_CANCELED",
{
widgetId: widgetId,
templateName: selectedTemplateName,
},
);
}}
onOpenChange={(flag: boolean) => {
if (!flag) {
setSelectedTemplate(null);
setSelectedTemplateName("");
setOpen(false);
AnalyticsUtil.logEvent(
"CUSTOM_WIDGET_BUILDER_TEMPLATE_SELECT_CANCELED",
{
widgetId: widgetId,
templateName: selectedTemplateName,
},
);
}
}}
onReplace={() => {
selectedTemplate && bulkUpdate?.(selectedTemplate);
setSelectedTemplate(null);
setSelectedTemplateName("");
setOpen(false);
AnalyticsUtil.logEvent(
"CUSTOM_WIDGET_BUILDER_TEMPLATE_SELECT_CONFIRMED",
{
widgetId: widgetId,
templateName: selectedTemplateName,
},
);
}}
open={open}
/>

View File

@ -7,6 +7,7 @@ import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import AnalyticsUtil from "utils/AnalyticsUtil";
const StyledSegmentedControl = styled(SegmentedControl)`
& .ads-v2-icon {
@ -15,10 +16,17 @@ const StyledSegmentedControl = styled(SegmentedControl)`
`;
export default function LayoutControls() {
const context = useContext(CustomWidgetBuilderContext);
const { selectedLayout, selectLayout, widgetId } = useContext(
CustomWidgetBuilderContext,
);
const onChange = (value: string) => {
context.selectLayout?.(value);
selectLayout?.(value);
AnalyticsUtil.logEvent("CUSTOM_WIDGET_BUILDER_LAYOUT_CHANGED", {
widgetId: widgetId,
layoutName: value,
});
};
return (
@ -37,7 +45,7 @@ export default function LayoutControls() {
value: "tabs",
},
]}
value={context.selectedLayout}
value={selectedLayout}
/>
</div>
);

View File

@ -6,14 +6,23 @@ import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import AnalyticsUtil from "utils/AnalyticsUtil";
export default function ReferenceTrigger() {
const { isReferenceOpen, toggleReference } = useContext(
const { isReferenceOpen, toggleReference, widgetId } = useContext(
CustomWidgetBuilderContext,
);
const onClick = () => {
toggleReference?.();
AnalyticsUtil.logEvent(
"CUSTOM_WIDGET_BUILDER_REFERENCE_VISIBILITY_CHANGED",
{
widgetId: widgetId,
visible: !isReferenceOpen,
},
);
};
return (

View File

@ -14,6 +14,7 @@ import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import AnalyticsUtil from "utils/AnalyticsUtil";
const StyledLazyCodeEditorWrapper = styled.div`
.CodeMirror-line.CodeMirror-line {
@ -31,11 +32,16 @@ const StyledLazyCodeEditorWrapper = styled.div`
`;
export default function Events() {
const { events } = useContext(CustomWidgetBuilderContext);
const { events, widgetId } = useContext(CustomWidgetBuilderContext);
const [openState, setOpenState] = useState<Record<string, boolean>>({});
const toggleOpen = useCallback((event: string) => {
AnalyticsUtil.logEvent("CUSTOM_WIDGET_BUILDER_REFERENCE_EVENT_OPENED", {
widgetId: widgetId,
eventName: event,
});
setOpenState((prev) => {
return {
...prev,

View File

@ -63,6 +63,9 @@
font-weight: 400;
margin-right: 2px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
width: calc(100% - 22px);
}
.eventControl {

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect } from "react";
import React, { useCallback, useContext, useEffect } from "react";
import { Tabs, TabsList, Tab, TabPanel, Icon, Tooltip } from "design-system";
import DebuggerItem from "./debuggerItem";
import styles from "./styles.module.css";
@ -11,6 +11,7 @@ import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import AnalyticsUtil from "utils/AnalyticsUtil";
const LOCAL_STORAGE_KEYS_IS_DEBUGGER_OPEN =
"custom-widget-builder-context-state-is-debugger-open";
@ -23,7 +24,7 @@ export default function Debugger() {
false,
);
const { clearDegbuggerLogs, debuggerLogs } = useContext(
const { clearDegbuggerLogs, debuggerLogs, widgetId } = useContext(
CustomWidgetBuilderContext,
);
@ -31,6 +32,17 @@ export default function Debugger() {
scrollToRef.current?.scrollIntoView({ behavior: "smooth" });
}, [debuggerLogs]);
const toggle = useCallback(() => {
setOpen(!open);
AnalyticsUtil.logEvent(
"CUSTOM_WIDGET_BUILDER_DEBUGGER_VISIBILITY_CHANGED",
{
widgetId: widgetId,
visible: !open,
},
);
}, [open]);
return (
<div className={styles.wrapper}>
<div className={styles.debuggerActions}>
@ -43,31 +55,36 @@ export default function Debugger() {
debuggerLogs?.filter((d) => d.type === DebuggerLogType.LOG)
.length || 0
}
onClick={() => setOpen(!open)}
onClick={() => toggle()}
warn={
debuggerLogs?.filter((d) => d.type === DebuggerLogType.WARN)
.length || 0
}
/>
<Tooltip content="clear console">
<Tooltip content="Clear console">
<Icon
name="forbid-line"
onClick={() => clearDegbuggerLogs?.()}
onClick={() => {
clearDegbuggerLogs?.();
AnalyticsUtil.logEvent("CUSTOM_WIDGET_BUILDER_DEBUGGER_CLEARED", {
widgetId: widgetId,
});
}}
size="md"
style={{ cursor: "pointer" }}
/>
</Tooltip>
<Tooltip content={open ? "close console" : "open console"}>
<Tooltip content={open ? "Close console" : "Open console"}>
<Icon
name={open ? "arrow-down-s-line" : "arrow-up-s-line"}
onClick={() => setOpen(!open)}
onClick={() => toggle()}
size="lg"
style={{ cursor: "pointer" }}
/>
</Tooltip>
</div>
<Tabs value={"Debugger"}>
<TabsList className={styles.debuggerTab} onClick={() => setOpen(!open)}>
<TabsList className={styles.debuggerTab} onClick={() => toggle()}>
<Tab key="Debugger" value="Debugger">
{createMessage(CUSTOM_WIDGET_FEATURE.debugger.title)}
</Tab>

View File

@ -11,8 +11,15 @@ import {
import type { AppThemeProperties } from "entities/AppTheming";
export default function Preview() {
const { key, model, srcDoc, theme, updateDebuggerLogs, updateModel } =
useContext(CustomWidgetBuilderContext);
const {
key,
model,
srcDoc,
theme,
updateDebuggerLogs,
updateModel,
widgetId,
} = useContext(CustomWidgetBuilderContext);
const [dimensions, setDimensions] = useState({
width: 300,
@ -100,6 +107,7 @@ export default function Preview() {
args: [{ message }, { message: data }],
});
}}
widgetId={widgetId || ""}
width={dimensions.width}
/>
<Debugger />

View File

@ -19,6 +19,7 @@ export const LOCAL_STORAGE_KEYS_SELECTED_LAYOUT =
export const DEFAULT_CONTEXT_VALUE = {
name: "",
widgetId: "",
srcDoc: {
html: "<div>Hello World</div>",
js: "function test() {console.log('Hello World');}",
@ -55,3 +56,6 @@ export const DEFAULT_CONTEXT_VALUE = {
export const CUSTOM_WIDGET_DOC_URL =
"https://docs.appsmith.com/reference/widgets/custom";
export const CUSTOM_WIDGET_DEFAULT_MODEL_DOC_URL =
"https://docs.appsmith.com/reference/widgets/custom#default-model";

View File

@ -22,6 +22,8 @@ import {
} from "./types";
import { compileSrcDoc } from "./utility";
import ConnectionLost from "./connectionLost";
import Helmet from "react-helmet";
import AnalyticsUtil from "utils/AnalyticsUtil";
export const CustomWidgetBuilderContext = React.createContext<
Partial<CustomWidgetBuilderContextType>
@ -56,7 +58,7 @@ export default function CustomWidgetBuilder() {
});
if (contextValue.lastSaved) {
window.opener.postMessage(
window.opener?.postMessage(
{
type: CUSTOM_WIDGET_BUILDER_EVENTS.UPDATE_SRCDOC,
srcDoc: result.code,
@ -106,7 +108,7 @@ export default function CustomWidgetBuilder() {
setSelectedLayout(layout);
},
close: () => {
window.opener.focus();
window.opener?.focus();
window.close();
},
bulkUpdate: (uncompiledSrcDoc: SrcDoc) => {
@ -129,6 +131,11 @@ export default function CustomWidgetBuilder() {
lastSaved: Date.now(),
};
});
AnalyticsUtil.logEvent("CUSTOM_WIDGET_BUILDER_SRCDOC_UPDATE", {
widgetId: contextValue.widgetId,
srcDocFile: editor,
});
},
updateModel: (model: Record<string, unknown>) => {
setContextValue((prev) => {
@ -187,6 +194,7 @@ export default function CustomWidgetBuilder() {
return {
...prev,
name: event.data.name,
widgetId: event.data.widgetId,
srcDoc: event.data.srcDoc,
uncompiledSrcDoc: event.data.uncompiledSrcDoc,
initialSrcDoc: event.data.uncompiledSrcDoc,
@ -222,13 +230,21 @@ export default function CustomWidgetBuilder() {
return {
...prev,
showConnectionLostMessage: false,
name: event.data.name,
widgetId: event.data.widgetId,
srcDoc: event.data.srcDoc,
uncompiledSrcDoc: event.data.uncompiledSrcDoc,
initialSrcDoc: event.data.uncompiledSrcDoc,
model: event.data.model,
events: event.data.events,
theme: event.data.theme,
};
});
break;
}
});
window.opener.postMessage(
window.opener?.postMessage(
{
type: CUSTOM_WIDGET_BUILDER_EVENTS.READY,
},
@ -236,7 +252,7 @@ export default function CustomWidgetBuilder() {
);
window.addEventListener("beforeunload", () => {
window.opener.postMessage(
window.opener?.postMessage(
{
type: CUSTOM_WIDGET_BUILDER_EVENTS.DISCONNECTED,
},
@ -252,6 +268,10 @@ export default function CustomWidgetBuilder() {
return (
<CustomWidgetBuilderContext.Provider value={context}>
<Helmet>
<meta charSet="utf-8" />
<title>{`${contextValue.name} | Builder | Appsmith`}</title>
</Helmet>
<Header />
{loading ? (
<Spinner className={styles.loader} size="lg" />

View File

@ -27,6 +27,7 @@ export interface SrcDoc {
export interface CustomWidgetBuilderContextValueType {
//Custom widget name
name: string;
widgetId: string;
isReferenceOpen: boolean;
selectedLayout: string;

View File

@ -70,6 +70,11 @@ const ResetIcon = importSvg(
const StyledDeviated = styled.div`
background-color: var(--ads-v2-color-bg-brand);
`;
const LabelContainer = styled.div<{ hasEditIcon: boolean }>`
${(props) => props.hasEditIcon && "max-width: calc(100% - 110px);"}
`;
type Props = PropertyPaneControlConfig & {
panel: IPanelProps;
theme: EditorTheme;
@ -646,6 +651,10 @@ const PropertyControl = memo((props: Props) => {
onDeleteProperties([props.propertyName]);
}
resetEditing();
AnalyticsUtil.logEvent("CUSTOM_WIDGET_EDIT_EVENT_SAVE_CLICKED", {
widgetId: widgetProperties.widgetId,
});
}, [
props,
onBatchUpdateProperties,
@ -657,6 +666,10 @@ const PropertyControl = memo((props: Props) => {
const resetEditing = useCallback(() => {
setEditedName(props.propertyName);
setIsRenaming(false);
AnalyticsUtil.logEvent("CUSTOM_WIDGET_EDIT_EVENT_CANCEL_CLICKED", {
widgetId: widgetProperties.widgetId,
});
}, [props.propertyName]);
const { propertyName } = props;
@ -913,8 +926,15 @@ const PropertyControl = memo((props: Props) => {
</div>
) : (
<div className="flex items-center justify-between">
<div className={clsx("flex items-center justify-right gap-1")}>
<LabelContainer
className={clsx("flex items-center justify-right gap-1")}
hasEditIcon={
!!config.controlConfig?.allowEdit ||
!!config.controlConfig?.allowDelete
}
>
<PropertyHelpLabel
className="w-full"
label={label}
theme={props.theme}
tooltip={helpText}
@ -964,7 +984,7 @@ const PropertyControl = memo((props: Props) => {
</button>
</>
)}
</div>
</LabelContainer>
<div className={clsx("flex items-center justify-right")}>
{config.controlConfig?.allowEdit && (
<Button
@ -975,7 +995,15 @@ const PropertyControl = memo((props: Props) => {
)}
isIconButton
kind="tertiary"
onClick={() => setIsRenaming(true)}
onClick={() => {
setIsRenaming(true);
AnalyticsUtil.logEvent(
"CUSTOM_WIDGET_EDIT_EVENT_CLICKED",
{
widgetId: widgetProperties.widgetId,
},
);
}}
size="small"
startIcon="pencil-line"
/>
@ -1000,6 +1028,13 @@ const PropertyControl = memo((props: Props) => {
onBatchUpdateProperties(updates);
}
onDeleteProperties([config.propertyName]);
AnalyticsUtil.logEvent(
"CUSTOM_WIDGET_DELETE_EVENT_CLICKED",
{
widgetId: widgetProperties.widgetId,
},
);
}}
size="small"
startIcon="trash"

View File

@ -33,9 +33,9 @@ function PropertyHelpLabel(props: Props) {
content={props.tooltip || ""}
isDisabled={!toolTipDefined}
>
<div onClick={props.onClick}>
<div className="w-full" onClick={props.onClick}>
<Label
className={`t--property-control-label`}
className={`t--property-control-label w-full block text-ellipsis overflow-hidden`}
style={{
cursor: toolTipDefined ? "help" : "default",
}}

View File

@ -389,6 +389,20 @@ function getDynamicTriggerPathListUpdate(
};
}
const DYNAMIC_BINDING_IGNORED_LIST = [
/* Table widget */
"primaryColumns",
"derivedColumns",
/* custom widget */
"srcDoc.html",
"srcDoc.css",
"srcDoc.js",
"uncompiledSrcDoc.html",
"uncompiledSrcDoc.css",
"uncompiledSrcDoc.js",
];
function getDynamicBindingPathListUpdate(
widget: WidgetProps,
propertyPath: string,
@ -400,9 +414,12 @@ function getDynamicBindingPathListUpdate(
stringProp = JSON.stringify(propertyValue);
}
//TODO(abhinav): This is not appropriate from the platform's archtecture's point of view.
/*
* TODO(Balaji Soundararajan): This is not appropriate from the platform's archtecture's point of view.
* This setting should come from widget configuration
*/
// Figure out a holistic solutions where we donot have to stringify above.
if (propertyPath === "primaryColumns" || propertyPath === "derivedColumns") {
if (DYNAMIC_BINDING_IGNORED_LIST.includes(propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.NOOP,

View File

@ -86,11 +86,13 @@ export default class CustomWidgetBuilderService {
}
}
static closeConnection(widgetId: string) {
static closeConnection(widgetId: string, skipClosing?: boolean) {
if (this.builderWindowConnections.has(widgetId)) {
const connection = this.builderWindowConnections.get(widgetId);
if (!skipClosing) {
const connection = this.builderWindowConnections.get(widgetId);
connection?.window?.close();
connection?.window?.close();
}
this.builderWindowConnections.delete(widgetId);
}

View File

@ -291,8 +291,7 @@ export class ContainerWidget extends BaseWidget<
{
propertyName: "borderRadius",
label: "Border radius",
helpText:
"Rounds the corners of the icon button's outer border edge",
helpText: "Rounds the corners of the widgets's outer border edge",
controlType: "BORDER_RADIUS_OPTIONS",
isJSConvertible: true,
isBindProperty: true,

View File

@ -7,15 +7,22 @@ export const CUSTOM_WIDGET_LOAD_EVENTS = {
export const getAppsmithScriptSchema = (model: Record<string, unknown>) => ({
appsmith: {
mode: "",
onUiChange: Function,
onModelChange: Function,
updateModel: Function,
triggerEvent: Function,
model: model,
ui: {
width: 1,
height: 2,
},
theme: {
primaryColor: "",
backgroundColor: "",
borderRadius: "",
boxShadow: "",
},
onUiChange: Function,
onModelChange: Function,
onThemeChange: Function,
updateModel: Function,
triggerEvent: Function,
onReady: Function,
},
});

View File

@ -14,6 +14,15 @@ import appsmithConsole from "!!raw-loader!./appsmithConsole.js";
import css from "!!raw-loader!./reset.css";
import clsx from "clsx";
import type { AppThemeProperties } from "entities/AppTheming";
import WidgetStyleContainer from "components/designSystems/appsmith/WidgetStyleContainer";
import type { BoxShadow } from "components/designSystems/appsmith/WidgetStyleContainer";
import type { Color } from "constants/Colors";
import { connect } from "react-redux";
import type { AppState } from "@appsmith/reducers";
import { combinedPreviewModeSelector } from "selectors/editorSelectors";
import { getAppMode } from "@appsmith/selectors/applicationSelectors";
import { APP_MODE } from "entities/App";
import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors";
const StyledIframe = styled.iframe<{ width: number; height: number }>`
width: ${(props) => props.width - 8}px;
@ -191,16 +200,26 @@ function CustomComponent(props: CustomComponentProps) {
})}
>
{props.needsOverlay && <OverlayDiv data-testid="iframe-overlay" />}
<StyledIframe
height={props.height}
onLoad={() => {
setLoading(false);
}}
ref={iframe}
sandbox="allow-scripts allow-downloads"
srcDoc={srcDoc}
width={props.width}
/>
<WidgetStyleContainer
backgroundColor={props.backgroundColor}
borderColor={props.borderColor}
borderRadius={props.borderRadius}
borderWidth={props.borderWidth}
boxShadow={props.boxShadow}
widgetId={props.widgetId}
>
<StyledIframe
height={props.height}
loading="lazy"
onLoad={() => {
setLoading(false);
}}
ref={iframe}
sandbox="allow-scripts allow-downloads"
srcDoc={srcDoc}
width={props.width}
/>
</WidgetStyleContainer>
</div>
);
}
@ -221,6 +240,30 @@ export interface CustomComponentProps {
onConsole?: (type: string, message: string) => void;
renderMode: "EDITOR" | "DEPLOYED" | "BUILDER";
theme: AppThemeProperties;
borderColor?: Color;
backgroundColor?: Color;
borderWidth?: number;
borderRadius?: number;
boxShadow?: BoxShadow;
widgetId: string;
}
export default CustomComponent;
/**
* TODO: Balaji soundararajan - to refactor code to move out selected widget details to platform
*/
export const mapStateToProps = (
state: AppState,
ownProps: CustomComponentProps,
) => {
const isPreviewMode = combinedPreviewModeSelector(state);
const appMode = getAppMode(state);
return {
needsOverlay:
appMode == APP_MODE.EDIT &&
!isPreviewMode &&
ownProps.widgetId !== getWidgetPropsForPropertyPane(state)?.widgetId,
};
};
export default connect(mapStateToProps)(CustomComponent);

View File

@ -7,8 +7,7 @@ export default {
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);
border-radius: 0px;
}
.tip-container {
@ -37,13 +36,14 @@ export default {
.button-container button {
margin: 0 10px;
border-radius: var(--appsmith-theme-borderRadius) !important;
}
.button-container button.primary {
background: var(--appsmith-theme-primaryColor) !important;
}
.button-container button.reset {
.button-container button.reset:not([disabled]) {
color: var(--appsmith-theme-primaryColor) !important;
border-color: var(--appsmith-theme-primaryColor) !important;
}`,
@ -75,7 +75,7 @@ function App() {
</div>
<div className="button-container">
<Button className="primary" onClick={handleNext} type="primary">Next Tip</Button>
<Button className="reset" onClick={handleReset}>Reset</Button>
<Button className="reset" disabled={currentIndex === 0} onClick={handleReset}>Reset</Button>
</div>
</Card>
);
@ -93,8 +93,7 @@ appsmith.onReady(() => {
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);
border-radius: 0px;
}
.tip-container {
@ -123,13 +122,14 @@ appsmith.onReady(() => {
.button-container button {
margin: 0 10px;
border-radius: var(--appsmith-theme-borderRadius) !important;
}
.button-container button.primary {
background: var(--appsmith-theme-primaryColor) !important;
}
.button-container button.reset {
.button-container button.reset:not([disabled]) {
color: var(--appsmith-theme-primaryColor) !important;
border-color: var(--appsmith-theme-primaryColor) !important;
}`,
@ -161,6 +161,7 @@ function App() {
type: "primary"
}, "Next Tip"), /*#__PURE__*/React.createElement(Button, {
className: "reset",
disabled: currentIndex === 0,
onClick: handleReset
}, "Reset")));
}

View File

@ -8,9 +8,13 @@ import BaseWidget from "widgets/BaseWidget";
import CustomComponent from "../component";
import IconSVG from "../icon.svg";
import { RenderModes, WIDGET_TAGS } from "constants/WidgetConstants";
import { WIDGET_TAGS } from "constants/WidgetConstants";
import { ValidationTypes } from "constants/WidgetValidation";
import type { AppThemeProperties, SetterConfig } from "entities/AppTheming";
import type {
AppThemeProperties,
SetterConfig,
Stylesheet,
} from "entities/AppTheming";
import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils";
import type { AutocompletionDefinitions } from "WidgetProvider/constants";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
@ -19,9 +23,14 @@ import { DEFAULT_MODEL } from "../constants";
import defaultApp from "./defaultApp";
import type { ExtraDef } from "utils/autocomplete/defCreatorUtils";
import { generateTypeDef } from "utils/autocomplete/defCreatorUtils";
import { CUSTOM_WIDGET_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants";
import {
CUSTOM_WIDGET_DEFAULT_MODEL_DOC_URL,
CUSTOM_WIDGET_DOC_URL,
} from "pages/Editor/CustomWidgetBuilder/constants";
import { Link } from "design-system";
import styled from "styled-components";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { Colors } from "constants/Colors";
const StyledLink = styled(Link)`
display: inline-block;
@ -52,7 +61,7 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
return {
widgetName: "Custom",
rows: 30,
columns: 20,
columns: 23,
version: 1,
onResetClick: "{{showAlert('Successfully reset!!', '');}}",
events: ["onResetClick"],
@ -62,6 +71,9 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
uncompiledSrcDoc: defaultApp.uncompiledSrcDoc,
theme: "{{appsmith.theme}}",
dynamicBindingPathList: [{ key: "theme" }],
borderColor: Colors.GREY_5,
borderWidth: "1",
backgroundColor: "#FFFFFF",
};
}
@ -83,6 +95,13 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
};
}
static getStylesheetConfig(): Stylesheet {
return {
borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}",
boxShadow: "{{appsmith.theme.boxShadow.appBoxShadow}}",
};
}
static getPropertyPaneContentConfig() {
return [
{
@ -126,7 +145,7 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
kind="secondary"
rel="noopener noreferrer"
target="_blank"
to={CUSTOM_WIDGET_DOC_URL}
to={CUSTOM_WIDGET_DEFAULT_MODEL_DOC_URL}
>
Read more
</StyledLink>
@ -190,6 +209,7 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
},
},
dependencies: ["events"],
helpText: "when the event is triggered from custom widget",
}));
},
children: [
@ -217,7 +237,71 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
}
static getPropertyPaneStyleConfig() {
return [];
return [
{
sectionName: "Color",
children: [
{
helpText: "Use a html color name, HEX, RGB or RGBA value",
placeholderText: "#FFFFFF / Gray / rgb(255, 99, 71)",
propertyName: "backgroundColor",
label: "Background color",
controlType: "COLOR_PICKER",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
helpText: "Use a html color name, HEX, RGB or RGBA value",
placeholderText: "#FFFFFF / Gray / rgb(255, 99, 71)",
propertyName: "borderColor",
label: "Border color",
controlType: "COLOR_PICKER",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
],
},
{
sectionName: "Border and shadow",
children: [
{
helpText: "Enter value for border width",
propertyName: "borderWidth",
label: "Border width",
placeholderText: "Enter value in px",
controlType: "INPUT_TEXT",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.NUMBER },
postUpdateAction: ReduxActionTypes.CHECK_CONTAINERS_FOR_AUTO_HEIGHT,
},
{
propertyName: "borderRadius",
label: "Border radius",
helpText: "Rounds the corners of the widgets's outer border edge",
controlType: "BORDER_RADIUS_OPTIONS",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "boxShadow",
label: "Box shadow",
helpText:
"Enables you to cast a drop shadow from the frame of the widget",
controlType: "BOX_SHADOW_OPTIONS",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
],
},
];
}
static getDerivedPropertiesMap(): DerivedPropertiesMap {
@ -270,17 +354,19 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
getWidgetView() {
return (
<CustomComponent
backgroundColor={this.props.backgroundColor}
borderColor={this.props.borderColor}
borderRadius={this.props.borderRadius}
borderWidth={this.props.borderWidth}
boxShadow={this.props.boxShadow}
execute={this.execute}
height={this.props.componentHeight}
model={this.props.model || {}}
needsOverlay={
this.props.renderMode === RenderModes.CANVAS &&
!this.props.isWidgetSelected
}
renderMode={this.getRenderMode()}
srcDoc={this.props.srcDoc}
theme={this.props.theme}
update={this.update}
widgetId={this.props.widgetId}
width={this.props.componentWidth}
/>
);