Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
Commits on Source (9)
Showing
with 217 additions and 50 deletions
apps/web/public/assets/sprite_selected.png

646 B | W: 0px | H: 0px

apps/web/public/assets/sprite_selected.png

697 B | W: 0px | H: 0px

apps/web/public/assets/sprite_selected.png
apps/web/public/assets/sprite_selected.png
apps/web/public/assets/sprite_selected.png
apps/web/public/assets/sprite_selected.png
  • 2-up
  • Swipe
  • Onion skin
apps/web/public/assets/sprite_selected_square.png

100 B | W: 0px | H: 0px

apps/web/public/assets/sprite_selected_square.png

96 B | W: 0px | H: 0px

apps/web/public/assets/sprite_selected_square.png
apps/web/public/assets/sprite_selected_square.png
apps/web/public/assets/sprite_selected_square.png
apps/web/public/assets/sprite_selected_square.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -172,7 +172,7 @@ export const SettingsForm = (props: { onClose(): void; open: 'add' | 'update'; s
<Button
variantType="primary"
className="flex-grow"
label={connection.updating ? formTitle.slice(0, -1) + 'ing...' : formTitle}
label={connection.updating ? (formTitle === 'Add' ? formTitle + 'ing...' : formTitle.slice(0, -1) + 'ing...') : formTitle}
onClick={(event) => {
event.preventDefault();
handleSubmit();
......
......@@ -645,7 +645,7 @@ GraphPolaris uses [Material UI](https://mui.com/material-ui/material-icons/) thr
</div>
```jsx
import Icon from '@graphpolaris/shared/lib/components/icon';
import { Icon } from '@graphpolaris/shared/lib/components/icon';
<Icon name="ArrowBack" size={32} />;
```
......
import React, { ReactElement, ReactPropTypes, useMemo } from 'react';
import styles from './buttons.module.scss';
import Icon, { Sizes } from '../icon';
import { Icon, Sizes } from '../icon';
import { forwardRef } from 'react';
type ButtonProps = {
......
import React, { useState, useEffect, useRef, ReactNode } from 'react';
import styles from './dropdowns.module.scss';
import Icon from '../icon';
import { Icon } from '../icon';
import { ArrowDropDown } from '@mui/icons-material';
import { PopoverContent, PopoverTrigger, Popover, PopoverOptions } from '../layout/Popover';
......@@ -45,7 +45,7 @@ export function DropdownTrigger({
const inner = children || (
<div
className={`inline-flex w-full truncate justify-between items-center gap-x-1.5 ${variantClass} ${textSizeClass} ${paddingClass} text-secondary-900 shadow-sm hover:bg-secondary-50 disabled:bg-secondary-100 disabled:cursor-not-allowed disabled:text-secondary-400 pl-1 truncate${className ? ` ${className}` : ''}`}
className={`inline-flex w-full truncate justify-between items-center gap-x-1.5 ${variantClass} ${textSizeClass} ${paddingClass} text-secondary-900 shadow-sm hover:bg-secondary-50 disabled:bg-secondary-100 disabled:cursor-not-allowed disabled:text-secondary-400 pl-1 truncate cursor-pointer${className ? ` ${className}` : ''}`}
>
<span className={`text-${size}`}>{title}</span>
<Icon component={<ArrowDropDown />} size={16} />
......@@ -70,6 +70,7 @@ type DropdownItemContainerProps = {
};
export const DropdownItemContainer = React.forwardRef<HTMLDivElement, DropdownItemContainerProps>(({ children, className }, ref) => {
if (!children || !React.Children.count(children)) return null;
return (
<PopoverContent
ref={ref}
......@@ -91,9 +92,10 @@ type DropdownItemProps = {
onClick?: (value: string) => void;
submenu?: React.ReactNode;
selected?: boolean;
children?: ReactNode;
};
export function DropdownItem({ value, disabled, className, onClick, submenu, selected }: DropdownItemProps) {
export function DropdownItem({ value, disabled, className, onClick, submenu, selected, children }: DropdownItemProps) {
const itemRef = useRef(null);
const submenuRef = useRef(null);
const [isSubmenuOpen, setIsSubmenuOpen] = useState(false);
......@@ -109,7 +111,7 @@ export function DropdownItem({ value, disabled, className, onClick, submenu, sel
onMouseEnter={() => setIsSubmenuOpen(true)}
onMouseLeave={() => setIsSubmenuOpen(false)}
>
{ value }
{value}
{submenu && isSubmenuOpen && <DropdownSubmenuContainer ref={submenuRef}>{submenu}</DropdownSubmenuContainer>}
</li>
);
......
import { StoryObj, Meta } from '@storybook/react';
import Icon from '../icon';
import { Icon } from '../icon';
import { ArrowBack, DeleteOutline, KeyboardArrowLeft, Settings } from '@mui/icons-material';
const Component: Meta<typeof Icon> = {
......
......@@ -5,9 +5,10 @@ export type Sizes = 12 | 14 | 16 | 20 | 24 | 28 | 32 | 40;
export type IconProps = SVGProps<SVGSVGElement> & {
component: ReactElement<any>;
size?: Sizes;
color?: string;
};
export const Icon: React.FC<IconProps> = ({ component, size = 24, ...props }) => {
export const Icon: React.FC<IconProps> = ({ component, size = 24, color, ...props }) => {
if (!component) {
console.error(`No icon found`);
return <div></div>;
......@@ -15,5 +16,3 @@ export const Icon: React.FC<IconProps> = ({ component, size = 24, ...props }) =>
return React.cloneElement(component, { style: { fontSize: size }, width: size, height: size, ...props });
};
export default Icon;
import { Canvas, Meta, Story } from '@storybook/blocks';
import * as IconStories from './icon.stories';
import Icon from '.';
import { Icon } from '.';
<Meta title="Components/Icon" component={Icon} />
......
import React from 'react';
import Icon from '../icon';
import { Icon } from '../icon';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
import { InfoOutlined } from '@mui/icons-material';
......
......@@ -101,7 +101,7 @@ export const Pill = React.memo((props: PillI) => {
></div>
)}
<div
className={'font-semibold bg-neutral-100 ' + (corner !== 'square' ? 'rounded-b-[3px]' : '')}
className={'font-semibold ' + (corner !== 'square' ? 'rounded-b-[3px]' : '')}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
......
import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import EntityPillSelector, { EntityPillSelectorProps } from './entityPillSelector';
const metaPillDropdown: 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;
type Story = StoryObj<typeof EntityPillSelector>;
export const entity: Story = {
args: {
dropdownNodes: ['kamerleden', 'commissies'],
},
};
import React, { useRef, useState } from 'react';
import { Button } from '../buttons';
import { ArrowDropDown } from '@mui/icons-material';
import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
import { DropdownContainer, DropdownItemContainer, DropdownTrigger, DropdownItem } from '../dropdowns';
export type EntityPillSelectorProps = {
selectedNode?: string;
dropdownNodes: string[];
onSelectOption: (option: string) => void;
};
export function EntityPillSelector({ dropdownNodes, onSelectOption, selectedNode }: EntityPillSelectorProps) {
const [isCollapsed, setIsCollapsed] = useState(true);
// const [initialNamePill, setInitialNamePill] = useState('Choose a node:');
const handleButtonClick = () => {
setIsCollapsed(!isCollapsed);
};
const handleOptionClick = (option: string) => {
setIsCollapsed(true);
onSelectOption(option);
};
return (
<DropdownContainer placement="bottom">
<DropdownTrigger title={selectedNode || 'Choose a node:'} size="sm">
<EntityPill
className="cursor-pointer"
title={
<div className="flex flex-row items-center justify-between pointer-events-none">
<span>{selectedNode || 'Choose a node:'}</span>
<Button variantType="secondary" variant="ghost" size="xs" iconComponent={<ArrowDropDown />} onClick={handleButtonClick} />
</div>
}
/>
</DropdownTrigger>
<DropdownItemContainer>
{dropdownNodes
.map((node, index) => (
<DropdownItem
className="my-0 cursor-pointer"
selected={selectedNode === node}
onClick={() => handleOptionClick(node)}
key={'entity_' + index + '-' + node}
value={node}
>
<EntityPill title={node} />
</DropdownItem>
))
.filter((node) => node.props.value !== selectedNode)}
</DropdownItemContainer>
</DropdownContainer>
);
}
......@@ -3,6 +3,7 @@ import Shape from './shape';
import Size from './size';
import Axis from './axis';
import Opacity from './opacity';
import EntityPill from './entityPillSelector';
export const EncodingSelector = {
Color: Color,
......@@ -10,6 +11,7 @@ export const EncodingSelector = {
Size: Size,
Axis: Axis,
Opacity: Opacity,
EntityPill: EntityPill,
};
// Inspiration: https://uwdata.github.io/visualization-curriculum/altair_marks_encoding.html
......@@ -5,6 +5,8 @@ import {
offset,
flip,
shift,
hide,
arrow,
useHover,
useFocus,
useDismiss,
......@@ -12,6 +14,7 @@ import {
useInteractions,
useMergeRefs,
FloatingPortal,
FloatingArrow
} from '@floating-ui/react';
import type { Placement } from '@floating-ui/react';
import { FloatingDelayGroup } from '@floating-ui/react';
......@@ -21,6 +24,8 @@ interface TooltipOptions {
placement?: Placement;
open?: boolean;
onOpenChange?: (open: boolean) => void;
boundaryElement?: React.RefObject<HTMLElement> | null;
showArrow?: boolean;
}
export function useTooltip({
......@@ -28,6 +33,8 @@ export function useTooltip({
placement = 'top',
open: controlledOpen,
onOpenChange: setControlledOpen,
boundaryElement = null,
showArrow = false
}: TooltipOptions = {}): {
open: boolean;
setOpen: (open: boolean) => void;
......@@ -38,8 +45,9 @@ export function useTooltip({
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;
const arrowRef = React.useRef<SVGSVGElement | null>(null);
const data = useFloating({
let config = {
placement,
open,
onOpenChange: setOpen,
......@@ -49,11 +57,25 @@ export function useTooltip({
flip({
crossAxis: placement.includes('-'),
fallbackAxisSideDirection: 'start',
padding: 5,
padding: 5
}),
shift({ padding: 5 }),
],
});
}
if (boundaryElement != null) {
const boundary = boundaryElement?.current ?? undefined;
config.middleware.find(x => x.name == 'flip')!.options[0].boundary = boundary;
config.middleware.find(x => x.name == 'shift')!.options[0].boundary = boundary;
config.middleware.push(hide({ boundary }));
}
if (showArrow) {
config.middleware.push(arrow({ element: arrowRef }));
}
const data = useFloating(config);
(data.refs as any).arrow = arrowRef;
const context = data.context;
......@@ -98,17 +120,48 @@ export function Tooltip({ children, ...options }: { children: React.ReactNode }
// This can accept any props as options, e.g. `placement`,
// or other positioning options.
const tooltip = useTooltip(options);
return <TooltipContext.Provider value={tooltip}>{children}</TooltipContext.Provider>;
return <TooltipContext.Provider value={tooltip}>
{children}
</TooltipContext.Provider>;
}
export const TooltipTrigger = React.forwardRef<HTMLElement, React.HTMLProps<HTMLElement> & { asChild?: boolean }>(function TooltipTrigger(
{ children, asChild = false, ...props },
export const TooltipTrigger = React.forwardRef<HTMLElement, React.HTMLProps<HTMLElement> & { asChild?: boolean, x?: number, y?: number }>(function TooltipTrigger(
{ children, asChild = false, x = null, y = null, ...props },
propRef,
) {
const context = useTooltipContext();
const childrenRef = (children as any).ref;
const childrenRef = React.useMemo(() => {
if (children == null) {
return null;
} else {
return (children as any).ref;
}
}, [children]);
const ref = useMergeRefs([context.data.refs.setReference, propRef, childrenRef]);
React.useEffect(() => {
if (x && y && context.data.refs.reference.current != null) {
const element = context.data.refs.reference.current as HTMLElement;
element.style.position = 'absolute';
const {x: offsetX, y: offsetY} = element.getBoundingClientRect();
element.getBoundingClientRect = () => {
return {
width: 0,
height: 0,
x: offsetX,
y: offsetY,
top: y + offsetY,
left: x + offsetX,
right: x + offsetX,
bottom: y + offsetY,
} as DOMRect
}
context.data.update();
}
}, [x, y]);
// `asChild` allows the user to pass any element as the anchor
if (asChild && React.isValidElement(children)) {
return React.cloneElement(
......@@ -149,13 +202,21 @@ export const TooltipContent = React.forwardRef<
<FloatingPortal>
<div
ref={ref}
className={`z-50 max-w-64 overflow-hidden rounded bg-light px-2 py-1 shadow text-xs border border-secondary-200 text-dark animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2${className ? ` ${className}` : ''}`}
className={`z-50 max-w-64 rounded bg-light px-2 py-1 shadow text-xs border border-secondary-200 text-dark animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2${className ? ` ${className}` : ''}`}
style={{
...context.data.floatingStyles,
...style,
display: context.data.middlewareData.hide?.referenceHidden ? 'none' : 'block',
}}
{...context.interactions.getFloatingProps(props)}
/>
>
{ props.children }
{ context.data.middlewareData.arrow ? <FloatingArrow
ref={(context.data.refs as any).arrow}
context={context.data.context}
style={{fill: 'white'}}
/> : null }
</div>
</FloatingPortal>
);
});
......
......@@ -49,7 +49,7 @@ export type Edge = {
// Define a type for the slice state
export type GraphQueryResult = {
metaData: GraphMetadata;
metaData?: GraphMetadata;
nodes: Node[];
edges: Edge[];
queryingBackend: boolean;
......@@ -57,7 +57,7 @@ export type GraphQueryResult = {
// Define the initial state using that type
export const initialState: GraphQueryResult = {
metaData: { nodes: { labels: [], types: {} }, edges: { labels: [], types: {} } },
metaData: undefined,
nodes: [],
edges: [],
queryingBackend: false,
......@@ -139,14 +139,14 @@ export const graphQueryResultSlice = createSlice({
const { metaData, nodes, edges } = graphQueryBackend2graphQuery(payload);
// Assign new state
state.metaData = extractStatistics(metaData);
state.metaData = metaData;
state.nodes = nodes;
state.edges = edges;
state.queryingBackend = false;
},
resetGraphQueryResults: (state) => {
// Assign new state
state.metaData = { nodes: { labels: [], types: {} }, edges: { labels: [], types: {} } };
state.metaData = undefined;
state.nodes = [];
state.edges = [];
state.queryingBackend = false;
......
......@@ -39,7 +39,7 @@ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
/** Gives the graphQueryResult from the store */
export const useGraphQueryResult: () => GraphQueryResult = () => useAppSelector(selectGraphQueryResult);
export const useGraphQueryResultMeta: () => GraphMetadata = () => useAppSelector(selectGraphQueryResultMetaData);
export const useGraphQueryResultMeta: () => GraphMetadata | undefined = () => useAppSelector(selectGraphQueryResultMetaData);
// Gives the schema
export const useSchemaGraph: () => SchemaGraph = () => useAppSelector(schemaGraph);
......
import { SchemaAttribute } from '../../..';
import { Handles, QueryElementTypes } from '../reactflow';
import { QueryGraphEdgeAttribute, QueryGraphEdgeHandle, QueryGraphNodes } from './model';
const metaAttribute: Record<string, QueryGraphEdgeAttribute> = {
'(# Connection)': {
attributeName: '(# Connection)',
attributeType: 'float',
attributeDimension: 'numerical',
},
};
export function checkForMetaAttributes(graphologyAttributes: QueryGraphNodes): QueryGraphEdgeHandle[] {
const ret: QueryGraphEdgeHandle[] = [];
const defaultHandleData = {
nodeId: graphologyAttributes.id,
nodeName: graphologyAttributes.name || '',
nodeType: graphologyAttributes.type,
handleType: graphologyAttributes.type === QueryElementTypes.Entity ? Handles.EntityAttribute : Handles.RelationAttribute,
};
// Only include if not already there
const metaAttributesToInclude = Object.keys(metaAttribute).filter((attributeName) => !(attributeName in graphologyAttributes.attributes));
return metaAttributesToInclude.map((attributeName) => ({
...defaultHandleData,
...metaAttribute[attributeName],
})) as QueryGraphEdgeHandle[];
}
......@@ -23,7 +23,7 @@ export type NodeDefaults = {
type: QueryElementTypes;
width?: number;
height?: number;
attributes?: NodeAttribute[];
attributes: NodeAttribute[];
selected?: boolean;
};
......@@ -33,6 +33,7 @@ export interface EntityData {
leftRelationHandleId?: QueryGraphEdgeHandle;
rightRelationHandleId?: QueryGraphEdgeHandle;
selected?: boolean;
type: QueryElementTypes.Entity;
}
/** Interface for the data in an relation node. */
......@@ -44,6 +45,7 @@ export interface RelationData {
rightEntityHandleId?: QueryGraphEdgeHandle;
direction?: 'left' | 'right' | 'both';
selected?: boolean;
type: QueryElementTypes.Relation;
}
export interface LogicData {
......@@ -53,18 +55,19 @@ export interface LogicData {
// key: string;
logic: GeneralDescription<AllLogicTypes>;
inputs: Record<string, InputNodeTypeTypes>; // name from InputNode -> InputNodeTypeTypes
type: QueryElementTypes.Logic;
}
export type EntityNodeAttributes = XYPosition & EntityData & NodeDefaults;
export type RelationNodeAttributes = XYPosition & RelationData & NodeDefaults;
export type LogicNodeAttributes = XYPosition & LogicData & NodeDefaults;
export type EntityNodeAttributes = XYPosition & NodeDefaults & EntityData;
export type RelationNodeAttributes = XYPosition & NodeDefaults & RelationData;
export type LogicNodeAttributes = XYPosition & NodeDefaults & LogicData;
export type QueryGraphNodes = EntityNodeAttributes | RelationNodeAttributes | LogicNodeAttributes;
export type QueryGraphEdgeAttribute = {
attributeName?: string;
attributeType?: InputNodeType;
attributeDimension?: InputNodeDimension;
attributeName: string;
attributeType: InputNodeType;
attributeDimension: InputNodeDimension;
};
export type QueryGraphEdgeHandle = {
......@@ -72,7 +75,7 @@ export type QueryGraphEdgeHandle = {
nodeName: string;
nodeType: QueryElementTypes;
handleType: Handles;
} & QueryGraphEdgeAttribute;
} & Partial<QueryGraphEdgeAttribute>;
export type QueryGraphEdges = {
type: string;
......@@ -80,10 +83,6 @@ export type QueryGraphEdges = {
targetHandleData: QueryGraphEdgeHandle;
};
export type QueryGraphEdgesOpt = {
type?: string;
sourceHandleData?: QueryGraphEdgeHandle;
targetHandleData?: QueryGraphEdgeHandle;
};
export type QueryGraphEdgesOpt = Partial<QueryGraphEdges>;
// export class QueryGraph extends Graph<QueryGraphNodes, GAttributes, GAttributes>; // is in utils.ts
......@@ -4,7 +4,6 @@ import { Attributes as GAttributes, Attributes, SerializedGraph } from 'grapholo
import {
EntityNodeAttributes,
LogicNodeAttributes,
QueryGraphEdgeAttribute,
QueryGraphEdgeHandle,
QueryGraphEdges,
QueryGraphEdgesOpt,
......@@ -15,6 +14,7 @@ import { XYPosition } from 'reactflow';
import { Handles, QueryElementTypes } from '../reactflow';
import { SchemaAttribute, SchemaAttributeTypes } from '@graphpolaris/shared/lib/schema';
import { InputNodeType, InputNodeTypeTypes } from '../logic/general';
import { checkForMetaAttributes } from './metaAttributes';
/** monospace fontsize table */
const widthPerFontsize = {
......@@ -54,6 +54,9 @@ export class QueryMultiGraphology extends Graph<QueryGraphNodes, QueryGraphEdges
if (!attributes.id) attributes.id = 'id_' + (Date.now() + Math.floor(Math.random() * 1000)).toString();
// Add to the beginning the meta attributes, such as (# Connection)
attributes.attributes = [...checkForMetaAttributes(attributes).map((a) => ({ handleData: a })), ...attributes.attributes];
return attributes;
}
......@@ -113,19 +116,17 @@ export class QueryMultiGraphology extends Graph<QueryGraphNodes, QueryGraphEdges
return attributes;
}
public addLogicPill2Graphology(attributes: QueryGraphNodes, inputValues: Record<string, InputNodeTypeTypes> = {}): QueryGraphNodes {
attributes = this.configureDefaults(attributes);
if (!attributes.type) attributes.type = QueryElementTypes.Logic;
public addLogicPill2Graphology(attributes: LogicNodeAttributes, inputValues: Record<string, InputNodeTypeTypes> = {}): QueryGraphNodes {
attributes = this.configureDefaults(attributes) as LogicNodeAttributes;
if (!attributes.name || !attributes.id) throw Error('type or name is not defined');
// add default inputs, but only if not there yet
if (attributes.type === QueryElementTypes.Logic) {
if ((attributes as LogicNodeAttributes).inputs === undefined) {
attributes = attributes as LogicNodeAttributes;
(attributes as LogicNodeAttributes).logic.inputs.forEach((input, i) => {
if (attributes.inputs === undefined || Object.keys(attributes.inputs).length === 0) {
attributes.logic.inputs.forEach((input, i) => {
// Setup default non-linked inputs as regular values matching the input expected type
if (!(attributes as LogicNodeAttributes).inputs) (attributes as LogicNodeAttributes).inputs = {};
(attributes as LogicNodeAttributes).inputs[input.name] = inputValues?.[input.name] || input.default;
if (!attributes.inputs) attributes.inputs = {};
attributes.inputs[input.name] = inputValues?.[input.name] || input.default;
});
// (attributes as LogicNodeAttributes).leftEntityHandleId = getHandleId(attributes.id, name, type, Handles.RelationLeft, '');
// (attributes as LogicNodeAttributes).rightEntityHandleId = getHandleId(attributes.id, name, type, Handles.RelationRight, '');
......