Implement minWidth property + refactor default property of new containers and main container (#34)
All checks were successful
continuous-integration/drone/push Build is passing

Co-authored-by: Eric NGUYEN <enguyen@techform.fr>
Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/34
This commit is contained in:
Siklos 2022-08-17 11:14:19 -04:00
parent 60329ef143
commit 2945febd91
12 changed files with 137 additions and 44 deletions

View file

@ -5,6 +5,7 @@ import { fetchConfiguration } from '../API/api';
import { IEditorState } from '../../Interfaces/IEditorState'; import { IEditorState } from '../../Interfaces/IEditorState';
import { LoadState } from './Load'; import { LoadState } from './Load';
import { XPositionReference } from '../../Enums/XPositionReference'; import { XPositionReference } from '../../Enums/XPositionReference';
import { DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default';
export function NewEditor( export function NewEditor(
setEditorState: Dispatch<SetStateAction<IEditorState>>, setEditorState: Dispatch<SetStateAction<IEditorState>>,
@ -17,19 +18,9 @@ export function NewEditor(
const MainContainer = new ContainerModel( const MainContainer = new ContainerModel(
null, null,
{ {
id: 'main', ...DEFAULT_MAINCONTAINER_PROPS,
parentId: 'null',
x: 0,
y: 0,
width: Number(configuration.MainContainer.Width), width: Number(configuration.MainContainer.Width),
height: Number(configuration.MainContainer.Height), height: Number(configuration.MainContainer.Height)
isRigidBody: false,
isAnchor: false,
XPositionReference: XPositionReference.Left,
style: {
fillOpacity: 0,
stroke: 'black'
}
} }
); );

View file

@ -152,7 +152,7 @@ export function constraintBodyInsideUnallocatedWidth(
// Check if the container actually fit inside // Check if the container actually fit inside
// It will usually fit if it was alrady fitting // It will usually fit if it was alrady fitting
const availableWidthFound = availableWidths.find((width) => const availableWidthFound = availableWidths.find((width) =>
isFitting(container, width) isFitting(container.properties.width, width)
); );
if (availableWidthFound === undefined) { if (availableWidthFound === undefined) {
@ -163,12 +163,18 @@ export function constraintBodyInsideUnallocatedWidth(
// We want the container to fit automatically inside the available space // We want the container to fit automatically inside the available space
// even if it means to resize the container // even if it means to resize the container
// The end goal is that the code never show the error message no matter what action is done const availableWidth: ISizePointer | undefined = availableWidths.find((width) => {
// TODO: Actually give an option to not fit and show the error message shown below return isFitting(container.properties.minWidth, width);
const availableWidth = availableWidths[0]; });
if (availableWidth === undefined) {
console.warn(`Container ${container.properties.id} cannot fit in any space due to its minimum width being to large. Consequently, its rigid body property is disabled.`);
container.properties.isRigidBody = false;
return container;
}
container.properties.x = availableWidth.x; container.properties.x = availableWidth.x;
container.properties.width = availableWidth.width; container.properties.width = availableWidth.width;
// throw new Error('[constraintBodyInsideUnallocatedWidth] BIGERR: No available space found on the parent container, even though there is some.');
return container; return container;
} }
@ -188,9 +194,9 @@ export function constraintBodyInsideUnallocatedWidth(
* @returns * @returns
*/ */
const isFitting = ( const isFitting = (
container: IContainerModel, containerWidth: number,
sizePointer: ISizePointer sizePointer: ISizePointer
): boolean => container.properties.width <= sizePointer.width; ): boolean => containerWidth <= sizePointer.width;
/** /**
* Get the unallocated widths inside a container * Get the unallocated widths inside a container

View file

@ -4,10 +4,9 @@ import { IConfiguration } from '../../Interfaces/IConfiguration';
import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
import { findContainerById } from '../../utils/itertools'; import { findContainerById } from '../../utils/itertools';
import { getCurrentHistory } from './Editor'; import { getCurrentHistory } from './Editor';
import IProperties from '../../Interfaces/IProperties';
import { AddMethod } from '../../Enums/AddMethod'; import { AddMethod } from '../../Enums/AddMethod';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { XPositionReference } from '../../Enums/XPositionReference'; import { GetDefaultContainerProps } from '../../utils/default';
/** /**
* Select a container * Select a container
@ -203,25 +202,17 @@ export function AddContainer(
let x = containerConfig.DefaultX ?? 0; let x = containerConfig.DefaultX ?? 0;
const y = containerConfig.DefaultY ?? 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); x = ApplyAddMethod(index, containerConfig, parentClone, x);
const defaultProperties: IProperties = { const defaultProperties = GetDefaultContainerProps(
id: `${type}-${count}`, type,
parentId: parentClone.properties.id, count,
parentClone,
x, x,
y, y,
width, containerConfig
height, );
isRigidBody: false,
isAnchor: false,
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
customSVG: containerConfig.CustomSVG,
style: containerConfig.Style,
userData: containerConfig.UserData
};
// Create the container // Create the container
const newContainer = new ContainerModel( const newContainer = new ContainerModel(
@ -265,6 +256,7 @@ function ApplyAddMethod(index: number, containerConfig: IAvailableContainer, par
if (index > 0 && ( if (index > 0 && (
containerConfig.AddMethod === undefined || containerConfig.AddMethod === undefined ||
containerConfig.AddMethod === AddMethod.Append)) { containerConfig.AddMethod === AddMethod.Append)) {
// Append method (default)
const lastChild: IContainerModel | undefined = parent.children.at(index - 1); const lastChild: IContainerModel | undefined = parent.children.at(index - 1);
if (lastChild !== undefined) { if (lastChild !== undefined) {

View file

@ -18,6 +18,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
minWidth: 1,
XPositionReference: XPositionReference.Left, XPositionReference: XPositionReference.Left,
isRigidBody: false, isRigidBody: false,
isAnchor: false isAnchor: false
@ -50,6 +51,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
minWidth: 1,
isRigidBody: false, isRigidBody: false,
isAnchor: false, isAnchor: false,
XPositionReference: XPositionReference.Left XPositionReference: XPositionReference.Left
@ -106,6 +108,7 @@ describe.concurrent('Elements sidebar', () => {
parentId: '', parentId: '',
x: 0, x: 0,
y: 0, y: 0,
minWidth: 1,
width: 2000, width: 2000,
height: 100, height: 100,
XPositionReference: XPositionReference.Left, XPositionReference: XPositionReference.Left,
@ -124,6 +127,7 @@ describe.concurrent('Elements sidebar', () => {
parentId: 'main', parentId: 'main',
x: 0, x: 0,
y: 0, y: 0,
minWidth: 1,
width: 0, width: 0,
height: 0, height: 0,
isRigidBody: false, isRigidBody: false,
@ -143,6 +147,7 @@ describe.concurrent('Elements sidebar', () => {
parentId: 'main', parentId: 'main',
x: 0, x: 0,
y: 0, y: 0,
minWidth: 1,
width: 0, width: 0,
height: 0, height: 0,
XPositionReference: XPositionReference.Left, XPositionReference: XPositionReference.Left,
@ -182,6 +187,7 @@ describe.concurrent('Elements sidebar', () => {
parentId: '', parentId: '',
x: 0, x: 0,
y: 0, y: 0,
minWidth: 1,
width: 2000, width: 2000,
height: 100, height: 100,
XPositionReference: XPositionReference.Left, XPositionReference: XPositionReference.Left,
@ -199,6 +205,7 @@ describe.concurrent('Elements sidebar', () => {
parentId: 'main', parentId: 'main',
x: 0, x: 0,
y: 0, y: 0,
minWidth: 1,
width: 0, width: 0,
height: 0, height: 0,
XPositionReference: XPositionReference.Left, XPositionReference: XPositionReference.Left,

View file

@ -11,6 +11,7 @@ interface IInputGroupProps {
checked?: boolean checked?: boolean
defaultValue?: string defaultValue?: string
defaultChecked?: boolean defaultChecked?: boolean
min?: number
isDisabled?: boolean isDisabled?: boolean
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
@ -42,6 +43,7 @@ export const InputGroup: React.FunctionComponent<IInputGroupProps> = (props) =>
checked={props.checked} checked={props.checked}
defaultChecked={props.defaultChecked} defaultChecked={props.defaultChecked}
onChange={props.onChange} onChange={props.onChange}
min={props.min}
disabled={props.isDisabled} disabled={props.isDisabled}
/> />
</>; </>;

View file

@ -70,12 +70,23 @@ const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
value={props.properties.y.toString()} value={props.properties.y.toString()}
onChange={(event) => props.onChange('y', Number(event.target.value))} onChange={(event) => props.onChange('y', Number(event.target.value))}
/> />
<InputGroup
labelText='Minimum width'
inputKey='minWidth'
labelClassName=''
inputClassName=''
type='number'
min={1}
value={props.properties.minWidth.toString()}
onChange={(event) => props.onChange('minWidth', Number(event.target.value))}
/>
<InputGroup <InputGroup
labelText='Width' labelText='Width'
inputKey='width' inputKey='width'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
type='number' type='number'
min={props.properties.minWidth}
value={props.properties.width.toString()} value={props.properties.width.toString()}
onChange={(event) => props.onChange('width', Number(event.target.value))} onChange={(event) => props.onChange('width', Number(event.target.value))}
/> />
@ -85,6 +96,7 @@ const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
type='number' type='number'
min={0}
value={props.properties.height.toString()} value={props.properties.height.toString()}
onChange={(event) => props.onChange('height', Number(event.target.value))} onChange={(event) => props.onChange('height', Number(event.target.value))}
/> />

View file

@ -27,6 +27,7 @@ describe.concurrent('Properties', () => {
y: 1, y: 1,
width: 1, width: 1,
height: 1, height: 1,
minWidth: 1,
XPositionReference: XPositionReference.Left, XPositionReference: XPositionReference.Left,
isRigidBody: false, isRigidBody: false,
isAnchor: false isAnchor: false

View file

@ -68,12 +68,22 @@ const StaticForm: React.FunctionComponent<IStaticFormProps> = (props) => {
type='number' type='number'
defaultValue={props.properties.y.toString()} defaultValue={props.properties.y.toString()}
/> />
<InputGroup
labelText='Minimum width'
inputKey='minWidth'
labelClassName=''
inputClassName=''
type='number'
min={0}
defaultValue={props.properties.minWidth.toString()}
/>
<InputGroup <InputGroup
labelText='Width' labelText='Width'
inputKey='width' inputKey='width'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
type='number' type='number'
min={props.properties.minWidth}
defaultValue={props.properties.width.toString()} defaultValue={props.properties.width.toString()}
/> />
<InputGroup <InputGroup
@ -82,6 +92,7 @@ const StaticForm: React.FunctionComponent<IStaticFormProps> = (props) => {
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
type='number' type='number'
min={1}
defaultValue={props.properties.height.toString()} defaultValue={props.properties.height.toString()}
/> />
<InputGroup <InputGroup

View file

@ -5,13 +5,14 @@ import { XPositionReference } from '../Enums/XPositionReference';
/** Model of available container used in application configuration */ /** Model of available container used in application configuration */
export interface IAvailableContainer { export interface IAvailableContainer {
Type: string Type: string
Width?: number
Height?: number
DefaultX?: number DefaultX?: number
DefaultY?: number DefaultY?: number
Width?: number
Height?: number
MinWidth?: number
AddMethod?: AddMethod AddMethod?: AddMethod
XPositionReference?: XPositionReference XPositionReference?: XPositionReference
CustomSVG?: string CustomSVG?: string
Style: React.CSSProperties Style?: React.CSSProperties
UserData?: object UserData?: object
} }

View file

@ -3,24 +3,67 @@ import { XPositionReference } from '../Enums/XPositionReference';
/** /**
* Properties of a container * Properties of a container
* @property id id of the container
* @property parentId id of the parent container
* @property x horizontal offset of the container
* @property y vertical offset of the container
* @property isRigidBody if true apply rigid body behaviors
* @property isAnchor if true apply anchor behaviors
*/ */
export default interface IProperties { export default interface IProperties {
/** id of the container */
id: string id: string
/** id of the parent container (null when there is no parent) */
parentId: string | null parentId: string | null
/** horizontal offset */
x: number x: number
/** vertical offset */
y: number y: number
/**
* Minimum width (min=1)
* Allows the container to set isRigidBody to false when it gets squeezed
* by an anchor
*/
minWidth: number
/** width */
width: number width: number
/** height */
height: number height: number
/** true if rigid, false otherwise */
isRigidBody: boolean isRigidBody: boolean
/** true if anchor, false otherwise */
isAnchor: boolean isAnchor: boolean
/** Horizontal alignment, also determines the visual location of x {Left = 0, Center, Right } */
XPositionReference: XPositionReference XPositionReference: XPositionReference
/**
* (optional)
* Replace a <rect> by a customized "SVG". It is not really an svg but it at least allows
* to draw some patterns that can be bind to the properties of the container
* Use {prop} to bind a property. Use {{ styleProp }} to use an object.
* Example :
* ```
* `<rect width="{width}" height="{height}" style="{style}"></rect>
* <rect width="{width}" height="{height}" stroke="black" fill-opacity="0"></rect>
* <line x1="0" y1="0" x2="{width}" y2="{height}" stroke="black" style='{{ "transform":"scaleY(0.5)"}}'></line>
* <line x1="{width}" y1="0" x2="0" y2="{height}" stroke="black" style='{userData.styleLine}'></line>
* `
* ```
*/
customSVG?: string customSVG?: string
/**
* (optional)
* Style of the <rect>
*/
style?: React.CSSProperties style?: React.CSSProperties
/**
* (optional)
* User data that can be used for data storage or custom SVG
*/
userData?: object userData?: object
} }

View file

@ -1,5 +1,7 @@
import { XPositionReference } from '../Enums/XPositionReference'; import { XPositionReference } from '../Enums/XPositionReference';
import { IAvailableContainer } from '../Interfaces/IAvailableContainer';
import { IConfiguration } from '../Interfaces/IConfiguration'; import { IConfiguration } from '../Interfaces/IConfiguration';
import { IContainerModel } from '../Interfaces/IContainerModel';
import IProperties from '../Interfaces/IProperties'; import IProperties from '../Interfaces/IProperties';
export const DEFAULT_CONFIG: IConfiguration = { export const DEFAULT_CONFIG: IConfiguration = {
@ -31,6 +33,7 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
parentId: 'null', parentId: 'null',
x: 0, x: 0,
y: 0, y: 0,
minWidth: 1,
width: Number(DEFAULT_CONFIG.MainContainer.Width), width: Number(DEFAULT_CONFIG.MainContainer.Width),
height: Number(DEFAULT_CONFIG.MainContainer.Height), height: Number(DEFAULT_CONFIG.MainContainer.Height),
isRigidBody: false, isRigidBody: false,
@ -42,6 +45,29 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
} }
}; };
export const GetDefaultContainerProps = (
type: string,
typeCount: number,
parent: IContainerModel,
x: number,
y: number,
containerConfig: IAvailableContainer
): IProperties => ({
id: `${type}-${typeCount}`,
parentId: parent.properties.id,
x,
y,
width: containerConfig.Width ?? containerConfig.MinWidth ?? parent.properties.width,
height: containerConfig.Height ?? parent.properties.height,
isRigidBody: false,
isAnchor: false,
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
minWidth: containerConfig.MinWidth ?? 0,
customSVG: containerConfig.CustomSVG,
style: containerConfig.Style,
userData: containerConfig.UserData
});
export const DIMENSION_MARGIN = 50; export const DIMENSION_MARGIN = 50;
export const NOTCHES_LENGTH = 4; export const NOTCHES_LENGTH = 4;

View file

@ -54,6 +54,7 @@ const GetSVGLayoutConfiguration = () => {
{ {
Type: 'Chassis', Type: 'Chassis',
Width: 500, Width: 500,
MinWidth: 200,
Style: { Style: {
fillOpacity: 1, fillOpacity: 1,
strokeWidth: 2, strokeWidth: 2,