Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MARKET-1466 Cascade Select #184

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
39 changes: 39 additions & 0 deletions components/bl-cascadeSelect-component/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Cascade Select

Cascade Select is a component of Backendless UI-Builder designer. This allows you to select a value from a nested structure of options.

The component based on external [Cascade Select](https://www.primefaces.org/primereact/cascadeselect/).
Valodya marked this conversation as resolved.
Show resolved Hide resolved

## Properties

| Property | Type | Default Value | Logic | Data Binding | UI Setting | Description |
|-------------------|---------|---------------------|-----------------------------|--------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| cascade | JSON | | Cascade Logic | YES | YES | Allows determinate an array of select items to display as the available options. Watch [Codeless Examples](#Examples). Signature of polygon: `[{name, code, ?children}]` |
| placeholder | String | | Placeholder Logic | YES | YES | Allows determinate placeholder for input |
v-excelsior marked this conversation as resolved.
Show resolved Hide resolved

## Events

| Name | Triggers | Context Blocks |
|---------------|-------------------------------|--------------------------------------------------------------|
| On Click Item | when the user select the item | Item: `{name: String, code: String, levelOfNesting: Number}` |

## Action

| Action | Inputs | Return |
|---------------|--------|--------------------------|
| Get Select in | | Object: of a select item |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about add an actions same as for default select?
Set Options
Get Options
Set Value
Get Value

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please describe these actions here in the doc


## <a name="Examples"></a> Codeless Examples

Addition of cascade data

![](example-images/cascade_example.jpg)

<details>
<summary>Try yourself</summary>

```
<block xmlns="http://www.w3.org/1999/xhtml" type="lists_create_with" id="I6`{YbX`1w)ZrZA[n(3l" x="-94.53923425078003" y="88.92089374100392"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="hd^`S({p+5%(tCzQMIkl"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="Rmz=y,*(iJ0^*7tqt^wN"><field name="TEXT">Australia</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="73(!S~9Bj8[(1dd.hOG%"><field name="TEXT">AU</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="+^[pE!*BrEq@/~$ZXJDM"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="ely].XX{?.SFw}g*Ux$F"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="0,Ub7#U7iP^Uc;F1W,l%"><field name="TEXT">New South Wales</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="w#9]c/AL5qytPDCGEGh^"><field name="TEXT">AU-NSW</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="NcaOOw}XzebfYg[$#]*V"><mutation items="3"></mutation><value name="ADD0"><block type="create_object" id="=@e``]rkt/IwmHZaxEGU"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="e(%`Bzh$xiJ9{xFo2YL/"><field name="TEXT">Sydney</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="[.3Q:STX@sn9Lw}pm7Qc"><field name="TEXT">AU-NSW-SY</field></block></value></block></value><value name="ADD1"><block type="create_object" id="dTF-V$w2/}%bGhSyA%]Q"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="0WOJ4K22**Az9=mscXx7"><field name="TEXT">Newcastle</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="^D)=+TRJD8Hbb(X%qAy."><field name="TEXT">AU-NSW-NC</field></block></value></block></value><value name="ADD2"><block type="create_object" id="ZAL1i-YAD!U*bPcteuR!"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="101HuiZBCvUT)Q=k;c7O"><field name="TEXT">Wollongong</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="II{6/P]UldmqR84w=#yo"><field name="TEXT">AU-NSW-WG</field></block></value></block></value></block></value></block></value><value name="ADD1"><block type="create_object" id="23VyPHa2^%a{BnrD;:oc"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="qdN:ohQ^xc{~Rsn:GAq+"><field name="TEXT">Queensland</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="UFX@h3$}X52c}@S^*p/^"><field name="TEXT">AU-QS</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="@=LxHd02i?4?t+rW|64h"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="%wf4cFQJ/qGl{7/Il2A$"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id=".Tn=|{GrFAU]w[=21lH."><field name="TEXT">Brisbane</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="e`w,YXw1(ceOTdI2j9+L"><field name="TEXT">AU-QS-BB</field></block></value></block></value><value name="ADD1"><block type="create_object" id="%rbhI}Sb!@Vue942V_W}"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="x4~`U{VoDZ3gMG`i3-Fg"><field name="TEXT">Townsville</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="v{y{ntd-%*4fjlFH4)=!"><field name="TEXT">AU-QS-TS</field></block></value></block></value></block></value></block></value></block></value></block></value><value name="ADD1"><block type="create_object" id="st!J)Cx*,C_Xx4E)5,,|"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="(*uAjQ7(V_NLb#`mc)!s"><field name="TEXT">Canada</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="JZrB.YadV5v/APu_6/xL"><field name="TEXT">CA</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="It-{1g[_kl})XaTbVnlK"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="[email protected]+b]ipJ3dnDD+"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="G7j*jnSN(Annx89J{Ko:"><field name="TEXT">Quebec</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="w[O4hm|+LN4`3kHsfo^v"><field name="TEXT">CA-QB</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="h8Aa4cJAZ+Z2bTu;v=@j"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="eKI@M]|TJAL_3O@|tV7}"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="}l+-hZaLgz)z,SSO~yE`"><field name="TEXT">Montreal</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="lpCB5?#F[vP_b{!`yDGK"><field name="TEXT">CA-QB-MR</field></block></value></block></value><value name="ADD1"><block type="create_object" id="^LaaQXj.d}=ii[.0=`~;"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="m0L`+S|g$=qv~fKFe(WF"><field name="TEXT">Quebec City</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="A;RO8hooP(x*Feh}~ohR"><field name="TEXT">CA-QB-CBC</field></block></value></block></value></block></value></block></value><value name="ADD1"><block type="create_object" id="zlA_eq+Dyde5(~QORUv0"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item><item id="property" prop-name="children"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="Yt7Xd-sMIFsp]0DrG*~D"><field name="TEXT">Ontario</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="~8l2Q5MC/Ko3KR:EJeEs"><field name="TEXT">CA-OT</field></block></value><value name="create_object_mutator_container_properties_stack_property2"><block type="lists_create_with" id="_GWtd^*`0F1VY`aNycML"><mutation items="2"></mutation><value name="ADD0"><block type="create_object" id="RO2Sy[E4,FQU#z$IuFvx"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="@P`$ZS$j6tVBtX_7*sqz"><field name="TEXT">Ottawa</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="%AxPNTGKb#_)r{welh5,"><field name="TEXT">CA-OT-OW</field></block></value></block></value><value name="ADD1"><block type="create_object" id="d|X]SW;eH:GO]Hi9ff(H"><mutation><properties><item id="property" prop-name="name"></item><item id="property" prop-name="code"></item></properties></mutation><value name="create_object_mutator_container_properties_stack_property0"><block type="text" id="yxz9yUj@6RM^$caT*;Y?"><field name="TEXT">Toronto</field></block></value><value name="create_object_mutator_container_properties_stack_property1"><block type="text" id="[Xw5938k[M2kk;lD_#Q5"><field name="TEXT">CA-OT-TR</field></block></value></block></value></block></value></block></value></block></value></block></value></block>
v-excelsior marked this conversation as resolved.
Show resolved Hide resolved
```
</details>

54 changes: 54 additions & 0 deletions components/bl-cascadeSelect-component/component.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"id": "c_4709a015328b307e652d915fc3f36fb7",
"name": "Cascade Select",
"description": "Cascade Select is a component to select a value from a nested structure of options.",
"showInToolbox": true,
"faIcon": "check-double",
"mainJS": "dist/index.js",
"type": "custom",
"category": "Custom Components",
"properties": [
{
"type": "json",
"name": "cascade",
"label": "Cascade",
"showInSettings": true,
"hasLogicHandler": true,
"handlerId": "cascadeLogic",
"handlerLabel": "Cascade Logic",
"dataBinding": true,
"handlerDescription": "This is a handler for the logic to determine an array of select items to display as the available options."
},
{
"type": "text",
"name": "placeholder",
"label": "Placeholder",
"showInSettings": true,
"hasLogicHandler": true,
"handlerId": "placeholderLogic",
"handlerLabel": "Placeholder Logic",
"dataBinding": true,
"handlerDescription": "This is a handler for the logic to determine the default text to display when no option is selected."
}
],
"eventHandlers": [
{
"name": "onClickItem",
"label": "On Click Item",
"contextBlocks": [
{
"id": "item",
"label": "Item"
}
],
"handlerDescription": "This event is triggered when user select item"
}
],
"actions": [
{
"id": "getSelected",
"label": "Get Selected in",
"hasReturn": true
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions components/bl-cascadeSelect-component/preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div data-module-type="system" data-module-id="block" data-display data-uid="97c1e475f636c6292fddcff60a990462" style="display:flex;flex-shrink:0;min-width:100px;border:2px solid #aaaaaa;flex-direction:row;justify-content:space-between;align-items:center;padding:10px 10px 10px 10px;border-radius:6px 6px 6px 6px;"><span data-content="Cascade Select" data-module-type="system" data-module-id="text" data-display data-uid="398503af28abc33291c6092d9661cbe5" class="bl-text" style="color:#aaaaaa;"></span><i data-icon="arrow_forward_ios" data-size="small" data-module-type="system" data-module-id="icon" data-display data-uid="57c3cba667be0bfe1fdeafc5362aa967" style="color:#aaaaaa;"></i></div>
90 changes: 90 additions & 0 deletions components/bl-cascadeSelect-component/src/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
export function isCyclic(obj) {
const keys = [];
const stack = [];
const stackSet = new Set();
let detected = false;
let locate;

function detect(obj, key) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detectCircular ?

if (obj && typeof obj != 'object') {
return;
}

if (stackSet.has(obj)) {
locate = keys.join('.') + '.' + key;
detected = true;

return;
}

keys.push(key);
stack.push(obj);
stackSet.add(obj);

for (const k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
detect(obj[k], k);
}
}

keys.pop();
stack.pop();
stackSet.delete(obj);

return;
Valodya marked this conversation as resolved.
Show resolved Hide resolved
}

detect(obj, 'obj');

return [detected, locate];
}

export const prepareCascade = (cascade, setParentItems) => {
let levelOfNesting = 0;
const parentItems = [];
const groupParentItems = [];

const prepare = cascade => {
const validCascade = cascade.map(item => {
let validItem = { ...item, levelOfNesting };

if (item.children) {
levelOfNesting++;
validItem = {
...validItem,
children: prepare(item.children),
};

parentItems.push({ code: item.code, isOpen: false, levelOfNesting });
}

return validItem;
});

levelOfNesting--;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please explain the logic around ++/-- levelOfNesting

if it needs to calculate nesting depth would not it be better to pass a new level into the prepare function?

 const prepare = (children, depth) => {
if (item.children) {
        validItem = {
          ...validItem,
          children: prepare(item.children, depth + 1),
        };

        parentItems.push({ code: item.code, isOpen: false, depth });
      } else {
        items.push(validItem);
      }
 }

 const preparedCascade = prepare(cascade);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, with this implementation, we will not be able to get the last depth value when this recursion ends. It's needed to get max depth

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need to know about the max depth?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get sorted and separated array of parentItems by levelOfNesting in the getNestedItems function


return validCascade;
};

const preparedCascade = prepare(cascade);

for (let i = 0; i <= levelOfNesting * -1; i++) {
v-excelsior marked this conversation as resolved.
Show resolved Hide resolved
groupParentItems.push(parentItems.filter(item => {
return item.levelOfNesting === i;
}));
v-excelsior marked this conversation as resolved.
Show resolved Hide resolved
}

setParentItems(groupParentItems);

return preparedCascade;
};

export const findParentItem = (parentItems, item) => {
for (let i = 0; i < parentItems.length; i++) {
for (let j = 0; j < parentItems[i].length; j++) {
if (parentItems[i][j].code === item.code) {
return parentItems[i][j];
}
}
}
};
79 changes: 79 additions & 0 deletions components/bl-cascadeSelect-component/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useState, useEffect, useCallback } from 'react';
import { CollapseButtonIcon, Cascade } from './subcomponent';
import { prepareCascade, isCyclic } from './helpers';

const { cn } = BackendlessUI.CSSUtils;

export default function CascadeSelect({ component, eventHandlers }) {
const { display, classList, style, cascade, placeholder } = component;
const { onClickItem } = eventHandlers;

const [itemsCascade, setItemsCascade] = useState();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference between them?

  • itemsCascade
  • parentItems
  • items

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • itemsCascade is a validated object which comes from property cascade
  • parentItems is the array of arrays of parent items(items which have children) separated by levelOfNesting
  • items is the array of items which we can select(which haven't children)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still do not understand why we need 3 lists instead of a single one itemsCascade
we can validate/transform the source list into a list that suits all needs, the target list can have any structure we need and items can have all necessary information about clickable/nestingLevel/etc

Copy link
Contributor Author

@StasivPavlo StasivPavlo Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it's easy to find items in an array, instead of an array of objects that have in turn, have their own array of objects

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parent reference should solve that problem

const [parentItems, setParentItems] = useState([]);
const [selected, setSelected] = useState({ name: placeholder });
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
const [detected, locate] = isCyclic(cascade);

if (detected) {
v-excelsior marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('cascade have cycling object in ' + locate);
}

if (cascade) {
setItemsCascade(prepareCascade(cascade, setParentItems));
}
}, [cascade]);

const openCascadeHandler = useCallback(item => {
const { code, levelOfNesting } = item;

setParentItems(state => {
const currentParentItems = [...state];
v-excelsior marked this conversation as resolved.
Show resolved Hide resolved

for (let i = 0; i < currentParentItems[levelOfNesting].length; i++) {
if (currentParentItems[levelOfNesting][i].code === code) {
currentParentItems[levelOfNesting][i].isOpen = !currentParentItems[levelOfNesting][i].isOpen;
} else {
currentParentItems[levelOfNesting][i].isOpen = false;
}
v-excelsior marked this conversation as resolved.
Show resolved Hide resolved
}

return currentParentItems;
});
}, []);

const openItemHandler = useCallback(item => {
setSelected(item);
setIsOpen(false);

onClickItem({ item });
}, []);

const onClickInput = () => setIsOpen(state => !state);

component.getSelected = () => selected;
Valodya marked this conversation as resolved.
Show resolved Hide resolved

if (!display) {
return null;
}

return (
<div className={ cn('bl-cascadeSelect-component', ...classList) } style={ style }>
<div
className={ cn('cascade-select__input', { 'cascade-select__input--selected': selected.code }) }
onClick={ onClickInput }>
<span>{ selected.name }</span>
<CollapseButtonIcon/>
</div>
<Cascade
isOpen={ isOpen }
selected={ selected }
itemsCascade={ itemsCascade }
parentItems={ parentItems }
openCascadeHandler={ openCascadeHandler }
openItemHandler={ openItemHandler }
/>
</div>
);
}
Loading