diff --git a/.drone.yml b/.drone.yml index b0d7165..e3b1f91 100644 --- a/.drone.yml +++ b/.drone.yml @@ -6,10 +6,11 @@ steps: - name: test image: node:16 commands: + - curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7 - node ./test-server/node-http.js & - - npm ci - - npm run test:nowatch - - npm run build + - pnpm install + - pnpm run test:nowatch + - pnpm run build --- kind: pipeline @@ -19,7 +20,8 @@ steps: - name: test image: node commands: + - curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7 - node ./test-server/node-http.js & - - npm ci - - npm run test:nowatch - - npm run build \ No newline at end of file + - pnpm install + - pnpm run test:nowatch + - pnpm run build \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9c5ea7c..13ac012 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -25,8 +25,7 @@ module.exports = { rules: { 'space-before-function-paren': ['error', 'never'], '@typescript-eslint/space-before-function-paren': ['error', 'never'], - indent: 'off', - '@typescript-eslint/indent': ['warn', 2, {SwitchCase: 1}], + indent: ['warn', 2, { SwitchCase: 1 }], semi: 'off', '@typescript-eslint/semi': ['warn', 'always'], 'no-unused-vars': 'off', diff --git a/docs/ComponentStructure.drawio b/docs/ComponentStructure.drawio index e4ee4af..2fdcefc 100644 --- a/docs/ComponentStructure.drawio +++ b/docs/ComponentStructure.drawio @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8156818d0348ba0edefc52d6ed8fc84b65305c7eca275a37d3ff1733391bb1d -size 19858 +oid sha256:59a34bc3428280ecd661efa7b4756bf9f4930f36c077984a40e7fdc6983aeeff +size 2063 diff --git a/docs/Dependencies.md b/docs/Dependencies.md index 34212ee..626458e 100644 --- a/docs/Dependencies.md +++ b/docs/Dependencies.md @@ -13,7 +13,6 @@ It depends on Vite in order to build the project. Others dependencies: - [react-dom](https://reactjs.org/docs/react-dom.html): library used to inject the app to `#root` html element. -- [react-window](https://www.npmjs.com/package/react-windows): component that offers component dynamic loading over scroll (very useful++) - [react-svg-pan-zoom](https://www.npmjs.com/package/react-svg-pan-zoom): component that offers pan + zoom to a svg element diff --git a/docs/Project_Structure.md b/docs/Project_Structure.md index 3daa579..890daf8 100644 --- a/docs/Project_Structure.md +++ b/docs/Project_Structure.md @@ -4,24 +4,22 @@ The project is structured this way ``` . -├── docs/ Documentation folder -├── public/ Public folder in which the index.html +├── docs Documentation folder +├── public Public folder in which the index.html │ import its resources -├── src/ Source folder for the react app -│ ├── assets/ Assets folder in which the react app +├── src Source folder for the react app +│ ├── assets Assets folder in which the react app │ │ import its resources -│ ├── Components/ Components folder -│ ├── Enums/ Enums folder -│ ├── Events/ API Events folder -│ ├── Interfaces/ Interface (+ types folder) -│ ├── test/ Setup folder for the tests -│ ├── tests/ Other tests + resources -│ ├── utils/ Utilities folder -│ ├── workers/ Webworkers folder +│ ├── Components Components folder +│ ├── Enums Enums folder +│ ├── Interfaces Interface (+ types folder) +│ ├── test Setup folder for the tests +│ ├── tests Other tests + resources +│ ├── utils Utilities folder │ ├── index.scss Tailwind CSS extends │ ├── main.tsx Entrypoint for App injection │ └── vite-env.d.ts Types for .env files -├── test-server/ Tests servers to test the API +├── test-server Tests servers to test the API │ ├── http.js Test server for bun.sh │ └── node-http.js Test server for Node.js ├── azure-pipelines.yml Azure Pipelines YAML config file diff --git a/src/Components/App/MenuActions.ts b/src/Components/App/MenuActions.ts index 1e37328..c144acc 100644 --- a/src/Components/App/MenuActions.ts +++ b/src/Components/App/MenuActions.ts @@ -4,7 +4,6 @@ import { ContainerModel } from '../../Interfaces/IContainerModel'; import { fetchConfiguration } from '../API/api'; import { IEditorState } from '../../Interfaces/IEditorState'; import { LoadState } from './Load'; -import { XPositionReference } from '../../Enums/XPositionReference'; export function NewEditor( setEditorState: Dispatch>, @@ -21,15 +20,12 @@ export function NewEditor( parentId: 'null', x: 0, y: 0, - width: Number(configuration.MainContainer.Width), - height: Number(configuration.MainContainer.Height), + width: configuration.MainContainer.Width, + height: configuration.MainContainer.Height, isRigidBody: false, isAnchor: false, - XPositionReference: XPositionReference.Left, - style: { - fillOpacity: 0, - stroke: 'black' - } + fillOpacity: 0, + stroke: 'black' } ); diff --git a/src/Components/Editor/Behaviors/AnchorBehaviors.ts b/src/Components/Editor/Behaviors/AnchorBehaviors.ts index 22ce2f4..d9731e8 100644 --- a/src/Components/Editor/Behaviors/AnchorBehaviors.ts +++ b/src/Components/Editor/Behaviors/AnchorBehaviors.ts @@ -49,7 +49,7 @@ function getOverlappingContainers( containers: IContainerModel[] ): IContainerModel[] { const min1 = container.properties.x; - const max1 = container.properties.x + container.properties.width; + const max1 = container.properties.x + Number(container.properties.width); const overlappingContainers: IContainerModel[] = []; for (const other of containers) { if (other === container) { @@ -57,7 +57,7 @@ function getOverlappingContainers( } const min2 = other.properties.x; - const max2 = other.properties.x + other.properties.width; + const max2 = other.properties.x + Number(other.properties.width); const isOverlapping = Math.min(max1, max2) - Math.max(min1, min2) > 0; if (!isOverlapping) { diff --git a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts index e0ffb2f..eabec85 100644 --- a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts +++ b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts @@ -42,8 +42,8 @@ function constraintBodyInsideParent( } const parentProperties = container.parent.properties; - const parentWidth = parentProperties.width; - const parentHeight = parentProperties.height; + const parentWidth = Number(parentProperties.width); + const parentHeight = Number(parentProperties.height); return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight); } @@ -66,10 +66,10 @@ function constraintBodyInsideSpace( height: number ): IContainerModel { const containerProperties = container.properties; - const containerX = containerProperties.x; - const containerY = containerProperties.y; - const containerWidth = containerProperties.width; - const containerHeight = containerProperties.height; + const containerX = Number(containerProperties.x); + const containerY = Number(containerProperties.y); + const containerWidth = Number(containerProperties.width); + const containerHeight = Number(containerProperties.height); // Check size bigger than parent const isBodyLargerThanParent = containerWidth > width; @@ -121,8 +121,8 @@ export function constraintBodyInsideUnallocatedWidth( // Get the available spaces of the parent const availableWidths = getAvailableWidths(container.parent, container); - const containerX = container.properties.x; - const containerWidth = container.properties.width; + const containerX = Number(container.properties.x); + const containerWidth = Number(container.properties.width); // Check if there is still some space if (availableWidths.length === 0) { @@ -177,7 +177,7 @@ export function constraintBodyInsideUnallocatedWidth( availableWidthFound.x, 0, availableWidthFound.width, - container.parent.properties.height + Number(container.parent.properties.height) ); } @@ -197,7 +197,7 @@ function getAvailableWidths( // Initialize the first size pointer // which takes full width of the available space const x = 0; - const width = container.properties.width; + const width = Number(container.properties.width); let unallocatedSpaces: ISizePointer[] = [{ x, width }]; // We will only uses containers that also are rigid or are anchors @@ -211,7 +211,7 @@ function getAvailableWidths( continue; } const childX = child.properties.x; - const childWidth = child.properties.width; + const childWidth = Number(child.properties.width); // get the space of the child that is inside the parent let newUnallocatedSpace: ISizePointer[] = []; @@ -309,4 +309,4 @@ function getAvailableWidthsTwoLines( const isFitting = ( container: IContainerModel, sizePointer: ISizePointer -): boolean => container.properties.width <= sizePointer.width; +): boolean => Number(container.properties.width) <= sizePointer.width; diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index 3d9e91e..f1736fc 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -5,9 +5,6 @@ import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerMode import { findContainerById } from '../../utils/itertools'; import { getCurrentHistory } from './Editor'; import IProperties from '../../Interfaces/IProperties'; -import { AddMethod } from '../../Enums/AddMethod'; -import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; -import { XPositionReference } from '../../Enums/XPositionReference'; /** * Select a container @@ -57,7 +54,7 @@ export function DeleteContainer( setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); - const current = history[history.length - 1]; + const current = history[historyCurrentStep]; const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); const container = findContainerById(mainContainerClone, containerId); @@ -172,10 +169,10 @@ export function AddContainer( } // Get the preset properties from the API - const containerConfig = configuration.AvailableContainers + const properties = configuration.AvailableContainers .find(option => option.Type === type); - if (containerConfig === undefined) { + if (properties === undefined) { throw new Error(`[AddContainer] Object type not found. Found: ${type}`); } @@ -201,24 +198,25 @@ export function AddContainer( throw new Error('[AddContainer] Container model was not found among children of the main container!'); } - let x = containerConfig.DefaultX ?? 0; - const y = containerConfig.DefaultY ?? 0; - const width = containerConfig.Width ?? parentClone.properties.width; - const height = containerConfig.Height ?? parentClone.properties.height; - - x = ApplyAddMethod(index, containerConfig, parentClone, x); + let x = 0; + if (index > 0) { + const lastChild: IContainerModel | undefined = parentClone.children.at(index - 1); + if (lastChild !== undefined) { + x = lastChild.properties.x + Number(lastChild.properties.width); + } + } const defaultProperties: IProperties = { id: `${type}-${count}`, parentId: parentClone.properties.id, x, - y, - width, - height, + y: 0, + width: properties.Width, + height: parentClone.properties.height, isRigidBody: false, isAnchor: false, - XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left, - style: containerConfig.Style + XPositionReference: properties.XPositionReference, + ...properties.Style }; // Create the container @@ -249,25 +247,3 @@ export function AddContainer( setHistory(history); setHistoryCurrentStep(history.length - 1); } - -/** - * Returns a new offset by applying an Add method (append, insert etc.) - * See AddMethod - * @param index Index of the container - * @param containerConfig Configuration of a container - * @param parent Parent container - * @param x Additionnal offset - * @returns New offset - */ -function ApplyAddMethod(index: number, containerConfig: IAvailableContainer, parent: IContainerModel, x: number): number { - if (index > 0 && ( - containerConfig.AddMethod === undefined || - containerConfig.AddMethod === AddMethod.Append)) { - const lastChild: IContainerModel | undefined = parent.children.at(index - 1); - - if (lastChild !== undefined) { - x += (lastChild.properties.x + lastChild.properties.width); - } - } - return x; -} diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index 75bf90c..3a728e3 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -91,15 +91,16 @@ const Editor: React.FunctionComponent = (props) => { setHistory, setHistoryCurrentStep )} - OnPropertyChange={(key, value, isStyle) => OnPropertyChange( - key, value, isStyle, + OnPropertyChange={(key, value) => OnPropertyChange( + key, value, history, historyCurrentStep, setHistory, setHistoryCurrentStep )} - OnPropertiesSubmit={(event) => OnPropertiesSubmit( + OnPropertiesSubmit={(event, properties) => OnPropertiesSubmit( event, + properties, history, historyCurrentStep, setHistory, @@ -132,8 +133,8 @@ const Editor: React.FunctionComponent = (props) => { LoadState={(move) => setHistoryCurrentStep(move)} /> { current.MainContainer } diff --git a/src/Components/Editor/PropertiesOperations.ts b/src/Components/Editor/PropertiesOperations.ts index 0e3802c..4bd4c2d 100644 --- a/src/Components/Editor/PropertiesOperations.ts +++ b/src/Components/Editor/PropertiesOperations.ts @@ -1,12 +1,12 @@ import { Dispatch, SetStateAction } from 'react'; import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel'; import { IHistoryState } from '../../Interfaces/IHistoryState'; +import IProperties from '../../Interfaces/IProperties'; import { findContainerById } from '../../utils/itertools'; import { getCurrentHistory } from './Editor'; -import { constraintBodyInsideUnallocatedWidth, RecalculatePhysics } from './Behaviors/RigidBodyBehaviors'; +import { RecalculatePhysics } from './Behaviors/RigidBodyBehaviors'; import { INPUT_TYPES } from '../Properties/PropertiesInputTypes'; import { ImposePosition } from './Behaviors/AnchorBehaviors'; -import { restoreX } from '../SVG/Elements/Container'; /** * Handled the property change event in the properties form @@ -17,7 +17,6 @@ import { restoreX } from '../SVG/Elements/Container'; export function OnPropertyChange( key: string, value: string | number | boolean, - isStyle: boolean = false, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, @@ -38,8 +37,8 @@ export function OnPropertyChange( throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); } - if (isStyle) { - (container.properties.style as any)[key] = value; + if (INPUT_TYPES[key] === 'number') { + (container.properties as any)[key] = Number(value); } else { (container.properties as any)[key] = value; } @@ -71,6 +70,7 @@ export function OnPropertyChange( */ export function OnPropertiesSubmit( event: React.SyntheticEvent, + properties: IProperties, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, @@ -92,29 +92,16 @@ export function OnPropertiesSubmit( throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); } - // Assign container properties - const form: HTMLFormElement = event.target as HTMLFormElement; - for (const property in container.properties) { - const input: HTMLInputElement | HTMLDivElement | null = form.querySelector(`#${property}`); - - if (input === null) { - continue; - } - + for (const property in properties) { + const input = (event.target as HTMLFormElement).querySelector(`#${property}`); if (input instanceof HTMLInputElement) { - submitHTMLInput(input, container, property, form); - continue; + (container.properties as any)[property] = input.value; + if (INPUT_TYPES[property] === 'number') { + (container.properties as any)[property] = Number(input.value); + } else { + (container.properties as any)[property] = input.value; + } } - - if (input instanceof HTMLDivElement) { - submitRadioButtons(input, container, property); - continue; - } - } - - // Assign cssproperties - for (const styleProperty in container.properties.style) { - submitCSSForm(form, styleProperty, container); } if (container.properties.isRigidBody) { @@ -131,67 +118,3 @@ export function OnPropertiesSubmit( setHistory(history); setHistoryCurrentStep(history.length - 1); } - -const submitHTMLInput = ( - input: HTMLInputElement, - container: IContainerModel, - property: string, - form: HTMLFormElement -): void => { - if (INPUT_TYPES[property] !== 'number') { - (container.properties as any)[property] = input.value; - } - - if (property === 'x') { - // Hardcoded fix for XPositionReference - const x = RestoreX(form, input); - (container.properties as any)[property] = x; - return; - } - - (container.properties as any)[property] = Number(input.value); -}; - -const submitCSSForm = (form: HTMLFormElement, styleProperty: string, container: ContainerModel): void => { - const input: HTMLInputElement | null = form.querySelector(`#${styleProperty}`); - if (input === null) { - return; - } - (container.properties.style as any)[styleProperty] = input.value; -}; - -const RestoreX = ( - form: HTMLFormElement, - input: HTMLInputElement -): number => { - const inputWidth: HTMLInputElement | null = form.querySelector('#width'); - const inputRadio: HTMLDivElement | null = form.querySelector('#XPositionReference'); - if (inputWidth === null || inputRadio === null) { - throw new Error('[OnPropertiesSubmit] Missing inputs for width or XPositionReference'); - } - - const radiobutton: HTMLInputElement | null = inputRadio.querySelector('input[name="XPositionReference"]:checked'); - if (radiobutton === null) { - throw new Error('[OnPropertiesSubmit] Missing inputs for XPositionReference'); - } - - return restoreX(Number(input.value), Number(inputWidth.value), Number(radiobutton.value)); -}; - -const submitRadioButtons = ( - div: HTMLDivElement, - container: IContainerModel, - property: string -): void => { - const radiobutton: HTMLInputElement | null = div.querySelector(`input[name="${property}"]:checked`); - if (radiobutton === null) { - return; - } - - if (INPUT_TYPES[property] === 'number') { - (container.properties as any)[property] = Number(radiobutton.value); - return; - } - - (container.properties as any)[property] = radiobutton.value; -}; diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index f207a46..8d34480 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -3,7 +3,6 @@ import * as React from 'react'; import { fireEvent, render, screen } from '../../utils/test-utils'; import { ElementsSidebar } from './ElementsSidebar'; import { IContainerModel } from '../../Interfaces/IContainerModel'; -import { XPositionReference } from '../../Enums/XPositionReference'; describe.concurrent('Elements sidebar', () => { it('With a MainContainer', () => { @@ -18,7 +17,6 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - XPositionReference: XPositionReference.Left, isRigidBody: false, isAnchor: false }, @@ -51,8 +49,7 @@ describe.concurrent('Elements sidebar', () => { width: 2000, height: 100, isRigidBody: false, - isAnchor: false, - XPositionReference: XPositionReference.Left + isAnchor: false }, userData: {} }; @@ -108,7 +105,6 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - XPositionReference: XPositionReference.Left, isRigidBody: false, isAnchor: false }, @@ -127,8 +123,7 @@ describe.concurrent('Elements sidebar', () => { width: 0, height: 0, isRigidBody: false, - isAnchor: false, - XPositionReference: XPositionReference.Left + isAnchor: false }, userData: {} } @@ -145,7 +140,6 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - XPositionReference: XPositionReference.Left, isRigidBody: false, isAnchor: false }, @@ -184,7 +178,6 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - XPositionReference: XPositionReference.Left, isRigidBody: false, isAnchor: false }, @@ -201,7 +194,6 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - XPositionReference: XPositionReference.Left, isRigidBody: false, isAnchor: false }, diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 98eb071..30f1617 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { FixedSizeList as List } from 'react-window'; import { Properties } from '../Properties/Properties'; +import ContainerProperties from '../../Interfaces/IProperties'; import { IContainerModel } from '../../Interfaces/IContainerModel'; import { getDepth, MakeIterator } from '../../utils/itertools'; import { Menu } from '../Menu/Menu'; @@ -13,8 +14,8 @@ interface IElementsSidebarProps { isOpen: boolean isHistoryOpen: boolean SelectedContainer: IContainerModel | null - OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void - OnPropertiesSubmit: (event: React.FormEvent) => void + OnPropertyChange: (key: string, value: string | number | boolean) => void + OnPropertiesSubmit: (event: React.FormEvent, properties: ContainerProperties) => void SelectContainer: (container: IContainerModel) => void DeleteContainer: (containerid: string) => void AddContainer: (index: number, type: string, parent: string) => void @@ -109,6 +110,8 @@ export const ElementsSidebar: React.FC = (props: IElement ); }; + const ROW_HEIGHT = 35; + const NUMBERS_OF_ROWS = 10; return (
diff --git a/src/Components/ElementsSidebar/MouseEventHandlers.ts b/src/Components/ElementsSidebar/MouseEventHandlers.ts index 5081033..1a814d2 100644 --- a/src/Components/ElementsSidebar/MouseEventHandlers.ts +++ b/src/Components/ElementsSidebar/MouseEventHandlers.ts @@ -36,8 +36,9 @@ export function handleLeftClick( } export function removeBorderClasses(target: HTMLButtonElement): void { - const bordersClasses = ['border-t-8', 'border-8', 'border-b-8']; - target.classList.remove(...bordersClasses); + target.classList.remove('border-t-8'); + target.classList.remove('border-8'); + target.classList.remove('border-b-8'); } export function handleDragLeave(event: React.DragEvent): void { @@ -53,19 +54,26 @@ export function handleDragOver( const target: HTMLButtonElement = event.target as HTMLButtonElement; const rect = target.getBoundingClientRect(); const y = event.clientY - rect.top; // y position within the element. - removeBorderClasses(target); if (target.id === mainContainer.properties.id) { target.classList.add('border-8'); + target.classList.remove('border-t-8'); + target.classList.remove('border-b-8'); return; } if (y < 12) { target.classList.add('border-t-8'); + target.classList.remove('border-b-8'); + target.classList.remove('border-8'); } else if (y < 24) { target.classList.add('border-8'); + target.classList.remove('border-t-8'); + target.classList.remove('border-b-8'); } else { target.classList.add('border-b-8'); + target.classList.remove('border-8'); + target.classList.remove('border-t-8'); } } diff --git a/src/Components/InputGroup/InputGroup.tsx b/src/Components/InputGroup/InputGroup.tsx deleted file mode 100644 index bc794d3..0000000 --- a/src/Components/InputGroup/InputGroup.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react'; - -interface IInputGroupProps { - labelKey?: string - labelText: string - inputKey: string - labelClassName: string - inputClassName: string - type: string - value?: string - checked?: boolean - defaultValue?: string - defaultChecked?: boolean - isDisabled?: boolean - onChange?: (event: React.ChangeEvent) => void -} - -const className = ` - w-full - text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2 - bg-white border-2 border-white rounded-lg placeholder-gray-800 - focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 - disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`; - -export const InputGroup: React.FunctionComponent = (props) => { - return <> - - - ; -}; diff --git a/src/Components/Properties/DynamicForm.tsx b/src/Components/Properties/DynamicForm.tsx deleted file mode 100644 index b5665b6..0000000 --- a/src/Components/Properties/DynamicForm.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { MenuAlt2Icon, MenuAlt3Icon, MenuIcon } from '@heroicons/react/outline'; -import * as React from 'react'; -import { XPositionReference } from '../../Enums/XPositionReference'; -import IProperties from '../../Interfaces/IProperties'; -import { InputGroup } from '../InputGroup/InputGroup'; -import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons'; -import { restoreX, transformX } from '../SVG/Elements/Container'; - -interface IDynamicFormProps { - properties: IProperties - onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void -} - -const getCSSInputs = ( - properties: IProperties, - onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void -): JSX.Element[] => { - const groupInput: JSX.Element[] = []; - for (const key in properties.style) { - groupInput.push( onChange(key, event.target.value, true)} - />); - } - return groupInput; -}; - -const DynamicForm: React.FunctionComponent = (props) => { - return ( -
- - - props.onChange('x', restoreX(Number(event.target.value), props.properties.width, props.properties.XPositionReference))} - /> - props.onChange('y', Number(event.target.value))} - /> - props.onChange('width', Number(event.target.value))} - /> - props.onChange('height', Number(event.target.value))} - /> - props.onChange('isRigidBody', event.target.checked)} - /> - props.onChange('isAnchor', event.target.checked)} - /> - - -
- ), - value: XPositionReference.Left.toString() - }, - { - text: ( -
- -
- ), - value: XPositionReference.Center.toString() - }, - { - text: ( -
- -
- ), - value: XPositionReference.Right.toString() - } - ]} - onChange={(event) => props.onChange('XPositionReference', Number(event.target.value))} - /> - { getCSSInputs(props.properties, props.onChange) } -
- ); -}; - -export default DynamicForm; diff --git a/src/Components/Properties/Form.tsx b/src/Components/Properties/Form.tsx deleted file mode 100644 index bc0f508..0000000 --- a/src/Components/Properties/Form.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; -import IProperties from '../../Interfaces/IProperties'; -import DynamicForm from './DynamicForm'; -import StaticForm from './StaticForm'; - -interface IFormProps { - properties: IProperties - isDynamicInput: boolean - onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void - onSubmit: (event: React.FormEvent) => void -} - -export const Form: React.FunctionComponent = (props) => { - if (props.isDynamicInput) { - return ; - } - return ; -}; diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index dc9473e..9fbef37 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -1,8 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react'; import * as React from 'react'; import { expect, describe, it, vi } from 'vitest'; -import { XPositionReference } from '../../Enums/XPositionReference'; -import IProperties from '../../Interfaces/IProperties'; import { Properties } from './Properties'; describe.concurrent('Properties', () => { @@ -20,14 +18,11 @@ describe.concurrent('Properties', () => { }); it('Some properties, change values with dynamic input', () => { - const prop: IProperties = { + const prop = { id: 'stuff', parentId: 'parentId', x: 1, y: 1, - width: 1, - height: 1, - XPositionReference: XPositionReference.Left, isRigidBody: false, isAnchor: false }; @@ -64,12 +59,12 @@ describe.concurrent('Properties', () => { fireEvent.change(propertyParentId as Element, { target: { value: 'parentedId' } }); fireEvent.change(propertyX as Element, { target: { value: '2' } }); fireEvent.change(propertyY as Element, { target: { value: '2' } }); - expect(handleChange).toBeCalledTimes(2); + expect(handleChange).toBeCalledTimes(4); - expect(prop.id).toBe('stuff'); - expect(prop.parentId).toBe('parentId'); - expect(prop.x).toBe(2); - expect(prop.y).toBe(2); + expect(prop.id).toBe('stuffed'); + expect(prop.parentId).toBe('parentedId'); + expect(prop.x).toBe('2'); + expect(prop.y).toBe('2'); rerender( { propertyX = container.querySelector('#x'); propertyY = container.querySelector('#y'); expect(propertyId).toBeDefined(); - expect((propertyId as HTMLInputElement).value).toBe('stuff'); + expect((propertyId as HTMLInputElement).value).toBe('stuffed'); expect(propertyParentId).toBeDefined(); - expect((propertyParentId as HTMLInputElement).value).toBe('parentId'); + expect((propertyParentId as HTMLInputElement).value).toBe('parentedId'); expect(propertyX).toBeDefined(); expect((propertyX as HTMLInputElement).value).toBe('2'); expect(propertyY).toBeDefined(); diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index cb38a60..516f23d 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; -import IProperties from '../../Interfaces/IProperties'; +import ContainerProperties from '../../Interfaces/IProperties'; import { ToggleButton } from '../ToggleButton/ToggleButton'; -import { Form } from './Form'; +import { INPUT_TYPES } from './PropertiesInputTypes'; interface IPropertiesProps { - properties?: IProperties - onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void - onSubmit: (event: React.FormEvent) => void + properties?: ContainerProperties + onChange: (key: string, value: string | number | boolean) => void + onSubmit: (event: React.FormEvent, properties: ContainerProperties) => void } export const Properties: React.FC = (props: IPropertiesProps) => { @@ -16,6 +16,26 @@ export const Properties: React.FC = (props: IPropertiesProps) return
; } + const groupInput: React.ReactNode[] = []; + Object + .entries(props.properties) + .forEach((pair) => handleProperties(pair, groupInput, isDynamicInput, props.onChange)); + + const form = isDynamicInput + ?
+ { groupInput } +
+ :
props.onSubmit(event, props.properties as ContainerProperties)} + > + +
+ { groupInput } +
+
+ ; + return (
= (props: IPropertiesProps) checked={isDynamicInput} onChange={() => setIsDynamicInput(!isDynamicInput)} /> -
+ { form }
); }; + +const handleProperties = ( + [key, value]: [string, string | number], + groupInput: React.ReactNode[], + isDynamicInput: boolean, + onChange: (key: string, value: string | number | boolean) => void +): void => { + const id = `property-${key}`; + let type = 'text'; + let checked; + + /// hardcoded stuff for ergonomy /// + if (typeof value === 'boolean') { + checked = value; + } + + if (key in INPUT_TYPES) { + type = INPUT_TYPES[key]; + } + + const className = ` + w-full + text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2 + bg-white border-2 border-white rounded-lg placeholder-gray-800 + focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 + disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`; + const isDisabled = ['id', 'parentId'].includes(key); + const input = isDynamicInput + ? { + if (type === 'checkbox') { + onChange(key, event.target.checked); + return; + } + onChange(key, event.target.value); + }} + disabled={isDisabled} + /> + : ; + + groupInput.push( + + ); + groupInput.push(input); +}; diff --git a/src/Components/Properties/PropertiesInputTypes.tsx b/src/Components/Properties/PropertiesInputTypes.tsx index b8f29d3..d91ddbc 100644 --- a/src/Components/Properties/PropertiesInputTypes.tsx +++ b/src/Components/Properties/PropertiesInputTypes.tsx @@ -4,6 +4,5 @@ export const INPUT_TYPES: Record = { width: 'number', height: 'number', isRigidBody: 'checkbox', - isAnchor: 'checkbox', - XPositionReference: 'number' + isAnchor: 'checkbox' }; diff --git a/src/Components/Properties/StaticForm.tsx b/src/Components/Properties/StaticForm.tsx deleted file mode 100644 index 9f6055b..0000000 --- a/src/Components/Properties/StaticForm.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { MenuAlt2Icon, MenuIcon, MenuAlt3Icon } from '@heroicons/react/outline'; -import * as React from 'react'; -import { XPositionReference } from '../../Enums/XPositionReference'; -import IProperties from '../../Interfaces/IProperties'; -import { InputGroup } from '../InputGroup/InputGroup'; -import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons'; -import { transformX } from '../SVG/Elements/Container'; - -interface IStaticFormProps { - properties: IProperties - onSubmit: (event: React.FormEvent) => void -} - -const getCSSInputs = (properties: IProperties): JSX.Element[] => { - const groupInput: JSX.Element[] = []; - for (const key in properties.style) { - groupInput.push(); - } - return groupInput; -}; - -const StaticForm: React.FunctionComponent = (props) => { - return ( props.onSubmit(event)} - > - -
- - - - - - - - - - -
- ), - value: XPositionReference.Left.toString() - }, - { - text: ( -
- -
- ), - value: XPositionReference.Center.toString() - }, - { - text: ( -
- -
- ), - value: XPositionReference.Right.toString() - } - ]} - /> - { getCSSInputs(props.properties) } -
- ); -}; - -export default StaticForm; diff --git a/src/Components/RadioGroupButtons/RadioGroupButtons.tsx b/src/Components/RadioGroupButtons/RadioGroupButtons.tsx deleted file mode 100644 index 8fb620b..0000000 --- a/src/Components/RadioGroupButtons/RadioGroupButtons.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from 'react'; -import { IInputGroup } from '../../Interfaces/IInputGroup'; - -interface IRadioGroupButtonsProps { - name: string - value?: string - defaultValue?: string - inputClassName: string - labelText: string - inputGroups: IInputGroup[] - onChange?: (event: React.ChangeEvent) => void -} - -export const RadioGroupButtons: React.FunctionComponent = (props) => { - let inputGroups; - if (props.value !== undefined) { - // dynamic - inputGroups = props.inputGroups.map((inputGroup) => ( -
- - -
- - )); - } else { - // static - inputGroups = props.inputGroups.map((inputGroup) => ( -
- - -
- )); - } - - return ( - <> - -
- { inputGroups } -
- - ); -}; diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index 5565ec1..6417570 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { XPositionReference } from '../../../Enums/XPositionReference'; import { IContainerModel } from '../../../Interfaces/IContainerModel'; -import { DIMENSION_MARGIN } from '../../../utils/default'; import { getDepth } from '../../../utils/itertools'; import { Dimension } from './Dimension'; @@ -9,16 +8,24 @@ interface IContainerProps { model: IContainerModel } +const GAP = 50; + /** * Render the container * @returns Render the container */ export const Container: React.FC = (props: IContainerProps) => { const containersElements = props.model.children.map(child => ); - const xText = props.model.properties.width / 2; - const yText = props.model.properties.height / 2; + const xText = Number(props.model.properties.width) / 2; + const yText = Number(props.model.properties.height) / 2; - const transform = `translate(${props.model.properties.x}, ${props.model.properties.y})`; + const [transformedX, transformedY] = transformPosition( + Number(props.model.properties.x), + Number(props.model.properties.y), + Number(props.model.properties.width), + props.model.properties.XPositionReference + ); + const transform = `translate(${transformedX}, ${transformedY})`; // g style const defaultStyle: React.CSSProperties = { @@ -30,39 +37,21 @@ export const Container: React.FC = (props: IContainerProps) => // Rect style const style = Object.assign( JSON.parse(JSON.stringify(defaultStyle)), - props.model.properties.style + props.model.properties ); + style.x = 0; + style.y = 0; + delete style.height; + delete style.width; // Dimension props - const depth = getDepth(props.model); - const dimensionMargin = DIMENSION_MARGIN * (depth + 1); const id = `dim-${props.model.properties.id}`; const xStart: number = 0; - const xEnd = props.model.properties.width; - const y = -dimensionMargin; + const xEnd = Number(props.model.properties.width); + const y = -(GAP * (getDepth(props.model) + 1)); const strokeWidth = 1; const text = (props.model.properties.width ?? 0).toString(); - let dimensionChildren: JSX.Element | null = null; - if (props.model.children.length > 1) { - const { - childrenId, - xChildrenStart, - xChildrenEnd, - yChildren, - textChildren - } = GetChildrenDimensionProps(props, dimensionMargin); - dimensionChildren = ; - } - return ( = (props: IContainerProps) => strokeWidth={strokeWidth} text={text} /> - { dimensionChildren } = (props: IContainerProps) => ); }; -function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: number): -{ childrenId: string, xChildrenStart: number, xChildrenEnd: number, yChildren: number, textChildren: string } { - const childrenId = `dim-children-${props.model.properties.id}`; - - const lastChild = props.model.children[props.model.children.length - 1]; - let xChildrenStart = transformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.XPositionReference); - let xChildrenEnd = transformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.XPositionReference); - - // Find the min and max - for (let i = props.model.children.length - 2; i >= 0; i--) { - const child = props.model.children[i]; - const left = transformX(child.properties.x, child.properties.width, child.properties.XPositionReference); - if (left < xChildrenStart) { - xChildrenStart = left; - } - const right = transformX(child.properties.x, child.properties.width, child.properties.XPositionReference); - if (right > xChildrenEnd) { - xChildrenEnd = right; - } - } - - const yChildren = props.model.properties.height + dimensionMargin; - const textChildren = (xChildrenEnd - xChildrenStart).toString(); - return { childrenId, xChildrenStart, xChildrenEnd, yChildren, textChildren }; -} - -export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number { - let transformedX = x; - if (xPositionReference === XPositionReference.Center) { - transformedX += width / 2; - } else if (xPositionReference === XPositionReference.Right) { - transformedX += width; - } - return transformedX; -} - -export function restoreX(x: number, width: number, xPositionReference = XPositionReference.Left): number { +function transformPosition(x: number, y: number, width: number, xPositionReference = XPositionReference.Left): [number, number] { let transformedX = x; if (xPositionReference === XPositionReference.Center) { transformedX -= width / 2; } else if (xPositionReference === XPositionReference.Right) { transformedX -= width; } - return transformedX; + return [transformedX, y]; } diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index c57daee..4fa36e6 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -8,6 +8,7 @@ import { IHistoryState } from '../../Interfaces/IHistoryState'; import { PhotographIcon, UploadIcon } from '@heroicons/react/outline'; import { FloatingButton } from '../FloatingButton/FloatingButton'; import { Bar } from '../Bar/Bar'; +import IProperties from '../../Interfaces/IProperties'; interface IUIProps { current: IHistoryState @@ -16,8 +17,8 @@ interface IUIProps { AvailableContainers: IAvailableContainer[] SelectContainer: (container: ContainerModel) => void DeleteContainer: (containerId: string) => void - OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void - OnPropertiesSubmit: (event: React.FormEvent) => void + OnPropertyChange: (key: string, value: string | number | boolean) => void + OnPropertiesSubmit: (event: React.FormEvent, properties: IProperties) => void AddContainerToSelectedContainer: (type: string) => void AddContainer: (index: number, type: string, parentId: string) => void SaveEditorAsJSON: () => void diff --git a/src/Enums/AddMethod.ts b/src/Enums/AddMethod.ts deleted file mode 100644 index c0d1e37..0000000 --- a/src/Enums/AddMethod.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Add method when creating a container - * - Append will append to the last children in list - * - Insert will always place it at the begining - * (default: Append) - */ -export enum AddMethod { - Append, - Insert -} diff --git a/src/Interfaces/IAvailableContainer.ts b/src/Interfaces/IAvailableContainer.ts index 2f4a011..c7ad3c1 100644 --- a/src/Interfaces/IAvailableContainer.ts +++ b/src/Interfaces/IAvailableContainer.ts @@ -1,15 +1,11 @@ import React from 'react'; -import { AddMethod } from '../Enums/AddMethod'; import { XPositionReference } from '../Enums/XPositionReference'; /** Model of available container used in application configuration */ export interface IAvailableContainer { Type: string - Width?: number - Height?: number - DefaultX?: number - DefaultY?: number - AddMethod?: AddMethod + Width: number + Height: number XPositionReference?: XPositionReference Style: React.CSSProperties } diff --git a/src/Interfaces/IInputGroup.ts b/src/Interfaces/IInputGroup.ts deleted file mode 100644 index dfc9942..0000000 --- a/src/Interfaces/IInputGroup.ts +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; - -export interface IInputGroup { - text: React.ReactNode - value: string -} diff --git a/src/Interfaces/IProperties.ts b/src/Interfaces/IProperties.ts index 2513d74..ef2db7e 100644 --- a/src/Interfaces/IProperties.ts +++ b/src/Interfaces/IProperties.ts @@ -10,15 +10,12 @@ import { XPositionReference } from '../Enums/XPositionReference'; * @property isRigidBody if true apply rigid body behaviors * @property isAnchor if true apply anchor behaviors */ -export default interface IProperties { +export default interface IProperties extends React.CSSProperties { id: string parentId: string | null x: number y: number - width: number - height: number isRigidBody: boolean isAnchor: boolean - XPositionReference: XPositionReference - style?: React.CSSProperties + XPositionReference?: XPositionReference } diff --git a/src/index.scss b/src/index.scss index d9712f5..f653990 100644 --- a/src/index.scss +++ b/src/index.scss @@ -46,10 +46,6 @@ @apply h-full w-full align-middle items-center justify-center } - .radio-button-icon { - @apply rounded-md shadow-sm bg-white w-8 cursor-pointer inline-block - } - .sidebar-tooltip { @apply absolute w-auto p-2 m-2 min-w-max left-14 rounded-md shadow-md diff --git a/src/utils/default.ts b/src/utils/default.ts index 97df4b0..027c540 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -1,4 +1,3 @@ -import { XPositionReference } from '../Enums/XPositionReference'; import { IConfiguration } from '../Interfaces/IConfiguration'; import IProperties from '../Interfaces/IProperties'; @@ -31,18 +30,14 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = { parentId: 'null', x: 0, y: 0, - width: Number(DEFAULT_CONFIG.MainContainer.Width), - height: Number(DEFAULT_CONFIG.MainContainer.Height), + width: DEFAULT_CONFIG.MainContainer.Width, + height: DEFAULT_CONFIG.MainContainer.Height, isRigidBody: false, isAnchor: false, - XPositionReference: XPositionReference.Left, - style: { - stroke: 'black', - fillOpacity: 0 - } + fillOpacity: 0, + stroke: 'black' }; -export const DIMENSION_MARGIN = 50; export const NOTCHES_LENGTH = 4; export const MAX_HISTORY = 200; diff --git a/src/utils/itertools.ts b/src/utils/itertools.ts index 221d6c0..d50a089 100644 --- a/src/utils/itertools.ts +++ b/src/utils/itertools.ts @@ -43,12 +43,12 @@ export function getDepth(parent: IContainerModel): number { * @returns The absolute position of the container */ export function getAbsolutePosition(container: IContainerModel): [number, number] { - let x = container.properties.x; - let y = container.properties.y; + let x = Number(container.properties.x); + let y = Number(container.properties.y); let current = container.parent; while (current != null) { - x += current.properties.x; - y += current.properties.y; + x += Number(current.properties.x); + y += Number(current.properties.y); current = current.parent; } return [x, y]; diff --git a/test-server/http.js b/test-server/http.js index e6b7051..69088c9 100644 --- a/test-server/http.js +++ b/test-server/http.js @@ -55,44 +55,27 @@ const GetSVGLayoutConfiguration = () => { Type: 'Chassis', Width: 500, Style: { - fillOpacity: 1, + fillOpacity: 0, borderWidth: 2, - stroke: 'red', - fill: '#78350F', stroke: 'red' } }, { Type: 'Trou', - DefaultX: 10, - DefaultY: 10, - Width: 480, - Height: 180, + Width: 300, Style: { - fillOpacity: 1, + fillOpacity: 0, borderWidth: 2, - stroke: 'green', - fill: 'white' - } - }, - { - Type: 'Remplissage', - Style: { - fillOpacity: 1, - borderWidth: 2, - stroke: '#bfdbfe', - fill: '#bfdbfe' + stroke: 'green' } }, { Type: 'Montant', - Width: 10, - XPositionReference: 1, + Width: 100, Style: { fillOpacity: 0, borderWidth: 2, - stroke: '#713f12', - fill: '#713f12', + stroke: 'blue', } } ],