From 3ef3798c921ef8a9000088c765f4e696eaffc0d3 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Fri, 29 Nov 2024 15:41:15 +0530 Subject: [PATCH 1/2] add custom widget --- .../component/appsmithConsole.js | 35 ++ .../WDSCustomWidget/component/constants.ts | 28 ++ .../component/customWidgetScript.test.ts | 412 +++++++++++++++ .../component/customWidgetscript.js | 332 +++++++++++++ .../wds/WDSCustomWidget/component/index.tsx | 325 ++++++++++++ .../wds/WDSCustomWidget/component/reset.css | 126 +++++ .../ui/wds/WDSCustomWidget/constants.ts | 9 + .../ui/wds/WDSCustomWidget/icon.svg | 1 + .../ui/wds/WDSCustomWidget/index.ts | 1 + .../ui/wds/WDSCustomWidget/thumbnail.svg | 1 + .../wds/WDSCustomWidget/widget/defaultApp.ts | 180 +++++++ .../ui/wds/WDSCustomWidget/widget/index.tsx | 469 ++++++++++++++++++ .../modules/ui-builder/ui/wds/constants.ts | 2 +- app/client/src/widgets/index.ts | 2 + 14 files changed, 1922 insertions(+), 1 deletion(-) create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/appsmithConsole.js create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/constants.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetScript.test.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetscript.js create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/reset.css create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/constants.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/icon.svg create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/index.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/thumbnail.svg create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/defaultApp.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/index.tsx diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/appsmithConsole.js b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/appsmithConsole.js new file mode 100644 index 000000000000..1c27b1b5eb03 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/appsmithConsole.js @@ -0,0 +1,35 @@ +(function (nativeConsole) { + const postMessage = (method, args) => { + window.parent.postMessage( + { + type: "CUSTOM_WIDGET_CONSOLE_EVENT", + data: { + type: method, + args: args.map((d) => ({ + message: d, + })), + }, + }, + "*", + ); + }; + + const createProxy = (method) => + new Proxy(nativeConsole[method], { + apply(target, _this, args) { + try { + postMessage(method, args); + } finally { + return Reflect.apply(target, _this, args); + } + }, + }); + + ["log", "warn", "info"].forEach((method) => { + nativeConsole[method] = createProxy(method); + }); + + window.addEventListener("error", (event) => { + postMessage("error", [event.message]); + }); +})(window.console); diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/constants.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/constants.ts new file mode 100644 index 000000000000..bb38bdde2f7c --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/constants.ts @@ -0,0 +1,28 @@ +export const CUSTOM_WIDGET_LOAD_EVENTS = { + STARTED: "started", + DOM_CONTENTED_LOADED: "DOMContentLoaded", + COMPLETED: "completed", +}; + +export const getAppsmithScriptSchema = (model: Record) => ({ + appsmith: { + mode: "", + model: model, + ui: { + width: 1, + height: 2, + }, + theme: { + primaryColor: "", + backgroundColor: "", + borderRadius: "", + boxShadow: "", + }, + onUiChange: Function, + onModelChange: Function, + onThemeChange: Function, + updateModel: Function, + triggerEvent: Function, + onReady: Function, + }, +}); diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetScript.test.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetScript.test.ts new file mode 100644 index 000000000000..964796d76a37 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetScript.test.ts @@ -0,0 +1,412 @@ +import { + createChannelToParent, + generateAppsmithCssVariables, + EVENTS, + main, +} from "./customWidgetscript"; + +jest.mock("queue-microtask", () => ({ + queueMicrotask: jest.fn().mockImplementationOnce((fn) => fn()), +})); + +declare global { + interface Window { + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + appsmith: any; + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + triggerEvent: any; + } +} + +describe("createChannelToParent", () => { + beforeEach(() => { + const events = new Map(); + + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window.addEventListener = (type: string, handler: any) => { + events.set(type, handler); + }; + + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window.triggerEvent = (type: string, event: any) => { + events.get(type)(event); + }; + }); + + it("should check the onMessage function", () => { + const channel = createChannelToParent(); + + const handler = jest.fn(); + + channel.onMessage("test", handler); + + expect(channel.onMessageMap.get("test")[0]).toBe(handler); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: "test", + }, + }); + + expect(handler).toHaveBeenCalledWith({ + type: "test", + }); + }); + + it("should check the postMessage function", async () => { + const channel = createChannelToParent(); + + window.parent.postMessage = jest.fn().mockImplementationOnce((data) => { + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK, + key: data.key, + success: true, + }, + }); + }); + + channel.postMessage("test1", { index: 1 }); + + channel.postMessage("test2", { index: 2 }); + + return new Promise((resolve) => { + setTimeout(() => { + expect(window.parent.postMessage).toHaveBeenCalledWith( + { + type: "test1", + data: { index: 1 }, + key: expect.any(Number), + }, + "*", + ); + + expect(window.parent.postMessage).toHaveBeenCalledWith( + { + type: "test2", + data: { index: 2 }, + key: expect.any(Number), + }, + "*", + ); + + resolve(true); + }); + }); + }); +}); + +describe("generateAppsmithCssVariables", () => { + it("should generate CSS variables in the style element", () => { + const source = { + key1: "value1", + key2: 42, + key3: [], + }; + + const provider = "model"; + + generateAppsmithCssVariables(provider)(source); + + expect( + document + .getElementById(`appsmith-${provider}-css-tokens`) + ?.innerHTML.replace(/\s+/g, "") + .replace(/\n/g, ""), + ).toBe(`:root{--appsmith-model-key1:value1;--appsmith-model-key2:42;}`); + }); +}); + +describe("CustomWidgetScript", () => { + beforeAll(() => { + const events = new Map(); + + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window.addEventListener = (type: string, handler: any) => { + events.set(type, handler); + }; + + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window.triggerEvent = (type: string, event: any) => { + events.get(type)(event); + }; + + window.parent.postMessage = jest.fn().mockImplementationOnce((data) => { + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK, + key: data.key, + success: true, + }, + }); + }); + + main(); + }); + + it("should check API functions - onReady and init", () => { + const handler = jest.fn(); + + window.appsmith.onReady(handler); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_READY_ACK, + model: { + test: 1, + }, + ui: { + width: 1, + height: 2, + }, + mode: "test", + theme: { + color: "#fff", + }, + }, + }); + + expect(window.appsmith.mode).toBe("test"); + + expect(window.appsmith.model).toEqual({ + test: 1, + }); + + expect(window.appsmith.ui).toEqual({ + width: 1, + height: 2, + }); + + expect(handler).toHaveBeenCalled(); + }); + + it("should check API functions - onModelChange", () => { + const handler = jest.fn(); + + const unlisten = window.appsmith.onModelChange(handler); + + expect(handler).toHaveBeenCalledWith({ + test: 1, + }); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_MODEL_CHANGE, + model: { + test: 2, + }, + }, + }); + + expect(window.appsmith.model).toEqual({ + test: 2, + }); + + expect(handler).toHaveBeenCalledWith( + { + test: 2, + }, + { + test: 1, + }, + ); + + handler.mockClear(); + unlisten(); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_MODEL_CHANGE, + model: { + test: 3, + }, + }, + }); + + expect(window.appsmith.model).toEqual({ + test: 3, + }); + + expect(handler).not.toHaveBeenCalled(); + }); + + it("should check API functions - onUiChange", () => { + const handler = jest.fn(); + + const unlisten = window.appsmith.onUiChange(handler); + + expect(handler).toHaveBeenCalledWith({ + width: 1, + height: 2, + }); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_UI_CHANGE, + ui: { + width: 2, + height: 3, + }, + }, + }); + + expect(window.appsmith.ui).toEqual({ + width: 2, + height: 3, + }); + + expect(handler).toHaveBeenCalledWith( + { + width: 2, + height: 3, + }, + { + width: 1, + height: 2, + }, + ); + + handler.mockClear(); + unlisten(); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_UI_CHANGE, + ui: { + width: 3, + height: 4, + }, + }, + }); + + expect(window.appsmith.ui).toEqual({ + width: 3, + height: 4, + }); + + expect(handler).not.toHaveBeenCalled(); + }); + + it("should check API functions - onThemeChange", () => { + const handler = jest.fn(); + + const unlisten = window.appsmith.onThemeChange(handler); + + expect(handler).toHaveBeenCalledWith({ + color: "#fff", + }); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_THEME_UPDATE, + theme: { + color: "#000", + }, + }, + }); + + expect(window.appsmith.theme).toEqual({ + color: "#000", + }); + + expect(handler).toHaveBeenCalledWith( + { + color: "#000", + }, + { + color: "#fff", + }, + ); + + handler.mockClear(); + unlisten(); + + window.triggerEvent("message", { + source: window.parent, + data: { + type: EVENTS.CUSTOM_WIDGET_THEME_UPDATE, + theme: { + color: "#f0f", + }, + }, + }); + + expect(window.appsmith.theme).toEqual({ + color: "#f0f", + }); + + expect(handler).not.toHaveBeenCalled(); + }); + + it("should check API functions - updateModel", async () => { + window.appsmith.updateModel({ + test: 4, + }); + + return new Promise((resolve) => { + setTimeout(() => { + expect(window.parent.postMessage).toHaveBeenCalledWith( + { + type: EVENTS.CUSTOM_WIDGET_UPDATE_MODEL, + data: { + test: 4, + }, + key: expect.any(Number), + }, + "*", + ); + + expect(window.appsmith.model).toEqual({ + test: 4, + }); + + resolve(true); + }); + }); + }); + + it("should check API functions - triggerEvent", async () => { + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window.parent.postMessage as any).mockClear(); + + window.appsmith.triggerEvent("test", { + test: 5, + }); + + return new Promise((resolve) => { + setTimeout(() => { + expect(window.parent.postMessage).toHaveBeenCalledWith( + { + type: EVENTS.CUSTOM_WIDGET_TRIGGER_EVENT, + data: { + eventName: "test", + contextObj: { + test: 5, + }, + }, + key: expect.any(Number), + }, + "*", + ); + + resolve(true); + }); + }); + }); +}); diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetscript.js b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetscript.js new file mode 100644 index 000000000000..14b40311d347 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/customWidgetscript.js @@ -0,0 +1,332 @@ +// Custom widget events definition +export const EVENTS = { + CUSTOM_WIDGET_READY: "CUSTOM_WIDGET_READY", + CUSTOM_WIDGET_READY_ACK: "CUSTOM_WIDGET_READY_ACK", + CUSTOM_WIDGET_UPDATE_MODEL: "CUSTOM_WIDGET_UPDATE_MODEL", + CUSTOM_WIDGET_TRIGGER_EVENT: "CUSTOM_WIDGET_TRIGGER_EVENT", + CUSTOM_WIDGET_MODEL_CHANGE: "CUSTOM_WIDGET_MODEL_CHANGE", + CUSTOM_WIDGET_UI_CHANGE: "CUSTOM_WIDGET_UI_CHANGE", + CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK: "CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK", + CUSTOM_WIDGET_CONSOLE_EVENT: "CUSTOM_WIDGET_CONSOLE_EVENT", + CUSTOM_WIDGET_THEME_UPDATE: "CUSTOM_WIDGET_THEME_UPDATE", + CUSTOM_WIDGET_UPDATE_HEIGHT: "CUSTOM_WIDGET_UPDATE_HEIGHT", +}; + +// Function to create a communication channel to the parent +export const createChannelToParent = () => { + const onMessageMap = new Map(); + + // Function to register an event handler for a message type + function onMessage(type, fn) { + let eventHandlerList = onMessageMap.get(type); + + if (eventHandlerList && eventHandlerList instanceof Array) { + eventHandlerList.push(fn); + } else { + eventHandlerList = [fn]; + onMessageMap.set(type, eventHandlerList); + } + + return () => { + // Function to unsubscribe an event handler + const index = eventHandlerList.indexOf(fn); + + eventHandlerList.splice(index, 1); + }; + } + + // Listen for 'message' events and dispatch to registered event handlers + window.addEventListener("message", (event) => { + if (event.source === window.parent) { + const handlerList = onMessageMap.get(event.data.type); + + if (handlerList) { + handlerList.forEach((fn) => fn(event.data)); + } + } + }); + // Queue to hold postMessage requests + const postMessageQueue = []; + // Flag to indicate if the flush is scheduled + let isFlushScheduled = false; + + /* + * Function to schedule microtask to flush postMessageQueue + * to ensure the order of message processed on the parent + */ + const scheduleMicrotaskToflushPostMessageQueue = () => { + if (!isFlushScheduled) { + isFlushScheduled = true; + queueMicrotask(() => { + (async () => { + while (postMessageQueue.length > 0) { + const message = postMessageQueue.shift(); + + await new Promise((resolve) => { + const key = Math.random(); + const unlisten = onMessage( + EVENTS.CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK, + (data) => { + if (data.key === key && data.success) { + unlisten(); + resolve(); + } + }, + ); + + // Send the message to the parent + window.parent.postMessage( + Object.assign(Object.assign({}, message), { key }), + "*", + ); + }); + } + + isFlushScheduled = false; + })(); + }); + } + }; + + return { + onMessageMap, + postMessage: (type, data) => { + try { + // Try block to catch non clonealbe data error while postmessaging + // throw error if the data is not cloneable + postMessageQueue.push({ + type, + data, + }); + + scheduleMicrotaskToflushPostMessageQueue(); + } catch (e) { + throw e; + } + }, + onMessage, + }; +}; + +/* + * Function to initialize the script + * wrapping this inside a function to make it testable + */ +export function main() { + // Create a communication channel to the parent + const channel = createChannelToParent(); + /* + * Variables to hold the subscriber functions + */ + const modelSubscribers = []; + const uiSubscribers = []; + const themeSubscribers = []; + /* + * Variables to hold ready function and state + */ + let onReady; + let isReady = false; + let isReadyCalled = false; + + const heightObserver = new ResizeObserver(() => { + const height = document.body.clientHeight; + + channel.postMessage(EVENTS.CUSTOM_WIDGET_UPDATE_HEIGHT, { + height, + }); + }); + + // Callback for when the READY_ACK message is received + channel.onMessage(EVENTS.CUSTOM_WIDGET_READY_ACK, (event) => { + window.appsmith.model = event.model; + window.appsmith.ui = event.ui; + window.appsmith.theme = event.theme; + window.appsmith.mode = event.mode; + heightObserver.observe(window.document.body); + + // Subscribe to model and UI changes + window.appsmith.onModelChange(generateAppsmithCssVariables("model")); + window.appsmith.onUiChange(generateAppsmithCssVariables("ui")); + window.appsmith.onThemeChange(generateAppsmithCssVariables("theme")); + + // Set the widget as ready + isReady = true; + + if (!isReadyCalled && onReady) { + onReady(); + isReadyCalled = true; + } + }); + // Callback for when MODEL_CHANGE message is received + channel.onMessage(EVENTS.CUSTOM_WIDGET_MODEL_CHANGE, (event) => { + if (event.model) { + const prevModel = window.appsmith.model; + + window.appsmith.model = event.model; + + // Notify model subscribers + modelSubscribers.forEach((fn) => { + fn(event.model, prevModel); + }); + } + }); + // Callback for when UI_CHANGE message is received + channel.onMessage(EVENTS.CUSTOM_WIDGET_UI_CHANGE, (event) => { + if (event.ui) { + const prevUi = window.appsmith.ui; + + window.appsmith.ui = event.ui; + // Notify UI subscribers + uiSubscribers.forEach((fn) => { + fn(event.ui, prevUi); + }); + } + }); + + channel.onMessage(EVENTS.CUSTOM_WIDGET_THEME_UPDATE, (event) => { + if (event.theme) { + const prevTheme = window.appsmith.theme; + + window.appsmith.theme = event.theme; + // Notify theme subscribers + themeSubscribers.forEach((fn) => { + fn(event.theme, prevTheme); + }); + } + }); + + if (!window.appsmith) { + // Define appsmith global object + Object.defineProperty(window, "appsmith", { + configurable: false, + writable: false, + value: { + mode: "", + theme: {}, + onThemeChange: (fn) => { + if (typeof fn !== "function") { + throw new Error("onThemeChange expects a function as parameter"); + } + + themeSubscribers.push(fn); + fn(window.appsmith.theme); + + return () => { + // Unsubscribe from theme changes + const index = themeSubscribers.indexOf(fn); + + if (index > -1) { + themeSubscribers.splice(index, 1); + } + }; + }, + onUiChange: (fn) => { + if (typeof fn !== "function") { + throw new Error("onUiChange expects a function as parameter"); + } + + uiSubscribers.push(fn); + fn(window.appsmith.ui); + + return () => { + // Unsubscribe from UI changes + const index = uiSubscribers.indexOf(fn); + + if (index > -1) { + uiSubscribers.splice(index, 1); + } + }; + }, + onModelChange: (fn) => { + if (typeof fn !== "function") { + throw new Error("onModelChange expects a function as parameter"); + } + + modelSubscribers.push(fn); + fn(window.appsmith.model); + + return () => { + // Unsubscribe from model changes + const index = modelSubscribers.indexOf(fn); + + if (index > -1) { + modelSubscribers.splice(index, 1); + } + }; + }, + updateModel: (obj) => { + if (!obj || typeof obj !== "object") { + throw new Error("updateModel expects an object as parameter"); + } + + appsmith.model = Object.assign( + Object.assign({}, appsmith.model), + obj, + ); + + // Send an update model message to the parent + channel.postMessage(EVENTS.CUSTOM_WIDGET_UPDATE_MODEL, obj); + }, + triggerEvent: (eventName, contextObj) => { + if (typeof eventName !== "string") { + throw new Error("eventName should be a string"); + } else if (contextObj && typeof contextObj !== "object") { + throw new Error("contextObj should be an object"); + } + + // Send a trigger event message to the parent + channel.postMessage(EVENTS.CUSTOM_WIDGET_TRIGGER_EVENT, { + eventName, + contextObj, + }); + }, + model: {}, + ui: {}, + onReady: (fn) => { + if (typeof fn !== "function") { + throw new Error("onReady expects a function as parameter"); + } + + onReady = fn; + + if (isReady && !isReadyCalled && onReady) { + onReady(); + isReadyCalled = true; + } + }, + }, + }); + } + + // Listen for the 'load' event and send READY message to the parent + window.addEventListener("load", () => { + channel.postMessage(EVENTS.CUSTOM_WIDGET_READY); + }); +} + +/* + * Function to create appsmith css variables for model and ui primitive values + * variables get regenerated every time the model or ui changes. + */ +export const generateAppsmithCssVariables = (provider) => (source) => { + let cssTokens = document.getElementById(`appsmith-${provider}-css-tokens`); + + if (!cssTokens) { + cssTokens = document.createElement("style"); + cssTokens.id = `appsmith-${provider}-css-tokens`; + window.document.head.appendChild(cssTokens); + } + + const cssTokensContent = Object.keys(source).reduce((acc, key) => { + if (typeof source[key] === "string" || typeof source[key] === "number") { + return ` + ${acc} + --appsmith-${provider}-${key}: ${source[key]}; + `; + } else { + return acc; + } + }, ""); + + cssTokens.innerHTML = `:root {${cssTokensContent}}`; +}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx new file mode 100644 index 000000000000..eb6740b48cd0 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx @@ -0,0 +1,325 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import styled from "styled-components"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore +import script from "!!raw-loader!./customWidgetscript.js"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore +import appsmithConsole from "!!raw-loader!./appsmithConsole.js"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore +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 "ee/reducers"; +import { combinedPreviewModeSelector } from "selectors/editorSelectors"; +import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { EVENTS } from "./customWidgetscript"; +import { DynamicHeight } from "utils/WidgetFeatures"; +import { getAppsmithConfigs } from "ee/configs"; +import { getIsAutoHeightWithLimitsChanging } from "utils/hooks/autoHeightUIHooks"; +import { GridDefaults } from "constants/WidgetConstants"; +import { LayoutSystemTypes } from "layoutSystems/types"; + +const StyledIframe = styled.iframe<{ + componentWidth: number; + componentHeight: number; + componentMinHeight: number; +}>` + width: ${(props) => props.componentWidth}px; + height: ${(props) => props.componentHeight}px; + min-height: ${(props) => props.componentMinHeight}px; +`; + +const OverlayDiv = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +`; + +const Container = styled.div` + height: 100%; + width: 100%; +`; + +const { disableIframeWidgetSandbox } = getAppsmithConfigs(); + +function CustomComponent(props: CustomComponentProps) { + const iframe = useRef(null); + + const [loading, setLoading] = React.useState(true); + + const [isIframeReady, setIsIframeReady] = useState(false); + + const [height, setHeight] = useState(props.height); + + const theme = useMemo(() => { + return { + ...props.theme?.colors, + borderRadius: props.theme?.borderRadius?.appBorderRadius, + boxShadow: props.theme?.boxShadow?.appBoxShadow, + }; + }, [props.theme]); + + useEffect(() => { + const handler = (event: MessageEvent) => { + const iframeWindow = + iframe.current?.contentWindow || + iframe.current?.contentDocument?.defaultView; + + if (event.source === iframeWindow) { + // Sending acknowledgement for all messages since we're queueing all the postmessage from iframe + iframe.current?.contentWindow?.postMessage( + { + type: EVENTS.CUSTOM_WIDGET_MESSAGE_RECEIVED_ACK, + key: event.data.key, + success: true, + }, + "*", + ); + + const message = event.data; + + switch (message.type) { + case EVENTS.CUSTOM_WIDGET_READY: + setIsIframeReady(true); + iframe.current?.contentWindow?.postMessage( + { + type: EVENTS.CUSTOM_WIDGET_READY_ACK, + model: props.model, + ui: { + width: props.width, + height: props.height, + }, + mode: props.renderMode, + theme, + }, + "*", + ); + + 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); + break; + case EVENTS.CUSTOM_WIDGET_TRIGGER_EVENT: + props.execute(message.data.eventName, message.data.contextObj); + break; + case EVENTS.CUSTOM_WIDGET_UPDATE_HEIGHT: + const height = message.data.height; + + if ( + props.renderMode !== "BUILDER" && + height && + (props.dynamicHeight !== DynamicHeight.FIXED || + props.layoutSystemType === LayoutSystemTypes.AUTO) + ) { + iframe.current?.style.setProperty("height", `${height}px`); + setHeight(height); + } + + break; + case "CUSTOM_WIDGET_CONSOLE_EVENT": + props.onConsole && + props.onConsole(message.data.type, message.data.args); + break; + } + } + }; + + window.addEventListener("message", handler, false); + + return () => window.removeEventListener("message", handler, false); + }, [ + props.model, + props.width, + props.height, + props.layoutSystemType, + props.dynamicHeight, + ]); + + useEffect(() => { + if (iframe.current && iframe.current.contentWindow && isIframeReady) { + iframe.current.contentWindow.postMessage( + { + type: EVENTS.CUSTOM_WIDGET_MODEL_CHANGE, + model: props.model, + }, + "*", + ); + } + }, [props.model]); + + useEffect(() => { + if (iframe.current && iframe.current.contentWindow && isIframeReady) { + iframe.current.contentWindow.postMessage( + { + type: EVENTS.CUSTOM_WIDGET_UI_CHANGE, + ui: { + width: props.width, + height: height, + }, + }, + "*", + ); + } + }, [props.width, height]); + + useEffect(() => { + if (iframe.current && iframe.current.contentWindow && isIframeReady) { + iframe.current.contentWindow.postMessage( + { + type: EVENTS.CUSTOM_WIDGET_THEME_UPDATE, + theme, + }, + "*", + ); + } + }, [theme]); + + useEffect(() => { + if ( + props.dynamicHeight === DynamicHeight.FIXED && + props.layoutSystemType === LayoutSystemTypes.FIXED + ) { + iframe.current?.style.setProperty("height", `${props.height}px`); + setHeight(props.height); + } + }, [ + props.dynamicHeight, + props.height, + iframe.current, + props.layoutSystemType, + ]); + + const srcDoc = ` + + + + + + + + ${props.srcDoc.html} + + + + + `; + + useEffect(() => { + setLoading(true); + }, [srcDoc]); + + return ( + + {props.needsOverlay && } + + { + setLoading(false); + }} + ref={iframe} + sandbox={ + disableIframeWidgetSandbox + ? undefined + : "allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-scripts" + } + srcDoc={srcDoc} + /> + + + ); +} + +export interface CustomComponentProps { + execute: (eventName: string, contextObj: Record) => void; + update: (data: Record) => void; + model: Record; + srcDoc: { + html: string; + js: string; + css: string; + }; + width: number; + height: number; + onLoadingStateChange?: (state: string) => void; + needsOverlay?: boolean; + onConsole?: (type: string, message: string) => void; + renderMode: "EDITOR" | "DEPLOYED" | "BUILDER"; + theme: AppThemeProperties; + borderColor?: Color; + backgroundColor?: Color; + borderWidth?: number; + borderRadius?: number; + boxShadow?: BoxShadow; + widgetId: string; + dynamicHeight: DynamicHeight; + minDynamicHeight: number; + layoutSystemType?: LayoutSystemTypes; +} + +/** + * 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); + + return { + needsOverlay: + (ownProps.renderMode === "EDITOR" && + !isPreviewMode && + ownProps.widgetId !== getWidgetPropsForPropertyPane(state)?.widgetId) || + getIsAutoHeightWithLimitsChanging(state), + }; +}; + +export default connect(mapStateToProps)(CustomComponent); diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/reset.css b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/reset.css new file mode 100644 index 000000000000..3d9744cc3115 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/reset.css @@ -0,0 +1,126 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + line-height: 1; +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/constants.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/constants.ts new file mode 100644 index 000000000000..e2aa0bc56784 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/constants.ts @@ -0,0 +1,9 @@ +// This file contains common constants which can be used across the widget configuration file (index.ts), widget and component folders. +export const DEFAULT_MODEL = `{ + "tips": [ + "Pass data to this widget in the default model field", + "Access data in the javascript file using the appsmith.model variable", + "Create events in the widget and trigger them in the javascript file using appsmith.triggerEvent('eventName')", + "Access data in CSS as var(--appsmith-model-{property-name})" + ] +}`; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/icon.svg b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/icon.svg new file mode 100644 index 000000000000..b83148e53119 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/index.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/index.ts new file mode 100644 index 000000000000..66f79966dd5a --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/index.ts @@ -0,0 +1 @@ +export { WDSCustomWidget } from "./widget"; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/thumbnail.svg b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/thumbnail.svg new file mode 100644 index 000000000000..b241ea4499d9 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/thumbnail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/defaultApp.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/defaultApp.ts new file mode 100644 index 000000000000..4ea2a3d9c58b --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/defaultApp.ts @@ -0,0 +1,180 @@ +import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants"; + +export default { + uncompiledSrcDoc: { + html: ` +
+`, + css: `.app { + width: calc(1px * var(--appsmith-ui-width)); + justify-content: center; + border-radius: 0px; + border: none; + + .tip-container { + margin-bottom: 20px; + + h2 { + margin-bottom: 20px; + font-size: 16px; + font-weight: 700; + } + + .tip-header { + display: flex; + justify-content: space-between; + align-items: baseline; + + div { + color: #999; + } + } + } + + .button-container { + text-align: right; + + button { + margin: 0 10px; + border-radius: var(--appsmith-theme-borderRadius) !important; + + &.primary { + background: var(--appsmith-theme-primaryColor) !important; + } + + &.reset:not([disabled]) { + 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'; + +function App() { + const [currentIndex, setCurrentIndex] = React.useState(0); + + const handleNext = () => { + setCurrentIndex((prevIndex) => (prevIndex + 1) % appsmith.model.tips.length); + }; + + const handleReset = () => { + setCurrentIndex(0); + appsmith.triggerEvent("onResetClick"); + }; + + return ( + +
+
+

Custom Widget

+
{currentIndex + 1} / {appsmith.model.tips.length}
+
+ {appsmith.model.tips[currentIndex]} +
+
+ + +
+
+); +} + +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(, document.getElementById("root")); +});`, + }, + srcDoc: { + html: ` +
+`, + css: `.app { + width: calc(var(--appsmith-ui-width) * 1px); + justify-content: center; + border-radius: 0px; + border: none; +} + +.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; + border-radius: var(--appsmith-theme-borderRadius) !important; +} + +.button-container button.primary { + background: var(--appsmith-theme-primaryColor) !important; +} + +.button-container button.reset:not([disabled]) { + 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'; + +function App() { + const [currentIndex, setCurrentIndex] = React.useState(0); + const handleNext = () => { + setCurrentIndex(prevIndex => (prevIndex + 1) % appsmith.model.tips.length); + }; + const handleReset = () => { + setCurrentIndex(0); + appsmith.triggerEvent("onResetClick"); + }; + return /*#__PURE__*/React.createElement(Card, { + className: "app", + }, /*#__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, { + className: "reset", + disabled: currentIndex === 0, + onClick: handleReset + }, "Reset"))); +} +appsmith.onReady(() => { + reactDom.render( /*#__PURE__*/React.createElement(App, null), document.getElementById("root")); +});`, + }, +}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/index.tsx new file mode 100644 index 000000000000..485a1e1b9d28 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/index.tsx @@ -0,0 +1,469 @@ +import React from "react"; + +import type { DerivedPropertiesMap } from "WidgetProvider/factory"; + +import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; +import BaseWidget from "widgets/BaseWidget"; + +import CustomComponent from "../component"; + +import IconSVG from "../icon.svg"; +import ThumbnailSVG from "../thumbnail.svg"; +import { WIDGET_PADDING, WIDGET_TAGS } from "constants/WidgetConstants"; +import { ValidationTypes } from "constants/WidgetValidation"; +import type { + AppThemeProperties, + SetterConfig, + Stylesheet, +} from "entities/AppTheming"; +import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils"; +import type { AutocompletionDefinitions } from "WidgetProvider/constants"; +import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; +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_DEFAULT_MODEL_DOC_URL, + CUSTOM_WIDGET_DOC_URL, + CUSTOM_WIDGET_HEIGHT_DOC_URL, +} from "pages/Editor/CustomWidgetBuilder/constants"; +import { Link } from "@appsmith/ads"; +import styled from "styled-components"; +import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; +import { Colors } from "constants/Colors"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { DynamicHeight, type WidgetFeatures } from "utils/WidgetFeatures"; +import { isAirgapped } from "ee/utils/airgapHelpers"; + +const StyledLink = styled(Link)` + display: inline-block; + span { + font-size: 12px; + } +`; + +export class WDSCustomWidget extends BaseWidget< + CustomWidgetProps, + WidgetState +> { + static type = "WDS_CUSTOM_WIDGET"; + + static getConfig() { + return { + name: "Custom", + iconSVG: IconSVG, + thumbnailSVG: ThumbnailSVG, + needsMeta: true, + isCanvas: false, + tags: [WIDGET_TAGS.DISPLAY], + searchTags: ["external"], + isSearchWildcard: true, + hideCard: isAirgapped(), + }; + } + + static getDefaults() { + return { + widgetName: "Custom", + rows: 30, + columns: 23, + version: 1, + onResetClick: "{{showAlert('Successfully reset!!', '');}}", + events: ["onResetClick"], + isVisible: true, + defaultModel: DEFAULT_MODEL, + srcDoc: defaultApp.srcDoc, + uncompiledSrcDoc: defaultApp.uncompiledSrcDoc, + theme: "{{appsmith.theme}}", + dynamicBindingPathList: [{ key: "theme" }], + dynamicTriggerPathList: [{ key: "onResetClick" }], + borderColor: Colors.GREY_5, + borderWidth: "1", + backgroundColor: "#FFFFFF", + }; + } + + static getFeatures(): WidgetFeatures { + return { + dynamicHeight: { + sectionIndex: 2, + active: true, + defaultValue: DynamicHeight.FIXED, + helperText: (props) => { + if (props?.dynamicHeight !== DynamicHeight.FIXED) { + return ( +
+ For the auto-height feature to function correctly, the custom + widget's container should not have a fixed height set.{" "} + + Read more + +
+ ); + } else { + return null; + } + }, + }, + }; + } + + static getAutoLayoutConfig() { + return { + autoDimension: { + height: true, + }, + disabledPropsDefaults: { + dynamicHeight: DynamicHeight.AUTO_HEIGHT, + }, + widgetSize: [ + { + viewportMinWidth: 0, + configuration: () => { + return { + minWidth: "120px", + minHeight: "40px", + }; + }, + }, + ], + disableResizeHandles: { + vertical: true, + }, + }; + } + + static getAutocompleteDefinitions(): AutocompletionDefinitions { + return (widget: CustomWidgetProps, extraDefsToDefine?: ExtraDef) => ({ + isVisible: DefaultAutocompleteDefinitions.isVisible, + model: generateTypeDef(widget.model, extraDefsToDefine), + }); + } + + static getSetterConfig(): SetterConfig { + return { + __setters: { + setVisibility: { + path: "isVisible", + type: "boolean", + }, + }, + }; + } + + static getStylesheetConfig(): Stylesheet { + return { + borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", + boxShadow: "{{appsmith.theme.boxShadow.appBoxShadow}}", + }; + } + + static getPropertyPaneContentConfig() { + return [ + { + sectionName: "Widget", + children: [ + { + propertyName: "editSource", + label: "", + controlType: "CUSTOM_WIDGET_EDIT_BUTTON_CONTROL", + isJSConvertible: false, + isBindProperty: false, + isTriggerProperty: false, + dependencies: ["srcDoc", "events", "uncompiledSrcDoc"], + evaluatedDependencies: ["defaultModel", "theme"], + dynamicDependencies: (widget: WidgetProps) => widget.events, + helperText: ( +
+ The source editor lets you add your own HTML, CSS and JS.{" "} + + Read more + +
+ ), + }, + ], + }, + { + sectionName: "Default Model", + children: [ + { + propertyName: "defaultModel", + helperText: ( +
+ This model exposes Appsmith data to the widget editor.{" "} + + Read more + +
+ ), + label: "", + controlType: "INPUT_TEXT", + defaultValue: "{}", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.OBJECT, + }, + }, + ], + }, + { + sectionName: "General", + children: [ + { + propertyName: "isVisible", + label: "Visible", + helpText: "Controls the visibility of the widget", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + ], + }, + { + sectionName: "Events", + hasDynamicProperties: true, + generateDynamicProperties: (widgetProps: WidgetProps) => { + return widgetProps.events?.map((event: string) => ({ + propertyName: event, + label: event, + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + controlConfig: { + allowEdit: true, + onEdit: (widget: CustomWidgetProps, newLabel: string) => { + const triggerPaths = []; + const updatedProperties = { + events: widget.events.map((e) => { + if (e === event) { + return newLabel; + } + + return e; + }), + }; + + if ( + widget.dynamicTriggerPathList + ?.map((d) => d.key) + .includes(event) + ) { + triggerPaths.push(newLabel); + } + + return { + modify: updatedProperties, + triggerPaths, + }; + }, + allowDelete: true, + onDelete: (widget: CustomWidgetProps) => { + return { + events: widget.events.filter((e) => e !== event), + }; + }, + }, + dependencies: ["events", "dynamicTriggerPathList"], + helpText: "when the event is triggered from custom widget", + })); + }, + children: [ + { + propertyName: "generateEvents", + label: "", + controlType: "CUSTOM_WIDGET_ADD_EVENT_BUTTON_CONTROL", + isJSConvertible: false, + isBindProperty: false, + buttonLabel: "Add Event", + onAdd: (widget: CustomWidgetProps, event: string) => { + const events = widget.events; + + return { + events: [...events, event], + }; + }, + isTriggerProperty: false, + dependencies: ["events"], + size: "md", + }, + ], + }, + ]; + } + + static getPropertyPaneStyleConfig() { + 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 { + return {}; + } + + static getDefaultPropertiesMap(): Record { + return { + model: "defaultModel", + }; + } + + // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static getMetaPropertiesMap(): Record { + return { + model: undefined, + }; + } + + execute = (eventName: string, contextObj: Record) => { + if (this.props.hasOwnProperty(eventName)) { + const eventString = this.props[eventName]; + + super.executeAction({ + triggerPropertyName: eventName, + dynamicString: eventString, + event: { + type: EventType.CUSTOM_WIDGET_EVENT, + }, + globalContext: contextObj, + }); + + AnalyticsUtil.logEvent("CUSTOM_WIDGET_API_TRIGGER_EVENT", { + widgetId: this.props.widgetId, + eventName, + }); + } + }; + + update = (data: Record) => { + this.props.updateWidgetMetaProperty("model", { + ...this.props.model, + ...data, + }); + + AnalyticsUtil.logEvent("CUSTOM_WIDGET_API_UPDATE_MODEL", { + widgetId: this.props.widgetId, + }); + }; + + getRenderMode = () => { + switch (this.props.renderMode) { + case "CANVAS": + return "EDITOR"; + default: + return "DEPLOYED"; + } + }; + + getWidgetView() { + return ( + + ); + } +} + +export interface CustomWidgetProps extends WidgetProps { + events: string[]; + theme: AppThemeProperties; +} diff --git a/app/client/src/modules/ui-builder/ui/wds/constants.ts b/app/client/src/modules/ui-builder/ui/wds/constants.ts index 8f897732e6b0..c05690676848 100644 --- a/app/client/src/modules/ui-builder/ui/wds/constants.ts +++ b/app/client/src/modules/ui-builder/ui/wds/constants.ts @@ -61,7 +61,7 @@ export const WDS_V2_WIDGET_MAP = { MULTILINE_INPUT_WIDGET: "WDS_MULTILINE_INPUT_WIDGET", WDS_SELECT_WIDGET: "WDS_SELECT_WIDGET", WDS_COMBOBOX_WIDGET: "WDS_COMBOBOX_WIDGET", - + WDS_CUSTOM_WIDGET: "WDS_CUSTOM_WIDGET", // Anvil layout widgets ZONE_WIDGET: anvilWidgets.ZONE_WIDGET, diff --git a/app/client/src/widgets/index.ts b/app/client/src/widgets/index.ts index c1e4cb2e52ca..e38e01e93988 100644 --- a/app/client/src/widgets/index.ts +++ b/app/client/src/widgets/index.ts @@ -87,6 +87,7 @@ import { WDSPasswordInputWidget } from "modules/ui-builder/ui/wds/WDSPasswordInp import { WDSNumberInputWidget } from "modules/ui-builder/ui/wds/WDSNumberInputWidget"; import { WDSMultilineInputWidget } from "modules/ui-builder/ui/wds/WDSMultilineInputWidget"; import { WDSSelectWidget } from "modules/ui-builder/ui/wds/WDSSelectWidget"; +import { WDSCustomWidget } from "modules/ui-builder/ui/wds/WDSCustomWidget"; import { EEWDSWidgets } from "ee/modules/ui-builder/ui/wds"; const LegacyWidgets = [ @@ -185,6 +186,7 @@ const WDSWidgets = [ WDSNumberInputWidget, WDSMultilineInputWidget, WDSSelectWidget, + WDSCustomWidget, ]; const Widgets = [ From a62717ff5a697bb1a7065ce3ec9bd43b6c336b31 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Mon, 2 Dec 2024 14:52:25 +0530 Subject: [PATCH 2/2] fix types --- .../wds/WDSCustomWidget/component/index.tsx | 116 +----- .../component/styles.module.css | 4 + .../wds/WDSCustomWidget/config/anvilConfig.ts | 9 + .../config/autocompleteConfig.ts | 13 + .../WDSCustomWidget/config/defaultsConfig.ts | 25 ++ .../ui/wds/WDSCustomWidget/config/index.ts | 6 + .../wds/WDSCustomWidget/config/metaConfig.ts | 17 + .../propertyPaneConfig/contentConfig.tsx | 162 ++++++++ .../config/propertyPaneConfig/index.ts | 2 + .../config/propertyPaneConfig/styleConfig.ts | 24 ++ .../WDSCustomWidget/config/setterConfig.ts | 8 + .../ui/wds/WDSCustomWidget/types.ts | 5 + .../wds/WDSCustomWidget/widget/defaultApp.ts | 7 +- .../ui/wds/WDSCustomWidget/widget/index.tsx | 388 +----------------- 14 files changed, 315 insertions(+), 471 deletions(-) create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/styles.module.css create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/anvilConfig.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/autocompleteConfig.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/defaultsConfig.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/index.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/metaConfig.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/contentConfig.tsx create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/index.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/styleConfig.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/setterConfig.ts create mode 100644 app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/types.ts diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx index eb6740b48cd0..857f500bc34f 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/index.tsx @@ -14,38 +14,12 @@ 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 "ee/reducers"; -import { combinedPreviewModeSelector } from "selectors/editorSelectors"; -import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { EVENTS } from "./customWidgetscript"; -import { DynamicHeight } from "utils/WidgetFeatures"; import { getAppsmithConfigs } from "ee/configs"; -import { getIsAutoHeightWithLimitsChanging } from "utils/hooks/autoHeightUIHooks"; -import { GridDefaults } from "constants/WidgetConstants"; -import { LayoutSystemTypes } from "layoutSystems/types"; - -const StyledIframe = styled.iframe<{ - componentWidth: number; - componentHeight: number; - componentMinHeight: number; -}>` - width: ${(props) => props.componentWidth}px; - height: ${(props) => props.componentHeight}px; - min-height: ${(props) => props.componentMinHeight}px; -`; - -const OverlayDiv = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -`; +import { Elevations } from "../../constants"; +import { ContainerComponent } from "../../Container"; +import styles from "./styles.module.css"; const Container = styled.div` height: 100%; @@ -127,12 +101,7 @@ function CustomComponent(props: CustomComponentProps) { case EVENTS.CUSTOM_WIDGET_UPDATE_HEIGHT: const height = message.data.height; - if ( - props.renderMode !== "BUILDER" && - height && - (props.dynamicHeight !== DynamicHeight.FIXED || - props.layoutSystemType === LayoutSystemTypes.AUTO) - ) { + if (props.renderMode !== "BUILDER" && height) { iframe.current?.style.setProperty("height", `${height}px`); setHeight(height); } @@ -149,13 +118,7 @@ function CustomComponent(props: CustomComponentProps) { window.addEventListener("message", handler, false); return () => window.removeEventListener("message", handler, false); - }, [ - props.model, - props.width, - props.height, - props.layoutSystemType, - props.dynamicHeight, - ]); + }, [props.model]); useEffect(() => { if (iframe.current && iframe.current.contentWindow && isIframeReady) { @@ -196,21 +159,6 @@ function CustomComponent(props: CustomComponentProps) { } }, [theme]); - useEffect(() => { - if ( - props.dynamicHeight === DynamicHeight.FIXED && - props.layoutSystemType === LayoutSystemTypes.FIXED - ) { - iframe.current?.style.setProperty("height", `${props.height}px`); - setHeight(props.height); - } - }, [ - props.dynamicHeight, - props.height, - iframe.current, - props.layoutSystemType, - ]); - const srcDoc = ` @@ -243,23 +191,14 @@ function CustomComponent(props: CustomComponentProps) { "bp3-skeleton": loading, })} > - {props.needsOverlay && } - - { setLoading(false); @@ -272,7 +211,7 @@ function CustomComponent(props: CustomComponentProps) { } srcDoc={srcDoc} /> - + ); } @@ -286,40 +225,13 @@ export interface CustomComponentProps { js: string; css: string; }; - width: number; - height: number; onLoadingStateChange?: (state: string) => void; needsOverlay?: boolean; onConsole?: (type: string, message: string) => void; renderMode: "EDITOR" | "DEPLOYED" | "BUILDER"; theme: AppThemeProperties; - borderColor?: Color; - backgroundColor?: Color; - borderWidth?: number; - borderRadius?: number; - boxShadow?: BoxShadow; widgetId: string; - dynamicHeight: DynamicHeight; - minDynamicHeight: number; - layoutSystemType?: LayoutSystemTypes; + elevatedBackground: boolean; } -/** - * 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); - - return { - needsOverlay: - (ownProps.renderMode === "EDITOR" && - !isPreviewMode && - ownProps.widgetId !== getWidgetPropsForPropertyPane(state)?.widgetId) || - getIsAutoHeightWithLimitsChanging(state), - }; -}; - -export default connect(mapStateToProps)(CustomComponent); +export default CustomComponent; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/styles.module.css b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/styles.module.css new file mode 100644 index 000000000000..472f3f496629 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/component/styles.module.css @@ -0,0 +1,4 @@ +.iframe { + width: 100%; + border-radius: inherit; +} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/anvilConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/anvilConfig.ts new file mode 100644 index 000000000000..85b1e5347f1c --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/anvilConfig.ts @@ -0,0 +1,9 @@ +export const anvilConfig = { + isLargeWidget: true, + widgetSize: { + minWidth: { + base: "100%", + "180px": "sizing-30", + }, + }, +}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/autocompleteConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/autocompleteConfig.ts new file mode 100644 index 000000000000..884e85a19764 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/autocompleteConfig.ts @@ -0,0 +1,13 @@ +import type { ExtraDef } from "utils/autocomplete/defCreatorUtils"; +import { generateTypeDef } from "utils/autocomplete/defCreatorUtils"; +import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils"; + +import type { CustomWidgetProps } from "../types"; + +export const autocompleteConfig = ( + widget: CustomWidgetProps, + extraDefsToDefine?: ExtraDef, +) => ({ + isVisible: DefaultAutocompleteDefinitions.isVisible, + model: generateTypeDef(widget.model, extraDefsToDefine), +}); diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/defaultsConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/defaultsConfig.ts new file mode 100644 index 000000000000..c9f47119554e --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/defaultsConfig.ts @@ -0,0 +1,25 @@ +import { Colors } from "constants/Colors"; +import { ResponsiveBehavior } from "layoutSystems/common/utils/constants"; + +import defaultApp from "../widget/defaultApp"; +import { DEFAULT_MODEL } from "../constants"; + +export const defaultsConfig = { + widgetName: "Custom", + rows: 30, + columns: 23, + version: 1, + onResetClick: "{{showAlert('Successfully reset!!', '');}}", + events: ["onResetClick"], + isVisible: true, + defaultModel: DEFAULT_MODEL, + srcDoc: defaultApp.srcDoc, + uncompiledSrcDoc: defaultApp.uncompiledSrcDoc, + theme: "{{appsmith.theme}}", + dynamicBindingPathList: [{ key: "theme" }], + dynamicTriggerPathList: [{ key: "onResetClick" }], + borderColor: Colors.GREY_5, + borderWidth: "1", + responsiveBehavior: ResponsiveBehavior.Fill, + elevatedBackground: false, +}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/index.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/index.ts new file mode 100644 index 000000000000..27d5d75485c1 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/index.ts @@ -0,0 +1,6 @@ +export * from "./propertyPaneConfig"; +export { metaConfig } from "./metaConfig"; +export { anvilConfig } from "./anvilConfig"; +export { setterConfig } from "./setterConfig"; +export { defaultsConfig } from "./defaultsConfig"; +export { autocompleteConfig } from "./autocompleteConfig"; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/metaConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/metaConfig.ts new file mode 100644 index 000000000000..a4f46c8aa962 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/metaConfig.ts @@ -0,0 +1,17 @@ +import { isAirgapped } from "ee/utils/airgapHelpers"; +import { WIDGET_TAGS } from "constants/WidgetConstants"; + +import IconSVG from "../icon.svg"; +import ThumbnailSVG from "../thumbnail.svg"; + +export const metaConfig = { + name: "Custom", + iconSVG: IconSVG, + thumbnailSVG: ThumbnailSVG, + needsMeta: true, + isCanvas: false, + tags: [WIDGET_TAGS.DISPLAY], + searchTags: ["external"], + isSearchWildcard: true, + hideCard: isAirgapped(), +}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/contentConfig.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/contentConfig.tsx new file mode 100644 index 000000000000..8531cb034bb6 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/contentConfig.tsx @@ -0,0 +1,162 @@ +import React from "react"; +import styled from "styled-components"; +import { Link } from "@appsmith/ads"; +import type { WidgetProps } from "widgets/BaseWidget"; +import { ValidationTypes } from "constants/WidgetValidation"; +import { + CUSTOM_WIDGET_DEFAULT_MODEL_DOC_URL, + CUSTOM_WIDGET_DOC_URL, +} from "pages/Editor/CustomWidgetBuilder/constants"; + +import type { CustomWidgetProps } from "../../types"; + +const StyledLink = styled(Link)` + display: inline-block; + span { + font-size: 12px; + } +`; + +export const propertyPaneContentConfig = [ + { + sectionName: "Widget", + children: [ + { + propertyName: "editSource", + label: "", + controlType: "CUSTOM_WIDGET_EDIT_BUTTON_CONTROL", + isJSConvertible: false, + isBindProperty: false, + isTriggerProperty: false, + dependencies: ["srcDoc", "events", "uncompiledSrcDoc"], + evaluatedDependencies: ["defaultModel", "theme"], + dynamicDependencies: (widget: WidgetProps) => widget.events, + helperText: ( +
+ The source editor lets you add your own HTML, CSS and JS.{" "} + + Read more + +
+ ), + }, + ], + }, + { + sectionName: "Default Model", + children: [ + { + propertyName: "defaultModel", + helperText: ( +
+ This model exposes Appsmith data to the widget editor.{" "} + + Read more + +
+ ), + label: "", + controlType: "INPUT_TEXT", + defaultValue: "{}", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.OBJECT, + }, + }, + ], + }, + { + sectionName: "General", + children: [ + { + propertyName: "isVisible", + label: "Visible", + helpText: "Controls the visibility of the widget", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + ], + }, + { + sectionName: "Events", + hasDynamicProperties: true, + generateDynamicProperties: (widgetProps: WidgetProps) => { + return widgetProps.events?.map((event: string) => ({ + propertyName: event, + label: event, + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + controlConfig: { + allowEdit: true, + onEdit: (widget: CustomWidgetProps, newLabel: string) => { + const triggerPaths = []; + const updatedProperties = { + events: widget.events.map((e) => { + if (e === event) { + return newLabel; + } + + return e; + }), + }; + + if ( + widget.dynamicTriggerPathList?.map((d) => d.key).includes(event) + ) { + triggerPaths.push(newLabel); + } + + return { + modify: updatedProperties, + triggerPaths, + }; + }, + allowDelete: true, + onDelete: (widget: CustomWidgetProps) => { + return { + events: widget.events.filter((e) => e !== event), + }; + }, + }, + dependencies: ["events", "dynamicTriggerPathList"], + helpText: "when the event is triggered from custom widget", + })); + }, + children: [ + { + propertyName: "generateEvents", + label: "", + controlType: "CUSTOM_WIDGET_ADD_EVENT_BUTTON_CONTROL", + isJSConvertible: false, + isBindProperty: false, + buttonLabel: "Add Event", + onAdd: (widget: CustomWidgetProps, event: string) => { + const events = widget.events; + + return { + events: [...events, event], + }; + }, + isTriggerProperty: false, + dependencies: ["events"], + size: "md", + }, + ], + }, +]; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/index.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/index.ts new file mode 100644 index 000000000000..21b90e5f8fdf --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/index.ts @@ -0,0 +1,2 @@ +export { propertyPaneStyleConfig } from "./styleConfig"; +export { propertyPaneContentConfig } from "./contentConfig"; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/styleConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/styleConfig.ts new file mode 100644 index 000000000000..e38afe93395c --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/propertyPaneConfig/styleConfig.ts @@ -0,0 +1,24 @@ +import { ValidationTypes } from "constants/WidgetValidation"; + +export const propertyPaneStyleConfig = [ + { + sectionName: "General", + children: [ + { + propertyName: "elevatedBackground", + label: "Visual Separation", + controlType: "SWITCH", + fullWidth: true, + helpText: + "Sets the semantic elevated background and/or borders of the section. This separates the section visually. This could be useful for separating the contents of this section visually from the rest of the sections in the page", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + isReusable: true, + validation: { + type: ValidationTypes.BOOLEAN, + }, + }, + ], + }, +]; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/setterConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/setterConfig.ts new file mode 100644 index 000000000000..ab8086a961a0 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/config/setterConfig.ts @@ -0,0 +1,8 @@ +export const setterConfig = { + __setters: { + setVisibility: { + path: "isVisible", + type: "boolean", + }, + }, +}; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/types.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/types.ts new file mode 100644 index 000000000000..213238fe0049 --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/types.ts @@ -0,0 +1,5 @@ +import type { WidgetProps } from "widgets/BaseWidget"; + +export interface CustomWidgetProps extends WidgetProps { + events: string[]; +} diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/defaultApp.ts b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/defaultApp.ts index 4ea2a3d9c58b..4e2056ce4ef0 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/defaultApp.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/defaultApp.ts @@ -97,11 +97,16 @@ appsmith.onReady(() => { html: `
`, - css: `.app { + css: `html, body { + background: transparent; +} + +.app { width: calc(var(--appsmith-ui-width) * 1px); justify-content: center; border-radius: 0px; border: none; + background: transparent; } .tip-container { diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/index.tsx index 485a1e1b9d28..bdbb043182f6 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSCustomWidget/widget/index.tsx @@ -1,47 +1,16 @@ import React from "react"; -import type { DerivedPropertiesMap } from "WidgetProvider/factory"; - -import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; import BaseWidget from "widgets/BaseWidget"; - -import CustomComponent from "../component"; - -import IconSVG from "../icon.svg"; -import ThumbnailSVG from "../thumbnail.svg"; -import { WIDGET_PADDING, WIDGET_TAGS } from "constants/WidgetConstants"; -import { ValidationTypes } from "constants/WidgetValidation"; -import type { - AppThemeProperties, - SetterConfig, - Stylesheet, -} from "entities/AppTheming"; -import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils"; -import type { AutocompletionDefinitions } from "WidgetProvider/constants"; -import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; -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_DEFAULT_MODEL_DOC_URL, - CUSTOM_WIDGET_DOC_URL, - CUSTOM_WIDGET_HEIGHT_DOC_URL, -} from "pages/Editor/CustomWidgetBuilder/constants"; -import { Link } from "@appsmith/ads"; -import styled from "styled-components"; -import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; -import { Colors } from "constants/Colors"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; -import { DynamicHeight, type WidgetFeatures } from "utils/WidgetFeatures"; -import { isAirgapped } from "ee/utils/airgapHelpers"; +import type { WidgetState } from "widgets/BaseWidget"; +import type { SetterConfig } from "entities/AppTheming"; +import type { AnvilConfig } from "WidgetProvider/constants"; +import type { DerivedPropertiesMap } from "WidgetProvider/factory"; +import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; -const StyledLink = styled(Link)` - display: inline-block; - span { - font-size: 12px; - } -`; +import * as config from "../config"; +import CustomComponent from "../component"; +import type { CustomWidgetProps } from "../types"; export class WDSCustomWidget extends BaseWidget< CustomWidgetProps, @@ -50,334 +19,27 @@ export class WDSCustomWidget extends BaseWidget< static type = "WDS_CUSTOM_WIDGET"; static getConfig() { - return { - name: "Custom", - iconSVG: IconSVG, - thumbnailSVG: ThumbnailSVG, - needsMeta: true, - isCanvas: false, - tags: [WIDGET_TAGS.DISPLAY], - searchTags: ["external"], - isSearchWildcard: true, - hideCard: isAirgapped(), - }; + return config.metaConfig; } static getDefaults() { - return { - widgetName: "Custom", - rows: 30, - columns: 23, - version: 1, - onResetClick: "{{showAlert('Successfully reset!!', '');}}", - events: ["onResetClick"], - isVisible: true, - defaultModel: DEFAULT_MODEL, - srcDoc: defaultApp.srcDoc, - uncompiledSrcDoc: defaultApp.uncompiledSrcDoc, - theme: "{{appsmith.theme}}", - dynamicBindingPathList: [{ key: "theme" }], - dynamicTriggerPathList: [{ key: "onResetClick" }], - borderColor: Colors.GREY_5, - borderWidth: "1", - backgroundColor: "#FFFFFF", - }; + return config.defaultsConfig; } - static getFeatures(): WidgetFeatures { - return { - dynamicHeight: { - sectionIndex: 2, - active: true, - defaultValue: DynamicHeight.FIXED, - helperText: (props) => { - if (props?.dynamicHeight !== DynamicHeight.FIXED) { - return ( -
- For the auto-height feature to function correctly, the custom - widget's container should not have a fixed height set.{" "} - - Read more - -
- ); - } else { - return null; - } - }, - }, - }; - } - - static getAutoLayoutConfig() { - return { - autoDimension: { - height: true, - }, - disabledPropsDefaults: { - dynamicHeight: DynamicHeight.AUTO_HEIGHT, - }, - widgetSize: [ - { - viewportMinWidth: 0, - configuration: () => { - return { - minWidth: "120px", - minHeight: "40px", - }; - }, - }, - ], - disableResizeHandles: { - vertical: true, - }, - }; - } - - static getAutocompleteDefinitions(): AutocompletionDefinitions { - return (widget: CustomWidgetProps, extraDefsToDefine?: ExtraDef) => ({ - isVisible: DefaultAutocompleteDefinitions.isVisible, - model: generateTypeDef(widget.model, extraDefsToDefine), - }); + static getAutocompleteDefinitions() { + return config.autocompleteConfig; } static getSetterConfig(): SetterConfig { - return { - __setters: { - setVisibility: { - path: "isVisible", - type: "boolean", - }, - }, - }; - } - - static getStylesheetConfig(): Stylesheet { - return { - borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", - boxShadow: "{{appsmith.theme.boxShadow.appBoxShadow}}", - }; + return config.setterConfig; } static getPropertyPaneContentConfig() { - return [ - { - sectionName: "Widget", - children: [ - { - propertyName: "editSource", - label: "", - controlType: "CUSTOM_WIDGET_EDIT_BUTTON_CONTROL", - isJSConvertible: false, - isBindProperty: false, - isTriggerProperty: false, - dependencies: ["srcDoc", "events", "uncompiledSrcDoc"], - evaluatedDependencies: ["defaultModel", "theme"], - dynamicDependencies: (widget: WidgetProps) => widget.events, - helperText: ( -
- The source editor lets you add your own HTML, CSS and JS.{" "} - - Read more - -
- ), - }, - ], - }, - { - sectionName: "Default Model", - children: [ - { - propertyName: "defaultModel", - helperText: ( -
- This model exposes Appsmith data to the widget editor.{" "} - - Read more - -
- ), - label: "", - controlType: "INPUT_TEXT", - defaultValue: "{}", - isBindProperty: true, - isTriggerProperty: false, - validation: { - type: ValidationTypes.OBJECT, - }, - }, - ], - }, - { - sectionName: "General", - children: [ - { - propertyName: "isVisible", - label: "Visible", - helpText: "Controls the visibility of the widget", - controlType: "SWITCH", - isJSConvertible: true, - isBindProperty: true, - isTriggerProperty: false, - validation: { type: ValidationTypes.BOOLEAN }, - }, - ], - }, - { - sectionName: "Events", - hasDynamicProperties: true, - generateDynamicProperties: (widgetProps: WidgetProps) => { - return widgetProps.events?.map((event: string) => ({ - propertyName: event, - label: event, - controlType: "ACTION_SELECTOR", - isJSConvertible: true, - isBindProperty: true, - isTriggerProperty: true, - controlConfig: { - allowEdit: true, - onEdit: (widget: CustomWidgetProps, newLabel: string) => { - const triggerPaths = []; - const updatedProperties = { - events: widget.events.map((e) => { - if (e === event) { - return newLabel; - } - - return e; - }), - }; - - if ( - widget.dynamicTriggerPathList - ?.map((d) => d.key) - .includes(event) - ) { - triggerPaths.push(newLabel); - } - - return { - modify: updatedProperties, - triggerPaths, - }; - }, - allowDelete: true, - onDelete: (widget: CustomWidgetProps) => { - return { - events: widget.events.filter((e) => e !== event), - }; - }, - }, - dependencies: ["events", "dynamicTriggerPathList"], - helpText: "when the event is triggered from custom widget", - })); - }, - children: [ - { - propertyName: "generateEvents", - label: "", - controlType: "CUSTOM_WIDGET_ADD_EVENT_BUTTON_CONTROL", - isJSConvertible: false, - isBindProperty: false, - buttonLabel: "Add Event", - onAdd: (widget: CustomWidgetProps, event: string) => { - const events = widget.events; - - return { - events: [...events, event], - }; - }, - isTriggerProperty: false, - dependencies: ["events"], - size: "md", - }, - ], - }, - ]; + return config.propertyPaneContentConfig; } static getPropertyPaneStyleConfig() { - 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 }, - }, - ], - }, - ]; + return config.propertyPaneStyleConfig; } static getDerivedPropertiesMap(): DerivedPropertiesMap { @@ -438,32 +100,22 @@ export class WDSCustomWidget extends BaseWidget< } }; + static getAnvilConfig(): AnvilConfig | null { + return config.anvilConfig; + } + getWidgetView() { return ( ); } } - -export interface CustomWidgetProps extends WidgetProps { - events: string[]; - theme: AppThemeProperties; -}