diff --git a/libs/shared/lib/components/dropdowns/index.tsx b/libs/shared/lib/components/dropdowns/index.tsx index a0ef1caaf176aa6a81f2323c801726faa2181b39..47f82156b32113805b40914e5dda9fbc027a10ed 100644 --- a/libs/shared/lib/components/dropdowns/index.tsx +++ b/libs/shared/lib/components/dropdowns/index.tsx @@ -114,7 +114,7 @@ export function DropdownItem({ value, disabled, className, onClick, submenu, sel <li ref={itemRef} style={{ border: 0 }} - className={`cursor-pointer divide-y origin-top-right rounded-sm truncate block text-sm px-2 py-1 hover:bg-secondary-200 ${className && className} ${selected ? 'bg-secondary-400 text-white hover:text-black' : ''}`} + className={`cursor-pointer divide-y origin-top-right rounded-sm truncate flex items-center justify-between block text-sm px-2 pe-1 py-1 hover:bg-secondary-200 ${className && className} ${selected ? 'bg-secondary-400 text-white hover:text-black' : ''}`} onClick={() => { !disabled && onClick && onClick(value); }} @@ -122,7 +122,9 @@ export function DropdownItem({ value, disabled, className, onClick, submenu, sel onMouseLeave={() => setIsSubmenuOpen(false)} > {value} + {submenu != null ? <Icon component='icon-[ic--baseline-arrow-right] ms-2' size={14} /> : ''} {submenu && isSubmenuOpen && <DropdownSubmenuContainer ref={submenuRef}>{submenu}</DropdownSubmenuContainer>} + {children} </li> ); } @@ -133,7 +135,7 @@ type DropdownSubmenuContainerProps = { export const DropdownSubmenuContainer = React.forwardRef<HTMLDivElement, DropdownSubmenuContainerProps>(({ children }, ref) => { return ( - <div ref={ref} className="z-10 absolute bg-light rounded shadow w-44 left-[-80%] -translate-y-7"> + <div ref={ref} className="absolute bg-light p-1 rounded max-h-60 overflow-auto focus:outline-none shadow-sm border left-[97%]" style={{transform: 'translate(0px, calc(50% - 19px))'}}> <ul className="text-sm text-secondary-700">{children}</ul> </div> ); diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx index 2f3f3d2e9cec08f61514b4c631a4f5f415562aa2..7d4a98f33ae1dfcb04be07f471192f5e6e85a57b 100644 --- a/libs/shared/lib/components/inputs/index.tsx +++ b/libs/shared/lib/components/inputs/index.tsx @@ -34,6 +34,7 @@ type TextProps = { className?: string; validate?: (value: any) => boolean; onChange?: (value: string) => void; + onClick?: (e: Event) => void; }; type NumberProps = { @@ -210,6 +211,7 @@ export const TextInput = ({ validate, disabled = false, onChange, + onClick = () => {}, inline = false, tooltip, info, @@ -249,6 +251,7 @@ export const TextInput = ({ onChange(e.target.value); } }} + onClick={onClick} required={required} disabled={disabled} /> diff --git a/libs/shared/lib/components/layout/Popover.tsx b/libs/shared/lib/components/layout/Popover.tsx index 27d5a5f9c14f59eaba0fa3614d83debb73f16248..89085999fc46b1f145f95d096994179b16da0cf2 100644 --- a/libs/shared/lib/components/layout/Popover.tsx +++ b/libs/shared/lib/components/layout/Popover.tsx @@ -99,7 +99,7 @@ type ContextType = }) | null; -const PopoverContext = React.createContext<ContextType>(null); +export const PopoverContext = React.createContext<ContextType>(null); export const usePopoverContext = () => { const context = React.useContext(PopoverContext); diff --git a/libs/shared/lib/components/pills/Pill.tsx b/libs/shared/lib/components/pills/Pill.tsx index b3c2dff7fe03bf968f41b3a3b5126f2fbf370d00..0164732da422c2d6b1261773fe630093ed786fc5 100644 --- a/libs/shared/lib/components/pills/Pill.tsx +++ b/libs/shared/lib/components/pills/Pill.tsx @@ -1,5 +1,5 @@ import React, { useContext, useState } from 'react'; -import { pillWidth, pillHeight, pillXPadding, pillInnerMargin, topLineHeight, pillBorderWidth, pillDropdownPadding } from './pill.const'; +import { pillWidth, pillHeight, pillXPadding, pillInnerMargin, topLineHeight, pillBorderWidth, pillAttributesPadding } from './pill.const'; import { Position } from 'reactflow'; import { PillHandle } from './PillHandle'; import { PillContext } from './PillContext'; @@ -124,8 +124,8 @@ export const Pill = React.memo((props: PillI) => { style={{ top: pillHeight - pillInnerMargin + (corner === 'diamond' ? pillHeight / 2 - pillBorderWidth : 0), width: pillWidth - (corner === 'diamond' ? pillWidth * 0.05 : 0), - paddingLeft: pillDropdownPadding + (corner === 'diamond' ? pillWidth * 0.05 : 0), - paddingRight: pillDropdownPadding, + paddingLeft: pillAttributesPadding + (corner === 'diamond' ? pillWidth * 0.05 : 0), + paddingRight: pillAttributesPadding, }} > {props.children} diff --git a/libs/shared/lib/components/pills/pill.const.ts b/libs/shared/lib/components/pills/pill.const.ts index b888e15cd7334365b542bb0a92950174440ecb05..28bd0c09973778c448f8f6577af054077236e68d 100644 --- a/libs/shared/lib/components/pills/pill.const.ts +++ b/libs/shared/lib/components/pills/pill.const.ts @@ -1,7 +1,7 @@ export const pillHeight = 26; export const pillInnerMargin = 2; export const pillXPadding = 10; -export const pillDropdownPadding = 2; +export const pillAttributesPadding = 2; export const pillWidth = 154; export const topLineHeight = 3; export const pillBorderWidth = 1; diff --git a/libs/shared/lib/components/selectors/entityPillSelector.stories.tsx b/libs/shared/lib/components/selectors/entityPillSelector.stories.tsx index 26bedb47eb0f0e2af156e1b17a1480dd445b7b6b..932e3a1dad9011a0b40c9c6936c00a0047651fab 100644 --- a/libs/shared/lib/components/selectors/entityPillSelector.stories.tsx +++ b/libs/shared/lib/components/selectors/entityPillSelector.stories.tsx @@ -2,13 +2,13 @@ import React, { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { EntityPillSelector } from './entityPillSelector'; -const metaPillDropdown: Meta<typeof EntityPillSelector> = { +const metaPillAttributes: Meta<typeof EntityPillSelector> = { component: EntityPillSelector, title: 'Components/Selectors/Entity', decorators: [(story) => <div className="flex items-center justify-center m-11 p-11">{story()}</div>], }; -export default metaPillDropdown; +export default metaPillAttributes; type Story = StoryObj<typeof EntityPillSelector>; diff --git a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx index 7fd7dae1e9204aab5e711b6512c141a2d6c7ae34..7bb306349e0948bd7e5044250ed590d0cfe9515d 100644 --- a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx @@ -487,12 +487,6 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { [graph], ); - const onNodeContextMenu = (event: React.MouseEvent, node: Node) => { - event.preventDefault(); - graphologyGraph.dropNode(node.id); - dispatch(setQuerybuilderGraphology(graphologyGraph)); - }; - function applyLayout() { if (queryBuilderSettings.layout !== 'manual') { const factory = new LayoutFactory(); @@ -629,8 +623,6 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} onDrop={onDrop} - // onContextMenu={onContextMenu} - onNodeContextMenu={onNodeContextMenu} // onNodesDelete={onNodesDelete} // onNodesChange={onNodesChange} deleteKeyCode="Backspace" diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx index e68c7682432d2afabf0acea4edf466fe54ef0c67..abdd1f8412325eea4c5e4481e5f34ecc4203f4c1 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx @@ -1,10 +1,18 @@ -import { useCheckPermissionPolicy, useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; +import { useCheckPermissionPolicy, useQuerybuilderGraph, useQuerybuilderHash } from '@graphpolaris/shared/lib/data-access'; +import { setQuerybuilderGraphology, toQuerybuilderGraphology, attributeShownToggle } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button } from '@graphpolaris/shared/lib/components/buttons'; +import { Icon } from '@graphpolaris/shared/lib/components/icon'; +import { TextInput } from '@graphpolaris/shared/lib/components/inputs'; import { EntityPill } from '@graphpolaris/shared/lib/components/pills'; import { Handle, Position, useUpdateNodeInternals } from 'reactflow'; -import { SchemaReactflowEntityNode, toHandleId } from '../../../model'; -import { PillDropdown } from '../../pilldropdown/PillDropdown'; +import { NodeAttribute, SchemaReactflowEntityNode, toHandleId } from '../../../model'; +import { PillAttributes } from '../../pillattributes/PillAttributes'; +import { DropdownTrigger, DropdownContainer, DropdownItemContainer, DropdownItem } from '@graphpolaris/shared/lib/components/dropdowns'; +import { PopoverContext } from '@graphpolaris/shared/lib/components/layout/Popover'; +import { useDispatch } from 'react-redux'; +import { useQuerybuilderAttributesShown } from '@graphpolaris/shared/lib/data-access/store'; +import { isEqual } from 'lodash-es'; /** * Component to render an entity flow element @@ -24,10 +32,19 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => { [graph], ); + const IconMap = { + temporal: 'icon-[ic--baseline-calendar-today]', + spatial: 'icon-[ic--baseline-map]', + numerical: 'icon-[ic--baseline-numbers]', + categorical: 'icon-[ic--baseline-abc]', + default: 'icon-[ic--baseline-place]', + }; + const [openDropdown, setOpenDropdown] = useState(false); const { canRead, canWrite } = useCheckPermissionPolicy(); const [readAllowed, setReadAllowed] = useState(false); const [writeAllowed, setWriteAllowed] = useState(false); + const [filter, setFilter] = useState<string>(''); const resource = 'query'; const checkReadPermission = useCallback(async () => { @@ -47,24 +64,83 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => { useEffect(() => { checkWritePermission(); }, [checkWritePermission]); + + const filteredAttributes = useMemo(() => { + if (filter == null || filter.length == 0) return data.attributes; + + return data.attributes.filter((attr) => { + return attr.handleData.attributeName?.toLocaleLowerCase().includes(filter.toLocaleLowerCase()); + }); + }, [filter]); + + const qbHash = useQuerybuilderHash(); + const graphologyGraph = useMemo(() => toQuerybuilderGraphology(graph), [graph, qbHash]); + const dispatch = useDispatch(); + + function removeNode() { + graphologyGraph.dropNode(node.id); + dispatch(setQuerybuilderGraphology(graphologyGraph)); + } + + function addAttribute(attribute: NodeAttribute) { + dispatch(attributeShownToggle(attribute.handleData)); + } + + const attributesBeingShown = useQuerybuilderAttributesShown(); + function isAttributeAdded(attribute: NodeAttribute): boolean { + return attributesBeingShown.some((x) => isEqual(x, attribute.handleData)) + } + return ( <div className="w-fit h-fit nowheel" ref={ref} id="asd"> <EntityPill title={ <div className="flex flex-row justify-between items-center"> <span>{data.name || ''}</span> - <Button - variantType="secondary" - variant="ghost" - size="2xs" - iconComponent={openDropdown ? 'icon-[ic--baseline-arrow-drop-up]' : 'icon-[ic--baseline-arrow-drop-down]'} - onMouseDownCapture={(e) => { - e.stopPropagation(); - e.preventDefault(); - setOpenDropdown(!openDropdown); - }} - className={openDropdown ? 'border-secondary-200' : ''} - /> + + <DropdownContainer> + <DropdownTrigger size="md"> + <Button + variantType="secondary" + variant="ghost" + size="2xs" + iconComponent={openDropdown ? 'icon-[ic--baseline-arrow-drop-up]' : 'icon-[ic--baseline-arrow-drop-down]'} + className={openDropdown ? 'border-secondary-200' : ''} + /> + </DropdownTrigger> + + <DropdownItemContainer> + <PopoverContext.Consumer> + {popover => <DropdownItem value={'Add/remove attribute'} onClick={(e) => { + popover?.setOpen(false); + setOpenDropdown(false); + }} submenu={ + [ + <TextInput + type={'text'} + placeholder="Filter" + size="xs" + className="mb-1 min-w-40 rounded-sm" + value={filter} + onClick={(e) => e.stopPropagation()} + onChange={(v) => setFilter(v)} />, + + filteredAttributes.map(attr => + <DropdownItem + key={attr.handleData.attributeName + attr.handleData.nodeId} + value={attr.handleData.attributeName ?? ''} + selected={isAttributeAdded(attr)} + onClick={(_) => addAttribute(attr)}> + <Icon component={attr?.handleData?.attributeDimension != null ? IconMap[attr.handleData.attributeDimension] : undefined} className="ms-2 float-end" size={16} /> + </DropdownItem> + ) + ] + } />} + </PopoverContext.Consumer> + <DropdownItem value="Remove" className="text-danger" onClick={(e) => removeNode()} /> + + </DropdownItemContainer> + </DropdownContainer> </div> } withHandles="horizontal" @@ -93,11 +169,10 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => { } > {data?.attributes && ( - <PillDropdown + <PillAttributes node={node} attributes={data.attributes} attributeEdges={attributeEdges.map((edge) => edge?.attributes)} - open={openDropdown} /> )} </EntityPill> diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx index 1f4ecc154ab653048a2e39db928b829b9b3e515d..adc55c38689cd9d0bf01f20dd6eafd950bb94cae 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx @@ -2,11 +2,16 @@ import { memo, useState, useMemo, useEffect } from 'react'; import { NodeAttribute, RelationNodeAttributes, SchemaReactflowRelationNode, toHandleId } from '../../../model'; import { useAppDispatch, useQuerybuilderGraph, useQuerybuilderSettings } from '@graphpolaris/shared/lib/data-access'; import { addWarning } from '@graphpolaris/shared/lib/data-access/store/configSlice'; -import { setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; -import { PillDropdown } from '../../pilldropdown/PillDropdown'; -import { Button, DropdownContainer, DropdownTrigger, RelationPill } from '@graphpolaris/shared/lib/components'; +import { setQuerybuilderGraphology, toQuerybuilderGraphology, attributeShownToggle } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; +import { PillAttributes } from '../../pillattributes/PillAttributes'; +import { Button, DropdownContainer, DropdownTrigger, RelationPill, DropdownItemContainer, DropdownItem } from '@graphpolaris/shared/lib/components'; +import { Icon } from '@graphpolaris/shared/lib/components/icon'; +import { TextInput } from '@graphpolaris/shared/lib/components/inputs'; +import { PopoverContext } from '@graphpolaris/shared/lib/components/layout/Popover'; import { Handle, Position } from 'reactflow'; import { pillWidth } from '@graphpolaris/shared/lib/components/pills/pill.const'; +import { useQuerybuilderAttributesShown } from '@graphpolaris/shared/lib/data-access/store'; +import { isEqual } from 'lodash-es'; export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { const data = node.data; @@ -18,12 +23,23 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { () => (graphologyGraph.hasNode(node.id) ? { ...(graphologyGraph.getNodeAttributes(node.id) as RelationNodeAttributes) } : undefined), [node.id], ); + + const IconMap = { + temporal: 'icon-[ic--baseline-calendar-today]', + spatial: 'icon-[ic--baseline-map]', + numerical: 'icon-[ic--baseline-numbers]', + categorical: 'icon-[ic--baseline-abc]', + default: 'icon-[ic--baseline-place]', + }; + const attributeEdges = useMemo( () => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType), [graph], ); const [openDropdown, setOpenDropdown] = useState(false); + const [filter, setFilter] = useState<string>(''); + const [depth, setDepth] = useState<{ min: number; max: number }>({ min: data.depth.min || settings.depth.min, max: data.depth.max || settings.depth.max, @@ -36,6 +52,14 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { setDepth({ min: data.depth.min || settings.depth.min, max: data.depth.max || settings.depth.max }); }, [data.depth]); + const filteredAttributes = useMemo(() => { + if (filter == null || filter.length == 0) return data.attributes; + + return data.attributes.filter((attr) => { + return attr.handleData.attributeName?.toLocaleLowerCase().includes(filter.toLocaleLowerCase()); + }); + }, [filter]); + const onNodeUpdated = () => { if (depth.min < 0) { dispatch(addWarning('The minimum depth cannot be smaller than 0')); @@ -49,6 +73,20 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { } }; + function removeNode() { + graphologyGraph.dropNode(node.id); + dispatch(setQuerybuilderGraphology(graphologyGraph)); + } + + function addAttribute(attribute: NodeAttribute) { + dispatch(attributeShownToggle(attribute.handleData)); + } + + const attributesBeingShown = useQuerybuilderAttributesShown(); + function isAttributeAdded(attribute: NodeAttribute): boolean { + return attributesBeingShown.some((x) => isEqual(x, attribute.handleData)) + } + // TODO: must do this once design is chosen // const onChangeDirection = () => { // if (direction === 'right') { @@ -67,18 +105,50 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { title={ <div className="flex flex-row w-full"> <span className="flex-grow text-justify truncate">{data?.name}</span> - <Button - variantType="secondary" - variant="ghost" - size="2xs" - iconComponent={openDropdown ? 'icon-[ic--baseline-arrow-drop-up]' : 'icon-[ic--baseline-arrow-drop-down]'} - onMouseDownCapture={(e) => { - e.stopPropagation(); - e.preventDefault(); - setOpenDropdown(!openDropdown); - }} - className={openDropdown ? 'border-secondary-200' : ''} - /> + + <DropdownContainer> + <DropdownTrigger size="md"> + <Button + variantType="secondary" + variant="ghost" + size="2xs" + iconComponent={openDropdown ? 'icon-[ic--baseline-arrow-drop-up]' : 'icon-[ic--baseline-arrow-drop-down]'} + className={openDropdown ? 'border-secondary-200' : ''} + /> + </DropdownTrigger> + + <DropdownItemContainer> + <PopoverContext.Consumer> + {popover => <DropdownItem value={'Add/remove attribute'} onClick={(e) => { + popover?.setOpen(false); + setOpenDropdown(false); + }} submenu={ + [ + <TextInput + type={'text'} + placeholder="Filter" + size="xs" + className="mb-1 min-w-40 rounded-sm" + value={filter} + onClick={(e) => e.stopPropagation()} + onChange={(v) => setFilter(v)} />, + + filteredAttributes.map(attr => + <DropdownItem + key={attr.handleData.attributeName + attr.handleData.nodeId} + value={attr.handleData.attributeName ?? ''} + selected={isAttributeAdded(attr)} + onClick={(_) => addAttribute(attr)}> + <Icon component={attr?.handleData?.attributeDimension != null ? IconMap[attr.handleData.attributeDimension] : undefined} className="ms-2 float-end" size={16} /> + </DropdownItem> + ) + ] + } />} + </PopoverContext.Consumer> + <DropdownItem value="Remove" className="text-danger" onClick={(e) => removeNode()} /> + + </DropdownItemContainer> + </DropdownContainer> </div> } withHandles="horizontal" @@ -101,11 +171,10 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => { ></Handle> } > - <PillDropdown + <PillAttributes node={node} attributes={data?.attributes || []} attributeEdges={attributeEdges.map((edge) => edge?.attributes)} - open={openDropdown} mr={-pillWidth * 0.05} /> </RelationPill> diff --git a/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributes.tsx b/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributes.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9f9b1c7bead15456095de1716a89bd42daf726ed --- /dev/null +++ b/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributes.tsx @@ -0,0 +1,58 @@ +import { useMemo, useState } from 'react'; +import { NodeAttribute, QueryGraphEdges, SchemaReactflowEntityNode, SchemaReactflowRelationNode } from '../../model'; +import { useAppDispatch, useQuerybuilderAttributesShown } from '../../..'; +import { isEqual } from 'lodash-es'; +import { PillAttributesItem } from './PillAttributesItem'; + +type PillAttributesProps = { + node: SchemaReactflowEntityNode | SchemaReactflowRelationNode; + attributes: NodeAttribute[]; + attributeEdges: (QueryGraphEdges | undefined)[]; + mr?: number; + className?: string; +}; + +type IconMapType = { + [key: string]: string | undefined; +}; + +const IconMap: IconMapType = { + temporal: 'icon-[ic--baseline-calendar-today]', + spatial: 'icon-[ic--baseline-map]', + numerical: 'icon-[ic--baseline-numbers]', + categorical: 'icon-[ic--baseline-abc]', + default: 'icon-[ic--baseline-place]', +}; + +export const PillAttributes = (props: PillAttributesProps) => { + const attributesBeingShown = useQuerybuilderAttributesShown(); + + const attributesOfInterest = useMemo(() => { + return props.attributes.map((attribute) => + attributesBeingShown.findIndex((x) => isEqual(x, attribute.handleData)) === -1 ? false : true, + ); + }, [attributesBeingShown]); + + return ( + <div className={'border-[1px] border-secondary-200 divide-y divide-secondary-200 !z-50'}> + {attributesOfInterest && + attributesOfInterest.map((showing, i) => { + if (!showing) return null; + return ( + <PillAttributesItem + key={props.attributes[i].handleData.attributeName || i} + node={props.node} + attribute={props.attributes[i]} + mr={props.mr} + className={props.className} + icon={ + props.attributes[i].handleData?.attributeDimension + ? IconMap[props.attributes[i].handleData.attributeDimension || 0] + : 'icon-[ic--outline-question-mark]' + } + /> + ); + })} + </div> + ); +}; diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdownItem.tsx b/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx similarity index 88% rename from libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdownItem.tsx rename to libs/shared/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx index 40093407a77b45551a2e1a6628f8cc1c4f7dc9ba..7052033b50c1431fe50d03dd554ab6ce53929508 100644 --- a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdownItem.tsx +++ b/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx @@ -8,11 +8,11 @@ import { } from '../../model'; import { Handle, Position } from 'reactflow'; import { PillHandle } from '@graphpolaris/shared/lib/components/pills/PillHandle'; -import { pillDropdownPadding } from '@graphpolaris/shared/lib/components/pills/pill.const'; +import { pillAttributesPadding } from '@graphpolaris/shared/lib/components/pills/pill.const'; import { Button } from '../../..'; import { QueryBuilderDispatcherContext } from '../../panel/QueryBuilderDispatcher'; -type PillDropdownItemProps = { +type PillAttributesItemProps = { attribute: NodeAttribute; node: SchemaReactflowEntityNode | SchemaReactflowRelationNode; className?: string; @@ -20,7 +20,7 @@ type PillDropdownItemProps = { icon: string | undefined; }; -export const PillDropdownItem = (props: PillDropdownItemProps) => { +export const PillAttributesItem = (props: PillAttributesItemProps) => { const { openLogicPillCreate } = useContext(QueryBuilderDispatcherContext); if (props.attribute.handleData.attributeName === undefined) { @@ -53,7 +53,7 @@ export const PillDropdownItem = (props: PillDropdownItemProps) => { }} /> <PillHandle - mr={-pillDropdownPadding + (props.mr || 0)} + mr={-pillAttributesPadding + (props.mr || 0)} handleTop="auto" position={Position.Right} className={`stroke-white${props.className ? ` ${props.className}` : ''}`} diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx deleted file mode 100644 index d17a78468cd1d356fe59f286128c0539044eaf7f..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useMemo, useState } from 'react'; -import { NodeAttribute, QueryGraphEdges, SchemaReactflowEntityNode, SchemaReactflowRelationNode } from '../../model'; -import { Button, TextInput, useAppDispatch, useQuerybuilderAttributesShown } from '../../..'; -import { attributeShownToggle } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; -import { isEqual } from 'lodash-es'; -import { PillDropdownItem } from './PillDropdownItem'; - -type PillDropdownProps = { - node: SchemaReactflowEntityNode | SchemaReactflowRelationNode; - attributes: NodeAttribute[]; - attributeEdges: (QueryGraphEdges | undefined)[]; - open: boolean; - mr?: number; - className?: string; -}; - -type IconMapType = { - [key: string]: string | undefined; -}; - -const IconMap: IconMapType = { - temporal: 'icon-[ic--baseline-calendar-today]', - spatial: 'icon-[ic--baseline-map]', - numerical: 'icon-[ic--baseline-numbers]', - categorical: 'icon-[ic--baseline-abc]', - default: 'icon-[ic--baseline-place]', -}; - -export const PillDropdown = (props: PillDropdownProps) => { - const forceOpen = false; - const [filter, setFilter] = useState<string>(''); - const dispatch = useAppDispatch(); - const attributesBeingShown = useQuerybuilderAttributesShown(); - - const attributesOfInterest = useMemo(() => { - return props.attributes.map((attribute) => - attributesBeingShown.findIndex((x) => isEqual(x, attribute.handleData)) === -1 ? false : true, - ); - }, [attributesBeingShown]); - - return ( - <div className={'border-[1px] border-secondary-200 divide-y divide-secondary-200 !z-50'}> - {attributesOfInterest && - attributesOfInterest.map((showing, i) => { - if (!showing) return null; - return ( - <PillDropdownItem - key={props.attributes[i].handleData.attributeName || i} - node={props.node} - attribute={props.attributes[i]} - mr={props.mr} - className={props.className} - icon={ - props.attributes[i].handleData?.attributeDimension - ? IconMap[props.attributes[i].handleData.attributeDimension || 0] - : 'icon-[ic--outline-question-mark]' - } - /> - ); - })} - {(props.open || forceOpen) && ( - <> - <h4 className="p-1 bg-white border-t-[2px] font-semibold text-2xs">Available Attributes:</h4> - <TextInput type={'text'} placeholder="Filter" size="xs" className="!p-0.5" value={filter} onChange={(v) => setFilter(v)} /> - <div className="max-h-28 overflow-auto flex flex-col bg-white"> - {props.attributes.map((attribute, i) => { - if (filter && !attribute.handleData.attributeName?.toLowerCase().includes(filter.toLowerCase())) return null; - if (attribute.handleData.attributeName === undefined) { - throw new Error('attribute.handleData.attributeName is undefined'); - } - - return ( - <Button - key={(attribute.handleData.attributeName || '') + i} - iconComponent={attribute?.handleData?.attributeDimension ? IconMap[attribute.handleData.attributeDimension] : undefined} - iconPosition="trailing" - className={`w-full ${attributesOfInterest[i] ? 'bg-secondary-100' : 'bg-white'} justify-between rounded-none text-[0.7em]`} - variant="ghost" - size={'xs'} - label={attribute.handleData.attributeName} - onMouseDownCapture={(e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - dispatch(attributeShownToggle(attribute.handleData)); - }} - ></Button> - ); - })} - </div> - </> - )} - </div> - ); -}; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/semSubsConfigPanel.stories.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/semSubsConfigPanel.stories.tsx index 4270326c4c6e0544f9e51863086925f1f103bcc4..f50a4bef36fa69f603244457611e38a05f182d0c 100644 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/semSubsConfigPanel.stories.tsx +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/configPanel/semSubsConfigPanel.stories.tsx @@ -2,13 +2,13 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { SemSubsConfigPanel } from '.'; -const metaPillDropdown: Meta<typeof SemSubsConfigPanel> = { +const metaPillAttributes: Meta<typeof SemSubsConfigPanel> = { component: SemSubsConfigPanel, title: 'Visualizations/SemanticSubstrates/configpanel', decorators: [(story) => <div className="flex items-center justify-center w-20 m-11">{story()}</div>], }; -export default metaPillDropdown; +export default metaPillAttributes; type Story = StoryObj<typeof SemSubsConfigPanel>;