UNPKG

@embeddable.com/react

Version:
254 lines (247 loc) 12.1 kB
import * as React from 'react'; import React__default from 'react'; import { isLoadDataParams, nativeTypes, setValue } from '@embeddable.com/core'; const EmbeddableStateContext = React__default.createContext({}); const useEmbeddableState = (initialState = {}) => { const ctx = React__default.useContext(EmbeddableStateContext); React__default.useEffect(() => { // Only call setter if it exists (when there's a provider) if (Array.isArray(ctx) && typeof ctx[1] === "function") { ctx[1](initialState); } }, [JSON.stringify(initialState)]); return ctx; }; const EmbeddableThemeContext = React__default.createContext({}); const useTheme = () => { return React__default.useContext(EmbeddableThemeContext); }; /** * Cleans up the inputs of an object, removing all sub inputs except for granularity. * @param obj */ function cleanInputsWithGranularity(obj) { if (!obj || !("inputs" in obj)) return obj; const { inputs, ...rest } = obj; if (typeof inputs === "object" && "granularity" in inputs) { return { ...rest, inputs: { granularity: inputs.granularity } }; } return rest; } /** * Cleans up the request parameters by removing all sub inputs except for granularity. * @param requestParams */ const cleanupRequestParams = (requestParams) => { return { ...requestParams, dimensions: Array.isArray(requestParams.dimensions) ? requestParams.dimensions.map(cleanInputsWithGranularity) : requestParams.dimensions, measures: Array.isArray(requestParams.measures) ? requestParams.measures.map(cleanInputsWithGranularity) : requestParams.measures, select: Array.isArray(requestParams.select) ? requestParams.select.map((item) => Array.isArray(item) ? item.map(cleanInputsWithGranularity) : cleanInputsWithGranularity(item)) : requestParams.select, orderBy: Array.isArray(requestParams.orderBy) ? requestParams.orderBy.map((item) => ({ ...item, property: cleanInputsWithGranularity(item.property), })) : requestParams.orderBy, filters: Array.isArray(requestParams.filters) ? requestParams.filters.map((filter) => ({ ...filter, property: cleanInputsWithGranularity(filter.property), })) : requestParams.filters, }; }; const UPDATE_CLIENT_CONTEXT_EVENT_NAME = "embeddable-event:update-client-context"; const UPDATE_PROPS_EVENT_NAME = "embeddable-event:update-props"; const RELOAD_DATASET_EVENT_NAME = "embeddable-event:reload-dataset"; const LOAD_DATA_RESULT_EVENT_NAME = "embeddable-event:load-data-result"; const UPDATE_THEME_EVENT_NAME = "embeddable-event:update-theme"; const ReducerActionTypes = { loading: "loading", error: "error", data: "data", }; const reducer = (state, action) => { switch (action.type) { case ReducerActionTypes.loading: { return { ...state, [action.inputName]: { data: state[action.inputName]?.data, isLoading: true, }, }; } case ReducerActionTypes.data: { return { ...state, [action.inputName]: { isLoading: false, data: action.payload.data, total: action.payload.total, }, }; } case ReducerActionTypes.error: { return { ...state, [action.inputName]: { isLoading: false, error: action.payload.message || action.payload, }, }; } } return state; }; const createInitialLoadersState = (dataLoaders) => Object.keys(dataLoaders).reduce((loaderState, loaderKey) => ({ ...loaderState, [loaderKey]: { isLoading: true }, }), {}); const deserializeProps = (props, meta) => Object.fromEntries(Object.entries(props).map(([propName, propValue]) => { const inputPropConfig = meta.inputs?.find((config) => config.name === propName); const deserialize = typeof inputPropConfig?.type === "string" ? Object.values(nativeTypes).find((type) => type.toString() === inputPropConfig?.type)?.typeConfig?.transform : inputPropConfig?.type?.typeConfig?.transform; return [propName, deserialize?.(propValue) ?? propValue]; })); const getInputValuesFromMeta = (meta) => { let inputValues = {}; (meta.inputs || []).forEach((input) => { inputValues = { ...inputValues, [input.name]: input.defaultValue ?? null, }; }); return inputValues; }; function defineComponent(InnerComponent, meta, config = {}) { function EmbeddableWrapper({ propsUpdateListener, clientContext, embeddableTheme, ...props }) { const [propsProxy, setPropsProxy] = React.useState(props); const [clientContextProxy, setClientContextProxy] = React.useState(clientContext); const embeddableState = React.useState(); const [calculatedOverridableProps, setCalculatedOverridableProps] = React.useState(embeddableTheme ?? {}); const { componentId } = props; const loadDataResultEventName = `${LOAD_DATA_RESULT_EVENT_NAME}:${componentId}`; const propsUpdateEventHandler = ({ detail, }) => setPropsProxy(detail); const clientContextUpdateEventHandler = ({ detail, }) => setClientContextProxy(detail); const themeUpdateEventHandler = ({ detail, }) => setCalculatedOverridableProps(detail); React.useEffect(() => { const notifyDevtoolListener = ({ detail, }) => { window.__EMBEDDABLE_DEVTOOLS__?.notifyPropsUpdated(componentId, meta, propsProxy, detail); }; propsUpdateListener.addEventListener(UPDATE_PROPS_EVENT_NAME, notifyDevtoolListener); return () => propsUpdateListener.removeEventListener(UPDATE_PROPS_EVENT_NAME, notifyDevtoolListener); }, [propsProxy]); React.useEffect(() => { propsUpdateListener.addEventListener(UPDATE_CLIENT_CONTEXT_EVENT_NAME, clientContextUpdateEventHandler); propsUpdateListener.addEventListener(UPDATE_PROPS_EVENT_NAME, propsUpdateEventHandler); propsUpdateListener.addEventListener(UPDATE_THEME_EVENT_NAME, themeUpdateEventHandler); const updatePropsEvent = new CustomEvent("embeddable-event:update-props-listen", { detail: { componentId }, }); window.dispatchEvent(updatePropsEvent); const updateClientContextEvent = new CustomEvent("embeddable-event:update-client-context-listen", { detail: { componentId }, }); window.dispatchEvent(updateClientContextEvent); return () => { propsUpdateListener.removeEventListener(UPDATE_CLIENT_CONTEXT_EVENT_NAME, clientContextUpdateEventHandler); propsUpdateListener.removeEventListener(UPDATE_PROPS_EVENT_NAME, propsUpdateEventHandler); }; }, []); const { extendedProps, dataLoaders } = React.useMemo(() => Object.entries({ ...getInputValuesFromMeta(meta), ...config?.props?.(deserializeProps(propsProxy, meta), embeddableState, clientContextProxy), }).reduce((acc, [key, value]) => { if (isLoadDataParams(value)) { acc.dataLoaders[key] = value; } else { acc.extendedProps[key] = value; } return acc; }, { extendedProps: {}, dataLoaders: {} }), [propsProxy, config?.props, embeddableState[0], clientContextProxy]); const [loadersState, dispatch] = React.useReducer(reducer, dataLoaders, createInitialLoadersState); const handleDataLoaded = (inputName, data, total) => dispatch({ type: ReducerActionTypes.data, inputName, payload: { data, total }, }); const handleError = (inputName, error) => dispatch({ type: ReducerActionTypes.error, inputName, payload: error }); const reloadDataset = (inputName, params) => { dispatch({ type: ReducerActionTypes.loading, inputName }); const error = params.dataLoader(propsUpdateListener, params.requestParams, componentId, inputName); if (error) handleError(inputName, error); }; const handleLoadDataResult = (ev) => { window.__EMBEDDABLE_DEVTOOLS__?.notifyDataLoaded(componentId, meta, ev.detail); if (ev.detail.isSuccess) { handleDataLoaded(ev.detail.propertyName, ev.detail.data, ev.detail.total); } else { handleError(ev.detail.propertyName, ev.detail.error); } }; const variableChangedEventHandler = ({ detail, }) => { const dataLoadersEntries = Object.entries(dataLoaders).filter(([_, params]) => params.requestParams.from.datasetId === detail.datasetId); window.__EMBEDDABLE_DEVTOOLS__?.notifyVariableUpdated(componentId, meta, detail, Object.fromEntries(dataLoadersEntries)); dataLoadersEntries.forEach(([inputName, params]) => reloadDataset(inputName, params)); }; React.useEffect(() => { Object.entries(dataLoaders).forEach(([inputName, params]) => reloadDataset(inputName, params)); window.addEventListener(RELOAD_DATASET_EVENT_NAME, variableChangedEventHandler); propsUpdateListener.addEventListener(loadDataResultEventName, handleLoadDataResult); return () => { window.removeEventListener(RELOAD_DATASET_EVENT_NAME, variableChangedEventHandler); propsUpdateListener.removeEventListener(loadDataResultEventName, handleLoadDataResult); }; }, [ JSON.stringify(Object.values(dataLoaders).map((loader) => cleanupRequestParams(loader.requestParams))), ]); const createEvent = (value, eventName) => setValue(propsUpdateListener, value, componentId, eventName); const events = config?.events; const eventProps = {}; if (events) { for (const event in events) { if (events.hasOwnProperty(event)) { let eventFunction = events[event]; eventProps[event] = (value) => createEvent(eventFunction(value), event); } } } return (React.createElement(EmbeddableStateContext.Provider, { value: embeddableState }, React.createElement(EmbeddableThemeContext.Provider, { value: calculatedOverridableProps }, React.createElement(InnerComponent, { ...extendedProps, ...eventProps, ...loadersState })))); } EmbeddableWrapper.displayName = `embedded(${InnerComponent.displayName ?? meta.name})`; return EmbeddableWrapper; } const isOperation = (value) => value?.operation && value?.__embeddableVariableMeta; function defineEditor(InnerComponent, meta, config) { function EmbeddableWrapper(props) { const { componentId, initialValue, propsUpdateListener } = props; const { type: { typeConfig: { transform }, }, } = meta; const [componentState, setComponentState] = React.useState(initialValue); const setter = (value) => { setComponentState(isOperation(value) ? value.value : value); setValue(propsUpdateListener, value, componentId); }; return (React.createElement(InnerComponent, { ...config.inputs(transform?.(componentState) || componentState, setter, meta.config), ...(config.mapProps?.(props) ?? {}) })); } EmbeddableWrapper.displayName = `embedded(${InnerComponent.displayName ?? meta.name})`; return EmbeddableWrapper; } export { EmbeddableThemeContext, defineComponent, defineEditor, useEmbeddableState, useTheme }; //# sourceMappingURL=index.esm.js.map