mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
* Add routes for multi-page apps * Modify Editor, Viewer and Inspector to accept new app structure * Show a page selector on left side bar * Align component deletion logic with new app schema * Make subcontainer work with multi-page apps * Load components state properly in viewer * Use UUID instead of handle for pages * Display sidebar on viewer to switch pages * Add proper URL suffixing for pages in viewer * Add action to switch page * Revert translation file back to its pre-existing linting * Fix bug that caused modal to not open/close * Add support for query params in page switch * Fix the issue that caused navigation to fail while accessed via slug * Add missing SwitchPage file * Add support for page level variables * Add migration to convert existing apps to new schema * Add rollback for converting multi-page definitions back to single-page * Fix migration for multi-page apps * Adapt import/export service for multi-pages * [improvements] Multi-page applications (#4755) * UI updates for page selector popup card * delete page * delete page check: if only one page exits * switch to home page if the selected page is removed * adds and switch to new page * updating page name * updates to home page and starting page * handle updating the home page when home page is deleted * search box for filtering pages and minor style updates for the page handler card * header search box style fixes * for creating a new page, page handle needs to be unique * seperating into smaller components * updated pinned icon for page selector styles and settinf styles * Leftsidebar header ui component * handle dark theme * page handle ui and dark theme fixes for page menu * page handler edit modal * pinned state and update pinned state for menu options triggered * dark theme fixes for edit modal * handle on update should not be empty or prev * page handler updater * added loading state for saving * handles cancels * fixes slug ui * fixes crash for older app versions * updates the query params when handle gets an update * update homePage to homePageId * removes console.log * go back to the popover for modal close * fixes: Difficult to select page * fixes: Difficult to select the three-dot menu * fixes: on visiting the root url, navigate to homepage on viewer * adds tooltip for url * updates the page selector sidebar with sync with query manager * refactor and cleanup * refactor and cleanup * Compute component state when page is switched * modal should not close on click outside * disable save button if there is not change in the page handle input * should show/hide page menu when hovered * page icon * updates delete icon for disabled state * query manager should always be on top of page selector * checks if homePage key exists in pages def * updates page handler menu * updates the clear icon * page handler menu position * page handler menu position * handle icon * alert msg * global settings handler for updating viewer page navigation * show/hode page navigation for viewer * info text for toggle * Multipages:with sortable list [DnD] (#4783) * applied sortable list * on sort updates the definitions * fixies: app crash for dnd * viwer: canvas width should be 100% when navigation drawer is disbaled * fixes: homepage/startpage reload * clean up Co-authored-by: Sherfin Shamsudeen <sherfin94@gmail.com> * Multipage UI viewer (#4801) * new ui changes for viewer pages * fixes postions for debugger and datasources popover * removes console.log * Multipage : hide page and unhide page feature (#4803) * adds: ability to hide pages * hides pages in viewer * unhide page * hide icon * allow accessing hidden pages from url * add: duplicate page (#4802) * add: duplicate page * do not copy the same references from the original page * page name and page handler should be unique for duplicate pages too * Add support for on-page-load events * Add icon from page settings menu item * Convert existing templates to multi-page schema * error logs for page level and app level errors (#4842) * Adapt comments feature for multi-pages * [Bugfix] multipage - page menu interactions (#4844) * fixes: menu popup interaction * fixes: on modal input focus, we switch the page * Adapt multi-player to multi-pages * Add editingPageId to ymap * Log self, others and editor props in real-time avatar generation * Save editing page id to appDef * Add editingPageId to presence in RealtimeCursors * adds no results ui for empty search results (#4869) * page icon updated (#4870) * fixes:Version switching crashes if the target version does not contain the current page (#4868) * Remove unnecessary setting of editingPageId on ymap * Remove unnecessary console.log * [Bugfix] Multipages: widget inspector event popover unmounts (#4887) * introduced a local state for events * cleaned up inspector.jsx * fixes: table widget inspector event accordion * Do not run switchPage twice when viewer is loaded * Preview should open the currently editing page * Properly place navigation and canvas in viewer * Update app definition whenever event manager changes are made * Add support for browser back and forward button in multi-pages * Rename handleBackButton to handlePageSwitchingBasedOnURLparam * Add support for cut/copy/paste and clone * Fix the crash caused by boxShadow * Add support for background colors in viewer in multi-pages * Run queries to be run on load on viewer, in multi-pages * Fix issue that caused inspector popovers to collapse * resolves workspace vars in viewer mode (#4892) * Multipage : Navigation for Mobile-ui (#4814) * refactored to components * burger menu for mobile ui * merge conflict fix for hidden pages * hamburger menu positioned in the header * viewer header reafctored * viewer mobile page manu styles * handles dark theme * mobile menu with dark mode toggle in the footer * components are moved to page level, handle for mobile layout * style fixes * removing unwanted code block * dark theme fixes * style fixes * fixes: events are sortable (#4895) * fixes: events are sortable * Remove uneccesarily repeated call of setEvents in EventManager Co-authored-by: Sherfin Shamsudeen <sherfin94@gmail.com> * renamed settings to Event handlers (#4898) * updates the page setting title to Page Events * temp commit * Add support for setting max width in percentage * fixes: paramUpdates for boxes: 🙌🏻 * [Bugfix] Multipage - viewer canvas dark theme (#4897) * fixes: darktheme bg for viewer canvas * reverts canvas size * Fix for inspector bouncing back to previous values * resolves pages variables in pythong and js transformation (#4905) * csa support to event manager for pages (#4907) * Add support for setting canvas width in percentages * Persist page level variables across page switches * latest definitions is merged with the current appdef (#4914) * latest definitions is merged with the current appdef * mutating the local obj * cleanup * iterate through pages for new versions are created Co-authored-by: Arpit <arpitnath42@gmail.com>
882 lines
37 KiB
JavaScript
882 lines
37 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { ActionTypes } from '../ActionTypes';
|
|
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
|
import Popover from 'react-bootstrap/Popover';
|
|
import { CodeHinter } from '../CodeBuilder/CodeHinter';
|
|
import { GotoApp } from './ActionConfigurationPanels/GotoApp';
|
|
import { SwitchPage } from './ActionConfigurationPanels/SwitchPage';
|
|
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
|
import useDraggableInPortal from '@/_hooks/useDraggableInPortal';
|
|
import _ from 'lodash';
|
|
import { componentTypes } from '../WidgetManager/components';
|
|
import Select from '@/_ui/Select';
|
|
import defaultStyles from '@/_ui/Select/styles';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
export const EventManager = ({
|
|
component,
|
|
componentMeta,
|
|
currentState,
|
|
components,
|
|
dataQueries,
|
|
eventsChanged,
|
|
apps,
|
|
excludeEvents,
|
|
popOverCallback,
|
|
popoverPlacement,
|
|
pages,
|
|
}) => {
|
|
const [events, setEvents] = useState(() => component.component.definition.events || []);
|
|
const [focusedEventIndex, setFocusedEventIndex] = useState(null);
|
|
const { t } = useTranslation();
|
|
|
|
let actionOptions = ActionTypes.map((action) => {
|
|
return { name: action.name, value: action.id };
|
|
});
|
|
|
|
const darkMode = localStorage.getItem('darkMode') === 'true';
|
|
const styles = {
|
|
...defaultStyles(darkMode),
|
|
menuPortal: (provided) => ({ ...provided, zIndex: 9999 }),
|
|
menuList: (base) => ({
|
|
...base,
|
|
}),
|
|
};
|
|
|
|
const actionLookup = Object.fromEntries(ActionTypes.map((actionType) => [actionType.id, actionType]));
|
|
|
|
let alertTypes = [
|
|
{
|
|
name: 'Info',
|
|
id: 'info',
|
|
},
|
|
{
|
|
name: 'Success',
|
|
id: 'success',
|
|
},
|
|
{
|
|
name: 'Warning',
|
|
id: 'warning',
|
|
},
|
|
{
|
|
name: 'Danger',
|
|
id: 'error',
|
|
},
|
|
];
|
|
|
|
let alertOptions = alertTypes.map((alert) => {
|
|
return { name: alert.name, value: alert.id };
|
|
});
|
|
|
|
excludeEvents = excludeEvents || [];
|
|
|
|
/* Filter events based on excludesEvents ( a list of event ids to exclude ) */
|
|
let possibleEvents = Object.keys(componentMeta.events)
|
|
.filter((eventId) => !excludeEvents.includes(eventId))
|
|
.map((eventId) => {
|
|
return {
|
|
name: componentMeta.events[eventId].displayName,
|
|
value: eventId,
|
|
};
|
|
});
|
|
|
|
function getComponentOptions(componentType = '') {
|
|
let componentOptions = [];
|
|
Object.keys(components || {}).forEach((key) => {
|
|
if (componentType === '' || components[key].component.component === componentType) {
|
|
componentOptions.push({
|
|
name: components[key].component.name,
|
|
value: key,
|
|
});
|
|
}
|
|
});
|
|
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 filteredComponents = Object.entries(components ?? {}).filter(([key, _value]) => key === componentId);
|
|
if (_.isEmpty(filteredComponents)) return [];
|
|
const component = filteredComponents[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 filteredComponents = Object.entries(components ?? {}).filter(([key, _value]) => key === componentId);
|
|
if (_.isEmpty(filteredComponents)) return {};
|
|
const component = filteredComponents[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
|
|
.filter((item) => item.slug !== undefined)
|
|
.forEach((item) => {
|
|
appsOptionsList.push({
|
|
name: item.name,
|
|
value: item.slug,
|
|
});
|
|
});
|
|
return appsOptionsList;
|
|
}
|
|
|
|
function getPageOptions() {
|
|
return pages.map((page) => ({
|
|
name: page.name,
|
|
value: page.id,
|
|
}));
|
|
}
|
|
|
|
function handlerChanged(index, param, value) {
|
|
let newEvents = [...events];
|
|
|
|
let updatedEvent = newEvents[index];
|
|
updatedEvent[param] = value;
|
|
|
|
newEvents[index] = updatedEvent;
|
|
|
|
setEvents(newEvents);
|
|
eventsChanged(newEvents);
|
|
}
|
|
|
|
function removeHandler(index) {
|
|
let newEvents = component.component.definition.events;
|
|
newEvents.splice(index, 1);
|
|
setEvents(newEvents);
|
|
eventsChanged(newEvents);
|
|
}
|
|
|
|
function addHandler() {
|
|
let newEvents = component.component.definition.events;
|
|
newEvents.push({
|
|
eventId: Object.keys(componentMeta.events)[0],
|
|
actionId: 'show-alert',
|
|
message: 'Hello world!',
|
|
alertType: 'info',
|
|
});
|
|
setEvents(newEvents);
|
|
eventsChanged(newEvents);
|
|
}
|
|
function eventPopover(event, index) {
|
|
return (
|
|
<Popover
|
|
id="popover-basic"
|
|
style={{ width: '350px', maxWidth: '350px' }}
|
|
className={`${darkMode && 'popover-dark-themed theme-dark'} shadow`}
|
|
data-cy="popover-card"
|
|
>
|
|
<Popover.Content>
|
|
<div className="row">
|
|
<div className="col-3 p-2">
|
|
<span data-cy="event-label">{t('editor.inspector.eventManager.event', 'Event')}</span>
|
|
</div>
|
|
<div className="col-9" data-cy="event-selection">
|
|
<Select
|
|
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
|
|
options={possibleEvents}
|
|
value={event.eventId}
|
|
search={false}
|
|
onChange={(value) => handlerChanged(index, 'eventId', value)}
|
|
placeholder={t('globals.select', 'Select') + '...'}
|
|
styles={styles}
|
|
useMenuPortal={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="row mt-3">
|
|
<div className="col-3 p-2">
|
|
<span data-cy="action-label">{t('editor.inspector.eventManager.action', 'Action')}</span>
|
|
</div>
|
|
<div className="col-9 popover-action-select-search" data-cy="action-selection">
|
|
<Select
|
|
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
|
|
options={actionOptions}
|
|
value={event.actionId}
|
|
search={false}
|
|
onChange={(value) => handlerChanged(index, 'actionId', value)}
|
|
placeholder={t('globals.select', 'Select') + '...'}
|
|
styles={styles}
|
|
useMenuPortal={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{actionLookup[event.actionId].options?.length > 0 && (
|
|
<div className="hr-text" data-cy="action-option">
|
|
{t('editor.inspector.eventManager.actionOptions', 'Action options')}
|
|
</div>
|
|
)}
|
|
<div>
|
|
{event.actionId === 'show-alert' && (
|
|
<>
|
|
<div className="row">
|
|
<div className="col-3 p-2" data-cy="message-label">
|
|
{t('editor.inspector.eventManager.message', 'Message')}
|
|
</div>
|
|
<div className="col-9" data-cy="alert-message-input-field">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.message}
|
|
onChange={(value) => handlerChanged(index, 'message', value)}
|
|
usePortalEditor={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="row mt-3">
|
|
<div className="col-3 p-2" data-cy="alert-type-label">
|
|
{t('editor.inspector.eventManager.alertType', 'Alert Type')}
|
|
</div>
|
|
<div className="col-9" data-cy="alert-message-type">
|
|
<Select
|
|
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
|
|
options={alertOptions}
|
|
value={event.alertType}
|
|
search={false}
|
|
onChange={(value) => handlerChanged(index, 'alertType', value)}
|
|
placeholder={t('globals.select', 'Select') + '...'}
|
|
styles={styles}
|
|
useMenuPortal={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{event.actionId === 'open-webpage' && (
|
|
<div className="p-1">
|
|
<label className="form-label mt-1">{t('editor.inspector.eventManager.url', 'URL')}</label>
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.url}
|
|
onChange={(value) => handlerChanged(index, 'url', value)}
|
|
usePortalEditor={false}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{event.actionId === 'go-to-app' && (
|
|
<GotoApp
|
|
event={event}
|
|
handlerChanged={handlerChanged}
|
|
eventIndex={index}
|
|
getAllApps={getAllApps}
|
|
currentState={currentState}
|
|
darkMode={darkMode}
|
|
/>
|
|
)}
|
|
|
|
{event.actionId === 'show-modal' && (
|
|
<div className="row">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.modal', 'Modal')}</div>
|
|
<div className="col-9">
|
|
<Select
|
|
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
|
|
options={getComponentOptions('Modal')}
|
|
value={event.modal?.id ?? event.modal}
|
|
search={true}
|
|
onChange={(value) => {
|
|
handlerChanged(index, 'modal', value);
|
|
}}
|
|
placeholder={t('globals.select', 'Select') + '...'}
|
|
styles={styles}
|
|
useMenuPortal={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{event.actionId === 'close-modal' && (
|
|
<div className="row">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.modal', 'Modal')}</div>
|
|
<div className="col-9">
|
|
<Select
|
|
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
|
|
options={getComponentOptions('Modal')}
|
|
value={event.modal?.id ?? event.modal}
|
|
search={true}
|
|
onChange={(value) => {
|
|
handlerChanged(index, 'modal', value);
|
|
}}
|
|
placeholder={t('globals.select', 'Select') + '...'}
|
|
styles={styles}
|
|
useMenuPortal={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{event.actionId === 'copy-to-clipboard' && (
|
|
<div className="p-1">
|
|
<label className="form-label mt-1">{t('editor.inspector.eventManager.text', 'Text')}</label>
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.contentToCopy}
|
|
onChange={(value) => handlerChanged(index, 'contentToCopy', value)}
|
|
usePortalEditor={false}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{event.actionId === 'run-query' && (
|
|
<div className="row">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.query', 'Query')}</div>
|
|
<div className="col-9" data-cy="query-selection-field">
|
|
<Select
|
|
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
|
|
options={dataQueries.map((query) => {
|
|
return { name: query.name, value: query.id };
|
|
})}
|
|
value={event.queryId}
|
|
search={true}
|
|
onChange={(value) => {
|
|
const query = dataQueries.find((dataquery) => dataquery.id === value);
|
|
handlerChanged(index, 'queryId', query.id);
|
|
handlerChanged(index, 'queryName', query.name);
|
|
}}
|
|
placeholder={t('globals.select', 'Select') + '...'}
|
|
styles={styles}
|
|
useMenuPortal={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{event.actionId === 'set-localstorage-value' && (
|
|
<>
|
|
<div className="row">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.key', 'Key')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.key}
|
|
onChange={(value) => handlerChanged(index, 'key', value)}
|
|
enablePreview={true}
|
|
usePortalEditor={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="row mt-3">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.value', 'Value')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.value}
|
|
onChange={(value) => handlerChanged(index, 'value', value)}
|
|
enablePreview={true}
|
|
usePortalEditor={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
{event.actionId === 'generate-file' && (
|
|
<>
|
|
<div className="row">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.type', 'Type')}</div>
|
|
<div className="col-9">
|
|
<Select
|
|
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
|
|
options={[
|
|
{ name: 'CSV', value: 'csv' },
|
|
{ name: 'Text', value: 'plaintext' },
|
|
]}
|
|
value={event.fileType ?? 'csv'}
|
|
search={true}
|
|
onChange={(value) => {
|
|
handlerChanged(index, 'fileType', value);
|
|
}}
|
|
placeholder={t('globals.select', 'Select') + '...'}
|
|
styles={styles}
|
|
useMenuPortal={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="row mt-3">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.fileName', 'File name')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.fileName}
|
|
onChange={(value) => handlerChanged(index, 'fileName', value)}
|
|
enablePreview={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="row mt-3">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.data', 'Data')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.data}
|
|
onChange={(value) => handlerChanged(index, 'data', value)}
|
|
enablePreview={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
{event.actionId === 'set-table-page' && (
|
|
<>
|
|
<div className="row">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.table', 'Table')}</div>
|
|
<div className="col-9">
|
|
<Select
|
|
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
|
|
options={getComponentOptions('Table')}
|
|
value={event.table}
|
|
search={true}
|
|
onChange={(value) => {
|
|
handlerChanged(index, 'table', value);
|
|
}}
|
|
placeholder={t('globals.select', 'Select') + '...'}
|
|
styles={styles}
|
|
useMenuPortal={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="row mt-3">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.pageIndex', 'Page index')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.pageIndex ?? '{{1}}'}
|
|
onChange={(value) => handlerChanged(index, 'pageIndex', value)}
|
|
enablePreview={true}
|
|
usePortalEditor={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
{event.actionId === 'set-custom-variable' && (
|
|
<>
|
|
<div className="row">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.key', 'Key')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.key}
|
|
onChange={(value) => handlerChanged(index, 'key', value)}
|
|
enablePreview={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="row mt-3">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.value', 'Value')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.value}
|
|
onChange={(value) => handlerChanged(index, 'value', value)}
|
|
enablePreview={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
{event.actionId === 'unset-custom-variable' && (
|
|
<>
|
|
<div className="row">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.key', 'Key')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.key}
|
|
onChange={(value) => handlerChanged(index, 'key', value)}
|
|
enablePreview={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
{event.actionId === 'set-page-variable' && (
|
|
<>
|
|
<div className="row">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.key', 'Key')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.key}
|
|
onChange={(value) => handlerChanged(index, 'key', value)}
|
|
enablePreview={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="row mt-3">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.value', 'Value')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.value}
|
|
onChange={(value) => handlerChanged(index, 'value', value)}
|
|
enablePreview={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
{event.actionId === 'unset-page-variable' && (
|
|
<>
|
|
<div className="row">
|
|
<div className="col-3 p-2">{t('editor.inspector.eventManager.key', 'Key')}</div>
|
|
<div className="col-9">
|
|
<CodeHinter
|
|
theme={darkMode ? 'monokai' : 'default'}
|
|
currentState={currentState}
|
|
initialValue={event.key}
|
|
onChange={(value) => handlerChanged(index, 'key', value)}
|
|
enablePreview={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
{event.actionId === 'switch-page' && (
|
|
<SwitchPage
|
|
event={event}
|
|
handlerChanged={handlerChanged}
|
|
eventIndex={index}
|
|
getPages={getPageOptions}
|
|
currentState={currentState}
|
|
darkMode={darkMode}
|
|
/>
|
|
)}
|
|
{event.actionId === 'control-component' && (
|
|
<>
|
|
<div className="row">
|
|
<div className="col-3 p-1" data-cy="action-options-component-field-label">
|
|
{t('editor.inspector.eventManager.component', 'Component')}
|
|
</div>
|
|
<div className="col-9" data-cy="action-options-component-selection-field">
|
|
<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={t('globals.select', 'Select') + '...'}
|
|
styles={styles}
|
|
useMenuPortal={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="row mt-2">
|
|
<div className="col-3 p-1" data-cy="action-options-action-field-label">
|
|
{t('editor.inspector.eventManager.action', 'Action')}
|
|
</div>
|
|
<div className="col-9" data-cy="action-options-action-selection-field">
|
|
<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={t('globals.select', '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" data-cy={`action-options-${param.displayName}-field-label`}>
|
|
{param.displayName}
|
|
</div>
|
|
<div
|
|
className={`${
|
|
param?.type ? 'col-7' : 'col-9 fx-container-eventmanager-code'
|
|
} fx-container-eventmanager ${param.type == 'select' && 'component-action-select'}`}
|
|
data-cy="action-options-text-input-field"
|
|
>
|
|
<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 : paramOfParamList
|
|
);
|
|
handlerChanged(index, 'componentSpecificActionParams', newParams);
|
|
}}
|
|
enablePreview={true}
|
|
type={param?.type}
|
|
fieldMeta={{ options: param?.options }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
</div>
|
|
</Popover.Content>
|
|
</Popover>
|
|
);
|
|
}
|
|
|
|
const reorderEvents = (startIndex, endIndex) => {
|
|
const result = [...component.component.definition.events];
|
|
const [removed] = result.splice(startIndex, 1);
|
|
result.splice(endIndex, 0, removed);
|
|
setEvents(result);
|
|
eventsChanged(result, true);
|
|
};
|
|
|
|
const onDragEnd = ({ source, destination }) => {
|
|
if (!destination || source?.index === destination?.index) {
|
|
return;
|
|
}
|
|
reorderEvents(source.index, destination.index);
|
|
};
|
|
|
|
const renderDraggable = useDraggableInPortal();
|
|
|
|
const renderHandlers = (events) => {
|
|
return (
|
|
<DragDropContext
|
|
onDragEnd={(result) => {
|
|
onDragEnd(result);
|
|
}}
|
|
className="w-100"
|
|
>
|
|
<Droppable droppableId="droppable">
|
|
{({ innerRef, droppableProps, placeholder }) => (
|
|
<div {...droppableProps} ref={innerRef}>
|
|
{events.map((event, index) => {
|
|
const actionMeta = ActionTypes.find((action) => action.id === event.actionId);
|
|
const rowClassName = `card-body p-0 ${focusedEventIndex === index ? ' bg-azure-lt' : ''}`;
|
|
return (
|
|
<Draggable key={`${event.eventId}-${index}`} draggableId={`${event.eventId}-${index}`} index={index}>
|
|
{renderDraggable((provided, snapshot) => {
|
|
if (snapshot.isDragging && focusedEventIndex !== null) {
|
|
setFocusedEventIndex(null);
|
|
document.body.click(); // Hack: Close overlay while dragging
|
|
}
|
|
return (
|
|
<OverlayTrigger
|
|
trigger="click"
|
|
placement={popoverPlacement || 'left'}
|
|
rootClose={true}
|
|
overlay={eventPopover(event, index)}
|
|
onHide={() => setFocusedEventIndex(null)}
|
|
onToggle={(showing) => {
|
|
if (showing) {
|
|
setFocusedEventIndex(index);
|
|
} else {
|
|
setFocusedEventIndex(null);
|
|
eventsChanged(events);
|
|
}
|
|
if (typeof popOverCallback === 'function') popOverCallback(showing);
|
|
}}
|
|
>
|
|
<div
|
|
ref={provided.innerRef}
|
|
{...provided.draggableProps}
|
|
{...provided.dragHandleProps}
|
|
className="mb-1"
|
|
>
|
|
<div className="card column-sort-row">
|
|
<div className={rowClassName} data-cy="event-handler-card">
|
|
<div className="row p-2" role="button">
|
|
<div className="col-auto" style={{ cursor: 'grab' }}>
|
|
<svg
|
|
width="8"
|
|
height="14"
|
|
viewBox="0 0 8 14"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
d="M0.666667 1.66667C0.666667 2.03486 0.965143 2.33333 1.33333 2.33333C1.70152 2.33333 2 2.03486 2 1.66667C2 1.29848 1.70152 1 1.33333 1C0.965143 1 0.666667 1.29848 0.666667 1.66667Z"
|
|
stroke="#8092AC"
|
|
strokeWidth="1.33333"
|
|
/>
|
|
<path
|
|
d="M5.99992 1.66667C5.99992 2.03486 6.2984 2.33333 6.66659 2.33333C7.03478 2.33333 7.33325 2.03486 7.33325 1.66667C7.33325 1.29848 7.03478 1 6.66659 1C6.2984 1 5.99992 1.29848 5.99992 1.66667Z"
|
|
stroke="#8092AC"
|
|
strokeWidth="1.33333"
|
|
/>
|
|
<path
|
|
d="M0.666667 7.00001C0.666667 7.3682 0.965143 7.66668 1.33333 7.66668C1.70152 7.66668 2 7.3682 2 7.00001C2 6.63182 1.70152 6.33334 1.33333 6.33334C0.965143 6.33334 0.666667 6.63182 0.666667 7.00001Z"
|
|
stroke="#8092AC"
|
|
strokeWidth="1.33333"
|
|
/>
|
|
<path
|
|
d="M5.99992 7.00001C5.99992 7.3682 6.2984 7.66668 6.66659 7.66668C7.03478 7.66668 7.33325 7.3682 7.33325 7.00001C7.33325 6.63182 7.03478 6.33334 6.66659 6.33334C6.2984 6.33334 5.99992 6.63182 5.99992 7.00001Z"
|
|
stroke="#8092AC"
|
|
strokeWidth="1.33333"
|
|
/>
|
|
<path
|
|
d="M0.666667 12.3333C0.666667 12.7015 0.965143 13 1.33333 13C1.70152 13 2 12.7015 2 12.3333C2 11.9651 1.70152 11.6667 1.33333 11.6667C0.965143 11.6667 0.666667 11.9651 0.666667 12.3333Z"
|
|
stroke="#8092AC"
|
|
strokeWidth="1.33333"
|
|
/>
|
|
<path
|
|
d="M5.99992 12.3333C5.99992 12.7015 6.2984 13 6.66659 13C7.03478 13 7.33325 12.7015 7.33325 12.3333C7.33325 11.9651 7.03478 11.6667 6.66659 11.6667C6.2984 11.6667 5.99992 11.9651 5.99992 12.3333Z"
|
|
stroke="#8092AC"
|
|
strokeWidth="1.33333"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="col text-truncate" data-cy="event-handler">
|
|
{componentMeta.events[event.eventId]['displayName']}
|
|
</div>
|
|
<div className="col text-truncate" data-cy="event-name">
|
|
<small className="event-action font-weight-light text-truncate">
|
|
{actionMeta.name}
|
|
</small>
|
|
</div>
|
|
<div className="col-auto">
|
|
<span
|
|
className="text-danger"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
removeHandler(index);
|
|
}}
|
|
data-cy="delete-button"
|
|
>
|
|
<svg
|
|
width="10"
|
|
height="16"
|
|
viewBox="0 0 10 16"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
d="M0 13.8333C0 14.75 0.75 15.5 1.66667 15.5H8.33333C9.25 15.5 10 14.75 10 13.8333V3.83333H0V13.8333ZM1.66667 5.5H8.33333V13.8333H1.66667V5.5ZM7.91667 1.33333L7.08333 0.5H2.91667L2.08333 1.33333H0V3H10V1.33333H7.91667Z"
|
|
fill="#8092AC"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</OverlayTrigger>
|
|
);
|
|
})}
|
|
</Draggable>
|
|
);
|
|
})}
|
|
{placeholder}
|
|
</div>
|
|
)}
|
|
</Droppable>
|
|
</DragDropContext>
|
|
);
|
|
};
|
|
|
|
const componentName = componentMeta.name ? componentMeta.name : 'query';
|
|
|
|
if (events.length === 0) {
|
|
return (
|
|
<>
|
|
<div className="text-left mb-3">
|
|
<button
|
|
className="btn btn-sm border-0 font-weight-normal padding-2 col-auto color-primary inspector-add-button"
|
|
onClick={addHandler}
|
|
data-cy="add-event-handler"
|
|
>
|
|
{t('editor.inspector.eventManager.addEventHandler', '+ Add event handler')}
|
|
</button>
|
|
</div>
|
|
<div className="text-left">
|
|
<small className="color-disabled" data-cy="no-event-handler-message">
|
|
{t('editor.inspector.eventManager.emptyMessage', "This {{componentName}} doesn't have any event handlers", {
|
|
componentName: componentName.toLowerCase(),
|
|
})}
|
|
</small>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="text-right mb-3">
|
|
<button
|
|
className="btn btn-sm border-0 font-weight-normal padding-2 col-auto color-primary inspector-add-button"
|
|
onClick={addHandler}
|
|
data-cy="add-more-event-handler"
|
|
>
|
|
{t('editor.inspector.eventManager.addHandler', '+ Add handler')}
|
|
</button>
|
|
</div>
|
|
{renderHandlers(events)}
|
|
</>
|
|
);
|
|
};
|