/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { Dropdown, DropdownToggle } from "@patternfly/react-core/dist/esm/deprecated/components/Dropdown"; import { Divider } from "@patternfly/react-core/dist/js/components/Divider"; import { Menu } from "@patternfly/react-core/dist/js/components/Menu/Menu"; import { MenuGroup } from "@patternfly/react-core/dist/js/components/Menu/MenuGroup"; import { MenuItem } from "@patternfly/react-core/dist/js/components/Menu/MenuItem"; import { MenuList } from "@patternfly/react-core/dist/js/components/Menu/MenuList"; import { CompressIcon } from "@patternfly/react-icons/dist/js/icons/compress-icon"; import { CopyIcon } from "@patternfly/react-icons/dist/js/icons/copy-icon"; import { CutIcon } from "@patternfly/react-icons/dist/js/icons/cut-icon"; import { ListIcon } from "@patternfly/react-icons/dist/js/icons/list-icon"; import { PasteIcon } from "@patternfly/react-icons/dist/js/icons/paste-icon"; import { TableIcon } from "@patternfly/react-icons/dist/js/icons/table-icon"; import { RebootingIcon } from "@patternfly/react-icons/dist/js/icons/rebooting-icon"; import { ResourcesAlmostEmptyIcon } from "@patternfly/react-icons/dist/js/icons/resources-almost-empty-icon"; import { ResourcesFullIcon } from "@patternfly/react-icons/dist/js/icons/resources-full-icon"; import * as React from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Action, BoxedExpression, Normalized } from "../../api"; import { MenuItemWithHelp } from "../../contextMenu/MenuWithHelp"; import { useBoxedExpressionEditorI18n } from "../../i18n"; import { useNestedExpressionContainer } from "../../resizing/NestedExpressionContainerContext"; import { useBoxedExpressionEditor, useBoxedExpressionEditorDispatch } from "../../BoxedExpressionEditorContext"; import { ContextExpression } from "../ContextExpression/ContextExpression"; import { DecisionTableExpression } from "../DecisionTableExpression/DecisionTableExpression"; import { FunctionExpression } from "../FunctionExpression/FunctionExpression"; import { InvocationExpression } from "../InvocationExpression/InvocationExpression"; import { ListExpression } from "../ListExpression/ListExpression"; import { LiteralExpression } from "../LiteralExpression/LiteralExpression"; import { RelationExpression } from "../RelationExpression/RelationExpression"; import { BoxedExpressionClipboard, buildClipboardFromExpression, DMN_BOXED_EXPRESSION_CLIPBOARD_MIME_TYPE, } from "../../clipboard/clipboard"; import { findAllIdsDeep, mutateExpressionRandomizingIds } from "../../ids/ids"; import "./ExpressionDefinitionLogicTypeSelector.css"; import { NavigationKeysUtils } from "../../keysUtils/keyUtils"; import { ConditionalExpression } from "../ConditionalExpression/ConditionalExpression"; import { IteratorExpressionComponent } from "../IteratorExpression/IteratorExpressionComponent"; import { FilterExpressionComponent } from "../FilterExpression/FilterExpressionComponent"; import FilterIcon from "@patternfly/react-icons/dist/esm/icons/filter-icon"; import { Icon } from "@patternfly/react-core/dist/js"; import { ContextMenuRef, ContextMenu } from "../../contextMenu"; export interface ExpressionDefinitionLogicTypeSelectorProps { /** Expression properties */ expression?: Normalized; /** Function to be invoked when logic type changes */ onLogicTypeSelected: (logicType: BoxedExpression["__$$element"] | undefined) => void; /** Function to be invoked when logic type is reset */ onLogicTypeReset: () => void; /** Function to be invoked to retrieve the DOM reference to be used for selector placement */ getPlacementRef: () => HTMLDivElement; isResetSupported: boolean; isNested: boolean; parentElementId: string; hideDmn14BoxedExpressions?: boolean; } const DEFAULT_LOGIC_TYPE_SELECTOR_HEIGHT = 550; const LOGIC_TYPE_SELECTOR_BOTTOM_MARGIN = 5; const PASTE_MENU_ITEM_ID = "paste"; export function ExpressionDefinitionLogicTypeSelector({ expression, onLogicTypeSelected, onLogicTypeReset, isResetSupported, isNested, parentElementId, hideDmn14BoxedExpressions, }: ExpressionDefinitionLogicTypeSelectorProps) { const nonSelectableLogicTypes = useMemo>( () => (isNested ? new Set([undefined]) : new Set([undefined, "functionDefinition"])), [isNested] ); const { i18n } = useBoxedExpressionEditorI18n(); const { setCurrentlyOpenContextMenu, widthsById, isReadOnly } = useBoxedExpressionEditor(); const selectableLogicTypes = useMemo>( () => [ "literalExpression", "relation", "context", "decisionTable", "list", "invocation", ...(isNested ? (["functionDefinition"] as const) : []), ...(!hideDmn14BoxedExpressions ? (["conditional"] as const) : []), ...(!hideDmn14BoxedExpressions ? (["for"] as const) : []), ...(!hideDmn14BoxedExpressions ? (["every"] as const) : []), ...(!hideDmn14BoxedExpressions ? (["some"] as const) : []), ...(!hideDmn14BoxedExpressions ? (["filter"] as const) : []), ], [hideDmn14BoxedExpressions, isNested] ); const renderExpression = useMemo(() => { const logicType = expression?.__$$element; if (!logicType) { return <>; } switch (logicType) { case "literalExpression": return ; case "relation": return ; case "context": return ; case "decisionTable": return ; case "list": return ; case "invocation": return ; case "functionDefinition": return ; case "conditional": return ; case "for": case "every": case "some": return ( ); case "filter": return ( ); default: assertUnreachable(logicType); } }, [expression, isNested, parentElementId]); const selectLogicType = useCallback( (mouseEvent: React.MouseEvent, itemId?: string | number) => { if (itemId !== PASTE_MENU_ITEM_ID) { onLogicTypeSelected(itemId as BoxedExpression["__$$element"] | undefined); } setCurrentlyOpenContextMenu(undefined); setVisibleHelp(""); mouseEvent.stopPropagation(); }, [onLogicTypeSelected, setCurrentlyOpenContextMenu] ); const resetLogicType = useCallback(() => { setCurrentlyOpenContextMenu(undefined); setDropdownOpen(false); onLogicTypeReset(); }, [onLogicTypeReset, setCurrentlyOpenContextMenu]); const cssClass = useMemo(() => { if (expression) { return `logic-type-selector logic-type-selected`; } else { return `logic-type-selector logic-type-not-present`; } }, [expression]); const resetContextMenuContainerRef = React.useRef(null); const selectExpressionMenuContainerRef = React.useRef(null); const contextMenuRef = React.useRef(null); useEffect(() => { function onContextMenu(e: MouseEvent) { e.stopPropagation(); e.preventDefault(); setDropdownOpen((prev) => !prev); } const a = resetContextMenuContainerRef?.current; a?.addEventListener("contextmenu", onContextMenu); return () => { a?.removeEventListener("contextmenu", onContextMenu); }; }, [expression]); const logicTypeIcon = useCallback((logicType: BoxedExpression["__$$element"] | undefined) => { switch (logicType) { case undefined: return ``; case "literalExpression": return ( FEEL ); case "context": return ( {`{}`} ); case "decisionTable": return ; case "relation": return ; case "functionDefinition": return ( {"f"} ); case "invocation": return ( {"f()"} ); case "list": return ; case "conditional": return ( {"if"} ); case "for": return ; case "every": return ; case "some": return ; case "filter": return ; default: assertUnreachable(logicType); } }, []); const copyExpression = useCallback(async () => { await navigator.clipboard.writeText(JSON.stringify(buildClipboardFromExpression(expression!, widthsById))); setDropdownOpen(false); }, [expression, widthsById]); const cutExpression = useCallback(async () => { await navigator.clipboard.writeText(JSON.stringify(buildClipboardFromExpression(expression!, widthsById))); onLogicTypeReset(); setDropdownOpen(false); }, [expression, onLogicTypeReset, widthsById]); const { setExpression, setWidthsById } = useBoxedExpressionEditorDispatch(); const [pasteExpressionError, setPasteExpressionError] = React.useState(""); const pasteExpression = useCallback(async () => { try { const clipboard: BoxedExpressionClipboard = JSON.parse(await navigator.clipboard.readText()); if (clipboard.mimeType !== DMN_BOXED_EXPRESSION_CLIPBOARD_MIME_TYPE) { throw new Error( "Pasted expression doesn't have the correct mime-type. Likely not copied from the Boxed Expression Editor." ); } const newIdsByOriginalId = mutateExpressionRandomizingIds(clipboard.expression); let oldExpression: Normalized | undefined; setExpression({ setExpressionAction: (prev: Normalized) => { oldExpression = prev; return clipboard.expression; }, // This is mutated to have new IDs by the ID randomizer above. expressionChangedArgs: { action: Action.ExpressionPastedFromClipboard }, }); setWidthsById(({ newMap }) => { for (const id of findAllIdsDeep(oldExpression)) { newMap.delete(id); } for (const originalId in clipboard.widthsById) { newMap.set(newIdsByOriginalId.get(originalId)!, clipboard.widthsById[originalId]); } }); setDropdownOpen(false); setCurrentlyOpenContextMenu(undefined); setPasteExpressionError(""); } catch (err) { setPasteExpressionError(err); } }, [setCurrentlyOpenContextMenu, setExpression, setWidthsById]); const menuIconContainerStyle = useMemo(() => { return { width: "40px", userSelect: "none" as const, position: "relative" as const, }; }, []); const showExpressionHeader = useMemo(() => { if (!expression) { return false; } if (!isNested) { return true; } return expression.__$$element !== "literalExpression" && !nonSelectableLogicTypes.has(expression.__$$element); }, [expression, isNested, nonSelectableLogicTypes]); const logicTypeHelp = useCallback( (logicType: BoxedExpression["__$$element"] | undefined) => { switch (logicType) { case "literalExpression": return i18n.logicTypeHelp.literal; case "context": return i18n.logicTypeHelp.context; case "decisionTable": return i18n.logicTypeHelp.decisionTable; case "relation": return i18n.logicTypeHelp.relation; case "functionDefinition": return i18n.logicTypeHelp.functionDefinition; case "invocation": return i18n.logicTypeHelp.invocation; case "list": return i18n.logicTypeHelp.list; case "conditional": return i18n.logicTypeHelp.conditional; case "for": return i18n.logicTypeHelp.for; case "every": return i18n.logicTypeHelp.every; case "some": return i18n.logicTypeHelp.some; case "filter": return i18n.logicTypeHelp.filter; default: return ""; } }, [ i18n.logicTypeHelp.conditional, i18n.logicTypeHelp.context, i18n.logicTypeHelp.decisionTable, i18n.logicTypeHelp.every, i18n.logicTypeHelp.filter, i18n.logicTypeHelp.for, i18n.logicTypeHelp.functionDefinition, i18n.logicTypeHelp.invocation, i18n.logicTypeHelp.list, i18n.logicTypeHelp.literal, i18n.logicTypeHelp.relation, i18n.logicTypeHelp.some, ] ); const headerMenuItems = useMemo(() => { return ( {!isReadOnly && isResetSupported && ( <> } > {i18n.terms.reset} )} } > {i18n.terms.copy} {!isReadOnly && isResetSupported && ( } > {i18n.terms.cut} )} {!isReadOnly && ( } > {i18n.terms.paste} )} ); }, [ pasteExpressionError, copyExpression, cutExpression, i18n, menuIconContainerStyle, pasteExpression, resetLogicType, isResetSupported, isReadOnly, ]); const [isDropdownOpen, setDropdownOpen] = useState(false); const nestedExpressionContainer = useNestedExpressionContainer(); const [visibleHelp, setVisibleHelp] = React.useState(""); const toggleVisibleHelp = useCallback((help: string) => { setVisibleHelp((previousHelp) => (previousHelp !== help ? help : "")); }, []); return ( <> {(!expression && (
{i18n.selectExpression} {selectableLogicTypes.map((key) => { const label = getLogicTypeLabel(key); return ( ); })}
} > {i18n.terms.paste} )) || (
{expression ? ( <> {showExpressionHeader && expression && (
{ if (NavigationKeysUtils.isEsc(e.key)) { setDropdownOpen(false); } }} style={{ width: "100%" }} toggle={ <>{logicTypeIcon(expression.__$$element)} } onToggle={(_event, val) => setDropdownOpen(val)} tabIndex={-1} style={{ padding: 0, fontSize: "0.8em" }} > {getLogicTypeLabel(expression?.__$$element)} {expression.__$$element === "functionDefinition" && ` (${expression["@_kind"]})`} } > <>{headerMenuItems}
)} {renderExpression} ) : ( i18n.selectExpression )}
)} {!showExpressionHeader && ( {headerMenuItems} )} ); } export function assertUnreachable(_x: never): never { throw new Error("Didn't expect to get here: " + _x); } function getLogicTypeLabel(logicType: BoxedExpression["__$$element"] | undefined) { switch (logicType) { case undefined: return "Undefined"; case "context": return "Context"; case "literalExpression": return "Literal"; case "relation": return "Relation"; case "decisionTable": return "Decision table"; case "list": return "List"; case "invocation": return "Invocation"; case "functionDefinition": return "Function"; case "for": return "For"; case "every": return "Every"; case "some": return "Some"; case "conditional": return "Conditional"; case "filter": return "Filter"; } }