Split components into separate files
Signed-off-by: Egor Savkin <es@m-labs.hk>
This commit is contained in:
parent
2d6cb872f1
commit
ecbbd6898d
File diff suppressed because one or more lines are too long
2483
static/js/shop.jsx
2483
static/js/shop.jsx
File diff suppressed because it is too large
Load Diff
118
static/js/shop/Backlog.jsx
Normal file
118
static/js/shop/Backlog.jsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import React, {PureComponent} from 'react';
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {v4 as uuidv4} from "uuid";
|
||||||
|
import {Droppable} from "react-beautiful-dnd";
|
||||||
|
import {ProductItem} from "./ProductItem.jsx";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders the backlog in the aside
|
||||||
|
*/
|
||||||
|
export class Backlog extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
currency: PropTypes.string,
|
||||||
|
data: PropTypes.object.isRequired,
|
||||||
|
items: PropTypes.object,
|
||||||
|
isMobile: PropTypes.bool,
|
||||||
|
onClickAddItem: PropTypes.func,
|
||||||
|
onClickToggleMobileSideMenu: PropTypes.func,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get defaultProps() {
|
||||||
|
return {
|
||||||
|
items: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
currency,
|
||||||
|
data,
|
||||||
|
items,
|
||||||
|
onClickAddItem,
|
||||||
|
onClickToggleMobileSideMenu,
|
||||||
|
isMobile,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
|
||||||
|
const ordered_groups = data.categories.map(groupItem => ({
|
||||||
|
name: groupItem.name,
|
||||||
|
items: groupItem.itemIds.map(itemId => items[itemId])
|
||||||
|
}));
|
||||||
|
let item_index = -1;
|
||||||
|
const groups = ordered_groups.map((group, g_index) => {
|
||||||
|
return (
|
||||||
|
<div className="accordion-item" key={`${group.name}`}>
|
||||||
|
<h2 className="accordion-header">
|
||||||
|
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target={`#collapse${g_index}`} aria-expanded="true"
|
||||||
|
aria-controls={`collapse${g_index}`}>
|
||||||
|
{group.name}
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id={`collapse${g_index}`} className="accordion-collapse collapse" aria-labelledby="headingOne"
|
||||||
|
data-bs-parent="#accordion_categories">
|
||||||
|
<div className="accordion-body">
|
||||||
|
{group.items.map(item => {
|
||||||
|
item_index++;
|
||||||
|
return (
|
||||||
|
<ProductItem
|
||||||
|
key={item.id}
|
||||||
|
id={uuidv4()}
|
||||||
|
index={item_index}
|
||||||
|
name={`${item.name_number} ${item.name}`}
|
||||||
|
name_codename={item.name_codename}
|
||||||
|
price={item.price}
|
||||||
|
currency={currency}
|
||||||
|
image={`/images/${item.image}`}
|
||||||
|
specs={item.specs}
|
||||||
|
datasheet_file={item.datasheet_file}
|
||||||
|
datasheet_name={item.datasheet_name}
|
||||||
|
onClickAddItem={onClickAddItem}
|
||||||
|
></ProductItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Droppable
|
||||||
|
droppableId={data.id}
|
||||||
|
isDropDisabled={true}>
|
||||||
|
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
className="backlog-container"
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.droppableProps}>
|
||||||
|
|
||||||
|
{isMobile ? (
|
||||||
|
<div className="mobileCloseMenu">
|
||||||
|
<button onClick={onClickToggleMobileSideMenu}>
|
||||||
|
<img src="/images/shop/icon-close-white.svg" alt="add"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="accordion accordion-flush" id="accordion_categories">
|
||||||
|
{groups}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{provided.placeholder && (
|
||||||
|
<div style={{display: 'none'}}>
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Droppable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
101
static/js/shop/Cart.jsx
Normal file
101
static/js/shop/Cart.jsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {Droppable} from "react-beautiful-dnd";
|
||||||
|
import {cartStyle, nbrOccupiedSlotsInCrate} from "./utils";
|
||||||
|
import {ProductCartItem} from "./ProductCartItem.jsx";
|
||||||
|
import {FakePlaceholder} from "./FakePlaceholder.jsx";
|
||||||
|
import {FillExtData} from "./options/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays a list of <ProductCartItem>
|
||||||
|
*/
|
||||||
|
export class Cart extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
isMobile: PropTypes.bool,
|
||||||
|
isTouch: PropTypes.bool,
|
||||||
|
nbrSlots: PropTypes.number,
|
||||||
|
itemHovered: PropTypes.string,
|
||||||
|
data: PropTypes.object.isRequired,
|
||||||
|
onToggleOverlayRemove: PropTypes.func,
|
||||||
|
onClickRemoveItem: PropTypes.func,
|
||||||
|
onCardUpdate: PropTypes.func,
|
||||||
|
onClickItem: PropTypes.func,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isMobile,
|
||||||
|
isTouch,
|
||||||
|
nbrSlots,
|
||||||
|
itemHovered,
|
||||||
|
data,
|
||||||
|
onToggleOverlayRemove,
|
||||||
|
onClickRemoveItem,
|
||||||
|
onClickItem,
|
||||||
|
onCardUpdate,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const nbrOccupied = nbrOccupiedSlotsInCrate(data.items);
|
||||||
|
|
||||||
|
const products = data.items.map((item, index) => {
|
||||||
|
let itemData;
|
||||||
|
let ext_data = FillExtData(data.itemsData, index);
|
||||||
|
if (data.itemsData && index in data.itemsData) {
|
||||||
|
itemData = data.itemsData[index];
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ProductCartItem
|
||||||
|
isMobile={isMobile}
|
||||||
|
isTouch={isTouch}
|
||||||
|
hovered={item.id === itemHovered}
|
||||||
|
key={item.id}
|
||||||
|
id={item.id}
|
||||||
|
index={index}
|
||||||
|
first={index === 0}
|
||||||
|
last={index === data.items.length - 1 && nbrOccupied >= nbrSlots}
|
||||||
|
data={itemData}
|
||||||
|
ext_data={ext_data}
|
||||||
|
onToggleOverlayRemove={onToggleOverlayRemove}
|
||||||
|
onClickRemoveItem={onClickRemoveItem}
|
||||||
|
onCardUpdate={onCardUpdate}
|
||||||
|
onClickItem={onClickItem}
|
||||||
|
model={item}>
|
||||||
|
</ProductCartItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Droppable droppableId={data.id} direction="horizontal">
|
||||||
|
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.droppableProps}
|
||||||
|
style={cartStyle(
|
||||||
|
provided.droppableProps.style,
|
||||||
|
snapshot,
|
||||||
|
)}
|
||||||
|
className="items-cart-list">
|
||||||
|
|
||||||
|
{products}
|
||||||
|
|
||||||
|
{provided.placeholder && (
|
||||||
|
<div style={{ display: 'none' }}>
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FakePlaceholder
|
||||||
|
nbrSlots={nbrSlots}
|
||||||
|
items={data.items}
|
||||||
|
isDraggingOver={snapshot.isDraggingOver} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Droppable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
44
static/js/shop/Crate.jsx
Normal file
44
static/js/shop/Crate.jsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React, {PureComponent} from 'react';
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays the main crate with reminder rules.
|
||||||
|
* It includes <Cart> and rules
|
||||||
|
*/
|
||||||
|
export class Crate extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
rules: PropTypes.array,
|
||||||
|
cart: PropTypes.element,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
rules,
|
||||||
|
cart,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="crate">
|
||||||
|
|
||||||
|
<div className="crate-products">
|
||||||
|
|
||||||
|
{cart}
|
||||||
|
|
||||||
|
{rules && rules.length > 0 && (
|
||||||
|
<div className="crate-info">
|
||||||
|
{rules.map((rule, index) => (
|
||||||
|
<p key={index} className="rule" style={{'color': rule.color ? rule.color : 'inherit'}}>
|
||||||
|
<img src={`/images${rule.icon}`} /> <i><strong>{rule.name}:</strong> {rule.message}</i>
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
48
static/js/shop/CrateMode.jsx
Normal file
48
static/js/shop/CrateMode.jsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React, {PureComponent} from 'react';
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays crate modes
|
||||||
|
*/
|
||||||
|
export class CrateMode extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
items: PropTypes.array.isRequired,
|
||||||
|
mode: PropTypes.string.isRequired,
|
||||||
|
onClickMode: PropTypes.func,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleOnClickMode = this.handleOnClickMode.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnClickMode(mode, e) {
|
||||||
|
if (this.props.onClickMode) {
|
||||||
|
this.props.onClickMode(mode);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
mode,
|
||||||
|
items,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="crate-mode">
|
||||||
|
{items.map(item => (
|
||||||
|
<a
|
||||||
|
key={item.id}
|
||||||
|
className={mode == item.id ? 'active' : ''}
|
||||||
|
onClick={this.handleOnClickMode.bind(this, item.id)}
|
||||||
|
href="#"
|
||||||
|
role="button">{item.name}</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
47
static/js/shop/FakePlaceholder.jsx
Normal file
47
static/js/shop/FakePlaceholder.jsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {nbrOccupiedSlotsInCrate} from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays a placeholder inside crate.
|
||||||
|
* Allows to display how it remains space for the current crate.
|
||||||
|
*/
|
||||||
|
export class FakePlaceholder extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
isDraggingOver: PropTypes.bool,
|
||||||
|
nbrSlots: PropTypes.number.isRequired,
|
||||||
|
items: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isDraggingOver,
|
||||||
|
nbrSlots,
|
||||||
|
items,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const fakePlaceholder = [];
|
||||||
|
const nbrOccupied = nbrOccupiedSlotsInCrate(items);
|
||||||
|
|
||||||
|
for (var i = (nbrSlots - nbrOccupied); i > 0; i--) {
|
||||||
|
fakePlaceholder.push(
|
||||||
|
<div key={i} style={{
|
||||||
|
display: isDraggingOver ? 'none' : 'block',
|
||||||
|
border: '1px dashed #ccc',
|
||||||
|
width: '45px',
|
||||||
|
marginBottom: '5px',
|
||||||
|
}}></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{fakePlaceholder}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
273
static/js/shop/Layout.jsx
Normal file
273
static/js/shop/Layout.jsx
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
import React, {PureComponent} from 'react';
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that provides a base layout (aside/main) for the page.
|
||||||
|
*/
|
||||||
|
export class Layout extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
aside: PropTypes.any,
|
||||||
|
main: PropTypes.any,
|
||||||
|
mobileSideMenuShouldOpen: PropTypes.bool,
|
||||||
|
isMobile: PropTypes.bool,
|
||||||
|
newCardJustAdded: PropTypes.bool,
|
||||||
|
onClickToggleMobileSideMenu: PropTypes.func,
|
||||||
|
onClickCloseRFQFeedback: PropTypes.func,
|
||||||
|
RFQBodyType: PropTypes.string,
|
||||||
|
RFQBodyOrder: PropTypes.string,
|
||||||
|
onClickLoadCustomConf: PropTypes.func,
|
||||||
|
items: PropTypes.object,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get defaultProps() {
|
||||||
|
return {
|
||||||
|
mobileSideMenuShouldOpen: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
customconf: '',
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleCustomConfig = this.handleCustomConfig.bind(this);
|
||||||
|
this.handleClickLoad = this.handleClickLoad.bind(this);
|
||||||
|
this.checkValidation = this.checkValidation.bind(this);
|
||||||
|
|
||||||
|
// retrieve list of available pn
|
||||||
|
const items_keys = Object.keys(props.items);
|
||||||
|
this.list_pn = items_keys.map(function (key) {
|
||||||
|
return props.items[key].name_number;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCustomConfig(e) {
|
||||||
|
const value = e.target.value;
|
||||||
|
|
||||||
|
this.checkValidation(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkValidation(conf) {
|
||||||
|
let conf_obj;
|
||||||
|
|
||||||
|
try {
|
||||||
|
conf_obj = JSON.parse(conf);
|
||||||
|
} catch (e) {
|
||||||
|
return this.setState({
|
||||||
|
...this.state,
|
||||||
|
customconf: conf,
|
||||||
|
customconf_ready: null,
|
||||||
|
error: 'invalid format',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!conf_obj) {
|
||||||
|
return this.setState({
|
||||||
|
...this.state,
|
||||||
|
customconf: conf,
|
||||||
|
customconf_ready: null,
|
||||||
|
error: 'invalid format',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!conf_obj.items || !conf_obj.type) &&
|
||||||
|
(Object.prototype.toString.call(conf_obj.items) !== '[object Array]' ||
|
||||||
|
Object.prototype.toString.call(conf_obj.type) !== '[object String]')) {
|
||||||
|
return this.setState({
|
||||||
|
...this.state,
|
||||||
|
customconf: conf,
|
||||||
|
customconf_ready: null,
|
||||||
|
error: 'invalid format',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conf_obj.type !== "desktop" && conf_obj.type !== "rack") {
|
||||||
|
return this.setState({
|
||||||
|
...this.state,
|
||||||
|
customconf: conf,
|
||||||
|
customconf_ready: null,
|
||||||
|
error: 'invalid format',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
conf_obj.items.map(function (item) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(item);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
conf_obj.items = conf_obj.items.filter(function (item) {
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (conf_obj.items.filter(function (item) {
|
||||||
|
return Object.prototype.toString.call(item) !== '[object Object]' || !item.pn || Object.prototype.toString.call(item.pn) !== '[object String]';
|
||||||
|
}).length > 0) {
|
||||||
|
return this.setState({
|
||||||
|
...this.state,
|
||||||
|
customconf: conf,
|
||||||
|
customconf_ready: null,
|
||||||
|
error: 'invalid format',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
conf_obj.items = conf_obj.items.map(function (item) {
|
||||||
|
return {
|
||||||
|
pn: item.pn,
|
||||||
|
options: item.options ? item.options : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
const unknow_pn = conf_obj.items.filter(function (item_pn) {
|
||||||
|
return self.list_pn.includes(item_pn.pn) === false;
|
||||||
|
}).map(function (item_pn) {
|
||||||
|
return item_pn.pn;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unknow_pn.length > 0) {
|
||||||
|
return this.setState({
|
||||||
|
...this.state,
|
||||||
|
customconf: conf,
|
||||||
|
customconf_ready: null,
|
||||||
|
error: `${unknow_pn.join(', ')} unknown${unknow_pn.length > 1 ? 's':''} pn number`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
customconf: conf,
|
||||||
|
error: null,
|
||||||
|
customconf_ready: conf_obj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickLoad() {
|
||||||
|
this.checkValidation(this.state.customconf);
|
||||||
|
|
||||||
|
if (this.props.onClickLoadCustomConf) {
|
||||||
|
this.props.onClickLoadCustomConf(this.state.customconf_ready);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
aside,
|
||||||
|
main,
|
||||||
|
mobileSideMenuShouldOpen,
|
||||||
|
isMobile,
|
||||||
|
newCardJustAdded,
|
||||||
|
onClickToggleMobileSideMenu,
|
||||||
|
onClickCloseRFQFeedback,
|
||||||
|
showRFQFeedback,
|
||||||
|
RFQBodyType,
|
||||||
|
RFQBodyOrder,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="layout">
|
||||||
|
|
||||||
|
<aside className={'aside ' + (mobileSideMenuShouldOpen ? 'menu-opened' : '')}>{aside}</aside>
|
||||||
|
|
||||||
|
{mobileSideMenuShouldOpen ? (
|
||||||
|
<section className="main" onClick={onClickToggleMobileSideMenu}>{main}</section>
|
||||||
|
) : (
|
||||||
|
<section className="main">{main}</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isMobile && newCardJustAdded ? (
|
||||||
|
<div className="feedback-add-success">
|
||||||
|
✓ added
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
|
||||||
|
<div className={`modal fade ${ showRFQFeedback ? 'show': ''}`} style={{'display': showRFQFeedback ? 'block':'none'}} id="exampleModal" tabIndex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||||
|
<div className="modal-dialog" role="document">
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-body rfqFeedback">
|
||||||
|
|
||||||
|
<div className="d-flex w-100">
|
||||||
|
|
||||||
|
{RFQBodyType === 'email' ? (
|
||||||
|
<div className="d-flex">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img width="30px" src="/images/shop/icon-done.svg" alt="close" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{'padding': '0 .5em'}}>
|
||||||
|
We've received your request and will be in contact soon.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
|
||||||
|
{RFQBodyType === 'show' ? (
|
||||||
|
<p>
|
||||||
|
{RFQBodyOrder}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{RFQBodyType === 'import' ? (
|
||||||
|
<div className="w-100">
|
||||||
|
|
||||||
|
<form className="w-100">
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="small">
|
||||||
|
Input the JSON description below. Should be something like:
|
||||||
|
<br />
|
||||||
|
{JSON.stringify({"items":[{"pn":"1124"},{"pn":"2118"},{"pn":"2118"},{"pn":"2128"}],"type":"desktop"})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 w-100">
|
||||||
|
<textarea
|
||||||
|
onChange={this.handleCustomConfig}
|
||||||
|
value={this.state.customconf}
|
||||||
|
className="form-control w-100"
|
||||||
|
rows="5"
|
||||||
|
placeholder="Input JSON description here." />
|
||||||
|
</div>
|
||||||
|
{this.state.error ? (
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-danger">{this.state.error}</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="d-flex flex-column flex-sm-row justify-content-end">
|
||||||
|
<a type="button" onClick={onClickCloseRFQFeedback} className="btn btn-sm btn-outline-primary m-0 mb-2 mb-sm-0 me-sm-2">Close</a>
|
||||||
|
<a type="button" onClick={this.handleClickLoad} className={`btn btn-sm btn-primary m-0 ms-sm-2 ${this.state.error ? 'disabled':''}`}>Load configuration</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button onClick={onClickCloseRFQFeedback}>
|
||||||
|
<img src="/images/shop/icon-close.svg" alt="close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div onClick={onClickCloseRFQFeedback} className={`modal-backdrop fade ${ showRFQFeedback ? 'show': ''}`} style={{'display': showRFQFeedback ? 'initial':'none'}}></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
220
static/js/shop/OrderForm.jsx
Normal file
220
static/js/shop/OrderForm.jsx
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Components that renders the form to request quote.
|
||||||
|
*/
|
||||||
|
export class OrderForm extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
isProcessing: PropTypes.bool,
|
||||||
|
isProcessingComplete: PropTypes.bool,
|
||||||
|
onClickSubmit: PropTypes.func,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
note: '',
|
||||||
|
email: '',
|
||||||
|
error: {
|
||||||
|
note: null,
|
||||||
|
email: null,
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
note: null,
|
||||||
|
email: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleEmail = this.handleEmail.bind(this);
|
||||||
|
this.handleNote = this.handleNote.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.resetEmptyError = this.resetEmptyError.bind(this);
|
||||||
|
this.checkValidation = this.checkValidation.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkValidation() {
|
||||||
|
let isValid = true;
|
||||||
|
let validationFields = {...this.state};
|
||||||
|
|
||||||
|
const {
|
||||||
|
isEmpty: isEmailEmpty,
|
||||||
|
isError: isEmailError
|
||||||
|
} = this.validateEmail(this.state.email);
|
||||||
|
|
||||||
|
validationFields = {
|
||||||
|
...validationFields,
|
||||||
|
error: {
|
||||||
|
...this.state.error,
|
||||||
|
email: isEmailError,
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
...this.state.empty,
|
||||||
|
email: isEmailEmpty,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(validationFields);
|
||||||
|
|
||||||
|
isValid =
|
||||||
|
!isEmailEmpty &&
|
||||||
|
!isEmailError
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateEmail(value) {
|
||||||
|
let isEmpty = null;
|
||||||
|
let isError = null;
|
||||||
|
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
isEmpty = true;
|
||||||
|
} else if (value && !value.match(/^\w+([\+\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/)) {
|
||||||
|
isError = {
|
||||||
|
message: 'Your email is incomplete',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isEmpty, isError };
|
||||||
|
}
|
||||||
|
|
||||||
|
validateNote(value) {
|
||||||
|
let isEmpty = null;
|
||||||
|
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
isEmpty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isEmpty };
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEmptyError(key) {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
error: {
|
||||||
|
...this.state.error,
|
||||||
|
[key]: null,
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
...this.state.empty,
|
||||||
|
[key]: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEmail(e) {
|
||||||
|
const value = e.target.value;
|
||||||
|
const { isEmpty, isError } = this.validateEmail(value);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
email: value,
|
||||||
|
error: {
|
||||||
|
...this.state.error,
|
||||||
|
email: isError,
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
...this.state.empty,
|
||||||
|
email: isEmpty,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNote(e) {
|
||||||
|
const value = e.target.value;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
note: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (this.props.onClickSubmit) {
|
||||||
|
// check validation input fields
|
||||||
|
const isValidated = this.checkValidation();
|
||||||
|
if (!isValidated) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onClickSubmit(this.state.note, this.state.email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
handleEmail,
|
||||||
|
handleNote,
|
||||||
|
resetEmptyError,
|
||||||
|
handleSubmit,
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
const {
|
||||||
|
onClickShow,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
note,
|
||||||
|
error,
|
||||||
|
empty
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const { isProcessing, isProcessingComplete } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="summary-form">
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className={`${error && error.email ? 'errorField':''}`}
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
onFocus={() => resetEmptyError('email')}
|
||||||
|
onChange={handleEmail}
|
||||||
|
onBlur={handleEmail}
|
||||||
|
value={email} />
|
||||||
|
|
||||||
|
{ empty && empty.email ? (
|
||||||
|
<div className="error">
|
||||||
|
<small>Required</small>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{ error && error.email ? (
|
||||||
|
<div className="error">
|
||||||
|
<small>Your email is incomplete</small>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
onChange={handleNote}
|
||||||
|
defaultValue={note}
|
||||||
|
rows="5"
|
||||||
|
placeholder="Additional notes" />
|
||||||
|
|
||||||
|
<div className="d-flex flex-column flex-sm-row justify-content-between">
|
||||||
|
<input
|
||||||
|
className="btn btn-outline-primary w-100 m-0 mb-2 mb-sm-0 me-sm-2"
|
||||||
|
style={{'cursor': 'pointer', 'fontWeight': '700'}}
|
||||||
|
defaultValue="Show JSON"
|
||||||
|
onClick={onClickShow}
|
||||||
|
readOnly={true} />
|
||||||
|
|
||||||
|
<input className="btn btn-primary w-100 m-0 ms-sm-2" type="submit" value={`${isProcessing ? 'Processing ...' : 'Request quote'}`} />
|
||||||
|
</div>
|
||||||
|
{/*This will open an email window. Send the email to make your request.*/}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
75
static/js/shop/OrderPanel.jsx
Normal file
75
static/js/shop/OrderPanel.jsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders all things for order.
|
||||||
|
* It acts like-a layout, this component do nothing more.
|
||||||
|
*/
|
||||||
|
export class OrderPanel extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
title: PropTypes.string,
|
||||||
|
description: PropTypes.element,
|
||||||
|
crateMode: PropTypes.element,
|
||||||
|
crate: PropTypes.element,
|
||||||
|
summaryPrice: PropTypes.element,
|
||||||
|
form: PropTypes.element,
|
||||||
|
isMobile: PropTypes.bool,
|
||||||
|
onClickToggleMobileSideMenu: PropTypes.func,
|
||||||
|
onClickOpenImport: PropTypes.func,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
crateMode,
|
||||||
|
crate,
|
||||||
|
summaryPrice,
|
||||||
|
form,
|
||||||
|
isMobile,
|
||||||
|
onClickToggleMobileSideMenu,
|
||||||
|
onClickOpenImport,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="panel">
|
||||||
|
|
||||||
|
<h2>{title}</h2>
|
||||||
|
|
||||||
|
<div className="control">
|
||||||
|
{description}
|
||||||
|
|
||||||
|
{crateMode}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline-primary m-0 mb-2"
|
||||||
|
style={{'cursor': 'pointer'}}
|
||||||
|
onClick={onClickOpenImport}>Import JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isMobile ? (
|
||||||
|
<div className="mobileBtnDisplaySideMenu">
|
||||||
|
<button onClick={onClickToggleMobileSideMenu}>
|
||||||
|
<img src="/images/shop/icon-add.svg" alt="add"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{crate}
|
||||||
|
|
||||||
|
<section className="summary">
|
||||||
|
{summaryPrice}
|
||||||
|
|
||||||
|
{form}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
216
static/js/shop/OrderSummary.jsx
Normal file
216
static/js/shop/OrderSummary.jsx
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import React, {PureComponent} from 'react';
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {SummaryPopup} from "./options/SummaryPopup.jsx";
|
||||||
|
import {formatMoney} from "./utils";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Components that displays the list of card that are used in the crate.
|
||||||
|
* It is a summary of purchase
|
||||||
|
*/
|
||||||
|
export class OrderSummary extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
currency: PropTypes.string,
|
||||||
|
modes: PropTypes.array,
|
||||||
|
currentMode: PropTypes.string,
|
||||||
|
summary: PropTypes.array,
|
||||||
|
itemsData: PropTypes.array,
|
||||||
|
onDeleteItem: PropTypes.func,
|
||||||
|
onDeleteAllItems: PropTypes.func,
|
||||||
|
onMouseEnterItem: PropTypes.func,
|
||||||
|
onMouseLeaveItem: PropTypes.func,
|
||||||
|
onClickSelectItem: PropTypes.func,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleOnDeleteItem = this.handleOnDeleteItem.bind(this);
|
||||||
|
this.handleOnDeleteAllItems = this.handleOnDeleteAllItems.bind(this);
|
||||||
|
this.handleOnMouseEnterItem = this.handleOnMouseEnterItem.bind(this);
|
||||||
|
this.handleOnMouseLeaveItem = this.handleOnMouseLeaveItem.bind(this);
|
||||||
|
this.handleOnClickSelectItem = this.handleOnClickSelectItem.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnDeleteItem(index, e) {
|
||||||
|
if (this.props.onDeleteItem) {
|
||||||
|
this.props.onDeleteItem(index);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnDeleteAllItems(e) {
|
||||||
|
if (this.props.onDeleteAllItems) {
|
||||||
|
this.props.onDeleteAllItems();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnMouseEnterItem(id, e) {
|
||||||
|
if (this.props.onMouseEnterItem) {
|
||||||
|
this.props.onMouseEnterItem(id);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnMouseLeaveItem(e) {
|
||||||
|
if (this.props.onMouseLeaveItem) {
|
||||||
|
this.props.onMouseLeaveItem();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnClickSelectItem(index, e) {
|
||||||
|
if (e.target.tagName !== 'IMG') {
|
||||||
|
if (this.props.onClickSelectItem) {
|
||||||
|
this.props.onClickSelectItem(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
currency,
|
||||||
|
modes,
|
||||||
|
currentMode,
|
||||||
|
summary,
|
||||||
|
itemsData,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const mode = modes.find(elem => elem.id === currentMode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="summary-price">
|
||||||
|
|
||||||
|
<table>
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td colSpan="2" className="summary-remove-all">
|
||||||
|
<span className="item-card-name">Remove all cards</span>
|
||||||
|
|
||||||
|
<button onClick={this.handleOnDeleteAllItems}>
|
||||||
|
<img src="/images/shop/icon-remove.svg" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{mode && (
|
||||||
|
<tr>
|
||||||
|
<td className="item-card-name">{mode.name}</td>
|
||||||
|
<td className="price">
|
||||||
|
<div>
|
||||||
|
{`${currency} ${formatMoney(mode.price)}`}
|
||||||
|
|
||||||
|
<button style={{'opacity': '0', 'cursor': 'initial'}}>
|
||||||
|
<img src="/images/shop/icon-remove.svg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span style={{
|
||||||
|
'display': 'inline-block',
|
||||||
|
'width': '30px',
|
||||||
|
}}> </span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{summary.map((item, index) => {
|
||||||
|
let alert, warning, options, options_data;
|
||||||
|
|
||||||
|
if (itemsData[index] && itemsData[index].warnings) {
|
||||||
|
alert = itemsData[index];
|
||||||
|
const warningsKeys = Object.keys(alert.warnings);
|
||||||
|
if (warningsKeys && warningsKeys.length > 0) {
|
||||||
|
warning = alert.warnings[warningsKeys[0]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options = itemsData[index] && itemsData[index].options;
|
||||||
|
options_data = itemsData[index] && itemsData[index].options_data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={item.id}
|
||||||
|
className={`hoverable ${item.selected ? 'selected' : ''}`}
|
||||||
|
onClick={this.handleOnClickSelectItem.bind(this, index)}
|
||||||
|
onMouseEnter={this.handleOnMouseEnterItem.bind(this, item.id)}
|
||||||
|
onMouseLeave={this.handleOnMouseLeaveItem}>
|
||||||
|
|
||||||
|
<td className="item-card-name">
|
||||||
|
<div>{`${item.name_number} ${item.name} ${item.name_codename}`}</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="price">
|
||||||
|
<div className="d-inline-flex align-content-center">
|
||||||
|
{`${currency} ${formatMoney(item.price)}`}
|
||||||
|
|
||||||
|
<button onClick={this.handleOnDeleteItem.bind(this, index)}>
|
||||||
|
<img src="/images/shop/icon-remove.svg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style={{'width': '45px', 'height': '20px'}} className="d-inline-flex align-content-center align-self-center justify-content-evenly">
|
||||||
|
{(warning ? (
|
||||||
|
<img
|
||||||
|
className="alert-warning align-self-start"
|
||||||
|
src={`/images/${warning.icon}`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span style={{
|
||||||
|
'display': 'inline-block',
|
||||||
|
'width': '20px',
|
||||||
|
}}> </span>
|
||||||
|
))}
|
||||||
|
{((options && options_data) ? (
|
||||||
|
<SummaryPopup id={item.id + "options"} options={options} data={options_data} />
|
||||||
|
) : (
|
||||||
|
<span style={{
|
||||||
|
'display': 'inline-block',
|
||||||
|
'width': '20px',
|
||||||
|
}}> </span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td className="item-card-name">Price estimate</td>
|
||||||
|
<td className="price">
|
||||||
|
<div>
|
||||||
|
{summary.length ? (
|
||||||
|
`${currency} ${formatMoney(summary.reduce(
|
||||||
|
(prev, next) => {
|
||||||
|
return prev + next.price;
|
||||||
|
}, 0
|
||||||
|
) + mode.price)}`
|
||||||
|
) : (
|
||||||
|
`${currency} ${formatMoney(mode.price)}`
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button style={{'opacity': '0', 'cursor': 'initial'}}>
|
||||||
|
<img src="/images/shop/icon-remove.svg" alt="icon remove"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span style={{
|
||||||
|
'display': 'inline-block',
|
||||||
|
'width': '30px',
|
||||||
|
}}> </span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
252
static/js/shop/ProductCartItem.jsx
Normal file
252
static/js/shop/ProductCartItem.jsx
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {Draggable} from "react-beautiful-dnd";
|
||||||
|
import {OverlayTrigger} from "react-bootstrap";
|
||||||
|
import {DialogPopup} from "./options/DialogPopup.jsx";
|
||||||
|
import {nbrClocksStyle, nbrConnectorsStyle, productStyle} from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders a product.
|
||||||
|
* Used in the crate
|
||||||
|
*/
|
||||||
|
export class ProductCartItem extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
isMobile: PropTypes.bool,
|
||||||
|
isTouch: PropTypes.bool,
|
||||||
|
hovered: PropTypes.bool,
|
||||||
|
first: PropTypes.bool,
|
||||||
|
last: PropTypes.bool,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
model: PropTypes.object.isRequired,
|
||||||
|
data: PropTypes.object,
|
||||||
|
ext_data: PropTypes.object,
|
||||||
|
onToggleOverlayRemove: PropTypes.func,
|
||||||
|
onClickRemoveItem: PropTypes.func,
|
||||||
|
onClickItem: PropTypes.func,
|
||||||
|
onCardUpdate: PropTypes.func,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get defaultProps() {
|
||||||
|
return {
|
||||||
|
hovered: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleOnMouseEnterRemoveItem = this.handleOnMouseEnterRemoveItem.bind(this);
|
||||||
|
this.handleOnMouseLeaveRemoveItem = this.handleOnMouseLeaveRemoveItem.bind(this);
|
||||||
|
this.handleOnClickRemoveItem = this.handleOnClickRemoveItem.bind(this);
|
||||||
|
this.handleOnClickItem = this.handleOnClickItem.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnMouseEnterRemoveItem(index, e) {
|
||||||
|
if (this.props.onToggleOverlayRemove && !this.props.isMobile) {
|
||||||
|
this.props.onToggleOverlayRemove(index, true);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnMouseLeaveRemoveItem(index, e) {
|
||||||
|
if (this.props.onToggleOverlayRemove && !this.props.isMobile) {
|
||||||
|
this.props.onToggleOverlayRemove(index, false);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnClickItem(index, e) {
|
||||||
|
if (this.props.onClickItem && this.props.isTouch) {
|
||||||
|
this.props.onClickItem(index);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnClickRemoveItem(index, e) {
|
||||||
|
if (this.props.onClickRemoveItem) {
|
||||||
|
this.props.onClickRemoveItem(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
hovered,
|
||||||
|
model,
|
||||||
|
data,
|
||||||
|
index,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
ext_data,
|
||||||
|
onCardUpdate,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let warning, options, options_data;
|
||||||
|
if (data && data.warnings) {
|
||||||
|
const warningsKeys = Object.keys(data.warnings);
|
||||||
|
if (warningsKeys && warningsKeys.length > 0) {
|
||||||
|
// we display only the first warning
|
||||||
|
warning = data.warnings[warningsKeys[0]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (data && data.options) {
|
||||||
|
options = data.options;
|
||||||
|
if (!data.options_data) data.options_data = {};
|
||||||
|
options_data = data.options_data;
|
||||||
|
options_data.ext_data = ext_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
let render_progress;
|
||||||
|
if (data) {
|
||||||
|
switch(model.type) {
|
||||||
|
case 'kasli':
|
||||||
|
render_progress = [
|
||||||
|
(<p key={model.type+model.id+"EEM"}>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors used`}</p>),
|
||||||
|
(<p key={model.type+model.id+"CLK"}>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors used`}</p>)
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'vhdcicarrier':
|
||||||
|
render_progress = (<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} EEM connectors used`}</p>);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'zotino':
|
||||||
|
case 'hd68':
|
||||||
|
render_progress = (<p>{`${data.nbrCurrentSlot}/${model.nbrSlotMax} connectors used`}</p>);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'clocker':
|
||||||
|
render_progress = (<p>{`${data.nbrCurrentClock}/${model.nbrClockMax} Clock connectors used`}</p>);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable draggableId={model.id} index={index}>
|
||||||
|
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={{ ...productStyle(
|
||||||
|
provided.draggableProps.style,
|
||||||
|
snapshot,
|
||||||
|
true,
|
||||||
|
!!hovered,
|
||||||
|
!!model.selected,
|
||||||
|
true
|
||||||
|
)}}
|
||||||
|
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
|
||||||
|
onMouseLeave={this.handleOnMouseLeaveRemoveItem.bind(this, index)}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* warning container */}
|
||||||
|
|
||||||
|
<div className="progress-container warning d-flex justify-content-evenly">
|
||||||
|
{warning &&
|
||||||
|
(<OverlayTrigger
|
||||||
|
placement="bottom"
|
||||||
|
trigger={['click', 'hover', 'focus']}
|
||||||
|
overlay={
|
||||||
|
({arrowProps, hasDoneInitialMeasure, show, ...props}) => (
|
||||||
|
<div className="k-popup-warning" {...props}>
|
||||||
|
<p className="rule warning">
|
||||||
|
<i>{warning.message}</i>
|
||||||
|
</p>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
rootClose
|
||||||
|
>
|
||||||
|
<img className="alert-warning" src={`/images${warning.icon}`}/>
|
||||||
|
</OverlayTrigger>)
|
||||||
|
}
|
||||||
|
|
||||||
|
{options && (
|
||||||
|
<DialogPopup
|
||||||
|
options={options}
|
||||||
|
data={options_data}
|
||||||
|
options_class={model.options_class}
|
||||||
|
key={"popover" + index}
|
||||||
|
id={"popover" + index}
|
||||||
|
big={model.size === "big"}
|
||||||
|
first={first}
|
||||||
|
last={last}
|
||||||
|
target={{
|
||||||
|
construct: ((outvar, value) => {
|
||||||
|
// console.log("construct", outvar, value, options_data);
|
||||||
|
options_data[outvar] = value;
|
||||||
|
}),
|
||||||
|
update: ((outvar, value) => {
|
||||||
|
// console.log("update", outvar, value, options_data);
|
||||||
|
if (outvar in options_data) options_data[outvar] = value;
|
||||||
|
onCardUpdate();
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6>{model.name_number}</h6>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onMouseEnter={this.handleOnMouseEnterRemoveItem.bind(this, index)}
|
||||||
|
onClick={this.handleOnClickItem.bind(this, index)}
|
||||||
|
>
|
||||||
|
|
||||||
|
<img
|
||||||
|
className='item-cart'
|
||||||
|
src={`/images${model.image}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* remove container */}
|
||||||
|
<div
|
||||||
|
style={{'display': model.showOverlayRemove ? 'flex':'none'}}
|
||||||
|
className="overlayRemove"
|
||||||
|
onClick={this.handleOnClickRemoveItem.bind(this, index)}>
|
||||||
|
|
||||||
|
<img src="/images/shop/icon-remove.svg" alt="rm"/>
|
||||||
|
|
||||||
|
<p>Remove</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* progression container */}
|
||||||
|
{render_progress && (
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="top"
|
||||||
|
trigger={['click', 'hover', 'focus']}
|
||||||
|
overlay={({arrowProps, hasDoneInitialMeasure, show, ...props}) => (<div className="k-popup-connectors" {...props}>{render_progress}</div>)}
|
||||||
|
rootClose
|
||||||
|
>
|
||||||
|
<div className="progress-container">
|
||||||
|
|
||||||
|
{model.nbrSlotMax > 0 && (
|
||||||
|
<div className="nbr-connectors">
|
||||||
|
<div style={{ ...nbrConnectorsStyle(data)}}></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{model.nbrClockMax > 0 && (
|
||||||
|
<div className="nbr-clocks">
|
||||||
|
<div style={{ ...nbrClocksStyle(data)}}></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</OverlayTrigger>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
120
static/js/shop/ProductItem.jsx
Normal file
120
static/js/shop/ProductItem.jsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, {PureComponent} from 'react';
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {Draggable} from "react-beautiful-dnd";
|
||||||
|
import {formatMoney, productStyle} from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders a product.
|
||||||
|
* Used in the aside (e.g backlog of product)
|
||||||
|
*/
|
||||||
|
export class ProductItem extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
name_codename: PropTypes.string,
|
||||||
|
price: PropTypes.number.isRequired,
|
||||||
|
currency: PropTypes.string.isRequired,
|
||||||
|
image: PropTypes.string.isRequired,
|
||||||
|
specs: PropTypes.array,
|
||||||
|
datasheet_file: PropTypes.string,
|
||||||
|
datasheet_name: PropTypes.string,
|
||||||
|
onClickAddItem: PropTypes.func,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleOnClickAddItem = this.handleOnClickAddItem.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnClickAddItem(id, tap, e) {
|
||||||
|
if (this.props.onClickAddItem) {
|
||||||
|
this.props.onClickAddItem(id, tap);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
name,
|
||||||
|
name_codename,
|
||||||
|
price,
|
||||||
|
currency,
|
||||||
|
image,
|
||||||
|
specs,
|
||||||
|
datasheet_file,
|
||||||
|
datasheet_name,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const render_specs = (specs && specs.length > 0 && (
|
||||||
|
<ul>
|
||||||
|
{specs.map((spec, index) =>
|
||||||
|
<li key={index}>{spec}</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
));
|
||||||
|
|
||||||
|
const render_datasheet_link = (datasheet_file && datasheet_name && (
|
||||||
|
<div className="ds">
|
||||||
|
<span className='doc-icon'></span>
|
||||||
|
<a href={datasheet_file} target="_blank" rel="noopener noreferrer">
|
||||||
|
{datasheet_name}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="productItem">
|
||||||
|
|
||||||
|
<div className="content">
|
||||||
|
<h3 style={{ 'marginBottom': name_codename ? '5px' : '20px'}}>{name}</h3>
|
||||||
|
{name_codename ? (
|
||||||
|
<p>{name_codename}</p>
|
||||||
|
) : null }
|
||||||
|
|
||||||
|
<div className="price">{`${currency} ${formatMoney(price)}`}</div>
|
||||||
|
|
||||||
|
{render_specs}
|
||||||
|
|
||||||
|
{render_datasheet_link}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content">
|
||||||
|
|
||||||
|
<button onClick={this.handleOnClickAddItem.bind(this, index, true)}>
|
||||||
|
<img src="/images/shop/icon-add.svg" alt="add" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Draggable draggableId={id} index={index}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<img
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={productStyle(
|
||||||
|
provided.draggableProps.style,
|
||||||
|
snapshot,
|
||||||
|
true, // hack: remove weird animation after a drop
|
||||||
|
)}
|
||||||
|
src={image} />
|
||||||
|
|
||||||
|
{/* Allows to simulate a clone */}
|
||||||
|
{snapshot.isDragging && (
|
||||||
|
<img className="simclone" src={image} />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
886
static/js/shop/Shop.jsx
Normal file
886
static/js/shop/Shop.jsx
Normal file
@ -0,0 +1,886 @@
|
|||||||
|
import React, {PureComponent} from 'react';
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {FilterOptions} from "./options/utils";
|
||||||
|
import {v4 as uuidv4} from "uuid";
|
||||||
|
import {DragDropContext} from "react-beautiful-dnd";
|
||||||
|
|
||||||
|
import {copy, itemsUnfoldedList, nbrOccupiedSlotsInCrate, remove, reorder} from "./utils";
|
||||||
|
|
||||||
|
import {Layout} from "./Layout.jsx";
|
||||||
|
import {Backlog} from "./Backlog.jsx";
|
||||||
|
import {OrderPanel} from "./OrderPanel.jsx";
|
||||||
|
import {CrateMode} from "./CrateMode.jsx";
|
||||||
|
import {Crate} from "./Crate.jsx";
|
||||||
|
import {Cart} from "./Cart.jsx";
|
||||||
|
import {OrderSummary} from "./OrderSummary.jsx";
|
||||||
|
import {OrderForm} from "./OrderForm.jsx";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that render the entire shop
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Shop extends PureComponent {
|
||||||
|
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
data: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = this.props.data;
|
||||||
|
this.handleCrateModeChange = this.handleCrateModeChange.bind(this);
|
||||||
|
this.handleOnDragEnd = this.handleOnDragEnd.bind(this);
|
||||||
|
this.handleDeleteItem = this.handleDeleteItem.bind(this);
|
||||||
|
this.handleDeleteAllItems = this.handleDeleteAllItems.bind(this);
|
||||||
|
this.handleMouseEnterItem = this.handleMouseEnterItem.bind(this);
|
||||||
|
this.handleMouseLeaveItem = this.handleMouseLeaveItem.bind(this);
|
||||||
|
this.handleClickAddItem = this.handleClickAddItem.bind(this);
|
||||||
|
this.checkAlerts = this.checkAlerts.bind(this);
|
||||||
|
this.handleClickSelectItem = this.handleClickSelectItem.bind(this);
|
||||||
|
this.handleClickSubmit = this.handleClickSubmit.bind(this);
|
||||||
|
this.handleToggleOverlayRemove = this.handleToggleOverlayRemove.bind(this);
|
||||||
|
this.handleShowOverlayRemove = this.handleShowOverlayRemove.bind(this);
|
||||||
|
this.handleClickToggleMobileSideMenu = this.handleClickToggleMobileSideMenu.bind(this);
|
||||||
|
this.handleClickCloseRFQFeedback = this.handleClickCloseRFQFeedback.bind(this);
|
||||||
|
this.handleClickShowOrder = this.handleClickShowOrder.bind(this);
|
||||||
|
this.handleClickOpenImport = this.handleClickOpenImport.bind(this);
|
||||||
|
this.handleLoadCustomConf = this.handleLoadCustomConf.bind(this);
|
||||||
|
this.handleCardsUpdated = this.handleCardsUpdated.bind(this);
|
||||||
|
|
||||||
|
this.timer = null;
|
||||||
|
this.timer_remove = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const source = {
|
||||||
|
droppableId: 'backlog',
|
||||||
|
indexes: [
|
||||||
|
itemsUnfoldedList.findIndex(element => element === "eem_pwr_mod"),
|
||||||
|
itemsUnfoldedList.findIndex(element => element === "kasli")
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const destination = {
|
||||||
|
droppableId: 'cart',
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleOnDragEnd({
|
||||||
|
source,
|
||||||
|
destination
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
/**
|
||||||
|
* We check alerts (reminder + warning) only when items inside crate or
|
||||||
|
* crate mode change.
|
||||||
|
*
|
||||||
|
* In the function checkAlerts, we DO NOT want to change items as we will
|
||||||
|
* trigger again this function (componentDidUpdate) and thus,
|
||||||
|
* making an infinite loop.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
(prevState.columns.cart.items !== this.state.columns.cart.items) ||
|
||||||
|
(prevState.currentMode !== this.state.currentMode)
|
||||||
|
) {
|
||||||
|
this.checkAlerts(this.state.columns.cart.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.newCardJustAdded) {
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.setState({
|
||||||
|
newCardJustAdded: false,
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCardsUpdated() {
|
||||||
|
this.checkAlerts(this.state.columns.cart.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCrateModeChange(mode) {
|
||||||
|
this.setState({
|
||||||
|
currentMode: mode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteItem(index) {
|
||||||
|
let cloned = Array.from(this.state.columns.cart.items);
|
||||||
|
let cloned_data = Array.from(this.state.columns.cart.itemsData);
|
||||||
|
cloned.splice(index, 1);
|
||||||
|
cloned_data.splice(index, 1);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
columns: {
|
||||||
|
...this.state.columns,
|
||||||
|
cart: {
|
||||||
|
...this.state.columns.cart,
|
||||||
|
items: cloned,
|
||||||
|
itemsData: cloned_data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteAllItems() {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
columns: {
|
||||||
|
...this.state.columns,
|
||||||
|
cart: {
|
||||||
|
...this.state.columns.cart,
|
||||||
|
items: [],
|
||||||
|
itemsData: []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseEnterItem(id) {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
currentItemHovered: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseLeaveItem() {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
currentItemHovered: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickAddItem(index, tap) {
|
||||||
|
const source = {
|
||||||
|
droppableId: 'backlog',
|
||||||
|
index: index,
|
||||||
|
};
|
||||||
|
const destination = {
|
||||||
|
droppableId: 'cart',
|
||||||
|
index: this.state.columns.cart.items.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleOnDragEnd({
|
||||||
|
source,
|
||||||
|
destination
|
||||||
|
}, tap);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickSelectItem(index) {
|
||||||
|
const itemsCloned = Array.from(this.state.columns.cart.items);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
columns: {
|
||||||
|
...this.state.columns,
|
||||||
|
cart: {
|
||||||
|
...this.state.columns.cart,
|
||||||
|
items: itemsCloned.map((item, id) => {
|
||||||
|
return {...item, selected: id === index ? true : false};
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleOverlayRemove(index, show) {
|
||||||
|
const itemsCloned = Array.from(this.state.columns.cart.items);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
columns: {
|
||||||
|
...this.state.columns,
|
||||||
|
cart: {
|
||||||
|
...this.state.columns.cart,
|
||||||
|
items: itemsCloned.map((item, id) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
showOverlayRemove: id === index ? show : false
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShowOverlayRemove(index) {
|
||||||
|
if (this.timer_remove)
|
||||||
|
clearTimeout(this.timer_remove);
|
||||||
|
|
||||||
|
this.handleToggleOverlayRemove(index, true);
|
||||||
|
|
||||||
|
this.timer_remove = setTimeout(() => {
|
||||||
|
this.handleToggleOverlayRemove(index, false);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickShowOrder() {
|
||||||
|
const crate = {
|
||||||
|
items: [],
|
||||||
|
type: this.state.currentMode,
|
||||||
|
};
|
||||||
|
const clonedCart = Array.from(this.state.columns.cart.items);
|
||||||
|
const clonedCartData = Array.from(this.state.columns.cart.itemsData);
|
||||||
|
for (const i in clonedCart) {
|
||||||
|
const item = clonedCart[i];
|
||||||
|
const item_data = clonedCartData[i];
|
||||||
|
crate.items.push({
|
||||||
|
'pn': item.name_number,
|
||||||
|
'options': (item_data.options_data && item_data.options) ? FilterOptions(item_data.options, item_data.options_data) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isProcessing: false,
|
||||||
|
shouldShowRFQFeedback: true,
|
||||||
|
RFQBodyType: 'show',
|
||||||
|
RFQBodyOrder: JSON.stringify(crate, null, 2),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickOpenImport() {
|
||||||
|
this.setState({
|
||||||
|
isProcessing: false,
|
||||||
|
shouldShowRFQFeedback: true,
|
||||||
|
RFQBodyType: 'import',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadCustomConf(customconf) {
|
||||||
|
if (!customconf) {return; }
|
||||||
|
|
||||||
|
const items = this.props.data.items;
|
||||||
|
|
||||||
|
let new_items = [];
|
||||||
|
let new_items_data = [];
|
||||||
|
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
columns: {
|
||||||
|
...this.state.columns,
|
||||||
|
cart: {
|
||||||
|
...this.state.columns.cart,
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, function () {
|
||||||
|
|
||||||
|
customconf.items.map(function (item) {
|
||||||
|
Object.keys(items).map(key => {
|
||||||
|
if (item.pn && item.pn === items[key].name_number) {
|
||||||
|
new_items.push(Object.assign({
|
||||||
|
...items[key],
|
||||||
|
}, {
|
||||||
|
id: uuidv4(),
|
||||||
|
options_data: item.options ? item.options : null,
|
||||||
|
}));
|
||||||
|
new_items_data.push({options_data: item.options ? item.options : null});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
columns: {
|
||||||
|
...this.state.columns,
|
||||||
|
cart: {
|
||||||
|
...this.state.columns.cart,
|
||||||
|
items: new_items,
|
||||||
|
itemsData: new_items_data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentMode: customconf.type,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickSubmit(note, email) {
|
||||||
|
const crate = {
|
||||||
|
items: [],
|
||||||
|
type: this.state.currentMode,
|
||||||
|
};
|
||||||
|
const clonedCart = Array.from(this.state.columns.cart.items);
|
||||||
|
const clonedCartData = Array.from(this.state.columns.cart.itemsData);
|
||||||
|
for (const i in clonedCart) {
|
||||||
|
const item = clonedCart[i];
|
||||||
|
const item_data = clonedCartData[i];
|
||||||
|
crate.items.push({
|
||||||
|
'pn': item.name_number,
|
||||||
|
'options': (item_data.options_data && item_data.options) ? FilterOptions(item_data.options, item_data.options_data) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data} = this.props;
|
||||||
|
|
||||||
|
this.setState({isProcessing: true});
|
||||||
|
|
||||||
|
fetch(data.API_RFQ, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
note,
|
||||||
|
configuration: JSON.stringify(crate)
|
||||||
|
})
|
||||||
|
}).then(response => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw Error("Response status is not OK: " + response.status + ".\n" + response);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
isProcessing: false,
|
||||||
|
shouldShowRFQFeedback: true,
|
||||||
|
RFQBodyType: 'email',
|
||||||
|
isProcessingComplete: true,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Request failed, reason:", err)
|
||||||
|
this.setState({isProcessing: false}, () => {
|
||||||
|
alert("We cannot receive your request. Try using the export by coping the configuration and send it to us at sales@m-labs.hk");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnDragEnd(result, newAdded) {
|
||||||
|
const {
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
} = result;
|
||||||
|
let dragged_items = [];
|
||||||
|
if (source.indexes) {
|
||||||
|
source.indexes.forEach((card_index, _) => {
|
||||||
|
dragged_items.push(itemsUnfoldedList[card_index]);
|
||||||
|
})
|
||||||
|
} else if (source.index >= 0) {
|
||||||
|
dragged_items.push(itemsUnfoldedList[source.index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!destination) {
|
||||||
|
if (source.droppableId === 'cart') {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
newCardJustAdded: false,
|
||||||
|
columns: {
|
||||||
|
...this.state.columns,
|
||||||
|
[source.droppableId]: {
|
||||||
|
...this.state.columns[source.droppableId],
|
||||||
|
items: remove(
|
||||||
|
this.state.columns[source.droppableId].items,
|
||||||
|
source.index,
|
||||||
|
),
|
||||||
|
itemsData: remove(
|
||||||
|
this.state.columns[source.droppableId].itemsData,
|
||||||
|
source.index,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(source.droppableId) {
|
||||||
|
|
||||||
|
case 'backlog':
|
||||||
|
if (source.droppableId !== destination.droppableId) {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
newCardJustAdded: newAdded ? true : false,
|
||||||
|
columns: {
|
||||||
|
...this.state.columns,
|
||||||
|
[destination.droppableId]: {
|
||||||
|
...this.state.columns[destination.droppableId],
|
||||||
|
items: copy(
|
||||||
|
this.state.items,
|
||||||
|
this.state.columns[source.droppableId],
|
||||||
|
this.state.columns[destination.droppableId],
|
||||||
|
dragged_items,
|
||||||
|
destination,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case destination.droppableId:
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
newCardJustAdded: false,
|
||||||
|
columns: {
|
||||||
|
...this.state.columns,
|
||||||
|
[destination.droppableId]: {
|
||||||
|
...this.state.columns[destination.droppableId],
|
||||||
|
items: reorder(
|
||||||
|
this.state.columns[destination.droppableId].items,
|
||||||
|
source.index,
|
||||||
|
destination.index,
|
||||||
|
),
|
||||||
|
itemsData: reorder(
|
||||||
|
this.state.columns[destination.droppableId].itemsData,
|
||||||
|
source.index,
|
||||||
|
destination.index,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickToggleMobileSideMenu() {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
mobileSideMenuShouldOpen: !this.state.mobileSideMenuShouldOpen,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickCloseRFQFeedback() {
|
||||||
|
this.setState({
|
||||||
|
shouldShowRFQFeedback: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAlerts(newItems) {
|
||||||
|
console.log('--- START CHECKING CRATE WARNING ---');
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentMode,
|
||||||
|
crateModeSlots,
|
||||||
|
crateRules,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const itemsCloned = Array.from(newItems);
|
||||||
|
const itemsData = [];
|
||||||
|
const rules = {};
|
||||||
|
|
||||||
|
itemsCloned.forEach((elem, idx) => {
|
||||||
|
if (!(idx in itemsData)) itemsData[idx] = elem;
|
||||||
|
if (idx in this.state.columns.cart.itemsData && this.state.columns.cart.itemsData[idx].options_data) {
|
||||||
|
itemsCloned[idx].options_data = this.state.columns.cart.itemsData[idx].options_data;
|
||||||
|
}
|
||||||
|
itemsData[idx].warnings = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// check number of slot in crate
|
||||||
|
const nbrOccupied = nbrOccupiedSlotsInCrate(newItems);
|
||||||
|
if (nbrOccupied > crateModeSlots[currentMode]) {
|
||||||
|
rules[crateRules.maxSlot.type] = {...crateRules.maxSlot};
|
||||||
|
} else if (crateModeSlots[currentMode] === 21 && nbrOccupied <= 10) {
|
||||||
|
rules[crateRules.compactSlot.type] = {...crateRules.compactSlot};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// check the number of EEM connectors available for all Kasli
|
||||||
|
const idxK = itemsCloned.reduce((prev, next, i) => {
|
||||||
|
if (next.type === 'kasli' || next.type === 'vhdcicarrier') {
|
||||||
|
prev.push(i);
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, []);
|
||||||
|
for (let i = 0; i <= idxK.length - 1; i++) {
|
||||||
|
let slots;
|
||||||
|
let nbUsedSlot = 0;
|
||||||
|
let nbrCurrentClock = 0;
|
||||||
|
let idx = idxK[i];
|
||||||
|
|
||||||
|
if (i !== idxK.length - 1) {
|
||||||
|
slots = itemsCloned.slice(idx + 1, idxK[i + 1]);
|
||||||
|
} else {
|
||||||
|
slots = itemsCloned.slice(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
const slots_need_resource = itemsCloned.slice(0, idx);
|
||||||
|
const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources));
|
||||||
|
|
||||||
|
if (idx_need != -1) {
|
||||||
|
if (idx_need in itemsData) {
|
||||||
|
if ('warnings' in itemsData[idx_need]) {
|
||||||
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
||||||
|
} else {
|
||||||
|
itemsData[idx_need].warnings = {};
|
||||||
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
itemsData[idx_need] = {...itemsCloned[idx_need]};
|
||||||
|
itemsData[idx_need].warnings = {};
|
||||||
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const process_slots = (item) => {
|
||||||
|
if (!item.options_data
|
||||||
|
|| item.options_data.ext_pwr === false
|
||||||
|
|| item.options_data.mono_eem === false
|
||||||
|
)
|
||||||
|
return item.slotOccupied;
|
||||||
|
else if (item.options_data.ext_pwr === true)
|
||||||
|
return 0;
|
||||||
|
else if (item.options_data.mono_eem === true || item.options_data.n_eem === "1 EEM")
|
||||||
|
return 1;
|
||||||
|
else if (item.options_data.n_eem === "3 EEM")
|
||||||
|
return 3;
|
||||||
|
|
||||||
|
return item.slotOccupied;
|
||||||
|
}
|
||||||
|
|
||||||
|
nbUsedSlot = slots
|
||||||
|
.filter(item => item.type !== 'idc-bnc')
|
||||||
|
.reduce((prev, next) => {
|
||||||
|
return prev + process_slots(next);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
nbrCurrentClock = slots
|
||||||
|
.reduce((prev, next) => {
|
||||||
|
return next.type === 'clocker' ? prev + ((next.options_data && next.options_data.ext_clk === true) ? 0 : next.clockOccupied) : prev;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (idx in itemsData) {
|
||||||
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
||||||
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
||||||
|
if (!('warnings' in itemsData[idx])) {
|
||||||
|
itemsData[idx].warnings = {};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
itemsData[idx] = {...itemsCloned[idx]};
|
||||||
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
||||||
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
||||||
|
itemsData[idx].warnings = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
|
||||||
|
if (itemsCloned[idx].rules.maxSlot.message) {
|
||||||
|
rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
|
||||||
|
}
|
||||||
|
itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
|
||||||
|
rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
|
||||||
|
itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsCloned.length > (idx + 1)) {
|
||||||
|
const ddkali = itemsCloned[idx + 1];
|
||||||
|
if (ddkali.type === 'kasli' || ddkali.type === 'vhdcicarrier') {
|
||||||
|
rules[ddkali.rules.follow.type] = {...ddkali.rules.follow};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idxK.length === 0) {
|
||||||
|
const slots_need_resource = itemsCloned.slice(0);
|
||||||
|
const idx_need = slots_need_resource.findIndex(e => (e.rules && e.rules.resources));
|
||||||
|
|
||||||
|
if (idx_need != -1) {
|
||||||
|
if (idx_need in itemsData) {
|
||||||
|
if ('warnings' in itemsData[idx_need]) {
|
||||||
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
||||||
|
} else {
|
||||||
|
itemsData[idx_need].warnings = {};
|
||||||
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
itemsData[idx_need] = {...itemsCloned[idx_need]};
|
||||||
|
itemsData[idx_need].warnings = {};
|
||||||
|
itemsData[idx_need].warnings.resources = {...itemsCloned[idx_need].rules.resources};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// check number of clock connector available
|
||||||
|
const idxC = itemsCloned.reduce((prev, next, i) => {
|
||||||
|
if (next.type === 'kasli' || next.type === 'clocker') {
|
||||||
|
prev.push(i);
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, []);
|
||||||
|
for (let i = 0; i <= idxC.length - 1; i++) {
|
||||||
|
let slots;
|
||||||
|
let nbrCurrentClock = 0;
|
||||||
|
let idx = idxC[i];
|
||||||
|
|
||||||
|
if (i !== idxC.length - 1) {
|
||||||
|
slots = itemsCloned.slice(idx + 1, idxC[i + 1]);
|
||||||
|
} else {
|
||||||
|
slots = itemsCloned.slice(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
nbrCurrentClock = slots.reduce((prev, next) => {
|
||||||
|
return prev + ((next.options_data && next.options_data.ext_clk && next.options_data.ext_clk.checked) ? 0 : next.clockOccupied);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (idx in itemsData) {
|
||||||
|
if (itemsData[idx].nbrCurrentClock && itemsData[idx].type !== "clocker") {
|
||||||
|
itemsData[idx].nbrCurrentClock += nbrCurrentClock;
|
||||||
|
} else {
|
||||||
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
itemsData[idx] = {...itemsCloned[idx]};
|
||||||
|
itemsData[idx].nbrCurrentClock = nbrCurrentClock;
|
||||||
|
itemsData[idx].warnings = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbrCurrentClock > itemsCloned[idx].nbrClockMax) {
|
||||||
|
rules[itemsCloned[idx].rules.maxClock.type] = {...itemsCloned[idx].rules.maxClock};
|
||||||
|
itemsData[idx].warnings.maxClockWarning = {...itemsCloned[idx].rules.maxClockWarning};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (itemsCloned.find(elem => elem.type === 'urukul')) {
|
||||||
|
if (this.state.items['urukul'].rules.info) {
|
||||||
|
rules[this.state.items['urukul'].rules.info.type] = {...this.state.items['urukul'].rules.info};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// check if IDC-BNC is correctly positionned (after Zotino or HD68)
|
||||||
|
const idxIDCBNC = itemsCloned.reduce((prev, next, i) => {
|
||||||
|
if (next.type === 'idc-bnc') {
|
||||||
|
prev.push(i);
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, []);
|
||||||
|
for (var i = idxIDCBNC.length - 1; i >= 0; i--) {
|
||||||
|
const ce = idxIDCBNC[i];
|
||||||
|
let shouldWarning = false;
|
||||||
|
|
||||||
|
if (ce == 0) {
|
||||||
|
shouldWarning = true;
|
||||||
|
} else if (ce >= 1) {
|
||||||
|
const pe = idxIDCBNC[i] - 1;
|
||||||
|
if (itemsCloned[pe].type !== 'zotino' &&
|
||||||
|
itemsCloned[pe].type !== 'hd68' &&
|
||||||
|
itemsCloned[pe].type !== 'idc-bnc') {
|
||||||
|
shouldWarning = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldWarning) {
|
||||||
|
itemsData[ce] = {...itemsCloned[ce]};
|
||||||
|
itemsData[ce].warnings = {};
|
||||||
|
itemsData[ce].warnings.wrong = {...itemsCloned[ce].rules.wrong};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// check number of IDC-BNC adapters for a Zotino and HD68-IDC
|
||||||
|
const idxZH = itemsCloned.reduce((prev, next, i) => {
|
||||||
|
if (next.type === 'zotino' || next.type === 'hd68') {
|
||||||
|
prev.push(i);
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, []);
|
||||||
|
for (let i = 0; i <= idxZH.length - 1; i++) {
|
||||||
|
let slots;
|
||||||
|
let nbUsedSlot = 0;
|
||||||
|
let idx = idxZH[i];
|
||||||
|
|
||||||
|
if (i !== idxZH.length - 1) {
|
||||||
|
slots = itemsCloned.slice(idx + 1, idxZH[i + 1]);
|
||||||
|
} else {
|
||||||
|
slots = itemsCloned.slice(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stopCount = false;
|
||||||
|
nbUsedSlot = slots.reduce((prev, next, ci, ca) => {
|
||||||
|
if (ci === 0 && next.type === 'idc-bnc') {
|
||||||
|
return prev + 1;
|
||||||
|
} else if (ca[0].type === 'idc-bnc' && ci > 0 && ca[ci - 1].type === 'idc-bnc') {
|
||||||
|
if (next.type !== 'idc-bnc') { stopCount = true; }
|
||||||
|
return prev + (next.type === 'idc-bnc' && !stopCount ? 1 : 0);
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (idx in itemsData) {
|
||||||
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
||||||
|
if (!('warnings' in itemsData[idx])) {
|
||||||
|
itemsData[idx].warnings = {};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
itemsData[idx] = {...itemsCloned[idx]};
|
||||||
|
itemsData[idx].nbrCurrentSlot = nbUsedSlot;
|
||||||
|
itemsData[idx].warnings = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbUsedSlot > 0) {
|
||||||
|
if (itemsCloned[idx].rules.maxSlot.message) {
|
||||||
|
rules[itemsCloned[idx].rules.maxSlot.type] = {...itemsCloned[idx].rules.maxSlot};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nbUsedSlot > itemsCloned[idx].nbrSlotMax) {
|
||||||
|
itemsData[idx].warnings.maxSlotWarning = {...itemsCloned[idx].rules.maxSlotWarning};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if HD68-IDC has at least 1 IDC-BNC adapter
|
||||||
|
if (itemsCloned[idx].type === 'hd68') {
|
||||||
|
let shouldWarning = false;
|
||||||
|
|
||||||
|
if (idx < itemsCloned.length - 1) {
|
||||||
|
if (itemsCloned[idx + 1].type !== 'idc-bnc') {
|
||||||
|
shouldWarning = true;
|
||||||
|
}
|
||||||
|
} else if (idx === itemsCloned.length - 1) {
|
||||||
|
shouldWarning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldWarning) {
|
||||||
|
if (idx in itemsData) {
|
||||||
|
itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter};
|
||||||
|
} else {
|
||||||
|
itemsData[idx] = {...itemsCloned[idx]};
|
||||||
|
itemsData[idx].warnings = {};
|
||||||
|
itemsData[idx].warnings.minAdapter = {...itemsCloned[idx].rules.minAdapter};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update state with rules
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
columns: {
|
||||||
|
...this.state.columns,
|
||||||
|
cart: {
|
||||||
|
...this.state.columns.cart,
|
||||||
|
itemsData: itemsData,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...rules,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
const {
|
||||||
|
currency,
|
||||||
|
currentItemHovered,
|
||||||
|
currentMode,
|
||||||
|
crateModeSlots,
|
||||||
|
crateModeItems,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
rules,
|
||||||
|
mobileSideMenuShouldOpen,
|
||||||
|
newCardJustAdded,
|
||||||
|
isProcessing,
|
||||||
|
shouldShowRFQFeedback,
|
||||||
|
RFQBodyType,
|
||||||
|
RFQBodyOrder,
|
||||||
|
isProcessingComplete,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const isMobile = window.deviceIsMobile();
|
||||||
|
const isTouch = window.isTouchEnabled();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragDropContext onDragEnd={this.handleOnDragEnd}>
|
||||||
|
|
||||||
|
<Layout
|
||||||
|
showRFQFeedback={shouldShowRFQFeedback}
|
||||||
|
RFQBodyType={RFQBodyType}
|
||||||
|
RFQBodyOrder={RFQBodyOrder}
|
||||||
|
className="shop"
|
||||||
|
mobileSideMenuShouldOpen={mobileSideMenuShouldOpen}
|
||||||
|
isMobile={isMobile}
|
||||||
|
newCardJustAdded={newCardJustAdded}
|
||||||
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
||||||
|
onClickCloseRFQFeedback={this.handleClickCloseRFQFeedback}
|
||||||
|
onClickLoadCustomConf={this.handleLoadCustomConf}
|
||||||
|
items={items}
|
||||||
|
aside={
|
||||||
|
<Backlog
|
||||||
|
currency={currency}
|
||||||
|
items={items}
|
||||||
|
data={columns['backlog']}
|
||||||
|
onClickAddItem={this.handleClickAddItem}
|
||||||
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
||||||
|
isMobile={isMobile}>
|
||||||
|
</Backlog>
|
||||||
|
}
|
||||||
|
main={(
|
||||||
|
<OrderPanel
|
||||||
|
onClickToggleMobileSideMenu={this.handleClickToggleMobileSideMenu}
|
||||||
|
onClickOpenImport={this.handleClickOpenImport}
|
||||||
|
isMobile={isMobile}
|
||||||
|
title="Order hardware"
|
||||||
|
description={(<p className="description">Drag and drop the cards you want into the crate below to see how the combination would look like. Setup card's configuration by tapping at the top of the card, most of the options can be modified after shipment. If you have any issues with this ordering system, or if you need other configurations, email us directly anytime at <a href="mailto:sales@m-labs.hk">sales@m-labs.hk</a>. The price is estimated and must be confirmed by a quote.</p>)}
|
||||||
|
crateMode={
|
||||||
|
<CrateMode
|
||||||
|
items={crateModeItems}
|
||||||
|
mode={currentMode}
|
||||||
|
onClickMode={this.handleCrateModeChange}>
|
||||||
|
</CrateMode>}
|
||||||
|
crate={
|
||||||
|
<Crate
|
||||||
|
cart={
|
||||||
|
<Cart
|
||||||
|
nbrSlots={crateModeSlots[currentMode]}
|
||||||
|
data={columns['cart']}
|
||||||
|
isMobile={isMobile}
|
||||||
|
isTouch={isTouch}
|
||||||
|
itemHovered={currentItemHovered}
|
||||||
|
onToggleOverlayRemove={this.handleToggleOverlayRemove}
|
||||||
|
onClickRemoveItem={this.handleDeleteItem}
|
||||||
|
onClickItem={this.handleShowOverlayRemove}
|
||||||
|
onCardUpdate={this.handleCardsUpdated}>
|
||||||
|
</Cart>
|
||||||
|
}
|
||||||
|
rules={Object.values(rules).filter(rule => rule)}>
|
||||||
|
</Crate>
|
||||||
|
}
|
||||||
|
summaryPrice={
|
||||||
|
<OrderSummary
|
||||||
|
currency={currency}
|
||||||
|
currentMode={currentMode}
|
||||||
|
modes={crateModeItems}
|
||||||
|
summary={columns['cart'].items}
|
||||||
|
itemsData={columns.cart.itemsData}
|
||||||
|
onMouseEnterItem={this.handleMouseEnterItem}
|
||||||
|
onMouseLeaveItem={this.handleMouseLeaveItem}
|
||||||
|
onDeleteItem={this.handleDeleteItem}
|
||||||
|
onDeleteAllItems={this.handleDeleteAllItems}
|
||||||
|
onClickSelectItem={this.handleClickSelectItem}>
|
||||||
|
</OrderSummary>
|
||||||
|
}
|
||||||
|
form={
|
||||||
|
<OrderForm
|
||||||
|
isProcessingComplete={isProcessingComplete}
|
||||||
|
processingComplete={this.handleProcessingComplete}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
onClickSubmit={this.handleClickSubmit}
|
||||||
|
onClickShow={this.handleClickShowOrder}>
|
||||||
|
</OrderForm>
|
||||||
|
}>
|
||||||
|
</OrderPanel>
|
||||||
|
)}>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
</DragDropContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
34
static/js/shop/options/DialogPopup.jsx
Normal file
34
static/js/shop/options/DialogPopup.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React, {useState} from "react";
|
||||||
|
import {useClickAway} from "@uidotdev/usehooks";
|
||||||
|
import {ProcessOptions} from "./Options.jsx";
|
||||||
|
|
||||||
|
export function DialogPopup({options, data, target, id, big, first, last, options_class}) {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const ref = useClickAway((e) => {
|
||||||
|
if (e.type === "mousedown") // ignore touchstart
|
||||||
|
setShow(false)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last) ? "overlay-last" : ""} ${options_class || ""}`;
|
||||||
|
const handleClick = (event) => {
|
||||||
|
setShow(!show);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<img className="alert-info" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
|
||||||
|
onClick={handleClick}/>
|
||||||
|
<div style={{'display': show ? 'flex' : 'none'}} className={div_classes}>
|
||||||
|
<ProcessOptions
|
||||||
|
options={options}
|
||||||
|
data={data}
|
||||||
|
key={"processed_options_" + id}
|
||||||
|
id={"processed_options_" + id}
|
||||||
|
target={target}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
42
static/js/shop/options/Options.jsx
Normal file
42
static/js/shop/options/Options.jsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import jsonLogic from "json-logic-js";
|
||||||
|
import {componentsList} from "./components/components";
|
||||||
|
import {true_type_of} from "./utils";
|
||||||
|
|
||||||
|
export function ProcessOptions({options, data, target, id}) {
|
||||||
|
let options_t = true_type_of(options);
|
||||||
|
|
||||||
|
if (options_t === "array") {
|
||||||
|
return Array.from(
|
||||||
|
options.map((option_item, i) => ProcessOptions({
|
||||||
|
options: option_item,
|
||||||
|
data: data,
|
||||||
|
target: target,
|
||||||
|
id: id + i
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} else if (options_t === "object") {
|
||||||
|
if (
|
||||||
|
true_type_of(options.type) === "string" &&
|
||||||
|
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
|
||||||
|
) {
|
||||||
|
if (options.type in componentsList) {
|
||||||
|
return componentsList[options.type](target, id + options.type, data, options.args);
|
||||||
|
} else if (options.type === "Group") {
|
||||||
|
return (
|
||||||
|
<div className="border rounded" key={id + "group"}>
|
||||||
|
{ProcessOptions({
|
||||||
|
options: jsonLogic.apply(options.items, data),
|
||||||
|
data: data,
|
||||||
|
target: target,
|
||||||
|
id: id
|
||||||
|
})}
|
||||||
|
</div>);
|
||||||
|
} else {
|
||||||
|
return componentsList["Default"](options.type, id + "missing");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ProcessOptions({options: jsonLogic.apply(options, data), data: data, target: target, id: id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
95
static/js/shop/options/SummaryPopup.jsx
Normal file
95
static/js/shop/options/SummaryPopup.jsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import {useClickAway} from "@uidotdev/usehooks";
|
||||||
|
import {FilterOptions, true_type_of} from "./utils";
|
||||||
|
|
||||||
|
export function SummaryPopup({id, options, data}) {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const [position, setPosition] = useState({x: 0, y: 0});
|
||||||
|
const [size, setSize] = useState({w: 0, h: 0});
|
||||||
|
let display_options = FilterOptions(options, data);
|
||||||
|
const close = () => {
|
||||||
|
setShow(false);
|
||||||
|
document.removeEventListener("scroll", handleScroll, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ref = useClickAway(close);
|
||||||
|
|
||||||
|
const reposition = () => {
|
||||||
|
let popup_button = document.getElementById(id + "img");
|
||||||
|
if (!popup_button) {
|
||||||
|
document.removeEventListener("scroll", handleScroll, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let rect = popup_button.getBoundingClientRect()
|
||||||
|
let pos_x = (rect.left + rect.right) / 2;
|
||||||
|
let pos_y = (rect.top + rect.bottom) / 2;
|
||||||
|
if (pos_x + size.w > window.innerWidth) {
|
||||||
|
setPosition({x: pos_x - size.w - 20, y: pos_y - size.h / 2});
|
||||||
|
} else {
|
||||||
|
setPosition({x: pos_x - size.w / 2, y: pos_y - size.h - 20});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = (e) => {
|
||||||
|
if (e.target !== document.getElementById(id)) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) {
|
||||||
|
let popup = document.getElementById(id);
|
||||||
|
let width = popup.offsetWidth;
|
||||||
|
let height = popup.offsetHeight;
|
||||||
|
setSize({w: width, h: height});
|
||||||
|
reposition()
|
||||||
|
}
|
||||||
|
}, [show])
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) {
|
||||||
|
reposition();
|
||||||
|
}
|
||||||
|
}, [show, size])
|
||||||
|
|
||||||
|
const handleClick = (event) => {
|
||||||
|
setShow(!show);
|
||||||
|
if (!show) {
|
||||||
|
document.addEventListener("scroll", handleScroll, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stringify = (value) => {
|
||||||
|
let value_type = true_type_of(value);
|
||||||
|
if (value_type === "string") {
|
||||||
|
return value;
|
||||||
|
} else if (value_type === "object") {
|
||||||
|
if (value.checked === false) {
|
||||||
|
return "off";
|
||||||
|
} else if (value.checked === true && value.text) {
|
||||||
|
return value.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<img className="alert-info" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
|
||||||
|
id={id + "img"}
|
||||||
|
onClick={handleClick}/>
|
||||||
|
<div style={{'display': show ? 'flex' : 'none', 'top': position.y, 'left': position.x}}
|
||||||
|
className="overlayVariant card border rounded"
|
||||||
|
id={id}>
|
||||||
|
<div className="card-body">
|
||||||
|
{Array.from(Object.entries(display_options)
|
||||||
|
.filter(([key, value], _) => key !== "ext_data")
|
||||||
|
.map(([key, value], _) => {
|
||||||
|
return (<p className="card-text" key={id + key}><i>{key}</i>: {stringify(value)}</p>);
|
||||||
|
}))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
43
static/js/shop/options/components/Line.jsx
Normal file
43
static/js/shop/options/components/Line.jsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Tip} from "./Tip.jsx";
|
||||||
|
|
||||||
|
class Line extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
// Initialize the state object with the initial values from the props
|
||||||
|
this.state = {
|
||||||
|
text: props.outvar in props.data ? props.data[props.outvar] : (props.fallback ? props.fallback : "")
|
||||||
|
};
|
||||||
|
// Bind the event handler to this
|
||||||
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
this.props.target.construct(this.props.outvar, this.state.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(element) {
|
||||||
|
let text = element.target.value;
|
||||||
|
this.setState({
|
||||||
|
text: text
|
||||||
|
});
|
||||||
|
this.props.target.update(this.props.outvar, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let key = this.props.id + this.props.outvar;
|
||||||
|
return (
|
||||||
|
<div className="shop-line" key={this.props.id}>
|
||||||
|
<label htmlFor={key} className="form-label">
|
||||||
|
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||||
|
{this.props.title}:
|
||||||
|
</label>
|
||||||
|
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||||
|
<input type="text" className="form-control form-control-sm" id={key} onChange={this.handleClick}
|
||||||
|
value={this.state.text}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
|
||||||
|
return <Line target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||||
|
id={id} data={data}/>;
|
||||||
|
}
|
60
static/js/shop/options/components/Radio.jsx
Normal file
60
static/js/shop/options/components/Radio.jsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Tip} from "./Tip.jsx";
|
||||||
|
|
||||||
|
class Radio extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
// Initialize the state object with the initial values from the props
|
||||||
|
this.state = {
|
||||||
|
variant: props.outvar in props.data ? props.data[props.outvar] : props.variants[props.fallback ? props.fallback : 0],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bind the event handler to this
|
||||||
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
this.props.target.construct(this.props.outvar, this.state.variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(variant) {
|
||||||
|
// Update the state object with the new value for outvar
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
variant: variant
|
||||||
|
});
|
||||||
|
this.props.target.update(this.props.outvar, variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let key = this.props.id + this.props.outvar;
|
||||||
|
return (
|
||||||
|
<div className="shop-radio" key={this.props.id}>
|
||||||
|
<div style={{"display": "inline"}}>
|
||||||
|
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||||
|
{this.props.title}
|
||||||
|
</div>
|
||||||
|
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||||
|
{this.props.variants.map((variant, _) => (
|
||||||
|
<div className="form-check" key={key + variant}>
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name={key}
|
||||||
|
id={key + variant}
|
||||||
|
checked={this.state.variant === variant}
|
||||||
|
onClick={() => this.handleClick(variant)}
|
||||||
|
onChange={() => this.handleClick(variant)}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor={key + variant}>
|
||||||
|
{variant}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RadioWrapper(target, id, data, {title, variants, outvar, fallback, icon, tip}) {
|
||||||
|
return <Radio target={target} title={title} variants={variants} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||||
|
fallback={fallback}
|
||||||
|
id={id} data={data}/>;
|
||||||
|
}
|
54
static/js/shop/options/components/Switch.jsx
Normal file
54
static/js/shop/options/components/Switch.jsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Tip} from "./Tip.jsx";
|
||||||
|
|
||||||
|
class Switch extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
// Initialize the state object with the initial values from the props
|
||||||
|
this.state = {
|
||||||
|
checked: props.outvar in props.data ? !!(props.data[props.outvar]) : !!(props.fallback)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bind the event handler to this
|
||||||
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
this.props.target.construct(this.props.outvar, this.state.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick() {
|
||||||
|
// Update the state object with the new value for outvar
|
||||||
|
let new_checked = !this.state.checked;
|
||||||
|
this.setState({
|
||||||
|
checked: new_checked
|
||||||
|
});
|
||||||
|
this.props.target.update(this.props.outvar, new_checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let key = this.props.id + this.props.outvar;
|
||||||
|
return (
|
||||||
|
<div className="shop-switch" key={this.props.id}>
|
||||||
|
<div className="form-check form-switch" key={key}>
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id={key}
|
||||||
|
checked={this.state.checked}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
onChange={this.handleClick}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor={key} style={{"display": "inline"}}>
|
||||||
|
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||||
|
{this.props.title}
|
||||||
|
</label>
|
||||||
|
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SwitchWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
|
||||||
|
return <Switch target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||||
|
id={id} data={data}/>;
|
||||||
|
}
|
67
static/js/shop/options/components/SwitchLine.jsx
Normal file
67
static/js/shop/options/components/SwitchLine.jsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, {Component} from "react";
|
||||||
|
import {Tip} from "./Tip.jsx";
|
||||||
|
|
||||||
|
class SwitchLine extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
// Initialize the state object with the initial values from the props
|
||||||
|
this.state = {
|
||||||
|
text: props.outvar in props.data ? props.data[props.outvar].text : (props.fallback ? props.fallback.text : ""),
|
||||||
|
checked: props.outvar in props.data ? props.data[props.outvar].checked : (props.fallback ? props.fallback.checked : false)
|
||||||
|
};
|
||||||
|
// Bind the event handler to this
|
||||||
|
this.handleText = this.handleText.bind(this);
|
||||||
|
this.handleCheck = this.handleCheck.bind(this);
|
||||||
|
this.props.target.construct(this.props.outvar, this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleText(element) {
|
||||||
|
let new_state = {
|
||||||
|
...this.state,
|
||||||
|
text: element.target.value
|
||||||
|
}
|
||||||
|
this.setState(new_state);
|
||||||
|
this.props.target.update(this.props.outvar, new_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCheck() {
|
||||||
|
// Update the state object with the new value for outvar
|
||||||
|
let new_state = {
|
||||||
|
...this.state,
|
||||||
|
checked: !this.state.checked
|
||||||
|
}
|
||||||
|
this.setState(new_state);
|
||||||
|
this.props.target.update(this.props.outvar, new_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let key = this.props.id + this.props.outvar;
|
||||||
|
return (
|
||||||
|
<div className="shop-switch-line" key={this.props.id}>
|
||||||
|
<div className="form-check form-switch" key={key}>
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id={key + "switch"}
|
||||||
|
checked={this.state.checked}
|
||||||
|
onClick={this.handleCheck}
|
||||||
|
onChange={this.handleCheck}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor={key + "switch"}>
|
||||||
|
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
||||||
|
{this.props.title}
|
||||||
|
</label>
|
||||||
|
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
||||||
|
</div>
|
||||||
|
<input type="text" className="form-control form-control-sm" id={key + "line"} onChange={this.handleText}
|
||||||
|
value={this.state.text} disabled={!this.state.checked}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SwitchLineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
|
||||||
|
return <SwitchLine target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
||||||
|
id={id} data={data}/>;
|
||||||
|
}
|
15
static/js/shop/options/components/Tip.jsx
Normal file
15
static/js/shop/options/components/Tip.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {OverlayTrigger, Tooltip} from "react-bootstrap";
|
||||||
|
|
||||||
|
export function Tip({id, tip}) {
|
||||||
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="auto"
|
||||||
|
trigger={['click', 'hover', 'focus']}
|
||||||
|
style={{display: 'inline'}}
|
||||||
|
overlay={<Tooltip id={id}>{tip}</Tooltip>}
|
||||||
|
>
|
||||||
|
<img src={`/images/shop/icon-reminder.svg`} className="options-icon"/>
|
||||||
|
</OverlayTrigger>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function UnimplementedComponent(type, id) {
|
||||||
|
//console.error("Missing component with type:", type)
|
||||||
|
return <div key={type + id} style={{background: "red"}}>UNIMPLEMENTED</div>
|
||||||
|
}
|
15
static/js/shop/options/components/components.js
Normal file
15
static/js/shop/options/components/components.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
import {LineWrapper} from "./Line.jsx";
|
||||||
|
import {RadioWrapper} from "./Radio.jsx";
|
||||||
|
import {SwitchWrapper} from "./Switch.jsx";
|
||||||
|
import {SwitchLineWrapper} from "./SwitchLine.jsx";
|
||||||
|
import {UnimplementedComponent} from "./UnimplementedComponent.jsx";
|
||||||
|
|
||||||
|
export const componentsList = {
|
||||||
|
"Radio": RadioWrapper,
|
||||||
|
"Switch": SwitchWrapper,
|
||||||
|
"Line": LineWrapper,
|
||||||
|
"SwitchLine": SwitchLineWrapper,
|
||||||
|
"Default": UnimplementedComponent,
|
||||||
|
};
|
38
static/js/shop/options/utils.js
Normal file
38
static/js/shop/options/utils.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import jsonLogic from "json-logic-js";
|
||||||
|
import {componentsList} from "./components/components";
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/70511311
|
||||||
|
export const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
|
||||||
|
|
||||||
|
export function FillExtData(data, index) {
|
||||||
|
return {
|
||||||
|
has_other_dio: data.filter((value, item_index) => index !== item_index && value.name &&value.name.endsWith("-TTL")).length > 0,
|
||||||
|
has_dds: data.filter(((value, _) => value.name === "DDS" && value.name_number === "4410" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
|
||||||
|
has_sampler: data.filter(((value, _) => value.name === "Sampler" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FilterOptions(options, data) {
|
||||||
|
let options_t = true_type_of(options);
|
||||||
|
let target = {};
|
||||||
|
|
||||||
|
if (options_t === "array") {
|
||||||
|
options.map((option_item, _) => {
|
||||||
|
Object.assign(target, FilterOptions(option_item, data))
|
||||||
|
});
|
||||||
|
} else if (options_t === "object") {
|
||||||
|
if (
|
||||||
|
true_type_of(options.type) === "string" &&
|
||||||
|
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
|
||||||
|
) {
|
||||||
|
if (options.type in componentsList) {
|
||||||
|
target[options.args.outvar] = data[options.args.outvar];
|
||||||
|
} else if (options.type === "Group") {
|
||||||
|
Object.assign(target, FilterOptions(jsonLogic.apply(options.items, data), data))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Object.assign(target, FilterOptions(jsonLogic.apply(options, data), data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
}
|
131
static/js/shop/utils.js
Normal file
131
static/js/shop/utils.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
'use strict';
|
||||||
|
import {v4 as uuidv4} from "uuid";
|
||||||
|
|
||||||
|
|
||||||
|
export const data = window.shop_data;
|
||||||
|
export const itemsUnfoldedList = Array.from(data.columns.backlog.categories.map(groupId => groupId.itemIds).flat());
|
||||||
|
|
||||||
|
export const copy = (
|
||||||
|
model,
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
draggableSource,
|
||||||
|
droppableDestination
|
||||||
|
) => {
|
||||||
|
const destClone = Array.from(destination.items);
|
||||||
|
|
||||||
|
destClone.splice(droppableDestination.index, 0, ...draggableSource.map((dragged_item, _) => {
|
||||||
|
return {
|
||||||
|
...model[dragged_item],
|
||||||
|
id: uuidv4(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return destClone;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reorder = (list, startIndex, endIndex) => {
|
||||||
|
const result = Array.from(list);
|
||||||
|
const [removed] = result.splice(startIndex, 1);
|
||||||
|
result.splice(endIndex, 0, removed);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const remove = (list, startIndex) => {
|
||||||
|
const result = Array.from(list);
|
||||||
|
result.splice(startIndex, 1);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const productStyle = (style, snapshot, removeAnim, hovered, selected, cart=false) => {
|
||||||
|
const custom = {
|
||||||
|
opacity: snapshot.isDragging ? .7 : 1,
|
||||||
|
backgroundColor: (hovered || selected) ? '#eae7f7' : 'initial',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!cart && snapshot.draggingOver == null && // hack for backlog
|
||||||
|
((!snapshot.isDragging) // prevent next elements from animation
|
||||||
|
|| (snapshot.isDragging && snapshot.isDropAnimating))) { // prevent dragged element from weird animation
|
||||||
|
style.transform = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.isDropAnimating) {
|
||||||
|
return { ...style, ...custom};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeAnim) {
|
||||||
|
// cannot be 0, but make it super tiny
|
||||||
|
custom.transitionDuration = '0.001s';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...style,
|
||||||
|
...custom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cartStyle = (style, snapshot) => {
|
||||||
|
const isDraggingOver = snapshot.isDraggingOver;
|
||||||
|
return {
|
||||||
|
...style,
|
||||||
|
...{
|
||||||
|
backgroundColor: isDraggingOver ? '#f2f2f2' : '#f9f9f9',
|
||||||
|
border: isDraggingOver ? '1px dashed #ccc' : '0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const nbrConnectorsStyle = (data) => {
|
||||||
|
if (!data || !data.nbrCurrentSlot) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = data.nbrCurrentSlot * 100 / data.nbrSlotMax;
|
||||||
|
if (p > 100) {
|
||||||
|
p = 100;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: `${p}%`,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nbrClocksStyle = (data) => {
|
||||||
|
if (!data || !data.nbrCurrentClock) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = data.nbrCurrentClock * 100 / data.nbrClockMax;
|
||||||
|
if (p > 100) {
|
||||||
|
p = 100;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: `${p}%`,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const nbrOccupiedSlotsInCrate = (items) => {
|
||||||
|
return items.reduce((prev, next) => {
|
||||||
|
return prev + (next.hp === 8 ? 2 : 1);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatMoney(amount, decimalCount = 2, decimal = ".", thousands = ",") {
|
||||||
|
// https://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-currency-string-in-javascript
|
||||||
|
// changes: return amount if error in order to avoid empty value
|
||||||
|
try {
|
||||||
|
decimalCount = Math.abs(decimalCount);
|
||||||
|
decimalCount = isNaN(decimalCount) ? 2 : decimalCount;
|
||||||
|
|
||||||
|
const negativeSign = amount < 0 ? "-" : "";
|
||||||
|
|
||||||
|
let i = parseInt(amount = Math.abs(Number(amount) || 0).toFixed(decimalCount)).toString();
|
||||||
|
let j = (i.length > 3) ? i.length % 3 : 0;
|
||||||
|
|
||||||
|
return negativeSign + (j ? i.substr(0, j) + thousands : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousands) + (decimalCount ? decimal + Math.abs(amount - i).toFixed(decimalCount).slice(2) : "");
|
||||||
|
} catch (e) {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
};
|
@ -1,450 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
import React, {Component} from "react";
|
|
||||||
import jsonLogic from 'json-logic-js';
|
|
||||||
import {useState, useEffect} from 'react';
|
|
||||||
import {useClickAway} from "@uidotdev/usehooks";
|
|
||||||
import {OverlayTrigger, Tooltip} from "react-bootstrap";
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/70511311
|
|
||||||
const true_type_of = (obj) => Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
|
|
||||||
|
|
||||||
|
|
||||||
function Tip({id, tip}) {
|
|
||||||
return (
|
|
||||||
<OverlayTrigger
|
|
||||||
placement="auto"
|
|
||||||
trigger={['click', 'hover', 'focus']}
|
|
||||||
style={{display: 'inline'}}
|
|
||||||
overlay={<Tooltip id={id}>{tip}</Tooltip>}
|
|
||||||
>
|
|
||||||
<img src={`/images/shop/icon-reminder.svg`} className="options-icon"/>
|
|
||||||
</OverlayTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Radio extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
// Initialize the state object with the initial values from the props
|
|
||||||
this.state = {
|
|
||||||
variant: props.outvar in props.data ? props.data[props.outvar] : props.variants[props.fallback ? props.fallback : 0],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bind the event handler to this
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
|
||||||
this.props.target.construct(this.props.outvar, this.state.variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick(variant) {
|
|
||||||
// Update the state object with the new value for outvar
|
|
||||||
this.setState({
|
|
||||||
...this.state,
|
|
||||||
variant: variant
|
|
||||||
});
|
|
||||||
this.props.target.update(this.props.outvar, variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let key = this.props.id + this.props.outvar;
|
|
||||||
return (
|
|
||||||
<div className="shop-radio" key={this.props.id}>
|
|
||||||
<div style={{"display": "inline"}}>
|
|
||||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
|
||||||
{this.props.title}
|
|
||||||
</div>
|
|
||||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
|
||||||
{this.props.variants.map((variant, _) => (
|
|
||||||
<div className="form-check" key={key + variant}>
|
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="radio"
|
|
||||||
name={key}
|
|
||||||
id={key + variant}
|
|
||||||
checked={this.state.variant === variant}
|
|
||||||
onClick={() => this.handleClick(variant)}
|
|
||||||
onChange={() => this.handleClick(variant)}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label" htmlFor={key + variant}>
|
|
||||||
{variant}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function RadioWrapper(target, id, data, {title, variants, outvar, fallback, icon, tip}) {
|
|
||||||
return <Radio target={target} title={title} variants={variants} outvar={outvar} icon={icon} tip={tip} key={id}
|
|
||||||
fallback={fallback}
|
|
||||||
id={id} data={data}/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Switch extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
// Initialize the state object with the initial values from the props
|
|
||||||
this.state = {
|
|
||||||
checked: props.outvar in props.data ? !!(props.data[props.outvar]) : !!(props.fallback)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bind the event handler to this
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
|
||||||
this.props.target.construct(this.props.outvar, this.state.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick() {
|
|
||||||
// Update the state object with the new value for outvar
|
|
||||||
let new_checked = !this.state.checked;
|
|
||||||
this.setState({
|
|
||||||
checked: new_checked
|
|
||||||
});
|
|
||||||
this.props.target.update(this.props.outvar, new_checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let key = this.props.id + this.props.outvar;
|
|
||||||
return (
|
|
||||||
<div className="shop-switch" key={this.props.id}>
|
|
||||||
<div className="form-check form-switch" key={key}>
|
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
id={key}
|
|
||||||
checked={this.state.checked}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
onChange={this.handleClick}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label" htmlFor={key} style={{"display": "inline"}}>
|
|
||||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
|
||||||
{this.props.title}
|
|
||||||
</label>
|
|
||||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function SwitchWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
|
|
||||||
return <Switch target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
|
||||||
id={id} data={data}/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Line extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
// Initialize the state object with the initial values from the props
|
|
||||||
this.state = {
|
|
||||||
text: props.outvar in props.data ? props.data[props.outvar] : (props.fallback ? props.fallback : "")
|
|
||||||
};
|
|
||||||
// Bind the event handler to this
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
|
||||||
this.props.target.construct(this.props.outvar, this.state.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick(element) {
|
|
||||||
let text = element.target.value;
|
|
||||||
this.setState({
|
|
||||||
text: text
|
|
||||||
});
|
|
||||||
this.props.target.update(this.props.outvar, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let key = this.props.id + this.props.outvar;
|
|
||||||
return (
|
|
||||||
<div className="shop-line" key={this.props.id}>
|
|
||||||
<label htmlFor={key} className="form-label">
|
|
||||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
|
||||||
{this.props.title}:
|
|
||||||
</label>
|
|
||||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
|
||||||
<input type="text" className="form-control form-control-sm" id={key} onChange={this.handleClick}
|
|
||||||
value={this.state.text}/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function LineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
|
|
||||||
return <Line target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
|
||||||
id={id} data={data}/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SwitchLine extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
// Initialize the state object with the initial values from the props
|
|
||||||
this.state = {
|
|
||||||
text: props.outvar in props.data ? props.data[props.outvar].text : (props.fallback ? props.fallback.text : ""),
|
|
||||||
checked: props.outvar in props.data ? props.data[props.outvar].checked : (props.fallback ? props.fallback.checked : false)
|
|
||||||
};
|
|
||||||
// Bind the event handler to this
|
|
||||||
this.handleText = this.handleText.bind(this);
|
|
||||||
this.handleCheck = this.handleCheck.bind(this);
|
|
||||||
this.props.target.construct(this.props.outvar, this.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleText(element) {
|
|
||||||
let new_state = {
|
|
||||||
...this.state,
|
|
||||||
text: element.target.value
|
|
||||||
}
|
|
||||||
this.setState(new_state);
|
|
||||||
this.props.target.update(this.props.outvar, new_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCheck() {
|
|
||||||
// Update the state object with the new value for outvar
|
|
||||||
let new_state = {
|
|
||||||
...this.state,
|
|
||||||
checked: !this.state.checked
|
|
||||||
}
|
|
||||||
this.setState(new_state);
|
|
||||||
this.props.target.update(this.props.outvar, new_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let key = this.props.id + this.props.outvar;
|
|
||||||
return (
|
|
||||||
<div className="shop-switch-line" key={this.props.id}>
|
|
||||||
<div className="form-check form-switch" key={key}>
|
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
id={key + "switch"}
|
|
||||||
checked={this.state.checked}
|
|
||||||
onClick={this.handleCheck}
|
|
||||||
onChange={this.handleCheck}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label" htmlFor={key + "switch"}>
|
|
||||||
{this.props.icon && <img src={`/images${this.props.icon}`} className="options-icon"/>}
|
|
||||||
{this.props.title}
|
|
||||||
</label>
|
|
||||||
{this.props.tip && <Tip id={this.props.id + "tooltip"} tip={this.props.tip}/>}
|
|
||||||
</div>
|
|
||||||
<input type="text" className="form-control form-control-sm" id={key + "line"} onChange={this.handleText}
|
|
||||||
value={this.state.text} disabled={!this.state.checked}/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function SwitchLineWrapper(target, id, data, {title, fallback, outvar, icon, tip}) {
|
|
||||||
return <SwitchLine target={target} title={title} fallback={fallback} outvar={outvar} icon={icon} tip={tip} key={id}
|
|
||||||
id={id} data={data}/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function UnimplementedComponent(type, id) {
|
|
||||||
//console.error("Missing component with type:", type)
|
|
||||||
return <div key={type + id} style={{background: "red"}}>UNIMPLEMENTED</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentsList = {
|
|
||||||
"Radio": RadioWrapper,
|
|
||||||
"Switch": SwitchWrapper,
|
|
||||||
"Line": LineWrapper,
|
|
||||||
"SwitchLine": SwitchLineWrapper,
|
|
||||||
"Default": UnimplementedComponent,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export function ProcessOptions({options, data, target, id}) {
|
|
||||||
let options_t = true_type_of(options);
|
|
||||||
|
|
||||||
if (options_t === "array") {
|
|
||||||
return Array.from(
|
|
||||||
options.map((option_item, i) => ProcessOptions({
|
|
||||||
options: option_item,
|
|
||||||
data: data,
|
|
||||||
target: target,
|
|
||||||
id: id + i
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
} else if (options_t === "object") {
|
|
||||||
if (
|
|
||||||
true_type_of(options.type) === "string" &&
|
|
||||||
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
|
|
||||||
) {
|
|
||||||
if (options.type in componentsList) {
|
|
||||||
return componentsList[options.type](target, id + options.type, data, options.args);
|
|
||||||
} else if (options.type === "Group") {
|
|
||||||
return (
|
|
||||||
<div className="border rounded" key={id + "group"}>
|
|
||||||
{ProcessOptions({
|
|
||||||
options: jsonLogic.apply(options.items, data),
|
|
||||||
data: data,
|
|
||||||
target: target,
|
|
||||||
id: id
|
|
||||||
})}
|
|
||||||
</div>);
|
|
||||||
} else {
|
|
||||||
return componentsList["Default"](options.type, id + "missing");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return ProcessOptions({options: jsonLogic.apply(options, data), data: data, target: target, id: id});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FilterOptions(options, data) {
|
|
||||||
let options_t = true_type_of(options);
|
|
||||||
let target = {};
|
|
||||||
|
|
||||||
if (options_t === "array") {
|
|
||||||
options.map((option_item, _) => {
|
|
||||||
Object.assign(target, FilterOptions(option_item, data))
|
|
||||||
});
|
|
||||||
} else if (options_t === "object") {
|
|
||||||
if (
|
|
||||||
true_type_of(options.type) === "string" &&
|
|
||||||
(true_type_of(options.args) === "object" || true_type_of(options.items) === "array")
|
|
||||||
) {
|
|
||||||
if (options.type in componentsList) {
|
|
||||||
target[options.args.outvar] = data[options.args.outvar];
|
|
||||||
} else if (options.type === "Group") {
|
|
||||||
Object.assign(target, FilterOptions(jsonLogic.apply(options.items, data), data))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Object.assign(target, FilterOptions(jsonLogic.apply(options, data), data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OptionsDialogPopup({options, data, target, id, big, first, last, options_class}) {
|
|
||||||
const [show, setShow] = useState(false);
|
|
||||||
const ref = useClickAway((e) => {
|
|
||||||
if (e.type === "mousedown") // ignore touchstart
|
|
||||||
setShow(false)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let div_classes = `overlayVariant border rounded ${big ? "overlay-bigcard" : "overlay-smallcard"} ${(!big && first) ? "overlay-first" : ""} ${(!big && last) ? "overlay-last" : ""} ${options_class || ""}`;
|
|
||||||
const handleClick = (event) => {
|
|
||||||
setShow(!show);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref}>
|
|
||||||
<img className="alert-info" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
|
|
||||||
onClick={handleClick}/>
|
|
||||||
<div style={{'display': show ? 'flex' : 'none'}} className={div_classes}>
|
|
||||||
<ProcessOptions
|
|
||||||
options={options}
|
|
||||||
data={data}
|
|
||||||
key={"processed_options_" + id}
|
|
||||||
id={"processed_options_" + id}
|
|
||||||
target={target}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OptionsSummaryPopup({id, options, data}) {
|
|
||||||
const [show, setShow] = useState(false);
|
|
||||||
const [position, setPosition] = useState({x: 0, y: 0});
|
|
||||||
const [size, setSize] = useState({w: 0, h: 0});
|
|
||||||
let display_options = FilterOptions(options, data);
|
|
||||||
const close = () => {
|
|
||||||
setShow(false);
|
|
||||||
document.removeEventListener("scroll", handleScroll, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ref = useClickAway(close);
|
|
||||||
|
|
||||||
const reposition = () => {
|
|
||||||
let popup_button = document.getElementById(id + "img");
|
|
||||||
if (!popup_button) {
|
|
||||||
document.removeEventListener("scroll", handleScroll, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let rect = popup_button.getBoundingClientRect()
|
|
||||||
let pos_x = (rect.left + rect.right) / 2;
|
|
||||||
let pos_y = (rect.top + rect.bottom) / 2;
|
|
||||||
if (pos_x + size.w > window.innerWidth) {
|
|
||||||
setPosition({x: pos_x - size.w - 20, y: pos_y - size.h / 2});
|
|
||||||
} else {
|
|
||||||
setPosition({x: pos_x - size.w / 2, y: pos_y - size.h - 20});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleScroll = (e) => {
|
|
||||||
if (e.target !== document.getElementById(id)) {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (show) {
|
|
||||||
let popup = document.getElementById(id);
|
|
||||||
let width = popup.offsetWidth;
|
|
||||||
let height = popup.offsetHeight;
|
|
||||||
setSize({w: width, h: height});
|
|
||||||
reposition()
|
|
||||||
}
|
|
||||||
}, [show])
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (show) {
|
|
||||||
reposition();
|
|
||||||
}
|
|
||||||
}, [show, size])
|
|
||||||
|
|
||||||
const handleClick = (event) => {
|
|
||||||
setShow(!show);
|
|
||||||
if (!show) {
|
|
||||||
document.addEventListener("scroll", handleScroll, true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stringify = (value) => {
|
|
||||||
let value_type = true_type_of(value);
|
|
||||||
if (value_type === "string") {
|
|
||||||
return value;
|
|
||||||
} else if (value_type === "object") {
|
|
||||||
if (value.checked === false) {
|
|
||||||
return "off";
|
|
||||||
} else if (value.checked === true && value.text) {
|
|
||||||
return value.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return JSON.stringify(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref}>
|
|
||||||
<img className="alert-info" src={show ? "/images/shop/icon-close.svg" : "/images/shop/icon-customize.svg"}
|
|
||||||
id={id + "img"}
|
|
||||||
onClick={handleClick}/>
|
|
||||||
<div style={{'display': show ? 'flex' : 'none', 'top': position.y, 'left': position.x}}
|
|
||||||
className="overlayVariant card border rounded"
|
|
||||||
id={id}>
|
|
||||||
<div className="card-body">
|
|
||||||
{Array.from(Object.entries(display_options)
|
|
||||||
.filter(([key, value], _) => key !== "ext_data")
|
|
||||||
.map(([key, value], _) => {
|
|
||||||
return (<p className="card-text" key={id + key}><i>{key}</i>: {stringify(value)}</p>);
|
|
||||||
}))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FillExtData(data, index) {
|
|
||||||
return {
|
|
||||||
has_other_dio: data.filter((value, item_index) => index !== item_index && value.name &&value.name.endsWith("-TTL")).length > 0,
|
|
||||||
has_dds: data.filter(((value, _) => value.name === "DDS" && value.name_number === "4410" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
|
|
||||||
has_sampler: data.filter(((value, _) => value.name === "Sampler" && (!value.options_data || !value.options_data.mono_eem))).length > 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user