Skip to content

Commit

Permalink
[Feature]: Component Specific Actions (ToolJet#3448)
Browse files Browse the repository at this point in the history
* Build architecture for component specific actions

* Resolve references component action variables

* Add architecture to read component action params from widgetConfig.js

* Add Set table page to component specific actions

* List only components with actions for control component action

* Make component specific actions async

* fx button styles

* fx in action types

* Improve zIndex for component action param select

* fix for select component width

* adding margin to remove height toggle during fx

* Updated SelectSearch with Select

* Added component specific action to Text widget

* Fixed bug on showing the component with actions

* fix :: fx should align neatly inside the empty space designated for it,If type is not specified, codehinter full width

* removing bug :: popover closes on select

* height toggle bug

* Ensure that action is cleared when a different component is selected

* bugfix :: select width issue

Co-authored-by: Sherfin Shamsudeen <sherfin94@gmail.com>
Co-authored-by: stepinfwd <stepinfwd@gmail.com>
  • Loading branch information
3 people authored Jul 1, 2022
1 parent dc8f69e commit 126bdcd
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 21 deletions.
8 changes: 8 additions & 0 deletions frontend/src/Editor/ActionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,12 @@ export const ActionTypes = [
id: 'unset-custom-variable',
options: [{ name: 'key', type: 'code', default: '' }],
},
{
name: 'Control component',
id: 'control-component',
options: [
{ name: 'component', type: 'text', default: '' },
{ name: 'action', type: 'text', default: '' },
],
},
];
8 changes: 7 additions & 1 deletion frontend/src/Editor/Box.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,13 @@ export const Box = function Box({
exposedVariables={exposedVariables}
styles={resolvedStyles}
setExposedVariable={(variable, value) => onComponentOptionChanged(component, variable, value, extraProps)}
registerAction={(actionName, func) => onComponentOptionChanged(component, actionName, func)}
registerAction={(actionName, func, paramHandles = []) => {
if (Object.keys(exposedVariables).includes(actionName)) return Promise.resolve();
else {
func.paramHandles = paramHandles;
return onComponentOptionChanged(component, actionName, func);
}
}}
fireEvent={fireEvent}
validate={validate}
parentId={parentId}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/Editor/CodeBuilder/CodeHinter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export function CodeHinter({

return (
<div ref={wrapperRef}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div className="fx-outer-wrapper" style={{ display: 'flex', justifyContent: 'space-between' }}>
{paramLabel && (
<div className={`mb-2 field ${options.className}`} data-cy="accordion-components">
<ToolTip label={paramLabel} meta={fieldMeta} />
Expand All @@ -263,7 +263,7 @@ export function CodeHinter({
</div>
<div
className={`row${height === '150px' || height === '300px' ? ' tablr-gutter-x-0' : ''}`}
style={{ width: width, display: codeShow ? 'flex' : 'none' }}
style={{ width: width, display: codeShow ? 'flex' : 'none', marginBottom: codeShow && '8.5px' }}
>
<div className={`col code-hinter-col`} style={{ marginBottom: '0.5rem' }}>
<div className="code-hinter-wrapper" style={{ width: '100%', backgroundColor: darkMode && '#272822' }}>
Expand Down
17 changes: 14 additions & 3 deletions frontend/src/Editor/Components/Button.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import cx from 'classnames';
var tinycolor = require('tinycolor2');

export const Button = function Button({ height, properties, styles, fireEvent }) {
export const Button = function Button({ height, properties, styles, fireEvent, registerAction }) {
const { loadingState, text } = properties;
const { backgroundColor, textColor, borderRadius, visibility, disabledState, loaderColor } = styles;

const [label, setLabel] = useState(text);
useEffect(() => setLabel(text), [text]);

const computedStyles = {
backgroundColor,
color: textColor,
Expand All @@ -17,6 +20,14 @@ export const Button = function Button({ height, properties, styles, fireEvent })
'--loader-color': tinycolor(loaderColor ?? '#fff').toString(),
};

registerAction('click', async function () {
fireEvent('onClick');
});

registerAction('setLabel', async function (label) {
setLabel(label);
});

return (
<div className="widget-button">
<button
Expand All @@ -31,7 +42,7 @@ export const Button = function Button({ height, properties, styles, fireEvent })
}}
data-cy="button-widget"
>
{text}
{label}
</button>
</div>
);
Expand Down
13 changes: 6 additions & 7 deletions frontend/src/Editor/Components/Table/Table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -804,16 +804,15 @@ export function Table({
}
);

const registerSetPageAction = () => {
registerAction('setPage', (targetPageIndex) => {
registerAction(
'setPage',
async function (targetPageIndex) {
setPaginationInternalPageIndex(targetPageIndex);
setExposedVariable('pageIndex', targetPageIndex);
if (!serverSidePagination && clientSidePagination) gotoPage(targetPageIndex - 1);
});
};

useEffect(registerSetPageAction, []);
useEffect(registerSetPageAction, [serverSidePagination, clientSidePagination]);
},
['targetPageIndex']
);

useEffect(() => {
const selectedRowsOriginalData = selectedFlatRows.map((row) => row.original);
Expand Down
17 changes: 13 additions & 4 deletions frontend/src/Editor/Components/Text.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import React, { useState, useEffect } from 'react';
import DOMPurify from 'dompurify';

export const Text = function Text({ height, properties, styles, darkMode }) {
const [loadingState, setLoadingState] = useState(false);

export const Text = function Text({ height, properties, styles, darkMode, registerAction }) {
const { textSize, textColor, textAlign, visibility, disabledState } = styles;

const text = properties.text === 0 || properties.text === false ? properties.text?.toString() : properties.text;
const [loadingState, setLoadingState] = useState(false);
const [text, setText] = useState(() => computeText());

const color = textColor === '#000' ? (darkMode ? '#fff' : '#000') : textColor;

// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => setText(() => computeText()), [properties.text]);
useEffect(() => {
const loadingStateProperty = properties.loadingState;
setLoadingState(loadingStateProperty);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.loadingState]);

registerAction('setText', async function (text) {
setText(text);
});

function computeText() {
return properties.text === 0 || properties.text === false ? properties.text?.toString() : properties.text;
}

const computedStyles = {
color,
height,
Expand Down
136 changes: 134 additions & 2 deletions frontend/src/Editor/Inspector/EventManager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import { CodeHinter } from '../CodeBuilder/CodeHinter';
import { GotoApp } from './ActionConfigurationPanels/GotoApp';
import _ from 'lodash';
import { componentTypes } from '../WidgetManager/components';
import Select from '@/_ui/Select';
import defaultStyles from '@/_ui/Select/styles';

Expand Down Expand Up @@ -71,10 +73,10 @@ export const EventManager = ({
};
});

function getComponentOptions(componentType) {
function getComponentOptions(componentType = '') {
let componentOptions = [];
Object.keys(components || {}).forEach((key) => {
if (components[key].component.component === componentType) {
if (componentType === '' || components[key].component.component === componentType) {
componentOptions.push({
name: components[key].component.name,
value: key,
Expand All @@ -84,6 +86,59 @@ export const EventManager = ({
return componentOptions;
}

function getComponentOptionsOfComponentsWithActions(componentType = '') {
let componentOptions = [];
Object.keys(components || {}).forEach((key) => {
const targetComponentMeta = componentTypes.find(
(componentType) => components[key].component.component === componentType.component
);
if ((targetComponentMeta?.actions?.length ?? 0) > 0) {
if (componentType === '' || components[key].component.component === componentType) {
componentOptions.push({
name: components[key].component.name,
value: key,
});
}
}
});
return componentOptions;
}

function getComponentActionOptions(componentId) {
if (componentId == undefined) return [];
const component = Object.entries(components ?? {}).filter(([key, _value]) => key === componentId)[0][1];
const targetComponentMeta = componentTypes.find(
(componentType) => component.component.component === componentType.component
);
const actions = targetComponentMeta.actions;

const options = actions.map((action) => ({
name: action.displayName,
value: action.handle,
}));

return options;
}

function getAction(componentId, actionHandle) {
if (componentId == undefined || actionHandle == undefined) return {};
const component = Object.entries(components ?? {}).filter(([key, _value]) => key === componentId)[0][1];
const targetComponentMeta = componentTypes.find(
(componentType) => component.component.component === componentType.component
);
const actions = targetComponentMeta.actions;
return actions.find((action) => action.handle === actionHandle);
}

function getComponentActionDefaultParams(componentId, actionHandle) {
const action = getAction(componentId, actionHandle);
const defaultParams = (action.params ?? []).map((param) => ({
handle: param.handle,
value: param.defaultValue,
}));
return defaultParams;
}

function getAllApps() {
let appsOptionsList = [];
apps
Expand Down Expand Up @@ -465,6 +520,83 @@ export const EventManager = ({
</div>
</>
)}
{event.actionId === 'control-component' && (
<>
<div className="row">
<div className="col-3 p-1">Component</div>
<div className="col-9">
<Select
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
options={getComponentOptionsOfComponentsWithActions()}
value={event?.componentId}
search={true}
onChange={(value) => {
handlerChanged(index, 'componentSpecificActionHandle', '');
handlerChanged(index, 'componentId', value);
}}
placeholder="Select.."
styles={styles}
useMenuPortal={false}
/>
</div>
</div>
<div className="row mt-2">
<div className="col-3 p-1">Action</div>
<div className="col-9">
<Select
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
options={getComponentActionOptions(event?.componentId)}
value={event?.componentSpecificActionHandle}
search={true}
onChange={(value) => {
handlerChanged(index, 'componentSpecificActionHandle', value);
handlerChanged(
index,
'componentSpecificActionParams',
getComponentActionDefaultParams(event?.componentId, value)
);
}}
placeholder="Select.."
styles={styles}
useMenuPortal={false}
/>
</div>
</div>
{event?.componentId &&
event?.componentSpecificActionHandle &&
(getAction(event?.componentId, event?.componentSpecificActionHandle).params ?? []).map((param) => (
<div className="row mt-2" key={param.handle}>
<div className="col-3 p-1">{param.displayName}</div>
<div
className={`${
param?.type ? 'col-7' : 'col-9 fx-container-eventmanager-code'
} fx-container-eventmanager ${param.type == 'select' && 'component-action-select'}`}
>
<CodeHinter
theme={darkMode ? 'monokai' : 'default'}
currentState={currentState}
mode="javascript"
initialValue={
event?.componentSpecificActionParams?.find((paramItem) => paramItem.handle === param.handle)
?.value ?? param.defaultValue
}
onChange={(value) => {
const newParam = { ...param, value: value };
const params = event?.componentSpecificActionParams ?? [];
const newParams = params.map((paramOfParamList) =>
paramOfParamList.handle === param.handle ? newParam : param
);
handlerChanged(index, 'componentSpecificActionParams', newParams);
}}
enablePreview={true}
type={param?.type}
fieldMeta={{ options: param?.options }}
/>
</div>
</div>
))}
</>
)}
</div>
</Popover.Content>
</Popover>
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/Editor/WidgetManager/widgetConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,19 @@ export const widgets = [
searchText: '',
selectedRows: [],
},
actions: [
{
handle: 'setPage',
displayName: 'Set page',
params: [
{
handle: 'page',
displayName: 'Page',
defaultValue: '{{1}}',
},
],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
Expand Down Expand Up @@ -166,6 +179,17 @@ export const widgets = [
borderRadius: { type: 'number', displayName: 'Border radius' },
},
exposedVariables: {},
actions: [
{
handle: 'click',
displayName: 'Click',
},
{
handle: 'setLabel',
displayName: 'Set label',
params: [{ handle: 'label', displayName: 'Label', defaultValue: 'New label' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
Expand Down Expand Up @@ -802,6 +826,13 @@ export const widgets = [
disabledState: { type: 'toggle', displayName: 'Disable' },
},
exposedVariables: {},
actions: [
{
handle: 'setText',
displayName: 'Set Text',
params: [{ handle: 'text', displayName: 'Text', defaultValue: 'New text' }],
},
],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/_helpers/appUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,19 @@ export const executeAction = (_ref, event, mode, customVariables) => {
},
});
}

case 'control-component': {
const component = Object.values(_ref.state.currentState?.components ?? {}).filter(
(component) => component.id === event.componentId
)[0];
const action = component[event.componentSpecificActionHandle];
const actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
...param,
value: resolveReferences(param.value, _ref.state.currentState, undefined, customVariables),
}));
const actionPromise = action(...actionArguments.map((argument) => argument.value));
return actionPromise ?? Promise.resolve();
}
}
}
};
Expand Down
Loading

0 comments on commit 126bdcd

Please sign in to comment.